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.
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:
timer
's seconds-remaining count to a file, so it's available and mutable to outside programsespeak
All this fits in under 150 lines of shell script.
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
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.
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.