A Bash Script You Can Trust: Status, Tracing and Core Dumps
You will finish with a small Bash script that checks its inputs, propagates failures, can be traced when it misbehaves, and tells you where Linux is configured to write core dumps. The examples match Bash 5.2, which is the version installed on the machine used for this guide.
The route
Jump straight to the step you need, or tick off Done means at the end.
Allow about fifteen minutes. You need Bash and a terminal. The workflow is read-only apart from the script file you create in a test directory. It does not need elevated privileges. Do not run a script copied from an untrusted source with sudo; inspect it first.
Checkpoint 1: confirm the shell you are using
Bash has several modes which look similar from the prompt but read different startup files and handle input differently. A script should normally invoke Bash explicitly, rather than relying on whichever shell happens to launch it.
$ bash --version | head -1
GNU bash, version 5.2.21(1)-release (x86_64-pc-linux-gnu)
$ bash -c 'printf "interactive=%s\n" "$-"'
interactive=hBc
The exact version string will vary. The -c command runs non-interactively, so it does not read your normal interactive configuration in the same way as a terminal shell. An interactive Bash reads /etc/bash.bashrc and ~/.bashrc when those files exist. A login shell instead reads /etc/profile, then the first readable file from ~/.bash_profile, ~/.bash_login and ~/.profile.
That distinction is a common trap: an alias or exported variable that works at your prompt may not exist in a scheduled script. A non-interactive Bash can also source the file named by BASH_ENV. Treat that variable as part of the execution environment, not as a convenient hidden configuration channel.
Checkpoint 2: make failures visible
Create a test script in a directory you control. The shebang selects Bash, -u rejects unset variables, and pipefail makes a pipeline fail when a command other than its last member fails. -e exits after an unhandled failure, but it has documented exceptions, including commands used as the test in if, while and until. It is a guardrail, not a substitute for checking important results.
#!/usr/bin/env bash
set -Eeuo pipefail
trap 'rc=$?; printf "failed: line %s, status %s\n" "$LINENO" "$rc" >&2' ERR
trap 'rc=$?; printf "finished with status %s\n" "$rc" >&2' EXIT
usage() {
printf 'usage: %s FILE\n' "$0" >&2
}
if (( $# != 1 )); then
usage
exit 2
fi
input=$1
if [[ ! -r $input ]]; then
printf 'cannot read: %s\n' "$input" >&2
exit 1
fi
printf 'first line: '
IFS= read -r first_line < "$input" || true
printf '%s\n' "$first_line"
Save it as check-file, then make it executable if you want to invoke it directly. This changes only the mode of that one file, so undo it with chmod u-x check-file; alternatively run it as bash ./check-file FILE and skip the mode change.
$ bash -n ./check-file
$ bash ./check-file /etc/hostname
first line: <the-hostname-on-this-machine>
finished with status 0
bash -n parses the script without executing commands. The read command returns a failure at end-of-file if a final line has no newline, so the example deliberately handles that expected condition with || true. Without that exception, strict mode would turn an ordinary short file into a failed run.
Checkpoint 3: verify exit status instead of output
Every simple Bash command has an exit status. Zero conventionally means success, and a script invoked from a file exits with the status of its last command unless it exits earlier. Use status values for automation; printed text is for people and may change.
$ bash ./check-file /path/that/does/not/exist
cannot read: /path/that/does/not/exist
failed: line 18, status 1
finished with status 1
$ printf 'status=%s\n' "$?"
status=1
The ERR trap prints a diagnostic, while the EXIT trap reports the final status. The line number is useful but not a complete stack trace. Also remember that traps and set -e interact with functions, conditional commands and subshells. If a failure is expected, write that expectation explicitly:
if grep -q '^enabled=yes$' "$input"; then
printf 'feature enabled\n'
else
printf 'feature not enabled\n'
fi
Do not write grep ... || true merely to silence a failure unless every non-zero result really is safe to ignore. In a pipeline, add pipefail as above and test the whole operation. For a command whose non-zero result is meaningful, capture it in an if or assign and test it deliberately.
Checkpoint 4: trace one run, carefully
Use -x to make Bash print commands as it executes them. This is often the fastest way to find a wrong path, expansion or branch:
$ bash -x ./check-file /etc/hostname
+ input=/etc/hostname
+ [[ ! -r /etc/hostname ]]
first line: <the-hostname-on-this-machine>
finished with status 0
The exact trace includes a prompt prefix and can differ with the script and hostname. Tracing can expose passwords, tokens, private paths and command arguments in the terminal or CI log. Do not enable it around secrets, and do not paste a trace into a public issue without reviewing it. Once the cause is found, remove the debug invocation rather than leaving a permanent set -x in a production script.
Checkpoint 5: check whether Linux can write a core dump
A core dump is a memory image produced when a process terminates because of a signal whose default action is to dump core. It is useful with a debugger, but it can contain credentials and other sensitive process memory. Bash is usually the orchestrator here; a native child process is the more usual crash source.
First inspect the current shell's soft limit and the kernel naming policy. These commands read state only:
$ ulimit -c
0
$ cat /proc/sys/kernel/core_pattern
|/usr/share/apport/apport -p%p -s%s -c%c -d%d -P%P -u%u -g%g -F%F -- %E
0 means this shell and its children have a zero soft RLIMIT_CORE, so an ordinary file core dump is capped at zero bytes. A value such as unlimited removes that per-process size cap, but it does not guarantee a file: the destination must be writable, the filesystem must have space, and the kernel or service manager may route dumps elsewhere. The value above is this machine's apport configuration; yours may say core, contain a different handler, or be empty. On systems using systemd, the core manpage says that systemd may determine the destination.
For a temporary diagnostic run, you can raise the limit in a child shell without changing the login configuration:
$ bash -c 'ulimit -c unlimited; ulimit -c; exec ./program-under-test --safe-input'
unlimited
This requires that the caller is permitted to raise its soft limit up to the hard limit. It does not alter the parent shell. Treat any resulting dump as sensitive: restrict access, collect only what you need, and remove it using your normal approved retention process after analysis. Do not change /proc/sys/kernel/core_pattern or service-wide limits casually; those are system-level changes and may affect every process.
Done means
- The script starts with Bash and passes
bash -n. - Missing input produces a non-zero status and a useful diagnostic.
- Expected non-zero commands are tested explicitly rather than hidden with a blanket
true. - You can trace one run with
bash -xwithout leaking its arguments or secrets. - You checked both
ulimit -cand/proc/sys/kernel/core_patternbefore expecting a core file.