diff --git a/lib/bats/helpers b/lib/bats/helpers index 382d59b..b406ba9 100644 --- a/lib/bats/helpers +++ b/lib/bats/helpers @@ -210,6 +210,7 @@ test_printf() { printf "$@" >&2 fi } +export -f test_printf # Skips a test if `TEST_FILTER` is set but doesn't match `BATS_TEST_DESCRIPTION` # diff --git a/lib/format b/lib/format index bac932f..059b749 100644 --- a/lib/format +++ b/lib/format @@ -3,9 +3,11 @@ # Text formatting utilities # # Exports: +# @go.array_printf +# Assigns `printf` transformations of its arguments to an array # # @go.pad_items -# Pads each string in an array to match the length of the longest element +# Right-pads each string with spaces to match the length of the longest # # @go.zip_items # Concatenates parallel elements from each input array @@ -13,67 +15,101 @@ # @go.strip_formatting_codes # Strips ANSI escape codes of the form `\e[...(;...)m` from a string -# Pads each string in an array to match the length of the longest element +. "$_GO_USE_MODULES" 'strings' 'validation' + +# Assigns `printf` transformations of its arguments to an array +# +# Since `printf -v` can't print to an array subscript prior to Bash 4.1, this +# provides a portable means of printing to an array variable while avoiding the +# use of `eval`. +# +# NOTE: By default, this function relies on the ASCII Record Separator character +# ($'\x1f') to delimit generated strings before splitting them into the result +# array. If you have strings containing this character, you can set a new +# delimiter via `_GO_ARRAY_PRINTF_DELIMITER`. +# +# Globals: +# _GO_ARRAY_PRINTF_DELIMITER: +# If set, used to separate generated strings prior to array assignment +# +# Arguments: +# result: Name of the caller-declared output array +# format: `printf`-style format specification +# ...: Items to pass to `printf` and store in `result` +@go.array_printf() { + @go.validate_identifier_or_die 'Result array name' "$1" + local __go_array_printf_delim="${_GO_ARRAY_PRINTF_DELIMITER:-$'\x1f'}" + local __tmp_go_array_printf + printf -v __tmp_go_array_printf -- "${2}${__go_array_printf_delim}" "${@:3}" + @go.split "$__go_array_printf_delim" "$__tmp_go_array_printf" "$1" +} + +# Right-pads each string with spaces to match the length of the longest +# +# Globals: +# _GO_ARRAY_PRINTF_DELIMITER: See the comments for `@go.array_printf` # # Arguments: -# $1: Name of the input array in the caller's scope -# Outputs: -# __go_padded_result: The caller-declared array to which results are assigned +# result: Name of the caller-declared output array +# ...: Items to right-pad with spaces to match the longest one @go.pad_items() { - local items_reference=("${1}[@]") - local item - local padding='' - local padding_len=0 + @go.validate_identifier_or_die 'Result array name' "$1" + local __go_pad_items_items=("${@:2}") + local __item + local padding_size=0 - for item in "${!items_reference}"; do - while [[ "${#padding}" -lt "${#item}" ]]; do - padding+=' ' + for __item in "${__go_pad_items_items[@]}"; do + while [[ "$padding_size" -lt "${#__item}" ]]; do + padding_size="${#__item}" done done - - for item in "${!items_reference}"; do - padding_len="$((${#padding} - ${#item}))" - __go_padded_result+=("${item}${padding:0:padding_len}") - done + @go.array_printf "$1" "%-${padding_size}s" "${__go_pad_items_items[@]}" } # Concatenates parallel elements from each input array # # Will produce a number of results matching that of the left-hand input array. # +# Globals: +# _GO_ARRAY_PRINTF_DELIMITER: See the comments for `@go.array_printf` +# # Arguments: -# $1: Name of the left-hand input array in the caller's scope -# $2: Name of the right-hand input array in the caller's scope -# $3: The string used as a delimiter between elements (defaults to two spaces) -# Outputs: -# __go_zipped_result: The caller-declared array to which results are assigned +# lhs: Name of the left-hand input array in the caller's scope +# rhs: Name of the right-hand input array in the caller's scope +# delim: String used as a delimiter between elements (default: two spaces) +# result: Name of the caller-declared output array @go.zip_items() { + @go.validate_identifier_or_die 'Result array name' "$4" local lhs_array_reference="${1}[@]" - local rhs_reference="$2" - local delimiter="${3:- }" local rhs_item_ref local item - local i=-1 + local i=0 + local __tmp_go_zip_items_result=() for item in "${!lhs_array_reference}"; do - rhs_item_ref="${rhs_reference}[$((++i))]" - __go_zipped_result+=("${item}${delimiter}${!rhs_item_ref}") + rhs_item_ref="${2}[$((i++))]" + __tmp_go_zip_items_result+=("${item}${3}${!rhs_item_ref}") done + @go.array_printf "$4" '%s' "${__tmp_go_zip_items_result[@]}" } -# Strips ANSI escape codes of the form `\e[...(;...)m` from a string +# Strips ANSI escape codes from a string # # Used primarily by `@go.log`. # # Arguments: -# $1: The string to strip -# Outputs: -# __go_stripped_value: The caller-declared variable for the stripped result +# original: The string to strip +# result: Name of the caller-declared output variable @go.strip_formatting_codes() { - __go_stripped_value="$1" - local format_pattern='\\e\[[0-9]{1,3}(;[0-9]{1,3})*m' + @go.validate_identifier_or_die 'Result variable name' "$2" + printf -v "$2" -- '%b' "$1" + + if [[ -z "$__GO_STRIP_FORMATTING_PATTERN" ]]; then + printf -v __GO_STRIP_FORMATTING_PATTERN '%b' '\e[[0-9]{1,3}(;[0-9]{1,3})*m' + readonly __GO_STRIP_FORMATTING_PATTERN + fi - while [[ "$__go_stripped_value" =~ $format_pattern ]]; do - __go_stripped_value="${__go_stripped_value/"${BASH_REMATCH[0]}"}" + while [[ "${!2}" =~ $__GO_STRIP_FORMATTING_PATTERN ]]; do + printf -v "$2" -- '%s' "${!2/"${BASH_REMATCH[0]}"}" done } diff --git a/lib/log b/lib/log index c674564..191f861 100644 --- a/lib/log +++ b/lib/log @@ -225,7 +225,7 @@ readonly __GO_LOG_COMMAND_EXIT_PATTERN='^@go.log_command (exit|fatal):([0-9]+)$' local log_level="${args[0]^^}" local exit_status=0 local log_msg - local __go_stripped_value + local stripped_log_msg local level_fd unset 'args[0]' @@ -266,10 +266,10 @@ readonly __GO_LOG_COMMAND_EXIT_PATTERN='^@go.log_command (exit|fatal):([0-9]+)$' _@go.log_command_should_skip_file_descriptor "$level_fd"; then continue elif [[ ! -t "$level_fd" && -z "$_GO_LOG_FORMATTING" ]]; then - if [[ -z "$__go_stripped_value" ]]; then - @go.strip_formatting_codes "$log_msg" + if [[ -z "$stripped_log_msg" ]]; then + @go.strip_formatting_codes "$log_msg" 'stripped_log_msg' fi - printf '%s\n' "$__go_stripped_value" >&"$level_fd" + printf '%s\n' "$stripped_log_msg" >&"$level_fd" else printf '%b\n' "$log_msg" >&"$level_fd" fi diff --git a/libexec/modules b/libexec/modules index 34dcaf0..6a427cf 100755 --- a/libexec/modules +++ b/libexec/modules @@ -151,23 +151,23 @@ _@go.modules_produce_listing() { . "$_GO_CORE_DIR/lib/format" - local __go_padded_result=() - local __go_zipped_result=() + local padded_modules=() + local zipped_modules=() - @go.pad_items 'modules' - modules=("${__go_padded_result[@]}") + @go.pad_items padded_modules "${modules[@]}" + modules=("${padded_modules[@]}") case "$action" in paths) local relative_paths=("${__go_modules[@]#$_GO_ROOTDIR/}") - @go.zip_items 'modules' 'relative_paths' + @go.zip_items modules relative_paths ' ' zipped_modules ;; summaries) local __go_modules_summaries=() if ! _@go.modules_summaries; then return 1 fi - @go.zip_items 'modules' '__go_modules_summaries' + @go.zip_items modules __go_modules_summaries ' ' zipped_modules ;; *) # Should only happen if _@go.modules is updated and this case statement @@ -175,7 +175,7 @@ _@go.modules_produce_listing() { @go.printf 'ERROR: Unknown action: %s\n' "$action" >&2 return 1 esac - __go_modules_listing=("${__go_zipped_result[@]}") + __go_modules_listing=("${zipped_modules[@]}") } _@go.modules_search() { diff --git a/tests/bats-helpers.bats b/tests/bats-helpers.bats index 201ac86..6d4b389 100644 --- a/tests/bats-helpers.bats +++ b/tests/bats-helpers.bats @@ -145,7 +145,6 @@ teardown() { @test "$SUITE: test_printf" { create_bats_test_script test-script \ - ". '$_GO_CORE_DIR/lib/bats/helpers'" \ "test_printf '%s\n' 'some test debug output'" run "$BATS_TEST_ROOTDIR/test-script" diff --git a/tests/format.bats b/tests/format.bats index 46973c6..21ec931 100644 --- a/tests/format.bats +++ b/tests/format.bats @@ -3,67 +3,158 @@ load environment setup() { - . 'lib/format' + test_filter } -@test "$SUITE: does nothing for empty argv" { - local items=() - local __go_padded_result=() - @go.pad_items 'items' - assert_equal '' "${__go_padded_result[*]}" +teardown() { + remove_test_go_rootdir } -@test "$SUITE: pads argv items" { - local items=('foo' 'bar' 'baz' 'xyzzy' 'quux') - local __go_padded_result=() - @go.pad_items 'items' +create_go_format_script() { + create_test_go_script '. "$_GO_USE_MODULES" format' "$@" +} + +run_array_printf_script() { + create_go_format_script \ + 'declare result=()' \ + '@go.array_printf result "%s" "$@"' \ + 'IFS="|"' \ + 'printf "%s\n" "${result[*]}"' + run "$TEST_GO_SCRIPT" "$@" +} + +run_pad_items_script() { + create_go_format_script \ + 'declare padded=()' \ + '@go.pad_items padded "$@"' \ + 'IFS="|"' \ + 'printf "%s\n" "${padded[*]}"' + run "$TEST_GO_SCRIPT" "$@" +} + +run_zip_items_script() { + create_go_format_script \ + 'declare lhs_items=($1)' \ + 'declare rhs_items=($2)' \ + 'declare zipped=()' \ + '@go.zip_items lhs_items rhs_items "$3" zipped' \ + 'printf "%s\n" "${zipped[@]}"' + run "$TEST_GO_SCRIPT" "$@" +} - local IFS='|' - assert_equal 'foo |bar |baz |xyzzy|quux ' "${__go_padded_result[*]}" +run_strip_formatting_codes_script() { + create_go_format_script \ + 'declare stripped' \ + '@go.strip_formatting_codes "$*" stripped' \ + 'printf "%s\n" "$stripped"' + run "$TEST_GO_SCRIPT" "$@" } -@test "$SUITE: zip empty items" { - local lhs=() - local rhs=() - local __go_zipped_result=() - @go.zip_items 'lhs' 'rhs' '=' +@test "$SUITE: array_printf validates result array name" { + create_test_go_script '. "$_GO_USE_MODULES" format' \ + '@go.array_printf "3foobar"' + run "$TEST_GO_SCRIPT" - assert_equal '' "${__go_zipped_result[*]}" + local err_msg='Result array name "3foobar" for @go.array_printf ' + err_msg+='must not start with a number at:' + assert_failure "$err_msg" \ + " $TEST_GO_SCRIPT:4 main" } -@test "$SUITE: zip matching items" { +@test "$SUITE: array_printf does nothing for empty argv" { + run_array_printf_script + assert_success '' +} + +@test "$SUITE: array_printf prints argv items" { + run_array_printf_script 'foo' 'bar' 'baz' 'xyzzy' 'quux' + assert_success 'foo|bar|baz|xyzzy|quux' +} + +@test "$SUITE: array_printf prints argv items with different delimiter" { + _GO_ARRAY_PRINTF_DELIMITER=$'\x1e' run_array_printf_script \ + $'foo\x1f' $'bar\x1f' $'baz\x1f' $'xyzzy\x1f' $'quux\x1f' + assert_success $'foo\x1f|bar\x1f|baz\x1f|xyzzy\x1f|quux\x1f' +} + +@test "$SUITE: pad_items validates result array name" { + create_test_go_script '. "$_GO_USE_MODULES" format' \ + '@go.pad_items "3foobar"' + run "$TEST_GO_SCRIPT" + + local err_msg='Result array name "3foobar" for @go.pad_items ' + err_msg+='must not start with a number at:' + assert_failure "$err_msg" \ + " $TEST_GO_SCRIPT:4 main" +} + +@test "$SUITE: pad_items does nothing for empty argv" { + run_pad_items_script + assert_success '' +} + +@test "$SUITE: pad_items pads argv items" { + run_pad_items_script 'foo' 'bar' 'baz' 'xyzzy' 'quux' + assert_success 'foo |bar |baz |xyzzy|quux ' +} + +@test "$SUITE: zip_items validates result array name" { + create_test_go_script '. "$_GO_USE_MODULES" format' \ + '@go.zip_items lhs rhs = "3foobar"' + run "$TEST_GO_SCRIPT" + + local err_msg='Result array name "3foobar" for @go.zip_items ' + err_msg+='must not start with a number at:' + assert_failure "$err_msg" \ + " $TEST_GO_SCRIPT:4 main" +} + +@test "$SUITE: zip_items does nothing for empty argv" { + run_zip_items_script + assert_success '' +} + +@test "$SUITE: zip_items zips matching items with supplied delimiter" { local lhs=('foo' 'xyzzy' 'quux') local rhs=('bar' 'baz' 'plugh') - local __go_zipped_result=() - @go.zip_items 'lhs' 'rhs' '=' + run_zip_items_script "${lhs[*]}" "${rhs[*]}" '=' + assert_success 'foo=bar' 'xyzzy=baz' 'quux=plugh' +} + +@test "$SUITE: strip_formatting_codes validates result array name" { + create_test_go_script '. "$_GO_USE_MODULES" format' \ + '@go.strip_formatting_codes "foobar" "3foobar"' + run "$TEST_GO_SCRIPT" + + local err_msg='Result variable name "3foobar" for @go.strip_formatting_codes ' + err_msg+='must not start with a number at:' + assert_failure "$err_msg" \ + " $TEST_GO_SCRIPT:4 main" +} - local expected=('foo=bar' 'xyzzy=baz' 'quux=plugh') - local IFS=$'\n' - local indent=' ' - assert_equal $'\n'"${expected[*]/#/$indent}" \ - $'\n'"${__go_zipped_result[*]/#/$indent}" +@test "$SUITE: strip_formatting_codes from empty string" { + run_strip_formatting_codes_script '' + assert_success '' } -@test "$SUITE: strip formatting codes from empty string" { - local __go_stripped_value - @go.strip_formatting_codes '' - assert_equal '' "$__go_stripped_value" +@test "$SUITE: strip_formatting_codes from string with no codes" { + run_strip_formatting_codes_script 'foobar' + assert_success 'foobar' } -@test "$SUITE: strip formatting codes from string with no codes" { - local __go_stripped_value - @go.strip_formatting_codes 'foobar' - assert_equal 'foobar' "$__go_stripped_value" +@test "$SUITE: strip_formatting_codes from string with one code" { + run_strip_formatting_codes_script 'foobar\e[0m' + assert_success 'foobar' } -@test "$SUITE: strip formatting codes from string with one code" { - local __go_stripped_value - @go.strip_formatting_codes 'foobar\e[0m' - assert_equal 'foobar' "$__go_stripped_value" +@test "$SUITE: strip_formatting_codes from string with multiple codes" { + run_strip_formatting_codes_script '\e[1mf\e[30;47mo\e[0;111mo\e[32mbar\e[0m' + assert_success 'foobar' } -@test "$SUITE: strip formatting codes from string with multiple codes" { - local __go_stripped_value - @go.strip_formatting_codes '\e[1mf\e[30;47mo\e[0;111mo\e[32mbar\e[0m' - assert_equal 'foobar' "$__go_stripped_value" +@test "$SUITE: strip_formatting_codes from string with expanded codes" { + local orig_value + printf -v orig_value '%b' '\e[1mf\e[30;47mo\e[0;111mo\e[32mbar\e[0m' + run_strip_formatting_codes_script "$orig_value" + assert_success 'foobar' } diff --git a/tests/strings/split.bats b/tests/strings/split.bats index c9d1b8d..81184a7 100644 --- a/tests/strings/split.bats +++ b/tests/strings/split.bats @@ -42,6 +42,14 @@ teardown() { assert_success 'foo bar baz' } +@test "$SUITE: multiple items using ASCII unit separator" { + create_strings_test_script 'declare result=()' \ + "@go.split \$'\x1f' $'foo\x1fbar\x1fbaz' result" \ + 'echo "${result[@]}"' + run "$TEST_GO_SCRIPT" + assert_success 'foo bar baz' +} + @test "$SUITE: split items into same variable" { create_strings_test_script 'declare items="foo,bar,baz"' \ '@go.split "," "$items" "items"' \ diff --git a/tests/vars.bats b/tests/vars.bats index 2f9cc85..b0dea67 100644 --- a/tests/vars.bats +++ b/tests/vars.bats @@ -73,13 +73,18 @@ quotify_expected() { "[4]=\"$TEST_GO_PLUGINS_DIR/plugin2/bin\"" "[5]=\"$TEST_GO_SCRIPTS_DIR\"") + # Note that the `format` module imports `strings` and `validation`. + local expected_modules=('[0]="complete"' + '[1]="format"' + '[2]="strings"' + '[3]="validation"') local expected=("declare -rx _GO_CMD=\"$TEST_GO_SCRIPT\"" "declare -ax _GO_CMD_ARGV=(${cmd_argv[*]})" 'declare -ax _GO_CMD_NAME=([0]="test-command" [1]="test-subcommand")' "declare -rx _GO_CORE_DIR=\"$_GO_CORE_DIR\"" "declare -rx _GO_CORE_URL=\"$_GO_CORE_URL\"" "declare -rx _GO_CORE_VERSION=\"$_GO_CORE_VERSION\"" - 'declare -a _GO_IMPORTED_MODULES=([0]="complete" [1]="format")' + "declare -a _GO_IMPORTED_MODULES=(${expected_modules[*]})" "declare -- _GO_PLUGINS_DIR=\"$TEST_GO_PLUGINS_DIR\"" "declare -a _GO_PLUGINS_PATHS=(${plugins_paths[*]})" "declare -rx _GO_ROOTDIR=\"$TEST_GO_ROOTDIR\""