Short BASH Snippets

BASH script starter

I put this at the top of all my scripts because most of the time I want scripts to fail on errors, and half the time I want the script to run in the directory it's in.

#!/bin/bash

# exit the script on command errors or unset variables
# http://redsymbol.net/articles/unofficial-bash-strict-mode/
set -euo pipefail
IFS=$'\n\t'

# https://stackoverflow.com/a/246128/295807
# readonly script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
# cd "${script_dir}"

Get the full path to a file

This is perl wrapped in Bash, but it's cross-platform and works on Mac and Linux. The alternative, readlink -f doesn't work on Mac.

fullpath() {
    local -r full=$(perl -e 'use Cwd "abs_path";print abs_path(shift)' "$1")
    echo "$full"
}

This snippet prints the command before running it. Stolen from StackOverflow. Great for debugging!

set -x
command
{ set +x; } 2>/dev/null

Generate and use colored print commands

Running scripts with colored output can make them much friendlier. Consider taking out the newlines if you want nested color prints. I almost never do, so I'm leaving them in...

Define the function factory

make_print_color() {
    color_name="$1"
    color_code="$2"
    color_reset="$(tput sgr0)"
    if [ -t 1 ] ; then
        eval "print_${color_name}() { printf \"${color_code}%s${color_reset}\\n\" \"\$1\"; }"
    else  # Don't print colors on pipes
        eval "print_${color_name}() { printf \"%s\\n\" \"\$1\"; }"
    fi
}

Generate pretty print functions and use them

# https://unix.stackexchange.com/a/269085/185953
make_print_color "red" "$(tput setaf 1)"
make_print_color "green" "$(tput setaf 2)"
make_print_color "yellow" "$(tput setaf 3)"
make_print_color "blue" "$(tput setaf 4)"

print_red "Always"
print_green "Seeing"
print_yellow "in"
print_blue "Color!"

# print to stderr: https://stackoverflow.com/a/2990533/2958070
print_red "Error!" >&2

Simple but effective backup command.

bak() {
    date_string="$(date +'%Y-%m-%d.%H.%M.%S')"
    if [[ -d "$1" ]]; then
        local -r no_slash="${1%/}"
        cp -r "${no_slash}" "${no_slash}.${date_string}.bak"
    elif [[ -f "$1" ]]; then
        cp "$1" "${1}.${date_string}.bak"
    else
        echo "Only files and directories supported"
    fi
}

Run a shell command on file change

I like to use entr for this. Generate some filenames and pipe them to entr. The -c option clears the screen and the -s option means use the shell.

ls log.txt | entr -c -s 'date && tail log.txt'

I use a snippet similar to this when I want to open a browser after I run a blocking command (usually starting a server). I use this particular example to learn Elm. I have something similar to run Jekyll for my blog.

learn_elm() {
    cd ~/Code/Elm || echo "Non-existant dir"
    code .
    if [[ "$(uname)" == "Darwin" ]]; then
        open_command=open
    elif [[ "$(uname)" == "Linux" ]]; then
        open_command=xdg-open
    fi
    # Open a subshell in a fork
    (sleep 2 && "${open_command}" "http://127.0.0.1:8000") &
    # Run the blocking command
    elm reactor
}

Simple Task Runner

For when you want to run some long commands with a shortcut. It does very limited arg parsing.

print_help(){
    cat << EOF
Workflow:
    $0 first|1
    $0 second|2
EOF
}

first() {
    echo "I'm first"
}

second() {
    echo "I'm second!"
}

set +u
if [ -z ${1+x} ]; then
    print_help
fi
set -u

case "$1" in
    first|1)
        first
    ;;
    second|2)
        second
    ;;
    *)
        echo "Unmatched command: $1"
        print_help
    ;;
esac

Tee stderr and stdoutto files

Save both stderr and stdout to a file. Only works in Bash. From StackOverflow

# https://stackoverflow.com/a/59435204
{ { time ./tmp_import.sh | tee tmp_import_log.stdout;} 3>&1 1>&2 2>&3- | tee tmp_import_log.stderr;} 3>&1 1>&2 2>&3-

Process each line on a file

From Unix StackExchange. I like to combine it with printing the command used.

while IFS='' read -r line || [ -n "${line}" ]; do
    set -x
    echo "$line"
    { set +x; } 2>/dev/null
done < ./file.txt

You can also pipe lines to the while loop:

pbpaste | while IFS='' read -r line || [ -n "${line}" ]; do
    echo "line: $line"
done

Iterating inline arrays in Zsh

Not a Bash snippet, but useful nonetheless :) . From SuperUser

for d (www.linkedin.com www.reddit.com www.google.com) {
    dig +short +noshort "$d"
}

Diff everything in a directory!

Consider doing a git clean before this:

git clean -xd --dry-run
git clean -xd --force
diff -qr -x '.git' folder1/ folder2/

Cross-platform colored diff

A colleague got this from somewhere on StackOverflow:

function vdiff() {
    # colored diff
    diff $@ | sed 's/^-\([^-]*\)/\x1b[31;1m-\1/;s/^+\([^+]*\)/\x1b[32;1m+\1/;s/^@/\x1b[36;1m@/;s/$/\x1b[0m/'
}

Customize dig

Unfortunately, there's no way to use multiple name servers

dig +noall +answer +question +identify @dns2.p09.nsone.net. -q linkedin.com -t ns -q linkedin.com -t a

Search and replace across files

Most people use sed for this, but sed differs between MacOS and Linux. Taken from StackOverflow:

perl -pi -w -e 's/search/replace/g;' *.php
  • -e means execute the following line of code.
  • -i means edit in-place
  • -w write warnings
  • -p loop through lins and print

See Perl 101 - Command-line Switches for other useful Perl switches.

This can be combined with find to run recursively:

find . -name '*.py' -print0 | xargs -0 perl -pi -w -e 's/"2022-04-01"/"2022-04-01-preview"/g;'

It's also possible to ignore files (NOTE: this works on MacOS)

find . -type f -not -path '*/\.git\/*' -not -path '\./rename.sh' -print0 \
    | xargs -0 perl -pi -w -e 's/example-python-cli/new-project-name/g;'

Clean unwanted Homebrew formulas

Show dependency graph (optional)

brew deps --installed --graph

Show formulas that nothing depends on (and how many dependencies they have) - https://stackoverflow.com/a/55445034/2958070

brew leaves | xargs brew deps --formula --for-each | sed "s/^.*:/$(tput setaf 4)&$(tput sgr0)/"

Then uninstall something:

brew uninstall [thing]

brew will refuse to uninstall the formula if another formula depends on it.

Run https://docs.brew.sh/Manpage#autoremove---dry-run to uninstall dependencies that are no longer needed.

brew autoremove