Running a script, and what a variable really holds
◈ 9 cardsRun the same script three ways and say what each route changed; assign and read shell variables; quote a multi-word value; and recognise the silent null string an unset name expands to.
Three ways to run a script, and what changes between them
A Bourne shell script is a text file holding commands you could have typed at the prompt. Nothing in the file marks it as a program — no compilation, no header, no extension that matters. The whole question is how you ask for it to be run, and there are exactly three ways to ask.
Here is the file. It is called greet and it is four lines long:
echo "Hello from a script."
echo "Home directory: $HOME"
echo "Terminal type: $TERM"
date
Route 1 — hand the file to an interpreter. sh greet starts a fresh Bourne shell and tells it to read its commands from greet. The file has to be readable, and that is the whole requirement: no execute permission, no #! line, no ownership of the file. Reach for this route for a script you have just been sent, for a file whose permissions you cannot change, or when you want to force one particular interpreter for one particular run.
Route 2 — make the file executable and run it by name. chmod u+x greet turns on the owner execute bit and ./greet runs it. Two details on that command line earn marks.
The ./ is not decoration. It is the relative pathname of the file in the current directory, and you need it because . is deliberately kept off your PATH — a directory that anyone can write to should not be searched for commands. Leave it off and the shell searches PATH, fails, and reports greet: command not found about a file sitting right in front of you.
The second detail is the one the exam is really asking about. With no #! line, ./greet is executed by a child of the shell you are sitting in. If your login shell is csh, that child is a C shell, and a C shell reading Bourne syntax produces a spray of errors from constructs it cannot parse. Nothing is wrong with the script. The wrong program is reading it.
Route 3 — name the interpreter inside the file. Make this the first line, occupying the first two bytes of the file:
#!/bin/sh
The kernel, not the shell, acts on it. When the file is exec'd the kernel sees the two-byte marker, reads the rest of the line as the absolute pathname of an interpreter, and runs that program with the script as its argument. ./greet therefore becomes /bin/sh ./greet no matter which shell you launched it from, and the choice of interpreter travels with the file from now on. Route 3 still needs the execute bit, because the #! line is consulted only when the file is exec'd: it does nothing for sh greet, where you already named the interpreter, and it is an ordinary comment to any shell that is already reading the file.
Which to use. Route 1 for a one-off, an audit, or a file you cannot chmod. Route 3 plus chmod u+x for anything you will run twice or hand to anybody else. Route 2 without a #! line is the one to avoid: it works on your machine and breaks on a colleague's, for reasons that have nothing to do with your code.
A variable is a name for a string
The assignment form is var=value with no space on either side of the equals sign. That is not a style rule, it is a consequence of how the shell reads a line: it splits the line on whitespace first, and looks for an assignment only inside a single unsplit word. Write name = John and the shell sees three words — the command name, then the arguments = and John — and reports name: command not found.
Now the classic. What does this line do?
name=John Doe
It does not set name to John Doe, and it does not report a syntax error. The shell reads name=John as a temporary assignment attached to a command, and reads Doe as the command to run with that assignment in its environment. There is no program called Doe, so you get Doe: command not found — and back in your shell, name is still unset. Quote the value and it behaves:
name="John Doe"
echo "$name"
To read a variable back, prefix the name with a dollar sign. Wrap the name in braces when the character that follows could be read as part of the name: ${count}th is unambiguous where $countth names a variable that does not exist.
Every variable holds a string. count=10 stores the two characters 1 and 0, not the integer ten. The Bourne shell has no numeric type at all, which is why arithmetic needs a detour — the subject of the next module.
An unset variable expands to the null string, silently. No warning, no error, no non-zero status:
$ echo "[$nosuchvariable]"
[]
Almost every mysterious Bourne script bug starts there. A typo in a name becomes an empty string, the empty string vanishes into the command line, and the command that actually runs is not the command you wrote.
Two families of variable
Environment variables are the ones marked for export, so they are copied into every child process the shell creates: HOME, PATH, TERM, PWD, MAIL, PS1. Most are writable, and writing them is how you configure a session — extending the command search path, for instance:
PATH="$PATH:$HOME/bin"
export PATH
User-defined variables are everything else. They live in this shell alone until you export them, which is the next lesson but one. set with no arguments lists every variable the shell knows about, exported or not; env lists only the exported ones, which makes the pair a quick way to tell which is which.
Three writable environment variables worth knowing beyond the obvious ones. PS2 is the continuation prompt — the > you see after a line that ended in a backslash — where PS1 is the ordinary one. CDPATH is a search path for cd, so that cd project can find a directory buried under your work tree from anywhere. EDITOR names the program that crontab and similar tools launch when they need text from you.