, ,

Bash Scripting From Scratch for Beginners (Linux for Beginners, Part 16)

Turn the commands you repeat every day into a script that runs itself. Shebangs, variables, arguments, loops, and the one line that stops a script from doing real damage.

Linux for Beginners · Part 16 of 24

Every morning you run the same four commands: check the disk, tail a log, restart a service, print a summary. You have typed them so many times your fingers know them. That is the exact moment to stop typing them. A shell script is nothing more than those commands saved in a file so the machine runs them for you, in order, every time, the same way. The pipelines from the shell part and the schedule from the cron part only become real automation once you can wrap logic in a script.

Scripting also has one failure mode that turns a helpful little file into a destructive one, and almost every beginner writes it at least once. We will build up to that on purpose so you never ship it.

Never written a script? Type each example into a file and run it; that is the fastest way to learn. Already gluing things together with scripts? Go straight to the safe defaults section, because one line at the top prevents the worst bug on this page. This builds on the pipes from Part 3 and pairs with the schedule from Part 15.

Bottom line

A script is a text file of shell commands with a shebang on the first line naming the interpreter. Make it executable with chmod +x and run it with ./name.sh. Store data in variables, capture command output with $(...), branch with if, repeat with for and while, and read arguments as $1 and $2. Quote every variable, and start every serious script with set -euo pipefail so a failed step stops the script instead of charging ahead into damage.

#!/usr/bin/env bash set -euo pipefail # back up a directory src=/etc stamp=$(date +%F) echo backing up $src shebang: which interpreter runs this safe defaults, stop on any error a comment, ignored by the shell a variable, no spaces around the equals command substitution captures output use the variable with a dollar sign

From commands to a file you can run

The first line of a script is the shebang, the two characters #! followed by the path to the interpreter. It tells the kernel what to run the file with. You will see two common forms. #!/bin/bash points straight at bash. #!/usr/bin/env bash asks the environment to find bash on the PATH, which is more portable across systems where bash lives in a different place. Either works; pick one and be consistent.

A file is not runnable just because it contains commands. It needs the execute permission from the permissions part of this series, and then you run it by giving a path to it. Leave off the ./ and the shell looks on your PATH instead of the current directory and reports that it cannot find the file.

$ cat > hello.sh << 'EOF'
#!/usr/bin/env bash
echo Hello from a script
EOF
$ ./hello.sh
bash: ./hello.sh: Permission denied
$ chmod +x hello.sh
$ ./hello.sh
Hello from a script

That Permission denied on the first run is the most common first stumble. The file was fine; it just was not marked executable yet. One chmod +x fixes it for good.

Variables, arguments, and quoting

A variable is a name for a value. Assign with no spaces around the equals sign, because name = value with spaces is read as a command called name, not an assignment. Read the value back with a dollar sign. Capture the output of a command into a variable with $(...), called command substitution. Values passed on the command line arrive as numbered variables: $1 is the first argument, $2 the second, $@ is all of them, and $# is how many there are.

SymbolHolds
$1 $2The first and second arguments
$@All arguments, as separate words
$#The number of arguments
$?Exit code of the last command
$$The process id of the script

Now the habit that separates working scripts from time bombs: quote your variables. Always write "$file", not $file. Without quotes, a value containing a space is split into two words, so a file named quarterly report.txt becomes two arguments, quarterly and report.txt. Feed that to rm and you delete the wrong things. Quoting keeps the value whole.

Making decisions and repeating work

Scripts get useful when they decide and repeat. An if block runs commands only when a test passes. The test itself lives in double brackets in bash, [[ ... ]], which is safer than the older single bracket. Every command returns an exit code, zero for success and nonzero for failure, and if simply checks that code.

test a condition true false then: do this else: do that
TestTrue when
[[ $a -eq $b ]]Two numbers are equal
[[ $a -lt $b ]]Number a is less than b
[[ $s == ok ]]String s equals the word ok
[[ -z $s ]]String s is empty
[[ -f path ]]path is an existing file
[[ -d path ]]path is an existing directory

Numeric tests use the letter operators like -eq and -lt, while string tests use == and !=. Mixing them up is a common early bug, so keep numbers on the letter operators. A for loop walks a list, running the body once per item. A while loop runs as long as its test stays true. Together with a condition they cover most of what a maintenance script needs to do.

for item in list run the body next item done
#!/usr/bin/env bash
set -euo pipefail

