8 min

Why Bash and Shell Scripting Still Matter for DevOps Automation

Bash and shell scripts still power CI jobs, servers, and quick fixes. Learn where they shine, how to write safer scripts, and when to use other tools.

Why Bash and Shell Scripting Still Matter for DevOps Automation

Bash and Shell Basics in DevOps Terms

When people say “shell scripting,” they usually mean writing a small program that runs inside a command-line shell. The shell reads your commands and launches other programs. On most Linux servers, that shell is either POSIX sh (a standardized baseline) or Bash (the most common “sh-like” shell with extra features).

Bash vs. “shell” (sh, bash, zsh) in plain terms

  • sh (POSIX sh): the portable, lowest-common-denominator syntax. Great for scripts that must run on many Unix-like systems.
  • bash: “Bourne Again SHell.” Adds conveniences (better conditionals, arrays, safer options) and is installed almost everywhere on Linux.
  • zsh/fish: popular for interactive use, but less common as the default interpreter for server scripts.

In DevOps terms, shell scripts are the thin glue layer that ties together OS tools, cloud CLIs, build tools, and configuration files.

Why the shell is still the default glue on servers

Linux machines already ship with core utilities (like grep, sed, awk, tar, curl, systemctl). A shell script can call these tools directly without adding runtimes, packages, or extra dependencies—especially useful in minimal images, recovery shells, or locked-down environments.

The “small tools combined” model

Shell scripting shines because most tools follow simple contracts:

  • Text streams: output goes to stdout, errors to stderr.
  • Pipes: connect programs like building blocks (cmd1 | cmd2).
  • Exit codes: 0 means success; non-zero means failure—critical for automation.

What this post will (and won’t) cover

We’ll focus on how Bash/shell fits into DevOps automation, CI/CD, containers, troubleshooting, portability, and safety practices. We won’t try to turn shell into a full application framework—when you need that, we’ll call out better options (and how shell still helps around them).

Where Shell Scripting Still Shows Up Every Day

Shell scripting isn’t just “legacy glue.” It’s a small, reliable layer that turns manual command sequences into repeatable actions—especially when you’re moving quickly across servers, environments, and tools.

Bootstrapping and one-time setup

Even if your long-term goal is fully managed infrastructure, there’s often a moment where you need to prepare a host: install a package, drop a config file, set permissions, create a user, or fetch secrets from a safe source. A short shell script is perfect for these one-time (or “rarely repeated”) tasks because it runs anywhere you have a shell and SSH.

Operational runbooks in executable form

Many teams keep runbooks as documents, but the highest-signal runbooks are scripts you can run during routine ops:

  • Start/stop/restart services and verify health checks
  • Rotate logs or prune old files to keep disks from filling up
  • Trigger a backup command and validate the output

Turning a runbook into a script reduces human error, makes results more consistent, and improves handoffs.

Fast data wrangling for answers now

When an incident hits, you rarely want a full app or dashboard—you want clarity. Shell pipelines with tools like grep, sed, awk, and jq are still the quickest way to slice logs, compare outputs, and spot patterns across nodes.

Automating repetitive CLI workflows

Daily work often means running the same CLI steps in dev, staging, and prod: tagging artifacts, syncing files, checking status, or performing safe rollouts. Shell scripts capture these workflows so they’re consistent across environments.

Bridging gaps between tools

Not everything integrates cleanly. Shell scripts can connect “Tool A outputs JSON” to “Tool B expects environment variables,” orchestrate calls, and add missing checks and retries—without waiting on new integrations or plugins.

Shell Scripts vs IaC and Config Management

Shell scripting and tools like Terraform, Ansible, Chef, and Puppet solve related problems, but they’re not interchangeable.

“Glue code” vs “system of record”

Think of IaC/config management as the system of record: the place where desired state is defined, reviewed, versioned, and applied consistently. Terraform declares infrastructure (networks, load balancers, databases). Ansible/Chef/Puppet describe machine configuration and ongoing convergence.

Shell scripts are usually glue code: the thin layer that connects steps, tools, and environments. A script might not “own” the final state, but it makes automation practical by coordinating actions.

Where scripts complement IaC

Shell is a great companion to IaC when you need:

  • Wrapping and orchestration: run Terraform for multiple workspaces/accounts, sequence applies, handle environment selection.
  • Validation and guardrails: check required variables, enforce naming rules, verify cloud credentials, block applies outside approved regions.
  • Integration: call CLIs, format outputs, upload artifacts, notify chat systems, or open tickets.

Example: Terraform creates resources, but a Bash script validates inputs, ensures the correct backend is configured, and runs terraform plan + policy checks before allowing apply.

Trade-offs to be honest about

Shell is fast to implement and has minimal dependencies—ideal for urgent automation and small coordination tasks. The downside is long-term governance: scripts can drift into “mini platforms” with inconsistent patterns, weak idempotency, and limited auditing.

A practical rule: use IaC/config tools for stateful, repeatable infrastructure and configuration; use shell for short, composable workflows around them. When a script becomes business-critical, migrate core logic into the system-of-record tool and keep shell as the wrapper.

CI/CD Pipelines: Why Bash Often Runs the Build

CI/CD systems orchestrate steps, but they still need something to actually do the work. Bash (or POSIX sh) remains the default glue because it’s available on most runners, easy to invoke, and can chain tools together without extra runtime dependencies.

The everyday CI jobs Bash handles

Most pipelines use shell steps for the unglamorous but essential tasks: installing dependencies, running builds, packaging outputs, and uploading artifacts.

Typical examples include:

  • Installing tooling (language runtimes, CLIs) and project dependencies
  • Running build/test commands and producing versioned packages
  • Generating metadata (commit SHA, build number) and writing it to files
  • Uploading artifacts to the CI system or an internal registry

Environment variables and secrets (without leaking them)

Pipelines pass configuration through environment variables, so shell scripts naturally become the router for those values. A safe pattern is: read secrets from env, never echo them, and avoid writing them to disk.

Prefer:

  • set +x around sensitive sections (so commands aren’t printed)
  • Passing tokens via headers/STDIN rather than command-line args (which can show up in logs)
  • Masking supported by your CI platform, plus minimal logging by default

Making scripts CI-friendly

CI needs predictable behavior. Good pipeline scripts:

  • Use clear exit codes (fail fast on errors, return non-zero)
  • Produce deterministic output (consistent file names, stable paths)
  • Print high-signal logs (“what” and “where”), not noisy debug dumps

Caching, parallelism, and team readability

Caching and parallel steps are usually controlled by the CI system, not the script—Bash can’t reliably manage shared caches across jobs. What it can do is make cache keys and directories consistent.

To keep scripts readable across teams, treat them like product code: small functions, consistent naming, and a short usage header. Store shared scripts in-repo (for example under /ci/) so changes are reviewed alongside the code they build.

Accelerating pipeline scripting with Koder.ai (without losing control)

If your team is constantly writing “one more CI script,” an AI-assisted workflow can help—especially for boilerplate like argument parsing, retries, safe logging, and guardrails. On Koder.ai, you can describe the pipeline job in plain language and generate a starter Bash/sh script, then iterate in a planning mode before you run it. Because Koder.ai supports source code export plus snapshots and rollback, it’s also easier to treat scripts as reviewed artifacts rather than ad-hoc snippets copied into CI YAML.

Containers and Cloud: Practical Automation with Shell

Shell scripting remains a practical glue layer in container and cloud workflows because so many tools expose a CLI first. Even when your infrastructure is defined elsewhere, you still need small, dependable automations to launch, validate, collect, and recover.

Inside containers: entry points and init tasks

A common place you’ll still see shell is the container entrypoint. Small scripts can:

  • Render config from environment variables
  • Run database migrations before starting the app
  • Perform a quick dependency check (DNS, ports, credentials)

The key is to keep entrypoint scripts short and predictable—do setup, then exec the main process so signals and exit codes behave correctly.

Kubernetes ops helpers

Day-to-day Kubernetes work often benefits from lightweight helpers: kubectl wrappers that confirm you’re on the right context/namespace, collect logs from multiple pods, or fetch recent events during an incident.

For example, a script can refuse to run if you’re pointed at production, or automatically bundle logs into a single artifact for a ticket.

Cloud CLIs for quick automation

AWS/Azure/GCP CLIs are ideal for batch tasks: tagging resources, rotating secrets, exporting inventories, or stopping non-prod environments at night. Shell is often the fastest way to chain these actions into a repeatable command.

Pitfalls and safer patterns

Two common failure points are brittle parsing and unreliable APIs. Prefer structured output whenever possible:

  • Use JSON output flags (for example, --output json) and parse with jq rather than grepping human-formatted tables.
  • Expect rate limits and transient failures; add retries with backoff, and fail clearly when limits are exceeded.

