Recent Posts

Reaching for the Unix Toolchain

The first time I used the Unix shell, I was hooked. The idea of having all these little programs, each of which did one thing, and being able to chain them together to do more complicated things, made perfect sense. Coming from an 8-bit background, where you were always up against the limits of the machine and waiting for programs to load, keeping everything small and focused was great.

So even though I don't do as much Unix system administration as I used to (though I am available), I still reach for the toolchain on my own systems on a daily basis. There are many times when I could reach for a language like Perl or C and write a complete application, but so many times that isn't necessary -- I just want to do one thing, one time, and do it right now with as little work as possible.

For instance: today I was going to listen to music on my MP3 player, but the plug is getting loose, so it wouldn't play right. So I plugged it into the computer, mounted it, and figured I'd play the songs there. But I wanted to use the playlist from the unit, and shuffle the songs like it does, and the songs are in multiple directories.

Now, I'm sure that I could install a program like XMMS, and it would handle all that, but installing and learning it would take time, and it would mean running a full app to get at 1% of its features. That's not very Unix-y. So here's what I did. It's an example of starting with one function and then adding tools to the chain until you're finished.

First of all, after changing directory to the player's MUSIC directory, the playlist is in "music.m3u". The format of this file has one comment line at the top, and then each song is on its own line, with blank lines between each song. So the first thing I needed to do was just get the actual song lines. Since all the songs have an mp3 suffix, that was easy:

cat music.m3u | grep mp3

Okay, that gives me the list, but they're in order. (I know, useless use of cat, but I reckon it makes it clearer what I'm doing.) To shuffle the list, I reach for the random utility:

cat music.m3u | grep mp3 | random -f - 1

Now I have a shuffled list of songs, so I need to loop through them and pass them to a program that will play them. First, I usually just loop and echo the lines to make sure that works:

for i in `cat music.m3u | grep mp3 | random -f - 1`; do echo $i; done

The backquotes there pass the output of my chain to the for loop, so it can loop through them. Uh oh, that shows a problem: by default, for breaks on all whitespace, and my songs have spaces in their names, so every word is being echoed on a separate line. That won't work, so I need to tell for to break only on newlines. I do that by setting the IFS environment variable:

IFS="
"; for i in `cat music.m3u | grep mp3 | random -f - 1`; do echo $i; done

Great, now it's seeing one song each time through the loop and putting it in $i. So now I add my MP3 playing program:

IFS="
"; for i in `cat music.m3u | grep mp3 | random -f - 1`; do mpg321 "$i"; done

I put quotes around $i so that mpg321 will see the filename as one argument as well. But now I've discovered one more problem that I didn't notice before: the filenames in the playlist use backslashes between directory and filenames, rather than the forward slashes that my system uses. So I need to insert a command to change those:

IFS="
"; for i in `cat music.m3u | grep mp3 | random -f - 1 | sed 's/\\\\/\\//g'`; do mpg321 "$i"; done

Here's the deal with that sed command. The first four backslashes end up being seen as sed as a single literal backslash to be replaced, because the shell evaulates each pair as a single escaped backslash, then sed does the same. The second pair of backslashes are turned into a single literal backslash by the shell, and then sed uses that to escape the forward slash that follows. The result is to replace each backslash in the line with a forward slash.

Now it works, but there's one small issue: it's hard to kill out of it. Killing the current mpg321 process allows the loop to continue and start the next one. If I keep pressing Control-C, it just keeps killing the mpg321 processes, not the for loop itself. So let's add a one-second sleep after each song, so I can Control-C twice: once to kill the song, then again to kill the loop while it's sleeping:

IFS="                                                                         
"; for i in `cat music.m3u | grep mp3 | random -f - 1 | sed 's/\\\\/\\//g'` ; do mpg321 "$i"; sleep 1; done

And that's it! It took me maybe 3 minutes to hack it together; I didn't even sit down. Now, if I were doing this for pay, or as a program I expected to use on a regular basis, there are a lot of things I'd add, and I'd probably redo it as a single program. I'd want a cleaner exit method. I'd want it to handle non-MP3 files. I'd want it to deal more gracefully with unusual filenames -- this will choke on a file that actually has a backslash in the name, for instance (which is very unlikely, but possible). If others were using it, I might write my own randomizer, in case their system doesn't have 'random' installed, and I'd want it to ask them what audio player to use. There would be lots of ways to nice it up.

But in this case, just wanting to get the music started so I could get back to what I was doing, this was the best solution. And that's often the case with sysadmin work: someone says, "Can you tell me what email came in at 7:45:03 last night?" I could write a program to let the client enter a time and see a report of emails from that time -- or I could just toss together a pipeline of a few commands and answer the question. You have to know when it makes more sense to build something more complete and lasting, but many times the best solution is the quickest one using the tools at hand.

Posted on: 2015-10-13