Bash shift builtin command

Updated: 03/06/2020 by Computer Hope
shift command

On Unix-like operating systems, shift is a builtin command of the Bash shell. When executed, it shifts the positional parameters (such as arguments passed to a bash script) to the left, putting each parameter in a lower position.

Description

When you run shift, the current positional parameters are shifted left n times. Positional parameter x is given the value of parameter x+n. If parameter x+n does not exist, parameter x is unset.

If not specified, the default value of n is 1. So the commands "shift 1" and "shift" (with no argument) do the same thing.

If a parameter is shifted to a position with a number less than 1, it "falls off" — its value is discarded. So the command shift always discards the previous value of $1, and shift 2 always discards the previous values of $1 and $2.

The special positional parameter $0 is excluded from all shift operations, and never modified by the shift command.

Syntax

shift [n]

The shift command takes only one argument:

n The number of positions that parameters should be shifted to the left. This value can be any non-negative integer. If n is zero (0), no parameter shift will be performed. The default value of n is 1.

Exit status

When shift exits, it returns an exit status of 0 (no error), unless: n is negative, or n is greater than the current number of positional parameters, in which case exit status is nonzero.

Positional parameters in bash

In bash, whenever a command is executed, the environment for that command includes special variables containing the individual parameters given to the command.

The name of these variables is a number, corresponding to the position of that parameter on the command line.

For instance, consider the following command:

mv file_old.txt file_new.txt

This command has three positional parameters, numbered as follows:

Parameter Position Description
mv 0 The mv command, which moves files. In this case, it renames file_old.txt to file_new.txt by "moving" it to the new file name.
file_old.txt 1 The original file name.
file_new.txt 2 The new file name.

The first parameter, 0, contains the name of the command. If there are no arguments, this will be the only positional parameter.

When arguments are included after a command, each is stored in the shell variables named 1, 2, etc. Like any other shell variables, their value can be referenced by putting a dollar sign before the variable name. So the value of positional parameter 1 can be referenced with $1, the value of parameter 2 can be referenced with $2, etc.

Parameters with a number 10 or greater can be referenced by putting the number in brackets, for example ${10}, ${11}, or ${12345}.

Bash keeps track of the total number of positional parameters. This number is stored in the special shell variable $#.

The value of $# decreases by n every time you run shift.

Special positional parameter zero

Positional parameter zero (0) contains the name of the command that was used to start the current process, as it was executed. You can always find out what command was used to launch the current process by checking the value of this variable.

For instance: from a bash prompt, you can run echo $0 to see the command that launched your current bash session:

echo $0
/bin/bash

This is the location of the bash executable.

There are different ways to run an executable file, however — for instance, many programs are executed using a symlink (symbolic link). If you run a command using a symlink, $0 contains the name of the link you used.

For instance, you can create a symlink to bash in your home directory:

ln -s /bin/bash ~/mybash

And make the symlink executable, using chmod:

chmod u+x ~/mybash

Then, you can start a new bash child process by executing the symlink:

~/mybash

You're now in a new bash shell, inside your previous shell. If you run echo $0, you'll see the difference:

echo $0
/home/hope/mybash

This is the complete path to your home directory, as expanded by bash when you used the alias ~ (a tilde). You can type exit now, to return to your original bash shell.

Examples

Shifting parameters

Let's create a script that accepts arguments. We'll look at how arguments are stored in the environment as positional parameters, and how shift affects them.

Create a new script with your favorite text editor, such as pico or vim. The script should look like this:

#!/bin/bash
shift 0
echo 0: $0
echo 1: $1
echo 2: $2
echo 3: $3
echo 4: $4

The first line, which begins with #! (a "shebang") defines which shell program should be used to run the script.

The second line ("shift 0") does nothing — yet. After we run the script like this, we'll change 0 to another number.

Now, save this to a file called myargs.sh, and exit the text editor.

Make myargs.sh executable:

chmod u+x ./myargs.sh

Then run it, and give it some arguments:

./myargs.sh one two three four five
0: ./myargs.sh
1: one
2: two
3: three
4: four

