Conventions   11 comments

I like Bash, I really like Bash. It’s simple, it’s neat, it’s quick, it’s many things. No one will convince me otherwise. It’s not for big things though, I know, but for most system administration tasks it will suffice.
The problem with Bash begins where incompetent SysAdmins start to use it. They will most likely treat it poorly and lamely.
Conventions are truly needed when writing in Bash. In our subversion we have ~30,000 lines of bash. And HELL NO! our product is definitely not a lame conglomerate of Bash scripts gluing many pieces of binaries together. Bash is there for everything that C++ (in our case) shouldn’t do, things like:

  • Packaging and anything related to packaging (post scripts of packages for instance)
  • SysV service infrastructure
  • Backups
  • Customization of the development environment
  • Deployment infrastructure

Yes, so where have we been? – Oh, how do we manage ~30,000 lines of Bash without getting everything messed out. For this, we need 3 main things:

  • Version control system (CVS, SVN, git, pick your kick)
  • Competent people
  • Coding conventions

Configuring a version control system is easy, espcially if you are a competent developer, I’ll skip to the 3rd one.

Conventions. Show me just one competent C/C++/Java/C# developer who would skip on using coding conventions and program like an ape. OK, I know, there might be some, but we both think the same thing about them. Scripting in Bash shouldn’t be an exception in that case. We should use strict scripting conventions on Bash in particular and on any other scripting language in general.
There’s nothing uglier and messier than a Bash script that is written without conventions (well, maybe Perl without conventions).

I’ll try in the following post to introduce my Bash scripting conventions and if you find them neat – feel free to adopt them. Up until today, sadly, I havn’t seen anyone suggesting any Bash scripting conventions.

Before starting any Bash script, I have the following skeleton :

#!/bin/bash

main() {
}

main "$@"

Call me crazy, but without my main() function I ain’t going anywhere. Now we can start writing some bash. Once you have your main() it means you’re going to write code ONLY inside functions. I don’t care it’s a scripting language – let’s be strict.

#!/bin/bash

# $1 - hostname
# $2 - destination directory
main() {
	local hostname=$1; shift
	local destination_directory=$1; shift
}

main "$@"

Need to receive any arguments in a function? – Do the following:

  • Document the variables above the function
  • Use shift after receiving each variable, and always use $1, it’s easier to later move the order of the variables

Notice that i’m using the local keyword to make sure the scope of the variable is only within its function. Be strict with variables. If for some reason you decide you need a global variable, it’s OK, but make sure it’s in capital letters and declared as needed:

# a read only variable
declare -r CONFIGURATION_FILE="/etc/named.conf"
# a read only integer variable
declare -i -r TIMEOUT=600

# inside a function
sample_function() {
	local -i retries=0
}

You are getting the point – I’m not going to make it any easier for you. Adopt whatever you like and remember that being strict in Bash yields code in higher quality, not to mention better readability. Feeling like writing a sloppy script today? – Go home and come back tomorrow.

Following is a script written by the conventions I’m suggesting. This is a very simple script that simply displays a dialog and asks the user which host he would like to ping, then displays the result in a tailbox dialog. This is more or less how many of my scripts look like. I admit it took me a while to find a script which is not too long (under 100 lines) and can still represent some of the ideas I was mentioning.

I urge you to question my way and conventions and suggest some of your own, anyway, here it is:

#!/bin/bash

# dialog options (notice it's read only)
# notice as well it's in capital letters
declare -r DIALOG_OPTS="0 0 0"
declare -i -r OUTPUT_DIALOG_WIDTH=60

# ping timeout in seconds (using declare -i because it's an integer and -r because it's read only)
declare -i -r PING_TIMEOUT=5
# size of pings
declare -i -r DEFAULT_SIZE_OF_PINGS=56
# number of pings to perform
declare -i -r DEFAULT_NUMBER_OF_PINGS=1

# neatly pipes a command to a tailbox dialog
# here we can specify the parameters the function expects
# this is how i like to do it, perhaps there are smarter ways that may integrate better
# with doxygen or some other tools
# $1 - tailbox dialog height
# $2 - title
# $3 - command
pipe_command_to_tailbox_dialog() {
	# parameters extraction, always using $1, with `shift` right afterwards
	# in case you want to play with the order of parameters, just move the lines around
	local -i height=5+$1; shift
	local title="$1"; shift
	local command="$1"; shift

	# need a temporary file? - always use `mktemp`, please spare me the `/tmp/$$` stuff
	# or other insecure alternative to temporary file creations, use only `mktemp`
	local output_file=`mktemp`
	# run in a subshell and with eval, so we can pass pipes and stuff...
	# eval is a favorite of mine, it means you truely understand the Bash shell
	# nevertheless - it is really needed in this case
	(eval $command) >& $output_file &
	# need to obtain a pid? - it's surely an integer, so use 'local -i'
	local -i bg_job=$!

	# ok, show the user the dialog
	dialog --title "$title" --tailbox $output_file $height $OUTPUT_DIALOG_WIDTH

	# TODO my lame way of checking if a process is running on linux, anyone has a better way??
	# my way of specifying a 'TODO' is in the above line
	if [ -d /proc/$bg_job ]; then
		# if the process is stubborn, use 'kill -9'
		kill $bg_job || kill -9 $bg_job >& /dev/null
	fi
	# wait for process to end itself
	wait $bg_job

	# not cleaning up your temporary files is similar to a memory leak in C++
	rm -f $output_file
}

