Skip to content
VLSI Mentor

Linux & Bash · Part I · Module 15 · Writing and Running Bash Scripts

From Command Line to Script

Fourteen modules of typing commands, and the step to writing them in a file turns out to involve no new language at all. What it does involve is a permission bit, a path that must be spelled a particular way, and the fact that from now on somebody other than you will run what you wrote.

Foundation16 min readLinuxPart IFundamentalsScripting

Part I · Module 15 · Chapter 15.1 · From Command Line to Script

Everything so far has been typed at a prompt. This module puts it in a file.

1. A Script Is a File of Commands

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/bin/bash
cd /home/engineer/project/build
make clean
make all
echo "build finished"

That is a complete script, and every line is something you could have typed. There is no separate scripting language — the thing you have been using interactively for fourteen modules is the language.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ chmod +x build.sh
$ ./build.sh

Two steps: make it executable, then run it.

2. Making It Runnable

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ printf 'echo "ran"\n' > s.sh

$ ./s.sh
bash: ./s.sh: Permission denied

A file is not executable because it contains commands. It needs the execute bit from Chapter 5.5:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ chmod +x s.sh
$ ./s.sh
ran

The exit status of that failure is worth recognising:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ ./noexec.sh 2>/dev/null; echo $?
126

126 means "found it, could not run it" — Chapter 11.1's table, and almost always a missing execute bit.

3. Three Ways to Run It

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ ./s.sh                 # as a program, needs +x
$ bash s.sh              # hand the file to an interpreter — no +x needed
$ /tmp/m15v/s.sh         # by absolute path, needs +x

bash s.sh works without the execute bit, because the file is being read by bash rather than executed by the kernel. The permission that matters there is read, not execute.

That is occasionally useful — running somebody's script without modifying it — and it is not how a script should be used in anger, because it bypasses the shebang (Chapter 15.2) and runs the file in whatever interpreter you named.

Why the ./

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ s.sh
bash: s.sh: command not found

The file is right there. The shell will not run it.

. is not on PATH, deliberately — Chapter 10.2 showed why: an empty or dot entry means anyone who can write a file in a directory you happen to be standing in can choose what your next command does.

So the shell refuses to look in the current directory, and ./s.sh is you saying explicitly that you mean this one. The ./ is not noise; it is the whole security property.

4. The Shebang, Briefly

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/bin/bash

The first line names the interpreter. Without it the shell makes a guess, and the guess is not always the one you want:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ ./noshebang.sh
interpreter: sh

That script ran under sh, not bash — so any Bash-specific feature in it would have failed, on a machine where sh is a different shell.

Put #!/bin/bash on the first line of every script. Chapter 15.2 is the full story, including why #!/usr/bin/env bash is often better and what happens when the interpreter is missing.

5. Writing One That Is Worth Keeping

The three-line version:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/bin/bash
cd build
make

The version that will still work next month:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/bin/bash
# Build the simulator and report whether it worked.
set -euo pipefail

cd "$(dirname "$0")/build" || { echo "no build directory" >&2; exit 2; }

make clean
make all

echo "build finished"

Five additions, each from an earlier module:

  • A comment saying what it is for — Chapter 15.5.
  • set -euo pipefail — Chapter 10.6, so a failing step stops the script instead of the next one running on nothing.
  • A guarded cd — Chapter 11.3, because cd build; make builds in the wrong directory when the cd fails.
  • A path relative to the script, not to where it was run from — Chapter 8.4, and the subject of Chapter 15.3.
  • A deliberate message at the end rather than silence.

None of that is new. It is the first chapter where it all has to be used at once.

6. The Script Is a Different Process

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ echo "parent PID=$$ cwd=$(pwd)"
parent PID=72879 cwd=/tmp/m15v

$ ./inherit.sh
  PID=72930  PPID=72879
  cwd=/tmp/m15v
  changed to /tmp

$ pwd
/tmp/m15v

Different PID, same starting directory, and the cd did not escape.

Chapter 7.1 proved this before scripts were the subject; here it is with the subject in place. A script runs as a child process, so:

  • it inherits the environment and working directory;
  • it may change them freely;
  • nothing it changed comes back.

That is Chapter 15.3, and it is the single most common source of "but it worked when I typed it".

7. From a Command Line You Already Ran

The most reliable way to write a first script is not to write it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ history | tail -20

The commands are already there. You worked out the pipeline interactively — Chapter 13.9 — and the script is that sequence with the exploration removed.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# worked out interactively:
cd build
make clean
make all
cp simv ../bin/

# saved as a script:
#!/bin/bash
set -euo pipefail
cd "$(dirname "$0")/build"
make clean
make all
cp simv ../bin/

Then improve it in place: add the comment, guard the cd, give it an exit status. A script that started as a command line you actually ran is already known to work, which is a considerably better starting point than one written from imagination.

