diff --git a/NEWS.adoc b/NEWS.adoc index e54e3f1c81..55702121e0 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -620,6 +620,14 @@ https://github.com/networkupstools/nut/milestone/13 aspect of that, it should now abort with an explanation (and not proceed with insecure start-up like it could do before). [issue #3331, PR #3435] + - The `nut-driver-enumerator.sh` script (NDE) updates: + * The shell/awk based parser now decodes `ups.conf` values using NUT token + syntax, preserving quoted hashes and escaped characters and ignoring + trailing comments. For example, `port = "auto" # comment` now properly + selects USB media for `nutdrv_qx`. The normalized configuration output + retains quotes and backslashes; changed section checksums can cause a + one-time reconciliation after upgrading. [PR #3635] + - Recipes, CI and helper script updates not classified above: * Introduced `AGENTS.md` for AI agents to help in a consistent manner with minimal friction. [#3608] diff --git a/UPGRADING.adoc b/UPGRADING.adoc index 0485efadcb..c72e36f161 100644 --- a/UPGRADING.adoc +++ b/UPGRADING.adoc @@ -74,6 +74,14 @@ The new `-A filename` option defaults to trying to use a `nutauth.conf` file reading one ('none' as the legacy default). See the updated manual pages for more details. [issues #3329, #3411] +- `nut-driver-enumerator` now preserves quotes and backslashes in normalized + configuration output and decodes values once using NUT token syntax. + Value queries omit trailing comments, take the first value token and + treat single quotes literally. This corrected parsing can change media + dependencies. Section checksums may change on upgrade and trigger the + usual reconciliation, including driver restarts; subsequent checks of + unchanged configuration remain stable. [PR #3635] + - The `powervar_cx_usb`, `tripplite_usb` drivers used a built-in limit on reconnection attempts after which they exited ('60' and '10' respectively). This was revised to follow the new common setting `reconnect_max_tries`, diff --git a/docs/man/nut-driver-enumerator.txt b/docs/man/nut-driver-enumerator.txt index 5e02cbb55c..f07c61e2f7 100644 --- a/docs/man/nut-driver-enumerator.txt +++ b/docs/man/nut-driver-enumerator.txt @@ -108,13 +108,22 @@ NUT device names which each such unit wraps *nut-driver-enumerator.sh --show-all-configs*:: Show the complete normalized list of device configuration blocks -(same as used later by the parser in the script to make decisions) +(same as used later by the parser in the script to make decisions). +Quotes and backslashes are retained until values are queried. +Physical lines joined by a backslash continuation or inside double +quotes are combined. Section checksums use this normalized text, +including a final newline. *nut-driver-enumerator.sh --show-device-config DEV*:: Show configuration block of the specified NUT device *nut-driver-enumerator.sh --show-device-config-value DEV KEY*:: -Show single configuration key of the specified NUT device +Show the decoded value of a configuration key of the specified NUT device. +The first value token is used: double quotes group a token, backslash +escapes the next character, and an unquoted hash starts a comment. +Single quotes are literal characters. A flag without a value prints its +name. Multiple keys may be requested; missing keys print an empty line +and cause a nonzero exit status. ENVIRONMENT VARIABLES --------------------- diff --git a/scripts/upsdrvsvcctl/nut-driver-enumerator.sh.in b/scripts/upsdrvsvcctl/nut-driver-enumerator.sh.in index 12d9bcbd97..76fa27cdf0 100644 --- a/scripts/upsdrvsvcctl/nut-driver-enumerator.sh.in +++ b/scripts/upsdrvsvcctl/nut-driver-enumerator.sh.in @@ -13,7 +13,7 @@ # is to just pick out some strings relevant for tracking config changes. # # Copyright (C) 2016-2020 Eaton -# Copyright (C) 2020-2025 Jim Klimov +# Copyright (C) 2020-2026 Jim Klimov # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -502,7 +502,7 @@ upsconf_getSection_content() { SECTION_CONTENT="" RES=1 [ -n "$1" ] || RES=0 - while read LINE ; do + while IFS= read -r LINE ; do case "$LINE" in \["$1"\]) if [ "$RES" = 0 ]; then @@ -539,7 +539,7 @@ $LINE" done if [ "${#SECTION_CONTENT}" != 0 ]; then - echo "$SECTION_CONTENT" + printf '%s\n' "$SECTION_CONTENT" fi [ "$RES" = 0 ] || log_error "Section [$1] was not found in the '$UPSCONF' file" @@ -570,6 +570,56 @@ ${UPSCONF_DATA_SDP} EOF } +upsconf_getValue_awk() { + # Helper for upsconf_getValue() below + + # Like common/upsconf.c, take the first value token after the + # assignment. Quotes only open at token start; single quotes are + # ordinary characters. Decode backslash escapes exactly once. + + NDE_KEY="$1" LC_ALL=C awk ' +BEGIN { key = ENVIRON["NDE_KEY"] } +{ + if (index($0, key "=") == 1) { + line = substr($0, length(key) + 2) + } else if ($0 == key || (index($0, key) == 1 && + substr($0, length(key) + 1) ~ /^[ \t]*#/)) { + print key + found = 1 + next + } else next + + value = "" + started = quoted = escaped = 0 + for (i = 1; i <= length(line); i++) { + c = substr(line, i, 1) + if (escaped) { + if (c ~ /^[ -\177]$/) value = value c + escaped = 0 + } else if (c == "\\") { + escaped = started = 1 + } else if (quoted) { + if (c == "\"") break + if (c ~ /^[ -\177]$/) value = value c + } else if (c == "#") break + else if (c ~ /^[ \t\r\n\013\014]$/) { + if (started) break + } else if (c == "=" && started) break + else if (c == "\"" && !started) { + quoted = started = 1 + } else { + if (c ~ /^[ -\177]$/) value = value c + started = 1 + if (c == "=") break + } + } + print value + found = 1 +} +END { if (!found) exit 1 } +' +} + upsconf_getValue() { # "$1" = name of ups.conf section, may be empty for global config # "$2..$N" = name of config key; we will echo its value @@ -579,8 +629,7 @@ upsconf_getValue() { CURR_SECTION="" # Gets set by a GETSECTION implementation RES=0 - # Note: Primary aim of this egrep is to pick either assignments or flags - # As a by-product it can be used to test if a particular value is set ;) + # Keep the normalized syntax intact until this final value lookup. SECTION_CONTENT="`$GETSECTION \"$1\"`" || return shift KEYS="$*" @@ -589,13 +638,11 @@ upsconf_getValue() { RES_L=0 VALUE="" - LINE="`echo \"$SECTION_CONTENT\" | $EGREP '(^'\"$1\"'=|^'\"$1\"'$)'`" \ - && VALUE="$(echo "$LINE" | $SED -e "s,^$1=,," -e 's,^\"\(.*\)\"$,\1,' -e "s,^'\(.*\)'\$,\1,")" \ - || RES_L=$? + VALUE="`printf '%s\n' \"${SECTION_CONTENT}\" | upsconf_getValue_awk \"$1\"`" || RES_L=$? [ "$RES_L" = 0 ] || { RES="$RES_L" ; log_error "Section [$CURR_SECTION] or key '$1' in it was not found in the '$UPSCONF' file" ; } - echo "$VALUE" + printf '%s\n' "$VALUE" shift done @@ -692,9 +739,9 @@ upsconf_getDriverMedia() { CURR_PORT="`upsconf_getPort \"$1\"`" || CURR_PORT="" case "$CURR_PORT" in *@localhost|*@|*@127.0.0.1|*@::1) - printf '%s\n%s\n' "$CURR_DRV" "network-localhost,drivers=`echo "${CURR_PORT}" | $SED 's,@.*$,,'`" ; return ;; + printf '%s\n%s\n' "$CURR_DRV" "network-localhost,drivers=`printf '%s\n' \"${CURR_PORT}\" | $SED 's,@.*$,,'`" ; return ;; *@"`hostname`"*) # Technically also local host, but via public host name (so networking must be up) - printf '%s\n%s\n' "$CURR_DRV" "network,drivers=`echo "${CURR_PORT}" | $SED 's,@.*$,,'`" ; return ;; + printf '%s\n%s\n' "$CURR_DRV" "network,drivers=`printf '%s\n' \"${CURR_PORT}\" | $SED 's,@.*$,,'`" ; return ;; *@*) # Might be local host via public IP address, but harder to detect portably printf '%s\n%s\n' "$CURR_DRV" "network" ; return ;; *) # Assume DEV/SEQ file: @@ -710,7 +757,7 @@ upsconf_getDriverMedia() { upsconf_getMedia() { _DRVMED="`upsconf_getDriverMedia \"$1\"`" || return - echo "${_DRVMED}" | @TAIL@ @TAIL_ARGS_FROM_NTH_LINE@ +2 + printf '%s\n' "${_DRVMED}" | @TAIL@ @TAIL_ARGS_FROM_NTH_LINE@ +2 return 0 } @@ -738,13 +785,13 @@ upsconf_debug() { NAME_MD5="`calc_md5 \"$1\"`" # Not public, not in usage() # Used by tests, echo as formatted here to stdout - echo "INST: ${NAME_MD5}~[$1]: DRV='${_DRV}' PORT='${_PRT}' MEDIA='${_MED}' SECTIONMD5='${_MD5}'" + printf '%s\n' "INST: ${NAME_MD5}~[$1]: DRV='${_DRV}' PORT='${_PRT}' MEDIA='${_MED}' SECTIONMD5='${_MD5}'" } calc_md5() { # Tries several ways to produce an MD5 of the "$1" argument - _MD5="`echo \"$1\" | md5sum 2>/dev/null | awk '{print $1}'`" && [ -n "${_MD5}" ] || \ - { _MD5="`echo \"$1\" | openssl dgst -md5 2>/dev/null | awk '{print $NF}'`" && [ -n "${_MD5}" ]; } || \ + _MD5="`printf '%s\n' \"$1\" | md5sum 2>/dev/null | awk '{print $1}'`" && [ -n "${_MD5}" ] || \ + { _MD5="`printf '%s\n' \"$1\" | openssl dgst -md5 2>/dev/null | awk '{print $NF}'`" && [ -n "${_MD5}" ]; } || \ return 1 echo "${_MD5}" @@ -768,7 +815,7 @@ smf_validFullUnitName() { esac } smf_validInstanceName() { - echo "MD5_`calc_md5 "$1"`" + echo "MD5_`calc_md5 \"$1\"`" } smf_validInstanceSuffixName() { case "$1" in @@ -826,7 +873,7 @@ smf_registerInstance() { for OTHERDEV in $UPSLIST_FILE ; do case "${DEPDRV}" in *-"${OTHERDEV}") - OTHERDRV="'`echo "${OTHERLIST}" | awk '($1 == "'"${OTHERDEV}"'") { print $2 }'`'" || OTHERDRV="" + OTHERDRV="'`echo \"${OTHERLIST}\" | awk '($1 == \"'\"${OTHERDEV}\"'\") { print $2 }'`'" || OTHERDRV="" log_warn "Device ${DEVICE} depends on another as '${DEPDRV}', but the other possible device '${OTHERDEV}' uses an unexpected driver: ${OTHERDRV}" ;; esac @@ -1047,7 +1094,7 @@ systemd_validFullUnitName() { esac } systemd_validInstanceName() { - echo "MD5_`calc_md5 "$1"`" + echo "MD5_`calc_md5 \"$1\"`" } systemd_validInstanceSuffixName() { echo "$1" | $SED -e 's,^.*@,,' -e 's,\.service$,,' @@ -1112,7 +1159,7 @@ systemd_registerInstance() { for OTHERDEV in $UPSLIST_FILE ; do case "${DEPDRV}" in *-"${OTHERDEV}") - OTHERDRV="'`echo "${OTHERLIST}" | awk '($1 == "'"${OTHERDEV}"'") { print $2 }'`'" || OTHERDRV="" + OTHERDRV="'`echo \"${OTHERLIST}\" | awk '($1 == \"'\"${OTHERDEV}\"'\") { print $2 }'`'" || OTHERDRV="" log_warn "Device ${DEVICE} depends on another as '${DEPDRV}', but the other possible device '${OTHERDEV}' uses an unexpected driver: ${OTHERDRV}" ;; esac @@ -1291,25 +1338,65 @@ upslist_normalizeFile_filter() { # See upslist_normalizeFile() detailed comments below; this routine # is a pipe worker to prepare the text into a simpler expected form. - # Pick the lines which contain a bracket or assignment character, - # or a single token (certain keywords come as just NUT "flags"), - # trim leading and trailing whitespace, comment-only lines, and in - # assignment lines trim the spaces around equality character and - # quoting characters around assignment of values without whitespaces. - # Any whitespace characters around a section name (single token that - # starts the line and is enclosed in brackets) and a trailing comment - # are dropped. Note that brackets with spaces inside, and brackets - # that do not start the non-whitespace payload of the line, are not - # sections. - $EGREP -v '(^$|^#)' | \ - $SED -e 's,^['"$TABCHAR"'\ ]*,,' \ - -e 's,^\#.*$,,' \ - -e 's,['"$TABCHAR"'\ ]*$,,' \ - -e 's,^\([^=\ '"$TABCHAR"']*\)['"$TABCHAR"'\ ]*=['"$TABCHAR"'\ ]*,\1=,g' \ - -e 's,=\"\([^\ '"$TABCHAR"']*\)\"$,=\1,' \ - -e 's,^\(\[[^]'"$TABCHAR"'\ ]*\]\)['"$TABCHAR"'\ ]*\(#.*\)*$,\1,' \ - | $EGREP -v '^$' \ - | $EGREP '([\[\=]|^[^ '"$TABCHAR"']*$|^[^ '"$TABCHAR"']*[ '"$TABCHAR"']*#.*$)' + # Preserve quotes and escapes for upsconf_getValue(). Join physical + # continuations, and trim only whitespace outside tokens, so escaped + # trailing spaces survive. Keep the existing normalized text format + # (not a serialization of decoded values) for section checksums. + awk ' + BEGIN { start = 1 } + { + offset = length(line) + line = line $0 + for (i = 1; i <= length($0); i++) { + c = substr($0, i, 1) + if (comment) { + if (c !~ /^[ \t]$/) last = offset + i + } else if (escaped) { + last = offset + i + escaped = 0 + } else if (c == "\\") { + last = offset + i + escaped = 1 + start = 0 + } else if (quoted) { + last = offset + i + if (c == "\"") { quoted = 0; start = 1 } + } else if (c == "#") { + last = offset + i + comment = 1 + } else if (c ~ /^[ \t]$/) start = 1 + else { + last = offset + i + if (c == "\"" && start) quoted = 1 + start = (c == "=") + } + } + if (escaped) { + line = substr(line, 1, length(line) - 1) + last = length(line) + escaped = 0 + next + } + # Like parseconf, ignore physical newlines inside quotes. + if (quoted) next + line = substr(line, 1, last) + sub(/^[ \t]*/, "", line) + # Normalize only the assignment prefix, never its value. + if (match(line, /^[^= \t]+[ \t]*=[ \t]*/)) { + prefix = substr(line, 1, RLENGTH) + gsub(/[ \t]/, "", prefix) + line = prefix substr(line, RLENGTH + 1) + } + if (line ~ /^\[[^] \t]*\][ \t]*(#.*)?$/) + sub(/\][ \t]*(#.*)?$/, "]", line) + if (line !~ /^#/ && (index(line, "[") || index(line, "=") || + line ~ /^[^ \t]+([ \t]*#.*)?$/)) print line + line = "" + last = quoted = comment = 0 + start = 1 + } + END { if (line != "") print line } + ' } upslist_normalizeFile() { @@ -1335,7 +1422,7 @@ upslist_normalizeFile() { # Store a normalized version of NUT configuration file contents. # Also use a SDP subset with just section, driver and port info # for faster parsing when determining driver-required media etc. - UPSCONF_DATA="$(upslist_normalizeFile_filter < "$UPSCONF")" \ + UPSCONF_DATA="`upslist_normalizeFile_filter < \"${UPSCONF}\"`" \ && [ "${#UPSCONF_DATA}" != 0 ] \ && UPSCONF_DATA_SDP="`$EGREP '^(\[.*\]|driver=|port=)' << EOF $UPSCONF_DATA @@ -1368,7 +1455,7 @@ upslist_readFile() { if [ "${#UPSCONF_DATA}" != 0 ] ; then # Note that section-name brackets should contain a single token - UPSLIST_FILE="$(echo "$UPSCONF_DATA_SDP" | $EGREP '^\[[^'"$TABCHAR"'\ ]*\]$' | $SED 's,^\[\(.*\)\]$,\1,' | sort -k1n -k1)" \ + UPSLIST_FILE="`printf '%s\n' \"$UPSCONF_DATA_SDP\" | $EGREP '^\[[^'\"$TABCHAR\"'\ ]*\]$' | $SED 's,^\[\(.*\)\]$,\1,' | sort -k1n -k1`" \ || UPSLIST_FILE="" if [ "${#UPSLIST_FILE}" = 0 ] ; then log_error "Could not read the '$UPSCONF' file or it does not declare any device configurations: no section declarations in parsed normalized contents" @@ -1551,7 +1638,7 @@ nut_driver_enumerator_main() { if [ "$RESTART_ALL" = yes ] ; then # Save new checksum of global config - $hook_setSavedMD5 "" "`upsconf_getSection_MD5 ""`" + $hook_setSavedMD5 "" "`upsconf_getSection_MD5 ''`" fi if [ "${#UPSLIST_FILE}" != 0 ]; then @@ -1628,7 +1715,7 @@ nut_driver_enumerator_full_reconfigure() { fi # Save new checksum of global config - $hook_setSavedMD5 "" "`upsconf_getSection_MD5 ""`" + $hook_setSavedMD5 "" "`upsconf_getSection_MD5 ''`" # Service units were manipulated, including saving of checksums; # refresh the service management daemon if needed @@ -2056,7 +2143,7 @@ while [ $# -gt 0 ]; do [ "${#UPSLIST_FILE}" != 0 ] \ || { log_warn "No devices detected in '$UPSCONF'" ; RES=1 ; } # echo this to stdout: - echo "$UPSCONF_DATA" + printf '%s\n' "$UPSCONF_DATA" exit $RES ;; --show-config|--show-device-config) diff --git a/tests/nut-driver-enumerator-test--ups.conf b/tests/nut-driver-enumerator-test--ups.conf index e3d9f3f144..f7f9354d5c 100644 --- a/tests/nut-driver-enumerator-test--ups.conf +++ b/tests/nut-driver-enumerator-test--ups.conf @@ -9,6 +9,32 @@ globalflag driver = dummy-ups port = file1.dev desc = "This is ups-1" + empty = "" + spaces = " " + quote = "\"edge\"" + backslashes = "C:\\new\\test.dev" + adjacent = "a\\\"b" + doubleescape = "\\n\\t\\r\\#" + apostrophe = "it's literal" + equals = one\=two=ignored + equalfirst = =ignored + quoteinword = one"two + closecommits = "first"second + escapedhash = file\#1.dev # ignored + quotedhash = "file#1.dev" # ignored + quotedescapedhash = "file\#1.dev" + escapedspace = two\ words ignored + trailing = word\ + continued = "two\ + words" + continuedbare = two\ +words + multiline = "a +b" + comment = value # not a continuation \ + aftercomment = intact + repeat = first + repeat = "second value" [epdu-2] # This is an ePDU driver=netxml-ups @@ -59,8 +85,10 @@ driver = 'dummy-ups ' port = /dev/ttyb [qx-usb1] - driver=nutdrv_qx - port = auto + driver=nutdrv\ +_qx + port = "au +to" # USB auto-detection [qx-usb2] driver=nutdrv_qx port = /dev/usb/8 @@ -73,3 +101,7 @@ driver = 'dummy-ups ' driver=nutdrv_qx # comment port = /dev/usb/8 # comment commentedDriverFlag # This flag gotta mean something + +[escapedPath] + driver = dummy-ups + port = "C:\\new\\test.dev" # path diff --git a/tests/nut-driver-enumerator-test.sh b/tests/nut-driver-enumerator-test.sh index 3b284793d1..dbe9a0e83f 100755 --- a/tests/nut-driver-enumerator-test.sh +++ b/tests/nut-driver-enumerator-test.sh @@ -145,8 +145,8 @@ run_testcase_generic() { printf '\t--- expected ---\n%s\n\t--- received ---\n%s\n\t--- MISMATCH ABOVE\n\n' "$EXPECT_TEXT" "$OUT" >&2 # Give a nice output to help track the problem: ( rm -f "/tmp/.nde.text.expected.$$" "/tmp/.nde.text.actual.$$" \ - && echo "$EXPECT_TEXT" > "/tmp/.nde.text.expected.$$" \ - && echo "$OUT" > "/tmp/.nde.text.actual.$$" \ + && printf '%s\n' "$EXPECT_TEXT" > "/tmp/.nde.text.expected.$$" \ + && printf '%s\n' "$OUT" > "/tmp/.nde.text.actual.$$" \ && { OUTD="`diff -u \"/tmp/.nde.text.expected.$$\" \"/tmp/.nde.text.actual.$$\" 2>/dev/null`" if echo "$OUTD" | head -1 | ${EGREP} '^[-+]' >/dev/null ; then echo "$OUTD" @@ -195,6 +195,7 @@ dummy-proxy-localhost dummy1 epdu-2 epdu-2-snmp +escapedPath qx-serial qx-usb1 qx-usb2 @@ -210,7 +211,8 @@ valueHasQuotedHashtag" \ } testcase_show_all_configs() { - # We expect whitespace trimmed, comment-only lines removed + # Trim formatting whitespace and comments on their own lines; retain + # quotes, escapes and escaped trailing spaces for later value decoding. run_testcase "Show all configs" 0 \ 'maxstartdelay=180 globalflag @@ -218,17 +220,40 @@ globalflag driver=dummy-ups port=file1.dev desc="This is ups-1" +empty="" +spaces=" " +quote="\"edge\"" +backslashes="C:\\new\\test.dev" +adjacent="a\\\"b" +doubleescape="\\n\\t\\r\\#" +apostrophe="it'"'"'s literal" +equals=one\=two=ignored +equalfirst==ignored +quoteinword=one"two +closecommits="first"second +escapedhash=file\#1.dev # ignored +quotedhash="file#1.dev" # ignored +quotedescapedhash="file\#1.dev" +escapedspace=two\ words ignored +trailing=word\'" "' +continued="two words" +continuedbare=twowords +multiline="ab" +comment=value # not a continuation \ +aftercomment=intact +repeat=first +repeat="second value" [epdu-2] driver=netxml-ups -port=http://172.16.1.2 +port="http://172.16.1.2" synchronous=yes [epdu-2-snmp] driver=snmp-ups port=172.16.1.2 synchronous=no [usb_3] -driver=usbhid-ups -port=auto +driver="usbhid-ups" +port="auto" [serial.4] driver=serial-ups driverflag @@ -241,7 +266,7 @@ driver="dummy-ups " port=remoteUPS@RemoteHost.local [dummy-proxy-localhost] driver='"'dummy-ups '"' -port=localUPS@127.0.0.1 +port="localUPS@127.0.0.1" [valueHasEquals] driver=dummy=ups port=file1.dev # key = val, right? @@ -250,13 +275,13 @@ driver=dummy-ups port=file#1.dev [valueHasQuotedHashtag] driver=dummy-ups -port=file#1.dev +port="file#1.dev" [qx-serial] driver=nutdrv_qx port=/dev/ttyb [qx-usb1] driver=nutdrv_qx -port=auto +port="auto" # USB auto-detection [qx-usb2] driver=nutdrv_qx port=/dev/usb/8 @@ -268,7 +293,10 @@ desc="value with [brackets]" [sectionWithCommentWhitespace] driver=nutdrv_qx # comment port=/dev/usb/8 # comment -commentedDriverFlag # This flag gotta mean something' \ +commentedDriverFlag # This flag gotta mean something +[escapedPath] +driver=dummy-ups +port="C:\\new\\test.dev" # path' \ --show-all-configs } @@ -277,30 +305,25 @@ testcase_upslist_debug() { run_testcase "List decided MEDIA and config checksums for all devices" 0 \ "INST: 68b329da9893e34099c7d8ad5cb9c940~[]: DRV='' PORT='' MEDIA='' SECTIONMD5='9a1f372a850f1ee3ab1fc08b185783e0' INST: 010cf0aed6dd49865bb49b70267946f5~[dummy-proxy]: DRV='dummy-ups ' PORT='remoteUPS@RemoteHost.local' MEDIA='network' SECTIONMD5='aff543fc07d7fbf83e81001b181c8b97' -INST: 1ea79c6eea3681ba73cc695f3253e605~[dummy-proxy-localhost]: DRV='dummy-ups ' PORT='localUPS@127.0.0.1' MEDIA='network-localhost,drivers=localUPS' SECTIONMD5='73e6b7e3e3b73558dc15253d8cca51b2' -INST: 76b645e28b0b53122b4428f4ab9eb4b9~[dummy1]: DRV='dummy-ups' PORT='file1.dev' MEDIA='' SECTIONMD5='9e0a326b67e00d455494f8b4258a01f1' -INST: a293d65e62e89d6cc3ac6cb88bc312b8~[epdu-2]: DRV='netxml-ups' PORT='http://172.16.1.2' MEDIA='network' SECTIONMD5='0d9a0147dcf87c7c720e341170f69ed4' +INST: 1ea79c6eea3681ba73cc695f3253e605~[dummy-proxy-localhost]: DRV=''dummy-ups' PORT='localUPS@127.0.0.1' MEDIA='network-localhost,drivers=localUPS' SECTIONMD5='ba7de199978fe89f5f2e3341ede71df4' +INST: 76b645e28b0b53122b4428f4ab9eb4b9~[dummy1]: DRV='dummy-ups' PORT='file1.dev' MEDIA='' SECTIONMD5='548240a045d5647bcf8c219e1bac66d6' +INST: a293d65e62e89d6cc3ac6cb88bc312b8~[epdu-2]: DRV='netxml-ups' PORT='http://172.16.1.2' MEDIA='network' SECTIONMD5='dc396adae98faff8d424f273478f49c7' INST: 9a5561464ff8c78dd7cb544740ce2adc~[epdu-2-snmp]: DRV='snmp-ups' PORT='172.16.1.2' MEDIA='network' SECTIONMD5='2631b6c21140cea0dd30bb88b942ce3f' +INST: eec97fcac3f2ac0bb3db76001e000b1d~[escapedPath]: DRV='dummy-ups' PORT='C:\\new\\test.dev' MEDIA='' SECTIONMD5='f249649add564a91f9bd04c296106a57' INST: 16adbdafb22d9fdff1d09038520eb32e~[qx-serial]: DRV='nutdrv_qx' PORT='/dev/ttyb' MEDIA='serial' SECTIONMD5='e3e6e586fbe5b3c0a89432f4b993f4ad' -INST: a21bd2b786228b9619f6adba6db8fa83~[qx-usb1]: DRV='nutdrv_qx' PORT='auto' MEDIA='usb' SECTIONMD5='a6139c5da35bef89dc5b96e2296f5369' +INST: a21bd2b786228b9619f6adba6db8fa83~[qx-usb1]: DRV='nutdrv_qx' PORT='auto' MEDIA='usb' SECTIONMD5='59e8b68feca187fc3e2e3ffafe81009d' INST: 0066605e07c66043a17eccecbeea1ac5~[qx-usb2]: DRV='nutdrv_qx' PORT='/dev/usb/8' MEDIA='usb' SECTIONMD5='5722dd9c21d07a1f5bcb516dbc458deb' INST: b377c8ec5f35b3c3df361686627e1625~[ragtech-serial]: DRV='ragtech' PORT='/dev/ttyACM0' MEDIA='serial' SECTIONMD5='bd2de13822ec93bde2aeeeca450fc228' -INST: 1280a731e03116f77290e51dd2a2f37e~[sectionWithComment]: DRV='nutdrv_qx#comment' PORT='/dev/usb/8' MEDIA='' SECTIONMD5='be30e15e17d0579c85eecaf176b4a064' -INST: 770abd5659061a29ed3ae4f7c0b00915~[sectionWithCommentWhitespace]: DRV='nutdrv_qx # comment' PORT='/dev/usb/8 # comment' MEDIA='' SECTIONMD5='c757822a331521cdc97310d0241eba28' -INST: efdb1b4698215fdca36b9bc06d24661d~[serial.4]: DRV='serial-ups' PORT='/dev/ttyS1 # some path' MEDIA='' SECTIONMD5='9c485f733aa6d6c85c1724f162929443' -INST: f4a1c33db201c2ca897a3337993c10fc~[usb_3]: DRV='usbhid-ups' PORT='auto' MEDIA='usb' SECTIONMD5='1f6a24becde9bd31c9852610658ef84a' -INST: 8e5686f92a5ba11901996c813e7bb23d~[valueHasEquals]: DRV='dummy=ups' PORT='file1.dev # key = val, right?' MEDIA='' SECTIONMD5='2f04d65da53e3b13771bb65422f0f4c0' -INST: 99da99b1e301e84f34f349443aac545b~[valueHasHashtag]: DRV='dummy-ups' PORT='file#1.dev' MEDIA='' SECTIONMD5='6029bda216de0cf1e81bd55ebd4a0fff' -INST: d50c3281f9b68a94bf9df72a115fbb5c~[valueHasQuotedHashtag]: DRV='dummy-ups' PORT='file#1.dev' MEDIA='' SECTIONMD5='af59c3c0caaa68dcd796d7145ae403ee'" \ +INST: 1280a731e03116f77290e51dd2a2f37e~[sectionWithComment]: DRV='nutdrv_qx' PORT='/dev/usb/8' MEDIA='usb' SECTIONMD5='be30e15e17d0579c85eecaf176b4a064' +INST: 770abd5659061a29ed3ae4f7c0b00915~[sectionWithCommentWhitespace]: DRV='nutdrv_qx' PORT='/dev/usb/8' MEDIA='usb' SECTIONMD5='c757822a331521cdc97310d0241eba28' +INST: efdb1b4698215fdca36b9bc06d24661d~[serial.4]: DRV='serial-ups' PORT='/dev/ttyS1' MEDIA='' SECTIONMD5='9c485f733aa6d6c85c1724f162929443' +INST: f4a1c33db201c2ca897a3337993c10fc~[usb_3]: DRV='usbhid-ups' PORT='auto' MEDIA='usb' SECTIONMD5='072d77825673bc1ce5d99d3e848697ed' +INST: 8e5686f92a5ba11901996c813e7bb23d~[valueHasEquals]: DRV='dummy' PORT='file1.dev' MEDIA='' SECTIONMD5='2f04d65da53e3b13771bb65422f0f4c0' +INST: 99da99b1e301e84f34f349443aac545b~[valueHasHashtag]: DRV='dummy-ups' PORT='file' MEDIA='' SECTIONMD5='6029bda216de0cf1e81bd55ebd4a0fff' +INST: d50c3281f9b68a94bf9df72a115fbb5c~[valueHasQuotedHashtag]: DRV='dummy-ups' PORT='file#1.dev' MEDIA='' SECTIONMD5='4c1a0f08861adbdb62e5de5525b1618c'" \ upslist_debug - # FIXME : in [valueHasEquals] and [serial.4] the PORT value is quite bogus - # with its embedded comments. Check vs. binary config parser, whether in - # unquoted case only first token is the valid value, and how comments are - # handled in general? - # FIXME : in [valueHasHashtag] the line after "#" should likely be dropped - # (check in binary config parser first) while in [valueHasQuotedHashtag] - # it should stay. + } testcase_getValue() { @@ -355,6 +378,92 @@ globalflag" \ --show-config-value '' nosuchflag } +callNDE_SDP() ( + GETSECTION=upsconf_getSection_SDP + export GETSECTION + callNDE "$@" +) + +callNDE_stable() ( + # Repeated parsing and parsing the normalized text must agree, including + # section hashes. Use only our test configuration, never service actions. + mkdir -p "$NUT_CONFPATH" || exit + NORMALIZED="$NUT_CONFPATH/normalized-$$.conf" + trap 'rm -f "$NORMALIZED"' 0 + callNDE --show-all-configs > "$NORMALIZED" || exit + FIRST="`callNDE upslist_debug`" || exit + SECOND="`callNDE upslist_debug`" || exit + [ "$FIRST" = "$SECOND" ] || exit 1 + UPSCONF="$NORMALIZED" + export UPSCONF + SECOND="`callNDE upslist_debug`" || exit + [ "$FIRST" = "$SECOND" ] || exit 1 + echo stable +) + +testcase_valueSyntax() { + run_testcase "Decode values once using NUT token syntax" 0 \ +' +'" "' +"edge" +C:\new\test.dev +a\"b +\n\t\r\# +it'"'"'s literal +one=two += +one"two +first +file#1.dev +file#1.dev +file#1.dev +two words +word'" "' +two words +twowords +ab +value +intact +first +second value' \ + --show-device-config-value dummy1 empty spaces quote backslashes \ + adjacent doubleescape apostrophe equals equalfirst quoteinword \ + closecommits escapedhash quotedhash quotedescapedhash escapedspace \ + trailing continued continuedbare multiline comment aftercomment repeat + + run_testcase "Only the first unquoted value token is used" 0 'dummy' \ + --show-device-config-value valueHasEquals driver + run_testcase "Single quotes are literal" 0 "'dummy-ups" \ + --show-device-config-value dummy-proxy-localhost driver + run_testcase "Flags may have trailing comments" 0 'commentedDriverFlag' \ + --show-device-config-value sectionWithCommentWhitespace commentedDriverFlag + run_testcase "Missing section retains failure status" 1 '' \ + --show-device-config-value missingSection port + run_testcase "Key lookup is literal" 1 '' \ + --show-device-config-value dummy1 '.*' + run_testcase "Missing value between present keys retains output and status" 1 \ +'C:\new\test.dev + +'" "' +intact' \ + --show-device-config-value dummy1 backslashes missing spaces aftercomment + + # Same externally visible values from the complete and smaller caches. + for CASE_CMD in callNDE callNDE_SDP ; do + run_testcase_generic "$CASE_CMD" "Quoted multiline auto with comment ($CASE_CMD)" 0 \ +'nutdrv_qx +auto' --show-device-config-value qx-usb1 driver port + run_testcase_generic "$CASE_CMD" "Escaped path ($CASE_CMD)" 0 \ + 'C:\new\test.dev' --show-device-config-value escapedPath port + run_testcase_generic "$CASE_CMD" "Quoted hash ($CASE_CMD)" 0 \ + 'file#1.dev' --show-device-config-value valueHasQuotedHashtag port + run_testcase_generic "$CASE_CMD" "Unquoted hash ($CASE_CMD)" 0 \ + 'file' --show-device-config-value valueHasHashtag port + done + run_testcase_generic callNDE_stable "Repeated and normalized parsing keeps checksums stable" \ + 0 stable +} + # This one is not about NDE as such, but piggy-backs on our ability to test # multiple shells at once, with a test relevant for how we write scripts # (with backticks to remain compatible with older Bourne-like shells). @@ -651,6 +760,7 @@ testsuite() { testcase_show_all_configs testcase_getValue testcase_globalSection + testcase_valueSyntax # This one can take a while, put it last testcase_upslist_debug # Something very different