# pings a host with a nice dialog
ping_host_dialog() {
	local ping_params_tmp=`mktemp`
	# slice lines nicely, i have long commands, i'm not going even to mention
	# line indentation - that goes without saying
	# i like to use dialogs, it makes more people use your scripts
	if dialog --ok-label "Submit" \
		--form "Ping host" \
		$DIALOG_OPTS \
			"Address:" 1 1 "" 1 30 40 0 \
			"Size of pings:" 2 1 "$DEFAULT_SIZE_OF_PINGS" 2 30 40 0 \
			"Number of pings:" 3 1 "$DEFAULT_NUMBER_OF_PINGS" 3 30 40 0 2> $ping_params_tmp; then
		# ping_params_tmp will be empty if the user aborted the dialog...
		local address=`head -1 $ping_params_tmp | tail -1`
		# yet again if you expect an integer, use 'local -i'
		local -i size_of_pings=`head -2 $ping_params_tmp | tail -1`
		local -i number_of_pings=`head -3 $ping_params_tmp | tail -1`
	fi
	rm -f $ping_params_tmp

	# this is my standard way of checking if a variable is empty
	# may not be the prettiest way, but it surely catches the eye...
	if [ x"$address" != x ]; then
		pipe_command_to_tailbox_dialog 15 "Pinging host \"$address\"" "ping -c $number_of_pings -s $size_of_pings -W $PING_TIMEOUT \"$address\""
	fi
}

# main function, can't live without it
main() {
	ping_host_dialog
}

# although there are no parameters passed in this script, i still pass $* to main, as a habit
#main $*
# after Uri's comment, I'm fixing the following and calling "$@" instead
main "$@"

I don’t want this post to be too long (it is already quite long), but I think you got the idea. I still have some conventions I did not introduce here. In case you liked what you’ve seen – do not hesitate to contact me so I can provide you with a document describing all of my Bash scripting conventions.

Advertisements

11 responses to “Conventions

Subscribe to comments with RSS.

  1. Good post.

    A comment regarding this line:
    main $*
    What you actually want to use is this:
    main “$@”
    because it handles correctly arguments with spaces and empty arguments.

    You can see it with this script:

    #!/bin/bash

    main() {
    local i=1
    while [ $# -gt 0 ]; do
    echo “$i = ‘$1′”
    shift
    ((++i))
    done
    }

    echo “Using \$*:”
    main $*
    echo “Using \”\$@\”:”
    main “$@”

    Running:
    ./script “1 2” “” 3

    You get:

    Using $*:
    1 = ‘1’
    2 = ‘2’
    3 = ‘3’
    Using “$@”:
    1 = ‘1 2’
    2 = ”
    3 = ‘3’

    The second one is probably what you want.

    • Why didn’t you tell me that all the time we used to work together??? 🙂

      Generally speaking, i discourage using arguments with spaces, but, yeah, I’ll use that in my scripts from now on – I wasn’t aware, to be honest…

  2. Also, there’s nothing lame about checking /proc to see if a process is running. On the contrary, /proc is the “official” programmatic API the Linux kernel provides to the process list, keeping with the Unix “everything is a file” principle. Checks like this are exactly what /proc was designed for.

  3. Dear malkodan,

    i copy your ping script, and process exit :

    TAB key and Enter —> Screen Blank.. why…

    Cant u help me…

    Thanks,
    Denny

  4. Dear Dan,

    My dialog installed, but why ping still process i press tab and Enter my screen blank… hmn….

    help…

    Thanks,
    Denny

  5. Awesome article bro. This is going to get bookmarked.

  6. Hello, I agree there schould be some conventions when writing bash scripts because these could be than very hard to read if no rules are followed…as you wrote: ..scripting like an ape is path to hell:-)). As I am also developer I prefer function based scripting too. even if if in some cases I use case statement which route any activity then to functions as my main block, for example:
    case $1 in
    meta-data)
    MetaData
    exit $BACKUP_SUCCESS
    ;;
    backup-tidevapp)
    StopServices
    CheckMountPoint
    DoBackup tidevapp
    ClearOldBackups
    UnmountRemoteStorage
    StartServices
    exit $BACKUP_SUCCESS
    ;;
    restore)
    Restore
    ;;
    list-backups)
    ListBackups
    ;;
    usage|help)
    Usage
    ;;
    *)
    Usage
    ;;
    esac
    rc=$?
    echo “Script finished with return code = : $rc” # — end of case —

    I would strongly recommend using VIM editor as main IDE for writing and debugging scripts with plugin for BASH support. You can find some useful information at:
    gVIM
    or at vim.org

    This piece of software let you unified any structure in bash script using templates, so every structure will look same (function, documentation, etc)

    Best regards,

    Ladislav

  7. that’s a very good article, thanks for posting it.

  8. “if [ x”$address” != x ]; then” ????

    [ -n “$address” ]

    or:

    [[ $address ]]

    • That’s a good point, both other ways you suggest would work perfectly, however with the conventions I suggest there’s also a bit of my style involved. We can argue whether it is pretty or ugly, but it is solely my style (read the comment above the line).

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: