Your First Scripts · about 40 min

Script files, the shebang and chmod

Why this matters

Everything you have typed so far disappears when you close the shell. A script is a text file of shell commands that you can run again, give to someone else, schedule to run at night, and improve over time. Scripts are also where the precision you learned in the expansion module pays off, because a script runs unattended on data you have not seen. This lesson covers how a text file becomes something you can run, what the kernel does with the first line, and the three different ways of running a script.

A script is just a text file

Create a file hello.sh in your lesson shell. The easiest editor in the sandbox is nano:

$ nano hello.sh

Type:

#!/bin/bash
# hello.sh: my first script
echo "Hello from $(hostname)"
echo "Today is $(date +%A)"

Save with Ctrl-O then Enter, and exit with Ctrl-X. (You could also create it with printf and >; any way of writing text to a file works.)

Run it by handing it to bash:

$ bash hello.sh
Hello from sandbox
Today is Sunday

Here the program is bash, and hello.sh is just an argument: bash opens the file, reads it line by line, and runs each line exactly as if you had typed it. Lines starting with # are comments (the shebang line is a comment too, as far as bash is concerned).

Making it a command: the shebang and chmod

To run it as ./hello.sh, like a real program, two things are needed.

1. The execute permission. The kernel only executes files that have the x bit set for you. You learned chmod in the permissions module:

$ ./hello.sh
bash: ./hello.sh: Permission denied
$ chmod +x hello.sh
$ ls -l hello.sh
-rwxr-xr-x 1 learner learner 87 Nov  1 14:20 hello.sh
$ ./hello.sh
Hello from sandbox
Today is Sunday

2. The shebang. The first line, #!/bin/bash, is called the shebang (from "hash" and "bang", the nicknames of # and !). It tells the kernel which interpreter to use.

What the kernel does with #!

When bash runs ./hello.sh, it forks and calls execve("./hello.sh", ...) as for any program. The kernel opens the file and looks at its first bytes:

 ./hello.sh  with first line  #!/bin/bash
       │
       ▼   kernel rewrites the exec as:
 execve("/bin/bash", ["/bin/bash", "./hello.sh"], env)

So ./hello.sh ends up as /bin/bash ./hello.sh, which is the same as what you typed by hand. The same mechanism runs Python (#!/usr/bin/python3), awk (#!/usr/bin/awk -f) and others. You can see it with file:

$ file hello.sh /usr/bin/ls
hello.sh:    Bourne-Again shell script, ASCII text executable
/usr/bin/ls: ELF 64-bit LSB pie executable, x86-64, ...

You will often see #!/usr/bin/env bash instead. env searches PATH for bash, which helps on systems where bash is not at /bin/bash (some BSDs, macOS with a newer bash installed elsewhere). On Linux both work; this course uses #!/bin/bash.

Note: The shebang must be the very first line, starting at the very first byte. With a blank line or a space before #!, the kernel refuses the file (Exec format error). bash then quietly runs it itself as a bash script, which hides the problem, but other programs that start it, such as cron or a Python program, simply fail.

Why ./?

./hello.sh contains a slash, so bash does no PATH search and runs that exact file. Plain hello.sh would search PATH, which does not include the current directory, and fail with command not found. To run your scripts by name from anywhere, put them in a directory and add it to PATH: commonly ~/bin or ~/.local/bin:

export PATH="$HOME/bin:$PATH"

(You would put that line in ~/.bashrc so it applies to every new shell.)

File names

The .sh extension is only a hint for humans; the kernel and bash ignore it. Many installed commands are scripts without any extension. A common convention is .sh for files you run during development and no extension for commands installed into a PATH directory.

Three ways to run a script

HowWhat runs itNew process?Needs x?Uses the shebang?
bash script.sha new bashyesno (read only)no, you chose bash
./script.shthe interpreter named by #!yesyesyes
source script.sh or . script.shyour current shellnonono

The first two run the script in a child process. Its variables, cds and other changes disappear when it exits, exactly like the bash -c experiments in the expansion module. source (whose POSIX name is .) reads the file into your current shell. That is what you want for files that configure your shell:

$ cat settings.sh
export PROJECT=/work/project
cd "$PROJECT" 2>/dev/null || echo "no project dir yet"
$ bash settings.sh
no project dir yet
$ echo "[$PROJECT]"
[]
$ source settings.sh
no project dir yet
$ echo "[$PROJECT]"
[/work/project]

Your ~/.bashrc is sourced by every interactive bash at startup for the same reason.

The script's exit status

A script finishes with the status of the last command it ran, unless it calls exit N. The lesson after next covers this in depth; for now, note that the status is visible in $? after running it, just as for any program.

Common mistakes

These messages are worth recognizing; you will see all of them.

Exercises

The two quizzes cover the process model; the tasks are done in the lesson shell, and their checks actually run your script.

Recap

Going further: find out which installed commands are really scripts: grep -l '^#!' /usr/bin/* 2>/dev/null | head, then look at the first line of a few of them with head -n 1 to see which interpreters they use.

Exercises

Exercise 1 · quiz

Permission denied

You wrote hello.sh with a correct #!/bin/bash first line. bash hello.sh works, but ./hello.sh says bash: ./hello.sh: Permission denied. What is missing?

Answer
  • The file needs execute permission: chmod +x hello.sh

bash hello.sh runs the program bash, which merely reads the file, so only read permission is needed. ./hello.sh asks the kernel to execute the file itself, and the kernel refuses unless the execute bit is set (the status is 126, "found but not executable"). chmod +x fixes it. Neither PATH nor root has anything to do with it.

Exercise 2 · quiz

Where am I afterwards?

go.sh contains the single line cd /tmp. You are in /work. Which statements are true? Select all that apply.

Answer
  • After bash go.sh you are still in /work.

  • After source go.sh you are in /tmp.

bash go.sh and ./go.sh both start a new process; the cd happens in that child and is lost when it exits. source go.sh and its short form . go.sh read the file into the current shell, as if you had typed its lines, so the cd sticks. Use source for files that are meant to change your shell (settings, functions, virtual environment activation), and run everything else as a program.

Exercise 3 · task in a shell

An executable script

Create the directory /work/bin and in it a script named hello (no .sh) that:

Use nano bin/hello to write it (Ctrl-O Enter to save, Ctrl-X to exit), or printf/echo with redirection, then chmod.

Hint 1

The file needs three lines: the shebang, an echo, and an echo containing $(whoami).

Hint 2

After saving, chmod +x bin/hello, then test with ./bin/hello.

Reference solution
mkdir -p bin
printf '%s\n' '#!/bin/bash' 'echo "Hello from a script"' 'echo "running as $(whoami)"' > bin/hello
chmod +x bin/hello

Exercise 4 · task in a shell

The script from Windows

A colleague sent /work/report.sh, written on Windows. It is executable, but running ./report.sh fails with a confusing message. Investigate it (try cat -A report.sh, which shows a line end as $ and a carriage return as ^M), then fix the file in place so that ./report.sh runs and prints report ok. Keep its name and its execute permission.

Hint 1

cat -A report.sh shows ^M$ at each line end: a carriage return before the newline. The first line therefore names the interpreter /bin/bash followed by an invisible character.

Hint 2

sed -i 's/\r$//' report.sh deletes a carriage return at the end of every line and keeps the file's permissions.

Reference solution
sed -i 's/\r$//' report.sh