Fact-checked by Grok 2 weeks ago

Process substitution

Process substitution is a feature in shells such as and ksh that allows the input or output of a process to be treated as a , enabling commands to read from or write to the process as if it were a . It employs two primary forms: <(list), where the output of the command list is made available as input to another command via a temporary file descriptor, and >(list), where output directed to the temporary file serves as input to the command list. This mechanism facilitates advanced without requiring temporary files on disk, making it particularly useful for scenarios involving multiple inputs or outputs in shell scripting. The process list within the substitution executes asynchronously, with the shell creating a (FIFO) or using the /dev/fd method to generate a special —typically of the form /dev/fd/N—that is passed as an argument to the invoking command. Support for process substitution requires an underlying system that accommodates s or naming via /dev/fd, which is standard on most modern operating systems. Expansion of process substitutions occurs simultaneously with parameter expansion, variable expansion, command substitution, and arithmetic expansion, ensuring seamless integration into shell commands. Originally introduced in the Korn shell (ksh) and adopted in , process substitution extends the capabilities of traditional by allowing non-linear data flows, such as comparing outputs from multiple commands or feeding process results into tools that require file arguments. While not part of the standard, it is also supported in Zsh and provides a powerful tool for efficient scripting, though care must be taken with spacing in the syntax to avoid misinterpretation as standard redirection.

Fundamentals

Definition

Process substitution is a construct in Unix-like shells, such as ksh, , and zsh, that expands to a filename representing the input or output stream of a process. This feature allows the output of a command to be treated as a file for purposes of input redirection or other operations that require a filename argument. The primary purpose of process substitution is to enable seamless interaction between commands where one expects a but receives dynamic output instead, avoiding the overhead of writing to and reading from actual temporary files on disk. It facilitates scenarios like comparing outputs from multiple commands or feeding results into tools designed for file input, all while maintaining the efficiency of in-memory handling. The list executes asynchronously in a subshell. At its core, process substitution relies on underlying mechanisms such as named pipes (FIFOs) or the /dev/fd method for naming open file descriptors to simulate file-like access to process streams. These anonymous pipes or descriptors provide a virtual that points to the process's I/O without creating persistent files, ensuring that the streams behave as readable or writable entities in commands. In contrast to standard redirection operators like < for input or > for output, which connect streams directly and synchronously between processes, process substitution generates a for asynchronous use, allowing multiple processes to reference the same dynamic "file" for complex interactions. The syntax generally takes forms like <(command) to capture output as input or >(command) to provide input to a process.

Syntax

Process substitution in Bash and the Korn shell (ksh) employs two primary forms: <(command_list), which provides the standard output of command_list as a readable file, and >(command_list), which accepts input directed to it as standard input for command_list. These constructs must be written without spaces between the redirection operator and the opening parenthesis, and the command_list undergoes parameter, variable, command, and arithmetic expansion prior to execution. In Zsh, the syntax mirrors that of and ksh with support for <(command_list) and >(command_list), but includes an extension =(command_list), which expands to the pathname of a capturing the output of command_list. Zsh's implementation allows for more flexible handling of complex command_lists through standard quoting mechanisms, ensuring arguments with spaces or special characters are preserved correctly. The expands process substitutions to paths, typically /dev/fd/N on systems supporting naming or /proc/self/fd/N on , before the enclosing command executes; on systems without such support, named pipes (FIFOs) may be used instead. Process substitutions are valid only in contexts expecting filenames, such as input/output redirections (e.g., command < <(list)) or command arguments that treat inputs as files; they cannot stand alone as commands or be directly assigned to variables without additional quoting or context. To manage commands containing spaces, special characters, or subprocesses within process substitutions, enclose the command_list in quotes, such as <("ls -l"); nesting is permitted, enabling constructs like <(cmd >(subcmd)) where inner substitutions expand similarly to outer ones. In Zsh, quoting rules for nested or complex substitutions emphasize preserving word boundaries to avoid unintended expansion.

Usage

Basic Examples

