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
| Symbol | Means |
|---|---|
$1, $2, … | The first, second, … argument |
$@ | All arguments, each kept as a separate word when quoted as "$@" |
$# | How many arguments there were |
$0 | The 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>—$1isstage,$2isdeploy,$3is 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. $1is 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.