Skip to content

Commit 750febb

Browse files
committed
lib/format: Add array_printf for Bash < 4.1
Turns out Bash versions earlier than 4.1 can't use `printf -v` to print to a subscripted array variable.
1 parent 4436b78 commit 750febb

File tree

3 files changed

+102
-27
lines changed

3 files changed

+102
-27
lines changed

lib/format

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
# Text formatting utilities
44
#
55
# Exports:
6+
# @go.array_printf
7+
# Assigns `printf` transformations of its arguments to an array
68
#
79
# @go.pad_items
810
# Pads each string in an array to match the length of the longest element
@@ -13,53 +15,82 @@
1315
# @go.strip_formatting_codes
1416
# Strips ANSI escape codes of the form `\e[...(;...)m` from a string
1517

16-
. "$_GO_USE_MODULES" 'validation'
18+
. "$_GO_USE_MODULES" 'strings' 'validation'
1719

18-
# Pads each string in an array to match the length of the longest element
20+
# Assigns `printf` transformations of its arguments to an array
21+
#
22+
# Since `printf -v` can't print to an array subscript prior to Bash 4.1, this
23+
# provides a portable means of printing to an array variable while avoiding the
24+
# use of `eval`.
25+
#
26+
# NOTE: By default, this function relies on the ASCII Record Separator character
27+
# ($'\x1f') to delimit generated strings before splitting them into the result
28+
# array. If you have strings containing this character, you can set a new
29+
# delimiter via `_GO_ARRAY_PRINTF_DELIMITER`.
30+
#
31+
# Globals:
32+
# _GO_ARRAY_PRINTF_DELIMITER:
33+
# If set, used to separate generated strings prior to array assignment
34+
#
35+
# Arguments:
36+
# result: Name of the caller-declared output array
37+
# format: `printf`-style format specification
38+
# ...: Items to pass to `printf` and store in `result`
39+
@go.array_printf() {
40+
@go.validate_identifier_or_die 'Result array name' "$1"
41+
local __go_array_printf_delim="${_GO_ARRAY_PRINTF_DELIMITER:-$'\x1f'}"
42+
local __tmp_go_array_printf
43+
printf -v __tmp_go_array_printf -- "${2}${__go_array_printf_delim}" "${@:3}"
44+
@go.split "$__go_array_printf_delim" "$__tmp_go_array_printf" "$1"
45+
}
46+
47+
# Right-pads each string in an array to match the length of the longest element
48+
#
49+
# Globals:
50+
# _GO_ARRAY_PRINTF_DELIMITER: See the comments for `@go.array_printf`
1951
#
2052
# Arguments:
2153
# items: Name of the input array in the caller's scope
2254
# result: Name of the caller-declared output array
2355
@go.pad_items() {
24-
@go.validate_identifier_or_die 'Result variable name' "$2"
56+
@go.validate_identifier_or_die 'Result array name' "$2"
2557
local items_reference="${1}[@]"
2658
local item
2759
local padding_size=0
28-
local pad_format=''
29-
local item_index=0
3060

3161
for item in "${!items_reference}"; do
3262
while [[ "$padding_size" -lt "${#item}" ]]; do
3363
padding_size="${#item}"
3464
done
3565
done
36-
pad_format="%-${padding_size}s"
37-
38-
for item in "${!items_reference}"; do
39-
printf -v "$2[$((item_index++))]" -- "$pad_format" "$item"
40-
done
66+
@go.array_printf "$2" "%-${padding_size}s" "${!items_reference}"
4167
}
4268

