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:
commit
879b4f363d
37 changed files with 1255 additions and 631 deletions
225
CONTRIBUTING.md
225
CONTRIBUTING.md
|
@ -38,205 +38,15 @@ CI. However, you can use `#[cfg(...)]` attributes to create platform dependent f
|
||||||
VirtualBox and Parallels) for development:
|
VirtualBox and Parallels) for development:
|
||||||
<https://developer.microsoft.com/windows/downloads/virtual-machines/>
|
<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
|
To setup your local development environment for this project please follow [DEVELOPMENT.md guide](DEVELOPMENT.md)
|
||||||
section explains how to run those checks locally to avoid waiting for the CI.
|
|
||||||
|
|
||||||
### 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
|
## Improving the GNU compatibility
|
||||||
automatically checking every git commit you make to ensure it compiles, and
|
|
||||||
passes `clippy` and `rustfmt` without warnings.
|
|
||||||
|
|
||||||
To use the provided hook:
|
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)
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
The Python script `./util/remaining-gnu-error.py` shows the list of failing
|
The Python script `./util/remaining-gnu-error.py` shows the list of failing
|
||||||
tests in the CI.
|
tests in the CI.
|
||||||
|
@ -326,30 +136,7 @@ gitignore: add temporary files
|
||||||
|
|
||||||
## Code coverage
|
## Code coverage
|
||||||
|
|
||||||
<!-- spell-checker:ignore (flags) Ccodegen Coverflow Cpanic Zinstrument Zpanic -->
|
To generate code coverage report locally please follow [Code coverage report](DEVELOPMENT.md#code-coverage-report) section of [DEVELOPMENT.md](DEVELOPMENT.md)
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## Other implementations
|
## Other implementations
|
||||||
|
|
||||||
|
|
20
Cargo.lock
generated
20
Cargo.lock
generated
|
@ -205,9 +205,9 @@ checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytecount"
|
name = "bytecount"
|
||||||
version = "0.6.3"
|
version = "0.6.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c"
|
checksum = "ad152d03a2c813c80bb94fedbf3a3f02b28f793e39e7c214c8a0bcc196343de7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
|
@ -1291,9 +1291,9 @@ checksum = "5486aed0026218e61b8a01d5fbd5a0a134649abb71a0e53b7bc088529dced86e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memmap2"
|
name = "memmap2"
|
||||||
version = "0.8.0"
|
version = "0.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "43a5a03cefb0d953ec0be133036f14e109412fa594edc2f77227249db66cc3ed"
|
checksum = "deaba38d7abf1d4cca21cc89e932e542ba2b9258664d2a9ef0e61512039c9375"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
@ -1499,9 +1499,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parse_datetime"
|
name = "parse_datetime"
|
||||||
version = "0.4.0"
|
version = "0.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fecceaede7767a9a98058687a321bc91742eff7670167a34104afb30fc8757df"
|
checksum = "3bbf4e25b13841080e018a1e666358adfe5e39b6d353f986ca5091c210b586a1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"regex",
|
"regex",
|
||||||
|
@ -1740,9 +1740,9 @@ checksum = "f1bfbf25d7eb88ddcbb1ec3d755d0634da8f7657b2cb8b74089121409ab8228f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.9.5"
|
version = "1.9.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47"
|
checksum = "ebee201405406dbf528b8b672104ae6d6d63e6d118cb10e4d51abbc7b58044ff"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
@ -1752,9 +1752,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-automata"
|
name = "regex-automata"
|
||||||
version = "0.3.8"
|
version = "0.3.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795"
|
checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
|
|
@ -261,7 +261,7 @@ test = ["uu_test"]
|
||||||
bigdecimal = "0.4"
|
bigdecimal = "0.4"
|
||||||
binary-heap-plus = "0.5.0"
|
binary-heap-plus = "0.5.0"
|
||||||
bstr = "1.6"
|
bstr = "1.6"
|
||||||
bytecount = "0.6.3"
|
bytecount = "0.6.4"
|
||||||
byteorder = "1.4.3"
|
byteorder = "1.4.3"
|
||||||
chrono = { version = "^0.4.31", default-features = false, features = [
|
chrono = { version = "^0.4.31", default-features = false, features = [
|
||||||
"std",
|
"std",
|
||||||
|
@ -292,7 +292,7 @@ lscolors = { version = "0.15.0", default-features = false, features = [
|
||||||
"nu-ansi-term",
|
"nu-ansi-term",
|
||||||
] }
|
] }
|
||||||
memchr = "2"
|
memchr = "2"
|
||||||
memmap2 = "0.8"
|
memmap2 = "0.9"
|
||||||
nix = { version = "0.27", default-features = false }
|
nix = { version = "0.27", default-features = false }
|
||||||
nom = "7.1.3"
|
nom = "7.1.3"
|
||||||
notify = { version = "=6.0.1", features = ["macos_kqueue"] }
|
notify = { version = "=6.0.1", features = ["macos_kqueue"] }
|
||||||
|
@ -301,7 +301,7 @@ num-traits = "0.2.16"
|
||||||
number_prefix = "0.4"
|
number_prefix = "0.4"
|
||||||
once_cell = "1.18.0"
|
once_cell = "1.18.0"
|
||||||
onig = { version = "~6.4", default-features = false }
|
onig = { version = "~6.4", default-features = false }
|
||||||
parse_datetime = "0.4.0"
|
parse_datetime = "0.5.0"
|
||||||
phf = "0.11.2"
|
phf = "0.11.2"
|
||||||
phf_codegen = "0.11.2"
|
phf_codegen = "0.11.2"
|
||||||
platform-info = "2.0.2"
|
platform-info = "2.0.2"
|
||||||
|
@ -310,7 +310,7 @@ rand = { version = "0.8", features = ["small_rng"] }
|
||||||
rand_core = "0.6"
|
rand_core = "0.6"
|
||||||
rayon = "1.8"
|
rayon = "1.8"
|
||||||
redox_syscall = "0.4"
|
redox_syscall = "0.4"
|
||||||
regex = "1.9.5"
|
regex = "1.9.6"
|
||||||
rstest = "0.18.2"
|
rstest = "0.18.2"
|
||||||
rust-ini = "0.19.0"
|
rust-ini = "0.19.0"
|
||||||
same-file = "1.0.6"
|
same-file = "1.0.6"
|
||||||
|
|
329
DEVELOPMENT.md
Normal file
329
DEVELOPMENT.md
Normal 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.
|
|
@ -3,6 +3,9 @@
|
||||||
// For the full copyright and license information, please view the LICENSE
|
// For the full copyright and license information, please view the LICENSE
|
||||||
// file that was distributed with this source code.
|
// 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::process::Command;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use std::sync::{atomic::AtomicBool, Once};
|
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");
|
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),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -9,6 +9,6 @@ fuzz_target!(|data: &[u8]| {
|
||||||
let args = data
|
let args = data
|
||||||
.split(|b| *b == delim)
|
.split(|b| *b == delim)
|
||||||
.filter_map(|e| std::str::from_utf8(e).ok())
|
.filter_map(|e| std::str::from_utf8(e).ok())
|
||||||
.map(|e| OsString::from(e));
|
.map(OsString::from);
|
||||||
uumain(args);
|
uumain(args);
|
||||||
});
|
});
|
||||||
|
|
|
@ -12,35 +12,11 @@ use rand::seq::SliceRandom;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
|
|
||||||
use libc::{dup, dup2, STDOUT_FILENO};
|
|
||||||
use std::process::Command;
|
|
||||||
mod fuzz_common;
|
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";
|
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 {
|
fn generate_random_string(max_length: usize) -> String {
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
let valid_utf8: Vec<char> = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
let valid_utf8: Vec<char> = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||||
|
@ -108,55 +84,10 @@ fuzz_target!(|_data: &[u8]| {
|
||||||
let mut args = vec![OsString::from("expr")];
|
let mut args = vec![OsString::from("expr")];
|
||||||
args.extend(expr.split_whitespace().map(OsString::from));
|
args.extend(expr.split_whitespace().map(OsString::from));
|
||||||
|
|
||||||
// Save the original stdout file descriptor
|
let (rust_output, uumain_exit_code) = generate_and_run_uumain(&args, uumain);
|
||||||
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();
|
|
||||||
|
|
||||||
// Run GNU expr with the provided arguments and compare the output
|
// 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)) => {
|
Ok((gnu_output, gnu_exit_code)) => {
|
||||||
let gnu_output = gnu_output.trim().to_owned();
|
let gnu_output = gnu_output.trim().to_owned();
|
||||||
if uumain_exit_code != gnu_exit_code {
|
if uumain_exit_code != gnu_exit_code {
|
||||||
|
@ -165,16 +96,16 @@ fuzz_target!(|_data: &[u8]| {
|
||||||
println!("GNU code: {}", gnu_exit_code);
|
println!("GNU code: {}", gnu_exit_code);
|
||||||
panic!("Different error codes");
|
panic!("Different error codes");
|
||||||
}
|
}
|
||||||
if rust_output != gnu_output {
|
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 {
|
|
||||||
println!(
|
println!(
|
||||||
"Outputs matched for expression: {} => Result: {}",
|
"Outputs matched for expression: {} => Result: {}",
|
||||||
expr, rust_output
|
expr, rust_output
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
println!("Expression: {}", expr);
|
||||||
|
println!("Rust output: {}", rust_output);
|
||||||
|
println!("GNU output: {}", gnu_output);
|
||||||
|
panic!("Different output between Rust & GNU");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
|
|
|
@ -5,6 +5,6 @@ use uucore::parse_glob;
|
||||||
|
|
||||||
fuzz_target!(|data: &[u8]| {
|
fuzz_target!(|data: &[u8]| {
|
||||||
if let Ok(s) = std::str::from_utf8(data) {
|
if let Ok(s) = std::str::from_utf8(data) {
|
||||||
_ = parse_glob::from_str(s)
|
_ = parse_glob::from_str(s);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -12,8 +12,8 @@ use rand::seq::SliceRandom;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
|
|
||||||
use libc::{dup, dup2, STDOUT_FILENO};
|
mod fuzz_common;
|
||||||
use std::process::Command;
|
use crate::fuzz_common::{generate_and_run_uumain, run_gnu_cmd};
|
||||||
|
|
||||||
#[derive(PartialEq, Debug, Clone)]
|
#[derive(PartialEq, Debug, Clone)]
|
||||||
enum ArgType {
|
enum ArgType {
|
||||||
|
@ -26,18 +26,7 @@ enum ArgType {
|
||||||
// Add any other types as needed
|
// Add any other types as needed
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_gnu_test(args: &[OsString]) -> Result<(String, i32), std::io::Error> {
|
static CMD_PATH: &str = "test";
|
||||||
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,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_random_string(max_length: usize) -> String {
|
fn generate_random_string(max_length: usize) -> String {
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
|
@ -153,7 +142,7 @@ fn generate_test_arg() -> String {
|
||||||
0 => {
|
0 => {
|
||||||
arg.push_str(&rng.gen_range(-100..=100).to_string());
|
arg.push_str(&rng.gen_range(-100..=100).to_string());
|
||||||
}
|
}
|
||||||
1 | 2 | 3 => {
|
1..=3 => {
|
||||||
let test_arg = test_args
|
let test_arg = test_args
|
||||||
.choose(&mut rng)
|
.choose(&mut rng)
|
||||||
.expect("Failed to choose a random test argument");
|
.expect("Failed to choose a random test argument");
|
||||||
|
@ -210,69 +199,23 @@ fuzz_target!(|_data: &[u8]| {
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
let max_args = rng.gen_range(1..=6);
|
let max_args = rng.gen_range(1..=6);
|
||||||
let mut args = vec![OsString::from("test")];
|
let mut args = vec![OsString::from("test")];
|
||||||
let uumain_exit_status;
|
|
||||||
|
|
||||||
for _ in 0..max_args {
|
for _ in 0..max_args {
|
||||||
args.push(OsString::from(generate_test_arg()));
|
args.push(OsString::from(generate_test_arg()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the original stdout file descriptor
|
let (rust_output, uumain_exit_status) = generate_and_run_uumain(&args, uumain);
|
||||||
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();
|
|
||||||
|
|
||||||
// Run GNU test with the provided arguments and compare the output
|
// 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)) => {
|
Ok((gnu_output, gnu_exit_status)) => {
|
||||||
let gnu_output = gnu_output.trim().to_owned();
|
let gnu_output = gnu_output.trim().to_owned();
|
||||||
println!("gnu_exit_status {}", gnu_exit_status);
|
println!("gnu_exit_status {}", gnu_exit_status);
|
||||||
println!("uumain_exit_status {}", uumain_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!("Discrepancy detected!");
|
||||||
println!("Test: {:?}", &args[1..]);
|
println!("Test: {:?}", &args[1..]);
|
||||||
println!("My output: {}", my_output);
|
println!("My output: {}", rust_output);
|
||||||
println!("GNU output: {}", gnu_output);
|
println!("GNU output: {}", gnu_output);
|
||||||
println!("My exit status: {}", uumain_exit_status);
|
println!("My exit status: {}", uumain_exit_status);
|
||||||
println!("GNU exit status: {}", gnu_exit_status);
|
println!("GNU exit status: {}", gnu_exit_status);
|
||||||
|
|
|
@ -335,9 +335,7 @@ impl Chmoder {
|
||||||
let mut new_mode = fperm;
|
let mut new_mode = fperm;
|
||||||
let mut naively_expected_new_mode = new_mode;
|
let mut naively_expected_new_mode = new_mode;
|
||||||
for mode in cmode_unwrapped.split(',') {
|
for mode in cmode_unwrapped.split(',') {
|
||||||
// cmode is guaranteed to be Some in this case
|
let result = if mode.chars().any(|c| c.is_ascii_digit()) {
|
||||||
let arr: &[char] = &['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
|
||||||
let result = if mode.contains(arr) {
|
|
||||||
mode::parse_numeric(new_mode, mode, file.is_dir()).map(|v| (v, v))
|
mode::parse_numeric(new_mode, mode, file.is_dir()).map(|v| (v, v))
|
||||||
} else {
|
} else {
|
||||||
mode::parse_symbolic(new_mode, mode, get_umask(), file.is_dir()).map(|m| {
|
mode::parse_symbolic(new_mode, mode, get_umask(), file.is_dir()).map(|m| {
|
||||||
|
@ -352,20 +350,22 @@ impl Chmoder {
|
||||||
(m, naive_mode)
|
(m, naive_mode)
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok((mode, naive_mode)) => {
|
Ok((mode, naive_mode)) => {
|
||||||
new_mode = mode;
|
new_mode = mode;
|
||||||
naively_expected_new_mode = naive_mode;
|
naively_expected_new_mode = naive_mode;
|
||||||
}
|
}
|
||||||
Err(f) => {
|
Err(f) => {
|
||||||
if self.quiet {
|
return if self.quiet {
|
||||||
return Err(ExitCode::new(1));
|
Err(ExitCode::new(1))
|
||||||
} else {
|
} else {
|
||||||
return Err(USimpleError::new(1, f));
|
Err(USimpleError::new(1, f))
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.change_file(fperm, new_mode, file)?;
|
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 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 {
|
if (new_mode & !naively_expected_new_mode) != 0 {
|
||||||
|
|
|
@ -1289,23 +1289,16 @@ fn copy_source(
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OverwriteMode {
|
impl OverwriteMode {
|
||||||
fn verify(&self, path: &Path, verbose: bool) -> CopyResult<()> {
|
fn verify(&self, path: &Path) -> CopyResult<()> {
|
||||||
match *self {
|
match *self {
|
||||||
Self::NoClobber => {
|
Self::NoClobber => {
|
||||||
if verbose {
|
eprintln!("{}: not replacing {}", util_name(), path.quote());
|
||||||
println!("skipped {}", path.quote());
|
|
||||||
} else {
|
|
||||||
eprintln!("{}: not replacing {}", util_name(), path.quote());
|
|
||||||
}
|
|
||||||
Err(Error::NotAllFilesCopied)
|
Err(Error::NotAllFilesCopied)
|
||||||
}
|
}
|
||||||
Self::Interactive(_) => {
|
Self::Interactive(_) => {
|
||||||
if prompt_yes!("overwrite {}?", path.quote()) {
|
if prompt_yes!("overwrite {}?", path.quote()) {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
if verbose {
|
|
||||||
println!("skipped {}", path.quote());
|
|
||||||
}
|
|
||||||
Err(Error::Skipped)
|
Err(Error::Skipped)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1513,7 +1506,7 @@ fn handle_existing_dest(
|
||||||
return Err(format!("{} and {} are the same file", source.quote(), dest.quote()).into());
|
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);
|
let backup_path = backup_control::get_backup_path(options.backup, dest, &options.backup_suffix);
|
||||||
if let Some(backup_path) = backup_path {
|
if let Some(backup_path) = backup_path {
|
||||||
|
@ -1926,7 +1919,7 @@ fn copy_helper(
|
||||||
File::create(dest).context(dest.display().to_string())?;
|
File::create(dest).context(dest.display().to_string())?;
|
||||||
} else if source_is_fifo && options.recursive && !options.copy_contents {
|
} else if source_is_fifo && options.recursive && !options.copy_contents {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
copy_fifo(dest, options.overwrite, options.verbose)?;
|
copy_fifo(dest, options.overwrite)?;
|
||||||
} else if source_is_symlink {
|
} else if source_is_symlink {
|
||||||
copy_link(source, dest, symlinked_files)?;
|
copy_link(source, dest, symlinked_files)?;
|
||||||
} else {
|
} else {
|
||||||
|
@ -1951,9 +1944,9 @@ fn copy_helper(
|
||||||
// "Copies" a FIFO by creating a new one. This workaround is because Rust's
|
// "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).
|
// built-in fs::copy does not handle FIFOs (see rust-lang/rust/issues/79390).
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
fn copy_fifo(dest: &Path, overwrite: OverwriteMode, verbose: bool) -> CopyResult<()> {
|
fn copy_fifo(dest: &Path, overwrite: OverwriteMode) -> CopyResult<()> {
|
||||||
if dest.exists() {
|
if dest.exists() {
|
||||||
overwrite.verify(dest, verbose)?;
|
overwrite.verify(dest)?;
|
||||||
fs::remove_file(dest)?;
|
fs::remove_file(dest)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -63,8 +63,15 @@ pub(crate) fn copy_on_write(
|
||||||
{
|
{
|
||||||
// clonefile(2) fails if the destination exists. Remove it and try again. Do not
|
// 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.
|
// bother to check if removal worked because we're going to try to clone again.
|
||||||
let _ = fs::remove_file(dest);
|
// first lets make sure the dest file is not read only
|
||||||
error = pfn(src.as_ptr(), dst.as_ptr(), 0);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
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)
|
DateSource::Human(duration)
|
||||||
} else {
|
} else {
|
||||||
DateSource::Custom(date.into())
|
DateSource::Custom(date.into())
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
use clap::{crate_version, Arg, ArgAction, Command};
|
use clap::{crate_version, Arg, ArgAction, Command};
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
use std::iter::Peekable;
|
use std::iter::Peekable;
|
||||||
|
use std::ops::ControlFlow;
|
||||||
use std::str::Chars;
|
use std::str::Chars;
|
||||||
use uucore::error::{FromIo, UResult};
|
use uucore::error::{FromIo, UResult};
|
||||||
use uucore::{format_usage, help_about, help_section, help_usage};
|
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";
|
pub const DISABLE_BACKSLASH_ESCAPE: &str = "disable_backslash_escape";
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_code(
|
#[repr(u8)]
|
||||||
input: &mut Peekable<Chars>,
|
#[derive(Clone, Copy)]
|
||||||
base: u32,
|
enum Base {
|
||||||
max_digits: u32,
|
Oct = 8,
|
||||||
bits_per_digit: u32,
|
Hex = 16,
|
||||||
) -> 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,
|
|
||||||
None => break,
|
|
||||||
}
|
|
||||||
input.next();
|
|
||||||
}
|
|
||||||
std::char::from_u32(ret)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_escaped(input: &str, mut output: impl Write) -> io::Result<bool> {
|
impl Base {
|
||||||
let mut should_stop = false;
|
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<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;
|
||||||
|
|
||||||
// TODO `cargo +nightly clippy` complains that `.peek()` is never
|
// We can safely ignore the None case because we just peeked it.
|
||||||
// called on `iter`. However, `peek()` is called inside the
|
let _ = input.next();
|
||||||
// `parse_code()` function that borrows `iter`.
|
|
||||||
|
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<ControlFlow<()>> {
|
||||||
let mut iter = input.chars().peekable();
|
let mut iter = input.chars().peekable();
|
||||||
while let Some(mut c) = iter.next() {
|
while let Some(c) = iter.next() {
|
||||||
let mut start = 1;
|
if c != '\\' {
|
||||||
|
write!(output, "{c}")?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if c == '\\' {
|
// This is for the \NNN syntax for octal sequences.
|
||||||
if let Some(next) = iter.next() {
|
// Note that '0' is intentionally omitted because that
|
||||||
c = match next {
|
// would be the \0NNN syntax.
|
||||||
'\\' => '\\',
|
if let Some('1'..='8') = iter.peek() {
|
||||||
'a' => '\x07',
|
if let Some(parsed) = parse_code(&mut iter, Base::Oct) {
|
||||||
'b' => '\x08',
|
write!(output, "{parsed}")?;
|
||||||
'c' => {
|
continue;
|
||||||
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
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer[1] = c;
|
if let Some(next) = iter.next() {
|
||||||
|
let unescaped = match next {
|
||||||
// because printing char slices is apparently not available in the standard library
|
'\\' => '\\',
|
||||||
for ch in &buffer[start..] {
|
'a' => '\x07',
|
||||||
write!(output, "{ch}")?;
|
'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]
|
#[uucore::main]
|
||||||
|
@ -148,9 +174,8 @@ fn execute(no_newline: bool, escaped: bool, free: &[String]) -> io::Result<()> {
|
||||||
write!(output, " ")?;
|
write!(output, " ")?;
|
||||||
}
|
}
|
||||||
if escaped {
|
if escaped {
|
||||||
let should_stop = print_escaped(input, &mut output)?;
|
if print_escaped(input, &mut output)?.is_break() {
|
||||||
if should_stop {
|
return Ok(());
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
write!(output, "{input}")?;
|
write!(output, "{input}")?;
|
||||||
|
|
|
@ -131,16 +131,8 @@ fn parse_arguments(args: impl uucore::Args) -> UResult<(Vec<String>, FmtOptions)
|
||||||
fmt_opts.use_anti_prefix = true;
|
fmt_opts.use_anti_prefix = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(s) = matches.get_one::<String>(OPT_WIDTH) {
|
if let Some(width) = matches.get_one::<usize>(OPT_WIDTH) {
|
||||||
fmt_opts.width = match s.parse::<usize>() {
|
fmt_opts.width = *width;
|
||||||
Ok(t) => t,
|
|
||||||
Err(e) => {
|
|
||||||
return Err(USimpleError::new(
|
|
||||||
1,
|
|
||||||
format!("Invalid WIDTH specification: {}: {}", s.quote(), e),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if fmt_opts.width > MAX_WIDTH {
|
if fmt_opts.width > MAX_WIDTH {
|
||||||
return Err(USimpleError::new(
|
return Err(USimpleError::new(
|
||||||
1,
|
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) {
|
if let Some(goal) = matches.get_one::<usize>(OPT_GOAL) {
|
||||||
fmt_opts.goal = match s.parse::<usize>() {
|
fmt_opts.goal = *goal;
|
||||||
Ok(t) => t,
|
|
||||||
Err(e) => {
|
|
||||||
return Err(USimpleError::new(
|
|
||||||
1,
|
|
||||||
format!("Invalid GOAL specification: {}: {}", s.quote(), e),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if !matches.contains_id(OPT_WIDTH) {
|
if !matches.contains_id(OPT_WIDTH) {
|
||||||
fmt_opts.width = cmp::max(
|
fmt_opts.width = cmp::max(
|
||||||
fmt_opts.goal * 100 / DEFAULT_GOAL_TO_WIDTH_RATIO,
|
fmt_opts.goal * 100 / DEFAULT_GOAL_TO_WIDTH_RATIO,
|
||||||
|
@ -372,14 +356,16 @@ pub fn uu_app() -> Command {
|
||||||
.short('w')
|
.short('w')
|
||||||
.long("width")
|
.long("width")
|
||||||
.help("Fill output lines up to a maximum of WIDTH columns, default 75.")
|
.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(
|
||||||
Arg::new(OPT_GOAL)
|
Arg::new(OPT_GOAL)
|
||||||
.short('g')
|
.short('g')
|
||||||
.long("goal")
|
.long("goal")
|
||||||
.help("Goal width, default of 93% of WIDTH. Must be less than WIDTH.")
|
.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(
|
||||||
Arg::new(OPT_QUICK)
|
Arg::new(OPT_QUICK)
|
||||||
|
|
|
@ -9,10 +9,7 @@ use uucore::mode;
|
||||||
|
|
||||||
/// Takes a user-supplied string and tries to parse to u16 mode bitmask.
|
/// 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> {
|
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'];
|
if mode_string.chars().any(|c| c.is_ascii_digit()) {
|
||||||
|
|
||||||
// Passing 000 as the existing permissions seems to mirror GNU behavior.
|
|
||||||
if mode_string.contains(numbers) {
|
|
||||||
mode::parse_numeric(0, mode_string, considering_dir)
|
mode::parse_numeric(0, mode_string, considering_dir)
|
||||||
} else {
|
} else {
|
||||||
mode::parse_symbolic(0, mode_string, umask, considering_dir)
|
mode::parse_symbolic(0, mode_string, umask, considering_dir)
|
||||||
|
|
|
@ -38,31 +38,27 @@ fn get_mode(_matches: &ArgMatches, _mode_had_minus_prefix: bool) -> Result<u32,
|
||||||
|
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
fn get_mode(matches: &ArgMatches, mode_had_minus_prefix: bool) -> Result<u32, String> {
|
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
|
// Not tested on Windows
|
||||||
let mut new_mode = DEFAULT_PERM;
|
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(',') {
|
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)?;
|
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 {
|
} else {
|
||||||
let cmode = if mode_had_minus_prefix {
|
mode.to_string()
|
||||||
// clap parsing is finished, now put prefix back
|
};
|
||||||
format!("-{mode}")
|
new_mode = mode::parse_symbolic(new_mode, &cmode, mode::get_umask(), true)?;
|
||||||
} else {
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 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> {
|
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.chars().any(|c| c.is_ascii_digit()) {
|
||||||
let result = if mode.contains(arr) {
|
|
||||||
mode::parse_numeric(MODE_RW_UGO as u32, mode)
|
mode::parse_numeric(MODE_RW_UGO as u32, mode)
|
||||||
} else {
|
} else {
|
||||||
mode::parse_symbolic(MODE_RW_UGO as u32, mode, true)
|
mode::parse_symbolic(MODE_RW_UGO as u32, mode, true)
|
||||||
|
|
|
@ -448,19 +448,11 @@ fn rename(
|
||||||
|
|
||||||
match b.overwrite {
|
match b.overwrite {
|
||||||
OverwriteMode::NoClobber => {
|
OverwriteMode::NoClobber => {
|
||||||
let err_msg = if b.verbose {
|
let err_msg = format!("not replacing {}", to.quote());
|
||||||
println!("skipped {}", to.quote());
|
|
||||||
String::new()
|
|
||||||
} else {
|
|
||||||
format!("not replacing {}", to.quote())
|
|
||||||
};
|
|
||||||
return Err(io::Error::new(io::ErrorKind::Other, err_msg));
|
return Err(io::Error::new(io::ErrorKind::Other, err_msg));
|
||||||
}
|
}
|
||||||
OverwriteMode::Interactive => {
|
OverwriteMode::Interactive => {
|
||||||
if !prompt_yes!("overwrite {}?", to.quote()) {
|
if !prompt_yes!("overwrite {}?", to.quote()) {
|
||||||
if b.verbose {
|
|
||||||
println!("skipped {}", to.quote());
|
|
||||||
}
|
|
||||||
return Err(io::Error::new(io::ErrorKind::Other, ""));
|
return Err(io::Error::new(io::ErrorKind::Other, ""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,15 @@ pub fn parse_options(settings: &mut crate::Settings, opts: &clap::ArgMatches) ->
|
||||||
// This vector holds error messages encountered.
|
// This vector holds error messages encountered.
|
||||||
let mut errs: Vec<String> = vec![];
|
let mut errs: Vec<String> = vec![];
|
||||||
settings.renumber = opts.get_flag(options::NO_RENUMBER);
|
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) {
|
if let Some(val) = opts.get_one::<String>(options::NUMBER_SEPARATOR) {
|
||||||
settings.number_separator = val.to_owned();
|
settings.number_separator = val.to_owned();
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ pub struct Settings {
|
||||||
body_numbering: NumberingStyle,
|
body_numbering: NumberingStyle,
|
||||||
footer_numbering: NumberingStyle,
|
footer_numbering: NumberingStyle,
|
||||||
// The variable corresponding to -d
|
// The variable corresponding to -d
|
||||||
section_delimiter: [char; 2],
|
section_delimiter: String,
|
||||||
// The variables corresponding to the options -v, -i, -l, -w.
|
// The variables corresponding to the options -v, -i, -l, -w.
|
||||||
starting_line_number: i64,
|
starting_line_number: i64,
|
||||||
line_increment: i64,
|
line_increment: i64,
|
||||||
|
@ -43,7 +43,7 @@ impl Default for Settings {
|
||||||
header_numbering: NumberingStyle::None,
|
header_numbering: NumberingStyle::None,
|
||||||
body_numbering: NumberingStyle::NonEmpty,
|
body_numbering: NumberingStyle::NonEmpty,
|
||||||
footer_numbering: NumberingStyle::None,
|
footer_numbering: NumberingStyle::None,
|
||||||
section_delimiter: ['\\', ':'],
|
section_delimiter: String::from("\\:"),
|
||||||
starting_line_number: 1,
|
starting_line_number: 1,
|
||||||
line_increment: 1,
|
line_increment: 1,
|
||||||
join_blank_lines: 1,
|
join_blank_lines: 1,
|
||||||
|
@ -56,14 +56,14 @@ impl Default for Settings {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Stats {
|
struct Stats {
|
||||||
line_number: i64,
|
line_number: Option<i64>,
|
||||||
consecutive_empty_lines: u64,
|
consecutive_empty_lines: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Stats {
|
impl Stats {
|
||||||
fn new(starting_line_number: i64) -> Self {
|
fn new(starting_line_number: i64) -> Self {
|
||||||
Self {
|
Self {
|
||||||
line_number: starting_line_number,
|
line_number: Some(starting_line_number),
|
||||||
consecutive_empty_lines: 0,
|
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 mod options {
|
||||||
pub const HELP: &str = "help";
|
pub const HELP: &str = "help";
|
||||||
pub const FILE: &str = "file";
|
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;
|
stats.consecutive_empty_lines = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// FIXME section delimiters are hardcoded and settings.section_delimiter is ignored
|
let new_numbering_style = match SectionDelimiter::parse(&line, &settings.section_delimiter)
|
||||||
// because --section-delimiter is not correctly implemented yet
|
{
|
||||||
let _ = settings.section_delimiter; // XXX suppress "field never read" warning
|
Some(SectionDelimiter::Header) => Some(&settings.header_numbering),
|
||||||
let new_numbering_style = match line.as_str() {
|
Some(SectionDelimiter::Body) => Some(&settings.body_numbering),
|
||||||
"\\:\\:\\:" => Some(&settings.header_numbering),
|
Some(SectionDelimiter::Footer) => Some(&settings.footer_numbering),
|
||||||
"\\:\\:" => Some(&settings.body_numbering),
|
None => None,
|
||||||
"\\:" => Some(&settings.footer_numbering),
|
|
||||||
_ => None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(new_style) = new_numbering_style {
|
if let Some(new_style) = new_numbering_style {
|
||||||
current_numbering_style = new_style;
|
current_numbering_style = new_style;
|
||||||
if settings.renumber {
|
if settings.renumber {
|
||||||
stats.line_number = settings.starting_line_number;
|
stats.line_number = Some(settings.starting_line_number);
|
||||||
}
|
}
|
||||||
println!();
|
println!();
|
||||||
} else {
|
} else {
|
||||||
|
@ -340,18 +364,21 @@ fn nl<T: Read>(reader: &mut BufReader<T>, stats: &mut Stats, settings: &Settings
|
||||||
};
|
};
|
||||||
|
|
||||||
if is_line_numbered {
|
if is_line_numbered {
|
||||||
|
let Some(line_number) = stats.line_number else {
|
||||||
|
return Err(USimpleError::new(1, "line number overflow"));
|
||||||
|
};
|
||||||
println!(
|
println!(
|
||||||
"{}{}{}",
|
"{}{}{}",
|
||||||
settings
|
settings
|
||||||
.number_format
|
.number_format
|
||||||
.format(stats.line_number, settings.number_width),
|
.format(line_number, settings.number_width),
|
||||||
settings.number_separator,
|
settings.number_separator,
|
||||||
line
|
line
|
||||||
);
|
);
|
||||||
// update line number for the potential next line
|
// update line number for the potential next line
|
||||||
match stats.line_number.checked_add(settings.line_increment) {
|
match line_number.checked_add(settings.line_increment) {
|
||||||
Some(new_line_number) => stats.line_number = new_line_number,
|
Some(new_line_number) => stats.line_number = Some(new_line_number),
|
||||||
None => return Err(USimpleError::new(1, "line number overflow")),
|
None => stats.line_number = None, // overflow
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let spaces = " ".repeat(settings.number_width + 1);
|
let spaces = " ".repeat(settings.number_width + 1);
|
||||||
|
|
|
@ -73,30 +73,13 @@ fn parse_no_decimal_no_exponent(s: &str) -> Result<PreciseNumber, ParseNumberErr
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
// Possibly "NaN" or "inf".
|
// Possibly "NaN" or "inf".
|
||||||
//
|
let float_val = match s.to_ascii_lowercase().as_str() {
|
||||||
// TODO In Rust v1.53.0, this change
|
"inf" | "infinity" => ExtendedBigDecimal::Infinity,
|
||||||
// https://github.com/rust-lang/rust/pull/78618 improves the
|
"-inf" | "-infinity" => ExtendedBigDecimal::MinusInfinity,
|
||||||
// parsing of floats to include being able to parse "NaN"
|
"nan" | "-nan" => return Err(ParseNumberError::Nan),
|
||||||
// and "inf". So when the minimum version of this crate is
|
_ => return Err(ParseNumberError::Float),
|
||||||
// increased to 1.53.0, we should just use the built-in
|
};
|
||||||
// `f32` parsing instead.
|
Ok(PreciseNumber::new(Number::Float(float_val), 0, 0))
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -483,11 +466,23 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_inf() {
|
fn test_parse_inf() {
|
||||||
assert_eq!(parse("inf"), 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::Infinity));
|
assert_eq!(parse("+inf"), Number::Float(ExtendedBigDecimal::Infinity));
|
||||||
|
assert_eq!(
|
||||||
|
parse("+infinity"),
|
||||||
|
Number::Float(ExtendedBigDecimal::Infinity)
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse("-inf"),
|
parse("-inf"),
|
||||||
Number::Float(ExtendedBigDecimal::MinusInfinity)
|
Number::Float(ExtendedBigDecimal::MinusInfinity)
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse("-infinity"),
|
||||||
|
Number::Float(ExtendedBigDecimal::MinusInfinity)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -11,3 +11,16 @@ Create output files containing consecutive or interleaved sections of input
|
||||||
## After Help
|
## 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.
|
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
|
||||||
|
|
|
@ -11,7 +11,7 @@ mod platform;
|
||||||
|
|
||||||
use crate::filenames::FilenameIterator;
|
use crate::filenames::FilenameIterator;
|
||||||
use crate::filenames::SuffixType;
|
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::env;
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
@ -39,6 +39,7 @@ static OPT_HEX_SUFFIXES_SHORT: &str = "-x";
|
||||||
static OPT_SUFFIX_LENGTH: &str = "suffix-length";
|
static OPT_SUFFIX_LENGTH: &str = "suffix-length";
|
||||||
static OPT_DEFAULT_SUFFIX_LENGTH: &str = "0";
|
static OPT_DEFAULT_SUFFIX_LENGTH: &str = "0";
|
||||||
static OPT_VERBOSE: &str = "verbose";
|
static OPT_VERBOSE: &str = "verbose";
|
||||||
|
static OPT_SEPARATOR: &str = "separator";
|
||||||
//The ---io and ---io-blksize parameters are consumed and ignored.
|
//The ---io and ---io-blksize parameters are consumed and ignored.
|
||||||
//The parameter is included to make GNU coreutils tests pass.
|
//The parameter is included to make GNU coreutils tests pass.
|
||||||
static OPT_IO: &str = "-io";
|
static OPT_IO: &str = "-io";
|
||||||
|
@ -55,7 +56,6 @@ const AFTER_HELP: &str = help_section!("after help", "split.md");
|
||||||
#[uucore::main]
|
#[uucore::main]
|
||||||
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||||
let (args, obs_lines) = handle_obsolete(args);
|
let (args, obs_lines) = handle_obsolete(args);
|
||||||
|
|
||||||
let matches = uu_app().try_get_matches_from(args)?;
|
let matches = uu_app().try_get_matches_from(args)?;
|
||||||
|
|
||||||
match Settings::from(&matches, &obs_lines) {
|
match Settings::from(&matches, &obs_lines) {
|
||||||
|
@ -145,6 +145,7 @@ fn should_extract_obs_lines(
|
||||||
&& !slice.starts_with("-C")
|
&& !slice.starts_with("-C")
|
||||||
&& !slice.starts_with("-l")
|
&& !slice.starts_with("-l")
|
||||||
&& !slice.starts_with("-n")
|
&& !slice.starts_with("-n")
|
||||||
|
&& !slice.starts_with("-t")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper function to [`filter_args`]
|
/// Helper function to [`filter_args`]
|
||||||
|
@ -208,13 +209,18 @@ fn handle_preceding_options(
|
||||||
|| &slice[2..] == OPT_ADDITIONAL_SUFFIX
|
|| &slice[2..] == OPT_ADDITIONAL_SUFFIX
|
||||||
|| &slice[2..] == OPT_FILTER
|
|| &slice[2..] == OPT_FILTER
|
||||||
|| &slice[2..] == OPT_NUMBER
|
|| &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)
|
// 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
|
// following slice should be treaded as value for this option
|
||||||
// even if it starts with '-' (which would be treated as hyphen prefixed value)
|
// even if it starts with '-' (which would be treated as hyphen prefixed value)
|
||||||
*preceding_short_opt_req_value =
|
*preceding_short_opt_req_value = slice == "-b"
|
||||||
slice == "-b" || slice == "-C" || slice == "-l" || slice == "-n" || slice == "-a";
|
|| slice == "-C"
|
||||||
|
|| slice == "-l"
|
||||||
|
|| slice == "-n"
|
||||||
|
|| slice == "-a"
|
||||||
|
|| slice == "-t";
|
||||||
// slice is a value
|
// slice is a value
|
||||||
// reset preceding option flags
|
// reset preceding option flags
|
||||||
if !slice.starts_with('-') {
|
if !slice.starts_with('-') {
|
||||||
|
@ -278,7 +284,7 @@ pub fn uu_app() -> Command {
|
||||||
.long(OPT_FILTER)
|
.long(OPT_FILTER)
|
||||||
.allow_hyphen_values(true)
|
.allow_hyphen_values(true)
|
||||||
.value_name("COMMAND")
|
.value_name("COMMAND")
|
||||||
.value_hint(clap::ValueHint::CommandName)
|
.value_hint(ValueHint::CommandName)
|
||||||
.help(
|
.help(
|
||||||
"write to shell COMMAND; file name is $FILE (Currently not implemented for Windows)",
|
"write to shell COMMAND; file name is $FILE (Currently not implemented for Windows)",
|
||||||
),
|
),
|
||||||
|
@ -293,7 +299,7 @@ pub fn uu_app() -> Command {
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new(OPT_NUMERIC_SUFFIXES_SHORT)
|
Arg::new(OPT_NUMERIC_SUFFIXES_SHORT)
|
||||||
.short('d')
|
.short('d')
|
||||||
.action(clap::ArgAction::SetTrue)
|
.action(ArgAction::SetTrue)
|
||||||
.overrides_with_all([
|
.overrides_with_all([
|
||||||
OPT_NUMERIC_SUFFIXES,
|
OPT_NUMERIC_SUFFIXES,
|
||||||
OPT_NUMERIC_SUFFIXES_SHORT,
|
OPT_NUMERIC_SUFFIXES_SHORT,
|
||||||
|
@ -314,12 +320,13 @@ pub fn uu_app() -> Command {
|
||||||
OPT_HEX_SUFFIXES,
|
OPT_HEX_SUFFIXES,
|
||||||
OPT_HEX_SUFFIXES_SHORT
|
OPT_HEX_SUFFIXES_SHORT
|
||||||
])
|
])
|
||||||
|
.value_name("FROM")
|
||||||
.help("same as -d, but allow setting the start value"),
|
.help("same as -d, but allow setting the start value"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new(OPT_HEX_SUFFIXES_SHORT)
|
Arg::new(OPT_HEX_SUFFIXES_SHORT)
|
||||||
.short('x')
|
.short('x')
|
||||||
.action(clap::ArgAction::SetTrue)
|
.action(ArgAction::SetTrue)
|
||||||
.overrides_with_all([
|
.overrides_with_all([
|
||||||
OPT_NUMERIC_SUFFIXES,
|
OPT_NUMERIC_SUFFIXES,
|
||||||
OPT_NUMERIC_SUFFIXES_SHORT,
|
OPT_NUMERIC_SUFFIXES_SHORT,
|
||||||
|
@ -340,6 +347,7 @@ pub fn uu_app() -> Command {
|
||||||
OPT_HEX_SUFFIXES,
|
OPT_HEX_SUFFIXES,
|
||||||
OPT_HEX_SUFFIXES_SHORT
|
OPT_HEX_SUFFIXES_SHORT
|
||||||
])
|
])
|
||||||
|
.value_name("FROM")
|
||||||
.help("same as -x, but allow setting the start value"),
|
.help("same as -x, but allow setting the start value"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
|
@ -357,6 +365,15 @@ pub fn uu_app() -> Command {
|
||||||
.help("print a diagnostic just before each output file is opened")
|
.help("print a diagnostic just before each output file is opened")
|
||||||
.action(ArgAction::SetTrue),
|
.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(
|
||||||
Arg::new(OPT_IO)
|
Arg::new(OPT_IO)
|
||||||
.long("io")
|
.long("io")
|
||||||
|
@ -372,7 +389,7 @@ pub fn uu_app() -> Command {
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new(ARG_INPUT)
|
Arg::new(ARG_INPUT)
|
||||||
.default_value("-")
|
.default_value("-")
|
||||||
.value_hint(clap::ValueHint::FilePath),
|
.value_hint(ValueHint::FilePath),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new(ARG_PREFIX)
|
Arg::new(ARG_PREFIX)
|
||||||
|
@ -696,6 +713,7 @@ struct Settings {
|
||||||
filter: Option<String>,
|
filter: Option<String>,
|
||||||
strategy: Strategy,
|
strategy: Strategy,
|
||||||
verbose: bool,
|
verbose: bool,
|
||||||
|
separator: u8,
|
||||||
|
|
||||||
/// Whether to *not* produce empty files when using `-n`.
|
/// 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
|
/// Suffix is not large enough to split into specified chunks
|
||||||
SuffixTooSmall(usize),
|
SuffixTooSmall(usize),
|
||||||
|
|
||||||
|
/// Multi-character (Invalid) separator
|
||||||
|
MultiCharacterSeparator(String),
|
||||||
|
|
||||||
|
/// Multiple different separator characters
|
||||||
|
MultipleSeparatorCharacters,
|
||||||
|
|
||||||
/// The `--filter` option is not supported on Windows.
|
/// The `--filter` option is not supported on Windows.
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
NotSupported,
|
NotSupported,
|
||||||
|
@ -743,6 +767,12 @@ impl fmt::Display for SettingsError {
|
||||||
Self::Strategy(e) => e.fmt(f),
|
Self::Strategy(e) => e.fmt(f),
|
||||||
Self::SuffixNotParsable(s) => write!(f, "invalid suffix length: {}", s.quote()),
|
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::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!(
|
Self::SuffixContainsSeparator(s) => write!(
|
||||||
f,
|
f,
|
||||||
"invalid suffix {}, contains directory separator",
|
"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 {
|
let result = Self {
|
||||||
suffix_length: suffix_length_str
|
suffix_length: suffix_length_str
|
||||||
.parse()
|
.parse()
|
||||||
|
@ -791,6 +841,7 @@ impl Settings {
|
||||||
suffix_start,
|
suffix_start,
|
||||||
additional_suffix,
|
additional_suffix,
|
||||||
verbose: matches.value_source("verbose") == Some(ValueSource::CommandLine),
|
verbose: matches.value_source("verbose") == Some(ValueSource::CommandLine),
|
||||||
|
separator,
|
||||||
strategy,
|
strategy,
|
||||||
input: matches.get_one::<String>(ARG_INPUT).unwrap().to_owned(),
|
input: matches.get_one::<String>(ARG_INPUT).unwrap().to_owned(),
|
||||||
prefix: matches.get_one::<String>(ARG_PREFIX).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.
|
// corresponds to the current chunk number.
|
||||||
let mut prev = 0;
|
let mut prev = 0;
|
||||||
let mut total_bytes_written = 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
|
// If we have exceeded the number of lines to write in the
|
||||||
// current chunk, then start a new chunk and its
|
// current chunk, then start a new chunk and its
|
||||||
// corresponding writer.
|
// corresponding writer.
|
||||||
|
@ -1036,8 +1088,8 @@ impl<'a> Write for LineChunkWriter<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the line, starting from *after* the previous
|
// Write the line, starting from *after* the previous
|
||||||
// newline character and ending *after* the current
|
// separator character and ending *after* the current
|
||||||
// newline character.
|
// separator character.
|
||||||
let n = self.inner.write(&buf[prev..i + 1])?;
|
let n = self.inner.write(&buf[prev..i + 1])?;
|
||||||
total_bytes_written += n;
|
total_bytes_written += n;
|
||||||
prev = i + 1;
|
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();
|
self.num_bytes_remaining_in_current_chunk = self.chunk_size.try_into().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the first newline character in the buffer.
|
// Find the first separator (default - newline character) in the buffer.
|
||||||
match memchr::memchr(b'\n', buf) {
|
let sep = self.settings.separator;
|
||||||
// If there is no newline character and the buffer is
|
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
|
// not empty, then write as many bytes as we can and
|
||||||
// then move on to the next chunk if necessary.
|
// then move on to the next chunk if necessary.
|
||||||
None => {
|
None => {
|
||||||
let end = self.num_bytes_remaining_in_current_chunk;
|
let end = self.num_bytes_remaining_in_current_chunk;
|
||||||
|
|
||||||
// This is ugly but here to match GNU behavior. If the input
|
// 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`.
|
// the second to last segment chunk. See `line-bytes.sh`.
|
||||||
if end == buf.len()
|
if end == buf.len()
|
||||||
&& self.num_bytes_remaining_in_current_chunk
|
&& self.num_bytes_remaining_in_current_chunk
|
||||||
< self.chunk_size.try_into().unwrap()
|
< 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;
|
self.num_bytes_remaining_in_current_chunk = 0;
|
||||||
} else {
|
} else {
|
||||||
|
@ -1200,8 +1253,8 @@ impl<'a> Write for LineBytesChunkWriter<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is a newline character and the line
|
// If there is a separator character and the line
|
||||||
// (including the newline character) will fit in the
|
// (including the separator character) will fit in the
|
||||||
// current chunk, then write the entire line and
|
// current chunk, then write the entire line and
|
||||||
// continue to the next iteration. (See chunk 1 in the
|
// continue to the next iteration. (See chunk 1 in the
|
||||||
// example comment above.)
|
// example comment above.)
|
||||||
|
@ -1212,8 +1265,8 @@ impl<'a> Write for LineBytesChunkWriter<'a> {
|
||||||
buf = &buf[num_bytes_written..];
|
buf = &buf[num_bytes_written..];
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is a newline character, the line
|
// If there is a separator character, the line
|
||||||
// (including the newline character) will not fit in
|
// (including the separator character) will not fit in
|
||||||
// the current chunk, *and* no other lines have been
|
// the current chunk, *and* no other lines have been
|
||||||
// written to the current chunk, then write as many
|
// written to the current chunk, then write as many
|
||||||
// bytes as we can and continue to the next
|
// bytes as we can and continue to the next
|
||||||
|
@ -1230,8 +1283,8 @@ impl<'a> Write for LineBytesChunkWriter<'a> {
|
||||||
buf = &buf[num_bytes_written..];
|
buf = &buf[num_bytes_written..];
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is a newline character, the line
|
// If there is a separator character, the line
|
||||||
// (including the newline character) will not fit in
|
// (including the separator character) will not fit in
|
||||||
// the current chunk, and at least one other line has
|
// the current chunk, and at least one other line has
|
||||||
// been written to the current chunk, then signal to
|
// been written to the current chunk, then signal to
|
||||||
// the next iteration that a new chunk needs to be
|
// 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 num_bytes_remaining_in_current_chunk = chunk_size;
|
||||||
let mut i = 0;
|
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 line = line_result.unwrap();
|
||||||
let maybe_writer = writers.get_mut(i);
|
let maybe_writer = writers.get_mut(i);
|
||||||
let writer = maybe_writer.unwrap();
|
let writer = maybe_writer.unwrap();
|
||||||
let bytes = line.as_bytes();
|
let bytes = line.as_slice();
|
||||||
writer.write_all(bytes)?;
|
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;
|
let num_bytes = bytes.len() + 1;
|
||||||
if num_bytes > num_bytes_remaining_in_current_chunk {
|
if num_bytes > num_bytes_remaining_in_current_chunk {
|
||||||
num_bytes_remaining_in_current_chunk = chunk_size;
|
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 num_bytes_remaining_in_current_chunk = chunk_size;
|
||||||
let mut i = 0;
|
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 line = line_result?;
|
||||||
let bytes = line.as_bytes();
|
let bytes = line.as_slice();
|
||||||
if i == chunk_number {
|
if i == chunk_number {
|
||||||
writer.write_all(bytes)?;
|
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;
|
let num_bytes = bytes.len() + 1;
|
||||||
if num_bytes >= num_bytes_remaining_in_current_chunk {
|
if num_bytes >= num_bytes_remaining_in_current_chunk {
|
||||||
num_bytes_remaining_in_current_chunk = chunk_size;
|
num_bytes_remaining_in_current_chunk = chunk_size;
|
||||||
|
@ -1601,13 +1656,14 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
let num_chunks: usize = num_chunks.try_into().unwrap();
|
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 line = line_result.unwrap();
|
||||||
let maybe_writer = writers.get_mut(i % num_chunks);
|
let maybe_writer = writers.get_mut(i % num_chunks);
|
||||||
let writer = maybe_writer.unwrap();
|
let writer = maybe_writer.unwrap();
|
||||||
let bytes = line.as_bytes();
|
let bytes = line.as_slice();
|
||||||
writer.write_all(bytes)?;
|
writer.write_all(bytes)?;
|
||||||
writer.write_all(b"\n")?;
|
writer.write_all(&[sep])?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -1632,7 +1688,7 @@ where
|
||||||
/// * [`split_into_n_chunks_by_line_round_robin`], which splits its input in the
|
/// * [`split_into_n_chunks_by_line_round_robin`], which splits its input in the
|
||||||
/// same way, but writes each chunk to its own file.
|
/// same way, but writes each chunk to its own file.
|
||||||
fn kth_chunk_by_line_round_robin<R>(
|
fn kth_chunk_by_line_round_robin<R>(
|
||||||
_settings: &Settings,
|
settings: &Settings,
|
||||||
reader: &mut R,
|
reader: &mut R,
|
||||||
chunk_number: u64,
|
chunk_number: u64,
|
||||||
num_chunks: u64,
|
num_chunks: u64,
|
||||||
|
@ -1646,12 +1702,13 @@ where
|
||||||
|
|
||||||
let num_chunks: usize = num_chunks.try_into().unwrap();
|
let num_chunks: usize = num_chunks.try_into().unwrap();
|
||||||
let chunk_number: usize = chunk_number.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 line = line_result?;
|
||||||
let bytes = line.as_bytes();
|
let bytes = line.as_slice();
|
||||||
if (i % num_chunks) == chunk_number {
|
if (i % num_chunks) == chunk_number {
|
||||||
writer.write_all(bytes)?;
|
writer.write_all(bytes)?;
|
||||||
writer.write_all(b"\n")?;
|
writer.write_all(&[sep])?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -68,6 +68,10 @@ fn datetime_to_filetime<T: TimeZone>(dt: &DateTime<T>) -> FileTime {
|
||||||
FileTime::from_unix_time(dt.timestamp(), dt.timestamp_subsec_nanos())
|
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]
|
#[uucore::main]
|
||||||
#[allow(clippy::cognitive_complexity)]
|
#[allow(clippy::cognitive_complexity)]
|
||||||
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||||
|
@ -88,35 +92,19 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||||
) {
|
) {
|
||||||
(Some(reference), Some(date)) => {
|
(Some(reference), Some(date)) => {
|
||||||
let (atime, mtime) = stat(Path::new(reference), !matches.get_flag(options::NO_DEREF))?;
|
let (atime, mtime) = stat(Path::new(reference), !matches.get_flag(options::NO_DEREF))?;
|
||||||
if let Ok(offset) = parse_datetime::from_str(date) {
|
let atime = filetime_to_datetime(&atime).ok_or_else(|| {
|
||||||
let seconds = offset.num_seconds();
|
USimpleError::new(1, "Could not process the reference access time")
|
||||||
let nanos = offset.num_nanoseconds().unwrap_or(0) % 1_000_000_000;
|
})?;
|
||||||
|
let mtime = filetime_to_datetime(&mtime).ok_or_else(|| {
|
||||||
let ref_atime_secs = atime.unix_seconds();
|
USimpleError::new(1, "Could not process the reference modification time")
|
||||||
let ref_atime_nanos = atime.nanoseconds();
|
})?;
|
||||||
let atime = FileTime::from_unix_time(
|
(parse_date(atime, date)?, parse_date(mtime, date)?)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
(Some(reference), None) => {
|
(Some(reference), None) => {
|
||||||
stat(Path::new(reference), !matches.get_flag(options::NO_DEREF))?
|
stat(Path::new(reference), !matches.get_flag(options::NO_DEREF))?
|
||||||
}
|
}
|
||||||
(None, Some(date)) => {
|
(None, Some(date)) => {
|
||||||
let timestamp = parse_date(date)?;
|
let timestamp = parse_date(Local::now(), date)?;
|
||||||
(timestamp, timestamp)
|
(timestamp, timestamp)
|
||||||
}
|
}
|
||||||
(None, None) => {
|
(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
|
// 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
|
// be any simple specification for what format this parameter allows and I'm
|
||||||
// not about to implement GNU parse_datetime.
|
// 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) {
|
if let Ok(dt) = parse_datetime::parse_datetime_at_date(ref_time, s) {
|
||||||
let dt = Local::now() + duration;
|
|
||||||
return Ok(datetime_to_filetime(&dt));
|
return Ok(datetime_to_filetime(&dt));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"))]
|
#[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 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.chars().any(|c| c.is_ascii_digit()) {
|
||||||
let result = if mode.contains(arr) {
|
|
||||||
parse_numeric(fperm, mode, true)
|
parse_numeric(fperm, mode, true)
|
||||||
} else {
|
} else {
|
||||||
parse_symbolic(fperm, mode, get_umask(), true)
|
parse_symbolic(fperm, mode, get_umask(), true)
|
||||||
|
|
|
@ -483,7 +483,8 @@ fn test_cp_arg_interactive_verbose() {
|
||||||
ucmd.args(&["-vi", "a", "b"])
|
ucmd.args(&["-vi", "a", "b"])
|
||||||
.pipe_in("N\n")
|
.pipe_in("N\n")
|
||||||
.fails()
|
.fails()
|
||||||
.stdout_is("skipped 'b'\n");
|
.stderr_is("cp: overwrite 'b'? ")
|
||||||
|
.no_stdout();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -494,7 +495,8 @@ fn test_cp_arg_interactive_verbose_clobber() {
|
||||||
at.touch("b");
|
at.touch("b");
|
||||||
ucmd.args(&["-vin", "a", "b"])
|
ucmd.args(&["-vin", "a", "b"])
|
||||||
.fails()
|
.fails()
|
||||||
.stdout_is("skipped 'b'\n");
|
.stderr_is("cp: not replacing 'b'\n")
|
||||||
|
.no_stdout();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -3466,3 +3468,19 @@ fn test_cp_only_source_no_target() {
|
||||||
panic!("Failure: stderr was \n{stderr_str}");
|
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");
|
||||||
|
}
|
||||||
|
|
|
@ -122,7 +122,7 @@ fn test_escape_no_further_output() {
|
||||||
new_ucmd!()
|
new_ucmd!()
|
||||||
.args(&["-e", "a\\cb", "c"])
|
.args(&["-e", "a\\cb", "c"])
|
||||||
.succeeds()
|
.succeeds()
|
||||||
.stdout_only("a\n");
|
.stdout_only("a");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -236,3 +236,47 @@ fn test_hyphen_values_between() {
|
||||||
.success()
|
.success()
|
||||||
.stdout_is("dumdum dum dum dum -e dum\n");
|
.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");
|
||||||
|
}
|
||||||
|
|
|
@ -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]
|
#[ignore]
|
||||||
#[test]
|
#[test]
|
||||||
fn test_fmt_goal() {
|
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]
|
#[test]
|
||||||
fn test_fmt_set_goal_not_contain_width() {
|
fn test_fmt_set_goal_not_contain_width() {
|
||||||
for param in ["-g", "--goal"] {
|
for param in ["-g", "--goal"] {
|
||||||
|
|
|
@ -1928,6 +1928,35 @@ fn test_ls_recursive() {
|
||||||
result.stdout_contains("a\\b:\nb");
|
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]
|
#[test]
|
||||||
fn test_ls_color() {
|
fn test_ls_color() {
|
||||||
let scene = TestScenario::new(util_name!());
|
let scene = TestScenario::new(util_name!());
|
||||||
|
|
|
@ -1323,7 +1323,7 @@ fn test_mv_interactive_error() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_mv_info_self() {
|
fn test_mv_into_self() {
|
||||||
let scene = TestScenario::new(util_name!());
|
let scene = TestScenario::new(util_name!());
|
||||||
let at = &scene.fixtures;
|
let at = &scene.fixtures;
|
||||||
let dir1 = "dir1";
|
let dir1 = "dir1";
|
||||||
|
@ -1350,7 +1350,7 @@ fn test_mv_arg_interactive_skipped() {
|
||||||
.ignore_stdin_write_error()
|
.ignore_stdin_write_error()
|
||||||
.fails()
|
.fails()
|
||||||
.stderr_is("mv: overwrite 'b'? ")
|
.stderr_is("mv: overwrite 'b'? ")
|
||||||
.stdout_is("skipped 'b'\n");
|
.no_stdout();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -1360,7 +1360,8 @@ fn test_mv_arg_interactive_skipped_vin() {
|
||||||
at.touch("b");
|
at.touch("b");
|
||||||
ucmd.args(&["-vin", "a", "b"])
|
ucmd.args(&["-vin", "a", "b"])
|
||||||
.fails()
|
.fails()
|
||||||
.stdout_is("skipped 'b'\n");
|
.stderr_is("mv: not replacing 'b'\n")
|
||||||
|
.no_stdout();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
//
|
//
|
||||||
// For the full copyright and license information, please view the LICENSE
|
// For the full copyright and license information, please view the LICENSE
|
||||||
// file that was distributed with this source code.
|
// 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;
|
use crate::common::util::TestScenario;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -537,3 +538,99 @@ fn test_line_number_overflow() {
|
||||||
.stdout_is(format!("{}\ta\n", i64::MIN))
|
.stdout_is(format!("{}\ta\n", i64::MIN))
|
||||||
.stderr_is("nl: line number overflow\n");
|
.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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -623,11 +623,21 @@ fn test_neg_inf() {
|
||||||
run(&["--", "-inf", "0"], b"-inf\n-inf\n-inf\n");
|
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]
|
#[test]
|
||||||
fn test_inf() {
|
fn test_inf() {
|
||||||
run(&["inf"], b"1\n2\n3\n");
|
run(&["inf"], b"1\n2\n3\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_infinity() {
|
||||||
|
run(&["infinity"], b"1\n2\n3\n");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_inf_width() {
|
fn test_inf_width() {
|
||||||
run(
|
run(
|
||||||
|
|
|
@ -1483,3 +1483,242 @@ fn test_split_non_utf8_argument_windows() {
|
||||||
.fails()
|
.fails()
|
||||||
.stderr_contains("error: invalid UTF-8 was detected in one or more arguments");
|
.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();
|
||||||
|
}
|
||||||
|
|
|
@ -844,3 +844,15 @@ fn test_touch_dash() {
|
||||||
|
|
||||||
ucmd.args(&["-h", "-"]).succeeds().no_stderr().no_stdout();
|
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
BIN
tests/fixtures/split/separator_nul.txt
vendored
Normal file
Binary file not shown.
1
tests/fixtures/split/separator_semicolon.txt
vendored
Normal file
1
tests/fixtures/split/separator_semicolon.txt
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
1;2;3;4;5;
|
Loading…
Add table
Add a link
Reference in a new issue