Work the Shell

Scripting Lite: Shell Aliases and Functions

Dave Taylor

Issue #220, August 2012

You've probably never paid much attention to the aliases in your .bashrc, but Dave has. In this column, he talks about aliases and command-line functions as an easy way to expand your command-line options.

Let's take a side trip this month, a journey away from the busy thoroughfare of shell script programming and into the relatively peaceful eddy of shell aliases. You probably are thinking “Yeah, that's two paragraphs. Then what?” But stick with me—there's a bit more to aliases than you may have realized.

To start, you can see what system and shell aliases you have simply by typing alias on the command line:

$ alias
alias adb='/home/taylor/Documents/Android\ SDK/platform-tools/adb'
alias grep='grep --color=always'
alias ksnap='/home/taylor/Documents/Android\ SDK/tools/ddms'
alias ls='ls -F'
alias scale='/home/taylor/bin/scale.sh'
alias vps='ssh taylor@intuitive.com'

For the most part, these are straightforward substitutions of one word for another. Type in vps, and the shell expands that to be the command ssh taylor@intuitive.com.

Look at the alias for ls though. You can start to see a tiny bit of the sophistication that the shell has by realizing that it's not an alias loop. I mean, ls = ls -F, so shouldn't ls -F expand to ls ls -F -F, and so on? Fortunately, it doesn't, and that's one reason you easily can tweak and expand your command-line capabilities with aliases.

In fact, my sequence is often: type in the command until it's complex enough for an alias, which works until it's sufficiently complex for a shell script.

Let me show you a handy trick by demonstrating how I'd turn a complex find command into an alias:

$ find . -name "*core*" -print

Now let me use a shell shortcut, !!, for the most recent command I've typed in within a more-complicated sequence:

$ echo "alias findcore=\"!!\"" >> ~/.bashrc

There's no feedback, so what did it actually do? To find out, examine the last line of the .bashrc:

$ tail -1 ~/.bashrc
alias findcore="find . -name *core* -print"

Nice. Easy. When your alias has become sufficiently complicated that a shell script would be easier, you can do the same basic thing again:

$ alias findcore > findcore.sh

Or, if you're really organized like I am, perhaps you could put the script into your ~/bin directory, which changes the last line to:

$ alias findcore > ~/bin/findcore.sh
$ vi !$

There's another shell shortcut for you: !$ is the last word of the previous command—in this case, it'll be the filename you just created. Bash actually has a ton of handy little shortcuts like that, and they're doubly useful if you have complex directory names and filenames!

Complicated Aliases

The most common problem with an alias is that you want to be able to have the alias process the arguments you've specified, but not just automatically at the end of the command sequence. For the ls alias above, it's no big deal, but what if you wanted to have something like this:

alias ls="ls -F | tee -a ~/ls.log"

This would let you have a running log of all output from the ls command. The alias works fine until you invoke it with something like:

$ ls /home

in which case it's expanded to:

ls -F | tee -a ~/ls.log /home

That's going to generate an error.

To get this to work, you need to switch from an alias to a shell function. It's not too common on the command line, but quite reasonable for your .bashrc. Here's how I'd write it:

list() {
   ls -F $* | tee -a ~/ls.log
}

Experimentation reveals that you can shrink things down to a minimum of two lines, but no shorter. So, do something like this:

list() { ls -F $* | tee -a ~/ls.log }

and the shell will give you the secondary prompt (usually >) waiting for the closing curly brace. Because you're going to drop this into the .bashrc, it doesn't really matter much.

Let's do some more interesting things with these functions, fancier aliases that demonstrate a bit of the power of these command-line functions.

First off, convert to uppercase:

upper() {
   echo $1 | tr '[a-z]' '[A-Z]'
}

That looks good, but there's a flaw in this function. Can you see it? The problem is that if I use $1, I don't get multiword input:

$ upper linux journal rocks
LINUX

Oops. It's easily fixed by substituting $* instead. While I'm at it though, let's use defined letter groups and sets rather than explicit ranges:

upper() {
   echo $* | tr '[[:lower:]]' '[[:upper:]]'
}

$ upper linux journal rocks
LINUX JOURNAL ROCKS

That's better.

A very nice feature of most modern Linux systems is a change directory command pair called pushd and popd. Use pushd instead of cd to change your current working directory, and the system will remember where you were. Do a quick popd, and you're back. That's helpful when you're bouncing around the filesystem, but what if you don't have these available?

Implementing a similar directory stack in a couple command functions is actually straightforward. Here's my version:

push() {
   current=$(pwd)
   cd $1 ; echo "Moved to $1"; ls -CF
}

pop() {
   echo "Moving back to $current"
   cd $current
}

Let's give it a shot:

$ push $HOME
Moved to /home/taylor
Desktop/        Dropbox/        Movies/        Presentations/
Documents/      Google Drive/   Music/         Public/
Downloads/      Library/        Pictures/      Sites/

What's happened to the “current” shared variable? Let's have a look:

$ echo $current
/home/taylor/Desktop

To switch back to the previous working directory, therefore, all that's needed is:

$ pop
Moving back to /home/taylor/Desktop

That's easy!

To make this more sophisticated, you would need to use an array of directory names and keep track of your array depth. It's not too difficult, but I'll leave that one for an enthused reader!

One big difference between aliases and functions, I should note, is that you can use an alias with the same name as a command, but most shells complain about attempts to define a function where the name collides with an alias:

$ ls() {
-bash: syntax error near unexpected token '('
$ unalias ls
$ ls() {
   echo "file listing disabled"
}
$ ls /
file listing disabled

Sneaky, isn't it? You can imagine how you really could make some changes to how the Linux command line works for an unsuspecting colleague, but perhaps you should leave that for when a new person joins your team and you feel the need for some hazing.

Anyway, that's it for aliases and command-line functions this time. I encourage you to explore and experiment with what you can do in your .bashrc to make your command-line interaction more pleasant and efficient.

Dave Taylor has been hacking shell scripts for more than 30 years. Really. He's the author of the popular Wicked Cool Shell Scripts and can be found on Twitter as @DaveTaylor and more generally at www.DaveTaylorOnline.com.