A sysadmin's guide to Bash

Tips and tricks for making the Bash shell work better for you.
253 readers like this
253 readers like this
GitHub launches Open Source Friday 

Opensource.com

Each trade has a tool that masters in that trade wield most often. For many sysadmins, that tool is their shell. On the majority of Linux and other Unix-like systems out there, the default shell is Bash.

Bash is a fairly old program—it originated in the late 1980s—but it builds on much, much older shells, like the C shell (csh), which is easily 10 years its senior. Because the concept of a shell is that old, there is an enormous amount of arcane knowledge out there waiting to be consumed to make any sysadmin guy's or gal's life a lot easier.

Let's take a look at some of the basics.

Who has, at some point, unintentionally ran a command as root and caused some kind of issue? raises hand

I'm pretty sure a lot of us have been that guy or gal at one point. Very painful. Here are some very simple tricks to prevent you from hitting that stone a second time.

Use aliases

First, set up aliases for commands like mv and rm that point to mv -I and rm -I. This will make sure that running rm -f /boot at least asks you for confirmation. In Red Hat Enterprise Linux, these aliases are set up by default if you use the root account.

If you want to set those aliases for your normal user account as well, just drop these two lines into a file called .bashrc in your home directory (these will also work with sudo):

alias mv='mv -i'
alias rm='rm -i'

Make your root prompt stand out

Another thing you can do to prevent mishaps is to make sure you are aware when you are using the root account. I usually do that by making the root prompt stand out really well from the prompt I use for my normal, everyday work.

If you drop the following into the .bashrc file in root's home directory, you will have a root prompt that is red on black, making it crystal clear that you (or anyone else) should tread carefully.

export PS1="\[$(tput bold)$(tput setab 0)$(tput setaf 1)\]\u@\h:\w # \[$(tput sgr0)\]"

In fact, you should refrain from logging in as root as much as possible and instead run the majority of your sysadmin commands through sudo, but that's a different story.

Having implemented a couple of minor tricks to help prevent "unintentional side-effects" of using the root account, let's look at a couple of nice things Bash can help you do in your daily work.

Control your history

You probably know that when you press the Up arrow key in Bash, you can see and reuse all (well, many) of your previous commands. That is because those commands have been saved to a file called .bash_history in your home directory. That history file comes with a bunch of settings and commands that can be very useful.

First, you can view your entire recent command history by typing history, or you can limit it to your last 30 commands by typing history 30. But that's pretty vanilla. You have more control over what Bash saves and how it saves it.

For example, if you add the following to your .bashrc, any commands that start with a space will not be saved to the history list:

HISTCONTROL=ignorespace

This can be useful if you need to pass a password to a command in plaintext. (Yes, that is horrible, but it still happens.)

If you don't want a frequently executed command to show up in your history, use:

HISTCONTROL=ignorespace:erasedups

With this, every time you use a command, all its previous occurrences are removed from the history file, and only the last invocation is saved to your history list.

A history setting I particularly like is the HISTTIMEFORMAT setting. This will prepend all entries in your history file with a timestamp. For example, I use:

HISTTIMEFORMAT="%F %T  "

When I type history 5, I get nice, complete information, like this:

1009  2018-06-11 22:34:38  cat /etc/hosts
1010  2018-06-11 22:34:40  echo $foo
1011  2018-06-11 22:34:42  echo $bar
1012  2018-06-11 22:34:44  ssh myhost
1013  2018-06-11 22:34:55  vim .bashrc

That makes it a lot easier to browse my command history and find the one I used two days ago to set up an SSH tunnel to my home lab (which I forget again, and again, and again…).

Best Bash practices

