Shell Scripting & Automation

Your First Bash Script

18 min Lesson 1 of 28

Your First Bash Script

Every production system you will ever work on runs shell scripts — backup jobs, deployment hooks, health checks, log rotation, CI pipeline steps. Bash is not a toy language you learn once and forget; it is the glue that holds infrastructure together at companies like Google, Meta, and Amazon. This lesson builds the foundation: how a script is structured, how the shell finds and executes it, and the habits that separate a throwaway one-liner from a production-grade automation file.

What Is a Shell Script?

A shell script is a plain text file containing a sequence of commands that the shell — most commonly bash — reads and executes line by line. There is no compiler step; the interpreter reads your file directly. That simplicity is its power and its danger: a bad command in a production script runs immediately, with no type checker to warn you.

The Shebang Line

The very first line of every script must declare which interpreter should run it. This two-character sequence #! followed by the interpreter path is called the shebang (also written hashbang).

#!/usr/bin/env bash # This is a comment — ignored by the shell

Why /usr/bin/env bash and not /bin/bash? On most Linux servers /bin/bash works fine, but macOS, Alpine Linux, and some minimal container images place Bash at different paths. Using env delegates the lookup to the system PATH, making the shebang portable across environments — a critical property for scripts that run on developer laptops, CI runners, and production servers alike.

Big-tech practice: Google's internal shell style guide mandates #!/usr/bin/env bash for all portable scripts. Reserve #!/bin/bash only when you are certain of the deployment target and need the explicit path for security reasons (e.g., hardened containers where PATH is deliberately minimal).

If the shebang is missing, the kernel falls back to /bin/sh — which on Debian/Ubuntu is dash, not Bash. Your Bash-specific syntax (arrays, [[, local, process substitution) will silently break. Always include the shebang.

Script Structure: The Anatomy of a Production Script

A well-structured script is immediately readable by anyone on your team — including the on-call engineer at 3 AM who has never seen your code before.

#!/usr/bin/env bash # ============================================================= # deploy-check.sh — pre-flight health check before a release # Author : ops-team@example.com # Created: 2025-01-15 # Usage : ./deploy-check.sh [environment] # ============================================================= # --- configuration ------------------------------------------- ENVIRONMENT="${1:-staging}" HEALTH_URL="https://${ENVIRONMENT}.example.com/health" TIMEOUT=10 # --- main logic ---------------------------------------------- echo "Running pre-flight check for: ${ENVIRONMENT}" echo "Endpoint: ${HEALTH_URL}" # (remaining logic goes here) echo "Done."

Notice the structure: shebang, a header block with purpose and usage, a configuration section, then logic. This layout costs you 30 seconds to write and saves the next engineer 30 minutes of guessing.

Making a Script Executable

A text file is not runnable until you grant it execute permission. The canonical command is:

# Grant execute permission to the owner (most common) chmod +x deploy-check.sh # More precise: owner-execute only (no group/world execute) chmod 700 deploy-check.sh # Verify the permission bits ls -l deploy-check.sh # Output: -rwx------ 1 you staff 312 Jan 15 10:00 deploy-check.sh

The permission model matters in production. A script that runs as root and is world-writable is a privilege-escalation vulnerability. For system automation scripts (cron jobs, systemd services), use chmod 700 and ensure the file is owned by the correct service account — not root unless absolutely necessary.

How to Run a Script

There are three distinct ways to execute a script, and the difference between them is not trivial:

  • ./deploy-check.sh — Runs the script in a subshell. The kernel reads the shebang, spawns a new Bash process, and your current shell is unaffected. This is the correct way to run most scripts.
  • bash deploy-check.sh — Explicitly invokes Bash, overriding the shebang. Useful when the file is not yet executable or when you are debugging. Also runs in a subshell.
  • source deploy-check.sh (or . deploy-check.sh) — Runs the script in the current shell process, no subshell. Variable assignments and cd commands affect your current session. Used for environment-setup scripts (e.g., activating a virtualenv, loading secrets from a file).
Production pitfall — sourcing instead of executing: If a sourced script calls exit, it closes your entire terminal session, not just the script. If it unsets a variable you rely on, that variable is gone from your shell. Only source a script when you explicitly want it to modify your environment.

Script Execution Flow: How the Kernel Reads Your File

Shell script execution flow You type ./script.sh Kernel reads first 2 bytes (#!) Spawns interpreter /usr/bin/env bash Bash reads & runs each line in order Shell receives exec syscall Checks execute permission bit PATH lookup via env command Subshell — your session unchanged
How the kernel processes a Bash script from invocation to execution.

Your First Real Script

Let us write a script that a DevOps engineer would actually use: a system information summary that could run as a pre-check before deployment.

#!/usr/bin/env bash # ============================================================= # sysinfo.sh — print a quick system snapshot # Usage: ./sysinfo.sh # ============================================================= echo "=== System Snapshot ===" echo "Hostname : $(hostname)" echo "OS : $(uname -s) $(uname -r)" echo "Uptime : $(uptime -p 2>/dev/null || uptime)" echo "Disk : $(df -h / | awk 'NR==2{print $5 " used on " $1}')" echo "Memory : $(free -h 2>/dev/null | awk '/^Mem:/{print $3 " used / " $2 " total"}')" echo "Load avg : $(cat /proc/loadavg 2>/dev/null | awk '{print $1,$2,$3}')" echo "========================"

Save this as sysinfo.sh, run chmod +x sysinfo.sh, then execute with ./sysinfo.sh. You will immediately see why shell scripts are indispensable: in eight lines you assembled output from six different system sources into a coherent report.

Workflow tip — use a scripts directory: In real projects, keep automation scripts in a top-level scripts/ directory committed to your repository. Name scripts with verbs (build.sh, deploy.sh, check-health.sh). Add a README documenting each script's purpose and required environment variables. This is standard practice at companies with large infrastructure codebases.

Comments and Self-Documentation

Bash comments start with # and run to the end of the line. Unlike many languages, shell scripts often live for years with little modification, touched by engineers who were not on the team when they were written. Treat comments as operational documentation:

  • Explain why, not what. # retry 3 times to handle transient DNS failures is useful. # loop 3 times is not.
  • Document external dependencies: which tools must be installed, which environment variables must be set.
  • Mark any non-obvious workarounds with a note explaining the underlying issue.

Common Failure Modes

Knowing what goes wrong before it happens in production is half the job:

  • Permission denied on execution — you forgot chmod +x. Fix: chmod +x script.sh.
  • Bad interpreter: No such file or directory — the shebang path is wrong, or the script was edited on Windows and has CRLF line endings (\r\n). Fix line endings with sed -i 's/\r//' script.sh or dos2unix script.sh.
  • Command not found when running via cron or systemd — non-interactive shells have a minimal PATH. Fix: set PATH explicitly near the top of your script (PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin).
  • Script works as you but fails as the service account — the service account lacks read permission on a file your script needs, or its home directory does not exist. Always test scripts under the same user that will run them in production.

With these fundamentals in place — shebang, permissions, execution modes, and structure — you have everything you need to write scripts that your teammates can read, your on-call rotation can debug, and your automation systems can run reliably. Every subsequent lesson in this tutorial builds on this foundation.