Bash scripting guide · part 2 of 32
Running scripts
The four ways to start a script (./script, bash script, PATH, source), what each one changes, and the errors you get when one goes wrong.
A shell script is a text file of commands. There are four common ways to run one, and they differ in two things that matter: which program interprets the file, and whether it runs in a new process or in your current shell.
| How | Interpreter | Process |
|---|---|---|
./deploy.sh |
the shebang line | new child process |
bash deploy.sh |
bash, named by you | new child process |
deploy.sh (found in PATH) |
the shebang line | new child process |
source deploy.sh or . deploy.sh |
your current shell | the current shell |
Running a file by its path
To run a script as a program it needs the execute permission bit, and you have to name it by
a path. A bare name like deploy.sh is looked up only in the directories listed in PATH,
never in the current directory (unlike Windows cmd.exe).
A new file is not executable
#!/usr/bin/env bash
echo "hello from $0"
./hello.sh; echo "exit status: $?"
ls -l hello.sh
/bin/bash: line 1: ./hello.sh: Permission denied exit status: 126 -rw-r--r-- 1 dev dev 41 Oct 31 20:22 hello.sh
Files are created without the execute bit, so the kernel refuses with Permission
denied and bash returns status 126, meaning "found but could not execute". ls -l
shows -rw-r--r--: read and write, no x.
Make it executable and run it
#!/usr/bin/env bash
echo "hello from $0"
chmod +x hello.sh
./hello.sh
hello.sh; echo "exit status: $?"
hello from ./hello.sh /bin/bash: line 3: hello.sh: command not found exit status: 127
With chmod +x, ./hello.sh works and $0 holds the path it was called by. Without the
./, bash searches PATH, does not find it and reports command not found with status
127. The ./ is how you say "this file, here".
Naming the interpreter yourself
bash hello.sh starts bash and tells it to read the file. No execute bit is needed, because
the kernel is executing /usr/bin/bash, not your file, and the shebang is just a comment. That
makes it handy for a quick run of a script you have not chmoded, and a trap when the
interpreter you name is not the one the author intended.
The interpreter you name wins over the shebang
#!/bin/bash
if [[ -n $BASH_VERSION ]]; then echo "bash $BASH_VERSION"; else echo "not bash"; fi
bash which.sh
sh which.sh
bash 5.2.37(1)-release which.sh: 2: [[: not found not bash
bash which.sh runs fine without the execute bit. sh which.sh runs the same file under
dash, which does not understand [[: it prints [[: not found and falls into the else
branch. Run scripts the way their shebang says, or simply as ./script.
Two useful flags while developing: bash -n script parses the file without running anything,
catching syntax errors, and bash -x script prints each command after expansion before it
runs (see Debugging bash scripts).
Check syntax without running anything
#!/usr/bin/env bash
echo "starting"
for f in *.txt; do
echo "$f"
echo "done"
bash -n broken.sh
broken.sh: line 6: syntax error: unexpected end of file
Bash reaches the end of the file still inside the for loop and reports unexpected end
of file with status 2. The line number points at the end, not at the loop missing its
done, so read upward from there. starting was never printed: -n parses but does not
execute. Useful as a pre-commit check.
Trace each command as it runs
bash -x scripts/greet.sh Ana
+ echo 'Hello, Ana!' Hello, Ana!
Every command is echoed to stderr with a + prefix after variables are expanded, so you see
that ${1:-world} became Ana. Note that bash -x ran a #!/bin/sh script under bash;
for a faithful trace of a POSIX script use sh -x.
Running scripts from PATH
To call your own scripts by name from anywhere, put them in a directory that is in PATH.
~/.local/bin and ~/bin are the usual personal choices; Debian's default ~/.profile adds
both to PATH when they exist at login. Drop the .sh extension if you like: the shebang
decides the interpreter, not the name.
Install a script into a PATH directory
mkdir -p ~/.local/bin
cp scripts/greet.sh ~/.local/bin/greet
export PATH="$HOME/.local/bin:$PATH"
greet
type greet
Hello, world! greet is hashed (/home/dev/.local/bin/greet)
Once the directory is in PATH, greet works like any installed command. type shows
which file bash resolved, and that it is now hashed: bash remembers the location so it
does not search PATH again. If you later move the script, hash -r forgets stale paths.
A child process cannot change your shell
./script and bash script run in a separate process. Whatever the script does to its own
state, such as cd, setting variables, or changing options, disappears when it exits. That is
a feature: a script cannot accidentally wreck your interactive shell. It also explains the
common surprise that a script containing cd /some/dir leaves you where you were.
A script's cd does not move you
#!/usr/bin/env bash
cd logs
LAST_DIR=$PWD
echo "inside the script: $PWD"
chmod +x goto-logs.sh
./goto-logs.sh
echo "after it: $PWD, LAST_DIR='$LAST_DIR'"
source goto-logs.sh
echo "after source: $PWD, LAST_DIR='$LAST_DIR'"
inside the script: /home/dev/demo/logs after it: /home/dev/demo, LAST_DIR='' inside the script: /home/dev/demo/logs after source: /home/dev/demo/logs, LAST_DIR='/home/dev/demo/logs'
Run as a program, the script's cd and variable vanish with its process. With source,
the same lines run in the current shell, so the directory change and LAST_DIR stick.
That is what source (or its POSIX spelling .) is for: loading functions, variables and
settings into the current shell, as in source ~/.bashrc or a project's source venv/bin/activate.
A file meant to be sourced does not need the execute bit. Avoid exit in it: exit would
close the shell that sourced it. Use return instead, which works at the top level of a
sourced file.
Exit status and $0
A script's exit status is the status of exit N if it calls it, otherwise that of the last
command it ran. Callers, if, && and CI systems all read that number, so end with an
explicit exit when the last command's status could mislead (see Exit codes).
The last command decides the exit status
#!/usr/bin/env bash
grep -q "TODO" notes.txt && echo "notes have a TODO"
grep -q "TODO" todo.txt && echo "todo.txt has a TODO"
bash check.sh; echo "exit status: $?"
notes have a TODO exit status: 1
The script's work went fine, but its last line was a grep that found nothing, so the whole
script exits 1. A trailing cond && action is a classic way to make a script report failure
by accident; end with exit 0 or use an if.
Inside a script, $0 is the path it was invoked by, and ${BASH_SOURCE[0]} is the file
currently being read, which stays correct even when the file is sourced. The usual way to
find files that live next to the script:
Find the directory the script lives in
#!/usr/bin/env bash
here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
echo "\$0 is $0"
echo "script dir is $here"
chmod +x tools/where.sh
tools/where.sh
cd /tmp && ~/demo/tools/where.sh
$0 is tools/where.sh script dir is /home/dev/demo/tools $0 is /home/dev/demo/tools/where.sh script dir is /home/dev/demo/tools
$0 changes with how you call the script, but here is always the absolute directory
containing it, so the script can open "$here/config" no matter what the caller's current
directory is.
Related
sourceRun the commands in a file inside the current shell, so its variables, functions and directory changes stay in effect.typeShow how the shell would interpret a name, as an alias, keyword, function, builtin or file on PATH.execReplace the shell with another program, or, with only redirections, change where the shell's own input and output go.envPrint the environment, or run a command with a modified environment.- The shebang lineWhat the kernel does with #!, why
#!/usr/bin/env bashand#!/bin/bashboth exist, and the ways a shebang line breaks. - Environment variablesHow export passes variables to child processes, the one-command NAME=value prefix, loading .env files, and why a child cannot change its parent.
- Exit codesWhat exit statuses mean, how $? works, the reserved values 126, 127 and 128+N, PIPESTATUS, and how to choose codes for your own scripts.
- Debugging bash scriptsCheck syntax with bash -n, trace with set -x, add file and line numbers through PS4, send the trace elsewhere with BASH_XTRACEFD, and report failures with trap ERR.
- Subshells and command groupingThe difference between ( ... ) and { ...; }, what a subshell inherits and what it cannot change, which constructs create one, and what that costs.