Don't Walk with Rocks in Your Shoes
Published 2025 March 29
I recently finished Efficient Linux at the Command Line1 by Daniel Barrett2. On my journey to master the command line, this book has advanced my use of Linux tools. As with learning any new skill, it takes practical experience and theoretical knowledge. Reading a book on a subject won't fully transfer into real-world practice. Luckily, this book doesn't waste time inviting you to get your hands dirty!
Unraveling the Shell #
After reading this book, I have a better intuition for the shell and its environment. Barrett makes a concerted effort throughout the book to continuously clarify what the shell is and what it is not. Here is what he has to say:
Linux does a great job of hiding the fact that a shell is an ordinary program. When you log in, Linux automatically runs an instance of the shell for you, known as your login shell. It launches so seamlessly that it appears to be Linux, when really it’s just a program launched on your behalf to interact with Linux.
A running shell holds a bunch of important information in variables: the search path, the current directory, your preferred text editor, your customized shell prompt, and more. The variables of a running shell are collectively called the shell’s environment. When the shell exits, its environment is destroyed.
A new programmer's introduction to the shell is typically a package installer like npm
or apt
. Or running a python script (Wait! Is it python3
, python
, or py
?). Rarely do we get to drill down into the mechanics of how our commands are executed. The shell in particular employs subtlety that can be a source of frustration. Unless you pick up a book and read about how the shell evaluates your input, it will be a continual surprise. Thankfully, even though things can go sideways relatively quickly in the shell, there is usually a good explanation for it. Barrett gives us some more insight:
When you print the value of a variable with echo:
$ echo $HOME /home/smith
you might think that the echo command examines the
HOME
variable and prints its value. That is not the case. echo knows nothing about variables. It just prints whatever arguments you hand it. What’s really happening is that the shell evaluates$HOME
before running echo. From echo’s perspective, you typed:$ echo /home/smith
This behavior is extremely important to understand, especially as we delve into more complicated commands. The shell evaluates the variables in a command—as well as patterns and other shell constructs—before executing the command.
These operations are commonly referred to as expansions. The shell understands a special syntax that allows it to "expand" or in-place evaluate your input. The example above is known as parameter expansion3. Understanding these will strengthen your efficient use of the shell.
Command Composition #
This is the Unix philosophy: Write programs that do one thing and do it well. Write programs to work together. Write programs to handle text streams, because that is a universal interface. 4
The Unix pipe5 |
allows you to pipe the output of one command to the input of another. In chapters 1 and 5, Barrett builds upon this idea of composition to sort, transform, and query text. It's a masterclass in command line wizardry. He states:
Linux systems come with thousands of command-line programs. Experienced users typically rely on a smaller subset—a toolbox of sorts—that they return to again and again.
He breaks the commands down into the following categories:
- Producing Text
date
seq
- Brace Expansion
find
yes
- Isolating Text
grep
tail
awk
- Combining Text
tac
paste
diff
- Transforming Text
tac
rev
awk
sed
It is helpful that he categorizes the commands by their role. It's a toolbox of commands that together can accomplish virtually anything you can think of. A simple example from the book that I use on a regular basis is pretty printing the $PATH
environment variable.
Running
echo $PATH
/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/share:/usr/share:/home/ben/bin
gives a rather unreadable output. This is easily fixable with the tr
command. It translates a selected character into another. We can pretty print $PATH
like so
echo $PATH | tr : '\n'
/usr/bin
/usr/local/bin
/bin
/usr/sbin
/sbin
/usr/local/share
/usr/share
/home/ben/bin
Much better!
11 Ways to Run a Bash Command #
My favorite chapter in the book is a culmination of several ideas that Barrett plants earlier. If you only read one chapter in the entire book, read this one! Working with bash in a single shell is straightforward, but as soon as you add remote shells, long-running jobs, or running multiple commands with elevated permissions things can get tricky. Here is a list of every technique covered in the book:
- Conditional Lists
false || echo 'Prior command returned non-zero exit code'
- Unconditional Lists
sleep 1000; echo 'You did it'
- Command Substitution
echo $(ls file.txt)
- Process Substitution
diff <(sort file.txt) <(sort file2.txt)
- Passing a Command as an Argument to bash
bash -c "echo foo"
- Piping a Command to bash
echo "echo foo" | bash
- Executing a String Remotely with ssh
echo "echo foo" | ssh username@host
- Running a List of Commands with xargs
ls *.txt | xargs wc -l
- Backgrounding a Command
sleep 60 && echo 'timer done' &
- Explicit Subshells
(cd /some-dir && ls)
- Process Replacement
exec ls
Of all of these my favorite is sixth in the list: Piping a Command to Bash. This allows you to construct commands as strings and then pipe them into bash. My favorite way to do this is with awk. Let's say for example that I have a bunch of text files in nested directories and I want to move them all to another directory. This could be accomplished in several ways. One way would be to use
mv */*.txt ~/tmp/
Or I could list the files to see what I am grabbing.
ls */*.txt
Or I could generate the commands as strings to preview them!
ls */*.txt | awk '{printf "mv %s ~/tmp\n", $1}'
Once I am satisfied with the output I can pipe it to bash.
ls */*.txt | awk '{printf "mv %s ~/tmp\n", $1}' | bash
Learning the command line is often a frustrating experience. Discoverability is poor, feedback is often ambiguous, and non-standard features can create pitfalls for novice users. However, avoiding learning the command line as a programmer is like walking with rocks in your shoes. It is an essential skill to master.
In this post I have only covered a small part of Efficient Linux at the Command Line. It is a rich resource of not only tips and tools but will change the you way think about the command line. It's the best resource I have found on getting to the next level as a command line user.
Appendix: Expansions #
Pathname Expansion
echo *.txt
file.txt file2.txt
Tilde Expansion
echo ~/file.txt
/home/ben/file.txt
Arithmetic Expansion
echo $((1 + 1))
2
Brace Expansion
echo {1..10}
1 2 3 4 5 6 7 8 9 10
Parameter Expansion
echo $HOME
/home/ben
Command Substitution
echo $(which date)
/bin/date
You can disable expansions by wrapping them in single quotes:
echo '{1..10}'
{1..10}
References #
-
Daniel Barrett is also the author of several other popular programming books. I actually came across this book in particular after I listened to his interview on Episode #547 of The Changelog Podcast ↩︎
-
The Linux Command Line by William Shotts provides a good list of other expansions employed in bash. See Appendix: Expansions ↩︎
-
Douglas McIlroy on The Unix Philosophy ↩︎