Expert Solution for 2011 Scripting Games Advanced Event 10: Use a PowerShell Proxy Function to Create Temporary Files

Expert Solution for 2011 Scripting Games Advanced Event 10: Use a PowerShell Proxy Function to Create Temporary Files

  • Comments 2
  • Likes

Summary: Microsoft PowerShell MVP Shay Levy shows how to use a proxy function to easily create temporary files and solve Advanced Event 10 in the 2011 Scripting Games.

Microsoft Scripting Guy, Ed Wilson, here. As we wrap up the 2011 Scripting Games today, we have Shay Levy as our expert commentator for Advanced Event 10.

Photo of Shay Levy

Shay Levy is a Windows PowerShell MVP. He often writes about scripting issues in his blog, Script Fanatic. He is also the author of the very popular PowerShell Community Toolbar, which you can download from his blog. Shay is also a moderator of the Official Scripting Guys Forum and the Windows PowerShell forum.

Shay's contact information:
Blog: Script Fanatic
Twitter: ShayLevy

Worked solution

Before we start with our solution for Advanced Event 10, let’s refresh our memory with the steps needed for the event:

1. The function must accept pipelined content. We need to create a parameter that uses a ValueFromPipeline attribute.

2. The temporary file should have a temporary name, and reside in the temporary folder. Creating the file in the temp directory can be done easily—we can get the path to the temp directory with $env:TEMP environment variable, but what about the file name? We can choose an arbitrary file name but what if a file name with that name already exists in the temp folder? Do we need to write custom code that tests if the file already exists, and if so change the name? Should we do it in a loop until we find a name that doesn’t exist? We will see how we can use the .NET library to do that.

3. Additional points for adding a switch that will display the contents of the temporary file in Notepad. After we create the file, we can invoke Notepad and pass the file path to it.

4. Additional points for supporting an Encoding parameter and supporting ASCII and Unicode as encoding types. This is the major hurdle! How do we do that? Most of us are not developers. How many of us know how to write code to encode files? Fear not! The solution is actually very simple.

5. Additional points for supporting the WhatIf, Debug, and Verbose parameters. Luckily, we can do this with a very short instruction. Advanced functions makes it very easy to add common parameters.

So, how are we going to write it? The solution for this event is going to use one of the coolest and exciting new features in Windows PowerShell 2.0: Proxy functions!

Proxy commands are used primarily by the remoting mechanism in Windows PowerShell. It is used in Restricted Runspace configurations (that is, end points) where we can modify sessions by adding or removing certain commands, providers, cmdlet parameters and every other components of a Windows PowerShell session. For instance, we can remove the ComputerName parameter from all cmdlets that support that parameter.

Another major usage of Proxy commands is performed in implicit remoting. In a nutshell, implicit remoting lets you use commands from a remote session inside your local session by using the Import-PSSession cmdlet. For example, say you manage Exchange Server 2010, and you do not have the Exchange admin tools installed on your admin machine. Normally you would create a remote desktop session, launch the Exchange Management Shell (EMS), and perform your tasks. With implicit remoting, you can create a PSSession to the Exchange server and import the Exchange commands into your local session.

When you do that, Windows PowerShell dynamically creates a module in your temp folder, and all of the imported commands are converted to functions—Proxy functions. From now on, each time you run an Exchange command in your local session, it will run on the remote server. This feature enables us to manage multiple servers without installing each server's admin tools locally!

So what is a Proxy function? It is a wrapper function around a cmdlet (or another function). When we proxy a command, we get access to its parameters (which we can add or remove). We also get control over the command 'Steppable Pipeline'—the three script blocks of a Windows PowerShell function: Begin, Process, and End.

The process of creating a Proxy function is relatively easy. You choose a cmdlet that you want to wrap and then get the cmdlet metadata (its grammar). In the following example, we get the code for the Out-File cmdlet.

PS > $cmdlet = Get-Command -Name Out-File –CommandType Cmdlet
PS > $CommandMetadata= New-Object System.Management.Automation.CommandMetadata -ArgumentList $cmdlet
PS > [System.Management.Automation.ProxyCommand]::Create($CommandMetadata)

