Skip to content

Instantly share code, notes, and snippets.

@jlinoff
Created February 8, 2018 17:59
Show Gist options
  • Save jlinoff/d7e7d789b32c9caeb1ada7877e1f1a3f to your computer and use it in GitHub Desktop.
Save jlinoff/d7e7d789b32c9caeb1ada7877e1f1a3f to your computer and use it in GitHub Desktop.
Use bash introspection to enable simple state handling.
#!/bin/bash
#
# Tests the state machine handler.
# Define 5 functions, auto-generate a state machine control mechanism to
# determine which functions are executed.
#
# License: MIT Open Source
# Copyright (c) 2018 by Joe Linoff
source state-machine.sh
# ================================================================
# Functions
# ================================================================
function s01:func() {
if ! _state_enabled ; then return 0 ; fi
echo "Function: ${FUNCNAME[0]}"
}
function s02:func() {
if ! _state_enabled ; then return 0 ; fi
echo "Function: ${FUNCNAME[0]}"
}
function s03:func() {
if ! _state_enabled ; then return 0 ; fi
echo "Function: ${FUNCNAME[0]}"
}
function s04:func() {
if ! _state_enabled ; then return 0 ; fi
echo "Function: ${FUNCNAME[0]}"
}
function s05:func() {
if ! _state_enabled ; then return 0 ; fi
echo "Function: ${FUNCNAME[0]}"
}
# ================================================================
# Main
# ================================================================
_state_init
# Figure out what to print.
if ! _state_define_range $1 ; then
echo "ERROR: Invalid range: '$1'."
exit 1
fi
s01:func
s02:func
s03:func
s04:func
s05:func
#!/bin/bash
#
# Implements a state machine for functions of the form:
# s##:<func-name>
#
# Examples:
# s01:_clean
# s02:_create_build_directory
#
# It is used like this.
# 1. call _state_init early in the program. It parses
# the bash script using introspection and uses sets
# up the state infrastructure.
#
# 2. In each function with the S##: prefix, put this
# at the top:
# if ! _state_enabled ; then return 0 ; fi
#
# 3. Add this to the argument parsing to process the
# range information:
# if ! _state_define_range $1 ; then
# echo "ERROR: Invalid range: '$1'."
# exit 1
# fi
# Valid ranges are described in the comments for
# for the _state_define_range function.
#
# License: MIT Open Source
# Copyright (c) 2018 by Joe Linoff
# ================================================================
# Functions
# ================================================================
# Get the state id from the function name.
# We expect a s##: prefix.
function _state_get_id() {
local FuncName="$1"
echo $FuncName | awk -F: '{x=substr($1,2); gsub("^0*", "", x); print x}'
}
# Determine whether the caller should execute.
# Here is how the function should test:
# if ! _state_enabled ${FUNCNAME[0]} ; then return 0 ; fi
# OR
# if ! _state_enabled ; then return 0 ; fi
function _state_enabled() {
local FuncName="$1"
[ -z "$FuncName" ] && FuncName=${FUNCNAME[1]}
local StateId=$(_state_get_id $FuncName)
return ${_STATE_TABLE[$StateId]}
}
# Initialize the state handling.
# When this function completes, all state functions will have been
# analyzed using bash introspection and a global array of states
# called _STATE_TABLE will have been declared and populated.
# The contents of this array can be used by each state function to
# determine whether it should execute.
# It also defines three constants:
# _STATE_ENABLE - enables a specific state
# _STATE_DISABLE - disables a specific state
# _STATE_TABLE_MAX - max entry in the state table (for range checking)
function _state_init() {
local Verbose=0
[ -n "$1" ] && Verbose=1
# Find the state functions.
(( Verbose )) && echo "Function: ${FUNCNAME[0]}" || true
local Names=($(set | grep '^s[0-9][0-9]:' | awk '{print $1}') )
# Iterate over them to find the maximum.
_STATE_TABLE_MAX=0
for Name in ${Names[@]} ; do
local Id=$(_state_get_id $Name)
(( Verbose )) && echo " State: $Id $Name" || true
(( Id > _STATE_TABLE_MAX )) && _STATE_TABLE_MAX=$Id || true
done
(( Verbose )) && echo " Max: $_STATE_TABLE_MAX" || true
# Declare a global array based on the maximum.
declare -a _STATE_TABLE
for (( i=0; i<=_STATE_TABLE_MAX; i++ )) ; do
_STATE_TABLE[$i]=0
done
_STATE_ENABLE=0
_STATE_DISABLE=1
}
# Parse the state range specification.
# The state range specification defines a subset of the available
# states that will be executed.
#
# The form of the range is <BEG>:<END> where <BEG> is the first state
# function in the range to execute and <END> is the last (inclusive).
# If <BEG> is not specified, the first state function is assumed.
# If <ENV> is not specified, the last state function is assumed.
#
# Examples:
# 3 Only execute state 3, all others are disabled.
# :3 Execute all states less than or equal to state 3.
# 0:3 Execute all states less than or equal to state 3.
# 3: Execute all states greater than or equal to state 3.
# 3:5 Execute all states 3, 4 and 5.
# When this function is complete the global state table will be set
# properly for enabling and disabling state functions.
function _state_define_range() {
local Range="$1"
local RangeBeg=0
local RangeEnd=0
if [ -n "$Range" ] ; then
# Parse the range.
if [[ "$Range" =~ ^[0-9]+$ ]] ; then
RangeBeg=$Range
RangeEnd=$Range
elif [[ "$Range" =~ ^[0-9]+:[0-9]+$ ]] ; then
RangeBeg=$(echo "$Range" | awk -F: '{print $1}')
RangeEnd=$(echo "$Range" | awk -F: '{print $2}')
elif [[ "$Range" =~ ^:[0-9]+$ ]] ; then
RangeBeg=1
RangeEnd=$(echo "$Range" | awk -F: '{print $1}')
elif [[ "$Range" =~ ^[0-9]+:$ ]] ; then
RangeBeg=$(echo "$Range" | awk -F: '{print $1}')
RangeEnd=$_STATE_TABLE_MAX
else
# Error, unrecognized range.
return 1
fi
# Is there anything to do?
if (( RangeEnd < RangeBeg )) ; then
# The convention is have a monotonically increasing range.
return 0
fi
if (( RangeBeg > _STATE_TABLE_MAX )) ; then
# The beginning is out of range, there is nothing to do.
return 0 ;
fi
if (( RangeEnd < 1 )) ; then
# The end is out of range, there is nothing to do.
return 0 ;
fi
# Set the states.
for (( i=1 ; i <= _STATE_TABLE_MAX ; i++ )) ; do
if (( i < RangeBeg )) || (( i > RangeEnd )) ; then
_STATE_TABLE[$i]=$_STATE_DISABLE
else
_STATE_TABLE[$i]=$_STATE_ENABLE
fi
done
fi
return 0
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment