chore: bump version to 17.1.2

- fixed rustfmt drift and clippy errors in pi-shell moreutils builtins (combine, errno, ifne, isutf8, sponge, ts) that blocked the release check gate
This commit is contained in:
can1357
2026-07-24 16:53:52 +02:00
parent c55b28a26d
commit 7ebf141743
35 changed files with 245 additions and 176 deletions
+1
View File
@@ -80,6 +80,7 @@ uu_sed = { path = "../vendor/uu-sed" }
uu_xargs = { path = "../vendor/uu-xargs" }
jaq = { path = "../vendor/jaq" }
jiff = { version = "0.2", features = ["tzdb-bundle-platform"] }
memchr = "2"
[target.'cfg(unix)'.dependencies]
libc.workspace = true
+13 -7
View File
@@ -5,7 +5,7 @@
//! byte strings; membership comparison strips a trailing `\n`, so a final line
//! without a newline still matches, and is emitted as-is (no newline added).
//! Usage errors (wrong arg count, unknown OP, both sides stdin) exit 1, unlike
//! moreutils' die() exit 255.
//! moreutils' `die()` exit 255.
use std::{
collections::HashSet,
@@ -36,7 +36,7 @@ enum Error {
impl From<String> for Error {
fn from(msg: String) -> Self {
Error::Msg(msg)
Self::Msg(msg)
}
}
@@ -56,9 +56,15 @@ pub fn run(argv: Vec<OsString>) -> i32 {
},
};
let file1 = matches.get_one::<OsString>(ARG_FILE1).expect("required").clone();
let file1 = matches
.get_one::<OsString>(ARG_FILE1)
.expect("required")
.clone();
let op = matches.get_one::<String>(ARG_OP).expect("required");
let file2 = matches.get_one::<OsString>(ARG_FILE2).expect("required").clone();
let file2 = matches
.get_one::<OsString>(ARG_FILE2)
.expect("required")
.clone();
match execute(&file1, op, &file2) {
Ok(()) => 0,
@@ -158,8 +164,7 @@ fn open_input(name: &OsStr) -> Result<Box<dyn BufRead>, Error> {
return Ok(Box::new(BufReader::new(pi_uutils_ctx::stdin())));
}
let path = pi_uutils_ctx::resolve(name);
let file =
File::open(path).map_err(|err| Error::Msg(input_error(name, &err.to_string())))?;
let file = File::open(path).map_err(|err| Error::Msg(input_error(name, &err.to_string())))?;
Ok(Box::new(BufReader::new(file)))
}
@@ -202,7 +207,8 @@ fn key(line: &[u8]) -> &[u8] {
}
fn write_line(out: &mut impl Write, line: &[u8]) -> Result<(), Error> {
out.write_all(line).map_err(|err| Error::Msg(err.to_string()))
out.write_all(line)
.map_err(|err| Error::Msg(err.to_string()))
}
fn input_error(name: &OsStr, err: &str) -> String {
+17 -20
View File
@@ -9,8 +9,7 @@
//! then name; `--search` prints entries whose description contains all the
//! given words case-insensitively.
use std::ffi::OsString;
use std::io::Write;
use std::{ffi::OsString, io::Write};
use clap::{Arg, ArgAction, Command};
use pi_uutils_ctx::format_usage;
@@ -144,7 +143,7 @@ pub fn run(argv: Vec<OsString>) -> i32 {
failed = true;
}
}
if failed { 1 } else { 0 }
i32::from(failed)
}
fn command() -> Command {
@@ -203,17 +202,15 @@ fn lookup(arg: &str) -> bool {
},
None => false,
}
} else if let Some((name, value)) = ERRNOS
.iter()
.find(|(name, _)| name.eq_ignore_ascii_case(arg))
{
print_entry(name, *value);
true
} else {
match ERRNOS.iter().find(|(name, _)| name.eq_ignore_ascii_case(arg)) {
Some((name, value)) => {
print_entry(name, *value);
true
},
None => {
let _ = writeln!(pi_uutils_ctx::stderr(), "errno: unknown errno {arg}");
false
},
}
let _ = writeln!(pi_uutils_ctx::stderr(), "errno: unknown errno {arg}");
false
}
}
@@ -272,14 +269,14 @@ mod tests {
let stdout = Arc::new(Mutex::new(Vec::new()));
let stderr = Arc::new(Mutex::new(Vec::new()));
let io = ScopeIo {
stdin: Box::new(Cursor::new(Vec::new())),
stdin_fd: None,
stdin: Box::new(Cursor::new(Vec::new())),
stdin_fd: None,
stdin_is_search_input: false,
stdout: Box::new(SharedWriter(Arc::clone(&stdout))),
stderr: Box::new(SharedWriter(Arc::clone(&stderr))),
cwd: std::env::temp_dir(),
env: HashMap::new(),
cancel: Arc::new(AtomicBool::new(false)),
stdout: Box::new(SharedWriter(Arc::clone(&stdout))),
stderr: Box::new(SharedWriter(Arc::clone(&stderr))),
cwd: std::env::temp_dir(),
env: HashMap::new(),
cancel: Arc::new(AtomicBool::new(false)),
};
let argv = std::iter::once("errno")
.chain(args.iter().copied())
+15 -17
View File
@@ -19,7 +19,11 @@ const CHUNK: usize = 64 * 1024;
pub fn run(argv: Vec<OsString>) -> i32 {
let mut args = argv.into_iter().skip(1).peekable();
let mut invert = false;
match args.peek().map(|a| a.to_string_lossy().into_owned()).as_deref() {
match args
.peek()
.map(|a| a.to_string_lossy().into_owned())
.as_deref()
{
Some("-n") => {
invert = true;
args.next();
@@ -90,11 +94,7 @@ fn spawn_and_pump(command: &[OsString], first: Option<u8>, stdin: &mut impl Read
{
Ok(child) => child,
Err(err) => {
let _ = writeln!(
pi_uutils_ctx::stderr(),
"ifne: {}: {err}",
command[0].to_string_lossy()
);
let _ = writeln!(pi_uutils_ctx::stderr(), "ifne: {}: {err}", command[0].to_string_lossy());
return 127;
},
};
@@ -120,9 +120,7 @@ fn spawn_and_pump(command: &[OsString], first: Option<u8>, stdin: &mut impl Read
// Ignore BrokenPipe: the child may exit before consuming its stdin
// (e.g. `ifne head -1`).
let pump = match copy_cancellable(stdin, &mut child_stdin, first) {
Err(CopyError::Io(err)) if err.kind() != ErrorKind::BrokenPipe => {
Err(CopyError::Io(err))
},
Err(CopyError::Io(err)) if err.kind() != ErrorKind::BrokenPipe => Err(CopyError::Io(err)),
Err(CopyError::Cancelled) => Err(CopyError::Cancelled),
_ => Ok(()),
};
@@ -170,7 +168,7 @@ fn copy_cancellable(
if let Some(byte) = first {
dst.write_all(&[byte]).map_err(CopyError::Io)?;
}
let mut buf = [0u8; CHUNK];
let mut buf = vec![0u8; CHUNK].into_boxed_slice();
loop {
if pi_uutils_ctx::is_cancelled() {
return Err(CopyError::Cancelled);
@@ -231,14 +229,14 @@ mod tests {
let stdout = Arc::new(Mutex::new(Vec::new()));
let stderr = Arc::new(Mutex::new(Vec::new()));
let io = ScopeIo {
stdin: Box::new(Cursor::new(stdin.to_vec())),
stdin_fd: None,
stdin: Box::new(Cursor::new(stdin.to_vec())),
stdin_fd: None,
stdin_is_search_input: false,
stdout: Box::new(SharedWriter(Arc::clone(&stdout))),
stderr: Box::new(SharedWriter(Arc::clone(&stderr))),
cwd: std::env::temp_dir(),
env: HashMap::from([("PATH".to_string(), "/usr/bin:/bin".to_string())]),
cancel: Arc::new(AtomicBool::new(false)),
stdout: Box::new(SharedWriter(Arc::clone(&stdout))),
stderr: Box::new(SharedWriter(Arc::clone(&stderr))),
cwd: std::env::temp_dir(),
env: HashMap::from([("PATH".to_string(), "/usr/bin:/bin".to_string())]),
cancel: Arc::new(AtomicBool::new(false)),
};
let argv = std::iter::once("ifne")
.chain(args.iter().copied())
+10 -14
View File
@@ -57,8 +57,7 @@ pub fn run(argv: Vec<OsString>) -> i32 {
let invert = matches.get_flag(OPT_INVERT);
let files: Vec<OsString> = matches
.get_many::<OsString>(ARG_FILES)
.map(|values| values.cloned().collect())
.unwrap_or_else(|| vec![OsString::from("-")]);
.map_or_else(|| vec![OsString::from("-")], |values| values.cloned().collect());
let mut any_failed = false;
let mut io_error = false;
@@ -91,13 +90,7 @@ pub fn run(argv: Vec<OsString>) -> i32 {
}
}
if io_error {
2
} else if any_failed {
1
} else {
0
}
if io_error { 2 } else { i32::from(any_failed) }
}
fn command() -> Command {
@@ -202,9 +195,9 @@ fn validate(input: &mut impl Read) -> io::Result<Verdict> {
/// Updates line/char counters over `text`, a slice already known to be valid
/// UTF-8 (chars are counted as non-continuation bytes, so no re-decode).
fn advance(text: &[u8], line: &mut u64, chars_in_line: &mut u64) {
match text.iter().rposition(|&b| b == b'\n') {
match memchr::memrchr(b'\n', text) {
Some(last) => {
*line += text.iter().filter(|&&b| b == b'\n').count() as u64;
*line += memchr::memchr_iter(b'\n', text).count() as u64;
*chars_in_line = count_chars(&text[last + 1..]);
},
None => *chars_in_line += count_chars(text),
@@ -212,7 +205,7 @@ fn advance(text: &[u8], line: &mut u64, chars_in_line: &mut u64) {
}
fn count_chars(bytes: &[u8]) -> u64 {
bytes.iter().filter(|&&b| (b & 0xC0) != 0x80).count() as u64
bytes.iter().filter(|&&b| (b & 0xc0) != 0x80).count() as u64
}
fn display_name(name: &OsStr) -> String {
@@ -319,7 +312,7 @@ mod tests {
fn truncated_sequence_at_chunk_boundary_is_invalid() {
let (_dir, root) = tempdir();
let mut bytes = vec![b'a'; 65535];
bytes.push(0xC3); // incomplete at the exact chunk boundary
bytes.push(0xc3); // incomplete at the exact chunk boundary
bytes.extend_from_slice(b"zzz");
fs::write(root.join("cut"), &bytes).unwrap();
@@ -374,7 +367,10 @@ mod tests {
fn stdin_is_validated_when_no_files_given() {
let (_dir, root) = tempdir();
assert_eq!(run_in(root.clone(), "héllo\n".as_bytes(), &[]), (0, String::new(), String::new()));
assert_eq!(
run_in(root.clone(), "héllo\n".as_bytes(), &[]),
(0, String::new(), String::new())
);
let (code, stdout, _) = run_in(root, b"h\xFFi", &[]);
assert_eq!(code, 1);
+20 -5
View File
@@ -85,7 +85,11 @@ fn command() -> Command {
.action(ArgAction::SetTrue),
)
.arg(Arg::new("help").long("help").action(ArgAction::Help))
.arg(Arg::new("version").long("version").action(ArgAction::Version))
.arg(
Arg::new("version")
.long("version")
.action(ArgAction::Version),
)
.arg(
Arg::new(ARG_FILE)
.value_name("FILE")
@@ -103,7 +107,7 @@ enum SoakError {
fn soak_stdin() -> Result<Vec<u8>, SoakError> {
let mut stdin = pi_uutils_ctx::stdin();
let mut buffer = Vec::new();
let mut chunk = [0u8; CHUNK_SIZE];
let mut chunk = vec![0u8; CHUNK_SIZE].into_boxed_slice();
loop {
if pi_uutils_ctx::is_cancelled() {
return Err(SoakError::Cancelled);
@@ -135,7 +139,12 @@ fn replace_atomically(target: &Path, buffer: &[u8]) -> io::Result<()> {
result
}
fn write_and_swap(target: &Path, temp_path: &Path, temp: &mut File, buffer: &[u8]) -> io::Result<()> {
fn write_and_swap(
target: &Path,
temp_path: &Path,
temp: &mut File,
buffer: &[u8],
) -> io::Result<()> {
temp.write_all(buffer)?;
temp.flush()?;
if let Ok(metadata) = fs::metadata(target) {
@@ -148,8 +157,14 @@ fn write_and_swap(target: &Path, temp_path: &Path, temp: &mut File, buffer: &[u8
/// `target` with `create_new`, retrying on collision.
fn create_sibling_temp(target: &Path) -> io::Result<(PathBuf, File)> {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let dir = target.parent().filter(|p| !p.as_os_str().is_empty()).unwrap_or(Path::new("."));
let base = target.file_name().unwrap_or(OsStr::new("sponge")).to_string_lossy();
let dir = target
.parent()
.filter(|p| !p.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."));
let base = target
.file_name()
.unwrap_or_else(|| OsStr::new("sponge"))
.to_string_lossy();
for _ in 0..32 {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
+46 -25
View File
@@ -6,8 +6,8 @@
//! - `-i`: time elapsed since the previous line (default format `%H:%M:%S`)
//! - `-s`: time elapsed since program start (default format `%H:%M:%S`)
//! - `-m`: use the monotonic clock for `-i`/`-s` elapsed computation
//! - `-r`: rewrite an existing leading timestamp (RFC3339/ISO8601 or syslog
//! `%b %d %H:%M:%S`) into a human-relative form
//! - `-r`: rewrite an existing leading timestamp (RFC3339/ISO8601 or syslog `%b
//! %d %H:%M:%S`) into a human-relative form
//!
//! Elapsed durations are formatted as if they were seconds since the Unix
//! epoch rendered in UTC (matching moreutils), so 90 elapsed seconds with
@@ -100,8 +100,16 @@ fn command() -> Command {
.action(ArgAction::SetTrue),
)
.arg(Arg::new("help").long("help").action(ArgAction::Help))
.arg(Arg::new("version").long("version").action(ArgAction::Version))
.arg(Arg::new(ARG_FORMAT).value_name("FORMAT").help("strftime format string"))
.arg(
Arg::new("version")
.long("version")
.action(ArgAction::Version),
)
.arg(
Arg::new(ARG_FORMAT)
.value_name("FORMAT")
.help("strftime format string"),
)
}
/// Timestamping mode selected by the flags.
@@ -149,7 +157,9 @@ fn timestamp_lines(matches: &ArgMatches) -> Result<i32, String> {
return Ok(130);
}
buf.clear();
let n = reader.read_until(b'\n', &mut buf).map_err(|err| err.to_string())?;
let n = reader
.read_until(b'\n', &mut buf)
.map_err(|err| err.to_string())?;
if n == 0 {
break;
}
@@ -162,8 +172,10 @@ fn timestamp_lines(matches: &ArgMatches) -> Result<i32, String> {
let year = now.to_zoned(tz.clone()).year();
if let Some((consumed, then)) = parse_leading_timestamp(content, year, &tz) {
let rel = render_relative(then, now);
out.write_all(rel.as_bytes()).map_err(|err| err.to_string())?;
out.write_all(&content[consumed..]).map_err(|err| err.to_string())?;
out.write_all(rel.as_bytes())
.map_err(|err| err.to_string())?;
out.write_all(&content[consumed..])
.map_err(|err| err.to_string())?;
} else {
out.write_all(content).map_err(|err| err.to_string())?;
}
@@ -171,24 +183,34 @@ fn timestamp_lines(matches: &ArgMatches) -> Result<i32, String> {
Mode::Absolute => {
let zoned = Timestamp::now().to_zoned(tz.clone());
let stamp = strtime::format(&format, &zoned).map_err(|err| err.to_string())?;
out.write_all(stamp.as_bytes()).map_err(|err| err.to_string())?;
out.write_all(stamp.as_bytes())
.map_err(|err| err.to_string())?;
out.write_all(b" ").map_err(|err| err.to_string())?;
out.write_all(content).map_err(|err| err.to_string())?;
},
Mode::SinceLast | Mode::SinceStart => {
let nanos = if monotonic {
let now = Instant::now();
let anchor = if mode == Mode::SinceLast { last_mono } else { start_mono };
let anchor = if mode == Mode::SinceLast {
last_mono
} else {
start_mono
};
last_mono = now;
i128::try_from(now.duration_since(anchor).as_nanos()).unwrap_or(i128::MAX)
} else {
let now = Timestamp::now();
let anchor = if mode == Mode::SinceLast { last_wall } else { start_wall };
let anchor = if mode == Mode::SinceLast {
last_wall
} else {
start_wall
};
last_wall = now;
now.duration_since(anchor).as_nanos().max(0)
};
let stamp = format_elapsed(nanos, &format)?;
out.write_all(stamp.as_bytes()).map_err(|err| err.to_string())?;
out.write_all(stamp.as_bytes())
.map_err(|err| err.to_string())?;
out.write_all(b" ").map_err(|err| err.to_string())?;
out.write_all(content).map_err(|err| err.to_string())?;
},
@@ -262,11 +284,11 @@ fn expand_subseconds(format: &str) -> String {
/// Length of the UTF-8 sequence introduced by `first` (1 for continuation or
/// invalid bytes, which only arise from already-valid `&str` input here).
fn utf8_len(first: u8) -> usize {
const fn utf8_len(first: u8) -> usize {
match first {
0xC0..=0xDF => 2,
0xE0..=0xEF => 3,
0xF0..=0xF7 => 4,
0xc0..=0xdf => 2,
0xe0..=0xef => 3,
0xf0..=0xf7 => 4,
_ => 1,
}
}
@@ -364,14 +386,14 @@ mod tests {
let stdout = Arc::new(Mutex::new(Vec::new()));
let stderr = Arc::new(Mutex::new(Vec::new()));
let io = ScopeIo {
stdin: Box::new(Cursor::new(stdin.to_vec())),
stdin_fd: None,
stdin: Box::new(Cursor::new(stdin.to_vec())),
stdin_fd: None,
stdin_is_search_input: false,
stdout: Box::new(SharedWriter(Arc::clone(&stdout))),
stderr: Box::new(SharedWriter(Arc::clone(&stderr))),
cwd: std::env::temp_dir(),
env: HashMap::from([("TZ".to_string(), "UTC".to_string())]),
cancel: Arc::new(AtomicBool::new(false)),
stdout: Box::new(SharedWriter(Arc::clone(&stdout))),
stderr: Box::new(SharedWriter(Arc::clone(&stderr))),
cwd: std::env::temp_dir(),
env: HashMap::from([("TZ".to_string(), "UTC".to_string())]),
cancel: Arc::new(AtomicBool::new(false)),
};
let argv = std::iter::once("ts")
.chain(args.iter().copied())
@@ -444,7 +466,7 @@ mod tests {
#[test]
fn relative_conflicts_with_elapsed_modes() {
let (code, _, _) = run_in(b"", &["-r", "-i"]);
let (code, ..) = run_in(b"", &["-r", "-i"]);
assert_eq!(code, 2);
}
@@ -475,8 +497,7 @@ mod tests {
assert_eq!(ts, "2024-01-01T17:00:00.5Z".parse::<Timestamp>().unwrap());
// Civil datetime without offset resolves in the provided timezone.
let (consumed, ts) =
parse_leading_timestamp(b"2024-01-01T12:00:00 x", 2024, &tz).unwrap();
let (consumed, ts) = parse_leading_timestamp(b"2024-01-01T12:00:00 x", 2024, &tz).unwrap();
assert_eq!(consumed, 19);
assert_eq!(ts, "2024-01-01T12:00:00Z".parse::<Timestamp>().unwrap());
+8 -7
View File
@@ -2234,13 +2234,14 @@ mod tests {
params.set_fd(OpenFiles::STDERR_FD, null_file().expect("null stderr"));
let source_info = SourceInfo::from("pi-natives:test");
let script = "PATH=/definitely-missing\n\
echo x | ts -s '%H:%M:%S' | sponge out || exit 10\n\
isutf8 out || exit 11\n\
echo x | combine - and out || exit 12\n\
ifne /definitely-missing/tool || exit 13";
let result =
session.shell.run_string(script, &source_info, &params).await.expect("moreutils script");
let script = "PATH=/definitely-missing\necho x | ts -s '%H:%M:%S' | sponge out || exit \
10\nisutf8 out || exit 11\necho x | combine - and out || exit 12\nifne \
/definitely-missing/tool || exit 13";
let result = session
.shell
.run_string(script, &source_info, &params)
.await
.expect("moreutils script");
assert_eq!(exit_code(&result), 0);
// `ts -s` stamps the first line with zero elapsed time: `HH:MM:SS x`.