Shell scripting is very powerful

By dkl9, written 2024-226, revised 2024-226 (0 revisions)


Followup to: 10 underused shell scripting techniques

I started computer programming for learning and for its intrinsic fun. Having finished that part, in the past two years, most of my programming has been to make computers do things, novel or highly customised, which I need for immediate use. Intuitively, Python or JavaScript would be the languages to use. But shell scripting fits just a tad more natively than either into a heavily-riced Linux system, and suffices for most of my tasks. By having fewer features, the shell is both easier to master and trickier to apply, making it a great way to sustain a flow-state of fascination and insight.

§ Pomodoro-like timer

In its basic form, a timer is a loop that keeps track of and shows remaining time. The timer ends when it starts, plus however long it runs — maybe a minute.

timer() {
    TR="${1:-60}"
    CT=`date '+%s'`
    ET="$((CT+TR))"
    while [ "$TR" -gt 0 ]
    do
        echo "$TR"
        sleep 4
        CT=`date '+%s'`
        TR="$((ET-CT))"
    done
}

echo start work
timer 1500
echo take a break
timer 300
echo done

But there is so much more to be done. For now, the timer ends silently in a terminal window you might have hidden. It's more effective if it interrupts you in fullscreen.

echo start work
timer 1500
xterm -fullscreen -fs 48 -fa monospace -e sh stop.sh
timer 300
xterm -fullscreen -fs 48 -fa monospace -e sh done.sh

Those -e sh script.sh clauses are to give xterm code by which to write text onto its window. In a more mainstream programming language, you'd make a window and write text to it in the same program.

screen = pygame.display.set_mode()
# ...
screen.blit(FONT.render("hello world", False, "green"), (0, 0))

Now it appears we need multiple files, some annoyingly short, for one shell program.

Or do we?

if [ -z "$1" ]
then
    echo start work
    timer 1500
    xterm -fullscreen -fs 48 -fa monospace -e sh "$0" stop
    timer 300
    xterm -fullscreen -fs 48 -fa monospace -e sh "$0" done
elif [ "$1" = stop ]
then
    echo take a break
    sleep 2
elif [ "$1" = done ]
then
    echo done
    sleep 2
fi

If you want to enforce the break, rather than just annoyingly suggest it, you can shift the five-minute break timer into the stop branch. As each part of the program is in the same file and can use timer defined earlier, this is trivial.

if [ -z "$1" ]
then
    echo start work
    timer 1500
    xterm -fullscreen -fs 48 -fa monospace -e sh "$0" stop
elif [ "$1" = stop ]
then
    echo take a break
    timer 300
    echo done
    sleep 2
fi

There is so much more you can do with this, and which I already have done with this:

All this fits in under 150 lines of shell script.

§ Morse code generator

In retrospect, this one would've worked with the even-simpler make — but that's a bit far.

To make Morse code from a message, you need a message.

printf 'message: '
read MESSAGE

A message in Morse code is the sum of the signal for each letter in the message. So we need to iterate over the characters in $MESSAGE.

Normally, a shell script iterates over lines with generate-lines | while read LINE; do ... done. Here, we need each character. As it happens, read -n 1 X reads a single character, rather than a whole line.

echo "$MESSAGE" | while read -n 1 CHAR
do
    :
done

Each letter has a signal. We could store those in letter/a.mp3 thru letter/z.mp3. ffmpeg can concatenate audio (or video) files based on a custom ffconcat format.

file letters/h.mp3
file letters/e.mp3
file letters/l.mp3
# ...
file letters/l.mp3
file letters/d.mp3

Generating those sound clips will come first in the script. For now, make the ffconcat files by filling message-characters into copies of that line —

while read -n 1 CHAR
do
    if [ -f "letters/${CHAR}.mp3" ]
    then
        echo "file letters/${CHAR}.mp3" >>message.txt
    else
        echo "character '${CHAR}' unavailable"
    fi
done

— and use them:

ffmpeg -f concat -i message.txt message.mp3

Make sure to reset the ffconcat file message.txt on each run.

printf 'message: '
echo >message.txt
read MESSAGE

§ Describing letters

Let's use a perfectly natural, intuitive format to express the sequence of dots and dashes for each letter.

a .-
b -...
c -.-.
d -..
e .
# ...
x -..-
y -.--
z --..

Thanks to the magic of read's field-separation, a shell script can use this easily. It's like destructuring syntax in for loops, before it was cool.

cd letters
IFS=' '
while read LETTER SIGNAL
do
    if ! [ -f "${LETTER}.mp3" ]
    then
        : make ${LETTER}.mp3 according to $SIGNAL
    fi
done <alphabet.txt

Sound clips for letters are chains of copies of dots, dashes, and pauses, which ffmpeg will easily concatenate. Each letter's sound clip starts with a pause for spacing. Making pause.mp3 (likewise, dot.mp3 and dash.mp3) will come earlier in the script.

echo 'file ../pause.mp3' >concat.txt

The rest is dots and dashes, according to the characters in $SIGNAL.

echo "$SIGNAL" | while read -n 1 DD
do
    : add dot or dash to concat.txt
done

Complication: echo adds an extra newline.

printf '%s' "$SIGNAL" | while read -n 1 DD
do
    if [ "$DD" = '.' ]
    then
        echo 'file ../dot.mp3' >>concat.txt
    elif [ "$DD" = '-' ]
    then
        echo 'file ../dash.mp3' >>concat.txt
    fi
done

Back in the loop over alphabet.txt, go on and merge the sound-clips:

ffmpeg -f concat -safe 0 -i concat.txt "${LETTER}.mp3"

ffmpeg, of course, reads a word from input, messing up the read command iterating over alphabet.txt.

echo '' | ffmpeg -f concat -safe 0 -i concat.txt "${LETTER}.mp3"

Making dot.mp3, dash.mp3, and pause.mp3, I leave as an exercise to the reader. It's more a matter of ffmpeg than shell.

§ Document build script

The script converting metadata, Markdown, and other inputs into webpages here can convert itself into a largely-self-documenting webpage. In about 250 lines of shell (ignoring comments), it fills in headers with metadata, adds heading fragments to cmark's HTML output, converts itself to Markdown and HTML, generates an index page, and makes an Atom feed.