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/

Meld is also a FANTASTIC GUI app for graphically diffing directory contents.

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;'

Here's another find example to format YAML files:

find . \( -name '*.yaml' -o -name '*.yml' \) -exec yq -i -P 'sort_keys(..)' {} \;

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

List images in the terminal

Expecially useful for getting the right images in blog Markdown image links. imgcat comes from these iTerm2 docs.

imgcat -H 400px -r -p index.assets/*

Curl tips

Mastering curl: interactive text guide is an EXCELLENT blog post, but my current favorite tip is how to send an HTTP request to another IP (the below example also only shows the headers).

curl \
    --head \
    --request GET \
    --connect-to www.linkedin.com:443:www.linkedin.cn:443 \
    https://www.linkedin.com/