In this output, we can see the values of positional parameters 04. (The value of $5 is "five", but we aren't using that value in our script.)

These values are specific to our current command — the script. When the script is done, these values revert to their values before the script was run. So now, at the command prompt, you can run:

echo $0, $1, $2, $3, $4.
/bin/bash, , , , .

These are the values for our bash session. $0 is the location of the bash executable, and the other parameters have no value.

Now let's change shift 0 to shift 1. Open the script in your text editor and change the second line so the script looks like this:

$!/bin/bash
shift       # same as shift 1
echo 1: $1
echo 2: $2
echo 3: $3
echo 4: $4

Everything after the hash mark ("#") on line 2 is a comment, and is ignored when the script runs.

Save the file and exit the text editor. Then, run the script again:

./myargs.sh one two three four five
0: ./myargs.sh
1: two
2: three
3: four
4: five

Notice that the position of all the parameters are shifted down by one. The original value of $1 was discarded, and the value of $5 is now at $4. The value of $0 has not changed.

But, running shift only once isn't very useful. Usually, you will want to run it several times, in a loop. We can re-write the above script as a loop, where each iteration shifts the parameters once. In your text editor, change the script to look like this:

#!/bin/bash
for (( i = 0; i <= 4; i++ ));
do
  echo Shifted $i time(s):
  echo -----------------
  echo 1: $1
  echo 2: $2
  echo 3: $3
  echo 4: $4
  echo
  shift
done

Notice that shift is the last command in the loop. This allows us to do something to the parameters before they are shifted. Then, as a final step, we shift the parameters and run the loop again.

Save your changes, exit the text editor, and run the script. It will loop five times (with i incrementing from 0 to 4, once per loop). Each loop will shift the values once, and echo the values 04 to the terminal.

./myargs.sh one two three four five
Shifted 0 time(s):
------------------
1: one
2: two
3: three
4: four
Shifted 1 time(s):
------------------
1: two
2: three
3: four
4: five
Shifted 2 time(s):
------------------
1: three
2: four
3: five
4:
Shifted 3 time(s):
------------------
1: four
2: five
3:
4:
Shifted 4 time(s):
------------------
1: five
2:
3:
4:

Now that we've seen how parameters are shifted, let's create a script with a practical purpose.

Real-world example

The following script, clean-old-files.sh, accepts a list of directory names as parameters on the command line. It scans each named directory for any files that haven't been accessed in more than a year, and deletes them.

#!/bin/bash
# Scan directories for old files (over 365 days) and delete them.
USAGE="Usage: $0 dir1 dir2 dir3 ..."
if [ "$#" == "0" ]; then                 # If zero arguments were supplied,
  echo "Error: no directory names provided."
  echo "$USAGE"                          # display a help message
  exit 1                                 # and return an error.
fi
while (( "$#" )); do        # While there are arguments still to be shifted
  while IFS= read -r -d $'\0' file; do
    echo "Removing $file..."
    rm $file
  done < <(find "$1" -type f -atime +365 -print0)
  shift
done
echo "Done."
exit 0

Let's look at the individual parts of this script, and analyze what they do:

USAGE="Usage: $0 dir1 dir2 dir3 ..."
if [ "$#" == "0" ]; then
  echo "Error: no directory names provided."
  echo "$USAGE"
  exit 1
fi

In this if conditional statement, the script checks if there are any arguments. The test uses single-bracket notation ( [] ), which is equivalent to using the builtin command test. The special shell variable $# holds the total number of positional parameters. If its value equals zero, that means the user didn't provide any directory names, so we echo a help message and terminate the script with an error (exit status 1).

while (( "$#" )); do

Otherwise, we proceed to the outer while loop, which uses double-parentheses evaluation to see if $# is true — that is, if its value is anything other than zero. Every time the loop starts, if $# is zero, the expression evaluates as false and the loop exits.

Tip

The expressions (( "$#" )) and [ "$#" != "0" ] give equivalent results, and are functionally interchangeable.

Next, let's look at both sides of the inner while loop:

  while IFS= read -r -d $'\0' file; do

This line says: while there are read items that are delimited ( -d ) by a null character ( $'\0' ), read one item into the variable file, then perform the commands inside the loop.

The items are provided by the line at the end of the loop:

  done < <(find "$1" -type f -atime +365 -print0)

This line says: find all files, beginning the search in the directory dir1 (in positional parameter $1), that were last accessed (-atime) more than ( + ) 365 days ago. Delimit the list of matching file names with null characters (-print0).

The find command is enclosed in <(), which uses process substitution to treat the output as if it's a file. The contents of this "file" are redirected ( < ) to the inner while loop. There, read interprets everything up to a null character as the next file name, and assigns this name to the variable file. Then, the inside of the loop is executed:

    echo "Removing $file..."
    rm $file

Which prints the file name and removes the file.

The inner loop continues until there are no more file names, so read returns false. The inner loop exits to the outer loop, where, we...

  shift

shift the positional parameters, so that dir2 ($2) is now dir1 ($1). The old $1 is discarded, and $# is automatically decremented by 1.

done

Go back to the beginning of the outer while loop. If there are no more positional parameters, the test (( "$#" )) returns false, the outer loop exits, and...

echo "Done."
exit 0

We exit with success (exit status 0).

Running the script looks like this:

./clean-old-files.sh mydir1 mydir2
Removing mydir1/subdir1/Netscape-Navigator-1.0.zip...
Removing mydir1/subdir2/my-soon-to-be-finished-novel.rtf...
Removing mydir2/subdir1/grunge-the-new-phenomenon.doc...
Removing mydir2/subdir2/my-geocities-page.htm...
Removing mydir2/subdir3/half-life-2-leaked-beta.rar...
Done.

getopts — Parse arguments passed to a shell script.