“Programs must be written for people to read, and only incidentally for machines to execute.”
— Harold “Hal” Abelson
This highfalutin aphorism applies from the most cerebral application programming language of your choice, right down to the humble (or not-so-humble) bash script. Love or hate them, bash scripts are the primary method of launching processes on Linux in many settings. (You may well run some kind of fancy-pants orchestration tool, but when that calls into your app it is 50/50 if it ends up calling a bash wrapper script or not.) I think of bash as a tool for writing things which set the execution context and run the main program, and no more than that.
All too often, one sees examples on the web of people saying “launch this program under docker via this bash one-liner”, and then they proceed to demonstrate with a command-line which goes on and on for ages and never seems to stop, very much like this sentence.
Sometimes, people are considerate enough to lay out their example over several lines, all but the last of which has a trailing back slash character “\” to indicate to bash that the command continues on the next line. Here’s a contrived example of what I mean.
#! /bin/bash /usr/bin/java \ -XX:+UnlockExperimentalVMOptions \ -XX:+PrintFlagsFinal \ -version
Unfortunately, there are a few problems with this notation in the real world. (The above example is not the real world.)
- It does not allow for individual lines to be commented out.
- It does not allow for any characters after the \, which means that
- it does not allow for explanatory comments to be added at the end of a line
- invisible (depending on the text editor) space characters can creep in after the backslash and cause weird and annoying error messages
Because of the above, this syntax looks and feels brittle. In the real world (that thing again) one often wishes to comment and uncomment command line options when experimenting with a new configuration. Never fear, bash “array notation” to the rescue…
#! /bin/bash argv=( /usr/bin/java ) # plain old java argv+=( -XX:+UnlockExperimentalVMOptions ) # argv+=( -XX:+PrintFlagsFinal ) argv+=( -version ) "${argv[@]}"
This solves all three of the original problems, but it looks a bit clumsy, what with the repeated mention of the variable name and brackets on every line. All I’m doing is enumerating the arguments to a command for goodness’ sake.
Happily, we can remove the += stuff and most of the brackets, make it a single array, and get straight to the point, viz.
#! /bin/bash argv=( /usr/bin/java -XX:+UnlockExperimentalVMOptions # trailing comment # -XX:+PrintFlagsFinal -version ) "${argv[@]}"
Ta-da! Note that this only works with single commands; if you have multiple commands which need to be connected via pipes they will each need to be declared as individual arrays and called as one pipeline thus:
"${first_command[@]}" | "${second_command[@]}"
If you’ve ever had cause to tangle with the joy that is “setenv.sh” on the installation of an Atlassian application, I’m sure you’ll appreciate the clarity and usability of the above syntax. Yes, I know they have to run under other systems like FreeBSD or Solaris, which most likely don’t have bash. I’m talking about in-house stuff, where the toolset is well-known.
Update: I have recently come across this quote from someone, who, as usual, sums it up brilliantly:
“Any fool can write code that a computer can understand.
— Kent Beck
Good programmers write code that humans can understand.”