Skip to content

Instantly share code, notes, and snippets.

@wcarhart
Last active July 13, 2024 22:54
Show Gist options
  • Save wcarhart/23008155c0699b497879595c84294296 to your computer and use it in GitHub Desktop.
Save wcarhart/23008155c0699b497879595c84294296 to your computer and use it in GitHub Desktop.
Helpful Bash design patterns

Helpful Bash tidbits

# ========== Check file/directory existence ==========
if [[ -f $thing ]] ; then
echo "$thing is a file"
elif [[ -d $thing ]] ; then
echo "$thing is a directory"
elif [[ -s $thing ]] ; then
echo "$thing is a nonempty file"
fi
# ========== Check filetype ==========
function check_filetype {
# $1 is the filename
# $2 is the desired filetype
if [[ "$1" == *$2 ]] ; then
return 1
fi
return 0
}
# ========== Build a CLI parser ==========
if [[ $# -eq 0 ]] ; then
usage
exit 1
fi
while [[ $# -gt 0 ]] ; do
key="$1"
case "$key" in
# short option
-a )
# do something
shift
;;
# long option
-b|--bb )
# do something
shift
;;
# argument
-c|--arg )
# consume the argument
parse_arg "$2"
shift 2
;;
# help menu
-h|--help )
usage
exit 0
;;
# handle default
* )
>&2 echo "-err: unknown option '$key'"
exit 1
;;
esac
done
# ========== Copy to clipboard if on MacOS ==========
if [[ `uname -s` == *Darwin* ]] ; then
echo -n "$content" | pbcopy
fi
# ========== Run a detached command without an attached terminal ==========
# can be useful over SSH
cmd="echo 'hello world'"
echo "$cmd &" | script 'screen -'
# ========== Allow wildcards to return empty lists ==========
shopt -s nullglob
shopt -s dotglob
files=( /dir0/dir1/* )
shopt -u nullglob
shopt -u dotglob
# ========== Exit on errors ==========
set -e
some_risky_command
set +e
# ========== Array manipulation ==========
arr=( )
for item in "${arr[@]}" ; do ... ; done
len="${#arr[@]}"
arr2=( "abc" "def" "ghi" )
arr3=( "jkl" "mno" "pqr" )
arr=( "${arr2[@]}" "${arr3[@]}" )
for ((i = 0; i< ${#arr[@]}; i++)) ; do item="${arr[$i]}" ; ... ; done
# ========== Helpful shortcuts ==========
$$ # PID of the shell, or PID of the invoking shell (if in a subshell)
$? # exit code of previous command
$@ # expands all arguments into an array
$* # expands all arguments into a string
$# # number of arguments
$- # current option flags
$! # PID of process most recently placed in the background
$_ # at shell startup, absolute path to shell; subsequently, the last argument of the last command
$0 # name of the current script or current shell
$1 # 1st argument
$2 # 2nd argument
!! # previous command
!$ # last argument of previous command
!^ # first argument of previous command
!:2 # second argument of previous command
!:3 # third argument of previous command
# ========== Helpful string manipulation ==========
test_string="filename.json.gz"
# get prefix with '%'
echo "${test_string%.*}" # filename.json
echo "${test_string%%.*}" # filename
# get suffix with '#'
echo "${test_string#*.}" # json.gz
echo "${test_string##*.}" # gz
# ========== Modular script template ==========
#!/bin/bash
set -o errexit -o nounset -o pipefail
function main {
: Provide default command logic. ;
# parse args
# verify something
# run main logic
}
# utilities
function msg { out "$*" >&2 ;}
function err { local x=$? ; msg "$*" ; return $(( $x == 0 ? 1 : $x )) ;}
function out { printf '%s\n' "$*" ;}
# handles "no-match" exit code specified by POSIX for filtering tools.
function maybe { "$@" || return $(( $? == 1 ? 0 : $? )) ;}
# delegates to subcommands or runs main, as appropriate
if declare -F -- "${1:-}" >/dev/null ; then
"$@"
else
main
fi
# ========== Redirect multiline string to file ==========
cat << EndOfString >> myfile.txt
line 0
line 1
line 2
EndOfString
# ========== Build a script that uses a set of subcommands as functions ==========
function f1 {
if [[ "$1" == "--help" || "$1" == "-h" ]] ; then
echo -e ">> scriptname.sh f1\nDescription of function f1" | fold -w 100 -s
return
fi
# do something in f1
}
function f2 {
if [[ "$1" == "--help" || "$1" == "-h" ]] ; then
echo -e ">> scriptname.sh f2\nDescription of function f2" | fold -w 100 -s
return
fi
# do something in f2
}
function help {
if [[ "$1" == "--help" || "$1" == "-h" ]] ; then
echo -e ">> help\nShow this menu and exit" | fold -w 100 -s
return
fi
cat << EndOfHelp
Script description
Usage:
scriptname.sh COMMAND
Available commands:
`declare -F | awk '{print $NF}' | sed "s/^/ /"`
$( \
for cmd in $(declare -F | awk '{print $NF}') ; do \
echo "$(/path/to/scriptname.sh $cmd --help)" ; \
echo ; \
done \
)
EndOfHelp
}
if declare -F -- "${1:-}" >/dev/null ; then
"$@"
else
>&2 echo "-err: no such command '$1'"
>&2 echo "Use 'help' for available commands"
exit 1
fi
# ========== Create tab autocomplete ==========
# your script will need a `list` option that lists all subcommands/options
function _autocomplete {
local cur prev opts
COMPREPLY=( )
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
opts="$(/path/to/scriptname.sh list | tr '\n' ' ')"
# if you want to autocomplete flags (options starting with '-')
if [[ ${cur} == -* ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
fi
# if you want to autocomplete subcommands (options not starting with '-')
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
}
complete -F _autocomplete /path/to/scriptname.sh
# if you alias your script, like so:
alias myscript='/path/to/scriptname.sh'
# then you can tell `complete` to use the alias instead of the file:
complete -F _autocomplete myscript
# ========== Make a usage function ==========
function usage {
cat << EndOfUsage
Program description
Usage:
my_program.sh [-h] [ARGS]
Required arguments:
...
Optional arguments:
-h, --help show this help menu and exit
...
EndOfUsage
}
# you can also use <<- with HEREDOCs to avoid weird indentation
# ========== Set up variables from arguments ==========
function validate_arg {
# $1 is the variable name
# $2 is the variable content
# perform validations on $2, return 1 if they fail
# then, read into variable
read $1 <<< "$2"
# if variable content is a path, you can make it absolute to be safe:
read $1 <<< "`readlink -m $2`"
}
# ========== Verify files in a list of directories ==========
shopt -s nullglob
for res in "${resources[@]}" ; do
files=( ${!res}* )
# verify files
@w0ltage
Copy link

w0ltage commented Feb 8, 2024

Wow, this is exactly what I needed all along. Thank you for creating this gist!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment