Create Bash Script
#!/bin/bash
set -euo pipefail
echo "hello world"
It all starts with a Shebang
The #!/bin/bash at the beginning of the script is called a "shebang" and is essential.
There are two shells to pick from
- The Bourne Shell (
#!/bin/sh)
- The Bourne Again Shell (
#!/bin/bash)
If you're scripting for modern systems /bin/bash is preferable. It includes a wider range of features including:
- Arrays
- Advanced string manipulation
- Improved process substitution syntax
- Brace expansion
- Sophisticated control flow constructs
So when should you use the Bourne Shell /bin/sh? Only when you have no other choice.
Strict Mode Options
set -euo pipefail helps you write more reliable scripts and for the curious, here is what those options do:
- set -e (errexit): The script will exit immediately if any simple command within it exits with a non-zero status (signaling an error). This helps prevent errors from silently propagating.
- set -u (nounset): The script will fail if you try to use an undeclared variable. This helps to avoid typos and catch potential mistakes.
- set -o pipefail: If any command within a pipeline fails (returns a non-zero status), the entire pipeline's exit status will also be non-zero. This means errors are caught as they occur within a pipeline, rather than just checking the final command in the chain.
Positional Arguments
#!/bin/bash
set -euo pipefail
echo "First arg: $1"
echo "Number of arguments: $#"
These are arguments passed directly after the script name on the command line.
./your_script.sh arg1 arg2
They are accessed using special variables $n, where n is a number starting from 1.
$1: First argument
$2: Second argument
$#: Number of arguments passed
Looping over arguments
for arg in "$@"
do
echo "Processing argument: $arg"
done
Advanced Arguments
Special argument variables
$@ - Represents all arguments passed as a single string (useful for passing arguments to another command).
$* - Similar to $@ but treats arguments with spaces as a single argument (be cautious with spaces).
Flags
Flags are an even more flexible and power way to pass arguments
while getopts ":hf:v" opt; do
case $opt in
h) echo "This script helps you process files";;
f) file="$OPTARG";; # Assign argument after -f to 'file' variable
v) verbose=true;; # Set a flag for verbose mode
\?) echo "Invalid option: -$OPTARG" >&2; exit 1;;
esac
done
Important Note:
The colon (: ) at the beginning of getopts ":hf:v" is essential:
- An option preceded by a colon (f:) indicates that it requires an additional argument (e.g., the filename after -f).
- Options without colons are treated as simple flags (e.g., -v).
- Let me know if you'd like more examples or scenarios on how to call the script!
Using the Flags
Displaying Help (-h)
yourscript.sh -h
Output: Prints the help message: "This script helps you process files"
Specifying a File (-f)
yourscript.sh -f input.txt
Effect: Assigns the value "input.txt" to the file variable within the script, allowing you to work with that file.
Verbose Mode (-v)
yourscript.sh -v
Effect: Sets the verbose flag within the script. The script can then use this flag to output additional info or debugging messages.
Combining Options
You can combine short options that don't require additional arguments:
yourscript.sh -hvf input.txt
This is equivalent to yourscript.sh -h -v -f input.txt and would:
Display the help message Set the 'verbose' flag Assign the value "input.txt" to the file variable.
Source scripts
You have other bash scripts and want to include it in the one you are writing.
First get the path of your current script
SCRIPT_PATH="$( dirname "${BASH_SOURCE[0]}" )"
Then source the other script
source "$SCRIPT_PATH/other_script.sh"
Want to quiet any output from the other script?
source "../other_script.sh" > /dev/null 2>&1
There you go!
Environment Variables
Passing a environment variable to a script is easy
MY_VARIABLE=true ./the_script.sh
Getting an environment variable in the script is even easier
echo "$MY_VARIABLE"
Test: has a value
myvar=${MY_VARIABLE:-}
if [ ! -z "$myvar" ]; then
echo "MY_VARIABLE is set"
fi
Test: has no value
myvar=${MY_VARIABLE:-}
if [ -z "$myvar" ]; then
echo "MY_VARIABLE is not set"
fi
Default value
myvar=${MY_VARIABLE:-true}
OS and Architecture
If your script needs to know which OS the system has or which CPU architecture (x86 or arm64)
OS=$(uname -s)
ARCH=$(uname -m)
Capture that info and test for it
if [[ ${ARCH} == 'arm64' ]]; then
echo "detected arm or apple silicon"
fi
if [[ ${OS} != Linux ]]; then
echo "you are on linux"
fi
if [[ ${UNAME} == Darwin ]]; then
echo "nice macbook"
fi
I use this for all sorts of conditional commands
Functions
Making a function is easy
my_function() {
echo "hi"
}
Passing arguments to it is easy
my_function() {
arg1=${1:-}
arg2=${2:-}
}
Return a value with echo
my_function() {
arg1=${1:-}
arg2=${2:-}
echo "$arg1 $arg2"
}
Calling the function
my_function "hello" "world"
Saving the result to a variable
result=$(my_function "hello" "world")
Parallel Processes
Want to run commands in parallel to get them done faster
- run the commands in the background
- log their output in a helpful way
- show when they command and if they were successful
#!/bin/bash
set -euo pipefail
LOG_FILEPATHS_ARRAY=()
PIDS_ARRAY=()
process_pids () {
log_prefix=${1:-}
echo ""
echo " waiting"
echo " 👀 tail -f /tmp/$log_prefix.*.output"
echo ""
for pid in "${PIDS_ARRAY[@]}"
do
werr=0
wait $pid || werr=$?
if [ "$werr" != 0 ] && [ "$werr" != 127 ] ; then
echo " ℹ failed - ${LOG_FILEPATHS_ARRAY[$pid]}"
else
echo " ✔ ${LOG_FILEPATHS_ARRAY[$pid]}"
fi
done
}
run_command() {
log_prefix=${1:-}
cmd_name=${2:-}
cmd=${3:-}
# log file
log_filepath="/tmp/$log_prefix.$cmd_name.output"
echo " ⌛ start $cmd_name"
# log header
echo "" >> $log_filepath
echo "start $cmd_name..." > $log_filepath
echo $cmd >> $log_filepath
echo "" >> $log_filepath
# go!
$cmd >> $log_filepath 2>&1 &
# save
pid=$!
PIDS_ARRAY+=($pid)
LOG_FILEPATHS_ARRAY[$pid]=$log_filepath
}
Now you can execute long running commands and watch them as they finish
log_prefix="do.something"
run_command $log_prefix "cmd1" "echo 'cmd1 done'"
run_command $log_prefix "cmd2" "echo 'cmd2 done'"
process_pids
and you'll get output like this
⌛ start cmd1
⌛ start cmd2
waiting
👀 tail -f /tmp/.*.output
✔ /tmp/do.something.cmd1.output
ℹ failed - /tmp/do.something.cmd2.output
The tail command allows you to watch all the processes execute in real time.
When a command succeeds you see a "✔" and when one fails "ℹ failed".