A small shift—JSON + jq, plus basic retry logic—turns “works on my laptop” scripts into dependable automation you can run repeatedly.

Incident Response and Troubleshooting Faster

Harden container startup
Draft entrypoint and init scripts for containers, then refine them with clear checks.

When something breaks, you usually don’t need a new toolchain—you need answers in minutes. Shell is perfect for incident response because it’s already on the host, it’s fast to run, and it can stitch together small, reliable commands into a clear picture of what’s happening.

“Get me answers now” diagnostics

During an outage, you’re often validating a handful of basics:

  • Disk: is the filesystem full or inode-starved? (df -h, df -i)
  • Memory/CPU pressure: are we swapping or throttling? (free -m, vmstat 1 5, uptime)
  • Ports and processes: is the service listening, and on the right interface? (ss -lntp, ps aux | grep ...)
  • DNS: can this host resolve what it needs? (getent hosts name, dig +short name)
  • HTTP checks: does the endpoint respond, and how quickly? (curl -fsS -m 2 -w '%{http_code} %{time_total}\n' URL)

Shell scripts shine here because you can standardize these checks, run them consistently across hosts, and paste results into your incident channel without manual formatting.

Capture evidence for later (without slowing down)

A good incident script collects a snapshot: timestamps, hostname, kernel version, recent logs, current connections, and resource usage. That “state bundle” helps root-cause analysis after the fire is out.

#!/usr/bin/env bash
set -euo pipefail
out="incident_$(hostname)_$(date -u +%Y%m%dT%H%M%SZ).log"
{
  date -u
  hostname
  uname -a
  df -h
  free -m
  ss -lntp
  journalctl -n 200 --no-pager 2>/dev/null || true
} | tee "$out"

Reduce blast radius by default

Incident automation should be read-only first. Treat “fix” actions as explicit, with confirmation prompts (or a --yes flag) and clear output about what will change. That way, the script helps responders move faster—without creating a second incident.

Portability: POSIX sh, Bash, and Cross-Platform Gotchas

Portability matters whenever your automation runs on “whatever the runner happens to have”: minimal containers (Alpine/BusyBox), different Linux distros, CI images, or developer laptops (macOS). The biggest source of pain is assuming every machine has the same shell.

POSIX sh vs Bash (plain English)

POSIX sh is the lowest common denominator: basic variables, case, for, if, pipelines, and simple functions. It’s what you pick when you want the script to run almost anywhere.

Bash is a feature-rich shell with conveniences like arrays, [[ ... ]] tests, process substitution (<(...)), set -o pipefail, extended globbing, and nicer string handling. Those features speed up DevOps automation—but they can break on systems where /bin/sh is not Bash.

How to decide what to target

  • Target POSIX sh for maximum portability (Alpine’s ash, Debian dash, BusyBox, minimal init containers).
  • Target Bash when you control the environment (your CI image, your ops tooling host) or you genuinely need Bash-only features.

On macOS, users may have Bash 3.2 by default, while Linux CI images might have Bash 5.x—so even “Bash scripts” can hit version differences.

Avoiding “bashisms” when portability matters

Common bashisms include [[ ... ]], arrays, source (use .), and echo -e behavior differences. If you mean POSIX, write and test it with a real POSIX shell (e.g., dash or BusyBox sh).

Pin the interpreter and document it

Use a shebang that matches your intent:

#!/bin/sh

or:

#!/usr/bin/env bash

Then document requirements in the repo (e.g., “requires Bash ≥ 4.0”) so CI, containers, and teammates stay aligned.

Let ShellCheck catch portability issues early

Run shellcheck in CI to flag bashisms, quoting mistakes, and unsafe patterns. It’s one of the fastest ways to prevent “works on my machine” shell failures. For setup ideas, link your team to a simple internal guide like /blog/shellcheck-in-ci.

Security and Safety Practices for Shell Scripts

Graduate from script to tool
When Bash grows too big, scaffold a real internal tool and keep shell as a thin wrapper.

Shell scripts often run with access to production systems, credentials, and sensitive logs. A few defensive habits make the difference between “handy automation” and an incident.

Safe defaults (and their sharp edges)

Many teams start scripts with:

set -euo pipefail
  • -e stops on errors, but it can surprise you in if conditions, while tests, and some pipelines. Know where failures are expected and handle them explicitly.
  • -u treats unset variables as errors—great for catching typos.
  • pipefail ensures a failing command inside a pipeline fails the whole pipeline.

When you intentionally allow a command to fail, make it obvious: command || true, or better, check and handle the error.

Quoting: your first security control

Unquoted variables can cause word-splitting and wildcard expansion:

rm -rf $TARGET   # dangerous
rm -rf -- "$TARGET"  # safer

Always quote variables unless you specifically want splitting. Prefer arrays in Bash when building command arguments.

Validate input, avoid eval, use least privilege

Treat parameters, env vars, filenames, and command output as untrusted.

  • Validate inputs (allow-lists beat block-lists).
  • Avoid eval and building shell code as strings.
  • Run with the minimum permissions required; use sudo for a single command, not the whole script.

Secrets: reduce exposure

  • Never print secrets (echo, debug traces, verbose curl output).
  • Be careful with logging and set -x; disable tracing around sensitive commands.
  • Prefer passing tokens via stdin or files with tight permissions.

Safe file operations and cleanup

Use mktemp for temporary files and trap for cleanup:

tmp="$(mktemp)"
trap 'rm -f "$tmp"' EXIT

Also use -- to end option parsing (rm -- "$file") and set a restrictive umask when creating files that might contain sensitive data.

Maintainability: Testing, Linting, and Team Standards

Shell scripts often start as a quick fix, then quietly become “production.” Maintainability is what keeps that production from turning into a mystery file everyone avoids touching.

Make scripts easy to find and understand

A small bit of structure pays off quickly:

  • Keep operational scripts in a dedicated scripts/ (or ops/) folder so they’re discoverable.
  • Use clear naming (backup-db.sh, rotate-logs.sh, release-tag.sh) over inside-joke names.
  • Add a short header block: purpose, required env vars, and a safe example invocation.

Inside the script, prefer readable functions (small, single-purpose) and consistent logging. A simple log_info / log_warn / log_error pattern makes troubleshooting faster and avoids inconsistent echo spam.

Also: support -h/--help. Even a minimal usage message turns a script into a tool your teammates can confidently run.

Test the “dangerous” parts

Shell isn’t hard to test—it’s just easy to skip. Start lightweight:

  • Smoke tests that run the script with safe flags (like --dry-run) and validate output.
  • Containerized test runs (for example, in a minimal Debian/Alpine image) so behavior matches CI, not someone’s laptop.
  • For more coverage, use bats (Bash Automated Testing System) to assert exit codes, outputs, and file changes.

Focus tests on inputs/outputs: arguments, exit status, log lines, and side effects (files created, commands invoked).

Automate linting and formatting in CI

Two tools catch most issues before review:

  • ShellCheck: flags quoting bugs, undefined variables, and common pitfalls.
  • shfmt: enforces consistent formatting so diffs stay readable.

Run both in CI so standards don’t depend on who remembers to run what.

Treat scripts like real code

Operational scripts should be versioned, code-reviewed, and tied to change management just like application code. Require PRs for changes, document behavior changes in commit messages, and consider simple version tags when scripts are consumed by multiple repos or teams.

Proven Patterns for Reliable Infrastructure Scripts

Reliable infrastructure scripts behave like good automation: predictable, safe to re-run, and readable under pressure. A few patterns turn “works on my machine” into something your team can trust.

Make re-runs safe with idempotency

Assume the script will be executed twice—by humans, cron, or a retrying CI job. Prefer “ensure state” over “do action.”

  • Create directories with mkdir -p, not mkdir.
  • Check before changing: “is the user present?”, “is the package installed?”, “is the setting already applied?”

A simple rule: if the desired end state already exists, the script should exit successfully without doing extra work.

Retries with exponential backoff

Networks fail. Registries rate-limit. APIs timeout. Wrap flaky operations with retries and increasing delays.

retry() {
  n=0; max=5; delay=1
  while :; do
    "$@" && break
    n=$((n+1))
    [ "$n" -ge "$max" ] && return 1
    sleep "$delay"; delay=$((delay*2))
  done
}

Safer API calls with curl

For automation, treat HTTP status as data. Prefer curl -fsS (fail on non-2xx, show errors) and capture the status when needed.

resp=$(curl -sS -w "\n%{http_code}" -H "Authorization: Bearer $TOKEN" "$URL")
body=${resp%$'\n'*}; code=${resp##*$'\n'}
[ "$code" = "200" ] || { echo "API failed: $code" >&2; exit 1; }

If you must parse JSON, use jq rather than fragile grep pipelines.

Prevent concurrent runs

Two copies of a script fighting over the same resource is a common outage pattern. Use flock when available, or a lockfile with a PID check.

Output for humans and machines

Log clearly (timestamps, key actions), but also offer a machine-readable mode (JSON) for dashboards and CI artifacts. A small --json flag often pays for itself the first time you need to automate reporting.

When to Use Something Else (and Still Keep Shell)

Kickstart your CI script
Describe your CI job and get a clean starter script you can review and tweak.

Shell is a great glue language: it chains commands, moves files, and coordinates tools that already exist on the box. But it’s not the best choice for every kind of automation.

Clear signals you’ve outgrown shell

Move beyond Bash when the script starts to feel like a tiny application:

  • Complex branching and state (lots of nested if, temporary flags, and special cases)
  • Non-trivial data structures (parsing JSON, building maps/lists, heavy text processing)
  • You need dependable libraries (HTTP clients, auth, retries, YAML/JSON parsing)
  • Cross-platform requirements, especially Windows runners or mixed environments
  • Long-term ownership concerns: multiple teams, frequent changes, and high blast radius

When Python is a better fit

Python shines when you’re integrating with APIs (cloud providers, ticketing systems), working with JSON/YAML, or needing unit tests and reusable modules. If your “script” needs real error handling, rich logging, and structured configuration, Python usually reduces the amount of fragile parsing.

When Go is a better fit

Go is a strong pick for distributable tooling: a single static binary, predictable performance, and strong typing that catches mistakes earlier. It’s ideal for internal CLI tools you want to run in minimal containers or locked-down hosts without a full runtime.

The hybrid approach: keep shell, but make it thin

A practical pattern is using shell as a wrapper for a real tool:

  • Bash handles environment checks, argument parsing, and calling commands
  • A Python/Go program handles “business logic” (API calls, data transformations)

This is also where platforms like Koder.ai can fit well: you can prototype the workflow as a thin Bash wrapper, then generate or scaffold the heavier service/tooling (web, backend, or mobile) from a chat-driven spec. When the logic graduates from “ops script” to “internal product,” exporting the source and moving it into your normal repo/CI keeps governance intact.

Quick decision checklist

Choose shell if it’s mostly: orchestrating commands, short-lived, and easy to test in a terminal.

Choose another language if you need: libraries, structured data, cross-platform support, or maintainable code with tests that will grow over time.

How to Learn Bash for DevOps Without Getting Stuck

Learning Bash for DevOps works best when you treat it like a toolbelt, not a programming language you must “master” all at once. Focus on the 20% you’ll use weekly, then add features only when you feel real pain.

A practical learning path (what to learn first)

Start with core commands and the rules that make automation predictable:

  • Files and text: ls, find, grep, sed, awk, tar, curl, jq (yes, it’s not shell—but it’s essential)
  • Pipes and redirection: |, >, >>, 2>, 2>&1, here-strings
  • Exit codes: $?, set -e tradeoffs, and explicit checks like cmd || exit 1
  • Variables and quoting: "$var", arrays, and when word-splitting bites
  • Functions and parameters: foo() { ... }, $1, $@, default values

Aim to write small scripts that glue tools together rather than building large “applications.”

Exercises that mirror real DevOps work

Pick one short project per week and keep it runnable from a fresh terminal:

  1. Deploy helper: validate inputs, build a Docker image, tag it, and push; print clear errors and exit codes.
  2. Log collector: grab logs from a service, compress them, and upload to a known path (S3/SSH/local folder).
  3. Health check script: test DNS, HTTP status, disk space, and a critical process; return non-zero on failure.

Keep each script under ~100 lines at first. If it grows, split into functions.

References that save time

Use primary sources instead of random snippets:

  • man bash, help set, and man test
  • The Bash Reference Manual
  • ShellCheck docs (and rules): /blog/shellcheck-basics

Team onboarding: make “good shell” the default

Create a simple starter template and a review checklist:

  • Header with set -euo pipefail (or a documented alternative)
  • Consistent logging, input validation, and trap for cleanup
  • ShellCheck in CI, plus a tiny README: usage + examples

Wrap-up

Shell scripting pays off most when you need fast, portable glue: running builds, inspecting systems, and automating repeatable admin tasks with minimal dependencies.

If you standardize a few safety defaults (quoting, input validation, retries, linting), shell becomes a dependable part of your automation stack—not a collection of fragile one-offs. And when you want to move from “script” to “product,” tools like Koder.ai can help you evolve that automation into a maintainable app or internal tool while keeping source control, reviews, and rollbacks in the loop.

FAQ

What does “shell scripting” mean in DevOps terms?

In DevOps, a shell script is usually glue code: a small program that chains existing tools together (Linux utilities, cloud CLIs, CI steps) using pipes, exit codes, and environment variables.

It’s best when you need quick, dependency-light automation on servers or runners where the shell is already available.

When should I choose POSIX sh vs Bash?

Use POSIX sh when the script must run across varied environments (BusyBox/Alpine, minimal containers, unknown CI runners).

Use Bash when you control the runtime (your CI image, an ops host) or you need Bash features like [[ ... ]], arrays, pipefail, or process substitution.

Pin the interpreter via the shebang (e.g., #!/bin/sh or #!/usr/bin/env bash) and document required versions.

Why is shell still the default automation “glue” on servers and CI runners?

Because it’s already there: most Linux images include a shell and core utilities (grep, sed, awk, tar, curl, systemctl).

That makes shell ideal for:

  • Bootstrapping hosts and one-time setup
  • CI/CD “do the work” steps
  • Incident-response diagnostics
  • Quick orchestration around IaC/config tools
How do shell scripts fit with Terraform/Ansible/Chef/Puppet?

IaC/config tools are usually the system of record (desired state, reviewable changes, repeatable applies). Shell scripts are best as a wrapper that adds orchestration and guardrails.

Examples where shell complements IaC:

  • Selecting workspaces/accounts and sequencing commands
  • Validating required variables/credentials before plan/apply
  • Integrating with CLIs, artifacts, notifications, or policy checks
What are best practices for Bash in CI/CD pipelines?

Make them predictable and safe:

  • Fail clearly: use exit codes and don’t ignore errors accidentally
  • Avoid leaking secrets: disable tracing with set +x around sensitive commands
  • Prefer structured output: parse JSON with jq instead of grepping tables
  • Keep logs high-signal: print what’s happening and where outputs go

If a step is flaky (network/API), add retries with backoff and a hard failure when exhausted.

What’s the right way to use shell scripts as container entrypoints?

Keep entrypoints small and deterministic:

  • Do minimal init (render config, run migrations/checks)
  • Then exec the main process so signals and exit codes propagate correctly

Also avoid long-running background processes in the entrypoint unless you have a clear supervision strategy; otherwise shutdowns and restarts become unreliable.

What are the most common portability problems in shell scripts?

Common gotchas:

  • /bin/sh might be dash (Debian/Ubuntu) or BusyBox sh (Alpine), not Bash
  • macOS often ships an older Bash (3.2), so some Bash 4+ features break
  • echo -e, sed -i, and test syntax vary across platforms

If portability matters, test with the target shell (e.g., dash/BusyBox) and run ShellCheck in CI to catch “bashisms” early.

What safety and security defaults should every shell script have?

A solid baseline is:

set -euo pipefail

Then add these habits:

  • Quote variables: "$var" (prevents word-splitting/globbing bugs)
  • Avoid eval and string-built commands
  • Validate inputs (prefer allow-lists)
  • Use -- to end option parsing (e.g., rm -- "$file")
  • Use mktemp + trap for secure temp files and cleanup

Be careful with set -e: handle expected failures explicitly (cmd || true or proper checks).

How can shell help with incident response without making things worse?

For fast, consistent diagnostics, standardize a small set of commands and capture outputs with timestamps.

Typical checks include:

  • Disk/inodes: df -h, df -i
  • CPU/memory pressure: uptime, free -m, vmstat 1 5
  • Listening ports: ss -lntp
  • Service logs: journalctl -n 200 --no-pager
  • HTTP sanity: curl -fsS -m 2 URL

Prefer “read-only first” scripts, and make any write/fix action explicit (prompt or --yes).

How do I keep shell scripts maintainable (linting, formatting, testing)?

Two tools cover most teams’ needs:

  • ShellCheck for correctness and safety (quoting, undefined vars, portability)
  • shfmt for consistent formatting

Add lightweight tests:

  • Smoke tests (including a --dry-run mode)
  • Containerized runs that match CI environments
  • Use bats if you want assertions on exit codes, output, and file changes

Store scripts in a predictable location (e.g., scripts/ or ops/) and include a minimal --help usage block.

Related posts