diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 6f67eb828..695e5ad18 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -38,205 +38,15 @@ CI. However, you can use `#[cfg(...)]` attributes to create platform dependent f
VirtualBox and Parallels) for development:
-## Tools
+## Setting up your development environment
-We have an extensive CI that will check your code before it can be merged. This
-section explains how to run those checks locally to avoid waiting for the CI.
+To setup your local development environment for this project please follow [DEVELOPMENT.md guide](DEVELOPMENT.md)
-### pre-commit hooks
+It covers [installation of necessary tools and prerequisites](DEVELOPMENT.md#tools) as well as using those tools to [test your code changes locally](DEVELOPMENT.md#testing)
-A configuration for `pre-commit` is provided in the repository. It allows
-automatically checking every git commit you make to ensure it compiles, and
-passes `clippy` and `rustfmt` without warnings.
+## Improving the GNU compatibility
-To use the provided hook:
-
-1. [Install `pre-commit`](https://pre-commit.com/#install)
-1. Run `pre-commit install` while in the repository directory
-
-Your git commits will then automatically be checked. If a check fails, an error
-message will explain why, and your commit will be canceled. You can then make
-the suggested changes, and run `git commit ...` again.
-
-### clippy
-
-```shell
-cargo clippy --all-targets --all-features
-```
-
-The `msrv` key in the clippy configuration file `clippy.toml` is used to disable
-lints pertaining to newer features by specifying the minimum supported Rust
-version (MSRV).
-
-### rustfmt
-
-```shell
-cargo fmt --all
-```
-
-### cargo-deny
-
-This project uses [cargo-deny](https://github.com/EmbarkStudios/cargo-deny/) to
-detect duplicate dependencies, checks licenses, etc. To run it locally, first
-install it and then run with:
-
-```
-cargo deny --all-features check all
-```
-
-### Markdown linter
-
-We use [markdownlint](https://github.com/DavidAnson/markdownlint) to lint the
-Markdown files in the repository.
-
-### Spell checker
-
-We use `cspell` as spell checker for all files in the project. If you are using
-VS Code, you can install the
-[code spell checker](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker)
-extension to enable spell checking within your editor. Otherwise, you can
-install [cspell](https://cspell.org/) separately.
-
-If you want to make the spell checker ignore a word, you can add
-
-```rust
-// spell-checker:ignore word_to_ignore
-```
-
-at the top of the file.
-
-## Testing
-
-Testing can be done using either Cargo or `make`.
-
-### Testing with Cargo
-
-Just like with building, we follow the standard procedure for testing using
-Cargo:
-
-```shell
-cargo test
-```
-
-By default, `cargo test` only runs the common programs. To run also platform
-specific tests, run:
-
-```shell
-cargo test --features unix
-```
-
-If you would prefer to test a select few utilities:
-
-```shell
-cargo test --features "chmod mv tail" --no-default-features
-```
-
-If you also want to test the core utilities:
-
-```shell
-cargo test -p uucore -p coreutils
-```
-
-Or to test the pure Rust tests in the utility itself:
-
-```shell
-cargo test -p uu_ls --lib
-```
-
-Running the complete test suite might take a while. We use [nextest](https://nexte.st/index.html) in
-the CI and you might want to try it out locally. It can speed up the execution time of the whole
-test run significantly if the cpu has multiple cores.
-
-```shell
-cargo nextest run --features unix --no-fail-fast
-```
-
-To debug:
-
-```shell
-gdb --args target/debug/coreutils ls
-(gdb) b ls.rs:79
-(gdb) run
-```
-
-### Testing with GNU Make
-
-To simply test all available utilities:
-
-```shell
-make test
-```
-
-To test all but a few of the available utilities:
-
-```shell
-make SKIP_UTILS='UTILITY_1 UTILITY_2' test
-```
-
-To test only a few of the available utilities:
-
-```shell
-make UTILS='UTILITY_1 UTILITY_2' test
-```
-
-To include tests for unimplemented behavior:
-
-```shell
-make UTILS='UTILITY_1 UTILITY_2' SPEC=y test
-```
-
-To run tests with `nextest` just use the nextest target. Note you'll need to
-[install](https://nexte.st/book/installation.html) `nextest` first. The `nextest` target accepts the
-same arguments like the default `test` target, so it's possible to pass arguments to `nextest run`
-via `CARGOFLAGS`:
-
-```shell
-make CARGOFLAGS='--no-fail-fast' UTILS='UTILITY_1 UTILITY_2' nextest
-```
-
-### Run Busybox Tests
-
-This testing functionality is only available on *nix operating systems and
-requires `make`.
-
-To run busybox tests for all utilities for which busybox has tests
-
-```shell
-make busytest
-```
-
-To run busybox tests for a few of the available utilities
-
-```shell
-make UTILS='UTILITY_1 UTILITY_2' busytest
-```
-
-To pass an argument like "-v" to the busybox test runtime
-
-```shell
-make UTILS='UTILITY_1 UTILITY_2' RUNTEST_ARGS='-v' busytest
-```
-
-### Comparing with GNU
-
-To run uutils against the GNU test suite locally, run the following commands:
-
-```shell
-bash util/build-gnu.sh
-# Build uutils without release optimizations
-UU_MAKE_PROFILE=debug bash util/build-gnu.sh
-bash util/run-gnu-test.sh
-# To run a single test:
-bash util/run-gnu-test.sh tests/touch/not-owner.sh # for example
-# To run several tests:
-bash util/run-gnu-test.sh tests/touch/not-owner.sh tests/rm/no-give-up.sh # for example
-# If this is a perl (.pl) test, to run in debug:
-DEBUG=1 bash util/run-gnu-test.sh tests/misc/sm3sum.pl
-```
-
-Note that it relies on individual utilities (not the multicall binary).
-
-### Improving the GNU compatibility
+Please make sure you have installed [GNU utils and prerequisites](DEVELOPMENT.md#gnu-utils-and-prerequisites) and can execute commands described in [Comparing with GNU](DEVELOPMENT.md#comparing-with-gnu) section of [DEVELOPMENT.md](DEVELOPMENT.md)
The Python script `./util/remaining-gnu-error.py` shows the list of failing
tests in the CI.
@@ -326,30 +136,7 @@ gitignore: add temporary files
## Code coverage
-
-
-Code coverage report can be generated using [grcov](https://github.com/mozilla/grcov).
-
-### Using Nightly Rust
-
-To generate [gcov-based](https://github.com/mozilla/grcov#example-how-to-generate-gcda-files-for-a-rust-project) coverage report
-
-```shell
-export CARGO_INCREMENTAL=0
-export RUSTFLAGS="-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort"
-export RUSTDOCFLAGS="-Cpanic=abort"
-cargo build # e.g., --features feat_os_unix
-cargo test # e.g., --features feat_os_unix test_pathchk
-grcov . -s . --binary-path ./target/debug/ -t html --branch --ignore-not-existing --ignore build.rs --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?\#\[derive\()" -o ./target/debug/coverage/
-# open target/debug/coverage/index.html in browser
-```
-
-if changes are not reflected in the report then run `cargo clean` and run the above commands.
-
-### Using Stable Rust
-
-If you are using stable version of Rust that doesn't enable code coverage instrumentation by default
-then add `-Z-Zinstrument-coverage` flag to `RUSTFLAGS` env variable specified above.
+To generate code coverage report locally please follow [Code coverage report](DEVELOPMENT.md#code-coverage-report) section of [DEVELOPMENT.md](DEVELOPMENT.md)
## Other implementations
diff --git a/Cargo.lock b/Cargo.lock
index c00755198..33ee7ff14 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -205,9 +205,9 @@ checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
[[package]]
name = "bytecount"
-version = "0.6.3"
+version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c"
+checksum = "ad152d03a2c813c80bb94fedbf3a3f02b28f793e39e7c214c8a0bcc196343de7"
[[package]]
name = "byteorder"
@@ -1291,9 +1291,9 @@ checksum = "5486aed0026218e61b8a01d5fbd5a0a134649abb71a0e53b7bc088529dced86e"
[[package]]
name = "memmap2"
-version = "0.8.0"
+version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43a5a03cefb0d953ec0be133036f14e109412fa594edc2f77227249db66cc3ed"
+checksum = "deaba38d7abf1d4cca21cc89e932e542ba2b9258664d2a9ef0e61512039c9375"
dependencies = [
"libc",
]
@@ -1499,9 +1499,9 @@ dependencies = [
[[package]]
name = "parse_datetime"
-version = "0.4.0"
+version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fecceaede7767a9a98058687a321bc91742eff7670167a34104afb30fc8757df"
+checksum = "3bbf4e25b13841080e018a1e666358adfe5e39b6d353f986ca5091c210b586a1"
dependencies = [
"chrono",
"regex",
@@ -1740,9 +1740,9 @@ checksum = "f1bfbf25d7eb88ddcbb1ec3d755d0634da8f7657b2cb8b74089121409ab8228f"
[[package]]
name = "regex"
-version = "1.9.5"
+version = "1.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47"
+checksum = "ebee201405406dbf528b8b672104ae6d6d63e6d118cb10e4d51abbc7b58044ff"
dependencies = [
"aho-corasick",
"memchr",
@@ -1752,9 +1752,9 @@ dependencies = [
[[package]]
name = "regex-automata"
-version = "0.3.8"
+version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795"
+checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9"
dependencies = [
"aho-corasick",
"memchr",
diff --git a/Cargo.toml b/Cargo.toml
index a40d066d4..e7fc2851b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -261,7 +261,7 @@ test = ["uu_test"]
bigdecimal = "0.4"
binary-heap-plus = "0.5.0"
bstr = "1.6"
-bytecount = "0.6.3"
+bytecount = "0.6.4"
byteorder = "1.4.3"
chrono = { version = "^0.4.31", default-features = false, features = [
"std",
@@ -292,7 +292,7 @@ lscolors = { version = "0.15.0", default-features = false, features = [
"nu-ansi-term",
] }
memchr = "2"
-memmap2 = "0.8"
+memmap2 = "0.9"
nix = { version = "0.27", default-features = false }
nom = "7.1.3"
notify = { version = "=6.0.1", features = ["macos_kqueue"] }
@@ -301,7 +301,7 @@ num-traits = "0.2.16"
number_prefix = "0.4"
once_cell = "1.18.0"
onig = { version = "~6.4", default-features = false }
-parse_datetime = "0.4.0"
+parse_datetime = "0.5.0"
phf = "0.11.2"
phf_codegen = "0.11.2"
platform-info = "2.0.2"
@@ -310,7 +310,7 @@ rand = { version = "0.8", features = ["small_rng"] }
rand_core = "0.6"
rayon = "1.8"
redox_syscall = "0.4"
-regex = "1.9.5"
+regex = "1.9.6"
rstest = "0.18.2"
rust-ini = "0.19.0"
same-file = "1.0.6"
diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md
new file mode 100644
index 000000000..24a1bdeb5
--- /dev/null
+++ b/DEVELOPMENT.md
@@ -0,0 +1,329 @@
+
+
+# Setting up your local development environment
+
+For contributing rules and best practices please refer to [CONTRIBUTING.md](CONTRIBUTING.md)
+
+## Before you start
+
+For this guide we assume that you already have GitHub account and have `git` and your favorite code editor or IDE installed and configured.
+Before you start working on coreutils, please follow these steps:
+
+1. Fork the [coreutils repository](https://github.com/uutils/coreutils) to your GitHub account.
+***Tip:*** See [this GitHub guide](https://docs.github.com/en/get-started/quickstart/fork-a-repo) for more information on this step.
+2. Clone that fork to your local development environment:
+
+```shell
+git clone https://github.com/YOUR-GITHUB-ACCOUNT/coreutils
+cd coreutils
+```
+
+## Tools
+
+You will need the tools mentioned in this section to build and test your code changes locally.
+This section will explain how to install and configure these tools.
+We also have an extensive CI that uses these tools and will check your code before it can be merged.
+The next section [Testing](#testing) will explain how to run those checks locally to avoid waiting for the CI.
+
+### Rust toolchain
+
+[Install Rust](https://www.rust-lang.org/tools/install)
+
+If you're using rustup to install and manage your Rust toolchains, `clippy` and `rustfmt` are usually already installed. If you are using one of the alternative methods, please make sure to install them manually. See following sub-sections for their usage: [clippy](#clippy) [rustfmt](#rustfmt).
+
+***Tip*** You might also need to add 'llvm-tools' component if you are going to [generate code coverage reports locally](#code-coverage-report):
+
+```shell
+rustup component add llvm-tools-preview
+```
+
+### GNU utils and prerequisites
+
+If you are developing on Linux, most likely you already have all/most GNU utilities and prerequisites installed.
+
+To make sure, please check GNU coreutils [README-prereq](https://github.com/coreutils/coreutils/blob/master/README-prereq).
+
+You will need these to [run uutils against the GNU test suite locally](#comparing-with-gnu).
+
+For MacOS and Windows platform specific setup please check [MacOS GNU utils](#macos-gnu-utils) and [Windows GNU utils](#windows-gnu-utils) sections respectfully.
+
+### pre-commit hooks
+
+A configuration for `pre-commit` is provided in the repository. It allows
+automatically checking every git commit you make to ensure it compiles, and
+passes `clippy` and `rustfmt` without warnings.
+
+To use the provided hook:
+
+1. [Install `pre-commit`](https://pre-commit.com/#install)
+1. Run `pre-commit install` while in the repository directory
+
+Your git commits will then automatically be checked. If a check fails, an error
+message will explain why, and your commit will be canceled. You can then make
+the suggested changes, and run `git commit ...` again.
+
+**NOTE: On MacOS** the pre-commit hooks are currently broken. There are workarounds involving switching to unstable nightly Rust and components.
+
+### clippy
+
+```shell
+cargo clippy --all-targets --all-features
+```
+
+The `msrv` key in the clippy configuration file `clippy.toml` is used to disable
+lints pertaining to newer features by specifying the minimum supported Rust
+version (MSRV).
+
+### rustfmt
+
+```shell
+cargo fmt --all
+```
+
+### cargo-deny
+
+This project uses [cargo-deny](https://github.com/EmbarkStudios/cargo-deny/) to
+detect duplicate dependencies, checks licenses, etc. To run it locally, first
+install it and then run with:
+
+```shell
+cargo deny --all-features check all
+```
+
+### Markdown linter
+
+We use [markdownlint](https://github.com/DavidAnson/markdownlint) to lint the
+Markdown files in the repository.
+
+### Spell checker
+
+We use `cspell` as spell checker for all files in the project. If you are using
+VS Code, you can install the
+[code spell checker](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker)
+extension to enable spell checking within your editor. Otherwise, you can
+install [cspell](https://cspell.org/) separately.
+
+If you want to make the spell checker ignore a word, you can add
+
+```rust
+// spell-checker:ignore word_to_ignore
+```
+
+at the top of the file.
+
+## Testing
+
+This section explains how to run our CI checks locally.
+Testing can be done using either Cargo or `make`.
+
+### Testing with Cargo
+
+Just like with building, we follow the standard procedure for testing using
+Cargo:
+
+```shell
+cargo test
+```
+
+By default, `cargo test` only runs the common programs. To run also platform
+specific tests, run:
+
+```shell
+cargo test --features unix
+```
+
+If you would prefer to test a select few utilities:
+
+```shell
+cargo test --features "chmod mv tail" --no-default-features
+```
+
+If you also want to test the core utilities:
+
+```shell
+cargo test -p uucore -p coreutils
+```
+
+Running the complete test suite might take a while. We use [nextest](https://nexte.st/index.html) in
+the CI and you might want to try it out locally. It can speed up the execution time of the whole
+test run significantly if the cpu has multiple cores.
+
+```shell
+cargo nextest run --features unix --no-fail-fast
+```
+
+To debug:
+
+```shell
+gdb --args target/debug/coreutils ls
+(gdb) b ls.rs:79
+(gdb) run
+```
+
+### Testing with GNU Make
+
+To simply test all available utilities:
+
+```shell
+make test
+```
+
+To test all but a few of the available utilities:
+
+```shell
+make SKIP_UTILS='UTILITY_1 UTILITY_2' test
+```
+
+To test only a few of the available utilities:
+
+```shell
+make UTILS='UTILITY_1 UTILITY_2' test
+```
+
+To include tests for unimplemented behavior:
+
+```shell
+make UTILS='UTILITY_1 UTILITY_2' SPEC=y test
+```
+
+To run tests with `nextest` just use the nextest target. Note you'll need to
+[install](https://nexte.st/book/installation.html) `nextest` first. The `nextest` target accepts the
+same arguments like the default `test` target, so it's possible to pass arguments to `nextest run`
+via `CARGOFLAGS`:
+
+```shell
+make CARGOFLAGS='--no-fail-fast' UTILS='UTILITY_1 UTILITY_2' nextest
+```
+
+### Run Busybox Tests
+
+This testing functionality is only available on *nix operating systems and
+requires `make`.
+
+To run busybox tests for all utilities for which busybox has tests
+
+```shell
+make busytest
+```
+
+To run busybox tests for a few of the available utilities
+
+```shell
+make UTILS='UTILITY_1 UTILITY_2' busytest
+```
+
+To pass an argument like "-v" to the busybox test runtime
+
+```shell
+make UTILS='UTILITY_1 UTILITY_2' RUNTEST_ARGS='-v' busytest
+```
+
+### Comparing with GNU
+
+To run uutils against the GNU test suite locally, run the following commands:
+
+```shell
+bash util/build-gnu.sh
+# Build uutils without release optimizations
+UU_MAKE_PROFILE=debug bash util/build-gnu.sh
+bash util/run-gnu-test.sh
+# To run a single test:
+bash util/run-gnu-test.sh tests/touch/not-owner.sh # for example
+# To run several tests:
+bash util/run-gnu-test.sh tests/touch/not-owner.sh tests/rm/no-give-up.sh # for example
+# If this is a perl (.pl) test, to run in debug:
+DEBUG=1 bash util/run-gnu-test.sh tests/misc/sm3sum.pl
+```
+
+***Tip:*** First time you run `bash util/build-gnu.sh` command, it will provide instructions on how to checkout GNU coreutils repository at the correct release tag. Please follow those instructions and when done, run `bash util/build-gnu.sh` command again.
+
+Note that GNU test suite relies on individual utilities (not the multicall binary).
+
+## Code coverage report
+
+Code coverage report can be generated using [grcov](https://github.com/mozilla/grcov).
+
+### Using Nightly Rust
+
+To generate [gcov-based](https://github.com/mozilla/grcov#example-how-to-generate-gcda-files-for-a-rust-project) coverage report
+
+```shell
+export CARGO_INCREMENTAL=0
+export RUSTFLAGS="-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort"
+export RUSTDOCFLAGS="-Cpanic=abort"
+cargo build # e.g., --features feat_os_unix
+cargo test # e.g., --features feat_os_unix test_pathchk
+grcov . -s . --binary-path ./target/debug/ -t html --branch --ignore-not-existing --ignore build.rs --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?\#\[derive\()" -o ./target/debug/coverage/
+# open target/debug/coverage/index.html in browser
+```
+
+if changes are not reflected in the report then run `cargo clean` and run the above commands.
+
+### Using Stable Rust
+
+If you are using stable version of Rust that doesn't enable code coverage instrumentation by default
+then add `-Z-Zinstrument-coverage` flag to `RUSTFLAGS` env variable specified above.
+
+## Tips for setting up on Mac
+
+### C Compiler and linker
+
+On MacOS you'll need to install C compiler & linker:
+
+```shell
+xcode-select --install
+```
+
+### MacOS GNU utils
+
+On MacOS you will need to install [Homebrew](https://docs.brew.sh/Installation) and use it to install the following Homebrew formulas:
+
+```shell
+brew install \
+ coreutils \
+ autoconf \
+ gettext \
+ wget \
+ texinfo \
+ xz \
+ automake \
+ gnu-sed \
+ m4 \
+ bison \
+ pre-commit \
+ findutils
+```
+
+After installing these Homebrew formulas, please make sure to add the following lines to your `zsh` or `bash` rc file, i.e. `~/.profile` or `~/.zshrc` or `~/.bashrc` ...
+(assuming Homebrew is installed at default location `/opt/homebrew`):
+
+```shell
+eval "$(/opt/homebrew/bin/brew shellenv)"
+export PATH="/opt/homebrew/opt/coreutils/libexec/gnubin:$PATH"
+export PATH="/opt/homebrew/opt/bison/bin:$PATH"
+export PATH="/opt/homebrew/opt/findutils/libexec/gnubin:$PATH"
+```
+
+Last step is to link Homebrew coreutils version of `timeout` to `/usr/local/bin` (as admin user):
+
+```shell
+sudo ln -s /opt/homebrew/bin/timeout /usr/local/bin/timeout
+```
+
+Do not forget to either source updated rc file or restart you terminal session to update environment variables.
+
+## Tips for setting up on Windows
+
+### MSVC build tools
+
+On Windows you'll need the MSVC build tools for Visual Studio 2013 or later.
+
+If you are using `rustup-init.exe` to install Rust toolchain, it will guide you through the process of downloading and installing these prerequisites.
+
+Otherwise please follow [this guide](https://learn.microsoft.com/en-us/windows/dev-environment/rust/setup).
+
+### Windows GNU utils
+
+If you have used [Git for Windows](https://gitforwindows.org) to install `git` on you Windows system you might already have some GNU core utilities installed as part of "GNU Bash" included in Git for Windows package, but it is not a complete package. [This article](https://gist.github.com/evanwill/0207876c3243bbb6863e65ec5dc3f058) provides instruction on how to add more to it.
+
+Alternatively you can install [Cygwin](https://www.cygwin.com) and/or use [WSL2](https://learn.microsoft.com/en-us/windows/wsl/compare-versions#whats-new-in-wsl-2) to get access to all GNU core utilities on Windows.
diff --git a/fuzz/fuzz_targets/fuzz_common.rs b/fuzz/fuzz_targets/fuzz_common.rs
index fb1f498e9..a94963ef0 100644
--- a/fuzz/fuzz_targets/fuzz_common.rs
+++ b/fuzz/fuzz_targets/fuzz_common.rs
@@ -3,6 +3,9 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
+use libc::{dup, dup2, STDOUT_FILENO};
+use std::ffi::OsString;
+use std::io;
use std::process::Command;
use std::sync::atomic::Ordering;
use std::sync::{atomic::AtomicBool, Once};
@@ -28,3 +31,77 @@ pub fn is_gnu_cmd(cmd_path: &str) -> Result<(), std::io::Error> {
panic!("Not the GNU implementation");
}
}
+
+pub fn generate_and_run_uumain(args: &[OsString], uumain_function: F) -> (String, i32)
+where
+ F: FnOnce(std::vec::IntoIter) -> i32,
+{
+ let uumain_exit_status;
+
+ let original_stdout_fd = unsafe { dup(STDOUT_FILENO) };
+ println!("Running test {:?}", &args[1..]);
+ let mut pipe_fds = [-1; 2];
+ unsafe { libc::pipe(pipe_fds.as_mut_ptr()) };
+
+ {
+ unsafe { dup2(pipe_fds[1], STDOUT_FILENO) };
+ uumain_exit_status = uumain_function(args.to_owned().into_iter());
+ unsafe { dup2(original_stdout_fd, STDOUT_FILENO) };
+ unsafe { libc::close(original_stdout_fd) };
+ }
+ unsafe { libc::close(pipe_fds[1]) };
+
+ let mut captured_output = Vec::new();
+ let mut read_buffer = [0; 1024];
+ loop {
+ let bytes_read = unsafe {
+ libc::read(
+ pipe_fds[0],
+ read_buffer.as_mut_ptr() as *mut libc::c_void,
+ read_buffer.len(),
+ )
+ };
+ if bytes_read <= 0 {
+ break;
+ }
+ captured_output.extend_from_slice(&read_buffer[..bytes_read as usize]);
+ }
+
+ unsafe { libc::close(pipe_fds[0]) };
+
+ let my_output = String::from_utf8_lossy(&captured_output)
+ .to_string()
+ .trim()
+ .to_owned();
+
+ (my_output, uumain_exit_status)
+}
+
+pub fn run_gnu_cmd(
+ cmd_path: &str,
+ args: &[OsString],
+ check_gnu: bool,
+) -> Result<(String, i32), io::Error> {
+ if check_gnu {
+ is_gnu_cmd(cmd_path)?; // Check if it's a GNU implementation
+ }
+
+ let mut command = Command::new(cmd_path);
+ for arg in args {
+ command.arg(arg);
+ }
+
+ let output = command.output()?;
+ let exit_code = output.status.code().unwrap_or(-1);
+ if output.status.success() || !check_gnu {
+ Ok((
+ String::from_utf8_lossy(&output.stdout).to_string(),
+ exit_code,
+ ))
+ } else {
+ Err(io::Error::new(
+ io::ErrorKind::Other,
+ format!("GNU command execution failed with exit code {}", exit_code),
+ ))
+ }
+}
diff --git a/fuzz/fuzz_targets/fuzz_date.rs b/fuzz/fuzz_targets/fuzz_date.rs
index 96c56cc6b..0f9cb262c 100644
--- a/fuzz/fuzz_targets/fuzz_date.rs
+++ b/fuzz/fuzz_targets/fuzz_date.rs
@@ -9,6 +9,6 @@ fuzz_target!(|data: &[u8]| {
let args = data
.split(|b| *b == delim)
.filter_map(|e| std::str::from_utf8(e).ok())
- .map(|e| OsString::from(e));
+ .map(OsString::from);
uumain(args);
});
diff --git a/fuzz/fuzz_targets/fuzz_expr.rs b/fuzz/fuzz_targets/fuzz_expr.rs
index e364342b8..fb7b17309 100644
--- a/fuzz/fuzz_targets/fuzz_expr.rs
+++ b/fuzz/fuzz_targets/fuzz_expr.rs
@@ -12,35 +12,11 @@ use rand::seq::SliceRandom;
use rand::Rng;
use std::ffi::OsString;
-use libc::{dup, dup2, STDOUT_FILENO};
-use std::process::Command;
mod fuzz_common;
-use crate::fuzz_common::is_gnu_cmd;
+use crate::fuzz_common::{generate_and_run_uumain, run_gnu_cmd};
static CMD_PATH: &str = "expr";
-fn run_gnu_expr(args: &[OsString]) -> Result<(String, i32), std::io::Error> {
- is_gnu_cmd(CMD_PATH)?; // Check if it's a GNU implementation
-
- let mut command = Command::new(CMD_PATH);
- for arg in args {
- command.arg(arg);
- }
- let output = command.output()?;
- let exit_code = output.status.code().unwrap_or(-1);
- if output.status.success() {
- Ok((
- String::from_utf8_lossy(&output.stdout).to_string(),
- exit_code,
- ))
- } else {
- Err(std::io::Error::new(
- std::io::ErrorKind::Other,
- format!("GNU expr execution failed with exit code {}", exit_code),
- ))
- }
-}
-
fn generate_random_string(max_length: usize) -> String {
let mut rng = rand::thread_rng();
let valid_utf8: Vec = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
@@ -108,55 +84,10 @@ fuzz_target!(|_data: &[u8]| {
let mut args = vec![OsString::from("expr")];
args.extend(expr.split_whitespace().map(OsString::from));
- // Save the original stdout file descriptor
- let original_stdout_fd = unsafe { dup(STDOUT_FILENO) };
-
- // Create a pipe to capture stdout
- let mut pipe_fds = [-1; 2];
- unsafe { libc::pipe(pipe_fds.as_mut_ptr()) };
- let uumain_exit_code;
- {
- // Redirect stdout to the write end of the pipe
- unsafe { dup2(pipe_fds[1], STDOUT_FILENO) };
-
- // Run uumain with the provided arguments
- uumain_exit_code = uumain(args.clone().into_iter());
-
- // Restore original stdout
- unsafe { dup2(original_stdout_fd, STDOUT_FILENO) };
- unsafe { libc::close(original_stdout_fd) };
- }
- // Close the write end of the pipe
- unsafe { libc::close(pipe_fds[1]) };
-
- // Read captured output from the read end of the pipe
- let mut captured_output = Vec::new();
- let mut read_buffer = [0; 1024];
- loop {
- let bytes_read = unsafe {
- libc::read(
- pipe_fds[0],
- read_buffer.as_mut_ptr() as *mut libc::c_void,
- read_buffer.len(),
- )
- };
- if bytes_read <= 0 {
- break;
- }
- captured_output.extend_from_slice(&read_buffer[..bytes_read as usize]);
- }
-
- // Close the read end of the pipe
- unsafe { libc::close(pipe_fds[0]) };
-
- // Convert captured output to a string
- let rust_output = String::from_utf8_lossy(&captured_output)
- .to_string()
- .trim()
- .to_owned();
+ let (rust_output, uumain_exit_code) = generate_and_run_uumain(&args, uumain);
// Run GNU expr with the provided arguments and compare the output
- match run_gnu_expr(&args[1..]) {
+ match run_gnu_cmd(CMD_PATH, &args[1..], true) {
Ok((gnu_output, gnu_exit_code)) => {
let gnu_output = gnu_output.trim().to_owned();
if uumain_exit_code != gnu_exit_code {
@@ -165,16 +96,16 @@ fuzz_target!(|_data: &[u8]| {
println!("GNU code: {}", gnu_exit_code);
panic!("Different error codes");
}
- if rust_output != gnu_output {
- println!("Expression: {}", expr);
- println!("Rust output: {}", rust_output);
- println!("GNU output: {}", gnu_output);
- panic!("Different output between Rust & GNU");
- } else {
+ if rust_output == gnu_output {
println!(
"Outputs matched for expression: {} => Result: {}",
expr, rust_output
);
+ } else {
+ println!("Expression: {}", expr);
+ println!("Rust output: {}", rust_output);
+ println!("GNU output: {}", gnu_output);
+ panic!("Different output between Rust & GNU");
}
}
Err(_) => {
diff --git a/fuzz/fuzz_targets/fuzz_parse_glob.rs b/fuzz/fuzz_targets/fuzz_parse_glob.rs
index 061569bc4..e235c0c9d 100644
--- a/fuzz/fuzz_targets/fuzz_parse_glob.rs
+++ b/fuzz/fuzz_targets/fuzz_parse_glob.rs
@@ -5,6 +5,6 @@ use uucore::parse_glob;
fuzz_target!(|data: &[u8]| {
if let Ok(s) = std::str::from_utf8(data) {
- _ = parse_glob::from_str(s)
+ _ = parse_glob::from_str(s);
}
});
diff --git a/fuzz/fuzz_targets/fuzz_test.rs b/fuzz/fuzz_targets/fuzz_test.rs
index 537e21abd..4805a41af 100644
--- a/fuzz/fuzz_targets/fuzz_test.rs
+++ b/fuzz/fuzz_targets/fuzz_test.rs
@@ -12,8 +12,8 @@ use rand::seq::SliceRandom;
use rand::Rng;
use std::ffi::OsString;
-use libc::{dup, dup2, STDOUT_FILENO};
-use std::process::Command;
+mod fuzz_common;
+use crate::fuzz_common::{generate_and_run_uumain, run_gnu_cmd};
#[derive(PartialEq, Debug, Clone)]
enum ArgType {
@@ -26,18 +26,7 @@ enum ArgType {
// Add any other types as needed
}
-fn run_gnu_test(args: &[OsString]) -> Result<(String, i32), std::io::Error> {
- let mut command = Command::new("test");
- for arg in args {
- command.arg(arg);
- }
- let output = command.output()?;
- let exit_status = output.status.code().unwrap_or(-1); // Capture the exit status code
- Ok((
- String::from_utf8_lossy(&output.stdout).to_string(),
- exit_status,
- ))
-}
+static CMD_PATH: &str = "test";
fn generate_random_string(max_length: usize) -> String {
let mut rng = rand::thread_rng();
@@ -153,7 +142,7 @@ fn generate_test_arg() -> String {
0 => {
arg.push_str(&rng.gen_range(-100..=100).to_string());
}
- 1 | 2 | 3 => {
+ 1..=3 => {
let test_arg = test_args
.choose(&mut rng)
.expect("Failed to choose a random test argument");
@@ -210,69 +199,23 @@ fuzz_target!(|_data: &[u8]| {
let mut rng = rand::thread_rng();
let max_args = rng.gen_range(1..=6);
let mut args = vec![OsString::from("test")];
- let uumain_exit_status;
for _ in 0..max_args {
args.push(OsString::from(generate_test_arg()));
}
- // Save the original stdout file descriptor
- let original_stdout_fd = unsafe { dup(STDOUT_FILENO) };
- println!("Running test {:?}", &args[1..]);
- // Create a pipe to capture stdout
- let mut pipe_fds = [-1; 2];
- unsafe { libc::pipe(pipe_fds.as_mut_ptr()) };
-
- {
- // Redirect stdout to the write end of the pipe
- unsafe { dup2(pipe_fds[1], STDOUT_FILENO) };
-
- // Run uumain with the provided arguments
- uumain_exit_status = uumain(args.clone().into_iter());
-
- // Restore original stdout
- unsafe { dup2(original_stdout_fd, STDOUT_FILENO) };
- unsafe { libc::close(original_stdout_fd) };
- }
- // Close the write end of the pipe
- unsafe { libc::close(pipe_fds[1]) };
-
- // Read captured output from the read end of the pipe
- let mut captured_output = Vec::new();
- let mut read_buffer = [0; 1024];
- loop {
- let bytes_read = unsafe {
- libc::read(
- pipe_fds[0],
- read_buffer.as_mut_ptr() as *mut libc::c_void,
- read_buffer.len(),
- )
- };
- if bytes_read <= 0 {
- break;
- }
- captured_output.extend_from_slice(&read_buffer[..bytes_read as usize]);
- }
-
- // Close the read end of the pipe
- unsafe { libc::close(pipe_fds[0]) };
-
- // Convert captured output to a string
- let my_output = String::from_utf8_lossy(&captured_output)
- .to_string()
- .trim()
- .to_owned();
+ let (rust_output, uumain_exit_status) = generate_and_run_uumain(&args, uumain);
// Run GNU test with the provided arguments and compare the output
- match run_gnu_test(&args[1..]) {
+ match run_gnu_cmd(CMD_PATH, &args[1..], false) {
Ok((gnu_output, gnu_exit_status)) => {
let gnu_output = gnu_output.trim().to_owned();
println!("gnu_exit_status {}", gnu_exit_status);
println!("uumain_exit_status {}", uumain_exit_status);
- if my_output != gnu_output || uumain_exit_status != gnu_exit_status {
+ if rust_output != gnu_output || uumain_exit_status != gnu_exit_status {
println!("Discrepancy detected!");
println!("Test: {:?}", &args[1..]);
- println!("My output: {}", my_output);
+ println!("My output: {}", rust_output);
println!("GNU output: {}", gnu_output);
println!("My exit status: {}", uumain_exit_status);
println!("GNU exit status: {}", gnu_exit_status);
diff --git a/src/uu/chmod/src/chmod.rs b/src/uu/chmod/src/chmod.rs
index b007bb1d7..31663b1af 100644
--- a/src/uu/chmod/src/chmod.rs
+++ b/src/uu/chmod/src/chmod.rs
@@ -335,9 +335,7 @@ impl Chmoder {
let mut new_mode = fperm;
let mut naively_expected_new_mode = new_mode;
for mode in cmode_unwrapped.split(',') {
- // cmode is guaranteed to be Some in this case
- let arr: &[char] = &['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
- let result = if mode.contains(arr) {
+ let result = if mode.chars().any(|c| c.is_ascii_digit()) {
mode::parse_numeric(new_mode, mode, file.is_dir()).map(|v| (v, v))
} else {
mode::parse_symbolic(new_mode, mode, get_umask(), file.is_dir()).map(|m| {
@@ -352,20 +350,22 @@ impl Chmoder {
(m, naive_mode)
})
};
+
match result {
Ok((mode, naive_mode)) => {
new_mode = mode;
naively_expected_new_mode = naive_mode;
}
Err(f) => {
- if self.quiet {
- return Err(ExitCode::new(1));
+ return if self.quiet {
+ Err(ExitCode::new(1))
} else {
- return Err(USimpleError::new(1, f));
- }
+ Err(USimpleError::new(1, f))
+ };
}
}
}
+
self.change_file(fperm, new_mode, file)?;
// if a permission would have been removed if umask was 0, but it wasn't because umask was not 0, print an error and fail
if (new_mode & !naively_expected_new_mode) != 0 {
diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs
index e0a7984f2..c45436b8d 100644
--- a/src/uu/cp/src/cp.rs
+++ b/src/uu/cp/src/cp.rs
@@ -1289,23 +1289,16 @@ fn copy_source(
}
impl OverwriteMode {
- fn verify(&self, path: &Path, verbose: bool) -> CopyResult<()> {
+ fn verify(&self, path: &Path) -> CopyResult<()> {
match *self {
Self::NoClobber => {
- if verbose {
- println!("skipped {}", path.quote());
- } else {
- eprintln!("{}: not replacing {}", util_name(), path.quote());
- }
+ eprintln!("{}: not replacing {}", util_name(), path.quote());
Err(Error::NotAllFilesCopied)
}
Self::Interactive(_) => {
if prompt_yes!("overwrite {}?", path.quote()) {
Ok(())
} else {
- if verbose {
- println!("skipped {}", path.quote());
- }
Err(Error::Skipped)
}
}
@@ -1513,7 +1506,7 @@ fn handle_existing_dest(
return Err(format!("{} and {} are the same file", source.quote(), dest.quote()).into());
}
- options.overwrite.verify(dest, options.verbose)?;
+ options.overwrite.verify(dest)?;
let backup_path = backup_control::get_backup_path(options.backup, dest, &options.backup_suffix);
if let Some(backup_path) = backup_path {
@@ -1926,7 +1919,7 @@ fn copy_helper(
File::create(dest).context(dest.display().to_string())?;
} else if source_is_fifo && options.recursive && !options.copy_contents {
#[cfg(unix)]
- copy_fifo(dest, options.overwrite, options.verbose)?;
+ copy_fifo(dest, options.overwrite)?;
} else if source_is_symlink {
copy_link(source, dest, symlinked_files)?;
} else {
@@ -1951,9 +1944,9 @@ fn copy_helper(
// "Copies" a FIFO by creating a new one. This workaround is because Rust's
// built-in fs::copy does not handle FIFOs (see rust-lang/rust/issues/79390).
#[cfg(unix)]
-fn copy_fifo(dest: &Path, overwrite: OverwriteMode, verbose: bool) -> CopyResult<()> {
+fn copy_fifo(dest: &Path, overwrite: OverwriteMode) -> CopyResult<()> {
if dest.exists() {
- overwrite.verify(dest, verbose)?;
+ overwrite.verify(dest)?;
fs::remove_file(dest)?;
}
diff --git a/src/uu/cp/src/platform/macos.rs b/src/uu/cp/src/platform/macos.rs
index 8c62c78d9..77bdbbbdb 100644
--- a/src/uu/cp/src/platform/macos.rs
+++ b/src/uu/cp/src/platform/macos.rs
@@ -63,8 +63,15 @@ pub(crate) fn copy_on_write(
{
// clonefile(2) fails if the destination exists. Remove it and try again. Do not
// bother to check if removal worked because we're going to try to clone again.
- let _ = fs::remove_file(dest);
- error = pfn(src.as_ptr(), dst.as_ptr(), 0);
+ // first lets make sure the dest file is not read only
+ if fs::metadata(dest).map_or(false, |md| !md.permissions().readonly()) {
+ // remove and copy again
+ // TODO: rewrite this to better match linux behavior
+ // linux first opens the source file and destination file then uses the file
+ // descriptors to do the clone.
+ let _ = fs::remove_file(dest);
+ error = pfn(src.as_ptr(), dst.as_ptr(), 0);
+ }
}
}
}
diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs
index 745fd5423..b5ab8993a 100644
--- a/src/uu/date/src/date.rs
+++ b/src/uu/date/src/date.rs
@@ -166,7 +166,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
};
let date_source = if let Some(date) = matches.get_one::(OPT_DATE) {
- if let Ok(duration) = parse_datetime::from_str(date.as_str()) {
+ let ref_time = Local::now();
+ if let Ok(new_time) = parse_datetime::parse_datetime_at_date(ref_time, date.as_str()) {
+ let duration = new_time.signed_duration_since(ref_time);
DateSource::Human(duration)
} else {
DateSource::Custom(date.into())
diff --git a/src/uu/echo/src/echo.rs b/src/uu/echo/src/echo.rs
index cd9467714..b3707b6f8 100644
--- a/src/uu/echo/src/echo.rs
+++ b/src/uu/echo/src/echo.rs
@@ -6,6 +6,7 @@
use clap::{crate_version, Arg, ArgAction, Command};
use std::io::{self, Write};
use std::iter::Peekable;
+use std::ops::ControlFlow;
use std::str::Chars;
use uucore::error::{FromIo, UResult};
use uucore::{format_usage, help_about, help_section, help_usage};
@@ -21,73 +22,98 @@ mod options {
pub const DISABLE_BACKSLASH_ESCAPE: &str = "disable_backslash_escape";
}
-fn parse_code(
- input: &mut Peekable,
- base: u32,
- max_digits: u32,
- bits_per_digit: u32,
-) -> Option {
- let mut ret = 0x8000_0000;
- for _ in 0..max_digits {
- match input.peek().and_then(|c| c.to_digit(base)) {
- Some(n) => ret = (ret << bits_per_digit) | n,
- None => break,
- }
- input.next();
- }
- std::char::from_u32(ret)
+#[repr(u8)]
+#[derive(Clone, Copy)]
+enum Base {
+ Oct = 8,
+ Hex = 16,
}
-fn print_escaped(input: &str, mut output: impl Write) -> io::Result {
- let mut should_stop = false;
+impl Base {
+ fn max_digits(&self) -> u8 {
+ match self {
+ Self::Oct => 3,
+ Self::Hex => 2,
+ }
+ }
+}
- let mut buffer = ['\\'; 2];
+/// Parse the numeric part of the `\xHHH` and `\0NNN` escape sequences
+fn parse_code(input: &mut Peekable, base: Base) -> Option {
+ // All arithmetic on `ret` needs to be wrapping, because octal input can
+ // take 3 digits, which is 9 bits, and therefore more than what fits in a
+ // `u8`. GNU just seems to wrap these values.
+ // Note that if we instead make `ret` a `u32` and use `char::from_u32` will
+ // yield incorrect results because it will interpret values larger than
+ // `u8::MAX` as unicode.
+ let mut ret = input.peek().and_then(|c| c.to_digit(base as u32))? as u8;
- // TODO `cargo +nightly clippy` complains that `.peek()` is never
- // called on `iter`. However, `peek()` is called inside the
- // `parse_code()` function that borrows `iter`.
+ // We can safely ignore the None case because we just peeked it.
+ let _ = input.next();
+
+ for _ in 1..base.max_digits() {
+ match input.peek().and_then(|c| c.to_digit(base as u32)) {
+ Some(n) => ret = ret.wrapping_mul(base as u8).wrapping_add(n as u8),
+ None => break,
+ }
+ // We can safely ignore the None case because we just peeked it.
+ let _ = input.next();
+ }
+
+ Some(ret.into())
+}
+
+fn print_escaped(input: &str, mut output: impl Write) -> io::Result> {
let mut iter = input.chars().peekable();
- while let Some(mut c) = iter.next() {
- let mut start = 1;
+ while let Some(c) = iter.next() {
+ if c != '\\' {
+ write!(output, "{c}")?;
+ continue;
+ }
- if c == '\\' {
- if let Some(next) = iter.next() {
- c = match next {
- '\\' => '\\',
- 'a' => '\x07',
- 'b' => '\x08',
- 'c' => {
- should_stop = true;
- break;
- }
- 'e' => '\x1b',
- 'f' => '\x0c',
- 'n' => '\n',
- 'r' => '\r',
- 't' => '\t',
- 'v' => '\x0b',
- 'x' => parse_code(&mut iter, 16, 2, 4).unwrap_or_else(|| {
- start = 0;
- next
- }),
- '0' => parse_code(&mut iter, 8, 3, 3).unwrap_or('\0'),
- _ => {
- start = 0;
- next
- }
- };
+ // This is for the \NNN syntax for octal sequences.
+ // Note that '0' is intentionally omitted because that
+ // would be the \0NNN syntax.
+ if let Some('1'..='8') = iter.peek() {
+ if let Some(parsed) = parse_code(&mut iter, Base::Oct) {
+ write!(output, "{parsed}")?;
+ continue;
}
}
- buffer[1] = c;
-
- // because printing char slices is apparently not available in the standard library
- for ch in &buffer[start..] {
- write!(output, "{ch}")?;
+ if let Some(next) = iter.next() {
+ let unescaped = match next {
+ '\\' => '\\',
+ 'a' => '\x07',
+ 'b' => '\x08',
+ 'c' => return Ok(ControlFlow::Break(())),
+ 'e' => '\x1b',
+ 'f' => '\x0c',
+ 'n' => '\n',
+ 'r' => '\r',
+ 't' => '\t',
+ 'v' => '\x0b',
+ 'x' => {
+ if let Some(c) = parse_code(&mut iter, Base::Hex) {
+ c
+ } else {
+ write!(output, "\\")?;
+ 'x'
+ }
+ }
+ '0' => parse_code(&mut iter, Base::Oct).unwrap_or('\0'),
+ c => {
+ write!(output, "\\")?;
+ c
+ }
+ };
+ write!(output, "{unescaped}")?;
+ } else {
+ write!(output, "\\")?;
}
}
- Ok(should_stop)
+ Ok(ControlFlow::Continue(()))
}
#[uucore::main]
@@ -148,9 +174,8 @@ fn execute(no_newline: bool, escaped: bool, free: &[String]) -> io::Result<()> {
write!(output, " ")?;
}
if escaped {
- let should_stop = print_escaped(input, &mut output)?;
- if should_stop {
- break;
+ if print_escaped(input, &mut output)?.is_break() {
+ return Ok(());
}
} else {
write!(output, "{input}")?;
diff --git a/src/uu/fmt/src/fmt.rs b/src/uu/fmt/src/fmt.rs
index c5eac7073..c30d923b7 100644
--- a/src/uu/fmt/src/fmt.rs
+++ b/src/uu/fmt/src/fmt.rs
@@ -131,16 +131,8 @@ fn parse_arguments(args: impl uucore::Args) -> UResult<(Vec, FmtOptions)
fmt_opts.use_anti_prefix = true;
};
- if let Some(s) = matches.get_one::(OPT_WIDTH) {
- fmt_opts.width = match s.parse::() {
- Ok(t) => t,
- Err(e) => {
- return Err(USimpleError::new(
- 1,
- format!("Invalid WIDTH specification: {}: {}", s.quote(), e),
- ));
- }
- };
+ if let Some(width) = matches.get_one::(OPT_WIDTH) {
+ fmt_opts.width = *width;
if fmt_opts.width > MAX_WIDTH {
return Err(USimpleError::new(
1,
@@ -156,16 +148,8 @@ fn parse_arguments(args: impl uucore::Args) -> UResult<(Vec, FmtOptions)
);
};
- if let Some(s) = matches.get_one::(OPT_GOAL) {
- fmt_opts.goal = match s.parse::() {
- Ok(t) => t,
- Err(e) => {
- return Err(USimpleError::new(
- 1,
- format!("Invalid GOAL specification: {}: {}", s.quote(), e),
- ));
- }
- };
+ if let Some(goal) = matches.get_one::(OPT_GOAL) {
+ fmt_opts.goal = *goal;
if !matches.contains_id(OPT_WIDTH) {
fmt_opts.width = cmp::max(
fmt_opts.goal * 100 / DEFAULT_GOAL_TO_WIDTH_RATIO,
@@ -372,14 +356,16 @@ pub fn uu_app() -> Command {
.short('w')
.long("width")
.help("Fill output lines up to a maximum of WIDTH columns, default 75.")
- .value_name("WIDTH"),
+ .value_name("WIDTH")
+ .value_parser(clap::value_parser!(usize)),
)
.arg(
Arg::new(OPT_GOAL)
.short('g')
.long("goal")
.help("Goal width, default of 93% of WIDTH. Must be less than WIDTH.")
- .value_name("GOAL"),
+ .value_name("GOAL")
+ .value_parser(clap::value_parser!(usize)),
)
.arg(
Arg::new(OPT_QUICK)
diff --git a/src/uu/install/src/mode.rs b/src/uu/install/src/mode.rs
index f9018e16f..ebdec14af 100644
--- a/src/uu/install/src/mode.rs
+++ b/src/uu/install/src/mode.rs
@@ -9,10 +9,7 @@ use uucore::mode;
/// Takes a user-supplied string and tries to parse to u16 mode bitmask.
pub fn parse(mode_string: &str, considering_dir: bool, umask: u32) -> Result {
- let numbers: &[char] = &['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
-
- // Passing 000 as the existing permissions seems to mirror GNU behavior.
- if mode_string.contains(numbers) {
+ if mode_string.chars().any(|c| c.is_ascii_digit()) {
mode::parse_numeric(0, mode_string, considering_dir)
} else {
mode::parse_symbolic(0, mode_string, umask, considering_dir)
diff --git a/src/uu/mkdir/src/mkdir.rs b/src/uu/mkdir/src/mkdir.rs
index 2044855e4..76aa51f07 100644
--- a/src/uu/mkdir/src/mkdir.rs
+++ b/src/uu/mkdir/src/mkdir.rs
@@ -38,31 +38,27 @@ fn get_mode(_matches: &ArgMatches, _mode_had_minus_prefix: bool) -> Result Result {
- let digits: &[char] = &['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
- // Translate a ~str in octal form to u16, default to 777
// Not tested on Windows
let mut new_mode = DEFAULT_PERM;
- match matches.get_one::(options::MODE) {
- Some(m) => {
- for mode in m.split(',') {
- if mode.contains(digits) {
- new_mode = mode::parse_numeric(new_mode, m, true)?;
+
+ if let Some(m) = matches.get_one::(options::MODE) {
+ for mode in m.split(',') {
+ if mode.chars().any(|c| c.is_ascii_digit()) {
+ new_mode = mode::parse_numeric(new_mode, m, true)?;
+ } else {
+ let cmode = if mode_had_minus_prefix {
+ // clap parsing is finished, now put prefix back
+ format!("-{mode}")
} else {
- let cmode = if mode_had_minus_prefix {
- // clap parsing is finished, now put prefix back
- format!("-{mode}")
- } else {
- mode.to_string()
- };
- new_mode = mode::parse_symbolic(new_mode, &cmode, mode::get_umask(), true)?;
- }
+ mode.to_string()
+ };
+ new_mode = mode::parse_symbolic(new_mode, &cmode, mode::get_umask(), true)?;
}
- Ok(new_mode)
- }
- None => {
- // If no mode argument is specified return the mode derived from umask
- Ok(!mode::get_umask() & 0o0777)
}
+ Ok(new_mode)
+ } else {
+ // If no mode argument is specified return the mode derived from umask
+ Ok(!mode::get_umask() & 0o0777)
}
}
diff --git a/src/uu/mknod/src/parsemode.rs b/src/uu/mknod/src/parsemode.rs
index adacaa45b..c38800bcb 100644
--- a/src/uu/mknod/src/parsemode.rs
+++ b/src/uu/mknod/src/parsemode.rs
@@ -11,8 +11,7 @@ use uucore::mode;
pub const MODE_RW_UGO: mode_t = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
pub fn parse_mode(mode: &str) -> Result {
- let arr: &[char] = &['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
- let result = if mode.contains(arr) {
+ let result = if mode.chars().any(|c| c.is_ascii_digit()) {
mode::parse_numeric(MODE_RW_UGO as u32, mode)
} else {
mode::parse_symbolic(MODE_RW_UGO as u32, mode, true)
diff --git a/src/uu/mv/src/mv.rs b/src/uu/mv/src/mv.rs
index 9f7a96618..43f8eb6b6 100644
--- a/src/uu/mv/src/mv.rs
+++ b/src/uu/mv/src/mv.rs
@@ -448,19 +448,11 @@ fn rename(
match b.overwrite {
OverwriteMode::NoClobber => {
- let err_msg = if b.verbose {
- println!("skipped {}", to.quote());
- String::new()
- } else {
- format!("not replacing {}", to.quote())
- };
+ let err_msg = format!("not replacing {}", to.quote());
return Err(io::Error::new(io::ErrorKind::Other, err_msg));
}
OverwriteMode::Interactive => {
if !prompt_yes!("overwrite {}?", to.quote()) {
- if b.verbose {
- println!("skipped {}", to.quote());
- }
return Err(io::Error::new(io::ErrorKind::Other, ""));
}
}
diff --git a/src/uu/nl/src/helper.rs b/src/uu/nl/src/helper.rs
index fe550e6a0..ae14a6d59 100644
--- a/src/uu/nl/src/helper.rs
+++ b/src/uu/nl/src/helper.rs
@@ -13,6 +13,15 @@ pub fn parse_options(settings: &mut crate::Settings, opts: &clap::ArgMatches) ->
// This vector holds error messages encountered.
let mut errs: Vec = vec![];
settings.renumber = opts.get_flag(options::NO_RENUMBER);
+ if let Some(delimiter) = opts.get_one::(options::SECTION_DELIMITER) {
+ // check whether the delimiter is a single ASCII char (1 byte)
+ // because GNU nl doesn't add a ':' to single non-ASCII chars
+ settings.section_delimiter = if delimiter.len() == 1 {
+ format!("{delimiter}:")
+ } else {
+ delimiter.to_owned()
+ };
+ }
if let Some(val) = opts.get_one::(options::NUMBER_SEPARATOR) {
settings.number_separator = val.to_owned();
}
diff --git a/src/uu/nl/src/nl.rs b/src/uu/nl/src/nl.rs
index 6e1cb6835..71b4aac28 100644
--- a/src/uu/nl/src/nl.rs
+++ b/src/uu/nl/src/nl.rs
@@ -23,7 +23,7 @@ pub struct Settings {
body_numbering: NumberingStyle,
footer_numbering: NumberingStyle,
// The variable corresponding to -d
- section_delimiter: [char; 2],
+ section_delimiter: String,
// The variables corresponding to the options -v, -i, -l, -w.
starting_line_number: i64,
line_increment: i64,
@@ -43,7 +43,7 @@ impl Default for Settings {
header_numbering: NumberingStyle::None,
body_numbering: NumberingStyle::NonEmpty,
footer_numbering: NumberingStyle::None,
- section_delimiter: ['\\', ':'],
+ section_delimiter: String::from("\\:"),
starting_line_number: 1,
line_increment: 1,
join_blank_lines: 1,
@@ -56,14 +56,14 @@ impl Default for Settings {
}
struct Stats {
- line_number: i64,
+ line_number: Option,
consecutive_empty_lines: u64,
}
impl Stats {
fn new(starting_line_number: i64) -> Self {
Self {
- line_number: starting_line_number,
+ line_number: Some(starting_line_number),
consecutive_empty_lines: 0,
}
}
@@ -134,6 +134,32 @@ impl NumberFormat {
}
}
+enum SectionDelimiter {
+ Header,
+ Body,
+ Footer,
+}
+
+impl SectionDelimiter {
+ // A valid section delimiter contains the pattern one to three times,
+ // and nothing else.
+ fn parse(s: &str, pattern: &str) -> Option {
+ if s.is_empty() || pattern.is_empty() {
+ return None;
+ }
+
+ let pattern_count = s.matches(pattern).count();
+ let is_length_ok = pattern_count * pattern.len() == s.len();
+
+ match (pattern_count, is_length_ok) {
+ (3, true) => Some(Self::Header),
+ (2, true) => Some(Self::Body),
+ (1, true) => Some(Self::Footer),
+ _ => None,
+ }
+ }
+}
+
pub mod options {
pub const HELP: &str = "help";
pub const FILE: &str = "file";
@@ -307,20 +333,18 @@ fn nl(reader: &mut BufReader, stats: &mut Stats, settings: &Settings
stats.consecutive_empty_lines = 0;
};
- // FIXME section delimiters are hardcoded and settings.section_delimiter is ignored
- // because --section-delimiter is not correctly implemented yet
- let _ = settings.section_delimiter; // XXX suppress "field never read" warning
- let new_numbering_style = match line.as_str() {
- "\\:\\:\\:" => Some(&settings.header_numbering),
- "\\:\\:" => Some(&settings.body_numbering),
- "\\:" => Some(&settings.footer_numbering),
- _ => None,
+ let new_numbering_style = match SectionDelimiter::parse(&line, &settings.section_delimiter)
+ {
+ Some(SectionDelimiter::Header) => Some(&settings.header_numbering),
+ Some(SectionDelimiter::Body) => Some(&settings.body_numbering),
+ Some(SectionDelimiter::Footer) => Some(&settings.footer_numbering),
+ None => None,
};
if let Some(new_style) = new_numbering_style {
current_numbering_style = new_style;
if settings.renumber {
- stats.line_number = settings.starting_line_number;
+ stats.line_number = Some(settings.starting_line_number);
}
println!();
} else {
@@ -340,18 +364,21 @@ fn nl(reader: &mut BufReader, stats: &mut Stats, settings: &Settings
};
if is_line_numbered {
+ let Some(line_number) = stats.line_number else {
+ return Err(USimpleError::new(1, "line number overflow"));
+ };
println!(
"{}{}{}",
settings
.number_format
- .format(stats.line_number, settings.number_width),
+ .format(line_number, settings.number_width),
settings.number_separator,
line
);
// update line number for the potential next line
- match stats.line_number.checked_add(settings.line_increment) {
- Some(new_line_number) => stats.line_number = new_line_number,
- None => return Err(USimpleError::new(1, "line number overflow")),
+ match line_number.checked_add(settings.line_increment) {
+ Some(new_line_number) => stats.line_number = Some(new_line_number),
+ None => stats.line_number = None, // overflow
}
} else {
let spaces = " ".repeat(settings.number_width + 1);
diff --git a/src/uu/seq/src/numberparse.rs b/src/uu/seq/src/numberparse.rs
index b22cf90c0..469917255 100644
--- a/src/uu/seq/src/numberparse.rs
+++ b/src/uu/seq/src/numberparse.rs
@@ -73,30 +73,13 @@ fn parse_no_decimal_no_exponent(s: &str) -> Result {
// Possibly "NaN" or "inf".
- //
- // TODO In Rust v1.53.0, this change
- // https://github.com/rust-lang/rust/pull/78618 improves the
- // parsing of floats to include being able to parse "NaN"
- // and "inf". So when the minimum version of this crate is
- // increased to 1.53.0, we should just use the built-in
- // `f32` parsing instead.
- if s.eq_ignore_ascii_case("inf") {
- Ok(PreciseNumber::new(
- Number::Float(ExtendedBigDecimal::Infinity),
- 0,
- 0,
- ))
- } else if s.eq_ignore_ascii_case("-inf") {
- Ok(PreciseNumber::new(
- Number::Float(ExtendedBigDecimal::MinusInfinity),
- 0,
- 0,
- ))
- } else if s.eq_ignore_ascii_case("nan") || s.eq_ignore_ascii_case("-nan") {
- Err(ParseNumberError::Nan)
- } else {
- Err(ParseNumberError::Float)
- }
+ let float_val = match s.to_ascii_lowercase().as_str() {
+ "inf" | "infinity" => ExtendedBigDecimal::Infinity,
+ "-inf" | "-infinity" => ExtendedBigDecimal::MinusInfinity,
+ "nan" | "-nan" => return Err(ParseNumberError::Nan),
+ _ => return Err(ParseNumberError::Float),
+ };
+ Ok(PreciseNumber::new(Number::Float(float_val), 0, 0))
}
}
}
@@ -483,11 +466,23 @@ mod tests {
#[test]
fn test_parse_inf() {
assert_eq!(parse("inf"), Number::Float(ExtendedBigDecimal::Infinity));
+ assert_eq!(
+ parse("infinity"),
+ Number::Float(ExtendedBigDecimal::Infinity)
+ );
assert_eq!(parse("+inf"), Number::Float(ExtendedBigDecimal::Infinity));
+ assert_eq!(
+ parse("+infinity"),
+ Number::Float(ExtendedBigDecimal::Infinity)
+ );
assert_eq!(
parse("-inf"),
Number::Float(ExtendedBigDecimal::MinusInfinity)
);
+ assert_eq!(
+ parse("-infinity"),
+ Number::Float(ExtendedBigDecimal::MinusInfinity)
+ );
}
#[test]
diff --git a/src/uu/split/split.md b/src/uu/split/split.md
index d3a481fd3..836e3a0c6 100644
--- a/src/uu/split/split.md
+++ b/src/uu/split/split.md
@@ -11,3 +11,16 @@ Create output files containing consecutive or interleaved sections of input
## After Help
Output fixed-size pieces of INPUT to PREFIXaa, PREFIXab, ...; default size is 1000, and default PREFIX is 'x'. With no INPUT, or when INPUT is -, read standard input.
+
+The SIZE argument is an integer and optional unit (example: 10K is 10*1024).
+Units are K,M,G,T,P,E,Z,Y,R,Q (powers of 1024) or KB,MB,... (powers of 1000).
+Binary prefixes can be used, too: KiB=K, MiB=M, and so on.
+
+CHUNKS may be:
+
+- N split into N files based on size of input
+- K/N output Kth of N to stdout
+- l/N split into N files without splitting lines/records
+- l/K/N output Kth of N to stdout without splitting lines/records
+- r/N like 'l' but use round robin distribution
+- r/K/N likewise but only output Kth of N to stdout
diff --git a/src/uu/split/src/split.rs b/src/uu/split/src/split.rs
index a61c0e812..756248539 100644
--- a/src/uu/split/src/split.rs
+++ b/src/uu/split/src/split.rs
@@ -11,7 +11,7 @@ mod platform;
use crate::filenames::FilenameIterator;
use crate::filenames::SuffixType;
-use clap::{crate_version, parser::ValueSource, Arg, ArgAction, ArgMatches, Command};
+use clap::{crate_version, parser::ValueSource, Arg, ArgAction, ArgMatches, Command, ValueHint};
use std::env;
use std::ffi::OsString;
use std::fmt;
@@ -39,6 +39,7 @@ static OPT_HEX_SUFFIXES_SHORT: &str = "-x";
static OPT_SUFFIX_LENGTH: &str = "suffix-length";
static OPT_DEFAULT_SUFFIX_LENGTH: &str = "0";
static OPT_VERBOSE: &str = "verbose";
+static OPT_SEPARATOR: &str = "separator";
//The ---io and ---io-blksize parameters are consumed and ignored.
//The parameter is included to make GNU coreutils tests pass.
static OPT_IO: &str = "-io";
@@ -55,7 +56,6 @@ const AFTER_HELP: &str = help_section!("after help", "split.md");
#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let (args, obs_lines) = handle_obsolete(args);
-
let matches = uu_app().try_get_matches_from(args)?;
match Settings::from(&matches, &obs_lines) {
@@ -145,6 +145,7 @@ fn should_extract_obs_lines(
&& !slice.starts_with("-C")
&& !slice.starts_with("-l")
&& !slice.starts_with("-n")
+ && !slice.starts_with("-t")
}
/// Helper function to [`filter_args`]
@@ -208,13 +209,18 @@ fn handle_preceding_options(
|| &slice[2..] == OPT_ADDITIONAL_SUFFIX
|| &slice[2..] == OPT_FILTER
|| &slice[2..] == OPT_NUMBER
- || &slice[2..] == OPT_SUFFIX_LENGTH;
+ || &slice[2..] == OPT_SUFFIX_LENGTH
+ || &slice[2..] == OPT_SEPARATOR;
}
// capture if current slice is a preceding short option that requires value and does not have value in the same slice (value separated by whitespace)
// following slice should be treaded as value for this option
// even if it starts with '-' (which would be treated as hyphen prefixed value)
- *preceding_short_opt_req_value =
- slice == "-b" || slice == "-C" || slice == "-l" || slice == "-n" || slice == "-a";
+ *preceding_short_opt_req_value = slice == "-b"
+ || slice == "-C"
+ || slice == "-l"
+ || slice == "-n"
+ || slice == "-a"
+ || slice == "-t";
// slice is a value
// reset preceding option flags
if !slice.starts_with('-') {
@@ -278,7 +284,7 @@ pub fn uu_app() -> Command {
.long(OPT_FILTER)
.allow_hyphen_values(true)
.value_name("COMMAND")
- .value_hint(clap::ValueHint::CommandName)
+ .value_hint(ValueHint::CommandName)
.help(
"write to shell COMMAND; file name is $FILE (Currently not implemented for Windows)",
),
@@ -293,7 +299,7 @@ pub fn uu_app() -> Command {
.arg(
Arg::new(OPT_NUMERIC_SUFFIXES_SHORT)
.short('d')
- .action(clap::ArgAction::SetTrue)
+ .action(ArgAction::SetTrue)
.overrides_with_all([
OPT_NUMERIC_SUFFIXES,
OPT_NUMERIC_SUFFIXES_SHORT,
@@ -314,12 +320,13 @@ pub fn uu_app() -> Command {
OPT_HEX_SUFFIXES,
OPT_HEX_SUFFIXES_SHORT
])
+ .value_name("FROM")
.help("same as -d, but allow setting the start value"),
)
.arg(
Arg::new(OPT_HEX_SUFFIXES_SHORT)
.short('x')
- .action(clap::ArgAction::SetTrue)
+ .action(ArgAction::SetTrue)
.overrides_with_all([
OPT_NUMERIC_SUFFIXES,
OPT_NUMERIC_SUFFIXES_SHORT,
@@ -340,6 +347,7 @@ pub fn uu_app() -> Command {
OPT_HEX_SUFFIXES,
OPT_HEX_SUFFIXES_SHORT
])
+ .value_name("FROM")
.help("same as -x, but allow setting the start value"),
)
.arg(
@@ -357,6 +365,15 @@ pub fn uu_app() -> Command {
.help("print a diagnostic just before each output file is opened")
.action(ArgAction::SetTrue),
)
+ .arg(
+ Arg::new(OPT_SEPARATOR)
+ .short('t')
+ .long(OPT_SEPARATOR)
+ .allow_hyphen_values(true)
+ .value_name("SEP")
+ .action(ArgAction::Append)
+ .help("use SEP instead of newline as the record separator; '\0' (zero) specifies the NUL character"),
+ )
.arg(
Arg::new(OPT_IO)
.long("io")
@@ -372,7 +389,7 @@ pub fn uu_app() -> Command {
.arg(
Arg::new(ARG_INPUT)
.default_value("-")
- .value_hint(clap::ValueHint::FilePath),
+ .value_hint(ValueHint::FilePath),
)
.arg(
Arg::new(ARG_PREFIX)
@@ -696,6 +713,7 @@ struct Settings {
filter: Option,
strategy: Strategy,
verbose: bool,
+ separator: u8,
/// Whether to *not* produce empty files when using `-n`.
///
@@ -722,6 +740,12 @@ enum SettingsError {
/// Suffix is not large enough to split into specified chunks
SuffixTooSmall(usize),
+ /// Multi-character (Invalid) separator
+ MultiCharacterSeparator(String),
+
+ /// Multiple different separator characters
+ MultipleSeparatorCharacters,
+
/// The `--filter` option is not supported on Windows.
#[cfg(windows)]
NotSupported,
@@ -743,6 +767,12 @@ impl fmt::Display for SettingsError {
Self::Strategy(e) => e.fmt(f),
Self::SuffixNotParsable(s) => write!(f, "invalid suffix length: {}", s.quote()),
Self::SuffixTooSmall(i) => write!(f, "the suffix length needs to be at least {i}"),
+ Self::MultiCharacterSeparator(s) => {
+ write!(f, "multi-character separator {}", s.quote())
+ }
+ Self::MultipleSeparatorCharacters => {
+ write!(f, "multiple separator characters specified")
+ }
Self::SuffixContainsSeparator(s) => write!(
f,
"invalid suffix {}, contains directory separator",
@@ -783,6 +813,26 @@ impl Settings {
}
}
}
+
+ // Make sure that separator is only one UTF8 character (if specified)
+ // defaults to '\n' - newline character
+ // If the same separator (the same value) was used multiple times - `split` should NOT fail
+ // If the separator was used multiple times but with different values (not all values are the same) - `split` should fail
+ let separator = match matches.get_many::(OPT_SEPARATOR) {
+ Some(mut sep_values) => {
+ let first = sep_values.next().unwrap(); // it is safe to just unwrap here since Clap should not return empty ValuesRef<'_,String> in the option from get_many() call
+ if !sep_values.all(|s| s == first) {
+ return Err(SettingsError::MultipleSeparatorCharacters);
+ }
+ match first.as_str() {
+ "\\0" => b'\0',
+ s if s.as_bytes().len() == 1 => s.as_bytes()[0],
+ s => return Err(SettingsError::MultiCharacterSeparator(s.to_owned())),
+ }
+ }
+ None => b'\n',
+ };
+
let result = Self {
suffix_length: suffix_length_str
.parse()
@@ -791,6 +841,7 @@ impl Settings {
suffix_start,
additional_suffix,
verbose: matches.value_source("verbose") == Some(ValueSource::CommandLine),
+ separator,
strategy,
input: matches.get_one::(ARG_INPUT).unwrap().to_owned(),
prefix: matches.get_one::(ARG_PREFIX).unwrap().to_owned(),
@@ -1019,7 +1070,8 @@ impl<'a> Write for LineChunkWriter<'a> {
// corresponds to the current chunk number.
let mut prev = 0;
let mut total_bytes_written = 0;
- for i in memchr::memchr_iter(b'\n', buf) {
+ let sep = self.settings.separator;
+ for i in memchr::memchr_iter(sep, buf) {
// If we have exceeded the number of lines to write in the
// current chunk, then start a new chunk and its
// corresponding writer.
@@ -1036,8 +1088,8 @@ impl<'a> Write for LineChunkWriter<'a> {
}
// Write the line, starting from *after* the previous
- // newline character and ending *after* the current
- // newline character.
+ // separator character and ending *after* the current
+ // separator character.
let n = self.inner.write(&buf[prev..i + 1])?;
total_bytes_written += n;
prev = i + 1;
@@ -1175,21 +1227,22 @@ impl<'a> Write for LineBytesChunkWriter<'a> {
self.num_bytes_remaining_in_current_chunk = self.chunk_size.try_into().unwrap();
}
- // Find the first newline character in the buffer.
- match memchr::memchr(b'\n', buf) {
- // If there is no newline character and the buffer is
+ // Find the first separator (default - newline character) in the buffer.
+ let sep = self.settings.separator;
+ match memchr::memchr(sep, buf) {
+ // If there is no separator character and the buffer is
// not empty, then write as many bytes as we can and
// then move on to the next chunk if necessary.
None => {
let end = self.num_bytes_remaining_in_current_chunk;
// This is ugly but here to match GNU behavior. If the input
- // doesn't end with a \n, pretend that it does for handling
+ // doesn't end with a separator, pretend that it does for handling
// the second to last segment chunk. See `line-bytes.sh`.
if end == buf.len()
&& self.num_bytes_remaining_in_current_chunk
< self.chunk_size.try_into().unwrap()
- && buf[buf.len() - 1] != b'\n'
+ && buf[buf.len() - 1] != sep
{
self.num_bytes_remaining_in_current_chunk = 0;
} else {
@@ -1200,8 +1253,8 @@ impl<'a> Write for LineBytesChunkWriter<'a> {
}
}
- // If there is a newline character and the line
- // (including the newline character) will fit in the
+ // If there is a separator character and the line
+ // (including the separator character) will fit in the
// current chunk, then write the entire line and
// continue to the next iteration. (See chunk 1 in the
// example comment above.)
@@ -1212,8 +1265,8 @@ impl<'a> Write for LineBytesChunkWriter<'a> {
buf = &buf[num_bytes_written..];
}
- // If there is a newline character, the line
- // (including the newline character) will not fit in
+ // If there is a separator character, the line
+ // (including the separator character) will not fit in
// the current chunk, *and* no other lines have been
// written to the current chunk, then write as many
// bytes as we can and continue to the next
@@ -1230,8 +1283,8 @@ impl<'a> Write for LineBytesChunkWriter<'a> {
buf = &buf[num_bytes_written..];
}
- // If there is a newline character, the line
- // (including the newline character) will not fit in
+ // If there is a separator character, the line
+ // (including the separator character) will not fit in
// the current chunk, and at least one other line has
// been written to the current chunk, then signal to
// the next iteration that a new chunk needs to be
@@ -1489,15 +1542,16 @@ where
let mut num_bytes_remaining_in_current_chunk = chunk_size;
let mut i = 0;
- for line_result in reader.lines() {
+ let sep = settings.separator;
+ for line_result in reader.split(sep) {
let line = line_result.unwrap();
let maybe_writer = writers.get_mut(i);
let writer = maybe_writer.unwrap();
- let bytes = line.as_bytes();
+ let bytes = line.as_slice();
writer.write_all(bytes)?;
- writer.write_all(b"\n")?;
+ writer.write_all(&[sep])?;
- // Add one byte for the newline character.
+ // Add one byte for the separator character.
let num_bytes = bytes.len() + 1;
if num_bytes > num_bytes_remaining_in_current_chunk {
num_bytes_remaining_in_current_chunk = chunk_size;
@@ -1546,15 +1600,16 @@ where
let mut num_bytes_remaining_in_current_chunk = chunk_size;
let mut i = 0;
- for line_result in reader.lines() {
+ let sep = settings.separator;
+ for line_result in reader.split(sep) {
let line = line_result?;
- let bytes = line.as_bytes();
+ let bytes = line.as_slice();
if i == chunk_number {
writer.write_all(bytes)?;
- writer.write_all(b"\n")?;
+ writer.write_all(&[sep])?;
}
- // Add one byte for the newline character.
+ // Add one byte for the separator character.
let num_bytes = bytes.len() + 1;
if num_bytes >= num_bytes_remaining_in_current_chunk {
num_bytes_remaining_in_current_chunk = chunk_size;
@@ -1601,13 +1656,14 @@ where
}
let num_chunks: usize = num_chunks.try_into().unwrap();
- for (i, line_result) in reader.lines().enumerate() {
+ let sep = settings.separator;
+ for (i, line_result) in reader.split(sep).enumerate() {
let line = line_result.unwrap();
let maybe_writer = writers.get_mut(i % num_chunks);
let writer = maybe_writer.unwrap();
- let bytes = line.as_bytes();
+ let bytes = line.as_slice();
writer.write_all(bytes)?;
- writer.write_all(b"\n")?;
+ writer.write_all(&[sep])?;
}
Ok(())
@@ -1632,7 +1688,7 @@ where
/// * [`split_into_n_chunks_by_line_round_robin`], which splits its input in the
/// same way, but writes each chunk to its own file.
fn kth_chunk_by_line_round_robin(
- _settings: &Settings,
+ settings: &Settings,
reader: &mut R,
chunk_number: u64,
num_chunks: u64,
@@ -1646,12 +1702,13 @@ where
let num_chunks: usize = num_chunks.try_into().unwrap();
let chunk_number: usize = chunk_number.try_into().unwrap();
- for (i, line_result) in reader.lines().enumerate() {
+ let sep = settings.separator;
+ for (i, line_result) in reader.split(sep).enumerate() {
let line = line_result?;
- let bytes = line.as_bytes();
+ let bytes = line.as_slice();
if (i % num_chunks) == chunk_number {
writer.write_all(bytes)?;
- writer.write_all(b"\n")?;
+ writer.write_all(&[sep])?;
}
}
Ok(())
diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs
index 85eb97bc4..6555773ee 100644
--- a/src/uu/touch/src/touch.rs
+++ b/src/uu/touch/src/touch.rs
@@ -68,6 +68,10 @@ fn datetime_to_filetime(dt: &DateTime) -> FileTime {
FileTime::from_unix_time(dt.timestamp(), dt.timestamp_subsec_nanos())
}
+fn filetime_to_datetime(ft: &FileTime) -> Option> {
+ Some(DateTime::from_timestamp(ft.unix_seconds(), ft.nanoseconds())?.into())
+}
+
#[uucore::main]
#[allow(clippy::cognitive_complexity)]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
@@ -88,35 +92,19 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
) {
(Some(reference), Some(date)) => {
let (atime, mtime) = stat(Path::new(reference), !matches.get_flag(options::NO_DEREF))?;
- if let Ok(offset) = parse_datetime::from_str(date) {
- let seconds = offset.num_seconds();
- let nanos = offset.num_nanoseconds().unwrap_or(0) % 1_000_000_000;
-
- let ref_atime_secs = atime.unix_seconds();
- let ref_atime_nanos = atime.nanoseconds();
- let atime = FileTime::from_unix_time(
- ref_atime_secs + seconds,
- ref_atime_nanos + nanos as u32,
- );
-
- let ref_mtime_secs = mtime.unix_seconds();
- let ref_mtime_nanos = mtime.nanoseconds();
- let mtime = FileTime::from_unix_time(
- ref_mtime_secs + seconds,
- ref_mtime_nanos + nanos as u32,
- );
-
- (atime, mtime)
- } else {
- let timestamp = parse_date(date)?;
- (timestamp, timestamp)
- }
+ let atime = filetime_to_datetime(&atime).ok_or_else(|| {
+ USimpleError::new(1, "Could not process the reference access time")
+ })?;
+ let mtime = filetime_to_datetime(&mtime).ok_or_else(|| {
+ USimpleError::new(1, "Could not process the reference modification time")
+ })?;
+ (parse_date(atime, date)?, parse_date(mtime, date)?)
}
(Some(reference), None) => {
stat(Path::new(reference), !matches.get_flag(options::NO_DEREF))?
}
(None, Some(date)) => {
- let timestamp = parse_date(date)?;
+ let timestamp = parse_date(Local::now(), date)?;
(timestamp, timestamp)
}
(None, None) => {
@@ -336,7 +324,7 @@ fn stat(path: &Path, follow: bool) -> UResult<(FileTime, FileTime)> {
))
}
-fn parse_date(s: &str) -> UResult {
+fn parse_date(ref_time: DateTime, s: &str) -> UResult {
// This isn't actually compatible with GNU touch, but there doesn't seem to
// be any simple specification for what format this parameter allows and I'm
// not about to implement GNU parse_datetime.
@@ -385,8 +373,7 @@ fn parse_date(s: &str) -> UResult {
}
}
- if let Ok(duration) = parse_datetime::from_str(s) {
- let dt = Local::now() + duration;
+ if let Ok(dt) = parse_datetime::parse_datetime_at_date(ref_time, s) {
return Ok(datetime_to_filetime(&dt));
}
diff --git a/src/uucore/src/lib/features/mode.rs b/src/uucore/src/lib/features/mode.rs
index cbaea71bf..147624891 100644
--- a/src/uucore/src/lib/features/mode.rs
+++ b/src/uucore/src/lib/features/mode.rs
@@ -147,8 +147,7 @@ pub fn parse_mode(mode: &str) -> Result {
#[cfg(any(target_os = "freebsd", target_vendor = "apple", target_os = "android"))]
let fperm = (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH) as u32;
- let arr: &[char] = &['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
- let result = if mode.contains(arr) {
+ let result = if mode.chars().any(|c| c.is_ascii_digit()) {
parse_numeric(fperm, mode, true)
} else {
parse_symbolic(fperm, mode, get_umask(), true)
diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs
index 95d130f88..e85cd0c57 100644
--- a/tests/by-util/test_cp.rs
+++ b/tests/by-util/test_cp.rs
@@ -483,7 +483,8 @@ fn test_cp_arg_interactive_verbose() {
ucmd.args(&["-vi", "a", "b"])
.pipe_in("N\n")
.fails()
- .stdout_is("skipped 'b'\n");
+ .stderr_is("cp: overwrite 'b'? ")
+ .no_stdout();
}
#[test]
@@ -494,7 +495,8 @@ fn test_cp_arg_interactive_verbose_clobber() {
at.touch("b");
ucmd.args(&["-vin", "a", "b"])
.fails()
- .stdout_is("skipped 'b'\n");
+ .stderr_is("cp: not replacing 'b'\n")
+ .no_stdout();
}
#[test]
@@ -3466,3 +3468,19 @@ fn test_cp_only_source_no_target() {
panic!("Failure: stderr was \n{stderr_str}");
}
}
+
+#[test]
+fn test_cp_dest_no_permissions() {
+ let ts = TestScenario::new(util_name!());
+ let at = &ts.fixtures;
+
+ at.touch("valid.txt");
+ at.touch("invalid_perms.txt");
+ at.set_readonly("invalid_perms.txt");
+
+ ts.ucmd()
+ .args(&["valid.txt", "invalid_perms.txt"])
+ .fails()
+ .stderr_contains("invalid_perms.txt")
+ .stderr_contains("denied");
+}
diff --git a/tests/by-util/test_echo.rs b/tests/by-util/test_echo.rs
index 3a8e7f86b..875ff66cb 100644
--- a/tests/by-util/test_echo.rs
+++ b/tests/by-util/test_echo.rs
@@ -122,7 +122,7 @@ fn test_escape_no_further_output() {
new_ucmd!()
.args(&["-e", "a\\cb", "c"])
.succeeds()
- .stdout_only("a\n");
+ .stdout_only("a");
}
#[test]
@@ -236,3 +236,47 @@ fn test_hyphen_values_between() {
.success()
.stdout_is("dumdum dum dum dum -e dum\n");
}
+
+#[test]
+fn wrapping_octal() {
+ // Some odd behavior of GNU. Values of \0400 and greater do not fit in the
+ // u8 that we write to stdout. So we test that it wraps:
+ //
+ // We give it this input:
+ // \o501 = 1_0100_0001 (yes, **9** bits)
+ // This should be wrapped into:
+ // \o101 = 'A' = 0100_0001,
+ // because we only write a single character
+ new_ucmd!()
+ .arg("-e")
+ .arg("\\0501")
+ .succeeds()
+ .stdout_is("A\n");
+}
+
+#[test]
+fn old_octal_syntax() {
+ new_ucmd!()
+ .arg("-e")
+ .arg("\\1foo")
+ .succeeds()
+ .stdout_is("\x01foo\n");
+
+ new_ucmd!()
+ .arg("-e")
+ .arg("\\43foo")
+ .succeeds()
+ .stdout_is("#foo\n");
+
+ new_ucmd!()
+ .arg("-e")
+ .arg("\\101 foo")
+ .succeeds()
+ .stdout_is("A foo\n");
+
+ new_ucmd!()
+ .arg("-e")
+ .arg("\\1011")
+ .succeeds()
+ .stdout_is("A1\n");
+}
diff --git a/tests/by-util/test_fmt.rs b/tests/by-util/test_fmt.rs
index 7d23cbd52..4fd059080 100644
--- a/tests/by-util/test_fmt.rs
+++ b/tests/by-util/test_fmt.rs
@@ -48,6 +48,17 @@ fn test_fmt_width_too_big() {
}
}
+#[test]
+fn test_fmt_invalid_width() {
+ for param in ["-w", "--width"] {
+ new_ucmd!()
+ .args(&["one-word-per-line.txt", param, "invalid"])
+ .fails()
+ .code_is(1)
+ .stderr_contains("invalid value 'invalid'");
+ }
+}
+
#[ignore]
#[test]
fn test_fmt_goal() {
@@ -70,6 +81,17 @@ fn test_fmt_goal_too_big() {
}
}
+#[test]
+fn test_fmt_invalid_goal() {
+ for param in ["-g", "--goal"] {
+ new_ucmd!()
+ .args(&["one-word-per-line.txt", param, "invalid"])
+ .fails()
+ .code_is(1)
+ .stderr_contains("invalid value 'invalid'");
+ }
+}
+
#[test]
fn test_fmt_set_goal_not_contain_width() {
for param in ["-g", "--goal"] {
diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs
index 23dfafa32..7d0f86298 100644
--- a/tests/by-util/test_ls.rs
+++ b/tests/by-util/test_ls.rs
@@ -1928,6 +1928,35 @@ fn test_ls_recursive() {
result.stdout_contains("a\\b:\nb");
}
+#[test]
+fn test_ls_recursive_1() {
+ let scene = TestScenario::new(util_name!());
+ let at = &scene.fixtures;
+ at.mkdir("x");
+ at.mkdir("y");
+ at.mkdir("a");
+ at.mkdir("b");
+ at.mkdir("c");
+ at.mkdir("a/1");
+ at.mkdir("a/2");
+ at.mkdir("a/3");
+ at.touch("f");
+ at.touch("a/1/I");
+ at.touch("a/1/II");
+ #[cfg(unix)]
+ let out = "a:\n1\n2\n3\n\na/1:\nI\nII\n\na/2:\n\na/3:\n\nb:\n\nc:\n";
+ #[cfg(windows)]
+ let out = "a:\n1\n2\n3\n\na\\1:\nI\nII\n\na\\2:\n\na\\3:\n\nb:\n\nc:\n";
+ scene
+ .ucmd()
+ .arg("-R1")
+ .arg("a")
+ .arg("b")
+ .arg("c")
+ .succeeds()
+ .stdout_is(out);
+}
+
#[test]
fn test_ls_color() {
let scene = TestScenario::new(util_name!());
diff --git a/tests/by-util/test_mv.rs b/tests/by-util/test_mv.rs
index eb8a30ac4..f7f9622f5 100644
--- a/tests/by-util/test_mv.rs
+++ b/tests/by-util/test_mv.rs
@@ -1323,7 +1323,7 @@ fn test_mv_interactive_error() {
}
#[test]
-fn test_mv_info_self() {
+fn test_mv_into_self() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
let dir1 = "dir1";
@@ -1350,7 +1350,7 @@ fn test_mv_arg_interactive_skipped() {
.ignore_stdin_write_error()
.fails()
.stderr_is("mv: overwrite 'b'? ")
- .stdout_is("skipped 'b'\n");
+ .no_stdout();
}
#[test]
@@ -1360,7 +1360,8 @@ fn test_mv_arg_interactive_skipped_vin() {
at.touch("b");
ucmd.args(&["-vin", "a", "b"])
.fails()
- .stdout_is("skipped 'b'\n");
+ .stderr_is("mv: not replacing 'b'\n")
+ .no_stdout();
}
#[test]
diff --git a/tests/by-util/test_nl.rs b/tests/by-util/test_nl.rs
index 118c4cf04..78c8975a8 100644
--- a/tests/by-util/test_nl.rs
+++ b/tests/by-util/test_nl.rs
@@ -2,7 +2,8 @@
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
-// spell-checker:ignore binvalid finvalid hinvalid iinvalid linvalid ninvalid vinvalid winvalid
+//
+// spell-checker:ignore binvalid finvalid hinvalid iinvalid linvalid nabcabc nabcabcabc ninvalid vinvalid winvalid
use crate::common::util::TestScenario;
#[test]
@@ -537,3 +538,99 @@ fn test_line_number_overflow() {
.stdout_is(format!("{}\ta\n", i64::MIN))
.stderr_is("nl: line number overflow\n");
}
+
+#[test]
+fn test_line_number_no_overflow() {
+ new_ucmd!()
+ .arg(format!("--starting-line-number={}", i64::MAX))
+ .pipe_in("a\n\\:\\:\nb")
+ .succeeds()
+ .stdout_is(format!("{0}\ta\n\n{0}\tb\n", i64::MAX));
+
+ new_ucmd!()
+ .arg(format!("--starting-line-number={}", i64::MIN))
+ .arg("--line-increment=-1")
+ .pipe_in("a\n\\:\\:\nb")
+ .succeeds()
+ .stdout_is(format!("{0}\ta\n\n{0}\tb\n", i64::MIN));
+}
+
+#[test]
+fn test_section_delimiter() {
+ for arg in ["-dabc", "--section-delimiter=abc"] {
+ new_ucmd!()
+ .arg(arg)
+ .pipe_in("a\nabcabcabc\nb") // header section
+ .succeeds()
+ .stdout_is(" 1\ta\n\n b\n");
+
+ new_ucmd!()
+ .arg(arg)
+ .pipe_in("a\nabcabc\nb") // body section
+ .succeeds()
+ .stdout_is(" 1\ta\n\n 1\tb\n");
+
+ new_ucmd!()
+ .arg(arg)
+ .pipe_in("a\nabc\nb") // footer section
+ .succeeds()
+ .stdout_is(" 1\ta\n\n b\n");
+ }
+}
+
+#[test]
+fn test_one_char_section_delimiter_expansion() {
+ for arg in ["-da", "--section-delimiter=a"] {
+ new_ucmd!()
+ .arg(arg)
+ .pipe_in("a\na:a:a:\nb") // header section
+ .succeeds()
+ .stdout_is(" 1\ta\n\n b\n");
+
+ new_ucmd!()
+ .arg(arg)
+ .pipe_in("a\na:a:\nb") // body section
+ .succeeds()
+ .stdout_is(" 1\ta\n\n 1\tb\n");
+
+ new_ucmd!()
+ .arg(arg)
+ .pipe_in("a\na:\nb") // footer section
+ .succeeds()
+ .stdout_is(" 1\ta\n\n b\n");
+ }
+}
+
+#[test]
+fn test_non_ascii_one_char_section_delimiter() {
+ for arg in ["-dä", "--section-delimiter=ä"] {
+ new_ucmd!()
+ .arg(arg)
+ .pipe_in("a\näää\nb") // header section
+ .succeeds()
+ .stdout_is(" 1\ta\n\n b\n");
+
+ new_ucmd!()
+ .arg(arg)
+ .pipe_in("a\nää\nb") // body section
+ .succeeds()
+ .stdout_is(" 1\ta\n\n 1\tb\n");
+
+ new_ucmd!()
+ .arg(arg)
+ .pipe_in("a\nä\nb") // footer section
+ .succeeds()
+ .stdout_is(" 1\ta\n\n b\n");
+ }
+}
+
+#[test]
+fn test_empty_section_delimiter() {
+ for arg in ["-d ''", "--section-delimiter=''"] {
+ new_ucmd!()
+ .arg(arg)
+ .pipe_in("a\n\nb")
+ .succeeds()
+ .stdout_is(" 1\ta\n \n 2\tb\n");
+ }
+}
diff --git a/tests/by-util/test_seq.rs b/tests/by-util/test_seq.rs
index 8f879263b..da28181eb 100644
--- a/tests/by-util/test_seq.rs
+++ b/tests/by-util/test_seq.rs
@@ -623,11 +623,21 @@ fn test_neg_inf() {
run(&["--", "-inf", "0"], b"-inf\n-inf\n-inf\n");
}
+#[test]
+fn test_neg_infinity() {
+ run(&["--", "-infinity", "0"], b"-inf\n-inf\n-inf\n");
+}
+
#[test]
fn test_inf() {
run(&["inf"], b"1\n2\n3\n");
}
+#[test]
+fn test_infinity() {
+ run(&["infinity"], b"1\n2\n3\n");
+}
+
#[test]
fn test_inf_width() {
run(
diff --git a/tests/by-util/test_split.rs b/tests/by-util/test_split.rs
index 7c6d271e6..6fc3a3706 100644
--- a/tests/by-util/test_split.rs
+++ b/tests/by-util/test_split.rs
@@ -1483,3 +1483,242 @@ fn test_split_non_utf8_argument_windows() {
.fails()
.stderr_contains("error: invalid UTF-8 was detected in one or more arguments");
}
+
+// Test '--separator' / '-t' option following GNU tests example
+// test separators: '\n' , '\0' , ';'
+// test with '--lines=2' , '--line-bytes=4' , '--number=l/3' , '--number=r/3' , '--number=l/1/3' , '--number=r/1/3'
+#[test]
+fn test_split_separator_nl_lines() {
+ let (at, mut ucmd) = at_and_ucmd!();
+ ucmd.args(&["--lines=2", "-t", "\n"])
+ .pipe_in("1\n2\n3\n4\n5\n")
+ .succeeds();
+
+ assert_eq!(file_read(&at, "xaa"), "1\n2\n");
+ assert_eq!(file_read(&at, "xab"), "3\n4\n");
+ assert_eq!(file_read(&at, "xac"), "5\n");
+ assert!(!at.plus("xad").exists());
+}
+
+#[test]
+fn test_split_separator_nl_line_bytes() {
+ let (at, mut ucmd) = at_and_ucmd!();
+ ucmd.args(&["--line-bytes=4", "-t", "\n"])
+ .pipe_in("1\n2\n3\n4\n5\n")
+ .succeeds();
+
+ assert_eq!(file_read(&at, "xaa"), "1\n2\n");
+ assert_eq!(file_read(&at, "xab"), "3\n4\n");
+ assert_eq!(file_read(&at, "xac"), "5\n");
+ assert!(!at.plus("xad").exists());
+}
+
+#[test]
+fn test_split_separator_nl_number_l() {
+ let (at, mut ucmd) = at_and_ucmd!();
+ ucmd.args(&["--number=l/3", "--separator=\n", "fivelines.txt"])
+ .succeeds();
+
+ assert_eq!(file_read(&at, "xaa"), "1\n2\n");
+ assert_eq!(file_read(&at, "xab"), "3\n4\n");
+ assert_eq!(file_read(&at, "xac"), "5\n");
+ assert!(!at.plus("xad").exists());
+}
+
+#[test]
+fn test_split_separator_nl_number_r() {
+ let (at, mut ucmd) = at_and_ucmd!();
+ ucmd.args(&["--number=r/3", "--separator", "\n", "fivelines.txt"])
+ .succeeds();
+
+ assert_eq!(file_read(&at, "xaa"), "1\n4\n");
+ assert_eq!(file_read(&at, "xab"), "2\n5\n");
+ assert_eq!(file_read(&at, "xac"), "3\n");
+ assert!(!at.plus("xad").exists());
+}
+
+#[test]
+fn test_split_separator_nul_lines() {
+ let (at, mut ucmd) = at_and_ucmd!();
+ ucmd.args(&["--lines=2", "-t", "\\0", "separator_nul.txt"])
+ .succeeds();
+
+ assert_eq!(file_read(&at, "xaa"), "1\02\0");
+ assert_eq!(file_read(&at, "xab"), "3\04\0");
+ assert_eq!(file_read(&at, "xac"), "5\0");
+ assert!(!at.plus("xad").exists());
+}
+
+#[test]
+fn test_split_separator_nul_line_bytes() {
+ let (at, mut ucmd) = at_and_ucmd!();
+ ucmd.args(&["--line-bytes=4", "-t", "\\0", "separator_nul.txt"])
+ .succeeds();
+
+ assert_eq!(file_read(&at, "xaa"), "1\02\0");
+ assert_eq!(file_read(&at, "xab"), "3\04\0");
+ assert_eq!(file_read(&at, "xac"), "5\0");
+ assert!(!at.plus("xad").exists());
+}
+
+#[test]
+fn test_split_separator_nul_number_l() {
+ let (at, mut ucmd) = at_and_ucmd!();
+ ucmd.args(&["--number=l/3", "--separator=\\0", "separator_nul.txt"])
+ .succeeds();
+
+ assert_eq!(file_read(&at, "xaa"), "1\02\0");
+ assert_eq!(file_read(&at, "xab"), "3\04\0");
+ assert_eq!(file_read(&at, "xac"), "5\0");
+ assert!(!at.plus("xad").exists());
+}
+
+#[test]
+fn test_split_separator_nul_number_r() {
+ let (at, mut ucmd) = at_and_ucmd!();
+ ucmd.args(&["--number=r/3", "--separator=\\0", "separator_nul.txt"])
+ .succeeds();
+
+ assert_eq!(file_read(&at, "xaa"), "1\04\0");
+ assert_eq!(file_read(&at, "xab"), "2\05\0");
+ assert_eq!(file_read(&at, "xac"), "3\0");
+ assert!(!at.plus("xad").exists());
+}
+
+#[test]
+fn test_split_separator_semicolon_lines() {
+ let (at, mut ucmd) = at_and_ucmd!();
+ ucmd.args(&["--lines=2", "-t", ";", "separator_semicolon.txt"])
+ .succeeds();
+
+ assert_eq!(file_read(&at, "xaa"), "1;2;");
+ assert_eq!(file_read(&at, "xab"), "3;4;");
+ assert_eq!(file_read(&at, "xac"), "5;");
+ assert!(!at.plus("xad").exists());
+}
+
+#[test]
+fn test_split_separator_semicolon_line_bytes() {
+ let (at, mut ucmd) = at_and_ucmd!();
+ ucmd.args(&["--line-bytes=4", "-t", ";", "separator_semicolon.txt"])
+ .succeeds();
+
+ assert_eq!(file_read(&at, "xaa"), "1;2;");
+ assert_eq!(file_read(&at, "xab"), "3;4;");
+ assert_eq!(file_read(&at, "xac"), "5;");
+ assert!(!at.plus("xad").exists());
+}
+
+#[test]
+fn test_split_separator_semicolon_number_l() {
+ let (at, mut ucmd) = at_and_ucmd!();
+ ucmd.args(&["--number=l/3", "--separator=;", "separator_semicolon.txt"])
+ .succeeds();
+
+ assert_eq!(file_read(&at, "xaa"), "1;2;");
+ assert_eq!(file_read(&at, "xab"), "3;4;");
+ assert_eq!(file_read(&at, "xac"), "5;");
+ assert!(!at.plus("xad").exists());
+}
+
+#[test]
+fn test_split_separator_semicolon_number_r() {
+ let (at, mut ucmd) = at_and_ucmd!();
+ ucmd.args(&["--number=r/3", "--separator=;", "separator_semicolon.txt"])
+ .succeeds();
+
+ assert_eq!(file_read(&at, "xaa"), "1;4;");
+ assert_eq!(file_read(&at, "xab"), "2;5;");
+ assert_eq!(file_read(&at, "xac"), "3;");
+ assert!(!at.plus("xad").exists());
+}
+
+#[test]
+fn test_split_separator_semicolon_number_kth_l() {
+ new_ucmd!()
+ .args(&[
+ "--number=l/1/3",
+ "--separator",
+ ";",
+ "separator_semicolon.txt",
+ ])
+ .succeeds()
+ .stdout_only("1;2;");
+}
+
+#[test]
+fn test_split_separator_semicolon_number_kth_r() {
+ new_ucmd!()
+ .args(&[
+ "--number=r/1/3",
+ "--separator",
+ ";",
+ "separator_semicolon.txt",
+ ])
+ .succeeds()
+ .stdout_only("1;4;");
+}
+
+// Test error edge cases for separator option
+#[test]
+fn test_split_separator_no_value() {
+ new_ucmd!()
+ .args(&["-t"])
+ .ignore_stdin_write_error()
+ .pipe_in("a\n")
+ .fails()
+ .stderr_contains(
+ "error: a value is required for '--separator ' but none was supplied",
+ );
+}
+
+#[test]
+fn test_split_separator_invalid_usage() {
+ let scene = TestScenario::new(util_name!());
+ scene
+ .ucmd()
+ .args(&["--separator=xx"])
+ .ignore_stdin_write_error()
+ .pipe_in("a\n")
+ .fails()
+ .no_stdout()
+ .stderr_contains("split: multi-character separator 'xx'");
+ scene
+ .ucmd()
+ .args(&["-ta", "-tb"])
+ .ignore_stdin_write_error()
+ .pipe_in("a\n")
+ .fails()
+ .no_stdout()
+ .stderr_contains("split: multiple separator characters specified");
+ scene
+ .ucmd()
+ .args(&["-t'\n'", "-tb"])
+ .ignore_stdin_write_error()
+ .pipe_in("a\n")
+ .fails()
+ .no_stdout()
+ .stderr_contains("split: multiple separator characters specified");
+}
+
+// Test using same separator multiple times
+#[test]
+fn test_split_separator_same_multiple() {
+ let scene = TestScenario::new(util_name!());
+ scene
+ .ucmd()
+ .args(&["--separator=:", "--separator=:", "fivelines.txt"])
+ .succeeds();
+ scene
+ .ucmd()
+ .args(&["-t:", "--separator=:", "fivelines.txt"])
+ .succeeds();
+ scene
+ .ucmd()
+ .args(&["-t", ":", "-t", ":", "fivelines.txt"])
+ .succeeds();
+ scene
+ .ucmd()
+ .args(&["-t:", "-t:", "-t,", "fivelines.txt"])
+ .fails();
+}
diff --git a/tests/by-util/test_touch.rs b/tests/by-util/test_touch.rs
index c9c0d700e..7b659fc51 100644
--- a/tests/by-util/test_touch.rs
+++ b/tests/by-util/test_touch.rs
@@ -844,3 +844,15 @@ fn test_touch_dash() {
ucmd.args(&["-h", "-"]).succeeds().no_stderr().no_stdout();
}
+
+#[test]
+// Chrono panics for now
+#[ignore]
+fn test_touch_invalid_date_format() {
+ let (_at, mut ucmd) = at_and_ucmd!();
+ let file = "test_touch_invalid_date_format";
+
+ ucmd.args(&["-m", "-t", "+1000000000000 years", file])
+ .fails()
+ .stderr_contains("touch: invalid date format ‘+1000000000000 years’");
+}
diff --git a/tests/fixtures/split/separator_nul.txt b/tests/fixtures/split/separator_nul.txt
new file mode 100644
index 000000000..c4c49609d
Binary files /dev/null and b/tests/fixtures/split/separator_nul.txt differ
diff --git a/tests/fixtures/split/separator_semicolon.txt b/tests/fixtures/split/separator_semicolon.txt
new file mode 100644
index 000000000..a8396d8ee
--- /dev/null
+++ b/tests/fixtures/split/separator_semicolon.txt
@@ -0,0 +1 @@
+1;2;3;4;5;
\ No newline at end of file