ClipboardText.psm1
<#
IMPORTANT: THIS MODULE MUST REMAIN PSv2-COMPATIBLE. #> # Module-wide defaults. # !! PSv2: We dectivate even the check for accessing nonexistent variables, because # !! of a pitfall where parameter variables belonging to a parameter set # !! other than the one selected by a given invocation are considered undefined. if ($PSVersionTable.PSVersion.Major -gt 2) { Set-StrictMode -Version 1 } #region == ALIASES Set-Alias scbt Set-ClipboardText Set-Alias gcbt Get-ClipboardText #endregion #region == Exported functions function Get-ClipboardText { <# .SYNOPSIS Gets text from the clipboard. .DESCRIPTION Retrieves text from the system clipboard as an arry of lines (by default) or as-is (with -Raw). If the clipboard is empty or contains no text, $null is returned. LINUX CAVEAT: The xclip utility must be installed; on Debian-based platforms such as Ubuntu, install it with: sudo apt install xclip .PARAMETER Raw Output the retrieved text as-is, even if it spans multiple lines. By default, if the retrieved text is a multi-line string, each line is output individually. .NOTES This function is a "polyfill" to make up for the lack of built-in clipboard support in Windows Powershell v5.0- and in PowerShell Core as of v6.1, albeit only with respect to text. In Windows PowerShell v5.1+, you can use the built-in Get-Clipboard cmdlet instead (which this function invokes, if available). .EXAMPLE Get-ClipboardText | ForEach-Object { $i=0 } { '#{0}: {1}' -f (++$i), $_ } Retrieves text from the clipboard and sends its lines individually through the pipeline, using a ForEach-Object command to prefix each line with its line number. .EXAMPLE Get-ClipboardText -Raw > out.txt Retrieves text from the clipboard as-is and saves it to file out.txt (with a newline appended). #> [CmdletBinding()] [OutputType([string])] param( [switch] $Raw ) $rawText = $lines = $null if (test-WindowsPowerShell) { # *Windows PowerShell* # Determine this thread's COM threading model, because the clipboard-access method # must be chosen accordingly. $isSTA = [threading.thread]::CurrentThread.ApartmentState.ToString() -eq 'STA' if ($PSVersionTable.PSVersion.Major -ge 5 -and $isSTA) { # Ps*Win* v5+ has Get-Clipboard / Set-Clipboard cmdlets, but they too require STA mode. if ($Raw) { $rawText = Get-Clipboard -Format Text -Raw } else { $lines = Get-Clipboard -Format Text } } else { # WinPSv4- or WinPSv5+ explicitly started with the -MTA switch Add-Type -AssemblyName System.Windows.Forms if ($isSTA) { # -- STA mode: Write-Verbose "STA mode: Using [Windows.Forms.Clipboard] directly." # To be safe, we explicitly specify that Unicode (UTF-16) be used - older platforms may default to ANSI. $rawText = [System.Windows.Forms.Clipboard]::GetText([System.Windows.Forms.TextDataFormat]::UnicodeText) } else { # $isMTA # -- MTA mode: Since the clipboard must be accessed in STA mode, we use a [System.Windows.Forms.TextBox] instance to mediate. Write-Verbose "MTA mode: Using a [System.Windows.Forms.TextBox] instance for clipboard access." $tb = New-Object System.Windows.Forms.TextBox $tb.Multiline = $True $tb.Paste() $rawText = $tb.Text } } } else { # PowerShell *Core* # No native PS support for writing to the clipboard -> external utilities # must be used. # Since PS automatically splits external-program output into individual # lines and trailing empty lines can get lost in the process, we # must, unfortunately, send the text to a temporary *file* and read # that. $tempFile = [io.path]::GetTempFileName() try { if ($IsWindows) { # Use an ad-hoc JScript to access the clipboard. # Gratefully adapted from http://stackoverflow.com/a/15747067/45375 # Note that trying the following directly from PowerShell Core does NOT work, # (New-Object -ComObject htmlfile).parentWindow.clipboardData.getData('text') # because .parentWindow is always $null in *older* PS versions, e.g. in v2. # Passing true as the last argument to .CreateTextFile() creates a UTF16-LE file (with BOM). $tempScript = [io.path]::GetTempFileName() @" var txt = WSH.CreateObject('htmlfile').parentWindow.clipboardData.getData('text'); var f = WSH.CreateObject('Scripting.FileSystemObject').CreateTextFile('$($tempFile -replace "\\", "\\")', true, true); f.Write(txt); f.Close(); "@ | Set-content -Encoding ASCII -LiteralPath $tempScript cscript /nologo /e:JScript $tempScript Remove-Item $tempScript } elseif ($IsMacOS) { # Note: For full robustness, using the full path to sh, '/bin/sh' is preferable, but then # we couldn't use mock functions to override the command for testing. sh -c "pbpaste > '$tempFile'" } else { # $IsLinux # Note: Requires xclip, which is not installed by default on most Linux distros # and works with freedesktop.org-compliant, X11 desktops. sh -c "xclip -selection clipboard -out > '$tempFile'" if ($LASTEXITCODE -eq 127) { new-StatementTerminatingError "xclip is not installed; please install it via your platform's package manager; e.g., on Debian-based distros such as Ubuntu: sudo apt install xclip" } } if ($LASTEXITCODE) { new-StatementTerminatingError "Invoking the native clipboard utility failed unexpectedly." } # Read the contents of the temp. file into a string variable. if ($IsWindows) { # temp. file is UTF16-LE $rawText = [IO.File]::ReadAllText($tempFile, [Text.Encoding]::Unicode) } else { # temp. file is UTF8, which is the default encoding $rawText = [IO.File]::ReadAllText($tempFile) } } finally { Remove-Item $tempFile } } # Output the retrieved text if ($Raw) { # as-is (potentially multi-line) $result = $rawText } else { # as an array of lines (as the PsWinV5+ Get-Clipboard cmdlet does) if ($null -eq $lines) { # Note: This returns [string[]] rather than [object[]], but that should be fine. $lines = $rawText -split '\r?\n' } $result = $lines } # If the effective result is the *empty string* [wrapped in a single-element array], we output # $null, because that's what the PsWinV5+ Get-Clipboard cmdlet does. if (-not $result) { # !! To be consistent with Get-Clipboard, we output $null even in the absence of -Raw, # !! even though you could argue that *nothing* should be output (i.e., implicitly, the "null collection", # !! [System.Management.Automation.Internal.AutomationNull]::Value) # !! so that trying to *enumerate* the result sends nothing through the pipeline. # !! (A similar, but opposite inconsistency is that Get-Content with a zero-byte file outputs the "null collection" # !! both with and withour -Raw). $null } else { $result } } function Set-ClipboardText { <# .SYNOPSIS Copies text to the clipboard. .DESCRIPTION Copies a text representation of the input to the system clipboard. Input can be provided via the pipeline or via the -InputObject parameter. If you provide no input, the empty string, or $null, the clipboard is effectively cleared. Non-text input is formatted the same way as it would print to the console, which means that the console/terminal window's [buffer] width determines the output line width, which may result in truncated data (indicated with "..."). To avoid that, you can increase the max. line width with -Width, but see the caveats in the parameter description. LINUX CAVEAT: The xclip utility must be installed; on Debian-based platforms such as Ubuntu, install it with: sudo apt install xclip WINDOWS CAVEAT: In MTA mode, passing an empty string is not supported; a newline will be copied instead, and a warning issued. .PARAMETER Width For non-text input, determines the maximum output-line length. The default is Out-String's default, which is the current console/terminal window's [buffer] width. Be careful with high values and avoid [int]::MaxValue, however, because in the case of (implicit) Format-Table output each output line is padded to that very width, which can require a lot of memory. .PARAMETER PassThru In addition to copying the resulting string representation of the input to the clipboard, also outputs it, as single string. .NOTES This function is a "polyfill" to make up for the lack of built-in clipboard support in Windows Powershell v5.0- and in PowerShell Core as of v6.1, albeit only with respect to text. In Windows PowerShell v5.1+, you can use the built-in Set-Clipboard cmdlet instead (which this function invokes, if available). .EXAMPLE Set-ClipboardText "Text to copy" Copies the specified text to the clipboard. .EXAMPLE Get-ChildItem -File -Name | Set-ClipboardText Copies the names of all files the current directory to the clipboard. .EXAMPLE Get-ChildItem | Set-ClipboardText -Width 500 Copies the text representations of the output from Get-ChildItem to the clipboard, ensuring that output lines are 500 characters wide. #> [CmdletBinding(DefaultParameterSetName='Default')] # !! PSv2 doesn't support PositionalBinding=$False [OutputType([string], ParameterSetName='PassThru')] param( [Parameter(Position=0, ValueFromPipeline = $True)] # Note: The built-in PsWinV5.0+ Set-Clipboard cmdlet does NOT have mandatory input, in which case the clipbard is effectively *cleared*. [AllowNull()] # Note: The built-in PsWinV5.0+ Set-Clipboard cmdlet allows $null too. $InputObject , [int] $Width # max. output-line width for non-string input , [Parameter(ParameterSetName='PassThru')] [switch] $PassThru ) begin { # Initialize an array to collect all input objects in. # !! Incredibly, in PSv2 using either System.Collections.Generic.List[object] or # !! System.Collections.ArrayList ultimately results in different ... | Out-String # !! output, with the group header ('Directory:') for input `GetItem / | Out-String` # !! inexplicably missing - even .ToArray() conversion or an [object[]] cast # !! before piping to Out-String doesn't help. # !! Given that we don't expect large collections to be sent to the clipboard, # !! we make do with inefficiently "growing" an *array* ([object[]]), i.e. # !! cloning the old array for each input object. $inputObjs = @() } process { # Collect the input objects. $inputObjs += $InputObject } end { # * The input as a whole is converted to a a single string with # Out-String, which formats objects the same way you would see on the # console. # * Since Out-String invariably appends a trailing newline, we must remove it. # (The PS Core v6 -NoNewline switch is NOT an option, as it also doesn't # place newlines *between* objects.) $widthParamIfAny = if ($PSBoundParameters.ContainsKey('Width')) { @{ Width = $Width } } else { @{} } $allText = ($inputObjs | Out-String @widthParamIfAny) -replace '\r?\n\z' if (test-WindowsPowerShell) { # *Windows PowerShell* # Determine this thread's COM threading model, because the clipboard-access method # must be chosen accordingly. $isSTA = [threading.thread]::CurrentThread.ApartmentState.ToString() -eq 'STA' if ($PSVersionTable.PSVersion.Major -ge 5 -and $isSTA) { # Ps*Win* v5+ has Get-Clipboard / Set-Clipboard cmdlets, but they too require STA mode. # !! As of PsWinV5.1, `Set-Clipboard ''` reports a spurious error (but still manages to effectively) clear the clipboard. # !! By contrast, using `Set-Clipboard $null` succeeds. Set-Clipboard -Value ($allText, $null)[$allText.Length -eq 0] } else { # WinPSv4- or WinPSv5+ explicitly started with the -MTA switch Add-Type -AssemblyName System.Windows.Forms if ($isSTA) { # -- STA mode: we can use [Windows.Forms.Clipboard] directly. Write-Verbose "STA mode: Using [Windows.Forms.Clipboard] directly." if ($allText.Length -eq 0) { $AllText = "`0" } # Strangely, SetText() breaks with an empty string, claiming $null was passed -> use a null char. # To be safe, we explicitly specify that Unicode (UTF-16) be used - older platforms may default to ANSI. [System.Windows.Forms.Clipboard]::SetText($allText, [System.Windows.Forms.TextDataFormat]::UnicodeText) } else { # $isMTA # -- MTA mode: Since the clipboard must be accessed in STA mode, we use a [System.Windows.Forms.TextBox] instance to mediate. if ($allText.Length -eq 0) { # !! The [System.Windows.Forms.TextBox] approach cannot be used set the clipboard to an empty string, because a text box must # !! must be *non-empty* in order to copy something. Hence we use clip.exe Write-Verbose "MTA mode: Using clip.exe rather than a [System.Windows.Forms.TextBox] instance, because the empty string is to be copied." $null | clip.exe if ($LASTEXITCODE) { new-StatementTerminatingError 'Invoking clip.exe with $null input failed unexpectedly.' } } else { Write-Verbose "MTA mode: Using a [System.Windows.Forms.TextBox] instance for clipboard access." $tb = New-Object System.Windows.Forms.TextBox $tb.Multiline = $True $tb.Text = $allText $tb.SelectAll() $tb.Copy() } } } } else { # PowerShell *Core* # No native PS support for writing to the clipboard -> # external utilities must be used. # To prevent adding a trailing \n, which PS inevitably adds when sending # a string through the pipeline to an external command, use a temp. file, # whose content can be provided via native input redirection (<) $tmpFile = [io.path]::GetTempFileName() if ($IsWindows) { # The clip.exe utility requires *BOM-less* UTF16-LE for full Unicode support. [IO.File]::WriteAllText($tmpFile, $allText, (New-Object System.Text.UnicodeEncoding $False, $False)) } else { # $IsUnix -> use BOM-less UTF8 # PowerShell's UTF8 encoding invariably creates a file WITH BOM # so we use the .NET Framework, whose default is BOM-*less* UTF8. [IO.File]::WriteAllText($tmpFile, $allText) } # Feed the contents of the temporary file via stdin to the # platform-appropriate clipboard utility. try { if ($IsWindows) { Write-Verbose "Windows: using clip.exe" cmd.exe /c clip.exe '<' $tmpFile # !! Invoke `cmd` as `cmd.exe` so as to support Pester-based `Mock`s - at least as of v4.3.1, that's a requirement; see https://github.com/pester/Pester/issues/1043 } elseif ($IsMacOS) { Write-Verbose "macOS: Using pbcopy" sh -c "pbcopy < '$tmpFile'" } else { # $IsLinux Write-Verbose "Linux: using xclip" sh -c "xclip -selection clipboard -in < '$tmpFile' >&-" # !! >&- (i.e., closing stdout) is necessary, because xclip hangs if you try to redirect its - nonexistent output with `-in`, which also happens impliclity via `$null = ...` in the context of Pester tests. if ($LASTEXITCODE -eq 127) { new-StatementTerminatingError "xclip is not installed; please install it via your platform's package manager; e.g., on Debian-based distros such as Ubuntu: sudo apt install xclip" } } if ($LASTEXITCODE) { new-StatementTerminatingError "Invoking the platform-specific clipboard utility failed unexpectedly." } } finally { Remove-Item $tmpFile } } if ($PassThru) { $allText } } } #endregion #region == Private helper functions # Throw a statement-terminating error (instantly exits the calling function and its enclosing statement). function new-StatementTerminatingError([string] $Message, [System.Management.Automation.ErrorCategory] $Category = 'InvalidOperation') { $PSCmdlet.ThrowTerminatingError((New-Object System.Management.Automation.ErrorRecord ` $Message, $null, # a custom error ID (string) $Category, # the PS error category - do NOT use NotSpecified - see below. $null # the target object (what object the error relates to) )) } # Determine if we're runnning in Windows PowerShell. function test-WindowsPowerShell { # !! $IsCoreCLR is not available in Windows PowerShell and, if # !! Set-StrictMode is set, trying to access it would fail. $null, 'Desktop' -contains $PSVersionTable.PSEdition } #endregion |