A guide to PowerShell’s Advanced Functions

Someone asked on the PowerShell Newsgroup about writing Advanced Functions, and specifically:

looking for a … guide to putting together an advanced function that is visible and usable every time I start Powershell. By visible I mean that when I do a ‘get-command’ I want my [advanced function]s to be listed alongside all the regular cmdlets. What makes that possible? ... what do I need to do to make that happen? Whats the difference between an [advanced function] and a module?

There are lots of articles on the Microsoft PowerShell team blog about both topics, but it seems there’s not really been any sort of step-by-step written, so I posted this to the newsgroup, and since the person who asked the original question said he found it useful, I figured I’d share it here…

An Aside: Advanced Functions vs. Modules

Advanced functions and script modules are unrelated topics. A script module could have nothing but old PowerShell 1 -type functions, hypothetically, it could even not have any functions at all. Advanced functions can be defined in a plain .ps1 script, in your profile, or by typing them in on the command-line, etc. The two things are essentially unrelated except for both being new features in PowerShell 2.0.

How To write an Advanced Function

Step 1: Have something you want to do in a function.

I can’t emphasize enough how important it is that you should have a specific goal here. Writing a script for a specific purpose is much easier, because you don’t have to think about whether or not specific features are necessary, etc.

For the purposes of this example, I want a function to start executables and return me the process object (so I can wait for it to finish, or whatever). I will call it Start-Process.

Step 2: Write a first step for the logic of your function.

Be sure to specify the parameters using the function Name{Param(...)} syntax rather than function Name(...){}, then you will be able to easily convert it to an advanced function by adding [CmdletBinding()] on the line right before the Param like this:

function Start-Process {
[CmdletBinding()]
param($app,$params)
   if($param) {
      [Diagnostics.Process]::Start( $app, $param )
   } else {
      [Diagnostics.Process]::Start( $app )
   }
}

Step 3: Start writing documentation and auto-help, including your parameters.

function Start-Process {
################################################################
#.Synopsis
#  Starts an application, with optional command-line parameters.
#.Parameter App
#  The path to the application you want to start
#.Parameter Params
#  The string consisting of all the parameters to pass to App
################################################################
[CmdletBinding()]
param($app,$params)
   if($param) {
      [Diagnostics.Process]::Start( $app, $param )
   } else {
      [Diagnostics.Process]::Start( $app )
   }
}

Step 4: Exploit advanced features by marking up your parameters.

In my case, I had a look at the signature of the start method by running [Diagnostics.Process]::Start.OverloadDefinitions and discovered that it can take credentials too. So I wanted an optional credential parameter. I specified it without a position, and made it part of a non-default parameter set.

I also wanted the params parameter to just take any additional parameters which I pass to the function, and pass them on to the process that I’m starting.

Finally, I wanted the “App” parameter to be able to come from the pipeline. Specifically, I wanted to be able to pass the output of Get-Command or Get-ChildItem to it. In order to enable that, I defined an alias on it to the relevant properties of the objects which come from those two commands

Hopefully you’re getting the idea of what’s possible here …

