Lesson 06 / 24
Script Anatomy
The shebang, execute permission, the three ways to run a script, and the difference between a subprocess and sourcing.
Contents
The previous topic established chaining commands together with streams and exit codes. The resulting pipelines were one-liners: they ran the moment they were written and then vanished. When the same pipeline needs to run every day, gain options, and be handed off to someone else, a single line is not enough.
A script is an ordinary text file holding commands that the shell reads and executes in sequence. This lesson takes up what the file is, how the kernel runs it, and what the three different ways of running it mean in terms of processes.
The First Script
The previous lesson’s final pipeline is put into a file:
#!/usr/bin/env bash # report.sh — produces a summary report from the access log. echo "== Most requested paths ==" cut -d' ' -f7 access.log | sort | uniq -c | sort -rn | head -5
This file is saved as report.sh. Throughout the course, every topic will
advance this script one step further; by the end it will become a tool that
takes arguments, tolerates errors, and runs on a schedule.
Lines beginning with # are comments; the shell ignores them. The comment
marker does not have to be at the start of a line, but it does not start a
comment in the middle of a word:
echo abc#def echo abc #def
abc#def abc
For a comment to start, the # mark must sit at the start of a word. This is a
rule that matters when # appears in file names and URLs.
Execute Permission
When a script file is created, it does not carry execute permission. If it is invoked directly:
./report.sh
bash: ./report.sh: Permission denied
echo $?
126
The code 126 means “file found but could not be executed”; it appeared among
the reserved codes in the previous lesson. Permission is granted:
chmod +x report.sh ./report.sh
== Most requested paths == 6 /api/data 4 /missing.html 4 /index.html 3 /static/style.css 3 /product/45
If the ./ in front of the script’s name is dropped, the result changes:
report.sh
bash: report.sh: command not found
echo $?
127
The rule established in the command path lesson of the Introduction to Linux
course applies here: a name without a slash is searched for only in the
directories listed in PATH, and the current directory is not on that list.
Writing ./report.sh turns the name into a path, and no search happens.
Adding the current directory to the PATH list is not a widely recommended
practice: anyone who drops a file named ls into that directory can hijack
every command you run there.
The Shebang
The #! sequence on a script’s first line looks like a comment, but its
function belongs to the kernel’s domain, not the shell’s. If the file’s first
two bytes are #!, the kernel does not attempt to execute it directly; it
reads the rest of the line as a program path, starts that program, and gives
it the script file’s path as an argument.
This mechanism is called the shebang. It has three details.
It is valid only on the first line. A #! on the second line is an
ordinary comment. If the first line’s first two bytes are not #!, there is
no shebang.
The path is absolute; no search happens. The kernel does not look at
PATH. A script that writes #!/bin/bash does not run on a system where bash
is not at the path /bin/bash.
Without a shebang, the behavior is left to the caller. The kernel cannot execute the file and returns an error; the calling shell then, in most cases, has the file read into its own subshell. The result depends on the shell running the script — that is, it is not portable.
printf 'echo "ran without a shebang: $0"\n' > plain.sh chmod +x plain.sh ./plain.sh
ran without a shebang: ./plain.sh
It ran, but which shell it ran with is not guaranteed. Writing a shebang on every script is a mandatory habit.
Which Interpreter
There are two common forms, and the choice depends on what the script uses.
#!/bin/sh declares the POSIX shell language. Which shell sits at the end of
this path varies by system; the only guarantee given is that it will interpret
the POSIX shell language. Bash extensions such as arrays, [[ ]], <<<,
${var,,} cannot be used with this declaration — that they appear to work on
some systems is misleading, because there /bin/sh is bash’s POSIX mode.
#!/usr/bin/env bash calls the env command to search for bash in PATH.
This form absorbs bash being installed in different places on different
systems. Its cost is that an indirect search runs through PATH; in strict
environments that require a fixed path, #!/bin/bash is preferred.
Decision rule: if the script uses even a single bash-specific feature, declare bash. Declaring POSIX while using a bash feature is the most common reason a script silently breaks on another system. In this course, every bash-specific feature will be marked explicitly in the text.
There is also a difference between bash versions. On a given system,
/bin/bash may be an old major version; features such as associative arrays
do not exist there. The Arrays and Associative Arrays lesson will give a
measurable example of this difference.
The Three Ways to Run It
The same script can be run in three different ways, and the three are not the same thing.
Direct execution (./report.sh) requires execute permission and a
shebang. A new process is created.
Giving it as an argument to the interpreter (bash report.sh) requires
neither permission nor a shebang; even if a shebang is present, it is ignored,
because the file is not executed, it is read. A new process is created here
too.
Sourcing (. report.sh or source report.sh) creates no new process. The
commands run inside the current shell.
The difference of the third can be observed. Let config.sh just assign a
variable:
LOG_FILE=access.log
bash config.sh echo "after subprocess: [${LOG_FILE-undefined}]" . ./config.sh echo "after source: [${LOG_FILE-undefined}]"
after subprocess: [undefined] after source: [access.log]
The assignment made in the subprocess vanished along with the process. This is
the same subshell behavior as in the pipeline lesson: variables do not pass
between processes. Sourcing is therefore used for configuration files and
function libraries; it is not suitable for scripts that do work, because the
script’s exit call closes the calling shell.
The . form is POSIX; source is a bash extension; the two do the same
thing. In the POSIX form, if the file name contains no slash, it is searched
for in PATH — this is why the ./ in . ./config.sh is required.
The Script’s Exit Code
A script returns the code of the last command it ran. If the following
script’s last line is a grep that finds no match:
#!/usr/bin/env bash grep -q '999' access.log
./finalcode.sh; echo "code=$?"
code=1
The script ran successfully but reported failure. If this behavior is not
wanted, the code is written explicitly: exit 0. Deliberately setting the
exit code is what makes a script usable in a chain or a scheduled job.
Syntax Checking Without Running
The bash -n option reads and parses the script but does not run it. If a
construct is left incomplete, it reports it:
bash -n broken.sh
broken.sh: line 4: syntax error: unexpected end of file from `if' command on line 2
This check sees only syntax; it does not catch nonexistent commands, wrong options, or logic errors. Still, it is the cheapest test to run before running a script with destructive operations for the first time. The full set of debugging tools will be taken up in the Script Debugging lesson.
Summary
- A script is a text file holding commands that the shell runs in sequence; a
word starting with
#opens a comment. - Direct execution requires execute permission; if permission is missing,
code 126 is returned, and if the name is not found in
PATH, 127. - For a file whose first two bytes are
#!, the kernel starts the absolute path in the rest of the line as the interpreter; it does not search, and it looks only at the first line. #!/bin/shguarantees only the POSIX shell language; a script using a bash extension must declare bash.bash scriptcreates a new process,. scriptdoes not; in the latter, assignments stay in the calling shell.
Next Step
The log’s name is hardcoded in the script’s body for now. Taking it into a variable looks, at first glance, like just a matter of tidiness; but in the shell, the result of variable expansion depends on quoting, and an unquoted expansion silently makes the script run wrong on file names that contain spaces. The next lesson takes up expansion, word splitting, and quoting rules.
To keep your progress and take notes, Log in
My notes
Log in to take notes.