Shell Scripting Fundamentals

ShellBeginner
Practice Now

Introduction

A shell script stores commands in a file so a task can be repeated consistently. Reliable beginner scripts make their interpreter explicit, quote variable expansions, validate input before acting, and return an exit status that other commands can understand.

In this lab, you will build small Bash scripts incrementally. You will run scripts through Bash and as executable files, use variables and positional arguments, read interactive input, branch with if and file tests, choose exit codes, iterate with for, and combine the ideas in a simple log summarizer.

Create and Run Your First Script

In this step, you will create a Bash script, run it through an interpreter, and make it directly executable.

Enter the lab workspace:

cd /home/labex/project/shell-lab

Create hello.sh with a here-document. The first line is a shebang that selects Bash when the file is executed directly:

cat > hello.sh <<'EOF'
#!/bin/bash

echo "Hello from a Bash script"
pwd
EOF

Inspect the file before running it:

cat hello.sh

Pass the file to Bash explicitly. This works even before the executable permission is added:

bash hello.sh

Add executable permission and inspect the mode:

chmod u+x hello.sh
ls -l hello.sh

Execute the file from the current directory. ./ supplies a path; the shell does not normally search the current directory automatically:

./hello.sh

Use Variables and Safe Quoting

In this step, you will assign variables, expand them, and see why double quotes protect values containing spaces.

Create a script with two variables:

cat > variables.sh <<'EOF'
#!/bin/bash

course="Linux for Noobs"
workspace="/home/labex/project/shell-lab"

echo "Course: $course"
echo "Workspace: $workspace"
printf 'Quoted argument count: %s\n' "$#"
printf 'First argument: %s\n' "${1:-not provided}"
EOF

No spaces surround = in a shell assignment. Double quotes allow $course to expand while keeping Linux for Noobs as one value.

Run the script with one argument that contains spaces:

bash variables.sh "beginner learner"

The special variable $# is the number of positional arguments, and $1 is the first argument. ${1:-not provided} supplies a fallback when it is absent.

Save the output for inspection:

bash variables.sh "beginner learner" > variable-output.txt
cat variable-output.txt

Validate Positional Arguments

In this step, you will require an argument, print usage guidance, and return different exit statuses for failure and success.

Create greet-argument.sh:

cat > greet-argument.sh <<'EOF'
#!/bin/bash

if [ "$#" -ne 1 ]; then
  echo "Usage: $0 NAME" >&2
  exit 2
fi

name=$1
echo "Hello, $name"
exit 0
EOF

The test command [ ... ] checks whether the argument count is not equal to one. >&2 sends usage text to standard error, and exit 2 reports invalid usage.

Run it without an argument, then immediately display $?, the previous command's exit status:

bash greet-argument.sh
echo "missing argument status: $?"

The expected status is 2. Run it correctly:

bash greet-argument.sh "Ada Lovelace"
echo "valid argument status: $?"

Save the successful greeting:

bash greet-argument.sh "Ada Lovelace" > argument-greeting.txt

Read Interactive Input

In this step, you will prompt a user, read one line safely, and handle empty input.

Create ask-name.sh:

cat > ask-name.sh <<'EOF'
#!/bin/bash

read -r -p "Enter your name: " name

if [ -z "$name" ]; then
  echo "A name is required" >&2
  exit 1
fi

echo "Welcome, $name"
EOF

read -r reads a line without treating backslashes specially. -p displays a prompt, and -z tests whether the quoted value is empty.

Run the script:

bash ask-name.sh

At the prompt, type:

Grace Hopper

You should see Welcome, Grace Hopper. For a reproducible saved result, pipe one input line into the same script:

printf 'Grace Hopper\n' | bash ask-name.sh > interactive-greeting.txt
cat interactive-greeting.txt

Branch with File Tests

In this step, you will use if, elif, and else with file tests to classify a path.

Create classify-path.sh:

cat > classify-path.sh <<'EOF'
#!/bin/bash

path=${1:-}

if [ -z "$path" ]; then
  echo "Usage: $0 PATH" >&2
  exit 2
elif [ -d "$path" ]; then
  echo "directory: $path"
elif [ -f "$path" ]; then
  echo "file: $path"
else
  echo "missing: $path" >&2
  exit 1
fi

exit 0
EOF

-d tests for a directory and -f tests for a regular file. The order matters: invalid usage is handled before path type.

Try all three path outcomes:

bash classify-path.sh logs
bash classify-path.sh notes.txt
bash classify-path.sh missing.txt
echo "missing path status: $?"

Save successful results together. && runs the second command only if the first succeeds:

bash classify-path.sh logs > path-results.txt && bash classify-path.sh notes.txt >> path-results.txt
cat path-results.txt

Process Multiple Files with a For Loop

In this step, you will iterate over log files and count matching records in each one.

Create count-errors.sh:

cat > count-errors.sh <<'EOF'
#!/bin/bash

log_dir=${1:-logs}

if [ ! -d "$log_dir" ]; then
  echo "Log directory not found: $log_dir" >&2
  exit 1
fi

for log_file in "$log_dir"/*.log; do
  error_count=$(grep -c '^ERROR ' "$log_file" || true)
  printf '%s errors=%s\n' "$(basename "$log_file")" "$error_count"
done
EOF

The loop assigns one matched path to log_file on each iteration. Quoting "$log_file" protects spaces, while basename removes the directory prefix. grep -c counts matching lines; || true prevents zero matches from stopping a larger strict workflow.

Run the loop and save its output:

bash count-errors.sh logs | tee error-counts.txt

You should see one result for each .log file.

Combine Validation, Loops, and Exit Status

In this step, you will build one reusable script that validates a directory, summarizes its logs, and reports success or failure clearly.

Create summarize-logs.sh:

cat > summarize-logs.sh <<'EOF'
#!/bin/bash

input_dir=${1:-}
output_file=${2:-}

if [ -z "$input_dir" ] || [ -z "$output_file" ]; then
  echo "Usage: $0 INPUT_DIR OUTPUT_FILE" >&2
  exit 2
fi

if [ ! -d "$input_dir" ]; then
  echo "Input directory not found: $input_dir" >&2
  exit 1
fi

: > "$output_file"

for log_file in "$input_dir"/*.log; do
  lines=$(wc -l < "$log_file")
  errors=$(grep -c '^ERROR ' "$log_file" || true)
  printf '%s lines=%s errors=%s\n' "$(basename "$log_file")" "$lines" "$errors" >> "$output_file"
done

echo "Wrote summary to $output_file"
exit 0
EOF

The command : > "$output_file" safely creates or empties the destination before the loop. Run the script with both required arguments:

chmod u+x summarize-logs.sh
./summarize-logs.sh logs log-summary.txt
echo "summary status: $?"
cat log-summary.txt

Test the failure path with a missing directory:

./summarize-logs.sh missing log-summary.txt
echo "missing directory status: $?"

The success status should be 0, while the missing-directory status should be 1.

Summary

You created and executed Bash scripts, used shebangs and permissions, expanded quoted variables, read positional and interactive input, and returned meaningful exit statuses. You also branched with if/elif/else, tested files and directories, iterated over logs with for, and combined the ideas in a reusable summarizer.

These fundamentals make later automation easier to read, safer to rerun, and easier for other commands or scheduled jobs to evaluate.