Install-MAML.ps1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 |
function Install-MAML { <# .Synopsis Installs MAML into a module .Description Installs MAML into a module. This generates a single script that: * Includes all commands * Removes their multiline comments * Directs the commands to use external help You should then include this script in your module import. Ideally, you should use the allcommands script .Example Install-MAML -Module HelpOut .Link Save-MAML .Link ConvertTo-MAML #> [OutputType([Nullable], [IO.FileInfo])] param( # The name of one or more modules. [Parameter(Mandatory=$true,Position=0,ParameterSetName='Module',ValueFromPipelineByPropertyName=$true)] [string[]] $Module, # If set, will refresh the documentation for the module before generating the commands file. [Parameter(ValueFromPipelineByPropertyName=$true)] [switch] $NoRefresh, # If set, will compact the generated MAML. This will be ignored if -Refresh is not passed, since no new MAML will be generated. [Parameter(ValueFromPipelineByPropertyName=$true)] [switch] $Compact, # The name of the combined script. By default, allcommands.ps1. [Parameter(Position=1,ValueFromPipelineByPropertyName=$true)] [string] $ScriptName = 'allcommands.ps1', # The root directories containing functions. If not provided, the function root will be the module root. [Parameter(ValueFromPipelineByPropertyName=$true)] [string[]] $FunctionRoot, # If set, the function roots will not be recursively searched. [Parameter(ValueFromPipelineByPropertyName=$true)] [switch] $NoRecurse, # The encoding of the combined script. By default, UTF8. [Parameter(Position=2,ValueFromPipelineByPropertyName=$true)] [ValidateNotNull()] [Text.Encoding] $Encoding = [Text.Encoding]::UTF8, # A list of wildcards to exclude. This list will always contain the ScriptName. [Parameter(ValueFromPipelineByPropertyName=$true)] [string[]] $Exclude, # If set, the generate MAML will not contain a version number. # This slightly reduces the size of the MAML file, and reduces the rate of changes in the MAML file. [Parameter(ValueFromPipelineByPropertyName=$true)] [Alias('Unversioned')] [switch] $NoVersion, # If provided, will save the MAML to a different directory than the current UI culture. [Parameter(ValueFromPipelineByPropertyName=$true)] [Globalization.CultureInfo] $Culture, # If set, will return the files that were generated. [Parameter(ValueFromPipelineByPropertyName=$true)] [switch] $PassThru ) process { if ($ScriptName -notlike '*.ps1') { # First, let's check that the scriptname is a .PS1. $ScriptName += '.ps1' # If it wasn't, add the extension. } $Exclude += $ScriptName # Then, add the script name to the list of exclusions. if (-not $Culture) { # If no culture was specified, $Culture = [Globalization.CultureInfo]::CurrentUICulture # use the current UI culture. } foreach ($m in $Module) { $theModule = Get-Module $m # Resolve the module. if (-not $theModule) { continue } # If we couldn't, continue to the next. $theModuleRoot = $theModule | Split-Path # Find the module's root. if ($PSBoundParameters.FunctionRoot) { # If we provided a function root parameter $functionRoot = foreach ($f in $FunctionRoot) { # then turn each root into an absolute path. if ([IO.File]::Exists($F)) { $f } else { Join-Path $theModuleRoot $f } } } else { $FunctionRoot = "$theModuleRoot" # otherwise, just use the module root. } $fileList = @(foreach ($f in $FunctionRoot) { # Walk thru each function root. Get-ChildItem -Path $f -Recurse:$(-not $Recurse) -Filter *.ps1 | # recursively find all .PS1s & { process { if ($_.Name -notlike '*-*' -or $_.Name -like '*.*.*') { return } foreach ($ex in $Exclude) { if ($_.Name -like $ex) { return } } return $_ } } }) #region Save the MAMLs if (-not $NoRefresh) { # If we're refreshing the MAML, $saveMamlCmd = # find the command Save-MAML if ($MyInvocation.MyCommand.ScriptBlock.Module) { $MyInvocation.MyCommand.ScriptBlock.Module.ExportedCommands['Save-MAML'] } else { $ExecutionContext.SessionState.InvokeCommand.GetCommand('Save-MAML', 'Function') } $saveMamlSplat = @{} + $PSBoundParameters # and pick out the parameters that this function and Save-MAML have in common. foreach ($k in @($saveMamlSplat.Keys)) { if (-not $saveMamlCmd.Parameters.ContainsKey($k)) { $saveMamlSplat.Remove($k) } } $saveMamlSplat.Module = $m # then, set the module Save-MAML @saveMamlSplat # and call Save-MAML } #endregion Save the MAMLs #region Generate the Combined Script # Prepare a regex to find function definitions. $regex = [Regex]::new(' (?<![-\s\#]{1,}) # not preceeded by a -, or whitespace, or a comment function # function keyword \s{1,1} # a single space or tab (?<Name>[^\-]{1,1}\S+) # any non-whitespace, starting with a non-dash \s{0,} # optional whitespace [\(\{] # opening parenthesis or brackets ', 'MultiLine,IgnoreCase,IgnorePatternWhitespace', '00:00:05') $newFileContent = # We'll assign new file content by foreach ($f in $fileList) { # walking thru each file. $fCmd = $ExecutionContext.SessionState.InvokeCommand.GetCommand($f.FullName, 'ExternalScript') $fileContent = $fCmd.ScriptBlock # and read it as a string. $start = 0 do { $matched = $regex.Match($fileContent,$start) # See if we find a functon. if ($matched.Success) { # If we found one, $insert = ([Environment]::NewLine + "#.ExternalHelp $M-Help.xml" + [Environment]::NewLine) # insert a line for help. $fileContent = if ($matched.Index) { $fileContent.Insert($matched.Index - 1, $insert) } else { $insert + $fileContent } $start += $matched.Index + $matched.Length $start += $insert.Length # and update our starting position. } # Keep doing this until we've reached the end of the file or the end of the matches. } while ($start -le $filecontent.Length -and $matched.Success) # Then output the file content. $fileContent } # Last but not least, we $combinedCommandsPath = Join-Path $theModuleRoot $ScriptName # determine the path for our combined commands file. "### DO NOT EDIT THIS FILE DIRECTLY ###" | Set-Content -Path $combinedCommandsPath -Encoding $Encoding.HeaderName.Replace('-','') # add a header [IO.File]::AppendAllText($combinedCommandsPath, $newFileContent, $Encoding) # and add our content. #endregion Generate the Combined Script if ($PassThru) { Get-Item -Path $combinedCommandsPath } } } } |