diff --git a/command/info_sub/variables.sh b/command/info_sub/variables.sh index dd268175..9e3fab8b 100644 --- a/command/info_sub/variables.sh +++ b/command/info_sub/variables.sh @@ -17,7 +17,6 @@ # with bashdb; see the file COPYING. If not, write to the Free Software # Foundation, 59 Temple Place, Suite 330, Boston, MA 02111 USA. - # V [![pat]] List variables and values for whose variables names which # match pat $1. If ! is used, list variables that *don't* match. # If pat ($1) is omitted, use * (everything) for the pattern. @@ -56,147 +55,130 @@ See also: ' 1 -if ! typeset -F getopts_long >/dev/null 2>&1 ; then +if ! typeset -F getopts_long >/dev/null 2>&1; then + # shellcheck source=./../../getopts_long.sh . "${_Dbg_libdir}/getopts_long.sh" fi -# Should declare typset_flags before calling. -# That it the implit returned value -function _Dbg_info_variables_parse_options { - - typeset -i _Dbg_rc=0 +function _Dbg_do_info_variables() { + declare _Dbg_typeset_flags="" + # 0: print variables with flags + # 1: filter by flags in $_Dbg_typeset_flags and don't print flags + declare -i _Dbg_typeset_filtered=1 + _Dbg_info_variables_parse_options "$@" + (($? != 0)) && return + + # Caveats: + # Bash >= 5.2: 'declare -p' properly escapes special characters within $'', e.g. $'\n\t' + # Bash < 5.2: 'declare -p' does not escape special characters within $'', but only 'declare' does + # "declare -p" outputs variables without values, but "declare" does not + # + # To work with all of Bash 5.x, we're collecting all variables and values from 'declare'. + # Because a plain "declare" also prints functions we're only iterating until the first function definition was found. + # + # Then we run declare with the filter parameters (-p, -i, etc.) to retrieve the variables to output. + + # create an associative array of all available variables with bash-escaped values + declare _Dbg_var_line + declare -A _Dbg_var_values=() + while IFS=$'\n' read -r _Dbg_var_line; do + # skip _Dbg variables + if [[ $_Dbg_var_line =~ ^_Dbg_ ]]; then continue; fi + + # break when first function was found + if [[ $_Dbg_var_line =~ ^[^=\ ]+\ \(\)$ || $_Dbg_var_line =~ ^\{ ]]; then + break + fi + + # record escaped key-value pair, allow for empty values as in "-- name=" + if [[ $_Dbg_var_line =~ ^([^=]+)=(.*)$ ]]; then + _Dbg_var_values["${BASH_REMATCH[1]}"]="${BASH_REMATCH[2]}" + fi + done < <(declare) + + declare _Dbg_var_flags _Dbg_var_name + if [[ $_Dbg_typeset_filtered -eq 0 ]]; then + # run "declare -p" and print flags, name and value + while IFS=$'\n' read -r _Dbg_var_line; do + if [[ $_Dbg_var_line =~ ^(declare|local)\ (-[^\ ]+)\ ([^=]+)= ]]; then + _Dbg_var_name="${BASH_REMATCH[3]}" + if [[ -n ${_Dbg_var_values["$_Dbg_var_name"]+x} ]]; then + _Dbg_msg_verbatim "${BASH_REMATCH[2]} $_Dbg_var_name=${_Dbg_var_values["$_Dbg_var_name"]}" + fi + elif [[ $_Dbg_var_line =~ ^(declare|local)\ (-[^\ ]+)\ ([^=]+) ]]; then + # no value, only print flags and name + _Dbg_var_name="${BASH_REMATCH[3]}" + if [[ -n ${_Dbg_var_values["$_Dbg_var_name"]+x} ]]; then + _Dbg_msg_verbatim "${BASH_REMATCH[2]} $_Dbg_var_name" + fi + fi + done < <(declare -p) + elif [[ -n "$_Dbg_typeset_flags" ]]; then + # run "declare $_Dbg_typeset_flags" and print name and value + while IFS=$'\n' read -r _Dbg_var_line; do + if [[ $_Dbg_var_line =~ ^(declare|local)\ (-[^\ ]+)\ ([^=]+)= ]]; then + _Dbg_var_name="${BASH_REMATCH[3]}" + if [[ -n ${_Dbg_var_values["$_Dbg_var_name"]+x} ]]; then + _Dbg_msg_verbatim "$_Dbg_var_name=${_Dbg_var_values["$_Dbg_var_name"]}" + fi + fi + done < <(declare $_Dbg_typeset_flags) + else + # for a plain "declare" just print available name=value pairs to avoid calling "declare", + # which would output function declarations + for _Dbg_var_name in "${!_Dbg_var_values[@]}"; do + if [[ -n ${_Dbg_var_values["$_Dbg_var_name"]+x} ]]; then + _Dbg_msg_verbatim "$_Dbg_var_name=${_Dbg_var_values["$_Dbg_var_name"]}" + fi + done + fi +} +# Parse flags passed to the "info variables" command. +# The caller should declare _Dbg_typeset_flags and _Dbg_typeset_filtered before calling, +# which are implicitly returned values. +function _Dbg_info_variables_parse_options { _Dbg_typeset_flags="" _Dbg_typeset_filtered=1 - OPTLIND='' - while getopts_long irxaAtp opt \ - integer no_argument \ - readonly no_argument \ - exports no_argument \ - indexed no_argument \ - associative no_argument \ - trace no_argument \ - properties no_argument \ - '' $@ - do - case "$opt" in - i | integer ) - _Dbg_typeset_flags="-i $_Dbg_typeset_flags";; - r | readonly ) - _Dbg_typeset_flags="-r _$_Dbg_typeset_flags";; - x | exports ) - _Dbg_typeset_flags="-x $_Dbg_typeset_flags";; - a | indexed ) - _Dbg_typeset_flags="-a $_Dbg_typeset_flags";; - A | associative ) - _Dbg_typeset_flags="-A $_Dbg_typeset_flags";; - t | trace ) - _Dbg_typeset_flags="-t $_Dbg_typeset_flags";; - p | properties ) - _Dbg_typeset_filtered=0;; - * ) - _Dbg_errmsg "Invalid argument in $@; use only -x, -i, -r, -a, -A, -t, or -p" - _Dbg_rc=1 - ;; - esac + typeset -i _Dbg_rc=0 + typeset OPTLIND='' + while getopts_long irxaAtp opt \ + integer no_argument \ + readonly no_argument \ + exports no_argument \ + indexed no_argument \ + associative no_argument \ + trace no_argument \ + properties no_argument \ + '' "$@"; do + case "$opt" in + i | integer) + _Dbg_typeset_flags="-i $_Dbg_typeset_flags" + ;; + r | readonly) + _Dbg_typeset_flags="-r $_Dbg_typeset_flags" + ;; + x | exports) + _Dbg_typeset_flags="-x $_Dbg_typeset_flags" + ;; + a | indexed) + _Dbg_typeset_flags="-a $_Dbg_typeset_flags" + ;; + A | associative) + _Dbg_typeset_flags="-A $_Dbg_typeset_flags" + ;; + t | trace) + _Dbg_typeset_flags="-t $_Dbg_typeset_flags" + ;; + p | properties) + _Dbg_typeset_filtered=0 + ;; + *) + _Dbg_errmsg "Invalid argument in $*; use only -x, -i, -r, -a, -A, -t, or -p" + _Dbg_rc=1 + ;; + esac done return $_Dbg_rc } - -function _Dbg_do_info_variables { - _Dbg_typeset_flags="" - local -i _Dbg_typeset_filtered=1 - _Dbg_info_variables_parse_options "$@" - (( $? != 0 )) && return - - local _Dbg_old_glob="$GLOBIGNORE" - GLOBIGNORE="*" - - _Dbg_match='*' - local _Dbg_list=$(declare -p $_Dbg_typeset_flags) - local _Dbg_old_ifs=${IFS} - IFS=" -" - local _Dbg_temp=${_Dbg_list} - _Dbg_list="" - local -i _Dbg_i=0 - local -a _Dbg_list - - # GLOBIGNORE protects us against using the result of - # a glob expansion, but it doesn't protect us from - # actually performing it, and this can bring bash down - # with a huge _Dbg_source_ variable being globbed. - # So here we disable globbing momentarily - set -o noglob - for _Dbg_item in ${_Dbg_temp}; do - _Dbg_list[${_Dbg_i}]="${_Dbg_item}" - _Dbg_i=${_Dbg_i}+1 - done - set +o noglob - IFS=${_Dbg_old_ifs} - local _Dbg_item="" - local _Dbg_skip=0 - local _Dbg_show_cmd="" - _Dbg_show_cmd=`echo -e "case \\${_Dbg_item} in \n${_Dbg_match})\n echo yes;;\n*)\necho no;; esac"` - - for (( _Dbg_i=0; (( _Dbg_i < ${#_Dbg_list[@]} )) ; _Dbg_i++ )) ; do - _Dbg_item=${_Dbg_list[$_Dbg_i]} - - - # Ignore all _Dbg_ variables here because the following - # substitutions takes a long while when it encounters - # a big _Dbg_source_ - if [[ ${_Dbg_item} =~ "_Dbg_" ]] ; then - continue; - fi - - - case ${_Dbg_item} in - *\ \(\)\ ) - _Dbg_skip=1 - ;; - \}) - _Dbg_skip=0 - continue - esac - if [[ _Dbg_skip -eq 1 ]]; then - continue - fi - _Dbg_item=${_Dbg_item/=/==/} - _Dbg_item=${_Dbg_item%%=[^=]*} - case ${_Dbg_item} in - _=);; - *=) - _Dbg_item=${_Dbg_item%=} - local _Dbg_show=`eval $_Dbg_show_cmd` - if [[ "$_Dbg_show" != "$_Dbg_match_inverted" ]]; then - if [[ -n ${_Dbg_item} ]]; then - local _Dbg_var=`declare -p ${_Dbg_typeset_flags} ${_Dbg_item} 2>/dev/null` - if [[ -n "$_Dbg_var" ]]; then - # Uncomment the following 3 lines to use literal - # linefeeds - # _Dbg_var=${_Dbg_var//\\\\n/\\n} - # _Dbg_var=${_Dbg_var// - #/\n} - # Comment the following 3 lines to use literal linefeeds - _Dbg_var=${_Dbg_var//\\\\n/\\\\\\n} - _Dbg_var=${_Dbg_var// - /\\n} - if (( _Dbg_typeset_filtered == 1 )); then - _Dbg_var=${_Dbg_var#* * } - else - _Dbg_var=${_Dbg_var#* } - fi - _Dbg_msg ${_Dbg_var} - fi - fi - fi - ;; - *) - ;; - esac - - done - GLOBIGNORE=$_Dbg_old_glob -} diff --git a/lib/msg.sh b/lib/msg.sh index 5c7fb4ad..3c1fb2cd 100644 --- a/lib/msg.sh +++ b/lib/msg.sh @@ -131,6 +131,21 @@ function _Dbg_printf_nocr { fi } +# Like _Dbg_msg but does not evaluate escape sequences which are embedded in the arguments +# print message to output device +function _Dbg_msg_verbatim { + if (( _Dbg_logging )) ; then + builtin echo -E "$@" >>$_Dbg_logging_file + fi + if (( ! _Dbg_logging_redirect )) ; then + if [[ -n $_Dbg_tty ]] && [[ $_Dbg_tty != '&1' ]] ; then + builtin echo -E "$@" >>$_Dbg_tty + else + builtin echo -E "$@" + fi + fi +} + typeset _Dbg_dashes='---------------------------------------------------' # print message to output device diff --git a/test/unit/test-cmd-info-variables.sh.in b/test/unit/test-cmd-info-variables.sh.in index 8603e0fa..8dd0934b 100755 --- a/test/unit/test-cmd-info-variables.sh.in +++ b/test/unit/test-cmd-info-variables.sh.in @@ -1,17 +1,18 @@ #!@SH_PROG@ # -*- shell-script -*- -test_cmd_info_variables() +_test_cmd_info_variables() { typeset -i test_cmd_integer=123 typeset -x test_cmd_export=exported typeset -i too_permissive=0 typeset -i found=0 + for line in $(_Dbg_do_info_variables -i); do - if [[ "$line" == 'test_cmd_integer="123"' ]]; then - found=1 - elif [[ "$line" == 'test_cmd_export="exported"' ]]; then - too_permissive=1 - fi + if [[ "$line" == 'test_cmd_integer="123"' ]]; then + found=1 + elif [[ "$line" == 'test_cmd_export="exported"' ]]; then + too_permissive=1 + fi done assertEquals "Filtering with -i must show integer variables" "1" "$found" @@ -20,11 +21,11 @@ test_cmd_info_variables() found=0 too_permissive=0 for line in $(_Dbg_do_info_variables -x); do - if [[ "$line" == 'test_cmd_integer="123"' ]]; then - too_permissive=1 - elif [[ "$line" == 'test_cmd_export="exported"' ]]; then - found=1 - fi + if [[ "$line" == 'test_cmd_integer="123"' ]]; then + too_permissive=1 + elif [[ "$line" == 'test_cmd_export="exported"' ]]; then + found=1 + fi done assertEquals "Filtering with -x must show exported variables" "1" "$found" @@ -32,18 +33,124 @@ test_cmd_info_variables() # FIXME try -x -i, and no options. try invalid opts } +test_cmd_info_variables_filtered() { + # setup variables to test + typeset -i _test_integer=42 + + typeset _test_scalar="value" + typeset _test_scalar_spaces="value value" + typeset _test_scalar_linefeed=$'value\nvalue' + typeset _test_scalar_empty="" + + typeset -a _test_array_empty=() + typeset -a _test_array_one=(first) + typeset -a _test_array_two=(first second) + typeset -a _test_array_two_linefeed=($'first with spaces\nand linefeed' $'second with spaces\nand linefeed') + typeset -a -g _test_array_global=(first) + + typeset -A _test_assoc_array_empty=() + typeset -A _test_assoc_array_one=(["key_first"]=first) + typeset -A _test_assoc_array_two=(["key first"]=first ["key second"]=second) + typeset -A _test_assoc_array_two_linefeed=( + [$'key first\nwith\linefeed']=$'first with spaces\nand linefeed' + [$'key second\nwith\nlinefeed']=$'second with spaces\nand linefeed' + ) + + typeset expected_all + + expected_all="$( + cat - <<'EOF' +-- _test_scalar=value +-- _test_scalar_linefeed=$'value\nvalue' +-- _test_scalar_spaces='value value' +-- _test_scalar_empty= +-A _test_assoc_array_empty=() +-A _test_assoc_array_one=([key_first]="first" ) +-A _test_assoc_array_two=(["key first"]="first" ["key second"]="second" ) +-A _test_assoc_array_two_linefeed=([$'key second\nwith\nlinefeed']=$'second with spaces\nand linefeed' [$'key first\nwith\\linefeed']=$'first with spaces\nand linefeed' ) +-a _test_array_empty=() +-a _test_array_global=([0]="first") +-a _test_array_one=([0]="first") +-a _test_array_two=([0]="first" [1]="second") +-a _test_array_two_linefeed=([0]=$'first with spaces\nand linefeed' [1]=$'second with spaces\nand linefeed') +-i _test_integer=42 +EOF + )" + + assertEquals "Output of 'info variables -p' must match" \ + "$(echo "$expected_all" | sort)" \ + "$(_Dbg_do_info_variables -p | grep -E '^-[^ ]+ _test' | sort)" + + assertEquals "Output of 'info variables -p' must not contain _Dbg_ debugger variables" \ + "" \ + "$(_Dbg_do_info_variables -p | grep -E '^-[^ ]+ _Dbg_' | sort)" +} + +test_cmd_info_variables_flags() { + # setup variables to test + typeset -i _test_integer=42 + + typeset _test_scalar="value" + typeset _test_scalar_spaces="value value" + typeset _test_scalar_linefeed=$'value\nvalue' + + typeset -a _test_array_empty=() + typeset -a _test_array_one=(first) + typeset -a _test_array_two=(first second) + typeset -a _test_array_two_linefeed=($'first with spaces\nand linefeed' $'second with spaces\nand linefeed') + typeset -a -g _test_array_global=(first) + + typeset -A _test_assoc_array_empty=() + typeset -A _test_assoc_array_one=(["key_first"]=first) + typeset -A _test_assoc_array_two=(["key first"]=first ["key second"]=second) + typeset -A _test_assoc_array_two_linefeed=( + [$'key first\nwith\linefeed']=$'first with spaces\nand linefeed' + [$'key second\nwith\nlinefeed']=$'second with spaces\nand linefeed' + ) + + typeset expected_array expected_integer + + expected_array="$( + cat - <<'EOF' +_test_array_empty=() +_test_array_global=([0]="first") +_test_array_one=([0]="first") +_test_array_two=([0]="first" [1]="second") +_test_array_two_linefeed=([0]=$'first with spaces\nand linefeed' [1]=$'second with spaces\nand linefeed') +EOF + )" + + expected_integer="$( + cat - <<'EOF' +_test_integer=42 +EOF + )" + + assertEquals "Output of 'info variables -a' must match" \ + "$(echo "$expected_array" | sort)" \ + "$(_Dbg_do_info_variables -a | grep -E '^_test' | sort)" + + assertEquals "Output of 'info variables -i' must match" \ + "$(echo "$expected_integer" | sort)" \ + "$(_Dbg_do_info_variables -i | grep -E '^_test' | sort)" + + assertEquals "Output of 'info variables' must not contain _Dbg_ debugger variables" \ + "" \ + "$(_Dbg_do_info_variables | grep -E '^_Dbg_' | sort)" +} + abs_top_srcdir=@abs_top_srcdir@ if [ ! -d $abs_top_scrdir ] ; then - >&2 echo "Something is wrong in configuation: abs_top_srcdir is not set properly." + >&2 echo "Something is wrong in configuration: abs_top_srcdir is not set properly." exit 1 fi # Make sure $abs_top_src has a trailing slash abs_top_srcdir=${abs_top_srcdir%%/}/ -. ${abs_top_srcdir}test/unit/helper.sh -. $abs_top_srcdir/lib/help.sh -. $abs_top_srcdir/lib/msg.sh -. $abs_top_srcdir/command/info_sub/variables.sh +. "${abs_top_srcdir}test/unit/helper.sh" +. "${abs_top_srcdir}/lib/help.sh" +. "${abs_top_srcdir}/lib/msg.sh" +. "${abs_top_srcdir}/command/info_sub/variables.sh" set -- # reset $# so shunit2 doesn't get confused.