SSH Key Management

Useful Shell for Code Development Safe Defaults & Patterns

By the Domain India teamPublished 6 min read
Knowledge base article
Contents (8 sections)

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.

Key takeaways

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

bash
#!/usr/bin/env bash
set -Eeuo pipefail
IFS=$'\n\t'
SettingWhat it does
#!/usr/bin/env bashFinds bash on the PATH, so the script runs where bash isn't at /bin/bash
set -eExits when a command fails
set -uTreats an unset variable as an error instead of an empty string
set -o pipefailA pipeline fails if any command in it fails, not only the last
set -ELets 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; then or myfunc || echo failed runs with -e switched off for its whole body, so a failing command inside it doesn't stop it.
  • Command substitution in local hides failures. In local out=$(cmd), the exit status is that of local, which succeeds. Declare first, then assign: local out; out=$(cmd).
  • grep with no match returns 1. Under pipefail, grep pattern file | wc -l fails when nothing matches.

Handle expected failures on purpose, and say why:

bash
# 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

bash
#!/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

Guard destructive paths
rm -rf -- "${OUT_DIR:?OUT_DIR not set}/" stops before an empty variable turns into rm -rf /.
Write atomically
Generate into a temp file in the same folder, then mv it into place, so readers never see a half-written file.
Create folders idempotently
mkdir -p dist/logs succeeds whether or not the folder exists.
Read lines safely
mapfile -t lines < <(cmd) avoids word-splitting bugs from for x in $(cmd).

Atomic write in full:

bash
tmp="$(mktemp "output.json.XXXXXX")"
generate_report >"$tmp"
mv -- "$tmp" output.json

mv 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

  1. Lint with ShellCheck.
    shellcheck -x script.sh catches unquoted variables, the local trap above and many more. Treat its warnings as bugs.
  2. Format with shfmt.
    shfmt -d . shows differences; shfmt -w . rewrites files in a consistent style.
  3. Validate data files.
    jq empty package.json fails on invalid JSON without printing it.
  4. Verify downloads.
    Compare sha256sum file.tar.gz with the published checksum before you unpack anything.
  5. 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:

bash
curl --fail --location --silent --show-error -o artifact.tgz "$URL"

Retries with backoff:

bash
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-run only 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, ln and mv are available, but rsync is not. Deploy by packing an archive locally, copying it with scp, 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 build vendor/ 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.

Want jailed SSH on your hosting?

Jailed SSH is available on every shared hosting plan. Open a ticket and we will enable it for your account.

Ask support

Was this article helpful?

Your answer helps us decide what to improve next.

Still need help? Open a support ticket and our team will reply.

Prefer an app? Add this site to your home screen.Get the app