Unit 3: Bash Scripting Fundamentals

CSC104 — It Fundamentals 11 min read

I. Orientation

Bash, the Bourne Again Shell, is a command interpreter and scripting language developed for the GNU Project (1989). It executes commands interactively in a terminal or reads them from text files called scripts, making it useful for automation, system administration, file processing, and combining command-line utilities.

  • Execution model: Bash reads commands, performs expansions and substitutions, executes them, and records an exit status.
  • Command order: Statements normally run sequentially from top to bottom unless conditionals, loops, or functions alter the flow.
  • Case sensitivity: name, Name, and NAME are different identifiers.
  • Shell syntax: Spaces, quotation marks, semicolons, and line breaks have syntactic meaning.
  • Portability: Code written specifically for Bash may use features unavailable in simpler POSIX shells such as sh.
  • Core convention: A status of 0 means success; a non-zero status indicates failure or another exceptional result.

II. Script Foundations — Creating and Organizing Programs

A. Introduction to Bash scripting

Bash scripting stores shell commands in a file so that a repeatable task can be executed as one program.

  • Purpose: Scripts automate operations such as backups, software setup, log analysis, and file renaming.
  • Interpreter: The bash program processes Bash syntax and launches external commands such as cp, grep, and find.
  • Execution methods:
    • bash report.sh runs the file through Bash without requiring execute permission.
    • ./report.sh uses the script's shebang and requires execute permission.
  • Important distinction: Running ./report.sh starts a child shell, whereas source report.sh executes commands in the current shell.

B. Bash script structure

A Bash script commonly contains a shebang, comments, variable definitions, commands, control structures, and an explicit exit.

  • Shebang: The first line selects the interpreter.
BASH
#!/usr/bin/env bash
  • Main body: Commands and statements appear after the shebang in their required execution order.
  • Exit statement: exit 0 explicitly reports successful completion.
  • Readable layout: Indentation is not generally syntactic, but consistently indenting conditional and loop bodies exposes their structure.
  • Safety options: set -u treats unset variables as errors; set -e exits in many, but not all, failure contexts.

C. Hello World

A Hello World script demonstrates the minimum structure needed to produce output.

  • Output command: printf provides predictable formatting, while echo is convenient for simple text.
  • Example:
BASH
#!/usr/bin/env bash
printf '%s\n' 'Hello, World!'
  • Format meaning: In printf '%s\n', %s accepts a string and \n adds a newline.
  • Execution: After chmod +x hello.sh, the command ./hello.sh prints Hello, World!.

D. Comments

Comments document intent and are ignored by Bash during normal execution.

  • Single-line syntax: Text following # is a comment, except when #! forms the opening shebang.
BASH
# Remove reports older than seven days.
find reports -type f -mtime +7 -delete
  • Placement: Comments may occupy a complete line or follow a command, provided # is not quoted.
  • Good practice: Explain why a command exists or why an unusual option is needed, rather than merely restating its syntax.
  • No native block syntax: Bash has no dedicated multiline comment operator; consecutive # lines are clearest.

E. Basic script creation

Basic script creation involves writing the file, granting suitable permissions, and executing it through Bash.

  • Create and edit: Use an editor such as nano task.sh or vim task.sh.
  • Add content:
BASH
#!/usr/bin/env bash
printf 'Current directory: %s\n' "$PWD"
  • Validate syntax: bash -n task.sh checks parsing without executing ordinary commands.
  • Make executable: chmod u+x task.sh grants the owner execute permission.
  • Run safely: ./task.sh runs the file from the current directory; the ./ prevents Bash from searching only PATH.

III. Data and Input — Working with Values

A. Variables

Variables associate names with values and are assigned without spaces around =.

  • Assignment: course="IT Fundamentals" stores a string in course.
  • Expansion: $course or ${course} retrieves the value; braces clarify boundaries in expressions such as "${course}_notes".
  • Quotation: "$course" preserves spaces and prevents filename expansion.
  • Command substitution: today=$(date +%F) stores command output.
  • Arithmetic: total=$((price * quantity)) evaluates integer arithmetic.
  • Environment variables: export MODE="production" makes MODE available to child processes.

B. User input

The read builtin collects text from standard input and stores it in one or more variables.

  • Prompting: read -r -p "Name: " name displays a prompt and assigns the response to name.
  • Backslashes: -r prevents backslashes from being treated as escape characters and is normally recommended.
  • Silent input: read -r -s password hides typed characters, which is useful for secrets.
  • Timed input: read -r -t 10 answer waits at most ten seconds.
  • Validation: Input remains text unless the script checks its format or uses it in an arithmetic context.

