<<< Back to Tips Index

13 Nov 2015

Shifting through Parameters

Using the shift command to work through command-line arguments

Is it 'parameters' or 'arguments'? Those words that you add to a command: "ls -l foo bar". For the purposes of this article, I'll use them both equally. Technically, "-l" is a switch, and foo and bar are arguments. Or possibly parameters. Anyway, this post is about the much simpler scripts one sometimes needs to write, which don't merit all the glory of getopts, and just take some information from the command line.

The shift command is a very useful one for getting rid of command-line arguments once they have been parsed.

For example, if you want to write a script which will take a host, username and password (which might contain spaces), you might write this:

#!/bin/bash
HOST=$1
USERNAME=$2
PASSWORD=$3
echo "Host is $HOST ; Username is $USERNAME ; Password is $PASSWORD"

However, if it was called with arguments of "node27 appuser foo bar", then you might expect that appuser on node27 would get a password of "foo bar". We know that hostnames can't contain spaces, and usernames don't tend to contain spaces, either, but a password could well contain a space. Disappointingly, this script would say:

Host is node27 ; USERNAME is appuser ; PASSWORD is foo

It missed the final word. So you can update your code like this:

#!/bin/bash
HOST=$1
USERNAME=$2
shift 2
PASSWORD=$@

Here, we grab the first two arguments - the hostname and the username, and by running "shift 2", we ditch those two (shift 2) parameters, and the $PASSWORD variable gets set to whatever arguments are left on the commandline - in this case, it's "foo bar". That way, it doesn't matter how many spaces are in the password, we don't have to say "PASSWORD="$3 $4 $5 $6"" and hope that there aren't more than three spaces in the password, we just say that all remaining arguments (which is what "$@" gives us) comprise the password.

So that's good, and a useful trick to know.

However.

What if you call this script with just "node27"?. Or as "node27 appuser"?

The shift command will try to push two arguments off the command-line. If it can't do that, because there aren't enough in the first place, then it just won't do anything at all.

At first, everything looks fine:

$ ./shift.sh node27 appuser foo bar
Host is node27 ; Username is appuser ; Password is foo bar
$ ./shift.sh node27 appuser foo
Host is node27 ; Username is appuser ; Password is foo
$ ./shift.sh node27 appuser
Host is node27 ; Username is appuser ; Password is 
$ 

However, if you drop off the username, then there is only one argument, "$1", the hostname. The shift command lets us down badly here:

$ ./shift.sh node27
Host is node27 ; Username is  ; Password is node27
$ 

Although the USERNAME variable is - correctly - undefined, somehow the third, PASSWORD variable, has picked up the value of "$1". What is happening?!

Because "shift 2" failed to shift two arguments - there weren't two arguments to shift - it simply failed to do anything at all. So setting "PASSWORD=$@" meant that PASSWORD got set to the original set of arguments to the script, which was just the host name.

To get around this, if you want to shift by a specific number of arguments, then call shift each time:

#!/bin/bash
HOST=$1
USERNAME=$2
shift  # drop the HOST
shift  # drop the USERNAME
PASSWORD=$@  # PASSWORD is whatever remains

This works as expected:

$ ./shift.sh node27 appuser foo bar
Host is node27 ; Username is appuser ; Password is foo bar
$ ./shift.sh node27 appuser foo
Host is node27 ; Username is appuser ; Password is foo
$ ./shift.sh node27 appuser 
Host is node27 ; Username is appuser ; Password is 
$ ./shift.sh node27 
Host is node27 ; Username is  ; Password is 
$ 

A single call of shift will always drop the first argument; the bash man page says: "If n is greater than $#, the positional parameters are not changed." (where $# is the number of arguments passed to the script, and n is the argument passed to shift).

If you wanted to drop a large (or variable) number of arguments, then you could always use a loop:

#!/bin/bash
HOST=$1
USERNAME=$2
SKIP_COUNT=$3
echo "Host is $HOST ; Username is $USERNAME ; Skip Count is $SKIP_COUNT"
for x in `seq 1 $SKIP_COUNT`
do
  echo "Skipping something ($1) ..."
  shift
done
echo "Anything left is: $@"

This will skip the number of variables told in argument 3:

$ ./shift-some.sh node27 appuser 5 one two three four five six seven eight
Host is node27 ; Username is appuser ; Max Skips is 5
Skipping something (node27) ...
Skipping something (appuser) ...
Skipping something (5) ...
Skipping something (one) ...
Skipping something (two) ...
Anything left is: three four five six seven eight
$ ./shift-some.sh node27 appuser 2 one two three four five six seven eight
Host is node27 ; Username is appuser ; Max Skips is 2
Skipping something (node27) ...
Skipping something (appuser) ...
Anything left is: 2 one two three four five six seven eight
$ ./shift-some.sh node27 appuser 7 one two three four five six seven eight
Host is node27 ; Username is appuser ; Max Skips is 7
Skipping something (node27) ...
Skipping something (appuser) ...
Skipping something (7) ...
Skipping something (one) ...
Skipping something (two) ...
Skipping something (three) ...
Skipping something (four) ...
Anything left is: five six seven eight
$ 

Invest in your career. Buy my Shell Scripting Tutorial today:

 

Steve's Bourne / Bash shell scripting tips
Share on Twitter Share on Facebook Share on LinkedIn Share on Identi.ca Share on StumbleUpon