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

2

12

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?).
Save answered 30/1, 2019 at 9:30 Comment(1)
Cool use of parameter expansion. Personally I would create two functions. One for normal logging (log) and one for logging piped input (log_pipe or whatever).Howzell
E
7
  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.

Endaendall answered 30/1, 2019 at 9:49 Comment(2)
Sorry my mistake, I have a bad habit of referring to them both as the same thing... I'll edit.Save
ok, sorry for my first answer where i confused /dev/stdin and /dev/ttyEndaendall
D
2

Answering your questions

  • You have confused the syntax between using process substitution which takes the syntax of <(cmd) and simple shell re-direction < file. In plain form, < basically is to read from the standard input and > to write to standard output. The syntax < file is a short-hand syntax to put the contents of the file made available on the standard input which can be read by the commands.
  • So when you run cat < file, it basically puts the content of the file in standard input file descriptor which is later than read by the cat process. The advantage of using $(<file) would be the shell doesn't have fork an external process cat and just use its own mechanism to read the file content.
  • The $(..) is a syntax for command-substitution where command is run in a sub-shell environment and $(..) is replaced with the standard output of the command. For a special case of "$(<file)" i.e. without any commands and only re-directions involved, the shell instead of reading from the standard input starts reading from the start of the file and puts the result on the standard output.
Diu answered 30/1, 2019 at 10:27 Comment(2)
Actually stdin could well be a pipe, not a terminal, right?Humbug
@Diu thanks for the answer. I mixed up the terminology of command vs process substitution, but I am clear on the difference between < file/ > file and <(file) and $(). I just hadn't come across $(<file) before.Save

© 2022 - 2024 — McMap. All rights reserved.