Shell scripting automation
Skill greymandev/devops-agent-skills/skills/shell-scripting-automation
Guides agents through robust shell scripting and automation. Use when writing, reviewing, or fixing Bash scripts, Python automation CLIs, or PowerShell scripts, especially scripts that ignore failures or continue silently. Enforces set -Eeuo pipefail, idempotency, error handling, and exit codes.From its SKILL.md
npx -y skills add greymandev/devops-agent-skills --skill shell-scripting-automationAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
9.7 KB, as published. Nobody here has run it
Shell Scripting and Automation
Overview
Automation is the DevOps engine, and a bad script is worse than no script: it fails halfway, leaves systems in unknown states, and lies about it with exit code 0. Every script you write is production code — it gets reviewed, tested, and maintained like one.
Idempotency is the contract. Running a script twice must produce the same state as running it once. Scripts that depend on "what happened before" are incidents waiting for a retry.
Fail loudly, fail early, fail safe. A script that silently continues after an error is a corrupted state generator.
When to Use
- Automating any task performed more than twice
- Writing Bash scripts for Linux operations
- Building Python CLI tools for complex automation (APIs, data transformation, validation)
- Writing PowerShell for Windows Server, Active Directory, IIS, or .NET estates
- Reviewing existing scripts for safety
Choosing the Tool
Is it mostly orchestrating existing commands on Linux?
└── Yes, and < ~100 lines, no complex data structures → Bash
Does it parse JSON/YAML, call APIs, need retries, tests, or validation?
└── Yes → Python (typer/click + requests + pydantic + pytest)
Does it touch Windows Server, AD, IIS, Exchange, or .NET?
└── Yes → PowerShell (7+ where possible; WinRM/PSRemoting for remote)
Do not write JSON-parsing logic in Bash with grep/sed. That's what jq (acceptable) or Python (better) is for.
Bash: The Mandatory Template
Every Bash script starts like this. No exceptions:
#!/usr/bin/env bash
set -Eeuo pipefail
IFS=$'\n\t'
# What each safety flag buys you:
# -e exit on any command failure
# -E ERR traps propagate into functions/subshells
# -u treat unset variables as errors (no more rm -rf "$EMPTY_VAR/")
# -o pipefail a failed pipe stage fails the pipeline (no more lost errors)
log() { printf '[%s] %s\n' "$(date -u +%FT%TZ)" "$*" >&2; }
die() { log "ERROR: $*"; exit 1; }
cleanup() {
# Always runs (success, failure, Ctrl+C): remove temp files, release locks
rm -f "$TMPFILE" 2>/dev/null || true
}
trap cleanup EXIT
trap 'die "Failed at line $LINENO"' ERR
usage() {
cat <<EOF
Usage: $(basename "$0") [--dry-run] <environment>
Deploys the thing. Idempotent: safe to re-run.
EOF
exit "${1:-0}"
}
DRY_RUN=0
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) DRY_RUN=1; shift ;;
-h|--help) usage 0 ;;
-*) usage 1 ;;
*) break ;;
esac
done
[[ $# -ge 1 ]] || usage 1
ENV="$1"
[[ "$ENV" =~ ^(dev|staging|prod)$ ]] || die "Invalid environment: $ENV"
main() {
TMPFILE="$(mktemp)"
log "Starting against ${ENV} (dry_run=${DRY_RUN})"
# ... work here ...
log "Done"
}
main "$@"
Bash: The Idempotency Patterns
# Create-or-update, never create-and-fail
mkdir -p /opt/app # -p: no error if it exists
install -m 0644 -o app -g app config.toml /etc/app/config.toml
# Package installation: check first (or use the package manager's own idempotency)
dpkg -s nginx &>/dev/null || apt-get install -y nginx
# Line-in-file: replace or append, never blind-append
grep -q '^maxconn ' /etc/haproxy/haproxy.cfg \
&& sed -i 's/^maxconn .*/maxconn 4096/' /etc/haproxy/haproxy.cfg \
|| echo 'maxconn 4096' >> /etc/haproxy/haproxy.cfg
# Services: enable --now is idempotent
systemctl enable --now nginx
# Downloads: verify, don't re-download
if [[ ! -f app.tar.gz ]] || ! sha256sum -c app.tar.gz.sha256; then
curl -fsSL -o app.tar.gz "$URL"
sha256sum -c app.tar.gz.sha256
fi
Destructive operations need guards:
# Never: rm -rf "$DIR/" with an unquoted or possibly-empty variable
# Always:
[[ -n "$WORKDIR" && "$WORKDIR" == /tmp/build-* ]] || die "Refusing unsafe WORKDIR='$WORKDIR'"
rm -rf -- "$WORKDIR"
Bash: The Safety Rules
- Quote every expansion:
"$var", not$var. Word-splitting bugs are the #1 Bash footgun. - Use
[[ ]]not[ ]for conditionals — safer pattern matching, no pathname expansion surprises. - Never parse
ls— use globs orfind -print0 | while IFS= read -r -d ''. - Validate all input, including positional params, before acting on it.
- Use
mktempfor temp files — never predictable paths in/tmp(symlink attacks). - Return meaningful exit codes:
0success,1runtime error,2usage error. - Lint everything:
shellcheck -x script.shis part of CI, not optional. - Format consistently:
shfmt -i 2 -ci -w .in pre-commit. - No secrets on the command line — they're visible in
ps, shell history, and logs. Read from files with0600perms or from a secrets manager.
Python: When Bash Ends
#!/usr/bin/env python3
"""Sync DNS records from the zone file to the provider API.
Idempotent: creates missing records, updates drifted ones, never deletes.
"""
from __future__ import annotations
import sys
import typer
import requests
from pydantic import BaseModel, Field
app = typer.Typer(help="Idempotent DNS record sync.")
class DnsRecord(BaseModel):
name: str
type: str = Field(pattern="^(A|AAAA|CNAME|TXT)$")
value: str
ttl: int = 300
@app.command()
def sync(zone: str, dry_run: bool = False) -> None:
"""Reconcile desired records with the provider."""
desired = load_desired(zone) # validated with DnsRecord
current = fetch_current(zone) # via requests, with retries
plan = diff(desired, current) # create/update/none
for change in plan:
typer.echo(f"{change.action}: {change.record.name}")
if not dry_run and change.action != "none":
apply_change(change)
typer.echo(f"Done: {plan.summary()}")
if __name__ == "__main__":
app()
Python automation standards:
typer(orclick) for CLIs — real--help, real validationpydanticfor anything crossing a boundary (API payloads, YAML configs)requestswith explicittimeout=and retry logic (urllib3Retry ortenacity)pytesttests for the pure logic (diffing, transformation) — automation deserves tests too- Structured logging (
loggingmodule), never bareprintfor operational output - Same contract as Bash: idempotent, meaningful exit codes,
--dry-runfor anything that changes state
PowerShell: The Windows Estate
#Requires -Version 7.0
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)]
[ValidateSet('dev','staging','prod')]
[string]$Environment
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# Idempotent: check before change
$feature = 'Web-Server'
if ((Get-WindowsFeature -Name $feature).InstallState -ne 'Installed') {
Install-WindowsFeature -Name $feature -IncludeManagementTools
}
# Remote administration via PowerShell Remoting (WinRM)
Invoke-Command -ComputerName web01, web02 -ScriptBlock {
Get-Service -Name W3SVC | Select-Object PSComputerName, Status
} -Credential (Get-Credential)
PowerShell standards: Set-StrictMode, $ErrorActionPreference = 'Stop', SupportsShouldProcess (-WhatIf) on anything destructive, approved verbs (Get-, Set-, New-, Remove-), and PSScriptAnalyzer in CI.
Testing Scripts
Scripts get tested like code:
- Bash:
shellcheck+batsfor behavior tests on critical scripts - Python:
pytestwith mocked HTTP (responseslibrary) - PowerShell:
PSScriptAnalyzer+Pester - All: a
--dry-run/-WhatIfmode exercised in CI, so the planning path never rots
Common Rationalizations
| Rationalization | Reality |
|---|---|
| "It's a throwaway script" | Throwaway scripts get copy-pasted into cron jobs and run for three years. Write it properly or don't write it. |
| "set -e is annoying, my script fails for no reason" | It fails because of a reason you were ignoring. A script that continues after an error produces corrupt state with exit code 0. |
| "Quoting everything is ugly" | Unquoted expansions are how rm -rf $DIR/ deletes a filesystem when DIR is empty. Ugliness is cheaper than recovery. |
| "I'll parse the API output with grep" | JSON field order and whitespace are not a contract. Use jq or Python — grep-on-JSON is a time bomb. |
| "Idempotency is overkill for a one-off" | One-offs get retried after they half-fail at 2 AM. The retry is when non-idempotency hurts. |
| "Testing a shell script is over-engineering" | The script manages production systems. It deserves at least shellcheck and a dry-run mode. |
Red Flags
- Scripts without
set -Eeuo pipefail(Bash) /Set-StrictMode(PowerShell) - Unquoted variables anywhere
- Secrets as positional arguments or hardcoded literals
rm -rfwith unvalidated variables- Scripts that blind-append to config files (run it twice, broken config)
- No
--dry-runon state-changing scripts - Exit code 0 on failure paths ("error handling" that prints and continues)
- Cron jobs pointing at scripts in someone's home directory
Verification
-
shellcheck/PSScriptAnalyzer/pytestpass clean - Script succeeds when run twice in a row (idempotency proven, not assumed)
-
--dry-runmode exists and is exercised for state-changing scripts - Meaningful exit codes:
0success, non-zero on any failure - All variables quoted; all inputs validated before use
- No secrets in arguments, literals, or logs
- Error paths tested: what happens when the API is down / the disk is full / the lock is held?
- The script is in version control, reviewed like production code