Shell Scripts with Multiple Arguments
In a business or personal relationship, having multiple arguments is generally unpleasant and therefore to be avoided. However, in the case of the Linux shell, having multiple arguments is downright handy. Of course, in the Linux world, the word argument does not refer to a dispute; instead, it refers to a word appearing on the command line following the name of a program or script. Shell scripts that process multiple arguments afford economy and ease of use; you can simply type a command name once and have that command operate on an entire series of arguments. So this month we'll look at incorporating this capability into a home-brew script.
Here's a simple shell script that demonstrates the use of multiple arguments:
echo $1 are not $2
If you create a file named simple that contains this script and then execute it like this:
sh simple dogs cats
you'll see the output:
dogs are not cats
Let's examine how this shell script works. The echo command simply prints the value of each of its arguments. If you read the June Newbies column dealing with environment variables (located online at http://www.linux-mag.com/2001-06/newbies_01.html), you may recognize that $1 and $2 are variable names. They can be recognized as such due to the dollar sign that appears as the first character of each name.
More specifically, $1 and $2 are known as shell variables, because the shell sets their value whenever a shell script or command is invoked. The variable $1 holds the first argument (if any) of the command; the variable $2 holds the second argument (if any) of the command.
So, in the previous example, the variable $1 has the value dogs, and the variable $2 has the value cats. Thus, the output of the echo command is dogs are not cats.
Suppose the script had read:
echo $1 are not $3
Notice that the variable named $2 has been replaced by one named $3. If you issued the command:
sh simple dogs cats birds
you'd see the output:
dogs are not birds
The variable named $3 has the value of the third argument, birds. The value of the second argument, cats, is not referenced by the script.
Ready for a curve ball? Try creating this script:
echo $1 are not $10
Notice in this script that the second variable is $10. If you were to then issue the command:
sh simple dogs cats birds ants flies worms skunks turtles fish ocelots
you'd see the output:
dogs are not dogs0
You probably expected to see:
dogs are not ocelots
The reason for the unexpected output is that the shell defines only nine positional arguments, $1 through $9. The shell interprets the token $10 as $1 followed by the digit zero. Hence, the output is dogs0.
Table One summarizes the shell variables related to arguments, including several variables not yet introduced. Among these is $0, which basically spits back the name of the script or command invoked.
For example, place the following script in a file named me:
Then, issue the command:
The output will consist of the name of the script file:
The value of the shell variable $# is the number of script arguments. For example, place the following script in a file named counter:
If you invoke this script like so:
sh counter 1 2 3
you'll see the output 3; if you invoke the script like so:
sh counter 1 2 3 4 5
you'll see the output 5.
The shell variables $@ and $* are quite similar. Each provides the values of the script arguments. However, $@ provides the value of each argument, whereas $* provides a single value that consists of each shell argument, separated by a space. To witness this distinction firsthand, use the counter script given earlier and construct a new script, tester, that references it:
sh counter "$@"
If you invoke the new script like so:
sh tester 1 2 3
you'll see the output:
The output shows that $@ returns three argument values, whereas $* returns only one.
Okay, now that we've covered the basics of handling script arguments, let's see if we can do something useful with them.
One Microsoft Windows feature for which Linux newbies often yearn is the recycle bin. You can obtain Linux applications that provide this feature, such as Deltrree (see http://migas.mine.nu/en_deltree_index.shtml). However, in the true spirit of Linux, we will create our own.
To do so, let's create a script named del that has these contents:
mkdir -p /tmp/$USER/del 2>/dev/null
Here's what the script does. The first command, mkdir, creates a subdirectory of /tmp. This is named after the user running the script. The -p argument instructs mkdir to make a second subdirectory within the first one. The second subdirectory is named del, after the name of the script. The del directory is the recycle bin for its associated user.
The redirection, 2>/dev/null, suppresses any error messages generated by mkdir. This is necessary because the second and subsequent times that the script is run, the subdirectories will already exist. Attempting to create an existing directory results in an error message, which is best suppressed in order to avoid confusing the user.
The second command, the mv command, moves the file whose name is given as the first script argument. The file is moved to the directory created by the mkdir command on this, or a previous, invocation of the script.
You can use this script to non-irrevocably delete a file. For instance, suppose you're tired of the file movies. txt. If you issue the command rm movies.txt, the file is gone forever. Instead, you can issue the command del movies.txt, which moves the unwanted file to your recycle bin. Files in the /tmp directory are usually permanently deleted after an interval established by the system administrator. Until a file is deleted, you can recover it simply by using mv to move it out of the /tmp directory tree.
Okay, a few important words and warnings are in order here. First, you won't be able to execute the del script in the simple fashion indicated unless you give yourself execute access to the script and place the script in a directory that's on your execution path. Previous articles in this series include instructions on how to do this. A workaround is to issue the command sh del, where del is the path to the del script.
Second, this script won't allow you to recycle files that you delete via a GUI application, such as GNOME's file manager. The script does its work only if you invoke it.
Third, bear in mind that files don't remain in /tmp indefinitely; at some point, they will be automatically deleted. Several Linux distributions use the tmpwatch program to delete temporary files older than 10 days.
Fourth and finally, the del script doesn't distinguish identically named files originally residing in distinct directories. For example, if you delete /x/z by using del, you will overwrite any saved copy of /y/z. You can overcome this limitation with a little extra code, if you like. However, use del at your own risk. I don't plan to respond to e-mails concerning lost files (grin).
Handling More Than Nine Arguments
Because the shell provides only nine variables representing arguments, you might conclude that it's impossible to access a tenth or subsequent argument. However, that'd be a hasty conclusion. The shift command solves this particular problem. When the shell executes the shift command, it discards the value of $1; it then shifts the value of $2 into $1, the value of $3 into $2, and so on. The value of the tenth argument ends up in $9. Here's proof. Place the following script in a file named tener:
and execute it like this:
sh tener 1 2 3 4 5 6 7 8 9 10
The output should be the value of the tenth argument, 10.
Handling Many Arguments
As convenient as shell arguments are, the shell cannot infinitely handle many arguments. Early versions of the Unix shell were quite restrictive in this regard. Today's BASH shell is more powerful, but if you attempt to feed the shell 10,000 arguments, you may find that it fails to fully cooperate. As you might expect, it is possible to circumvent this limitation.
One technique is to use xargs, a program that runs a specified command on files whose names appear in the program's input stream. Because the file names are sent via the input stream rather than the command line, this technique escapes limitations that restrict the size of the command line or the number of command-line arguments.
As an example, consider how we might re-implement whatsit using xargs. First, let's use lsto generate a list of files in the current directory and send the list to the standard output device:
Now, let's send that result to the xargs command:
ls | xargs
Next, let's specify an argument telling xargs what operation to perform:
ls | xargs whatis
Finally, let's filter and paginate the output as before:
ls | xargs whatis | grep -v 'nothing appropriate' | more
The result works like the original whatsit except that it won't melt down if the current directory contains many files.
The xargs command interprets several useful options. Table Two summarizes them. One of the most useful of xarg's arguments is --interactive. When this option is specified, xargsprompts for user confirmation before executing the specified command.
As an example of using this option, consider the following script:
ls |grep '.*bak'|xargs --interactive --max-lines=1 rm
This script uses ls to list the names of files in the current directory. Then it uses grep to remove lines pertaining to files other than those having names ending with bak. Finally, xargs is used to interactively delete these backup files. The user must respond y in order for a file to be deleted. If you try this example, be sure to do so in a directory that contains no valuable files. A slight error in typing can result in the deletion of many important files.
Okay, now you know how to have multiple arguments without ever losing a single one. This should make you the envy of debaters everywhere. Next month, we'll consider conditional statements, which let you construct scripts that make decisions.
Until then, happy arguing!