Skip to content

Instantly share code, notes, and snippets.

@Jamie-BitFlight
Created March 9, 2023 19:47
Show Gist options
  • Save Jamie-BitFlight/1d335e3bc8b3a320528fb8a71660e1bf to your computer and use it in GitHub Desktop.
Save Jamie-BitFlight/1d335e3bc8b3a320528fb8a71660e1bf to your computer and use it in GitHub Desktop.
To help choose which to use to check if a command or function exists this is a benchmark to compare the execution times of `type`, `declare`, `command`, and `which` in bash version 3, bash version 5, zsh version 5.8 and zsh version 5.9
#!/usr/bin/env bash
# shellcheck disable=SC2034,SC2296,SC2162
# Posix compliant way to check if a command exists,
# works in bash and zsh, and is used in this script
command_exists() { command -v "${@}" > /dev/null 2>&1; }
# I'm avoiding subshells in bash and zsh for performance reasons
\shopt -s lastpipe 2> /dev/null
reenable_lastpipe() { \shopt -u lastpipe 2> /dev/null; }
trap reenable_lastpipe EXIT
# a newline character
newline=$'\n'
# Some colors for pretty printing
cyan=$'\e[36m'
grey=$'\e[90m'
yellow=$'\e[33m'
color_reset=$'\e[0m'
# The directory to store benchmark reports
BENCHMARK_REPORT_DIR="${BENCHMARK_REPORT_DIR:-${HOME}/benchmarks}"
# Pretty print a KV pair as arguments
command_exists print_pair || print_pair() { printf '%s%s%s:%s %s%s\n' "${cyan}" "${1}" "${grey}" "${yellow}" "${2}" "${color_reset}"; }
# Pretty print a colon seperated KV pair from std input
command_exists pipe_pair || pipe_pair() { sed -E 's/^([[:alnum:]_ "'"'"'-]+):(.*)$/'$'\e[36m''\1:'$'\e[33m''\2'$'\e[0m''\n/'; }
# Indent a newline separated string
command_exists indent_newline || indent_newline() { fold -w60 -s | pr -to "${1:-17}" | sed '1s/^ *//'; }
if ! command_exists logformat_log; then
# create a templated timestamped formatted log message
logformat_log() {
local l_level="${1}"
shift
local msg="$(pipe_pair <<< "${*//\\n/${newline}}")"
date '+%H:%M:%S' | read -r l_ts
printf '%8.8s [%5.5s] %s\n' "${l_ts}" "${l_level}" "${msg}" | indent_newline
}
fi
# log a message to stdout
command_exists info_log || info_log() { logformat_log "INFO" "${*}"; }
# log a message to stderr
command_exists error_log || error_log() { logformat_log "ERROR" "${*}" 1>&2; }
# Check that the benchmark utility is installed
if ! command_exists hyperfine && command_exists brew; then
info_log "Installing: hyperfine"
if brew install hyperfine > /dev/null 2>&1; then
info_log "hyperfine installed"
else
error_log "hyperfine failed to automatically install using 'brew install hyperfine'"
fi
fi
# Announce how to install the benchmark utility if it's not installed
if ! command_exists hyperfine; then
printf 'hyperfine is not installed, please install it with your package manager\n'
printf 'https://github.com/sharkdp/hyperfine#installation\n\n'
exit 1
fi
# track each run of this script with an incrementing run id
run_id() {
local BENCHMARK_REPORT_DIR="${1:-${HOME}/benchmarks}"
[[ -d ${BENCHMARK_REPORT_DIR} ]] || mkdir -p "${BENCHMARK_REPORT_DIR}"
local id_file_path="${BENCHMARK_REPORT_DIR}/.runId"
local RUN_ID
if [[ -f ${id_file_path} ]]; then
read -r RUN_ID < "${id_file_path}"
fi
# If the file is empty, set it to 0
: "${RUN_ID:=0}"
# Increment the run id
((RUN_ID += 1))
# Write the new run id to the file, and stdout
printf '%s' "${RUN_ID}" | tee "${BENCHMARK_REPORT_DIR}/.runId"
}
# Print first line of the shell version output
shelli() { "${@}" --version 2>&1 | head -n 1; }
# clean, as in, no profile, or rc files to slow down startup times
clean_shell_command() {
local sn="$1"
case "${sn}" in
*"bash") printf '%s' "${sn} --norc --noprofile" ;;
*"zsh") printf '%s' "${sn} -fd" ;;
*) printf '%s' "${sn}" ;;
esac
}
# Get the shell name and version for labeling the report
shell_command_report_name() {
local sn="$1"
local runId="$2"
local version
case "${sn}" in
*"bash") ${sn} -c 'echo ${BASH_VERSION}' | read -r version ;;
*"zsh") ${sn} -c 'echo ${ZSH_VERSION}' | read -r version ;;
*) exit 1 ;;
esac
printf '%s' "${sn##*/}-${version}-run${runId}"
}
main() {
local BENCHMARK_REPORT_DIR="${BENCHMARK_REPORT_DIR:-${HOME}/benchmarks}"
[[ -d ${BENCHMARK_REPORT_DIR} ]] || mkdir -p "${BENCHMARK_REPORT_DIR}"
# Read in the output of run_id() and set it to RUN_ID without using a subshell
# works in bash and zsh
\shopt -s lastpipe 2> /dev/null
run_id "${BENCHMARK_REPORT_DIR:-}" | read -r RUN_ID
uname -v | read -r SYSTEM_INFO
info_log "This is run number: ${RUN_ID}\nSystem info: ${SYSTEM_INFO}\nBenchmarking functions to check if a command or function exists already\n"
test_with_shell() {
local sn="${1}"
local test_negative_result="${2:-false}"
if [[ -z ${sn} ]]; then
printf 'No shell provided to test_with_shell()\n'
return 1
fi
clean_shell_command "${sn}" | read -r CLEAN_SHELL_COMMAND
info_log "Shell command: ${CLEAN_SHELL_COMMAND}"
local prefix
if [[ ${test_negative_result} == "false" ]]; then
prefix="positive_result-"
hf_params+=("--prepare" "ef() { return 0; };")
else
# Allow Failures
hf_params+=("-i")
prefix="negative_result-"
fi
local report_name="${prefix:-}$(shell_command_report_name "${sn}" "${RUN_ID}")"
local hf_params=(
"--export-markdown=${BENCHMARK_REPORT_DIR}/${report_name}.md"
"--warmup=10"
"--shell=${CLEAN_SHELL_COMMAND}"
)
if [[ -n ${DEBUG+x} ]]; then
if [[ ${DEBUG} == "stdout" ]]; then
hf_params+=("--show-output")
else
hf_params+=("--output=${BENCHMARK_REPORT_DIR}/${report_name}-output.txt")
fi
fi
info_log "Benchmarking within: $(shelli "${sn}")"
tests_to_run=(
"-n" "${report_name} type" "ef() { return 0; }; type ef"
"-n" "${report_name} command" "ef() { return 0; }; command -v ef"
"-n" "${report_name} which" "ef() { return 0; }; which ef"
)
if [[ -n ${TEST_SHELL_FUNCTIONS_ONLY+x} ]]; then
tests_to_run+=("-n" "${report_name} declare" "ef() { return 0; }; declare -Ff ef")
fi
hyperfine \
"${hf_params[@]}" \
"${tests_to_run[@]}" \
2>> "${BENCHMARK_REPORT_DIR}/hyperfine.error.log"
}
## Other ways that can be used in bash to check if a function exists
## Discarded as they didn't have an equivalent in zsh
################################################################################################
#
# As per: https://zsh.sourceforge.io/Doc/Release/Shell-Builtin-Commands.html
# """"
# declare
# Same as typeset.
# """"
## -n "${report_name} typeset" "ef() { return 0; };typeset -fF ef" \
#
# Compgen is a bash builtin, not a zsh builtin
# so for zsh it requires this to be run first to load the bashcompinit function:
# ```
# autoload bashcompinit
# bashcompinit
# ```
# but it still needs to be piped to grep to get the same output as bash
## -n "${report_name} compgen" "ef() { return 0; };compgen -A function | grep -E '^ef$'"
# Find the path to the brew installed zsh and bash
brew --prefix zsh | read -r BREW_ZSH_PREFIX
brew --prefix bash | read -r BREW_BASH_PREFIX
# test Success with GNU bash, version 3.2.57(1)-release
[[ -n ${ZSH_TESTS_ONLY+x} ]] || test_with_shell '/bin/bash'
# Test Success with GNU bash, version 5.2.15(1)-release
[[ -n ${ZSH_TESTS_ONLY+x} ]] || test_with_shell "${BREW_BASH_PREFIX}/bin/bash"
# test failure with GNU bash, version 3.2.57(1)-release
[[ -n ${ZSH_TESTS_ONLY+x} ]] || test_with_shell '/bin/bash' "true"
# Test failure with GNU bash, version 5.2.15(1)-release
[[ -n ${ZSH_TESTS_ONLY+x} ]] || test_with_shell "${BREW_BASH_PREFIX}/bin/bash" "true"
# Test Success with zsh 5.8.1 (x86_64-apple-darwin21.0)
[[ -n ${BASH_TESTS_ONLY+x} ]] || test_with_shell '/bin/zsh'
# Test Success with zsh 5.9 (x86_64-apple-darwin21.3.0)
[[ -n ${BASH_TESTS_ONLY+x} ]] || test_with_shell "${BREW_ZSH_PREFIX}/bin/zsh"
# Test failure with zsh 5.8.1 (x86_64-apple-darwin21.0)
[[ -n ${BASH_TESTS_ONLY+x} ]] || test_with_shell '/bin/zsh' "true"
# Test failure with zsh 5.9 (x86_64-apple-darwin21.3.0)
[[ -n ${BASH_TESTS_ONLY+x} ]] || test_with_shell "${BREW_ZSH_PREFIX}/bin/zsh" "true"
}
if [[ $1 == 'bash' ]]; then
BASH_TESTS_ONLY=true
elif [[ $1 == 'zsh' ]]; then
ZSH_TESTS_ONLY=true
fi
main

