Introduction
Originally, we only had one shell on unix. When ran a command, the shell would attempt to invoke one of the exec() system calls on it. It the command was an executable, the exec would succeed and the command would run. If the exec() failed, the shell would not give up, instead it would try to interpret the command file as if it were a shell script. This works fine as long as there is only one shell on the system. But what if you are using one shell as your interactive and want to run a script written in another shell's language?
This is where the #! trick comes in. The idea of using # to represent a comment originated with csh and was quickly added to the bourne shell. Now all shells know to ignore stuff after a #. So we can add a leading line something like "#! /usr/bin/ksh". To the shell this line is just a comment. But if the kernel tries to execute a file with this line, it will exec the specified interpreter and pass the script to it. So shell scripts become executable pretty much like real executables are. Now when a shell tries to exec a shell script, it succeeds.
What if you leave off the #! line? Well, the kernel exec will fail. Your average shell will then try to run the script itself. A few shells will try to inspect the script to try to guess the language. This is not good. You may be running ksh as your interactive shell and writing ksh scripts. If you later switch to bash as an interactive shell, some of your scripts may continue to run while others may fail. Also the line is a comment that provides an important clue to a programmer who looks at the script to understand it. Knowing which language the author is attempting to use is a big help.
While I have used the term "shell", actually this technique can be used with many programs that are not shells. Here is a "script" to display a multiline message:
#! /usr/bin/cat
Line 1
Line 2
Line 3
This will display that "#! /usr/bin/cat" line, but other than that, it works fairly well.
Passing an Argument
You can pass a single argument like this:
#! /usr/local/bin/perl -w
But, in general, you are limited to one argument. On most systems, a line like: "#! /some/interpreter -a -b" will result in "-a -b" being passed as a single argument. However, the single argument is not limited to starting with a hyphen. We can improve on our message script:
#! /usr/bin/sed 1d
Line 1
Line 2
Line 3
Example
Let's put all of this together with an example. Here is a perl script that I will call perlargs:
#! /usr/local/bin/perl -w
#! /usr/local/bin/perl -w
print "script name is ", $0, "\n";
while (@ARGV) {
$ARGV = shift @ARGV;
print "argument ", $i++, " is ", $ARGV, "\n";
}
system "ps -f -ww";
The -w asks perl issue warning messages. The script simply displays its arguments, then runs the ps command. When I run it, I get:
$ ./perlargs one two three
Name "main::i" used only once: possible typo at ./perlargs line 10.
script name is ./perlargs
argument 0 is one
argument 1 is two
argument 2 is three
UID PID PPID STIME TTY TIME CMD
perderabo 69 1 17:47:28 n01 0:00.24 /bin/ksh -l
perderabo 201 69 18:28:22 n01 0:00.03 /usr/local/bin/perl -w ./perlargs one two three
perderabo 2055 201 18:28:22 n01 0:00.01 ps -f -ww
$
Notice that the perl process was called with 5 arguments. The 2nd argument is the name of the script. It is up to the perl process to to present the final 3 arguments as the argument list seen by the script. Also realize the kernel started the perl process. After that, it is up to perl to open the script and read it and execute each line. This is why scripts need to be readable. You cannot execute a non-readable script.
Conclusion
This should be enough information to understand what is happening with those #! lines. In the following posts, I will add details on various aspects of the process.