This is a clean, blog-ready learning guide inspired by a Bash shell scripting tutorial page. It keeps the same full learning journey and topic coverage in an original rewritten format: what Bash is, how to write scripts, variables, arguments, input, arrays, conditionals, loops, functions, debugging, and practical scripting habits.
Bash, short for Bourne Again Shell, is both a command interpreter and a scripting language. It was created as a free GNU replacement for the original Bourne shell and is commonly used on Linux, Unix-like systems, macOS, and Windows environments that provide a Unix-style shell layer.
A shell is the layer between you and the operating system. You type instructions into the shell, and it interprets them, runs programs, and returns results. In practice, it is both an interactive command environment and a programmable automation tool.
A Bash script should begin with a shebang so the system knows which interpreter should execute it.
#!/bin/bash
The #! marker introduces the interpreter path. Using a shebang makes script behavior more predictable across environments.
touch hello.sh
chmod +x hello.sh
nano hello.sh
./hello.sh
touch creates an empty file if it does not exist.chmod +x adds execute permission.nano, vim, or VS Code is used to add the script content../hello.sh runs the file from the current directory.#!/bin/bash
# A simple first script
echo "Hello World!"
Comments begin with # and are ignored by the interpreter. The echo command prints text to the terminal.
#!/bin/bash
echo "Hello World!"
whoami
id
ls -la
pwd
uname -a
Bash scripts can run normal terminal commands just like you would type them manually.
# A single-line comment
echo "This executes" # Inline comment
: '
A common multi-line comment workaround.
Useful for temporarily disabling a block.
'
Q: What symbol comments out a line? A: #
Q: What does echo "BishBashBosh" print? A: BishBashBosh
Variables act as named storage for values. In Bash, they are often used for strings, numbers, command results, file paths, and reusable settings across a script.
# Valid examples
name="John"
user_name="john_doe"
USER1="admin"
_temp="tmp"
# Invalid examples
# 1user="john"
# user-name="john"
# user name="john"
=# Correct
name="Enes"
age=24
city="Istanbul"
# Incorrect
# name = "Enes"
# age= 24
# city ="Istanbul"
name="Enes"
echo $name
echo "My name is $name"
name="Enes"
age=24
echo "$name is $age years old"
WORD="script"
echo "${WORD}ing is fun"
Braces are helpful when text appears directly after the variable name or when an expression is more complex.
user=$(whoami)
current_dir=$(pwd)
today=$(date)
file_count=$(ls -1 | wc -l)
echo "Hello $user"
echo "Current directory: $current_dir"
echo "Today is: $today"
echo "Files here: $file_count"
Q: What will echo "$name is $age years old" output if name="Jammy" and age="21"? A: Jammy is 21 years old
Script arguments are values passed when a script runs. They let one script behave differently depending on the input you provide.
./greet.sh John
Inside the script, the first argument is available as $1, the second as $2, and so on.
$0The script name itself.$1 to $9Individual positional arguments.$#Total number of arguments.$@All arguments, individually preserved when quoted.$*All arguments as one combined string.$?Exit status of the last command.#!/bin/bash
echo "Script name: $0"
echo "First argument: $1"
echo "Second argument: $2"
echo "Argument count: $#"
echo "All arguments: $@"
#!/bin/bash
if [[ $# -eq 0 ]]; then
echo "Error: No name provided"
echo "Usage: $0 <name>"
exit 1
fi
name=$1
echo "Hello, $name!"
This style prevents a script from running with missing input and provides a clear usage message.
Q: How do you get the number of supplied arguments? A: $#
Q: How do you get the script filename? A: $0
Q: What does echo $1 $3 print if the script was run with ./script.sh hello hola aloha? A: hello aloha
Bash scripts can pause and collect interactive input from the user. This is commonly done with the read command.
#!/bin/bash
read name
echo "Hello, $name"
read -p "Enter your name: " name
echo "Welcome, $name"
read -s -p "Enter password: " password
echo
# Continue processing
read -p "Enter first and last name: " first last
echo "First: $first"
echo "Last: $last"
Input should be validated before it is trusted. Examples include checking whether a response is empty, verifying file names, and making sure numeric values really are numbers.
read -p "Enter age: " age
if [[ ! $age =~ ^[0-9]+$ ]]; then
echo "Please enter a valid number"
exit 1
fi
Q: If a script asks for input, how do you store it in a variable named test? A: read test
Arrays allow one variable name to hold many values. In standard indexed arrays, each item has a numeric position starting at 0.
transport=("car" "train" "bike" "bus")
echo "First item: ${transport[0]}"
echo "Second item: ${transport[1]}"
echo "All items: ${transport[@]}"
echo "Number of items: ${#transport[@]}"
transport+=("tram")
transport[2]="bicycle"
unset transport[1]
for item in "${transport[@]}"; do
echo "$item"
done
numbers=(1 2 3 4 5 6 7 8 9 10)
echo "${numbers[@]:2:4}" # 3 4 5 6
echo "${numbers[@]:5}" # 6 7 8 9 10
copy=("${numbers[@]}")
fruits=("apple" "banana")
vegetables=("carrot" "broccoli")
food=("${fruits[@]}" "${vegetables[@]}")
echo "${food[@]}"
Bash 4+ supports associative arrays, where values are stored under string keys instead of numeric indexes.
declare -A person
person[name]="John"
person[age]=30
person[city]="New York"
echo "${person[name]}"
echo "Keys: ${!person[@]}"
echo "Values: ${person[@]}"
Conditionals let your script decide what to do based on whether a test succeeds or fails.
if [[ condition ]]; then
# true branch
fi
if [[ condition ]]; then
# true branch
else
# false branch
fi
if [[ condition1 ]]; then
# branch 1
elif [[ condition2 ]]; then
# branch 2
else
# fallback
fi
if [[ "$a" == "$b" ]]; then
echo "Strings are equal"
fi
if [[ -z "$name" ]]; then
echo "Empty string"
fi
if [[ $age -gt 17 ]]; then
echo "Adult"
fi
-eq equal to-ne not equal to-gt greater than-ge greater than or equal to-lt less than-le less than or equal toif [[ -e "$file" ]]; then echo "Exists"; fi
if [[ -f "$file" ]]; then echo "Regular file"; fi
if [[ -d "$dir" ]]; then echo "Directory"; fi
if [[ -r "$file" ]]; then echo "Readable"; fi
if [[ -w "$file" ]]; then echo "Writable"; fi
if [[ -x "$file" ]]; then echo "Executable"; fi
cp "$source" "$dest"
if [[ $? -eq 0 ]]; then
echo "Copy successful"
else
echo "Copy failed"
fi
read -p "Enter a command (start/stop/restart): " cmd
case $cmd in
start)
echo "Starting service..."
;;
stop)
echo "Stopping service..."
;;
restart)
echo "Restarting service..."
;;
*)
echo "Unknown command"
;;
esac
Use case when you are checking many possible values of the same variable. It is often cleaner than a long if/elif chain.
Loops repeat an action. Bash commonly uses for, while, and until loops.
for name in Alice Bob Charlie; do
echo "Hello, $name"
done
for i in {1..5}; do
echo "Number: $i"
done
items=("apple" "banana" "orange")
for item in "${items[@]}"; do
echo "$item"
done
for i in {1..3}; do
for j in {1..3}; do
echo "$i x $j = $((i * j))"
done
done
counter=1
while [[ $counter -le 5 ]]; do
echo "Count: $counter"
((counter++))
done
while read line; do
echo "Line: $line"
done < input.txt
while true; do
echo "Press Ctrl+C to stop"
sleep 1
done
counter=1
until [[ $counter -gt 5 ]]; do
echo "Count: $counter"
((counter++))
done
A while loop keeps going while the condition is true. An until loop keeps going until the condition becomes true.
Functions package reusable logic into named blocks. They improve organization, reduce repetition, and make scripts easier to maintain.
function greet {
echo "Hello from a function"
}
greet
function greet_user {
local name=$1
echo "Hello, $name!"
}
greet_user "John"
greet_user "Alice"
function is_even {
local number=$1
if (( number % 2 == 0 )); then
return 0
else
return 1
fi
}
if is_even 10; then
echo "Even"
fi
function check_file {
local path=$1
if [[ -e "$path" ]]; then
echo "Exists"
else
echo "Not found"
fi
}
function add_numbers {
echo $(($1 + $2))
}
function divide_numbers {
if [[ $# -ne 2 ]]; then
echo "Need exactly 2 arguments"
return 1
fi
if [[ $2 -eq 0 ]]; then
echo "Division by zero is not allowed"
return 1
fi
echo $(($1 / $2))
}
function backup_file {
local source=$1
if [[ ! -f "$source" ]]; then
echo "Source file not found"
return 1
fi
local timestamp=$(date +%Y%m%d_%H%M%S)
local backup="${source}.${timestamp}.backup"
cp "$source" "$backup"
if [[ $? -eq 0 ]]; then
echo "Backup created: $backup"
return 0
else
echo "Backup failed"
return 1
fi
}
function to_uppercase {
echo "$1" | tr '[:lower:]' '[:upper:]'
}
function to_lowercase {
echo "$1" | tr '[:upper:]' '[:lower:]'
}
function string_length {
echo ${#1}
}
function show_date { date; }
function show_users { who; }
function show_disk { df -h; }
function show_menu {
echo "1. Show date"
echo "2. Show users"
echo "3. Show disk usage"
echo "4. Exit"
}
while true; do
show_menu
read -p "Enter choice: " choice
case $choice in
1) show_date ;;
2) show_users ;;
3) show_disk ;;
4) echo "Goodbye!"; exit 0 ;;
*) echo "Invalid option" ;;
esac
read -p "Press Enter to continue..."
done
local variables inside functions.#!/bin/bash
# Script metadata and purpose
# Global variables
# Functions
# Main logic
main() {
echo "Script started"
# Do work here
echo "Script completed"
}
main "$@"
set -e
set -u
set -o pipefail
trap 'echo "Error on line $LINENO"' ERR
These options help a script fail fast and report problems more clearly.
if [[ $# -eq 0 ]]; then
echo "Usage: $0 <filename>"
exit 1
fi
filename=$1
if [[ ! -f "$filename" ]]; then
echo "Error: File does not exist"
exit 1
fi
# Better
user_count=10
backup_directory="/backups"
log_file_path="/var/log/app.log"
# Worse
uc=10
bd="/backups"
lfp="/var/log/app.log"
filename="my file.txt"
# Good
cat "$filename"
# Risky when spaces exist
# cat $filename
function print_header {
echo "================================"
echo "$1"
echo "================================"
}
function show_usage {
cat << EOF
Usage: $0 [OPTIONS] <file>
OPTIONS:
-h, --help Show this help
-v, --verbose Verbose mode
-o, --output Output file
EOF
}
set -x to trace commands as they execute.echo.$? when troubleshooting failures.set -x
# commands here
set +x
eval with user-controlled input.read -p "Enter filename: " filename
if [[ "$filename" =~ [^a-zA-Z0-9._-] ]]; then
echo "Invalid filename"
exit 1
fi
/bin/ls /home/user
Strong Bash scripts are not only about making something work. They should also be readable, safe, predictable, and easy to maintain.
#!/bin/bash
Quote variables
Validate input
Reuse functions
Comment clearly
Handle errors
Test thoroughly
bash -x and ShellCheck.