I'll wrap this up with my top 11 list of the best (or good, at least; I don't claim omniscience) practices when writing Bash scripts.

  1. Bash scripts can become complicated and comments are cheap. If you wonder whether to add a comment, add a comment. If you return after the weekend and have to spend time figuring out what you were trying to do last Friday, you forgot to add a comment.

  1. Wrap all your variable names in curly braces, like ${myvariable}. Making this a habit makes things like ${variable}_suffix possible and improves consistency throughout your scripts.
  1. Do not use backticks when evaluating an expression; use the $() syntax instead. So use:
    for  file in $(ls); do

    not

    for  file in `ls`; do

    The former option is nestable, more easily readable, and keeps the general sysadmin population happy. Do not use backticks.

  1. Consistency is good. Pick one style of doing things and stick with it throughout your script. Obviously, I would prefer if people picked the $() syntax over backticks and wrapped their variables in curly braces. I would prefer it if people used two or four spaces—not tabs—to indent, but even if you choose to do it wrong, do it wrong consistently.
  1. Use the proper shebang for a Bash script. As I'm writing Bash scripts with the intention of only executing them with Bash, I most often use #!/usr/bin/bash as my shebang. Do not use #!/bin/sh or #!/usr/bin/sh. Your script will execute, but it'll run in compatibility mode—potentially with lots of unintended side effects. (Unless, of course, compatibility mode is what you want.)
  1. When comparing strings, it's a good idea to quote your variables in if-statements, because if your variable is empty, Bash will throw an error for lines like these:
    if [ ${myvar} == "foo" ]; then
      echo "bar"
    fi

    And will evaluate to false for a line like this:

    if [ "${myvar}" == "foo" ]; then
      echo "bar"
    fi  

    Also, if you are unsure about the contents of a variable (e.g., when you are parsing user input), quote your variables to prevent interpretation of some special characters and make sure the variable is considered a single word, even if it contains whitespace.

  1. This is a matter of taste, I guess, but I prefer using the double equals sign (==) even when comparing strings in Bash. It's a matter of consistency, and even though—for string comparisons only—a single equals sign will work, my mind immediately goes "single equals is an assignment operator!"
  1. Use proper exit codes. Make sure that if your script fails to do something, you present the user with a written failure message (preferably with a way to fix the problem) and send a non-zero exit code:
    # we have failed
    echo "Process has failed to complete, you need to manually restart the whatchamacallit"
    exit 1

    This makes it easier to programmatically call your script from yet another script and verify its successful completion.

  1. Use Bash's built-in mechanisms to provide sane defaults for your variables or throw errors if variables you expect to be defined are not defined:
    # this sets the value of $myvar to redhat, and prints 'redhat'
    echo ${myvar:=redhat}
    # this throws an error reading 'The variable myvar is undefined, dear reader' if $myvar is undefined
    ${myvar:?The variable myvar is undefined, dear reader}
  1. Especially if you are writing a large script, and especially if you work on that large script with others, consider using the local keyword when defining variables inside functions. The local keyword will create a local variable, that is one that's visible only within that function. This limits the possibility of clashing variables.
  1. Every sysadmin must do it sometimes: debug something on a console, either a real one in a data center or a virtual one through a virtualization platform. If you have to debug a script that way, you will thank yourself for remembering this: Do not make the lines in your scripts too long!



    On many systems, the default width of a console is still 80 characters. If you need to debug a script on a console and that script has very long lines, you'll be a sad panda. Besides, a script with shorter lines—the default is still 80 characters—is a lot easier to read and understand in a normal editor, too! 

I truly love Bash. I can spend hours writing about it or exchanging nice tricks with fellow enthusiasts. Make sure you drop your favorites in the comments!

Hi! I'm Maxim, a solution architect and evangelist in Red Hat's Benelux team. Red Hat radiates open source in every possible way, which makes it the perfect company to work for, for an open source enthusiast like me.

16 Comments

Great article, Maxim! I have been doing Linux for a long time and I have learned a couple new things from your article. I particularly like the usages, ${variable} and "for file in $(ls)". I don't know why I did not know about these before.

I do disagree with your statement about using sudo. I prefer to login as root because using sudo means more typing which is less efficient.

An excellent article.

Thanks David! Glad you like it!

As for the sudo thing: though I agree plainly logging in as root is slightly easier, from a security point of view, it takes away some important opportunities to audit users and potentially correlate individual actions to incidents.

It's also a lot easier to give someone temporary privileged access to a machine with sudo, or to provide someone with just some privileges and not just all of them ;)

In fact, I use sudo even on my own machines: the time I need to spend typing in my password is also the time I have to rethink that command I just typed ;)

As for typing less: one thing I have been using to make that a reality is Ansible. There is a recent article about using Ansible as well: https://opensource.com/article/18/7/sysadmin-tasks-ansible