Process substitution allows commands to treat the output of other commands as temporary files, enabling operations that would otherwise require intermediate storage. One common use is comparing the sorted contents of two files without creating temporary sorted copies on disk. For instance, the command diff <(sort file1) <(sort file2) executes sort on each file, providing the results to diff as if they were files named /dev/fd/63 and /dev/fd/64 (or equivalent FIFO paths on systems without /dev/fd). Another basic application involves archiving and compressing a directory in a single step, avoiding an uncompressed intermediate tar file. The command tar cf >(gzip > archive.tar.gz) directory creates a tar archive of directory and redirects its output directly to gzip for compression, writing the result to archive.tar.gz. This leverages the >(command) form, where the tar output is piped to the gzip process via a simulated file path. In both examples, the process unfolds as follows: first, the shell expands the process substitution during command-line parsing, launching the inner command (e.g., sort or gzip) in a subshell and associating it with a file descriptor or named pipe; second, the outer command (e.g., diff or tar) receives this as a file-like argument and reads from or writes to it; finally, once the outer command completes, the inner process terminates, cleaning up the temporary descriptor. This flow simulates file paths while keeping operations in memory or via pipes, enhancing efficiency for simple tasks. Common pitfalls in these setups include commands that do not reliably output to standard output, leading to empty or incomplete substitutions—for example, if sort encounters an error or a command like tar receives invalid input paths, the resulting "file" may appear empty to the outer process. Additionally, spaces between the redirection operator and parentheses (e.g., < (sort file1)) will cause syntax errors, as the forms <( ) and >( ) must be contiguous.

Advanced Applications

Process substitution enables sophisticated multi-tool integrations in shell pipelines, particularly when branching output streams to multiple destinations without intermediate files. For instance, the tee command can duplicate a stream to both a log file and a processing command using process substitution, as in ps aux | tee >(grep "nginx" > nginx_processes.log) | sort -k3 -nr | head -10, which logs nginx processes to a file while sorting and displaying the top CPU consumers from all processes. This approach leverages named pipes for efficient, in-memory data flow, avoiding the need for temporary disk storage in compatible systems. In scripting scenarios, process substitution facilitates dynamic input generation for control structures like loops, allowing commands to produce file-like inputs on-the-fly. A common pattern processes search results iteratively without subshell pitfalls that could lose variable scope, such as while IFS= read -r file; do echo "Processing $file"; done < <(find . -name "*.txt"), which safely iterates over text files found in the current directory while preserving outer script variables. This technique is particularly useful in functions or scripts handling variable numbers of inputs, enabling modular code that treats command outputs as pseudo-files for operations like sorting or filtering. For error handling in complex chains, process substitution can be combined with shell traps and redirects to create robust pipelines that capture failures across asynchronous processes. By enabling the errtrace option (set -E) and setting a trap on ERR, scripts can propagate error signals to substituted processes, as in a pipeline where trap 'echo "Error in pipeline" >&2; [exit](/page/Exit) 1' ERR ensures cleanup or even if a background substitution like >(logger -t script) fails due to I/O issues. Redirects such as 2>/dev/null within substitutions further suppress noise while directing errors to handlers, enhancing reliability in long-running scripts. Performance in large-scale applications hinges on the underlying , balancing usage against potential disk fallback. On systems supporting /dev/fd or named pipes, substitutions operate primarily in via asynchronous execution, minimizing for high-throughput pipelines— for example, sorting sort -m <(cmd1) <(cmd2) processes inputs concurrently without blocking. However, in environments lacking FIFO support, temporary files on disk may be created, increasing I/O overhead for voluminous data; thus, monitoring with tools like strace reveals trade-offs, favoring process substitution over explicit temp files for -efficient scaling in resource-constrained setups.

Implementation

Internal Mechanism

