1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 03:27:44 +00:00

Merge branch 'main' into dd-seconds-precision-3

This commit is contained in:
jfinkels 2023-03-19 13:29:14 -04:00 committed by GitHub
commit 59d34ce667
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 1068 additions and 649 deletions

View file

@ -37,8 +37,6 @@ jobs:
## ToDO: [2021-11-10; rivy] 'Style/deps' needs more informative output and better integration of results into the GHA dashboard ## ToDO: [2021-11-10; rivy] 'Style/deps' needs more informative output and better integration of results into the GHA dashboard
name: Style/deps name: Style/deps
runs-on: ${{ matrix.job.os }} runs-on: ${{ matrix.job.os }}
# env:
# STYLE_FAIL_ON_FAULT: false # overrides workflow default
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -73,7 +71,6 @@ jobs:
run: | run: |
rustup toolchain install nightly --no-self-update --profile minimal rustup toolchain install nightly --no-self-update --profile minimal
rustup default nightly rustup default nightly
- uses: Swatinem/rust-cache@v2
- name: Install `cargo-udeps` - name: Install `cargo-udeps`
run: cargo install cargo-udeps run: cargo install cargo-udeps
env: env:
@ -93,8 +90,6 @@ jobs:
style_format: style_format:
name: Style/format name: Style/format
runs-on: ${{ matrix.job.os }} runs-on: ${{ matrix.job.os }}
# env:
# STYLE_FAIL_ON_FAULT: false # overrides workflow default
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -124,7 +119,6 @@ jobs:
## Install `rust` toolchain ## Install `rust` toolchain
rustup toolchain install stable --no-self-update -c rustfmt --profile minimal rustup toolchain install stable --no-self-update -c rustfmt --profile minimal
rustup default stable rustup default stable
- uses: Swatinem/rust-cache@v2
- name: "`cargo fmt` testing" - name: "`cargo fmt` testing"
shell: bash shell: bash
run: | run: |
@ -151,7 +145,7 @@ jobs:
- name: Install `cargo-fuzz` - name: Install `cargo-fuzz`
run: cargo install cargo-fuzz run: cargo install cargo-fuzz
- name: Run fuzz_date for XX seconds - name: Run fuzz_date for XX seconds
# TODO: fix the issues # TODO: fix https://github.com/uutils/coreutils/issues/4494
continue-on-error: true continue-on-error: true
shell: bash shell: bash
run: | run: |
@ -159,8 +153,6 @@ jobs:
cd fuzz cd fuzz
cargo +nightly fuzz run fuzz_date -- -max_total_time=${{ env.RUN_FOR }} -detect_leaks=0 cargo +nightly fuzz run fuzz_date -- -max_total_time=${{ env.RUN_FOR }} -detect_leaks=0
- name: Run fuzz_parse_glob for XX seconds - name: Run fuzz_parse_glob for XX seconds
# TODO: fix the issues
continue-on-error: true
shell: bash shell: bash
run: | run: |
## Run it ## Run it
@ -173,8 +165,6 @@ jobs:
cd fuzz cd fuzz
cargo +nightly fuzz run fuzz_parse_size -- -max_total_time=${{ env.RUN_FOR }} -detect_leaks=0 cargo +nightly fuzz run fuzz_parse_size -- -max_total_time=${{ env.RUN_FOR }} -detect_leaks=0
- name: Run fuzz_parse_time for XX seconds - name: Run fuzz_parse_time for XX seconds
# TODO: fix the issues
continue-on-error: true
shell: bash shell: bash
run: | run: |
## Run it ## Run it
@ -184,8 +174,9 @@ jobs:
style_lint: style_lint:
name: Style/lint name: Style/lint
runs-on: ${{ matrix.job.os }} runs-on: ${{ matrix.job.os }}
# env: env:
# STYLE_FAIL_ON_FAULT: false # overrides workflow default SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -195,6 +186,11 @@ jobs:
- { os: windows-latest , features: feat_os_windows } - { os: windows-latest , features: feat_os_windows }
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: Swatinem/rust-cache@v2
- name: Run sccache-cache
uses: mozilla-actions/sccache-action@v0.0.2
with:
version: "v0.4.0-pre.11"
- name: Initialize workflow variables - name: Initialize workflow variables
id: vars id: vars
shell: bash shell: bash
@ -229,7 +225,6 @@ jobs:
## Install `rust` toolchain ## Install `rust` toolchain
rustup toolchain install stable --no-self-update -c clippy --profile minimal rustup toolchain install stable --no-self-update -c clippy --profile minimal
rustup default stable rustup default stable
- uses: Swatinem/rust-cache@v2
- name: "`cargo clippy` lint testing" - name: "`cargo clippy` lint testing"
shell: bash shell: bash
run: | run: |
@ -244,15 +239,12 @@ jobs:
style_spellcheck: style_spellcheck:
name: Style/spelling name: Style/spelling
runs-on: ${{ matrix.job.os }} runs-on: ${{ matrix.job.os }}
# env:
# STYLE_FAIL_ON_FAULT: false # overrides workflow default
strategy: strategy:
matrix: matrix:
job: job:
- { os: ubuntu-latest , features: feat_os_unix } - { os: ubuntu-latest , features: feat_os_unix }
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: Swatinem/rust-cache@v2
- name: Initialize workflow variables - name: Initialize workflow variables
id: vars id: vars
shell: bash shell: bash
@ -292,6 +284,9 @@ jobs:
doc_warnings: doc_warnings:
name: Documentation/warnings name: Documentation/warnings
runs-on: ${{ matrix.job.os }} runs-on: ${{ matrix.job.os }}
env:
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -304,6 +299,11 @@ jobs:
# - { os: windows-latest , features: feat_os_windows } # - { os: windows-latest , features: feat_os_windows }
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: Swatinem/rust-cache@v2
- name: Run sccache-cache
uses: mozilla-actions/sccache-action@v0.0.2
with:
version: "v0.4.0-pre.11"
- name: Initialize workflow variables - name: Initialize workflow variables
id: vars id: vars
shell: bash shell: bash
@ -331,7 +331,6 @@ jobs:
## Install `rust` toolchain ## Install `rust` toolchain
rustup toolchain install stable --no-self-update -c clippy --profile minimal rustup toolchain install stable --no-self-update -c clippy --profile minimal
rustup default stable rustup default stable
- uses: Swatinem/rust-cache@v2
- name: "`cargo doc` with warnings" - name: "`cargo doc` with warnings"
shell: bash shell: bash
run: | run: |
@ -347,12 +346,20 @@ jobs:
min_version: min_version:
name: MinRustV # Minimum supported rust version (aka, MinSRV or MSRV) name: MinRustV # Minimum supported rust version (aka, MinSRV or MSRV)
runs-on: ${{ matrix.job.os }} runs-on: ${{ matrix.job.os }}
env:
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
strategy: strategy:
matrix: matrix:
job: job:
- { os: ubuntu-latest , features: feat_os_unix } - { os: ubuntu-latest , features: feat_os_unix }
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: Swatinem/rust-cache@v2
- name: Run sccache-cache
uses: mozilla-actions/sccache-action@v0.0.2
with:
version: "v0.4.0-pre.11"
- name: Initialize workflow variables - name: Initialize workflow variables
id: vars id: vars
shell: bash shell: bash
@ -369,7 +376,6 @@ jobs:
## Install `rust` toolchain (v${{ env.RUST_MIN_SRV }}) ## Install `rust` toolchain (v${{ env.RUST_MIN_SRV }})
rustup toolchain install --no-self-update ${{ env.RUST_MIN_SRV }} --profile minimal rustup toolchain install --no-self-update ${{ env.RUST_MIN_SRV }} --profile minimal
rustup default ${{ env.RUST_MIN_SRV }} rustup default ${{ env.RUST_MIN_SRV }}
- uses: Swatinem/rust-cache@v2
- name: Confirm MinSRV compatible 'Cargo.lock' - name: Confirm MinSRV compatible 'Cargo.lock'
shell: bash shell: bash
run: | run: |
@ -424,7 +430,6 @@ jobs:
## Install `rust` toolchain ## Install `rust` toolchain
rustup toolchain install stable --no-self-update --profile minimal rustup toolchain install stable --no-self-update --profile minimal
rustup default stable rustup default stable
- uses: Swatinem/rust-cache@v2
- name: "`cargo update` testing" - name: "`cargo update` testing"
shell: bash shell: bash
run: | run: |
@ -436,6 +441,9 @@ jobs:
name: Build/Makefile name: Build/Makefile
needs: [ min_version, deps ] needs: [ min_version, deps ]
runs-on: ${{ matrix.job.os }} runs-on: ${{ matrix.job.os }}
env:
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -443,12 +451,16 @@ jobs:
- { os: ubuntu-latest , features: feat_os_unix } - { os: ubuntu-latest , features: feat_os_unix }
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: Swatinem/rust-cache@v2
- name: Run sccache-cache
uses: mozilla-actions/sccache-action@v0.0.2
with:
version: "v0.4.0-pre.11"
- name: Install `rust` toolchain - name: Install `rust` toolchain
run: | run: |
## Install `rust` toolchain ## Install `rust` toolchain
rustup toolchain install stable --no-self-update --profile minimal rustup toolchain install stable --no-self-update --profile minimal
rustup default stable rustup default stable
- uses: Swatinem/rust-cache@v2
- name: "`make build`" - name: "`make build`"
shell: bash shell: bash
run: | run: |
@ -470,12 +482,14 @@ jobs:
env: env:
RUST_BACKTRACE: "1" RUST_BACKTRACE: "1"
build_rust_stable: build_rust_stable:
name: Build/stable name: Build/stable
needs: [ min_version, deps ] needs: [ min_version, deps ]
runs-on: ${{ matrix.job.os }} runs-on: ${{ matrix.job.os }}
timeout-minutes: 90 timeout-minutes: 90
env:
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -485,12 +499,16 @@ jobs:
- { os: windows-latest , features: feat_os_windows } - { os: windows-latest , features: feat_os_windows }
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: Swatinem/rust-cache@v2
- name: Run sccache-cache
uses: mozilla-actions/sccache-action@v0.0.2
with:
version: "v0.4.0-pre.11"
- name: Install `rust` toolchain - name: Install `rust` toolchain
run: | run: |
## Install `rust` toolchain ## Install `rust` toolchain
rustup toolchain install stable --no-self-update --profile minimal rustup toolchain install stable --no-self-update --profile minimal
rustup default stable rustup default stable
- uses: Swatinem/rust-cache@v2
- name: Test - name: Test
run: cargo test ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} run: cargo test ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }}
env: env:
@ -501,6 +519,9 @@ jobs:
needs: [ min_version, deps ] needs: [ min_version, deps ]
runs-on: ${{ matrix.job.os }} runs-on: ${{ matrix.job.os }}
timeout-minutes: 90 timeout-minutes: 90
env:
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -510,12 +531,16 @@ jobs:
- { os: windows-latest , features: feat_os_windows } - { os: windows-latest , features: feat_os_windows }
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: Swatinem/rust-cache@v2
- name: Run sccache-cache
uses: mozilla-actions/sccache-action@v0.0.2
with:
version: "v0.4.0-pre.11"
- name: Install `rust` toolchain - name: Install `rust` toolchain
run: | run: |
## Install `rust` toolchain ## Install `rust` toolchain
rustup toolchain install nightly --no-self-update --profile minimal rustup toolchain install nightly --no-self-update --profile minimal
rustup default nightly rustup default nightly
- uses: Swatinem/rust-cache@v2
- name: Test - name: Test
run: cargo test ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} run: cargo test ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }}
env: env:
@ -525,6 +550,9 @@ jobs:
name: Binary sizes name: Binary sizes
needs: [ min_version, deps ] needs: [ min_version, deps ]
runs-on: ${{ matrix.job.os }} runs-on: ${{ matrix.job.os }}
env:
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -532,6 +560,11 @@ jobs:
- { os: ubuntu-latest , features: feat_os_unix } - { os: ubuntu-latest , features: feat_os_unix }
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: Swatinem/rust-cache@v2
- name: Run sccache-cache
uses: mozilla-actions/sccache-action@v0.0.2
with:
version: "v0.4.0-pre.11"
- name: Install dependencies - name: Install dependencies
shell: bash shell: bash
run: | run: |
@ -543,7 +576,6 @@ jobs:
## Install `rust` toolchain ## Install `rust` toolchain
rustup toolchain install stable --no-self-update --profile minimal rustup toolchain install stable --no-self-update --profile minimal
rustup default stable rustup default stable
- uses: Swatinem/rust-cache@v2
- name: "`make install`" - name: "`make install`"
shell: bash shell: bash
run: | run: |
@ -579,6 +611,8 @@ jobs:
timeout-minutes: 90 timeout-minutes: 90
env: env:
DOCKER_OPTS: '--volume /etc/passwd:/etc/passwd --volume /etc/group:/etc/group' DOCKER_OPTS: '--volume /etc/passwd:/etc/passwd --volume /etc/group:/etc/group'
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -605,6 +639,11 @@ jobs:
- { os: windows-latest , target: x86_64-pc-windows-msvc , features: feat_os_windows } - { os: windows-latest , target: x86_64-pc-windows-msvc , features: feat_os_windows }
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: Swatinem/rust-cache@v2
- name: Run sccache-cache
uses: mozilla-actions/sccache-action@v0.0.2
with:
version: "v0.4.0-pre.11"
- name: Initialize workflow variables - name: Initialize workflow variables
id: vars id: vars
shell: bash shell: bash
@ -732,9 +771,6 @@ jobs:
## rust toolchain ~ install ## rust toolchain ~ install
rustup toolchain install --no-self-update ${{ env.RUST_MIN_SRV }} -t ${{ matrix.job.target }} --profile minimal rustup toolchain install --no-self-update ${{ env.RUST_MIN_SRV }} -t ${{ matrix.job.target }} --profile minimal
rustup default ${{ env.RUST_MIN_SRV }} rustup default ${{ env.RUST_MIN_SRV }}
- uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.job.os }}-${{ matrix.job.target }}
- name: Initialize toolchain-dependent workflow variables - name: Initialize toolchain-dependent workflow variables
id: dep_vars id: dep_vars
shell: bash shell: bash
@ -844,6 +880,9 @@ jobs:
name: Tests/BusyBox test suite name: Tests/BusyBox test suite
needs: [ min_version, deps ] needs: [ min_version, deps ]
runs-on: ${{ matrix.job.os }} runs-on: ${{ matrix.job.os }}
env:
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -858,6 +897,10 @@ jobs:
echo "TEST_SUMMARY_FILE=busybox-result.json" >> $GITHUB_OUTPUT echo "TEST_SUMMARY_FILE=busybox-result.json" >> $GITHUB_OUTPUT
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- name: Run sccache-cache
uses: mozilla-actions/sccache-action@v0.0.2
with:
version: "v0.4.0-pre.11"
- name: Install/setup prerequisites - name: Install/setup prerequisites
shell: bash shell: bash
run: | run: |
@ -917,6 +960,9 @@ jobs:
name: Tests/Toybox test suite name: Tests/Toybox test suite
needs: [ min_version, deps ] needs: [ min_version, deps ]
runs-on: ${{ matrix.job.os }} runs-on: ${{ matrix.job.os }}
env:
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -933,6 +979,10 @@ jobs:
outputs TEST_SUMMARY_FILE outputs TEST_SUMMARY_FILE
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- name: Run sccache-cache
uses: mozilla-actions/sccache-action@v0.0.2
with:
version: "v0.4.0-pre.11"
- name: rust toolchain ~ install - name: rust toolchain ~ install
run: | run: |
## rust toolchain ~ install ## rust toolchain ~ install
@ -1010,8 +1060,15 @@ jobs:
arch: [x86] # , arm64-v8a arch: [x86] # , arm64-v8a
env: env:
TERMUX: v0.118.0 TERMUX: v0.118.0
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: Swatinem/rust-cache@v2
- name: Run sccache-cache
uses: mozilla-actions/sccache-action@v0.0.2
with:
version: "v0.4.0-pre.11"
- name: AVD cache - name: AVD cache
uses: actions/cache@v3 uses: actions/cache@v3
id: avd-cache id: avd-cache
@ -1065,9 +1122,15 @@ jobs:
- { os: macos-12 , features: unix } ## GHA MacOS-11.0 VM won't have VirtualBox; refs: <https://github.com/actions/virtual-environments/issues/4060> , <https://github.com/actions/virtual-environments/pull/4010> - { os: macos-12 , features: unix } ## GHA MacOS-11.0 VM won't have VirtualBox; refs: <https://github.com/actions/virtual-environments/issues/4060> , <https://github.com/actions/virtual-environments/pull/4010>
env: env:
mem: 4096 mem: 4096
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- name: Run sccache-cache
uses: mozilla-actions/sccache-action@v0.0.2
with:
version: "v0.4.0-pre.11"
- name: Prepare, build and test - name: Prepare, build and test
## spell-checker:ignore (ToDO) sshfs usesh vmactions ## spell-checker:ignore (ToDO) sshfs usesh vmactions
uses: vmactions/freebsd-vm@v0.3.0 uses: vmactions/freebsd-vm@v0.3.0
@ -1132,6 +1195,9 @@ jobs:
name: Code Coverage name: Code Coverage
runs-on: ${{ matrix.job.os }} runs-on: ${{ matrix.job.os }}
timeout-minutes: 90 timeout-minutes: 90
env:
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -1141,6 +1207,11 @@ jobs:
- { os: windows-latest , features: windows } - { os: windows-latest , features: windows }
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: Swatinem/rust-cache@v2
- name: Run sccache-cache
uses: mozilla-actions/sccache-action@v0.0.2
with:
version: "v0.4.0-pre.11"
# - name: Reattach HEAD ## may be needed for accurate code coverage info # - name: Reattach HEAD ## may be needed for accurate code coverage info
# run: git checkout ${{ github.head_ref }} # run: git checkout ${{ github.head_ref }}
- name: Initialize workflow variables - name: Initialize workflow variables
@ -1194,7 +1265,6 @@ jobs:
## rust toolchain ~ install ## rust toolchain ~ install
rustup toolchain install ${{ steps.vars.outputs.TOOLCHAIN }} --no-self-update --profile minimal rustup toolchain install ${{ steps.vars.outputs.TOOLCHAIN }} --no-self-update --profile minimal
rustup default ${{ steps.vars.outputs.TOOLCHAIN }} rustup default ${{ steps.vars.outputs.TOOLCHAIN }}
- uses: Swatinem/rust-cache@v2
- name: Initialize toolchain-dependent workflow variables - name: Initialize toolchain-dependent workflow variables
id: dep_vars id: dep_vars
shell: bash shell: bash
@ -1260,3 +1330,4 @@ jobs:
flags: ${{ steps.vars.outputs.CODECOV_FLAGS }} flags: ${{ steps.vars.outputs.CODECOV_FLAGS }}
name: codecov-umbrella name: codecov-umbrella
fail_ci_if_error: false fail_ci_if_error: false

