back
151 comments
If you're using:

    set -o errexit # or set -e if you prefer
Then you probably also want:

    set -o pipefail
Otherwise, it only checks that the last command succeeds, so something like:

    ls *.ssjkfle | wc -l
will actually continue as success despite the "ls" failing.
'set -o nounset' is a must have. I Just suffered this script from Samsung Printer setting Utility. sudo ./uninstall wiped the /opt

  DEST_PATH=/opt/$VENDOR/$DEST_DIRNAME
  #remove destination
  VERSION=`cat "$DEST_PATH/bin/.version"`
  if rm -fr "$DEST_PATH"; then
  	echo "INFO: $APP_NAME (ver.$VERSION) has been uninstalled successfully."
  ...
Wow, nounset or not, doing a `rm -rf` on a variable without any check is quite irresponsible. Especially if they expect the script to be run as sudo.
Just checking if .version exists and if it doesn't exiting with an error code would be a huge improvement, checking if $DEST_PATH is set is a must as well.

Scary to see a big company like Samsung release code this bad.

Another useful capability I use to do cleanup is `trap`.

    function cleanup {
        ...
    }
    trap cleanup EXIT
See more here: http://linux.die.net/Bash-Beginners-Guide/sect_12_02.html
to clean the command prompt line after CTRL-C:

    trap "{ echo; exit 1; }" INT
Is there a way to determine whether the exit occurred due to the bash script finishing successfully, or due to an error (via set -o errexit)?
Use:

  #!/usr/bin/env bash
Instead of:

  #!/bin/bash
This makes the script more portable as you don't rely on bash (or any executable) to be in /bin.

http://en.wikipedia.org/wiki/Shebang_(Unix)#Portability

Are there any situations where you wouldn't be able to find bash in /bin/bash? I haven't ever seen such a system, but I'd like to know if it's something I might run into...

The other thing is, if you're targeting obscure systems, wouldn't it be just as likely that there wouldn't be a /usr/bin/env, /usr/ might not be mounted, or that bash might not be installed at all?

I suppose what I'm asking is: for practical purposes, is the lowest common denominator /usr/bin/env or is it /bin/sh?

If I want to be that portable, I just use #!/bin/sh and write a straight POSIX shell-compatible script instead.
The only real situation I'd like to use that is on Android, but it does not have /usr/bin/env either.
I'm glad this included the "Signs you should not be using a bash script" section. Bash is a very good solution for many cases, but it becomes downright unruly when dealing with a lot of string manipulation and more complex objects.

    your script is longer than a few hundred lines of code
    you need data structures beyond simple arrays
    you have a hard time working around quoting issues
    you do a lot of string manipulation
    you do not have much need for invoking other programs or pipe-lining them
    you worry about performance
It's not a sign you shouldn't be using bash. It's a sign you shouldn't be using ONLY bash.

