fragglet (fragglet) wrote,

What's so bad about shell scripts?

I think that probably almost all smart people have realised that scripting using the Bourne shell is a bad idea if the script in question is more complicated than simply automating what can be typed by hand. I mostly avoid writing shell scripts, preferring to write scripts in either Ruby or Python. However, the ability to write shell scripts is still a useful skill; there are certain situations where writing a shell script really is the easier thing to do - mostly situations that involve mostly revolve around executing commands, or where they're the "standard" thing to do - init.d scripts, for example. It's also useful to be able to debug shell scripts that other people have written. To this end, I recently set about honing my shell scripting skills.

To this end, I wrote a script called branch_helper, which is for automating some of the drudge of managing Subversion branches. The main aim of this was to make maintenance of Strawberry Doom easier, as it is developed as a branch within the Chocolate Doom repository and needs periodic updates.

The result is a script that is probably as complicated a shell script as I am ever going to write; certainly the most complicated that I am ever going to want to write. The process did, however, give me deeper insight into why shell scripts, as a "programming language" are quite so unscalable and only suitable for very simple scripts.


  • One of the most fundamental drawbacks of shell scripts is the lack of a proper list construct. Almost all programming languages give you arrays of some form or other; the closest that you can get with shell scripts is "a string containing a list of items separated by spaces". While this sort-of suffices for some situations, the most obvious drawback is that you can't put items in the "list" that contain spaces themselves. The result of all this is that almost all semi-complex shell scripts are broken if you try to use them with files/directories that contain a space. To demonstrate this, try running a configure script in Cygwin from a directory containing a space (eg. "Documents and Settings").

    Bash has arrays as an extension, but, obviously, that won't work with any other Bourne shells. However, the standard Bourne shell does have one type of list - namely, the list of arguments to a function. It's sometimes possible to make use of this if you structure the script in the right way.

  • Semi-related to the first problem is the problem of how variables are expanded. command "$arg" and command $arg have different meanings, for example, as they expand into either one argument or (potentially) several arguments, respectively. One useful thing to do trying to write "correct" shell scripts is to continually ask yourself - "what would happen if this variable contained a space?"

  • The inability to easily "return" useful information from a function is one annoying drawback. Every function acts as a "mini-subprogram", which is rather aesthetically pleasing in a way, and actually incredibly useful in some situations. However, it suffers from the fact that the only result that programs in Unix can return is a single 8-bit value (exit code).

    The result is that the typical way to pass a value back from a function to its caller is to do something slightly hideous like this:

    result=`myfunction "$arg1" "$arg2"`


  • You can also get all kinds of insidious "gotchas" from the fact that the shell will sometimes fork. For example, the following give different output:

    result=0
    
    while true; do
        result=1
        break
    done
    
    echo $result

    and
    result=0
    
    echo broken | while true; do
        result=1
        break
    done
    
    echo $result
    

    (In the latter, the loop runs in a separate process, so the "result" variable is set in that separate process, and the value lost when the loop finishes).

  • This is actually another manifestation of the previous problem, but handling error situations can be problematic. The simple requirement of "check if a program runs correctly; if it fails, exit the script with an error" can actually be quite tricky to achieve. As the shell can fork to run different parts of the script (especially if you use the backticks trick to pass back values from functions), the "exit" command does different things in different places. If you're in a main script, "exit" will exit the script, but if you're in a section of code that has been forked off into a separate process, it only exits from that other process.

    I wrote a function called "error" to exit with an error, and used it to check that functions run correctly and, if they don't, chain back up to the top and exit properly. So in the end, calling a function looks like this:

    result=`myfunction "$arg1" "$arg2"` || error


    Jon points out that error handling can be significantly simplified by using "set -e", which causes commands to exit if they return an error status.


  • Portability issues. This isn't so much of a problem nowadays because you can pretty much rely on bash being installed on most systems and take advantage of its extensions. However, if you really do want to write a proper "portable" Bourne shell script, there are some things that catch you out. For example, bash lets you define functions using "function myfunction() {" but this isn't supported elsewhere. Similarly, when doing comparisons, bash lets you do eg. "[ "$value" == "shoes" ]" in addition to the standard syntax, which is "[ "$value" = "shoes" ]".

    Some very old systems have quirky interpreters that mean you have to do tricks like "[ "x$value" = "xshoes" ]", because, without the "x", if "value" was empty, that would expand to " [ = shoes ]", which is a syntax error.



All in all, some rather nasty quirks that rapidly turn into gigantic annoyances when you try to do anything complicated. However, it's not to say that shell scripting is completely without merits.
  • Post a new comment

    Error

    default userpic

    Your reply will be screened

    Your IP address will be recorded  

  • 2 comments