2. Group commands. Subshells. Process substitutions.
-
Group commands and subshells.
Group command:
{ command1; command2; [command3; ...] }
Subshell:
(command1; command2; [command3;...])
Because of the way bash implements group commands, the braces must be separated from the commands by a space and the last command must be terminated with either a semicolon or a newline prior to the closing brace.
Group commands and subshells are both used to manage redirection:
date > foo.txt
ls -l > output.txt
echo "Listing of foo.txt" >> output.txt
cat foo.txt >> output.txt{ ls -l; echo "Listing of foo.txt"; cat foo.txt; } > output.txt
(ls -l; echo "Listing of foo.txt"; cat foo.txt) > output.txt
{ ls -l; echo "Listing of foo.txt"; cat foo.txt; } | less
-
Let's see an example that prints a listing of the files in a directory, along with the names of the file's owner and group owner. At the end of the listing, the script prints a tally of the number of files belonging to each owner and group.
./array-2.sh /usr/bin
vim array-2.sh
array-2.sh
#!/bin/bash
# array-2: Use arrays to tally file owners
declare -A files file_group file_owner groups owners
if [[ ! -d "$1" ]]; then
echo "Usage: ${0##/} dir" >&2
exit 1
fi
for i in "$1"/*; do
owner="$(stat -c %U "$i")"
group="$(stat -c %G "$i")"
files["$i"]="$i"
file_owner["$i"]="$owner"
file_group["$i"]="$group"
((++owners[$owner]))
((++groups[$group]))
done
# List the collected files
{
for i in "${files[@]}"; do
printf "%-40s %-10s %-10s\n" \
"$i" "${file_owner["$i"]}" "${file_group["$i"]}"
done
} | sort
echo
# List owners
echo "File owners:"
{
for i in "${!owners[@]}"; do
printf "%-10s: %5d file(s)\n" "$i" "${owners["$i"]}"
done
} | sort
echo
# List groups
echo "File group owners:"
{
for i in "${!groups[@]}"; do
printf "%-10s: %5d file(s)\n" "$i" "${groups["$i"]}"
done
} | sort -
A group command (
{ . . . }
) executes all of its commands in the current shell.A subshell (
( . . . )
), as the name suggests, executes its commands in a child copy of the current shell. This means the environment is copied and given to a new instance of the shell. When the subshell exits, the copy of the environment is lost, so any changes made to the subshell’s environment (including variable assignment) are lost as well. Therefore, in most cases, unless a script requires a subshell, group commands are preferable to subshells. Group commands are both faster and require less memory.We have seen before that using
read
with pipe does not work as we might expect:echo "foo" | read
echo $REPLY
This is because the shell executes the command after the pipe (
read
in this case) in a subshell. The commandread
assigns a value to the variableREPLAY
in the environment of the subshell, but once the command is done executing, the subshell and its environment are destroyed. So, the variableREPLAY
of the current shell still is unassigned (does not have a value). -
To work around this problem, shell provides a special form of expansion, called process substitution.
For processes that produce standard output it looks like this:
<(list-of-commands)
For processes that intake standard input it looks like this:
>(list-of-commands)
To solve our problem with read, we can employ process substitution like this:
read < <(echo "foo")
echo $REPLY
What is happening is that process substitution allows us to treat the output of a subshell as an ordinary file for purposes of redirection.
echo <(echo "foo")
By using
echo
we see that the output of the subshell is being provided by a file named/dev/fd/63
. -
Let's see an example of a
read
loop that processes the contents of a directory listing created by a subshell:vim pro-sub.sh
pro-sub.sh
#!/bin/bash
# pro-sub: demo of process substitution
while read attr links owner group size d1 d2 d3 filename; do
cat <<- EOF
Filename: $filename
Size: $size
Owner: $owner
Group: $group
Modified: $d1 $d2 $d3
Links: $links
Attributes: $attr
EOF
done < <(ls -lh $1 | tail -n +2)Because we are using
read
, we cannot use a pipe to send data to it../pro-sub.sh
./pro-sub.sh /usr/bin | less