Underline the offending argument through `uucore::diagnostics` when
stderr is a terminal or `UUTILS_DIAG=always`. The plain line still
leads, and captured output stays the single GNU line.
The report heading comes from uucore's `util_name()`, which keeps the
`.exe` on Windows; the plain line keeps `program_name()` like every
other message.
Quote the predicate or operator as GNU 4.10.0 does, report an operator
cut off by ')' and an unmatched ')' with GNU's messages, and name a
binary operator as spelled.
The -ls formatter only knew directories and regular files and ignored
setuid, setgid and sticky bits. Use uucore::fs::display_permissions, as
-printf %M already does.
advance_one read a full `char` via front() but then dropped it by slicing one
byte (`&self.string[1..]`). When that char is multibyte (e.g. a `€` after a `%`
conversion, a `\` escape, or a `%A`/`%C`/`%T` time directive), byte index 1 is
not a char boundary and the slice panics. Advance by the char's UTF-8 length so
multibyte input routes through the normal handling instead of aborting.
These three entry points collected their arguments with std::env::args,
which panics when an argument is not valid Unicode. An argument that is
not valid UTF-8 therefore aborted the process with exit code 101 before
any work started.
Collect the arguments with args_os instead, and report the first invalid
argument on stderr with exit code 1, as find already does.
convert_arg_to_number parsed the depth with Rust's usize parser, which accepts a
leading '+', so "-maxdepth +1" was silently accepted. GNU find rejects a signed
value there. Only accept plain decimal digits. Adds a test_find.rs test.
std::env::args panics on a non-UTF-8 argument. Read the arguments with args_os
and report the first invalid one with a clean error and exit 1, instead of
aborting (#816). Adds a helper unit test and a test_find.rs test.
The starting points and the answers to the prompts would both be read
from standard input, so they compete for the same bytes and the result
depends on buffering. GNU find rejects the combination up front; we
accepted it and prompted, consuming input meant for the caller. The bfs
tests gnu/files0_from_stdin_ok and gnu/ok_files0_from_stdin now pass, and
the former no longer hangs on the testsuite's stdin.
Reading the starting points from a file leaves stdin free for the
prompts, so that combination stays allowed.
Both actions run the command after changing into the directory being
scanned, so a relative $PATH entry -- including the empty string and ".",
which mean the current directory -- would be looked up inside a directory
somebody else may be able to write to. GNU find rejects this at parse
time; we ran the command anyway, and for -okdir we first prompted, which
meant reading the answer from whatever stdin happened to be. In the bfs
testsuite that is the harness's own stdin, so gnu/okdir_path_empty hung
and reported no result at all instead of failing (bfs gnu/okdir_path_*
and gnu/execdir_path_* now pass).
An unset $PATH is left alone: the command is then looked up in the
system default path.
The -size argument was parsed with an un-anchored regex, so "x5c" or "abc5c"
silently matched the "5c" in the middle. Anchor the regex and allow only the
one extra '+' GNU accepts after the comparison sign, so "++5c"/"-+5c" stay valid
while the junk-prefix and invalid-sign forms are rejected. Adds a test_find.rs
test.
Two divergences from GNU find:
- An unrecognized %X directive silently dropped the '%' and printed
only the following character.
- An unrecognized \X escape was a hard parse error (exit 1) instead of
a warning.
Both now print a "find: warning: ..." message to stderr and emit the
directive/escape literally (with its '%' or '\' prefix), matching GNU
find's behavior and exit code (0).
Also fixes advance_one(), which sliced the remaining format string at
a fixed 1-byte offset and panicked on multibyte characters (e.g. '€')
immediately after '%' or '\' — using char::len_utf8() instead.
Fixes#815
-files0-from read the whole input into memory with read_to_end before
splitting on NUL, so an endless source such as /dev/urandom or /dev/zero
exhausted memory and produced no output. Read the starting points
incrementally with read_until(0) and walk each one as it is parsed.
Fixes#779
Co-authored-by: Pablo Garcia <pablito@MacBook-Neo-de-Pablo.local>
When input ended with a blank immediately before its final newline,
xargs passed one extra empty argument to the command:
$ printf 'aaa \nbbb \n' | xargs printf '[%s]'
[aaa][bbb][] # was
[aaa][bbb] # GNU findutils 4.10.0, and now
WhitespaceDelimitedArgumentReader::next used one `result.is_empty()`
check to answer two different questions. The whitespace branch broke out
of the loop only if `result` was non-empty, so a delimiter seen before
any token was skipped -- correct for blanks, but it left "we are
mid-token" indistinguishable from "we have not started one". The EOF
branch then used `i == 0` as a proxy for "nothing was consumed", which
is false for a final call that consumes only the trailing newline, so it
flushed a zero-length token. Input ending without a newline happened to
work because nothing was left to consume.
Track that state explicitly with an `in_argument` flag, mirroring
`seen_arg` in GNU's read_line, and test `result.is_empty()` at EOF. A
delimiter now ends an argument only when we are inside one, and EOF
flushes only a non-empty buffer.
This also fixes a second symptom of the same conflation: quoted empty
arguments were dropped mid-stream, since quotes start an argument
without contributing any bytes. `printf '"" x\n'` gave [x] and now gives
[][x], again matching GNU. An unterminated `x ''` at end of input still
yields just [x], as GNU's `if (p == linebuf) return -1` does.
-I/-i is unaffected: with a replace string and no explicit delimiter,
xargs uses the newline-delimited reader instead.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>