diff --git a/bash.sh b/bash.sh index 907b628..6c1adeb 100755 --- a/bash.sh +++ b/bash.sh @@ -12,40 +12,83 @@ # # The only proper way to do this is by looping through each item. Other # solutions claim to do this using string matching, but they are often -# dangerous because don't handle edge cases. +# dangerous because they don't handle edge cases, e.g. may be susceptible to +# partial match or may not properly handle elements containing newlines. # # # Returns 0 if item is in the array; 1 otherwise. # -# $1: The array value to search for -# $@: The array values, e.g. "${myarray[@]}" +# $1: name of the array variable to search +# $2: array item to search for # array_contains() { - local item=$1; shift - for val in "$@"; do + local arr_name=$1 + local item=$2 + + eval "local -a _tmp_arr=(\"\${${arr_name}[@]}\")" + + for val in "${_tmp_arr[@]}"; do if [ "$val" == "$item" ]; then return 0 fi done + + return 1 +} + + +# +# Returns 0 if regexp matches any item in the array; 1 otherwise. +# +# $1: name of the array variable to search +# $2: regexp to match +# +array_contains_regexp() { + local arr_name=$1 + local item=$2 + + eval "local -a _tmp_arr=(\"\${${arr_name}[@]}\")" + + for val in "${_tmp_arr[@]}"; do + if [[ "$val" =~ $item ]]; then + return 0 + fi + done + return 1 } # ### Array filter # -# Return all elements of an array with the specified item removed. +# Remove elements from the named array. Iterate over each element and call +# named function. If the function returns false, the element is removed. # -# $1: The array value to remove -# $@: The array values, e.g. "${myarray[@]}" +# $1: name of the array variable +# $@: name of the function to call plus any arguments; element to test will be +# provided as the last arg # array_filter() { - local item=$1; shift - for val in "$@"; do - if [ "$val" != "$item" ]; then - echo $val + local arr_name=$1; shift + local func_name=$1; shift + local -a func_args=("$@") + + # Escape func args + for i in "${!func_args[@]}"; do + func_args[$i]=\'"${func_args[@]}"\' + done + + eval "local -a _tmp_arr=(\"\${${arr_name}[@]}\")" + + local -a filtered_arr=() + for val in "${_tmp_arr[@]}"; do + if ! eval "$func_name" "${func_args[@]}" "$val"; then + filtered_arr+=("$val") fi done + + eval "${arr_name}=(\"\${filtered_arr[@]}\")" } # ### Array join @@ -61,6 +104,34 @@ array_join() { IFS=$sep eval 'echo "$*"' } +# +# $1: name of the array variable +# $2: index of element to remove +# +array_pop() { + local arr_name=$1 + local -i index=$2 + + eval "local -a _tmp_arr=(\"\${${arr_name}[@]}\")" + + if [ "$index" -gt $((${#_tmp_arr[@]} - 1)) ]; then + echo "array_pop: index out of range" >&2 + return 1 + fi + + local -a filtered_arr=() + local -i c=-1 + for val in "${_tmp_arr[@]}"; do + c=$((c+1)) + # Skip item at index + [ $c -eq $index ] && continue + + filtered_arr+=("$val") + done + + eval "${arr_name}=(\"\${filtered_arr[@]}\")" +} + ################ # Benchmarking # ################ @@ -275,6 +346,22 @@ parallel() { # Testing # ########### +# +# $1: string to match +# $2: value to check +# +match_string() { + [ "$2" == "$1" ] +} + +# +# $1: regexp +# $2: value to check +# +match_regexp() { + [[ "$2" =~ $1 ]] +} + # ### List all test functions # diff --git a/test-bash.sh b/test-bash.sh index 8132d26..f03be88 100755 --- a/test-bash.sh +++ b/test-bash.sh @@ -36,6 +36,85 @@ test_absolute_path_single_dot() { ) } +test_array_contains() { + local -a test_input=("foo" "bar") + array_contains test_input foo +} + +test_array_contains_multiline() { + local -a test_input=("foo" "bar"$'\n'"baz") + array_contains test_input "bar"$'\n'"baz" + ! array_contains test_input baz +} + +test_array_contains_false() { + local -a test_input=("foo" "bar") + ! array_contains test_input baz +} + +test_array_contains_does_not_do_partial_match() { + local -a test_input=("foobar" "baz") + ! array_contains test_input foo +} + +test_array_contains_handles_whitespace_and_empty() { + # We want to make sure our implementation doesn't collapse whitespaces, or + # consider them to be the same, or consider "" to be equivalent. + local -a test_input=(" " $'\n' " ") + ! array_contains test_input "" +} + +test_array_contains_regexp() { + local -a test_input=("foobar" "baz") + array_contains_regexp test_input foo +} + +test_array_filter() { + local -a arr=("foo" "bar" "baz") + + array_filter arr match_string bar + + [ ${#arr[@]} -eq 2 ] + [ "${arr[0]}" == "foo" ] + [ "${arr[1]}" == "baz" ] +} + +test_array_filter_multiline() { + local -a arr=("foo" "bar"$'\n'"baz" "quux") + [ ${#arr[@]} -eq 4 ] + + array_filter arr match_string "foo" + + [ ${#arr[@]} -eq 2 ] + [ "${arr[0]}" == "bar"$'\n'"baz" ] + [ "${arr[1]}" == "quux" ] +} + +test_array_filter_regexp() { + local -a arr=("foo" "bar" "baz") + + array_filter arr match_regexp "foo|bar" + + [ ${#arr[@]} -eq 1 ] + [ "${arr[0]}" == "baz" ] +} + +test_array_pop() { + local -a arr=("foo" "bar"$'\n'"baz" "qux") + [ ${#arr} -eq 3 ] + + array_pop arr 1 + + [ ${#arr} -eq 2 ] + [ "${arr[0]}" == "foo" ] + [ "${arr[1]}" == "qux" ] +} + +test_array_pop_out_of_range() { + local -a arr=("foo" "bar") + ! array_pop arr 2 +} + test_resolve_symlinks() { touch /tmp/foo mkdir -p /tmp/1/2 @@ -73,10 +152,16 @@ tests() { } main() { + local -a tests_to_run=("$@") + + if [ ${#tests_to_run[@]} -eq 0 ]; then + tests_to_run=($(compgen -A function | grep -E ^test_)) + fi + echo "Testing bash version: ${BASH_VERSION}" - tests "$@" + tests "${tests_to_run[@]}" } if [ "$0" == "$BASH_SOURCE" ]; then - main $(compgen -A function | grep -E ^test_) + main "$@" fi