People who insist on rewriting an ENTIRE program in Python, Perl, or Ruby fail to understand the Unix philosophy (this is a real misunderstanding I've encountered in my work, not a straw man).

You can just write the complex part in another language, but keep the rest in bash. In other words, main() stays in bash. Python et. al. is used as just another process. bash is a language for coordinating processes.

You don't want a 2000 line bash script. But it can be worse to have a 5,000 line Python script that shells out to tons of other utilities, or awkwardly and verbosely reimplements 'xargs -P' or 'sed -i'. Often you can do the same job with 500 lines of bash and 500 lines of Python (or C), and that is the ideal solution.

Python and bash are pretty radically different languages, and they are not interchangeable (the advices "just rewrite in Python" seems to imply they are). You can use each one for the things they are good at.

I generally, whatever I'm writing, start with bash or considering bash. Usually that means I start with bash, excluding the obvious cases such as graphics or doing heavy data processing where I know from start that line-based approach isn't enough.

Then, when bash isn't enough, I write the difficult parts in Python as standalone utilities and use those from bash. Consequently, I already have a library of little tools in my ~/bin that are generic enough so that I can just use them from bash right away. But every now and then I need to write a special tool that my bash program can use.

Most of my programs "finish" after this stage and they can live for years as a completed bash script calling helper programs. They do their job and there's nothing to add. As long as the program continues to "live on" I just make changes to the bash script or the helper programs.

If at some point I need features that bash can't offer, such as more speed or more complex data processing, I begin to consider rewriting my program in one language. But this is only after the first revision of the program is already complete and I know exactly what to do when porting.

Thus, porting the program is initially only about rewriting in another language, not developing it further at the same time. The first months or years of using the bash script turn into a prototyping stage, and this allows my rewrite be concise, well-thought and clear-partitioned.

In other words, the rewrite becomes a new stable baseline that contains only the best of my bash prototyping, and only solves a perfectly scoped problem. As soon as the port becomes a drop-in replacement for my original bash program I switch to using it and start a development branch to support the new features, if any. Usually the newly gained speed is enough already.

And usually the rewrite happens using C or Scheme. There's rarely enough point in rewriting the script in Python which may already be used to some extent in utility programs that the script calls. At that point I don't want to extend the program but lift it onto a new platform to cut some old baggage that's not needed anymore. There's a certain kind of joy rewriting in a new language something that you know completely before hand. This allows you to only work with the language since the underlying problem is already solved.

Coincidentally, this new C or Scheme program might then, one day in the future, be called from a new bash script...

I'd go as far as saying "having more than a dozen lines". Bash syntax for anything other than basic execution and piping a few commands, is a disaster IMHO. Anything that requires logic should be written in a "real" scripting language.

Python is my choice when I need a bit of logic in my scripts, but it's just a personal choice of course. A good addition for simplifying execution is the "sh" library that lets you call system commands easily as if they were python functions: http://amoffat.github.io/sh/

God yes. I spent a year maintaining ~90k lines of bash. Interesting experience...
I love the very last list »Signs that you should not be using a bash script«. That should be a required part of every language/tool introduction/tutorial.

So very often people lose track of when to use what tools. (Although admittedly, so very often people are forced into some tools by external constraints.)

I found this web page, from the author of musl libc, very insightful:

http://www.etalabs.net/sh_tricks.html

Shell scripts are great, I use and write them every day (and quite advanced ones, too). But it's very hard to make a shell script robust.

Unfortunately it's hard to find a replacement that is stable and installed everywhere. Perl is pretty close. And python too, if you are careful about making your script compatible with all the different versions.

A link on HN about improving bash and it wasn't instructions on how to install zsh. I'm pleasantly surprised :)
I like bash and all, but a well tuned zsh is amazing. I was hesitant for a while but it really improved my workflow in the shell.

  complete -r
disables Bash 'smart tab completion', which in theory is a great idea ( use tab to complete arguments or only list files applicable to the program ) but which never seems to work properly for me.

Disabling it saves a lot of frustrated tab-banging.

Something is terribly wrong with your setup if command line completion is not working.
Nice here document feature I have found recently is heredoc with pipe, e.g.

  cat <<REQUEST_BODY |      
  {
    "from" : 0,
    "size" : 40
  }
  REQUEST_BODY
  curl http://localhost -d @-
It allows to pass heredoc text to standard input of next command.
This is a prime example of useless use of cat. Heredoc already means "pass this as stdin", there's no need to pipe it. Your example without cat:

    curl http://localhost -d @- <<REQUEST_BODY
    {
      "from" : 0,
      "size" : 40
    }
    REQUEST_BODY
My issue with this is that unless you're maintaining a lot of context and understanding the precise weirdness of HEREDOC piping, that looks at first glance like you're catting the HEREDOC to STDOUT and then running a random curl command. It's clever, sure, but it's harder to read and maintain.
You can also write curl after the pipe

    cat <<REQUEST_BODY | curl http://localhost -d @-
      {
        "from" : 0,
        "size" : 40
      }
    REQUEST_BODY
are we going to get a better bash at one point? I've always felt like the only thing bash scripts are good at describing is I/O redirection. But conditionals, dealing with variables, pretty much everything else is frustrating and error-prone

I use fish as my main shell and its slightly better, but just testing things on variables can be a huge mess.

