< Previous | Contents | Next >
Group Commands And Subshells
bash allows commands to be grouped together. This can be done in one of two ways; ei- ther with a group command or with a subshell. Here are examples of the syntax of each:
Group command:
{ command1; command2; [command3; ...] }
Subshell:
(command1; command2; [command3;...])
The two forms differ in that a group command surrounds its commands with braces and a subshell uses parentheses. It is important to note that, due to 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 clos- ing brace.
So what are group commands and subshells good for? While they have an important dif- ference (which we will get to in a moment), they are both used to manage redirection. Let’s consider a script segment that performs redirections on multiple commands:
ls -l > output.txt
echo "Listing of foo.txt" >> output.txt cat foo.txt >> output.txt
ls -l > output.txt
echo "Listing of foo.txt" >> output.txt cat foo.txt >> output.txt
This is pretty straightforward. Three commands with their output redirected to a file named output.txt. Using a group command, we could code this as follows:
{ ls -l; echo "Listing of foo.txt"; cat foo.txt; } > output.txt
{ ls -l; echo "Listing of foo.txt"; cat foo.txt; } > output.txt
Using a subshell is similar:
(ls -l; echo "Listing of foo.txt"; cat foo.txt) > output.txt
(ls -l; echo "Listing of foo.txt"; cat foo.txt) > output.txt
Using this technique we have saved ourselves some typing, but where a group command or subshell really shines is with pipelines. When constructing a pipeline of commands, it is often useful to combine the results of several commands into a single stream. Group commands and subshells make this easy:
{ ls -l; echo "Listing of foo.txt"; cat foo.txt; } | lpr
{ ls -l; echo "Listing of foo.txt"; cat foo.txt; } | lpr
Here we have combined the output of our three commands and piped them into the input of lpr to produce a printed report.
In the script that follows, we will use groups commands and look at several programming techniques that can be employed in conjunction with associative arrays. This script, called array-2, when given the name of a directory, prints a listing of the files in the 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. Here we see the results (condensed for brevity) when the script is given the directory
/usr/bin:
[me@linuxbox ~]$ array-2 /usr/bin | ||
/usr/bin/2to3-2.6 | root | root |
/usr/bin/2to3 | root | root |
/usr/bin/a2p | root | root |
/usr/bin/abrowser | root | root |
/usr/bin/aconnect | root | root |
/usr/bin/acpi_fakekey | root | root |
/usr/bin/acpi_listen | root | root |
/usr/bin/add-apt-repository | root | root |
. | ||
. | ||
. | ||
/usr/bin/zipgrep | root | root |
/usr/bin/zipinfo | root | root |
/usr/bin/zipnote | root | root |
/usr/bin/zip | root | root |
/usr/bin/zipsplit /usr/bin/zjsdecode /usr/bin/zsoelim | root root root | root root root | |
File owners: daemon : 1 | file(s) | ||
root : 1394 | file(s) |
File group owners: crontab : 1 file(s) daemon : 1 file(s) lpadmin : 1 file(s) mail : 4 file(s) mlocate : 1 file(s) root : 1380 file(s) shadow : 2 file(s) ssh : 1 file(s)
tty : 2 file(s)
utmp : 2 file(s)
#!/bin/bash
#!/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: array-2 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
# array-2: Use arrays to tally file owners
declare -A files file_group file_owner groups owners if [[ ! -d "$1" ]]; then
echo "Usage: array-2 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
Here is a listing (with line numbers) of the script:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
27
28
29
30
31
32
33
34
35
36
37
38
39
40
echo
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
# 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
Let's take a look at the mechanics of this script:
Line 5: Associative arrays must be created with the declare command using the -A
option. In this script we create five arrays as follows:
files contains the names of the files in the directory, indexed by filename file_group contains the group owner of each file, indexed by filename file_owner contains the owner of each file, indexed by file name groups contains the number of files belonging to the indexed group owners contains the number of files belonging to the indexed owner
Lines 7-10: Checks to see that a valid directory name was passed as a positional parame- ter. If not, a usage message is displayed and the script exits with an exit status of 1.
Lines 12-20: Loop through the files in the directory. Using the stat command, lines 13 and 14 extract the names of the file owner and group owner and assign the values to their respective arrays (lines 16, 17) using the name of the file as the array index. Like- wise the file name itself is assigned to the files array (line 15).
Lines 18-19: The total number of files belonging to the file owner and group owner are incremented by one.
Lines 22-27: The list of files is output. This is done using the "${array[@]}" parameter expansion which expands into the entire list of array elements with each element treated as a separate word. This allows for the possibility that a file name may contain embedded spaces. Also note that the entire loop is enclosed in braces thus forming a group com- mand. This permits the entire output of the loop to be piped into the sort command. This is necessary because the expansion of the array elements is not sorted.
Lines 29-40: These two loops are similar to the file list loop except that they use the "${!
array[@]}" expansion which expands into the list of array indexes rather than the list of array elements.