Happy sysadminning!

In reply to by David Both

I was with you until you recommended using spaces for tabs, you filthy heathen.

In all seriousness, there's some good tips in here. I've actually started using a few of these myself recently (parameter substitution is fantastic). Bash apparently isn't the hip thing anymore, but it's incredibly solid. Good to see some love for it.

You're much better off not messing with any well-known commands like mv or rm, because one day you'll be on a system where you don't have those aliases, it'll be 3am, and you'll get a really nasty surprise. Probably as root.

More workable: use a script (say, "del") to delete your files by moving them to a trashcan directory on the same filesystem, so you don't have to wait all day to remove something really big. This at least gives you a chance to change your mind.

I respectfully disagree. It's not relying on that alias to exist, it's a safety net for when you type the wrong command inadvertently, or as the wrong user.

But if consistency is the point that needs solving, Ansible does a great job in making sure all aliases are identical on all of your servers ;)

In reply to by Karl Vogel (not verified)

Tabs are wonderful!

I recommend running all your scripts through shellcheck (https://github.com/koalaman/shellcheck) Available for most linux distros. It will give you warnings when not using your best practices above. It's also easy to integrate with your CI workflow (for example through the jenkins checkstyle plugin)

Also, you can download a precompiled binary of shellcheck from its github repo if you rather have the latest.

BTW, shellcheck will complain that you should'nt use "for file in $(ls)" but "for file in *" :)

In reply to by Johan Ekblad (not verified)

-f overrides the -i in the rm command. The alias will block "rm /" but not "rm -f /"

Otherwise, some useful information here

If you are in vim and working on a file but forgot to sudo, instead of exiting and redoing your work, use this
:w !sudo tee %

Hi Maxim,

Nice article! I've heard the backticks being deprecated before from somebody else. Could you elaborate what's behind that though?

I used to be a big fan of those, but have been adapting since I heard it's deprecated.

I wouldn't know about them being deprecated, but the fact that you cannot nest them is reason enough for me not to use them. I also prefer the syntax of $(foo) over `foo`: it's quite clear where a $(foo) statement starts and ends, even if you do $(foo $(bar)). That's much harder with `foo `bar``.

In reply to by Ted Kraan (not verified)

Allow me a correction/suggestion. Conditional tests are best expressed in bash with [[ ]], as in:

if [[ ${myvar} == "foo" ]]; then
echo "bar"
fi

This snippet doesn't throw an error if myvar is undefined or empty. Better still, it doesn't do word splitting on the expansion ${myvar}.

Using [ ] is the sh-way of doing tests, not the bash-way.

Hi Maxim,
Thank you for this interesting article.
- For Shebang, I personally prefer using "#!/usr/bin/env bash". It is better for portability, as you cannot be sure to have the bash executable always under /usr/bin.
- I personnaly use exclusively $() instead of backticks, and love the use of curly braces with variables. It is clean and safe.
- An mentionned by Paulo Marcelo Coelho Aragao, I prefer the use double square brackets [[ ]] for conditional test. (Cf. http://tldp.org/LDP/abs/html/testconstructs.html#DBLBRACKETS)
- For clarity, in scripts, always use a usage function that you may call while testing script arguments. Example:
usage() {
echo "Usage: $0 hostName"
echo " hostName: Name of the host to check"
exit 1
}

[[ -z ${hostName} ]] && usage

- It is also a good idea to have your own scripts that help you do thing easily and quickly. I generally put them in a directory that I append to the PATH env variable.

Hi Mossaab,

thanks for your remarks! (And sorry for my own late reply... Vacation happened :))

I'm not a huge fan of the env statement myself, partially because my scripts are all aimed at Linux, so Bash in /usr/local/bin/bash, like on BSD, is not a big risk. The second, more prominent reason, is that an env statement potentially uses a different bash than the system one, with potentially unexpected results. (Granted, this is less of a problem with bash than with, say, Python.)

The double brackers is a fair point, and something I'll take into account for future versions of this - or other - articles.

The usage() function is a nice one as well, thanks!

In reply to by Mossaab Stiri

Creative Commons LicenseThis work is licensed under a Creative Commons Attribution-Share Alike 4.0 International License.