2019-11-02 16:59:48 -0000, Kenny McCormack:
> I just found out today that if you do:
>
> $ foo=$(< file)
>
> in bash, foo gets assigned the contents of "file". It is equivalent to:
[...]
You'll find lots of details on that at
https://unix.stackexchange.com/questions/189749/understanding-bashs-read-a-file-command-substitution/368663#368663
reproduced below for convenience:
$(<file) (also works with `<file`) is a special operator of
the Korn shell copied by zsh and bash. It does look a lot like
command substitution but it's not really.
In POSIX shells, a simple command is:
< file var1=value1 > file2 cmd 2> file3 args 3> file4
All parts are optional, you can have redirections only, command
only, assignment only or combinations.
If there are redirections but no command, the redirections are
performed (so a > file would open and truncate file), but then
nothing happens. So
< file
Opens file for reading, but then nothing happens as there's no
command. So the file is then closed and that's it. If $(< file)
was a simple command substitution, then it would expand to
nothing.
In the [1]POSIX specification, in $(script), if script consists
only of redirections, that produces unspecified results. That's
to allow that special behaviour of the Korn shell.
In ksh (here tested with ksh93u+), if the script consists of one
and only one simple command (though comments are allowed before
and after) that consists only of redirections (no command, no
assignment) and if the first redirection is a stdin (fd 0) input
only (<, << or <<<) redirection, so:
* $(< file)
* $(0< file)
* $(<&3) (also $(0>&3) actually as that's in effect the same
operator)
* $(< file > foo 2> $(whatever))
but not:
* $(> foo < file)
* nor $(0<> file)
* nor $(< file; sleep 1)
* nor $(< file; < file2)
then
* all but the first redirection are ignored (they are parsed
away)
* and it expands to the content of the file/heredoc/herestring
(or whatever can be read from the file descriptor if using
things like <&3) minus the trailing newline characters.
as if using $(cat < file) except that
* the reading is done internally by the shell and not by cat
* no pipe nor extra process is involved
* as a consequence of the above, since the code inside is not
run in a subshell, any modification remain thereafter (as in
$(<${file=foo.txt}) or $(<file$((++n))))
* read errors (though not errors while opening files or
duplicating file descriptors) are silently ignored.
In zsh, it's the same except that that special behaviour is only
triggered when there's only one file input redirection (<file or
0< file, no <&3, <<<here, < a < b...)
However, except when emulating other shells, in:
< file
<&3
<<< here...
that is when there are only input redirections without commands,
outside of command substitution, zsh runs the $READNULLCMD (a
pager by default), and when there are both input and output
redirections, the $NULLCMD (cat by default), so even if $(<&3)
is not recognized as that special operator, it will still work
like in ksh though by invoking a pager to do it (that pager
acting like cat since its stdout will be a pipe).
However while ksh's $(< a < b) would expand to the content of a,
in zsh, it expands to the content of a and b (or just b if the
multios option is disabled), $(< a > b) would copy a to b and
expand to nothing, etc.
bash has a similar operator but with a few differences:
* comments are allowed before but not after:
echo "$(
# getting the content of file
< file)"
works but:
echo "$(< file
# getting the content of file
)"
expands to nothing.
* like in zsh, only one file stdin redirection, though there's
no fall back to a $READNULLCMD, so $(<&3), $(< a < b) do
perform the redirections but expand to nothing.
* for some reason, while bash does not invoke cat, it still
forks a process that feeds the content of the file through a
pipe making it much less of an optimisation than in other
shells. It's in effect like a $(cat < file) where cat would
be a builtin cat.
* as a consequence of the above, any change made within are
lost afterwards (in the $(<${file=foo.txt}), mentioned above
for instance, that $file assignment is lost afterwards).
In bash, IFS= read -rd '' var < file (also works in zsh) is a
more effective way to read the content of a text file into a
variable. It also has the benefit of preserving the trailing
newline characters. See also $mapfile[file] in zsh (in the
zsh/mapfile module and only for regular files) which also works
with binary files.
Note that the pdksh-based variants of ksh have a few variations
compared to ksh93. Of interest, in mksh (one of those
pdksh-derived shells), in
var=$(<<'EOF'
That's multi-line
test with *all* sorts of "special"
characters
EOF
)
is optimised in that the content of the here document (without
the trailing characters) is expanded without a temporary file or
pipe being used as is otherwise the case for here documents,
which makes it an effective multi-line quoting syntax.
To be portable to all versions of ksh, zsh and bash, best is to
limit to only $(<file) avoiding comments and bearing in mind
that modifications to variables made within may or may not be
preserved.
References
Visible links
1.
http://pubs.opengroup.org/onlinepubs/9699919799.2016edition/utilities/V3_chap02.html#tag_18_06_03