---
title: 'Script Anatomy'
source: 'https://academia.sh/en/courses/shell-programming/script-anatomy'
course: 'Shell Programming'
language: en
updated: '2026-08-17T18:10:01+00:00'
license: 'CC BY-SA 4.0'
---

# Script Anatomy

The shebang, execute permission, the three ways to run a script, and the difference between a subprocess and sourcing.

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:

```sh
#!/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:

```sh
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:

```sh
./report.sh
```

```
bash: ./report.sh: Permission denied
```

```sh
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:

```sh
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:

```sh
report.sh
```

```
bash: report.sh: command not found
```

```sh
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.

```sh
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:

```sh
LOG_FILE=access.log
```

```sh
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:

```sh
#!/usr/bin/env bash
grep -q '999' access.log
```

```sh
./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:

```sh
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/sh` guarantees only the POSIX shell language; a script using a bash
  extension must declare bash.
- `bash script` creates a new process, `. script` does 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.
