Bash/Shell Substitution Cheat Sheet (Command vs. Process Substitution vs. Redirection etc.)

The premise of Unix is that everything is a file. Such simplicity. Yet somehow when it comes time to write the script to glue everything together, the number of concepts involved explodes:

  1. Pipes (cmd | …)
  2. Redirection (< some.file)
  3. Command substitution ($(cmd))
  4. Process substitution (<(cmd))
  5. Here docs (<<EOF … EOF)
  6. Here strings (<<<foo)

What gives?

Plus, because the syntax is so terse,1 it took me years to wrap my head around when to use all of them. Even then, I would still back myself into abominations like:

cmd1 < <(cmd2)

(Hint: would a pipe be more straightforward here?2 cmd2 | cmd1)

I don’t remember when it dawned on me that all the different syntaxes basically boil down to making square pegs fit in round holes, and that I could approach any problem as: (starting shape, desired shape) ⇒ syntax.

If you find yourself currently confused by the various syntaxes, hopefully I can jump start a better understanding in your brain by visually mapping out the table.

Bash Substitutions

I need: Value Parameter File Parameter STDIN
I have:
Constant
"foo" 
<(echo "foo") 
<<<"foo"
Multi-line
Constant
"foo
bar
"
<(echo "foo
bar
")
<< EOF
foo
bar
EOF
Variable
"$var" 
<(printf %s "$var") 
<<<"$var"
File
"$(< some.file)" 
"some.file" 
… < "some.file"
Command
"$(cmd arg1)"
<(cmd arg1)
cmd arg1 | … 

By Value Parameter, I mean normal parameters when calling a command. For example, foo in this grep command:

grep -r foo .

By File Parameter, I mean parameters that are specifically expected to contain a path to a file. The second positional parameter passed to grep is a File Parameter. Although a more typical example of when you would need process substitution (<(cmd)) is for functionality like grep’s --file option, which expects a file–patterns.txt in the below example–but you may want to be the output of a command instead for whatever reason:

grep -r -f ../patterns.txt .

By STDIN, I mean the receiving command will read your data–the output of tail -f some.log in the below example–from standard input, instead of taking the data as parameters:

tail -f some.log | grep foo

Bonus: POSIX Substitutions

If your shell script starts with:

#!/bin/sh

Use the following table instead, since it avoids Bash-isms from the above table that won’t work in dash or Busybox.

I need: Value Parameter File Parameter STDIN
I have:
Constant
"foo" 
Use a mktemp file
echo "foo" | …
Multi-line
Constant
"foo
bar
"
Use a mktemp file
<< EOF
foo
bar
EOF
Variable
"$var" 
Use a mktemp file
printf %s "$var" | …
File
"$(cat some.file)"
"some.file" 
… < "some.file"
Command
"$(cmd arg1)"
Use a mktemp file
cmd arg1 | … 

Notes

  1. Not to mention that in the pre-ChatGPT days, it was impossible to Google what most of the syntax does unless you happened to know the syntax’s true name.
  2. There are subtle implications to using pipes, so I am not saying you should never use < <(cmd), but start with the traditional form that everyone understands (pipes) and only use other forms when there is a specific need.