Skip to main content
Updated Feb 16, 2026

Bash Scripting Foundations

In Lesson 5, you learned to keep terminal sessions alive across disconnections with tmux. Now you'll capture your knowledge as executable scripts -- reusable automation that runs the same way every time.

Every Digital FTE deployment involves a repeatable sequence: create directories, install dependencies, configure services, verify everything works. Typing these commands manually each time is slow and error-prone. One mistyped path or forgotten step can leave an agent partially deployed, silently broken.

Bash scripts solve this by encoding your deployment knowledge as executable files. A script captures the exact sequence, handles errors gracefully, and runs identically whether you execute it at 2 PM or 2 AM. By the end of this lesson, you'll write scripts that create agent workspaces, validate their own execution, and adapt to different environments through variables and functions.


Your First Script: Three Lines

Every bash script starts with three elements: a shebang line, a command, and a way to run it.

Create a file called hello-agent.sh:

cat > /tmp/hello-agent.sh << 'EOF'
#!/bin/bash
echo "Agent deployment starting..."
echo "Timestamp: $(date)"
EOF

Output:

(no output -- the file was created silently)

The file exists but cannot run yet. Try executing it:

/tmp/hello-agent.sh

Output:

bash: /tmp/hello-agent.sh: Permission denied

Scripts need executable permission. Grant it with chmod +x:

chmod +x /tmp/hello-agent.sh
/tmp/hello-agent.sh

Output:

Agent deployment starting...
Timestamp: Sun Feb 9 14:30:00 UTC 2026

Three things made this work:

  • #!/bin/bash -- The shebang tells the system which interpreter runs this file. Without it, the system doesn't know this is a bash script.
  • chmod +x -- Adds executable permission so the file can be run as a program.
  • $(date) -- Command substitution captures the output of date and inserts it into the string.

Variables and Quoting

Hard-coded values break when anything changes. Variables make scripts flexible.

cat > /tmp/setup-agent.sh << 'EOF'
#!/bin/bash

AGENT_NAME="customer-support"
AGENT_DIR="/tmp/agents/${AGENT_NAME}"
LOG_DIR="${AGENT_DIR}/logs"

echo "Setting up ${AGENT_NAME}..."
echo "Directory: ${AGENT_DIR}"

mkdir -p "${AGENT_DIR}" "${LOG_DIR}"

echo "Created workspace for ${AGENT_NAME}"
EOF

chmod +x /tmp/setup-agent.sh
/tmp/setup-agent.sh

Output:

Setting up customer-support...
Directory: /tmp/agents/customer-support
Created workspace for customer-support

Verify the directories were created:

ls -R /tmp/agents/customer-support

Output:

/tmp/agents/customer-support:
logs

/tmp/agents/customer-support/logs:

Why Quoting Matters

Always wrap variable expansions in double quotes. Without quotes, paths containing spaces break:

# WRONG -- breaks on spaces
DIR=/tmp/my agent
mkdir $DIR

# RIGHT -- preserves the full path
DIR="/tmp/my agent"
mkdir "${DIR}"

The ${VAR} syntax with curly braces is clearest because it shows exactly where the variable name ends. Compare $AGENT_NAMElog (bash looks for variable AGENT_NAMElog) with ${AGENT_NAME}log (bash uses AGENT_NAME and appends log).

Environment Variables

Your scripts can read values set outside the script:

AGENT_NAME="sales-bot" /tmp/setup-agent.sh

Output:

Setting up sales-bot...
Directory: /tmp/agents/sales-bot
Created workspace for sales-bot

Same script, different agent -- no editing required.


Error Handling: set -euo pipefail

Without error handling, scripts continue running after failures. This is dangerous for deployments.

The Problem

cat > /tmp/broken.sh << 'EOF'
#!/bin/bash
cd /nonexistent/path
echo "This prints even though cd failed!"
rm -rf important-files/
EOF

chmod +x /tmp/broken.sh
/tmp/broken.sh

Output:

/tmp/broken.sh: line 2: cd: /nonexistent/path: No such file or directory
This prints even though cd failed!

The script kept running after cd failed. In a real deployment, subsequent commands would execute in the wrong directory.

The Solution

Add set -euo pipefail immediately after the shebang:

cat > /tmp/safe.sh << 'EOF'
#!/bin/bash
set -euo pipefail

cd /nonexistent/path
echo "This never prints"
EOF

chmod +x /tmp/safe.sh
/tmp/safe.sh

Output:

/tmp/safe.sh: line 4: cd: /nonexistent/path: No such file or directory

The script stopped immediately. No further commands executed.

