Search code examples
bashstdincommand-substitutionparameter-expansion

The mechanics of this shell syntax: ${1:-$(</dev/stdin)}


I recently came across this really neat syntax, in the context of creating bash functions which can accept an argument or a stream from STDIN (i.e. can be piped to). On the surface, I understand what's happening here, but I'd like a bit more explanation of the actual mechanics of how this is working.

Here's the syntax (as per the title): ${1:-$(</dev/stdin)}

And in context, one might use it as:

log(){
 echo -e >&1 "INFO: ${1:-$(</dev/stdin)}"
}

Allowing the following usage:

$ log foo
INFO: foo

Alternatively, you could also do this

mv -v foo.ext bar.ext | log
INFO: renamed 'foo.ext' -> 'bar.ext'

This is great because its the single most concise method I've seen for enabling both argument and pipe functionality with bash functions (I forget where I came across it now, unfortunately).

Now, I understand (or think I understand), most of what's happening here at least superficially, but I'd appreciate a deeper understanding. Here's how I interpret it, followed by my remaining questions:

${1:-$(</dev/stdin)}

  • ${1} is obviously the default argument that a function accepts
  • ${1:-x} is a variable/brace expansion 'fall back' to the string 'x' if $1 is otherwise empty (or unset?). In this case falling back to the STDIN process sub.
  • $() is obviously a process command substitution
  • and finally, </dev/stdin is obviously a redirect from standard input which allows the pipe to work at all.

This essentially says if $1 isn't populated by an argument, fall back to using STDIN - which I'm happy with conceptually.

So here are my questions:

  1. I've never seen a redirect (<) inside a process command substitution, without an actual command preceding it (e.g. $(cat < somefile.ext)). So what is actually happening (the nitty-gritty) when the process command substitution receives the redirect with no other command to invoke?
  2. Why is it necessary to wrap the STDIN redirect in a process command substitution at all? (actually, as I write this it occurs to me I haven't tested it without, but I'll keep it simple).
  3. Is this safe? I've used it with multiline STDIN, and it hasn't broken so far. Where might this fall down (if anywhere?).

Solution

    1. $(..): from bash manual is a command substitution not process substitution <(..). and from Command substitution

    The command substitution $(cat file) can be replaced by the equivalent but faster $(< file).

    1. /dev/stdin is a symlink to /proc/self/fd/0, convenient here because of $(<..) syntax which expects a file.

    2. this could cause a problem because the command may be blocked until stdin closed. it is safe in the meaning that multiline input will be preserved because au double-quotes.

    Finally creating a pipe and forking a process (like in mv -v foo.ext bar.ext | log) for each log command may be inefficient.