Process substitution in Unix-like shells, such as , relies on underlying system features to create temporary conduits for inter-process communication during command expansion. The shell generates either an anonymous pipe via the pipe() system call or a named pipe (FIFO) using mkfifo, depending on system capabilities. On systems supporting the /dev/fd mechanism—typically available on through /proc/self/fd and on many Unix variants—the preferred method involves unnamed pipes, where the shell opens file descriptors that can be referenced as paths like /dev/fd/N. The process flow begins with the shell parsing the command line and encountering the process substitution syntax, such as <(command) or >(command). For <(command), which treats the command's output as a file, the shell creates a pipe, forks a child process to execute the command, and in the child, redirects stdout to the pipe's write end using dup2(). The read end remains open in the parent shell, and its file descriptor N is substituted into the command line as /dev/fd/N, allowing the main command to open and read from it as if it were a regular file. Similarly, for >(command), which treats input to the command as a file, the shell redirects the child's stdin from the pipe's read end, leaving the write end open in the parent as /dev/fd/N for the main command to write to. This forking and redirection setup ensures asynchronous execution of the substituted process while connecting the streams seamlessly between parent and child. If /dev/fd is unavailable, the shell falls back to creating a named with mkfifo, often in a temporary like /tmp, and immediately unlinks the filesystem entry after opening it, rendering the pipe effectively anonymous while keeping the active. This approach ensures compatibility across environments but may introduce slight overhead due to the filesystem interaction. The mechanism is tightly integrated with the shell's redirection handling, performed concurrently with other expansions like and command . Garbage collection occurs automatically upon completion of the relevant processes: the closes the temporary s or when the main command and substituted subprocesses , preventing resource leaks without explicit user intervention. This cleanup is managed by the operating system's table, where closing the last reference to the or descriptor releases the resources. In environments lacking both /dev/fd and support, process substitution is simply unavailable, limiting its use to POSIX-compliant or extended Unix systems.

Shell-Specific Variations

Bash provides full support for process substitution since version 2.0, released in 1996, where it expands constructs like <(command) or >(command) into file paths under /dev/fd/N on systems supporting the /dev/fd method for naming open files. This implementation relies on named pipes (FIFOs) or file descriptors, allowing the output of a process to be treated as a file for input to another command. However, process substitution is disabled in mode, which is often enabled in non-interactive scripts for standards compliance, requiring explicit activation with shopt -u posix or equivalent to restore functionality. The Korn shell (ksh), where process substitution originated in ksh86 around 1986, uses similar syntax to but leverages earlier mechanisms like /proc/self/fd paths on systems with the /proc filesystem for substitution, predating widespread /dev/fd adoption. Both ksh and public-domain variants like pdksh (and its successor mksh) support the feature on systems with /dev/fd or equivalent, enabling asynchronous output to appear as filenames, though compatibility varies across ksh implementations due to historical differences in handling. Zsh enhances substitution with improved integration for and , allowing constructs like =(command) to generate temporary files that can be directly assigned to array elements or used within function scopes without scope limitations seen in some other shells. It also provides superior error reporting for substitution failures, such as invalid commands within <( ) or >( ), by propagating detailed diagnostics from the subshell , and supports both FIFO-based and temporary file-based substitutions for broader . This makes zsh particularly suitable for complex scripting involving array manipulations and error handling in process substitutions. Process substitution is absent from the sh standard, limiting portability across minimal shells like , where attempts to use <( ) or >( ) result in syntax errors. A common involves named created with mkfifo, allowing manual simulation of the feature by forking processes to read from or write to the pipe, though this requires explicit cleanup and handling of asynchronous execution to avoid deadlocks.

Historical Development

Origins

Process substitution was invented by David Korn in the late 1980s during his work on the (ksh) at Bell Laboratories, as part of enhancements to improve shell scripting and process handling capabilities. The primary motivation for its development was to overcome limitations in standard redirections, which supported only linear chains of commands, by enabling more flexible, non-linear process graphs through the treatment of command input and output as file descriptors. This feature made its first public appearance in the ksh86 release, distributed around 1986, well before its incorporation into other widely used shells such as . Influenced by the pipeline mechanisms in the (csh), process substitution extended those concepts by providing file-like access to process I/O, allowing commands to interact with dynamic streams as if they were static files.

Evolution and Adoption

