18

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.

IttayD
  • 1,087
  • 4
  • 11
  • 14
  • 3
    afaik there is no such thing, but it would be interesting to be proven wrong. – Zoredache May 05 '15 at 06: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 05 '15 at 09: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 06 '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

2 Answers2

5

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.

mklement
  • 566
  • 5
  • 11
  • 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 08: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
  • 3
    @JimB GNU Diffutils diff only operates on files, in case you're intrested. – Pavel Jun 22 '18 at 17:19
  • @pavel a. You'd use compare-object in powershell, b. Operating on files is very different than taking files in as command input. Generally speaking you just don't need this in powershell since the pipeline handles this. It's very rare to find something that simply can't be done in .Net – Jim B Jun 24 '18 at 03:35
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 #>}
James Ruskin
  • 479
  • 5
  • 14
  • This is a fantastic answer!! – GregL Jun 03 '15 at 01:56
  • 2
    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 01:06
  • 3
    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