First, we get the Out-File cmdlet by using the Get-Command cmdlet. Then we create the metadata for the command, and finally, we generate the code. The command metadata is shown here.

[CmdletBinding(DefaultParameterSetName='ByPath', SupportsShouldProcess=$true, ConfirmImpact='Medium')]

param(
    [Parameter(ParameterSetName='ByPath', Mandatory=$true, Position=0)]
    [string]
    ${FilePath},

    [Parameter(ParameterSetName='ByLiteralPath', Mandatory=$true, Position=0, ValueFromPipelineByPropertyName=$true)]
    [Alias('PSPath')]
    [string]
    ${LiteralPath},

    [Parameter(Position=1)]
    [ValidateNotNullOrEmpty()]
[ValidateSet('unknown','string','unicode','bigendianunicode','utf8','utf7','utf32','ascii','default','oem')]

    [string]
    ${Encoding},

    [switch]
    ${Append},

    [switch]
    ${Force},

    [Alias('NoOverwrite')]
    [switch]
    ${NoClobber},

    [ValidateRange(2, 2147483647)]
    [int]
    ${Width},

    [Parameter(ValueFromPipeline=$true)]
    [psobject]
    ${InputObject})

 

Begin
{
    try {
        $outBuffer = $null
        if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
        {
            $PSBoundParameters['OutBuffer'] = 1
        }

        $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Out-File', [System.Management.Automation.CommandTypes]::Cmdlet)
        $scriptCmd = {& $wrappedCmd @PSBoundParameters }
        $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
        $steppablePipeline.Begin($PSCmdlet)
    } catch {
        throw
    }
}

 

Process
{
    try {
        $steppablePipeline.Process($_)
    } catch {
        throw
    }
}

End
{
    try {
        $steppablePipeline.End()
    } catch {
        Throw
    }
}
<#
.ForwardHelpTargetName Out-File
.ForwardHelpCategory Cmdlet
#>

And that took only three lines of code! Do not get intimidated by the result. That is what a Proxy function looks like. As you can see, the code starts by including all the cmdlet parameters and then it defines the script blocks of the command. At the end, there are instructions that are used by the Get-Help cmdlet. 

In the Begin block, the Out-File command ($wrappedCmd) is retrieved, and the parameters are passed to it (@PSBoundParameters), a Steppable Pipeline is initialized ($steppablePipeline) which invokes the cmdlet ($scriptCmd), and finally the Begin block starts. At the end of the code, you can find a block of multiline comment that is used to instruct the Get-Help cmdlet. It should display this Help when you type Get-Help <ProxyFunction>.

The code starts with the CmdletBinding attribute. It is an attribute that declares a function that acts similar to a cmdlet. CmdletBinding can have several attributes, SupportsShouldProcess, for example. When the SupportsShouldProcess argument is set to True, it indicates that the function supports calls to the ShouldProcess method, which is used to prompt the user for feedback before the function makes a change to the system. When this argument is specified, the Confirm and WhatIf parameters are enabled for the function. Another CmdletBinding argument is DefaultParameterSetName. When the function has more than one ParameterSet, this attribute argument defines which one is the default ParameterSet for the function.

The Out-File cmdlet has two ParameterSets: ByPath and ByLiteralPath. The first parameter, ByPath is set as the DefaultParameterSetName.

After CmdletBinding comes the parameters section. In our solution, we are going to modify the param block, remove some parameters and add a new one.

Because our function doesn't require multiple parameters sets we need to remove "DefaultParameterSetName='ByPath'". After the change it will look like this:

[CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='Medium')]

Now let us move to the parameters section. We need to create a file in the user's temp directory, which means that the path is always the user's temp directory and that makes the path parameters redundant.

We are going to remove the FilePath and the LiteralPath parameters. One thing though, both the FilePath and the LiteralPath parameters are mandatory parameters. If we try to call our proxy command without one of them, the command will fail, so we are going to introduce the FilePath inside the function in the Begin block. We don't need the Append, Force, NoClobber, and the Width parameters, although we can leave them in place, but for this solution we are going to remove them. The modified param block is shown here.

param(

    [Parameter(Position=1)]
    [ValidateNotNullOrEmpty()]    [ValidateSet('unknown','string','unicode','bigendianunicode','utf8','utf7','utf32','ascii','default','oem')]
    [string] ${Encoding},

    [Parameter(ValueFromPipeline=$true)]
    [psobject]
    ${InputObject},

    [switch]${Invoke}   
)

Notice that I added a new switch parameter, Invoke. This parameter, when specified will open the file in Notepad.

Next is the Begin block. Here, we need to remove the Invoke parameter if it has been specified. The Invoke parameter is not an original parameter of the Out-File cmdlet, and we will get an error if it is passed to Out-File. We do that by checking to see if it was specified at the command line. We can check that with the $PSBoundParameters variable.

$PSBoundParameters is a special variable that is available inside advanced functions. It is a hashtable that contains the parameters that were bound and the values that were bound to them.

if($PSBoundParameters['Invoke'])
{
      $null = $PSBoundParameters.Remove('Invoke')
}

Now we are going to create the value of the FilePath parameter, but first we need to check if the WhatIf switch parameter is present. If the WhatIf switch was bound, we don't want to create the temp file. We are using the GetRandomFileName() static method of the System.IO.Path .NET class to generate a random file or folder name for us (this does not create a file). Otherwise, we use a second method of the System.IO.Path .NET class, GetTempFileName(), which creates a uniquely named, zero-byte temporary file on the disk and returns the full path of that file. We assign the result to the $tempFile variable, and then we add back the FilePath parameter by using the Add() method of the $PSBoundParameters member. These modifications are reflected in the code that is shown here.

Begin
{
    try {
      $outBuffer = $null
      if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
      {
          $PSBoundParameters['OutBuffer'] = 1
      }

      if($PSBoundParameters['Invoke'])
      {
            $null = $PSBoundParameters.Remove('Invoke')
      }

      if($PSBoundParameters['WhatIf'])
      {
            $tempFile = Join-Path -Path $env:TEMP -ChildPath ([System.IO.Path]::GetRandomFileName())
      }
      else
      {
            $tempFile = [System.IO.Path]::GetTempFileName()
      }


      $PSBoundParameters.Add('FilePath',$tempFile)

      $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Out-File', [System.Management.Automation.CommandTypes]::Cmdlet)
      $scriptCmd = {& $wrappedCmd @PSBoundParameters }
      $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
      $steppablePipeline.Begin($PSCmdlet)
    } catch {
      throw
    }
}

The Process block remains the same; no changes are made in it. In the End block, after all content is piped to the file, we check if the user wants to open the file in Notepad. First, we need to check that the WhatIf switch was not specified. If the Invoke parameter is present, we pass the value of the $tempFile variable to Notepad.exe and we return the newly created temp file as a FileInfo object. Otherwise, we return a FileInfo object. This is shown here.

End
{
    try {
      $steppablePipeline.End()

      if(!$PSBoundParameters['WhatIf'])
      {
            if($Invoke)
            {
                  Write-Verbose "Invoking file in notepad"
                  Get-ChildItem –Path  $tempFile
                  notepad $tempFile
            }
            else
            {
                  Write-Verbose "No parameters has been specified, emit temp file."
                  Get-ChildItem –Path $tempFile
            }
      }
    } catch {
      throw
    }
}

After all the modifications we made to the code, we wrap the code in a function declaration and call the function New-TempFile. Comment-based Help is also added at the end of the function. This is shown here.

function New-TempFile
{
      [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='Medium')]

      param(
          [Parameter(Position=1)]
          [ValidateNotNullOrEmpty()]
[ValidateSet('unknown','string','unicode','bigendianunicode','utf8','utf7','utf32','ascii','default','oem')]

          [string]
          ${Encoding},

          [Parameter(ValueFromPipeline=$true)]
          [psobject]
          ${InputObject},

          [switch]${Invoke}
      )

                       

      Begin
      {
          try {
            $outBuffer = $null
            if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
            {
                $PSBoundParameters['OutBuffer'] = 1
            }

            if($PSBoundParameters['Invoke'])
            {
                  $null = $PSBoundParameters.Remove('Invoke')
            }

            if($PSBoundParameters['WhatIf'])
            {
                  $tempFile = Join-Path -Path $env:TEMP -ChildPath ([System.IO.Path]::GetRandomFileName())
            }
            else
            {
                  $tempFile = [System.IO.Path]::GetTempFileName()
            }

            $PSBoundParameters.Add('FilePath',$tempFile)

 

            $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Out-File', [System.Management.Automation.CommandTypes]::Cmdlet)
            $scriptCmd = {& $wrappedCmd @PSBoundParameters }
            $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
            $steppablePipeline.Begin($PSCmdlet)
          } catch {
            throw
          }
      }

 

      Process
      {
          try {
            $steppablePipeline.Process($_)
          } catch {
            throw
          }
      }

 

      End
      {
          try {
            $steppablePipeline.End()
            if(!$PSBoundParameters['WhatIf'])
            {
                  if($Invoke)
                  {
                        Write-Verbose "Invoking file in notepad"
                        Get-ChildItem $tempFile
                        notepad $tempFile
                  }
                  else
                  {
                        Write-Verbose "No parameters has been specified, emit temp file."
                        Get-ChildItem $tempFile
                  }
            }
          } catch {
            throw
          }
      }
<#
.DESCRIPTION
      The New-TempFile function creates a new temp file in the user's temp directory.

.SYNOPSIS
      The New-TempFile function creates a new temp file in the user's temp directory.

.PARAMETER Encoding
      Specifies the type of character encoding used in the file. Valid values are "Unicode", "UTF7", "UTF8", "UTF32",

      "ASCII", "BigEndianUnicode", "Default", and "OEM". "Unicode" is the default.
      "Default" uses the encoding of the system's current ANSI code page.
      "OEM" uses the current original equipment manufacturer code page identifier for the operating system.

.PARAMETER InputObject
      Specifies the objects to be written to the file. Enter a variable that contains the objects or type a command or expression that gets the objects.

.PARAMETER Invoke
      Opens the new file in notepad.exe.

.EXAMPLE
      New-TempFile

      Directory: C:\Users\<UserName>\AppData\Local\Temp

      Mode                LastWriteTime     Length Name
      ----                -------------     ------ ----
      -a---          4/4/2011   4:32 PM          0 tmpF58D.tmp

      This command creates an empty temp file in the user's temp folder

.EXAMPLE
      New-TempFile -WhatIf

      What if: Performing operation "Output to File" on Target "C:\Users\<UserName>\AppData\Local\Temp\n54us1bn.b5v"

      This command shows would have have happen if  creates a temp file in the user's temp folder and sets the file content with the result of the Get-Process cmdlet. If the Invoke switch has been specified , the file is displayed in notepad.exe.

.EXAMPLE
      Get-Service | New-TempFile

      Directory: C:\Users\<UserName>\AppData\Local\Temp

      Mode                LastWriteTime     Length Name
      ----                -------------     ------ ----
      -a---          4/4/2011   5:27 PM      26937 tmp71B7.tmp

      This command creates a temp file in the user's temp directory and sets the    file content with the result of the Get-Service cmdlet. The command returns the new temp file as a FileInfo object. Since no value has been specified for the Encoding parameter, the file is encoded as Unicode (default value).

.EXAMPLE
      New-TempFile -Encoding ASCII -InputObject (Get-Process) -Invoke

      Directory: C:\Users\<UserName>\AppData\Local\Temp

      Mode                LastWriteTime     Length Name
      ----                -------------     ------ ----
      -a---          4/4/2011   5:41 PM      11498 tmp94E5.tmp

      This command creates a temp file in the user's temp directory and sets the file content with the result of the Get-Process cmdlet. The result is a FileInfo object of the file. When the Invoke is specified, the file is displayed in notepad.exe. The file is ASCII encoded.

.INPUTS
      System.Management.Automation.PSObject

.OUTPUTS
      System.IO.FileInfo
      When the Invoke switch parameter is specified the function will open the file in notepad.exe.

.NOTES
      Author: Shay Levy
      Blog  : http://PowerShay.com

#>
}


Notice that the original Help instructions for Get-Help has been removed:

<#
.ForwardHelpTargetName Out-File
.ForwardHelpCategory Cmdlet
#>

We don't want Get-Help to display the Help for Out-File, so we remove them and add our own custom comment-based Help. Help for the Encoding and InputObject parameters are copied from the Out-File Help.

Now let's see the New-TempFile function in action. We will use the examples in the comment-based Help to test it. Before we do that, we need to load the function into the console. We can do that with a copy/paste, or by saving the function code to a new script file and then dot-source it. In the following command example, I've saved the file as New-TempFile.ps1 in the root of drive C, and then I loaded it into the shell by dot-sourcing it:

PS > . C:\New-TempFile.ps1

Now let's run some tests.

PS > New-TempFile

      Directory: C:\Users\<UserName>\AppData\Local\Temp

      Mode                LastWriteTime     Length Name
      ----                -------------     ------ ----
      -a---          4/4/2011   4:32 PM          0 tmpF58D.tmp

Calling New-TempFile without any parameters creates a new temp file in the users temp directory. We can see that the file name is unique and that the file is zero bytes size.

Now let's try with the WhatIf switch:

PS > New-TempFile -WhatIf

      What if: Performing operation "Output to File" on Target "C:\Users\<UserName>\AppData\Local\Temp\n54us1bn.b5v"

In this example the file was not created (the GetRandomFileName method was used), and we get a WhatIf that describes what would have happen if we actually ran the command.

PS > Get-Service | New-TempFile

      Directory: C:\Users\<UserName>\AppData\Local\Temp

      Mode                LastWriteTime     Length Name
      ----                -------------     ------ ----
      -a---          4/4/2011   5:27 PM      26937 tmp71B7.tmp

In this example, we test content piping. We pipe the result of the Get-Service cmdlet to the New-TempFile function. The InputObject parameter of New-TempFile gets the content (inside the Process block, it is configured to accept pipeline input), and the content is written to the temp file. You can see that the size of the file is 26937 bytes. The file is Unicode encoded, which is the default value of the Encoding parameter.

PS> New-TempFile -Encoding ASCII -InputObject (Get-Process) -Invoke

      Directory: C:\Users\<UserName>\AppData\Local\Temp

      Mode                LastWriteTime     Length Name
      ----                -------------     ------ ----
      -a---          4/4/2011   5:41 PM      7992 tmp31EC.tmp

This example writes the content of the Get-Process cmdlet to a new temp file. The file is ASCII encoded, and the file is invoked in Notepad.

Image of command output

We can verify that the file is ASCII encoded by going to the File > Save As… menu option, and then checking the value of Encoding.

Image of file

The last step is making the function available across sessions. To do this, simply add it to your $PROFILE.

That's it. As you can see, the solution with Proxy functions didn’t require us to write a lot of code. We were able to accomplish all the required steps of this event by utilizing an existing cmdlet, modifying it, and extending its functionality with a new feature. Proxy functions are great fun and very powerful, so go ahead and try them—I'm sure you'll find endless options to harness their power to your needs.

Shay's complete script can be found at the Script Repository.

Thank you Shay. That was a great approach to this problem.

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy

Your comment has been posted.   Close
Thank you, your comment requires moderation so it may take a while to appear.   Close
Leave a Comment
  • Funny, I was also thinking about proxy for that one, but my 'target' was set-content. I guess out-file is much better option for this one. And code looks so clean. :) But anyway - I'm happy that I've done it 'traditional' way, had some fun with getting ShouldProcess to work as I wanted it to. :) Great script, Shay. In my opinion best from expert solutions. :)

  • Thanks Bartek, you did a great job. Congratulations on the winning. Well deserved!