Skip to main content

Neat bash script

· 5 min read
Abhishek Tripathi
Curiosity brings awareness.

Neat bash script example. We'll learn about >&2 and pushd and popd in this script.

Details
#!/usr/bin/env bash

set -euo pipefail # -e: exit on error, -u: treat unset vars as error, -o pipefail: fail if any command in a pipeline fails

exit_failure=1

project_root=$(git rev-parse --show-toplevel)

yeet_name=${1:-}
location=$project_root

render_file_template() {
if [ "$(uname)" == "Darwin" ]; then
sed -i '' "s/$1/$2/g" $3
else
sed -i "s/$1/$2/g" $3
fi
}

render_yeet_template() {
render_file_template "__TARGET__" "$yeet_name" Makefile
render_file_template "__TARGET__" "$yeet_name" YEET
}

prompt_user() {
prompt=$1
default=${2:-}
while :; do
if [ -n "$default" ]; then
read -p "$prompt [default: $default]: " response
else
read -p "$prompt: " response
fi

if [ -n "$response" ]; then
echo "$response"
return
fi

if [ -z "$response" ] && [ -n "$default" ]; then
response=$default
echo "$response"
return
fi

>&2 echo "Invalid input." # Print error message to stderr
done
}

process_variables() {
if [ -z "$yeet_name" ]; then
yeet_name=$(prompt_user "What would you like to name the new yeet?")
fi
}

main() {
process_variables

>&2 echo "Creating directory for $yeet_name"
if [ -d "$location/$yeet_name" ]; then
>&2 echo "Directory for $yeet_name already exists!"
exit $exit_failure
fi

mkdir -p "$location/$yeet_name"
cp -r .templates/yeet/* "$location/$yeet_name"
cp -r .templates/yeet/.clang-format "$location/$yeet_name"

pushd "$location/$yeet_name" > /dev/null 2>&1 # Save current dir, go to new dir (suppress output)
render_yeet_template
git init
popd > /dev/null 2>&1 # Return to previous dir (suppress output)
}

pushd "$project_root" > /dev/null 2>&1
main
popd > /dev/null 2>&1


This is a bash script I found on internet. It uses some very good patterns.

Here’s a progression of examples—from the simplest to more advanced—showing how and why you’d redirect output to stderr with constructs like >&2.


1. Basic: Send a message to stderr

By default, echo writes to stdout (file descriptor 1). To send it to stderr (fd 2) instead:

echo "Oops, something went wrong!" >&2

This is equivalent to:

echo "Oops, something went wrong!" 1>&2

Here:

  • 1> means “take stdout (fd 1)”
  • 2 is the target (stderr, fd 2)

Use this when you want your message to count as an error (so it won’t be mixed with normal output if someone is piping or capturing only stdout).


2. Intermediate: Functions and logging

Wrap stderr-writes in a helper function for cleaner code:

log_error() {
echo "ERROR: $*" >&2
}

# usage
if ! cp "$src" "$dst"; then
log_error "Failed to copy $src to $dst"
exit 1
fi

Here, any call to log_error "…” goes to stderr, keeping it separate from “happy-path” output.


3. Redirecting entire blocks

You can redirect all output in a block to stderr:

{
echo "Step 1 passed"
echo "Step 2 passed"
echo "Final check failed!"
} >&2

Everything inside { … } now writes to fd 2.


4. Advanced: Using exec for global redirection

If you want your entire script (or a large section) to send stdout to stderr, you can exec:

# At top of script, before any output:
exec 1>&2

echo "This goes to stderr"
# any subsequent echo also goes to stderr

You can even swap streams:

# Swap stdout and stderr for the remainder of the script:
exec 3>&1 1>&2 2>&3 3>&-

After this:

  • What was stderr goes to stdout
  • What was stdout goes to stderr

5. Super-advanced: Custom descriptors & logging to files

Unix supports more fds than just 0,1,2. You can open your own:

# Open fd 3 for appending to a logfile:
exec 3>>/var/log/my_script.log

# Send normal info to fd 3:
echo "All OK so far" >&3

# Send errors to stderr as usual:
echo "Fatal error" >&2

# Restore/close when done:
exec 3>&-

This lets you:

  • Keep “info” messages in a log file via fd 3
  • Keep “errors” distinct on stderr

Why stderr vs. stdout?

  1. Separation of concerns

    • stdout → data/results
    • stderr → diagnostics/errors
  2. Safe piping

    myscript.sh | grep Success

    only sees stdout successes, not debug or error chatter.

  3. Better automation

    Tools can capture or redirect each stream independently.


Rundown of pushd/popd, from simple to advanced:

  1. Basic directory stack

    pushd /path/to/dir    # cd into DIR, and push old cwd onto a stack
    popd # pop the top stack entry and cd back to it

    Use when you need to temporarily work in another directory and then return.

  2. Inspecting & nesting

    pushd /one
    pushd /two
    dirs # shows stack order: /two /one /original
    popd # returns to /one (next entry on stack)

    Stack lets you nest multiple switches without manually tracking each path.

  3. Rotating through entries

    pushd A B C           # push A, then B, then C; cwd ends at C
    dirs # C B A
    pushd +1 # rotate: bring second entry (B) to top; cwd=B
    pushd +2 # now rotates third (A) to top; cwd=A

    Quickly cycle among saved locations by index instead of full paths.

  4. Silent switching

    pushd /tmp > /dev/null 2>&1  
    popd > /dev/null 2>&1

    Suppresses the usual “stack” output when you just want the directory change.

  5. Robust scripting

    pushd "$dir" || { echo "Cannot enter $dir" >&2; exit 1; }
    # perform actions in $dir…
    popd || exit 1

    Combine with error checks to ensure you always return (or bail) on failure.


Why use it?

  • Convenience: No need to save and restore $OLDPWD manually.
  • Clarity: dirs shows your navigation history.
  • Flexibility: Rotate and jump by index for quick context switches.