C. Arguments

Arguments provide values when a script is launched, as in ./copy.sh source.txt backup.txt.

  • Positional parameters: $1 is the first argument, $2 the second, and ${10} the tenth.
  • Script identity: $0 contains the invoked script name.
  • Argument count: $# gives the number of supplied arguments.
  • All arguments: "$@" expands to separate quoted arguments and is safer for iteration than $*.
  • Argument removal: shift discards $1 and moves later arguments down one position.
  • Required value: ${1:?Usage: copy.sh SOURCE} terminates with a message when $1 is absent or empty.

D. Arrays

Bash supports indexed arrays and associative arrays for storing multiple related values.

  1. Indexed arrays: Numeric subscripts begin at 0.
BASH
servers=("web" "database" "cache")
printf '%s\n' "${servers[1]}"
  1. Associative arrays: String keys require declare -A.
BASH
declare -A ports=([http]=80 [https]=443)
printf '%s\n' "${ports[https]}"
  • Expansion: "${servers[@]}" expands every element separately, while "${#servers[@]}" returns the element count.
  • Iteration: for server in "${servers[@]}" preserves each element exactly.

E. String slicing

Bash parameter expansion can extract a substring without launching an external program.

  • Syntax: ${value:offset:length} selects length characters starting at zero-based offset.
  • Example:
BASH
code="BASH2025"
printf '%s\n' "${code:0:4}"   # BASH
  • Omitted length: ${code:4} returns all characters from position 4 onward.
  • Negative offset: ${code: -4} selects the final four characters; the space prevents confusion with the :- default-value operator.
  • Limitation: Slicing is character-oriented under the active locale and does not perform pattern matching.

IV. Decisions and Status — Controlling Execution

A. Conditional expressions (file, string, arithmetic)

Conditional expressions test system state or values and return an exit status used by control structures.

  1. File expressions: [[ -f $path ]] tests for a regular file, -d for a directory, -e for existence, and -r for readability.
  2. String expressions: [[ -z $text ]] tests for an empty string, while [[ $role == admin ]] compares values.
  3. Arithmetic expressions: (( count >= 10 )) succeeds when the integer comparison is true.
  • Preferred syntax: [[ ... ]] is safer and more expressive in Bash than the older [ ... ] command.
  • Operators: Within [[ ]], use == for string comparison; within (( )), use operators such as ==, <, and >= numerically.

B. Exit status

Every command produces an integer exit status between 0 and 255.

  • Success convention: 0 represents success, while non-zero values represent failure or a special condition.
  • Latest status: $? contains the status of the most recently completed command.
BASH
grep -q "ERROR" app.log
status=$?
  • Explicit return: exit 2 ends a script with status 2; return 2 leaves a function.
  • Command chaining: command1 && command2 runs the second command after success, whereas command1 || command2 runs it after failure.
  • Preservation: Store $? immediately because any subsequent command replaces it.

C. If-else statements

An if statement executes different command blocks according to a condition's exit status.

  • Structure:
BASH
if [[ -f $file ]]; then
    printf '%s\n' "File found"
elif [[ -d $file ]]; then
    printf '%s\n' "Directory found"
else
    printf '%s\n' "Path not found"
fi
  • Condition: Bash executes the first branch whose test returns 0.
  • Keywords: then begins a branch, elif adds another condition, else provides a fallback, and fi closes the statement.
  • Quotation: Variables used in ordinary commands should remain quoted even when [[ ... ]] provides safer expansion rules.

D. Switch-case statements

A case statement selects a branch by matching one value against shell patterns.

  • Structure:
BASH
case $command in
    start) start_service ;;
    stop)  stop_service ;;
    *)     printf '%s\n' "Unknown command" >&2 ;;
esac
  • Patterns: Alternatives may be combined as yes|y|Y, and * acts as the default match.
  • Termination: ;; ends the selected branch, and esac closes the statement.
  • Best use: case is clearer than repeated if tests when one value has several possible forms.

V. Repetition and Reuse — Structuring Operations

A. Loops (for, while, until)

Loops repeat commands over values or while a condition has a particular status.

  1. For loop: Iterates over a known list.
BASH
for file in *.log; do
    printf '%s\n' "$file"
