Advanced Linux Subshells With Examples

If you read our previous linux subshells for beginners with examples article, or are experienced with subshells already, you know that subshells are a powerful way to manipulate Bash commands inline, and in a context sensitive manner.

In this tutorial you will learn:

  • How to create more advanced subshell commands
  • Where you can employ more advanced subshells in your own code
  • Examples of more advanced subshell commands

Advanced Linux Subshells With Examples

Advanced Linux Subshells With Examples

Software requirements and conventions used

Software Requirements and Linux Command Line Conventions
Category Requirements, Conventions or Software Version Used
System Linux Distribution-independent
Software Bash command line, Linux based system
Other Any utility which is not included in the Bash shell by default can be installed using sudo apt-get install utility-name (or yum instead of apt-get)
Conventions # – requires linux-commands to be executed with root privileges either directly as a root user or by use of sudo command
$ – requires linux-commands to be executed as a regular non-privileged user

Example 1: Counting files

$ if [ $(ls [a-z]* 2>/dev/null | wc -l) -gt 0 ]; then echo "Found one or more occurrences of [a-z]* files!"; fi


Here we have an if statement with as it’s first comparison value a subshell. This works well and provides a great amount of flexibility when it comes to writing if statements. It is different then the binary (true or false) like operation of for example an if grep -q 'search_term' ./docfile.txt statement. Rather, it is evaluated per se as a standard comparison (matched against the greater-then-zero -gt 0 clause).

The subshell tries to directory list files named [a-z]*, i.e. files starting with at least one letter in the a-z range, followed by any subsequent character. It is error-safe by adding 2>/dev/null – i.e. any error displayed (on stderr – the standard error output, signified by the 2) will be redirected > to /dev/null – i.e. the Linux null device – and thus ignored.

Finally we pass the ls input to wc -l which will count for us how many lines (or in this case, files) were seen. If the result was more then 0, the informative note is shown.

Note how the context in which the subshell is operating is varied. Firstly, in this case, the subshell is working inside the current working directory (i.e. $PWD) which is notably also the default i.e. subshells by default start with their own environment PWD set to the current working directory. Secondly, the subshell is working inside the context of an if statement.

No output is generated by this command, as it is being executed inside an empty directory. However, note that the fact that no output is generated also means that our error suppression is working. Let’s verify that:

$ if [ $(ls [a-z]* | wc -l) -gt 0 ]; then echo "Found one or more occurrences of [a-z]* files!"; fi
ls: cannot access '[a-z]*': No such file or directory

We can see how removing the error suppression worked in the earlier example. Let’s next create a file and see how our one-liner performs:

$ touch a
$ if [ $(ls [a-z]* 2>/dev/null | wc -l) -gt 0 ]; then echo "Found one or more occurrences of [a-z]* files!"; fi
Found one or more occurrences of [a-z]* files!


Great, it looks like our one-liner script performs well. Let’s next add a secondary file, and see if we can improve the message

$ touch b
$ if [ $(ls [a-z]* 2>/dev/null | wc -l) -gt 0 ]; then echo "Found one or more occurrences of [a-z]* files!"; fi
Found one or more occurrences of [a-z]* files!
$ if [ $(ls [a-z]* 2>/dev/null | wc -l) -gt 0 ]; then echo "Found exactly $(ls [a-z]* 2>/dev/null | wc -l) occurrences of [a-z]* files!"; fi
Found exactly 2 occurrences of [a-z]* files!

Here we see that the adding a second file (by touch b) does not make any difference (as seen in the first if command), unless we change the output to actually report how many files were found by inserting a secondary subshell in the output.

This is however not optimally coded; in this case, two subshells require execution (the cost of a subshell creation is very minimal, but if you have many subshells being created in high frequency, cost does matter), and the directly listing is requested twice (generating additional I/O and slowing down our code to the speed of the I/O subsystem and type of disk used). Let’s put this in a variable:

$ COUNT="$(ls [a-z]* 2>/dev/null | wc -l)"; if [ ${COUNT} -gt 0 ]; then echo "Found exactly ${COUNT} occurrences of [a-z]* files!"; fi
Found exactly 2 occurrences of [a-z]* files!

Great. This is more optimal code; a single subshell is used and the outcome is stored in a variable which is then used twice, and only a single disk directory listing retrieval is necessary. Note also that this solution may be more thread-safe.

For example, in the if statement which had two subshells, if in the time in-between execution of those subshells a third file was created, the outcome may look like this: Found exactly 3 occurrences of [a-z]* files! whereas the first if statement (using the first subshell) really qualified on if 2 -gt 0 – i.e. 2. It would make little difference in this case, but you can see how in some coding this may become very important to watch out for.

Example 2: Subshells for calculation

$ touch z
$ echo $[ $(date +%s) - $(stat -c %Z ./z) ]
1
$ echo $[ $(date +%s) - $(stat -c %Z ./z) ]
5

Here we created a file, namely z, and subsequently found out the age of the file in seconds by using the second command. A few seconds later, we executed the command again and we can see the file is now 5 seconds old.

The date +%s command gives us the current time in seconds since epoch (1970-01-01 UTC), and stat -c %Z gives us the seconds since epoch for the file which was previously created, and now referenced here as ./z, so all we subsequently need to do is to subtract these two from each other. We place the date +%s first as this is the highest number (the current time) and thus correctly calculate the offset in seconds.

The -c option to stat simply indicates we want a particular output formatting, in this case %Z, or in other words the time since epoch. For date the syntax for the same idea is +%s, though in connection with current time and not related to a particular file.

Example 3: Subshells inside sed and other tools

$ echo '0' > a
$ sed -i "s|0|$(whoami)|" ./a
$ cat a
roel


As you can see, we can use a subshell in almost any command we execute on the command line.

In this case, we create a file a with as contents 0 and subsequently inline replace the 0 to $(whoami) which, when the subshell is executed as the command is being parsed, will substitute to the username roel. Be careful not to use single quotes as this will render the subshell inactive because the string will be interpreted as literal text:

$ echo '0' > a
$ sed -i 's|0|$(whoami)|' ./a
$ cat a
$(whoami)

Note here that the sed enabled syntax (s|0|...|) still works correctly (!), whereas the Bash subshell functionality $() did not!

Example 4: Using eval and a for loop

$ LOOPS=3
$ echo {1..${LOOPS}}
{1..3}
$ eval echo {1..${LOOPS}}
1 2 3
$ for i in $(echo {1..${LOOPS}}); do echo "${i}"; done
{1..3}
$ for i in $(eval echo {1..${LOOPS}}); do echo "${i}"; done
1
2
3

This example, whilst not the optimal way to perform a straightforward for loop, shows us a few ways of integrating subshells even inside loops. We use the eval statement to process the {1..3} text into 1 2 3 which can then be used directly inside the for loop repeat clause.

At times, using subshells and supplying information in-line in-context through subshells is not always self-evident, and may require some testing, tweaking and fine-tuning before the subshells execute as expected. This is normal and much in line with normal Bash coding.

Conclusion

In this article, we explored some more in-depth and advanced examples of using subshells in Bash. The power of subshells will allow you to transform most one-liner scripts to much more powerful versions thereof, not to mention the possibility of using them inside your scripts. When you start exploring subshells and find some nice ways to use them, please post them below in the comments!

Enjoy!



Comments and Discussions
Linux Forum