if [[ $# -lt 1 ]]; then
  echo "usage: $0 DIRECTORY"
  exit 1
fi

for f in "$1"/*.log; do
  echo "compressing $f"
  gzip "$f"
done

Read that top to bottom and it is plain enough to follow. If no argument was given, print how to use it and exit with a nonzero code. Otherwise, loop over every log file in the given directory and compress it. The quotes around "$1" and "$f" are what make it survive directories and filenames with spaces.

The one line that stops a script from doing damage

Here is the failure mode promised at the start. By default a shell script keeps running after a command fails. It ignores the error, moves to the next line, and that is how a backup script turns into a deletion script. Look at this pattern, which appears in real scripts everywhere:

#!/usr/bin/env bash
cd /var/www/cache
rm -rf *

The intent is to empty a cache directory. But if that cd fails, because the directory was renamed or the disk was not mounted, the script does not stop. It carries on to the next line and runs rm -rf * in whatever directory it happened to be in, often your home directory or the root of the filesystem. The command did its job perfectly. The script never checked whether it was standing where it thought it was.

The fix is one line at the top: set -euo pipefail. The -e makes the script exit the moment any command fails, so a failed cd stops everything before the rm ever runs. The -u treats the use of an unset variable as an error, catching typos in variable names. The pipefail makes a pipeline fail if any stage fails, not just the last one. Combine that with an explicit check and the danger is gone:

#!/usr/bin/env bash
set -euo pipefail
cd /var/www/cache || { echo "cache dir missing"; exit 1; }
rm -rf ./*

Two habits, huge payoff. Start with set -euo pipefail, and guard a risky cd so the script refuses to continue from the wrong place. The ./* instead of a bare * is a small extra guard that avoids surprises with filenames that begin with a dash.

The bug in every first script

An unquoted variable is the quiet one that gets you. cp $src $dest looks correct and works in every test, right up until a path contains a space and the words split apart. Suddenly you are copying the wrong number of files to the wrong place, with no error to warn you.

The rule is blunt: put double quotes around every variable expansion, every time, even when you are sure it is safe. cp "$src" "$dest". Run your script through the free shellcheck tool and it flags the unquoted ones for you, which is why it belongs in every scripter toolkit.

A real opinion: safe defaults are not optional

Most beginner tutorials show scripts that start straight at the first command, no set -euo pipefail, no quoting discipline. That teaches a style that works on the tutorial example and breaks in production. My position is firm: a script without set -euo pipefail is a draft, not a finished tool. The three or four seconds it takes to type that line buys you a script that stops at the first sign of trouble instead of plowing ahead into a mess you have to clean up.

The same goes for shellcheck. It is a static analyzer that reads your script and points at the quoting bugs, the unset variables, and the portability traps before they ever run. Run it on every script you write. The people who tell you real admins do not need a linter are the same people whose backup script deleted a home directory at 2am. Tools that catch mistakes are a sign of skill, not a lack of it.

Build this now

Write a small script that takes a directory and prints how many files it holds, then check it with shellcheck. Save this as count.sh:

#!/usr/bin/env bash
set -euo pipefail
dir="${1:-.}"
count=$(find "$dir" -type f | wc -l)
echo "$dir holds $count files"

Run chmod +x count.sh then ./count.sh /etc. Then install shellcheck and run shellcheck count.sh. A clean report means your quoting is right. The ${1:-.} gives a default of the current directory when no argument is passed.

Real interview question

A cleanup script does cd to a directory and then rm -rf on everything. Why is that risky, and how do you make it safe?

The risk is that the cd can fail while the script keeps going, so the rm runs in the wrong directory and deletes the wrong files. You make it safe by starting the script with set -euo pipefail so any failure stops execution, and by guarding the cd with an explicit exit if it fails. Bonus points for quoting variables and mentioning shellcheck. This question is really asking whether you understand that scripts do not stop on errors by default, which is the fact behind a large share of production accidents.

What beginners ask first

Should I use #!/bin/bash or #!/usr/bin/env bash? Use #!/usr/bin/env bash when you want the script to find bash wherever it is installed, which is more portable. Use #!/bin/bash when you know the exact path and want to pin it. For most work either is fine; just be deliberate.

Why does name = value fail? The spaces. The shell reads name as a command with arguments = and value. Assignment must be written with no spaces: name=value.

What is the difference between single and double brackets in a test? Single [ ] is the old portable test command. Double [[ ]] is a bash builtin that handles unquoted variables and pattern matching more safely. In a bash script, prefer [[ ]].

How do I debug a script that behaves strangely? Run it with bash -x yourscript.sh, which prints each line as it executes with the variables filled in. Watching the real values flow past finds most bugs in a minute.

When should I stop using bash and switch to Python? When the script grows past simple file and command wrangling into real data structures, arithmetic, or parsing, reach for Python. bash is superb glue and a poor programming language; know where the line is.

You can now turn a routine into a file that runs itself, guard it against the errors that make scripts dangerous, and schedule it with what you learned about cron. Next we tighten the machine itself with basic security hardening, the settings that keep a fresh server from becoming someone else problem. Work the series in order from the Complete Guide.

References

Linux for Beginners · Part 16 of 24
« Previous: Part 15  |  Complete Guide  |  Next: Part 17

About The Author


Discover more from Journal of Intelligent Infrastructure – By Dr Pranay Jha

Subscribe to get the latest posts sent to your email.

Leave a Reply

Your email address will not be published. Required fields are marked *

Architect’s Toolkit

About the Author

Dr. Pranay Jha is a Cloud and AI Consultant with 18+ years of experience in hybrid cloud, virtualization, and enterprise infrastructure transformation. He specializes in VMware technologies, multi-cloud strategy, and Generative AI solutions. He holds a PhD in Computer Applications with research focused on Cloud and AI, has published multiple research papers, and has been a VMware vExpert since 2016 and a VMUG Community Leader.

Discover more from Journal of Intelligent Infrastructure - By Dr Pranay Jha

Subscribe now to keep reading and get access to the full archive.

Continue reading