14

Bash has <(..) for process substitution. What is Powershell's equivalent?

I know there is $(...), but it returns a string, while <(..) returns a file the outer command can read from, which is what it expects.

I'm also not looking for a pipe based solution, but something I can stick in the middle of the command line.

  • 3
    afaik there is no such thing, but it would be interesting to be proven wrong. – Zoredache May 5 '15 at 6:04
  • 4
    Can you give a mock-up example of how you would expect it to be used? I'm wondering if $(... | select -expandproperty objectyouwanttopass) might suit a single substitution case. – Andy May 5 '15 at 9:03
  • 2
    In PowerShell $() is the subexpression operator, you could use it like this: Write-Output "The BITS service is $(Get-Service bits | select -ExpandProperty Stauts)" to get the status of the BITS service without loading it into a variable first. Looking at Process Substitution, this isn't exactly the same, but it might still solve the problem you're facing – mortenya May 6 '15 at 15:41
  • @Andy: The feature would help with external utilities that require filename operands. An example is psftp.exe for SFTP transfers: its -b option requires you to provide commands to run on the server via a file, which is inconvenient, if you just want to run, say, mget *. If PowerShell had process substitution, you'd be able to do something like psftp.exe -l user -p password somehost -b <( "mget *" ). – mklement Jan 24 '16 at 15:40
4

This answer is NOT for you, if you:
- rarely, if ever, need to use external CLIs (which is generally worth striving for - PowerShell-native commands play much better together and have no need for such a feature).
- aren't familiar with Bash's process substitution.
This answer IS for you, if you:
- frequently use external CLIs (whether out of habit or due to lack of (good) PowerShell-native alternatives), especially while writing scripts.
- are used to and appreciate what Bash's process substitution can do.
- Update: Now that PowerShell is supported on Unix platforms too, this feature is of increasing interest - see this feature request on GitHub, which suggests that PowerShell implement a feature akin to process substitution.

In the Unix world, in Bash/Ksh/Zsh, a process substitution is offers treating command output as if it were a temporary file that cleans up after itself; e.g. cat <(echo 'hello'), where cat sees the output from the echo command as the path of a temporary file containing the command output.

While PowerShell-native commands have no real need for such a feature, it can be handy when dealing with external CLIs.

Emulating the feature in PowerShell is cumbersome, but may be worth it, if you find yourself needing it often.

Picture a function named cf that accepts a script block, executes the block and writes its output to a temp. file created on demand, and returns the temp. file's path; e.g.:

 findstr.exe "Windows" (cf { Get-ChildItem c:\ }) # findstr sees the temp. file's path.

This is a simple example that doesn't illustrate the need for such a feature well. Perhaps a more convincing scenario is the use of psftp.exe for SFTP transfers: its batch (automated) use requires providing an input file containing the desired commands, whereas such commands can easily be created as a string on the fly.

So as to be as widely compatible with external utilities as possible, the temp. file should use UTF-8 encoding without a BOM (byte-order mark) by default, although you can request a UTF-8 BOM with -BOM, if needed.