8. A First Real Script

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/bin/bash
#
# run_sim.sh — compile and run one simulation, logging everything.
#
# usage: run_sim.sh <test-name>
#
# exit: 0  the test passed
#       1  the test failed
#       2  bad usage
#       3  the simulator is not on PATH

set -uo pipefail

test_name="${1:?usage: $0 <test-name>}"

command -v vcs >/dev/null 2>&1 || { echo "vcs not on PATH" >&2; exit 3; }

script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
logdir="$script_dir/runs/$test_name"
mkdir -p "$logdir" || { echo "cannot create $logdir" >&2; exit 2; }

echo "running $test_name, logging to $logdir"

vcs -full64 -sverilog -f "$script_dir/filelist.f" -o "$logdir/simv" \
  > "$logdir/compile.log" 2>&1 \
  && "$logdir/simv" +UVM_TESTNAME="$test_name" \
  > "$logdir/sim.log" 2>&1

status=${PIPESTATUS[0]}

if grep -q 'TEST PASSED' "$logdir/sim.log" 2>/dev/null; then
  echo "PASS"
  exit 0
else
  echo "FAIL — see $logdir/sim.log" >&2
  exit 1
fi

Thirty lines, and every construct in it has already been taught:

LineFrom
#!/bin/bash15.2
set -uo pipefail10.6
${1:?usage: …}8.2
command -v … || { …; exit 3; }10.5, 11.4
$(cd "$(dirname …)" && pwd)8.4
> file 2>&112.3
&& chaining11.3
${PIPESTATUS[0]}12.8
grep -q14.1
Documented exit codes11.1, 15.6

The only genuinely new thing is if, which is Module 16.

Practice

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
mkdir -p /tmp/first && cd /tmp/first

1 — Write the smallest possible script.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf 'echo "ran"\n' > s.sh
./s.sh
chmod +x s.sh
./s.sh

2 — Read the failure status.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf 'echo hi\n' > noexec.sh
./noexec.sh 2>/dev/null; echo "status: $?"

3 — Run it three ways.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
./s.sh
bash s.sh
/tmp/first/s.sh
s.sh

The last one fails. Say why before reading §3 again.

4 — Watch $0 change.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf 'echo "\$0 is [$0]"\n' > zero.sh; chmod +x zero.sh
./zero.sh
bash zero.sh
/tmp/first/zero.sh

5 — Find out what runs a script with no shebang.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf 'echo "interpreter: $(ps -o comm= -p $$)"\n' > a.sh; chmod +x a.sh; ./a.sh
printf '#!/bin/bash\necho "interpreter: $(ps -o comm= -p $$)"\n' > b.sh; chmod +x b.sh; ./b.sh

6 — Prove the script is a separate process.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
cat > p.sh <<'EOF'
#!/bin/bash
echo "  script PID=$$ PPID=$PPID cwd=$(pwd)"
cd /tmp
echo "  script moved to $(pwd)"
EOF
chmod +x p.sh
echo "shell PID=$$ cwd=$(pwd)"
./p.sh
echo "shell cwd after: $(pwd)"

7 — Turn a command line into a script. Pick three commands from your own history, put them in a file with #!/bin/bash and set -euo pipefail, make it executable and run it.

8 — Put one on PATH.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
mkdir -p ~/bin
cp s.sh ~/bin/mytest
chmod +x ~/bin/mytest
PATH="$PATH:$HOME/bin" mytest

9 — Clean up. cd /tmp && rm -rf /tmp/first ~/bin/mytest

Check Your Understanding

1. What new language does a script use?

2. ./build.sh gives "Permission denied" and exit status 126. What is wrong?

3. Why does bash s.sh work on a file with no execute bit?

4. The script is in the current directory and s.sh says "command not found". Why is that deliberate?

5. A script does export TOOL_HOME=/opt/tool and the variable is unset afterwards. Is that a bug?

6. Why is a command line you already ran a better starting point than writing a script from scratch?

9. Remember

  • A script is a file of commands in the language you already use. No new syntax.
  • What is new is the file, the interface and the contract — how it is run, what it receives, what it returns.
  • It needs the execute bit. Status 126 means found-but-not-runnable; 127 means not found.
  • bash script.sh works without +x because the file is read rather than executed — and it bypasses the shebang.
  • ./ is required because . is not on PATH, and that is a security property rather than an inconvenience.
  • Put your own scripts in ~/bin, drop the .sh, and append to PATH so they cannot shadow a system command.
  • Every script gets #!/bin/bash on line one. Without it the shell guesses, and may pick sh.
  • A script is a child process — it inherits, it may change its copy, and nothing comes back.
  • A script that must change your shell has to be sourced, and should not be executable.
  • Start from a command line you actually ran. It is already known to work.

The first line of every script in this chapter has been taken on trust. The next chapter is what it does.

Where this fits

Part of the Linux & Bash curriculum.