View file

@ -20,6 +20,7 @@ exacl
filetime filetime
formatteriteminfo formatteriteminfo
fsext fsext
fundu
getopts getopts
getrandom getrandom
globset globset

View file

@ -1,20 +1,12 @@
<!-- spell-checker:ignore reimplementing toybox RUNTEST -->
# Contributing to coreutils # Contributing to coreutils
Contributions are very welcome, and should target Rust's main branch until the Contributions are very welcome via Pull Requests. If you don't know where to
standard libraries are stabilized. You may *claim* an item on the to-do list by start, take a look at the
following these steps: [`good-first-issues`](https://github.com/uutils/coreutils/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22).
If you have any questions, feel free to ask them in the issues or on
1. Open an issue named "Implement [the utility of your choice]", e.g. "Implement [Discord](https://discord.gg/wQVJbvJ).
ls".
1. State that you are working on this utility.
1. Develop the utility.
1. Add integration tests.
1. Add the reference to your utility into Cargo.toml and Makefile.
1. Remove utility from the to-do list in the README.
1. Submit a pull request and close the issue.
The steps above imply that, before starting to work on a utility, you should
search the issues to make sure no one else is working on it.
## Best practices ## Best practices
@ -38,36 +30,240 @@ search the issues to make sure no one else is working on it.
## Platforms ## Platforms
We take pride in supporting many operating systems and architectures. We take pride in supporting many operating systems and architectures. Any code
you contribute must at least compile without warnings for all platforms in the
CI. However, you can use `#[cfg(...)]` attributes to create platform dependent features.
**Tip:** **Tip:** For Windows, Microsoft provides some images (VMWare, Hyper-V,
For Windows, Microsoft provides some images (VMWare, Hyper-V, VirtualBox and Parallels) VirtualBox and Parallels) for development:
for development:
<https://developer.microsoft.com/windows/downloads/virtual-machines/> <https://developer.microsoft.com/windows/downloads/virtual-machines/>
## Tools
We have an extensive CI that will check your code before it can be merged. This
section explains how to run those checks locally to avoid waiting for the CI.
### 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.
### 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
```
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
```
### 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
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
tests in the CI.
To improve the GNU compatibility, the following process is recommended:
1. Identify a test (the smaller, the better) on a program that you understand or
is easy to understand. You can use the `./util/remaining-gnu-error.py` script
to help with this decision.
1. Build both the GNU and Rust coreutils using: `bash util/build-gnu.sh`
1. Run the test with `bash util/run-gnu-test.sh <your test>`
1. Start to modify `<your test>` to understand what is wrong. Examples:
1. Add `set -v` to have the bash verbose mode
1. Add `echo $?` where needed
1. When the variable `fail` is used in the test, `echo $fail` to see when the
test started to fail
1. Bump the content of the output (ex: `cat err`)
1. ...
1. Or, if the test is simple, extract the relevant information to create a new
test case running both GNU & Rust implementation
1. Start to modify the Rust implementation to match the expected behavior
1. Add a test to make sure that we don't regress (our test suite is super quick)
## Commit messages ## Commit messages
To help the project maintainers review pull requests from contributors across To help the project maintainers review pull requests from contributors across
numerous utilities, the team has settled on conventions for commit messages. numerous utilities, the team has settled on conventions for commit messages.
From <http://git-scm.com/book/ch5-2.html>: From <https://git-scm.com/book/ch5-2.html>:
``` ```
Short (50 chars or less) summary of changes Capitalized, short (50 chars or less) summary
More detailed explanatory text, if necessary. Wrap it to about 72 More detailed explanatory text, if necessary. Wrap it to about 72
characters or so. In some contexts, the first line is treated as the characters or so. In some contexts, the first line is treated as the
subject of an email and the rest of the text as the body. The blank subject of an email and the rest of the text as the body. The blank
line separating the summary from the body is critical (unless you omit line separating the summary from the body is critical (unless you omit
the body entirely); tools like rebase can get confused if you run the the body entirely); tools like rebase will confuse you if you run the
two together. two together.
Write your commit message in the imperative: "Fix bug" and not "Fixed bug"
or "Fixes bug." This convention matches up with commit messages generated
by commands like git merge and git revert.
Further paragraphs come after blank lines. Further paragraphs come after blank lines.
- Bullet points are okay, too - Bullet points are okay, too
- Typically a hyphen or asterisk is used for the bullet, preceded by a - Typically a hyphen or asterisk is used for the bullet, followed by a
single space, with blank lines in between, but conventions vary here single space, with blank lines in between, but conventions vary here
- Use a hanging indent
``` ```
Furthermore, here are a few examples for a summary line: Furthermore, here are a few examples for a summary line:
@ -103,15 +299,49 @@ uutils: add new utility
gitignore: add temporary files gitignore: add temporary files
``` ```
## cargo-deny ## Code coverage
This project uses [cargo-deny](https://github.com/EmbarkStudios/cargo-deny/) to <!-- spell-checker:ignore (flags) Ccodegen Coverflow Cpanic Zinstrument Zpanic -->
detect duplicate dependencies, checks licenses, etc. To run it locally, first
install it and then run with:
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
``` ```
cargo deny --all-features check all
``` 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
The Coreutils have different implementations, with different levels of completions:
* [GNU's](https://git.savannah.gnu.org/gitweb/?p=coreutils.git)
* [OpenBSD](https://github.com/openbsd/src/tree/master/bin)
* [Busybox](https://github.com/mirror/busybox/tree/master/coreutils)
* [Toybox (Android)](https://github.com/landley/toybox/tree/master/toys/posix)
* [V lang](https://github.com/vlang/coreutils)
* [SerenityOS](https://github.com/SerenityOS/serenity/tree/master/Userland/Utilities)
* [Initial Unix](https://github.com/dspinellis/unix-history-repo)
However, when reimplementing the tools/options in Rust, don't read their source codes
when they are using reciprocal licenses (ex: GNU GPL, GNU LGPL, etc).
## Licensing ## Licensing

67
Cargo.lock generated
View file

@ -89,9 +89,9 @@ dependencies = [
[[package]] [[package]]
name = "bindgen" name = "bindgen"
version = "0.62.0" version = "0.63.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6720a8b7b2d39dd533285ed438d458f65b31b5c257e6ac7bb3d7e82844dd722" checksum = "36d860121800b2a9a94f9b5604b332d5cffb234ce17609ea479d723dbc9d3885"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"cexpr", "cexpr",
@ -870,9 +870,9 @@ dependencies = [
[[package]] [[package]]
name = "fts-sys" name = "fts-sys"
version = "0.2.3" version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32bd98333d10742c0b048272ebf4cb05336d415423b853961c92ccb398966a03" checksum = "9a66c0a21e344f20c87b4ca12643cf4f40a7018f132c98d344e989b959f49dd1"
dependencies = [ dependencies = [
"bindgen", "bindgen",
"libc", "libc",
@ -880,9 +880,9 @@ dependencies = [
[[package]] [[package]]
name = "fundu" name = "fundu"
version = "0.3.0" version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925250bc259498d4008ee072bf16586083ab2c491aa4b06b3c4d0a6556cebd74" checksum = "da58c38fe7b706cead98429d8a8535261addbe55fd531c7d7c7d770346464010"
[[package]] [[package]]
name = "futures" name = "futures"
@ -1139,12 +1139,6 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "io-lifetimes"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ce5ef949d49ee85593fc4d3f3f95ad61657076395cbbce23e2121fc5542074"
[[package]] [[package]]
name = "io-lifetimes" name = "io-lifetimes"
version = "1.0.5" version = "1.0.5"
@ -1162,8 +1156,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e18b0a45d56fe973d6db23972bf5bc46f988a4a2385deac9cc29572f09daef" checksum = "22e18b0a45d56fe973d6db23972bf5bc46f988a4a2385deac9cc29572f09daef"
dependencies = [ dependencies = [
"hermit-abi 0.3.1", "hermit-abi 0.3.1",
"io-lifetimes 1.0.5", "io-lifetimes",
"rustix 0.36.8", "rustix",
"windows-sys 0.45.0", "windows-sys 0.45.0",
] ]
@ -1257,12 +1251,6 @@ dependencies = [
"cc", "cc",
] ]
[[package]]
name = "linux-raw-sys"
version = "0.0.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4d2456c373231a208ad294c33dc5bff30051eafd954cd4caae83a712b12854d"
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.1.4" version = "0.1.4"
@ -1719,15 +1707,15 @@ dependencies = [
[[package]] [[package]]
name = "procfs" name = "procfs"
version = "0.14.1" version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dfb6451c91904606a1abe93e83a8ec851f45827fa84273f256ade45dc095818" checksum = "943ca7f9f29bab5844ecd8fdb3992c5969b6622bb9609b9502fef9b4310e3f1f"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"byteorder", "byteorder",
"hex", "hex",
"lazy_static", "lazy_static",
"rustix 0.35.13", "rustix",
] ]
[[package]] [[package]]
@ -1921,20 +1909,6 @@ dependencies = [
"semver", "semver",
] ]
[[package]]
name = "rustix"
version = "0.35.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "727a1a6d65f786ec22df8a81ca3121107f235970dc1705ed681d3e6e8b9cd5f9"
dependencies = [
"bitflags",
"errno",
"io-lifetimes 0.7.5",
"libc",
"linux-raw-sys 0.0.46",
"windows-sys 0.42.0",
]
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.36.8" version = "0.36.8"
@ -1943,9 +1917,9 @@ checksum = "f43abb88211988493c1abb44a70efa56ff0ce98f233b7b276146f1f3f7ba9644"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"errno", "errno",
"io-lifetimes 1.0.5", "io-lifetimes",
"libc", "libc",
"linux-raw-sys 0.1.4", "linux-raw-sys",
"windows-sys 0.45.0", "windows-sys 0.45.0",
] ]
@ -1992,9 +1966,9 @@ dependencies = [
[[package]] [[package]]
name = "selinux-sys" name = "selinux-sys"
version = "0.6.1" version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4c02c5c6e2db8a78b3ffffc666f75fcda5bbd7068ba3c0f560e5504f4d88443" checksum = "806d381649bb85347189d2350728817418138d11d738e2482cb644ec7f3c755d"
dependencies = [ dependencies = [
"bindgen", "bindgen",
"cc", "cc",
@ -2185,7 +2159,7 @@ dependencies = [
"cfg-if", "cfg-if",
"fastrand", "fastrand",
"redox_syscall", "redox_syscall",
"rustix 0.36.8", "rustix",
"windows-sys 0.42.0", "windows-sys 0.42.0",
] ]
@ -2209,12 +2183,12 @@ dependencies = [
[[package]] [[package]]
name = "terminal_size" name = "terminal_size"
version = "0.2.2" version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40ca90c434fd12083d1a6bdcbe9f92a14f96c8a1ba600ba451734ac334521f7a" checksum = "4c9afddd2cec1c0909f06b00ef33f94ab2cc0578c4a610aa208ddfec8aa2b43a"
dependencies = [ dependencies = [
"rustix 0.35.13", "rustix",
"windows-sys 0.42.0", "windows-sys 0.45.0",
] ]
[[package]] [[package]]
@ -3041,6 +3015,7 @@ name = "uu_sleep"
version = "0.0.17" version = "0.0.17"
dependencies = [ dependencies = [
"clap", "clap",
"fundu",
"uucore", "uucore",
] ]

View file

@ -282,7 +282,7 @@ filetime = "0.2"
fnv = "1.0.7" fnv = "1.0.7"
fs_extra = "1.1.0" fs_extra = "1.1.0"
fts-sys = "0.2" fts-sys = "0.2"
fundu = "0.3.0" fundu = "0.4.2"
gcd = "2.2" gcd = "2.2"
glob = "0.3.0" glob = "0.3.0"
half = "2.1" half = "2.1"
@ -320,7 +320,7 @@ strum = "0.24.1"
strum_macros = "0.24.2" strum_macros = "0.24.2"
tempfile = "3.4.0" tempfile = "3.4.0"
term_grid = "0.1.5" term_grid = "0.1.5"
terminal_size = "0.2.2" terminal_size = "0.2.5"
textwrap = { version="0.16.0", features=["terminal_size"] } textwrap = { version="0.16.0", features=["terminal_size"] }
thiserror = "1.0" thiserror = "1.0"
time = { version="0.3" } time = { version="0.3" }
@ -493,7 +493,7 @@ hex-literal = "0.3.1"
rstest = "0.16.0" rstest = "0.16.0"
[target.'cfg(any(target_os = "linux", target_os = "android"))'.dev-dependencies] [target.'cfg(any(target_os = "linux", target_os = "android"))'.dev-dependencies]
procfs = { version = "0.14", default-features = false } procfs = { version = "0.15", default-features = false }
rlimit = "0.9.1" rlimit = "0.9.1"
[target.'cfg(unix)'.dev-dependencies] [target.'cfg(unix)'.dev-dependencies]

View file

@ -1,68 +0,0 @@
# Documentation
The source of the documentation is available on:
<https://uutils.github.io/dev/coreutils/>
The documentation is updated everyday on this repository:
<https://github.com/uutils/uutils.github.io/>
## Running GNU tests
<!-- spell-checker:ignore gnulib -->
- Check out <https://github.com/coreutils/coreutils> next to your fork as gnu
- Check out <https://github.com/coreutils/gnulib> next to your fork as gnulib
- Rename the checkout of your fork to uutils
At the end you should have uutils, gnu and gnulib checked out next to each other.
- Run `cd uutils && ./util/build-gnu.sh && cd ..` to get everything ready (this may take a while)
- Finally, you can run tests with `bash uutils/util/run-gnu-test.sh <tests>`. Instead of `<tests>` insert the tests you want to run, e.g. `tests/misc/wc-proc.sh`.
## Code Coverage Report Generation
<!-- spell-checker:ignore (flags) Ccodegen Coverflow Cpanic Zinstrument Zpanic -->
Code coverage report can be generated using [grcov](https://github.com/mozilla/grcov).
### Using Nightly Rust
To generate [gcov-based](https://github.com/mozilla/grcov#example-how-to-generate-gcda-files-for-a-rust-project) coverage report
```shell
export CARGO_INCREMENTAL=0
export RUSTFLAGS="-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort"
export RUSTDOCFLAGS="-Cpanic=abort"
cargo build <options...> # e.g., --features feat_os_unix
cargo test <options...> # e.g., --features feat_os_unix test_pathchk
grcov . -s . --binary-path ./target/debug/ -t html --branch --ignore-not-existing --ignore build.rs --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?\#\[derive\()" -o ./target/debug/coverage/
# open target/debug/coverage/index.html in browser
```
if changes are not reflected in the report then run `cargo clean` and run the above commands.
### Using Stable Rust
If you are using stable version of Rust that doesn't enable code coverage instrumentation by default
then add `-Z-Zinstrument-coverage` flag to `RUSTFLAGS` env variable specified above.
## 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.
## Using Clippy
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). However, this key is only supported on `nightly`. To invoke clippy without errors, use `cargo +nightly clippy`. In order to also check tests and non-default crate features, use `cargo +nightly clippy --all-targets --all-features`.
## Markdown linter
We use <https://github.com/DavidAnson/markdownlint> to lint the Markdown files.

325
README.md
View file

@ -1,3 +1,10 @@
<!-- markdownlint-disable MD033 MD041 MD002 -->
<!-- markdownlint-disable commands-show-output no-duplicate-heading -->
<!-- spell-checker:ignore markdownlint ; (options) DESTDIR UTILNAME manpages reimplementation -->
<div align="center">
![uutils logo](docs/src/logo.svg)
# uutils coreutils # uutils coreutils
[![Crates.io](https://img.shields.io/crates/v/coreutils.svg)](https://crates.io/crates/coreutils) [![Crates.io](https://img.shields.io/crates/v/coreutils.svg)](https://crates.io/crates/coreutils)
@ -9,15 +16,14 @@
[![CodeCov](https://codecov.io/gh/uutils/coreutils/branch/master/graph/badge.svg)](https://codecov.io/gh/uutils/coreutils) [![CodeCov](https://codecov.io/gh/uutils/coreutils/branch/master/graph/badge.svg)](https://codecov.io/gh/uutils/coreutils)
![MSRV](https://img.shields.io/badge/MSRV-1.64.0-brightgreen) ![MSRV](https://img.shields.io/badge/MSRV-1.64.0-brightgreen)
----------------------------------------------- </div>
<!-- markdownlint-disable commands-show-output no-duplicate-heading --> ---
<!-- spell-checker:ignore markdownlint ; (options) DESTDIR RUNTEST UTILNAME manpages -->
uutils is an attempt at writing universal (as in cross-platform) CLI
utilities in [Rust](http://www.rust-lang.org). uutils coreutils is a cross-platform reimplementation of the GNU coreutils in
While all programs have been implemented, some options might be missing [Rust](http://www.rust-lang.org). While all programs have been implemented, some
or different behavior might be experienced. options might be missing or different behavior might be experienced.
To install it: To install it:
@ -27,13 +33,15 @@ cargo install coreutils
``` ```
<!-- markdownlint-disable-next-line MD026 --> <!-- markdownlint-disable-next-line MD026 -->
## Why?
uutils aims to work on as many platforms as possible, to be able to use the ## Goals
same utils on Linux, Mac, Windows and other platforms. This ensures, for
example, that scripts can be easily transferred between platforms. Rust was uutils aims to be a drop-in replacement for the GNU utils. Differences with GNU
chosen not only because it is fast and safe, but is also excellent for are treated as bugs.
writing cross-platform code.
uutils aims to work on as many platforms as possible, to be able to use the same
utils on Linux, Mac, Windows and other platforms. This ensures, for example,
that scripts can be easily transferred between platforms.
## Documentation ## Documentation
@ -42,10 +50,11 @@ uutils has both user and developer documentation available:
- [User Manual](https://uutils.github.io/user/) - [User Manual](https://uutils.github.io/user/)
- [Developer Documentation](https://uutils.github.io/dev/coreutils/) - [Developer Documentation](https://uutils.github.io/dev/coreutils/)
Both can also be generated locally, the instructions for that can be found in the Both can also be generated locally, the instructions for that can be found in
[coreutils docs](https://github.com/uutils/uutils.github.io) repository. the [coreutils docs](https://github.com/uutils/uutils.github.io) repository.
<!-- ANCHOR: build (this mark is needed for mdbook) --> <!-- ANCHOR: build (this mark is needed for mdbook) -->
## Requirements ## Requirements
- Rust (`cargo`, `rustc`) - Rust (`cargo`, `rustc`)
@ -53,13 +62,13 @@ Both can also be generated locally, the instructions for that can be found in th
### Rust Version ### Rust Version
uutils follows Rust's release channels and is tested against stable, beta and nightly. uutils follows Rust's release channels and is tested against stable, beta and
The current Minimum Supported Rust Version (MSRV) is `1.64.0`. nightly. The current Minimum Supported Rust Version (MSRV) is `1.64.0`.
## Building ## Building
There are currently two methods to build the uutils binaries: either Cargo There are currently two methods to build the uutils binaries: either Cargo or
or GNU Make. GNU Make.
> Building the full package, including all documentation, requires both Cargo > Building the full package, including all documentation, requires both Cargo
> and Gnu Make on a Unix platform. > and Gnu Make on a Unix platform.
@ -73,8 +82,8 @@ cd coreutils
### Cargo ### Cargo
Building uutils using Cargo is easy because the process is the same as for Building uutils using Cargo is easy because the process is the same as for every
every other Rust program: other Rust program:
```shell ```shell
cargo build --release cargo build --release
@ -83,9 +92,9 @@ cargo build --release
This command builds the most portable common core set of uutils into a multicall This command builds the most portable common core set of uutils into a multicall
(BusyBox-type) binary, named 'coreutils', on most Rust-supported platforms. (BusyBox-type) binary, named 'coreutils', on most Rust-supported platforms.
Additional platform-specific uutils are often available. Building these Additional platform-specific uutils are often available. Building these expanded
expanded sets of uutils for a platform (on that platform) is as simple as sets of uutils for a platform (on that platform) is as simple as specifying it
specifying it as a feature: as a feature:
```shell ```shell
cargo build --release --features macos cargo build --release --features macos
@ -96,18 +105,18 @@ cargo build --release --features unix
``` ```
If you don't want to build every utility available on your platform into the If you don't want to build every utility available on your platform into the
final binary, you can also specify which ones you want to build manually. final binary, you can also specify which ones you want to build manually. For
For example: example:
```shell ```shell
cargo build --features "base32 cat echo rm" --no-default-features cargo build --features "base32 cat echo rm" --no-default-features
``` ```
If you don't want to build the multicall binary and would prefer to build If you don't want to build the multicall binary and would prefer to build the
the utilities as individual binaries, that is also possible. Each utility utilities as individual binaries, that is also possible. Each utility is
is contained in its own package within the main repository, named contained in its own package within the main repository, named "uu_UTILNAME". To
"uu_UTILNAME". To build individual utilities, use cargo to build just the build individual utilities, use cargo to build just the specific packages (using
specific packages (using the `--package` [aka `-p`] option). For example: the `--package` [aka `-p`] option). For example:
```shell ```shell
cargo build -p uu_base32 -p uu_cat -p uu_echo -p uu_rm cargo build -p uu_base32 -p uu_cat -p uu_echo -p uu_rm
@ -148,13 +157,15 @@ make UTILS='UTILITY_1 UTILITY_2'
Likewise, installing can simply be done using: Likewise, installing can simply be done using:
```shell ```shell
cargo install --path . cargo install --path . --locked
``` ```
This command will install uutils into Cargo's *bin* folder (*e.g.* `$HOME/.cargo/bin`). This command will install uutils into Cargo's _bin_ folder (_e.g._
`$HOME/.cargo/bin`).
This does not install files necessary for shell completion or manpages. This does not install files necessary for shell completion or manpages. For
For manpages or shell completion to work, use `GNU Make` or see `Manually install shell completions`/`Manually install manpages`. manpages or shell completion to work, use `GNU Make` or see
`Manually install shell completions`/`Manually install manpages`.
### Install with GNU Make ### Install with GNU Make
@ -207,8 +218,8 @@ be generated; See `Manually install shell completions`.
### Manually install shell completions ### Manually install shell completions
The `coreutils` binary can generate completions for the `bash`, `elvish`, `fish`, `powershell` The `coreutils` binary can generate completions for the `bash`, `elvish`,
and `zsh` shells. It prints the result to stdout. `fish`, `powershell` and `zsh` shells. It prints the result to stdout.
The syntax is: The syntax is:
@ -216,8 +227,8 @@ The syntax is:
cargo run completion <utility> <shell> cargo run completion <utility> <shell>
``` ```
So, to install completions for `ls` on `bash` to `/usr/local/share/bash-completion/completions/ls`, So, to install completions for `ls` on `bash` to
run: `/usr/local/share/bash-completion/completions/ls`, run:
```shell ```shell
cargo run completion ls bash > /usr/local/share/bash-completion/completions/ls cargo run completion ls bash > /usr/local/share/bash-completion/completions/ls
@ -226,12 +237,12 @@ cargo run completion ls bash > /usr/local/share/bash-completion/completions/ls
### Manually install manpages ### Manually install manpages
To generate manpages, the syntax is: To generate manpages, the syntax is:
```bash ```bash
cargo run manpage <utility> cargo run manpage <utility>
``` ```
So, to install the manpage for `ls` to `/usr/local/share/man/man1/ls.1` So, to install the manpage for `ls` to `/usr/local/share/man/man1/ls.1` run:
run:
```bash ```bash
cargo run manpage ls > /usr/local/share/man/man1/ls.1 cargo run manpage ls > /usr/local/share/man/man1/ls.1
@ -280,245 +291,21 @@ make PREFIX=/my/path uninstall
<!-- ANCHOR_END: build (this mark is needed for mdbook) --> <!-- ANCHOR_END: build (this mark is needed for mdbook) -->
## Testing ## GNU test suite compatibility
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
```
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
```
### 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
Below is the evolution of how many GNU tests uutils passes. A more detailed Below is the evolution of how many GNU tests uutils passes. A more detailed
breakdown of the GNU test results of the main branch can be found breakdown of the GNU test results of the main branch can be found
[in the user manual](https://uutils.github.io/user/test_coverage.html). [in the user manual](https://uutils.github.io/user/test_coverage.html).
See <https://github.com/uutils/coreutils/issues/3336> for the main meta bugs
(many are missing).
![Evolution over time](https://github.com/uutils/coreutils-tracking/blob/main/gnu-results.png?raw=true) ![Evolution over time](https://github.com/uutils/coreutils-tracking/blob/main/gnu-results.png?raw=true)
To run locally:
```shell
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 tests in the CI.
To improve the GNU compatibility, the following process is recommended:
1. Identify a test (the smaller, the better) on a program that you understand or is easy to understand. You can use the `./util/remaining-gnu-error.py` script to help with this decision.
1. Build both the GNU and Rust coreutils using: `bash util/build-gnu.sh`
1. Run the test with `bash util/run-gnu-test.sh <your test>`
1. Start to modify `<your test>` to understand what is wrong. Examples:
1. Add `set -v` to have the bash verbose mode
1. Add `echo $?` where needed
1. When the variable `fail` is used in the test, `echo $fail` to see when the test started to fail
1. Bump the content of the output (ex: `cat err`)
1. ...
1. Or, if the test is simple, extract the relevant information to create a new test case running both GNU & Rust implementation
1. Start to modify the Rust implementation to match the expected behavior
1. Add a test to make sure that we don't regress (our test suite is super quick)
## Contributing ## Contributing
To contribute to uutils, please see [CONTRIBUTING](CONTRIBUTING.md). To contribute to uutils, please see [CONTRIBUTING](CONTRIBUTING.md).
## Utilities
Please note that this is not fully accurate:
- Some new options can be added / removed in the GNU implementation;
- Some error management might be missing;
- Some behaviors might be different.
See <https://github.com/uutils/coreutils/issues/3336> for the main meta bugs
(many are missing).
| Done | WIP |
|-----------|-----------|
| arch | cp |
| base32 | date |
| base64 | dd |
| basename | df |
| basenc | expr |
| cat | install |
| chcon | ls |
| chgrp | more |
| chmod | numfmt |
| chown | od (`--strings` and 128-bit data types missing) |
| chroot | pr |
| cksum | printf |
| comm | sort |
| csplit | split |
| cut | tac |
| dircolors | test |
| dirname | dir |
| du | vdir |
| echo | stty |
| env | |
| expand | |
| factor | |
| false | |
| fmt | |
| fold | |
| groups | |
| hashsum | |
| head | |
| hostid | |
| hostname | |
| id | |
| join | |
| kill | |
| link | |
| ln | |
| logname | |
| ~~md5sum~~ (replaced by [hashsum](https://github.com/uutils/coreutils/blob/main/src/uu/hashsum/src/hashsum.rs)) | |
| ~~sha1sum~~ (replaced by [hashsum](https://github.com/uutils/coreutils/blob/main/src/uu/hashsum/src/hashsum.rs)) | |
| ~~sha224sum~~ (replaced by [hashsum](https://github.com/uutils/coreutils/blob/main/src/uu/hashsum/src/hashsum.rs)) | |
| ~~sha256sum~~ (replaced by [hashsum](https://github.com/uutils/coreutils/blob/main/src/uu/hashsum/src/hashsum.rs)) | |
| ~~sha384sum~~ (replaced by [hashsum](https://github.com/uutils/coreutils/blob/main/src/uu/hashsum/src/hashsum.rs)) | |
| ~~sha512sum~~ (replaced by [hashsum](https://github.com/uutils/coreutils/blob/main/src/uu/hashsum/src/hashsum.rs)) | |
| mkdir | |
| mkfifo | |
| mknod | |
| mktemp | |
| mv | |
| nice | |
| nl | |
| nohup | |
| nproc | |
| paste | |
| pathchk | |
| pinky | |
| printenv | |
| ptx | |
| pwd | |
| readlink | |
| realpath | |
| relpath | |
| rm | |
| rmdir | |
| runcon | |
| seq | |
| shred | |
| shuf | |
| sleep | |
| stat | |
| stdbuf | |
| sum | |
| sync | |
| tail | |
| tee | |
| timeout | |
| touch | |
| tr | |
| true | |
| truncate | |
| tsort | |
| tty | |
| uname | |
| unexpand | |
| uniq | |
| unlink | |
| uptime | |
| users | |
| wc | |
| who | |
| whoami | |
| yes | |
## License ## License
uutils is licensed under the MIT License - see the `LICENSE` file for details uutils is licensed under the MIT License - see the `LICENSE` file for details

View file

@ -20,6 +20,8 @@ pub fn main() {
for (key, val) in env::vars() { for (key, val) in env::vars() {
if val == "1" && key.starts_with(ENV_FEATURE_PREFIX) { if val == "1" && key.starts_with(ENV_FEATURE_PREFIX) {
let krate = key[ENV_FEATURE_PREFIX.len()..].to_lowercase(); let krate = key[ENV_FEATURE_PREFIX.len()..].to_lowercase();
// Allow this as we have a bunch of info in the comments
#[allow(clippy::match_same_arms)]
match krate.as_ref() { match krate.as_ref() {
"default" | "macos" | "unix" | "windows" | "selinux" | "zip" => continue, // common/standard feature names "default" | "macos" | "unix" | "windows" | "selinux" | "zip" => continue, // common/standard feature names
"nightly" | "test_unimplemented" => continue, // crate-local custom features "nightly" | "test_unimplemented" => continue, // crate-local custom features

View file

@ -8,6 +8,7 @@
// spell-checker:ignore (ToDO) Chmoder cmode fmode fperm fref ugoa RFILE RFILE's // spell-checker:ignore (ToDO) Chmoder cmode fmode fperm fref ugoa RFILE RFILE's
use clap::{crate_version, Arg, ArgAction, Command}; use clap::{crate_version, Arg, ArgAction, Command};
use std::ffi::OsString;
use std::fs; use std::fs;
use std::os::unix::fs::{MetadataExt, PermissionsExt}; use std::os::unix::fs::{MetadataExt, PermissionsExt};
use std::path::Path; use std::path::Path;
@ -35,14 +36,64 @@ mod options {
pub const FILE: &str = "FILE"; pub const FILE: &str = "FILE";
} }
/// Extract negative modes (starting with '-') from the rest of the arguments.
///
/// This is mainly required for GNU compatibility, where "non-positional negative" modes are used
/// as the actual positional MODE. Some examples of these cases are:
/// * "chmod -w -r file", which is the same as "chmod -w,-r file"
/// * "chmod -w file -r", which is the same as "chmod -w,-r file"
///
/// These can currently not be handled by clap.
/// Therefore it might be possible that a pseudo MODE is inserted to pass clap parsing.
/// The pseudo MODE is later replaced by the extracted (and joined) negative modes.
fn extract_negative_modes(mut args: impl uucore::Args) -> (Option<String>, Vec<OsString>) {
// we look up the args until "--" is found
// "-mode" will be extracted into parsed_cmode_vec
let (parsed_cmode_vec, pre_double_hyphen_args): (Vec<OsString>, Vec<OsString>) =
args.by_ref().take_while(|a| a != "--").partition(|arg| {
let arg = if let Some(arg) = arg.to_str() {
arg.to_string()
} else {
return false;
};
arg.len() >= 2
&& arg.starts_with('-')
&& matches!(
arg.chars().nth(1).unwrap(),
'r' | 'w' | 'x' | 'X' | 's' | 't' | 'u' | 'g' | 'o' | '0'..='7'
)
});
let mut clean_args = Vec::new();
if !parsed_cmode_vec.is_empty() {
// we need a pseudo cmode for clap, which won't be used later.
// this is required because clap needs the default "chmod MODE FILE" scheme.
clean_args.push("w".into());
}
clean_args.extend(pre_double_hyphen_args);
if let Some(arg) = args.next() {
// as there is still something left in the iterator, we previously consumed the "--"
// -> add it to the args again
clean_args.push("--".into());
clean_args.push(arg);
}
clean_args.extend(args);
let parsed_cmode = Some(
parsed_cmode_vec
.iter()
.map(|s| s.to_str().unwrap())
.collect::<Vec<&str>>()
.join(","),
)
.filter(|s| !s.is_empty());
(parsed_cmode, clean_args)
}
#[uucore::main] #[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let mut args = args.collect_lossy(); let (parsed_cmode, args) = extract_negative_modes(args.skip(1)); // skip binary name
// Before we can parse 'args' with clap (and previously getopts),
// a possible MODE prefix '-' needs to be removed (e.g. "chmod -x FILE").
let mode_had_minus_prefix = mode::strip_minus_from_mode(&mut args);
let matches = uu_app().after_help(LONG_USAGE).try_get_matches_from(args)?; let matches = uu_app().after_help(LONG_USAGE).try_get_matches_from(args)?;
let changes = matches.get_flag(options::CHANGES); let changes = matches.get_flag(options::CHANGES);
@ -62,13 +113,14 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
}, },
None => None, None => None,
}; };
let modes = matches.get_one::<String>(options::MODE).unwrap(); // should always be Some because required
let cmode = if mode_had_minus_prefix { let modes = matches.get_one::<String>(options::MODE);
// clap parsing is finished, now put prefix back let cmode = if let Some(parsed_cmode) = parsed_cmode {
format!("-{modes}") parsed_cmode
} else { } else {
modes.to_string() modes.unwrap().to_string() // modes is required
}; };
// FIXME: enable non-utf8 paths
let mut files: Vec<String> = matches let mut files: Vec<String> = matches
.get_many::<String>(options::FILE) .get_many::<String>(options::FILE)
.map(|v| v.map(ToString::to_string).collect()) .map(|v| v.map(ToString::to_string).collect())
@ -107,6 +159,7 @@ pub fn uu_app() -> Command {
.override_usage(format_usage(USAGE)) .override_usage(format_usage(USAGE))
.args_override_self(true) .args_override_self(true)
.infer_long_args(true) .infer_long_args(true)
.no_binary_name(true)
.arg( .arg(
Arg::new(options::CHANGES) Arg::new(options::CHANGES)
.long(options::CHANGES) .long(options::CHANGES)
@ -376,3 +429,34 @@ impl Chmoder {
} }
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_negative_modes() {
// "chmod -w -r file" becomes "chmod -w,-r file". clap does not accept "-w,-r" as MODE.
// Therefore, "w" is added as pseudo mode to pass clap.
let (c, a) = extract_negative_modes(vec!["-w", "-r", "file"].iter().map(OsString::from));
assert_eq!(c, Some("-w,-r".to_string()));
assert_eq!(a, vec!["w", "file"]);
// "chmod -w file -r" becomes "chmod -w,-r file". clap does not accept "-w,-r" as MODE.
// Therefore, "w" is added as pseudo mode to pass clap.
let (c, a) = extract_negative_modes(vec!["-w", "file", "-r"].iter().map(OsString::from));
assert_eq!(c, Some("-w,-r".to_string()));
assert_eq!(a, vec!["w", "file"]);
// "chmod -w -- -r file" becomes "chmod -w -r file", where "-r" is interpreted as file.
// Again, "w" is needed as pseudo mode.
let (c, a) = extract_negative_modes(vec!["-w", "--", "-r", "f"].iter().map(OsString::from));
assert_eq!(c, Some("-w".to_string()));
assert_eq!(a, vec!["w", "--", "-r", "f"]);
// "chmod -- -r file" becomes "chmod -r file".
let (c, a) = extract_negative_modes(vec!["--", "-r", "file"].iter().map(OsString::from));
assert_eq!(c, None);
assert_eq!(a, vec!["--", "-r", "file"]);
}
}

23
src/uu/cksum/cksum.md Normal file
View file

@ -0,0 +1,23 @@
# cksum
```
cksum [OPTIONS] [FILE]...
```
Print CRC and size for each file
## After Help
DIGEST determines the digest algorithm and default output format:
- `-a=sysv`: (equivalent to sum -s)
- `-a=bsd`: (equivalent to sum -r)
- `-a=crc`: (equivalent to cksum)
- `-a=md5`: (equivalent to md5sum)
- `-a=sha1`: (equivalent to sha1sum)
- `-a=sha224`: (equivalent to sha224sum)
- `-a=sha256`: (equivalent to sha256sum)
- `-a=sha384`: (equivalent to sha384sum)
- `-a=sha512`: (equivalent to sha512sum)
- `-a=blake2b`: (equivalent to b2sum)
- `-a=sm3`: (only available through cksum)

View file

@ -15,15 +15,16 @@ use std::iter;
use std::path::Path; use std::path::Path;
use uucore::{ use uucore::{
error::{FromIo, UResult}, error::{FromIo, UResult},
format_usage, format_usage, help_about, help_section, help_usage,
sum::{ sum::{
div_ceil, Blake2b, Digest, DigestWriter, Md5, Sha1, Sha224, Sha256, Sha384, Sha512, Sm3, div_ceil, Blake2b, Digest, DigestWriter, Md5, Sha1, Sha224, Sha256, Sha384, Sha512, Sm3,
BSD, CRC, SYSV, BSD, CRC, SYSV,
}, },
}; };
const USAGE: &str = "{} [OPTIONS] [FILE]..."; const USAGE: &str = help_usage!("cksum.md");
const ABOUT: &str = "Print CRC and size for each file"; const ABOUT: &str = help_about!("cksum.md");
const AFTER_HELP: &str = help_section!("after help", "cksum.md");
const ALGORITHM_OPTIONS_SYSV: &str = "sysv"; const ALGORITHM_OPTIONS_SYSV: &str = "sysv";
const ALGORITHM_OPTIONS_BSD: &str = "bsd"; const ALGORITHM_OPTIONS_BSD: &str = "bsd";
@ -205,21 +206,6 @@ mod options {
pub static ALGORITHM: &str = "algorithm"; pub static ALGORITHM: &str = "algorithm";
} }
const ALGORITHM_HELP_DESC: &str =
"DIGEST determines the digest algorithm and default output format:\n\
\n\
-a=sysv: (equivalent to sum -s)\n\
-a=bsd: (equivalent to sum -r)\n\
-a=crc: (equivalent to cksum)\n\
-a=md5: (equivalent to md5sum)\n\
-a=sha1: (equivalent to sha1sum)\n\
-a=sha224: (equivalent to sha224sum)\n\
-a=sha256: (equivalent to sha256sum)\n\
-a=sha384: (equivalent to sha384sum)\n\
-a=sha512: (equivalent to sha512sum)\n\
-a=blake2b: (equivalent to b2sum)\n\
-a=sm3: (only available through cksum)\n";
#[uucore::main] #[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let args = args.collect_ignore(); let args = args.collect_ignore();
@ -278,5 +264,5 @@ pub fn uu_app() -> Command {
ALGORITHM_OPTIONS_SM3, ALGORITHM_OPTIONS_SM3,
]), ]),
) )
.after_help(ALGORITHM_HELP_DESC) .after_help(AFTER_HELP)
} }

View file

@ -22,7 +22,7 @@ use uucore::display::Quotable;
#[cfg(not(any(target_os = "macos", target_os = "redox")))] #[cfg(not(any(target_os = "macos", target_os = "redox")))]
use uucore::error::FromIo; use uucore::error::FromIo;
use uucore::error::{UResult, USimpleError}; use uucore::error::{UResult, USimpleError};
use uucore::{format_usage, help_about, help_usage, show_error}; use uucore::{format_usage, help_about, help_usage, show};
#[cfg(windows)] #[cfg(windows)]
use windows_sys::Win32::{Foundation::SYSTEMTIME, System::SystemInformation::SetSystemTime}; use windows_sys::Win32::{Foundation::SYSTEMTIME, System::SystemInformation::SetSystemTime};
@ -114,8 +114,8 @@ impl<'a> From<&'a str> for Iso8601Format {
SECONDS | SECOND => Self::Seconds, SECONDS | SECOND => Self::Seconds,
NS => Self::Ns, NS => Self::Ns,
DATE => Self::Date, DATE => Self::Date,
// Should be caught by clap // Note: This is caught by clap via `possible_values`
_ => panic!("Invalid format: {s}"), _ => unreachable!(),
} }
} }
} }
@ -203,9 +203,6 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
return set_system_datetime(date); return set_system_datetime(date);
} else { } else {
// Declare a file here because it needs to outlive the `dates` iterator.
let file: File;
// Get the current time, either in the local time zone or UTC. // Get the current time, either in the local time zone or UTC.
let now: DateTime<FixedOffset> = if settings.utc { let now: DateTime<FixedOffset> = if settings.utc {
let now = Utc::now(); let now = Utc::now();
@ -222,12 +219,19 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let iter = std::iter::once(date); let iter = std::iter::once(date);
Box::new(iter) Box::new(iter)
} }
DateSource::File(ref path) => { DateSource::File(ref path) => match File::open(path) {
file = File::open(path).unwrap(); Ok(file) => {
let lines = BufReader::new(file).lines(); let lines = BufReader::new(file).lines();
let iter = lines.filter_map(Result::ok).map(parse_date); let iter = lines.filter_map(Result::ok).map(parse_date);
Box::new(iter) Box::new(iter)
} }
Err(_err) => {
return Err(USimpleError::new(
2,
format!("{}: No such file or directory", path.display()),
));
}
},
DateSource::Now => { DateSource::Now => {
let iter = std::iter::once(Ok(now)); let iter = std::iter::once(Ok(now));
Box::new(iter) Box::new(iter)
@ -257,7 +261,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
.replace("%f", "%N"); .replace("%f", "%N");
println!("{formatted}"); println!("{formatted}");
} }
Err((input, _err)) => show_error!("invalid date {}", input.quote()), Err((input, _err)) => show!(USimpleError::new(
1,
format!("invalid date {}", input.quote())
)),
} }
} }
} }
@ -291,6 +298,9 @@ pub fn uu_app() -> Command {
.short('I') .short('I')
.long(OPT_ISO_8601) .long(OPT_ISO_8601)
.value_name("FMT") .value_name("FMT")
.value_parser([DATE, HOUR, HOURS, MINUTE, MINUTES, SECOND, SECONDS, NS])
.num_args(0..=1)
.default_missing_value(OPT_DATE)
.help(ISO_8601_HELP_STRING), .help(ISO_8601_HELP_STRING),
) )
.arg( .arg(
@ -304,6 +314,7 @@ pub fn uu_app() -> Command {
Arg::new(OPT_RFC_3339) Arg::new(OPT_RFC_3339)
.long(OPT_RFC_3339) .long(OPT_RFC_3339)
.value_name("FMT") .value_name("FMT")
.value_parser([DATE, SECOND, SECONDS, NS])
.help(RFC_3339_HELP_STRING), .help(RFC_3339_HELP_STRING),
) )
.arg( .arg(

View file

@ -27,11 +27,14 @@ use std::cmp;
use std::env; use std::env;
use std::ffi::OsString; use std::ffi::OsString;
use std::fs::{File, OpenOptions}; use std::fs::{File, OpenOptions};
use std::io::{self, Read, Seek, SeekFrom, Stdin, Stdout, Write}; use std::io::{self, Read, Seek, SeekFrom, Stdout, Write};
#[cfg(unix)]
use std::os::unix::fs::FileTypeExt;
#[cfg(any(target_os = "linux", target_os = "android"))] #[cfg(any(target_os = "linux", target_os = "android"))]
use std::os::unix::fs::OpenOptionsExt; use std::os::unix::fs::OpenOptionsExt;
#[cfg(unix)]
use std::os::unix::{
fs::FileTypeExt,
io::{AsRawFd, FromRawFd},
};
use std::path::Path; use std::path::Path;
use std::sync::mpsc; use std::sync::mpsc;
use std::thread; use std::thread;
@ -93,21 +96,44 @@ impl Num {
} }
/// Data sources. /// Data sources.
///
/// Use [`Source::stdin_as_file`] if available to enable more
/// fine-grained access to reading from stdin.
enum Source { enum Source {
/// Input from stdin. /// Input from stdin.
Stdin(Stdin), #[cfg(not(unix))]
Stdin(io::Stdin),
/// Input from a file. /// Input from a file.
File(File), File(File),
/// Input from stdin, opened from its file descriptor.
#[cfg(unix)]
StdinFile(File),
/// Input from a named pipe, also known as a FIFO. /// Input from a named pipe, also known as a FIFO.
#[cfg(unix)] #[cfg(unix)]
Fifo(File), Fifo(File),
} }
impl Source { impl Source {
/// Create a source from stdin using its raw file descriptor.
///
/// This returns an instance of the `Source::StdinFile` variant,
/// using the raw file descriptor of [`std::io::Stdin`] to create
/// the [`std::fs::File`] parameter. You can use this instead of
/// `Source::Stdin` to allow reading from stdin without consuming
/// the entire contents of stdin when this process terminates.
#[cfg(unix)]
fn stdin_as_file() -> Self {
let fd = io::stdin().as_raw_fd();
let f = unsafe { File::from_raw_fd(fd) };
Self::StdinFile(f)
}
fn skip(&mut self, n: u64) -> io::Result<u64> { fn skip(&mut self, n: u64) -> io::Result<u64> {
match self { match self {
#[cfg(not(unix))]
Self::Stdin(stdin) => match io::copy(&mut stdin.take(n), &mut io::sink()) { Self::Stdin(stdin) => match io::copy(&mut stdin.take(n), &mut io::sink()) {
Ok(m) if m < n => { Ok(m) if m < n => {
show_error!("'standard input': cannot skip to specified offset"); show_error!("'standard input': cannot skip to specified offset");
@ -116,6 +142,15 @@ impl Source {
Ok(m) => Ok(m), Ok(m) => Ok(m),
Err(e) => Err(e), Err(e) => Err(e),
}, },
#[cfg(unix)]
Self::StdinFile(f) => match io::copy(&mut f.take(n), &mut io::sink()) {
Ok(m) if m < n => {
show_error!("'standard input': cannot skip to specified offset");
Ok(m)
}
Ok(m) => Ok(m),
Err(e) => Err(e),
},
Self::File(f) => f.seek(io::SeekFrom::Start(n)), Self::File(f) => f.seek(io::SeekFrom::Start(n)),
#[cfg(unix)] #[cfg(unix)]
Self::Fifo(f) => io::copy(&mut f.take(n), &mut io::sink()), Self::Fifo(f) => io::copy(&mut f.take(n), &mut io::sink()),
@ -126,9 +161,12 @@ impl Source {
impl Read for Source { impl Read for Source {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self { match self {
#[cfg(not(unix))]
Self::Stdin(stdin) => stdin.read(buf), Self::Stdin(stdin) => stdin.read(buf),
Self::File(f) => f.read(buf), Self::File(f) => f.read(buf),
#[cfg(unix)] #[cfg(unix)]
Self::StdinFile(f) => f.read(buf),
#[cfg(unix)]
Self::Fifo(f) => f.read(buf), Self::Fifo(f) => f.read(buf),
} }
} }
@ -151,7 +189,10 @@ struct Input<'a> {
impl<'a> Input<'a> { impl<'a> Input<'a> {
/// Instantiate this struct with stdin as a source. /// Instantiate this struct with stdin as a source.
fn new_stdin(settings: &'a Settings) -> UResult<Self> { fn new_stdin(settings: &'a Settings) -> UResult<Self> {
#[cfg(not(unix))]
let mut src = Source::Stdin(io::stdin()); let mut src = Source::Stdin(io::stdin());
#[cfg(unix)]
let mut src = Source::stdin_as_file();
if settings.skip > 0 { if settings.skip > 0 {
src.skip(settings.skip)?; src.skip(settings.skip)?;
} }

8
src/uu/fold/fold.md Normal file
View file

@ -0,0 +1,8 @@
# fold
```
fold [OPTION]... [FILE]...
```
Writes each file (or standard input if no files are given)
to standard output whilst breaking long lines

View file

@ -13,13 +13,12 @@ use std::io::{stdin, BufRead, BufReader, Read};
use std::path::Path; use std::path::Path;
use uucore::display::Quotable; use uucore::display::Quotable;
use uucore::error::{FromIo, UResult, USimpleError}; use uucore::error::{FromIo, UResult, USimpleError};
use uucore::format_usage; use uucore::{format_usage, help_about, help_usage};
const TAB_WIDTH: usize = 8; const TAB_WIDTH: usize = 8;
static USAGE: &str = "{} [OPTION]... [FILE]..."; const USAGE: &str = help_usage!("fold.md");
static ABOUT: &str = "Writes each file (or standard input if no files are given) const ABOUT: &str = help_about!("fold.md");
to standard output whilst breaking long lines";
mod options { mod options {
pub const BYTES: &str = "bytes"; pub const BYTES: &str = "bytes";

7
src/uu/mkfifo/mkfifo.md Normal file
View file

@ -0,0 +1,7 @@
# mkfifo
```
mkfifo [OPTION]... NAME...
```
Create a FIFO with the given name.

View file

@ -10,10 +10,10 @@ use libc::mkfifo;
use std::ffi::CString; use std::ffi::CString;
use uucore::display::Quotable; use uucore::display::Quotable;
use uucore::error::{UResult, USimpleError}; use uucore::error::{UResult, USimpleError};
use uucore::{format_usage, show}; use uucore::{format_usage, help_about, help_usage, show};
static USAGE: &str = "{} [OPTION]... NAME..."; static USAGE: &str = help_usage!("mkfifo.md");
static ABOUT: &str = "Create a FIFO with the given name."; static ABOUT: &str = help_about!("mkfifo.md");
mod options { mod options {
pub static MODE: &str = "mode"; pub static MODE: &str = "mode";

25
src/uu/mknod/mknod.md Normal file
View file

@ -0,0 +1,25 @@
# mknod
```
mknod [OPTION]... NAME TYPE [MAJOR MINOR]
```
Create the special file NAME of the given TYPE.
## After Help
Mandatory arguments to long options are mandatory for short options too.
`-m`, `--mode=MODE` set file permission bits to `MODE`, not `a=rw - umask`
Both `MAJOR` and `MINOR` must be specified when `TYPE` is `b`, `c`, or `u`, and they
must be omitted when `TYPE` is `p`. If `MAJOR` or `MINOR` begins with `0x` or `0X`,
it is interpreted as hexadecimal; otherwise, if it begins with 0, as octal;
otherwise, as decimal. `TYPE` may be:
* `b` create a block (buffered) special file
* `c`, `u` create a character (unbuffered) special file
* `p` create a FIFO
NOTE: your shell may have its own version of mknod, which usually supersedes
the version described here. Please refer to your shell's documentation
for details about the options it supports.

View file

@ -14,28 +14,11 @@ use std::ffi::CString;
use uucore::display::Quotable; use uucore::display::Quotable;
use uucore::error::{set_exit_code, UResult, USimpleError, UUsageError}; use uucore::error::{set_exit_code, UResult, USimpleError, UUsageError};
use uucore::format_usage; use uucore::{format_usage, help_about, help_section, help_usage};
static ABOUT: &str = "Create the special file NAME of the given TYPE."; const ABOUT: &str = help_about!("mknod.md");
static USAGE: &str = "{} [OPTION]... NAME TYPE [MAJOR MINOR]"; const USAGE: &str = help_usage!("mknod.md");
static LONG_HELP: &str = "Mandatory arguments to long options are mandatory for short options too. const AFTER_HELP: &str = help_section!("after help", "mknod.md");
-m, --mode=MODE set file permission bits to MODE, not a=rw - umask
--help display this help and exit
--version output version information and exit
Both MAJOR and MINOR must be specified when TYPE is b, c, or u, and they
must be omitted when TYPE is p. If MAJOR or MINOR begins with 0x or 0X,
it is interpreted as hexadecimal; otherwise, if it begins with 0, as octal;
otherwise, as decimal. TYPE may be:
b create a block (buffered) special file
c, u create a character (unbuffered) special file
p create a FIFO
NOTE: your shell may have its own version of mknod, which usually supersedes
the version described here. Please refer to your shell's documentation
for details about the options it supports.
";
const MODE_RW_UGO: mode_t = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH; const MODE_RW_UGO: mode_t = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
@ -142,7 +125,7 @@ pub fn uu_app() -> Command {
Command::new(uucore::util_name()) Command::new(uucore::util_name())
.version(crate_version!()) .version(crate_version!())
.override_usage(format_usage(USAGE)) .override_usage(format_usage(USAGE))
.after_help(LONG_HELP) .after_help(AFTER_HELP)
.about(ABOUT) .about(ABOUT)
.infer_long_args(true) .infer_long_args(true)
.arg( .arg(

7
src/uu/mktemp/mktemp.md Normal file
View file

@ -0,0 +1,7 @@
# mktemp
```
mktemp [OPTION]... [TEMPLATE]
```
Create a temporary file or directory.

View file

@ -11,7 +11,7 @@
use clap::{crate_version, Arg, ArgAction, ArgMatches, Command}; use clap::{crate_version, Arg, ArgAction, ArgMatches, Command};
use uucore::display::{println_verbatim, Quotable}; use uucore::display::{println_verbatim, Quotable};
use uucore::error::{FromIo, UError, UResult, UUsageError}; use uucore::error::{FromIo, UError, UResult, UUsageError};
use uucore::format_usage; use uucore::{format_usage, help_about, help_usage};
use std::env; use std::env;
use std::error::Error; use std::error::Error;
@ -28,8 +28,8 @@ use std::os::unix::prelude::PermissionsExt;
use rand::Rng; use rand::Rng;
use tempfile::Builder; use tempfile::Builder;
static ABOUT: &str = "Create a temporary file or directory."; const ABOUT: &str = help_about!("mktemp.md");
const USAGE: &str = "{} [OPTION]... [TEMPLATE]"; const USAGE: &str = help_usage!("mktemp.md");
static DEFAULT_TEMPLATE: &str = "tmp.XXXXXXXXXX"; static DEFAULT_TEMPLATE: &str = "tmp.XXXXXXXXXX";

View file

@ -15,10 +15,10 @@ use uucore::{format_usage, help_about, help_usage};
use uucore::display::println_verbatim; use uucore::display::println_verbatim;
use uucore::error::{FromIo, UResult}; use uucore::error::{FromIo, UResult};
static ABOUT: &str = help_about!("pwd.md"); const ABOUT: &str = help_about!("pwd.md");
const USAGE: &str = help_usage!("pwd.md"); const USAGE: &str = help_usage!("pwd.md");
static OPT_LOGICAL: &str = "logical"; const OPT_LOGICAL: &str = "logical";
static OPT_PHYSICAL: &str = "physical"; const OPT_PHYSICAL: &str = "physical";
fn physical_path() -> io::Result<PathBuf> { fn physical_path() -> io::Result<PathBuf> {
// std::env::current_dir() is a thin wrapper around libc::getcwd(). // std::env::current_dir() is a thin wrapper around libc::getcwd().
@ -84,36 +84,22 @@ fn logical_path() -> io::Result<PathBuf> {
{ {
use std::fs::metadata; use std::fs::metadata;
use std::os::unix::fs::MetadataExt; use std::os::unix::fs::MetadataExt;
let path_info = match metadata(path) { match (metadata(path), metadata(".")) {
Ok(info) => info, (Ok(info1), Ok(info2)) => {
Err(_) => return false, info1.dev() == info2.dev() && info1.ino() == info2.ino()
}; }
let real_info = match metadata(".") { _ => false,
Ok(info) => info,
Err(_) => return false,
};
if path_info.dev() != real_info.dev() || path_info.ino() != real_info.ino() {
return false;
} }
} }
#[cfg(not(unix))] #[cfg(not(unix))]
{ {
use std::fs::canonicalize; use std::fs::canonicalize;
let canon_path = match canonicalize(path) { match (canonicalize(path), canonicalize(".")) {
Ok(path) => path, (Ok(path1), Ok(path2)) => path1 == path2,
Err(_) => return false, _ => false,
};
let real_path = match canonicalize(".") {
Ok(path) => path,
Err(_) => return false,
};
if canon_path != real_path {
return false;
} }
} }
true
} }
match env::var_os("PWD").map(PathBuf::from) { match env::var_os("PWD").map(PathBuf::from) {

View file

@ -0,0 +1,8 @@
# relpath
```
relpath [-d DIR] TO [FROM]
```
Convert TO destination to the relative path from the FROM dir.
If FROM path is omitted, current working dir will be used.

View file

@ -12,12 +12,11 @@ use std::env;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use uucore::display::println_verbatim; use uucore::display::println_verbatim;
use uucore::error::{FromIo, UResult}; use uucore::error::{FromIo, UResult};
use uucore::format_usage;
use uucore::fs::{canonicalize, MissingHandling, ResolveMode}; use uucore::fs::{canonicalize, MissingHandling, ResolveMode};
use uucore::{format_usage, help_about, help_usage};
static ABOUT: &str = "Convert TO destination to the relative path from the FROM dir. const USAGE: &str = help_usage!("relpath.md");
If FROM path is omitted, current working dir will be used."; const ABOUT: &str = help_about!("relpath.md");
const USAGE: &str = "{} [-d DIR] TO [FROM]";
mod options { mod options {
pub const DIR: &str = "DIR"; pub const DIR: &str = "DIR";

View file

@ -16,6 +16,7 @@ path = "src/sleep.rs"
[dependencies] [dependencies]
clap = { workspace=true } clap = { workspace=true }
fundu = { workspace=true }
uucore = { workspace=true } uucore = { workspace=true }
[[bin]] [[bin]]

View file

@ -14,6 +14,7 @@ use uucore::{
}; };
use clap::{crate_version, Arg, ArgAction, Command}; use clap::{crate_version, Arg, ArgAction, Command};
use fundu::{self, DurationParser, ParseError};
static ABOUT: &str = help_about!("sleep.md"); static ABOUT: &str = help_about!("sleep.md");
const USAGE: &str = help_usage!("sleep.md"); const USAGE: &str = help_usage!("sleep.md");
@ -61,14 +62,34 @@ pub fn uu_app() -> Command {
fn sleep(args: &[&str]) -> UResult<()> { fn sleep(args: &[&str]) -> UResult<()> {
let mut arg_error = false; let mut arg_error = false;
use fundu::TimeUnit::*;
let parser = DurationParser::with_time_units(&[Second, Minute, Hour, Day]);
let sleep_dur = args let sleep_dur = args
.iter() .iter()
.filter_map(|input| { .filter_map(|input| match parser.parse(input.trim()) {
uucore::parse_time::from_str(input.trim()).ok().or_else(|| { Ok(duration) => Some(duration),
Err(error) => {
arg_error = true; arg_error = true;
show_error!("invalid time interval '{input}'");
let reason = match error {
ParseError::Empty if input.is_empty() => "Input was empty".to_string(),
ParseError::Empty => "Found only whitespace in input".to_string(),
ParseError::Syntax(pos, description)
| ParseError::TimeUnit(pos, description) => {
format!("{description} at position {}", pos.saturating_add(1))
}
ParseError::NegativeExponentOverflow | ParseError::PositiveExponentOverflow => {
"Exponent was out of bounds".to_string()
}
ParseError::NegativeNumber => "Number was negative".to_string(),
error => error.to_string(),
};
show_error!("invalid time interval '{input}': {reason}");
None None
}) }
}) })
.fold(Duration::ZERO, |acc, n| acc.saturating_add(n)); .fold(Duration::ZERO, |acc, n| acc.saturating_add(n));

View file

@ -196,6 +196,22 @@ fn report_if_verbose(signal: usize, cmd: &str, verbose: bool) {
} }
} }
fn send_signal(process: &mut Child, signal: usize, foreground: bool) {
// NOTE: GNU timeout doesn't check for errors of signal.
// The subprocess might have exited just after the timeout.
// Sending a signal now would return "No such process", but we should still try to kill the children.
_ = process.send_signal(signal);
if !foreground {
_ = process.send_signal_group(signal);
let kill_signal = signal_by_name_or_value("KILL").unwrap();
let continued_signal = signal_by_name_or_value("CONT").unwrap();
if signal != kill_signal && signal != continued_signal {
_ = process.send_signal(continued_signal);
_ = process.send_signal_group(continued_signal);
}
}
}
/// Wait for a child process and send a kill signal if it does not terminate. /// Wait for a child process and send a kill signal if it does not terminate.
/// ///
/// This function waits for the child `process` for the time period /// This function waits for the child `process` for the time period
@ -217,10 +233,11 @@ fn report_if_verbose(signal: usize, cmd: &str, verbose: bool) {
/// If there is a problem sending the `SIGKILL` signal or waiting for /// If there is a problem sending the `SIGKILL` signal or waiting for
/// the process after that signal is sent. /// the process after that signal is sent.
fn wait_or_kill_process( fn wait_or_kill_process(
mut process: Child, process: &mut Child,
cmd: &str, cmd: &str,
duration: Duration, duration: Duration,
preserve_status: bool, preserve_status: bool,
foreground: bool,
verbose: bool, verbose: bool,
) -> std::io::Result<i32> { ) -> std::io::Result<i32> {
match process.wait_or_timeout(duration) { match process.wait_or_timeout(duration) {
@ -234,7 +251,7 @@ fn wait_or_kill_process(
Ok(None) => { Ok(None) => {
let signal = signal_by_name_or_value("KILL").unwrap(); let signal = signal_by_name_or_value("KILL").unwrap();
report_if_verbose(signal, cmd, verbose); report_if_verbose(signal, cmd, verbose);
process.send_signal(signal)?; send_signal(process, signal, foreground);
process.wait()?; process.wait()?;
Ok(ExitStatus::SignalSent(signal).into()) Ok(ExitStatus::SignalSent(signal).into())
} }
@ -300,7 +317,7 @@ fn timeout(
enable_pipe_errors()?; enable_pipe_errors()?;
let mut process = process::Command::new(&cmd[0]) let process = &mut process::Command::new(&cmd[0])
.args(&cmd[1..]) .args(&cmd[1..])
.stdin(Stdio::inherit()) .stdin(Stdio::inherit())
.stdout(Stdio::inherit()) .stdout(Stdio::inherit())
@ -335,7 +352,7 @@ fn timeout(
.into()), .into()),
Ok(None) => { Ok(None) => {
report_if_verbose(signal, &cmd[0], verbose); report_if_verbose(signal, &cmd[0], verbose);
process.send_signal(signal)?; send_signal(process, signal, foreground);
match kill_after { match kill_after {
None => { None => {
if preserve_status { if preserve_status {
@ -350,6 +367,7 @@ fn timeout(
&cmd[0], &cmd[0],
kill_after, kill_after,
preserve_status, preserve_status,
foreground,
verbose, verbose,
) { ) {
Ok(status) => Err(status.into()), Ok(status) => Err(status.into()),
@ -363,11 +381,8 @@ fn timeout(
} }
Err(_) => { Err(_) => {
// We're going to return ERR_EXIT_STATUS regardless of // We're going to return ERR_EXIT_STATUS regardless of
// whether `send_signal()` succeeds or fails, so just // whether `send_signal()` succeeds or fails
// ignore the return value. send_signal(process, signal, foreground);
process
.send_signal(signal)
.map_err(|e| USimpleError::new(ExitStatus::TimeoutFailed.into(), format!("{e}")))?;
Err(ExitStatus::TimeoutFailed.into()) Err(ExitStatus::TimeoutFailed.into())
} }
} }

View file

@ -11,17 +11,13 @@
use chrono::{Local, TimeZone, Utc}; use chrono::{Local, TimeZone, Utc};
use clap::{crate_version, Arg, ArgAction, Command}; use clap::{crate_version, Arg, ArgAction, Command};
use uucore::format_usage;
// import crate time from utmpx
pub use uucore::libc;
use uucore::libc::time_t; use uucore::libc::time_t;
use uucore::{format_usage, help_about, help_usage};
use uucore::error::{UResult, USimpleError}; use uucore::error::{UResult, USimpleError};
static ABOUT: &str = "Display the current time, the length of time the system has been up,\n\ const ABOUT: &str = help_about!("uptime.md");
the number of users on the system, and the average number of jobs\n\ const USAGE: &str = help_usage!("uptime.md");
in the run queue over the last 1, 5 and 15 minutes.";
const USAGE: &str = "{} [OPTION]...";
pub mod options { pub mod options {
pub static SINCE: &str = "since"; pub static SINCE: &str = "since";
} }

9
src/uu/uptime/uptime.md Normal file
View file

@ -0,0 +1,9 @@
# uptime
```
uptime [OPTION]...
```
Display the current time, the length of time the system has been up,
the number of users on the system, and the average number of jobs
in the run queue over the last 1, 5 and 15 minutes.

View file

@ -18,7 +18,7 @@ use std::ffi::CStr;
use std::fmt::Write; use std::fmt::Write;
use std::os::unix::fs::MetadataExt; use std::os::unix::fs::MetadataExt;
use std::path::PathBuf; use std::path::PathBuf;
use uucore::format_usage; use uucore::{format_usage, help_about, help_usage};
mod options { mod options {
pub const ALL: &str = "all"; pub const ALL: &str = "all";
@ -38,8 +38,8 @@ mod options {
pub const FILE: &str = "FILE"; // if length=1: FILE, if length=2: ARG1 ARG2 pub const FILE: &str = "FILE"; // if length=1: FILE, if length=2: ARG1 ARG2
} }
static ABOUT: &str = "Print information about users who are currently logged in."; const ABOUT: &str = help_about!("who.md");
const USAGE: &str = "{} [OPTION]... [ FILE | ARG1 ARG2 ]"; const USAGE: &str = help_usage!("who.md");
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
static RUNLEVEL_HELP: &str = "print current runlevel"; static RUNLEVEL_HELP: &str = "print current runlevel";

8
src/uu/who/who.md Normal file
View file

@ -0,0 +1,8 @@
# who
```
who [OPTION]... [ FILE | ARG1 ARG2 ]
```
Print information about users who are currently logged in.

View file

@ -11,10 +11,12 @@ use clap::{crate_version, Command};
use uucore::display::println_verbatim; use uucore::display::println_verbatim;
use uucore::error::{FromIo, UResult}; use uucore::error::{FromIo, UResult};
use uucore::{format_usage, help_about, help_usage};
mod platform; mod platform;
static ABOUT: &str = "Print the current username."; const ABOUT: &str = help_about!("whoami.md");
const USAGE: &str = help_usage!("whoami.md");
#[uucore::main] #[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uumain(args: impl uucore::Args) -> UResult<()> {
@ -28,5 +30,6 @@ pub fn uu_app() -> Command {
Command::new(uucore::util_name()) Command::new(uucore::util_name())
.version(crate_version!()) .version(crate_version!())
.about(ABOUT) .about(ABOUT)
.override_usage(format_usage(USAGE))
.infer_long_args(true) .infer_long_args(true)
} }

7
src/uu/whoami/whoami.md Normal file
View file

@ -0,0 +1,7 @@
# whoami
```
whoami
```
Print the current username.

View file

@ -122,6 +122,15 @@ fn parse_change(mode: &str, fperm: u32, considering_dir: bool) -> (u32, usize) {
'o' => srwx = ((fperm << 6) & 0o700) | ((fperm << 3) & 0o070) | (fperm & 0o007), 'o' => srwx = ((fperm << 6) & 0o700) | ((fperm << 3) & 0o070) | (fperm & 0o007),
_ => break, _ => break,
}; };
if ch == 'u' || ch == 'g' || ch == 'o' {
// symbolic modes only allows perms to be a single letter of 'ugo'
// therefore this must either be the first char or it is unexpected
if pos != 0 {
break;
}
pos = 1;
break;
}
pos += 1; pos += 1;
} }
if pos == 0 { if pos == 0 {

View file

@ -48,6 +48,9 @@ pub trait ChildExt {
/// send the signal to an unrelated process that recycled the PID. /// send the signal to an unrelated process that recycled the PID.
fn send_signal(&mut self, signal: usize) -> io::Result<()>; fn send_signal(&mut self, signal: usize) -> io::Result<()>;
/// Send a signal to a process group.
fn send_signal_group(&mut self, signal: usize) -> io::Result<()>;
/// Wait for a process to finish or return after the specified duration. /// Wait for a process to finish or return after the specified duration.
/// A `timeout` of zero disables the timeout. /// A `timeout` of zero disables the timeout.
fn wait_or_timeout(&mut self, timeout: Duration) -> io::Result<Option<ExitStatus>>; fn wait_or_timeout(&mut self, timeout: Duration) -> io::Result<Option<ExitStatus>>;
@ -62,6 +65,18 @@ impl ChildExt for Child {
} }
} }
fn send_signal_group(&mut self, signal: usize) -> io::Result<()> {
// Ignore the signal, so we don't go into a signal loop.
if unsafe { libc::signal(signal as i32, libc::SIG_IGN) } != 0 {
return Err(io::Error::last_os_error());
}
if unsafe { libc::kill(0, signal as i32) } != 0 {
Err(io::Error::last_os_error())
} else {
Ok(())
}
}
fn wait_or_timeout(&mut self, timeout: Duration) -> io::Result<Option<ExitStatus>> { fn wait_or_timeout(&mut self, timeout: Duration) -> io::Result<Option<ExitStatus>> {
if timeout == Duration::from_micros(0) { if timeout == Duration::from_micros(0) {
return self.wait().map(Some); return self.wait().map(Some);

View file

@ -4,9 +4,8 @@ use std::fs::{metadata, set_permissions, OpenOptions, Permissions};
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt}; use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
use std::sync::Mutex; use std::sync::Mutex;
extern crate libc;
use uucore::mode::strip_minus_from_mode;
extern crate chmod; extern crate chmod;
extern crate libc;
use self::libc::umask; use self::libc::umask;
static TEST_FILE: &str = "file"; static TEST_FILE: &str = "file";
@ -503,35 +502,6 @@ fn test_chmod_symlink_non_existing_file_recursive() {
.no_stderr(); .no_stderr();
} }
#[test]
fn test_chmod_strip_minus_from_mode() {
let tests = vec![
// ( before, after )
("chmod -v -xw -R FILE", "chmod -v xw -R FILE"),
("chmod g=rwx FILE -c", "chmod g=rwx FILE -c"),
(
"chmod -c -R -w,o+w FILE --preserve-root",
"chmod -c -R w,o+w FILE --preserve-root",
),
("chmod -c -R +w FILE ", "chmod -c -R +w FILE "),
("chmod a=r,=xX FILE", "chmod a=r,=xX FILE"),
(
"chmod -v --reference REF_FILE -R FILE",
"chmod -v --reference REF_FILE -R FILE",
),
("chmod -Rvc -w-x FILE", "chmod -Rvc w-x FILE"),
("chmod 755 -v FILE", "chmod 755 -v FILE"),
("chmod -v +0004 FILE -R", "chmod -v +0004 FILE -R"),
("chmod -v -0007 FILE -R", "chmod -v 0007 FILE -R"),
];
for test in tests {
let mut args: Vec<String> = test.0.split(' ').map(|v| v.to_string()).collect();
let _mode_had_minus_prefix = strip_minus_from_mode(&mut args);
assert_eq!(test.1, args.join(" "));
}
}
#[test] #[test]
fn test_chmod_keep_setgid() { fn test_chmod_keep_setgid() {
for (from, arg, to) in [ for (from, arg, to) in [
@ -671,3 +641,68 @@ fn test_quiet_n_verbose_used_multiple_times() {
.arg("file") .arg("file")
.succeeds(); .succeeds();
} }
#[test]
fn test_gnu_invalid_mode() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
at.touch("file");
scene.ucmd().arg("u+gr").arg("file").fails();
}
#[test]
fn test_gnu_options() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
at.touch("file");
scene.ucmd().arg("-w").arg("file").succeeds();
scene.ucmd().arg("file").arg("-w").succeeds();
scene.ucmd().arg("-w").arg("--").arg("file").succeeds();
}
#[test]
fn test_gnu_repeating_options() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
at.touch("file");
scene.ucmd().arg("-w").arg("-w").arg("file").succeeds();
scene
.ucmd()
.arg("-w")
.arg("-w")
.arg("-w")
.arg("file")
.succeeds();
}
#[test]
fn test_gnu_special_filenames() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
let perms_before = Permissions::from_mode(0o100640);
let perms_after = Permissions::from_mode(0o100440);
make_file(&at.plus_as_string("--"), perms_before.mode());
scene.ucmd().arg("-w").arg("--").arg("--").succeeds();
assert_eq!(at.metadata("--").permissions(), perms_after);
set_permissions(at.plus("--"), perms_before.clone()).unwrap();
scene.ucmd().arg("--").arg("-w").arg("--").succeeds();
assert_eq!(at.metadata("--").permissions(), perms_after);
at.remove("--");
make_file(&at.plus_as_string("-w"), perms_before.mode());
scene.ucmd().arg("-w").arg("--").arg("-w").succeeds();
assert_eq!(at.metadata("-w").permissions(), perms_after);
set_permissions(at.plus("-w"), perms_before).unwrap();
scene.ucmd().arg("--").arg("-w").arg("-w").succeeds();
assert_eq!(at.metadata("-w").permissions(), perms_after);
}
#[test]
fn test_gnu_special_options() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
at.touch("file");
scene.ucmd().arg("--").arg("--").arg("file").succeeds();
scene.ucmd().arg("--").arg("--").fails();
}

View file

@ -44,16 +44,91 @@ fn test_date_rfc_3339() {
} }
#[test] #[test]
fn test_date_rfc_8601() { fn test_date_rfc_3339_invalid_arg() {
for param in ["--iso-3339", "--rfc-3"] {
new_ucmd!().arg(format!("{param}=foo")).fails();
}
}
#[test]
fn test_date_rfc_8601_default() {
let re = Regex::new(r"^\d{4}-\d{2}-\d{2}\n$").unwrap();
for param in ["--iso-8601", "--i"] { for param in ["--iso-8601", "--i"] {
new_ucmd!().arg(format!("{param}=ns")).succeeds(); new_ucmd!().arg(param).succeeds().stdout_matches(&re);
}
}
#[test]
fn test_date_rfc_8601() {
let re = Regex::new(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2},\d{9}[+-]\d{2}:\d{2}\n$").unwrap();
for param in ["--iso-8601", "--i"] {
new_ucmd!()
.arg(format!("{param}=ns"))
.succeeds()
.stdout_matches(&re);
}
}
#[test]
fn test_date_rfc_8601_invalid_arg() {
for param in ["--iso-8601", "--i"] {
new_ucmd!().arg(format!("{param}=@")).fails();
} }
} }
#[test] #[test]
fn test_date_rfc_8601_second() { fn test_date_rfc_8601_second() {
let re = Regex::new(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}\n$").unwrap();
for param in ["--iso-8601", "--i"] { for param in ["--iso-8601", "--i"] {
new_ucmd!().arg(format!("{param}=second")).succeeds(); new_ucmd!()
.arg(format!("{param}=second"))
.succeeds()
.stdout_matches(&re);
new_ucmd!()
.arg(format!("{param}=seconds"))
.succeeds()
.stdout_matches(&re);
}
}
#[test]
fn test_date_rfc_8601_minute() {
let re = Regex::new(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}[+-]\d{2}:\d{2}\n$").unwrap();
for param in ["--iso-8601", "--i"] {
new_ucmd!()
.arg(format!("{param}=minute"))
.succeeds()
.stdout_matches(&re);
new_ucmd!()
.arg(format!("{param}=minutes"))
.succeeds()
.stdout_matches(&re);
}
}
#[test]
fn test_date_rfc_8601_hour() {
let re = Regex::new(r"^\d{4}-\d{2}-\d{2}T\d{2}[+-]\d{2}:\d{2}\n$").unwrap();
for param in ["--iso-8601", "--i"] {
new_ucmd!()
.arg(format!("{param}=hour"))
.succeeds()
.stdout_matches(&re);
new_ucmd!()
.arg(format!("{param}=hours"))
.succeeds()
.stdout_matches(&re);
}
}
#[test]
fn test_date_rfc_8601_date() {
let re = Regex::new(r"^\d{4}-\d{2}-\d{2}\n$").unwrap();
for param in ["--iso-8601", "--i"] {
new_ucmd!()
.arg(format!("{param}=date"))
.succeeds()
.stdout_matches(&re);
} }
} }
@ -198,6 +273,24 @@ fn test_date_set_valid_2() {
} }
} }
#[test]
fn test_date_for_invalid_file() {
let result = new_ucmd!().arg("--file").arg("invalid_file").fails();
result.no_stdout();
assert_eq!(
result.stderr_str().trim(),
"date: invalid_file: No such file or directory",
);
}
#[test]
fn test_date_for_file() {
let (at, mut ucmd) = at_and_ucmd!();
let file = "test_date_for_file";
at.touch(file);
ucmd.arg("--file").arg(file).succeeds();
}
#[test] #[test]
#[cfg(all(unix, not(target_os = "macos")))] #[cfg(all(unix, not(target_os = "macos")))]
/// TODO: expected to fail currently; change to succeeds() when required. /// TODO: expected to fail currently; change to succeeds() when required.
@ -232,3 +325,13 @@ fn test_invalid_format_string() {
result.no_stdout(); result.no_stdout();
assert!(result.stderr_str().starts_with("date: invalid format ")); assert!(result.stderr_str().starts_with("date: invalid format "));
} }
#[test]
fn test_invalid_date_string() {
new_ucmd!()
.arg("-d")
.arg("foo")
.fails()
.no_stdout()
.stderr_contains("invalid date");
}

View file

@ -7,7 +7,7 @@ use regex::Regex;
use std::fs::{File, OpenOptions}; use std::fs::{File, OpenOptions};
use std::io::{BufReader, Read, Write}; use std::io::{BufReader, Read, Write};
use std::path::PathBuf; use std::path::PathBuf;
#[cfg(all(not(windows), not(target_os = "macos")))] #[cfg(all(unix, not(target_os = "macos"), not(target_os = "freebsd")))]
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
#[cfg(not(windows))] #[cfg(not(windows))]
use std::thread::sleep; use std::thread::sleep;
@ -1520,3 +1520,17 @@ fn test_skip_input_fifo() {
assert!(output.stdout.is_empty()); assert!(output.stdout.is_empty());
assert_eq!(&output.stderr, b"1+0 records in\n1+0 records out\n"); assert_eq!(&output.stderr, b"1+0 records in\n1+0 records out\n");
} }
/// Test for reading part of stdin from each of two child processes.
#[cfg(all(not(windows), feature = "printf"))]
#[test]
fn test_multiple_processes_reading_stdin() {
// TODO Investigate if this is possible on Windows.
let printf = format!("{TESTS_BINARY} printf 'abcdef\n'");
let dd_skip = format!("{TESTS_BINARY} dd bs=1 skip=3 count=0");
let dd = format!("{TESTS_BINARY} dd");
UCommand::new()
.arg(format!("{printf} | ( {dd_skip} && {dd} ) 2> /dev/null"))
.succeeds()
.stdout_only("def\n");
}

View file

@ -82,10 +82,10 @@ fn _du_basics_subdir(s: &str) {
))] ))]
fn _du_basics_subdir(s: &str) { fn _du_basics_subdir(s: &str) {
// MS-WSL linux has altered expected output // MS-WSL linux has altered expected output
if !uucore::os::is_wsl_1() { if uucore::os::is_wsl_1() {
assert_eq!(s, "8\tsubdir/deeper\n");
} else {
assert_eq!(s, "0\tsubdir/deeper\n"); assert_eq!(s, "0\tsubdir/deeper\n");
} else {
assert_eq!(s, "8\tsubdir/deeper\n");
} }
} }
@ -164,10 +164,10 @@ fn _du_soft_link(s: &str) {
))] ))]
fn _du_soft_link(s: &str) { fn _du_soft_link(s: &str) {
// MS-WSL linux has altered expected output // MS-WSL linux has altered expected output
if !uucore::os::is_wsl_1() { if uucore::os::is_wsl_1() {
assert_eq!(s, "16\tsubdir/links\n");
} else {
assert_eq!(s, "8\tsubdir/links\n"); assert_eq!(s, "8\tsubdir/links\n");
} else {
assert_eq!(s, "16\tsubdir/links\n");
} }
} }
@ -212,10 +212,10 @@ fn _du_hard_link(s: &str) {
))] ))]
fn _du_hard_link(s: &str) { fn _du_hard_link(s: &str) {
// MS-WSL linux has altered expected output // MS-WSL linux has altered expected output
if !uucore::os::is_wsl_1() { if uucore::os::is_wsl_1() {
assert_eq!(s, "16\tsubdir/links\n");
} else {
assert_eq!(s, "8\tsubdir/links\n"); assert_eq!(s, "8\tsubdir/links\n");
} else {
assert_eq!(s, "16\tsubdir/links\n");
} }
} }
@ -255,10 +255,10 @@ fn _du_d_flag(s: &str) {
))] ))]
fn _du_d_flag(s: &str) { fn _du_d_flag(s: &str) {
// MS-WSL linux has altered expected output // MS-WSL linux has altered expected output
if !uucore::os::is_wsl_1() { if uucore::os::is_wsl_1() {
assert_eq!(s, "28\t./subdir\n36\t.\n");
} else {
assert_eq!(s, "8\t./subdir\n8\t.\n"); assert_eq!(s, "8\t./subdir\n8\t.\n");
} else {
assert_eq!(s, "28\t./subdir\n36\t.\n");
} }
} }
@ -303,10 +303,10 @@ fn _du_dereference(s: &str) {
))] ))]
fn _du_dereference(s: &str) { fn _du_dereference(s: &str) {
// MS-WSL linux has altered expected output // MS-WSL linux has altered expected output
if !uucore::os::is_wsl_1() { if uucore::os::is_wsl_1() {
assert_eq!(s, "8\tsubdir/links/deeper_dir\n24\tsubdir/links\n");
} else {
assert_eq!(s, "0\tsubdir/links/deeper_dir\n8\tsubdir/links\n"); assert_eq!(s, "0\tsubdir/links/deeper_dir\n8\tsubdir/links\n");
} else {
assert_eq!(s, "8\tsubdir/links/deeper_dir\n24\tsubdir/links\n");
} }
} }

View file

@ -1280,7 +1280,7 @@ fn test_ls_long_formats() {
// Zero or one "." for indicating a file with security context // Zero or one "." for indicating a file with security context
// Regex for three names, so all of author, group and owner // Regex for three names, so all of author, group and owner
let re_three = Regex::new(r"[xrw-]{9}\.? \d ([-0-9_a-z]+ ){3}0").unwrap(); let re_three = Regex::new(r"[xrw-]{9}\.? \d ([-0-9_a-z_A-Z]+ ){3}0").unwrap();
#[cfg(unix)] #[cfg(unix)]
let re_three_num = Regex::new(r"[xrw-]{9}\.? \d (\d+ ){3}0").unwrap(); let re_three_num = Regex::new(r"[xrw-]{9}\.? \d (\d+ ){3}0").unwrap();
@ -1289,13 +1289,13 @@ fn test_ls_long_formats() {
// - group and owner // - group and owner
// - author and owner // - author and owner
// - author and group // - author and group
let re_two = Regex::new(r"[xrw-]{9}\.? \d ([-0-9_a-z]+ ){2}0").unwrap(); let re_two = Regex::new(r"[xrw-]{9}\.? \d ([-0-9_a-z_A-Z]+ ){2}0").unwrap();
#[cfg(unix)] #[cfg(unix)]
let re_two_num = Regex::new(r"[xrw-]{9}\.? \d (\d+ ){2}0").unwrap(); let re_two_num = Regex::new(r"[xrw-]{9}\.? \d (\d+ ){2}0").unwrap();
// Regex for one name: author, group or owner // Regex for one name: author, group or owner
let re_one = Regex::new(r"[xrw-]{9}\.? \d [-0-9_a-z]+ 0").unwrap(); let re_one = Regex::new(r"[xrw-]{9}\.? \d [-0-9_a-z_A-Z]+ 0").unwrap();
#[cfg(unix)] #[cfg(unix)]
let re_one_num = Regex::new(r"[xrw-]{9}\.? \d \d+ 0").unwrap(); let re_one_num = Regex::new(r"[xrw-]{9}\.? \d \d+ 0").unwrap();

View file

@ -25,7 +25,6 @@ fn test_shred_remove() {
assert!(at.file_exists(file_b)); assert!(at.file_exists(file_b));
} }
#[cfg(not(target_os = "freebsd"))]
#[test] #[test]
fn test_shred_force() { fn test_shred_force() {
let scene = TestScenario::new(util_name!()); let scene = TestScenario::new(util_name!());

View file

@ -10,7 +10,7 @@ fn test_output_is_random_permutation() {
let input_seq = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; let input_seq = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let input = input_seq let input = input_seq
.iter() .iter()
.map(|x| x.to_string()) .map(ToString::to_string)
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join("\n"); .join("\n");
@ -52,7 +52,7 @@ fn test_echo() {
.args( .args(
&input_seq &input_seq
.iter() .iter()
.map(|x| x.to_string()) .map(ToString::to_string)
.collect::<Vec<String>>(), .collect::<Vec<String>>(),
) )
.succeeds(); .succeeds();
@ -74,7 +74,7 @@ fn test_head_count() {
let input_seq = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; let input_seq = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let input = input_seq let input = input_seq
.iter() .iter()
.map(|x| x.to_string()) .map(ToString::to_string)
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join("\n"); .join("\n");
@ -105,7 +105,7 @@ fn test_repeat() {
let input_seq = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; let input_seq = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let input = input_seq let input = input_seq
.iter() .iter()
.map(|x| x.to_string()) .map(ToString::to_string)
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join("\n"); .join("\n");

View file

@ -10,11 +10,11 @@ fn test_invalid_time_interval() {
new_ucmd!() new_ucmd!()
.arg("xyz") .arg("xyz")
.fails() .fails()
.usage_error("invalid time interval 'xyz'"); .usage_error("invalid time interval 'xyz': Invalid character: 'x' at position 1");
new_ucmd!() new_ucmd!()
.args(&["--", "-1"]) .args(&["--", "-1"])
.fails() .fails()
.usage_error("invalid time interval '-1'"); .usage_error("invalid time interval '-1': Number was negative");
} }
#[test] #[test]
@ -204,14 +204,16 @@ fn test_sleep_when_input_has_only_whitespace_then_error(#[case] input: &str) {
.arg(input) .arg(input)
.timeout(Duration::from_secs(10)) .timeout(Duration::from_secs(10))
.fails() .fails()
.usage_error(format!("invalid time interval '{input}'")); .usage_error(format!(
"invalid time interval '{input}': Found only whitespace in input"
));
} }
#[test] #[test]
fn test_sleep_when_multiple_input_some_with_error_then_shows_all_errors() { fn test_sleep_when_multiple_input_some_with_error_then_shows_all_errors() {
let expected = "invalid time interval 'abc'\n\ let expected = "invalid time interval 'abc': Invalid character: 'a' at position 1\n\
sleep: invalid time interval '1years'\n\ sleep: invalid time interval '1years': Invalid time unit: 'years' at position 2\n\
sleep: invalid time interval ' '"; sleep: invalid time interval ' ': Found only whitespace in input";
// Even if one of the arguments is valid, but the rest isn't, we should still fail and exit early. // Even if one of the arguments is valid, but the rest isn't, we should still fail and exit early.
// So, the timeout of 10 seconds ensures we haven't executed `thread::sleep` with the only valid // So, the timeout of 10 seconds ensures we haven't executed `thread::sleep` with the only valid
@ -228,5 +230,5 @@ fn test_negative_interval() {
new_ucmd!() new_ucmd!()
.args(&["--", "-1"]) .args(&["--", "-1"])
.fails() .fails()
.usage_error("invalid time interval '-1'"); .usage_error("invalid time interval '-1': Number was negative");
} }

View file

@ -4459,7 +4459,7 @@ fn test_follow_when_files_are_pointing_to_same_relative_file_and_file_stays_same
} }
#[rstest] #[rstest]
#[case::exponent_exceed_float_max("1.0e2048")] #[case::exponent_exceed_float_max("1.0e100000")]
#[case::underscore_delimiter("1_000")] #[case::underscore_delimiter("1_000")]
#[case::only_point(".")] #[case::only_point(".")]
#[case::space_in_primes("' '")] #[case::space_in_primes("' '")]

View file

@ -138,3 +138,19 @@ fn test_kill_after_long() {
.no_stdout() .no_stdout()
.no_stderr(); .no_stderr();
} }
#[test]
fn test_kill_subprocess() {
new_ucmd!()
.args(&[
// Make sure the CI can spawn the subprocess.
"10",
"sh",
"-c",
"sh -c \"trap 'echo xyz' TERM; sleep 30\"",
])
.fails()
.code_is(124)
.stdout_contains("xyz")
.stderr_contains("Terminated");
}

View file

@ -2822,7 +2822,7 @@ mod tests {
#[should_panic] #[should_panic]
fn test_cmd_result_stdout_check_when_false_then_panics() { fn test_cmd_result_stdout_check_when_false_then_panics() {
let result = TestScenario::new("echo").ucmd().arg("Hello world").run(); let result = TestScenario::new("echo").ucmd().arg("Hello world").run();
result.stdout_check(|s| s.is_empty()); result.stdout_check(<[u8]>::is_empty);
} }
#[cfg(feature = "echo")] #[cfg(feature = "echo")]
@ -3065,9 +3065,10 @@ mod tests {
// check `child.is_alive()` and `child.delay()` is working // check `child.is_alive()` and `child.delay()` is working
let mut trials = 10; let mut trials = 10;
while child.is_alive() { while child.is_alive() {
if trials <= 0 { assert!(
panic!("Assertion failed: child process is still alive.") trials > 0,
} "Assertion failed: child process is still alive."
);
child.delay(500); child.delay(500);
trials -= 1; trials -= 1;

View file

@ -18,7 +18,7 @@ for filepath in test_dir.glob("**/*.log"):
current[key] = {} current[key] = {}
current = current[key] current = current[key]
try: try:
with open(path) as f: with open(path, errors="ignore") as f:
content = f.read() content = f.read()
result = re.search( result = re.search(
r"(PASS|FAIL|SKIP|ERROR) [^ ]+ \(exit status: \d+\)$", content r"(PASS|FAIL|SKIP|ERROR) [^ ]+ \(exit status: \d+\)$", content