Fun with #metaprogramming in #bash by mdszy
#Bash? #Metaprogramming? WTF? Turns out, bash's everything-is-a-string philosophy makes it pretty awesome for metaprogramming.
I've been messing with Conky and writing a script for getting the currently playing music with it. The first version of the script looked like this.
what_is_running() {
if [ -n "$(pgrep pianobar)" ]; then
echo pianobar
elif [ -n "$(pgrep mpd)" ] && [ -n "$(mpc current)" ]; then
echo mpd
else
echo none
fi
}
trim() {
cat /dev/stdin | sed 's/^ *//g' | sed 's/ *$//g'
}
running=$(what_is_running)
if [ "$running" = "none" ]; then
echo "Nothing playing."
elif [ "$running" = "pianobar" ]; then
title=$(cat ~/.config/pianobar/nowplaying | cut -f1 -d:)
artist=$(cat ~/.config/pianobar/nowplaying | cut -f2 -d:)
echo "Now Playing: $title by $artist"
elif [ "$running" = "mpd" ]; then
artist=$(mpc current | cut -f1 -d- | trim)
title=$(mpc current | cut -f2 -d- | trim)
echo "Now Playing: $title by $artist"
fi
It's pretty ugly. Very repetitive. First in the what_is_running function. All the process searching is hardcoded in. Next we have all those if...elif clauses. Those are awful and very hard to read.
Now, when we introduce metaprogramming into the mix.
services=(pianobar mpd)
what_is_running() {
for service in ${services[@]}; do
if [ -n "$(pgrep $service)" ]; then
echo $service
return
fi
done
echo none
}
trim() {
cat /dev/stdin | sed 's/^ *//g' | sed 's/ *$//g'
}
none_fn() {
echo "Nothing playing."
}
pianobar_fn() {
title=$(cat ~/.config/pianobar/nowplaying | cut -f1 -d:)
artist=$(cat ~/.config/pianobar/nowplaying | cut -f2 -d:)
echo "Now Playing: $title by $artist"
}
mpd_fn() {
if [ -n "$(mpc current)" ]; then
artist=$(mpc current | cut -f1 -d- | trim)
title=$(mpc current | cut -f2 -d- | trim)
echo "Now Playing: $title by $artist"
else
none_fn
fi
}
running=$(what_is_running)
${running}_fn
Much, much, much nicer, isn't it? The first difference is that our hardcoded process searching is replaced with a services array and a for loop. Then we have all of our music outputting routines done in functions. But here is where it gets a little confusing.
running=$(what_is_running)
${running}_fn
WTF? You just have a variable on its own? How the hell is that supposed to work? Well, it does! This is where bash's magic comes into play. With bash, variables are directly expanded into strings. And since ${running} is directly expanded, that line becomes mpd_fn or pianboar_fn or none_fn at runtime, and so instead of throwing an error like any other language would do, bash just calls the function!
Neat, huh? And you thought Ruby and Lisp were the only languages with metaprogramming!
Comments
makaroni4 commented 8 months ago
@mdszy thanks for amazing article!!!
mdszy commented 8 months ago
@makaroni4 Haha, I don't think it's that amazing, but thanks!