function Start-Process {
################################################################
#.Synopsis
#  Starts an application, with optional command-line parameters.
#.Parameter App
#  The path to the application you want to start
#.Parameter Params
#  The string consisting of all the parameters to pass to App
#.Parameter Credential
#  PSCredential containing valid login to "run as" another user.
################################################################
[CmdletBinding(DefaultParameterSetName="NoCreds")]
param(
  [Parameter(Position=1,Mandatory=$true,
             ValueFromPipelineByPropertyName=$true)]
  [Alias("FullName","Path")]
  $app
,
  [Parameter(Mandatory=$true,ParameterSetName="RunAs")]
  [Alias("PSCredential")]
  [System.Management.Automation.PSCredential]$Credential
,
  [Parameter(Position=3, Mandatory=$false,
             ValueFromRemainingArguments=$true )
  [string[]]$params
)

  if($credential) {
    $cred=$credential.GetNetworkCredential()
    if($params) {
      [Diagnostics.Process]::Start(
         $app,
         $([string]::join(" ",$params)),
         $cred.UserName,
         $credential.Password,
         $cred.Domain )
    } else {
      [Diagnostics.Process]::Start(
         $app,
         $cred.UserName,
         $credential.Password,
         $cred.Domain )
    }
  } else {
    if($params) {
      [Diagnostics.Process]::Start(
         $app,
         $([string]::join(" ",$params)) )
    } else {
      [Diagnostics.Process]::Start( $app )
    }
  }
}

Bonus Step: Make the function be automatically defined

To be honest, there are a lot of ways to do this, here’s a few:

Option 1. Define it in your Profile.

Include the full text of the function in your Profile.ps1 script or Microsoft.PowerShell_profile.ps1 … just run notepad $Profile and paste the whole function in at the top, then save.

Option 2. Dot-source it in your Profile.

Save the function into a script file, e.g.: “StartProcessFunction.ps1” in your DocumentsWindowsPowerShell folder (next to your profile script) then put the line: . StartProcessFunction.ps1 into your profile script to dot-source it. Using the dot causes the script to be loaded into the context as though you had put the contents of the script in, instead of the dot-source line.

Option 3. Import this function from a script file.

This is basically the same as Option 2, except instead of “.” you can use Import-Module. The Import-Module cmdlet, when used on a file with the .ps1 extension, is basically the same as dot-sourcing it, so there’s not a whole lot of benefit over dot-sourcing in this case.

Option 4. Put the function in a module.

You should only do this if you think you might want to add additional functions to the module. For instance, lets say you want to write a few other functions for working with process objects. If you put them together in the same module (say: “ProcessUtility”) they can share script-scoped variables or private functions, and can be identified as being part of the same module using the Get-Command -Module ProcessUtility command.

If you just have the single function, there’s really no point in changing it to a Module, but I’m not aware of any downside, either.

Option 4a. Make a module by just saving as a .psm1

All you do is save the function into a module file, e.g.: “ProcessUtility.psm1” in your DocumentsWindowsPowerShell folder and then put the line: Import-Module ProcessUtility.psm1 into your profile script.

Option 4b. Make a module folder.

This is by far the most extensible way to create modules. You paste the function into “ProcessUtility.psm1” and then save it into a folder “ProcessUtility” which must be created in one of your module folders. You can figure out where the module folders are by checking $Env:PSMODULEPATH — the normal spot is your DocumentsWindowsPowerShellModules.

Once that’s done, you can import with the simpler command Import-Module ProcessUtility without any extension.

The nice thing about creating the folder is that you can put any additional resources in that folder with it, and get at them using the $PsScriptRoot to define the folder. You can even add additional functions by putting them in separate files (which you can Import-Module from the main file, or you can define using a .psd1, but that’s another article).

Similar Posts:

10 thoughts on “A guide to PowerShell’s Advanced Functions”

  1. Dear Joel,

    firstOfAll: thank you very much for your powershell guides!!!
    I love to read through them … (if I’ve got the time) ...

    Just one question and a short comment:

    1) You specified: DefaultParameterSetName=“NoCreds”
    I’m no PS-expert … but shouldn’t there be a ParameterSet with the tag “NoCred” defined somewhere in the param-section?

    2) If I try to start a process with:

    [Diagnostics.Process]::Start("notepad.exe", $c.GetNetworkCredential().UserName, $c.Password, $c.GetNetworkCredential().Domain)

    ... that’s what your adv.function will issue, if I add a parameter ( $c = get-Credential ) to the call:
    “Start-Process “notepad.exe” -Credential $c”
    I’m receiving an exception

    PS C:\Dokumente und Einstellungen\Schulte> Start-Process “notepad.exe” -Credential $c
    Exception calling “Start” with “4” argument(s): “Der Verzeichnisname ist ungültig”
    At H:\Powershell\Start-Process.ps1:38 char:35
    + [Diagnostics.Process]::Start <<<< (
    + CategoryInfo : NotSpecified: ( :) [], MethodInvocationException
    + FullyQualifiedErrorId : DotNetMethodException

    That exception ( in english: “invalid directory name” ) occurs only, if I add the Credentials to the call. This works:
    Start-Process “notepad.exe”

    As I figured out, that’s a problem with the “working directory”, that you can set explicitly using the “StartProcessInfo” parameter as an overload to the Start method. Using C# this works:

    System.Diagnostics.ProcessStartInfo psi = new System.Diagnostics.ProcessStartInfo();
    psi.Arguments = null;
    psi.Domain = "MyDomain";
    psi.FileName = @"c:\windows\System32\Notepad.exe";
    psi.Password = pwd;
    psi.UserName = "UserName";
    psi.UseShellExecute = false;
    psi.WorkingDirectory="C:\";
    System.Diagnostics.Process.Start(psi);

    Commenting out the psi.WorkingDirectory line results in the “invalid directory” exception!

    Strange but true :-(

    king regards, Klaus

  2. Nice guide – thanks for posting it! I really haven’t been taking advantage of what’s possible with advanced functions. So far, I’ve primarily been using it to require parameters (with [Parameter(Mandatory=$true)]). I need to try some more features out.

  3. Modules and Advanced functions seem a lot clearer to me now —Thanks for the guide.

    I’ve also heard the term “library” used to describe function(s) which you source. Should “processutility.ps1” be “processutility.psm1” in 4a?

  4. Klaus: I haven’t seen that invalid working directory exception … you must be trying to start the process with an account that doesn’t have rights to the folder you’re in.

    When you launch an app without specifying the Working Directory, it will be set to [Environment]::CurrentDirectory … if you use an account that can’t access that folder, you will get a Win32Exception “The directory name is invalid” (in English). The simplest solution is to set your CurrentDirectory, but as you discovered, it also works to use the ProcessStartInfo — which holds many other powers too :D

  5. Hello Joel,

    thank you very much for your reply!
    I won’t argue any more … but I’d like to add these lines and finish the discussion anyway. Things are sometimes not the way they should be and I can live with that :-)

    # A credential object: $c was created
    PS H:\Powershell> $c

    UserName                 Password
    DOMS\schulte            System.Security.SecureString

    # My CurrentDirectory is writeable (and readable) by my account
    PS H:\Powershell> out-file -inp "I can write to $([Environment]::CurrentDirectory)!" "$([Environment]::CurrentDirectory)\WriteToMe.txt"

    # I CAN'T start a process with my Credentials:
    PS H:\Powershell> [System.Diagnostics.Process]::Start("Notepad", $c.GetNetworkCredential().UserName, $c.Password, $c.GetNetworkCredential().Domain)

    Exception calling "Start" with "4" argument(s): "Der Verzeichnisname ist ungültig"
    At line:1 char:36
    + [System.Diagnostics.Process]::Start &lt;&lt;&lt; [System.Diagnostics.Process]::Start("Notepad")

    Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName                                                            
    -------  ------    -----      ----- -----   ------     -- -----------                                                            
          0       0      140         84     1     0,02   4304 notepad                                                                
     

    • Life is NOT ALWAYS dair ******

    kind regards, Klaus

    1. Yeah, that’s probably because $PWD -ne [Environment]::CurrentDirectory

      If you want it to be, you should add a line to your prompt function to set it: [Environment]::CurrentDirectory = $pwd

  6. Hi Jaykul,

    I think it was I who posted the message in the NG asking for clarification.

    As usual you come through with something far exceeding what I could have hoped for. I can’t thank you enough for the help you have provided me both in the NG and the IRC.

    I’m gonna email Jeffrey Snover every week till he gives you the MVP and backdates it a year Haha!!

    Thanks again,
    Stuart

Comments are closed.