Tuesday 2010-08-31

I hate it when I have a complex bash script that I need to run and it has lots of steps in it, any one of which can fail. However, NOT re-running all the previously succeeded steps seems efficient. And in some circumstances, running any previous steps prove catastrophic.

I could just run the script and hope it works, and when it fails, fix the failing bits, while removing the already run code, but keeping the variable declarations.

Or I could source this bash script. ;)


#!/bin/bash
set -o functrace
set -o history
set +o noclobber
shopt -s extdebug
[[ ${BASH_VERSINFO[0]} -lt 4 ]] && { 
    echo "BASH Version 4 or higher required by idempotency.sh"
	exit 1
}

idem_debug() {
	[[ "${DEBUG}" != "" ]] && echo "$@"
}
idem_is_command() {
	local cmd="$( echo "$1" | awk '{print $2}' )"
	command -v "$cmd" >/dev/null 2>&1  && return 0
	[[ -z "$( which "$cmd" 2>/dev/null)" ]] && return 1
	return 0
}

idem_debug "idempotency using source file '${0}'"
idem_dir="${0%\/*}"
[[ "${idem_dir}" == "" ]] || [[ "${idem_dir}" == "$0" ]] && { 
	idem_dir="."
}
idem_debug "idempotency using source dir '${idem_dir}'"
idem_file="${idem_dir}/.${0/#*\/}.idempotent"
idem_debug "idempotency using history file '${idem_file}'"

idem_trap() {
	local cmd=$(history 1)
	idem_debug "idempotency checking command '${cmd}' subpart '${BASH_COMMAND}'"
	
	[[ "${idem_last}" == "${cmd}" ]] && { 
		idem_debug "idempotency not re-checking subshell/pipe breakdown for '${BASH_COMMAND}'"
		return 0 # return ok
	}

	! idem_is_command "${cmd}" && { 
		idem_debug "idempotency running non-command '${cmd}'"
		return 0
	}

	export idem_last="${cmd}"
	[[ -f "${idem_file}" ]] && grep -qF "${cmd}" ${idem_file} && {
		idem_debug "idempotency not running previously run command '${cmd}'"
		return 1 # don't run the command again
	}

	echo "${cmd}" >> ${idem_file}
	idem_debug "idempotency running command '${cmd}'"
	return 0
}

trap idem_trap DEBUG

It uses the preexec hack idea from glyph, and I need to write a bunch of tests for it.