Shell scripts hold together most build, test and deploy steps, and a small mistake in one can delete the wrong folder or ship a half-built release. A few safe defaults prevent most of these accidents. This guide gives you a copy-ready Bash header, the traps those defaults bring with them, and patterns for temp files, atomic writes, retries and deploys.
Start scripts with #!/usr/bin/env bash and set -Eeuo pipefail, quote every variable, and use ${VAR:?message} for anything a destructive command depends on. Clean up with trap … EXIT, write files atomically with a temp file and mv, and lint every script with ShellCheck. Know where set -e does not stop the script, and handle expected failures explicitly with if ! cmd; then ….
1. The safe starting lines
#!/usr/bin/env bash
set -Eeuo pipefail
IFS=$'\n\t'| Setting | What it does |
|---|---|
#!/usr/bin/env bash | Finds bash on the PATH, so the script runs where bash isn't at /bin/bash |
set -e | Exits when a command fails |
set -u | Treats an unset variable as an error instead of an empty string |
set -o pipefail | A pipeline fails if any command in it fails, not only the last |
set -E | Lets an ERR trap fire inside functions and subshells too |
IFS=$'\n\t' | Splits words on newlines and tabs only, which avoids many filename bugs |
The IFS line is optional. It helps with unquoted expansions, but quoting ("$var", "${arr[@]}") is the real fix. Keep quoting everything either way.
2. Where these settings surprise you
set -e is useful but has well-known gaps. Know them before you rely on it:
- It is ignored inside conditions. A function called as
if myfunc; thenormyfunc || echo failedruns with-eswitched off for its whole body, so a failing command inside it doesn't stop it. - Command substitution in
localhides failures. Inlocal out=$(cmd), the exit status is that oflocal, which succeeds. Declare first, then assign:local out; out=$(cmd). grepwith no match returns 1. Underpipefail,grep pattern file | wc -lfails when nothing matches.
Handle expected failures on purpose, and say why:
# A command that may legitimately fail
if ! grep -q "pattern" file.txt; then
echo "pattern not found, continuing" >&2
fi
# Optional variable with a default
PORT="${PORT:-8080}"
# Required variable: stop with a clear message if it's missing
: "${DEPLOY_DIR:?set DEPLOY_DIR before running}"Avoid a bare || true except where the failure genuinely doesn't matter, and add a comment when you use it.
3. A drop-in script template
#!/usr/bin/env bash
set -Eeuo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_DIR
log() { printf '[%(%F %T)T] %s\n' -1 "$*" >&2; }
die() { log "ERROR: $*"; exit 1; }
need() { command -v "$1" >/dev/null 2>&1 || die "missing command: $1"; }
usage() {
cat <<'EOF'
Usage: build.sh [-v] [-e dev|staging|prod]
-v verbose (trace commands)
-e environment (default: dev)
EOF
}
verbose=false
env="dev"
while getopts ":ve:h" opt; do
case "$opt" in
v) verbose=true ;;
e) env="$OPTARG" ;;
h) usage; exit 0 ;;
:) die "option -$OPTARG needs a value" ;;
\?) die "unknown option: -$OPTARG" ;;
esac
done
shift $((OPTIND - 1))
$verbose && set -x
tmp="$(mktemp -d)"
trap 'rm -rf -- "$tmp"' EXIT
trap 'log "failed at line $LINENO"' ERR
main() {
need git
log "environment: $env"
# build and test steps go here
}
main "$@"The EXIT trap runs on normal exit, on errors and on Ctrl+C, so temporary files never pile up.
4. File-safety patterns
rm -rf -- "${OUT_DIR:?OUT_DIR not set}/" stops before an empty variable turns into rm -rf /.mv it into place, so readers never see a half-written file.mkdir -p dist/logs succeeds whether or not the folder exists.mapfile -t lines < <(cmd) avoids word-splitting bugs from for x in $(cmd).Atomic write in full:
tmp="$(mktemp "output.json.XXXXXX")"
generate_report >"$tmp"
mv -- "$tmp" output.jsonmv within one file system is atomic. A temp file in /tmp may be on a different file system, so keep it beside the target.
5. Lint and format every script
- Lint with ShellCheck.
shellcheck -x script.shcatches unquoted variables, thelocaltrap above and many more. Treat its warnings as bugs. - Format with shfmt.
shfmt -d .shows differences;shfmt -w .rewrites files in a consistent style. - Validate data files.
jq empty package.jsonfails on invalid JSON without printing it. - Verify downloads.Compare
sha256sum file.tar.gzwith the published checksum before you unpack anything. - Run the checks in a pre-commit hook or CI, so a broken script never reaches the main branch.
6. Useful everyday patterns
Downloads that fail loudly. Plain curl exits 0 on an HTTP 404. Add --fail:
curl --fail --location --silent --show-error -o artifact.tgz "$URL"Retries with backoff:
retry() {
local tries="$1" delay=1 i
shift
for ((i = 1; i <= tries; i++)); do
"$@" && return 0
sleep "$delay"
delay=$((delay * 2))
done
return 1
}
retry 5 curl --fail --silent --show-error -o data.json "$URL"Time limits. timeout 300 ./long-task.sh kills a job that hangs instead of blocking a pipeline for ever.
Bulk edits: preview first. Run the search on its own (grep -rn 'old_name' src/), check the results, and only then edit. A bulk sed -i across a code base without a preview and without version control is a common way to break a project.
Git in scripts. git rev-parse --short HEAD for build labels, git diff --name-only origin/main...HEAD to see what a branch changed, and git status --porcelain to refuse to deploy from a dirty working tree.
7. Deploy safely
- Preview first. On your own server,
rsync -av --delete --dry-run dist/ user@server:/var/www/app/shows what would change, including deletions. Remove--dry-runonly after reading the list. - Ship an archive. Build a versioned archive, copy it, unpack it into a new release folder, then switch a symlink. Rolling back is just switching the symlink back.
- Never deploy from an uncommitted tree. Tag what you ship so you can rebuild it.
8. Running scripts on Domain India
- Shared hosting (cPanel, DirectAdmin, Webuzo): jailed SSH access is available on every shared hosting plan. It is off by default; ask support to enable it. Login is by SSH key only. See Enabling and accessing jailed SSH.
- What's in the jail: on cPanel and DirectAdmin,
scp, SFTP,tar,git,php,lnandmvare available, butrsyncis not. Deploy by packing an archive locally, copying it withscp, and unpacking it on the server, or use Git deployment. Other tools vary, so ask support before relying on one; run ShellCheck and shfmt on your own computer or in CI. - Cron jobs: on cPanel and DirectAdmin, jobs set to run every 1, 2 or 3 minutes are changed to every 4 minutes, so 4 minutes is the effective minimum. See Limiting cron job frequency.
- PHP from scripts: PHP's
exec-family functions are disabled on shared hosting, so Composer scripts stop there; run Composer with--no-scripts, or buildvendor/locally or in CI and upload it. Details are in PHP disabled functions on shared hosting. - VPS: full root access, so every pattern here, including
rsync, works. A VPS is self-managed. See VPS hosting.
For command basics, see Linux server mastery: essential shell commands.
What does set -euo pipefail do in Bash?
set -e exits the script when a command fails, set -u treats unset variables as errors, and set -o pipefail makes a pipeline fail if any command in it fails rather than only the last. Adding -E lets an ERR trap fire inside functions too.
Why didn't set -e stop my script when a command failed?
set -e is switched off inside conditions, so a function called from an if statement or with || keeps running after a failure. Failures inside local var=$(cmd) are also hidden. Declare the variable first, assign it separately, and check important commands explicitly.
How do I stop rm -rf from deleting the wrong folder?
Quote the variable and make it required: rm -rf -- "${DIR:?DIR not set}/"*. The ${VAR:?} form stops the script with a message if the variable is empty or unset, instead of expanding to the root folder.
Is rsync available on Domain India shared hosting?
No. The jailed shell on cPanel and DirectAdmin has scp, SFTP, tar and git, but not rsync. Pack an archive, copy it with scp and unpack it on the server, or deploy with Git. A VPS has no such limit.
Can I run shell scripts over SSH on shared hosting?
Yes, once jailed SSH is enabled for your account. It is available on every shared hosting plan but off by default, so ask support to enable it, then log in with an SSH key.
What is the shortest cron interval on shared hosting?
Four minutes. On cPanel and DirectAdmin, jobs set to run every 1, 2 or 3 minutes are rewritten to run every 4 minutes.
Need a server where you control every tool? See VPS hosting. For shell access on your shared plan, open a ticket and ask for jailed SSH to be enabled.
Jailed SSH is available on every shared hosting plan. Open a ticket and we will enable it for your account.
Ask support