Unfortunately, the automatic cleanup aspect of process substitutions cannot be directly emulated, so an explicit cleanup call is needed; cleanup is performed by calling cf without arguments:

  • For interactive use, you can automate the cleanup by adding the cleanup call to your prompt function as follows (the prompt function returns the prompt string, but can also be used to perform behind-the-scenes commands every time the prompt is displayed, similar to Bash's $PROMPT_COMMAND variable); for availability in any interactive session, add the following as well as the definition of cf below to your PowerShell profile:

    "function prompt { cf 4>`$null; $((get-item function:prompt).definition) }" |
      Invoke-Expression
    
  • For use in scripts, to ensure that cleanup is performed, the block that uses cf - potentially the whole script - needs to be wrapped in a try / finally block, in which cf without arguments is called for cleanup:

# Example
try {

  # Pass the output from `Get-ChildItem` via a temporary file.
  findstr.exe "Windows" (cf { Get-ChildItem c:\ })

  # cf() will reuse the existing temp. file for additional invocations.
  # Invoking it without parameters will delete the temp. file.

} finally {
  cf  # Clean up the temp. file.
}

Here's the implementation: advanced function ConvertTo-TempFile and its succinct alias, cf:

Note: The use of New-Module, which requires PSv3+, to define the function via a dynamic module ensures that there can be no variable conflicts between the function parameters and variables referenced inside the script block passed.

$null = New-Module {  # Load as dynamic module
  # Define a succinct alias.
  set-alias cf ConvertTo-TempFile
  function ConvertTo-TempFile {
    [CmdletBinding(DefaultParameterSetName='Cleanup')]
    param(
        [Parameter(ParameterSetName='Standard', Mandatory=$true, Position=0)]
        [ScriptBlock] $ScriptBlock
      , [Parameter(ParameterSetName='Standard', Position=1)]
        [string] $LiteralPath
      , [Parameter(ParameterSetName='Standard')]
        [string] $Extension
      , [Parameter(ParameterSetName='Standard')]
        [switch] $BOM
    )

    $prevFilePath = Test-Path variable:__cttfFilePath
    if ($PSCmdlet.ParameterSetName -eq 'Cleanup') {
      if ($prevFilePath) { 
        Write-Verbose "Removing temp. file: $__cttfFilePath"
        Remove-Item -ErrorAction SilentlyContinue $__cttfFilePath
        Remove-Variable -Scope Script  __cttfFilePath
      } else {
        Write-Verbose "Nothing to clean up."
      }
    } else { # script block specified
      if ($Extension -and $Extension -notlike '.*') { $Extension = ".$Extension" }
      if ($LiteralPath) {
        # Since we'll be using a .NET framework classes directly, 
        # we must sync .NET's notion of the current dir. with PowerShell's.
        [Environment]::CurrentDirectory = $pwd
        if ([System.IO.Directory]::Exists($LiteralPath)) { 
          $script:__cttfFilePath = [IO.Path]::Combine($LiteralPath, [IO.Path]::GetRandomFileName() + $Extension)
          Write-Verbose "Creating file with random name in specified folder: '$__cttfFilePath'."
        } else { # presumptive path to a *file* specified
          if (-not [System.IO.Directory]::Exists((Split-Path $LiteralPath))) {
            Throw "Output folder '$(Split-Path $LiteralPath)' must exist."
          }
          $script:__cttfFilePath = $LiteralPath
          Write-Verbose "Using explicitly specified file path: '$__cttfFilePath'."
        }
      } else { # Create temp. file in the user's temporary folder.
        if (-not $prevFilePath) { 
          if ($Extension) {
            $script:__cttfFilePath = [IO.Path]::Combine([IO.Path]::GetTempPath(), [IO.Path]::GetRandomFileName() + $Extension)
          } else {
            $script:__cttfFilePath = [IO.Path]::GetTempFilename() 
          }
          Write-Verbose "Creating temp. file: $__cttfFilePath"
        } else {
          Write-Verbose "Reusing temp. file: $__cttfFilePath"      
        }
      }
      if (-not $BOM) { # UTF8 file *without* BOM
        # Note: Out-File, sadly, doesn't support creating UTF8-encoded files 
        #       *without a BOM*, so we must use the .NET framework.
        #       [IO.StreamWriter] by default writes UTF-8 files without a BOM.
        $sw = New-Object IO.StreamWriter $__cttfFilePath
        try {
            . $ScriptBlock | Out-String -Stream | % { $sw.WriteLine($_) }
        } finally { $sw.Close() }
      } else { # UTF8 file *with* BOM
        . $ScriptBlock | Out-File -Encoding utf8 $__cttfFilePath
      }
      return $__cttfFilePath
    }
  }
}

Note the ability to optionally specify an output [file] path and/or filename extension.

| improve this answer | |
  • The idea that you would ever need to do this is dubious at best,and would simply be making things more difficult for the sake of simply not wanting to use PowerShell. – Jim B Jun 15 '16 at 8:04
  • 1
    @JimB: I personally use it with psftp.exe, which is what prompted me to write it. Even though it's preferable to do everything natively in PowerShell, that's not always possible; invoking external CLIs from PowerShell does and will continue to happen; if you find yourself repeatedly dealing with CLIs that require file input that can (more) easily be constructed in memory / by another command, the function in this answer can make your life easier. – mklement Jun 15 '16 at 18:01
  • Are you joking? none of that is required. I have yet to find a command that only accepts files with commands for parameters. As far as SFTP goes a simple search showed me 2 simple addin assemblies to natively perform FTP in PowerShell. – Jim B Jun 15 '16 at 20:50
  • 1
    @JimB: If you want to continue this conversation in a constructive manner, change your tone. – mklement Jun 15 '16 at 21:16
  • 3
    @JimB GNU Diffutils diff only operates on files, in case you're intrested. – Pavel Jun 22 '18 at 17:19
3

When not enclosed in double quotes, $(...) returns a PowerShell Object (or rather, whatever is returned by the code enclosed), evaluating the enclosed code first. This should be suitable for your purposes ("something [I] can stick in the middle of the command line"), assuming that command-line is PowerShell.

You can test this by piping various versions to Get-Member, or even just outputting it directly.

PS> "$(ls C:\Temp\Files)"
new1.txt new2.txt

PS> $(ls C:\Temp\Files)


    Directory: C:\Temp\Files


Mode                LastWriteTime         Length Name                                                                      
----                -------------         ------ ----                                                                      
-a----       02/06/2015     14:58              0 new1.txt                                                                  
-a----       02/06/2015     14:58              0 new2.txt   

PS> "$(ls C:\Temp\Files)" | gm


   TypeName: System.String
<# snip #>

PS> $(ls C:\Temp\Files) | gm


   TypeName: System.IO.FileInfo
<# snip #>

When enclosed in double quotes, as you've noticed, `"$(...)" will just return a string.

In this way, if you wanted to insert, say, the contents of a file directly on a line, you could use something like:

Invoke-Command -ComputerName (Get-Content C:\Temp\Files\new1.txt) -ScriptBlock {<# something #>}
| improve this answer | |
  • This is a fantastic answer!! – GregL Jun 3 '15 at 1:56
  • What you're describing is not the equivalent of Bash's process substitution. Process substitution is designed for use with commands that require filename operands; that is, the output from a command enclosed in a process substitution is, loosely speaking, written to a temporary file, and that file's path is returned; additionally, the file's existence is scoped to the command that the process substitution is a part of. If PowerShell had such a feature, you'd expect something like the following to work: Get-Content <(Get-ChildItem) – mklement Jan 24 '16 at 15:20
  • Please correct me if I'm wrong, and this isn't what you're looking for, but doesn't Get-ChildItem | Get-Content work perfectly well? Or you could otherwise try Get-Content (Get-ChildItem).FullName for the same effect? You may be approaching this from a view thoroughly influenced by another scripting approach. – James Ruskin Jan 26 '16 at 1:06
  • 2
    Yes, in the realm of PowerShell there is no need for this feature; it is only of interest for use with external CLIs that require file input, and where the content of such files is easily constructed with a (PowerShell) command. See my comment on the question for a real-world example. You may never need such a feature, but for people who frequently need to call external CLIs it is of interest. You should at least preface your answer by saying that you're demonstrating the PowerShell way of doing things - as opposed to what the OP specifically asked for - and why you're doing so. – mklement Jan 27 '16 at 20:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.