Bash Scripting
Why Write Scripts?
Section titled “Why Write Scripts?”Shell scripts automate tasks that would otherwise require typing the same commands repeatedly or trusting humans to remember a procedure:
- Daily system backups
- Software installation and patching across multiple servers
- Periodic system monitoring and health checks
- Raising alarms and sending notifications
- Troubleshooting, auditing, and compliance checks
- Any sequence of steps you want to run consistently and repeatably
A script is simply a text file containing a sequence of commands. The shell executes them one by one - exactly as if you typed them.
Shell Interpreter Choices
Section titled “Shell Interpreter Choices”The interpreter is the program that reads and executes your script. Common choices:
| Interpreter | Path | Notes |
|---|---|---|
bash | /bin/bash | Most common; extensive feature set; default on most Linux systems |
sh | /bin/sh | POSIX-compliant; usually symlinked to bash or dash |
dash | /bin/dash | Faster than bash; fewer features; many distros use for /bin/sh |
zsh | /bin/zsh | Interactive-focused; not ideal for portability |
python3 | /usr/bin/python3 | For complex tasks requiring data structures, network, APIs |
perl | /usr/bin/perl | Powerful text processing; legacy systems |
The available shells on a system are listed in /etc/shells.
First Script
Section titled “First Script”#!/bin/bash# Lines beginning with # are comments (except the shebang)echo "Hello, Linux!"The first line is the shebang (#!) - it tells the kernel which interpreter to use. It’s not a comment - it’s a special directive parsed by the kernel.
# Save as hello.sh, make executable, and run:chmod +x hello.sh./hello.sh # run directlybash hello.sh # run without chmod via bash interpreterReading User Input
Section titled “Reading User Input”#!/bin/bash# Interactive reading of a variableecho "Enter your name:"read nameecho "Hello, $name!"Special Characters
Section titled “Special Characters”| Character | Purpose |
|---|---|
# | Comment (except #! on line 1) |
\ | Line continuation (join next line) |
; | Command separator (run sequentially) |
$ | Variable reference or command substitution prefix |
> | Redirect stdout (overwrite) |
>> | Redirect stdout (append) |
< | Redirect stdin |
| | Pipe output to next command |
& | Run in background |
&& | AND - run next command only if previous succeeded |
|| | OR - run next command only if previous failed |
Command Chaining
Section titled “Command Chaining”make ; make install ; make clean # run all, regardless of success/failuremake && make install && make clean # stop on first failurecat file1 || cat file2 || cat file3 # stop on first success
# Long lines - use backslash continuation:sudo apt-get install autoconf automake bison \ build-essential chrpath curl diffstat \ libsdl1.2-dev libtool lzop make screenReturn Values (Exit Codes)
Section titled “Return Values (Exit Codes)”Every command and script exits with a numeric code:
0= success- Non-zero = failure (specific values vary by program)
ls /etc/passwdecho $? # 0 (success)
ls /nonexistentecho $? # 2 (no such file)$? always holds the exit code of the most-recently-run command.
Use exit codes in scripts to signal success or failure:
#!/bin/bashif [ -f "$1" ]; then echo "File exists" exit 0fiecho "File not found"exit 1Variables
Section titled “Variables”User-Defined Variables
Section titled “User-Defined Variables”name="Alice"count=42greeting="Hello, $name" # variable interpolation in double quotes
echo $nameecho ${name} # brackets allow adjacent text: ${name}s → Alicesecho "$name" # always quote variables to handle spaces safelyScript Parameters
Section titled “Script Parameters”./script.sh arg1 arg2 arg3| Parameter | Value |
|---|---|
$0 | Script name |
$1 | First argument |
$2, $3 … | Second, third argument … |
$* | All arguments as one string |
$@ | All arguments as separate strings |
$# | Number of arguments |
Environment Variables
Section titled “Environment Variables”Variables are local to the current process by default. To pass them to child processes:
export VAR=value # set and export in one stepVAR=value; export VAR # or separately
# Child processes get a COPY - changes in the child don't affect the parentexport PATH="$PATH:/opt/myapp/bin" # add to PATH and exportTyping export with no arguments lists all currently exported variables.
Command Substitution
Section titled “Command Substitution”Capture and use the output of a command:
current_date=$(date +%Y-%m-%d) # preferred modern syntaxkernel_version=$(uname -r)ls /lib/modules/$(uname -r)/ # nested use
old_style=`date` # deprecated backtick syntaxAlways use $() over backticks - it’s readable, nestable, and unambiguous.
Functions
Section titled “Functions”function_name () { commands}
# Example:backup_file () { local src="$1" local dest="${2:-/tmp}" cp "$src" "$dest/$(basename $src).bak" echo "Backed up $src to $dest"}
# Call it:backup_file /etc/hostsbackup_file /etc/nginx/nginx.conf /var/backups- Declare before first use
- Parameters accessed via
$1,$2etc. (same as script params) localkeeps variables scoped to the function - prevents polluting global scope
Conditionals
Section titled “Conditionals”if Statement
Section titled “if Statement”if condition; then commandselif other_condition; then commandselse commandsfi# Test file existenceif [ -f "$1" ]; then echo "File $1 exists"else echo "File $1 does not exist"fi
# Modern double-bracket syntax (preferred):if [[ -f "$file" && -r "$file" ]]; then echo "File exists and is readable"fiThe double-bracket [[ ]] is safer - avoids subtle problems with empty variables and enables &&/|| inside the test.
File Tests
Section titled “File Tests”| Condition | True if |
|---|---|
-e file | File exists (any type) |
-f file | File exists and is a regular file |
-d file | File is a directory |
-s file | File exists and has non-zero size |
-r file | File is readable |
-w file | File is writable |
-x file | File is executable |
-g file | File has sgid set |
-u file | File has suid set |
man 1 test # full list of test conditionsString Comparisons
Section titled “String Comparisons”if [[ "$str1" == "$str2" ]]; then echo "equal"; fiif [[ "$str1" != "$str2" ]]; then echo "not equal"; fiif [[ -z "$str" ]]; then echo "empty string"; fiif [[ -n "$str" ]]; then echo "non-empty string"; fiNumeric Comparisons
Section titled “Numeric Comparisons”if [[ $num -eq 0 ]]; then echo "zero"; fi| Operator | Meaning |
|---|---|
-eq | Equal to |
-ne | Not equal to |
-gt | Greater than |
-lt | Less than |
-ge | Greater than or equal to |
-le | Less than or equal to |
Arithmetic
Section titled “Arithmetic”x=5y=3
echo $((x + y)) # preferred - shell arithmetic expansionecho $((x * y))echo $((x / y)) # integer divisioncount=$((count + 1)) # increment
let x=(1 + 2) # using let built-inexpr 8 + 8 # deprecated - use $((...)) insteadfor Loop
Section titled “for Loop”for var in list; do commandsdone
# Examples:for file in *.log; do echo "Processing $file" gzip "$file"done
for i in {1..10}; do echo "Number $i"done
# C-style:for (( i=0; i<10; i++ )); do echo "$i"donewhile Loop
Section titled “while Loop”while condition; do commandsdone
# Example: read file line by linewhile IFS= read -r line; do echo "$line"done < /etc/hosts
# Countdown:count=5while [[ $count -gt 0 ]]; do echo "T-$count" count=$((count - 1))doneuntil Loop
Section titled “until Loop”The opposite of while - runs while the condition is false:
until condition; do commandsdone
until ping -c1 google.com &>/dev/null; do echo "Waiting for network..." sleep 2doneecho "Network is up"Built-In Shell Commands
Section titled “Built-In Shell Commands”Some commands exist both as external programs in /usr/bin and as bash built-ins:
| Built-in | Notes |
|---|---|
echo | Built-in is slightly different from /bin/echo |
cd | Must be built-in - cannot change parent process’s directory otherwise |
pwd | More efficient as built-in |
read | Always built-in |
printf | More portable than echo for formatted output |
let | Arithmetic |
export | Environment management |
ulimit | Resource limits |
logout | End a login session |
help echo, help read, help cd - get built-in documentation from within bash.