From 57e1ccbb7ce30e5298ea7ec3934e462fa8d32237 Mon Sep 17 00:00:00 2001 From: Lasse Collin Date: Tue, 19 Jul 2022 23:13:24 +0300 Subject: [PATCH] xzgrep: Improve error handling, especially signals. xzgrep wouldn't exit on SIGPIPE or SIGQUIT when it clearly should have. It's quite possible that it's not perfect still but at least it's much better. If multiple exit statuses compete, now it tries to pick the largest of value. Some comments were added. The exit status handling of signals is still broken if the shell uses values larger than 255 in $? to indicate that a process died due to a signal ***and*** their "exit" command doesn't take this into account. This seems to work well with the ksh and yash versions I tried. However, there is a report in gzip/zgrep that OpenSolaris 5.11 (not 5.10) has a problem with "exit" truncating the argument to 8 bits: https://debbugs.gnu.org/cgi/bugreport.cgi?bug=22900#25 Such a bug would break xzgrep but I didn't add a workaround at least for now. 5.11 is old and I don't know if the problem exists in modern descendants, or if the problem exists in other ksh implementations in use. --- src/scripts/xzgrep.in | 70 ++++++++++++++++++++++++++++++++----------- 1 file changed, 52 insertions(+), 18 deletions(-) diff --git a/src/scripts/xzgrep.in b/src/scripts/xzgrep.in index 78f5bd31..c5b89430 100644 --- a/src/scripts/xzgrep.in +++ b/src/scripts/xzgrep.in @@ -182,7 +182,9 @@ for i; do *[-.]zst | *[-.]tzst) uncompress="zstd -cdfq";; # zstd needs -q. *) uncompress="$xz -cdf";; esac - # Fail if xz or grep (or sed) fails. + # xz_status will hold the decompressor's exit status. + # Exit status of grep (and in rare cases, printf or sed) is + # available as the exit status of this assignment command. xz_status=$( exec 5>&1 ($uncompress -- "$i" 5>&-; echo $? >&5) 3>&- | @@ -232,33 +234,65 @@ for i; do # $i already ends with a colon so don't add it here. sed_script="s|^|$i|" - # Fail if grep or sed fails. + # If grep or sed fails, pick the larger value of the two exit statuses. + # If sed fails, use at least 2 since we use >= 2 to indicate errors. r=$( exec 4>&1 (eval "$grep" 4>&-; echo $? >&4) 3>&- | LC_ALL=C sed "$sed_script" >&3 4>&- - ) || r=2 + ) || { + sed_status=$? + test "$sed_status" -lt 2 && sed_status=2 + test "$r" -lt "$sed_status" && r=$sed_status + } exit $r fi >&3 5>&- ) r=$? - # fail occurred previously, nothing worse can happen - test $res -gt 1 && continue + # If grep or sed or other non-decompression command failed with a signal, + # exit immediately and ignore the possible remaining files. + # + # NOTE: Instead of 128 + signal_number, some shells use + # 256 + signal_number (ksh) or 384 + signal_number (yash). + # This is fine for us since their "exit" and "kill -l" commands take + # this into account. (At least the versions I tried do but there is + # a report of an old ksh variant whose "exit" truncates the exit status + # to 8 bits without any special handling for values indicating a signal.) + test "$r" -ge 128 && exit "$r" - if test "$xz_status" -eq 0; then - : - elif test "$xz_status" -ge 128 \ - && test "$(kill -l "$xz_status" 2> /dev/null)" = "PIPE"; then - : - else - r=2 + if test -z "$xz_status"; then + # Something unusual happened, for example, we got a signal and + # the exit status of the decompressor was never echoed and thus + # $xz_status is empty. Exit immediately and ignore the possible + # remaining files. + exit 2 + elif test "$xz_status" -ge 128; then + # The decompressor died due to a signal. SIGPIPE is ignored since it can + # occur if grep exits before the whole file has been decompressed (grep -q + # can do that). If the decompressor died with some other signal, exit + # immediately and ignore the possible remaining files. + test "$(kill -l "$xz_status" 2> /dev/null)" != "PIPE" && exit "$xz_status" + elif test "$xz_status" -gt 0; then + # Decompression failed but we will continue with the remaining + # files anwyway. Set exit status to at least 2 to indicate an error. + test "$r" -lt 2 && r=2 fi - # still no match - test $r -eq 1 && continue - - # 0 == match, >=2 == fail - res=$r + # Since res=1 is the initial value, we only need to care about + # matches (r == 0) and errors (r >= 2) here; r == 1 can be ignored. + if test "$r" -ge 2; then + # An error occurred in decompressor, grep, or some other command. Update + # res unless a larger error code has been seen with an earlier file. + test "$res" -lt "$r" && res=$r + elif test "$r" -eq 0; then + # grep found a match and no errors occurred. Update res if no errors have + # occurred with earlier files. + test "$res" -eq 1 && res=0 + fi done -exit $res + +# 0: At least one file matched and no errors occurred. +# 1: No matches were found and no errors occurred. +# >=2: Error. It's unknown if matches were found. +exit "$res"