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!

Featured photo (front page) by jm3 on Flickr

Jed Reynolds
Jed Reynolds has been known to void warranties and super glue his fingers together. When he is not doing photography or fixing his bike, he can be found being a grey beard programmer analyst for Candela Technologies. Start stalking him at https://about.me/jed_reynolds.

Written by Jed Reynolds

Jed Reynolds has been known to void warranties and super glue his fingers together. When he is not doing photography or fixing his bike, he can be found being a grey beard programmer analyst for Candela Technologies. Start stalking him at https://about.me/jed_reynolds.

1 Comment

Leave a Reply