diff options
40 files changed, 1341 insertions, 901 deletions
diff --git a/.clippy.toml b/.clippy.toml index b1552463e..89fd1cccd 100644 --- a/.clippy.toml +++ b/.clippy.toml @@ -1,2 +1,4 @@ msrv = "1.70.0" cognitive-complexity-threshold = 24 +missing-docs-in-crate-items = true +check-private-items = true diff --git a/.vscode/cspell.dictionaries/jargon.wordlist.txt b/.vscode/cspell.dictionaries/jargon.wordlist.txt index 20e26990f..efc90eb3c 100644 --- a/.vscode/cspell.dictionaries/jargon.wordlist.txt +++ b/.vscode/cspell.dictionaries/jargon.wordlist.txt @@ -155,3 +155,7 @@ retval subdir val vals + +# * clippy +uninlined +nonminimal diff --git a/Cargo.toml b/Cargo.toml index b25e91d25..f8e2a11bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -556,3 +556,12 @@ inherits = "release" opt-level = "z" panic = "abort" strip = true + +[lints.clippy] +multiple_crate_versions = { level = "allow", priority = 1 } +cargo_common_metadata = { level = "allow", priority = 1 } +uninlined_format_args = { level = "allow", priority = 1 } +missing_panics_doc = { level = "allow", priority = 1 } +all = "deny" +cargo = "warn" +pedantic = "deny" @@ -11,14 +11,14 @@ use std::io::Write; use std::path::Path; pub fn main() { - if let Ok(profile) = env::var("PROFILE") { - println!("cargo:rustc-cfg=build={profile:?}"); - } - const ENV_FEATURE_PREFIX: &str = "CARGO_FEATURE_"; const FEATURE_PREFIX: &str = "feat_"; const OVERRIDE_PREFIX: &str = "uu_"; + if let Ok(profile) = env::var("PROFILE") { + println!("cargo:rustc-cfg=build={profile:?}"); + } + let out_dir = env::var("OUT_DIR").unwrap(); let mut crates = Vec::new(); @@ -46,6 +46,7 @@ pub fn main() { "type UtilityMap<T> = phf::OrderedMap<&'static str, (fn(T) -> i32, fn() -> Command)>;\n\ \n\ #[allow(clippy::too_many_lines)] + #[allow(clippy::unreadable_literal)] fn util_map<T: uucore::Args>() -> UtilityMap<T> {\n" .as_bytes(), ) diff --git a/src/bin/coreutils.rs b/src/bin/coreutils.rs index 6edb70821..5d5a9978b 100644 --- a/src/bin/coreutils.rs +++ b/src/bin/coreutils.rs @@ -34,6 +34,8 @@ fn usage<T>(utils: &UtilityMap<T>, name: &str) { ); } +/// # Panics +/// Panics if the binary path cannot be determined fn binary_path(args: &mut impl Iterator<Item = OsString>) -> PathBuf { match args.next() { Some(ref s) if !s.is_empty() => PathBuf::from(s), @@ -85,9 +87,8 @@ fn main() { process::exit(1); } - let util = match util_os.to_str() { - Some(util) => util, - None => not_found(&util_os), + let Some(util) = util_os.to_str() else { + not_found(&util_os) }; match util { @@ -113,9 +114,8 @@ fn main() { if util == "--help" || util == "-h" { // see if they want help on a specific util if let Some(util_os) = args.next() { - let util = match util_os.to_str() { - Some(util) => util, - None => not_found(&util_os), + let Some(util) = util_os.to_str() else { + not_found(&util_os) }; match utils.get(util) { @@ -145,6 +145,8 @@ fn main() { } /// Prints completions for the utility in the first parameter for the shell in the second parameter to stdout +/// # Panics +/// Panics if the utility map is empty fn gen_completions<T: uucore::Args>( args: impl Iterator<Item = OsString>, util_map: &UtilityMap<T>, @@ -183,6 +185,8 @@ fn gen_completions<T: uucore::Args>( } /// Generate the manpage for the utility in the first parameter +/// # Panics +/// Panics if the utility map is empty fn gen_manpage<T: uucore::Args>( args: impl Iterator<Item = OsString>, util_map: &UtilityMap<T>, @@ -215,6 +219,8 @@ fn gen_manpage<T: uucore::Args>( process::exit(0); } +/// # Panics +/// Panics if the utility map is empty fn gen_coreutils_app<T: uucore::Args>(util_map: &UtilityMap<T>) -> Command { let mut command = Command::new("coreutils"); for (name, (_, sub_app)) in util_map { diff --git a/src/bin/uudoc.rs b/src/bin/uudoc.rs index 77c7a2fcf..8ea11ed4d 100644 --- a/src/bin/uudoc.rs +++ b/src/bin/uudoc.rs @@ -13,6 +13,9 @@ use zip::ZipArchive; include!(concat!(env!("OUT_DIR"), "/uutils_map.rs")); +/// # Errors +/// Returns an error if the writer fails. +#[allow(clippy::too_many_lines)] fn main() -> io::Result<()> { let mut tldr_zip = File::open("docs/tldr.zip") .ok() @@ -170,6 +173,8 @@ struct MDWriter<'a, 'b> { } impl<'a, 'b> MDWriter<'a, 'b> { + /// # Errors + /// Returns an error if the writer fails. fn markdown(&mut self) -> io::Result<()> { write!(self.w, "# {}\n\n", self.name)?; self.additional()?; @@ -180,6 +185,8 @@ impl<'a, 'b> MDWriter<'a, 'b> { self.examples() } + /// # Errors + /// Returns an error if the writer fails. fn additional(&mut self) -> io::Result<()> { writeln!(self.w, "<div class=\"additional\">")?; self.platforms()?; @@ -187,6 +194,8 @@ impl<'a, 'b> MDWriter<'a, 'b> { writeln!(self.w, "</div>") } + /// # Errors + /// Returns an error if the writer fails. fn platforms(&mut self) -> io::Result<()> { writeln!(self.w, "<div class=\"platforms\">")?; for (feature, icon) in [ @@ -209,6 +218,10 @@ impl<'a, 'b> MDWriter<'a, 'b> { Ok(()) } + /// # Errors + /// Returns an error if the writer fails. + /// # Panics + /// Panics if the version is not found. fn version(&mut self) -> io::Result<()> { writeln!( self.w, @@ -217,6 +230,8 @@ impl<'a, 'b> MDWriter<'a, 'b> { ) } + /// # Errors + /// Returns an error if the writer fails. fn usage(&mut self) -> io::Result<()> { if let Some(markdown) = &self.markdown { let usage = uuhelp_parser::parse_usage(markdown); @@ -230,6 +245,8 @@ impl<'a, 'b> MDWriter<'a, 'b> { } } + /// # Errors + /// Returns an error if the writer fails. fn about(&mut self) -> io::Result<()> { if let Some(markdown) = &self.markdown { writeln!(self.w, "{}", uuhelp_parser::parse_about(markdown)) @@ -238,6 +255,8 @@ impl<'a, 'b> MDWriter<'a, 'b> { } } + /// # Errors + /// Returns an error if the writer fails. fn after_help(&mut self) -> io::Result<()> { if let Some(markdown) = &self.markdown { if let Some(after_help) = uuhelp_parser::parse_section("after help", markdown) { @@ -248,6 +267,8 @@ impl<'a, 'b> MDWriter<'a, 'b> { Ok(()) } + /// # Errors + /// Returns an error if the writer fails. fn examples(&mut self) -> io::Result<()> { if let Some(zip) = self.tldr_zip { let content = if let Some(f) = @@ -292,6 +313,8 @@ impl<'a, 'b> MDWriter<'a, 'b> { Ok(()) } + /// # Errors + /// Returns an error if the writer fails. fn options(&mut self) -> io::Result<()> { writeln!(self.w, "<h2>Options</h2>")?; write!(self.w, "<dl>")?; @@ -354,6 +377,8 @@ impl<'a, 'b> MDWriter<'a, 'b> { } } +/// # Panics +/// Panics if the archive is not ok fn get_zip_content(archive: &mut ZipArchive<impl Read + Seek>, name: &str) -> Option<String> { let mut s = String::new(); archive.by_name(name).ok()?.read_to_string(&mut s).unwrap(); diff --git a/src/uu/nohup/src/nohup.rs b/src/uu/nohup/src/nohup.rs index 486518342..60ad979bb 100644 --- a/src/uu/nohup/src/nohup.rs +++ b/src/uu/nohup/src/nohup.rs @@ -198,6 +198,8 @@ extern "C" { target_os = "freebsd", target_os = "openbsd" ))] +/// # Safety +/// This function is unsafe because it dereferences a raw pointer. unsafe fn _vprocmgr_detach_from_console(_: u32) -> *const libc::c_int { std::ptr::null() } diff --git a/src/uu/sync/src/sync.rs b/src/uu/sync/src/sync.rs index c0b8f3d00..a769f3488 100644 --- a/src/uu/sync/src/sync.rs +++ b/src/uu/sync/src/sync.rs @@ -36,6 +36,8 @@ mod platform { #[cfg(any(target_os = "linux", target_os = "android"))] use std::os::unix::io::AsRawFd; + /// # Safety + /// This function is unsafe because it calls `libc::sync` or `libc::syscall` which are unsafe. pub unsafe fn do_sync() -> isize { // see https://github.com/rust-lang/libc/pull/2161 #[cfg(target_os = "android")] @@ -46,6 +48,8 @@ mod platform { } #[cfg(any(target_os = "linux", target_os = "android"))] + /// # Safety + /// This function is unsafe because it calls `libc::syscall` which is unsafe. pub unsafe fn do_syncfs(files: Vec<String>) -> isize { for path in files { let f = File::open(path).unwrap(); @@ -56,6 +60,8 @@ mod platform { } #[cfg(any(target_os = "linux", target_os = "android"))] + /// # Safety + /// This function is unsafe because it calls `libc::syscall` which is unsafe. pub unsafe fn do_fdatasync(files: Vec<String>) -> isize { for path in files { let f = File::open(path).unwrap(); @@ -81,6 +87,8 @@ mod platform { }; use windows_sys::Win32::System::WindowsProgramming::DRIVE_FIXED; + /// # Safety + /// This function is unsafe because it calls an unsafe function. unsafe fn flush_volume(name: &str) { let name_wide = name.to_wide_null(); if GetDriveTypeW(name_wide.as_ptr()) == DRIVE_FIXED { @@ -99,6 +107,8 @@ mod platform { } } + /// # Safety + /// This function is unsafe because it calls an unsafe function. unsafe fn find_first_volume() -> (String, HANDLE) { let mut name: [u16; MAX_PATH as usize] = [0; MAX_PATH as usize]; let handle = FindFirstVolumeW(name.as_mut_ptr(), name.len() as u32); @@ -108,6 +118,8 @@ mod platform { (String::from_wide_null(&name), handle) } + /// # Safety + /// This function is unsafe because it calls an unsafe function. unsafe fn find_all_volumes() -> Vec<String> { let (first_volume, next_volume_handle) = find_first_volume(); let mut volumes = vec![first_volume]; @@ -127,6 +139,8 @@ mod platform { } } + /// # Safety + /// This function is unsafe because it calls `find_all_volumes` which is unsafe. pub unsafe fn do_sync() -> isize { let volumes = find_all_volumes(); for vol in &volumes { @@ -135,6 +149,8 @@ mod platform { 0 } + /// # Safety + /// This function is unsafe because it calls `find_all_volumes` which is unsafe. pub unsafe fn do_syncfs(files: Vec<String>) -> isize { for path in files { flush_volume( diff --git a/src/uucore/src/lib/features/entries.rs b/src/uucore/src/lib/features/entries.rs index e2cbafa59..d1c9f9c04 100644 --- a/src/uucore/src/lib/features/entries.rs +++ b/src/uucore/src/lib/features/entries.rs @@ -161,7 +161,9 @@ pub struct Passwd { pub expiration: time_t, } -/// SAFETY: ptr must point to a valid C string. +/// # Safety +/// ptr must point to a valid C string. +/// /// Returns None if ptr is null. unsafe fn cstr2string(ptr: *const c_char) -> Option<String> { if ptr.is_null() { @@ -172,7 +174,8 @@ unsafe fn cstr2string(ptr: *const c_char) -> Option<String> { } impl Passwd { - /// SAFETY: All the pointed-to strings must be valid and not change while + /// # Safety + /// All the pointed-to strings must be valid and not change while /// the function runs. That means PW_LOCK must be held. unsafe fn from_raw(raw: passwd) -> Self { Self { @@ -246,7 +249,8 @@ pub struct Group { } impl Group { - /// SAFETY: gr_name must be valid and not change while + /// # Safety + /// gr_name must be valid and not change while /// the function runs. That means PW_LOCK must be held. unsafe fn from_raw(raw: group) -> Self { Self { diff --git a/tests/by-util/test_cat.rs b/tests/by-util/test_cat.rs index b11c0ec0c..d67ecb0b1 100644 --- a/tests/by-util/test_cat.rs +++ b/tests/by-util/test_cat.rs @@ -485,7 +485,7 @@ fn test_dev_random() { } /// Reading from /dev/full should return an infinite amount of zero bytes. -/// Wikipedia says there is support on Linux, FreeBSD, and NetBSD. +/// Wikipedia says there is support on Linux, FreeBSD, and `NetBSD`. #[test] #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))] fn test_dev_full() { diff --git a/tests/by-util/test_chcon.rs b/tests/by-util/test_chcon.rs index a8dae9aed..1fd356e5b 100644 --- a/tests/by-util/test_chcon.rs +++ b/tests/by-util/test_chcon.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. // spell-checker:ignore (jargon) xattributes - +#![allow(clippy::missing_errors_doc, clippy::similar_names)] #![cfg(feature = "feat_selinux")] use std::ffi::CString; diff --git a/tests/by-util/test_chmod.rs b/tests/by-util/test_chmod.rs index 32bb7a966..167e79cd0 100644 --- a/tests/by-util/test_chmod.rs +++ b/tests/by-util/test_chmod.rs @@ -2,6 +2,7 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. + use crate::common::util::{AtPath, TestScenario, UCommand}; use std::fs::{metadata, set_permissions, OpenOptions, Permissions}; use std::os::unix::fs::{OpenOptionsExt, PermissionsExt}; @@ -32,12 +33,14 @@ fn make_file(file: &str, mode: u32) { fn run_single_test(test: &TestCase, at: &AtPath, mut ucmd: UCommand) { make_file(&at.plus_as_string(TEST_FILE), test.before); let perms = at.metadata(TEST_FILE).permissions().mode(); - if perms != test.before { - panic!( - "{}: expected: {:o} got: {:o}", - "setting permissions on test files before actual test run failed", test.after, perms - ); - } + + assert!( + perms == test.before, + "{}: expected: {:o} got: {:o}", + "setting permissions on test files before actual test run failed", + test.after, + perms + ); for arg in &test.args { ucmd.arg(arg); @@ -52,9 +55,13 @@ fn run_single_test(test: &TestCase, at: &AtPath, mut ucmd: UCommand) { } let perms = at.metadata(TEST_FILE).permissions().mode(); - if perms != test.after { - panic!("{}: expected: {:o} got: {:o}", ucmd, test.after, perms); - } + assert!( + perms == test.after, + "{}: expected: {:o} got: {:o}", + ucmd, + test.after, + perms + ); } fn run_tests(tests: Vec<TestCase>) { @@ -128,6 +135,7 @@ fn test_chmod_octal() { #[test] #[allow(clippy::unreadable_literal)] +#[allow(clippy::too_many_lines)] // spell-checker:disable-next-line fn test_chmod_ugoa() { let tests = vec![ diff --git a/tests/by-util/test_chown.rs b/tests/by-util/test_chown.rs index f893d2611..3fb6d69c3 100644 --- a/tests/by-util/test_chown.rs +++ b/tests/by-util/test_chown.rs @@ -29,9 +29,8 @@ fn skipping_test_is_okay(result: &CmdResult, needle: &str) -> bool { if is_ci() && result.stderr_str().contains(needle) { println!("test skipped:"); return true; - } else { - result.success(); } + result.success(); } false } diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index ca592f6fd..053e015f4 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -2328,9 +2328,9 @@ fn test_closes_file_descriptors() { #[cfg(any(target_os = "linux", target_os = "android"))] #[test] fn test_cp_sparse_never_empty() { + const BUFFER_SIZE: usize = 4096 * 4; let (at, mut ucmd) = at_and_ucmd!(); - const BUFFER_SIZE: usize = 4096 * 4; let buf: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE]; at.make_file("src_file1"); @@ -2348,10 +2348,10 @@ fn test_cp_sparse_never_empty() { #[cfg(any(target_os = "linux", target_os = "android"))] #[test] fn test_cp_sparse_always_empty() { + const BUFFER_SIZE: usize = 4096 * 4; for argument in ["--sparse=always", "--sparse=alway", "--sparse=al"] { let (at, mut ucmd) = at_and_ucmd!(); - const BUFFER_SIZE: usize = 4096 * 4; let buf: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE]; at.make_file("src_file1"); @@ -2368,9 +2368,9 @@ fn test_cp_sparse_always_empty() { #[cfg(any(target_os = "linux", target_os = "android"))] #[test] fn test_cp_sparse_always_non_empty() { + const BUFFER_SIZE: usize = 4096 * 16 + 3; let (at, mut ucmd) = at_and_ucmd!(); - const BUFFER_SIZE: usize = 4096 * 16 + 3; let mut buf: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE]; let blocks_to_touch = [buf.len() / 3, 2 * (buf.len() / 3)]; @@ -2438,12 +2438,11 @@ fn test_cp_sparse_never_reflink_always() { #[cfg(feature = "truncate")] #[test] fn test_cp_reflink_always_override() { - let scene = TestScenario::new(util_name!()); - const DISK: &str = "disk.img"; const ROOTDIR: &str = "disk_root/"; const USERDIR: &str = "dir/"; const MOUNTPOINT: &str = "mountpoint/"; + let scene = TestScenario::new(util_name!()); let src1_path: &str = &[MOUNTPOINT, USERDIR, "src1"].concat(); let src2_path: &str = &[MOUNTPOINT, USERDIR, "src2"].concat(); @@ -2575,12 +2574,12 @@ fn test_no_preserve_mode() { let umask: u16 = 0o022; ucmd.arg("file") .arg("dest") - .umask(umask as libc::mode_t) + .umask(libc::mode_t::from(umask)) .succeeds() .no_stderr() .no_stdout(); // remove sticky bit, setuid and setgid bit; apply umask - let expected_perms = PERMS_ALL & !0o7000 & !umask as u32; + let expected_perms = PERMS_ALL & !0o7000 & u32::from(!umask); assert_eq!( at.plus("dest").metadata().unwrap().mode() & 0o7777, expected_perms @@ -5507,16 +5506,17 @@ fn test_dir_perm_race_with_preserve_mode_and_ownership() { let start_time = std::time::Instant::now(); // wait for cp to create dirs loop { - if start_time.elapsed() >= timeout { - panic!("timed out: cp took too long to create destination directory") - } + assert!( + start_time.elapsed() < timeout, + "timed out: cp took too long to create destination directory" + ); if at.dir_exists(&format!("{}/{}", DEST_DIR, SRC_DIR)) { break; } std::thread::sleep(Duration::from_millis(100)); } let mode = at.metadata(&format!("{}/{}", DEST_DIR, SRC_DIR)).mode(); - #[allow(clippy::unnecessary_cast)] + #[allow(clippy::unnecessary_cast, clippy::cast_lossless)] let mask = if attr == "mode" { libc::S_IWGRP | libc::S_IWOTH } else { diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 13c340b07..aea8989b6 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -273,7 +273,7 @@ fn test_date_set_mac_unavailable() { #[test] #[cfg(all(unix, not(target_os = "macos")))] -/// TODO: expected to fail currently; change to succeeds() when required. +/// TODO: expected to fail currently; change to `succeeds()` when required. fn test_date_set_valid_2() { if geteuid() == 0 { let result = new_ucmd!() @@ -298,10 +298,11 @@ fn test_date_for_invalid_file() { #[test] #[cfg(unix)] fn test_date_for_no_permission_file() { - let (at, mut ucmd) = at_and_ucmd!(); + use std::os::unix::fs::PermissionsExt; const FILE: &str = "file-no-perm-1"; - use std::os::unix::fs::PermissionsExt; + let (at, mut ucmd) = at_and_ucmd!(); + let file = std::fs::OpenOptions::new() .create(true) .truncate(true) @@ -338,7 +339,7 @@ fn test_date_for_file() { #[test] #[cfg(all(unix, not(target_os = "macos")))] -/// TODO: expected to fail currently; change to succeeds() when required. +/// TODO: expected to fail currently; change to `succeeds()` when required. fn test_date_set_valid_3() { if geteuid() == 0 { let result = new_ucmd!() @@ -352,7 +353,7 @@ fn test_date_set_valid_3() { #[test] #[cfg(all(unix, not(target_os = "macos")))] -/// TODO: expected to fail currently; change to succeeds() when required. +/// TODO: expected to fail currently; change to `succeeds()` when required. fn test_date_set_valid_4() { if geteuid() == 0 { let result = new_ucmd!() @@ -422,8 +423,8 @@ fn test_date_overflow() { #[test] fn test_date_parse_from_format() { - let (at, mut ucmd) = at_and_ucmd!(); const FILE: &str = "file-with-dates"; + let (at, mut ucmd) = at_and_ucmd!(); at.write( FILE, diff --git a/tests/by-util/test_df.rs b/tests/by-util/test_df.rs index 996b8dc29..c67af5cba 100644 --- a/tests/by-util/test_df.rs +++ b/tests/by-util/test_df.rs @@ -3,6 +3,13 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. // spell-checker:ignore udev pcent iuse itotal iused ipcent +#![allow( + clippy::similar_names, + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + clippy::float_cmp +)] + use std::collections::HashSet; use crate::common::util::TestScenario; diff --git a/tests/by-util/test_dircolors.rs b/tests/by-util/test_dircolors.rs index d67368154..06d490c4a 100644 --- a/tests/by-util/test_dircolors.rs +++ b/tests/by-util/test_dircolors.rs @@ -200,15 +200,15 @@ TERM {term_pattern} .no_stderr(); } - let expectation_if_match = r#" + let expectation_if_match = r" LS_COLORS='*.term_matching=00;38;5;61:'; export LS_COLORS -"# +" .trim_start(); - let expectation_if_no_match = r#" + let expectation_if_no_match = r" LS_COLORS=''; export LS_COLORS -"# +" .trim_start(); // sanity checks diff --git a/tests/by-util/test_du.rs b/tests/by-util/test_du.rs index 4527b3d5a..c1cd356a1 100644 --- a/tests/by-util/test_du.rs +++ b/tests/by-util/test_du.rs @@ -545,7 +545,7 @@ fn test_du_h_flag_empty_file() { #[test] fn test_du_h_precision() { let test_cases = [ - (133456345, "128M"), + (133_456_345, "128M"), (12 * 1024 * 1024, "12M"), (8500, "8.4K"), ]; diff --git a/tests/by-util/test_env.rs b/tests/by-util/test_env.rs index f8c5c66d1..0a509c5d7 100644 --- a/tests/by-util/test_env.rs +++ b/tests/by-util/test_env.rs @@ -3,6 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. // spell-checker:ignore (words) bamf chdir rlimit prlimit COMSPEC cout cerr FFFD +#![allow(clippy::missing_errors_doc)] use crate::common::util::TestScenario; #[cfg(unix)] @@ -555,28 +556,28 @@ fn test_env_parsing_errors() { .stderr_is("env: invalid sequence '\\a' in -S\n"); ts.ucmd() - .arg(r#"-S\|\&\;"#) // no quotes, invalid escape sequence | + .arg(r"-S\|\&\;") // no quotes, invalid escape sequence | .fails() .code_is(125) .no_stdout() .stderr_is("env: invalid sequence '\\|' in -S\n"); ts.ucmd() - .arg(r#" |