About a year-and-a-half ago I decided to take the plunge and switched from Windows to Linux on all of my computers. When once I viewed the command line as an intimidating totem of geekdom — whose promptings could only be appeased with a prememorized sequence of ritualistic keymashing –I now wonder how I ever got anything done without it. Gone are the days when I’d throw out a “Darmok and Jalad at Tanagra” reference to test the geek-depth of newly-met friends, Do you even shebang, bro? is my new challenge of choice.
Imagine the nerdistential crisis, then, when I ran an old shell script I wrote to automate backups and was met with utter failure on my new (ArchLinux) environment. Thus began the first of many lessons in writing portable shell programs, the gist of which is that there is no such thing as shell “programming”, only the manipulation of core programs through the use of shell commands.
To unpack that a little, consider the following sequence, inspired by Pat Brisbin’s excellent “The Unix Shell’s Humble If”:
if [ "Darmok at Tanagra" != "Jalad at Tanagra" ]; then echo "Shaka, when the walls fell." else echo "His arms wide open." fi
Brisbin highlights that the opening bracket of the test condition for the above if block “is just another command” — technically, it’s just a second form of the test utility, similar in usage to test except that it requires “a trailing ]”. Very same; So copy — but why? As Brisbin notes, “this bit of cleverness leads to an intuitive and familiar form when the [ command is paired with if” but it also leads to improper usage among peeps who expect the Frankensteined block to parse intuitively. Having repeatedly mashed my brain against such cases, much head nodding happened when I read Rich Felker‘s preamable to the excellent POSIX shell tricks: “I am a strong believer that Bourne-derived languages are extremely bad, on the same order of badness as Perl, for programming, and consider programming sh for any purpose other than as a super-portable, lowest-common-denominator platform for build or bootstrap scripts and the like, as an extremely misguided endeavor.” Take the following code block, which does not do what you want it to do:
if [ grep -q 'Tanagra' /log/darmoks_trips.log ]; then echo "Shaka, when the walls fell." fi
The block fails because [ expects an expression to evaluate for an exit code . Remember, the first line is equivalent to:
if test grep -q 'Tanagra' /log/darmoks_trips.log; ...
…and the fix is simple:
if test "grep -q 'Tanagra' /log/darmoks_trips.log"; ...
But there’s no need for any of this because the if command already evaluates the expression that follows it. So all we need is:
if grep -q 'Tanagra' /log/darmoks_trips.log; ...
Which leads me to the second thing I’ve learned about writing portable shell scripts: Read The Fucking Manual. Without getting into a digression on Bashisms, POSIX-compliance, and the many subtle differences in shells, if you want to ensure that your scripts will work as expected, you should be aware of what is and is not part of the POSIX specification. So what does POSIX say about ifs? Glad you asked:
The if command shall execute a compound-list and use its exit status to determine whether to execute another compound-list.
The format for the if construct is as follows:if compound-list then compound-list [elif compound-list then compound-list] ... [else compound-list] fi
The if compound-list shall be executed; if its exit status is zero, the then compound-list shall be executed and the command shall complete. Otherwise, each elif compound-list shall be executed, in turn, and if its exit status is zero, the then compound-list shall be executed and the command shall complete. Otherwise, the else compound-list shall be executed.
The exit status of the if command shall be the exit status of the then or else compound-list that was executed, or zero, if none was executed.
The script creates a new directory and populates it with the following files and folders:
|-- assets/ |-- docs/ |-- src/ | |- main.js |-- tests/ |- bower.json |- gulpfile.js |- LICENSE |- package.json |- README.md |- .gitignore
Each file is configured based on user-defined script defaults or command-line arguments. Usage is straightforward:
usage: gitinitjs.sh [[[-a author_name] [-e author_email] [-g github_user_id] [-l license_type] [-n npm_user_id] [-p project_name] [-t target_directory] [-u author_url]] | [-h]]
-a author_name: the author's name (used to attribute ownership in various files) (the default value can be configured in $AUTHOR_NAME and is currently set to "Misaqe Ismailzai") -e author_email: the author's email address (used to set contact information in various files) (the default value can be configured in $AUTHOR_EMAIL and is currently set to "email@example.com") -g github_user_id: the github.com user id to associate this project with (used to generate links in various files) (the default value can be configured in $GITHUB_USER_ID and is currently set to "moismailzai") -l license_type: the type of license to generate and associate this project with valid types: agpl3 apache2 artistic2 bsd bsd3c eclipse gpl2 gpl3 lgpl2 lgpl3 mit mozilla2 unlicense none (the default value can be configured in $LICENSE_TYPE and is currently set to "mit") -n npm_user_id: the npmjs.com user id to associate this project with (used to generate links in various files) (the default value can be configured in $NPM_USER_ID and is currently set to "moismailzai") -p project_name: the project name (used as the project directory name as well) (the default value is the current directory's name, "/home/mo/Dropbox/dev/cars") -t target_directory: the target directory under which the project directory should be created (the default value can be configured in $TARGET_DIRECTORY and is currently set to "/home/mo/Dropbox/dev/cars/") -u author_url: the author's URL (used to set contact information in various files) (the default value can be configured in $AUTHOR_URL and is currently set to "http://www.moismailzai.com") -h: display this message
The script assumes you’ve set sane defaults so simply calling
is enough to get you rolling (though you’ll be asked to confirm that you want to use the current directory, and its name, as the root directory of a new project). Calling it like so
gitinitjs.sh -p ninjasgonewild
will bootstrap a project called “ninjasgonewild” in a directory called “ninjasgonewild” (provided the directory doesn’t already exist). By default, this project directory is placed wherever the script is invoked from but you can alter the behavior by changing the “TARGET_DIRECTORY” variable to a path of your choice. Calling it with any flag:
gitinitjs.sh -l none
will override that default. Here, it forces the “none” license (which generates a COPYRIGHT file instead of a LICENSE file).
You can force the script to completely override all defaults by explicitly providing all parameter (-a author name, -e author email, -g github.com username, -l license type, -n npmjs.com username, -p project name, -t project target directory, and -u author url):
gitinitjs.sh -a "Christopher Alesund" -e firstname.lastname@example.org -g GeT_RiGhT -l unlicense -n GeT_RiGhT -p ninjasgonewild -t /root/home/christopher -u http://nip.gl/players/get_right
Perhaps a bit much, but a fun weekend challenge to help automate yet another aspect of the daily grind.
Check out the project on GitHub and let me know what you think. And if you happen to delve into the code, line 94 will be our dirty little secret.
Also published on Medium.