Hyperfine Benchmark Results

Command Mean [ms] Min [ms] Max [ms] Relative
negative_result-bash-3.2.57(1)-release-run23 type 0.1 ± 0.7 0.0 17.7 5.63 ± 81.02
negative_result-bash-3.2.57(1)-release-run23 command 0.0 ± 0.1 0.0 0.8 1.00
Command Mean [ms] Min [ms] Max [ms] Relative
negative_result-bash-5.2.15(1)-release-run23 type 0.1 ± 0.9 0.0 15.6 3.09 ± 25.90
negative_result-bash-5.2.15(1)-release-run23 command 0.0 ± 0.2 0.0 1.4 1.00
Command Mean [ms] Min [ms] Max [ms] Relative
negative_result-zsh-5.8.1-run23 type 0.0 ± 0.1 0.0 1.1 1.00
negative_result-zsh-5.8.1-run23 command 0.1 ± 0.6 0.0 11.5 11.65 ± 92.59
negative_result-zsh-5.8.1-run23 which 0.1 ± 0.3 0.0 3.0 5.95 ± 46.50
Command Mean [ms] Min [ms] Max [ms] Relative
negative_result-zsh-5.9-run23 type 0.5 ± 1.3 0.0 17.8 6.72 ± 30.10
negative_result-zsh-5.9-run23 command 0.3 ± 2.0 0.0 27.1 4.84 ± 32.18
negative_result-zsh-5.9-run23 which 0.1 ± 0.3 0.0 2.6 1.00
Command Mean [ms] Min [ms] Max [ms] Relative
positive_result-bash-3.2.57(1)-release-run23 type 0.1 ± 0.7 0.0 13.3 1.00
positive_result-bash-3.2.57(1)-release-run23 command 0.1 ± 0.7 0.0 11.6 1.61 ± 21.51
Command Mean [ms] Min [ms] Max [ms] Relative
positive_result-bash-5.2.15(1)-release-run23 type 0.1 ± 0.8 0.0 13.2 21.40 ± 255.24
positive_result-bash-5.2.15(1)-release-run23 command 0.0 ± 0.0 0.0 0.5 1.00
Command Mean [ms] Min [ms] Max [ms] Relative
positive_result-zsh-5.8.1-run23 type 0.5 ± 3.2 0.0 45.2 88.90 ± 1045.56
positive_result-zsh-5.8.1-run23 command 0.0 ± 0.1 0.0 1.0 1.00
positive_result-zsh-5.8.1-run23 which 0.1 ± 1.2 0.0 25.7 15.09 ± 254.07
Command Mean [ms] Min [ms] Max [ms] Relative
positive_result-zsh-5.9-run23 type 0.3 ± 0.4 0.0 2.8 1.00
positive_result-zsh-5.9-run23 command 0.5 ± 1.8 0.0 27.5 1.48 ± 5.83
positive_result-zsh-5.9-run23 which 0.9 ± 2.3 0.0 25.1 2.76 ± 7.73
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment