When I write comment based help in a PowerShell CmdLet ps1 script,
that is contained in a module, is it possible to refer to other
CmdLets in the same module so that the resulting output to the user
will print the name of the referred CmdLet when imported with a
prefix?
So for example, if I write my comment based help like this:
function Get-Thing {
<#
.SYNOPSIS
Get the thing.
.DESCRIPTION
The Get-Thing CmdLet will get a thing.
#>
[CmdletBinding()]
Param(...)
and the user imports the module using a prefix:
Import-Module -Prefix My
Then I want the help to print the CmdLet name Get-Thing in the description field as Get-MyThing, honoring the module prefix value that the user provided:
> help Get-MyThing
NAME
...
SYNOPSIS
...
SYNTAX
...
DESCRIPTION
This Get-MyThing CmdLet will get a thing.
Is this possible?
The NAME and SYNTAX sections will be automatically updated to include the prefix when you use one. It's not possible to change it if you use it elsewhere in the help text because you cannot embed variables in a comment.
I would just suggest avoiding using the cmdlet name elsewhere where you can, although you'll generally want to in Examples and that will then be unchanged. However at least with the NAME and SYNTAX sections being accurate that should help users self-correct.
The only other way I can think to do it would be to have external help files or placeholders in the comments that are rewritten as the module is loaded by using PowerShell to read and edit the files, but the effort required doesn't seem worthwhile.
Related
This is for readability and safety (clobbering duplication functions) and finding a workaround for no namespace support with PowerShell modules.
I want to be able to do something like this:
Import-Module MyHelpers.psm1 -Functions "FuncOne" -as MyHelpers.Func-One
MyHelpers.Func-One -blah sfsdfsdf
This make it obvious where FuncOne lives. for larger scripts I consider this pretty serious requirement.
It would probably be good enough if I could at least explicitly define which functions I'm importing (without being able to rename them). At least I would see where they are coming from. Is there any support for this? If not then I'll just have to name all functions inside of MyHelpers like MyHelpers.Func-One but then PowerShell will complain the verb is wrong; would that also break other things too?
Here's my comment as answer:
You can use the Function parameter to restrict the function (s) you want to import.
Next, with the Prefix parameter you can add a prefix string to these imported functions.
See Import- module examples 5 and 6
Theo's answer is correct, just want to point out also that you can fully qualify commands that you call by prefixing them with the module name already, for example:
Microsoft.PowerShell.Core\Import-Module -Name ActiveDirectory
ActiveDirectory\Get-ADComputer $env:COMPUTERNAME
or to your example:
Import-Module MyHelpers.psm1 -Functions "FuncOne" -as MyHelpers.Func-One
MyHelpers\Func-One -blah sfsdfsdf
I am trying to create an alias (named which) of the Get-Command cmdlet in a way that it doesn't run if I'm not sending any arguments (because if it's run without arguments it outputs all the commands that are available).
I know this can be done using a function but I would like to keep the tab completion functionality without having to write a sizeable function that is to be placed into my $PROFILE.
In short, I only want the alias to work if it is being passed arguments.
You can't do it with an alias, because PowerShell aliases can only refer to another command name or path, and can therefore neither include arguments nor custom logic.
Therefore you do need a function, but it can be a short and simple one:
function which { if ($args.count) { Get-Command #args } else { Throw "Missing command name." } }
Note that while passing -? for showing Get-Command's help does work, tab completion of arguments does not.
In order to get tab completion as well, you'll need to write a wrapper (proxy) function or at least replicate Get-Command's parameter declarations - which then does make the function definition sizable.
If the concern is just the size of the $PROFILE file itself, you can write a proxy script instead - which.ps1 - which you can invoke with just which as well, assuming you place it in one of the directories listed in $env:Path[1]; see next section.
Defining a wrapper (proxy) script or function:
Defining a wrapper (proxy) function or script is a nontrivial undertaking, but allows you to implement a robust wrapper that supports tab completion and even forwarding to the original command's help.
Note:
Bug alert: As zett42 points out, as of PowerShell [Core] 7.1, System.Management.Automation.ProxyCommand.Create neglects to include dynamic parameters if the target command is an (advanced) function or script; however, compiled cmdlets are not affected; see GitHub issue #4792 and this answer for a workaround.
For simplicity, the following creates a wrapper script, which.ps1 , and saves it in the current directory. As stated, if you place it in one of the directories listed in $env:PATH, you'll be able to invoke it as just which.
The code below can easily be adapted to create a wrapper function instead: simply take the contents of the $wrapperCmdSource variable below and enclose it in function which { ... }.
As of PowerShell Core 7.0.0-preview.5, there are some problems with the auto-generated code, which may or may not affect you; they will be fixed at some point; to learn more and to learn how to manually correct them, see GitHub issue #10863.
# Create the wrapper scaffolding as source code (outputs a single [string])
$wrapperCmdSource =
[System.Management.Automation.ProxyCommand]::Create((Get-Command Get-Command))
# Write the auto-generated source code to a script file
$wrapperCmdSource > which.ps1
Note:
Even though System.Management.Automation.ProxyCommand.Create requires a System.Management.Automation.CommandMetadata instance to identify the target command, the System.Management.Automation.CommandInfo instances output by Get-Command can be used as-is.
Re comment-based help: By default, the proxy function simply forwards to the original cmdlet's help; however, you can optionally pass a string to serve as the comment-based help as the 2nd argument.
By using [System.Management.Automation.ProxyCommand]::GetHelpComments() in combination with output from Get-Help, you could start with a copy of the original command's help and modify it:
[System.Management.Automation.ProxyCommand]::GetHelpComments((Get-Help Get-Command))
You now have a fully functional which.ps1 wrapper script that behaves like Get-Command itself.
You can invoke it as follows:
./which # Same as: Get-Command; tab completion of parameters supported.
./which -? # Shows Get-Command's help.
You can now edit the script file to perform the desired customization.
Note: The auto-generated source code contains a lot of boilerplate code; however, typically only one or two places need tweaking to implement the custom functionality.
Specifically, place the following command as the first statement inside the begin { ... } block:
if (-not $MyInvocation.ExpectingInput -and -not ($Name -or $CommandType -or $Module -or $FullyQualifiedModule)) {
Throw "Missing command name or filter."
}
This causes the script to throw an error if the caller didn't provide some way of targeting a specific command or group of commands, either by direct argument or via the pipeline.
If you invoke the modified script without arguments now, you should see the desired error:
PS> ./which.ps1
Missing command name or filter.
...
Other common types of customizations are:
Removing parameters from the wrapper, by simply removing the parameter declaration.
Adding additional parameters to the invocation of the wrapped command, by modifying the following line in the begin block:
# Add parameters, as needed.
$scriptCmd = { & $wrappedCmd #PSBoundParameters }
Preprocessing pipeline input before passing it to the wrapped command, by customizing the process block and replacing $_ with your preprocessed input in the following line:
# Replace $_ with a preprocessed version of it, as needed.
$steppablePipeline.Process($_)
For an example of a complete implementation of a proxy function, see this answer.
[1] Caveat for Linux users: since the Linux file-system is case is case-sensitive, invocation of your script won't work case-insensitively, the way commands normally work in PowerShell. E.g., if your script file name is Get-Foo.ps1, only Get-Foo - using the exact same case - will work, not also get-foo, for instance.
Is there a way to find a list of script files that reference a given module (.psm1)? In other words, get all files that, in the script code, use at least 1 of the cmdlets defined in the module.
Obviously because of PowerShell 3.0 and above, most of my script files don't have an explicit Import-Module MODULE_NAME in the code somewhere, so I can't use that text to search on.
I know I can use Get-ChildItem -Path '...' -Recurse | Select-String 'TextToSearchFor' to search for a particular string inside of files, but that's not the same as searching for any reference to any cmdlet of a module. I could do a search for every single cmdlet in my module, but I was wondering if there is a better way.
Clarification: I'm only looking inside of a controlled environment where I have all the scripts in one file location.
Depending on the scenario, the callstack could be interesting to play around with. In that case you need to modify the functions which you want to find out about to gather information about the callstack at runtime and log it somewhere. Over time you might have enough logs to make some good assumptions.
function yourfunction {
$stack = Get-PSCallStack
if ($stack.Count -gt 1) {
$stack[1] # log this to a file or whatever you need
}
}
This might not work at all in your scenario, but I thought I throw it in there as an option.
Let's take the PowerShell statement below as an example:
powershell.exe c:\temp\windowsbroker.ps1 IIS
Is it possible to have it scripted within windowsbroker.ps1 to check for that IIS string, and if it's present to do a specific install script? The broker script would be intended to install different applications depending on what string followed it when it was called.
This may seem like an odd question, but I've been using CloudFormation to spin up application environments and I'm specifying an "ApplicationStack" parameter that will be referenced at the time when the powershell script is run so it knows which script to run to install the correct application during bootup.
What you're trying to do is called argument or parameter handling. In its simplest form PowerShell provides all arguments to a script in the automatic variable $args. That would allow you to check for an argument IIS like this:
if ($args -contains 'iis') {
# do something
}
or like this if you want the check to be case-sensitive (which I wouldn't recommend, since Windows and PowerShell usually aren't):
if ($args -ccontains 'IIS') {
# do something
}
However, since apparently you want to use the argument as a switch to trigger specific behavior of your script, there are better, more sophisticated ways of doing this. You could add a Param() section at the top of your script and check if the parameter was present in the arguments like this (for a list of things to install):
Param(
[Parameter()]
[string[]]$Install
)
$Install | ForEach-Object {
switch ($_) {
'IIS' {
# do something
}
...
}
}
or like this (for a single option):
Param(
[switch]$IIS
)
if ($IIS.IsPresent) {
# do something
}
You'd run the script like this:
powershell "c:\temp\windowsbroker.ps1" -Install "IIS",...
or like this respectively:
powershell "c:\temp\windowsbroker.ps1" -IIS
Usually I'd prefer switches over parameters with array arguments (unless you have a rather extensive list of options), because with the latter you have to worry about spelling of the array elements, whereas with switches you got a built-in spell check.
Using a Param() section will also automatically add a short usage description to your script:
PS C:\temp> Get-Help windowsbroker.ps1
windowsbroker.ps1 [-IIS]
You can further enhance this online help to your script via comment-based help.
Using parameters has a lot of other advantages on top of that (even though they probably aren't of that much use in your scenario). You can do parameter validation, make parameters mandatory, define default values, read values from the pipeline, make parameters depend on other parameters via parameter sets, and so on. See here and here for more information.
Yes, they are called positional parameters. You provide the parameters at the beginning of your script:
Param(
[string]$appToInstall
)
You could then write your script as follows:
switch ($appToInstall){
"IIS" {"Install IIS here"}
}
Is there a way to programmatically load the documentation of a .ps1 script file outside of commands like get-help? In other words, can text defined under .SYNOPSIS, .DESCRIPTION, etc. be accessed programmatically other than filtering the string output of get-help itself?
Among other things, I'm trying to find where I have gaps in documentation coverage in my script library. I'd also like to be able to display lists of certain scripts with their synopsis attached.
Yes, those are all accessible. Get-Help returns (just like any other cmdlet) an object, and the default rendering of that object is what you see in the console.
However, if you pump get-help's output through format-list, like this:
get-help get-childitem | format-list
You'll get a list of name-value pairs of the properties. To get the synopsis, you can do the following:
get-help get-childitem |select-object -property synopsis
And the output:
Synopsis
--------
Gets the files and folders in a file system drive.
If your .ps1 file has no cmdlets defined in it (your comment-based help covers the whole script), get-help file.ps1|select synopsis should work. Otherwise, you'll need to "dot-source" the files to load the cmdlet definitions into memory, then use get-help as above.