Process substitution, building upon its origins in the Korn shell, was integrated into starting with version 1.14 in 1994 by maintainer Chet Ramey, supporting GNU's emphasis on advanced scripting capabilities while maintaining compatibility with existing shell standards. The Zsh incorporated process substitution in its early versions during the 1990s, drawing from ksh compatibility goals, with key refinements in version 3.0 released in August 1996 that enhanced ksh compatibility and overall shell extensibility. Although influential in shell development discussions, process substitution has not been adopted into the standard, remaining a non-portable extension across major shells. Its adoption has extended to practical applications in scripting ecosystems, notably in Git-related workflows where it facilitates direct comparisons of command outputs, such as diffing file contents from different commits without intermediate files. Recent enhancements in versions 5.0 and later have focused on reliability and integration; for instance, 5.0 (2019) added support for the wait builtin to monitor process substitutions and improved history expansion recognition of the syntax. Further updates in 5.3 (2025) enhanced the wait -n builtin to return terminated process substitutions, improving process management.

Evaluation

Advantages

Process substitution offers significant efficiency gains by eliminating the need for temporary files, thereby reducing overhead and disk usage associated with writing and reading intermediate data to storage. Instead of creating physical files, it leverages named pipes or file descriptors in /dev/fd/, allowing commands to process data streams directly and concurrently, which minimizes resource consumption in scripting workflows. For instance, comparing the outputs of two programs can be achieved with cmp <(prog1) <(prog2), executing both processes simultaneously without intermediate storage. This feature enhances flexibility in pipeline constructions, particularly for non-linear data flows where a single command's output must feed multiple consumers or where multiple sources require simultaneous processing. Traditional pipelines are limited to linear sequences, but process substitution enables scenarios like sorting outputs from several directories at once, as in sort -k 9 <(ls -l /bin) <(ls -l /usr/bin) <(ls -l /usr/X11R6/bin), allowing parallel input handling without complex manual setups. In terms of simplicity, process substitution streamlines code for complex redirections that would otherwise require manual pipe or FIFO configurations, resulting in more readable and maintainable scripts. It replaces multi-step operations—such as piping to a compressor and then to a file—with a single, concise expression like tar cf >(bzip2 -c > file.tar.bz2) $directory_name, avoiding the verbosity of traditional methods. Furthermore, its composability allows seamless integration with other constructs, such as loops and conditionals, without introducing unnecessary subshells that could disrupt scoping or state. This integration supports dynamic ing patterns, like reading from a process-substituted input in a loop: while read i; do ... done < <(echo "random input"), preserving global and enhancing overall modularity.

Limitations

Process substitution is not compliant with POSIX standards and is therefore unavailable in POSIX-compliant shells like basic sh, limiting its portability across Unix-like systems. It is also unsupported natively in Windows Command Prompt (CMD) or without emulation tools such as Git Bash, which can introduce additional overhead and inconsistencies. In , enabling POSIX mode explicitly disables the feature to ensure stricter adherence to standards. Each instance of process substitution allocates a , typically via /dev/fd/N or a (FIFO), which can lead to exhaustion of available descriptors in scenarios involving deep nesting or numerous parallel substitutions within a single command or function. System limits on open file descriptors, often configurable via ulimit -n, may be reached more quickly than with simpler redirections, potentially causing failures in resource-intensive scripts. The temporary and opaque nature of substitution paths, such as /dev/fd/63, complicates debugging, as error messages or logs reference these ephemeral descriptors rather than the underlying processes, making it difficult to trace issues without additional tools like strace. These paths exist only for the duration of the command execution and may appear as "no such file or directory" if inadvertently referenced later, further obscuring problem diagnosis. Compatibility varies across shell versions and implementations; for instance, the scope and lifetime of file descriptors in process substitutions shortened in Bash 5.0 and later, potentially closing prematurely within functions and leading to unexpected "bad file descriptor" errors. Additionally, since Bash 5.1, processes within process substitutions have their standard input redirected to /dev/null, as they are asynchronous and non-interactive, which may affect scripts assuming inherited stdin. Some commands fail to process inputs from process substitution because they expect regular files and skip special files like /dev/fd/N or FIFOs, leading to errors even with binary data; this is due to the command's file validation logic rather than issues with binary handling. For large data streams, while substitution supports streaming without full buffering, certain commands may impose limits due to pipe buffering or memory constraints, reducing efficiency compared to direct file operations.