4369
# Concatenates parallel elements from each input array
4470
#
4571
# Will produce a number of results matching that of the left-hand input array.
4672
#
73+
# Globals:
74+
# _GO_ARRAY_PRINTF_DELIMITER: See the comments for `@go.array_printf`
75+
#
4776
# Arguments:
4877
# lhs: Name of the left-hand input array in the caller's scope
4978
# rhs: Name of the right-hand input array in the caller's scope
5079
# delim: String used as a delimiter between elements (default: two spaces)
5180
# result: Name of the caller-declared output array
5281
@go.zip_items() {
53-
@go.validate_identifier_or_die 'Result variable name' "$4"
82+
@go.validate_identifier_or_die 'Result array name' "$4"
5483
local lhs_array_reference="${1}[@]"
5584
local rhs_item_ref
5685
local item
5786
local i=0
87+
local __tmp_go_zip_items_result=()
5888

5989
for item in "${!lhs_array_reference}"; do
60-
rhs_item_ref="${2}[$i]"
61-
printf -v "$4[$((i++))]" -- '%s' "${item}${3}${!rhs_item_ref}"
90+
rhs_item_ref="${2}[$((i++))]"
91+
__tmp_go_zip_items_result+=("${item}${3}${!rhs_item_ref}")
6292
done
93+
@go.array_printf "$4" '%s' "${__tmp_go_zip_items_result[@]}"
6394
}
6495

6596
# Strips ANSI escape codes from a string

tests/format.bats

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@ create_go_format_script() {
1414
create_test_go_script '. "$_GO_USE_MODULES" format' "$@"
1515
}
1616

