From Fedora Project Wiki

Revision as of 17:15, 12 March 2009 by Smcbrien (talk | contribs)

The purpose of this page is to provide an easily readable document than the transcript of the Classroom session given on this subject. The example this document works with is about adding a new user, or users, to a machine, and some common customizations around this process.

A very simple shell script

The following has been added to a file, via vi, emacs, gedit, or the reader's preferred text editor. For the purposes of the example, assume the file used is useradd-wrapper.sh

useradd frank
echo mypassword | passwd --stdin frank

At it's simplest, a shell script is a series of commands to execute. You may not have seen passwd's --stdin option before, it allows the passwd command to take the target password to assign to a user over stdin or through an input redirection operator like a |. So in the example above, a user named frank is added to the system, then has an initial password set to mypassword.

Invoking a shell script

There are several ways that we can execute the above script. We could:

bash useradd-wrapper.sh

Using this method, we're invoking the bash shell and using it to execute the commands stored in the useradd-wrapper.sh script.

Another alternative, which is more popular, is to make the script executable and using the script name as the "command" to use on the shell commandline. So something like:

chmod a+x useradd-wrapper.sh

in order to make the file executable and

./useradd-wrapper.sh

to execute the instructions stored within the script.

The last alternative I'll bring up is running the script without the ./ at the beginning of the command. The problem is that usually the current directory (.) is not included on the PATH variable, which is the list of directories in which to search for executables. Fedora, however, includes a directory in each user's home directory on their default PATH built at login. ~user/bin, or in other words, in the user's home, the subdirectory bin/ is included by default. Our example uses useradd and other commands which are run as root, so for our example, we could move the useradd-wrapper.sh script to the /root/bin directory. If the file has already been made executable with the previously mentioned chmod command, we would now invoke the script as simply:

useradd-wrapper.sh