References

  1. [1]
    Process Substitution (Bash Reference Manual) - GNU.org
    Process Substitution (Bash Reference Manual) ... Process substitution is supported on systems that support named pipes (FIFOs) or the /dev/fd method of ...
  2. [2]
    bash(1) - Linux manual page - man7.org
    The element of BASH_REMATCH with index n is the portion of the string matching the nth parenthesized subexpression. Bash sets BASH_REMATCH in the global scope; ...
  3. [3]
    Korn Shell: Unix and Linux Programming Manual, Third Edition, The
    Process Substitution This feature is only available on versions of the Unix operating system that support the /dev/fd directory for naming open files.
  4. [4]
    zsh: 7 Redirection
    ### Process Substitution in zsh (Section 7 Redirection)
  5. [5]
    An Introduction to the Z Shell - Command/Process Substitution
    Command substitution in zsh can take two forms. In the traditional form, a command enclosed in backquotes ( `...` ) is replaced on the command line with its ...
  6. [6]
    ProcessSubstitution - Greg's Wiki
    Apr 19, 2025 · Process substitution is a very useful Bash extension (copied from Ksh). Process substitution comes in two forms: <(some command) and >(some command).
  7. [7]
    Chapter 23. Process Substitution
    Process substitution can compare the output of two different commands, or even the output of different options to the same command. bash$ comm <(ls -l) <(ls -al) ...
  8. [8]
    Bash Reference Manual
    Summary of each segment:
  9. [9]
    shell - When was process substitution first introduced?
    Oct 4, 2012 · ... introduced: http://wiki.bash-hackers.org/scripting/bashchanges ... version-of-bash-added-the-Process-Substitution-feature. The date on ...bash - Understanding the "process substitution" conceptWhy does BASH process substitution not work with some commands?More results from unix.stackexchange.com
  10. [10]
    Subshell and process substitution - Unix & Linux Stack Exchange
    May 9, 2017 · To my knowledge, no other Bourne-like shell supports it ( rc , es , fish , non-Bourne-like shells support it but with a different syntax).bash - Understanding the "process substitution" conceptProcess substitution and pipe - Unix & Linux Stack ExchangeMore results from unix.stackexchange.com
  11. [11]
    zsh process substitution on array elements - Stack Overflow
    Feb 14, 2018 · I'm trying to write a zsh script that takes one or more URLs as arguments and runs a command (in this case, the ImageMagick montage command) for each of them.zsh array assignment and no match errorsSyntax error in shell script with process substitutionMore results from stackoverflow.comMissing: features | Show results with:features
  12. [12]
    zshexpn - zsh expansion and substitution - Ubuntu Manpage
    Process Substitution Parameter Expansion Command Substitution Arithmetic ... It is an error if this nested substitution produces an array with more than one word.
  13. [13]
    Re: parse error in process substitution - Zsh
    Nov 13, 2008 · From: Peter Stephenson. Re: parse error in process substitution. From: Peter Stephenson. Messages sorted by: Reverse Date, Date, Thread, Author.
  14. [14]
    What is the portable (POSIX) way to achieve process substitution?
    Sep 13, 2016 · Some shells, like bash, support Process Substitution which is a way to present process output as a file, like this: $ diff <(sort file1) <(sort file2)How do you implement process substitution (using a bash script) in ...How to create dynamic process substitution based on the input ...More results from unix.stackexchange.comMissing: workaround | Show results with:workaround
  15. [15]
    POSIX shell equivalent to <() - bash - Stack Overflow
    Aug 5, 2016 · POSIX shell equivalent to <() · 5. POSIX does not specify process substitution, but you may used named pipes to accomplish the same thing.sh equivalent of this bash command that reads from a named pipeHow do I ensure termination of process substitution used with exec?More results from stackoverflow.comMissing: portability workaround
  16. [16]
    Work-around for process substitution in mksh
    Mar 7, 2015 · Process substitution is missing in mksh. Workarounds include using named pipes, a shell function, or a custom function with temporary files.Missing: portability POSIX
  17. [17]
    KSH-93 - Frequently Asked Questions - KornShell
    David Korn is currently at AT&T Laboratories. The first version of ksh was in 1983. It was the first shell to have command line editing with both emacs and vi ...
  18. [18]
    Bash Process Substitution | Linux Journal
    May 22, 2008 · The effect of process substitution is to make each list act like a file. This is done by giving the list a name in the file system and then ...
  19. [19]
  20. [20]
    2. Shell Command Language
    The shell is a command language interpreter. This chapter describes the syntax of that command language as it is used by the sh utility and the system() and ...<|control11|><|separator|>
  21. [21]
    bash - shell scripting process substitution <() inside command ...
    Sep 21, 2022 · This is bash specific syntax. /bin/sh cannot use pip/process substitution. Understand that <(command) syntax is a special substitution. It runs ...In bash, is it generally better to use process substitution or pipelinesadding process substitutions to a command line with a loop in bashMore results from stackoverflow.com
  22. [22]
    Bash-5.0 release available - GNU mailing lists
    Jan 7, 2019 · This release fixes several outstanding bugs in bash-4.4 and introduces several new features. The most significant bug fixes are an overhaul of how nameref ...Bash-5.3-release availableBash-5.1 release availableMore results from lists.gnu.org
  23. [23]
    Bash-5.3-release available - GNU mailing lists
    Jul 5, 2025 · New form of command substitution: ${ command; } or ${|command;} to capture the output of COMMAND without forking a child process and using pipes ...
  24. [24]
    Process Substitution - Learning the bash Shell, Second Edition [Book]
    A unique but rarely used feature of bash is process substitution. Let's say that you had two versions of a program that produced large quantities of output.
  25. [25]
    Process substitution - The Bash Hackers Wiki
    Process substitution is a form of redirection where the input or output of a process (some sequence of commands) appear as a temporary file. <( <LIST> ) >( < ...
  26. [26]
    Process Substitution Without Shell? - Jeff Kaufman
    Nov 28, 2023 · This approach replicates the behavior of Bash's process substitution in Python, allowing cmd to read from the streams as if they were files.
  27. [27]
    Re: Weird process substitution behavior - GNU mailing lists
    Nov 16, 2013 · I will have to see whether or not that causes file descriptor exhaustion under the right scenario. > > $ cat bug1.bash > #!/bin/bash ...
  28. [28]
    Bash running out of File Descriptors - Stack Overflow
    Jun 12, 2014 · I noticed with command: ls -l /proc/3657(pid here)/fd that the file descriptors were constantly increasing. Using Debian 7, GNU bash ...bash - How to use a different file-descriptor in a shell pipeline?Bash double process substitution gives bad file descriptorMore results from stackoverflow.comMissing: exhaustion | Show results with:exhaustion
  29. [29]
    Bash double process substitution gives bad file descriptor
    Oct 1, 2012 · When I try to refer to two process substitution pipes in a bash function, only the first one referenced works. The second one gives a "bad file descriptor" ...Process substitution working in zsh but not is bash - Stack OverflowProcess substitution - tr says 'extra operand /dev/fd/63'More results from stackoverflow.com
  30. [30]
    "/dev/fd/63 No such file or directory" - Ask Ubuntu
    Oct 24, 2018 · That's what /dev/fd/63 is. The idea is to allow external command ( here it's bash ) to treat another commands output as if it was a file.Missing: debugging | Show results with:debugging
  31. [31]
    Why process substitution with binary doesn't work? - Stack Overflow
    Aug 15, 2019 · However, as it is, there's no way to make it work with Bash's process substitution directly.process substitution not working in bash script - Stack OverflowPython: how to write binary data to the stdout so that bash script can ...More results from stackoverflow.comMissing: issues | Show results with:issues
  32. [32]
    Bash Process Substitution: A Smarter Way to Compare Test Outputs
    Oct 20, 2024 · Reduced Disk I/O: No need to write logs to temporary files - everything happens in memory, improving performance and reducing disk clutter.