17+
run_array_printf_script() {
18+
create_go_format_script \
19+
'declare result=()' \
20+
'@go.array_printf result "%s" "$@"' \
21+
'IFS="|"' \
22+
'printf "%s\n" "${result[*]}"'
23+
run "$TEST_GO_SCRIPT" "$@"
24+
}
25+
1726
run_pad_items_script() {
1827
create_go_format_script \
1928
'declare items=("$@")' \
@@ -42,51 +51,78 @@ run_strip_formatting_codes_script() {
4251
run "$TEST_GO_SCRIPT" "$@"
4352
}
4453

45-
@test "$SUITE: pad items validates result variable name" {
54+
@test "$SUITE: array_printf validates result array name" {
55+
create_test_go_script '. "$_GO_USE_MODULES" format' \
56+
'@go.array_printf "3foobar"'
57+
run "$TEST_GO_SCRIPT"
58+
59+
local err_msg='Result array name "3foobar" for @go.array_printf '
60+
err_msg+='must not start with a number at:'
61+
assert_failure "$err_msg" \
62+
" $TEST_GO_SCRIPT:4 main"
63+
}
64+
65+
@test "$SUITE: array_printf does nothing for empty argv" {
66+
run_array_printf_script
67+
assert_success ''
68+
}
69+
70+
@test "$SUITE: array_printf prints argv items" {
71+
run_array_printf_script 'foo' 'bar' 'baz' 'xyzzy' 'quux'
72+
assert_success 'foo|bar|baz|xyzzy|quux'
73+
}
74+
75+
@test "$SUITE: array_printf prints argv items with different delimiter" {
76+
_GO_ARRAY_PRINTF_DELIMITER=$'\x1e' run_array_printf_script \
77+
$'foo\x1f' $'bar\x1f' $'baz\x1f' $'xyzzy\x1f' $'quux\x1f'
78+
assert_success $'foo\x1f|bar\x1f|baz\x1f|xyzzy\x1f|quux\x1f'
79+
}
80+
81+
@test "$SUITE: pad_items validates result array name" {
4682
create_test_go_script '. "$_GO_USE_MODULES" format' \
4783
'@go.pad_items items "3foobar"'
4884
run "$TEST_GO_SCRIPT"
4985

50-
local err_msg='Result variable name "3foobar" for @go.pad_items '
86+
local err_msg='Result array name "3foobar" for @go.pad_items '
5187
err_msg+='must not start with a number at:'
5288
assert_failure "$err_msg" \
5389
" $TEST_GO_SCRIPT:4 main"
5490
}
5591

56-
@test "$SUITE: pad items does nothing for empty argv" {
92+
@test "$SUITE: pad_items does nothing for empty argv" {
5793
run_pad_items_script
5894
assert_success ''
5995
}
6096

61-
@test "$SUITE: pad items pads argv items" {
97+
@test "$SUITE: pad_items pads argv items" {
6298
run_pad_items_script 'foo' 'bar' 'baz' 'xyzzy' 'quux'
6399
assert_success 'foo |bar |baz |xyzzy|quux '
64100
}
65101

66-
@test "$SUITE: zip items validates result variable name" {
102+
@test "$SUITE: zip_items validates result array name" {
67103
create_test_go_script '. "$_GO_USE_MODULES" format' \
68104
'@go.zip_items lhs rhs = "3foobar"'
69105
run "$TEST_GO_SCRIPT"
70106

71-
local err_msg='Result variable name "3foobar" for @go.zip_items '
107+
local err_msg='Result array name "3foobar" for @go.zip_items '
72108
err_msg+='must not start with a number at:'
73109
assert_failure "$err_msg" \
74110
" $TEST_GO_SCRIPT:4 main"
75111
}
76112

77-
@test "$SUITE: zip items does nothing for empty argv" {
113+
@test "$SUITE: zip_items does nothing for empty argv" {
78114
run_zip_items_script
79115
assert_success ''
80116
}
81117

82-
@test "$SUITE: zip items zips matching items with supplied delimiter" {
118+
@test "$SUITE: zip_items zips matching items with supplied delimiter" {
83119
local lhs=('foo' 'xyzzy' 'quux')
84120
local rhs=('bar' 'baz' 'plugh')
85121
run_zip_items_script "${lhs[*]}" "${rhs[*]}" '='
86122
assert_success 'foo=bar' 'xyzzy=baz' 'quux=plugh'
87123
}
88124

89-
@test "$SUITE: strip formatting codes validates result variable name" {
125+
@test "$SUITE: strip_formatting_codes validates result array name" {
90126
create_test_go_script '. "$_GO_USE_MODULES" format' \
91127
'@go.strip_formatting_codes "foobar" "3foobar"'
92128
run "$TEST_GO_SCRIPT"
@@ -97,27 +133,27 @@ run_strip_formatting_codes_script() {
97133
" $TEST_GO_SCRIPT:4 main"
98134
}
99135

100-
@test "$SUITE: strip formatting codes from empty string" {
136+
@test "$SUITE: strip_formatting_codes from empty string" {
101137
run_strip_formatting_codes_script ''
102138
assert_success ''
103139
}
104140

105-
@test "$SUITE: strip formatting codes from string with no codes" {
141+
@test "$SUITE: strip_formatting_codes from string with no codes" {
106142
run_strip_formatting_codes_script 'foobar'
107143
assert_success 'foobar'
108144
}
109145

110-
@test "$SUITE: strip formatting codes from string with one code" {
146+
@test "$SUITE: strip_formatting_codes from string with one code" {
111147
run_strip_formatting_codes_script 'foobar\e[0m'
112148
assert_success 'foobar'
113149
}
114150

115-
@test "$SUITE: strip formatting codes from string with multiple codes" {
151+
@test "$SUITE: strip_formatting_codes from string with multiple codes" {
116152
run_strip_formatting_codes_script '\e[1mf\e[30;47mo\e[0;111mo\e[32mbar\e[0m'
117153
assert_success 'foobar'
118154
}
119155

120-
@test "$SUITE: strip formatting codes from string with expanded codes" {
156+
@test "$SUITE: strip_formatting_codes from string with expanded codes" {
121157
local orig_value
122158
printf -v orig_value '%b' '\e[1mf\e[30;47mo\e[0;111mo\e[32mbar\e[0m'
123159
run_strip_formatting_codes_script "$orig_value"

tests/strings/split.bats

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ teardown() {
4242
assert_success 'foo bar baz'
4343
}
4444

45+
@test "$SUITE: multiple items using ASCII unit separator" {
46+
create_strings_test_script 'declare result=()' \
47+
"@go.split \$'\x1f' $'foo\x1fbar\x1fbaz' result" \
48+
'echo "${result[@]}"'
49+
run "$TEST_GO_SCRIPT"
50+
assert_success 'foo bar baz'
51+
}
52+
4553
@test "$SUITE: split items into same variable" {
4654
create_strings_test_script 'declare items="foo,bar,baz"' \
4755
'@go.split "," "$items" "items"' \

0 commit comments

Comments
 (0)