Some simple enhancements (#!, Comments, and Positional Parameters)

Lets make some quick enhancements to our script.

#!/bin/bash

useradd $1
echo mypassword | passwd --stdin $1

# Invocation ./myscript.sh frank

The first change is the addition of the #!/bin/bash line. The significance of the #! line is that it indicates what interpreter, or language, to use when executing the commands in this script. If you've worked with perl, python, or another shell script language, you've likely seen a line like this. The issue that this fixes is that a bash script will be invoked, even if the shell running the script from the command line is different. The #! should be the first line in the script. Additional lines that begin with # will be interpreted as comments. We see a comment line as the last line in this script.

The other major change to our simple script has been the addition of a variable to replace the username we want to work with. Variables for our use within the script are created upon execution. We'll see a few of these as the document progresses, but above we're using $1. $1 represents the first argument passed to the shell script from the command line, so as the comment as the bottom of the file suggests, our command line would now include the script to execute and the user we wish to add and set the password for. Other positional parameters of note. $0 stores the name of the script called, $2 the second argument passed, $3 the third argument, ...

Quoted Execution

Next we're going to execute a command from our script who's output we wish to store, rather than display.

#!/bin/bash

useradd $1

password=`mkpasswd`

echo $password | passwd --stdin $1

# Invocation ./myscript.sh frank

Quickly, a sidebar about mkpasswd. Very likely it's not installed on your machine. The expect package provides the mkpasswd command. The mkpasswd command generates randomized strings of upper and lower case characters, digits, and special characters. The length of the string, selections of how many of each type of character, can be customized with options to the mkpasswd command.

What we're doing above is invoking a sub-shell, within that shell run mkpasswd, then take the output of mkpasswd (the randomized string) and store it in a newly declared variable, password. Later we change the command where we call passwd to pass it the random string rather than the static string "mypassword". I know there's currently a problem with this. If the password is set to a random string, how do we see it? We will fix this later when we talk about notifying the user of their new account.

One other note about quoted execution, above we're using the character to open and close the executed command. Another way we could express this is $(mkpasswd) Both methods work equally well, I prefer since it is easier to type, though I've been told that without a US keyboard $() is easier to write into a script.

Adding Choices with if

Shell script has evolved over the years into a programming language, which means it has conditional statements. In this section we're going to see the first use of an if statement.

#!/bin/bash

useradd $1

if <SOME COMMAND IS SUCCESSFUL>
then
  password=`mkpasswd`
  echo $password | passwd --stdin $1
fi

# Invocation ./myscript.sh frank

One of the problems with our script so far is that if the user already exists, the useradd fails, yet we would still continue to overwrite their password with a string of random characters. An if statement will allow us to run a command and look at the exit value of that command. If the command runs successfully, the command sets an exit value of 0 and the if statement's check will be successful and the internal commands will be executed. If the command is unsuccessful, the command returns a non-zero value on exit and the if command will be interpreted as unsuccessful, which causes the if statement's internal commands to be skipped. The if's conditional commands are included between the then and fi commands. Lets fill in the the loop to completeness:

#!/bin/bash

if useradd $1
then
  password=`mkpasswd`
  echo $password | passwd --stdin $1
fi

# Invocation ./myscript.sh frank

So at this point, our script tries to add the user, and if successful, will then set their password to a randomized string of characters. A little further down we'll run a test to see if the user exists as our if condition rather than checking the exit code from an attempted useradd.

Writing the iterative for loop

The idea behind the for loop is that you want to iterate through the loop's commands once for each item in the list of space separated values. Each time the loop is to be executed, the current item in the list to be iterated will be assigned to be the value of the placeholder variable. The commands to be executed for each iteration are bound by the do and done commands.

#!/bin/bash

for <PLACEHOLDER VARIABLE> in <SPACE SEPARATED LIST OF VALUES>
do

  if useradd $1
  then
    password=`mkpasswd`
    echo $password | passwd --stdin $1
  fi

done

# Invocation ./myscript.sh frank

The reason we're looking at this looping structure is that currently our script accepts only one user as an argument of a person to add, and we're going to use the for loop to change things such that we can provide multiple users on the command line of the script.

Filling in the details for the loop contents, one method would be something like:

#!/bin/bash

for user in $1 $2 $3 $4 $5
do

  if useradd $user
  then
    password=`mkpasswd`
    echo $password | passwd --stdin $user
  fi

done

# Invocation ./myscript.sh frank joe sally ...

In the above script, we'd be limited to no more than 5 users as arguments. So in the next example we're going to change the list of positional parameters to a special shell variable, $*, which will contain all arguments passed to the script. If the arguments were bill frank joe, then the $* variable would contain the string "bill frank joe".

#!/bin/bash

for user in $*
do

  if useradd $user
  then
    password=`mkpasswd`
    echo $password | passwd --stdin $user
  fi

done

# Invocation ./myscript.sh frank joe sally ...

Adding more error checking

Checking correct command invocation

Thusfar, our script hasn't handled erroneous user input very well. What happens when they call the script, but neglect to provide any users to add? Below we add an if statement wrapping the whole script to check and see if arguments are passed. If arguments are not passed, the user will fall into an else branch and receive a usage error message.

#!/bin/bash

if [ -n "$*" ]
then

  for user in $*
  do

    if useradd $user
    then
      password=`mkpasswd`
      echo $password | passwd --stdin $user
    fi

  done

else
  echo "Usage:  $0 <new username> <new username> <new username> ..."
  exit 7
fi

So several new bits here. The first is our syntax of what is provided to if. There is a command called test where you can test a condition. If the condition is true, test returns a 0, or success, and the contents after then will be executed. If the condition is false, test will provide a non 0 and the contents after then will be skipped. bash also includes a command [ which operates in the same way as test. The expression [ -n "$*" ] is checking to see if the contents of $* are non-zero. If $* has non-zero contents, ie it contains arguments passed via the command line, then we'll execute the commands after then.

The second new item is the use of else. In the event that the condition tested in the if is false, the then branch is skipped, but if there is an else branch it's contents will be executed. So in this example, we check to see if the script is invoked with command line arguments, if it is not, we provide a usage message and exit the script. The significance of exit is that not only will it terminate the script from running any additional commands, but it will also set an exit value. Remember that from earlier an exit value of 0 signifies a correct execution of our program, where an exit of a non-zero value indicates an error of some type occurred. Now, if we were to get a complaint from someone about our program, if they tell us that the exit value is 7, we know it's because they're running the program without providing any command line arguments.

Changing our check for adding the user

Earlier I promised a better check before we added the user account:

#!/bin/bash

if [ -n "$*" ]
then

  for user in $*
  do

    if ! gentent passwd $user
    then

      useradd $user
      password=`mkpasswd`
      echo $password | passwd --stdin $user

    else
    echo "$user already exists"
    fi

  done

else
  echo "Usage:  $0 <new username> <new username> <new username> ..."
  exit 7
fi