Wednesday, November 4, 2015

Bashing Your Shell: Shell Scripting Boot Camp

http://freedompenguin.com/articles/how-to/bashing-your-shell-shell-scripting-boot-camp

Spend enough time on the command line and you’ll eventually want to do many tasks…that take some intricate commands…repeatedly. A good example of this, is making thumbnails of photos. Basically, our workhorse of this script is not ImageMagick (which provides convert, identify and mogrify), but the for loop in bash itself. Ready? Grab your pen-knife and let’s whittle out a script:
  • [ ! -d ~/bin ] && mkdir ~/bin
  • cd ~/bin
  • touch makeThumbs.sh
  • chmod +x makeThumbs.sh
  • vim makeThumbs.sh
If you just copy-pasted that block of commands…you just executed a shell script and we’re done. See you next week!
Oh, OK, I’ll keep going since all that created was a file and got you into your editor. A proper shell script begins with a “shebang” (#!/bin/bash), (also pronounced in its Batman fight-scene parts: “pound, bang, slash, bin, slash, bash!”).
  • #!/bin/bash
  • for f in *.jpg ; do
  • echo $f
  • done
That’s pretty much how most shell scripts get going: looping over a series of file names. No matching file names? Let’s make a quick one:
  • touch asdf.jpg
Some safety tips:
1) start all your for loops this way, with your for-echo.
2) exit on errors immediately. Use touch asdf.jpg
3) need more help? use set -x, and watch it in action
4) double quote your variables
5) test for files with -f
6) test for error conditions (non-zero exit conditions)
7) need more help? Install the bash debugger (Debian/Ubuntu using apt): apt-get install bashdb
Let’s add these to our thumbnail program:
  • #!/bin/bash
  • set -e # this makes any command returning !0 exit the script
  • set -x # shows all script commands
  • for f in *.jpg ; do
  • echo "$f"
  • done
Now we can test for to see if the files actually exist, since there is no point calling a script on something that isn’t there.
  • for f in *.jpg ; do
  • if [ -f "$f" ]; then
  • echo "$f"
  • fi
  • done
Why do we put quotes around the variable? Consider that file names can have spaces, yet when we give arguments to for, it separates things with spaces.
  • touch "a b c.txt"
  • for f in a*txt; do
  • echo "# $f;"
  • done
Will output:
# a; # b; # c.txt;
So putting double quotes around for variables doesn’t work, does it? You’re right. That’s safer to use while read loops. We can pipe the output of ls to read in a while loop. The ls command outputs a file per line, and read ingests a whole line. Now we’ll do it right:
  • ls *jpg | while read f ; do
  • if [ -f "$f" ]; then
  • echo "$f"
  • fi
  • done
Next, let’s remove asdf.jpg and now we have no pictures in our directory. The set -e in our script will break and exit our script right before we get to the while. This is because ls exits with error code 2 if there are files not found. We can be strict and detect this and exit early:
ls *jpg &>/dev/null || ( echo "Nothing to thumbnail, bye"; exit 0)
ls *jpg | while read f ; do … done
Getting tough yet? If you’re keeping up with me, you’re a bad-ass. Most people skip this stuff. If they script enough, they have a bugger of a time figuring out where their script takes a dump. Let’s put some more meat on this skeleton and make a thumbnail:
  • #!/bin/bash
  • set -e
  • set -x
  • ls *jpg &>/dev/null || ( echo "Nothing to thumbnail, bye"; exit 0)
  • size=450x300
  • options="-quality 96 \
  • -thumbnail $size \
  • -bordercolor gray \
  • -border 10x10 \
  • -strip"
  • ls *jpg | while read f ; do
  • if [ -f "$f" ]; then
  • thumbnail="${f/.jpg/}-$size.jpg"
  • convert "$f" $options $thumbnail || true
  • fi
  • done
What we did if [ -f $f ]? For file a b c.txt, that would evaluate in the script as:
if [ -f a b c.txt ] and return "a: file not found".
See what I mean about convert not actually being the meat of the script? The script is the meat. Convert is merely the hammer that strikes the iron. Some things you might not have seen before:
  1. Line continuation looks like this: this \
    is the same line
  2. || true lets a command fail but not have set -e kill the script
  3. ${f/.jpg/} means ${variable/pattern/replacement} Basic pattern replacement! You do not need to call out to sed or perl to do regular expressions.
We do a lot of work in this script to detect our error conditions. Know what we can do when we get it working? We can take a deep breath and comment out set -e and set -x.
# set -e
# set -x
Be sure to leave that in so that any other shell script lover will appreciate our careful work!

No comments:

Post a Comment