Each flag prevents a different class of bug:

FlagPreventsExample
set -eContinuing after errorscd /wrong/path followed by rm -rf *
set -uUsing undefined variablesrm -rf ${TYPO_VAR}/ deleting /
set -o pipefailHiding failures in pipesfailing-cmd | grep "ok" masking the failure
Always Use set -euo pipefail

This single line prevents the most common scripting disasters. Add it to every script you write, right after the shebang line. The one minute it takes to type saves hours of debugging silent failures.


Functions: Reusable Script Logic

When scripts grow beyond 20 lines, functions keep them organized and readable.

Defining and Calling Functions

cat > /tmp/deploy-functions.sh << 'EOF'
#!/bin/bash
set -euo pipefail

create_workspace() {
local agent_name="$1"
local base_dir="/tmp/agents/${agent_name}"

mkdir -p "${base_dir}"/{src,config,logs,data}
echo "Created workspace: ${base_dir}"
}

verify_workspace() {
local agent_name="$1"
local base_dir="/tmp/agents/${agent_name}"

if [[ -d "${base_dir}/src" ]] && [[ -d "${base_dir}/logs" ]]; then
echo "Workspace verified: ${agent_name}"
else
echo "ERROR: Workspace incomplete for ${agent_name}" >&2
return 1
fi
}

# Main execution
create_workspace "analytics-engine"
verify_workspace "analytics-engine"
EOF

chmod +x /tmp/deploy-functions.sh
/tmp/deploy-functions.sh

Output:

Created workspace: /tmp/agents/analytics-engine
Workspace verified: analytics-engine

Key concepts in this script:

  • create_workspace() -- Defines a function. Parentheses are required but always empty in bash.
  • local agent_name="$1" -- local restricts the variable to this function. $1 is the first argument passed to the function.
  • "$1" -- Functions receive arguments positionally: $1 is the first, $2 the second, and so on.
  • return 1 -- Exits the function with error status (non-zero means failure).
  • >&2 -- Sends output to stderr (the error stream) instead of stdout.

Functions with Multiple Arguments

cat > /tmp/multi-deploy.sh << 'EOF'
#!/bin/bash
set -euo pipefail

deploy_agent() {
local name="$1"
local port="$2"
local base="/tmp/agents/${name}"

mkdir -p "${base}"/{src,config,logs}
echo "port: ${port}" > "${base}/config/settings.yaml"
echo "Deployed ${name} on port ${port}"
}

deploy_agent "support-bot" 8000
deploy_agent "sales-bot" 8001
deploy_agent "analytics" 8002
EOF

chmod +x /tmp/multi-deploy.sh
/tmp/multi-deploy.sh

Output:

Deployed support-bot on port 8000
Deployed sales-bot on port 8001
Deployed analytics on port 8002

Verify one of the configurations:

cat /tmp/agents/support-bot/config/settings.yaml

Output:

port: 8000

One function, three deployments. This is how production scripts scale.


Conditionals: Making Decisions

Scripts need to make decisions: Does a directory exist? Is a service running? Did the last command succeed?

if/else with [[ ]] Tests

cat > /tmp/check-workspace.sh << 'EOF'
#!/bin/bash
set -euo pipefail

AGENT_NAME="${1:-customer-support}"
AGENT_DIR="/tmp/agents/${AGENT_NAME}"

if [[ -d "${AGENT_DIR}" ]]; then
echo "${AGENT_NAME}: workspace exists at ${AGENT_DIR}"
FILE_COUNT=$(ls "${AGENT_DIR}" | wc -l)
echo " Contains ${FILE_COUNT} items"
else
echo "${AGENT_NAME}: workspace NOT found"
echo " Run setup script first"
fi
EOF

chmod +x /tmp/check-workspace.sh
/tmp/check-workspace.sh analytics-engine

Output:

analytics-engine: workspace exists at /tmp/agents/analytics-engine
Contains 4 items
/tmp/check-workspace.sh nonexistent-agent

Output:

nonexistent-agent: workspace NOT found
Run setup script first

Key patterns:

  • ${1:-customer-support} -- Uses the first argument if provided, otherwise defaults to customer-support.
  • [[ -d "${AGENT_DIR}" ]] -- Tests if a directory exists. Double brackets [[ ]] are the modern, safer form.
  • $(ls "${AGENT_DIR}" | wc -l) -- Command substitution counts items in the directory.

Common Test Operators

TestMeaning
[[ -d path ]]Directory exists
[[ -f path ]]Regular file exists
[[ -z "${VAR}" ]]Variable is empty
[[ -n "${VAR}" ]]Variable is not empty
[[ "${A}" == "${B}" ]]Strings are equal
[[ ${NUM} -gt 10 ]]Number is greater than 10

Idempotent Directory Creation

A practical pattern -- creating directories only when they don't exist, with feedback:

cat > /tmp/safe-setup.sh << 'EOF'
#!/bin/bash
set -euo pipefail

setup_dir() {
local dir_path="$1"
if [[ -d "${dir_path}" ]]; then
echo "Already exists: ${dir_path}"
else
mkdir -p "${dir_path}"
echo "Created: ${dir_path}"
fi
}

setup_dir "/tmp/agent-ws/logs"
setup_dir "/tmp/agent-ws/config"
setup_dir "/tmp/agent-ws/data"
EOF

chmod +x /tmp/safe-setup.sh
/tmp/safe-setup.sh

Output (first run):

Created: /tmp/agent-ws/logs
Created: /tmp/agent-ws/config
Created: /tmp/agent-ws/data
/tmp/safe-setup.sh

Output (second run):

Already exists: /tmp/agent-ws/logs
Already exists: /tmp/agent-ws/config
Already exists: /tmp/agent-ws/data

The script is idempotent -- safe to run multiple times without side effects.


Loops: Repeating Operations

Loops let scripts process multiple items without duplicating code.

for Loops

Iterate over a list of values:

cat > /tmp/setup-agents.sh << 'EOF'
#!/bin/bash
set -euo pipefail

AGENTS=("support-bot" "sales-bot" "analytics" "moderator")

for agent in "${AGENTS[@]}"; do
mkdir -p "/tmp/agents/${agent}"/{src,config,logs}
echo "Workspace ready: ${agent}"
done

echo "All ${#AGENTS[@]} agents configured"
EOF

chmod +x /tmp/setup-agents.sh
/tmp/setup-agents.sh

Output:

Workspace ready: support-bot
Workspace ready: sales-bot
Workspace ready: analytics
Workspace ready: moderator
All 4 agents configured

Key syntax:

  • AGENTS=("a" "b" "c") -- Declares an array.
  • "${AGENTS[@]}" -- Expands to all elements, each properly quoted.
  • ${#AGENTS[@]} -- The count of elements in the array.

Iterating Over Files

Process all files matching a pattern:

cat > /tmp/count-logs.sh << 'EOF'
#!/bin/bash
set -euo pipefail

LOG_DIR="${1:-.}"

echo "Log file report for: ${LOG_DIR}"
echo "---"

for logfile in "${LOG_DIR}"/*.log; do
if [[ -f "${logfile}" ]]; then
lines=$(wc -l < "${logfile}")
size=$(stat -c%s "${logfile}")
echo "$(basename "${logfile}"): ${lines} lines, ${size} bytes"
fi
done
EOF

chmod +x /tmp/count-logs.sh

Create some test log files and run it:

for i in 1 2 3; do
for j in $(seq 1 $((i * 10))); do
echo "Log entry ${j}" >> "/tmp/agents/support-bot/logs/agent-${i}.log"
done
done

/tmp/count-logs.sh /tmp/agents/support-bot/logs/

Output:

Log file report for: /tmp/agents/support-bot/logs/
---
agent-1.log: 10 lines, 140 bytes
agent-2.log: 20 lines, 290 bytes
agent-3.log: 30 lines, 450 bytes

while Loops

Use while for condition-based repetition:

cat > /tmp/wait-for-file.sh << 'EOF'
#!/bin/bash
set -euo pipefail

TARGET_FILE="${1:-/tmp/agent-ready.flag}"
MAX_WAIT=10
WAITED=0

echo "Waiting for ${TARGET_FILE}..."

while [[ ! -f "${TARGET_FILE}" ]] && [[ ${WAITED} -lt ${MAX_WAIT} ]]; do
sleep 1
((WAITED++))
echo " Waiting... (${WAITED}/${MAX_WAIT}s)"
done

if [[ -f "${TARGET_FILE}" ]]; then
echo "File found after ${WAITED} seconds"
else
echo "ERROR: Timed out after ${MAX_WAIT} seconds" >&2
exit 1
fi
EOF

chmod +x /tmp/wait-for-file.sh

Test it by creating the target file after a delay:

(sleep 3 && touch /tmp/agent-ready.flag) &
/tmp/wait-for-file.sh /tmp/agent-ready.flag

Output:

Waiting for /tmp/agent-ready.flag...
Waiting... (1/10s)
Waiting... (2/10s)
Waiting... (3/10s)
File found after 3 seconds

This pattern is essential for deployment scripts that need to wait for services to become ready before proceeding.


Exercises

Exercise 1: Build an Agent Workspace

Task: Write a script that creates an agent workspace with logs/, config/, and data/ subdirectories.

cat > /tmp/setup-workspace.sh << 'EOF'
#!/bin/bash
set -euo pipefail

WORKSPACE="/tmp/agent-ws"

mkdir -p "${WORKSPACE}"/{logs,config,data}
echo "Workspace created at ${WORKSPACE}"
EOF

chmod +x /tmp/setup-workspace.sh

Verify:

/tmp/setup-workspace.sh && ls /tmp/agent-ws/

Expected output:

Workspace created at /tmp/agent-ws
config data logs

All three directories should appear.

Exercise 2: Add Idempotency and Functions

Task: Enhance the workspace script with set -euo pipefail and a function that checks if a directory exists before creating it. The second run should output "already exists" instead of recreating.

cat > /tmp/setup-workspace-v2.sh << 'EOF'
#!/bin/bash
set -euo pipefail

WORKSPACE="/tmp/agent-ws-v2"

ensure_dir() {
local dir_path="$1"
if [[ -d "${dir_path}" ]]; then
echo "Already exists: $(basename "${dir_path}")"
else
mkdir -p "${dir_path}"
echo "Created: $(basename "${dir_path}")"
fi
}

ensure_dir "${WORKSPACE}/logs"
ensure_dir "${WORKSPACE}/config"
ensure_dir "${WORKSPACE}/data"
echo "Workspace ready at ${WORKSPACE}"
EOF

chmod +x /tmp/setup-workspace-v2.sh

Verify (run twice):

/tmp/setup-workspace-v2.sh
/tmp/setup-workspace-v2.sh

Expected output (second run):

Already exists: logs
Already exists: config
Already exists: data
Workspace ready at /tmp/agent-ws-v2

Exercise 3: Process Log Files with a Loop

Task: Write a loop that processes all .log files in a directory and reports their line counts.

cat > /tmp/report-logs.sh << 'EOF'
#!/bin/bash
set -euo pipefail

LOG_DIR="${1:-.}"

for logfile in "${LOG_DIR}"/*.log; do
if [[ -f "${logfile}" ]]; then
lines=$(wc -l < "${logfile}")
echo "$(basename "${logfile}"):${lines}"
fi
done
EOF

chmod +x /tmp/report-logs.sh

Verify:

mkdir -p /tmp/test-logs
echo -e "line1\nline2\nline3" > /tmp/test-logs/app.log
echo -e "line1\nline2" > /tmp/test-logs/error.log
/tmp/report-logs.sh /tmp/test-logs/

Expected output:

app.log:3
error.log:2

Each line shows filename:linecount for every .log file in the directory.


Try With AI

Write a setup script and get a production review:

Write a basic agent setup script that:
1. Creates a user called agent-runner
2. Creates directories /opt/agent/src, /opt/agent/config, /var/log/agents
3. Installs Python 3 and pip

Then review this script for production reliability. What error
conditions am I not handling? What happens if the disk is full,
the network is down, or the user already exists?

What you're learning: AI identifies failure modes you haven't experienced yet -- disk space exhaustion, network timeouts, duplicate user errors. These are the edge cases that separate development scripts from production-ready automation.

Provide specific constraints and compare outputs:

I need a setup script for a Python FastAPI agent that runs under
user agent-runner, stores logs in /var/log/agents/, and needs
Python 3.11 and uvicorn installed. Write this script with full
error handling (set -euo pipefail) and idempotent operations.

After generating it, tell me: what would happen if I ran this
script on a server where Python 3.11 isn't available in apt?
How should the script handle that case?

What you're learning: Providing precise constraints produces scripts tailored to your exact deployment. Asking "what if" questions surfaces assumptions the initial script makes implicitly.

Iteratively refine edge case handling:

Take the setup script you just generated. I tested it mentally
and found two problems:
1. If the agent-runner user already exists, useradd will fail
2. If /var/log/agents/ has wrong ownership, the agent can't write logs

Add handling for these edge cases. Then tell me what other edge
cases you can think of that we haven't addressed yet.

What you're learning: Iterative refinement catches real deployment issues that initial scripts miss. Each round of "what could go wrong?" makes the script more reliable.

Safety Reminder

Always test scripts in a non-production environment first. Use a VM, container, or test server. A script with a typo in a path variable combined with rm -rf can cause irreversible damage. Preview destructive operations with echo before executing them for real.