diff --git a/Bakefile.sh b/Bakefile.sh index d8b85d5..a36c976 100644 --- a/Bakefile.sh +++ b/Bakefile.sh @@ -1,5 +1,4 @@ # shellcheck shell=bash -# shellcheck shell=bash task.build() { nimble build dotfox "$@" diff --git a/bake b/bake index 5626e35..98b87a4 100755 --- a/bake +++ b/bake @@ -15,6 +15,8 @@ # # Learn more about it [on GitHub](https://github.com/hyperupcall/bake) +__global_bake_version='1.10.1' + if [ "$0" != "${BASH_SOURCE[0]}" ] && [ "$BAKE_INTERNAL_CAN_SOURCE" != 'yes' ]; then printf '%s\n' 'Error: This file should not be sourced' >&2 return 1 @@ -56,6 +58,7 @@ bake.info() { fi } +# breaking: remove in v2 # @description Dies if any of the supplied variables are empty. Deprecated in favor of 'bake.assert_not_empty' # @arg $@ string Names of variables to check for emptiness # @see bake.assert_not_empty @@ -91,6 +94,34 @@ bake.assert_cmd() { fi } +# @description Determine if a flag was passed as an argument +# @arg $1 string Flag name to test for +# @arg $@ string Rest of the arguments to search through +bake.has_flag() { + local flag_name="$1" + + if [ -z "$flag_name" ]; then + bake.die "Argument must not be empty" + fi + if ! shift; then + bake.die 'Failed to shift' + fi + + local -a flags=("$@") + if ((${#flags[@]} == 0)); then + flags=("${__bake_args_userflags[@]}") + fi + + local arg= + for arg in "${flags[@]}"; do + if [ "$arg" = "$flag_name" ]; then + return 0 + fi + done; unset -v arg + + return 1 +} + # @description Change the behavior of Bake. See [guide.md](./docs/guide.md) for details # @arg $1 string Name of config property to change # @arg $2 string New value of config property @@ -98,28 +129,34 @@ bake.cfg() { local cfg="$1" local value="$2" + # breaking: remove in v2 case $cfg in stacktrace) case $value in - yes|no) __bake_cfg_stacktrace=$value ;; - *) __bake_internal_die2 "Config property '$cfg' accepts only either 'yes' or 'no'" ;; + yes) __bake_internal_warn "Passing either 'yes' or 'no' as a value for 'bake.cfg stacktrace' is deprecated. Instead, use either 'on' or 'off'"; __bake_cfg_stacktrace='on' ;; + no) __bake_internal_warn "Passing either 'yes' or 'no' as a value for 'bake.cfg stacktrace' is deprecated. Instead, use either 'on' or 'off'"; __bake_cfg_stacktrace='off' ;; + on|off) __bake_cfg_stacktrace=$value ;; + *) __bake_internal_bigdie "Config property '$cfg' accepts only either 'on' or 'off'" ;; esac ;; - pedantic-task-cd) + big-print) case $value in - yes) trap '__bake_trap_debug' 'DEBUG' ;; - no) trap - 'DEBUG' ;; - *) __bake_internal_die2 "Config property '$cfg' accepts only either 'yes' or 'no'" ;; + yes|no) __bake_internal_warn "Passing either 'yes' or 'no' as a value for 'bake.cfg big-print' is deprecated. Instead, use either 'on' or 'off'" ;; + on|off) ;; + *) __bake_internal_bigdie "Config property '$cfg' accepts only either 'on' or 'off'" ;; esac ;; - big-print) + pedantic-task-cd) case $value in - yes|no) ;; - *) __bake_internal_die2 "Config property '$cfg' accepts only either 'yes' or 'no'" ;; + yes) __bake_internal_warn "Passing either 'yes' or 'no' as a value for 'bake.cfg pedantic-task-cd' is deprecated. Instead, use either 'on' or 'off'"; trap '__bake_trap_debug' 'DEBUG' ;; + no) __bake_internal_warn "Passing either 'yes' or 'no' as a value for 'bake.cfg pedantic-task-cd' is deprecated. Instead, use either 'on' or 'off'"; trap - 'DEBUG' ;; + on) trap '__bake_trap_debug' 'DEBUG' ;; + off) trap - 'DEBUG' ;; + *) __bake_internal_bigdie "Config property '$cfg' accepts only either 'on' or 'off'" ;; esac ;; *) - __bake_internal_die2 "No config property matched '$cfg'" + __bake_internal_bigdie "No config property matched '$cfg'" ;; esac } @@ -127,7 +164,7 @@ bake.cfg() { # @description Prints stacktrace # @internal __bake_print_stacktrace() { - if [ "$__bake_cfg_stacktrace" = 'yes' ]; then + if [ "$__bake_cfg_stacktrace" = 'on' ]; then if __bake_is_color; then printf '\033[4m%s\033[0m\n' 'Stacktrace:' else @@ -160,7 +197,7 @@ __bake_trap_debug() { if [[ $current_function != "$__global_bake_trap_debug_current_function" \ && $current_function == task.* ]]; then - if ! cd "$BAKE_ROOT"; then + if ! cd -- "$BAKE_ROOT"; then __bake_internal_die "Failed to cd to \$BAKE_ROOT" fi fi @@ -173,11 +210,27 @@ __bake_trap_debug() { # @exitcode 1 if should not print color # @internal __bake_is_color() { - if [[ -v NO_COLOR || $TERM == dumb ]]; then + local fd="1" + + if [ ${NO_COLOR+x} ]; then return 1 - else + fi + + if [[ $FORCE_COLOR == @(1|2|3) ]]; then return 0 + elif [[ $FORCE_COLOR == '0' ]]; then + return 1 fi + + if [ "$TERM" = 'dumb' ]; then + return 1 + fi + + if [ -t "$fd" ]; then + return 0 + fi + + return 1 } # @description Calls `__bake_internal_error` and terminates with code 1 @@ -192,7 +245,7 @@ __bake_internal_die() { # doing so, it closes with "<- ERROR" big text # @arg $1 string Text to print # @internal -__bake_internal_die2() { +__bake_internal_bigdie() { __bake_print_big '<- ERROR' __bake_internal_error "$1. Exiting" @@ -233,18 +286,128 @@ __bake_error() { fi } >&2 +# @description Parses the configuration for functions embeded in comments. This properly +# parses inherited config from the 'init' function +# @set string __bake_config_docstring +# @set array __bake_config_watchexec_args +# @set object __bake_config_map +# @internal +__bake_parse_task_comments() { + local task_name="$1" + + declare -g __bake_config_docstring= + declare -ga __bake_config_watchexec_args=() + declare -gA __bake_config_map=( + [stacktrace]='off' + [big-print]='on' + [pedantic-cd]='off' + ) + + local tmp_docstring= + local -a tmp_watch_args=() + local -A tmp_cfg_map=() + local line= + while IFS= read -r line || [ -n "$line" ]; do + if [[ $line =~ ^[[:space:]]*#[[:space:]](doc|watch|config):[[:space:]]*(.*?)$ ]]; then + local comment_category="${BASH_REMATCH[1]}" + local comment_content="${BASH_REMATCH[2]}" + + if [ "$comment_category" = 'doc' ]; then + tmp_docstring=$comment_content + elif [ "$comment_category" = 'watch' ]; then + readarray -td' ' tmp_watch_args <<< "$comment_content" + tmp_watch_args[-1]=${tmp_watch_args[-1]::-1} + elif [ "$comment_category" = 'config' ]; then + local -a pairs=() + readarray -td' ' pairs <<< "$comment_content" + pairs[-1]=${pairs[-1]::-1} + + # shellcheck disable=SC1007 + local pair= key= value= + for pair in "${pairs[@]}"; do + IFS='=' read -r key value <<< "$pair" + + tmp_cfg_map[$key]=${value:-on} + done; unset -v pair + fi + fi + + # function() + if [[ $line =~ ^([[:space:]]*function[[:space:]]*)?(.*?)[[:space:]]*\(\)[[:space:]]*\{ ]]; then + local function_name="${BASH_REMATCH[2]}" + + if [ "$function_name" == task."$task_name" ]; then + __bake_config_docstring=$tmp_docstring + + __bake_config_watchexec_args+=("${tmp_watch_args[@]}") + + local key= + for key in "${!tmp_cfg_map[@]}"; do + __bake_config_map[$key]=${tmp_cfg_map[$key]} + done; unset -v key + + break + elif [ "$function_name" == 'init' ]; then + __bake_config_watchexec_args+=("${tmp_watch_args[@]}") + + local key= + for key in "${!tmp_cfg_map[@]}"; do + __bake_config_map[$key]=${tmp_cfg_map[$key]} + done; unset -v key + fi + + tmp_docstring= + tmp_watch_args=() + tmp_cfg_map=() + fi + done < "$BAKE_FILE"; unset -v line +} + # @description Nicely prints all 'Bakefile.sh' tasks to standard output # @internal __bake_print_tasks() { - printf '%s\n' 'Tasks:' - local str= + local str=$'Tasks:\n' - # shellcheck disable=SC1007,SC2034 - local regex="^(([[:space:]]*function[[:space:]]*)?task\.(.*?)\(\)).*" - local line= + local -a task_flags=() + # shellcheck disable=SC1007 + local line= task_docstring= while IFS= read -r line || [ -n "$line" ]; do - if [[ "$line" =~ $regex ]]; then - str+=" -> ${BASH_REMATCH[3]}"$'\n' + # doc + if [[ $line =~ ^[[:space:]]*#[[:space:]]doc:[[:space:]](.*?) ]]; then + task_docstring=${BASH_REMATCH[1]} + fi + + # flag + if [[ $line =~ bake\.has_flag[[:space:]][\'\"]?([[:alnum:]]+) ]]; then + task_flags+=("[--${BASH_REMATCH[1]}]") + fi + + if [[ $line =~ ^([[:space:]]*function[[:space:]]*)?task\.(.*?)\(\)[[:space:]]*\{[[:space:]]*(#[[:space:]]*(.*))? ]]; then + local matched_function_name="${BASH_REMATCH[2]}" + local matched_comment="${BASH_REMATCH[4]}" + + if ((${#task_flags[@]} > 0)); then + str+=" ${task_flags[*]}"$'\n' + fi + task_flags=() + + str+=" -> $matched_function_name" + + if [[ -n "$matched_comment" || -n "$task_docstring" ]]; then + if [ -n "$matched_comment" ]; then + __bake_internal_warn "Adjacent documentation comments are deprecated. Instead, write a comment above 'task.$matched_function_name()' like so: '# doc: $matched_comment'" + task_docstring=$matched_comment + fi + + if __bake_is_color; then + str+=$' \033[3m'"($task_docstring)"$'\033[0m' + else + str+=" ($task_docstring)" + fi + fi + + str+=$'\n' + task_docstring= fi done < "$BAKE_FILE"; unset -v line @@ -265,7 +428,7 @@ __bake_print_tasks() { __bake_print_big() { local print_text="$1" - if [ "$__bake_cfg_big_print" = 'no' ]; then + if [ "$__bake_cfg_big_print" = 'off' ]; then return fi @@ -275,6 +438,7 @@ __bake_print_big() { if stty size &>/dev/null; then stty size else + # Only columns is used by Bake, so '20 was chosen arbitrarily if [ -n "$COLUMNS" ]; then printf '%s\n' "20 $COLUMNS" else @@ -299,11 +463,10 @@ __bake_print_big() { # @set REPLY Number of times to shift # @internal __bake_parse_args() { - unset REPLY; REPLY= + unset -v REPLY; REPLY= local -i total_shifts=0 - # FIXME: bug for when passing -v to child task argument - local __bake_arg= + local arg= for arg; do case $arg in -f) BAKE_FILE=$2 @@ -322,16 +485,30 @@ __bake_parse_args() { __bake_internal_die "Specified file '$BAKE_FILE' is not actually a file" fi ;; + -w) + ((total_shifts += 1)) + if ! shift; then + __bake_internal_die 'Failed to shift' + fi + + if [[ ! -v 'BAKE_INTERNAL_NO_WATCH_OVERRIDE' ]]; then + FLAG_WATCH='yes' + fi + ;; -v) - local bake_version='1.8.2' - printf '%s\n' "Version: $bake_version" + printf '%s\n' "Version: $__global_bake_version" + exit 0 ;; -h) local flag_help='yes' if ! shift; then __bake_internal_die 'Failed to shift' fi - esac done + ;; + *) + break + ;; + esac done; unset -v arg if [ -n "$BAKE_FILE" ]; then BAKE_ROOT=$( @@ -361,7 +538,7 @@ __bake_parse_args() { if [ "$flag_help" = 'yes' ]; then cat <<-"EOF" - Usage: bake [-h|-v] [-f ] [var=value ...] [args ...] + Usage: bake [-h|-v] [-w] [-f ] [var=value ...] [args ...] EOF __bake_print_tasks exit @@ -373,10 +550,11 @@ __bake_parse_args() { # @description Main function # @internal __bake_main() { - __bake_cfg_stacktrace='no' - __bake_cfg_big_print='yes' + bake.cfg stacktrace 'off' + bake.cfg big-print 'on' + bake.cfg pedantic-task-cd 'off' - # Environment boilerplate + # Environment and configuration boilerplate set -ETeo pipefail shopt -s dotglob extglob globasciiranges globstar lastpipe shift_verbose export LANG='C' LC_CTYPE='C' LC_NUMERIC='C' LC_TIME='C' LC_COLLATE='C' \ @@ -384,28 +562,35 @@ __bake_main() { LC_TELEPHONE='C' LC_MEASUREMENT='C' LC_IDENTIFICATION='C' LC_ALL='C' trap '__bake_trap_err' 'ERR' trap ':' 'INT' # Ensure Ctrl-C ends up printing <- ERROR ==== etc. - bake.cfg pedantic-task-cd 'no' + + declare -ga __bake_args_original=("$@") # Parse arguments # Set `BAKE_{ROOT,FILE}` - BAKE_ROOT=; BAKE_FILE= + BAKE_ROOT=; BAKE_FILE=; FLAG_WATCH= __bake_parse_args "$@" - if ! shift "$REPLY"; then + if ! shift $REPLY; then __bake_internal_die 'Failed to shift' fi # Set variables à la Make # shellcheck disable=SC1007 - local __bake_key= __bake_value= - local __bake_arg= + local __bake_key= __bake_value= __bake_arg= for __bake_arg; do case $__bake_arg in *=*) IFS='=' read -r __bake_key __bake_value <<< "$__bake_arg" + # breaking: remove in v2 + # If 'key=value' is passed, create global varaible $value declare -g "$__bake_key" local -n __bake_variable="$__bake_key" __bake_variable="$__bake_value" + # If 'key=value' is passed, create global varaible $value_key + declare -g "var_$__bake_key" + local -n __bake_variable="var_$__bake_key" + __bake_variable="$__bake_value" + if ! shift; then __bake_internal_die 'Failed to shift' fi @@ -425,39 +610,46 @@ __bake_main() { __bake_internal_die 'Failed to shift' fi - if ! cd "$BAKE_ROOT"; then - __bake_internal_die "Failed to cd" - fi + declare -ga __bake_args_userflags=("$@") - # shellcheck disable=SC2097,SC1007,SC1090 - __bake_task= source "$BAKE_FILE" + if [ "$FLAG_WATCH" = 'yes' ]; then + if ! command -v watchexec &>/dev/null; then + __bake_internal_die "Executable not found: 'watchexec'" + fi - if declare -f task."$__bake_task" >/dev/null 2>&1; then - local line= - local shouldTestNextLine='no' - while IFS= read -r line; do - if [ "$shouldTestNextLine" = 'yes' ]; then - if [[ $line == *'bake.cfg'*big-print*no* ]]; then - __bake_cfg_big_print='no' - fi - shouldTestNextLine='no' - fi + __bake_parse_task_comments "$__bake_task" - if [[ $line == @(task."$__bake_task"|init)*'('*')'*'{' ]]; then - shouldTestNextLine='yes' + # shellcheck disable=SC1007 + BAKE_INTERNAL_NO_WATCH_OVERRIDE= exec watchexec "${__bake_config_watchexec_args[@]}" "$BAKE_ROOT/bake" -- "${__bake_args_original[@]}" + else + if ! cd -- "$BAKE_ROOT"; then + __bake_internal_die "Failed to cd" + fi + + # shellcheck disable=SC2097,SC1007,SC1090 + __bake_task= source "$BAKE_FILE" + + if declare -f task."$__bake_task" >/dev/null 2>&1; then + __bake_parse_task_comments "$__bake_task" + + bake.cfg stacktrace "${__bake_config_map[stacktrace]}" + bake.cfg big-print "${__bake_config_map[big-print]}" + bake.cfg pedantic-task-cd "${__bake_config_map[pedantic-cd]}" + + __bake_print_big "-> RUNNING TASK '$__bake_task'" + + if declare -f init >/dev/null 2>&1; then + init "$__bake_task" fi - done < "$BAKE_FILE"; unset -v line shouldTestNextLine - __bake_print_big "-> RUNNING TASK '$__bake_task'" - if declare -f init >/dev/null 2>&1; then - init "$__bake_task" + task."$__bake_task" "${__bake_args_userflags[@]}" + + __bake_print_big "<- DONE" + else + __bake_internal_error "Task '$__bake_task' not found" + __bake_print_tasks + exit 1 fi - task."$__bake_task" "$@" - __bake_print_big "<- DONE" - else - __bake_internal_error "Task '$__bake_task' not found" - __bake_print_tasks - exit 1 fi }