Because I spend a great part of my day creating PowerShell related content, I often need to copy command output from a PowerShell session. The quick and dirty solution is to pipe my expression to the Clip.exe command line utility.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
get-service | where { $_.status -eq 'running'} | clip
This works in both the console and PowerShell ISE. But there are situations where I'd like to see the result so that I know it is worth pasting into whatever I'm working on. In other words I want to send the results of the command to the pipeline and the clipboard at the same time. PowerShell can already do something similar with the Tee-Object cmdlet. With that cmdlet you can tee output to a variable or a file. So I decided to build a wrapper around Tee-Object and add functionality to send tee output to the clipboard.
I created a copy of Tee-Object and started modifying. I didn't want to reverse-engineer the cmdlet. I simply wanted to add a new parameter. In some ways, my new command is like a proxy function.
#requires -version 4.0 <# This is a copy of: CommandType Name ModuleName ----------- ---- ---------- Cmdlet Tee-Object Microsoft.PowerShell.Utility Created: 8/12/2015 Author : Jeff Hicks @JeffHicks **************************************************************** * DO NOT USE IN A PRODUCTION ENVIRONMENT UNTIL YOU HAVE TESTED * * THOROUGHLY IN A LAB ENVIRONMENT. USE AT YOUR OWN RISK. IF * * YOU DO NOT UNDERSTAND WHAT THIS SCRIPT DOES OR HOW IT WORKS, * * DO NOT USE IT OUTSIDE OF A SECURE, TEST SETTING. * **************************************************************** #> Function Tee-MyObject { <# .SYNOPSIS Saves command output in a file, variable or Windows Clipboard and also sends it down the pipeline. .DESCRIPTION The cmdlet redirects output, that is, it sends the output of a command in two directions (like the letter "T"). It stores the output in a file or variable and also sends it down the pipeline. If is the last command in the pipeline, the command output is displayed at the prompt. This command is a modified version of Tee-Object. .PARAMETER Clipboard Copy the output to the Windows clipboard .PARAMETER Append Appends the output to the specified file. Without this parameter, the new content replaces any existing content in the file without warning. This parameter is introduced in Windows PowerShell 3.0. .PARAMETER FilePath Saves the object in the specified file. Wildcard characters are permitted, but must resolve to a single file. .PARAMETER InputObject Specifies the object to be saved and displayed. Enter a variable that contains the objects or type a command or expression that gets the objects. You can also pipe an object to . When you use the InputObject parameter with , instead of piping command results to , the InputObject value—even if the value is a collection that is the result of a command, such as –InputObject (Get-Process)—is treated as a single object. Because InputObject cannot return individual properties from an array or collection of objects, it is recommended that if you use to perform operations on a collection of objects for those objects that have specific values in defined properties, you use in the pipeline, as shown in the examples in this topic. .PARAMETER Variable Saves the object in the specified variable. Enter a variable name without the preceding dollar sign ($). .PARAMETER LiteralPath Saves the object in the specified file. Unlike FilePath, the value of the LiteralPath parameter is used exactly as it is typed. No characters are interpreted as wildcards. If the path includes escape characters, enclose it in single quotation marks. Single quotation marks tell Windows PowerShell not to interpret any characters as escape sequences. .EXAMPLE PS C:\> Get-Eventlog -list | Tee-MyObject -clipboard Max(K) Retain OverflowAction Entries Log ------ ------ -------------- ------- --- 20,480 0 OverwriteAsNeeded 30,245 Application 20,480 0 OverwriteAsNeeded 0 HardwareEvents 512 7 OverwriteOlder 0 Internet Explorer 20,480 0 OverwriteAsNeeded 0 Key Management Service 128 0 OverwriteAsNeeded 1,590 OAlerts 32,768 0 OverwriteAsNeeded 49,749 Security 20,480 0 OverwriteAsNeeded 33,273 System 15,360 0 OverwriteAsNeeded 11,442 Windows PowerShell Display eventlog information and also copy it to the Windows clipboard. .EXAMPLE PS C:\> get-process | Tee-MyObject -filepath C:\Test1\testfile2.txt Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName ------- ------ ----- ----- ----- ------ -- ----------- 83 4 2300 4520 39 0.30 4032 00THotkey 272 6 1400 3944 34 0.06 3088 alg 81 3 804 3284 21 2.45 148 ApntEx 81 4 2008 5808 38 0.75 3684 Apoint ... This command gets a list of the processes running on the computer and sends the result to a file. Because a second path is not specified, the processes are also displayed in the console. .EXAMPLE PS C:\> get-process notepad | Tee-MyObject -variable proc | select-object processname,handles ProcessName Handles ----------- ------- notepad 43 notepad 37 notepad 38 notepad 38 This command gets a list of the processes running on the computer and sends the result to a variable named "proc". It then pipes the resulting objects along to Select-Object, which selects the ProcessName and Handles property. Note that the $proc variable includes the default information returned by Get-Process. .EXAMPLE PS C:\>get-childitem –path D: –file –system –recurse | Tee-MyObject –file c:\test\AllSystemFiles.txt –append | out-file c:\test\NewSystemFiles.txt This command saves a list of system files in a two log files, a cumulative file and a current file. The command uses the Get-ChildItem cmdlet to do a recursive search for system files on the D: drive. A pipeline operator (|) sends the list to , which appends the list to the AllSystemFiles.txt file and passes the list down the pipeline to the Out-File cmdlet, which saves the list in the NewSystemFiles.txt file. .NOTES You can also use the Out-File cmdlet or the redirection operator, both of which save the output in a file but do not send it down the pipeline. uses Unicode encoding when it writes to files. As a result, the output might not be formatted properly in files with a different encoding. To specify the encoding, use the Out-File cmdlet. Learn more about PowerShell:Essential PowerShell Learning Resources.INPUTS System.Management.Automation.PSObject .OUTPUTS System.Management.Automation.PSObject .LINK Tee-Object Select-Object about_Redirection #> [CmdletBinding(DefaultParameterSetName='File')] Param( [Parameter(ValueFromPipeline=$true)] [psobject]$InputObject, [Parameter(ParameterSetName='File', Mandatory=$true, Position=0)] [string]$FilePath, [Parameter(ParameterSetName='LiteralFile', Mandatory=$true)] [Alias('PSPath')] [string]$LiteralPath, [Parameter(ParameterSetName='File')] [switch]$Append, [Parameter(ParameterSetName='Variable', Mandatory=$true)] [string]$Variable, [Parameter(ParameterSetName='Clip')] [Switch]$Clipboard ) Begin { Write-Verbose "Starting $($MyInvocation.Mycommand)" Write-Verbose "Using parameter set $($PSCmdlet.ParameterSetName)" #initialize array to hold all input. We will process it later #with Tee-Object $data=@() } #begin Process { #add each piped in object to the array $data+= $InputObject } #process End { Write-Verbose "Processing $($data.count) input items" #run results through Tee-Object or Clipboard #remove Clipboard from PSBoundParameters if ($Clipboard) { #copy to clipboard with extra spaces removed $text = ($data | Out-String).Trim() -replace "\s+`n","`n" #make sure type is loaded Write-Verbose "Adding necessary .NET assembly" Add-Type -AssemblyName system.windows.forms [System.Windows.Forms.Clipboard]::SetText( $text ) #write data to the pipeline $data } Else { $PSBoundParameters.InputObject = $data Write-Verbose "Using PSBoundparameters $($PSBoundParameters | Out-String)" Tee-Object @PSBoundParameters if ($Variable) { #raise the scope to the parent or calling level to persist it Write-Verbose "Adjusting variable scope" #create a copy of the variable in the parent scope New-Variable -Name $Variable -Value $(Get-Variable -name $variable).value -Scope 1 -Force } } Write-Verbose "Ending $($MyInvocation.Mycommand)" } #end } #end function Tee-Object #define an optional alias Set-Alias -Name tmo -Value Tee-MyObject
I use Tee-MyObject much the same way as Tee-Object, even with the same parameters. Those are splatted through $PSBoundParameters. There are a few items I want to point out, more as a matter of scripting technique than anything.
First, because my function is wrapping around Tee-Object, which typically has input piped to it, I needed to temporarily store piped in objects to my function. That's why I initialize a variable, $data and add each input object to it. After everything has been piped to my function, I can pass the data to Tee-Object, assuming I'm using one of its parameters.
$PSBoundParameters.InputObject = $data
Write-Verbose "Using PSBoundparameters $($PSBoundParameters | Out-String)"
Tee-Object @PSBoundParameters
However, because I am running Tee-Object inside of a function, scope becomes an issue. When I use the –Variable parameter, Tee-Object creates the variable with no problem. But when the function ends, the variable is destroyed. My solution is to create a new copy of the variable and specify the parent scope.
if ($Variable) {
#raise the scope to the parent or calling level to persist it
Write-Verbose "Adjusting variable scope"
#create a copy of the variable in the parent scope
New-Variable -Name $Variable -Value $(Get-Variable -name $variable).value -Scope 1 -Force
}
I needed this "tricks" in order to pass on full functionality between my wrapper function and Tee-Object.
For the clipboard part, I originally used code to simply pipe data to Clip.exe. However, this can temporary flash a console window and depending on the command I also end up with lots of extra white space at the end of each line. So I decided to use the .NET framework to add content to the clipboard. The tricky part was converting the output to text and cleaning it up. I ended up using the Trim() method to remove leading and trailing spaces after piping the data to Out-String. This leaves me with one long string. To clean up the extra space at the end of some lines, I use the –Replace operator to find anything that matches a bunch of spaces followed by the new line marker and replace it with just the new line marker. This has the effect of trimming away all those spaces.
$text = ($data | Out-String).Trim() -replace "\s+`n","`n"
And that's it! My version even has a modified copy of the help for Tee-Object.
Modified help
I can use my command in place of Tee-Object. I even defined my own alias.
Using Tee-MyObject
To be clear, I have not modified, overwritten or removed Tee-Object. I have simply created a new version of the command with some additional functionality. If you have any questions on my process or some scripting element, please post a comment.
Enjoy!
Great example of a proxy function!
Or, an almost-proxy function. 🙂
It would be very interesting to see this implemented as an actual proxy function, and then we could contrast the effort needed for each approach.
So I tried doing this as a true proxy function. In this situation I don’t think a proxy will work. Ran into a number of issues such as required parameters and scoping that don’t have quick and easy solutions.