5

I am taking the Linux Essentials class and was doing well until I hit the scripting chapter. I simply do not understand these concepts. Wondering if someone can break the following down into ultra simplistic terms or point me to a better reference to learn it. I'm currently using netacad's curriculum.


From the text book (with minor formatting changes):

There are some special variables in addition to the ones you set. You can pass arguments to your script:

#!/bin/bash
echo "Hello $1"

A dollar sign followed by a number N corresponds to the Nth argument passed to the script. If you call the example above with ./test.sh the output will be Hello Linux. The $0 variable contains the name of the script itself.

After a program runs, be it a binary or a script, it returns an exit code which is an integer between 0 and 255. You can test this through the $? variable to see if the previous command completed successfully.


I understand how to assign variables and how they work with the $ but the whole issue with $0 and $1 -- I just don't get it.

Any help would be much appreciated.

  • Why not create a bash script and have it echo values like $0, $1 in a way that allows you to view how it's processed (eg. echo \'$0\' \"$1\" and then run with different values and see for yourself). – guiverc Jun 19 at 4:05
  • 3
    The description from the book is wrong (or at least missing something). To get that script to print "Hello Linux", you'd run it with ./test.sh Linux. If you ran it with ./test.sh foo bar baz, then $0 would be "./test.sh", $1 would be "foo", $2 would be "bar", and $3 would be "baz". – Gordon Davisson Jun 19 at 4:09
7

The description from the book is wrong (or at least missing something). To get that script to print "Hello Linux", you'd run it like this:

./test.sh Linux

If you run it with just ./test.sh, then it'll only print "Hello ", because there was no first argument and $1 is not defined. On the other hand, suppose you ran it like this:

./test.sh foo bar baz

then within the script, $0 would be "./test.sh", $1 would be "foo", $2 would be "bar", and $3 would be "baz".

As for $?, consider the following script snippet:

ls nonexistentfile.txt
echo "The exit status of ls was: $?"
echo "The exit status of echo (the first one) was: $?"

When run that'll print something like:

ls: nonexistentfile.txt: No such file or directory
The exit status of ls was: 1
The exit status of echo (the first one) was: 0

The ls command can't list nonexistentfile.txt ('cause it doesn't exist), so it prints an error message to that effect, and exits with a nonzero status to indicate that something went wrong. The first echo command prints that exit status ($?), and since it does that successfully, it exits with a status of zero. When the second echo command runs, it gets $? from the first echo command, so it prints "0".

BTW, a lot of commands just use exit statuses of 0 (success) or 1 (some sort of failure), but some use different failure statuses to indicate exactly what went wrong. Here's an excerpt from the curl manual page:

EXIT CODES
       There are a bunch of different  error  codes  and  their  corresponding
       error  messages  that  may appear during bad conditions. At the time of
       this writing, the exit codes are:

       1      Unsupported protocol. This build of curl has no support for this
              protocol.

       2      Failed to initialize.

       3      URL malformed. The syntax was not correct.

       ...

       88     FTP chunk callback reported error

       89     No connection available, the session will be queued

       90     SSL public key does not matched pinned public key

       91     Invalid SSL certificate status.

       92     Stream error in HTTP/2 framing layer.

...so a script that used curl could check $? to figure out what went wrong, and respond differently depending on the problem.

4

$0 is the name you use to run the script. $1, $2, and so forth are the script's positional parameters, which hold the values of the command-line arguments you passed when you ran the script.

As Gordon Davisson said, the book's author must've meant to say that running ./test Linux would print Hello Linux. When you do that, ./test goes into to the special parameter 0, and Linux goes to into the first positional parameter 1. The script expands that first positional parameter by preceding it with a dollar sign ($1), just as you do with variables. If you had instead run ./test Hello Linux for Human Beings, then in the script, $1 would expand to Linux, $2 to for, $3 to Human, and $4 to Beings.

You can write a simple script to try this out:

#!/bin/bash

echo "\$0 expands to '$0'."
echo "\$1 expands to '$1'."
echo "\$2 expands to '$2'."
echo "\$3 expands to '$3'."

(Go on as far as you want. For positional parameters higher than 9, use the ${ } form of parameter expansion, e.g., expand 10 by writing ${10}. In scripts that work with many positional parameters, the special parameter @ is often used, avoiding repetition, but you can ignore that for now if you like.)

Try saving that in a file and marking the file executable, which you can do by running chmod +x simple where simple is replaced with the name of the file, if different. Then you can run it using commands like ./simple, ./simple foo, ./simple foo bar, and so on.

You'll notice that when fewer than three command-line arguments are passed, positional parameters that correspond to the ones that weren't passed expand to the empty string. That's what happens when you attempt to expand a shell parameter that is not defined. You'll notice, further, that when more command-line arguments are passed, the ones past the third are not used. That's probably what you'd expect, since the script doesn't refer to them at all.

Now try running ./simple *. The shell expands * to all the filenames in the current directory except those that start with ., so three of those will be shown as the first three positional parameters (or fewer if there aren't that many). You can try running it with other shell expansions, such as ./simple {5..10}.

You can pass command-line arguments containing blanks by enclosing them in quotes. For example, try ./simple 'foo bar' baz. Notice that $1 expands to foo bar this time, and not just to foo.

Because the shell performs various expansions, it's not always obvious how many command-line arguments you're passing to a command. An easy way to see what each argument will be is to replace the command with printf '[%s]\n'. For example:

$ printf '[%s]\n' f*
[fonts]
[fstab]
[fuse.conf]
[fwupd]
$ printf '[%s]\n' {1,3}{a..c}
[1a]
[1b]
[1c]
[3a]
[3b]
[3c]

Since you've only recently started shell scripting, the Bash reference manual may be challenging, and you might not want to read it from front to back. But I think it's a valuable resource even if you consider yourself a complete beginner. You may find the section on shell parameters useful, since it starts with what you already know--shell variables--and moves on to special parameters like ? (which people often call the $? parameter, since that's how you expand it). For general learning about Bash, especially at a more introductory level, I recommend these pages, including BashGuide.

1

One good book you should know about is William Shotts's "The Linux Command Line", published by No Starch Press and available as a free pdf on the author's website.

In every shell script, there is a collection of numbered variables, which are generally referred to as $1, $2, etc. These are the "positional parameters", more commonly known as the command line arguments. You can think of these as variables named 1, 2, etc. and to get their values, you would use $1, $2, etc. When you call a script named my_script via the command line ./my_script a b c, it would get three arguments which are stored in the three variables $1, $2, and $3. You cannot assign to these variables (except as a group), but you can examine and use their values. For example, echo "$1" would print the first argument to your script.

$0 is a little unusual; it is the name by which the script you are running was called. In the case above, it would have the value ./my_script. Again, you can see it's value but not change it.

$? is the "exit status" of the command that just ran. If the command succeeded, then its exit status will be 0 and otherwise it will be a small positive integer. You can compare $? to zero to see if the previous command succeeded or failed. For example, the following two command lines will run the grep command and then echo <1> because the grep failed and exited with a status of 1 (indicating that it failed).

grep blarg /etc/passwd
echo "<$?>"

The exit status is helpful in writing simple scripts like:

#!/bin/bash
# grep_pw: grep for a pattern in /etc/passwd
grep "$1" /etc/passwd
if [[ $? = 0 ]] ;then
  echo "Found it"
  exit 0
else
  echo "Unable to find the pattern '$1' in /etc/passwd"
  exit 1
fi

Put that text into a file named grep_pw, change it to be executable with chmod 700 grep_pw, and call it like ./grep_pw nologin to find the lines in /etc/passwd which contain the string nologin.

When I first learned about the shell, I found the following script invaluable for understanding how the shell parses command lines and consequently what command line arguments would be passed to a script.

#!/bin/bash
# echoargs: print all the arguments
counter=1
for a in "$@" ;do
   echo "arg $counter=<$a>"
   let counter=counter+1
done

Put those contents into a file named echoargs, change it to be executable with chmod 700 echoargs and call it like: ./echoargs a "b c" d.

  • I like this answer because it covers everything the OP asked about, with useful details, but in a compact way. I find that it's often hard to give compelling examples for $?, though, since the special parameter ? it shouldn't actually be used all that often. The first two (non-comment) lines of grep_pw could (and typically should) simply be combined into if grep "$1" /etc/passwd; then. – Eliah Kagan Jun 20 at 19:42
  • 1
    @EliahKagan Thanks. I agree with you about grep_pw, but I didn't want to complicate things for a beginning scripter. You're right that $? doesn't come up all that often in scripts because there are better ways of testing the exit status than using the $? variable, such as using && and ||. Some examples: CMD || die MSG (must define the die function) or CMD1 && CMD2 && CMD3. – jrw32982 supports Monica Jun 20 at 23:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.