I enjoy using fish as my main shell, but as soon as I ran into a "curl oneliner install" that failed in fish (in my case Homebrew's at the bottom of http://brew.sh/) and required me to jump into bash. I enjoy fish so much I continue to use it, but I am in a state of fear that I will at some point encounter some failing shell script that leaves my system in a broken state.

Do you have any recommendations to make fish play better with shell scripts intended for bash?

I'm making shok (http://shok.io) as an alternative, but it's in its infancy (read: currently useless). The basics should be usable in a few months.

shok proposes that the solution to awkward-shell-syntax is to syntactically separate the programming language from program-invocation.

Well, we've consistently gotten a better bash as bash development has proceeded. As for something that removes the warts and doesn't cut out anything too important and winds up with adoption - it'll be interesting to see...
I'd really recommend using `set -x` or `bash -x script` to sanity check all the commands and expected output.

See http://www.tldp.org/LDP/Bash-Beginners-Guide/html/sect_02_03...

The article mentioned both -x and -v, didn't it? ;)
Try moving all bash code into functions leaving only global variable/constant definitions and a call to “main” at the top-level.

One of my main complaints with bash.. the file is evaluated in order - you can't call a function on the line before it's declared.

This fails:

    #!/usr/bin/env bash
    bar='bar'
    foofunction

    foofunction(){
      echo 'foo'
      echo $bar
    }
Basically you have to write your entire script in reverse, and i'm unaware of a good way to get around it.
Well, what would you do instead? It's an interpreted language. When you type "foofunction" at a prompt, you don't want it to wait around in case you define that to have meaning later.

You probably want the interpreter to be smart: "Am I loading a script from a file, or am I receiving instructions interactively on the command-line?" But now there's two modes of execution, and code in a bash script won't work if you type it into the prompt yourself. That's a bit uncomfortable.

I really hate writing things in reverse, but a decent style in Bash is to put everything in functions, w/ a main() at the top:

    #! /usr/bin/env bash
    set -e -u
    bar='bar'
    
    main() {
      foofunction
    }
    
    foofunction() {
      echo 'foo'
      echo $bar
    }

    main "$@"
C is the same, but at least with C it's possible to declare functions before providing definitions. Is it possible that it's the same in bash?
If you'd like to see a decently written piece of bash that incorporates many of these suggestions, check out pass, the standard unix password manager.

Project page: http://www.zx2c4.com/projects/password-store/ Source: http://git.zx2c4.com/password-store/tree/src/password-store....

Excellent, bash the good parts. More than 15 minutes though.

Googling bashlint, shlint turns up some discussion (bash -n, ksh -n, zsh -n, some github projects), but I doubt they cover this article's specifics - though most (all?) of it could be automatically checked. I think some could be automatically added (e.g. set -o nounset) - perhaps a bash-subset (or coffeescript-style language) possible...

Try out shellcheck - there's an online checker at http://www.shellcheck.net/, and if you like it, the source for it is on github at https://github.com/koalaman/shellcheck.
I have had good results with Syntastic for Vim. Running

    :SyntasticInfo
lists 'sh' as the linting program for bash scripts.
The author uses ${} a lot more than I see in most code. Is it helpful to always use the ${var} syntax instead of simply writing $var?

I can see universal application of ${} being advantageous in avoiding accidental "$foo_bar where you meant ${foo}_bar" situations, and ${} makes it clearer that you're referencing a variable. The only cost would seem to be more typing.

I also like http://google-styleguide.googlecode.com/svn/trunk/shell.xml but of course some things that work well for Google might not work for you.
>> This will take care of two very common errors: >> Referencing undefined variables (which default to "") >> Ignoring failing commands

Better is subjective... About half my scripts depend on those features. For default arguments, and fail early.

He's advocating fail early with "set -e". And as for the other, you can allow overrides with syntax like:

    COULD_BE_SET=${COULD_BE_SET:altvalue}
There is no need for long set flags, e.g. use

  set -e
and not:

  set -o err
etc.
They are functionally equivalent, but the longhand versions are clearly more readable/greppable/google-able. The longhand versions have the exact same benefits as calling a variable eg. `target_file` instead of `a`.
My shell scripts almost always contain "set -e -x", and I always forget which is exit on error, and which is echo commands. The long form would help me avoid that problem.
One thing I've liked is throwing ${PIPESTATUS[*]} at the front of my PS1.
Or you can just use Zsh, which is superior in any way to Bash. ;)