Skip to main content

02 — Your first script

Read this first: this lesson turns commands you type into a file you can re-run. It covers the shebang line, making a file executable, reading arguments, and the quoting rule that causes more production incidents than any other single thing in shell. It does not yet cover failure handling — that is lesson 03, and it is the more important half.

Time: about 30 minutes. Assumes lesson 01.

Set up​

mkdir -p ~/devops-course/l02
cd ~/devops-course/l02

A script is just a file full of commands​

Create a file called greet.sh containing exactly this:

#!/usr/bin/env bash
echo "Hello, $1"

Two lines, and both need explaining.

Line 1 is the shebang. The #! at the very start of a file tells the operating system which program should interpret the rest of it. #!/usr/bin/env bash means "find bash on this system's PATH and run this file with it".

You will also see #!/bin/bash written directly. env bash is the more portable form — it finds bash wherever it is installed rather than assuming one path — and it is what every script in deploy/ uses. The shebang must be the first line, with no blank line or comment above it, or it is just a comment and the file is not a script.

Line 2 uses $1, which is the first argument passed to the script. $2 is the second, and so on.

Break it on purpose: the permission bit​

Predict: you just created the file. What happens if you run it right now?

./greet.sh
bash: ./greet.sh: Permission denied
echo "exit = $?"
exit = 126

The file exists and its contents are valid, but you have not said it is a program. Look at why:

ls -l greet.sh
-rw-rw-r-- 1 you you 37 Aug 5 19:49 greet.sh

Compare that permission string to lesson 01: rw- for owner, rw- for group, r-- for others. Read and write, but nowhere an x. Fix it:

chmod +x greet.sh
ls -l greet.sh
-rwxrwxr-x 1 you you 37 Aug 5 19:49 greet.sh

Three xs appeared. Now it runs:

./greet.sh Jomari
Hello, Jomari

Note ./greet.sh, not greet.sh. The shell only searches directories listed in PATH, and the current directory is deliberately not one of them — otherwise dropping a file named ls into a directory would hijack the real ls. ./ says "the one right here".

Exit code 126 means "found it, could not execute it". That is a distinct code from 127, which means "command not found". When a CI job fails with 126, the cause is almost always a script that was committed without its executable bit.

Arguments​

Create args.sh:

#!/usr/bin/env bash
echo "one: $1"
echo "all: $@"
echo "count: $#"
chmod +x args.sh
./args.sh "Jomari Abejo" second
one: Jomari Abejo
all: Jomari Abejo second
count: 2
SymbolMeans
$1, $2, …The first, second, … argument
$@All arguments, each kept as a separate word when quoted as "$@"
$#How many arguments there were
$0The script's own name

Predict: what does ./greet.sh print with no argument at all?

./greet.sh
Hello,

Not an error. $1 was simply empty, and the script carried on cheerfully. Shell does not check that you passed what the script needs — you have to, and lesson 03 shows how.

The quoting rule​

This is the part to get right. Create unq.sh:

#!/usr/bin/env bash
mkdir -p "$1"
ls -d $1

Notice that mkdir quotes "$1" and ls does not. Everything else is identical.

Predict: you are going to pass the single argument my folder — one argument, containing a space. What will mkdir do? What will ls do?

chmod +x unq.sh
./unq.sh "my folder"
ls: cannot access 'my': No such file or directory
ls: cannot access 'folder': No such file or directory

Look at what actually happened:

ls -l
total 16
-rwxrwxr-x 1 you you 67 Aug 5 19:49 args.sh
-rwxrwxr-x 1 you you 37 Aug 5 19:49 greet.sh
drwxrwxr-x 2 you you 4096 Aug 5 19:49 my folder
-rwxrwxr-x 1 you you 43 Aug 5 19:49 unq.sh

mkdir -p "$1" created one directory named my folder, correctly. Then ls -d $1 — the same variable, unquoted — was split into two words by the shell before ls ever saw it, so ls went looking for a file called my and a file called folder.

This is called word splitting, and it is the default. An unquoted variable is not "the value" — it is "the value, chopped at every space, and then any * or ? expanded against the filesystem".

Fix it​

#!/usr/bin/env bash
mkdir -p "$1"
ls -d "$1"
./q.sh "my folder"
my folder

The rule: put double quotes around every variable expansion, every time, unless you have a specific reason not to. "$1", "$@", "$HOME", "$(command)". It is not a style preference. An unquoted variable holding a path with a space is how a script ends up running rm -rf /some/path /with/space and deleting two things it was never pointed at.

The one common case where you deliberately leave quotes off is when a variable is meant to expand into several separate arguments. The right way to do that is a bash array, written "${extra[@]}" — quoted, but an array, which expands to zero or more correctly-separated words. deploy/deploy.sh uses exactly that to build optional compose flags.

Where this shows up in MotorPH​

  • Every script in deploy/ and scripts/ opens with #!/usr/bin/env bash, for the portability reason above.
  • deploy/deploy.sh is invoked as ./deploy/deploy.sh stage deploy <tag> — $1 is stage, $2 is deploy, $3 is the image tag. Reading it is now partly possible for you.
  • Its very first act is to validate $1, because an argument that arrived from a GitHub Actions expression is untrusted input, not a convenience.

Recap​

  • A script needs a shebang on line 1 and the executable bit (chmod +x); exit code 126 means you forgot the second one.
  • $1 is the first argument, $@ is all of them, $# is the count — and a missing argument is silently empty, not an error.
  • Quote every variable expansion. Unquoted variables get split on spaces, which is how scripts operate on paths they were never given.

Next: 03 — Scripts that fail safely, which is the lesson that separates a script you can trust from one you cannot.