diff --git a/Makefile.am b/Makefile.am index 75086ca3..1fb45a7a 100644 --- a/Makefile.am +++ b/Makefile.am @@ -130,6 +130,7 @@ DIALECT_NEUTRAL_TESTS = tests/case-00-hello.bash \ tests/case-20-exit-status.bash \ tests/case-20-fd-only-inclusion.bash \ tests/case-20-handle-missing-files.bash \ + tests/case-20-soft-errors.bash \ tests/case-20-offset-field.bash \ tests/case-20-repeat-count.bash \ tests/case-21-exit-Q-status.bash \ diff --git a/lib/common.h b/lib/common.h index 9db3c54f..1be025ab 100644 --- a/lib/common.h +++ b/lib/common.h @@ -590,6 +590,13 @@ enum ExitStatus { # define LSOF_SEARCH_FAILURE \ (FsearchErr ? LSOF_EXIT_ERROR : LSOF_EXIT_SUCCESS) +/* + * Argument helper results: 0 = success, 1 = hard error, + * SOFT_ERR = warning-level failure. Hard errors take precedence + * when combining results. + */ +enum { SOFT_ERR = 2 }; + /* * Structure definitions */ diff --git a/src/arg.c b/src/arg.c index 9bb09b9f..e47977b2 100644 --- a/src/arg.c +++ b/src/arg.c @@ -597,7 +597,7 @@ int enter_efsys(struct lsof_context *ctx, /* context */ (void)fprintf(stderr, "%s: -e not followed by a file system path: \"%s\"\n", Pn, e); - return (1); + return (SOFT_ERR); } if (!(ec = mkstrcpy(e, (MALLOC_S *)NULL))) { (void)fprintf(stderr, "%s: no space for -e string: ", Pn); @@ -609,7 +609,7 @@ int enter_efsys(struct lsof_context *ctx, /* context */ else { if (!(path = Readlink(ctx, ec))) { (void)free((FREE_P *)ec); - return (1); + return (SOFT_ERR); } if (Readlink_op == ec) Readlink_op = (char *)NULL; @@ -653,6 +653,7 @@ int enter_fd(struct lsof_context *ctx, /* context */ { char c, *cp1, *cp2, *dash; int err, excl, hi, lo; + int result, soft_err = 0; char *fc; /* * Check for non-empty list and make a copy. @@ -693,11 +694,17 @@ int enter_fd(struct lsof_context *ctx, /* context */ if (ckfd_range(ctx, cp1, dash, cp2, &lo, &hi)) err = 1; else { - if (enter_fd_lst(ctx, (char *)NULL, lo, hi, excl)) + result = enter_fd_lst(ctx, (char *)NULL, lo, hi, excl); + if (result == SOFT_ERR) + soft_err = 1; + else if (result) err = 1; } } else { - if (enter_fd_lst(ctx, cp1, 0, 0, excl)) + result = enter_fd_lst(ctx, cp1, 0, 0, excl); + if (result == SOFT_ERR) + soft_err = 1; + else if (result) err = 1; } } @@ -706,7 +713,7 @@ int enter_fd(struct lsof_context *ctx, /* context */ cp1 = cp2 + 1; } (void)free((FREE_P *)fc); - return (err); + return (err ? 1 : (soft_err ? SOFT_ERR : 0)); } /* @@ -749,7 +756,7 @@ static int enter_fd_lst(struct lsof_context *ctx, /* context */ excl ? "exclude" : "include", FdlTy ? "exclude" : "include", buf); } - return (1); + return (SOFT_ERR); } } /* @@ -884,10 +891,10 @@ int enter_dir(struct lsof_context *ctx, /* context */ if (!Fwarn) (void)fprintf(stderr, "%s: +d not followed by a directory path\n", Pn); - return (1); + return (SOFT_ERR); } if (!(dn = Readlink(ctx, d))) - return (1); + return (SOFT_ERR); if (statsafely(ctx, dn, &sb)) { if (!Fwarn) { en = errno; @@ -899,7 +906,7 @@ int enter_dir(struct lsof_context *ctx, /* context */ (void)free((FREE_P *)dn); dn = (char *)NULL; } - return (1); + return (SOFT_ERR); } if ((sb.st_mode & S_IFMT) != S_IFDIR) { if (!Fwarn) { @@ -910,7 +917,7 @@ int enter_dir(struct lsof_context *ctx, /* context */ (void)free((FREE_P *)dn); dn = (char *)NULL; } - return (1); + return (SOFT_ERR); } #if defined(HASSPECDEVD) @@ -1135,7 +1142,7 @@ int enter_dir(struct lsof_context *ctx, /* context */ "%s: WARNING: no files found in directory: ", Pn); safestrprt(d, stderr, 1); } - return (1); + return (SOFT_ERR); } return (0); } diff --git a/src/main.c b/src/main.c index 4946bc89..a7a9dcb9 100644 --- a/src/main.c +++ b/src/main.c @@ -107,8 +107,10 @@ int main(int argc, char *argv[]) { enum ExitStatus rv; int gopt_rv; int ad, c, i, n, se1, se2, ss; + int arg_result; char *cp; int err = 0; + int soft_err = 0; enum ExitStatus ev = LSOF_EXIT_SUCCESS; int fh = 0; char *fmtr = (char *)NULL; @@ -355,20 +357,29 @@ int main(int argc, char *argv[]) { #endif /* defined(HASNCACHE) */ case 'd': if (GOp == '+') { - if (enter_dir(ctx, GOv, 0)) + arg_result = enter_dir(ctx, GOv, 0); + if (arg_result == SOFT_ERR) + soft_err = 1; + else if (arg_result) err = 1; else { Selflags |= SELNM; xover = 1; } } else { - if (enter_fd(ctx, GOv)) + arg_result = enter_fd(ctx, GOv); + if (arg_result == SOFT_ERR) + soft_err = 1; + else if (arg_result) err = 1; } break; case 'D': if (GOp == '+') { - if (enter_dir(ctx, GOv, 1)) + arg_result = enter_dir(ctx, GOv, 1); + if (arg_result == SOFT_ERR) + soft_err = 1; + else if (arg_result) err = 1; else { Selflags |= SELNM; @@ -388,7 +399,10 @@ int main(int argc, char *argv[]) { #if defined(HASEOPT) case 'e': - if (enter_efsys(ctx, GOv, ((GOp == '+') ? 1 : 0))) + arg_result = enter_efsys(ctx, GOv, ((GOp == '+') ? 1 : 0)); + if (arg_result == SOFT_ERR) + soft_err = 1; + else if (arg_result) err = 1; break; #endif /* defined(HASEOPT) */ @@ -1181,8 +1195,19 @@ int main(int argc, char *argv[]) { select_default_fields(); } + /* + * Preserve usage output for unsuppressed soft failures. + * Hard errors and explicit help requests always reach usage(). + */ + if (soft_err && !Fwarn) + err = 1; if (DChelp || err || Fhelp || fh || version) usage(ctx, err ? 1 : 0, fh, version); + /* + * Suppressing warnings does not make the failed argument valid. + */ + if (soft_err) + Error(ctx); /* * Reduce the size of Suid[], if necessary. */ diff --git a/tests/case-20-soft-errors.bash b/tests/case-20-soft-errors.bash new file mode 100755 index 00000000..0fd6ea25 --- /dev/null +++ b/tests/case-20-soft-errors.bash @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# Warning-level argument failures must not hide unrelated usage errors. +source tests/common.bash + +work=$(mktemp -d "${TMPDIR:-/tmp}/lsof-soft-errors.XXXXXX") || exit 1 +trap 'rm -rf "$work"' EXIT +touch "$work/file" || exit 1 +missing=$work/missing + +check_output() { + local expected=$1 + shift + local output status expected_status=1 + [[ $expected == help ]] && expected_status=0 + output=$("$lsof" "$@" 2>&1) + status=$? + if [[ $status -ne $expected_status ]]; then + echo "unexpected exit status $status: $*" + return 1 + fi + case $expected in + quiet) + [[ -z $output ]] && return 0 + ;; + warning) + [[ $output == *WARNING:* && $output == *usage:* ]] && return 0 + ;; + diagnostic) + [[ $output == *WARNING:* && $output != *usage:* ]] && return 0 + ;; + usage|help) + [[ $output == *usage:* ]] && return 0 + ;; + esac + echo "expected $expected output: $*" + echo "$output" + return 1 +} + +{ + for option in +d +D; do + for path in "$missing" "$work/file"; do + check_output warning "$option" "$path" || exit 1 + for flag in -w -t; do + check_output quiet "$flag" "$option" "$path" || exit 1 + # A later flag suppresses usage, not an already printed warning. + check_output diagnostic "$option" "$path" "$flag" || exit 1 + # Illegal FD ranges are portable hard argument errors. + check_output usage "$flag" -d 3-1 "$option" "$path" || exit 1 + check_output usage "$flag" "$option" "$path" -d 3-1 || exit 1 + check_output help "$flag" "$option" "$path" -h || exit 1 + done + done + done + check_output quiet -w +d "$missing" +D "$missing" || exit 1 + check_output usage -w +D "$missing" +w || exit 1 + # Preserve soft results through enter_fd(), including range entries. + for list in '0,^1' '0,^1-2'; do + check_output quiet -w -d "$list" || exit 1 + check_output usage -d "$list" || exit 1 + done + # Hard errors must win in either order within one FD list. + check_output usage -w -d '0,^1,3-1' || exit 1 + check_output usage -w -d '0,3-1,^1' || exit 1 + echo "soft-error argument tests passed" +} > "$report" 2>&1