1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 19:47:45 +00:00

Merge branch 'main' into fix-5327

This commit is contained in:
tommady 2023-10-04 16:06:48 +08:00 committed by GitHub
commit 879b4f363d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 1255 additions and 631 deletions

View file

@ -38,205 +38,15 @@ CI. However, you can use `#[cfg(...)]` attributes to create platform dependent f
VirtualBox and Parallels) for development:
<https://developer.microsoft.com/windows/downloads/virtual-machines/>
## 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
<!-- spell-checker:ignore (flags) Ccodegen Coverflow Cpanic Zinstrument Zpanic -->
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 <options...> # e.g., --features feat_os_unix
cargo test <options...> # 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

20
Cargo.lock generated
View file

@ -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",

View file

@ -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"

329
DEVELOPMENT.md Normal file
View file

@ -0,0 +1,329 @@
<!-- spell-checker:ignore (flags) Ccodegen Coverflow Cpanic Zinstrument Zpanic reimplementing toybox RUNTEST CARGOFLAGS nextest prereq autopoint gettext texinfo automake findutils shellenv libexec gnubin toolchains -->
# 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 <options...> # e.g., --features feat_os_unix
cargo test <options...> # 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.

View file

@ -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<F>(args: &[OsString], uumain_function: F) -> (String, i32)
where
F: FnOnce(std::vec::IntoIter<OsString>) -> 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),
))
}
}

View file

@ -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);
});

View file

@ -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<char> = "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(_) => {

View file

@ -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);
}
});

View file

@ -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);

View file

@ -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 {

View file

@ -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());
}
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)?;
}

View file

@ -63,11 +63,18 @@ 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.
// 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);
}
}
}
}
if raw_pfn.is_null() || error != 0 {
// clonefile(2) is either not supported or it errored out (possibly because the FS does not

View file

@ -166,7 +166,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
};
let date_source = if let Some(date) = matches.get_one::<String>(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())

View file

@ -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<Chars>,
base: u32,
max_digits: u32,
bits_per_digit: u32,
) -> Option<char> {
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,
#[repr(u8)]
#[derive(Clone, Copy)]
enum Base {
Oct = 8,
Hex = 16,
}
impl Base {
fn max_digits(&self) -> u8 {
match self {
Self::Oct => 3,
Self::Hex => 2,
}
}
}
/// Parse the numeric part of the `\xHHH` and `\0NNN` escape sequences
fn parse_code(input: &mut Peekable<Chars>, base: Base) -> Option<char> {
// 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;
// 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,
}
input.next();
}
std::char::from_u32(ret)
// We can safely ignore the None case because we just peeked it.
let _ = input.next();
}
fn print_escaped(input: &str, mut output: impl Write) -> io::Result<bool> {
let mut should_stop = false;
Some(ret.into())
}
let mut buffer = ['\\'; 2];
// TODO `cargo +nightly clippy` complains that `.peek()` is never
// called on `iter`. However, `peek()` is called inside the
// `parse_code()` function that borrows `iter`.
fn print_escaped(input: &str, mut output: impl Write) -> io::Result<ControlFlow<()>> {
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;
}
// 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;
}
}
if c == '\\' {
if let Some(next) = iter.next() {
c = match next {
let unescaped = match next {
'\\' => '\\',
'a' => '\x07',
'b' => '\x08',
'c' => {
should_stop = true;
break;
}
'c' => return Ok(ControlFlow::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
'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, "\\")?;
}
}
buffer[1] = c;
// because printing char slices is apparently not available in the standard library
for ch in &buffer[start..] {
write!(output, "{ch}")?;
}
}
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}")?;

View file

@ -131,16 +131,8 @@ fn parse_arguments(args: impl uucore::Args) -> UResult<(Vec<String>, FmtOptions)
fmt_opts.use_anti_prefix = true;
};
if let Some(s) = matches.get_one::<String>(OPT_WIDTH) {
fmt_opts.width = match s.parse::<usize>() {
Ok(t) => t,
Err(e) => {
return Err(USimpleError::new(
1,
format!("Invalid WIDTH specification: {}: {}", s.quote(), e),
));
}
};
if let Some(width) = matches.get_one::<usize>(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<String>, FmtOptions)
);
};
if let Some(s) = matches.get_one::<String>(OPT_GOAL) {
fmt_opts.goal = match s.parse::<usize>() {
Ok(t) => t,
Err(e) => {
return Err(USimpleError::new(
1,
format!("Invalid GOAL specification: {}: {}", s.quote(), e),
));
}
};
if let Some(goal) = matches.get_one::<usize>(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)

View file

@ -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<u32, String> {
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)

View file

@ -38,14 +38,12 @@ fn get_mode(_matches: &ArgMatches, _mode_had_minus_prefix: bool) -> Result<u32,
#[cfg(not(windows))]
fn get_mode(matches: &ArgMatches, mode_had_minus_prefix: bool) -> Result<u32, String> {
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::<String>(options::MODE) {
Some(m) => {
if let Some(m) = matches.get_one::<String>(options::MODE) {
for mode in m.split(',') {
if mode.contains(digits) {
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 {
@ -58,13 +56,11 @@ fn get_mode(matches: &ArgMatches, mode_had_minus_prefix: bool) -> Result<u32, St
}
}
Ok(new_mode)
}
None => {
} else {
// If no mode argument is specified return the mode derived from umask
Ok(!mode::get_umask() & 0o0777)
}
}
}
#[cfg(windows)]
fn strip_minus_from_mode(_args: &mut [String]) -> bool {

View file

@ -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<mode_t, String> {
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)

View file

@ -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, ""));
}
}

View file

@ -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<String> = vec![];
settings.renumber = opts.get_flag(options::NO_RENUMBER);
if let Some(delimiter) = opts.get_one::<String>(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::<String>(options::NUMBER_SEPARATOR) {
settings.number_separator = val.to_owned();
}

View file

@ -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<i64>,
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<Self> {
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<T: Read>(reader: &mut BufReader<T>, 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<T: Read>(reader: &mut BufReader<T>, 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);

View file

@ -73,30 +73,13 @@ fn parse_no_decimal_no_exponent(s: &str) -> Result<PreciseNumber, ParseNumberErr
}
Err(_) => {
// 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]

View file

@ -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

View file

@ -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<String>,
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::<String>(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::<String>(ARG_INPUT).unwrap().to_owned(),
prefix: matches.get_one::<String>(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<R>(
_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(())

View file

@ -68,6 +68,10 @@ fn datetime_to_filetime<T: TimeZone>(dt: &DateTime<T>) -> FileTime {
FileTime::from_unix_time(dt.timestamp(), dt.timestamp_subsec_nanos())
}
fn filetime_to_datetime(ft: &FileTime) -> Option<DateTime<Local>> {
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<FileTime> {
fn parse_date(ref_time: DateTime<Local>, s: &str) -> UResult<FileTime> {
// 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<FileTime> {
}
}
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));
}

View file

@ -147,8 +147,7 @@ pub fn parse_mode(mode: &str) -> Result<mode_t, String> {
#[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)

View file

@ -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");
}

View file

@ -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");
}

View file

@ -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"] {

View file

@ -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!());

View file

@ -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]

View file

@ -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");
}
}

View file

@ -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(

View file

@ -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 <SEP>' 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();
}

View file

@ -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");
}

BIN
tests/fixtures/split/separator_nul.txt vendored Normal file

Binary file not shown.

View file

@ -0,0 +1 @@
1;2;3;4;5;