A Tutorial on Shell Scripting
Below is a step-by-step tutorial on Bash shell scripting. It starts with basic concepts and gradually moves on to more advanced topics and best practices. By the end, you should have a solid understanding of how to create and run scripts in a Linux/Unix environment, and how to write shell scripts to automate tasks.
1. What is a Shell and What is Bash?
-
Shell: In Unix-like operating systems, a shell is a command-line interface that allows you to interact with the operating system by typing commands. It interprets the commands you type and runs programs accordingly.
-
Bash: Bash stands for “Bourne-Again SHell.” It is one of the most common, feature-rich, and user-friendly shells. Most Linux distributions and macOS come with Bash (or a variant) as the default shell.
Bash scripting refers to writing a series of commands into an executable file that the Bash shell can interpret. This enables you to automate repetitive tasks, run batch jobs, manage systems, and build powerful command-line “apps.”
2. Creating and Running Your First Script
- Create a new file. You can use any text editor (e.g.,
nano
,vim
, orgedit
). For example: - Add the shebang line. The shebang
#!/bin/bash
tells the system which interpreter should run the script. So, your script could be: - Make the script executable. You need to set executable permission:
- Run the script. Run it in your terminal:
You should see
Hello, World!
displayed on the screen.
3. Basic Bash Script Structure
A typical Bash script has the following structure:
#!/bin/bash
# Comments describing the script’s purpose
# Author, date, version, etc. (optional but recommended)
# 1. Define variables
# 2. Use shell built-ins, commands, and control structures
# 3. Return (exit) an appropriate status code
Key points:
- Start every script with #!/bin/bash
or your preferred shell’s path.
- Use comments (#
) to describe code, increase readability, and clarify logic.
- Keep the script as simple and modular as possible.
4. Variables and Parameters
4.1. Defining Variables
You can define variables without a type; by default, they are strings, but they can be treated as numbers when needed.
- No spaces around
=
when defining variables. - Access variables with
$variable_name
(or${variable_name}
when more explicit syntax is needed).
4.2. Environment Variables
Environment variables like HOME
, PATH
, and USER
are automatically defined by the system. You can access them as with any variable:
4.3. Command Line Arguments
Command line arguments are variables accessed by position:
- $0
: The name of the script
- $1
, $2
, ...: The arguments passed to the script
- $#
: The number of arguments
- $@
: All arguments as a list
Example:
#!/bin/bash
echo "Script name: $0"
echo "First argument: $1"
echo "Second argument: $2"
echo "Number of arguments: $#"
echo "All arguments: $@"
If you run ./my_script.sh arg1 arg2
, you will see the assigned values for each positional parameter.
5. Working with User Input
Sometimes, you’ll want to prompt the user for input rather than passing arguments on the command line. You can do this using the read
command:
-
-p
option allows you to specify a prompt inline: -
-s
option hides user input (useful for passwords):
6. Conditional Statements
Bash supports if-then-else
, elif
(else-if), and case
statements.
6.1. if
Statements
When checking conditions, you can use:
- -z
to check if a string is empty
- -n
to check if a string is not empty
- -eq
, -ne
, -lt
, -le
, -gt
, -ge
for numeric comparisons
- ==
, !=
for string comparisons (within [[ ]]
)
Example:
#!/bin/bash
# Check if a number is positive, negative, or zero
read -p "Enter a number: " num
if [ $num -gt 0 ]; then
echo "Positive"
elif [ $num -lt 0 ]; then
echo "Negative"
else
echo "Zero"
fi
Tip: Use
[[ ... ]]
over[ ... ]
for more reliable scripting (e.g., easier string handling, pattern matching).
6.2. case
Statements
A case
statement is often cleaner than multiple if-elif
statements:
#!/bin/bash
read -p "Enter a letter (a/b/c): " letter
case $letter in
a)
echo "You entered 'a'"
;;
b)
echo "You entered 'b'"
;;
c)
echo "You entered 'c'"
;;
*)
echo "Unknown letter"
;;
esac
7. Loops
7.1. for
Loops
Bash has multiple ways to write a for
loop:
Iterate over a list:
Traditional C-style loop (requires (( ))
):
7.2. while
Loops
while
loops iterate as long as a condition is true:
#!/bin/bash
count=1
while [ $count -le 5 ]
do
echo "Count = $count"
((count++)) # increment the counter
done
7.3. until
Loops
until
loops are like while
, but they continue until a condition is true:
8. Functions in Bash
Functions let you organize your script into reusable blocks of code.
#!/bin/bash
# Function definition
function greet {
local name=$1
echo "Hello, $name!"
}
# Using the function
greet "Alice"
greet "Bob"
- You can define a function either with
function name { ... }
orname() { ... }
. - Use
local
to limit a variable’s scope to within the function. - Functions can return status codes using
return
, and you can retrieve output with command substitution (e.g.,value=$(my_function)
).
9. Command Substitution and Arithmetic
9.1. Command Substitution
You can capture the output of a command to a variable using backticks `command`
or $(command)
:
$( ... )
is preferred for clarity and nesting capability.
9.2. Arithmetic
Bash supports arithmetic expansion in the form of $(( expression ))
:
10. Working with Files and Directories
10.1. Checking File Existence and Permissions
You can use test operators (inside [ ]
or [[ ]]
) to check file properties:
-e file
: file exists-f file
: file is a regular file-d file
: file is a directory-r file
: file is readable-w file
: file is writable-x file
: file is executable
Example:
#!/bin/bash
file="data.txt"
if [ -e "$file" ]; then
echo "$file exists."
else
echo "$file does not exist."
fi
10.2. Reading Files in a Loop
You can read a file line-by-line:
-IFS=
sets the “Internal Field Separator” to an empty value, preserving leading/trailing whitespace.
11. Redirecting Output and Using Pipes
- Redirection:
>
overwrites a file.>>
appends to a file.2>
redirects error messages to a file.-
&>
redirects both standard output and errors. -
Pipes: Use the pipe operator
|
to send the output of one command to another:
12. Error Handling and Debugging
12.1. Exit Status
- Each command in Bash returns an exit status code (
0
for success, non-zero for error). - You can check the exit status of the last command using
$?
.
Example:
#!/bin/bash
some_command
if [ $? -ne 0 ]; then
echo "some_command failed!"
exit 1
fi
echo "some_command succeeded!"
12.2. set
Options
You can enable certain shell options to improve error handling:
set -e
: Exit immediately if a command exits with a non-zero status.set -u
: Treat unset variables as an error and exit immediately.set -x
: Print commands and their arguments as they are executed (useful for debugging).set -o pipefail
: Causes a pipeline to return the exit status of the last command that had a non-zero exit status.
You can combine them:
13. Best Practices
- Use meaningful variable names. This makes scripts more readable.
- Quote variables. In most cases, use
"$variable"
to prevent word splitting, especially with file paths that might contain spaces. - Use comments. Explain why you do something rather than what you’re doing (the code itself is the “what”).
- Check exit statuses of critical commands to handle errors robustly.
- Modularize. Use functions to group related tasks.
- ShellCheck. Consider using ShellCheck (if available) to lint your scripts and detect common pitfalls and mistakes.
14. Example: A Simple Backup Script
Below is a simple backup script that demonstrates many of the concepts covered:
#!/bin/bash
# A simple backup script to copy a directory to a backup folder with a timestamp
set -euo pipefail
SOURCE_DIR="$1"
BACKUP_DIR="$2"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
# Check if the source directory exists
if [ ! -d "$SOURCE_DIR" ]; then
echo "Error: Source directory $SOURCE_DIR does not exist." >&2
exit 1
fi
# Create backup directory if it doesn't exist
mkdir -p "$BACKUP_DIR"
# Construct the backup destination path
DESTINATION="$BACKUP_DIR/$(basename "$SOURCE_DIR")-$TIMESTAMP"
# Perform the backup using cp -r
cp -r "$SOURCE_DIR" "$DESTINATION"
echo "Backup of $SOURCE_DIR completed at $DESTINATION"
exit 0
How to run it:
- Takes command line arguments for the source and backup directories.
- Checks if the source directory exists.
- Creates the backup directory if it doesn’t exist.
- Copies (recursively) the source directory into a timestamped folder.
- Prints a success message.
Conclusion
Bash shell scripting is a powerful way to automate tasks and manage your Linux/Unix systems more efficiently. In this tutorial, we covered:
- Creating and running scripts (shebang, permissions)
- Variables, environment variables, and command-line arguments
- User input handling
- Conditionals (
if
,case
) - Loops (
for
,while
,until
) - Functions
- File operations
- Redirection and pipes
- Error handling and debugging
- Best practices
With these foundational skills, you can build a variety of tools – from simple automation scripts to more complex system management scripts and utility programs. Experiment, explore the Bash manual pages (e.g., man bash
), and consider additional resources such as the advanced features in Bash scripting to continue learning.
Happy scripting!