done
  1. While loop: Repeats while its condition succeeds, as in while (( count < 5 )).
  2. Until loop: Repeats while its condition fails, as in until [[ -f ready.flag ]].
  • Loop body: do begins repeated commands and done closes them.
  • Input loop: while IFS= read -r line; do ...; done < file safely processes a file line by line.
  • Termination: A loop must eventually change the state tested by its condition unless continuous execution is intentional.

B. Break and continue statements

break and continue alter the normal progression of a loop.

  1. Break: break immediately exits the current loop; break 2 exits two nested loop levels.
  2. Continue: continue skips the remaining body and begins the next iteration; continue 2 targets the next iteration of an outer loop.
  • Concrete use: [[ $line == STOP ]] && break ends input processing at a sentinel.
  • Filtering use: [[ -z $line ]] && continue skips empty lines.
  • Control clarity: These statements should represent obvious stopping or skipping rules, not replace a well-defined loop condition.

C. Functions

Functions group reusable commands under a name and execute in the current shell environment.

  • Definition:
BASH
greet() {
    local name=$1
    printf 'Hello, %s\n' "$name"
}
  • Invocation: greet "Asha" calls the function with "Asha" as $1.
  • Local variables: local name prevents the function from unintentionally replacing a global variable.
  • Result data: Functions can print data for capture with result=$(function_name).
  • Status result: return 0 reports success; return values are statuses, not general strings or large integers.

VI. Development and Shell Customization — Efficient, Persistent Workflows

A. Debugging

Debugging combines syntax checks, execution tracing, status inspection, and deliberate diagnostics.

  • Syntax check: bash -n script.sh identifies parsing errors without normal execution.
  • Trace mode: bash -x script.sh prints expanded commands before running them.
  • Local tracing: set -x enables tracing inside a script, and set +x disables it.
  • Diagnostics: printf 'value=%q\n' "$value" >&2 exposes special characters and sends output to standard error.
  • Common defects: Unquoted variables, incorrect spacing around =, missing fi or done, and overwritten $? frequently cause failures.
  • Secret protection: Disable tracing before commands containing passwords, tokens, or private keys.

B. Shortcuts

Bash and terminal keyboard shortcuts accelerate command-line editing and history navigation.

  • Movement: Ctrl+A moves to the line start, Ctrl+E to the end, and Alt+B or Alt+F moves by words.
  • Editing: Ctrl+U deletes to the line start, Ctrl+K deletes to the end, and Ctrl+W deletes the preceding word.
  • History: Ctrl+R searches command history; the Up and Down arrows move through entries.
  • Process control: Ctrl+C sends an interrupt, while Ctrl+Z suspends the foreground process.
  • Completion: Tab completes command names and paths or shows possible matches.

C. Custom commands

A custom command is usually an executable script or function made discoverable through the shell's PATH.

  • Executable script: A file with a shebang and execute permission can act like a standard command.
  • Command directory: Placing cleanup in $HOME/bin allows direct use when that directory is in PATH.
  • PATH lookup: Bash searches directories from left to right; command -v cleanup reveals the selected definition.
  • Naming: Avoid names that unintentionally replace important system commands.
  • Arguments: Custom commands should accept positional parameters, validate them, quote expansions, and return meaningful statuses.

D. Persistent changes

Persistent shell changes are stored in startup files so they are loaded in future sessions.

  • Interactive configuration: Bash commonly reads ~/.bashrc for interactive non-login shells.
  • Login configuration: Login shells may read ~/.bash_profile, ~/.bash_login, or ~/.profile, depending on which exists.
  • PATH example:
BASH
export PATH="$HOME/bin:$PATH"
  • Apply immediately: source ~/.bashrc reloads the file in the current shell.
  • Scope: User startup files affect one account; system files such as /etc/profile can affect multiple users.
  • Careful editing: A syntax error in a startup file can disrupt every newly opened shell.

E. Aliases

Aliases replace a command word with predefined text and are best suited to short interactive conveniences.

  • Definition: alias ll='ls -alF' creates the command ll.
  • Inspection: alias ll displays one definition; alias lists all current aliases.
  • Removal: unalias ll removes it from the current shell.
  • Persistence: Put alias definitions in ~/.bashrc and reload that file.
  • Limitation: Aliases do not accept parameters as structured functions do; use a function or script for argument handling, branching, or substantial logic.
  • Bypassing: command ls or \ls invokes ls without applying an alias named ls.