diff --git a/.cargo/config b/.cargo/config index 7724c1b54dc..a4dc4b423fe 100644 --- a/.cargo/config +++ b/.cargo/config @@ -3,10 +3,11 @@ linker = "x86_64-unknown-redox-gcc" [target.'cfg(feature = "cargo-clippy")'] rustflags = [ - "-Wclippy::use_self", - "-Wclippy::needless_pass_by_value", - "-Wclippy::semicolon_if_nothing_returned", - "-Wclippy::single_char_pattern", - "-Wclippy::explicit_iter_loop", + "-Wclippy::use_self", + "-Wclippy::needless_pass_by_value", + "-Wclippy::semicolon_if_nothing_returned", + "-Wclippy::single_char_pattern", + "-Wclippy::explicit_iter_loop", + "-Wclippy::if_not_else", ] diff --git a/.clippy.toml b/.clippy.toml index 22fd4be7375..bee70857bd1 100644 --- a/.clippy.toml +++ b/.clippy.toml @@ -1 +1,2 @@ msrv = "1.64.0" +cognitive-complexity-threshold = 10 diff --git a/.config/nextest.toml b/.config/nextest.toml new file mode 100644 index 00000000000..3ba8bb393a4 --- /dev/null +++ b/.config/nextest.toml @@ -0,0 +1,6 @@ +[profile.ci] +retries = 2 +status-level = "all" +final-status-level = "skip" +failure-output = "immediate-final" +fail-fast = false diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000000..ccae1f83130 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: uutils diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 0eeb948615b..47da567b286 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -1,9 +1,9 @@ name: CICD -# spell-checker:ignore (abbrev/names) CICD CodeCOV MacOS MinGW MSVC musl -# spell-checker:ignore (env/flags) Awarnings Ccodegen Coverflow Cpanic Dwarnings RUSTDOCFLAGS RUSTFLAGS Zpanic -# spell-checker:ignore (jargon) SHAs deps dequote softprops subshell toolchain -# spell-checker:ignore (people) Peltoche rivy +# spell-checker:ignore (abbrev/names) CICD CodeCOV MacOS MinGW MSVC musl taiki +# spell-checker:ignore (env/flags) Awarnings Ccodegen Coverflow Cpanic Dwarnings RUSTDOCFLAGS RUSTFLAGS Zpanic CARGOFLAGS +# spell-checker:ignore (jargon) SHAs deps dequote softprops subshell toolchain fuzzers +# spell-checker:ignore (people) Peltoche rivy dtolnay # spell-checker:ignore (shell/tools) choco clippy dmake dpkg esac fakeroot fdesc fdescfs gmake grcov halium lcov libssl mkdir popd printf pushd rsync rustc rustfmt rustup shopt utmpdump xargs # spell-checker:ignore (misc) aarch alnum armhf bindir busytest coreutils defconfig DESTDIR gecos gnueabihf issuecomment maint multisize nullglob onexitbegin onexitend pell runtest Swatinem tempfile testsuite toybox uutils @@ -48,6 +48,11 @@ jobs: - { os: windows-latest , features: feat_os_windows } steps: - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@nightly + ## note: requires 'nightly' toolchain b/c `cargo-udeps` uses the `rustc` '-Z save-analysis' option + ## * ... ref: + - uses: taiki-e/install-action@cargo-udeps + - uses: Swatinem/rust-cache@v2 - name: Initialize workflow variables id: vars shell: bash @@ -65,16 +70,6 @@ jobs: CARGO_FEATURES_OPTION='' ; if [ -n "${{ matrix.job.features }}" ]; then CARGO_FEATURES_OPTION='--features "${{ matrix.job.features }}"' ; fi outputs CARGO_FEATURES_OPTION - ## note: requires 'nightly' toolchain b/c `cargo-udeps` uses the `rustc` '-Z save-analysis' option - ## * ... ref: - - name: Install `rust` toolchain - run: | - rustup toolchain install nightly --no-self-update --profile minimal - rustup default nightly - - name: Install `cargo-udeps` - run: cargo install cargo-udeps - env: - RUSTUP_TOOLCHAIN: stable - name: Detect unused dependencies shell: bash run: | @@ -97,6 +92,11 @@ jobs: - { os: ubuntu-latest , features: feat_os_unix } steps: - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + components: rustfmt + - uses: Swatinem/rust-cache@v2 - name: Initialize workflow variables id: vars shell: bash @@ -114,11 +114,6 @@ jobs: CARGO_FEATURES_OPTION='' ; if [ -n "${{ matrix.job.features }}" ]; then CARGO_FEATURES_OPTION='--features "${{ matrix.job.features }}"' ; fi outputs CARGO_FEATURES_OPTION - - name: Install `rust` toolchain - run: | - ## Install `rust` toolchain - rustup toolchain install stable --no-self-update -c rustfmt --profile minimal - rustup default stable - name: "`cargo fmt` testing" shell: bash run: | @@ -137,16 +132,11 @@ jobs: RUN_FOR: 60 steps: - uses: actions/checkout@v3 - - uses: Swatinem/rust-cache@v2 - - name: Install `rust` toolchain - run: | - rustup toolchain install nightly --no-self-update --profile minimal - rustup default nightly + - uses: dtolnay/rust-toolchain@nightly - name: Install `cargo-fuzz` run: cargo install cargo-fuzz + - uses: Swatinem/rust-cache@v2 - name: Run fuzz_date for XX seconds - # TODO: fix https://github.com/uutils/coreutils/issues/4494 - continue-on-error: true shell: bash run: | ## Run it @@ -186,6 +176,10 @@ jobs: - { os: windows-latest , features: feat_os_windows } steps: - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + components: clippy - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache uses: mozilla-actions/sccache-action@v0.0.3 @@ -218,11 +212,6 @@ jobs: case '${{ matrix.job.os }}' in macos-latest) brew install coreutils ;; # needed for show-utils.sh esac - - name: Install `rust` toolchain - run: | - ## Install `rust` toolchain - rustup toolchain install stable --no-self-update -c clippy --profile minimal - rustup default stable - name: "`cargo clippy` lint testing" shell: bash run: | @@ -297,6 +286,10 @@ jobs: # - { os: windows-latest , features: feat_os_windows } steps: - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + components: clippy - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache uses: mozilla-actions/sccache-action@v0.0.3 @@ -322,16 +315,11 @@ jobs: echo UTILITY_LIST=${UTILITY_LIST} CARGO_UTILITY_LIST_OPTIONS="$(for u in ${UTILITY_LIST}; do echo -n "-puu_${u} "; done;)" outputs CARGO_UTILITY_LIST_OPTIONS - - name: Install `rust` toolchain - run: | - ## Install `rust` toolchain - rustup toolchain install stable --no-self-update -c clippy --profile minimal - rustup default stable - name: "`cargo doc` with warnings" shell: bash run: | RUSTDOCFLAGS="-Dwarnings" cargo doc ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} --no-deps --workspace --document-private-items - - uses: DavidAnson/markdownlint-cli2-action@v9 + - uses: DavidAnson/markdownlint-cli2-action@v11 with: command: fix globs: | @@ -351,6 +339,11 @@ jobs: - { os: ubuntu-latest , features: feat_os_unix } steps: - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.RUST_MIN_SRV }} + components: rustfmt + - uses: taiki-e/install-action@nextest - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache uses: mozilla-actions/sccache-action@v0.0.3 @@ -365,11 +358,6 @@ jobs: unset CARGO_FEATURES_OPTION if [ -n "${{ matrix.job.features }}" ]; then CARGO_FEATURES_OPTION='--features "${{ matrix.job.features }}"' ; fi outputs CARGO_FEATURES_OPTION - - name: Install `rust` toolchain (v${{ env.RUST_MIN_SRV }}) - run: | - ## Install `rust` toolchain (v${{ env.RUST_MIN_SRV }}) - rustup toolchain install --no-self-update ${{ env.RUST_MIN_SRV }} --profile minimal - rustup default ${{ env.RUST_MIN_SRV }} - name: Confirm MinSRV compatible 'Cargo.lock' shell: bash run: | @@ -404,7 +392,7 @@ jobs: RUSTUP_TOOLCHAIN=stable cargo fetch --locked --quiet RUSTUP_TOOLCHAIN=stable cargo tree --all --locked --no-dev-dependencies --no-indent ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} | grep -vE "$PWD" | sort --unique - name: Test - run: cargo test -v ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} -p uucore -p coreutils + run: cargo nextest run --hide-progress-bar --profile ci ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} -p uucore -p coreutils env: RUSTFLAGS: "-Awarnings" RUST_BACKTRACE: "1" @@ -419,11 +407,8 @@ jobs: - { os: ubuntu-latest , features: feat_os_unix } steps: - uses: actions/checkout@v3 - - name: Install `rust` toolchain - run: | - ## Install `rust` toolchain - rustup toolchain install stable --no-self-update --profile minimal - rustup default stable + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 - name: "`cargo update` testing" shell: bash run: | @@ -445,22 +430,18 @@ jobs: - { os: ubuntu-latest , features: feat_os_unix } steps: - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@stable + - uses: taiki-e/install-action@nextest - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache uses: mozilla-actions/sccache-action@v0.0.3 - - name: Install `rust` toolchain - run: | - ## Install `rust` toolchain - rustup toolchain install stable --no-self-update --profile minimal - rustup default stable - name: "`make build`" shell: bash run: | make build - - name: "`make test`" + - name: "`make nextest`" shell: bash - run: | - make test + run: make nextest CARGOFLAGS="--profile ci --hide-progress-bar" env: RUST_BACKTRACE: "1" - name: "`make install`" @@ -491,16 +472,13 @@ jobs: - { os: windows-latest , features: feat_os_windows } steps: - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@stable + - uses: taiki-e/install-action@nextest - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache uses: mozilla-actions/sccache-action@v0.0.3 - - name: Install `rust` toolchain - run: | - ## Install `rust` toolchain - rustup toolchain install stable --no-self-update --profile minimal - rustup default stable - name: Test - run: cargo test ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} + run: cargo nextest run --hide-progress-bar --profile ci ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} env: RUST_BACKTRACE: "1" @@ -521,16 +499,13 @@ jobs: - { os: windows-latest , features: feat_os_windows } steps: - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@nightly + - uses: taiki-e/install-action@nextest - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache uses: mozilla-actions/sccache-action@v0.0.3 - - name: Install `rust` toolchain - run: | - ## Install `rust` toolchain - rustup toolchain install nightly --no-self-update --profile minimal - rustup default nightly - name: Test - run: cargo test ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} + run: cargo nextest run --hide-progress-bar --profile ci ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} env: RUST_BACKTRACE: "1" @@ -548,6 +523,7 @@ jobs: - { os: ubuntu-latest , features: feat_os_unix } steps: - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache uses: mozilla-actions/sccache-action@v0.0.3 @@ -557,11 +533,6 @@ jobs: ## Install dependencies sudo apt-get update sudo apt-get install jq - - name: Install `rust` toolchain - run: | - ## Install `rust` toolchain - rustup toolchain install stable --no-self-update --profile minimal - rustup default stable - name: "`make install`" shell: bash run: | @@ -574,15 +545,70 @@ jobs: shell: bash run: | ## Compute uutil release sizes - SIZE=$(du -s target/size-release/usr/local/bin/|awk '{print $1}') - SIZE_MULTI=$(du -s target/size-multi-release/usr/local/bin/|awk '{print $1}') + DATE=$(date --rfc-email) + find target/size-release/usr/local/bin -type f -printf '%f\0' | sort -z | + while IFS= read -r -d '' name; do + size=$(du -s target/size-release/usr/local/bin/$name | awk '{print $1}') + echo "\"$name\"" + echo "$size" + done | \ + jq -n \ + --arg date "$DATE" \ + --arg sha "$GITHUB_SHA" \ + 'reduce inputs as $name ({}; . + { ($name): input }) | { ($date): {sha: $sha, sizes: map_values(.)} }' > individual-size-result.json + SIZE=$(cat individual-size-result.json | jq '[.[] | .sizes | .[]] | reduce .[] as $num (0; . + $num)') + SIZE_MULTI=$(du -s target/size-multi-release/usr/local/bin/coreutils | awk '{print $1}') jq -n \ - --arg date "$(date --rfc-email)" \ + --arg date "$DATE" \ --arg sha "$GITHUB_SHA" \ --arg size "$SIZE" \ --arg multisize "$SIZE_MULTI" \ '{($date): { sha: $sha, size: $size, multisize: $multisize, }}' > size-result.json - - uses: actions/upload-artifact@v3 + - name: Download the previous individual size result + uses: dawidd6/action-download-artifact@v2 + with: + workflow: CICD.yml + name: individual-size-result + repo: uutils/coreutils + path: dl + - name: Download the previous size result + uses: dawidd6/action-download-artifact@v2 + with: + workflow: CICD.yml + name: size-result + repo: uutils/coreutils + path: dl + - name: Check uutil release sizes + shell: bash + run: | + check() { + # Warn if the size increases by more than 5% + threshold='1.05' + ratio=$(jq -n "$2 / $3") + echo "$1: size=$2, previous_size=$3, ratio=$ratio, threshold=$threshold" + if [[ "$(jq -n "$ratio > $threshold")" == 'true' ]]; then + echo "::warning file=$4::Size of $1 increases by more than 5%" + fi + } + ## Check individual size result + while read -r name previous_size; do + size=$(cat individual-size-result.json | jq -r ".[] | .sizes | .\"$name\"") + check "\`$name\` binary" "$size" "$previous_size" 'individual-size-result.json' + done < <(cat dl/individual-size-result.json | jq -r '.[] | .sizes | to_entries[] | "\(.key) \(.value)"') + ## Check size result + size=$(cat size-result.json | jq -r '.[] | .size') + previous_size=$(cat dl/size-result.json | jq -r '.[] | .size') + check 'multiple binaries' "$size" "$previous_size" 'size-result.json' + multisize=$(cat size-result.json | jq -r '.[] | .multisize') + previous_multisize=$(cat dl/size-result.json | jq -r '.[] | .multisize') + check 'multicall binary' "$multisize" "$previous_multisize" 'size-result.json' + - name: Upload the individual size result + uses: actions/upload-artifact@v3 + with: + name: individual-size-result + path: individual-size-result.json + - name: Upload the size result + uses: actions/upload-artifact@v3 with: name: size-result path: size-result.json @@ -617,7 +643,13 @@ jobs: - { os: windows-latest , target: x86_64-pc-windows-msvc , features: feat_os_windows } steps: - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.RUST_MIN_SRV }} + targets: ${{ matrix.job.target }} - uses: Swatinem/rust-cache@v2 + with: + key: "${{ matrix.job.os }}_${{ matrix.job.target }}" - name: Run sccache-cache uses: mozilla-actions/sccache-action@v0.0.3 - name: Initialize workflow variables @@ -695,8 +727,7 @@ jobs: outputs CARGO_CMD # ** pass needed environment into `cross` container (iff `cross` not already configured via "Cross.toml") if [ "${CARGO_CMD}" = 'cross' ] && [ ! -e "Cross.toml" ] ; then - cargo install --version 0.2.1 cross - printf "[build.env]\npassthrough = [\"CI\", \"RUST_BACKTRACE\"]\n" > Cross.toml + printf "[build.env]\npassthrough = [\"CI\", \"RUST_BACKTRACE\", \"CARGO_TERM_COLOR\"]\n" > Cross.toml fi # * test only library and/or binaries for arm-type targets unset CARGO_TEST_OPTIONS ; case '${{ matrix.job.target }}' in aarch64-* | arm-*) CARGO_TEST_OPTIONS="--bins" ;; esac; @@ -709,6 +740,10 @@ jobs: *-pc-windows-msvc) STRIP="" ;; esac; outputs STRIP + - uses: taiki-e/install-action@v2 + if: steps.vars.outputs.CARGO_CMD == 'cross' + with: + tool: cross@0.2.1 - name: Create all needed build/work directories shell: bash run: | @@ -742,11 +777,6 @@ jobs: echo "foo" > /home/runner/.plan ;; esac - - name: rust toolchain ~ install - run: | - ## rust toolchain ~ install - rustup toolchain install --no-self-update ${{ env.RUST_MIN_SRV }} -t ${{ matrix.job.target }} --profile minimal - rustup default ${{ env.RUST_MIN_SRV }} - name: Initialize toolchain-dependent workflow variables id: dep_vars shell: bash @@ -952,14 +982,13 @@ jobs: TEST_SUMMARY_FILE="toybox-result.json" outputs TEST_SUMMARY_FILE - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.RUST_MIN_SRV }} + components: rustfmt - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache uses: mozilla-actions/sccache-action@v0.0.3 - - name: rust toolchain ~ install - run: | - ## rust toolchain ~ install - rustup toolchain install --no-self-update ${{ env.RUST_MIN_SRV }} --profile minimal - rustup default ${{ env.RUST_MIN_SRV }} - name: Build coreutils as multiple binaries shell: bash run: | @@ -1019,6 +1048,15 @@ jobs: name: toybox-result.json path: ${{ steps.vars.outputs.TEST_SUMMARY_FILE }} + toml_format: + runs-on: ubuntu-latest + steps: + - name: Clone repository + uses: actions/checkout@v3 + + - name: Check + run: npx --yes @taplo/cli fmt --check + coverage: name: Code Coverage runs-on: ${{ matrix.job.os }} @@ -1030,11 +1068,17 @@ jobs: fail-fast: false matrix: job: - - { os: ubuntu-latest , features: unix } - - { os: macos-latest , features: macos } - - { os: windows-latest , features: windows } + - { os: ubuntu-latest , features: unix, toolchain: nightly } + - { os: macos-latest , features: macos, toolchain: nightly } + - { os: windows-latest , features: windows, toolchain: nightly-x86_64-pc-windows-gnu } steps: - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.job.toolchain }} + components: rustfmt + - uses: taiki-e/install-action@nextest + - uses: taiki-e/install-action@grcov - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache uses: mozilla-actions/sccache-action@v0.0.3 @@ -1086,11 +1130,6 @@ jobs: echo "foo" > /home/runner/.plan ;; esac - - name: rust toolchain ~ install - run: | - ## rust toolchain ~ install - rustup toolchain install ${{ steps.vars.outputs.TOOLCHAIN }} --no-self-update --profile minimal - rustup default ${{ steps.vars.outputs.TOOLCHAIN }} - name: Initialize toolchain-dependent workflow variables id: dep_vars shell: bash @@ -1102,35 +1141,29 @@ jobs: CARGO_UTILITY_LIST_OPTIONS="$(for u in ${UTILITY_LIST}; do echo -n "-puu_${u} "; done;)" outputs CARGO_UTILITY_LIST_OPTIONS - name: Test uucore - run: cargo test --no-fail-fast -p uucore + run: cargo nextest run --profile ci --hide-progress-bar -p uucore env: - CARGO_INCREMENTAL: "0" RUSTC_WRAPPER: "" RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort" RUSTDOCFLAGS: "-Cpanic=abort" RUST_BACKTRACE: "1" # RUSTUP_TOOLCHAIN: ${{ steps.vars.outputs.TOOLCHAIN }} - name: Test - run: cargo test ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} --no-fail-fast + run: cargo nextest run --profile ci --hide-progress-bar ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} env: - CARGO_INCREMENTAL: "0" RUSTC_WRAPPER: "" RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort" RUSTDOCFLAGS: "-Cpanic=abort" RUST_BACKTRACE: "1" # RUSTUP_TOOLCHAIN: ${{ steps.vars.outputs.TOOLCHAIN }} - name: Test individual utilities - run: cargo test --no-fail-fast ${{ steps.dep_vars.outputs.CARGO_UTILITY_LIST_OPTIONS }} + run: cargo nextest run --profile ci --hide-progress-bar ${{ steps.dep_vars.outputs.CARGO_UTILITY_LIST_OPTIONS }} env: - CARGO_INCREMENTAL: "0" RUSTC_WRAPPER: "" RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort" RUSTDOCFLAGS: "-Cpanic=abort" RUST_BACKTRACE: "1" # RUSTUP_TOOLCHAIN: ${{ steps.vars.outputs.TOOLCHAIN }} - - name: "`grcov` ~ install" - id: build_grcov - run: cargo install grcov - name: Generate coverage data (via `grcov`) id: coverage shell: bash diff --git a/.github/workflows/FixPR.yml b/.github/workflows/FixPR.yml index 52561fb368f..e1729b17393 100644 --- a/.github/workflows/FixPR.yml +++ b/.github/workflows/FixPR.yml @@ -1,6 +1,6 @@ name: FixPR -# spell-checker:ignore Swatinem +# spell-checker:ignore Swatinem dtolnay # Trigger automated fixes for PRs being merged (with associated commits) @@ -36,11 +36,7 @@ jobs: # surface MSRV from CICD workflow RUST_MIN_SRV=$(grep -P "^\s+RUST_MIN_SRV:" .github/workflows/CICD.yml | grep -Po "(?<=\x22)\d+[.]\d+(?:[.]\d+)?(?=\x22)" ) outputs RUST_MIN_SRV - - name: Install `rust` toolchain (v${{ steps.vars.outputs.RUST_MIN_SRV }}) - run: | - ## Install `rust` toolchain (v${{ steps.vars.outputs.RUST_MIN_SRV }}) - rustup toolchain install ${{ steps.vars.outputs.RUST_MIN_SRV }} --profile minimal - rustup default ${{ steps.vars.outputs.RUST_MIN_SRV }} + - uses: dtolnay/rust-toolchain@${{ steps.vars.outputs.RUST_MIN_SRV }} - uses: Swatinem/rust-cache@v2 - name: Ensure updated 'Cargo.lock' shell: bash @@ -101,12 +97,10 @@ jobs: CARGO_FEATURES_OPTION='' ; if [ -n "${{ matrix.job.features }}" ]; then CARGO_FEATURES_OPTION='--features "${{ matrix.job.features }}"' ; fi outputs CARGO_FEATURES_OPTION - - name: Install `rust` toolchain - run: | - ## Install `rust` toolchain - rm -f "${HOME}/.cargo/bin/"{rustfmt,cargo-fmt} - rustup toolchain install stable -c rustfmt --profile minimal - rustup default stable + - uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + components: rustfmt - uses: Swatinem/rust-cache@v2 - name: "`cargo fmt`" shell: bash diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index 2e8297ba8e5..372bf0717d6 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -1,10 +1,10 @@ name: GnuTests -# spell-checker:ignore (abbrev/names) CodeCov gnulib GnuTests +# spell-checker:ignore (abbrev/names) CodeCov gnulib GnuTests Swatinem # spell-checker:ignore (jargon) submodules -# spell-checker:ignore (libs/utils) autopoint chksum gperf lcov libexpect pyinotify shopt texinfo valgrind +# spell-checker:ignore (libs/utils) autopoint chksum gperf lcov libexpect pyinotify shopt texinfo valgrind libattr libcap taiki-e # spell-checker:ignore (options) Ccodegen Coverflow Cpanic Zpanic -# spell-checker:ignore (people) Dawid Dziurla * dawidd +# spell-checker:ignore (people) Dawid Dziurla * dawidd dtolnay # spell-checker:ignore (vars) FILESET SUBDIRS XPASS # * note: to run a single test => `REPO/util/run-gnu-test.sh PATH/TO/TEST/SCRIPT` @@ -42,7 +42,7 @@ jobs: outputs path_GNU path_GNU_tests path_reference path_UUTILS # repo_default_branch="${{ github.event.repository.default_branch }}" - repo_GNU_ref="v9.2" + repo_GNU_ref="v9.3" repo_reference_branch="${{ github.event.repository.default_branch }}" outputs repo_default_branch repo_GNU_ref repo_reference_branch # @@ -58,6 +58,13 @@ jobs: uses: actions/checkout@v3 with: path: '${{ steps.vars.outputs.path_UUTILS }}' + - uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + components: rustfmt + - uses: Swatinem/rust-cache@v2 + with: + workspaces: "./${{ steps.vars.outputs.path_UUTILS }} -> target" - name: Checkout code (GNU coreutils) uses: actions/checkout@v3 with: @@ -75,12 +82,6 @@ jobs: # workflow_conclusion: success ## (default); * but, if commit with failed GnuTests is merged into the default branch, future commits will all show regression errors in GnuTests CI until o/w fixed workflow_conclusion: completed ## continually recalibrates to last commit of default branch with a successful GnuTests (ie, "self-heals" from GnuTest regressions, but needs more supervision for/of regressions) path: "${{ steps.vars.outputs.path_reference }}" - - name: Install `rust` toolchain - run: | - ## Install `rust` toolchain - rm -f "${HOME}/.cargo/bin/"{rustfmt,cargo-fmt} - rustup toolchain install stable -c rustfmt --profile minimal - rustup default stable - name: Install dependencies shell: bash run: | @@ -201,9 +202,10 @@ jobs: REF_LOG_FILE='${{ steps.vars.outputs.path_reference }}/test-logs/test-suite.log' REF_SUMMARY_FILE='${{ steps.vars.outputs.path_reference }}/test-summary/gnu-result.json' REPO_DEFAULT_BRANCH='${{ steps.vars.outputs.repo_default_branch }}' + path_UUTILS='${{ steps.vars.outputs.path_UUTILS }}' # https://github.com/uutils/coreutils/issues/4294 # https://github.com/uutils/coreutils/issues/4295 - IGNORE_INTERMITTENT='tests/tail-2/inotify-dir-recreate tests/misc/timeout tests/rm/rm1' + IGNORE_INTERMITTENT='${path_UUTILS}/.github/workflows/ignore-intermittent.txt' mkdir -p ${{ steps.vars.outputs.path_reference }} @@ -226,9 +228,17 @@ jobs: for LINE in ${REF_FAILING} do if ! grep -Fxq ${LINE}<<<"${NEW_FAILING}"; then - MSG="Congrats! The gnu test ${LINE} is no longer failing!" - echo "::warning ::$MSG" - echo $MSG >> ${COMMENT_LOG} + if ! grep ${LINE} ${IGNORE_INTERMITTENT} + then + MSG="Congrats! The gnu test ${LINE} is no longer failing!" + echo "::warning ::$MSG" + echo $MSG >> ${COMMENT_LOG} + else + MSG="Skipping an intermittent issue ${LINE}" + echo "::warning ::$MSG" + echo $MSG >> ${COMMENT_LOG} + echo "" + fi fi done for LINE in ${NEW_FAILING} @@ -305,14 +315,16 @@ jobs: with: repository: 'coreutils/coreutils' path: 'gnu' - ref: 'v9.2' + ref: 'v9.3' submodules: recursive - - name: Install `rust` toolchain - run: | - ## Install `rust` toolchain - rm -f "${HOME}/.cargo/bin/"{rustfmt,cargo-fmt} - rustup toolchain install nightly -c rustfmt --profile minimal - rustup default nightly + - uses: dtolnay/rust-toolchain@master + with: + toolchain: nightly + components: rustfmt + - uses: taiki-e/install-action@grcov + - uses: Swatinem/rust-cache@v2 + with: + workspaces: "./uutils -> target" - name: Install dependencies run: | ## Install dependencies @@ -333,7 +345,6 @@ jobs: locale -a - name: Build binaries env: - CARGO_INCREMENTAL: "0" RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort" RUSTDOCFLAGS: "-Cpanic=abort" run: | @@ -342,8 +353,6 @@ jobs: UU_MAKE_PROFILE=debug bash util/build-gnu.sh - name: Run GNU tests run: bash uutils/util/run-gnu-test.sh - - name: "`grcov` ~ install" - run: cargo install grcov - name: Generate coverage data (via `grcov`) id: coverage run: | diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index c5489919ebd..a6929b171cc 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -1,5 +1,7 @@ name: Android +# spell-checker:ignore TERMUX reactivecircus Swatinem noaudio pkill swiftshader dtolnay juliangruber + on: [push, pull_request] permissions: @@ -12,7 +14,7 @@ concurrency: jobs: test_android: - name: Test Android builds + name: Test builds runs-on: macos-latest timeout-minutes: 90 strategy: @@ -23,22 +25,18 @@ jobs: arch: [x86] # , arm64-v8a env: TERMUX: v0.118.0 - SCCACHE_GHA_ENABLED: "true" - RUSTC_WRAPPER: "sccache" steps: - uses: actions/checkout@v3 - - uses: Swatinem/rust-cache@v2 - - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.3 - - name: AVD cache - uses: actions/cache@v3 + - name: Restore AVD cache + uses: actions/cache/restore@v3 id: avd-cache with: path: | ~/.android/avd/* ~/.android/avd/*/snapshots/* ~/.android/adb* - key: avd-${{ matrix.api-level }}-${{ matrix.arch }}+termux-${{ env.TERMUX }} + ~/__rustc_hash__ + key: avd-${{ matrix.api-level }}-${{ matrix.arch }}+termux-${{ env.TERMUX }}+nextest+rustc-hash - name: Create and cache emulator image if: steps.avd-cache.outputs.cache-hit != 'true' uses: reactivecircus/android-emulator-runner@v2 @@ -47,26 +45,56 @@ jobs: target: ${{ matrix.target }} arch: ${{ matrix.arch }} ram-size: 2048M - disk-size: 5120M + disk-size: 7GB force-avd-creation: true emulator-options: -no-snapshot-load -noaudio -no-boot-anim -camera-back none script: | - wget https://github.com/termux/termux-app/releases/download/${{ env.TERMUX }}/termux-app_${{ env.TERMUX }}+github-debug_${{ matrix.arch }}.apk - util/android-commands.sh snapshot termux-app_${{ env.TERMUX }}+github-debug_${{ matrix.arch }}.apk - adb -s emulator-5554 emu avd snapshot save ${{ matrix.api-level }}-${{ matrix.arch }}+termux-${{ env.TERMUX }} - echo "Emulator image created." - pkill -9 qemu-system-x86_64 - - name: Build and Test on Android + util/android-commands.sh init "${{ matrix.arch }}" "${{ matrix.api-level }}" "${{ env.TERMUX }}" + - name: Save AVD cache + if: steps.avd-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v3 + with: + path: | + ~/.android/avd/* + ~/.android/avd/*/snapshots/* + ~/.android/adb* + ~/__rustc_hash__ + key: avd-${{ matrix.api-level }}-${{ matrix.arch }}+termux-${{ env.TERMUX }}+nextest+rustc-hash + - uses: juliangruber/read-file-action@v1 + id: read_rustc_hash + with: + # ~ expansion didn't work + path: /Users/runner/__rustc_hash__ + trim: true + - name: Restore rust cache + id: rust-cache + uses: actions/cache/restore@v3 + with: + path: ~/__rust_cache__ + # The version vX at the end of the key is just a development version to avoid conflicts in + # the github cache during the development of this workflow + key: ${{ matrix.arch }}_${{ matrix.target}}_${{ steps.read_rustc_hash.outputs.content }}_${{ hashFiles('**/Cargo.toml', '**/Cargo.lock') }}_v3 + - name: Build and Test uses: reactivecircus/android-emulator-runner@v2 with: api-level: ${{ matrix.api-level }} target: ${{ matrix.target }} arch: ${{ matrix.arch }} ram-size: 2048M - disk-size: 5120M + disk-size: 7GB force-avd-creation: false emulator-options: -no-snapshot-save -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -snapshot ${{ matrix.api-level }}-${{ matrix.arch }}+termux-${{ env.TERMUX }} + # This is not a usual script. Every line is executed in a separate shell with `sh -c`. If + # one of the lines returns with error the whole script is failed (like running a script with + # set -e) and in consequences the other lines (shells) are not executed. script: | - util/android-commands.sh sync + util/android-commands.sh sync_host util/android-commands.sh build util/android-commands.sh tests + if [[ "${{ steps.rust-cache.outputs.cache-hit }}" != 'true' ]]; then util/android-commands.sh sync_image; fi; exit 0 + - name: Save rust cache + if: steps.rust-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v3 + with: + path: ~/__rust_cache__ + key: ${{ matrix.arch }}_${{ matrix.target}}_${{ steps.read_rustc_hash.outputs.content }}_${{ hashFiles('**/Cargo.toml', '**/Cargo.lock') }}_v3 diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index b8d9ba0263c..9507b3a5644 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -1,5 +1,11 @@ name: FreeBSD +# spell-checker:ignore sshfs usesh vmactions taiki Swatinem esac fdescfs fdesc + +env: + # * style job configuration + STYLE_FAIL_ON_FAULT: true ## (bool) fail the build if a style job contains a fault (error or warning); may be overridden on a per-job basis + on: [push, pull_request] permissions: @@ -11,8 +17,97 @@ concurrency: cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} jobs: - test_freebsd: - name: Tests/FreeBSD test suite + style: + name: Style and Lint + runs-on: ${{ matrix.job.os }} + timeout-minutes: 90 + strategy: + fail-fast: false + matrix: + job: + - { os: macos-12 , features: unix } ## GHA MacOS-11.0 VM won't have VirtualBox; refs: , + env: + SCCACHE_GHA_ENABLED: "true" + RUSTC_WRAPPER: "sccache" + steps: + - uses: actions/checkout@v3 + - uses: Swatinem/rust-cache@v2 + - name: Run sccache-cache + uses: mozilla-actions/sccache-action@v0.0.3 + - name: Prepare, build and test + uses: vmactions/freebsd-vm@v0.3.0 + with: + usesh: true + # We need jq to run show-utils.sh and bash to use inline shell string replacement + prepare: pkg install -y curl sudo jq bash + run: | + ## Prepare, build, and test + # implementation modelled after ref: + # * NOTE: All steps need to be run in this block, otherwise, we are operating back on the mac host + set -e + # + TEST_USER=tester + REPO_NAME=${GITHUB_WORKSPACE##*/} + WORKSPACE_PARENT="/Users/runner/work/${REPO_NAME}" + WORKSPACE="${WORKSPACE_PARENT}/${REPO_NAME}" + # + pw adduser -n ${TEST_USER} -d /root/ -g wheel -c "Coreutils user to build" -w random + chown -R ${TEST_USER}:wheel /root/ "/Users/runner/work/${REPO_NAME}"/ + whoami + # + # Further work needs to be done in a sudo as we are changing users + sudo -i -u ${TEST_USER} bash << EOF + set -e + whoami + curl https://sh.rustup.rs -sSf --output rustup.sh + sh rustup.sh -y -c rustfmt,clippy --profile=minimal -t stable + . ${HOME}/.cargo/env + ## VARs setup + cd "${WORKSPACE}" + unset FAIL_ON_FAULT ; case '${{ env.STYLE_FAIL_ON_FAULT }}' in + ''|0|f|false|n|no|off) FAULT_TYPE=warning ;; + *) FAIL_ON_FAULT=true ; FAULT_TYPE=error ;; + esac; + FAULT_PREFIX=\$(echo "\${FAULT_TYPE}" | tr '[:lower:]' '[:upper:]') + # * determine sub-crate utility list + UTILITY_LIST="\$(./util/show-utils.sh --features ${{ matrix.job.features }})" + CARGO_UTILITY_LIST_OPTIONS="\$(for u in \${UTILITY_LIST}; do echo -n "-puu_\${u} "; done;)" + ## Info + # environment + echo "## environment" + echo "CI='${CI}'" + echo "REPO_NAME='${REPO_NAME}'" + echo "TEST_USER='${TEST_USER}'" + echo "WORKSPACE_PARENT='${WORKSPACE_PARENT}'" + echo "WORKSPACE='${WORKSPACE}'" + echo "FAULT_PREFIX='\${FAULT_PREFIX}'" + echo "UTILITY_LIST='\${UTILITY_LIST}'" + env | sort + # tooling info + echo "## tooling info" + cargo -V + rustc -V + # + # To ensure that files are cleaned up, we don't want to exit on error + set +e + unset FAULT + ## cargo fmt testing + echo "## cargo fmt testing" + # * convert any errors/warnings to GHA UI annotations; ref: + S=\$(cargo fmt -- --check) && printf "%s\n" "\$S" || { printf "%s\n" "\$S" ; printf "%s\n" "\$S" | sed -E -n -e "s/^Diff[[:space:]]+in[[:space:]]+\${PWD//\//\\\\/}\/(.*)[[:space:]]+at[[:space:]]+[^0-9]+([0-9]+).*\$/::\${FAULT_TYPE} file=\1,line=\2::\${FAULT_PREFIX}: \\\`cargo fmt\\\`: style violation (file:'\1', line:\2; use \\\`cargo fmt -- \"\1\"\\\`)/p" ; FAULT=true ; } + ## cargo clippy lint testing + if [ -z "\${FAULT}" ]; then + echo "## cargo clippy lint testing" + # * convert any warnings to GHA UI annotations; ref: + S=\$(cargo clippy --all-targets \${CARGO_UTILITY_LIST_OPTIONS} -- -W clippy::manual_string_new -D warnings 2>&1) && printf "%s\n" "\$S" || { printf "%s\n" "\$S" ; printf "%s" "\$S" | sed -E -n -e '/^error:/{' -e "N; s/^error:[[:space:]]+(.*)\\n[[:space:]]+-->[[:space:]]+(.*):([0-9]+):([0-9]+).*\$/::\${FAULT_TYPE} file=\2,line=\3,col=\4::\${FAULT_PREFIX}: \\\`cargo clippy\\\`: \1 (file:'\2', line:\3)/p;" -e '}' ; FAULT=true ; } + fi + # Clean to avoid to rsync back the files + cargo clean + if [ -n "\${FAIL_ON_FAULT}" ] && [ -n "\${FAULT}" ]; then exit 1 ; fi + EOF + + test: + name: Tests runs-on: ${{ matrix.job.os }} timeout-minutes: 90 strategy: @@ -30,7 +125,6 @@ jobs: - name: Run sccache-cache uses: mozilla-actions/sccache-action@v0.0.3 - name: Prepare, build and test - ## spell-checker:ignore (ToDO) sshfs usesh vmactions uses: vmactions/freebsd-vm@v0.3.0 with: usesh: true @@ -62,6 +156,9 @@ jobs: curl https://sh.rustup.rs -sSf --output rustup.sh sh rustup.sh -y --profile=minimal . $HOME/.cargo/env + # Install nextest + mkdir -p ~/.cargo/bin + curl -LsSf https://get.nexte.st/latest/freebsd | tar zxf - -C ~/.cargo/bin ## Info # environment echo "## environment" @@ -74,6 +171,7 @@ jobs: # tooling info echo "## tooling info" cargo -V + cargo nextest --version rustc -V # # To ensure that files are cleaned up, we don't want to exit on error @@ -81,9 +179,11 @@ jobs: cd "${WORKSPACE}" unset FAULT cargo build || FAULT=1 + export PATH=~/.cargo/bin:${PATH} export RUST_BACKTRACE=1 - if (test -z "\$FAULT"); then cargo test --features '${{ matrix.job.features }}' || FAULT=1 ; fi - if (test -z "\$FAULT"); then cargo test --all-features -p uucore || FAULT=1 ; fi + export CARGO_TERM_COLOR=always + if (test -z "\$FAULT"); then cargo nextest run --hide-progress-bar --profile ci --features '${{ matrix.job.features }}' || FAULT=1 ; fi + if (test -z "\$FAULT"); then cargo nextest run --hide-progress-bar --profile ci --all-features -p uucore || FAULT=1 ; fi # Clean to avoid to rsync back the files cargo clean if (test -n "\$FAULT"); then exit 1 ; fi diff --git a/.github/workflows/ignore-intermittent.txt b/.github/workflows/ignore-intermittent.txt new file mode 100644 index 00000000000..759bd96eb8a --- /dev/null +++ b/.github/workflows/ignore-intermittent.txt @@ -0,0 +1,3 @@ +tests/tail-2/inotify-dir-recreate +tests/misc/timeout +tests/rm/rm1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8199b6d87df..7dc2565c141 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ - + # Contributing to coreutils @@ -137,6 +137,14 @@ If you also want to test the core utilities: cargo test -p uucore -p coreutils ``` +Running the complete test suite might take a while. We use [nextest](https://nexte.st/index.html) in +the CI and you might want to try it out locally. It can speed up the execution time of the whole +test run significantly if the cpu has multiple cores. + +```shell +cargo nextest run --features unix --no-fail-fast +``` + To debug: ```shell @@ -171,6 +179,15 @@ To include tests for unimplemented behavior: make UTILS='UTILITY_1 UTILITY_2' SPEC=y test ``` +To run tests with `nextest` just use the nextest target. Note you'll need to +[install](https://nexte.st/book/installation.html) `nextest` first. The `nextest` target accepts the +same arguments like the default `test` target, so it's possible to pass arguments to `nextest run` +via `CARGOFLAGS`: + +```shell +make CARGOFLAGS='--no-fail-fast' UTILS='UTILITY_1 UTILITY_2' nextest +``` + ### Run Busybox Tests This testing functionality is only available on *nix operating systems and @@ -200,6 +217,8 @@ To run uutils against the GNU test suite locally, run the following commands: ```shell bash util/build-gnu.sh +# Build uutils without release optimizations +UU_MAKE_PROFILE=debug bash util/build-gnu.sh bash util/run-gnu-test.sh # To run a single test: bash util/run-gnu-test.sh tests/touch/not-owner.sh # for example @@ -326,7 +345,6 @@ if changes are not reflected in the report then run `cargo clean` and run the ab 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: @@ -339,10 +357,9 @@ The Coreutils have different implementations, with different levels of completio * [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 +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 uutils is distributed under the terms of the MIT License; see the `LICENSE` file diff --git a/Cargo.lock b/Cargo.lock index c742b50a2c0..2e39978ca6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,12 +34,27 @@ dependencies = [ "memchr", ] +[[package]] +name = "aho-corasick" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" +dependencies = [ + "memchr", +] + [[package]] name = "aliasable" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -49,6 +64,55 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" + +[[package]] +name = "anstyle-parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +dependencies = [ + "anstyle", + "windows-sys 0.48.0", +] + [[package]] name = "arrayref" version = "0.3.6" @@ -128,9 +192,9 @@ dependencies = [ [[package]] name = "blake3" -version = "1.3.3" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ae2468a89544a466886840aa467a25b766499f4f04bf7d9fcd10ecee9fccef" +checksum = "729b71f35bd3fa1a4c86b85d32c8b9069ea7fe14f7a53cfabb65f62d4265b888" dependencies = [ "arrayref", "arrayvec", @@ -151,9 +215,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.0.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fca0852af221f458706eb0725c03e4ed6c46af9ac98e6a689d5e634215d594dd" +checksum = "a246e68bb43f6cd9db24bea052a53e40405417c5fb372e3d1a8a7f770a564ef5" dependencies = [ "memchr", "once_cell", @@ -202,12 +266,12 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.24" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" dependencies = [ + "android-tzdata", "iana-time-zone", - "num-integer", "num-traits", "winapi", ] @@ -225,36 +289,42 @@ dependencies = [ [[package]] name = "clap" -version = "4.1.13" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c911b090850d79fc64fe9ea01e28e465f65e821e08813ced95bced72f7a8a9b" +checksum = "93aae7a4192245f70fe75dd9157fc7b4a5bf53e88d30bd4396f7d8f9284d5acc" dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f423e341edefb78c9caba2d9c7f7687d0e72e89df3ce3394554754393ac3990" +dependencies = [ + "anstream", + "anstyle", "bitflags", "clap_lex", - "is-terminal", "once_cell", "strsim", - "termcolor", "terminal_size", ] [[package]] name = "clap_complete" -version = "4.0.6" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7b3c9eae0de7bf8e3f904a5e40612b21fb2e2e566456d177809a48b892d24da" +checksum = "a04ddfaacc3bc9e6ea67d024575fafc2a813027cf374b8f24f7bc233c6b6be12" dependencies = [ "clap", ] [[package]] name = "clap_lex" -version = "0.3.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8" -dependencies = [ - "os_str_bytes", -] +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" [[package]] name = "clap_mangen" @@ -276,6 +346,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "compare" version = "0.1.0" @@ -284,15 +360,37 @@ checksum = "120133d4db2ec47efe2e26502ee984747630c67f51974fca0b6c1340cf2368d3" [[package]] name = "console" -version = "0.15.5" +version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d79fbe8970a77e3e34151cc13d3b3e248aa0faaecb9f6091fa07ebefe5ad60" +checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" dependencies = [ "encode_unicode", "lazy_static", "libc", "unicode-width", - "windows-sys 0.42.0", + "windows-sys 0.45.0", +] + +[[package]] +name = "const-random" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368a7a772ead6ce7e1de82bfb04c485f3db8ec744f72925af5735e29a22cc18e" +dependencies = [ + "const-random-macro", + "proc-macro-hack", +] + +[[package]] +name = "const-random-macro" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d7d6ab3c3a2282db210df5f02c4dab6e0a7057af0fb7ebd4070f30fe05c0ddb" +dependencies = [ + "getrandom", + "once_cell", + "proc-macro-hack", + "tiny-keccak", ] [[package]] @@ -318,7 +416,7 @@ checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" [[package]] name = "coreutils" -version = "0.0.17" +version = "0.0.19" dependencies = [ "chrono", "clap", @@ -347,7 +445,6 @@ dependencies = [ "textwrap", "time", "unindent", - "users", "uu_arch", "uu_base32", "uu_base64", @@ -452,6 +549,7 @@ dependencies = [ "uu_whoami", "uu_yes", "uucore", + "uuhelp_parser", "walkdir", "zip", ] @@ -507,7 +605,7 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fdaa01904c12a8989dbfa110b41ef27efc432ac9934f691b9732f01cb64dc01" dependencies = [ - "aho-corasick", + "aho-corasick 0.7.19", "byteorder", "cpp_common", "lazy_static", @@ -536,9 +634,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.6" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" dependencies = [ "cfg-if", "crossbeam-utils", @@ -557,9 +655,9 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.12" +version = "0.9.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96bf8df95e795db1a4aca2957ad884a2df35413b24bbeb3114422f3cc21498e8" +checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695" dependencies = [ "autocfg", "cfg-if", @@ -570,18 +668,18 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.13" +version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "422f23e724af1240ec469ea1e834d87a4b59ce2efe2c6a96256b0c47e2fd86aa" +checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" dependencies = [ "cfg-if", ] [[package]] name = "crossterm" -version = "0.25.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +checksum = "a84cda67535339806297f1b331d6dd6320470d2a0fe65381e79ee9e156dd3d13" dependencies = [ "bitflags", "crossterm_winapi", @@ -630,12 +728,12 @@ dependencies = [ [[package]] name = "ctrlc" -version = "3.2.4" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1631ca6e3c59112501a9d87fd86f21591ff77acd31331e8a73f8d80a65bbdd71" +checksum = "2a011bbe2c35ce9c1f143b7af6f94f29a167beb4cd1d29e6740ce836f723120e" dependencies = [ "nix", - "windows-sys 0.42.0", + "windows-sys 0.48.0", ] [[package]] @@ -690,15 +788,15 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.3.2" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" +checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "data-encoding-macro" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86927b7cd2fe88fa698b87404b287ab98d1a0063a34071d92e575b72d3029aca" +checksum = "c904b33cc60130e1aeea4956ab803d08a3f4a0ca82d64ed757afac3891f2bb99" dependencies = [ "data-encoding", "data-encoding-macro-internal", @@ -706,9 +804,9 @@ dependencies = [ [[package]] name = "data-encoding-macro-internal" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5bbed42daaa95e780b60a50546aa345b8413a1e46f9a40a12907d3598f038db" +checksum = "8fdf3fce3ce863539ec1d7fd1b6dcc3c645663376b43ed376bbf887733e4f772" dependencies = [ "data-encoding", "syn", @@ -722,9 +820,9 @@ checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" [[package]] name = "digest" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", @@ -733,27 +831,30 @@ dependencies = [ [[package]] name = "dlv-list" -version = "0.3.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" +checksum = "d529fd73d344663edfd598ccb3f344e46034db51ebd103518eae34338248ad73" +dependencies = [ + "const-random", +] [[package]] name = "dns-lookup" -version = "1.0.8" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53ecafc952c4528d9b51a458d1a8904b81783feff9fde08ab6ed2545ff396872" +checksum = "8f332aa79f9e9de741ac013237294ef42ce2e9c6394dc7d766725812f1238812" dependencies = [ "cfg-if", "libc", "socket2", - "winapi", + "windows-sys 0.48.0", ] [[package]] name = "dunce" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bd4b30a6560bbd9b4620f4de34c3f14f60848e58a9b7216801afcb4c7b31c3c" +checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" [[package]] name = "either" @@ -779,13 +880,13 @@ dependencies = [ [[package]] name = "errno" -version = "0.2.8" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" dependencies = [ "errno-dragonfly", "libc", - "winapi", + "windows-sys 0.48.0", ] [[package]] @@ -800,9 +901,9 @@ dependencies = [ [[package]] name = "exacl" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "129c7b60e19ea8393c47b2110f8e3cea800530fd962380ef110d1fef6591faee" +checksum = "1cfeb22a59deb24c3262c43ffcafd1eb807180f371f9fcc99098d181b5d639be" dependencies = [ "bitflags", "log", @@ -827,14 +928,14 @@ checksum = "31a7a908b8f32538a2143e59a6e4e2508988832d5d4d6f7c156b3cbc762643a5" [[package]] name = "filetime" -version = "0.2.18" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b9663d381d07ae25dc88dbdf27df458faa83a9b25336bcac83d5e452b5fc9d3" +checksum = "8a3de6e8d11b22ff9edc6d916f890800597d60f8b2da1caf2955c274638d6412" dependencies = [ "cfg-if", "libc", - "redox_syscall", - "windows-sys 0.42.0", + "redox_syscall 0.2.16", + "windows-sys 0.45.0", ] [[package]] @@ -880,9 +981,9 @@ dependencies = [ [[package]] name = "fundu" -version = "0.4.3" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46d94380e064d45b7067806001781761c861c9c4785bca7baf98361fa2149b32" +checksum = "47af3b646bdd738395be2db903fc11a5923b5e206016b8d4ad6db890bcae9bd5" [[package]] name = "futures" @@ -981,9 +1082,9 @@ dependencies = [ [[package]] name = "gcd" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4b1b088ad0a967aa29540456b82fc8903f854775d33f71e9709c4efb3dfbfd2" +checksum = "1d758ba1b47b00caf47f24925c0074ecb20d6dfcffe7f6d53395c0465674841a" [[package]] name = "generic-array" @@ -997,9 +1098,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" dependencies = [ "cfg-if", "libc", @@ -1008,15 +1109,15 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "half" -version = "2.1.0" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad6a9459c9c30b177b925162351f97e7d967c7ea8bab3b8352805327daf45554" +checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0" dependencies = [ "crunchy", ] @@ -1030,6 +1131,12 @@ dependencies = [ "ahash", ] +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -1053,9 +1160,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hex-literal" -version = "0.3.4" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" [[package]] name = "hostname" @@ -1068,6 +1175,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "humantime_to_duration" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714764645f21cc70c4c151d7798dd158409641f37ad820bed65224aae403cbed" +dependencies = [ + "regex", + "time", +] + [[package]] name = "iana-time-zone" version = "0.1.53" @@ -1135,24 +1252,25 @@ dependencies = [ [[package]] name = "io-lifetimes" -version = "1.0.5" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ + "hermit-abi 0.3.1", "libc", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] name = "is-terminal" -version = "0.4.3" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0a45d56fe973d6db23972bf5bc46f988a4a2385deac9cc29572f09daef" +checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" dependencies = [ "hermit-abi 0.3.1", "io-lifetimes", - "rustix", - "windows-sys 0.45.0", + "rustix 0.37.19", + "windows-sys 0.48.0", ] [[package]] @@ -1181,9 +1299,9 @@ dependencies = [ [[package]] name = "keccak" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3afef3b6eff9ce9d8ff9b3601125eec7f0c8cbac7abd14f355d053fa56c98768" +checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940" dependencies = [ "cpufeatures", ] @@ -1222,9 +1340,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.139" +version = "0.2.146" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" +checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b" [[package]] name = "libloading" @@ -1251,6 +1369,12 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + [[package]] name = "lock_api" version = "0.4.9" @@ -1272,9 +1396,9 @@ dependencies = [ [[package]] name = "lscolors" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2dedc85d67baf5327114fad78ab9418f8893b1121c17d5538dd11005ad1ddf2" +checksum = "18a9df1d1fb6d9e92fa043e9eb9a3ecf6892c7b542bae5137cd1e419e40aa8bf" dependencies = [ "nu-ansi-term", ] @@ -1302,18 +1426,18 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "memmap2" -version = "0.5.8" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b182332558b18d807c4ce1ca8ca983b34c3ee32765e47b3f0f69b90355cc1dc" +checksum = "180d4b35be83d33392d1d1bfbd2ae1eca7ff5de1a94d3fc87faaa99a069e7cbd" dependencies = [ "libc", ] [[package]] name = "memoffset" -version = "0.7.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" dependencies = [ "autocfg", ] @@ -1335,14 +1459,14 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" +checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" dependencies = [ "libc", "log", "wasi", - "windows-sys 0.42.0", + "windows-sys 0.45.0", ] [[package]] @@ -1359,9 +1483,9 @@ dependencies = [ [[package]] name = "nom" -version = "7.1.1" +version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", @@ -1369,9 +1493,9 @@ dependencies = [ [[package]] name = "notify" -version = "5.0.0" +version = "6.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2c66da08abae1c024c01d635253e402341b4060a12e99b31c7594063bf490a" +checksum = "5738a2795d57ea20abec2d6d76c6081186709c0024187cd5977265eda6598b51" dependencies = [ "bitflags", "crossbeam-channel", @@ -1382,17 +1506,16 @@ dependencies = [ "libc", "mio", "walkdir", - "winapi", + "windows-sys 0.45.0", ] [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "1df031e117bca634c262e9bd3173776844b6c17a90b3741c9163663b4385af76" dependencies = [ - "overload", - "winapi", + "windows-sys 0.45.0", ] [[package]] @@ -1452,9 +1575,9 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "once_cell" -version = "1.17.1" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "onig" @@ -1480,12 +1603,12 @@ dependencies = [ [[package]] name = "ordered-multimap" -version = "0.4.3" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" +checksum = "4ed8acf08e98e744e5384c8bc63ceb0364e68a6854187221c18df61c4797690e" dependencies = [ "dlv-list", - "hashbrown", + "hashbrown 0.13.2", ] [[package]] @@ -1497,12 +1620,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "os_str_bytes" -version = "6.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" - [[package]] name = "ouroboros" version = "0.15.6" @@ -1535,12 +1652,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "parking_lot" version = "0.12.1" @@ -1553,15 +1664,25 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.4" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0" +checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.2.16", "smallvec", - "windows-sys 0.42.0", + "windows-sys 0.45.0", +] + +[[package]] +name = "parse_datetime" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fecceaede7767a9a98058687a321bc91742eff7670167a34104afb30fc8757df" +dependencies = [ + "chrono", + "regex", ] [[package]] @@ -1628,9 +1749,9 @@ checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" [[package]] name = "platform-info" -version = "1.0.2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7c23cfae725ae06d9e43010153fa77bdfa8c827bf08fe4beeb2a3514e6be12" +checksum = "827dc4f7a81331d48c8abf11b5ac18673b390d33e9632327e286d940289aefab" dependencies = [ "libc", "winapi", @@ -1684,6 +1805,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + [[package]] name = "proc-macro2" version = "1.0.47" @@ -1703,7 +1830,7 @@ dependencies = [ "byteorder", "hex", "lazy_static", - "rustix", + "rustix 0.36.14", ] [[package]] @@ -1802,6 +1929,15 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags", +] + [[package]] name = "reference-counted-singleton" version = "0.1.2" @@ -1810,11 +1946,11 @@ checksum = "f1bfbf25d7eb88ddcbb1ec3d755d0634da8f7657b2cb8b74089121409ab8228f" [[package]] name = "regex" -version = "1.7.3" +version = "1.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" +checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" dependencies = [ - "aho-corasick", + "aho-corasick 1.0.1", "memchr", "regex-syntax", ] @@ -1827,9 +1963,9 @@ checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" [[package]] name = "regex-syntax" -version = "0.6.29" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" +checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" [[package]] name = "rlimit" @@ -1848,9 +1984,9 @@ checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316" [[package]] name = "rstest" -version = "0.16.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b07f2d176c472198ec1e6551dc7da28f1c089652f66a7b722676c2238ebc0edf" +checksum = "de1bb486a691878cd320c2f0d319ba91eeaa2e894066d8b5f8f117c000e9d962" dependencies = [ "futures", "futures-timer", @@ -1860,9 +1996,9 @@ dependencies = [ [[package]] name = "rstest_macros" -version = "0.16.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7229b505ae0706e64f37ffc54a9c163e11022a6636d58fe1f3f52018257ff9f7" +checksum = "290ca1a1c8ca7edb7c3283bd44dc35dd54fdec6253a3912e201ba1072018fca8" dependencies = [ "cfg-if", "proc-macro2", @@ -1874,9 +2010,9 @@ dependencies = [ [[package]] name = "rust-ini" -version = "0.18.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" +checksum = "7e2a3bcec1f113553ef1c88aae6c020a369d03d55b58de9869a0908930385091" dependencies = [ "cfg-if", "ordered-multimap", @@ -1899,18 +2035,32 @@ dependencies = [ [[package]] name = "rustix" -version = "0.36.8" +version = "0.36.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43abb88211988493c1abb44a70efa56ff0ce98f233b7b276146f1f3f7ba9644" +checksum = "14e4d67015953998ad0eb82887a0eb0129e18a7e2f3b7b0f6c422fddcd503d62" dependencies = [ "bitflags", "errno", "io-lifetimes", "libc", - "linux-raw-sys", + "linux-raw-sys 0.1.4", "windows-sys 0.45.0", ] +[[package]] +name = "rustix" +version = "0.37.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + [[package]] name = "same-file" version = "1.0.6" @@ -1983,9 +2133,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" dependencies = [ "cfg-if", "cpufeatures", @@ -1994,9 +2144,9 @@ dependencies = [ [[package]] name = "sha3" -version = "0.10.6" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdf0c33fae925bdc080598b84bc15c55e7b9a4a43b3c704da051f977469691c9" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" dependencies = [ "digest", "keccak", @@ -2055,9 +2205,9 @@ dependencies = [ [[package]] name = "sm3" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f943a7c5e3089f2bd046221d1e9f4fa59396bf0fe966360983649683086215da" +checksum = "ebb9a3b702d0a7e33bc4d85a14456633d2b165c2ad839c5fd9a8417c1ab15860" dependencies = [ "digest", ] @@ -2076,12 +2226,12 @@ checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" [[package]] name = "socket2" -version = "0.4.7" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" dependencies = [ "libc", - "winapi", + "windows-sys 0.48.0", ] [[package]] @@ -2115,15 +2265,16 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.4.0" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95" +checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" dependencies = [ + "autocfg", "cfg-if", "fastrand", - "redox_syscall", - "rustix", - "windows-sys 0.42.0", + "redox_syscall 0.3.5", + "rustix 0.37.19", + "windows-sys 0.48.0", ] [[package]] @@ -2146,12 +2297,12 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c9afddd2cec1c0909f06b00ef33f94ab2cc0578c4a610aa208ddfec8aa2b43a" +checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237" dependencies = [ - "rustix", - "windows-sys 0.45.0", + "rustix 0.37.19", + "windows-sys 0.48.0", ] [[package]] @@ -2215,6 +2366,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "typenum" version = "1.15.0" @@ -2233,15 +2393,15 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137" dependencies = [ - "hashbrown", + "hashbrown 0.12.3", "regex", ] [[package]] name = "unicode-segmentation" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" [[package]] name = "unicode-width" @@ -2257,29 +2417,19 @@ checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "unindent" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58ee9362deb4a96cef4d437d1ad49cffc9b9e92d202b6995674e928ce684f112" - -[[package]] -name = "users" -version = "0.11.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032" -dependencies = [ - "libc", - "log", -] +checksum = "5aa30f5ea51ff7edfc797c6d3f9ec8cbd8cfedef5371766b7181d33977f4814f" [[package]] -name = "utf-8" -version = "0.7.6" +name = "utf8parse" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uu_arch" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "platform-info", @@ -2288,7 +2438,7 @@ dependencies = [ [[package]] name = "uu_base32" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "uucore", @@ -2296,7 +2446,7 @@ dependencies = [ [[package]] name = "uu_base64" -version = "0.0.17" +version = "0.0.19" dependencies = [ "uu_base32", "uucore", @@ -2304,7 +2454,7 @@ dependencies = [ [[package]] name = "uu_basename" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "uucore", @@ -2312,7 +2462,7 @@ dependencies = [ [[package]] name = "uu_basenc" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "uu_base32", @@ -2321,7 +2471,7 @@ dependencies = [ [[package]] name = "uu_cat" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "is-terminal", @@ -2332,7 +2482,7 @@ dependencies = [ [[package]] name = "uu_chcon" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "fts-sys", @@ -2344,7 +2494,7 @@ dependencies = [ [[package]] name = "uu_chgrp" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "uucore", @@ -2352,7 +2502,7 @@ dependencies = [ [[package]] name = "uu_chmod" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "libc", @@ -2361,7 +2511,7 @@ dependencies = [ [[package]] name = "uu_chown" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "uucore", @@ -2369,7 +2519,7 @@ dependencies = [ [[package]] name = "uu_chroot" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "uucore", @@ -2377,7 +2527,7 @@ dependencies = [ [[package]] name = "uu_cksum" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "hex", @@ -2386,7 +2536,7 @@ dependencies = [ [[package]] name = "uu_comm" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "uucore", @@ -2394,7 +2544,7 @@ dependencies = [ [[package]] name = "uu_cp" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "exacl", @@ -2410,7 +2560,7 @@ dependencies = [ [[package]] name = "uu_csplit" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "regex", @@ -2420,7 +2570,7 @@ dependencies = [ [[package]] name = "uu_cut" -version = "0.0.17" +version = "0.0.19" dependencies = [ "bstr", "clap", @@ -2431,38 +2581,41 @@ dependencies = [ [[package]] name = "uu_date" -version = "0.0.17" +version = "0.0.19" dependencies = [ "chrono", "clap", "libc", + "parse_datetime", "uucore", - "windows-sys 0.42.0", + "windows-sys 0.48.0", ] [[package]] name = "uu_dd" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "gcd", "libc", + "nix", "signal-hook", "uucore", ] [[package]] name = "uu_df" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", + "tempfile", "unicode-width", "uucore", ] [[package]] name = "uu_dir" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "uu_ls", @@ -2471,7 +2624,7 @@ dependencies = [ [[package]] name = "uu_dircolors" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "uucore", @@ -2479,7 +2632,7 @@ dependencies = [ [[package]] name = "uu_dirname" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "uucore", @@ -2487,18 +2640,18 @@ dependencies = [ [[package]] name = "uu_du" -version = "0.0.17" +version = "0.0.19" dependencies = [ "chrono", "clap", "glob", "uucore", - "windows-sys 0.42.0", + "windows-sys 0.48.0", ] [[package]] name = "uu_echo" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "uucore", @@ -2506,7 +2659,7 @@ dependencies = [ [[package]] name = "uu_env" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "nix", @@ -2516,7 +2669,7 @@ dependencies = [ [[package]] name = "uu_expand" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "unicode-width", @@ -2525,7 +2678,7 @@ dependencies = [ [[package]] name = "uu_expr" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "num-bigint", @@ -2536,7 +2689,7 @@ dependencies = [ [[package]] name = "uu_factor" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "coz", @@ -2549,7 +2702,7 @@ dependencies = [ [[package]] name = "uu_false" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "uucore", @@ -2557,7 +2710,7 @@ dependencies = [ [[package]] name = "uu_fmt" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "unicode-width", @@ -2566,7 +2719,7 @@ dependencies = [ [[package]] name = "uu_fold" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "uucore", @@ -2574,7 +2727,7 @@ dependencies = [ [[package]] name = "uu_groups" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "uucore", @@ -2582,7 +2735,7 @@ dependencies = [ [[package]] name = "uu_hashsum" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "hex", @@ -2593,7 +2746,7 @@ dependencies = [ [[package]] name = "uu_head" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "memchr", @@ -2602,7 +2755,7 @@ dependencies = [ [[package]] name = "uu_hostid" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "libc", @@ -2611,17 +2764,17 @@ dependencies = [ [[package]] name = "uu_hostname" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "hostname", "uucore", - "windows-sys 0.42.0", + "windows-sys 0.48.0", ] [[package]] name = "uu_id" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "selinux", @@ -2630,19 +2783,18 @@ dependencies = [ [[package]] name = "uu_install" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "file_diff", "filetime", "libc", - "time", "uucore", ] [[package]] name = "uu_join" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "memchr", @@ -2651,7 +2803,7 @@ dependencies = [ [[package]] name = "uu_kill" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "nix", @@ -2660,7 +2812,7 @@ dependencies = [ [[package]] name = "uu_link" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "uucore", @@ -2668,7 +2820,7 @@ dependencies = [ [[package]] name = "uu_ln" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "uucore", @@ -2676,7 +2828,7 @@ dependencies = [ [[package]] name = "uu_logname" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "libc", @@ -2685,7 +2837,7 @@ dependencies = [ [[package]] name = "uu_ls" -version = "0.0.17" +version = "0.0.19" dependencies = [ "chrono", "clap", @@ -2703,7 +2855,7 @@ dependencies = [ [[package]] name = "uu_mkdir" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "uucore", @@ -2711,7 +2863,7 @@ dependencies = [ [[package]] name = "uu_mkfifo" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "libc", @@ -2720,7 +2872,7 @@ dependencies = [ [[package]] name = "uu_mknod" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "libc", @@ -2729,7 +2881,7 @@ dependencies = [ [[package]] name = "uu_mktemp" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "rand", @@ -2739,7 +2891,7 @@ dependencies = [ [[package]] name = "uu_more" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "crossterm", @@ -2752,7 +2904,7 @@ dependencies = [ [[package]] name = "uu_mv" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "fs_extra", @@ -2762,7 +2914,7 @@ dependencies = [ [[package]] name = "uu_nice" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "libc", @@ -2772,7 +2924,7 @@ dependencies = [ [[package]] name = "uu_nl" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "regex", @@ -2781,7 +2933,7 @@ dependencies = [ [[package]] name = "uu_nohup" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "is-terminal", @@ -2791,7 +2943,7 @@ dependencies = [ [[package]] name = "uu_nproc" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "libc", @@ -2800,7 +2952,7 @@ dependencies = [ [[package]] name = "uu_numfmt" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "uucore", @@ -2808,7 +2960,7 @@ dependencies = [ [[package]] name = "uu_od" -version = "0.0.17" +version = "0.0.19" dependencies = [ "byteorder", "clap", @@ -2818,7 +2970,7 @@ dependencies = [ [[package]] name = "uu_paste" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "uucore", @@ -2826,7 +2978,7 @@ dependencies = [ [[package]] name = "uu_pathchk" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "libc", @@ -2835,7 +2987,7 @@ dependencies = [ [[package]] name = "uu_pinky" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "uucore", @@ -2843,19 +2995,19 @@ dependencies = [ [[package]] name = "uu_pr" -version = "0.0.17" +version = "0.0.19" dependencies = [ + "chrono", "clap", "itertools", "quick-error", "regex", - "time", "uucore", ] [[package]] name = "uu_printenv" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "uucore", @@ -2863,7 +3015,7 @@ dependencies = [ [[package]] name = "uu_printf" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "uucore", @@ -2871,7 +3023,7 @@ dependencies = [ [[package]] name = "uu_ptx" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "regex", @@ -2880,7 +3032,7 @@ dependencies = [ [[package]] name = "uu_pwd" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "uucore", @@ -2888,7 +3040,7 @@ dependencies = [ [[package]] name = "uu_readlink" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "uucore", @@ -2896,7 +3048,7 @@ dependencies = [ [[package]] name = "uu_realpath" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "uucore", @@ -2904,7 +3056,7 @@ dependencies = [ [[package]] name = "uu_relpath" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "uucore", @@ -2912,18 +3064,18 @@ dependencies = [ [[package]] name = "uu_rm" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "libc", "uucore", "walkdir", - "windows-sys 0.42.0", + "windows-sys 0.48.0", ] [[package]] name = "uu_rmdir" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "libc", @@ -2932,7 +3084,7 @@ dependencies = [ [[package]] name = "uu_runcon" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "libc", @@ -2943,7 +3095,7 @@ dependencies = [ [[package]] name = "uu_seq" -version = "0.0.17" +version = "0.0.19" dependencies = [ "bigdecimal", "clap", @@ -2954,7 +3106,7 @@ dependencies = [ [[package]] name = "uu_shred" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "libc", @@ -2964,7 +3116,7 @@ dependencies = [ [[package]] name = "uu_shuf" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "memchr", @@ -2975,7 +3127,7 @@ dependencies = [ [[package]] name = "uu_sleep" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "fundu", @@ -2984,7 +3136,7 @@ dependencies = [ [[package]] name = "uu_sort" -version = "0.0.17" +version = "0.0.19" dependencies = [ "binary-heap-plus", "clap", @@ -3003,7 +3155,7 @@ dependencies = [ [[package]] name = "uu_split" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "memchr", @@ -3012,7 +3164,7 @@ dependencies = [ [[package]] name = "uu_stat" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "uucore", @@ -3020,7 +3172,7 @@ dependencies = [ [[package]] name = "uu_stdbuf" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "tempfile", @@ -3030,7 +3182,7 @@ dependencies = [ [[package]] name = "uu_stdbuf_libstdbuf" -version = "0.0.17" +version = "0.0.19" dependencies = [ "cpp", "cpp_build", @@ -3040,7 +3192,7 @@ dependencies = [ [[package]] name = "uu_stty" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "nix", @@ -3049,7 +3201,7 @@ dependencies = [ [[package]] name = "uu_sum" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "uucore", @@ -3057,18 +3209,18 @@ dependencies = [ [[package]] name = "uu_sync" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "libc", "nix", "uucore", - "windows-sys 0.42.0", + "windows-sys 0.48.0", ] [[package]] name = "uu_tac" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "memchr", @@ -3079,7 +3231,7 @@ dependencies = [ [[package]] name = "uu_tail" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "fundu", @@ -3087,15 +3239,16 @@ dependencies = [ "libc", "memchr", "notify", + "rstest", "same-file", "uucore", "winapi-util", - "windows-sys 0.42.0", + "windows-sys 0.48.0", ] [[package]] name = "uu_tee" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "libc", @@ -3104,17 +3257,17 @@ dependencies = [ [[package]] name = "uu_test" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "libc", - "redox_syscall", + "redox_syscall 0.3.5", "uucore", ] [[package]] name = "uu_timeout" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "libc", @@ -3124,18 +3277,19 @@ dependencies = [ [[package]] name = "uu_touch" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "filetime", + "humantime_to_duration", "time", "uucore", - "windows-sys 0.42.0", + "windows-sys 0.48.0", ] [[package]] name = "uu_tr" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "nom", @@ -3144,7 +3298,7 @@ dependencies = [ [[package]] name = "uu_true" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "uucore", @@ -3152,7 +3306,7 @@ dependencies = [ [[package]] name = "uu_truncate" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "uucore", @@ -3160,7 +3314,7 @@ dependencies = [ [[package]] name = "uu_tsort" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "uucore", @@ -3168,7 +3322,7 @@ dependencies = [ [[package]] name = "uu_tty" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "is-terminal", @@ -3178,7 +3332,7 @@ dependencies = [ [[package]] name = "uu_uname" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "platform-info", @@ -3187,7 +3341,7 @@ dependencies = [ [[package]] name = "uu_unexpand" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "unicode-width", @@ -3196,7 +3350,7 @@ dependencies = [ [[package]] name = "uu_uniq" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "uucore", @@ -3204,7 +3358,7 @@ dependencies = [ [[package]] name = "uu_unlink" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "uucore", @@ -3212,7 +3366,7 @@ dependencies = [ [[package]] name = "uu_uptime" -version = "0.0.17" +version = "0.0.19" dependencies = [ "chrono", "clap", @@ -3221,7 +3375,7 @@ dependencies = [ [[package]] name = "uu_users" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "uucore", @@ -3229,7 +3383,7 @@ dependencies = [ [[package]] name = "uu_vdir" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "uu_ls", @@ -3238,20 +3392,20 @@ dependencies = [ [[package]] name = "uu_wc" -version = "0.0.17" +version = "0.0.19" dependencies = [ "bytecount", "clap", "libc", "nix", + "thiserror", "unicode-width", - "utf-8", "uucore", ] [[package]] name = "uu_who" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "uucore", @@ -3259,26 +3413,27 @@ dependencies = [ [[package]] name = "uu_whoami" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", "libc", "uucore", - "windows-sys 0.42.0", + "windows-sys 0.48.0", ] [[package]] name = "uu_yes" -version = "0.0.17" +version = "0.0.19" dependencies = [ "clap", + "itertools", "nix", "uucore", ] [[package]] name = "uucore" -version = "0.0.17" +version = "0.0.19" dependencies = [ "blake2b_simd", "blake3", @@ -3301,24 +3456,30 @@ dependencies = [ "sha2", "sha3", "sm3", + "tempfile", "thiserror", "time", "uucore_procs", "walkdir", "wild", "winapi-util", - "windows-sys 0.42.0", + "windows-sys 0.48.0", "z85", ] [[package]] name = "uucore_procs" -version = "0.0.17" +version = "0.0.19" dependencies = [ "proc-macro2", "quote", + "uuhelp_parser", ] +[[package]] +name = "uuhelp_parser" +version = "0.0.19" + [[package]] name = "uuid" version = "1.2.2" @@ -3455,90 +3616,141 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" -version = "0.42.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows-targets 0.42.2", ] [[package]] name = "windows-sys" -version = "0.45.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.0", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" dependencies = [ - "windows-targets", + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] [[package]] name = "windows-targets" -version = "0.42.1" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" -version = "0.42.1" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" [[package]] name = "windows_i686_gnu" -version = "0.42.1" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" -version = "0.42.1" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" [[package]] name = "windows_x86_64_gnu" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.1" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" [[package]] name = "windows_x86_64_msvc" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "xattr" -version = "0.2.3" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" +checksum = "ea263437ca03c1522846a4ddafbca2542d0ad5ed9b784909d4b27b76f62bc34a" dependencies = [ "libc", ] @@ -3557,9 +3769,9 @@ checksum = "2a599daf1b507819c1121f0bf87fa37eb19daac6aff3aefefd4e6e2e0f2020fc" [[package]] name = "zip" -version = "0.6.3" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537ce7411d25e54e8ae21a7ce0b15840e7bfcff15b51d697ec3266cc76bdf080" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" dependencies = [ "byteorder", "crc32fast", diff --git a/Cargo.toml b/Cargo.toml index 08a0ef3bb8e..e92f6719e9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,11 @@ # coreutils (uutils) # * see the repository LICENSE, README, and CONTRIBUTING files for more information -# spell-checker:ignore (libs) libselinux gethostid procfs bigdecimal kqueue fundu mangen +# spell-checker:ignore (libs) libselinux gethostid procfs bigdecimal kqueue fundu mangen datetime uuhelp [package] name = "coreutils" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "coreutils ~ GNU coreutils (updated); implemented as universal (cross-platform) utils, written in Rust" @@ -22,16 +22,16 @@ edition = "2021" build = "build.rs" [features] -default = [ "feat_common_core" ] +default = ["feat_common_core"] ## OS feature shortcodes -macos = [ "feat_os_macos" ] -unix = [ "feat_os_unix" ] -windows = [ "feat_os_windows" ] +macos = ["feat_os_macos"] +unix = ["feat_os_unix"] +windows = ["feat_os_windows"] ## project-specific feature shortcodes nightly = [] test_unimplemented = [] # * only build `uudoc` when `--feature uudoc` is activated -uudoc = ["zip"] +uudoc = ["zip", "dep:uuhelp_parser"] ## features # "feat_acl" == enable support for ACLs (access control lists; by using`--features feat_acl`) # NOTE: @@ -42,426 +42,429 @@ feat_acl = ["cp/feat_acl"] # NOTE: # * The selinux(-sys) crate requires `libselinux` headers and shared library to be accessible in the C toolchain at compile time. # * Running a uutils compiled with `feat_selinux` requires an SELinux enabled Kernel at run time. -feat_selinux = ["cp/selinux", "id/selinux", "ls/selinux", "selinux", "feat_require_selinux"] +feat_selinux = [ + "cp/selinux", + "id/selinux", + "ls/selinux", + "selinux", + "feat_require_selinux", +] ## ## feature sets ## (common/core and Tier1) feature sets # "feat_common_core" == baseline core set of utilities which can be built/run on most targets feat_common_core = [ - "base32", - "base64", - "basename", - "basenc", - "cat", - "cksum", - "comm", - "cp", - "csplit", - "cut", - "date", - "df", - "dir", - "dircolors", - "dirname", - "dd", - "du", - "echo", - "env", - "expand", - "expr", - "factor", - "false", - "fmt", - "fold", - "hashsum", - "head", - "join", - "link", - "ln", - "ls", - "mkdir", - "mktemp", - "more", - "mv", - "nl", - "numfmt", - "od", - "paste", - "pr", - "printenv", - "printf", - "ptx", - "pwd", - "readlink", - "realpath", - "relpath", - "rm", - "rmdir", - "seq", - "shred", - "shuf", - "sleep", - "sort", - "split", - "sum", - "tac", - "tail", - "tee", - "test", - "tr", - "true", - "truncate", - "tsort", - "touch", - "unexpand", - "uniq", - "unlink", - "vdir", - "wc", - "yes", + "base32", + "base64", + "basename", + "basenc", + "cat", + "cksum", + "comm", + "cp", + "csplit", + "cut", + "date", + "df", + "dir", + "dircolors", + "dirname", + "dd", + "du", + "echo", + "env", + "expand", + "expr", + "factor", + "false", + "fmt", + "fold", + "hashsum", + "head", + "join", + "link", + "ln", + "ls", + "mkdir", + "mktemp", + "more", + "mv", + "nl", + "numfmt", + "od", + "paste", + "pr", + "printenv", + "printf", + "ptx", + "pwd", + "readlink", + "realpath", + "relpath", + "rm", + "rmdir", + "seq", + "shred", + "shuf", + "sleep", + "sort", + "split", + "sum", + "tac", + "tail", + "tee", + "test", + "tr", + "true", + "truncate", + "tsort", + "touch", + "unexpand", + "uniq", + "unlink", + "vdir", + "wc", + "yes", ] # "feat_Tier1" == expanded set of utilities which can be built/run on the usual rust "Tier 1" target platforms (ref: ) feat_Tier1 = [ - "feat_common_core", - # - "arch", - "hostname", - "nproc", - "sync", - "touch", - "uname", - "whoami", + "feat_common_core", + # + "arch", + "hostname", + "nproc", + "sync", + "touch", + "uname", + "whoami", ] ## (primary platforms) feature sets # "feat_os_macos" == set of utilities which can be built/run on the MacOS platform feat_os_macos = [ - "feat_os_unix", ## == a modern/usual *nix platform - # - "feat_require_unix_hostid", + "feat_os_unix", ## == a modern/usual *nix platform + # + "feat_require_unix_hostid", ] # "feat_os_unix" == set of utilities which can be built/run on modern/usual *nix platforms feat_os_unix = [ - "feat_Tier1", - # - "feat_require_crate_cpp", - "feat_require_unix", - "feat_require_unix_utmpx", + "feat_Tier1", + # + "feat_require_crate_cpp", + "feat_require_unix", + "feat_require_unix_utmpx", ] # "feat_os_windows" == set of utilities which can be built/run on modern/usual windows platforms feat_os_windows = [ - "feat_Tier1", ## == "feat_os_windows_legacy" + "hostname" + "feat_Tier1", ## == "feat_os_windows_legacy" + "hostname" ] ## (secondary platforms) feature sets # "feat_os_unix_gnueabihf" == set of utilities which can be built/run on the "arm-unknown-linux-gnueabihf" target (ARMv6 Linux [hardfloat]) feat_os_unix_gnueabihf = [ - "feat_Tier1", - # - "feat_require_unix", - "feat_require_unix_hostid", - "feat_require_unix_utmpx", + "feat_Tier1", + # + "feat_require_unix", + "feat_require_unix_hostid", + "feat_require_unix_utmpx", ] # "feat_os_unix_musl" == set of utilities which can be built/run on targets binding to the "musl" library (ref: ) feat_os_unix_musl = [ - "feat_Tier1", - # - "feat_require_unix", - "feat_require_unix_hostid", + "feat_Tier1", + # + "feat_require_unix", + "feat_require_unix_hostid", ] feat_os_unix_android = [ - "feat_Tier1", - # - "feat_require_unix", + "feat_Tier1", + # + "feat_require_unix", ] ## feature sets with requirements (restricting cross-platform availability) # # ** NOTE: these `feat_require_...` sets should be minimized as much as possible to encourage cross-platform availability of utilities # # "feat_require_crate_cpp" == set of utilities requiring the `cpp` crate (which fail to compile on several platforms; as of 2020-04-23) -feat_require_crate_cpp = [ - "stdbuf", -] +feat_require_crate_cpp = ["stdbuf"] # "feat_require_unix" == set of utilities requiring support which is only available on unix platforms (as of 2020-04-23) feat_require_unix = [ - "chgrp", - "chmod", - "chown", - "chroot", - "groups", - "id", - "install", - "kill", - "logname", - "mkfifo", - "mknod", - "nice", - "nohup", - "pathchk", - "stat", - "stty", - "timeout", - "tty", + "chgrp", + "chmod", + "chown", + "chroot", + "groups", + "id", + "install", + "kill", + "logname", + "mkfifo", + "mknod", + "nice", + "nohup", + "pathchk", + "stat", + "stty", + "timeout", + "tty", ] # "feat_require_unix_utmpx" == set of utilities requiring unix utmp/utmpx support # * ref: -feat_require_unix_utmpx = [ - "pinky", - "uptime", - "users", - "who", -] +feat_require_unix_utmpx = ["pinky", "uptime", "users", "who"] # "feat_require_unix_hostid" == set of utilities requiring gethostid in libc (only some unixes provide) -feat_require_unix_hostid = [ - "hostid", -] +feat_require_unix_hostid = ["hostid"] # "feat_require_selinux" == set of utilities depending on SELinux. -feat_require_selinux = [ - "chcon", - "runcon", -] +feat_require_selinux = ["chcon", "runcon"] ## (alternate/newer/smaller platforms) feature sets # "feat_os_unix_fuchsia" == set of utilities which can be built/run on the "Fuchsia" OS (refs: ; ) feat_os_unix_fuchsia = [ - "feat_common_core", - # - "feat_require_crate_cpp", - # - "chgrp", - "chmod", - "chown", - "du", - "groups", - "hostid", - "install", - "logname", - "mkfifo", - "mknod", - "nice", - "pathchk", - "tty", - "uname", - "unlink", + "feat_common_core", + # + "feat_require_crate_cpp", + # + "chgrp", + "chmod", + "chown", + "du", + "groups", + "hostid", + "install", + "logname", + "mkfifo", + "mknod", + "nice", + "pathchk", + "tty", + "uname", + "unlink", ] # "feat_os_unix_redox" == set of utilities which can be built/run on "Redox OS" (refs: ; ) feat_os_unix_redox = [ - "feat_common_core", - # - "chmod", - "uname", + "feat_common_core", + # + "chmod", + "uname", ] # "feat_os_windows_legacy" == slightly restricted set of utilities which can be built/run on early windows platforms (eg, "WinXP") feat_os_windows_legacy = [ - "feat_common_core", - # - "arch", - "nproc", - "sync", - "touch", - "whoami", + "feat_common_core", + # + "arch", + "nproc", + "sync", + "touch", + "whoami", ] ## # * bypass/override ~ translate 'test' feature name to avoid dependency collision with rust core 'test' crate (o/w surfaces as compiler errors during testing) -test = [ "uu_test" ] +test = ["uu_test"] [workspace.dependencies] bigdecimal = "0.3" binary-heap-plus = "0.5.0" -bstr = "1.0" +bstr = "1.5" bytecount = "0.6.3" -byteorder = "1.3.2" -chrono = { version="^0.4.24", default-features=false, features=["std", "alloc", "clock"]} -clap = { version = "4.1", features = ["wrap_help", "cargo"] } -clap_complete = "4.0" +byteorder = "1.4.3" +chrono = { version = "^0.4.26", default-features = false, features = [ + "std", + "alloc", + "clock", +] } +clap = { version = "4.3", features = ["wrap_help", "cargo"] } +clap_complete = "4.3" clap_mangen = "0.2" compare = "0.1.0" coz = { version = "0.1.3" } -crossterm = ">=0.19" -ctrlc = { version = "3.0", features = ["termination"] } -exacl = "0.9.0" +crossterm = ">=0.26.1" +ctrlc = { version = "3.4", features = ["termination"] } +exacl = "0.10.0" file_diff = "1.0.0" filetime = "0.2" fnv = "1.0.7" -fs_extra = "1.1.0" +fs_extra = "1.3.0" fts-sys = "0.2" -fundu = "0.4.3" -gcd = "2.2" -glob = "0.3.0" -half = "2.1" +fundu = "1.0.0" +gcd = "2.3" +glob = "0.3.1" +half = "2.2" indicatif = "0.17" -is-terminal = "0.4.3" -itertools = "0.10.0" -libc = "0.2.139" -lscolors = { version = "0.13.0", default-features=false, features = ["nu-ansi-term"] } +is-terminal = "0.4.7" +itertools = "0.10.5" +libc = "0.2.146" +lscolors = { version = "0.14.0", default-features = false, features = [ + "nu-ansi-term", +] } memchr = "2" -nix = { version="0.26", default-features=false } -nom = "7.1.1" -notify = { version = "=5.0.0", features=["macos_kqueue"]} -num_cpus = "1.14" +nix = { version = "0.26", default-features = false } +nom = "7.1.3" +notify = { version = "=6.0.1", features = ["macos_kqueue"] } num-bigint = "0.4.3" num-traits = "0.2.15" number_prefix = "0.4" -once_cell = "1.13.1" +once_cell = "1.18.0" onig = { version = "~6.4", default-features = false } ouroboros = "0.15.6" +parse_datetime = "0.4.0" phf = "0.11.1" phf_codegen = "0.11.1" -platform-info = "1.0.2" +platform-info = "2.0.1" quick-error = "2.0.1" rand = { version = "0.8", features = ["small_rng"] } rand_core = "0.6" rayon = "1.7" -redox_syscall = "0.2" -regex = "1.7.3" -rust-ini = "0.18.0" +redox_syscall = "0.3" +regex = "1.8.4" +rstest = "0.17.0" +rust-ini = "0.19.0" same-file = "1.0.6" selinux = "0.4" signal-hook = "0.3.15" smallvec = { version = "1.10", features = ["union"] } -tempfile = "3.4.0" +tempfile = "3.6.0" term_grid = "0.1.5" -terminal_size = "0.2.5" -textwrap = { version="0.16.0", features=["terminal_size"] } +terminal_size = "0.2.6" +textwrap = { version = "0.16.0", features = ["terminal_size"] } thiserror = "1.0" -time = { version="0.3" } -unicode-segmentation = "1.9.0" -unicode-width = "0.1.8" +time = { version = "0.3" } +unicode-segmentation = "1.10.1" +unicode-width = "0.1.10" utf-8 = "0.7.6" -walkdir = "2.2" +walkdir = "2.3" winapi-util = "0.1.5" -windows-sys = { version="0.42.0", default-features=false } -xattr = "0.2.3" -zip = { version = "0.6.3", default_features=false, features=["deflate"] } +windows-sys = { version = "0.48.0", default-features = false } +xattr = "1.0.0" +zip = { version = "0.6.6", default_features = false, features = ["deflate"] } hex = "0.4.3" md-5 = "0.10.5" sha1 = "0.10.5" -sha2 = "0.10.2" -sha3 = "0.10.6" +sha2 = "0.10.7" +sha3 = "0.10.8" blake2b_simd = "1.0.1" -blake3 = "1.3.3" -sm3 = "0.4.1" -digest = "0.10.6" +blake3 = "1.4.0" +sm3 = "0.4.2" +digest = "0.10.7" -uucore = { version=">=0.0.17", package="uucore", path="src/uucore" } -uucore_procs = { version=">=0.0.17", package="uucore_procs", path="src/uucore_procs" } -uu_ls = { version=">=0.0.17", path="src/uu/ls" } -uu_base32 = { version=">=0.0.17", path="src/uu/base32"} +uucore = { version = ">=0.0.19", package = "uucore", path = "src/uucore" } +uucore_procs = { version = ">=0.0.19", package = "uucore_procs", path = "src/uucore_procs" } +uu_ls = { version = ">=0.0.18", path = "src/uu/ls" } +uu_base32 = { version = ">=0.0.18", path = "src/uu/base32" } [dependencies] -clap = { workspace=true } -once_cell = { workspace=true } -uucore = { workspace=true } -clap_complete = { workspace=true } -clap_mangen = { workspace=true } -phf = { workspace=true } -selinux = { workspace=true, optional = true } -textwrap = { workspace=true } -zip = { workspace=true, optional = true } +clap = { workspace = true } +once_cell = { workspace = true } +uucore = { workspace = true } +clap_complete = { workspace = true } +clap_mangen = { workspace = true } +phf = { workspace = true } +selinux = { workspace = true, optional = true } +textwrap = { workspace = true } +zip = { workspace = true, optional = true } + +uuhelp_parser = { optional = true, version = ">=0.0.19", path = "src/uuhelp_parser" } # * uutils -uu_test = { optional=true, version="0.0.17", package="uu_test", path="src/uu/test" } +uu_test = { optional = true, version = "0.0.19", package = "uu_test", path = "src/uu/test" } # -arch = { optional=true, version="0.0.17", package="uu_arch", path="src/uu/arch" } -base32 = { optional=true, version="0.0.17", package="uu_base32", path="src/uu/base32" } -base64 = { optional=true, version="0.0.17", package="uu_base64", path="src/uu/base64" } -basename = { optional=true, version="0.0.17", package="uu_basename", path="src/uu/basename" } -basenc = { optional=true, version="0.0.17", package="uu_basenc", path="src/uu/basenc" } -cat = { optional=true, version="0.0.17", package="uu_cat", path="src/uu/cat" } -chcon = { optional=true, version="0.0.17", package="uu_chcon", path="src/uu/chcon" } -chgrp = { optional=true, version="0.0.17", package="uu_chgrp", path="src/uu/chgrp" } -chmod = { optional=true, version="0.0.17", package="uu_chmod", path="src/uu/chmod" } -chown = { optional=true, version="0.0.17", package="uu_chown", path="src/uu/chown" } -chroot = { optional=true, version="0.0.17", package="uu_chroot", path="src/uu/chroot" } -cksum = { optional=true, version="0.0.17", package="uu_cksum", path="src/uu/cksum" } -comm = { optional=true, version="0.0.17", package="uu_comm", path="src/uu/comm" } -cp = { optional=true, version="0.0.17", package="uu_cp", path="src/uu/cp" } -csplit = { optional=true, version="0.0.17", package="uu_csplit", path="src/uu/csplit" } -cut = { optional=true, version="0.0.17", package="uu_cut", path="src/uu/cut" } -date = { optional=true, version="0.0.17", package="uu_date", path="src/uu/date" } -dd = { optional=true, version="0.0.17", package="uu_dd", path="src/uu/dd" } -df = { optional=true, version="0.0.17", package="uu_df", path="src/uu/df" } -dir = { optional=true, version="0.0.17", package="uu_dir", path="src/uu/dir" } -dircolors= { optional=true, version="0.0.17", package="uu_dircolors", path="src/uu/dircolors" } -dirname = { optional=true, version="0.0.17", package="uu_dirname", path="src/uu/dirname" } -du = { optional=true, version="0.0.17", package="uu_du", path="src/uu/du" } -echo = { optional=true, version="0.0.17", package="uu_echo", path="src/uu/echo" } -env = { optional=true, version="0.0.17", package="uu_env", path="src/uu/env" } -expand = { optional=true, version="0.0.17", package="uu_expand", path="src/uu/expand" } -expr = { optional=true, version="0.0.17", package="uu_expr", path="src/uu/expr" } -factor = { optional=true, version="0.0.17", package="uu_factor", path="src/uu/factor" } -false = { optional=true, version="0.0.17", package="uu_false", path="src/uu/false" } -fmt = { optional=true, version="0.0.17", package="uu_fmt", path="src/uu/fmt" } -fold = { optional=true, version="0.0.17", package="uu_fold", path="src/uu/fold" } -groups = { optional=true, version="0.0.17", package="uu_groups", path="src/uu/groups" } -hashsum = { optional=true, version="0.0.17", package="uu_hashsum", path="src/uu/hashsum" } -head = { optional=true, version="0.0.17", package="uu_head", path="src/uu/head" } -hostid = { optional=true, version="0.0.17", package="uu_hostid", path="src/uu/hostid" } -hostname = { optional=true, version="0.0.17", package="uu_hostname", path="src/uu/hostname" } -id = { optional=true, version="0.0.17", package="uu_id", path="src/uu/id" } -install = { optional=true, version="0.0.17", package="uu_install", path="src/uu/install" } -join = { optional=true, version="0.0.17", package="uu_join", path="src/uu/join" } -kill = { optional=true, version="0.0.17", package="uu_kill", path="src/uu/kill" } -link = { optional=true, version="0.0.17", package="uu_link", path="src/uu/link" } -ln = { optional=true, version="0.0.17", package="uu_ln", path="src/uu/ln" } -ls = { optional=true, version="0.0.17", package="uu_ls", path="src/uu/ls" } -logname = { optional=true, version="0.0.17", package="uu_logname", path="src/uu/logname" } -mkdir = { optional=true, version="0.0.17", package="uu_mkdir", path="src/uu/mkdir" } -mkfifo = { optional=true, version="0.0.17", package="uu_mkfifo", path="src/uu/mkfifo" } -mknod = { optional=true, version="0.0.17", package="uu_mknod", path="src/uu/mknod" } -mktemp = { optional=true, version="0.0.17", package="uu_mktemp", path="src/uu/mktemp" } -more = { optional=true, version="0.0.17", package="uu_more", path="src/uu/more" } -mv = { optional=true, version="0.0.17", package="uu_mv", path="src/uu/mv" } -nice = { optional=true, version="0.0.17", package="uu_nice", path="src/uu/nice" } -nl = { optional=true, version="0.0.17", package="uu_nl", path="src/uu/nl" } -nohup = { optional=true, version="0.0.17", package="uu_nohup", path="src/uu/nohup" } -nproc = { optional=true, version="0.0.17", package="uu_nproc", path="src/uu/nproc" } -numfmt = { optional=true, version="0.0.17", package="uu_numfmt", path="src/uu/numfmt" } -od = { optional=true, version="0.0.17", package="uu_od", path="src/uu/od" } -paste = { optional=true, version="0.0.17", package="uu_paste", path="src/uu/paste" } -pathchk = { optional=true, version="0.0.17", package="uu_pathchk", path="src/uu/pathchk" } -pinky = { optional=true, version="0.0.17", package="uu_pinky", path="src/uu/pinky" } -pr = { optional=true, version="0.0.17", package="uu_pr", path="src/uu/pr" } -printenv = { optional=true, version="0.0.17", package="uu_printenv", path="src/uu/printenv" } -printf = { optional=true, version="0.0.17", package="uu_printf", path="src/uu/printf" } -ptx = { optional=true, version="0.0.17", package="uu_ptx", path="src/uu/ptx" } -pwd = { optional=true, version="0.0.17", package="uu_pwd", path="src/uu/pwd" } -readlink = { optional=true, version="0.0.17", package="uu_readlink", path="src/uu/readlink" } -realpath = { optional=true, version="0.0.17", package="uu_realpath", path="src/uu/realpath" } -relpath = { optional=true, version="0.0.17", package="uu_relpath", path="src/uu/relpath" } -rm = { optional=true, version="0.0.17", package="uu_rm", path="src/uu/rm" } -rmdir = { optional=true, version="0.0.17", package="uu_rmdir", path="src/uu/rmdir" } -runcon = { optional=true, version="0.0.17", package="uu_runcon", path="src/uu/runcon" } -seq = { optional=true, version="0.0.17", package="uu_seq", path="src/uu/seq" } -shred = { optional=true, version="0.0.17", package="uu_shred", path="src/uu/shred" } -shuf = { optional=true, version="0.0.17", package="uu_shuf", path="src/uu/shuf" } -sleep = { optional=true, version="0.0.17", package="uu_sleep", path="src/uu/sleep" } -sort = { optional=true, version="0.0.17", package="uu_sort", path="src/uu/sort" } -split = { optional=true, version="0.0.17", package="uu_split", path="src/uu/split" } -stat = { optional=true, version="0.0.17", package="uu_stat", path="src/uu/stat" } -stdbuf = { optional=true, version="0.0.17", package="uu_stdbuf", path="src/uu/stdbuf" } -stty = { optional=true, version="0.0.17", package="uu_stty", path="src/uu/stty" } -sum = { optional=true, version="0.0.17", package="uu_sum", path="src/uu/sum" } -sync = { optional=true, version="0.0.17", package="uu_sync", path="src/uu/sync" } -tac = { optional=true, version="0.0.17", package="uu_tac", path="src/uu/tac" } -tail = { optional=true, version="0.0.17", package="uu_tail", path="src/uu/tail" } -tee = { optional=true, version="0.0.17", package="uu_tee", path="src/uu/tee" } -timeout = { optional=true, version="0.0.17", package="uu_timeout", path="src/uu/timeout" } -touch = { optional=true, version="0.0.17", package="uu_touch", path="src/uu/touch" } -tr = { optional=true, version="0.0.17", package="uu_tr", path="src/uu/tr" } -true = { optional=true, version="0.0.17", package="uu_true", path="src/uu/true" } -truncate = { optional=true, version="0.0.17", package="uu_truncate", path="src/uu/truncate" } -tsort = { optional=true, version="0.0.17", package="uu_tsort", path="src/uu/tsort" } -tty = { optional=true, version="0.0.17", package="uu_tty", path="src/uu/tty" } -uname = { optional=true, version="0.0.17", package="uu_uname", path="src/uu/uname" } -unexpand = { optional=true, version="0.0.17", package="uu_unexpand", path="src/uu/unexpand" } -uniq = { optional=true, version="0.0.17", package="uu_uniq", path="src/uu/uniq" } -unlink = { optional=true, version="0.0.17", package="uu_unlink", path="src/uu/unlink" } -uptime = { optional=true, version="0.0.17", package="uu_uptime", path="src/uu/uptime" } -users = { optional=true, version="0.0.17", package="uu_users", path="src/uu/users" } -vdir = { optional=true, version="0.0.17", package="uu_vdir", path="src/uu/vdir" } -wc = { optional=true, version="0.0.17", package="uu_wc", path="src/uu/wc" } -who = { optional=true, version="0.0.17", package="uu_who", path="src/uu/who" } -whoami = { optional=true, version="0.0.17", package="uu_whoami", path="src/uu/whoami" } -yes = { optional=true, version="0.0.17", package="uu_yes", path="src/uu/yes" } +arch = { optional = true, version = "0.0.19", package = "uu_arch", path = "src/uu/arch" } +base32 = { optional = true, version = "0.0.19", package = "uu_base32", path = "src/uu/base32" } +base64 = { optional = true, version = "0.0.19", package = "uu_base64", path = "src/uu/base64" } +basename = { optional = true, version = "0.0.19", package = "uu_basename", path = "src/uu/basename" } +basenc = { optional = true, version = "0.0.19", package = "uu_basenc", path = "src/uu/basenc" } +cat = { optional = true, version = "0.0.19", package = "uu_cat", path = "src/uu/cat" } +chcon = { optional = true, version = "0.0.19", package = "uu_chcon", path = "src/uu/chcon" } +chgrp = { optional = true, version = "0.0.19", package = "uu_chgrp", path = "src/uu/chgrp" } +chmod = { optional = true, version = "0.0.19", package = "uu_chmod", path = "src/uu/chmod" } +chown = { optional = true, version = "0.0.19", package = "uu_chown", path = "src/uu/chown" } +chroot = { optional = true, version = "0.0.19", package = "uu_chroot", path = "src/uu/chroot" } +cksum = { optional = true, version = "0.0.19", package = "uu_cksum", path = "src/uu/cksum" } +comm = { optional = true, version = "0.0.19", package = "uu_comm", path = "src/uu/comm" } +cp = { optional = true, version = "0.0.19", package = "uu_cp", path = "src/uu/cp" } +csplit = { optional = true, version = "0.0.19", package = "uu_csplit", path = "src/uu/csplit" } +cut = { optional = true, version = "0.0.19", package = "uu_cut", path = "src/uu/cut" } +date = { optional = true, version = "0.0.19", package = "uu_date", path = "src/uu/date" } +dd = { optional = true, version = "0.0.19", package = "uu_dd", path = "src/uu/dd" } +df = { optional = true, version = "0.0.19", package = "uu_df", path = "src/uu/df" } +dir = { optional = true, version = "0.0.19", package = "uu_dir", path = "src/uu/dir" } +dircolors = { optional = true, version = "0.0.19", package = "uu_dircolors", path = "src/uu/dircolors" } +dirname = { optional = true, version = "0.0.19", package = "uu_dirname", path = "src/uu/dirname" } +du = { optional = true, version = "0.0.19", package = "uu_du", path = "src/uu/du" } +echo = { optional = true, version = "0.0.19", package = "uu_echo", path = "src/uu/echo" } +env = { optional = true, version = "0.0.19", package = "uu_env", path = "src/uu/env" } +expand = { optional = true, version = "0.0.19", package = "uu_expand", path = "src/uu/expand" } +expr = { optional = true, version = "0.0.19", package = "uu_expr", path = "src/uu/expr" } +factor = { optional = true, version = "0.0.19", package = "uu_factor", path = "src/uu/factor" } +false = { optional = true, version = "0.0.19", package = "uu_false", path = "src/uu/false" } +fmt = { optional = true, version = "0.0.19", package = "uu_fmt", path = "src/uu/fmt" } +fold = { optional = true, version = "0.0.19", package = "uu_fold", path = "src/uu/fold" } +groups = { optional = true, version = "0.0.19", package = "uu_groups", path = "src/uu/groups" } +hashsum = { optional = true, version = "0.0.19", package = "uu_hashsum", path = "src/uu/hashsum" } +head = { optional = true, version = "0.0.19", package = "uu_head", path = "src/uu/head" } +hostid = { optional = true, version = "0.0.19", package = "uu_hostid", path = "src/uu/hostid" } +hostname = { optional = true, version = "0.0.19", package = "uu_hostname", path = "src/uu/hostname" } +id = { optional = true, version = "0.0.19", package = "uu_id", path = "src/uu/id" } +install = { optional = true, version = "0.0.19", package = "uu_install", path = "src/uu/install" } +join = { optional = true, version = "0.0.19", package = "uu_join", path = "src/uu/join" } +kill = { optional = true, version = "0.0.19", package = "uu_kill", path = "src/uu/kill" } +link = { optional = true, version = "0.0.19", package = "uu_link", path = "src/uu/link" } +ln = { optional = true, version = "0.0.19", package = "uu_ln", path = "src/uu/ln" } +ls = { optional = true, version = "0.0.19", package = "uu_ls", path = "src/uu/ls" } +logname = { optional = true, version = "0.0.19", package = "uu_logname", path = "src/uu/logname" } +mkdir = { optional = true, version = "0.0.19", package = "uu_mkdir", path = "src/uu/mkdir" } +mkfifo = { optional = true, version = "0.0.19", package = "uu_mkfifo", path = "src/uu/mkfifo" } +mknod = { optional = true, version = "0.0.19", package = "uu_mknod", path = "src/uu/mknod" } +mktemp = { optional = true, version = "0.0.19", package = "uu_mktemp", path = "src/uu/mktemp" } +more = { optional = true, version = "0.0.19", package = "uu_more", path = "src/uu/more" } +mv = { optional = true, version = "0.0.19", package = "uu_mv", path = "src/uu/mv" } +nice = { optional = true, version = "0.0.19", package = "uu_nice", path = "src/uu/nice" } +nl = { optional = true, version = "0.0.19", package = "uu_nl", path = "src/uu/nl" } +nohup = { optional = true, version = "0.0.19", package = "uu_nohup", path = "src/uu/nohup" } +nproc = { optional = true, version = "0.0.19", package = "uu_nproc", path = "src/uu/nproc" } +numfmt = { optional = true, version = "0.0.19", package = "uu_numfmt", path = "src/uu/numfmt" } +od = { optional = true, version = "0.0.19", package = "uu_od", path = "src/uu/od" } +paste = { optional = true, version = "0.0.19", package = "uu_paste", path = "src/uu/paste" } +pathchk = { optional = true, version = "0.0.19", package = "uu_pathchk", path = "src/uu/pathchk" } +pinky = { optional = true, version = "0.0.19", package = "uu_pinky", path = "src/uu/pinky" } +pr = { optional = true, version = "0.0.19", package = "uu_pr", path = "src/uu/pr" } +printenv = { optional = true, version = "0.0.19", package = "uu_printenv", path = "src/uu/printenv" } +printf = { optional = true, version = "0.0.19", package = "uu_printf", path = "src/uu/printf" } +ptx = { optional = true, version = "0.0.19", package = "uu_ptx", path = "src/uu/ptx" } +pwd = { optional = true, version = "0.0.19", package = "uu_pwd", path = "src/uu/pwd" } +readlink = { optional = true, version = "0.0.19", package = "uu_readlink", path = "src/uu/readlink" } +realpath = { optional = true, version = "0.0.19", package = "uu_realpath", path = "src/uu/realpath" } +relpath = { optional = true, version = "0.0.19", package = "uu_relpath", path = "src/uu/relpath" } +rm = { optional = true, version = "0.0.19", package = "uu_rm", path = "src/uu/rm" } +rmdir = { optional = true, version = "0.0.19", package = "uu_rmdir", path = "src/uu/rmdir" } +runcon = { optional = true, version = "0.0.19", package = "uu_runcon", path = "src/uu/runcon" } +seq = { optional = true, version = "0.0.19", package = "uu_seq", path = "src/uu/seq" } +shred = { optional = true, version = "0.0.19", package = "uu_shred", path = "src/uu/shred" } +shuf = { optional = true, version = "0.0.19", package = "uu_shuf", path = "src/uu/shuf" } +sleep = { optional = true, version = "0.0.19", package = "uu_sleep", path = "src/uu/sleep" } +sort = { optional = true, version = "0.0.19", package = "uu_sort", path = "src/uu/sort" } +split = { optional = true, version = "0.0.19", package = "uu_split", path = "src/uu/split" } +stat = { optional = true, version = "0.0.19", package = "uu_stat", path = "src/uu/stat" } +stdbuf = { optional = true, version = "0.0.19", package = "uu_stdbuf", path = "src/uu/stdbuf" } +stty = { optional = true, version = "0.0.19", package = "uu_stty", path = "src/uu/stty" } +sum = { optional = true, version = "0.0.19", package = "uu_sum", path = "src/uu/sum" } +sync = { optional = true, version = "0.0.19", package = "uu_sync", path = "src/uu/sync" } +tac = { optional = true, version = "0.0.19", package = "uu_tac", path = "src/uu/tac" } +tail = { optional = true, version = "0.0.19", package = "uu_tail", path = "src/uu/tail" } +tee = { optional = true, version = "0.0.19", package = "uu_tee", path = "src/uu/tee" } +timeout = { optional = true, version = "0.0.19", package = "uu_timeout", path = "src/uu/timeout" } +touch = { optional = true, version = "0.0.19", package = "uu_touch", path = "src/uu/touch" } +tr = { optional = true, version = "0.0.19", package = "uu_tr", path = "src/uu/tr" } +true = { optional = true, version = "0.0.19", package = "uu_true", path = "src/uu/true" } +truncate = { optional = true, version = "0.0.19", package = "uu_truncate", path = "src/uu/truncate" } +tsort = { optional = true, version = "0.0.19", package = "uu_tsort", path = "src/uu/tsort" } +tty = { optional = true, version = "0.0.19", package = "uu_tty", path = "src/uu/tty" } +uname = { optional = true, version = "0.0.19", package = "uu_uname", path = "src/uu/uname" } +unexpand = { optional = true, version = "0.0.19", package = "uu_unexpand", path = "src/uu/unexpand" } +uniq = { optional = true, version = "0.0.19", package = "uu_uniq", path = "src/uu/uniq" } +unlink = { optional = true, version = "0.0.19", package = "uu_unlink", path = "src/uu/unlink" } +uptime = { optional = true, version = "0.0.19", package = "uu_uptime", path = "src/uu/uptime" } +users = { optional = true, version = "0.0.19", package = "uu_users", path = "src/uu/users" } +vdir = { optional = true, version = "0.0.19", package = "uu_vdir", path = "src/uu/vdir" } +wc = { optional = true, version = "0.0.19", package = "uu_wc", path = "src/uu/wc" } +who = { optional = true, version = "0.0.19", package = "uu_who", path = "src/uu/who" } +whoami = { optional = true, version = "0.0.19", package = "uu_whoami", path = "src/uu/whoami" } +yes = { optional = true, version = "0.0.19", package = "uu_yes", path = "src/uu/yes" } # this breaks clippy linting with: "tests/by-util/test_factor_benches.rs: No such file or directory (os error 2)" # factor_benches = { optional = true, version = "0.0.0", package = "uu_factor_benches", path = "tests/benches/factor" } @@ -472,35 +475,34 @@ yes = { optional=true, version="0.0.17", package="uu_yes", path="src/uu/yes #pin_cc = { version="1.0.61, < 1.0.62", package="cc" } ## cc v1.0.62 has compiler errors for MinRustV v1.32.0, requires 1.34 (for `std::str::split_ascii_whitespace()`) [dev-dependencies] -chrono = { workspace=true } +chrono = { workspace = true } conv = "0.3" -filetime = { workspace=true } -glob = { workspace=true } -libc = { workspace=true } +filetime = { workspace = true } +glob = { workspace = true } +libc = { workspace = true } pretty_assertions = "1" -rand = { workspace=true } -regex = { workspace=true } -sha1 = { version="0.10", features=["std"] } -tempfile = { workspace=true } -time = { workspace=true, features=["local-offset"] } -unindent = "0.1" -uucore = { workspace=true, features=["entries", "process", "signals"] } -walkdir = { workspace=true } -is-terminal = { workspace=true } -hex-literal = "0.3.4" -rstest = "0.16.0" +rand = { workspace = true } +regex = { workspace = true } +sha1 = { version = "0.10", features = ["std"] } +tempfile = { workspace = true } +time = { workspace = true, features = ["local-offset"] } +unindent = "0.2" +uucore = { workspace = true, features = ["entries", "process", "signals"] } +walkdir = { workspace = true } +is-terminal = { workspace = true } +hex-literal = "0.4.1" +rstest = "0.17.0" [target.'cfg(any(target_os = "linux", target_os = "android"))'.dev-dependencies] procfs = { version = "0.15", default-features = false } rlimit = "0.9.1" [target.'cfg(unix)'.dev-dependencies] -nix = { workspace=true, features=["process", "signal", "user"] } -rust-users = { version="0.11", package="users" } +nix = { workspace = true, features = ["process", "signal", "user"] } rand_pcg = "0.3" [build-dependencies] -phf_codegen = { workspace=true } +phf_codegen = { workspace = true } [[bin]] name = "coreutils" diff --git a/GNUmakefile b/GNUmakefile index 81b90d32f38..26d6de5ba63 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -1,4 +1,4 @@ -# spell-checker:ignore (misc) testsuite runtest findstring (targets) busytest toybox distclean pkgs ; (vars/env) BINDIR BUILDDIR CARGOFLAGS DESTDIR DOCSDIR INSTALLDIR INSTALLEES MULTICALL DATAROOTDIR TESTDIR +# spell-checker:ignore (misc) testsuite runtest findstring (targets) busytest toybox distclean pkgs nextest ; (vars/env) BINDIR BUILDDIR CARGOFLAGS DESTDIR DOCSDIR INSTALLDIR INSTALLEES MULTICALL DATAROOTDIR TESTDIR # Config options PROFILE ?= debug @@ -289,6 +289,9 @@ $(foreach test,$(filter-out $(SKIP_UTILS),$(PROGS)),$(eval $(call TEST_BUSYBOX,$ test: ${CARGO} test ${CARGOFLAGS} --features "$(TESTS) $(TEST_SPEC_FEATURE)" --no-default-features $(TEST_NO_FAIL_FAST) +nextest: + ${CARGO} nextest run ${CARGOFLAGS} --features "$(TESTS) $(TEST_SPEC_FEATURE)" --no-default-features $(TEST_NO_FAIL_FAST) + test_toybox: -(cd $(TOYBOX_SRC)/ && make tests) diff --git a/Makefile.toml b/Makefile.toml index ded2cd55b83..84698df5f98 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -20,15 +20,12 @@ run_task = "_init" [tasks._init] private = true -dependencies = [ - "_init-vars", -] +dependencies = ["_init-vars"] [tasks._init-vars] private = true script_runner = "@duckscript" -script = [ -''' +script = [''' # reset build/test flags set_env CARGO_MAKE_CARGO_BUILD_TEST_FLAGS "" # determine features @@ -90,54 +87,36 @@ for arg in "${args_utils_list}" end args_utils = trim "${args_utils}" set_env CARGO_MAKE_TASK_BUILD_UTILS_ARGS "${args_utils}" -''' -] +'''] ### tasks [tasks.default] description = "## *DEFAULT* Build (debug-mode) and test project" category = "[project]" -dependencies = [ - "action-build-debug", - "test-terse", -] +dependencies = ["action-build-debug", "test-terse"] ## [tasks.build] description = "## Build (release-mode) project" category = "[project]" -dependencies = [ - "core::pre-build", - "action-build-release", - "core::post-build", -] +dependencies = ["core::pre-build", "action-build-release", "core::post-build"] [tasks.build-debug] description = "## Build (debug-mode) project" category = "[project]" -dependencies = [ - "action-build-debug", -] +dependencies = ["action-build-debug"] [tasks.build-examples] description = "## Build (release-mode) project example(s); usage: `cargo make (build-examples | examples) [EXAMPLE]...`" category = "[project]" -dependencies = [ - "core::pre-build", - "action-build-examples", - "core::post-build", -] +dependencies = ["core::pre-build", "action-build-examples", "core::post-build"] [tasks.build-features] description = "## Build (with features; release-mode) project; usage: `cargo make (build-features | features) FEATURE...`" category = "[project]" -dependencies = [ - "core::pre-build", - "action-build-features", - "core::post-build", -] +dependencies = ["core::pre-build", "action-build-features", "core::post-build"] [tasks.build-release] alias = "build" @@ -148,9 +127,7 @@ alias = "build-debug" [tasks.example] description = "hidden singular-form alias for 'examples'" category = "[project]" -dependencies = [ - "examples", -] +dependencies = ["examples"] [tasks.examples] alias = "build-examples" @@ -161,17 +138,12 @@ alias = "build-features" [tasks.format] description = "## Format code files (with `cargo fmt`; includes tests)" category = "[project]" -dependencies = [ - "action-format", - "action-format-tests", -] +dependencies = ["action-format", "action-format-tests"] [tasks.help] description = "## Display help" category = "[project]" -dependencies = [ - "action-display-help", -] +dependencies = ["action-display-help"] [tasks.install] description = "## Install project binary (to $HOME/.cargo/bin)" @@ -182,10 +154,7 @@ args = ["install", "--path", "."] [tasks.lint] description = "## Display lint report" category = "[project]" -dependencies = [ - "action-clippy", - "action-fmt_report", -] +dependencies = ["action-clippy", "action-fmt_report"] [tasks.release] alias = "build" @@ -193,48 +162,32 @@ alias = "build" [tasks.test] description = "## Run project tests" category = "[project]" -dependencies = [ - "core::pre-test", - "core::test", - "core::post-test", -] +dependencies = ["core::pre-test", "core::test", "core::post-test"] [tasks.test-terse] description = "## Run project tests (with terse/summary output)" category = "[project]" -dependencies = [ - "core::pre-test", - "action-test_quiet", - "core::post-test", -] +dependencies = ["core::pre-test", "action-test_quiet", "core::post-test"] [tasks.test-util] description = "## Test (individual) utilities; usage: `cargo make (test-util | test-uutil) [UTIL_NAME...]`" category = "[project]" -dependencies = [ - "action-test-utils", -] +dependencies = ["action-test-utils"] [tasks.test-utils] description = "hidden plural-form alias for 'test-util'" category = "[project]" -dependencies = [ - "test-util", -] +dependencies = ["test-util"] [tasks.test-uutil] description = "hidden alias for 'test-util'" category = "[project]" -dependencies = [ - "test-util", -] +dependencies = ["test-util"] [tasks.test-uutils] description = "hidden alias for 'test-util'" category = "[project]" -dependencies = [ - "test-util", -] +dependencies = ["test-util"] [tasks.uninstall] description = "## Remove project binary (from $HOME/.cargo/bin)" @@ -246,63 +199,66 @@ args = ["uninstall"] description = "## Build (individual; release-mode) utilities; usage: `cargo make (util | uutil) [UTIL_NAME...]`" category = "[project]" dependencies = [ - "core::pre-build", - "action-determine-utils", - "action-build-utils", - "core::post-build", + "core::pre-build", + "action-determine-utils", + "action-build-utils", + "core::post-build", ] [tasks.utils] description = "hidden plural-form alias for 'util'" category = "[project]" -dependencies = [ - "util", -] +dependencies = ["util"] [tasks.uutil] description = "hidden alias for 'util'" category = "[project]" -dependencies = [ - "util", -] +dependencies = ["util"] [tasks.uutils] description = "hidden plural-form alias for 'util'" category = "[project]" -dependencies = [ - "util", -] +dependencies = ["util"] ### actions [tasks.action-build-release] description = "`cargo build --release`" command = "cargo" -args = ["build", "--release", "@@split(CARGO_MAKE_CARGO_BUILD_TEST_FLAGS, )" ] +args = ["build", "--release", "@@split(CARGO_MAKE_CARGO_BUILD_TEST_FLAGS, )"] [tasks.action-build-debug] description = "`cargo build`" command = "cargo" -args = ["build", "@@split(CARGO_MAKE_CARGO_BUILD_TEST_FLAGS, )" ] +args = ["build", "@@split(CARGO_MAKE_CARGO_BUILD_TEST_FLAGS, )"] [tasks.action-build-examples] description = "`cargo build (--examples|(--example EXAMPLE)...)`" command = "cargo" -args = ["build", "--release", "@@split(CARGO_MAKE_CARGO_BUILD_TEST_FLAGS, )", "${CARGO_MAKE_TASK_BUILD_EXAMPLES_ARGS}" ] +args = [ + "build", + "--release", + "@@split(CARGO_MAKE_CARGO_BUILD_TEST_FLAGS, )", + "${CARGO_MAKE_TASK_BUILD_EXAMPLES_ARGS}", +] [tasks.action-build-features] description = "`cargo build --release --features FEATURES`" command = "cargo" -args = ["build", "--release", "--no-default-features", "--features", "${CARGO_MAKE_TASK_BUILD_FEATURES_ARGS}" ] +args = [ + "build", + "--release", + "--no-default-features", + "--features", + "${CARGO_MAKE_TASK_BUILD_FEATURES_ARGS}", +] [tasks.action-build-utils] description = "Build individual utilities" -dependencies = [ - "action-determine-utils", -] +dependencies = ["action-determine-utils"] command = "cargo" # args = ["build", "@@remove-empty(CARGO_MAKE_TASK_BUILD_UTILS_ARGS)" ] -args = ["build", "--release", "@@split(CARGO_MAKE_TASK_BUILD_UTILS_ARGS, )" ] +args = ["build", "--release", "@@split(CARGO_MAKE_TASK_BUILD_UTILS_ARGS, )"] [tasks.action-clippy] description = "`cargo clippy` lint report" @@ -311,8 +267,7 @@ args = ["clippy", "@@split(CARGO_MAKE_CARGO_BUILD_TEST_FLAGS, )"] [tasks.action-determine-utils] script_runner = "@duckscript" -script = [ -''' +script = [''' package_options = get_env CARGO_MAKE_TASK_BUILD_UTILS_ARGS if is_empty "${package_options}" show_utils = get_env CARGO_MAKE_VAR_SHOW_UTILS @@ -335,13 +290,11 @@ if is_empty "${package_options}" package_options = trim "${package_options}" end_if set_env CARGO_MAKE_TASK_BUILD_UTILS_ARGS "${package_options}" -''' -] +'''] [tasks.action-determine-tests] script_runner = "@duckscript" -script = [ -''' +script = [''' test_files = glob_array tests/**/*.rs for file in ${test_files} file = replace "${file}" "\\" "/" @@ -354,8 +307,7 @@ for file in ${test_files} end_if end set_env CARGO_MAKE_VAR_TESTS "${tests}" -''' -] +'''] [tasks.action-format] description = "`cargo fmt`" @@ -364,9 +316,7 @@ args = ["fmt"] [tasks.action-format-tests] description = "`cargo fmt` tests" -dependencies = [ - "action-determine-tests", -] +dependencies = ["action-determine-tests"] command = "cargo" args = ["fmt", "--", "@@split(CARGO_MAKE_VAR_TESTS, )"] @@ -381,16 +331,18 @@ args = ["fmt", "--", "--check"] [tasks.action-spellcheck-codespell] description = "`codespell` spellcheck repository" command = "codespell" # (from `pip install codespell`) -args = [".", "--skip=*/.git,./target,./tests/fixtures", "--ignore-words-list=mut,od"] +args = [ + ".", + "--skip=*/.git,./target,./tests/fixtures", + "--ignore-words-list=mut,od", +] [tasks.action-test-utils] description = "Build individual utilities" -dependencies = [ - "action-determine-utils", -] +dependencies = ["action-determine-utils"] command = "cargo" # args = ["build", "@@remove-empty(CARGO_MAKE_TASK_BUILD_UTILS_ARGS)" ] -args = ["test", "@@split(CARGO_MAKE_TASK_BUILD_UTILS_ARGS, )" ] +args = ["test", "@@split(CARGO_MAKE_TASK_BUILD_UTILS_ARGS, )"] [tasks.action-test_quiet] description = "Test (in `--quiet` mode)" @@ -399,8 +351,7 @@ args = ["test", "--quiet", "@@split(CARGO_MAKE_CARGO_BUILD_TEST_FLAGS, )"] [tasks.action-display-help] script_runner = "@duckscript" -script = [ -''' +script = [''' echo "" echo "usage: `cargo make TARGET [ARGS...]`" echo "" @@ -432,5 +383,4 @@ script = [ end_if end echo "" -''' -] +'''] diff --git a/deny.toml b/deny.toml index aa05cdf3e7e..780de3c8cf4 100644 --- a/deny.toml +++ b/deny.toml @@ -11,7 +11,7 @@ unmaintained = "warn" yanked = "warn" notice = "warn" ignore = [ - #"RUSTSEC-0000-0000", + #"RUSTSEC-0000-0000", ] # This section is considered when running `cargo deny check licenses` @@ -20,15 +20,15 @@ ignore = [ [licenses] unlicensed = "deny" allow = [ - "MIT", - "Apache-2.0", - "ISC", - "BSD-2-Clause", - "BSD-2-Clause-FreeBSD", - "BSD-3-Clause", - "CC0-1.0", - "MPL-2.0", # XXX considered copyleft? - "Unicode-DFS-2016", + "MIT", + "Apache-2.0", + "ISC", + "BSD-2-Clause", + "BSD-2-Clause-FreeBSD", + "BSD-3-Clause", + "CC0-1.0", + "MPL-2.0", # XXX considered copyleft? + "Unicode-DFS-2016", ] copyleft = "deny" allow-osi-fsf-free = "neither" @@ -59,16 +59,36 @@ highlight = "all" # introduces it. # spell-checker: disable skip = [ - # is-terminal - { name = "hermit-abi", version = "0.3.1" }, - # is-terminal - { name = "rustix", version = "0.36.8" }, - # is-terminal (via rustix) - { name = "io-lifetimes", version = "1.0.5" }, - # is-terminal - { name = "linux-raw-sys", version = "0.1.4" }, - # is-terminal - { name = "windows-sys", version = "0.45.0" }, + # is-terminal + { name = "hermit-abi", version = "0.3.1" }, + # procfs + { name = "rustix", version = "0.36.14" }, + # rustix + { name = "linux-raw-sys", version = "0.1.4" }, + # various crates + { name = "windows-sys", version = "0.45.0" }, + # windows-sys + { name = "windows-targets", version = "0.42.2" }, + # windows-targets + { name = "windows_aarch64_gnullvm", version = "0.42.2" }, + # windows-targets + { name = "windows_aarch64_msvc", version = "0.42.2" }, + # windows-targets + { name = "windows_i686_gnu", version = "0.42.2" }, + # windows-targets + { name = "windows_i686_msvc", version = "0.42.2" }, + # windows-targets + { name = "windows_x86_64_gnu", version = "0.42.2" }, + # windows-targets + { name = "windows_x86_64_gnullvm", version = "0.42.2" }, + # windows-targets + { name = "windows_x86_64_msvc", version = "0.42.2" }, + # tempfile + { name = "redox_syscall", version = "0.3.5" }, + # cpp_macros + { name = "aho-corasick", version = "0.7.19" }, + # ordered-multimap (via rust-ini) + { name = "hashbrown", version = "0.13.2" }, ] # spell-checker: enable diff --git a/docs/book.toml b/docs/book.toml index b9b31cfaf61..f2da19338c5 100644 --- a/docs/book.toml +++ b/docs/book.toml @@ -10,4 +10,4 @@ git-repository-url = "https://github.com/rust-lang/cargo/tree/master/src/doc/src [preprocessor.toc] command = "mdbook-toc" -renderer = ["html"] \ No newline at end of file +renderer = ["html"] diff --git a/docs/src/extensions.md b/docs/src/extensions.md index 3e84f70d802..281d8ef2e7b 100644 --- a/docs/src/extensions.md +++ b/docs/src/extensions.md @@ -5,6 +5,21 @@ features that are not supported by GNU coreutils. We take care not to introduce features that are incompatible with the GNU coreutils. Below is a list of uutils extensions. +## General + +GNU coreutils provides two ways to define short options taking an argument: + +``` +$ ls -w 80 +$ ls -w80 +``` + +We support a third way: + +``` +$ ls -w=80 +``` + ## `env` `env` has an additional `-f`/`--file` flag that can parse `.env` files and set @@ -43,3 +58,10 @@ therefore welcomed. `cut` can separate fields by whitespace (Space and Tab) with `-w` flag. This feature is adopted from [FreeBSD](https://www.freebsd.org/cgi/man.cgi?cut). + +## `fmt` + +`fmt` has additional flags for prefixes: `-P/--skip-prefix`, `-x/--exact-prefix`, and +`-X/--exact-skip-prefix`. With `-m/--preserve-headers`, an attempt is made to detect and preserve +mail headers in the input. `-q/--quick` breaks lines more quickly. And `-T/--tab-width` defines the +number of spaces representing a tab when determining the line length. diff --git a/src/bin/coreutils.rs b/src/bin/coreutils.rs index ac9750f938e..8440689c64b 100644 --- a/src/bin/coreutils.rs +++ b/src/bin/coreutils.rs @@ -43,10 +43,11 @@ fn binary_path(args: &mut impl Iterator) -> PathBuf { } } -fn name(binary_path: &Path) -> &str { - binary_path.file_stem().unwrap().to_str().unwrap() +fn name(binary_path: &Path) -> Option<&str> { + binary_path.file_stem()?.to_str() } +#[allow(clippy::cognitive_complexity)] fn main() { uucore::panic::mute_sigpipe_panic(); @@ -54,7 +55,10 @@ fn main() { let mut args = uucore::args_os(); let binary = binary_path(&mut args); - let binary_as_util = name(&binary); + let binary_as_util = name(&binary).unwrap_or_else(|| { + usage(&utils, ""); + process::exit(0); + }); // binary name equals util name? if let Some(&(uumain, _)) = utils.get(binary_as_util) { diff --git a/src/bin/uudoc.rs b/src/bin/uudoc.rs index 8276d2ae129..5ac8582262d 100644 --- a/src/bin/uudoc.rs +++ b/src/bin/uudoc.rs @@ -2,7 +2,7 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore tldr +// spell-checker:ignore tldr uuhelp use clap::Command; use std::collections::HashMap; @@ -133,7 +133,7 @@ impl<'a, 'b> MDWriter<'a, 'b> { write!(self.w, "# {}\n\n", self.name)?; self.additional()?; self.usage()?; - self.description()?; + self.about()?; self.options()?; self.after_help()?; self.examples() @@ -177,54 +177,34 @@ impl<'a, 'b> MDWriter<'a, 'b> { } fn usage(&mut self) -> io::Result<()> { - writeln!(self.w, "\n```")?; - let mut usage: String = self - .command - .render_usage() - .to_string() - .lines() - .map(|l| l.strip_prefix("Usage:").unwrap_or(l)) - .map(|l| l.trim()) - .filter(|l| !l.is_empty()) - .collect::>() - .join("\n"); - usage = usage - .to_string() - .replace(uucore::execution_phrase(), self.name); - writeln!(self.w, "{}", usage)?; - writeln!(self.w, "```") - } + if let Some(markdown) = &self.markdown { + let usage = uuhelp_parser::parse_usage(markdown); + let usage = usage.replace("{}", self.name); - fn description(&mut self) -> io::Result<()> { - if let Some(after_help) = self.markdown_section("about") { - return writeln!(self.w, "\n\n{}", after_help); + writeln!(self.w, "\n```")?; + writeln!(self.w, "{}", usage)?; + writeln!(self.w, "```") + } else { + Ok(()) } + } - if let Some(about) = self - .command - .get_long_about() - .or_else(|| self.command.get_about()) - { - writeln!(self.w, "{}", about) + fn about(&mut self) -> io::Result<()> { + if let Some(markdown) = &self.markdown { + writeln!(self.w, "{}", uuhelp_parser::parse_about(markdown)) } else { Ok(()) } } fn after_help(&mut self) -> io::Result<()> { - if let Some(after_help) = self.markdown_section("after help") { - return writeln!(self.w, "\n\n{}", after_help); + if let Some(markdown) = &self.markdown { + if let Some(after_help) = uuhelp_parser::parse_section("after help", markdown) { + return writeln!(self.w, "\n\n{after_help}"); + } } - if let Some(after_help) = self - .command - .get_after_long_help() - .or_else(|| self.command.get_after_help()) - { - writeln!(self.w, "\n\n{}", after_help) - } else { - Ok(()) - } + Ok(()) } fn examples(&mut self) -> io::Result<()> { @@ -236,6 +216,10 @@ impl<'a, 'b> MDWriter<'a, 'b> { } else if let Some(f) = get_zip_content(zip, &format!("pages/linux/{}.md", self.name)) { f } else { + println!( + "Warning: Could not find tldr examples for page '{}'", + self.name + ); return Ok(()); }; @@ -274,10 +258,10 @@ impl<'a, 'b> MDWriter<'a, 'b> { write!(self.w, "
")?; let mut first = true; for l in arg.get_long_and_visible_aliases().unwrap_or_default() { - if !first { - write!(self.w, ", ")?; - } else { + if first { first = false; + } else { + write!(self.w, ", ")?; } write!(self.w, "")?; write!(self.w, "--{}", l)?; @@ -295,10 +279,10 @@ impl<'a, 'b> MDWriter<'a, 'b> { write!(self.w, "")?; } for s in arg.get_short_and_visible_aliases().unwrap_or_default() { - if !first { - write!(self.w, ", ")?; - } else { + if first { first = false; + } else { + write!(self.w, ", ")?; } write!(self.w, "")?; write!(self.w, "-{}", s)?; @@ -327,32 +311,6 @@ impl<'a, 'b> MDWriter<'a, 'b> { } writeln!(self.w, "\n") } - - fn markdown_section(&self, section: &str) -> Option { - let md = self.markdown.as_ref()?; - let section = section.to_lowercase(); - - fn is_section_header(line: &str, section: &str) -> bool { - line.strip_prefix("##") - .map_or(false, |l| l.trim().to_lowercase() == section) - } - - let result = md - .lines() - .skip_while(|&l| !is_section_header(l, §ion)) - .skip(1) - .take_while(|l| !l.starts_with("##")) - .collect::>() - .join("\n") - .trim() - .to_string(); - - if !result.is_empty() { - Some(result) - } else { - None - } - } } fn get_zip_content(archive: &mut ZipArchive, name: &str) -> Option { diff --git a/src/uu/arch/Cargo.toml b/src/uu/arch/Cargo.toml index 8e871233869..42ac16b8918 100644 --- a/src/uu/arch/Cargo.toml +++ b/src/uu/arch/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_arch" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "arch ~ (uutils) display machine architecture" @@ -15,9 +15,9 @@ edition = "2021" path = "src/arch.rs" [dependencies] -platform-info = { workspace=true } -clap = { workspace=true } -uucore = { workspace=true } +platform-info = { workspace = true } +clap = { workspace = true } +uucore = { workspace = true } [[bin]] name = "arch" diff --git a/src/uu/arch/src/arch.rs b/src/uu/arch/src/arch.rs index a2208d8b031..96eba1ef947 100644 --- a/src/uu/arch/src/arch.rs +++ b/src/uu/arch/src/arch.rs @@ -9,7 +9,7 @@ use platform_info::*; use clap::{crate_version, Command}; -use uucore::error::{FromIo, UResult}; +use uucore::error::{UResult, USimpleError}; use uucore::{help_about, help_section}; static ABOUT: &str = help_about!("arch.md"); @@ -19,8 +19,9 @@ static SUMMARY: &str = help_section!("after help", "arch.md"); pub fn uumain(args: impl uucore::Args) -> UResult<()> { uu_app().try_get_matches_from(args)?; - let uts = PlatformInfo::new().map_err_context(|| "cannot get system name".to_string())?; - println!("{}", uts.machine().trim()); + let uts = PlatformInfo::new().map_err(|_e| USimpleError::new(1, "cannot get system name"))?; + + println!("{}", uts.machine().to_string_lossy().trim()); Ok(()) } diff --git a/src/uu/base32/Cargo.toml b/src/uu/base32/Cargo.toml index d71e71f952e..ea718e8dc27 100644 --- a/src/uu/base32/Cargo.toml +++ b/src/uu/base32/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_base32" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "base32 ~ (uutils) decode/encode input (base32-encoding)" @@ -15,8 +15,8 @@ edition = "2021" path = "src/base32.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true, features = ["encoding"] } +clap = { workspace = true } +uucore = { workspace = true, features = ["encoding"] } [[bin]] name = "base32" diff --git a/src/uu/base32/src/base_common.rs b/src/uu/base32/src/base_common.rs index f68aa6b86c9..78698e8b941 100644 --- a/src/uu/base32/src/base_common.rs +++ b/src/uu/base32/src/base_common.rs @@ -160,18 +160,7 @@ pub fn handle_input( data = data.line_wrap(wrap); } - if !decode { - match data.encode() { - Ok(s) => { - wrap_print(&data, &s); - Ok(()) - } - Err(_) => Err(USimpleError::new( - 1, - "error: invalid input (length must be multiple of 4 characters)", - )), - } - } else { + if decode { match data.decode() { Ok(s) => { // Silent the warning as we want to the error message @@ -184,5 +173,16 @@ pub fn handle_input( } Err(_) => Err(USimpleError::new(1, "error: invalid input")), } + } else { + match data.encode() { + Ok(s) => { + wrap_print(&data, &s); + Ok(()) + } + Err(_) => Err(USimpleError::new( + 1, + "error: invalid input (length must be multiple of 4 characters)", + )), + } } } diff --git a/src/uu/base64/Cargo.toml b/src/uu/base64/Cargo.toml index af92c70c2dc..ba822907382 100644 --- a/src/uu/base64/Cargo.toml +++ b/src/uu/base64/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_base64" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "base64 ~ (uutils) decode/encode input (base64-encoding)" @@ -15,8 +15,8 @@ edition = "2021" path = "src/base64.rs" [dependencies] -uucore = { workspace=true, features = ["encoding"] } -uu_base32 = { workspace=true } +uucore = { workspace = true, features = ["encoding"] } +uu_base32 = { workspace = true } [[bin]] name = "base64" diff --git a/src/uu/basename/Cargo.toml b/src/uu/basename/Cargo.toml index 7d8377de5ed..a1ea12c5ee3 100644 --- a/src/uu/basename/Cargo.toml +++ b/src/uu/basename/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_basename" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "basename ~ (uutils) display PATHNAME with leading directory components removed" @@ -15,8 +15,8 @@ edition = "2021" path = "src/basename.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true } +clap = { workspace = true } +uucore = { workspace = true } [[bin]] name = "basename" diff --git a/src/uu/basenc/Cargo.toml b/src/uu/basenc/Cargo.toml index e6babef7619..20665402a9c 100644 --- a/src/uu/basenc/Cargo.toml +++ b/src/uu/basenc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_basenc" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "basenc ~ (uutils) decode/encode input" @@ -15,9 +15,9 @@ edition = "2021" path = "src/basenc.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true, features = ["encoding"] } -uu_base32 = { workspace=true } +clap = { workspace = true } +uucore = { workspace = true, features = ["encoding"] } +uu_base32 = { workspace = true } [[bin]] name = "basenc" diff --git a/src/uu/basenc/src/basenc.rs b/src/uu/basenc/src/basenc.rs index afc25c7367e..3ec8cede01a 100644 --- a/src/uu/basenc/src/basenc.rs +++ b/src/uu/basenc/src/basenc.rs @@ -24,15 +24,33 @@ use uucore::{help_about, help_usage}; const ABOUT: &str = help_about!("basenc.md"); const USAGE: &str = help_usage!("basenc.md"); -const ENCODINGS: &[(&str, Format)] = &[ - ("base64", Format::Base64), - ("base64url", Format::Base64Url), - ("base32", Format::Base32), - ("base32hex", Format::Base32Hex), - ("base16", Format::Base16), - ("base2lsbf", Format::Base2Lsbf), - ("base2msbf", Format::Base2Msbf), - ("z85", Format::Z85), +const ENCODINGS: &[(&str, Format, &str)] = &[ + ("base64", Format::Base64, "same as 'base64' program"), + ("base64url", Format::Base64Url, "file- and url-safe base64"), + ("base32", Format::Base32, "same as 'base32' program"), + ( + "base32hex", + Format::Base32Hex, + "extended hex alphabet base32", + ), + ("base16", Format::Base16, "hex encoding"), + ( + "base2lsbf", + Format::Base2Lsbf, + "bit string with least significant bit (lsb) first", + ), + ( + "base2msbf", + Format::Base2Msbf, + "bit string with most significant bit (msb) first", + ), + ( + "z85", + Format::Z85, + "ascii85-like encoding;\n\ + when encoding, input length must be a multiple of 4;\n\ + when decoding, input length must be a multiple of 5", + ), ]; pub fn uu_app() -> Command { @@ -41,6 +59,7 @@ pub fn uu_app() -> Command { command = command.arg( Arg::new(encoding.0) .long(encoding.0) + .help(encoding.2) .action(ArgAction::SetTrue), ); } diff --git a/src/uu/cat/Cargo.toml b/src/uu/cat/Cargo.toml index fbf91c24929..e341fb5353d 100644 --- a/src/uu/cat/Cargo.toml +++ b/src/uu/cat/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_cat" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "cat ~ (uutils) concatenate and display input" @@ -15,13 +15,13 @@ edition = "2021" path = "src/cat.rs" [dependencies] -clap = { workspace=true } +clap = { workspace = true } thiserror = { workspace = true } is-terminal = { workspace = true } -uucore = { workspace=true, features=["fs", "pipes"] } +uucore = { workspace = true, features = ["fs", "pipes"] } [target.'cfg(unix)'.dependencies] -nix = { workspace=true } +nix = { workspace = true } [[bin]] name = "cat" diff --git a/src/uu/cat/src/cat.rs b/src/uu/cat/src/cat.rs index e90bdd11656..4a5e7e0ad34 100644 --- a/src/uu/cat/src/cat.rs +++ b/src/uu/cat/src/cat.rs @@ -456,6 +456,7 @@ fn write_fast(handle: &mut InputHandle) -> CatResult<()> { /// Outputs file contents to stdout in a line-by-line fashion, /// propagating any errors that might occur. +#[allow(clippy::cognitive_complexity)] fn write_lines( handle: &mut InputHandle, options: &OutputOptions, diff --git a/src/uu/chcon/Cargo.toml b/src/uu/chcon/Cargo.toml index 2e28aa29efd..58d6b9b469a 100644 --- a/src/uu/chcon/Cargo.toml +++ b/src/uu/chcon/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_chcon" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "chcon ~ (uutils) change file security context" @@ -14,12 +14,12 @@ edition = "2021" path = "src/chcon.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true, features=["entries", "fs", "perms"] } -selinux = { workspace=true } +clap = { workspace = true } +uucore = { workspace = true, features = ["entries", "fs", "perms"] } +selinux = { workspace = true } thiserror = { workspace = true } -libc = { workspace=true } -fts-sys = { workspace=true } +libc = { workspace = true } +fts-sys = { workspace = true } [[bin]] name = "chcon" diff --git a/src/uu/chgrp/Cargo.toml b/src/uu/chgrp/Cargo.toml index 164a30e2094..97ffc09b728 100644 --- a/src/uu/chgrp/Cargo.toml +++ b/src/uu/chgrp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_chgrp" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "chgrp ~ (uutils) change the group ownership of FILE" @@ -15,8 +15,8 @@ edition = "2021" path = "src/chgrp.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true, features=["entries", "fs", "perms"] } +clap = { workspace = true } +uucore = { workspace = true, features = ["entries", "fs", "perms"] } [[bin]] name = "chgrp" diff --git a/src/uu/chgrp/chgrp.md b/src/uu/chgrp/chgrp.md index 47c1ae5e49b..79bc068d2d3 100644 --- a/src/uu/chgrp/chgrp.md +++ b/src/uu/chgrp/chgrp.md @@ -4,7 +4,7 @@ ``` chgrp [OPTION]... GROUP FILE... -[OPTION]... --reference=RFILE FILE... +chgrp [OPTION]... --reference=RFILE FILE... ``` Change the group of each FILE to GROUP. diff --git a/src/uu/chgrp/src/chgrp.rs b/src/uu/chgrp/src/chgrp.rs index 15a248ddf62..bd01e132b47 100644 --- a/src/uu/chgrp/src/chgrp.rs +++ b/src/uu/chgrp/src/chgrp.rs @@ -10,7 +10,7 @@ use uucore::display::Quotable; pub use uucore::entries; use uucore::error::{FromIo, UResult, USimpleError}; -use uucore::perms::{chown_base, options, IfFrom}; +use uucore::perms::{chown_base, options, GidUidOwnerFilter, IfFrom}; use uucore::{format_usage, help_about, help_usage}; use clap::{crate_version, Arg, ArgAction, ArgMatches, Command}; @@ -18,19 +18,25 @@ use clap::{crate_version, Arg, ArgAction, ArgMatches, Command}; use std::fs; use std::os::unix::fs::MetadataExt; -static ABOUT: &str = help_about!("chgrp.md"); +const ABOUT: &str = help_about!("chgrp.md"); const USAGE: &str = help_usage!("chgrp.md"); -fn parse_gid_and_uid(matches: &ArgMatches) -> UResult<(Option, Option, IfFrom)> { +fn parse_gid_and_uid(matches: &ArgMatches) -> UResult { + let mut raw_group: String = String::new(); let dest_gid = if let Some(file) = matches.get_one::(options::REFERENCE) { fs::metadata(file) - .map(|meta| Some(meta.gid())) + .map(|meta| { + let gid = meta.gid(); + raw_group = entries::gid2grp(gid).unwrap_or_else(|_| gid.to_string()); + Some(gid) + }) .map_err_context(|| format!("failed to get attributes of {}", file.quote()))? } else { let group = matches .get_one::(options::ARG_GROUP) .map(|s| s.as_str()) .unwrap_or_default(); + raw_group = group.to_string(); if group.is_empty() { None } else { @@ -45,7 +51,12 @@ fn parse_gid_and_uid(matches: &ArgMatches) -> UResult<(Option, Option, } } }; - Ok((dest_gid, None, IfFrom::All)) + Ok(GidUidOwnerFilter { + dest_gid, + dest_uid: None, + raw_owner: raw_group, + filter: IfFrom::All, + }) } #[uucore::main] diff --git a/src/uu/chmod/Cargo.toml b/src/uu/chmod/Cargo.toml index 833067208dd..a4b4b799d79 100644 --- a/src/uu/chmod/Cargo.toml +++ b/src/uu/chmod/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_chmod" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "chmod ~ (uutils) change mode of FILE" @@ -15,9 +15,9 @@ edition = "2021" path = "src/chmod.rs" [dependencies] -clap = { workspace=true } -libc = { workspace=true } -uucore = { workspace=true, features=["fs", "mode"] } +clap = { workspace = true } +libc = { workspace = true } +uucore = { workspace = true, features = ["fs", "mode"] } [[bin]] name = "chmod" diff --git a/src/uu/chmod/src/chmod.rs b/src/uu/chmod/src/chmod.rs index 14d761cf9e6..a798860376d 100644 --- a/src/uu/chmod/src/chmod.rs +++ b/src/uu/chmod/src/chmod.rs @@ -274,10 +274,10 @@ impl Chmoder { ) )); } - if !self.recursive { - r = self.chmod_file(file).and(r); - } else { + if self.recursive { r = self.walk_dir(file); + } else { + r = self.chmod_file(file).and(r); } } r @@ -360,10 +360,10 @@ impl Chmoder { naively_expected_new_mode = naive_mode; } Err(f) => { - if !self.quiet { - return Err(USimpleError::new(1, f)); - } else { + if self.quiet { return Err(ExitCode::new(1)); + } else { + return Err(USimpleError::new(1, f)); } } } diff --git a/src/uu/chown/Cargo.toml b/src/uu/chown/Cargo.toml index 9ff43b00640..cd1f881eed5 100644 --- a/src/uu/chown/Cargo.toml +++ b/src/uu/chown/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_chown" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "chown ~ (uutils) change the ownership of FILE" @@ -15,8 +15,8 @@ edition = "2021" path = "src/chown.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true, features=["entries", "fs", "perms"] } +clap = { workspace = true } +uucore = { workspace = true, features = ["entries", "fs", "perms"] } [[bin]] name = "chown" diff --git a/src/uu/chown/src/chown.rs b/src/uu/chown/src/chown.rs index 3faecd571e4..67e71b815b9 100644 --- a/src/uu/chown/src/chown.rs +++ b/src/uu/chown/src/chown.rs @@ -9,7 +9,7 @@ use uucore::display::Quotable; pub use uucore::entries::{self, Group, Locate, Passwd}; -use uucore::perms::{chown_base, options, IfFrom}; +use uucore::perms::{chown_base, options, GidUidOwnerFilter, IfFrom}; use uucore::{format_usage, help_about, help_usage}; use uucore::error::{FromIo, UResult, USimpleError}; @@ -23,7 +23,7 @@ static ABOUT: &str = help_about!("chown.md"); const USAGE: &str = help_usage!("chown.md"); -fn parse_gid_uid_and_filter(matches: &ArgMatches) -> UResult<(Option, Option, IfFrom)> { +fn parse_gid_uid_and_filter(matches: &ArgMatches) -> UResult { let filter = if let Some(spec) = matches.get_one::(options::FROM) { match parse_spec(spec, ':')? { (Some(uid), None) => IfFrom::User(uid), @@ -37,17 +37,34 @@ fn parse_gid_uid_and_filter(matches: &ArgMatches) -> UResult<(Option, Optio let dest_uid: Option; let dest_gid: Option; + let raw_owner: String; if let Some(file) = matches.get_one::(options::REFERENCE) { let meta = fs::metadata(file) .map_err_context(|| format!("failed to get attributes of {}", file.quote()))?; - dest_gid = Some(meta.gid()); - dest_uid = Some(meta.uid()); + let gid = meta.gid(); + let uid = meta.uid(); + dest_gid = Some(gid); + dest_uid = Some(uid); + raw_owner = format!( + "{}:{}", + entries::uid2usr(uid).unwrap_or_else(|_| uid.to_string()), + entries::gid2grp(gid).unwrap_or_else(|_| gid.to_string()) + ); } else { - let (u, g) = parse_spec(matches.get_one::(options::ARG_OWNER).unwrap(), ':')?; + raw_owner = matches + .get_one::(options::ARG_OWNER) + .unwrap() + .into(); + let (u, g) = parse_spec(&raw_owner, ':')?; dest_uid = u; dest_gid = g; } - Ok((dest_gid, dest_uid, filter)) + Ok(GidUidOwnerFilter { + dest_gid, + dest_uid, + raw_owner, + filter, + }) } #[uucore::main] @@ -198,7 +215,9 @@ fn parse_spec(spec: &str, sep: char) -> UResult<(Option, Option)> { let user = args.next().unwrap_or(""); let group = args.next().unwrap_or(""); - let uid = if !user.is_empty() { + let uid = if user.is_empty() { + None + } else { Some(match Passwd::locate(user) { Ok(u) => u.uid, // We have been able to get the uid Err(_) => @@ -225,10 +244,10 @@ fn parse_spec(spec: &str, sep: char) -> UResult<(Option, Option)> { } } }) - } else { - None }; - let gid = if !group.is_empty() { + let gid = if group.is_empty() { + None + } else { Some(match Group::locate(group) { Ok(g) => g.gid, Err(_) => match group.parse() { @@ -241,8 +260,6 @@ fn parse_spec(spec: &str, sep: char) -> UResult<(Option, Option)> { } }, }) - } else { - None }; if user.chars().next().map(char::is_numeric).unwrap_or(false) diff --git a/src/uu/chroot/Cargo.toml b/src/uu/chroot/Cargo.toml index 0cae53110b9..1256a96c817 100644 --- a/src/uu/chroot/Cargo.toml +++ b/src/uu/chroot/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_chroot" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "chroot ~ (uutils) run COMMAND under a new root directory" @@ -15,8 +15,8 @@ edition = "2021" path = "src/chroot.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true, features=["entries", "fs"] } +clap = { workspace = true } +uucore = { workspace = true, features = ["entries", "fs"] } [[bin]] name = "chroot" diff --git a/src/uu/cksum/Cargo.toml b/src/uu/cksum/Cargo.toml index 9b7f70f8f8b..345dfb23841 100644 --- a/src/uu/cksum/Cargo.toml +++ b/src/uu/cksum/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_cksum" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "cksum ~ (uutils) display CRC and size of input" @@ -15,9 +15,9 @@ edition = "2021" path = "src/cksum.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true, features=["sum"] } -hex = { workspace=true } +clap = { workspace = true } +uucore = { workspace = true, features = ["sum"] } +hex = { workspace = true } [[bin]] name = "cksum" diff --git a/src/uu/cksum/cksum.md b/src/uu/cksum/cksum.md index c54132ef52e..4b0d25f32c3 100644 --- a/src/uu/cksum/cksum.md +++ b/src/uu/cksum/cksum.md @@ -10,14 +10,14 @@ Print CRC and size for each file 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) +- `sysv`: (equivalent to sum -s) +- `bsd`: (equivalent to sum -r) +- `crc`: (equivalent to cksum) +- `md5`: (equivalent to md5sum) +- `sha1`: (equivalent to sha1sum) +- `sha224`: (equivalent to sha224sum) +- `sha256`: (equivalent to sha256sum) +- `sha384`: (equivalent to sha384sum) +- `sha512`: (equivalent to sha512sum) +- `blake2b`: (equivalent to b2sum) +- `sm3`: (only available through cksum) diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index 9bddd3d7ab3..a46f69302a2 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -6,7 +6,7 @@ // file that was distributed with this source code. // spell-checker:ignore (ToDO) fname, algo -use clap::{crate_version, Arg, Command}; +use clap::{crate_version, Arg, ArgAction, Command}; use hex::encode; use std::ffi::OsStr; use std::fs::File; @@ -103,6 +103,7 @@ struct Options { algo_name: &'static str, digest: Box, output_bits: usize, + untagged: bool, } /// Calculate checksum @@ -159,8 +160,22 @@ where div_ceil(sz, options.output_bits), filename.display() ), - (_, true) => println!("{sum} {sz}"), - (_, false) => println!("{sum} {sz} {}", filename.display()), + (ALGORITHM_OPTIONS_CRC, true) => println!("{sum} {sz}"), + (ALGORITHM_OPTIONS_CRC, false) => println!("{sum} {sz} {}", filename.display()), + (ALGORITHM_OPTIONS_BLAKE2B, _) if !options.untagged => { + println!("BLAKE2b ({}) = {sum}", filename.display()); + } + _ => { + if options.untagged { + println!("{sum} {}", filename.display()); + } else { + println!( + "{} ({}) = {sum}", + options.algo_name.to_ascii_uppercase(), + filename.display() + ); + } + } } } @@ -202,8 +217,9 @@ fn digest_read( } mod options { - pub static FILE: &str = "file"; - pub static ALGORITHM: &str = "algorithm"; + pub const ALGORITHM: &str = "algorithm"; + pub const FILE: &str = "file"; + pub const UNTAGGED: &str = "untagged"; } #[uucore::main] @@ -222,6 +238,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { algo_name: name, digest: algo, output_bits: bits, + untagged: matches.get_flag(options::UNTAGGED), }; match matches.get_many::(options::FILE) { @@ -264,5 +281,11 @@ pub fn uu_app() -> Command { ALGORITHM_OPTIONS_SM3, ]), ) + .arg( + Arg::new(options::UNTAGGED) + .long(options::UNTAGGED) + .help("create a reversed style checksum, without digest type") + .action(ArgAction::SetTrue), + ) .after_help(AFTER_HELP) } diff --git a/src/uu/comm/Cargo.toml b/src/uu/comm/Cargo.toml index fbe28903f0e..e5947bb84ef 100644 --- a/src/uu/comm/Cargo.toml +++ b/src/uu/comm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_comm" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "comm ~ (uutils) compare sorted inputs" @@ -15,8 +15,8 @@ edition = "2021" path = "src/comm.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true } +clap = { workspace = true } +uucore = { workspace = true } [[bin]] name = "comm" diff --git a/src/uu/cp/Cargo.toml b/src/uu/cp/Cargo.toml index 4aa871185f7..ff8f21160b3 100644 --- a/src/uu/cp/Cargo.toml +++ b/src/uu/cp/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "uu_cp" -version = "0.0.17" +version = "0.0.19" authors = [ - "Jordy Dickinson ", - "Joshua S. Miller ", - "uutils developers", + "Jordy Dickinson ", + "Joshua S. Miller ", + "uutils developers", ] license = "MIT" description = "cp ~ (uutils) copy SOURCE to DESTINATION" @@ -19,18 +19,18 @@ edition = "2021" path = "src/cp.rs" [dependencies] -clap = { workspace=true } -filetime = { workspace=true } -libc = { workspace=true } -quick-error = { workspace=true } -selinux = { workspace=true, optional=true } -uucore = { workspace=true, features=["entries", "fs", "perms", "mode"] } -walkdir = { workspace=true } -indicatif = { workspace=true } +clap = { workspace = true } +filetime = { workspace = true } +libc = { workspace = true } +quick-error = { workspace = true } +selinux = { workspace = true, optional = true } +uucore = { workspace = true, features = ["entries", "fs", "perms", "mode"] } +walkdir = { workspace = true } +indicatif = { workspace = true } [target.'cfg(unix)'.dependencies] -xattr = { workspace=true } -exacl = { workspace=true, optional=true } +xattr = { workspace = true } +exacl = { workspace = true, optional = true } [[bin]] name = "cp" diff --git a/src/uu/cp/cp.md b/src/uu/cp/cp.md index 5f3cabc18b5..7485340f2ac 100644 --- a/src/uu/cp/cp.md +++ b/src/uu/cp/cp.md @@ -7,3 +7,19 @@ cp [OPTION]... -t DIRECTORY SOURCE... ``` Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY. + +## After Help + +Do not copy a non-directory that has an existing destination with the same or newer modification timestamp; +instead, silently skip the file without failing. If timestamps are being preserved, the comparison is to the +source timestamp truncated to the resolutions of the destination file system and of the system calls used to +update timestamps; this avoids duplicate work if several `cp -pu` commands are executed with the same source +and destination. This option is ignored if the `-n` or `--no-clobber` option is also specified. Also, if +`--preserve=links` is also specified (like with `cp -au` for example), that will take precedence; consequently, +depending on the order that files are processed from the source, newer files in the destination may be replaced, +to mirror hard links in the source. which gives more control over which existing files in the destination are +replaced, and its value can be one of the following: + +* `all` This is the default operation when an `--update` option is not specified, and results in all existing files in the destination being replaced. +* `none` This is similar to the `--no-clobber` option, in that no files in the destination are replaced, but also skipping a file does not induce a failure. +* `older` This is the default operation when `--update` is specified, and results in files being replaced if they’re older than the corresponding source file. diff --git a/src/uu/cp/src/copydir.rs b/src/uu/cp/src/copydir.rs index c312b7cbb4a..aaeb73f5acf 100644 --- a/src/uu/cp/src/copydir.rs +++ b/src/uu/cp/src/copydir.rs @@ -404,8 +404,18 @@ pub(crate) fn copy_directory( Err(e) => show_error!("{}", e), } } + // Copy the attributes from the root directory to the target directory. - copy_attributes(root, target, &options.attributes)?; + if options.parents { + let dest = target.join(root.file_name().unwrap()); + copy_attributes(root, dest.as_path(), &options.attributes)?; + for (x, y) in aligned_ancestors(root, dest.as_path()) { + copy_attributes(x, y, &options.attributes)?; + } + } else { + copy_attributes(root, target, &options.attributes)?; + } + Ok(()) } diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 80540d222ec..60ef540954a 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -38,9 +38,14 @@ use uucore::backup_control::{self, BackupMode}; use uucore::display::Quotable; use uucore::error::{set_exit_code, UClapError, UError, UResult, UUsageError}; use uucore::fs::{ - canonicalize, paths_refer_to_same_file, FileInformation, MissingHandling, ResolveMode, + canonicalize, is_symlink_loop, paths_refer_to_same_file, FileInformation, MissingHandling, + ResolveMode, +}; +use uucore::update_control::{self, UpdateMode}; +use uucore::{ + crash, format_usage, help_about, help_section, help_usage, prompt_yes, show_error, + show_warning, util_name, }; -use uucore::{crash, format_usage, help_about, help_usage, prompt_yes, show_error, show_warning}; use crate::copydir::copy_directory; @@ -224,13 +229,83 @@ pub struct Options { recursive: bool, backup_suffix: String, target_dir: Option, - update: bool, + update: UpdateMode, + debug: bool, verbose: bool, progress_bar: bool, } +/// Enum representing various debug states of the offload and reflink actions. +#[derive(Debug)] +#[allow(dead_code)] // All of them are used on Linux +enum OffloadReflinkDebug { + Unknown, + No, + Yes, + Avoided, + Unsupported, +} + +/// Enum representing various debug states of the sparse detection. +#[derive(Debug)] +#[allow(dead_code)] // silent for now until we use them +enum SparseDebug { + Unknown, + No, + Zeros, + SeekHole, + SeekHoleZeros, + Unsupported, +} + +/// Struct that contains the debug state for each action in a file copy operation. +#[derive(Debug)] +struct CopyDebug { + offload: OffloadReflinkDebug, + reflink: OffloadReflinkDebug, + sparse_detection: SparseDebug, +} + +impl OffloadReflinkDebug { + fn to_string(&self) -> &'static str { + match self { + Self::No => "no", + Self::Yes => "yes", + Self::Avoided => "avoided", + Self::Unsupported => "unsupported", + Self::Unknown => "unknown", + } + } +} + +impl SparseDebug { + fn to_string(&self) -> &'static str { + match self { + Self::No => "no", + Self::Zeros => "zeros", + Self::SeekHole => "SEEK_HOLE", + Self::SeekHoleZeros => "SEEK_HOLE + zeros", + Self::Unsupported => "unsupported", + Self::Unknown => "unknown", + } + } +} + +/// This function prints the debug information of a file copy operation if +/// no hard link or symbolic link is required, and data copy is required. +/// It prints the debug information of the offload, reflink, and sparse detection actions. +fn show_debug(copy_debug: &CopyDebug) { + println!( + "copy offload: {}, reflink: {}, sparse detection: {}", + copy_debug.offload.to_string(), + copy_debug.reflink.to_string(), + copy_debug.sparse_detection.to_string(), + ); +} + const ABOUT: &str = help_about!("cp.md"); const USAGE: &str = help_usage!("cp.md"); +const AFTER_HELP: &str = help_section!("after help", "cp.md"); static EXIT_ERR: i32 = 1; @@ -264,7 +339,7 @@ mod options { pub const STRIP_TRAILING_SLASHES: &str = "strip-trailing-slashes"; pub const SYMBOLIC_LINK: &str = "symbolic-link"; pub const TARGET_DIRECTORY: &str = "target-directory"; - pub const UPDATE: &str = "update"; + pub const DEBUG: &str = "debug"; pub const VERBOSE: &str = "verbose"; } @@ -274,14 +349,22 @@ static PRESERVABLE_ATTRIBUTES: &[&str] = &[ "ownership", "timestamps", "context", + "link", "links", "xattr", "all", ]; #[cfg(not(unix))] -static PRESERVABLE_ATTRIBUTES: &[&str] = - &["mode", "timestamps", "context", "links", "xattr", "all"]; +static PRESERVABLE_ATTRIBUTES: &[&str] = &[ + "mode", + "timestamps", + "context", + "link", + "links", + "xattr", + "all", +]; pub fn uu_app() -> Command { const MODE_ARGS: &[&str] = &[ @@ -295,6 +378,10 @@ pub fn uu_app() -> Command { .version(crate_version!()) .about(ABOUT) .override_usage(format_usage(USAGE)) + .after_help(format!( + "{AFTER_HELP}\n\n{}", + backup_control::BACKUP_CONTROL_LONG_HELP + )) .infer_long_args(true) .arg( Arg::new(options::TARGET_DIRECTORY) @@ -353,6 +440,12 @@ pub fn uu_app() -> Command { .help("remove any trailing slashes from each SOURCE argument") .action(ArgAction::SetTrue), ) + .arg( + Arg::new(options::DEBUG) + .long(options::DEBUG) + .help("explain how a file is copied. Implies -v") + .action(ArgAction::SetTrue), + ) .arg( Arg::new(options::VERBOSE) .short('v') @@ -393,16 +486,8 @@ pub fn uu_app() -> Command { .arg(backup_control::arguments::backup()) .arg(backup_control::arguments::backup_no_args()) .arg(backup_control::arguments::suffix()) - .arg( - Arg::new(options::UPDATE) - .short('u') - .long(options::UPDATE) - .help( - "copy only when the SOURCE file is newer than the destination file \ - or when the destination file is missing", - ) - .action(ArgAction::SetTrue), - ) + .arg(update_control::arguments::update()) + .arg(update_control::arguments::update_no_args()) .arg( Arg::new(options::REFLINK) .long(options::REFLINK) @@ -430,6 +515,7 @@ pub fn uu_app() -> Command { PRESERVABLE_ATTRIBUTES, )) .num_args(0..) + .require_equals(true) .value_name("ATTR_LIST") .overrides_with_all([ options::ARCHIVE, @@ -564,13 +650,11 @@ pub fn uu_app() -> Command { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app() - .after_help(backup_control::BACKUP_CONTROL_LONG_HELP) - .try_get_matches_from(args); + let matches = uu_app().try_get_matches_from(args); // The error is parsed here because we do not want version or help being printed to stderr. if let Err(e) = matches { - let mut app = uu_app().after_help(backup_control::BACKUP_CONTROL_LONG_HELP); + let mut app = uu_app(); match e.kind() { clap::error::ErrorKind::DisplayHelp => { @@ -641,7 +725,11 @@ impl CopyMode { Self::Link } else if matches.get_flag(options::SYMBOLIC_LINK) { Self::SymLink - } else if matches.get_flag(options::UPDATE) { + } else if matches + .get_one::(update_control::arguments::OPT_UPDATE) + .is_some() + || matches.get_flag(update_control::arguments::OPT_UPDATE_NO_ARG) + { Self::Update } else if matches.get_flag(options::ATTRIBUTES_ONLY) { Self::AttrOnly @@ -711,7 +799,7 @@ impl Attributes { "ownership" => self.ownership = preserve_yes_required, "timestamps" => self.timestamps = preserve_yes_required, "context" => self.context = preserve_yes_required, - "links" => self.links = preserve_yes_required, + "link" | "links" => self.links = preserve_yes_required, "xattr" => self.xattr = preserve_yes_required, _ => { return Err(Error::InvalidArgument(format!( @@ -725,6 +813,7 @@ impl Attributes { } impl Options { + #[allow(clippy::cognitive_complexity)] fn from_matches(matches: &ArgMatches) -> CopyResult { let not_implemented_opts = vec![ #[cfg(not(any(windows, unix)))] @@ -749,6 +838,7 @@ impl Options { Err(e) => return Err(Error::Backup(format!("{e}"))), Ok(mode) => mode, }; + let update_mode = update_control::determine_update_mode(matches); let backup_suffix = backup_control::determine_backup_suffix(matches); @@ -826,8 +916,9 @@ impl Options { || matches.get_flag(options::DEREFERENCE), one_file_system: matches.get_flag(options::ONE_FILE_SYSTEM), parents: matches.get_flag(options::PARENTS), - update: matches.get_flag(options::UPDATE), - verbose: matches.get_flag(options::VERBOSE), + update: update_mode, + debug: matches.get_flag(options::DEBUG), + verbose: matches.get_flag(options::VERBOSE) || matches.get_flag(options::DEBUG), strip_trailing_slashes: matches.get_flag(options::STRIP_TRAILING_SLASHES), reflink_mode: { if let Some(reflink) = matches.get_one::(options::REFLINK) { @@ -1013,23 +1104,21 @@ fn preserve_hardlinks( } /// When handling errors, we don't always want to show them to the user. This function handles that. -/// If the error is printed, returns true, false otherwise. -fn show_error_if_needed(error: &Error) -> bool { +fn show_error_if_needed(error: &Error) { match error { // When using --no-clobber, we don't want to show // an error message - Error::NotAllFilesCopied => (), + Error::NotAllFilesCopied => { + // Need to return an error code + } Error::Skipped => { // touch a b && echo "n"|cp -i a b && echo $? // should return an error from GNU 9.2 - return true; } _ => { show_error!("{}", error); - return true; } } - false } /// Copy all `sources` to `target`. Returns an @@ -1086,9 +1175,8 @@ fn copy(sources: &[Source], target: &TargetSlice, options: &Options) -> CopyResu options, &mut symlinked_files, ) { - if show_error_if_needed(&error) { - non_fatal_errors = true; - } + show_error_if_needed(&error); + non_fatal_errors = true; } } seen_sources.insert(source); @@ -1147,25 +1235,41 @@ fn copy_source( } else { // Copy as file let dest = construct_dest_path(source_path, target, target_type, options)?; - copy_file( + let res = copy_file( progress_bar, source_path, dest.as_path(), options, symlinked_files, true, - ) + ); + if options.parents { + for (x, y) in aligned_ancestors(source, dest.as_path()) { + copy_attributes(x, y, &options.attributes)?; + } + } + res } } impl OverwriteMode { - fn verify(&self, path: &Path) -> CopyResult<()> { + fn verify(&self, path: &Path, verbose: bool) -> CopyResult<()> { match *self { - Self::NoClobber => Err(Error::NotAllFilesCopied), + Self::NoClobber => { + if verbose { + println!("skipped {}", path.quote()); + } else { + eprintln!("{}: not replacing {}", util_name(), path.quote()); + } + Err(Error::NotAllFilesCopied) + } Self::Interactive(_) => { if prompt_yes!("overwrite {}?", path.quote()) { Ok(()) } else { + if verbose { + println!("skipped {}", path.quote()); + } Err(Error::Skipped) } } @@ -1373,7 +1477,7 @@ fn handle_existing_dest( return Err(format!("{} and {} are the same file", source.quote(), dest.quote()).into()); } - options.overwrite.verify(dest)?; + options.overwrite.verify(dest, options.verbose)?; let backup_path = backup_control::get_backup_path(options.backup, dest, &options.backup_suffix); if let Some(backup_path) = backup_path { @@ -1388,11 +1492,10 @@ fn handle_existing_dest( backup_dest(dest, &backup_path)?; } } - match options.overwrite { // FIXME: print that the file was removed if --verbose is enabled OverwriteMode::Clobber(ClobberMode::Force) => { - if fs::metadata(dest)?.permissions().readonly() { + if is_symlink_loop(dest) || fs::metadata(dest)?.permissions().readonly() { fs::remove_file(dest)?; } } @@ -1465,6 +1568,7 @@ fn aligned_ancestors<'a>(source: &'a Path, dest: &'a Path) -> Vec<(&'a Path, &'a /// /// The original permissions of `source` will be copied to `dest` /// after a successful copy. +#[allow(clippy::cognitive_complexity)] fn copy_file( progress_bar: &Option, source: &Path, @@ -1473,7 +1577,9 @@ fn copy_file( symlinked_files: &mut HashSet, source_in_command_line: bool, ) -> CopyResult<()> { - if options.update && options.overwrite == OverwriteMode::Interactive(ClobberMode::Standard) { + if (options.update == UpdateMode::ReplaceIfOlder || options.update == UpdateMode::ReplaceNone) + && options.overwrite == OverwriteMode::Interactive(ClobberMode::Standard) + { // `cp -i --update old new` when `new` exists doesn't copy anything // and exit with 0 return Ok(()); @@ -1498,6 +1604,8 @@ fn copy_file( options.overwrite, OverwriteMode::Clobber(ClobberMode::RemoveDestination) ) + && !is_symlink_loop(dest) + && std::env::var_os("POSIXLY_CORRECT").is_none() { return Err(Error::Error(format!( "not writing through dangling symlink '{}'", @@ -1629,22 +1737,38 @@ fn copy_file( } CopyMode::Update => { if dest.exists() { - let dest_metadata = fs::symlink_metadata(dest)?; - - let src_time = source_metadata.modified()?; - let dest_time = dest_metadata.modified()?; - if src_time <= dest_time { - return Ok(()); - } else { - copy_helper( - source, - dest, - options, - context, - source_is_symlink, - source_is_fifo, - symlinked_files, - )?; + match options.update { + update_control::UpdateMode::ReplaceAll => { + copy_helper( + source, + dest, + options, + context, + source_is_symlink, + source_is_fifo, + symlinked_files, + )?; + } + update_control::UpdateMode::ReplaceNone => return Ok(()), + update_control::UpdateMode::ReplaceIfOlder => { + let dest_metadata = fs::symlink_metadata(dest)?; + + let src_time = source_metadata.modified()?; + let dest_time = dest_metadata.modified()?; + if src_time <= dest_time { + return Ok(()); + } else { + copy_helper( + source, + dest, + options, + context, + source_is_symlink, + source_is_fifo, + symlinked_files, + )?; + } + } } } else { copy_helper( @@ -1711,11 +1835,11 @@ fn copy_helper( File::create(dest).context(dest.display().to_string())?; } else if source_is_fifo && options.recursive && !options.copy_contents { #[cfg(unix)] - copy_fifo(dest, options.overwrite)?; + copy_fifo(dest, options.overwrite, options.verbose)?; } else if source_is_symlink { copy_link(source, dest, symlinked_files)?; } else { - copy_on_write( + let copy_debug = copy_on_write( source, dest, options.reflink_mode, @@ -1724,6 +1848,10 @@ fn copy_helper( #[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))] source_is_fifo, )?; + + if !options.attributes_only && options.debug { + show_debug(©_debug); + } } Ok(()) @@ -1732,9 +1860,9 @@ fn copy_helper( // "Copies" a FIFO by creating a new one. This workaround is because Rust's // built-in fs::copy does not handle FIFOs (see rust-lang/rust/issues/79390). #[cfg(unix)] -fn copy_fifo(dest: &Path, overwrite: OverwriteMode) -> CopyResult<()> { +fn copy_fifo(dest: &Path, overwrite: OverwriteMode, verbose: bool) -> CopyResult<()> { if dest.exists() { - overwrite.verify(dest)?; + overwrite.verify(dest, verbose)?; fs::remove_file(dest)?; } diff --git a/src/uu/cp/src/platform/linux.rs b/src/uu/cp/src/platform/linux.rs index 7d97813dd44..18f2520a2ad 100644 --- a/src/uu/cp/src/platform/linux.rs +++ b/src/uu/cp/src/platform/linux.rs @@ -13,7 +13,7 @@ use quick_error::ResultExt; use uucore::mode::get_umask; -use crate::{CopyResult, ReflinkMode, SparseMode}; +use crate::{CopyDebug, CopyResult, OffloadReflinkDebug, ReflinkMode, SparseDebug, SparseMode}; // From /usr/include/linux/fs.h: // #define FICLONE _IOW(0x94, 9, int) @@ -145,24 +145,51 @@ pub(crate) fn copy_on_write( sparse_mode: SparseMode, context: &str, source_is_fifo: bool, -) -> CopyResult<()> { +) -> CopyResult { + let mut copy_debug = CopyDebug { + offload: OffloadReflinkDebug::Unknown, + reflink: OffloadReflinkDebug::Unsupported, + sparse_detection: SparseDebug::No, + }; + let result = match (reflink_mode, sparse_mode) { - (ReflinkMode::Never, SparseMode::Always) => sparse_copy(source, dest), - (ReflinkMode::Never, _) => std::fs::copy(source, dest).map(|_| ()), - (ReflinkMode::Auto, SparseMode::Always) => sparse_copy(source, dest), + (ReflinkMode::Never, SparseMode::Always) => { + copy_debug.sparse_detection = SparseDebug::Zeros; + copy_debug.offload = OffloadReflinkDebug::Avoided; + copy_debug.reflink = OffloadReflinkDebug::No; + sparse_copy(source, dest) + } + (ReflinkMode::Never, _) => { + copy_debug.sparse_detection = SparseDebug::No; + copy_debug.reflink = OffloadReflinkDebug::No; + std::fs::copy(source, dest).map(|_| ()) + } + (ReflinkMode::Auto, SparseMode::Always) => { + copy_debug.offload = OffloadReflinkDebug::Avoided; + copy_debug.sparse_detection = SparseDebug::Zeros; + copy_debug.reflink = OffloadReflinkDebug::Unsupported; + sparse_copy(source, dest) + } (ReflinkMode::Auto, _) => { + copy_debug.sparse_detection = SparseDebug::No; + copy_debug.reflink = OffloadReflinkDebug::Unsupported; if source_is_fifo { copy_fifo_contents(source, dest).map(|_| ()) } else { clone(source, dest, CloneFallback::FSCopy) } } - (ReflinkMode::Always, SparseMode::Auto) => clone(source, dest, CloneFallback::Error), + (ReflinkMode::Always, SparseMode::Auto) => { + copy_debug.sparse_detection = SparseDebug::No; + copy_debug.reflink = OffloadReflinkDebug::Yes; + + clone(source, dest, CloneFallback::Error) + } (ReflinkMode::Always, _) => { return Err("`--reflink=always` can be used only with --sparse=auto".into()) } }; result.context(context)?; - Ok(()) + Ok(copy_debug) } diff --git a/src/uu/cp/src/platform/macos.rs b/src/uu/cp/src/platform/macos.rs index 4407e0edf9c..b173aa95915 100644 --- a/src/uu/cp/src/platform/macos.rs +++ b/src/uu/cp/src/platform/macos.rs @@ -11,7 +11,7 @@ use std::path::Path; use quick_error::ResultExt; -use crate::{CopyResult, ReflinkMode, SparseMode}; +use crate::{CopyDebug, CopyResult, OffloadReflinkDebug, ReflinkMode, SparseDebug, SparseMode}; /// Copies `source` to `dest` using copy-on-write if possible. /// @@ -24,10 +24,15 @@ pub(crate) fn copy_on_write( sparse_mode: SparseMode, context: &str, source_is_fifo: bool, -) -> CopyResult<()> { +) -> CopyResult { if sparse_mode != SparseMode::Auto { return Err("--sparse is only supported on linux".to_string().into()); } + let mut copy_debug = CopyDebug { + offload: OffloadReflinkDebug::Unknown, + reflink: OffloadReflinkDebug::Unsupported, + sparse_detection: SparseDebug::Unsupported, + }; // Extract paths in a form suitable to be passed to a syscall. // The unwrap() is safe because they come from the command-line and so contain non nul @@ -72,6 +77,7 @@ pub(crate) fn copy_on_write( return Err(format!("failed to clone {source:?} from {dest:?}: {error}").into()) } _ => { + copy_debug.reflink = OffloadReflinkDebug::Yes; if source_is_fifo { let mut src_file = File::open(source)?; let mut dst_file = File::create(dest)?; @@ -83,5 +89,5 @@ pub(crate) fn copy_on_write( }; } - Ok(()) + Ok(copy_debug) } diff --git a/src/uu/cp/src/platform/other.rs b/src/uu/cp/src/platform/other.rs index b70da2f2341..f5882f75ea3 100644 --- a/src/uu/cp/src/platform/other.rs +++ b/src/uu/cp/src/platform/other.rs @@ -8,7 +8,7 @@ use std::path::Path; use quick_error::ResultExt; -use crate::{CopyResult, ReflinkMode, SparseMode}; +use crate::{CopyDebug, CopyResult, OffloadReflinkDebug, ReflinkMode, SparseDebug, SparseMode}; /// Copies `source` to `dest` for systems without copy-on-write pub(crate) fn copy_on_write( @@ -17,7 +17,7 @@ pub(crate) fn copy_on_write( reflink_mode: ReflinkMode, sparse_mode: SparseMode, context: &str, -) -> CopyResult<()> { +) -> CopyResult { if reflink_mode != ReflinkMode::Never { return Err("--reflink is only supported on linux and macOS" .to_string() @@ -26,8 +26,12 @@ pub(crate) fn copy_on_write( if sparse_mode != SparseMode::Auto { return Err("--sparse is only supported on linux".to_string().into()); } - + let copy_debug = CopyDebug { + offload: OffloadReflinkDebug::Unsupported, + reflink: OffloadReflinkDebug::Unsupported, + sparse_detection: SparseDebug::Unsupported, + }; fs::copy(source, dest).context(context)?; - Ok(()) + Ok(copy_debug) } diff --git a/src/uu/csplit/Cargo.toml b/src/uu/csplit/Cargo.toml index 62b43c10e3b..023d972434e 100644 --- a/src/uu/csplit/Cargo.toml +++ b/src/uu/csplit/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_csplit" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "csplit ~ (uutils) Output pieces of FILE separated by PATTERN(s) to files 'xx00', 'xx01', ..., and output byte counts of each piece to standard output" @@ -15,10 +15,10 @@ edition = "2021" path = "src/csplit.rs" [dependencies] -clap = { workspace=true } +clap = { workspace = true } thiserror = { workspace = true } -regex = { workspace=true } -uucore = { workspace=true, features=["entries", "fs"] } +regex = { workspace = true } +uucore = { workspace = true, features = ["entries", "fs"] } [[bin]] name = "csplit" diff --git a/src/uu/csplit/src/csplit.rs b/src/uu/csplit/src/csplit.rs index 6e1eaf1a3a3..1a94b41565e 100644 --- a/src/uu/csplit/src/csplit.rs +++ b/src/uu/csplit/src/csplit.rs @@ -356,6 +356,7 @@ impl<'a> SplitWriter<'a> { /// - if no line matched, an [`CsplitError::MatchNotFound`]. /// - if there are not enough lines to accommodate the offset, an /// [`CsplitError::LineOutOfRange`]. + #[allow(clippy::cognitive_complexity)] fn do_to_match( &mut self, pattern_as_str: &str, diff --git a/src/uu/cut/Cargo.toml b/src/uu/cut/Cargo.toml index 8a5e8f1d231..6e8c28d74ac 100644 --- a/src/uu/cut/Cargo.toml +++ b/src/uu/cut/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_cut" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "cut ~ (uutils) display byte/field columns of input lines" @@ -15,11 +15,11 @@ edition = "2021" path = "src/cut.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true } -memchr = { workspace=true } -bstr = { workspace=true } -is-terminal = { workspace=true } +clap = { workspace = true } +uucore = { workspace = true } +memchr = { workspace = true } +bstr = { workspace = true } +is-terminal = { workspace = true } [[bin]] name = "cut" diff --git a/src/uu/date/Cargo.toml b/src/uu/date/Cargo.toml index cd7c505138b..e28762493e4 100644 --- a/src/uu/date/Cargo.toml +++ b/src/uu/date/Cargo.toml @@ -1,6 +1,7 @@ +# spell-checker:ignore datetime [package] name = "uu_date" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "date ~ (uutils) display or set the current time" @@ -15,15 +16,19 @@ edition = "2021" path = "src/date.rs" [dependencies] -chrono = { workspace=true } -clap = { workspace=true } -uucore = { workspace=true } +chrono = { workspace = true } +clap = { workspace = true } +uucore = { workspace = true } +parse_datetime = { workspace = true } [target.'cfg(unix)'.dependencies] -libc = { workspace=true } +libc = { workspace = true } [target.'cfg(windows)'.dependencies] -windows-sys = { workspace=true, features = ["Win32_Foundation", "Win32_System_SystemInformation"] } +windows-sys = { workspace = true, features = [ + "Win32_Foundation", + "Win32_System_SystemInformation", +] } [[bin]] name = "date" diff --git a/src/uu/date/date-usage.md b/src/uu/date/date-usage.md new file mode 100644 index 00000000000..bf2dc469d3a --- /dev/null +++ b/src/uu/date/date-usage.md @@ -0,0 +1,87 @@ +# `date` usage + + + +FORMAT controls the output. Interpreted sequences are: + +| Sequence | Description | Example | +| -------- | -------------------------------------------------------------------- | ---------------------- | +| %% | a literal % | % | +| %a | locale's abbreviated weekday name | Sun | +| %A | locale's full weekday name | Sunday | +| %b | locale's abbreviated month name | Jan | +| %B | locale's full month name | January | +| %c | locale's date and time | Thu Mar 3 23:05:25 2005| +| %C | century; like %Y, except omit last two digits | 20 | +| %d | day of month | 01 | +| %D | date; same as %m/%d/%y | 12/31/99 | +| %e | day of month, space padded; same as %_d | 3 | +| %F | full date; same as %Y-%m-%d | 2005-03-03 | +| %g | last two digits of year of ISO week number (see %G) | 05 | +| %G | year of ISO week number (see %V); normally useful only with %V | 2005 | +| %h | same as %b | Jan | +| %H | hour (00..23) | 23 | +| %I | hour (01..12) | 11 | +| %j | day of year (001..366) | 062 | +| %k | hour, space padded ( 0..23); same as %_H | 3 | +| %l | hour, space padded ( 1..12); same as %_I | 9 | +| %m | month (01..12) | 03 | +| %M | minute (00..59) | 30 | +| %n | a newline | \n | +| %N | nanoseconds (000000000..999999999) | 123456789 | +| %p | locale's equivalent of either AM or PM; blank if not known | PM | +| %P | like %p, but lower case | pm | +| %q | quarter of year (1..4) | 1 | +| %r | locale's 12-hour clock time | 11:11:04 PM | +| %R | 24-hour hour and minute; same as %H:%M | 23:30 | +| %s | seconds since 1970-01-01 00:00:00 UTC | 1615432800 | +| %S | second (00..60) | 30 | +| %t | a tab | \t | +| %T | time; same as %H:%M:%S | 23:30:30 | +| %u | day of week (1..7); 1 is Monday | 4 | +| %U | week number of year, with Sunday as first day of week (00..53) | 10 | +| %V | ISO week number, with Monday as first day of week (01..53) | 12 | +| %w | day of week (0..6); 0 is Sunday | 4 | +| %W | week number of year, with Monday as first day of week (00..53) | 11 | +| %x | locale's date representation | 03/03/2005 | +| %X | locale's time representation | 23:30:30 | +| %y | last two digits of year (00..99) | 05 | +| %Y | year | 2005 | +| %z | +hhmm numeric time zone | -0400 | +| %:z | +hh:mm numeric time zone | -04:00 | +| %::z | +hh:mm:ss numeric time zone | -04:00:00 | +| %:::z | numeric time zone with : to necessary precision | -04, +05:30 | +| %Z | alphabetic time zone abbreviation | EDT | + +By default, date pads numeric fields with zeroes. +The following optional flags may follow '%': + +* `-` (hyphen) do not pad the field +* `_` (underscore) pad with spaces +* `0` (zero) pad with zeros +* `^` use upper case if possible +* `#` use opposite case if possible + +After any flags comes an optional field width, as a decimal number; +then an optional modifier, which is either +E to use the locale's alternate representations if available, or +O to use the locale's alternate numeric symbols if available. + +Examples: +Convert seconds since the epoch (1970-01-01 UTC) to a date + +``` +date --date='@2147483647' +``` + +Show the time on the west coast of the US (use tzselect(1) to find TZ) + +``` +TZ='America/Los_Angeles' date +``` + +Show the local time for 9AM next Friday on the west coast of the US + +``` +date --date='TZ="America/Los_Angeles" 09:00 next Fri' +``` diff --git a/src/uu/date/date-usage.mkd b/src/uu/date/date-usage.mkd deleted file mode 100644 index 8290010956f..00000000000 --- a/src/uu/date/date-usage.mkd +++ /dev/null @@ -1,78 +0,0 @@ -# `date` usage - - - -``` text -FORMAT controls the output. Interpreted sequences are: - - %% a literal % - %a locale's abbreviated weekday name (e.g., Sun) - %A locale's full weekday name (e.g., Sunday) - %b locale's abbreviated month name (e.g., Jan) - %B locale's full month name (e.g., January) - %c locale's date and time (e.g., Thu Mar 3 23:05:25 2005) - %C century; like %Y, except omit last two digits (e.g., 20) - %d day of month (e.g., 01) - %D date; same as %m/%d/%y - %e day of month, space padded; same as %_d - %F full date; same as %Y-%m-%d - %g last two digits of year of ISO week number (see %G) - %G year of ISO week number (see %V); normally useful only with %V - %h same as %b - %H hour (00..23) - %I hour (01..12) - %j day of year (001..366) - %k hour, space padded ( 0..23); same as %_H - %l hour, space padded ( 1..12); same as %_I - %m month (01..12) - %M minute (00..59) - %n a newline - %N nanoseconds (000000000..999999999) - %p locale's equivalent of either AM or PM; blank if not known - %P like %p, but lower case - %q quarter of year (1..4) - %r locale's 12-hour clock time (e.g., 11:11:04 PM) - %R 24-hour hour and minute; same as %H:%M - %s seconds since 1970-01-01 00:00:00 UTC - %S second (00..60) - %t a tab - %T time; same as %H:%M:%S - %u day of week (1..7); 1 is Monday - %U week number of year, with Sunday as first day of week (00..53) - %V ISO week number, with Monday as first day of week (01..53) - %w day of week (0..6); 0 is Sunday - %W week number of year, with Monday as first day of week (00..53) - %x locale's date representation (e.g., 12/31/99) - %X locale's time representation (e.g., 23:13:48) - %y last two digits of year (00..99) - %Y year - %z +hhmm numeric time zone (e.g., -0400) - %:z +hh:mm numeric time zone (e.g., -04:00) - %::z +hh:mm:ss numeric time zone (e.g., -04:00:00) - %:::z numeric time zone with : to necessary precision (e.g., -04, +05:30) - %Z alphabetic time zone abbreviation (e.g., EDT) - -By default, date pads numeric fields with zeroes. -The following optional flags may follow '%': - - - (hyphen) do not pad the field - _ (underscore) pad with spaces - 0 (zero) pad with zeros - ^ use upper case if possible - # use opposite case if possible - -After any flags comes an optional field width, as a decimal number; -then an optional modifier, which is either -E to use the locale's alternate representations if available, or -O to use the locale's alternate numeric symbols if available. - -Examples: -Convert seconds since the epoch (1970-01-01 UTC) to a date - $ date --date='@2147483647' - -Show the time on the west coast of the US (use tzselect(1) to find TZ) - $ TZ='America/Los_Angeles' date - -Show the local time for 9AM next Friday on the west coast of the US - $ date --date='TZ="America/Los_Angeles" 09:00 next Fri' -``` diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index 8ff6283c50b..7bd64839c62 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -9,7 +9,7 @@ // spell-checker:ignore (chrono) Datelike Timelike ; (format) DATEFILE MMDDhhmm ; (vars) datetime datetimes use chrono::format::{Item, StrftimeItems}; -use chrono::{DateTime, FixedOffset, Local, Offset, Utc}; +use chrono::{DateTime, Duration, FixedOffset, Local, Offset, Utc}; #[cfg(windows)] use chrono::{Datelike, Timelike}; use clap::{crate_version, Arg, ArgAction, Command}; @@ -19,7 +19,7 @@ use std::fs::File; use std::io::{BufRead, BufReader}; use std::path::PathBuf; use uucore::display::Quotable; -#[cfg(not(any(target_os = "macos", target_os = "redox")))] +#[cfg(not(any(target_os = "redox")))] use uucore::error::FromIo; use uucore::error::{UResult, USimpleError}; use uucore::{format_usage, help_about, help_usage, show}; @@ -96,6 +96,7 @@ enum DateSource { Now, Custom(String), File(PathBuf), + Human(Duration), } enum Iso8601Format { @@ -139,6 +140,7 @@ impl<'a> From<&'a str> for Rfc3339Format { } #[uucore::main] +#[allow(clippy::cognitive_complexity)] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().try_get_matches_from(args)?; @@ -168,7 +170,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { }; let date_source = if let Some(date) = matches.get_one::(OPT_DATE) { - DateSource::Custom(date.into()) + if let Ok(duration) = parse_datetime::from_str(date.as_str()) { + DateSource::Human(duration) + } else { + DateSource::Custom(date.into()) + } } else if let Some(file) = matches.get_one::(OPT_FILE) { DateSource::File(file.into()) } else { @@ -219,19 +225,25 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let iter = std::iter::once(date); Box::new(iter) } - DateSource::File(ref path) => match File::open(path) { - Ok(file) => { - let lines = BufReader::new(file).lines(); - let iter = lines.filter_map(Result::ok).map(parse_date); - Box::new(iter) - } - Err(_err) => { + DateSource::Human(relative_time) => { + // Get the current DateTime for things like "1 year ago" + let current_time = DateTime::::from(Local::now()); + let iter = std::iter::once(Ok(current_time + relative_time)); + Box::new(iter) + } + DateSource::File(ref path) => { + if path.is_dir() { return Err(USimpleError::new( 2, - format!("{}: No such file or directory", path.display()), + format!("expected file, got directory {}", path.quote()), )); } - }, + let file = File::open(path) + .map_err_context(|| path.as_os_str().to_string_lossy().to_string())?; + let lines = BufReader::new(file).lines(); + let iter = lines.map_while(Result::ok).map(parse_date); + Box::new(iter) + } DateSource::Now => { let iter = std::iter::once(Ok(now)); Box::new(iter) @@ -246,6 +258,13 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { Ok(date) => { // GNU `date` uses `%N` for nano seconds, however crate::chrono uses `%f` let format_string = &format_string.replace("%N", "%f"); + // Refuse to pass this string to chrono as it is crashing in this crate + if format_string.contains("%#z") { + return Err(USimpleError::new( + 1, + format!("invalid format {}", format_string.replace("%f", "%N")), + )); + } // Hack to work around panic in chrono, // TODO - remove when a fix for https://github.com/chronotope/chrono/issues/623 is released let format_items = StrftimeItems::new(format_string); @@ -414,10 +433,10 @@ fn set_system_datetime(date: DateTime) -> UResult<()> { let result = unsafe { clock_settime(CLOCK_REALTIME, ×pec) }; - if result != 0 { - Err(std::io::Error::last_os_error().map_err_context(|| "cannot set date".to_string())) - } else { + if result == 0 { Ok(()) + } else { + Err(std::io::Error::last_os_error().map_err_context(|| "cannot set date".to_string())) } } diff --git a/src/uu/dd/Cargo.toml b/src/uu/dd/Cargo.toml index e7feaaeddf9..0ac25657eff 100644 --- a/src/uu/dd/Cargo.toml +++ b/src/uu/dd/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_dd" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "dd ~ (uutils) copy and convert files" @@ -15,13 +15,16 @@ edition = "2021" path = "src/dd.rs" [dependencies] -clap = { workspace=true } -gcd = { workspace=true } -libc = { workspace=true } -uucore = { workspace=true, features=["memo"] } +clap = { workspace = true } +gcd = { workspace = true } +libc = { workspace = true } +uucore = { workspace = true, features = ["memo"] } + +[target.'cfg(any(target_os = "linux"))'.dependencies] +nix = { workspace = true, features = ["fs"] } [target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies] -signal-hook = { workspace=true } +signal-hook = { workspace = true } [[bin]] name = "dd" diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index 00be1558c81..4e1287939d5 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -5,7 +5,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore fname, tname, fpath, specfile, testfile, unspec, ifile, ofile, outfile, fullblock, urand, fileio, atoe, atoibm, behaviour, bmax, bremain, cflags, creat, ctable, ctty, datastructures, doesnt, etoa, fileout, fname, gnudd, iconvflags, iseek, nocache, noctty, noerror, nofollow, nolinks, nonblock, oconvflags, oseek, outfile, parseargs, rlen, rmax, rremain, rsofar, rstat, sigusr, wlen, wstat seekable oconv canonicalized +// spell-checker:ignore fname, tname, fpath, specfile, testfile, unspec, ifile, ofile, outfile, fullblock, urand, fileio, atoe, atoibm, behaviour, bmax, bremain, cflags, creat, ctable, ctty, datastructures, doesnt, etoa, fileout, fname, gnudd, iconvflags, iseek, nocache, noctty, noerror, nofollow, nolinks, nonblock, oconvflags, oseek, outfile, parseargs, rlen, rmax, rremain, rsofar, rstat, sigusr, wlen, wstat seekable oconv canonicalized fadvise Fadvise FADV DONTNEED ESPIPE mod datastructures; use datastructures::*; @@ -36,15 +36,25 @@ use std::os::unix::{ io::{AsRawFd, FromRawFd}, }; use std::path::Path; -use std::sync::mpsc; +use std::sync::{ + atomic::{AtomicBool, Ordering::Relaxed}, + mpsc, Arc, +}; use std::thread; -use std::time; +use std::time::{Duration, Instant}; use clap::{crate_version, Arg, Command}; use gcd::Gcd; +#[cfg(target_os = "linux")] +use nix::{ + errno::Errno, + fcntl::{posix_fadvise, PosixFadviseAdvice}, +}; use uucore::display::Quotable; use uucore::error::{FromIo, UResult}; use uucore::{format_usage, help_about, help_section, help_usage, show_error}; +#[cfg(target_os = "linux")] +use uucore::{show, show_if_err}; const ABOUT: &str = help_about!("dd.md"); const AFTER_HELP: &str = help_section!("after help", "dd.md"); @@ -68,6 +78,45 @@ struct Settings { status: Option, } +/// A timer which triggers on a given interval +/// +/// After being constructed with [`Alarm::with_interval`], [`Alarm::is_triggered`] +/// will return true once per the given [`Duration`]. +/// +/// Can be cloned, but the trigger status is shared across all instances so only +/// the first caller each interval will yield true. +/// +/// When all instances are dropped the background thread will exit on the next interval. +#[derive(Debug, Clone)] +pub struct Alarm { + interval: Duration, + trigger: Arc, +} + +impl Alarm { + pub fn with_interval(interval: Duration) -> Self { + let trigger = Arc::new(AtomicBool::default()); + + let weak_trigger = Arc::downgrade(&trigger); + thread::spawn(move || { + while let Some(trigger) = weak_trigger.upgrade() { + thread::sleep(interval); + trigger.store(true, Relaxed); + } + }); + + Self { interval, trigger } + } + + pub fn is_triggered(&self) -> bool { + self.trigger.swap(false, Relaxed) + } + + pub fn get_interval(&self) -> Duration { + self.interval + } +} + /// A number in blocks or bytes /// /// Some values (seek, skip, iseek, oseek) can have values either in blocks or in bytes. @@ -131,6 +180,16 @@ impl Source { Self::StdinFile(f) } + /// The length of the data source in number of bytes. + /// + /// If it cannot be determined, then this function returns 0. + fn len(&self) -> std::io::Result { + match self { + Self::File(f) => Ok(f.metadata()?.len().try_into().unwrap_or(i64::MAX)), + _ => Ok(0), + } + } + fn skip(&mut self, n: u64) -> io::Result { match self { #[cfg(not(unix))] @@ -156,6 +215,23 @@ impl Source { Self::Fifo(f) => io::copy(&mut f.take(n), &mut io::sink()), } } + + /// Discard the system file cache for the given portion of the data source. + /// + /// `offset` and `len` specify a contiguous portion of the data + /// source. This function informs the kernel that the specified + /// portion of the source is no longer needed. If not possible, + /// then this function returns an error. + #[cfg(target_os = "linux")] + fn discard_cache(&self, offset: libc::off_t, len: libc::off_t) -> nix::Result<()> { + match self { + Self::File(f) => { + let advice = PosixFadviseAdvice::POSIX_FADV_DONTNEED; + posix_fadvise(f.as_raw_fd(), offset, len, advice) + } + _ => Err(Errno::ESPIPE), // "Illegal seek" + } + } } impl Read for Source { @@ -265,10 +341,10 @@ fn make_linux_iflags(iflags: &IFlags) -> Option { flag |= libc::O_SYNC; } - if flag != 0 { - Some(flag) - } else { + if flag == 0 { None + } else { + Some(flag) } } @@ -296,6 +372,29 @@ impl<'a> Read for Input<'a> { } impl<'a> Input<'a> { + /// Discard the system file cache for the given portion of the input. + /// + /// `offset` and `len` specify a contiguous portion of the input. + /// This function informs the kernel that the specified portion of + /// the input file is no longer needed. If not possible, then this + /// function prints an error message to stderr and sets the exit + /// status code to 1. + #[allow(unused_variables)] + fn discard_cache(&self, offset: libc::off_t, len: libc::off_t) { + #[cfg(target_os = "linux")] + { + show_if_err!(self + .src + .discard_cache(offset, len) + .map_err_context(|| "failed to discard cache for: 'standard input'".to_string())); + } + #[cfg(not(target_os = "linux"))] + { + // TODO Is there a way to discard filesystem cache on + // these other operating systems? + } + } + /// Fills a given buffer. /// Reads in increments of 'self.ibs'. /// The start of each ibs-sized read follows the previous one. @@ -317,13 +416,13 @@ impl<'a> Input<'a> { _ => break, } } - buf.truncate(bytes_total); Ok(ReadStat { reads_complete, reads_partial, // Records are not truncated when filling. records_truncated: 0, + bytes_total: bytes_total.try_into().unwrap(), }) } @@ -334,6 +433,7 @@ impl<'a> Input<'a> { let mut reads_complete = 0; let mut reads_partial = 0; let mut base_idx = 0; + let mut bytes_total = 0; while base_idx < buf.len() { let next_blk = cmp::min(base_idx + self.settings.ibs, buf.len()); @@ -342,11 +442,13 @@ impl<'a> Input<'a> { match self.read(&mut buf[base_idx..next_blk])? { 0 => break, rlen if rlen < target_len => { + bytes_total += rlen; reads_partial += 1; let padding = vec![pad; target_len - rlen]; buf.splice(base_idx + rlen..next_blk, padding.into_iter()); } - _ => { + rlen => { + bytes_total += rlen; reads_complete += 1; } } @@ -359,6 +461,7 @@ impl<'a> Input<'a> { reads_complete, reads_partial, records_truncated: 0, + bytes_total: bytes_total.try_into().unwrap(), }) } } @@ -447,6 +550,33 @@ impl Dest { _ => Ok(()), } } + + /// Discard the system file cache for the given portion of the destination. + /// + /// `offset` and `len` specify a contiguous portion of the + /// destination. This function informs the kernel that the + /// specified portion of the destination is no longer needed. If + /// not possible, then this function returns an error. + #[cfg(target_os = "linux")] + fn discard_cache(&self, offset: libc::off_t, len: libc::off_t) -> nix::Result<()> { + match self { + Self::File(f, _) => { + let advice = PosixFadviseAdvice::POSIX_FADV_DONTNEED; + posix_fadvise(f.as_raw_fd(), offset, len, advice) + } + _ => Err(Errno::ESPIPE), // "Illegal seek" + } + } + + /// The length of the data destination in number of bytes. + /// + /// If it cannot be determined, then this function returns 0. + fn len(&self) -> std::io::Result { + match self { + Self::File(f, _) => Ok(f.metadata()?.len().try_into().unwrap_or(i64::MAX)), + _ => Ok(0), + } + } } /// Decide whether the given buffer is all zeros. @@ -580,6 +710,29 @@ impl<'a> Output<'a> { Ok(Self { dst, settings }) } + /// Discard the system file cache for the given portion of the output. + /// + /// `offset` and `len` specify a contiguous portion of the output. + /// This function informs the kernel that the specified portion of + /// the output file is no longer needed. If not possible, then + /// this function prints an error message to stderr and sets the + /// exit status code to 1. + #[allow(unused_variables)] + fn discard_cache(&self, offset: libc::off_t, len: libc::off_t) { + #[cfg(target_os = "linux")] + { + show_if_err!(self + .dst + .discard_cache(offset, len) + .map_err_context(|| "failed to discard cache for: 'standard output'".to_string())); + } + #[cfg(target_os = "linux")] + { + // TODO Is there a way to discard filesystem cache on + // these other operating systems? + } + } + /// Write the given bytes one block at a time. /// /// This may write partial blocks (for example, if the underlying @@ -649,7 +802,7 @@ fn dd_copy(mut i: Input, mut o: Output) -> std::io::Result<()> { // of its report includes the throughput in bytes per second, // which requires knowing how long the process has been // running. - let start = time::Instant::now(); + let start = Instant::now(); // A good buffer size for reading. // @@ -669,11 +822,31 @@ fn dd_copy(mut i: Input, mut o: Output) -> std::io::Result<()> { // information. let (prog_tx, rx) = mpsc::channel(); let output_thread = thread::spawn(gen_prog_updater(rx, i.settings.status)); - let mut progress_as_secs = 0; // Optimization: if no blocks are to be written, then don't // bother allocating any buffers. if let Some(Num::Blocks(0) | Num::Bytes(0)) = i.settings.count { + // Even though we are not reading anything from the input + // file, we still need to honor the `nocache` flag, which + // requests that we inform the system that we no longer + // need the contents of the input file in a system cache. + // + // TODO Better error handling for overflowing `len`. + if i.settings.iflags.nocache { + let offset = 0; + #[allow(clippy::useless_conversion)] + let len = i.src.len()?.try_into().unwrap(); + i.discard_cache(offset, len); + } + // Similarly, discard the system cache for the output file. + // + // TODO Better error handling for overflowing `len`. + if i.settings.oflags.nocache { + let offset = 0; + #[allow(clippy::useless_conversion)] + let len = o.dst.len()?.try_into().unwrap(); + o.discard_cache(offset, len); + } return finalize(&mut o, rstat, wstat, start, &prog_tx, output_thread); }; @@ -681,6 +854,19 @@ fn dd_copy(mut i: Input, mut o: Output) -> std::io::Result<()> { // This is the max size needed. let mut buf = vec![BUF_INIT_BYTE; bsize]; + // Spawn a timer thread to provide a scheduled signal indicating when we + // should send an update of our progress to the reporting thread. + // + // This avoids the need to query the OS monotonic clock for every block. + let alarm = Alarm::with_interval(Duration::from_secs(1)); + + // Index in the input file where we are reading bytes and in + // the output file where we are writing bytes. + // + // These are updated on each iteration of the main loop. + let mut read_offset = 0; + let mut write_offset = 0; + // The main read/write loop. // // Each iteration reads blocks from the input and writes @@ -700,6 +886,30 @@ fn dd_copy(mut i: Input, mut o: Output) -> std::io::Result<()> { } let wstat_update = o.write_blocks(&buf)?; + // Discard the system file cache for the read portion of + // the input file. + // + // TODO Better error handling for overflowing `offset` and `len`. + let read_len = rstat_update.bytes_total; + if i.settings.iflags.nocache { + let offset = read_offset.try_into().unwrap(); + let len = read_len.try_into().unwrap(); + i.discard_cache(offset, len); + } + read_offset += read_len; + + // Discard the system file cache for the written portion + // of the output file. + // + // TODO Better error handling for overflowing `offset` and `len`. + let write_len = wstat_update.bytes_total; + if o.settings.oflags.nocache { + let offset = write_offset.try_into().unwrap(); + let len = write_len.try_into().unwrap(); + o.discard_cache(offset, len); + } + write_offset += write_len; + // Update the read/write stats and inform the progress thread once per second. // // If the receiver is disconnected, `send()` returns an @@ -708,9 +918,8 @@ fn dd_copy(mut i: Input, mut o: Output) -> std::io::Result<()> { // error. rstat += rstat_update; wstat += wstat_update; - let prog_update = ProgUpdate::new(rstat, wstat, start.elapsed(), false); - if prog_update.duration.as_secs() >= progress_as_secs { - progress_as_secs = prog_update.duration.as_secs() + 1; + if alarm.is_triggered() { + let prog_update = ProgUpdate::new(rstat, wstat, start.elapsed(), false); prog_tx.send(prog_update).unwrap_or(()); } } @@ -722,7 +931,7 @@ fn finalize( output: &mut Output, rstat: ReadStat, wstat: WriteStat, - start: time::Instant, + start: Instant, prog_tx: &mpsc::Sender, output_thread: thread::JoinHandle, ) -> std::io::Result<()> { @@ -752,6 +961,7 @@ fn finalize( } #[cfg(any(target_os = "linux", target_os = "android"))] +#[allow(clippy::cognitive_complexity)] fn make_linux_oflags(oflags: &OFlags) -> Option { let mut flag = 0; @@ -784,10 +994,10 @@ fn make_linux_oflags(oflags: &OFlags) -> Option { flag |= libc::O_SYNC; } - if flag != 0 { - Some(flag) - } else { + if flag == 0 { None + } else { + Some(flag) } } diff --git a/src/uu/dd/src/parseargs.rs b/src/uu/dd/src/parseargs.rs index 96f6ebfaa6c..20a8da1ee7b 100644 --- a/src/uu/dd/src/parseargs.rs +++ b/src/uu/dd/src/parseargs.rs @@ -293,8 +293,9 @@ impl Parser { } } + #[allow(clippy::cognitive_complexity)] fn parse_input_flags(&mut self, val: &str) -> Result<(), ParseError> { - let mut i = &mut self.iflag; + let i = &mut self.iflag; for f in val.split(',') { match f { // Common flags @@ -303,7 +304,7 @@ impl Parser { "directory" => linux_only!(f, i.directory = true), "dsync" => linux_only!(f, i.dsync = true), "sync" => linux_only!(f, i.sync = true), - "nocache" => return Err(ParseError::Unimplemented(f.to_string())), + "nocache" => linux_only!(f, i.nocache = true), "nonblock" => linux_only!(f, i.nonblock = true), "noatime" => linux_only!(f, i.noatime = true), "noctty" => linux_only!(f, i.noctty = true), @@ -324,8 +325,9 @@ impl Parser { Ok(()) } + #[allow(clippy::cognitive_complexity)] fn parse_output_flags(&mut self, val: &str) -> Result<(), ParseError> { - let mut o = &mut self.oflag; + let o = &mut self.oflag; for f in val.split(',') { match f { // Common flags @@ -334,7 +336,7 @@ impl Parser { "directory" => linux_only!(f, o.directory = true), "dsync" => linux_only!(f, o.dsync = true), "sync" => linux_only!(f, o.sync = true), - "nocache" => return Err(ParseError::Unimplemented(f.to_string())), + "nocache" => linux_only!(f, o.nocache = true), "nonblock" => linux_only!(f, o.nonblock = true), "noatime" => linux_only!(f, o.noatime = true), "noctty" => linux_only!(f, o.noctty = true), @@ -355,7 +357,7 @@ impl Parser { } fn parse_conv_flags(&mut self, val: &str) -> Result<(), ParseError> { - let mut c = &mut self.conv; + let c = &mut self.conv; for f in val.split(',') { match f { // Conversion diff --git a/src/uu/dd/src/parseargs/unit_tests.rs b/src/uu/dd/src/parseargs/unit_tests.rs index a135c3572da..54e17b882e2 100644 --- a/src/uu/dd/src/parseargs/unit_tests.rs +++ b/src/uu/dd/src/parseargs/unit_tests.rs @@ -55,7 +55,7 @@ fn unimplemented_flags_should_error() { let mut succeeded = Vec::new(); // The following flags are not implemented - for flag in ["cio", "nocache", "nolinks", "text", "binary"] { + for flag in ["cio", "nolinks", "text", "binary"] { let args = vec![format!("iflag={flag}")]; if Parser::new() diff --git a/src/uu/dd/src/progress.rs b/src/uu/dd/src/progress.rs index 51cfa92efd2..65af053b841 100644 --- a/src/uu/dd/src/progress.rs +++ b/src/uu/dd/src/progress.rs @@ -79,9 +79,9 @@ impl ProgUpdate { /// ```rust,ignore /// use std::io::Cursor; /// use std::time::Duration; - /// use crate::progress::{ProgUpdate, ReadState, WriteStat}; + /// use crate::progress::{ProgUpdate, ReadStat, WriteStat}; /// - /// let read_stat = ReadStat::new(1, 2, 3); + /// let read_stat = ReadStat::new(1, 2, 3, 999); /// let write_stat = WriteStat::new(4, 5, 6); /// let duration = Duration::new(789, 0); /// let prog_update = ProgUpdate { @@ -121,7 +121,7 @@ impl ProgUpdate { /// ```rust,ignore /// use std::io::Cursor; /// use std::time::Duration; - /// use crate::progress::{ProgUpdate, ReadState, WriteStat}; + /// use crate::progress::ProgUpdate; /// /// let prog_update = ProgUpdate { /// read_stat: Default::default(), @@ -191,7 +191,7 @@ impl ProgUpdate { /// ```rust,ignore /// use std::io::Cursor; /// use std::time::Duration; - /// use crate::progress::{ProgUpdate, ReadState, WriteStat}; + /// use crate::progress::ProgUpdate; /// /// let prog_update = ProgUpdate { /// read_stat: Default::default(), @@ -276,16 +276,20 @@ pub(crate) struct ReadStat { /// /// A truncated record can only occur in `conv=block` mode. pub(crate) records_truncated: u32, + + /// The total number of bytes read. + pub(crate) bytes_total: u64, } impl ReadStat { /// Create a new instance. #[allow(dead_code)] - fn new(complete: u64, partial: u64, truncated: u32) -> Self { + fn new(complete: u64, partial: u64, truncated: u32, bytes_total: u64) -> Self { Self { reads_complete: complete, reads_partial: partial, records_truncated: truncated, + bytes_total, } } @@ -315,6 +319,7 @@ impl std::ops::AddAssign for ReadStat { reads_complete: self.reads_complete + other.reads_complete, reads_partial: self.reads_partial + other.reads_partial, records_truncated: self.records_truncated + other.records_truncated, + bytes_total: self.bytes_total + other.bytes_total, } } } @@ -514,7 +519,7 @@ mod tests { #[test] fn test_read_stat_report() { - let read_stat = ReadStat::new(1, 2, 3); + let read_stat = ReadStat::new(1, 2, 3, 4); let mut cursor = Cursor::new(vec![]); read_stat.report(&mut cursor).unwrap(); assert_eq!(cursor.get_ref(), b"1+2 records in\n"); @@ -530,7 +535,7 @@ mod tests { #[test] fn test_prog_update_write_io_lines() { - let read_stat = ReadStat::new(1, 2, 3); + let read_stat = ReadStat::new(1, 2, 3, 4); let write_stat = WriteStat::new(4, 5, 6); let duration = Duration::new(789, 0); let complete = false; diff --git a/src/uu/df/Cargo.toml b/src/uu/df/Cargo.toml index 83ae2ae146b..2be3967afbe 100644 --- a/src/uu/df/Cargo.toml +++ b/src/uu/df/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_df" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "df ~ (uutils) display file system information" @@ -15,9 +15,12 @@ edition = "2021" path = "src/df.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true, features=["libc", "fsext"] } -unicode-width = { workspace=true } +clap = { workspace = true } +uucore = { workspace = true, features = ["libc", "fsext"] } +unicode-width = { workspace = true } + +[dev-dependencies] +tempfile = "3" [[bin]] name = "df" diff --git a/src/uu/df/src/filesystem.rs b/src/uu/df/src/filesystem.rs index aac63ac9dee..813846a6c76 100644 --- a/src/uu/df/src/filesystem.rs +++ b/src/uu/df/src/filesystem.rs @@ -42,7 +42,7 @@ pub(crate) struct Filesystem { /// This function returns the element of `mounts` on which `path` is /// mounted. If there are no matches, this function returns /// [`None`]. If there are two or more matches, then the single -/// [`MountInfo`] with the longest mount directory is returned. +/// [`MountInfo`] with the device name corresponding to the entered path. /// /// If `canonicalize` is `true`, then the `path` is canonicalized /// before checking whether it matches any mount directories. @@ -68,9 +68,19 @@ where path.as_ref().to_path_buf() }; + // Find the potential mount point that matches entered path let maybe_mount_point = mounts .iter() - .find(|mi| mi.dev_name.eq(&path.to_string_lossy())); + // Create pair MountInfo, canonicalized device name + // TODO Abstract from accessing real filesystem to + // make code more testable + .map(|m| (m, std::fs::canonicalize(&m.dev_name))) + // Ignore non existing paths + .filter(|m| m.1.is_ok()) + .map(|m| (m.0, m.1.ok().unwrap())) + // Try to find canonicalized device name corresponding to entered path + .find(|m| m.1.eq(&path)) + .map(|m| m.0); maybe_mount_point.or_else(|| { mounts @@ -83,9 +93,7 @@ where impl Filesystem { // TODO: resolve uuid in `mount_info.dev_name` if exists pub(crate) fn new(mount_info: MountInfo, file: Option) -> Option { - let _stat_path = if !mount_info.mount_dir.is_empty() { - mount_info.mount_dir.clone() - } else { + let _stat_path = if mount_info.mount_dir.is_empty() { #[cfg(unix)] { mount_info.dev_name.clone() @@ -95,6 +103,8 @@ impl Filesystem { // On windows, we expect the volume id mount_info.dev_id.clone() } + } else { + mount_info.mount_dir.clone() }; #[cfg(unix)] let usage = FsUsage::new(statfs(_stat_path).ok()?); @@ -211,10 +221,16 @@ mod tests { #[test] fn test_dev_name_match() { + let tmp = tempfile::TempDir::new().expect("Failed to create temp dir"); + let dev_name = std::fs::canonicalize(tmp.path()) + .expect("Failed to canonicalize tmp path") + .to_string_lossy() + .to_string(); + let mut mount_info = mount_info("/foo"); - mount_info.dev_name = "/dev/sda2".to_string(); + mount_info.dev_name = dev_name.clone(); let mounts = [mount_info]; - let actual = mount_info_from_path(&mounts, "/dev/sda2", false).unwrap(); + let actual = mount_info_from_path(&mounts, dev_name, false).unwrap(); assert!(mount_info_eq(actual, &mounts[0])); } } diff --git a/src/uu/dir/Cargo.toml b/src/uu/dir/Cargo.toml index 03235fdfcbc..d61b041dcfe 100644 --- a/src/uu/dir/Cargo.toml +++ b/src/uu/dir/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_dir" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "shortcut to ls -C -b" @@ -15,9 +15,9 @@ edition = "2021" path = "src/dir.rs" [dependencies] -clap = { workspace=true, features = ["env"] } -uucore = { workspace=true, features=["entries", "fs"] } -uu_ls = { workspace=true } +clap = { workspace = true, features = ["env"] } +uucore = { workspace = true, features = ["entries", "fs"] } +uu_ls = { workspace = true } [[bin]] name = "dir" diff --git a/src/uu/dircolors/Cargo.toml b/src/uu/dircolors/Cargo.toml index 9a3479426bc..952e7ad3f74 100644 --- a/src/uu/dircolors/Cargo.toml +++ b/src/uu/dircolors/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_dircolors" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "dircolors ~ (uutils) display commands to set LS_COLORS" @@ -15,8 +15,8 @@ edition = "2021" path = "src/dircolors.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true } +clap = { workspace = true } +uucore = { workspace = true } [[bin]] name = "dircolors" diff --git a/src/uu/dircolors/src/dircolors.rs b/src/uu/dircolors/src/dircolors.rs index abab966afc5..19c3f7e31c1 100644 --- a/src/uu/dircolors/src/dircolors.rs +++ b/src/uu/dircolors/src/dircolors.rs @@ -12,6 +12,7 @@ use std::borrow::Borrow; use std::env; use std::fs::File; use std::io::{BufRead, BufReader}; +use std::path::Path; use clap::{crate_version, Arg, ArgAction, Command}; use uucore::display::Quotable; @@ -42,7 +43,6 @@ pub enum OutputFmt { } pub fn guess_syntax() -> OutputFmt { - use std::path::Path; match env::var("SHELL") { Ok(ref s) if !s.is_empty() => { let shell_path: &Path = s.as_ref(); @@ -136,17 +136,28 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { )); } else if files[0].eq("-") { let fin = BufReader::new(std::io::stdin()); - result = parse(fin.lines().filter_map(Result::ok), &out_format, files[0]); + result = parse(fin.lines().map_while(Result::ok), &out_format, files[0]); } else { - match File::open(files[0]) { + let path = Path::new(files[0]); + if path.is_dir() { + return Err(USimpleError::new( + 2, + format!("expected file, got directory {}", path.quote()), + )); + } + match File::open(path) { Ok(f) => { let fin = BufReader::new(f); - result = parse(fin.lines().filter_map(Result::ok), &out_format, files[0]); + result = parse( + fin.lines().map_while(Result::ok), + &out_format, + &path.to_string_lossy(), + ); } Err(e) => { return Err(USimpleError::new( 1, - format!("{}: {}", files[0].maybe_quote(), e), + format!("{}: {}", path.maybe_quote(), e), )); } } @@ -273,6 +284,7 @@ enum ParseState { use std::collections::HashMap; use uucore::{format_usage, parse_glob}; +#[allow(clippy::cognitive_complexity)] fn parse(lines: T, fmt: &OutputFmt, fp: &str) -> Result where T: IntoIterator, diff --git a/src/uu/dirname/Cargo.toml b/src/uu/dirname/Cargo.toml index 1186a4697fa..46dec707df3 100644 --- a/src/uu/dirname/Cargo.toml +++ b/src/uu/dirname/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_dirname" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "dirname ~ (uutils) display parent directory of PATHNAME" @@ -15,8 +15,8 @@ edition = "2021" path = "src/dirname.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true } +clap = { workspace = true } +uucore = { workspace = true } [[bin]] name = "dirname" diff --git a/src/uu/dirname/src/dirname.rs b/src/uu/dirname/src/dirname.rs index 687aaa55001..cecd6aec8b9 100644 --- a/src/uu/dirname/src/dirname.rs +++ b/src/uu/dirname/src/dirname.rs @@ -38,7 +38,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .map(|s| s.to_owned()) .collect(); - if !dirnames.is_empty() { + if dirnames.is_empty() { + return Err(UUsageError::new(1, "missing operand")); + } else { for path in &dirnames { let p = Path::new(path); match p.parent() { @@ -59,8 +61,6 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } print!("{separator}"); } - } else { - return Err(UUsageError::new(1, "missing operand")); } Ok(()) diff --git a/src/uu/du/Cargo.toml b/src/uu/du/Cargo.toml index a8e8aead510..693e7c81cda 100644 --- a/src/uu/du/Cargo.toml +++ b/src/uu/du/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_du" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "du ~ (uutils) display disk usage" @@ -15,14 +15,17 @@ edition = "2021" path = "src/du.rs" [dependencies] -chrono = { workspace=true } +chrono = { workspace = true } # For the --exclude & --exclude-from options -glob = { workspace=true } -clap = { workspace=true } -uucore = { workspace=true } +glob = { workspace = true } +clap = { workspace = true } +uucore = { workspace = true } [target.'cfg(target_os = "windows")'.dependencies] -windows-sys = { workspace=true, features = ["Win32_Storage_FileSystem", "Win32_Foundation"] } +windows-sys = { workspace = true, features = [ + "Win32_Storage_FileSystem", + "Win32_Foundation", +] } [[bin]] name = "du" diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index cab9da22752..3325ca1f5d7 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -33,7 +33,7 @@ use std::time::{Duration, UNIX_EPOCH}; use std::{error::Error, fmt::Display}; use uucore::display::{print_verbatim, Quotable}; use uucore::error::FromIo; -use uucore::error::{UError, UResult}; +use uucore::error::{set_exit_code, UError, UResult}; use uucore::parse_glob; use uucore::parse_size::{parse_size, ParseSizeError}; use uucore::{ @@ -68,6 +68,7 @@ mod options { pub const TIME_STYLE: &str = "time-style"; pub const ONE_FILE_SYSTEM: &str = "one-file-system"; pub const DEREFERENCE: &str = "dereference"; + pub const DEREFERENCE_ARGS: &str = "dereference-args"; pub const INODES: &str = "inodes"; pub const EXCLUDE: &str = "exclude"; pub const EXCLUDE_FROM: &str = "exclude-from"; @@ -88,11 +89,18 @@ struct Options { total: bool, separate_dirs: bool, one_file_system: bool, - dereference: bool, + dereference: Deref, inodes: bool, verbose: bool, } +#[derive(PartialEq)] +enum Deref { + All, + Args(Vec), + None, +} + #[derive(PartialEq, Eq, Hash, Clone, Copy)] struct FileInfo { file_id: u128, @@ -112,13 +120,22 @@ struct Stat { } impl Stat { - fn new(path: PathBuf, options: &Options) -> Result { - let metadata = if options.dereference { - fs::metadata(&path)? - } else { - fs::symlink_metadata(&path)? + fn new(path: &Path, options: &Options) -> Result { + // Determine whether to dereference (follow) the symbolic link + let should_dereference = match &options.dereference { + Deref::All => true, + Deref::Args(paths) => paths.contains(&path.to_path_buf()), + Deref::None => false, }; + let metadata = if should_dereference { + // Get metadata, following symbolic links if necessary + fs::metadata(path) + } else { + // Get metadata without following symbolic links + fs::symlink_metadata(path) + }?; + #[cfg(not(windows))] let file_info = FileInfo { file_id: metadata.ino() as u128, @@ -126,7 +143,7 @@ impl Stat { }; #[cfg(not(windows))] return Ok(Self { - path, + path: path.to_path_buf(), is_dir: metadata.is_dir(), size: metadata.len(), blocks: metadata.blocks(), @@ -138,12 +155,12 @@ impl Stat { }); #[cfg(windows)] - let size_on_disk = get_size_on_disk(&path); + let size_on_disk = get_size_on_disk(path); #[cfg(windows)] - let file_info = get_file_info(&path); + let file_info = get_file_info(path); #[cfg(windows)] Ok(Self { - path, + path: path.to_path_buf(), is_dir: metadata.is_dir(), size: metadata.len(), blocks: size_on_disk / 1024 * 2, @@ -272,6 +289,7 @@ fn choose_size(matches: &ArgMatches, stat: &Stat) -> u64 { // this takes `my_stat` to avoid having to stat files multiple times. // XXX: this should use the impl Trait return type when it is stabilized +#[allow(clippy::cognitive_complexity)] fn du( mut my_stat: Stat, options: &Options, @@ -296,7 +314,7 @@ fn du( 'file_loop: for f in read { match f { Ok(entry) => { - match Stat::new(entry.path(), options) { + match Stat::new(&entry.path(), options) { Ok(this_stat) => { // We have an exclude list for pattern in exclude { @@ -397,6 +415,20 @@ fn convert_size_other(size: u64, _multiplier: u64, block_size: u64) -> String { format!("{}", ((size as f64) / (block_size as f64)).ceil()) } +fn get_convert_size_fn(matches: &ArgMatches) -> Box String> { + if matches.get_flag(options::HUMAN_READABLE) || matches.get_flag(options::SI) { + Box::new(convert_size_human) + } else if matches.get_flag(options::BYTES) { + Box::new(convert_size_b) + } else if matches.get_flag(options::BLOCK_SIZE_1K) { + Box::new(convert_size_k) + } else if matches.get_flag(options::BLOCK_SIZE_1M) { + Box::new(convert_size_m) + } else { + Box::new(convert_size_other) + } +} + #[derive(Debug)] enum DuError { InvalidMaxDepthArg(String), @@ -505,26 +537,33 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { summarize, )?; + let files = match matches.get_one::(options::FILE) { + Some(_) => matches + .get_many::(options::FILE) + .unwrap() + .map(PathBuf::from) + .collect(), + None => vec![PathBuf::from(".")], + }; + let options = Options { all: matches.get_flag(options::ALL), max_depth, total: matches.get_flag(options::TOTAL), separate_dirs: matches.get_flag(options::SEPARATE_DIRS), one_file_system: matches.get_flag(options::ONE_FILE_SYSTEM), - dereference: matches.get_flag(options::DEREFERENCE), + dereference: if matches.get_flag(options::DEREFERENCE) { + Deref::All + } else if matches.get_flag(options::DEREFERENCE_ARGS) { + // We don't care about the cost of cloning as it is rarely used + Deref::Args(files.clone()) + } else { + Deref::None + }, inodes: matches.get_flag(options::INODES), verbose: matches.get_flag(options::VERBOSE), }; - let files = match matches.get_one::(options::FILE) { - Some(_) => matches - .get_many::(options::FILE) - .unwrap() - .map(|s| s.as_str()) - .collect(), - None => vec!["."], - }; - if options.inodes && (matches.get_flag(options::APPARENT_SIZE) || matches.get_flag(options::BYTES)) { @@ -547,19 +586,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } else { 1024 }; - let convert_size_fn = { - if matches.get_flag(options::HUMAN_READABLE) || matches.get_flag(options::SI) { - convert_size_human - } else if matches.get_flag(options::BYTES) { - convert_size_b - } else if matches.get_flag(options::BLOCK_SIZE_1K) { - convert_size_k - } else if matches.get_flag(options::BLOCK_SIZE_1M) { - convert_size_m - } else { - convert_size_other - } - }; + + let convert_size_fn = get_convert_size_fn(&matches); + let convert_size = |size: u64| { if options.inodes { size.to_string() @@ -580,11 +609,12 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let excludes = build_exclude_patterns(&matches)?; let mut grand_total = 0; - 'loop_file: for path_string in files { + 'loop_file: for path in files { // Skip if we don't want to ignore anything if !&excludes.is_empty() { + let path_string = path.to_string_lossy(); for pattern in &excludes { - if pattern.matches(path_string) { + if pattern.matches(&path_string) { // if the directory is ignored, leave early if options.verbose { println!("{} ignored", path_string.quote()); @@ -594,9 +624,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } } - let path = PathBuf::from(&path_string); // Check existence of path provided in argument - if let Ok(stat) = Stat::new(path, &options) { + if let Ok(stat) = Stat::new(&path, &options) { // Kick off the computation of disk usage from the initial path let mut inodes: HashSet = HashSet::new(); if let Some(inode) = stat.inode { @@ -616,20 +645,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { if matches.contains_id(options::TIME) { let tm = { - let secs = { - match matches.get_one::(options::TIME) { - Some(s) => match s.as_str() { - "ctime" | "status" => stat.modified, - "access" | "atime" | "use" => stat.accessed, - "birth" | "creation" => stat - .created - .ok_or_else(|| DuError::InvalidTimeArg(s.into()))?, - // below should never happen as clap already restricts the values. - _ => unreachable!("Invalid field for --time"), - }, - None => stat.modified, - } - }; + let secs = matches + .get_one::(options::TIME) + .map(|s| get_time_secs(s, &stat)) + .transpose()? + .unwrap_or(stat.modified); DateTime::::from(UNIX_EPOCH + Duration::from_secs(secs)) }; if !summarize || index == len - 1 { @@ -652,9 +672,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } else { show_error!( "{}: {}", - path_string.maybe_quote(), + path.to_string_lossy().maybe_quote(), "No such file or directory" ); + set_exit_code(1); } } @@ -666,6 +687,19 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { Ok(()) } +fn get_time_secs(s: &str, stat: &Stat) -> std::result::Result { + let secs = match s { + "ctime" | "status" => stat.modified, + "access" | "atime" | "use" => stat.accessed, + "birth" | "creation" => stat + .created + .ok_or_else(|| DuError::InvalidTimeArg(s.into()))?, + // below should never happen as clap already restricts the values. + _ => unreachable!("Invalid field for --time"), + }; + Ok(secs) +} + fn parse_time_style(s: Option<&str>) -> UResult<&str> { match s { Some(s) => match s { @@ -785,7 +819,14 @@ pub fn uu_app() -> Command { Arg::new(options::DEREFERENCE) .short('L') .long(options::DEREFERENCE) - .help("dereference all symbolic links") + .help("follow all symbolic links") + .action(ArgAction::SetTrue) + ) + .arg( + Arg::new(options::DEREFERENCE_ARGS) + .short('D') + .long(options::DEREFERENCE_ARGS) + .help("follow only symlinks that are listed on the command line") .action(ArgAction::SetTrue) ) // .arg( diff --git a/src/uu/echo/Cargo.toml b/src/uu/echo/Cargo.toml index 0ae385cbec0..e12460a51b5 100644 --- a/src/uu/echo/Cargo.toml +++ b/src/uu/echo/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_echo" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "echo ~ (uutils) display TEXT" @@ -15,8 +15,8 @@ edition = "2021" path = "src/echo.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true } +clap = { workspace = true } +uucore = { workspace = true } [[bin]] name = "echo" diff --git a/src/uu/env/Cargo.toml b/src/uu/env/Cargo.toml index b2224c405b1..2b05cd3e7b7 100644 --- a/src/uu/env/Cargo.toml +++ b/src/uu/env/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_env" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "env ~ (uutils) set each NAME to VALUE in the environment and run COMMAND" @@ -15,12 +15,12 @@ edition = "2021" path = "src/env.rs" [dependencies] -clap = { workspace=true } -rust-ini = { workspace=true } -uucore = { workspace=true, features=["signals"]} +clap = { workspace = true } +rust-ini = { workspace = true } +uucore = { workspace = true, features = ["signals"] } [target.'cfg(unix)'.dependencies] -nix = { workspace=true, features = ["signal"] } +nix = { workspace = true, features = ["signal"] } [[bin]] diff --git a/src/uu/env/src/env.rs b/src/uu/env/src/env.rs index a29ee3b0fd5..b293bc9bf66 100644 --- a/src/uu/env/src/env.rs +++ b/src/uu/env/src/env.rs @@ -175,6 +175,7 @@ pub fn uu_app() -> Command { .arg(Arg::new("vars").action(ArgAction::Append)) } +#[allow(clippy::cognitive_complexity)] fn run_env(args: impl uucore::Args) -> UResult<()> { let app = uu_app(); let matches = app.try_get_matches_from(args).with_exit_code(125)?; @@ -299,7 +300,10 @@ fn run_env(args: impl uucore::Args) -> UResult<()> { env::set_var(name, val); } - if !opts.program.is_empty() { + if opts.program.is_empty() { + // no program provided, so just dump all env vars to stdout + print_env(opts.null); + } else { // we need to execute a command let (prog, args) = build_command(&mut opts.program); @@ -344,9 +348,6 @@ fn run_env(args: impl uucore::Args) -> UResult<()> { Err(_) => return Err(126.into()), Ok(_) => (), } - } else { - // no program provided, so just dump all env vars to stdout - print_env(opts.null); } Ok(()) diff --git a/src/uu/expand/Cargo.toml b/src/uu/expand/Cargo.toml index 362ed5a3a84..dd0b769827f 100644 --- a/src/uu/expand/Cargo.toml +++ b/src/uu/expand/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_expand" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "expand ~ (uutils) convert input tabs to spaces" @@ -15,9 +15,9 @@ edition = "2021" path = "src/expand.rs" [dependencies] -clap = { workspace=true } -unicode-width = { workspace=true } -uucore = { workspace=true } +clap = { workspace = true } +unicode-width = { workspace = true } +uucore = { workspace = true } [[bin]] name = "expand" diff --git a/src/uu/expand/src/expand.rs b/src/uu/expand/src/expand.rs index 40928aeaf35..98b292771ea 100644 --- a/src/uu/expand/src/expand.rs +++ b/src/uu/expand/src/expand.rs @@ -373,6 +373,7 @@ enum CharType { Other, } +#[allow(clippy::cognitive_complexity)] fn expand(options: &Options) -> std::io::Result<()> { use self::CharType::*; diff --git a/src/uu/expr/Cargo.toml b/src/uu/expr/Cargo.toml index 4ee2b5b1a45..68224ee45c0 100644 --- a/src/uu/expr/Cargo.toml +++ b/src/uu/expr/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_expr" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "expr ~ (uutils) display the value of EXPRESSION" @@ -15,11 +15,11 @@ edition = "2021" path = "src/expr.rs" [dependencies] -clap = { workspace=true } -num-bigint = { workspace=true } -num-traits = { workspace=true } -onig = { workspace=true } -uucore = { workspace=true } +clap = { workspace = true } +num-bigint = { workspace = true } +num-traits = { workspace = true } +onig = { workspace = true } +uucore = { workspace = true } [[bin]] name = "expr" diff --git a/src/uu/expr/src/syntax_tree.rs b/src/uu/expr/src/syntax_tree.rs index 936760f279a..1d2ddbced49 100644 --- a/src/uu/expr/src/syntax_tree.rs +++ b/src/uu/expr/src/syntax_tree.rs @@ -185,14 +185,14 @@ pub fn tokens_to_ast( maybe_dump_rpn(&out_stack); let result = ast_from_rpn(&mut out_stack); - if !out_stack.is_empty() { + if out_stack.is_empty() { + maybe_dump_ast(&result); + result + } else { Err( "syntax error (first RPN token does not represent the root of the expression AST)" .to_owned(), ) - } else { - maybe_dump_ast(&result); - result } }) } diff --git a/src/uu/factor/Cargo.toml b/src/uu/factor/Cargo.toml index f2e6245d500..a0fc539e167 100644 --- a/src/uu/factor/Cargo.toml +++ b/src/uu/factor/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_factor" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "factor ~ (uutils) display the prime factors of each NUMBER" @@ -12,15 +12,15 @@ categories = ["command-line-utilities"] edition = "2021" [build-dependencies] -num-traits = { workspace=true } # used in src/numerics.rs, which is included by build.rs +num-traits = { workspace = true } # used in src/numerics.rs, which is included by build.rs [dependencies] -clap = { workspace=true } -coz = { workspace=true, optional = true } -num-traits = { workspace=true } -rand = { workspace=true } -smallvec = { workspace=true } -uucore = { workspace=true } +clap = { workspace = true } +coz = { workspace = true, optional = true } +num-traits = { workspace = true } +rand = { workspace = true } +smallvec = { workspace = true } +uucore = { workspace = true } [dev-dependencies] quickcheck = "1.0.3" diff --git a/src/uu/factor/factor.md b/src/uu/factor/factor.md index b5bec6039b8..ccce4f2529c 100644 --- a/src/uu/factor/factor.md +++ b/src/uu/factor/factor.md @@ -1,7 +1,7 @@ # factor ``` -factor [NUMBER]... +factor [OPTION]... [NUMBER]... ``` Print the prime factors of the given NUMBER(s). diff --git a/src/uu/factor/src/cli.rs b/src/uu/factor/src/cli.rs index 956b6aa9f0e..714b38e5efb 100644 --- a/src/uu/factor/src/cli.rs +++ b/src/uu/factor/src/cli.rs @@ -27,6 +27,8 @@ const ABOUT: &str = help_about!("factor.md"); const USAGE: &str = help_usage!("factor.md"); mod options { + pub static EXPONENTS: &str = "exponents"; + pub static HELP: &str = "help"; pub static NUMBER: &str = "NUMBER"; } @@ -34,6 +36,7 @@ fn print_factors_str( num_str: &str, w: &mut io::BufWriter, factors_buffer: &mut String, + print_exponents: bool, ) -> Result<(), Box> { num_str .trim() @@ -41,8 +44,15 @@ fn print_factors_str( .map_err(|e| e.into()) .and_then(|x| { factors_buffer.clear(); - writeln!(factors_buffer, "{}:{}", x, factor(x))?; + // If print_exponents is true, use the alternate format specifier {:#} from fmt to print the factors + // of x in the form of p^e. + if print_exponents { + writeln!(factors_buffer, "{}:{:#}", x, factor(x))?; + } else { + writeln!(factors_buffer, "{}:{}", x, factor(x))?; + } w.write_all(factors_buffer.as_bytes())?; + w.flush()?; Ok(()) }) } @@ -50,6 +60,10 @@ fn print_factors_str( #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().try_get_matches_from(args)?; + + // If matches find --exponents flag than variable print_exponents is true and p^e output format will be used. + let print_exponents = matches.get_flag(options::EXPONENTS); + let stdout = stdout(); // We use a smaller buffer here to pass a gnu test. 4KiB appears to be the default pipe size for bash. let mut w = io::BufWriter::with_capacity(4 * 1024, stdout.lock()); @@ -57,7 +71,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { if let Some(values) = matches.get_many::(options::NUMBER) { for number in values { - if let Err(e) = print_factors_str(number, &mut w, &mut factors_buffer) { + if let Err(e) = print_factors_str(number, &mut w, &mut factors_buffer, print_exponents) + { show_warning!("{}: {}", number.maybe_quote(), e); } } @@ -66,7 +81,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let lines = stdin.lock().lines(); for line in lines { for number in line.unwrap().split_whitespace() { - if let Err(e) = print_factors_str(number, &mut w, &mut factors_buffer) { + if let Err(e) = + print_factors_str(number, &mut w, &mut factors_buffer, print_exponents) + { show_warning!("{}: {}", number.maybe_quote(), e); } } @@ -86,5 +103,19 @@ pub fn uu_app() -> Command { .about(ABOUT) .override_usage(format_usage(USAGE)) .infer_long_args(true) + .disable_help_flag(true) .arg(Arg::new(options::NUMBER).action(ArgAction::Append)) + .arg( + Arg::new(options::EXPONENTS) + .short('h') + .long(options::EXPONENTS) + .help("Print factors in the form p^e") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new(options::HELP) + .long(options::HELP) + .help("Print help information.") + .action(ArgAction::Help), + ) } diff --git a/src/uu/factor/src/factor.rs b/src/uu/factor/src/factor.rs index 5d16153423e..a87f4219e75 100644 --- a/src/uu/factor/src/factor.rs +++ b/src/uu/factor/src/factor.rs @@ -97,9 +97,14 @@ impl fmt::Display for Factors { let v = &mut (self.0).borrow_mut().0; v.sort_unstable(); + let include_exponents = f.alternate(); for (p, exp) in v.iter() { - for _ in 0..*exp { - write!(f, " {p}")?; + if include_exponents && *exp > 1 { + write!(f, " {p}^{exp}")?; + } else { + for _ in 0..*exp { + write!(f, " {p}")?; + } } } diff --git a/src/uu/factor/src/numeric/montgomery.rs b/src/uu/factor/src/numeric/montgomery.rs index dd4523022de..d411d0d419c 100644 --- a/src/uu/factor/src/numeric/montgomery.rs +++ b/src/uu/factor/src/numeric/montgomery.rs @@ -82,10 +82,10 @@ impl Montgomery { // (x + n*m) / R // in case of overflow, this is (2¹²⁸ + xnm)/2⁶⁴ - n = xnm/2⁶⁴ + (2⁶⁴ - n) let y = T::from_double_width(xnm >> t_bits) - + if !overflow { - T::zero() - } else { + + if overflow { n.wrapping_neg() + } else { + T::zero() }; if y >= *n { @@ -132,10 +132,10 @@ impl Arithmetic for Montgomery { let (r, overflow) = a.overflowing_add(&b); // In case of overflow, a+b = 2⁶⁴ + r = (2⁶⁴ - n) + r (working mod n) - let r = if !overflow { - r - } else { + let r = if overflow { r + self.n.wrapping_neg() + } else { + r }; // Normalize to [0; n[ diff --git a/src/uu/false/Cargo.toml b/src/uu/false/Cargo.toml index 1bd0bc26043..88b5751cb25 100644 --- a/src/uu/false/Cargo.toml +++ b/src/uu/false/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_false" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "false ~ (uutils) do nothing and fail" @@ -15,8 +15,8 @@ edition = "2021" path = "src/false.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true } +clap = { workspace = true } +uucore = { workspace = true } [[bin]] name = "false" diff --git a/src/uu/fmt/Cargo.toml b/src/uu/fmt/Cargo.toml index f37e9468afa..0de6218b864 100644 --- a/src/uu/fmt/Cargo.toml +++ b/src/uu/fmt/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_fmt" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "fmt ~ (uutils) reformat each paragraph of input" @@ -15,9 +15,9 @@ edition = "2021" path = "src/fmt.rs" [dependencies] -clap = { workspace=true } -unicode-width = { workspace=true } -uucore = { workspace=true } +clap = { workspace = true } +unicode-width = { workspace = true } +uucore = { workspace = true } [[bin]] name = "fmt" diff --git a/src/uu/fmt/src/fmt.rs b/src/uu/fmt/src/fmt.rs index bd03b5497ad..d144bdd8a94 100644 --- a/src/uu/fmt/src/fmt.rs +++ b/src/uu/fmt/src/fmt.rs @@ -11,7 +11,7 @@ use clap::{crate_version, Arg, ArgAction, Command}; use std::cmp; use std::fs::File; use std::io::{stdin, stdout, Write}; -use std::io::{BufReader, BufWriter, Read}; +use std::io::{BufReader, BufWriter, Read, Stdout}; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError}; use uucore::{format_usage, help_about, help_usage, show_warning}; @@ -60,10 +60,17 @@ pub struct FmtOptions { goal: usize, tabwidth: usize, } - -#[uucore::main] +/// Parse the command line arguments and return the list of files and formatting options. +/// +/// # Arguments +/// +/// * `args` - Command line arguments. +/// +/// # Returns +/// +/// A tuple containing a vector of file names and a `FmtOptions` struct. #[allow(clippy::cognitive_complexity)] -pub fn uumain(args: impl uucore::Args) -> UResult<()> { +fn parse_arguments(args: impl uucore::Args) -> UResult<(Vec, FmtOptions)> { let matches = uu_app().try_get_matches_from(args)?; let mut files: Vec = matches @@ -177,39 +184,68 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { files.push("-".to_owned()); } - let mut ostream = BufWriter::new(stdout()); + Ok((files, fmt_opts)) +} - for i in files.iter().map(|x| &x[..]) { - let mut fp = match i { - "-" => BufReader::new(Box::new(stdin()) as Box), - _ => match File::open(i) { - Ok(f) => BufReader::new(Box::new(f) as Box), - Err(e) => { - show_warning!("{}: {}", i.maybe_quote(), e); - continue; - } - }, - }; - let p_stream = ParagraphStream::new(&fmt_opts, &mut fp); - for para_result in p_stream { - match para_result { - Err(s) => { - ostream - .write_all(s.as_bytes()) - .map_err_context(|| "failed to write output".to_string())?; - ostream - .write_all(b"\n") - .map_err_context(|| "failed to write output".to_string())?; - } - Ok(para) => break_lines(¶, &fmt_opts, &mut ostream) - .map_err_context(|| "failed to write output".to_string())?, +/// Process the content of a file and format it according to the provided options. +/// +/// # Arguments +/// +/// * `file_name` - The name of the file to process. A value of "-" represents the standard input. +/// * `fmt_opts` - A reference to a `FmtOptions` struct containing the formatting options. +/// * `ostream` - A mutable reference to a `BufWriter` wrapping the standard output. +/// +/// # Returns +/// +/// A `UResult<()>` indicating success or failure. +fn process_file( + file_name: &str, + fmt_opts: &FmtOptions, + ostream: &mut BufWriter, +) -> UResult<()> { + let mut fp = match file_name { + "-" => BufReader::new(Box::new(stdin()) as Box), + _ => match File::open(file_name) { + Ok(f) => BufReader::new(Box::new(f) as Box), + Err(e) => { + show_warning!("{}: {}", file_name.maybe_quote(), e); + return Ok(()); + } + }, + }; + + let p_stream = ParagraphStream::new(fmt_opts, &mut fp); + for para_result in p_stream { + match para_result { + Err(s) => { + ostream + .write_all(s.as_bytes()) + .map_err_context(|| "failed to write output".to_string())?; + ostream + .write_all(b"\n") + .map_err_context(|| "failed to write output".to_string())?; } + Ok(para) => break_lines(¶, fmt_opts, ostream) + .map_err_context(|| "failed to write output".to_string())?, } + } + + // flush the output after each file + ostream + .flush() + .map_err_context(|| "failed to write output".to_string())?; + + Ok(()) +} + +#[uucore::main] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let (files, fmt_opts) = parse_arguments(args)?; + + let mut ostream = BufWriter::new(stdout()); - // flush the output after each file - ostream - .flush() - .map_err_context(|| "failed to write output".to_string())?; + for file_name in &files { + process_file(file_name, &fmt_opts, &mut ostream)?; } Ok(()) diff --git a/src/uu/fmt/src/linebreak.rs b/src/uu/fmt/src/linebreak.rs index 75cd633bb8f..e86623c8835 100644 --- a/src/uu/fmt/src/linebreak.rs +++ b/src/uu/fmt/src/linebreak.rs @@ -223,6 +223,7 @@ struct LineBreak<'a> { fresh: bool, } +#[allow(clippy::cognitive_complexity)] fn find_kp_breakpoints<'a, T: Iterator>>( iter: T, args: &BreakArgs<'a>, diff --git a/src/uu/fmt/src/parasplit.rs b/src/uu/fmt/src/parasplit.rs index c60be0a47df..a2d70b088ed 100644 --- a/src/uu/fmt/src/parasplit.rs +++ b/src/uu/fmt/src/parasplit.rs @@ -275,6 +275,7 @@ impl<'a> ParagraphStream<'a> { impl<'a> Iterator for ParagraphStream<'a> { type Item = Result; + #[allow(clippy::cognitive_complexity)] fn next(&mut self) -> Option> { // return a NoFormatLine in an Err; it should immediately be output let noformat = match self.lines.peek() { @@ -580,11 +581,11 @@ impl<'a> Iterator for WordSplit<'a> { // points to whitespace character OR end of string let mut word_nchars = 0; self.position = match self.string[word_start..].find(|x: char| { - if !x.is_whitespace() { + if x.is_whitespace() { + true + } else { word_nchars += char_width(x); false - } else { - true } }) { None => self.length, diff --git a/src/uu/fold/Cargo.toml b/src/uu/fold/Cargo.toml index d3e4417e1ab..f0377c5ab83 100644 --- a/src/uu/fold/Cargo.toml +++ b/src/uu/fold/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_fold" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "fold ~ (uutils) wrap each line of input" @@ -15,8 +15,8 @@ edition = "2021" path = "src/fold.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true } +clap = { workspace = true } +uucore = { workspace = true } [[bin]] name = "fold" diff --git a/src/uu/fold/src/fold.rs b/src/uu/fold/src/fold.rs index 01eba0b82a6..d53573d8225 100644 --- a/src/uu/fold/src/fold.rs +++ b/src/uu/fold/src/fold.rs @@ -207,6 +207,7 @@ fn fold_file_bytewise(mut file: BufReader, spaces: bool, width: usiz /// /// If `spaces` is `true`, attempt to break lines at whitespace boundaries. #[allow(unused_assignments)] +#[allow(clippy::cognitive_complexity)] fn fold_file(mut file: BufReader, spaces: bool, width: usize) -> UResult<()> { let mut line = String::new(); let mut output = String::new(); diff --git a/src/uu/groups/Cargo.toml b/src/uu/groups/Cargo.toml index 49034a4b37b..a33b34f7616 100644 --- a/src/uu/groups/Cargo.toml +++ b/src/uu/groups/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_groups" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "groups ~ (uutils) display group memberships for USERNAME" @@ -15,8 +15,8 @@ edition = "2021" path = "src/groups.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true, features=["entries", "process"] } +clap = { workspace = true } +uucore = { workspace = true, features = ["entries", "process"] } [[bin]] name = "groups" diff --git a/src/uu/hashsum/Cargo.toml b/src/uu/hashsum/Cargo.toml index f335211b866..0a12254d04b 100644 --- a/src/uu/hashsum/Cargo.toml +++ b/src/uu/hashsum/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_hashsum" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "hashsum ~ (uutils) display or check input digests" @@ -15,11 +15,11 @@ edition = "2021" path = "src/hashsum.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true } -memchr = { workspace=true } -regex = { workspace=true } -hex = { workspace=true } +clap = { workspace = true } +uucore = { workspace = true, features = ["sum"] } +memchr = { workspace = true } +regex = { workspace = true } +hex = { workspace = true } [[bin]] name = "hashsum" diff --git a/src/uu/hashsum/src/hashsum.rs b/src/uu/hashsum/src/hashsum.rs index d7e55e56730..7b571efcd50 100644 --- a/src/uu/hashsum/src/hashsum.rs +++ b/src/uu/hashsum/src/hashsum.rs @@ -14,6 +14,7 @@ use clap::crate_version; use clap::ArgAction; use clap::{Arg, ArgMatches, Command}; use hex::encode; +use regex::Captures; use regex::Regex; use std::cmp::Ordering; use std::error::Error; @@ -47,52 +48,143 @@ struct Options { strict: bool, warn: bool, output_bits: usize, + zero: bool, } -#[allow(clippy::cognitive_complexity)] +/// Creates a Blake2b hasher instance based on the specified length argument. +/// +/// # Returns +/// +/// Returns a tuple containing the algorithm name, the hasher instance, and the output length in bits. +/// +/// # Panics +/// +/// Panics if the length is not a multiple of 8 or if it is greater than 512. +fn create_blake2b(matches: &ArgMatches) -> (&'static str, Box, usize) { + match matches.get_one::("length") { + Some(0) | None => ("BLAKE2", Box::new(Blake2b::new()) as Box, 512), + Some(length_in_bits) => { + if *length_in_bits > 512 { + crash!(1, "Invalid length (maximum digest length is 512 bits)") + } + + if length_in_bits % 8 == 0 { + let length_in_bytes = length_in_bits / 8; + ( + "BLAKE2", + Box::new(Blake2b::with_output_bytes(length_in_bytes)), + *length_in_bits, + ) + } else { + crash!(1, "Invalid length (expected a multiple of 8)") + } + } + } +} + +/// Creates a SHA3 hasher instance based on the specified bits argument. +/// +/// # Returns +/// +/// Returns a tuple containing the algorithm name, the hasher instance, and the output length in bits. +/// +/// # Panics +/// +/// Panics if an unsupported output size is provided, or if the `--bits` flag is missing. +fn create_sha3(matches: &ArgMatches) -> (&'static str, Box, usize) { + match matches.get_one::("bits") { + Some(224) => ( + "SHA3-224", + Box::new(Sha3_224::new()) as Box, + 224, + ), + Some(256) => ( + "SHA3-256", + Box::new(Sha3_256::new()) as Box, + 256, + ), + Some(384) => ( + "SHA3-384", + Box::new(Sha3_384::new()) as Box, + 384, + ), + Some(512) => ( + "SHA3-512", + Box::new(Sha3_512::new()) as Box, + 512, + ), + Some(_) => crash!( + 1, + "Invalid output size for SHA3 (expected 224, 256, 384, or 512)" + ), + None => crash!(1, "--bits required for SHA3"), + } +} + +/// Creates a SHAKE-128 hasher instance based on the specified bits argument. +/// +/// # Returns +/// +/// Returns a tuple containing the algorithm name, the hasher instance, and the output length in bits. +/// +/// # Panics +/// +/// Panics if the `--bits` flag is missing. +fn create_shake128(matches: &ArgMatches) -> (&'static str, Box, usize) { + match matches.get_one::("bits") { + Some(bits) => ( + "SHAKE128", + Box::new(Shake128::new()) as Box, + *bits, + ), + None => crash!(1, "--bits required for SHAKE-128"), + } +} + +/// Creates a SHAKE-256 hasher instance based on the specified bits argument. +/// +/// # Returns +/// +/// Returns a tuple containing the algorithm name, the hasher instance, and the output length in bits. +/// +/// # Panics +/// +/// Panics if the `--bits` flag is missing. +fn create_shake256(matches: &ArgMatches) -> (&'static str, Box, usize) { + match matches.get_one::("bits") { + Some(bits) => ( + "SHAKE256", + Box::new(Shake256::new()) as Box, + *bits, + ), + None => crash!(1, "--bits required for SHAKE-256"), + } +} + +/// Detects the hash algorithm from the program name or command-line arguments. +/// +/// # Arguments +/// +/// * `program` - A string slice containing the program name. +/// * `matches` - A reference to the `ArgMatches` object containing the command-line arguments. +/// +/// # Returns +/// +/// Returns a tuple containing the algorithm name, the hasher instance, and the output length in bits. fn detect_algo( program: &str, matches: &ArgMatches, ) -> (&'static str, Box, usize) { - let mut alg: Option> = None; - let mut name: &'static str = ""; - let mut output_bits = 0; - match program { + let (name, alg, output_bits) = match program { "md5sum" => ("MD5", Box::new(Md5::new()) as Box, 128), "sha1sum" => ("SHA1", Box::new(Sha1::new()) as Box, 160), "sha224sum" => ("SHA224", Box::new(Sha224::new()) as Box, 224), "sha256sum" => ("SHA256", Box::new(Sha256::new()) as Box, 256), "sha384sum" => ("SHA384", Box::new(Sha384::new()) as Box, 384), "sha512sum" => ("SHA512", Box::new(Sha512::new()) as Box, 512), - "b2sum" => ("BLAKE2", Box::new(Blake2b::new()) as Box, 512), + "b2sum" => create_blake2b(matches), "b3sum" => ("BLAKE3", Box::new(Blake3::new()) as Box, 256), - "sha3sum" => match matches.get_one::("bits") { - Some(224) => ( - "SHA3-224", - Box::new(Sha3_224::new()) as Box, - 224, - ), - Some(256) => ( - "SHA3-256", - Box::new(Sha3_256::new()) as Box, - 256, - ), - Some(384) => ( - "SHA3-384", - Box::new(Sha3_384::new()) as Box, - 384, - ), - Some(512) => ( - "SHA3-512", - Box::new(Sha3_512::new()) as Box, - 512, - ), - Some(_) => crash!( - 1, - "Invalid output size for SHA3 (expected 224, 256, 384, or 512)" - ), - None => crash!(1, "--bits required for SHA3"), - }, + "sha3sum" => create_sha3(matches), "sha3-224sum" => ( "SHA3-224", Box::new(Sha3_224::new()) as Box, @@ -113,114 +205,95 @@ fn detect_algo( Box::new(Sha3_512::new()) as Box, 512, ), - "shake128sum" => match matches.get_one::("bits") { - Some(bits) => ( - "SHAKE128", - Box::new(Shake128::new()) as Box, - *bits, - ), + "shake128sum" => create_shake128(matches), + "shake256sum" => create_shake256(matches), + _ => create_algorithm_from_flags(matches), + }; + (name, alg, output_bits) +} + +/// Creates a hasher instance based on the command-line flags. +/// +/// # Arguments +/// +/// * `matches` - A reference to the `ArgMatches` object containing the command-line arguments. +/// +/// # Returns +/// +/// Returns a tuple containing the algorithm name, the hasher instance, and the output length in bits. +/// +/// # Panics +/// +/// Panics if multiple hash algorithms are specified or if a required flag is missing. +#[allow(clippy::cognitive_complexity)] +fn create_algorithm_from_flags(matches: &ArgMatches) -> (&'static str, Box, usize) { + let mut alg: Option> = None; + let mut name: &'static str = ""; + let mut output_bits = 0; + let mut set_or_crash = |n, val, bits| { + if alg.is_some() { + crash!(1, "You cannot combine multiple hash algorithms!"); + }; + name = n; + alg = Some(val); + output_bits = bits; + }; + + if matches.get_flag("md5") { + set_or_crash("MD5", Box::new(Md5::new()), 128); + } + if matches.get_flag("sha1") { + set_or_crash("SHA1", Box::new(Sha1::new()), 160); + } + if matches.get_flag("sha224") { + set_or_crash("SHA224", Box::new(Sha224::new()), 224); + } + if matches.get_flag("sha256") { + set_or_crash("SHA256", Box::new(Sha256::new()), 256); + } + if matches.get_flag("sha384") { + set_or_crash("SHA384", Box::new(Sha384::new()), 384); + } + if matches.get_flag("sha512") { + set_or_crash("SHA512", Box::new(Sha512::new()), 512); + } + if matches.get_flag("b2sum") { + set_or_crash("BLAKE2", Box::new(Blake2b::new()), 512); + } + if matches.get_flag("b3sum") { + set_or_crash("BLAKE3", Box::new(Blake3::new()), 256); + } + if matches.get_flag("sha3") { + let (n, val, bits) = create_sha3(matches); + set_or_crash(n, val, bits); + } + if matches.get_flag("sha3-224") { + set_or_crash("SHA3-224", Box::new(Sha3_224::new()), 224); + } + if matches.get_flag("sha3-256") { + set_or_crash("SHA3-256", Box::new(Sha3_256::new()), 256); + } + if matches.get_flag("sha3-384") { + set_or_crash("SHA3-384", Box::new(Sha3_384::new()), 384); + } + if matches.get_flag("sha3-512") { + set_or_crash("SHA3-512", Box::new(Sha3_512::new()), 512); + } + if matches.get_flag("shake128") { + match matches.get_one::("bits") { + Some(bits) => set_or_crash("SHAKE128", Box::new(Shake128::new()), *bits), None => crash!(1, "--bits required for SHAKE-128"), - }, - "shake256sum" => match matches.get_one::("bits") { - Some(bits) => ( - "SHAKE256", - Box::new(Shake256::new()) as Box, - *bits, - ), + } + } + if matches.get_flag("shake256") { + match matches.get_one::("bits") { + Some(bits) => set_or_crash("SHAKE256", Box::new(Shake256::new()), *bits), None => crash!(1, "--bits required for SHAKE-256"), - }, - _ => { - { - let mut set_or_crash = |n, val, bits| { - if alg.is_some() { - crash!(1, "You cannot combine multiple hash algorithms!") - }; - name = n; - alg = Some(val); - output_bits = bits; - }; - if matches.get_flag("md5") { - set_or_crash("MD5", Box::new(Md5::new()), 128); - } - if matches.get_flag("sha1") { - set_or_crash("SHA1", Box::new(Sha1::new()), 160); - } - if matches.get_flag("sha224") { - set_or_crash("SHA224", Box::new(Sha224::new()), 224); - } - if matches.get_flag("sha256") { - set_or_crash("SHA256", Box::new(Sha256::new()), 256); - } - if matches.get_flag("sha384") { - set_or_crash("SHA384", Box::new(Sha384::new()), 384); - } - if matches.get_flag("sha512") { - set_or_crash("SHA512", Box::new(Sha512::new()), 512); - } - if matches.get_flag("b2sum") { - set_or_crash("BLAKE2", Box::new(Blake2b::new()), 512); - } - if matches.get_flag("b3sum") { - set_or_crash("BLAKE3", Box::new(Blake3::new()), 256); - } - if matches.get_flag("sha3") { - match matches.get_one::("bits") { - Some(224) => set_or_crash( - "SHA3-224", - Box::new(Sha3_224::new()) as Box, - 224, - ), - Some(256) => set_or_crash( - "SHA3-256", - Box::new(Sha3_256::new()) as Box, - 256, - ), - Some(384) => set_or_crash( - "SHA3-384", - Box::new(Sha3_384::new()) as Box, - 384, - ), - Some(512) => set_or_crash( - "SHA3-512", - Box::new(Sha3_512::new()) as Box, - 512, - ), - Some(_) => crash!( - 1, - "Invalid output size for SHA3 (expected 224, 256, 384, or 512)" - ), - None => crash!(1, "--bits required for SHA3"), - } - } - if matches.get_flag("sha3-224") { - set_or_crash("SHA3-224", Box::new(Sha3_224::new()), 224); - } - if matches.get_flag("sha3-256") { - set_or_crash("SHA3-256", Box::new(Sha3_256::new()), 256); - } - if matches.get_flag("sha3-384") { - set_or_crash("SHA3-384", Box::new(Sha3_384::new()), 384); - } - if matches.get_flag("sha3-512") { - set_or_crash("SHA3-512", Box::new(Sha3_512::new()), 512); - } - if matches.get_flag("shake128") { - match matches.get_one::("bits") { - Some(bits) => set_or_crash("SHAKE128", Box::new(Shake128::new()), *bits), - None => crash!(1, "--bits required for SHAKE-128"), - } - } - if matches.get_flag("shake256") { - match matches.get_one::("bits") { - Some(bits) => set_or_crash("SHAKE256", Box::new(Shake256::new()), *bits), - None => crash!(1, "--bits required for SHAKE-256"), - } - } - } - let alg = alg.unwrap_or_else(|| crash!(1, "You must specify hash algorithm!")); - (name, alg, output_bits) } } + + let alg = alg.unwrap_or_else(|| crash!(1, "You must specify hash algorithm!")); + (name, alg, output_bits) } // TODO: return custom error type @@ -269,6 +342,7 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { let quiet = matches.get_flag("quiet") || status; let strict = matches.get_flag("strict"); let warn = matches.get_flag("warn") && !status; + let zero = matches.get_flag("zero"); let opts = Options { algoname: name, @@ -282,6 +356,7 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { quiet, strict, warn, + zero, }; match matches.get_many::("FILE") { @@ -359,6 +434,13 @@ pub fn uu_app_common() -> Command { .help("warn about improperly formatted checksum lines") .action(ArgAction::SetTrue), ) + .arg( + Arg::new("zero") + .short('z') + .long("zero") + .help("end each output line with NUL, not newline") + .action(ArgAction::SetTrue), + ) .arg( Arg::new("FILE") .index(1) @@ -369,6 +451,21 @@ pub fn uu_app_common() -> Command { ) } +pub fn uu_app_length() -> Command { + uu_app_opt_length(uu_app_common()) +} + +fn uu_app_opt_length(command: Command) -> Command { + command.arg( + Arg::new("length") + .short('l') + .long("length") + .help("digest length in bits; must not exceed the max for the blake2 algorithm (512) and must be a multiple of 8") + .value_name("BITS") + .value_parser(parse_bit_num), + ) +} + pub fn uu_app_b3sum() -> Command { uu_app_b3sum_opts(uu_app_common()) } @@ -444,7 +541,7 @@ fn uu_app(binary_name: &str) -> Command { uu_app_common() } // b2sum supports the md5sum options plus -l/--length. - "b2sum" => uu_app_common(), // TODO: Implement -l/--length + "b2sum" => uu_app_length(), // These have never been part of GNU Coreutils, but can function with the same // options as md5sum. "sha3-224sum" | "sha3-256sum" | "sha3-384sum" | "sha3-512sum" => uu_app_common(), @@ -508,22 +605,65 @@ where // regular expression, otherwise we use the `{n}` modifier, // where `n` is the number of bytes. let bytes = options.digest.output_bits() / 4; - let modifier = if bytes > 0 { + let bytes_marker = if bytes > 0 { format!("{{{bytes}}}") } else { "+".to_string() }; - let gnu_re = Regex::new(&format!( - r"^(?P[a-fA-F0-9]{modifier}) (?P[ \*])(?P.*)", - )) - .map_err(|_| HashsumError::InvalidRegex)?; + // BSD reversed mode format is similar to the default mode, but doesn’t use a character to distinguish binary and text modes. + let mut bsd_reversed = None; + + /// Creates a Regex for parsing lines based on the given format. + /// The default value of `gnu_re` created with this function has to be recreated + /// after the initial line has been parsed, as this line dictates the format + /// for the rest of them, and mixing of formats is disallowed. + fn gnu_re_template( + bytes_marker: &str, + format_marker: &str, + ) -> Result { + Regex::new(&format!( + r"^(?P[a-fA-F0-9]{bytes_marker}) {format_marker}(?P.*)" + )) + .map_err(|_| HashsumError::InvalidRegex) + } + let mut gnu_re = gnu_re_template(&bytes_marker, r"(?P[ \*])?")?; let bsd_re = Regex::new(&format!( r"^{algorithm} \((?P.*)\) = (?P[a-fA-F0-9]{digest_size})", algorithm = options.algoname, - digest_size = modifier, + digest_size = bytes_marker, )) .map_err(|_| HashsumError::InvalidRegex)?; + fn handle_captures( + caps: &Captures, + bytes_marker: &str, + bsd_reversed: &mut Option, + gnu_re: &mut Regex, + ) -> Result<(String, String, bool), HashsumError> { + if bsd_reversed.is_none() { + let is_bsd_reversed = caps.name("binary").is_none(); + let format_marker = if is_bsd_reversed { + "" + } else { + r"(?P[ \*])" + } + .to_string(); + + *bsd_reversed = Some(is_bsd_reversed); + *gnu_re = gnu_re_template(bytes_marker, &format_marker)?; + } + + Ok(( + caps.name("fileName").unwrap().as_str().to_string(), + caps.name("digest").unwrap().as_str().to_ascii_lowercase(), + if *bsd_reversed == Some(false) { + caps.name("binary").unwrap().as_str() == "*" + } else { + false + }, + )) + } + let buffer = file; for (i, maybe_line) in buffer.lines().enumerate() { let line = match maybe_line { @@ -531,14 +671,12 @@ where Err(e) => return Err(e.map_err_context(|| "failed to read file".to_string())), }; let (ck_filename, sum, binary_check) = match gnu_re.captures(&line) { - Some(caps) => ( - caps.name("fileName").unwrap().as_str(), - caps.name("digest").unwrap().as_str().to_ascii_lowercase(), - caps.name("binary").unwrap().as_str() == "*", - ), + Some(caps) => { + handle_captures(&caps, &bytes_marker, &mut bsd_reversed, &mut gnu_re)? + } None => match bsd_re.captures(&line) { Some(caps) => ( - caps.name("fileName").unwrap().as_str(), + caps.name("fileName").unwrap().as_str().to_string(), caps.name("digest").unwrap().as_str().to_ascii_lowercase(), true, ), @@ -559,7 +697,7 @@ where } }, }; - let f = match File::open(ck_filename) { + let f = match File::open(ck_filename.clone()) { Err(_) => { failed_open_file += 1; println!( @@ -610,9 +748,11 @@ where ) .map_err_context(|| "failed to read input".to_string())?; if options.tag { - println!("{} ({}) = {}", options.algoname, filename.display(), sum); + println!("{} ({:?}) = {}", options.algoname, filename.display(), sum); } else if options.nonames { println!("{sum}"); + } else if options.zero { + print!("{} {}{}\0", sum, binary_marker, filename.display()); } else { println!("{} {}{}", sum, binary_marker, filename.display()); } diff --git a/src/uu/head/Cargo.toml b/src/uu/head/Cargo.toml index c631cf33109..6b53b1526b9 100644 --- a/src/uu/head/Cargo.toml +++ b/src/uu/head/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_head" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "head ~ (uutils) display the first lines of input" @@ -15,9 +15,9 @@ edition = "2021" path = "src/head.rs" [dependencies] -clap = { workspace=true } -memchr = { workspace=true } -uucore = { workspace=true, features=["ringbuffer", "lines"] } +clap = { workspace = true } +memchr = { workspace = true } +uucore = { workspace = true, features = ["ringbuffer", "lines"] } [[bin]] name = "head" diff --git a/src/uu/head/src/head.rs b/src/uu/head/src/head.rs index b9ac5024bd1..a336c91d4be 100644 --- a/src/uu/head/src/head.rs +++ b/src/uu/head/src/head.rs @@ -432,6 +432,7 @@ fn head_file(input: &mut std::fs::File, options: &HeadOptions) -> std::io::Resul } } +#[allow(clippy::cognitive_complexity)] fn uu_head(options: &HeadOptions) -> UResult<()> { let mut first = true; for file in &options.files { @@ -621,12 +622,10 @@ mod tests { #[test] #[cfg(target_os = "linux")] fn test_arg_iterate_bad_encoding() { - #[allow(clippy::invalid_utf8_in_unchecked)] - let invalid = unsafe { std::str::from_utf8_unchecked(b"\x80\x81") }; + use std::os::unix::ffi::OsStringExt; + let invalid = OsString::from_vec(vec![b'\x80', b'\x81']); // this arises from a conversion from OsString to &str - assert!( - arg_iterate(vec![OsString::from("head"), OsString::from(invalid)].into_iter()).is_err() - ); + assert!(arg_iterate(vec![OsString::from("head"), invalid].into_iter()).is_err()); } #[test] fn read_early_exit() { diff --git a/src/uu/head/src/parse.rs b/src/uu/head/src/parse.rs index cbfc971529f..56c359a0c72 100644 --- a/src/uu/head/src/parse.rs +++ b/src/uu/head/src/parse.rs @@ -13,6 +13,7 @@ pub enum ParseError { } /// Parses obsolete syntax /// head -NUM\[kmzv\] // spell-checker:disable-line +#[allow(clippy::cognitive_complexity)] pub fn parse_obsolete(src: &str) -> Option, ParseError>> { let mut chars = src.char_indices(); if let Some((_, '-')) = chars.next() { @@ -113,7 +114,13 @@ pub fn parse_num(src: &str) -> Result<(u64, bool), ParseSizeError> { return Err(ParseSizeError::ParseFailure(src.to_string())); } - parse_size(size_string).map(|n| (n, all_but_last)) + // remove leading zeros so that size is interpreted as decimal, not octal + let trimmed_string = size_string.trim_start_matches('0'); + if trimmed_string.is_empty() { + Ok((0, all_but_last)) + } else { + parse_size(trimmed_string).map(|n| (n, all_but_last)) + } } #[cfg(test)] diff --git a/src/uu/hostid/Cargo.toml b/src/uu/hostid/Cargo.toml index 7aa28328aa0..175c3193058 100644 --- a/src/uu/hostid/Cargo.toml +++ b/src/uu/hostid/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_hostid" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "hostid ~ (uutils) display the numeric identifier of the current host" @@ -15,9 +15,9 @@ edition = "2021" path = "src/hostid.rs" [dependencies] -clap = { workspace=true } -libc = { workspace=true } -uucore = { workspace=true } +clap = { workspace = true } +libc = { workspace = true } +uucore = { workspace = true } [[bin]] name = "hostid" diff --git a/src/uu/hostname/Cargo.toml b/src/uu/hostname/Cargo.toml index e57e5c364f3..d94a703ebf3 100644 --- a/src/uu/hostname/Cargo.toml +++ b/src/uu/hostname/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_hostname" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "hostname ~ (uutils) display or set the host name of the current host" @@ -15,12 +15,15 @@ edition = "2021" path = "src/hostname.rs" [dependencies] -clap = { workspace=true } +clap = { workspace = true } hostname = { version = "0.3", features = ["set"] } -uucore = { workspace=true, features=["wide"] } +uucore = { workspace = true, features = ["wide"] } [target.'cfg(target_os = "windows")'.dependencies] -windows-sys = { workspace=true, features = ["Win32_Networking_WinSock", "Win32_Foundation"] } +windows-sys = { workspace = true, features = [ + "Win32_Networking_WinSock", + "Win32_Foundation", +] } [[bin]] name = "hostname" diff --git a/src/uu/hostname/src/hostname.rs b/src/uu/hostname/src/hostname.rs index 0e959b47192..83a22a82f38 100644 --- a/src/uu/hostname/src/hostname.rs +++ b/src/uu/hostname/src/hostname.rs @@ -41,10 +41,10 @@ mod wsa { let mut data = std::mem::MaybeUninit::::uninit(); WSAStartup(0x0202, data.as_mut_ptr()) }; - if err != 0 { - Err(io::Error::from_raw_os_error(err)) - } else { + if err == 0 { Ok(WsaHandle(())) + } else { + Err(io::Error::from_raw_os_error(err)) } } diff --git a/src/uu/id/Cargo.toml b/src/uu/id/Cargo.toml index 3a157ada4c9..4d32f6e59d0 100644 --- a/src/uu/id/Cargo.toml +++ b/src/uu/id/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_id" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "id ~ (uutils) display user and group information for USER" @@ -15,9 +15,9 @@ edition = "2021" path = "src/id.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true, features=["entries", "process"] } -selinux = { workspace=true, optional=true } +clap = { workspace = true } +uucore = { workspace = true, features = ["entries", "process"] } +selinux = { workspace = true, optional = true } [[bin]] name = "id" diff --git a/src/uu/id/src/id.rs b/src/uu/id/src/id.rs index 4a80099adc3..7a8e40059be 100644 --- a/src/uu/id/src/id.rs +++ b/src/uu/id/src/id.rs @@ -203,9 +203,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } for i in 0..=users.len() { - let possible_pw = if !state.user_specified { - None - } else { + let possible_pw = if state.user_specified { match Passwd::locate(users[i].as_str()) { Ok(p) => Some(p), Err(_) => { @@ -218,6 +216,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } } } + } else { + None }; // GNU's `id` does not support the flags: -p/-P/-A. diff --git a/src/uu/install/Cargo.toml b/src/uu/install/Cargo.toml index 0890fe8c97c..14be22a9774 100644 --- a/src/uu/install/Cargo.toml +++ b/src/uu/install/Cargo.toml @@ -1,10 +1,7 @@ [package] name = "uu_install" -version = "0.0.17" -authors = [ - "Ben Eills ", - "uutils developers", -] +version = "0.0.19" +authors = ["Ben Eills ", "uutils developers"] license = "MIT" description = "install ~ (uutils) copy files from SOURCE to DESTINATION (with specified attributes)" @@ -18,14 +15,11 @@ edition = "2021" path = "src/install.rs" [dependencies] -clap = { workspace=true } -filetime = { workspace=true } -file_diff = { workspace=true } -libc = { workspace=true } -uucore = { workspace=true, features=["fs", "mode", "perms", "entries"] } - -[dev-dependencies] -time = { workspace=true } +clap = { workspace = true } +filetime = { workspace = true } +file_diff = { workspace = true } +libc = { workspace = true } +uucore = { workspace = true, features = ["fs", "mode", "perms", "entries"] } [[bin]] name = "install" diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index 98fdb47d69d..e0307fe34b7 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -399,13 +399,13 @@ fn behavior(matches: &ArgMatches) -> UResult { .unwrap_or("") .to_string(); - let owner_id = if !owner.is_empty() { + let owner_id = if owner.is_empty() { + None + } else { match usr2uid(&owner) { Ok(u) => Some(u), Err(_) => return Err(InstallError::InvalidUser(owner.clone()).into()), } - } else { - None }; let group = matches @@ -414,13 +414,13 @@ fn behavior(matches: &ArgMatches) -> UResult { .unwrap_or("") .to_string(); - let group_id = if !group.is_empty() { + let group_id = if group.is_empty() { + None + } else { match grp2gid(&group) { Ok(g) => Some(g), Err(_) => return Err(InstallError::InvalidGroup(group.clone()).into()), } - } else { - None }; Ok(Behavior { @@ -682,36 +682,23 @@ fn chown_optional_user_group(path: &Path, b: &Behavior) -> UResult<()> { Ok(()) } -/// Copy one file to a new location, changing metadata. -/// -/// Returns a Result type with the Err variant containing the error message. +/// Perform backup before overwriting. /// /// # Parameters /// -/// _from_ must exist as a non-directory. -/// _to_ must be a non-existent file, whose parent directory exists. +/// * `to` - The destination file path. +/// * `b` - The behavior configuration. /// -/// # Errors +/// # Returns /// -/// If the copy system call fails, we print a verbose error and return an empty error value. +/// Returns an Option containing the backup path, or None if backup is not needed. /// -#[allow(clippy::cognitive_complexity)] -fn copy(from: &Path, to: &Path, b: &Behavior) -> UResult<()> { - if b.compare && !need_copy(from, to, b)? { - return Ok(()); - } - // Declare the path here as we may need it for the verbose output below. - let mut backup_path = None; - - // Perform backup, if any, before overwriting 'to' - // - // The codes actually making use of the backup process don't seem to agree - // on how best to approach the issue. (mv and ln, for example) +fn perform_backup(to: &Path, b: &Behavior) -> UResult> { if to.exists() { if b.verbose { println!("removed {}", to.quote()); } - backup_path = backup_control::get_backup_path(b.backup_mode, to, &b.suffix); + let backup_path = backup_control::get_backup_path(b.backup_mode, to, &b.suffix); if let Some(ref backup_path) = backup_path { // TODO!! if let Err(err) = fs::rename(to, backup_path) { @@ -723,8 +710,24 @@ fn copy(from: &Path, to: &Path, b: &Behavior) -> UResult<()> { .into()); } } + Ok(backup_path) + } else { + Ok(None) } +} +/// Copy a file from one path to another. +/// +/// # Parameters +/// +/// * `from` - The source file path. +/// * `to` - The destination file path. +/// +/// # Returns +/// +/// Returns an empty Result or an error in case of failure. +/// +fn copy_file(from: &Path, to: &Path) -> UResult<()> { if from.as_os_str() == "/dev/null" { /* workaround a limitation of fs::copy * https://github.com/rust-lang/rust/issues/79390 @@ -737,27 +740,53 @@ fn copy(from: &Path, to: &Path, b: &Behavior) -> UResult<()> { } else if let Err(err) = fs::copy(from, to) { return Err(InstallError::InstallFailed(from.to_path_buf(), to.to_path_buf(), err).into()); } + Ok(()) +} - if b.strip && cfg!(not(windows)) { - match process::Command::new(&b.strip_program).arg(to).output() { - Ok(o) => { - if !o.status.success() { - // Follow GNU's behavior: if strip fails, removes the target - let _ = fs::remove_file(to); - return Err(InstallError::StripProgramFailed( - String::from_utf8(o.stderr).unwrap_or_default(), - ) - .into()); - } - } - Err(e) => { +/// Strip a file using an external program. +/// +/// # Parameters +/// +/// * `to` - The destination file path. +/// * `b` - The behavior configuration. +/// +/// # Returns +/// +/// Returns an empty Result or an error in case of failure. +/// +fn strip_file(to: &Path, b: &Behavior) -> UResult<()> { + match process::Command::new(&b.strip_program).arg(to).output() { + Ok(o) => { + if !o.status.success() { // Follow GNU's behavior: if strip fails, removes the target let _ = fs::remove_file(to); - return Err(InstallError::StripProgramFailed(e.to_string()).into()); + return Err(InstallError::StripProgramFailed( + String::from_utf8(o.stderr).unwrap_or_default(), + ) + .into()); } } + Err(e) => { + // Follow GNU's behavior: if strip fails, removes the target + let _ = fs::remove_file(to); + return Err(InstallError::StripProgramFailed(e.to_string()).into()); + } } + Ok(()) +} +/// Set ownership and permissions on the destination file. +/// +/// # Parameters +/// +/// * `to` - The destination file path. +/// * `b` - The behavior configuration. +/// +/// # Returns +/// +/// Returns an empty Result or an error in case of failure. +/// +fn set_ownership_and_permissions(to: &Path, b: &Behavior) -> UResult<()> { // Silent the warning as we want to the error message #[allow(clippy::question_mark)] if mode::chmod(to, b.mode()).is_err() { @@ -766,20 +795,70 @@ fn copy(from: &Path, to: &Path, b: &Behavior) -> UResult<()> { chown_optional_user_group(to, b)?; - if b.preserve_timestamps { - let meta = match fs::metadata(from) { - Ok(meta) => meta, - Err(e) => return Err(InstallError::MetadataFailed(e).into()), - }; + Ok(()) +} - let modified_time = FileTime::from_last_modification_time(&meta); - let accessed_time = FileTime::from_last_access_time(&meta); +/// Preserve timestamps on the destination file. +/// +/// # Parameters +/// +/// * `from` - The source file path. +/// * `to` - The destination file path. +/// +/// # Returns +/// +/// Returns an empty Result or an error in case of failure. +/// +fn preserve_timestamps(from: &Path, to: &Path) -> UResult<()> { + let meta = match fs::metadata(from) { + Ok(meta) => meta, + Err(e) => return Err(InstallError::MetadataFailed(e).into()), + }; - match set_file_times(to, accessed_time, modified_time) { - Ok(_) => {} - Err(e) => show_error!("{}", e), + let modified_time = FileTime::from_last_modification_time(&meta); + let accessed_time = FileTime::from_last_access_time(&meta); + + match set_file_times(to, accessed_time, modified_time) { + Ok(_) => Ok(()), + Err(e) => { + show_error!("{}", e); + Ok(()) } } +} + +/// Copy one file to a new location, changing metadata. +/// +/// Returns a Result type with the Err variant containing the error message. +/// +/// # Parameters +/// +/// _from_ must exist as a non-directory. +/// _to_ must be a non-existent file, whose parent directory exists. +/// +/// # Errors +/// +/// If the copy system call fails, we print a verbose error and return an empty error value. +/// +fn copy(from: &Path, to: &Path, b: &Behavior) -> UResult<()> { + if b.compare && !need_copy(from, to, b)? { + return Ok(()); + } + // Declare the path here as we may need it for the verbose output below. + let backup_path = perform_backup(to, b)?; + + copy_file(from, to)?; + + #[cfg(not(windows))] + if b.strip { + strip_file(to, b)?; + } + + set_ownership_and_permissions(to, b)?; + + if b.preserve_timestamps { + preserve_timestamps(from, to)?; + } if b.verbose { print!("{} -> {}", from.quote(), to.quote()); diff --git a/src/uu/join/Cargo.toml b/src/uu/join/Cargo.toml index 5be4af1a726..946056cc189 100644 --- a/src/uu/join/Cargo.toml +++ b/src/uu/join/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_join" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "join ~ (uutils) merge lines from inputs with matching join fields" @@ -15,9 +15,9 @@ edition = "2021" path = "src/join.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true } -memchr = { workspace=true } +clap = { workspace = true } +uucore = { workspace = true } +memchr = { workspace = true } [[bin]] name = "join" diff --git a/src/uu/join/src/join.rs b/src/uu/join/src/join.rs index baf70c25977..de1a9181be7 100644 --- a/src/uu/join/src/join.rs +++ b/src/uu/join/src/join.rs @@ -600,6 +600,7 @@ impl<'a> State<'a> { } #[uucore::main] +#[allow(clippy::cognitive_complexity)] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().try_get_matches_from(args)?; diff --git a/src/uu/kill/Cargo.toml b/src/uu/kill/Cargo.toml index 81719008320..1f5515d03b2 100644 --- a/src/uu/kill/Cargo.toml +++ b/src/uu/kill/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_kill" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "kill ~ (uutils) send a signal to a process" @@ -15,9 +15,9 @@ edition = "2021" path = "src/kill.rs" [dependencies] -clap = { workspace=true } -nix = { workspace=true, features = ["signal"] } -uucore = { workspace=true, features=["signals"] } +clap = { workspace = true } +nix = { workspace = true, features = ["signal"] } +uucore = { workspace = true, features = ["signals"] } [[bin]] name = "kill" diff --git a/src/uu/link/Cargo.toml b/src/uu/link/Cargo.toml index e35e23cfc48..fae9d59d94f 100644 --- a/src/uu/link/Cargo.toml +++ b/src/uu/link/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_link" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "link ~ (uutils) create a hard (file system) link to FILE" @@ -15,8 +15,8 @@ edition = "2021" path = "src/link.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true } +clap = { workspace = true } +uucore = { workspace = true } [[bin]] name = "link" diff --git a/src/uu/ln/Cargo.toml b/src/uu/ln/Cargo.toml index f08f04e4163..c4260cb8f4e 100644 --- a/src/uu/ln/Cargo.toml +++ b/src/uu/ln/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_ln" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "ln ~ (uutils) create a (file system) link to TARGET" @@ -15,8 +15,8 @@ edition = "2021" path = "src/ln.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true, features=["fs"] } +clap = { workspace = true } +uucore = { workspace = true, features = ["fs"] } [[bin]] name = "ln" diff --git a/src/uu/ln/src/ln.rs b/src/uu/ln/src/ln.rs index bfff8d3ebce..c2bf25c5c29 100644 --- a/src/uu/ln/src/ln.rs +++ b/src/uu/ln/src/ln.rs @@ -199,7 +199,7 @@ pub fn uu_app() -> Command { Arg::new(options::LOGICAL) .short('L') .long(options::LOGICAL) - .help("dereference TARGETs that are symbolic links") + .help("follow TARGETs that are symbolic links") .overrides_with(options::PHYSICAL) .action(ArgAction::SetTrue), ) @@ -292,6 +292,7 @@ fn exec(files: &[PathBuf], settings: &Settings) -> UResult<()> { link(&files[0], &files[1], settings) } +#[allow(clippy::cognitive_complexity)] fn link_files_in_dir(files: &[PathBuf], target_dir: &Path, settings: &Settings) -> UResult<()> { if !target_dir.is_dir() { return Err(LnError::TargetIsDirectory(target_dir.to_owned()).into()); @@ -364,6 +365,7 @@ fn relative_path<'a>(src: &'a Path, dst: &Path) -> Cow<'a, Path> { src.into() } +#[allow(clippy::cognitive_complexity)] fn link(src: &Path, dst: &Path, settings: &Settings) -> UResult<()> { let mut backup_path = None; let source: Cow<'_, Path> = if settings.relative { diff --git a/src/uu/logname/Cargo.toml b/src/uu/logname/Cargo.toml index 18ecdc9ab48..a6bb6b0b7dd 100644 --- a/src/uu/logname/Cargo.toml +++ b/src/uu/logname/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_logname" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "logname ~ (uutils) display the login name of the current user" @@ -15,9 +15,9 @@ edition = "2021" path = "src/logname.rs" [dependencies] -libc = { workspace=true } -clap = { workspace=true } -uucore = { workspace=true } +libc = { workspace = true } +clap = { workspace = true } +uucore = { workspace = true } [[bin]] name = "logname" diff --git a/src/uu/ls/Cargo.toml b/src/uu/ls/Cargo.toml index 7c7add29413..196e29795fa 100644 --- a/src/uu/ls/Cargo.toml +++ b/src/uu/ls/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_ls" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "ls ~ (uutils) display directory contents" @@ -15,18 +15,18 @@ edition = "2021" path = "src/ls.rs" [dependencies] -clap = { workspace=true, features = ["env"] } -chrono = { workspace=true } -unicode-width = { workspace=true } -number_prefix = { workspace=true } -term_grid = { workspace=true } -terminal_size = { workspace=true } -glob = { workspace=true } -lscolors = { workspace=true } -uucore = { workspace=true, features = ["entries", "fs"] } -once_cell = { workspace=true } -is-terminal = { workspace=true } -selinux = { workspace=true, optional = true } +clap = { workspace = true, features = ["env"] } +chrono = { workspace = true } +unicode-width = { workspace = true } +number_prefix = { workspace = true } +term_grid = { workspace = true } +terminal_size = { workspace = true } +glob = { workspace = true } +lscolors = { workspace = true } +uucore = { workspace = true, features = ["entries", "fs"] } +once_cell = { workspace = true } +is-terminal = { workspace = true } +selinux = { workspace = true, optional = true } [[bin]] name = "ls" diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index c5d81183408..7db591cf3b7 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -41,7 +41,13 @@ use unicode_width::UnicodeWidthStr; target_os = "linux", target_os = "macos", target_os = "android", - target_os = "ios" + target_os = "ios", + target_os = "freebsd", + target_os = "dragonfly", + target_os = "netbsd", + target_os = "openbsd", + target_os = "illumos", + target_os = "solaris" ))] use uucore::libc::{dev_t, major, minor}; #[cfg(unix)] @@ -288,6 +294,7 @@ enum Sort { Time, Version, Extension, + Width, } #[derive(PartialEq)] @@ -426,36 +433,236 @@ struct PaddingCollection { block_size: usize, } +/// Extracts the format to display the information based on the options provided. +/// +/// # Returns +/// +/// A tuple containing the Format variant and an Option containing a &'static str +/// which corresponds to the option used to define the format. +fn extract_format(options: &clap::ArgMatches) -> (Format, Option<&'static str>) { + if let Some(format_) = options.get_one::(options::FORMAT) { + ( + match format_.as_str() { + "long" | "verbose" => Format::Long, + "single-column" => Format::OneLine, + "columns" | "vertical" => Format::Columns, + "across" | "horizontal" => Format::Across, + "commas" => Format::Commas, + // below should never happen as clap already restricts the values. + _ => unreachable!("Invalid field for --format"), + }, + Some(options::FORMAT), + ) + } else if options.get_flag(options::format::LONG) { + (Format::Long, Some(options::format::LONG)) + } else if options.get_flag(options::format::ACROSS) { + (Format::Across, Some(options::format::ACROSS)) + } else if options.get_flag(options::format::COMMAS) { + (Format::Commas, Some(options::format::COMMAS)) + } else if options.get_flag(options::format::COLUMNS) { + (Format::Columns, Some(options::format::COLUMNS)) + } else if std::io::stdout().is_terminal() { + (Format::Columns, None) + } else { + (Format::OneLine, None) + } +} + +/// Extracts the type of files to display +/// +/// # Returns +/// +/// A Files variant representing the type of files to display. +fn extract_files(options: &clap::ArgMatches) -> Files { + if options.get_flag(options::files::ALL) { + Files::All + } else if options.get_flag(options::files::ALMOST_ALL) { + Files::AlmostAll + } else { + Files::Normal + } +} + +/// Extracts the sorting method to use based on the options provided. +/// +/// # Returns +/// +/// A Sort variant representing the sorting method to use. +fn extract_sort(options: &clap::ArgMatches) -> Sort { + if let Some(field) = options.get_one::(options::SORT) { + match field.as_str() { + "none" => Sort::None, + "name" => Sort::Name, + "time" => Sort::Time, + "size" => Sort::Size, + "version" => Sort::Version, + "extension" => Sort::Extension, + "width" => Sort::Width, + // below should never happen as clap already restricts the values. + _ => unreachable!("Invalid field for --sort"), + } + } else if options.get_flag(options::sort::TIME) { + Sort::Time + } else if options.get_flag(options::sort::SIZE) { + Sort::Size + } else if options.get_flag(options::sort::NONE) { + Sort::None + } else if options.get_flag(options::sort::VERSION) { + Sort::Version + } else if options.get_flag(options::sort::EXTENSION) { + Sort::Extension + } else { + Sort::Name + } +} + +/// Extracts the time to use based on the options provided. +/// +/// # Returns +/// +/// A Time variant representing the time to use. +fn extract_time(options: &clap::ArgMatches) -> Time { + if let Some(field) = options.get_one::(options::TIME) { + match field.as_str() { + "ctime" | "status" => Time::Change, + "access" | "atime" | "use" => Time::Access, + "birth" | "creation" => Time::Birth, + // below should never happen as clap already restricts the values. + _ => unreachable!("Invalid field for --time"), + } + } else if options.get_flag(options::time::ACCESS) { + Time::Access + } else if options.get_flag(options::time::CHANGE) { + Time::Change + } else { + Time::Modification + } +} + +/// Extracts the color option to use based on the options provided. +/// +/// # Returns +/// +/// A boolean representing whether or not to use color. +fn extract_color(options: &clap::ArgMatches) -> bool { + match options.get_one::(options::COLOR) { + None => options.contains_id(options::COLOR), + Some(val) => match val.as_str() { + "" | "always" | "yes" | "force" => true, + "auto" | "tty" | "if-tty" => std::io::stdout().is_terminal(), + /* "never" | "no" | "none" | */ _ => false, + }, + } +} + +/// Extracts the quoting style to use based on the options provided. +/// +/// # Arguments +/// +/// * `options` - A reference to a clap::ArgMatches object containing command line arguments. +/// * `show_control` - A boolean value representing whether or not to show control characters. +/// +/// # Returns +/// +/// A QuotingStyle variant representing the quoting style to use. +fn extract_quoting_style(options: &clap::ArgMatches, show_control: bool) -> QuotingStyle { + let opt_quoting_style = options + .get_one::(options::QUOTING_STYLE) + .map(|cmd_line_qs| cmd_line_qs.to_owned()); + + if let Some(style) = opt_quoting_style { + match style.as_str() { + "literal" => QuotingStyle::Literal { show_control }, + "shell" => QuotingStyle::Shell { + escape: false, + always_quote: false, + show_control, + }, + "shell-always" => QuotingStyle::Shell { + escape: false, + always_quote: true, + show_control, + }, + "shell-escape" => QuotingStyle::Shell { + escape: true, + always_quote: false, + show_control, + }, + "shell-escape-always" => QuotingStyle::Shell { + escape: true, + always_quote: true, + show_control, + }, + "c" => QuotingStyle::C { + quotes: quoting_style::Quotes::Double, + }, + "escape" => QuotingStyle::C { + quotes: quoting_style::Quotes::None, + }, + _ => unreachable!("Should have been caught by Clap"), + } + } else if options.get_flag(options::quoting::LITERAL) { + QuotingStyle::Literal { show_control } + } else if options.get_flag(options::quoting::ESCAPE) { + QuotingStyle::C { + quotes: quoting_style::Quotes::None, + } + } else if options.get_flag(options::quoting::C) { + QuotingStyle::C { + quotes: quoting_style::Quotes::Double, + } + } else { + // TODO: use environment variable if available + QuotingStyle::Shell { + escape: true, + always_quote: false, + show_control, + } + } +} + +/// Extracts the indicator style to use based on the options provided. +/// +/// # Returns +/// +/// An IndicatorStyle variant representing the indicator style to use. +fn extract_indicator_style(options: &clap::ArgMatches) -> IndicatorStyle { + if let Some(field) = options.get_one::(options::INDICATOR_STYLE) { + match field.as_str() { + "none" => IndicatorStyle::None, + "file-type" => IndicatorStyle::FileType, + "classify" => IndicatorStyle::Classify, + "slash" => IndicatorStyle::Slash, + &_ => IndicatorStyle::None, + } + } else if let Some(field) = options.get_one::(options::indicator_style::CLASSIFY) { + match field.as_str() { + "never" | "no" | "none" => IndicatorStyle::None, + "always" | "yes" | "force" => IndicatorStyle::Classify, + "auto" | "tty" | "if-tty" => { + if std::io::stdout().is_terminal() { + IndicatorStyle::Classify + } else { + IndicatorStyle::None + } + } + &_ => IndicatorStyle::None, + } + } else if options.get_flag(options::indicator_style::SLASH) { + IndicatorStyle::Slash + } else if options.get_flag(options::indicator_style::FILE_TYPE) { + IndicatorStyle::FileType + } else { + IndicatorStyle::None + } +} + impl Config { #[allow(clippy::cognitive_complexity)] pub fn from(options: &clap::ArgMatches) -> UResult { let context = options.get_flag(options::CONTEXT); - let (mut format, opt) = if let Some(format_) = options.get_one::(options::FORMAT) { - ( - match format_.as_str() { - "long" | "verbose" => Format::Long, - "single-column" => Format::OneLine, - "columns" | "vertical" => Format::Columns, - "across" | "horizontal" => Format::Across, - "commas" => Format::Commas, - // below should never happen as clap already restricts the values. - _ => unreachable!("Invalid field for --format"), - }, - Some(options::FORMAT), - ) - } else if options.get_flag(options::format::LONG) { - (Format::Long, Some(options::format::LONG)) - } else if options.get_flag(options::format::ACROSS) { - (Format::Across, Some(options::format::ACROSS)) - } else if options.get_flag(options::format::COMMAS) { - (Format::Commas, Some(options::format::COMMAS)) - } else if options.get_flag(options::format::COLUMNS) { - (Format::Columns, Some(options::format::COLUMNS)) - } else if std::io::stdout().is_terminal() { - (Format::Columns, None) - } else { - (Format::OneLine, None) - }; + let (mut format, opt) = extract_format(options); + let files = extract_files(options); // The -o, -n and -g options are tricky. They cannot override with each // other because it's possible to combine them. For example, the option @@ -504,63 +711,11 @@ impl Config { } } - let files = if options.get_flag(options::files::ALL) { - Files::All - } else if options.get_flag(options::files::ALMOST_ALL) { - Files::AlmostAll - } else { - Files::Normal - }; + let sort = extract_sort(options); - let sort = if let Some(field) = options.get_one::(options::SORT) { - match field.as_str() { - "none" => Sort::None, - "name" => Sort::Name, - "time" => Sort::Time, - "size" => Sort::Size, - "version" => Sort::Version, - "extension" => Sort::Extension, - // below should never happen as clap already restricts the values. - _ => unreachable!("Invalid field for --sort"), - } - } else if options.get_flag(options::sort::TIME) { - Sort::Time - } else if options.get_flag(options::sort::SIZE) { - Sort::Size - } else if options.get_flag(options::sort::NONE) { - Sort::None - } else if options.get_flag(options::sort::VERSION) { - Sort::Version - } else if options.get_flag(options::sort::EXTENSION) { - Sort::Extension - } else { - Sort::Name - }; - - let time = if let Some(field) = options.get_one::(options::TIME) { - match field.as_str() { - "ctime" | "status" => Time::Change, - "access" | "atime" | "use" => Time::Access, - "birth" | "creation" => Time::Birth, - // below should never happen as clap already restricts the values. - _ => unreachable!("Invalid field for --time"), - } - } else if options.get_flag(options::time::ACCESS) { - Time::Access - } else if options.get_flag(options::time::CHANGE) { - Time::Change - } else { - Time::Modification - }; + let time = extract_time(options); - let mut needs_color = match options.get_one::(options::COLOR) { - None => options.contains_id(options::COLOR), - Some(val) => match val.as_str() { - "" | "always" | "yes" | "force" => true, - "auto" | "tty" | "if-tty" => std::io::stdout().is_terminal(), - /* "never" | "no" | "none" | */ _ => false, - }, - }; + let mut needs_color = extract_color(options); let cmd_line_bs = options.get_one::(options::size::BLOCK_SIZE); let opt_si = cmd_line_bs.is_some() @@ -681,90 +836,8 @@ impl Config { !std::io::stdout().is_terminal() }; - let opt_quoting_style = options - .get_one::(options::QUOTING_STYLE) - .map(|cmd_line_qs| cmd_line_qs.to_owned()); - - let mut quoting_style = if let Some(style) = opt_quoting_style { - match style.as_str() { - "literal" => QuotingStyle::Literal { show_control }, - "shell" => QuotingStyle::Shell { - escape: false, - always_quote: false, - show_control, - }, - "shell-always" => QuotingStyle::Shell { - escape: false, - always_quote: true, - show_control, - }, - "shell-escape" => QuotingStyle::Shell { - escape: true, - always_quote: false, - show_control, - }, - "shell-escape-always" => QuotingStyle::Shell { - escape: true, - always_quote: true, - show_control, - }, - "c" => QuotingStyle::C { - quotes: quoting_style::Quotes::Double, - }, - "escape" => QuotingStyle::C { - quotes: quoting_style::Quotes::None, - }, - _ => unreachable!("Should have been caught by Clap"), - } - } else if options.get_flag(options::quoting::LITERAL) { - QuotingStyle::Literal { show_control } - } else if options.get_flag(options::quoting::ESCAPE) { - QuotingStyle::C { - quotes: quoting_style::Quotes::None, - } - } else if options.get_flag(options::quoting::C) { - QuotingStyle::C { - quotes: quoting_style::Quotes::Double, - } - } else { - // TODO: use environment variable if available - QuotingStyle::Shell { - escape: true, - always_quote: false, - show_control, - } - }; - - let indicator_style = if let Some(field) = - options.get_one::(options::INDICATOR_STYLE) - { - match field.as_str() { - "none" => IndicatorStyle::None, - "file-type" => IndicatorStyle::FileType, - "classify" => IndicatorStyle::Classify, - "slash" => IndicatorStyle::Slash, - &_ => IndicatorStyle::None, - } - } else if let Some(field) = options.get_one::(options::indicator_style::CLASSIFY) { - match field.as_str() { - "never" | "no" | "none" => IndicatorStyle::None, - "always" | "yes" | "force" => IndicatorStyle::Classify, - "auto" | "tty" | "if-tty" => { - if std::io::stdout().is_terminal() { - IndicatorStyle::Classify - } else { - IndicatorStyle::None - } - } - &_ => IndicatorStyle::None, - } - } else if options.get_flag(options::indicator_style::SLASH) { - IndicatorStyle::Slash - } else if options.get_flag(options::indicator_style::FILE_TYPE) { - IndicatorStyle::FileType - } else { - IndicatorStyle::None - }; + let mut quoting_style = extract_quoting_style(options, show_control); + let indicator_style = extract_indicator_style(options); let time_style = parse_time_style(options)?; let mut ignore_patterns: Vec = Vec::new(); @@ -1251,9 +1324,9 @@ pub fn uu_app() -> Command { .arg( Arg::new(options::SORT) .long(options::SORT) - .help("Sort by : name, none (-U), time (-t), size (-S) or extension (-X)") + .help("Sort by : name, none (-U), time (-t), size (-S), extension (-X) or width") .value_name("field") - .value_parser(["name", "none", "time", "size", "version", "extension"]) + .value_parser(["name", "none", "time", "size", "version", "extension", "width"]) .require_equals(true) .overrides_with_all([ options::SORT, @@ -1358,7 +1431,7 @@ pub fn uu_app() -> Command { Arg::new(options::dereference::DIR_ARGS) .long(options::dereference::DIR_ARGS) .help( - "Do not dereference symlinks except when they link to directories and are \ + "Do not follow symlinks except when they link to directories and are \ given as command line arguments.", ) .overrides_with_all([ @@ -1372,7 +1445,7 @@ pub fn uu_app() -> Command { Arg::new(options::dereference::ARGS) .short('H') .long(options::dereference::ARGS) - .help("Do not dereference symlinks except when given as command line arguments.") + .help("Do not follow symlinks except when given as command line arguments.") .overrides_with_all([ options::dereference::ALL, options::dereference::DIR_ARGS, @@ -1866,6 +1939,12 @@ fn sort_entries(entries: &mut [PathData], config: &Config, out: &mut BufWriter entries.sort_by(|a, b| { + a.display_name + .len() + .cmp(&b.display_name.len()) + .then(a.display_name.cmp(&b.display_name)) + }), Sort::None => {} } @@ -1926,13 +2005,19 @@ fn should_display(entry: &DirEntry, config: &Config) -> bool { require_literal_separator: false, case_sensitive: true, }; - let file_name = entry.file_name().into_string().unwrap(); + let file_name = entry.file_name(); + // If the decoding fails, still show an incorrect rendering + let file_name = match file_name.to_str() { + Some(s) => s.to_string(), + None => file_name.to_string_lossy().into_owned(), + }; !config .ignore_patterns .iter() .any(|p| p.matches_with(&file_name, options)) } +#[allow(clippy::cognitive_complexity)] fn enter_directory( path_data: &PathData, read_dir: ReadDir, @@ -2008,16 +2093,16 @@ fn enter_directory( continue; } Ok(rd) => { - if !listed_ancestors + if listed_ancestors .insert(FileInformation::from_path(&e.p_buf, e.must_dereference)?) { - out.flush()?; - show!(LsError::AlreadyListedError(e.p_buf.clone())); - } else { writeln!(out, "\n{}:", e.p_buf.display())?; enter_directory(e, rd, config, out, listed_ancestors)?; listed_ancestors .remove(&FileInformation::from_path(&e.p_buf, e.must_dereference)?); + } else { + out.flush()?; + show!(LsError::AlreadyListedError(e.p_buf.clone())); } } } @@ -2123,6 +2208,7 @@ fn display_additional_leading_info( Ok(result) } +#[allow(clippy::cognitive_complexity)] fn display_items(items: &[PathData], config: &Config, out: &mut BufWriter) -> UResult<()> { // `-Z`, `--context`: // Display the SELinux security context or '?' if none is found. When used with the `-l` @@ -2312,6 +2398,7 @@ fn display_grid( /// ``` /// that decide the maximum possible character count of each field. #[allow(clippy::write_literal)] +#[allow(clippy::cognitive_complexity)] fn display_item_long( item: &PathData, padding: &PaddingCollection, @@ -2648,7 +2735,13 @@ fn display_len_or_rdev(metadata: &Metadata, config: &Config) -> SizeOrDeviceId { target_os = "linux", target_os = "macos", target_os = "android", - target_os = "ios" + target_os = "ios", + target_os = "freebsd", + target_os = "dragonfly", + target_os = "netbsd", + target_os = "openbsd", + target_os = "illumos", + target_os = "solaris" ))] { let ft = metadata.file_type(); @@ -2742,6 +2835,7 @@ fn classify_file(path: &PathData, out: &mut BufWriter) -> Option { /// Note that non-unicode sequences in symlink targets are dealt with using /// [`std::path::Path::to_string_lossy`]. #[allow(unused_variables)] +#[allow(clippy::cognitive_complexity)] fn display_file_name( path: &PathData, config: &Config, @@ -2867,10 +2961,10 @@ fn display_file_name( // to get correct alignment from later calls to`display_grid()`. if config.context { if let Some(pad_count) = prefix_context { - let security_context = if !matches!(config.format, Format::Commas) { - pad_left(&path.security_context, pad_count) - } else { + let security_context = if matches!(config.format, Format::Commas) { path.security_context.to_owned() + } else { + pad_left(&path.security_context, pad_count) }; name = format!("{security_context} {name}"); width += security_context.len() + 1; diff --git a/src/uu/mkdir/Cargo.toml b/src/uu/mkdir/Cargo.toml index b8bc1dddf2a..9d1edc5c663 100644 --- a/src/uu/mkdir/Cargo.toml +++ b/src/uu/mkdir/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_mkdir" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "mkdir ~ (uutils) create DIRECTORY" @@ -15,8 +15,8 @@ edition = "2021" path = "src/mkdir.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true, features=["fs", "mode"] } +clap = { workspace = true } +uucore = { workspace = true, features = ["fs", "mode"] } [[bin]] name = "mkdir" diff --git a/src/uu/mkdir/src/mkdir.rs b/src/uu/mkdir/src/mkdir.rs index 9f490ecf9b4..a94439af5ab 100644 --- a/src/uu/mkdir/src/mkdir.rs +++ b/src/uu/mkdir/src/mkdir.rs @@ -20,7 +20,7 @@ use uucore::mode; use uucore::{display::Quotable, fs::dir_strip_dot_for_creation}; use uucore::{format_usage, help_about, help_section, help_usage, show, show_if_err}; -static DEFAULT_PERM: u32 = 0o755; +static DEFAULT_PERM: u32 = 0o777; const ABOUT: &str = help_about!("mkdir.md"); const USAGE: &str = help_usage!("mkdir.md"); @@ -41,7 +41,7 @@ fn get_mode(_matches: &ArgMatches, _mode_had_minus_prefix: bool) -> Result Result { let digits: &[char] = &['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; - // Translate a ~str in octal form to u16, default to 755 + // Translate a ~str in octal form to u16, default to 777 // Not tested on Windows let mut new_mode = DEFAULT_PERM; match matches.get_one::(options::MODE) { @@ -158,7 +158,7 @@ fn exec(dirs: ValuesRef, recursive: bool, mode: u32, verbose: bool) -> } fn mkdir(path: &Path, recursive: bool, mode: u32, verbose: bool) -> UResult<()> { - create_dir(path, recursive, verbose)?; + create_dir(path, recursive, verbose, false)?; chmod(path, mode) } @@ -179,7 +179,9 @@ fn chmod(_path: &Path, _mode: u32) -> UResult<()> { Ok(()) } -fn create_dir(path: &Path, recursive: bool, verbose: bool) -> UResult<()> { +// `is_parent` argument is not used on windows +#[allow(unused_variables)] +fn create_dir(path: &Path, recursive: bool, verbose: bool, is_parent: bool) -> UResult<()> { if path.exists() && !recursive { return Err(USimpleError::new( 1, @@ -192,7 +194,7 @@ fn create_dir(path: &Path, recursive: bool, verbose: bool) -> UResult<()> { if recursive { match path.parent() { - Some(p) => create_dir(p, recursive, verbose)?, + Some(p) => create_dir(p, recursive, verbose, true)?, None => { USimpleError::new(1, "failed to create whole tree"); } @@ -207,6 +209,12 @@ fn create_dir(path: &Path, recursive: bool, verbose: bool) -> UResult<()> { path.quote() ); } + #[cfg(not(windows))] + if is_parent { + // directories created by -p have permission bits set to '=rwx,u+wx', + // which is umask modified by 'u+wx' + chmod(path, (!mode::get_umask() & 0o0777) | 0o0300)?; + } Ok(()) } Err(_) if path.is_dir() => Ok(()), diff --git a/src/uu/mkfifo/Cargo.toml b/src/uu/mkfifo/Cargo.toml index 9ffe3c765bd..54a4a51b56d 100644 --- a/src/uu/mkfifo/Cargo.toml +++ b/src/uu/mkfifo/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_mkfifo" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "mkfifo ~ (uutils) create FIFOs (named pipes)" @@ -15,9 +15,9 @@ edition = "2021" path = "src/mkfifo.rs" [dependencies] -clap = { workspace=true } -libc = { workspace=true } -uucore = { workspace=true } +clap = { workspace = true } +libc = { workspace = true } +uucore = { workspace = true } [[bin]] name = "mkfifo" diff --git a/src/uu/mknod/Cargo.toml b/src/uu/mknod/Cargo.toml index b74ed3b4635..d73ea59b7a8 100644 --- a/src/uu/mknod/Cargo.toml +++ b/src/uu/mknod/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_mknod" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "mknod ~ (uutils) create special file NAME of TYPE" @@ -16,9 +16,9 @@ name = "uu_mknod" path = "src/mknod.rs" [dependencies] -clap = { workspace=true } -libc = { workspace=true } -uucore = { workspace=true, features=["mode"] } +clap = { workspace = true } +libc = { workspace = true } +uucore = { workspace = true, features = ["mode"] } [[bin]] name = "mknod" diff --git a/src/uu/mktemp/Cargo.toml b/src/uu/mktemp/Cargo.toml index 029e89bc948..001bf541114 100644 --- a/src/uu/mktemp/Cargo.toml +++ b/src/uu/mktemp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_mktemp" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "mktemp ~ (uutils) create and display a temporary file or directory from TEMPLATE" @@ -15,10 +15,10 @@ edition = "2021" path = "src/mktemp.rs" [dependencies] -clap = { workspace=true } -rand = { workspace=true } -tempfile = { workspace=true } -uucore = { workspace=true } +clap = { workspace = true } +rand = { workspace = true } +tempfile = { workspace = true } +uucore = { workspace = true } [[bin]] name = "mktemp" diff --git a/src/uu/mktemp/src/mktemp.rs b/src/uu/mktemp/src/mktemp.rs index 4a77da4c402..fc360d0f5ae 100644 --- a/src/uu/mktemp/src/mktemp.rs +++ b/src/uu/mktemp/src/mktemp.rs @@ -42,6 +42,11 @@ static OPT_T: &str = "t"; static ARG_TEMPLATE: &str = "template"; +#[cfg(not(windows))] +const TMPDIR_ENV_VAR: &str = "TMPDIR"; +#[cfg(windows)] +const TMPDIR_ENV_VAR: &str = "TMP"; + #[derive(Debug)] enum MkTempError { PersistError(PathBuf), @@ -191,7 +196,16 @@ impl Options { (tmpdir, template.to_string()) } Some(template) => { - let tmpdir = matches.get_one::(OPT_TMPDIR).map(String::from); + let tmpdir = if env::var(TMPDIR_ENV_VAR).is_ok() && matches.get_flag(OPT_T) { + env::var(TMPDIR_ENV_VAR).ok() + } else if matches.contains_id(OPT_TMPDIR) { + matches.get_one::(OPT_TMPDIR).map(String::from) + } else if matches.get_flag(OPT_T) { + // mktemp -t foo.xxx should export in TMPDIR + Some(env::temp_dir().display().to_string()) + } else { + matches.get_one::(OPT_TMPDIR).map(String::from) + }; (tmpdir, template.to_string()) } } @@ -281,7 +295,7 @@ impl Params { .join(prefix_from_template) .display() .to_string(); - if options.treat_as_template && prefix.contains(MAIN_SEPARATOR) { + if options.treat_as_template && prefix_from_template.contains(MAIN_SEPARATOR) { return Err(MkTempError::PrefixContainsDirSeparator(options.template)); } if tmpdir.is_some() && Path::new(prefix_from_template).is_absolute() { diff --git a/src/uu/more/Cargo.toml b/src/uu/more/Cargo.toml index 01bb9618395..ff677ad8758 100644 --- a/src/uu/more/Cargo.toml +++ b/src/uu/more/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_more" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "more ~ (uutils) input perusal filter" @@ -15,15 +15,15 @@ edition = "2021" path = "src/more.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true } -crossterm = { workspace=true } -is-terminal = { workspace=true } -unicode-width = { workspace=true } -unicode-segmentation = { workspace=true } +clap = { workspace = true } +uucore = { workspace = true } +crossterm = { workspace = true } +is-terminal = { workspace = true } +unicode-width = { workspace = true } +unicode-segmentation = { workspace = true } [target.'cfg(all(unix, not(target_os = "fuchsia")))'.dependencies] -nix = { workspace=true } +nix = { workspace = true } [[bin]] name = "more" diff --git a/src/uu/more/src/more.rs b/src/uu/more/src/more.rs index 6cf9df1ab12..a43489566c5 100644 --- a/src/uu/more/src/more.rs +++ b/src/uu/more/src/more.rs @@ -14,13 +14,14 @@ use std::{ time::Duration, }; -use clap::{crate_version, Arg, ArgAction, Command}; +use clap::{crate_version, value_parser, Arg, ArgAction, ArgMatches, Command}; use crossterm::event::KeyEventKind; use crossterm::{ + cursor::{MoveTo, MoveUp}, event::{self, Event, KeyCode, KeyEvent, KeyModifiers}, execute, queue, style::Attribute, - terminal, + terminal::{self, Clear, ClearType}, }; use is_terminal::IsTerminal; @@ -51,12 +52,48 @@ pub mod options { const MULTI_FILE_TOP_PROMPT: &str = "::::::::::::::\n{}\n::::::::::::::\n"; +struct Options { + clean_print: bool, + lines: Option, + print_over: bool, + silent: bool, + squeeze: bool, +} + +impl Options { + fn from(matches: &ArgMatches) -> Self { + let lines = match ( + matches.get_one::(options::LINES).copied(), + matches.get_one::(options::NUMBER).copied(), + ) { + // We add 1 to the number of lines to display because the last line + // is used for the banner + (Some(number), _) if number > 0 => Some(number + 1), + (None, Some(number)) if number > 0 => Some(number + 1), + (_, _) => None, + }; + Self { + clean_print: matches.get_flag(options::CLEAN_PRINT), + lines, + print_over: matches.get_flag(options::PRINT_OVER), + silent: matches.get_flag(options::SILENT), + squeeze: matches.get_flag(options::SQUEEZE), + } + } +} + #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().get_matches_from(args); + let args = args.collect_lossy(); + let matches = match uu_app().try_get_matches_from(&args) { + Ok(m) => m, + Err(e) => return Err(e.into()), + }; + + let options = Options::from(&matches); let mut buff = String::new(); - let silent = matches.get_flag(options::SILENT); + if let Some(files) = matches.get_many::(options::FILES) { let mut stdout = setup_term(); let length = files.len(); @@ -81,16 +118,26 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { if length > 1 { buff.push_str(&MULTI_FILE_TOP_PROMPT.replace("{}", file.to_str().unwrap())); } - let mut reader = BufReader::new(File::open(file).unwrap()); + let opened_file = match File::open(file) { + Err(why) => { + terminal::disable_raw_mode().unwrap(); + return Err(USimpleError::new( + 1, + format!("cannot open {}: {}", file.quote(), why.kind()), + )); + } + Ok(opened_file) => opened_file, + }; + let mut reader = BufReader::new(opened_file); reader.read_to_string(&mut buff).unwrap(); - more(&buff, &mut stdout, next_file.copied(), silent)?; + more(&buff, &mut stdout, next_file.copied(), &options)?; buff.clear(); } reset_term(&mut stdout); } else if !std::io::stdin().is_terminal() { stdin().read_to_string(&mut buff).unwrap(); let mut stdout = setup_term(); - more(&buff, &mut stdout, None, silent)?; + more(&buff, &mut stdout, None, &options)?; reset_term(&mut stdout); } else { return Err(UUsageError::new(1, "bad usage")); @@ -104,6 +151,13 @@ pub fn uu_app() -> Command { .override_usage(format_usage(USAGE)) .version(crate_version!()) .infer_long_args(true) + .arg( + Arg::new(options::PRINT_OVER) + .short('c') + .long(options::PRINT_OVER) + .help("Do not scroll, display text and clean line ends") + .action(ArgAction::SetTrue), + ) .arg( Arg::new(options::SILENT) .short('d') @@ -111,60 +165,57 @@ pub fn uu_app() -> Command { .help("Display help instead of ringing bell") .action(ArgAction::SetTrue), ) - // The commented arguments below are unimplemented: - /* - .arg( - Arg::new(options::LOGICAL) - .short('f') - .long(options::LOGICAL) - .help("Count logical rather than screen lines"), - ) - .arg( - Arg::new(options::NO_PAUSE) - .short('l') - .long(options::NO_PAUSE) - .help("Suppress pause after form feed"), - ) - .arg( - Arg::new(options::PRINT_OVER) - .short('c') - .long(options::PRINT_OVER) - .help("Do not scroll, display text and clean line ends"), - ) .arg( Arg::new(options::CLEAN_PRINT) .short('p') .long(options::CLEAN_PRINT) - .help("Do not scroll, clean screen and display text"), + .help("Do not scroll, clean screen and display text") + .action(ArgAction::SetTrue), ) .arg( Arg::new(options::SQUEEZE) .short('s') .long(options::SQUEEZE) - .help("Squeeze multiple blank lines into one"), - ) - .arg( - Arg::new(options::PLAIN) - .short('u') - .long(options::PLAIN) - .help("Suppress underlining and bold"), + .help("Squeeze multiple blank lines into one") + .action(ArgAction::SetTrue), ) .arg( Arg::new(options::LINES) .short('n') .long(options::LINES) .value_name("number") - .takes_value(true) + .num_args(1) + .value_parser(value_parser!(u16).range(0..)) .help("The number of lines per screen full"), ) .arg( Arg::new(options::NUMBER) - .allow_hyphen_values(true) .long(options::NUMBER) .required(false) - .takes_value(true) + .num_args(1) + .value_parser(value_parser!(u16).range(0..)) .help("Same as --lines"), ) + // The commented arguments below are unimplemented: + /* + .arg( + Arg::new(options::LOGICAL) + .short('f') + .long(options::LOGICAL) + .help("Count logical rather than screen lines"), + ) + .arg( + Arg::new(options::NO_PAUSE) + .short('l') + .long(options::NO_PAUSE) + .help("Suppress pause after form feed"), + ) + .arg( + Arg::new(options::PLAIN) + .short('u') + .long(options::PLAIN) + .help("Suppress underlining and bold"), + ) .arg( Arg::new(options::FROM_LINE) .short('F') @@ -209,7 +260,7 @@ fn setup_term() -> usize { fn reset_term(stdout: &mut std::io::Stdout) { terminal::disable_raw_mode().unwrap(); // Clear the prompt - queue!(stdout, terminal::Clear(terminal::ClearType::CurrentLine)).unwrap(); + queue!(stdout, terminal::Clear(ClearType::CurrentLine)).unwrap(); // Move cursor to the beginning without printing new line print!("\r"); stdout.flush().unwrap(); @@ -219,11 +270,20 @@ fn reset_term(stdout: &mut std::io::Stdout) { #[inline(always)] fn reset_term(_: &mut usize) {} -fn more(buff: &str, stdout: &mut Stdout, next_file: Option<&str>, silent: bool) -> UResult<()> { - let (cols, rows) = terminal::size().unwrap(); +fn more( + buff: &str, + stdout: &mut Stdout, + next_file: Option<&str>, + options: &Options, +) -> UResult<()> { + let (cols, mut rows) = terminal::size().unwrap(); + if let Some(number) = options.lines { + rows = number; + } + let lines = break_buff(buff, usize::from(cols)); - let mut pager = Pager::new(rows, lines, next_file, silent); + let mut pager = Pager::new(rows, lines, next_file, options); pager.draw(stdout, None); if pager.should_close() { return Ok(()); @@ -257,6 +317,11 @@ fn more(buff: &str, stdout: &mut Stdout, next_file: Option<&str>, silent: bool) modifiers: KeyModifiers::NONE, .. }) + | Event::Key(KeyEvent { + code: KeyCode::PageDown, + modifiers: KeyModifiers::NONE, + .. + }) | Event::Key(KeyEvent { code: KeyCode::Char(' '), modifiers: KeyModifiers::NONE, @@ -272,8 +337,14 @@ fn more(buff: &str, stdout: &mut Stdout, next_file: Option<&str>, silent: bool) code: KeyCode::Up, modifiers: KeyModifiers::NONE, .. + }) + | Event::Key(KeyEvent { + code: KeyCode::PageUp, + modifiers: KeyModifiers::NONE, + .. }) => { pager.page_up(); + paging_add_back_message(options, stdout)?; } Event::Key(KeyEvent { code: KeyCode::Char('j'), @@ -294,7 +365,7 @@ fn more(buff: &str, stdout: &mut Stdout, next_file: Option<&str>, silent: bool) pager.prev_line(); } Event::Resize(col, row) => { - pager.page_resize(col, row); + pager.page_resize(col, row, options.lines); } Event::Key(KeyEvent { code: KeyCode::Char(k), @@ -303,6 +374,16 @@ fn more(buff: &str, stdout: &mut Stdout, next_file: Option<&str>, silent: bool) _ => continue, } + if options.print_over { + execute!( + std::io::stdout(), + MoveTo(0, 0), + Clear(ClearType::FromCursorDown) + ) + .unwrap(); + } else if options.clean_print { + execute!(std::io::stdout(), Clear(ClearType::All), MoveTo(0, 0)).unwrap(); + } pager.draw(stdout, wrong_key); } } @@ -317,10 +398,12 @@ struct Pager<'a> { next_file: Option<&'a str>, line_count: usize, silent: bool, + squeeze: bool, + line_squeezed: usize, } impl<'a> Pager<'a> { - fn new(rows: u16, lines: Vec, next_file: Option<&'a str>, silent: bool) -> Self { + fn new(rows: u16, lines: Vec, next_file: Option<&'a str>, options: &Options) -> Self { let line_count = lines.len(); Self { upper_mark: 0, @@ -328,7 +411,9 @@ impl<'a> Pager<'a> { lines, next_file, line_count, - silent, + silent: options.silent, + squeeze: options.squeeze, + line_squeezed: 0, } } @@ -354,7 +439,21 @@ impl<'a> Pager<'a> { } fn page_up(&mut self) { - self.upper_mark = self.upper_mark.saturating_sub(self.content_rows.into()); + let content_row_usize: usize = self.content_rows.into(); + self.upper_mark = self + .upper_mark + .saturating_sub(content_row_usize.saturating_add(self.line_squeezed)); + + if self.squeeze { + let iter = self.lines.iter().take(self.upper_mark).rev(); + for line in iter { + if line.is_empty() { + self.upper_mark = self.upper_mark.saturating_sub(1); + } else { + break; + } + } + } } fn next_line(&mut self) { @@ -366,11 +465,13 @@ impl<'a> Pager<'a> { } // TODO: Deal with column size changes. - fn page_resize(&mut self, _: u16, row: u16) { - self.content_rows = row.saturating_sub(1); + fn page_resize(&mut self, _: u16, row: u16, option_line: Option) { + if option_line.is_none() { + self.content_rows = row.saturating_sub(1); + }; } - fn draw(&self, stdout: &mut std::io::Stdout, wrong_key: Option) { + fn draw(&mut self, stdout: &mut std::io::Stdout, wrong_key: Option) { let lower_mark = self .line_count .min(self.upper_mark.saturating_add(self.content_rows.into())); @@ -379,13 +480,44 @@ impl<'a> Pager<'a> { stdout.flush().unwrap(); } - fn draw_lines(&self, stdout: &mut std::io::Stdout) { + fn draw_lines(&mut self, stdout: &mut std::io::Stdout) { execute!(stdout, terminal::Clear(terminal::ClearType::CurrentLine)).unwrap(); - let displayed_lines = self - .lines - .iter() - .skip(self.upper_mark) - .take(self.content_rows.into()); + + self.line_squeezed = 0; + let mut previous_line_blank = false; + let mut displayed_lines = Vec::new(); + let mut iter = self.lines.iter().skip(self.upper_mark); + + while displayed_lines.len() < self.content_rows as usize { + match iter.next() { + Some(line) => { + if self.squeeze { + match (line.is_empty(), previous_line_blank) { + (true, false) => { + previous_line_blank = true; + displayed_lines.push(line); + } + (false, true) => { + previous_line_blank = false; + displayed_lines.push(line); + } + (false, false) => displayed_lines.push(line), + (true, true) => { + self.line_squeezed += 1; + self.upper_mark += 1; + } + } + } else { + displayed_lines.push(line); + } + } + // if none the end of the file is reached + None => { + self.upper_mark = self.line_count; + break; + } + } + } for line in displayed_lines { stdout.write_all(format!("\r{line}\n").as_bytes()).unwrap(); @@ -424,6 +556,14 @@ impl<'a> Pager<'a> { } } +fn paging_add_back_message(options: &Options, stdout: &mut std::io::Stdout) -> UResult<()> { + if options.lines.is_some() { + execute!(stdout, MoveUp(1))?; + stdout.write_all("\n\r...back 1 page\n".as_bytes())?; + } + Ok(()) +} + // Break the lines on the cols of the terminal fn break_buff(buff: &str, cols: usize) -> Vec { let mut lines = Vec::with_capacity(buff.lines().count()); diff --git a/src/uu/mv/Cargo.toml b/src/uu/mv/Cargo.toml index 87ce36c5fde..2e67ec151a4 100644 --- a/src/uu/mv/Cargo.toml +++ b/src/uu/mv/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_mv" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "mv ~ (uutils) move (rename) SOURCE to DESTINATION" @@ -15,10 +15,10 @@ edition = "2021" path = "src/mv.rs" [dependencies] -clap = { workspace=true } -fs_extra = { workspace=true } -indicatif = { workspace=true } -uucore = { workspace=true } +clap = { workspace = true } +fs_extra = { workspace = true } +indicatif = { workspace = true } +uucore = { workspace = true, features = ["fs"] } [[bin]] name = "mv" diff --git a/src/uu/mv/mv.md b/src/uu/mv/mv.md index 772e4bfaf4a..6fcee46973f 100644 --- a/src/uu/mv/mv.md +++ b/src/uu/mv/mv.md @@ -5,5 +5,19 @@ mv [OPTION]... [-T] SOURCE DEST mv [OPTION]... SOURCE... DIRECTORY mv [OPTION]... -t DIRECTORY SOURCE... ``` - Move `SOURCE` to `DEST`, or multiple `SOURCE`(s) to `DIRECTORY`. + +## After Help + +When specifying more than one of -i, -f, -n, only the final one will take effect. + +Do not move a non-directory that has an existing destination with the same or newer modification timestamp; +instead, silently skip the file without failing. If the move is across file system boundaries, the comparison is +to the source timestamp truncated to the resolutions of the destination file system and of the system calls used +to update timestamps; this avoids duplicate work if several `mv -u` commands are executed with the same source +and destination. This option is ignored if the `-n` or `--no-clobber` option is also specified. which gives more control +over which existing files in the destination are replaced, and its value can be one of the following: + +* `all` This is the default operation when an `--update` option is not specified, and results in all existing files in the destination being replaced. +* `none` This is similar to the `--no-clobber` option, in that no files in the destination are replaced, but also skipping a file does not induce a failure. +* `older` This is the default operation when `--update` is specified, and results in files being replaced if they’re older than the corresponding source file. diff --git a/src/uu/mv/src/error.rs b/src/uu/mv/src/error.rs index 103c1116ab5..7810c3a9506 100644 --- a/src/uu/mv/src/error.rs +++ b/src/uu/mv/src/error.rs @@ -15,6 +15,7 @@ pub enum MvError { DirectoryToNonDirectory(String), NonDirectoryToDirectory(String, String), NotADirectory(String), + TargetNotADirectory(String), } impl Error for MvError {} @@ -34,7 +35,8 @@ impl Display for MvError { Self::NonDirectoryToDirectory(s, t) => { write!(f, "cannot overwrite non-directory {t} with directory {s}") } - Self::NotADirectory(t) => write!(f, "target {t} is not a directory"), + Self::NotADirectory(t) => write!(f, "target {t}: Not a directory"), + Self::TargetNotADirectory(t) => write!(f, "target directory {t}: Not a directory"), } } } diff --git a/src/uu/mv/src/mv.rs b/src/uu/mv/src/mv.rs index ba6f2198a79..6289e79f90a 100644 --- a/src/uu/mv/src/mv.rs +++ b/src/uu/mv/src/mv.rs @@ -24,8 +24,10 @@ use std::os::windows; use std::path::{Path, PathBuf}; use uucore::backup_control::{self, BackupMode}; use uucore::display::Quotable; -use uucore::error::{FromIo, UError, UResult, USimpleError, UUsageError}; -use uucore::{format_usage, help_about, help_usage, prompt_yes, show}; +use uucore::error::{set_exit_code, FromIo, UError, UResult, USimpleError, UUsageError}; +use uucore::fs::{are_hardlinks_or_one_way_symlink_to_same_file, are_hardlinks_to_same_file}; +use uucore::update_control::{self, UpdateMode}; +use uucore::{format_usage, help_about, help_section, help_usage, prompt_yes, show}; use fs_extra::dir::{ get_size as dir_get_size, move_dir, move_dir_with_progress, CopyOptions as DirCopyOptions, @@ -38,7 +40,7 @@ pub struct Behavior { overwrite: OverwriteMode, backup: BackupMode, suffix: String, - update: bool, + update: UpdateMode, target_dir: Option, no_target_dir: bool, verbose: bool, @@ -55,6 +57,7 @@ pub enum OverwriteMode { const ABOUT: &str = help_about!("mv.md"); const USAGE: &str = help_usage!("mv.md"); +const AFTER_HELP: &str = help_section!("after help", "mv.md"); static OPT_FORCE: &str = "force"; static OPT_INTERACTIVE: &str = "interactive"; @@ -62,14 +65,13 @@ static OPT_NO_CLOBBER: &str = "no-clobber"; static OPT_STRIP_TRAILING_SLASHES: &str = "strip-trailing-slashes"; static OPT_TARGET_DIRECTORY: &str = "target-directory"; static OPT_NO_TARGET_DIRECTORY: &str = "no-target-directory"; -static OPT_UPDATE: &str = "update"; static OPT_VERBOSE: &str = "verbose"; static OPT_PROGRESS: &str = "progress"; static ARG_FILES: &str = "files"; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let mut app = uu_app().after_help(backup_control::BACKUP_CONTROL_LONG_HELP); + let mut app = uu_app(); let matches = app.try_get_matches_from_mut(args)?; if !matches.contains_id(OPT_TARGET_DIRECTORY) @@ -96,6 +98,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let overwrite_mode = determine_overwrite_mode(&matches); let backup_mode = backup_control::determine_backup_mode(&matches)?; + let update_mode = update_control::determine_update_mode(&matches); if overwrite_mode == OverwriteMode::NoClobber && backup_mode != BackupMode::NoBackup { return Err(UUsageError::new( @@ -106,14 +109,22 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let backup_suffix = backup_control::determine_backup_suffix(&matches); + let target_dir = matches + .get_one::(OPT_TARGET_DIRECTORY) + .map(OsString::from); + + if let Some(ref maybe_dir) = target_dir { + if !Path::new(&maybe_dir).is_dir() { + return Err(MvError::TargetNotADirectory(maybe_dir.quote().to_string()).into()); + } + } + let behavior = Behavior { overwrite: overwrite_mode, backup: backup_mode, suffix: backup_suffix, - update: matches.get_flag(OPT_UPDATE), - target_dir: matches - .get_one::(OPT_TARGET_DIRECTORY) - .map(OsString::from), + update: update_mode, + target_dir, no_target_dir: matches.get_flag(OPT_NO_TARGET_DIRECTORY), verbose: matches.get_flag(OPT_VERBOSE), strip_slashes: matches.get_flag(OPT_STRIP_TRAILING_SLASHES), @@ -128,14 +139,17 @@ pub fn uu_app() -> Command { .version(crate_version!()) .about(ABOUT) .override_usage(format_usage(USAGE)) + .after_help(format!( + "{AFTER_HELP}\n\n{}", + backup_control::BACKUP_CONTROL_LONG_HELP + )) .infer_long_args(true) - .arg(backup_control::arguments::backup()) - .arg(backup_control::arguments::backup_no_args()) .arg( Arg::new(OPT_FORCE) .short('f') .long(OPT_FORCE) .help("do not prompt before overwriting") + .overrides_with_all([OPT_INTERACTIVE, OPT_NO_CLOBBER]) .action(ArgAction::SetTrue), ) .arg( @@ -143,6 +157,7 @@ pub fn uu_app() -> Command { .short('i') .long(OPT_INTERACTIVE) .help("prompt before override") + .overrides_with_all([OPT_FORCE, OPT_NO_CLOBBER]) .action(ArgAction::SetTrue), ) .arg( @@ -150,6 +165,7 @@ pub fn uu_app() -> Command { .short('n') .long(OPT_NO_CLOBBER) .help("do not overwrite an existing file") + .overrides_with_all([OPT_FORCE, OPT_INTERACTIVE]) .action(ArgAction::SetTrue), ) .arg( @@ -158,7 +174,11 @@ pub fn uu_app() -> Command { .help("remove any trailing slashes from each SOURCE argument") .action(ArgAction::SetTrue), ) + .arg(backup_control::arguments::backup()) + .arg(backup_control::arguments::backup_no_args()) .arg(backup_control::arguments::suffix()) + .arg(update_control::arguments::update()) + .arg(update_control::arguments::update_no_args()) .arg( Arg::new(OPT_TARGET_DIRECTORY) .short('t') @@ -176,16 +196,6 @@ pub fn uu_app() -> Command { .help("treat DEST as a normal file") .action(ArgAction::SetTrue), ) - .arg( - Arg::new(OPT_UPDATE) - .short('u') - .long(OPT_UPDATE) - .help( - "move only when the SOURCE file is newer than the destination file \ - or when the destination file is missing", - ) - .action(ArgAction::SetTrue), - ) .arg( Arg::new(OPT_VERBOSE) .short('v') @@ -228,96 +238,96 @@ fn determine_overwrite_mode(matches: &ArgMatches) -> OverwriteMode { } } -fn exec(files: &[OsString], b: &Behavior) -> UResult<()> { - let paths: Vec = { - let paths = files.iter().map(Path::new); - - // Strip slashes from path, if strip opt present - if b.strip_slashes { - paths - .map(|p| p.components().as_path().to_owned()) - .collect::>() - } else { - paths.map(|p| p.to_owned()).collect::>() - } - }; +fn parse_paths(files: &[OsString], b: &Behavior) -> Vec { + let paths = files.iter().map(Path::new); - if let Some(ref name) = b.target_dir { - return move_files_into_dir(&paths, &PathBuf::from(name), b); + if b.strip_slashes { + paths + .map(|p| p.components().as_path().to_owned()) + .collect::>() + } else { + paths.map(|p| p.to_owned()).collect::>() } - match paths.len() { - /* case 0/1 are not possible thanks to clap */ - 2 => { - let source = &paths[0]; - let target = &paths[1]; - // Here we use the `symlink_metadata()` method instead of `exists()`, - // since it handles dangling symlinks correctly. The method gives an - // `Ok()` results unless the source does not exist, or the user - // lacks permission to access metadata. - if source.symlink_metadata().is_err() { - return Err(MvError::NoSuchFile(source.quote().to_string()).into()); - } +} - // GNU semantics are: if the source and target are the same, no move occurs and we print an error - if source.eq(target) { - // Done to match GNU semantics for the dot file - if source.eq(Path::new(".")) || source.ends_with("/.") || source.is_file() { - return Err(MvError::SameFile( - source.quote().to_string(), - target.quote().to_string(), - ) - .into()); - } else { - return Err(MvError::SelfSubdirectory(source.display().to_string()).into()); - } - } +fn handle_two_paths(source: &Path, target: &Path, b: &Behavior) -> UResult<()> { + if source.symlink_metadata().is_err() { + return Err(MvError::NoSuchFile(source.quote().to_string()).into()); + } - if target.is_dir() { - if b.no_target_dir { - if source.is_dir() { - rename(source, target, b, None).map_err_context(|| { - format!("cannot move {} to {}", source.quote(), target.quote()) - }) - } else { - Err(MvError::DirectoryToNonDirectory(target.quote().to_string()).into()) - } - } else { - move_files_into_dir(&[source.clone()], target, b) - } - } else if target.exists() && source.is_dir() { - match b.overwrite { - OverwriteMode::NoClobber => return Ok(()), - OverwriteMode::Interactive => { - if !prompt_yes!("overwrite {}? ", target.quote()) { - return Ok(()); - } - } - OverwriteMode::Force => {} - }; - Err(MvError::NonDirectoryToDirectory( - source.quote().to_string(), - target.quote().to_string(), - ) - .into()) + if (source.eq(target) + || are_hardlinks_to_same_file(source, target) + || are_hardlinks_or_one_way_symlink_to_same_file(source, target)) + && b.backup == BackupMode::NoBackup + { + if source.eq(Path::new(".")) || source.ends_with("/.") || source.is_file() { + return Err( + MvError::SameFile(source.quote().to_string(), target.quote().to_string()).into(), + ); + } else { + return Err(MvError::SelfSubdirectory(source.display().to_string()).into()); + } + } + + if target.is_dir() { + if b.no_target_dir { + if source.is_dir() { + rename(source, target, b, None).map_err_context(|| { + format!("cannot move {} to {}", source.quote(), target.quote()) + }) } else { - rename(source, target, b, None).map_err(|e| USimpleError::new(1, format!("{e}"))) + Err(MvError::DirectoryToNonDirectory(target.quote().to_string()).into()) } + } else { + move_files_into_dir(&[source.to_path_buf()], target, b) } - _ => { - if b.no_target_dir { - return Err(UUsageError::new( - 1, - format!("mv: extra operand {}", files[2].quote()), - )); + } else if target.exists() && source.is_dir() { + match b.overwrite { + OverwriteMode::NoClobber => return Ok(()), + OverwriteMode::Interactive => { + if !prompt_yes!("overwrite {}? ", target.quote()) { + return Err(io::Error::new(io::ErrorKind::Other, "").into()); + } } - let target_dir = paths.last().unwrap(); - let sources = &paths[..paths.len() - 1]; + OverwriteMode::Force => {} + }; + Err(MvError::NonDirectoryToDirectory( + source.quote().to_string(), + target.quote().to_string(), + ) + .into()) + } else { + rename(source, target, b, None).map_err(|e| USimpleError::new(1, format!("{e}"))) + } +} - move_files_into_dir(sources, target_dir, b) - } +fn handle_multiple_paths(paths: &[PathBuf], b: &Behavior) -> UResult<()> { + if b.no_target_dir { + return Err(UUsageError::new( + 1, + format!("mv: extra operand {}", paths[2].quote()), + )); } + let target_dir = paths.last().unwrap(); + let sources = &paths[..paths.len() - 1]; + + move_files_into_dir(sources, target_dir, b) } +fn exec(files: &[OsString], b: &Behavior) -> UResult<()> { + let paths = parse_paths(files, b); + + if let Some(ref name) = b.target_dir { + return move_files_into_dir(&paths, &PathBuf::from(name), b); + } + + match paths.len() { + 2 => handle_two_paths(&paths[0], &paths[1], b), + _ => handle_multiple_paths(&paths, b), + } +} + +#[allow(clippy::cognitive_complexity)] fn move_files_into_dir(files: &[PathBuf], target_dir: &Path, b: &Behavior) -> UResult<()> { if !target_dir.is_dir() { return Err(MvError::NotADirectory(target_dir.quote().to_string()).into()); @@ -378,21 +388,23 @@ fn move_files_into_dir(files: &[PathBuf], target_dir: &Path, b: &Behavior) -> UR } } - let rename_result = rename(sourcepath, &targetpath, b, multi_progress.as_ref()) - .map_err_context(|| { - format!( - "cannot move {} to {}", - sourcepath.quote(), - targetpath.quote() - ) - }); - - if let Err(e) = rename_result { - match multi_progress { - Some(ref pb) => pb.suspend(|| show!(e)), - None => show!(e), - }; - }; + match rename(sourcepath, &targetpath, b, multi_progress.as_ref()) { + Err(e) if e.to_string() == "" => set_exit_code(1), + Err(e) => { + let e = e.map_err_context(|| { + format!( + "cannot move {} to {}", + sourcepath.quote(), + targetpath.quote() + ) + }); + match multi_progress { + Some(ref pb) => pb.suspend(|| show!(e)), + None => show!(e), + }; + } + Ok(()) => (), + } if let Some(ref pb) = count_progress { pb.inc(1); @@ -410,16 +422,39 @@ fn rename( let mut backup_path = None; if to.exists() { - if b.update && b.overwrite == OverwriteMode::Interactive { + if (b.update == UpdateMode::ReplaceIfOlder || b.update == UpdateMode::ReplaceNone) + && b.overwrite == OverwriteMode::Interactive + { // `mv -i --update old new` when `new` exists doesn't move anything // and exit with 0 return Ok(()); } + if b.update == UpdateMode::ReplaceNone { + return Ok(()); + } + + if (b.update == UpdateMode::ReplaceIfOlder) + && fs::metadata(from)?.modified()? <= fs::metadata(to)?.modified()? + { + return Ok(()); + } + match b.overwrite { - OverwriteMode::NoClobber => return Ok(()), + OverwriteMode::NoClobber => { + let err_msg = if b.verbose { + println!("skipped {}", to.quote()); + String::new() + } else { + format!("not replacing {}", to.quote()) + }; + return Err(io::Error::new(io::ErrorKind::Other, err_msg)); + } OverwriteMode::Interactive => { if !prompt_yes!("overwrite {}?", to.quote()) { + if b.verbose { + println!("skipped {}", to.quote()); + } return Err(io::Error::new(io::ErrorKind::Other, "")); } } @@ -430,10 +465,6 @@ fn rename( if let Some(ref backup_path) = backup_path { rename_with_fallback(to, backup_path, multi_progress)?; } - - if b.update && fs::metadata(from)?.modified()? <= fs::metadata(to)?.modified()? { - return Ok(()); - } } // "to" may no longer exist if it was backed up @@ -453,12 +484,12 @@ fn rename( if b.verbose { let message = match backup_path { Some(path) => format!( - "{} -> {} (backup: {})", + "renamed {} -> {} (backup: {})", from.quote(), to.quote(), path.quote() ), - None => format!("{} -> {}", from.quote(), to.quote()), + None => format!("renamed {} -> {}", from.quote(), to.quote()), }; match multi_progress { diff --git a/src/uu/nice/Cargo.toml b/src/uu/nice/Cargo.toml index 054daf27375..70d6f0f87c5 100644 --- a/src/uu/nice/Cargo.toml +++ b/src/uu/nice/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_nice" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "nice ~ (uutils) run PROGRAM with modified scheduling priority" @@ -15,10 +15,10 @@ edition = "2021" path = "src/nice.rs" [dependencies] -clap = { workspace=true } -libc = { workspace=true } -nix = { workspace=true } -uucore = { workspace=true } +clap = { workspace = true } +libc = { workspace = true } +nix = { workspace = true } +uucore = { workspace = true } [[bin]] name = "nice" diff --git a/src/uu/nl/Cargo.toml b/src/uu/nl/Cargo.toml index 5dd539db845..020eba82956 100644 --- a/src/uu/nl/Cargo.toml +++ b/src/uu/nl/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_nl" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "nl ~ (uutils) display input with added line numbers" @@ -15,9 +15,9 @@ edition = "2021" path = "src/nl.rs" [dependencies] -clap = { workspace=true } -regex = { workspace=true } -uucore = { workspace=true } +clap = { workspace = true } +regex = { workspace = true } +uucore = { workspace = true } [[bin]] name = "nl" diff --git a/src/uu/nl/src/helper.rs b/src/uu/nl/src/helper.rs index fa59d329aad..a62936d75b4 100644 --- a/src/uu/nl/src/helper.rs +++ b/src/uu/nl/src/helper.rs @@ -25,6 +25,7 @@ fn parse_style(chars: &[char]) -> Result { // parse_options loads the options into the settings, returning an array of // error messages. +#[allow(clippy::cognitive_complexity)] pub fn parse_options(settings: &mut crate::Settings, opts: &clap::ArgMatches) -> Vec { // This vector holds error messages encountered. let mut errs: Vec = vec![]; diff --git a/src/uu/nl/src/nl.rs b/src/uu/nl/src/nl.rs index eb33eb3b8fd..44fdec8c72e 100644 --- a/src/uu/nl/src/nl.rs +++ b/src/uu/nl/src/nl.rs @@ -236,6 +236,7 @@ pub fn uu_app() -> Command { } // nl implements the main functionality for an individual buffer. +#[allow(clippy::cognitive_complexity)] fn nl(reader: &mut BufReader, settings: &Settings) -> UResult<()> { let regexp: regex::Regex = regex::Regex::new(r".?").unwrap(); let mut line_no = settings.starting_line_number; diff --git a/src/uu/nohup/Cargo.toml b/src/uu/nohup/Cargo.toml index f358695065a..74bdd89ae7f 100644 --- a/src/uu/nohup/Cargo.toml +++ b/src/uu/nohup/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_nohup" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "nohup ~ (uutils) run COMMAND, ignoring hangup signals" @@ -15,10 +15,10 @@ edition = "2021" path = "src/nohup.rs" [dependencies] -clap = { workspace=true } -libc = { workspace=true } -is-terminal = { workspace=true } -uucore = { workspace=true, features=["fs"] } +clap = { workspace = true } +libc = { workspace = true } +is-terminal = { workspace = true } +uucore = { workspace = true, features = ["fs"] } [[bin]] name = "nohup" diff --git a/src/uu/nproc/Cargo.toml b/src/uu/nproc/Cargo.toml index c2c224a409c..239afef5e7e 100644 --- a/src/uu/nproc/Cargo.toml +++ b/src/uu/nproc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_nproc" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "nproc ~ (uutils) display the number of processing units available" @@ -15,9 +15,9 @@ edition = "2021" path = "src/nproc.rs" [dependencies] -libc = { workspace=true } -clap = { workspace=true } -uucore = { workspace=true, features=["fs"] } +libc = { workspace = true } +clap = { workspace = true } +uucore = { workspace = true, features = ["fs"] } [[bin]] name = "nproc" diff --git a/src/uu/numfmt/Cargo.toml b/src/uu/numfmt/Cargo.toml index 2e68cbab2d9..e17b4e56eb1 100644 --- a/src/uu/numfmt/Cargo.toml +++ b/src/uu/numfmt/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_numfmt" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "numfmt ~ (uutils) reformat NUMBER" @@ -15,8 +15,8 @@ edition = "2021" path = "src/numfmt.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true } +clap = { workspace = true } +uucore = { workspace = true } [[bin]] name = "numfmt" diff --git a/src/uu/numfmt/src/format.rs b/src/uu/numfmt/src/format.rs index 3e20d00eec7..eb75f7554fd 100644 --- a/src/uu/numfmt/src/format.rs +++ b/src/uu/numfmt/src/format.rs @@ -55,7 +55,7 @@ impl<'a> Iterator for WhitespaceSplitter<'a> { .unwrap_or(field.len()), ); - self.s = if !rest.is_empty() { Some(rest) } else { None }; + self.s = if rest.is_empty() { None } else { Some(rest) }; Some((prefix, field)) } diff --git a/src/uu/numfmt/src/options.rs b/src/uu/numfmt/src/options.rs index 634dc70be6e..8369bd0c756 100644 --- a/src/uu/numfmt/src/options.rs +++ b/src/uu/numfmt/src/options.rs @@ -105,6 +105,7 @@ impl FromStr for FormatOptions { // An optional zero (%010f) will zero pad the number. // An optional negative value (%-10f) will left align. // An optional precision (%.1f) determines the precision of the number. + #[allow(clippy::cognitive_complexity)] fn from_str(s: &str) -> Result { let mut iter = s.chars().peekable(); let mut options = Self::default(); @@ -200,14 +201,12 @@ impl FromStr for FormatOptions { } } - if !precision.is_empty() { - if let Ok(p) = precision.parse() { - options.precision = Some(p); - } else { - return Err(format!("invalid precision in format '{s}'")); - } - } else { + if precision.is_empty() { options.precision = Some(0); + } else if let Ok(p) = precision.parse() { + options.precision = Some(p); + } else { + return Err(format!("invalid precision in format '{s}'")); } } diff --git a/src/uu/od/Cargo.toml b/src/uu/od/Cargo.toml index c2bc937311f..f26006d7e54 100644 --- a/src/uu/od/Cargo.toml +++ b/src/uu/od/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_od" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "od ~ (uutils) display formatted representation of input" @@ -15,10 +15,10 @@ edition = "2021" path = "src/od.rs" [dependencies] -byteorder = { workspace=true } -clap = { workspace=true } -half = { workspace=true } -uucore = { workspace=true } +byteorder = { workspace = true } +clap = { workspace = true } +half = { workspace = true } +uucore = { workspace = true } [[bin]] name = "od" diff --git a/src/uu/od/src/od.rs b/src/uu/od/src/od.rs index 5e8f8814d0c..09765ed2b48 100644 --- a/src/uu/od/src/od.rs +++ b/src/uu/od/src/od.rs @@ -171,12 +171,7 @@ impl OdOptions { None => Radix::Octal, Some(s) => { let st = s.as_bytes(); - if st.len() != 1 { - return Err(USimpleError::new( - 1, - "Radix must be one of [d, o, n, x]".to_string(), - )); - } else { + if st.len() == 1 { let radix: char = *(st .first() .expect("byte string of length 1 lacks a 0th elem")) @@ -193,6 +188,11 @@ impl OdOptions { )) } } + } else { + return Err(USimpleError::new( + 1, + "Radix must be one of [d, o, n, x]".to_string(), + )); } } }; diff --git a/src/uu/od/src/parse_formats.rs b/src/uu/od/src/parse_formats.rs index 9614ed9c04b..c414abebe8e 100644 --- a/src/uu/od/src/parse_formats.rs +++ b/src/uu/od/src/parse_formats.rs @@ -96,6 +96,7 @@ fn od_argument_with_option(ch: char) -> bool { /// arguments with parameters like -w16 can only appear at the end: -fvoxw16 /// parameters of -t/--format specify 1 or more formats. /// if -- appears on the command line, parsing should stop. +#[allow(clippy::cognitive_complexity)] pub fn parse_format_flags(args: &[String]) -> Result, String> { let mut formats = Vec::new(); diff --git a/src/uu/od/src/parse_nrofbytes.rs b/src/uu/od/src/parse_nrofbytes.rs index 4c310755b38..7d3bca03db9 100644 --- a/src/uu/od/src/parse_nrofbytes.rs +++ b/src/uu/od/src/parse_nrofbytes.rs @@ -43,7 +43,7 @@ pub fn parse_number_of_bytes(s: &str) -> Result { len -= 1; } #[cfg(target_pointer_width = "64")] - Some('E') => { + Some('E') if radix != 16 => { multiply = 1024 * 1024 * 1024 * 1024 * 1024 * 1024; len -= 1; } @@ -84,6 +84,7 @@ fn test_parse_number_of_bytes() { // hex input assert_eq!(15, parse_number_of_bytes("0xf").unwrap()); + assert_eq!(14, parse_number_of_bytes("0XE").unwrap()); assert_eq!(15, parse_number_of_bytes("0XF").unwrap()); assert_eq!(27, parse_number_of_bytes("0x1b").unwrap()); assert_eq!(16 * 1024, parse_number_of_bytes("0x10k").unwrap()); diff --git a/src/uu/paste/Cargo.toml b/src/uu/paste/Cargo.toml index aa1f5fe90c3..e9a78d828bc 100644 --- a/src/uu/paste/Cargo.toml +++ b/src/uu/paste/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_paste" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "paste ~ (uutils) merge lines from inputs" @@ -15,8 +15,8 @@ edition = "2021" path = "src/paste.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true } +clap = { workspace = true } +uucore = { workspace = true } [[bin]] name = "paste" diff --git a/src/uu/paste/paste.md b/src/uu/paste/paste.md new file mode 100644 index 00000000000..74160588894 --- /dev/null +++ b/src/uu/paste/paste.md @@ -0,0 +1,8 @@ +# paste + +``` +paste [OPTIONS] [FILE]... +``` + +Write lines consisting of the sequentially corresponding lines from each +`FILE`, separated by `TAB`s, to standard output. diff --git a/src/uu/paste/src/paste.rs b/src/uu/paste/src/paste.rs index 3635ae967e2..6e52727f7ce 100644 --- a/src/uu/paste/src/paste.rs +++ b/src/uu/paste/src/paste.rs @@ -12,10 +12,11 @@ use std::fmt::Display; use std::fs::File; use std::io::{stdin, stdout, BufRead, BufReader, Read, Write}; use std::path::Path; -use uucore::error::{FromIo, UResult}; +use uucore::error::{FromIo, UResult, USimpleError}; +use uucore::{format_usage, help_about, help_usage}; -static ABOUT: &str = "Write lines consisting of the sequentially corresponding lines from each -FILE, separated by TABs, to standard output."; +const ABOUT: &str = help_about!("paste.md"); +const USAGE: &str = help_usage!("paste.md"); mod options { pub const DELIMITER: &str = "delimiters"; @@ -76,6 +77,7 @@ pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) + .override_usage(format_usage(USAGE)) .infer_long_args(true) .arg( Arg::new(options::SERIAL) @@ -109,6 +111,7 @@ pub fn uu_app() -> Command { ) } +#[allow(clippy::cognitive_complexity)] fn paste( filenames: Vec, serial: bool, @@ -127,6 +130,16 @@ fn paste( files.push(file); } + if delimiters.ends_with('\\') && !delimiters.ends_with("\\\\") { + return Err(USimpleError::new( + 1, + format!( + "delimiter list ends with an unescaped backslash: {}", + delimiters + ), + )); + } + let delimiters: Vec = unescape(delimiters).chars().collect(); let mut delim_count = 0; let mut delim_length = 1; @@ -220,10 +233,8 @@ fn paste( } // Unescape all special characters -// TODO: this will need work to conform to GNU implementation fn unescape(s: &str) -> String { s.replace("\\n", "\n") .replace("\\t", "\t") .replace("\\\\", "\\") - .replace('\\', "") } diff --git a/src/uu/pathchk/Cargo.toml b/src/uu/pathchk/Cargo.toml index 134f5bb5d27..f11d85b5cc4 100644 --- a/src/uu/pathchk/Cargo.toml +++ b/src/uu/pathchk/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_pathchk" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "pathchk ~ (uutils) diagnose invalid or non-portable PATHNAME" @@ -15,9 +15,9 @@ edition = "2021" path = "src/pathchk.rs" [dependencies] -clap = { workspace=true } -libc = { workspace=true } -uucore = { workspace=true } +clap = { workspace = true } +libc = { workspace = true } +uucore = { workspace = true } [[bin]] name = "pathchk" diff --git a/src/uu/pinky/Cargo.toml b/src/uu/pinky/Cargo.toml index a9abee40b9c..ee17a46c5bd 100644 --- a/src/uu/pinky/Cargo.toml +++ b/src/uu/pinky/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_pinky" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "pinky ~ (uutils) display user information" @@ -15,8 +15,8 @@ edition = "2021" path = "src/pinky.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true, features=["utmpx", "entries"] } +clap = { workspace = true } +uucore = { workspace = true, features = ["utmpx", "entries"] } [[bin]] name = "pinky" diff --git a/src/uu/pinky/pinky.md b/src/uu/pinky/pinky.md new file mode 100644 index 00000000000..965ae4cd090 --- /dev/null +++ b/src/uu/pinky/pinky.md @@ -0,0 +1,7 @@ +# pinky + +``` +pinky [OPTION]... [USER]... +``` + +Displays brief user information on Unix-based systems diff --git a/src/uu/pinky/src/pinky.rs b/src/uu/pinky/src/pinky.rs index 7e21fba9ea4..172a9e13803 100644 --- a/src/uu/pinky/src/pinky.rs +++ b/src/uu/pinky/src/pinky.rs @@ -20,10 +20,10 @@ use std::os::unix::fs::MetadataExt; use clap::{crate_version, Arg, ArgAction, Command}; use std::path::PathBuf; -use uucore::format_usage; +use uucore::{format_usage, help_about, help_usage}; -static ABOUT: &str = "Lightweight finger"; -const USAGE: &str = "{} [OPTION]... [USER]..."; +const ABOUT: &str = help_about!("pinky.md"); +const USAGE: &str = help_usage!("pinky.md"); mod options { pub const LONG_FORMAT: &str = "long_format"; @@ -218,10 +218,10 @@ impl Capitalize for str { fn capitalize(&self) -> String { self.char_indices() .fold(String::with_capacity(self.len()), |mut acc, x| { - if x.0 != 0 { - acc.push(x.1); - } else { + if x.0 == 0 { acc.push(x.1.to_ascii_uppercase()); + } else { + acc.push(x.1); } acc }) @@ -281,10 +281,10 @@ impl Pinky { match pts_path.metadata() { #[allow(clippy::unnecessary_cast)] Ok(meta) => { - mesg = if meta.mode() & S_IWGRP as u32 != 0 { - ' ' - } else { + mesg = if meta.mode() & S_IWGRP as u32 == 0 { '*' + } else { + ' ' }; last_change = meta.atime(); } @@ -312,10 +312,10 @@ impl Pinky { print!(" {}{:<8.*}", mesg, utmpx::UT_LINESIZE, ut.tty_device()); if self.include_idle { - if last_change != 0 { - print!(" {:<6}", idle_string(last_change)); - } else { + if last_change == 0 { print!(" {:<6}", "?????"); + } else { + print!(" {:<6}", idle_string(last_change)); } } diff --git a/src/uu/pr/Cargo.toml b/src/uu/pr/Cargo.toml index 773a71b7fb3..bae1b251da3 100644 --- a/src/uu/pr/Cargo.toml +++ b/src/uu/pr/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_pr" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "pr ~ (uutils) convert text files for printing" @@ -15,12 +15,12 @@ edition = "2021" path = "src/pr.rs" [dependencies] -clap = { workspace=true } -time = { workspace=true, features = ["local-offset", "macros", "formatting"] } -uucore = { workspace=true, features=["entries"] } -quick-error = { workspace=true } -itertools = { workspace=true } -regex = { workspace=true } +clap = { workspace = true } +uucore = { workspace = true, features = ["entries"] } +quick-error = { workspace = true } +itertools = { workspace = true } +regex = { workspace = true } +chrono = { workspace = true } [[bin]] name = "pr" diff --git a/src/uu/pr/pr.md b/src/uu/pr/pr.md new file mode 100644 index 00000000000..2c246b25f1c --- /dev/null +++ b/src/uu/pr/pr.md @@ -0,0 +1,27 @@ +# pr + +``` +pr [OPTIONS] [files]... +``` + +Write content of given file or standard input to standard output with pagination filter + +## After help + +`+PAGE` Begin output at page number page of the formatted input. +`-COLUMN` Produce multi-column output. See `--column` + +The pr utility is a printing and pagination filter for text files. +When multiple input files are specified, each is read, formatted, and written to standard output. +By default, the input is separated into 66-line pages, each with + +* A 5-line header with the page number, date, time, and the pathname of the file. +* A 5-line trailer consisting of blank lines. + +If standard output is associated with a terminal, diagnostic messages are suppressed until the pr +utility has completed processing. + +When multiple column output is specified, text columns are of equal width. +By default, text columns are separated by at least one ``. +Input lines that do not fit into a text column are truncated. +Lines are not truncated under single column output. diff --git a/src/uu/pr/src/pr.rs b/src/uu/pr/src/pr.rs index 9f1b6448377..37674bad7a6 100644 --- a/src/uu/pr/src/pr.rs +++ b/src/uu/pr/src/pr.rs @@ -6,6 +6,7 @@ // spell-checker:ignore (ToDO) adFfmprt, kmerge +use chrono::{DateTime, Local}; use clap::{crate_version, Arg, ArgAction, ArgMatches, Command}; use itertools::Itertools; use quick_error::ResultExt; @@ -15,39 +16,15 @@ use std::fs::{metadata, File}; use std::io::{stdin, stdout, BufRead, BufReader, Lines, Read, Write}; #[cfg(unix)] use std::os::unix::fs::FileTypeExt; -use time::macros::format_description; -use time::OffsetDateTime; use quick_error::quick_error; use uucore::display::Quotable; use uucore::error::UResult; +use uucore::{format_usage, help_about, help_section, help_usage}; -const ABOUT: &str = - "Write content of given file or standard input to standard output with pagination filter"; -const AFTER_HELP: &str = - " +PAGE\n Begin output at page number page of the formatted input. - -COLUMN\n Produce multi-column output. See --column - -The pr utility is a printing and pagination filter -for text files. When multiple input files are specified, -each is read, formatted, and written to standard -output. By default, the input is separated -into 66-line pages, each with - -o A 5-line header with the page number, date, - time, and the pathname of the file. - -o A 5-line trailer consisting of blank lines. - -If standard output is associated with a terminal, -diagnostic messages are suppressed until the pr -utility has completed processing. - -When multiple column output is specified, text columns -are of equal width. By default text columns -are separated by at least one . Input lines -that do not fit into a text column are truncated. -Lines are not truncated under single column output."; +const ABOUT: &str = help_about!("pr.md"); +const USAGE: &str = help_usage!("pr.md"); +const AFTER_HELP: &str = help_section!("after help", "pr.md"); const TAB: char = '\t'; const LINES_PER_PAGE: usize = 66; const LINES_PER_PAGE_FOR_FORM_FEED: usize = 63; @@ -59,8 +36,7 @@ const DEFAULT_COLUMN_WIDTH: usize = 72; const DEFAULT_COLUMN_WIDTH_WITH_S_OPTION: usize = 512; const DEFAULT_COLUMN_SEPARATOR: &char = &TAB; const FF: u8 = 0x0C_u8; -const DATE_TIME_FORMAT: &[time::format_description::FormatItem] = - format_description!("[month repr:short] [day] [hour]:[minute] [year]"); +const DATE_TIME_FORMAT: &str = "%b %d %H:%M %Y"; mod options { pub const HEADER: &str = "header"; @@ -195,6 +171,7 @@ pub fn uu_app() -> Command { .version(crate_version!()) .about(ABOUT) .after_help(AFTER_HELP) + .override_usage(format_usage(USAGE)) .infer_long_args(true) .args_override_self(true) .disable_help_flag(true) @@ -509,6 +486,7 @@ fn parse_usize(matches: &ArgMatches, opt: &str) -> Option .map(from_parse_error_to_pr_error) } +#[allow(clippy::cognitive_complexity)] fn build_options( matches: &ArgMatches, paths: &[&str], @@ -591,10 +569,8 @@ fn build_options( let line_separator = "\n".to_string(); let last_modified_time = if is_merge_mode || paths[0].eq(FILE_STDIN) { - // let date_time = Local::now(); - // date_time.format("%b %d %H:%M %Y").to_string() - let date_time = OffsetDateTime::now_local().unwrap(); - date_time.format(&DATE_TIME_FORMAT).unwrap() + let date_time = Local::now(); + date_time.format(DATE_TIME_FORMAT).to_string() } else { file_last_modified_time(paths.first().unwrap()) }; @@ -1042,6 +1018,7 @@ fn print_page( Ok(lines_written) } +#[allow(clippy::cognitive_complexity)] fn write_columns( lines: &[FileLine], options: &OutputOptions, @@ -1234,12 +1211,8 @@ fn file_last_modified_time(path: &str) -> String { .map(|i| { i.modified() .map(|x| { - let date_time: OffsetDateTime = x.into(); - let offset = OffsetDateTime::now_local().unwrap().offset(); - date_time - .to_offset(offset) - .format(&DATE_TIME_FORMAT) - .unwrap() + let date_time: DateTime = x.into(); + date_time.format(DATE_TIME_FORMAT).to_string() }) .unwrap_or_default() }) diff --git a/src/uu/printenv/Cargo.toml b/src/uu/printenv/Cargo.toml index 7f179e4114d..59dcee77827 100644 --- a/src/uu/printenv/Cargo.toml +++ b/src/uu/printenv/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_printenv" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "printenv ~ (uutils) display value of environment VAR" @@ -15,8 +15,8 @@ edition = "2021" path = "src/printenv.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true } +clap = { workspace = true } +uucore = { workspace = true } [[bin]] name = "printenv" diff --git a/src/uu/printf/Cargo.toml b/src/uu/printf/Cargo.toml index ffb266f188a..51812945f4c 100644 --- a/src/uu/printf/Cargo.toml +++ b/src/uu/printf/Cargo.toml @@ -1,10 +1,7 @@ [package] name = "uu_printf" -version = "0.0.17" -authors = [ - "Nathan Ross", - "uutils developers", -] +version = "0.0.19" +authors = ["Nathan Ross", "uutils developers"] license = "MIT" description = "printf ~ (uutils) FORMAT and display ARGUMENTS" @@ -18,8 +15,8 @@ edition = "2021" path = "src/printf.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true, features=["memo"] } +clap = { workspace = true } +uucore = { workspace = true, features = ["memo"] } [[bin]] name = "printf" diff --git a/src/uu/printf/printf.md b/src/uu/printf/printf.md new file mode 100644 index 00000000000..60b50354c6f --- /dev/null +++ b/src/uu/printf/printf.md @@ -0,0 +1,264 @@ + + +# printf + +``` +printf FORMATSTRING [ARGUMENT]... +``` + +Print output based off of the format string and proceeding arguments. + +## After Help + +basic anonymous string templating: + +prints format string at least once, repeating as long as there are remaining arguments +output prints escaped literals in the format string as character literals +output replaces anonymous fields with the next unused argument, formatted according to the field. + +Prints the `,` replacing escaped character sequences with character literals +and substitution field sequences with passed arguments + +literally, with the exception of the below +escaped character sequences, and the substitution sequences described further down. + +### ESCAPE SEQUENCES + +The following escape sequences, organized here in alphabetical order, +will print the corresponding character literal: + +* `\"` double quote + +* `\\\\` backslash + +* `\\a` alert (BEL) + +* `\\b` backspace + +* `\\c` End-of-Input + +* `\\e` escape + +* `\\f` form feed + +* `\\n` new line + +* `\\r` carriage return + +* `\\t` horizontal tab + +* `\\v` vertical tab + +* `\\NNN` byte with value expressed in octal value NNN (1 to 3 digits) + values greater than 256 will be treated + +* `\\xHH` byte with value expressed in hexadecimal value NN (1 to 2 digits) + +* `\\uHHHH` Unicode (IEC 10646) character with value expressed in hexadecimal value HHHH (4 digits) + +* `\\uHHHH` Unicode character with value expressed in hexadecimal value HHHH (8 digits) + +* `%%` a single % + +### SUBSTITUTIONS + +#### SUBSTITUTION QUICK REFERENCE + +Fields + +* `%s`: string +* `%b`: string parsed for literals second parameter is max length + +* `%c`: char no second parameter + +* `%i` or `%d`: 64-bit integer +* `%u`: 64 bit unsigned integer +* `%x` or `%X`: 64-bit unsigned integer as hex +* `%o`: 64-bit unsigned integer as octal + second parameter is min-width, integer + output below that width is padded with leading zeroes + +* `%f` or `%F`: decimal floating point value +* `%e` or `%E`: scientific notation floating point value +* `%g` or `%G`: shorter of specially interpreted decimal or SciNote floating point value. + second parameter is + `-max` places after decimal point for floating point output + `-max` number of significant digits for scientific notation output + +parameterizing fields + +examples: + +``` +printf '%4.3i' 7 +``` + +It has a first parameter of 4 and a second parameter of 3 and will result in ' 007' + +``` +printf '%.1s' abcde +``` + +It has no first parameter and a second parameter of 1 and will result in 'a' + +``` +printf '%4c' q +``` + +It has a first parameter of 4 and no second parameter and will result in ' q' + +The first parameter of a field is the minimum width to pad the output to +if the output is less than this absolute value of this width, +it will be padded with leading spaces, or, if the argument is negative, +with trailing spaces. the default is zero. + +The second parameter of a field is particular to the output field type. +defaults can be found in the full substitution help below + +special prefixes to numeric arguments + +* `0`: (e.g. 010) interpret argument as octal (integer output fields only) +* `0x`: (e.g. 0xABC) interpret argument as hex (numeric output fields only) +* `\'`: (e.g. \'a) interpret argument as a character constant + +#### HOW TO USE SUBSTITUTIONS + +Substitutions are used to pass additional argument(s) into the FORMAT string, to be formatted a +particular way. E.g. + +``` +printf 'the letter %X comes before the letter %X' 10 11 +``` + +will print + +``` +the letter A comes before the letter B +``` + +because the substitution field `%X` means +'take an integer argument and write it as a hexadecimal number' + +Passing more arguments than are in the format string will cause the format string to be +repeated for the remaining substitutions + +``` +printf 'it is %i F in %s \n' 22 Portland 25 Boston 27 New York +``` + +will print + +``` +it is 22 F in Portland +it is 25 F in Boston +it is 27 F in Boston +``` + +If a format string is printed but there are less arguments remaining +than there are substitution fields, substitution fields without +an argument will default to empty strings, or for numeric fields +the value 0 + +#### AVAILABLE SUBSTITUTIONS + +This program, like GNU coreutils printf, +interprets a modified subset of the POSIX C printf spec, +a quick reference to substitutions is below. + +#### STRING SUBSTITUTIONS + +All string fields have a 'max width' parameter +`%.3s` means 'print no more than three characters of the original input' + +* `%s`: string + +* `%b`: escaped string - the string will be checked for any escaped literals from + the escaped literal list above, and translate them to literal characters. + e.g. `\\n` will be transformed into a newline character. + One special rule about `%b` mode is that octal literals are interpreted differently + In arguments passed by `%b`, pass octal-interpreted literals must be in the form of `\\0NNN` + instead of `\\NNN`. (Although, for legacy reasons, octal literals in the form of `\\NNN` will + still be interpreted and not throw a warning, you will have problems if you use this for a + literal whose code begins with zero, as it will be viewed as in `\\0NNN` form.) + +#### CHAR SUBSTITUTIONS + +The character field does not have a secondary parameter. + +* `%c`: a single character + +#### INTEGER SUBSTITUTIONS + +All integer fields have a 'pad with zero' parameter +`%.4i` means an integer which if it is less than 4 digits in length, +is padded with leading zeros until it is 4 digits in length. + +* `%d` or `%i`: 64-bit integer + +* `%u`: 64-bit unsigned integer + +* `%x` or `%X`: 64-bit unsigned integer printed in Hexadecimal (base 16) + `%X` instead of `%x` means to use uppercase letters for 'a' through 'f' + +* `%o`: 64-bit unsigned integer printed in octal (base 8) + +#### FLOATING POINT SUBSTITUTIONS + +All floating point fields have a 'max decimal places / max significant digits' parameter +`%.10f` means a decimal floating point with 7 decimal places past 0 +`%.10e` means a scientific notation number with 10 significant digits +`%.10g` means the same behavior for decimal and Sci. Note, respectively, and provides the shortest +of each's output. + +Like with GNU coreutils, the value after the decimal point is these outputs is parsed as a +double first before being rendered to text. For both implementations do not expect meaningful +precision past the 18th decimal place. When using a number of decimal places that is 18 or +higher, you can expect variation in output between GNU coreutils printf and this printf at the +18th decimal place of +/- 1 + +* `%f`: floating point value presented in decimal, truncated and displayed to 6 decimal places by + default. There is not past-double behavior parity with Coreutils printf, values are not + estimated or adjusted beyond input values. + +* `%e` or `%E`: floating point value presented in scientific notation + 7 significant digits by default + `%E` means use to use uppercase E for the mantissa. + +* `%g` or `%G`: floating point value presented in the shortest of decimal and scientific notation + behaves differently from `%f` and `%E`, please see posix printf spec for full details, + some examples of different behavior: + Sci Note has 6 significant digits by default + Trailing zeroes are removed + Instead of being truncated, digit after last is rounded + +Like other behavior in this utility, the design choices of floating point +behavior in this utility is selected to reproduce in exact +the behavior of GNU coreutils' printf from an inputs and outputs standpoint. + +### USING PARAMETERS + +Most substitution fields can be parameterized using up to 2 numbers that can +be passed to the field, between the % sign and the field letter. + +The 1st parameter always indicates the minimum width of output, it is useful for creating +columnar output. Any output that would be less than this minimum width is padded with +leading spaces +The 2nd parameter is proceeded by a dot. +You do not have to use parameters + +### SPECIAL FORMS OF INPUT + +For numeric input, the following additional forms of input are accepted besides decimal: + +Octal (only with integer): if the argument begins with a 0 the proceeding characters +will be interpreted as octal (base 8) for integer fields + +Hexadecimal: if the argument begins with 0x the proceeding characters will be interpreted +will be interpreted as hex (base 16) for any numeric fields +for float fields, hexadecimal input results in a precision +limit (in converting input past the decimal point) of 10^-15 + +Character Constant: if the argument begins with a single quote character, the first byte +of the next character will be interpreted as an 8-bit unsigned integer. If there are +additional bytes, they will throw an error (unless the environment variable POSIXLY_CORRECT +is set) diff --git a/src/uu/printf/src/printf.rs b/src/uu/printf/src/printf.rs index 2592f212d1f..bf79369ccab 100644 --- a/src/uu/printf/src/printf.rs +++ b/src/uu/printf/src/printf.rs @@ -4,265 +4,14 @@ use clap::{crate_version, Arg, ArgAction, Command}; use uucore::error::{UResult, UUsageError}; -use uucore::format_usage; use uucore::memo::printf; +use uucore::{format_usage, help_about, help_section, help_usage}; const VERSION: &str = "version"; const HELP: &str = "help"; -const USAGE: &str = "{} FORMATSTRING [ARGUMENT]..."; -const ABOUT: &str = "Print output based off of the format string and proceeding arguments."; -const AFTER_HELP: &str = " -basic anonymous string templating: - -prints format string at least once, repeating as long as there are remaining arguments -output prints escaped literals in the format string as character literals -output replaces anonymous fields with the next unused argument, formatted according to the field. - -Prints the , replacing escaped character sequences with character literals - and substitution field sequences with passed arguments - -literally, with the exception of the below - escaped character sequences, and the substitution sequences described further down. - -ESCAPE SEQUENCES - -The following escape sequences, organized here in alphabetical order, -will print the corresponding character literal: - -\" double quote - -\\\\ backslash - -\\a alert (BEL) - -\\b backspace - -\\c End-of-Input - -\\e escape - -\\f form feed - -\\n new line - -\\r carriage return - -\\t horizontal tab - -\\v vertical tab - -\\NNN byte with value expressed in octal value NNN (1 to 3 digits) - values greater than 256 will be treated - -\\xHH byte with value expressed in hexadecimal value NN (1 to 2 digits) - -\\uHHHH Unicode (IEC 10646) character with value expressed in hexadecimal value HHHH (4 digits) - -\\uHHHH Unicode character with value expressed in hexadecimal value HHHH (8 digits) - -%% a single % - -SUBSTITUTIONS - -SUBSTITUTION QUICK REFERENCE - -Fields - -%s - string -%b - string parsed for literals - second parameter is max length - -%c - char - no second parameter - -%i or %d - 64-bit integer -%u - 64 bit unsigned integer -%x or %X - 64-bit unsigned integer as hex -%o - 64-bit unsigned integer as octal - second parameter is min-width, integer - output below that width is padded with leading zeroes - -%f or %F - decimal floating point value -%e or %E - scientific notation floating point value -%g or %G - shorter of specially interpreted decimal or SciNote floating point value. - second parameter is - -max places after decimal point for floating point output - -max number of significant digits for scientific notation output - -parameterizing fields - -examples: - -printf '%4.3i' 7 -has a first parameter of 4 - and a second parameter of 3 -will result in ' 007' - -printf '%.1s' abcde -has no first parameter - and a second parameter of 1 -will result in 'a' - -printf '%4c' q -has a first parameter of 4 - and no second parameter -will result in ' q' - -The first parameter of a field is the minimum width to pad the output to - if the output is less than this absolute value of this width, - it will be padded with leading spaces, or, if the argument is negative, - with trailing spaces. the default is zero. - -The second parameter of a field is particular to the output field type. - defaults can be found in the full substitution help below - -special prefixes to numeric arguments - 0 (e.g. 010) - interpret argument as octal (integer output fields only) - 0x (e.g. 0xABC) - interpret argument as hex (numeric output fields only) - \' (e.g. \'a) - interpret argument as a character constant - -HOW TO USE SUBSTITUTIONS - -Substitutions are used to pass additional argument(s) into the FORMAT string, to be formatted a -particular way. E.g. - - printf 'the letter %X comes before the letter %X' 10 11 - -will print - - 'the letter A comes before the letter B' - -because the substitution field %X means -'take an integer argument and write it as a hexadecimal number' - -Passing more arguments than are in the format string will cause the format string to be - repeated for the remaining substitutions - - printf 'it is %i F in %s \n' 22 Portland 25 Boston 27 New York - -will print - - 'it is 22 F in Portland - it is 25 F in Boston - it is 27 F in Boston - ' -If a format string is printed but there are less arguments remaining - than there are substitution fields, substitution fields without - an argument will default to empty strings, or for numeric fields - the value 0 - -AVAILABLE SUBSTITUTIONS - -This program, like GNU coreutils printf, -interprets a modified subset of the POSIX C printf spec, -a quick reference to substitutions is below. - - STRING SUBSTITUTIONS - All string fields have a 'max width' parameter - %.3s means 'print no more than three characters of the original input' - - %s - string - - %b - escaped string - the string will be checked for any escaped literals from - the escaped literal list above, and translate them to literal characters. - e.g. \\n will be transformed into a newline character. - - One special rule about %b mode is that octal literals are interpreted differently - In arguments passed by %b, pass octal-interpreted literals must be in the form of \\0NNN - instead of \\NNN. (Although, for legacy reasons, octal literals in the form of \\NNN will - still be interpreted and not throw a warning, you will have problems if you use this for a - literal whose code begins with zero, as it will be viewed as in \\0NNN form.) - - CHAR SUBSTITUTIONS - The character field does not have a secondary parameter. - - %c - a single character - - INTEGER SUBSTITUTIONS - All integer fields have a 'pad with zero' parameter - %.4i means an integer which if it is less than 4 digits in length, - is padded with leading zeros until it is 4 digits in length. - - %d or %i - 64-bit integer - - %u - 64 bit unsigned integer - - %x or %X - 64 bit unsigned integer printed in Hexadecimal (base 16) - %X instead of %x means to use uppercase letters for 'a' through 'f' - - %o - 64 bit unsigned integer printed in octal (base 8) - - FLOATING POINT SUBSTITUTIONS - - All floating point fields have a 'max decimal places / max significant digits' parameter - %.10f means a decimal floating point with 7 decimal places past 0 - %.10e means a scientific notation number with 10 significant digits - %.10g means the same behavior for decimal and Sci. Note, respectively, and provides the shorter - of each's output. - - Like with GNU coreutils, the value after the decimal point is these outputs is parsed as a - double first before being rendered to text. For both implementations do not expect meaningful - precision past the 18th decimal place. When using a number of decimal places that is 18 or - higher, you can expect variation in output between GNU coreutils printf and this printf at the - 18th decimal place of +/- 1 - - %f - floating point value presented in decimal, truncated and displayed to 6 decimal places by - default. There is not past-double behavior parity with Coreutils printf, values are not - estimated or adjusted beyond input values. - - %e or %E - floating point value presented in scientific notation - 7 significant digits by default - %E means use to use uppercase E for the mantissa. - - %g or %G - floating point value presented in the shorter of decimal and scientific notation - behaves differently from %f and %E, please see posix printf spec for full details, - some examples of different behavior: - - Sci Note has 6 significant digits by default - Trailing zeroes are removed - Instead of being truncated, digit after last is rounded - - Like other behavior in this utility, the design choices of floating point - behavior in this utility is selected to reproduce in exact - the behavior of GNU coreutils' printf from an inputs and outputs standpoint. - -USING PARAMETERS - Most substitution fields can be parameterized using up to 2 numbers that can - be passed to the field, between the % sign and the field letter. - - The 1st parameter always indicates the minimum width of output, it is useful for creating - columnar output. Any output that would be less than this minimum width is padded with - leading spaces - The 2nd parameter is proceeded by a dot. - You do not have to use parameters - -SPECIAL FORMS OF INPUT - For numeric input, the following additional forms of input are accepted besides decimal: - - Octal (only with integer): if the argument begins with a 0 the proceeding characters - will be interpreted as octal (base 8) for integer fields - - Hexadecimal: if the argument begins with 0x the proceeding characters will be interpreted - will be interpreted as hex (base 16) for any numeric fields - for float fields, hexadecimal input results in a precision - limit (in converting input past the decimal point) of 10^-15 - - Character Constant: if the argument begins with a single quote character, the first byte - of the next character will be interpreted as an 8-bit unsigned integer. If there are - additional bytes, they will throw an error (unless the environment variable POSIXLY_CORRECT - is set) - -WRITTEN BY : - Nathan E. Ross, et al. for the uutils project - -MORE INFO : - https://github.com/uutils/coreutils - -COPYRIGHT : - Copyright 2015 uutils project. - Licensed under the MIT License, please see LICENSE file for details - -"; +const USAGE: &str = help_usage!("printf.md"); +const ABOUT: &str = help_about!("printf.md"); +const AFTER_HELP: &str = help_section!("after help", "printf.md"); mod options { pub const FORMATSTRING: &str = "FORMATSTRING"; diff --git a/src/uu/ptx/Cargo.toml b/src/uu/ptx/Cargo.toml index 1cb2857d3b4..358c640db1c 100644 --- a/src/uu/ptx/Cargo.toml +++ b/src/uu/ptx/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_ptx" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "ptx ~ (uutils) display a permuted index of input" @@ -15,9 +15,9 @@ edition = "2021" path = "src/ptx.rs" [dependencies] -clap = { workspace=true } -regex = { workspace=true } -uucore = { workspace=true } +clap = { workspace = true } +regex = { workspace = true } +uucore = { workspace = true } [[bin]] name = "ptx" diff --git a/src/uu/ptx/src/ptx.rs b/src/uu/ptx/src/ptx.rs index 4d411ed311e..ecfd67ce815 100644 --- a/src/uu/ptx/src/ptx.rs +++ b/src/uu/ptx/src/ptx.rs @@ -107,6 +107,7 @@ struct WordFilter { } impl WordFilter { + #[allow(clippy::cognitive_complexity)] fn new(matches: &clap::ArgMatches, config: &Config) -> UResult { let (o, oset): (bool, HashSet) = if matches.contains_id(options::ONLY_FILE) { let words = diff --git a/src/uu/pwd/Cargo.toml b/src/uu/pwd/Cargo.toml index 2be20460811..60cb9aae004 100644 --- a/src/uu/pwd/Cargo.toml +++ b/src/uu/pwd/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_pwd" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "pwd ~ (uutils) display current working directory" @@ -15,8 +15,8 @@ edition = "2021" path = "src/pwd.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true } +clap = { workspace = true } +uucore = { workspace = true } [[bin]] name = "pwd" diff --git a/src/uu/pwd/src/pwd.rs b/src/uu/pwd/src/pwd.rs index 7af1948726d..9e04dd38bec 100644 --- a/src/uu/pwd/src/pwd.rs +++ b/src/uu/pwd/src/pwd.rs @@ -115,7 +115,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // if POSIXLY_CORRECT is set, we want to a logical resolution. // This produces a different output when doing mkdir -p a/b && ln -s a/b c && cd c && pwd // We should get c in this case instead of a/b at the end of the path - let cwd = if matches.get_flag(OPT_LOGICAL) || env::var("POSIXLY_CORRECT").is_ok() { + let cwd = if matches.get_flag(OPT_PHYSICAL) { + physical_path() + } else if matches.get_flag(OPT_LOGICAL) || env::var("POSIXLY_CORRECT").is_ok() { logical_path() } else { physical_path() diff --git a/src/uu/readlink/Cargo.toml b/src/uu/readlink/Cargo.toml index 59acd7731c4..4a0ad66e576 100644 --- a/src/uu/readlink/Cargo.toml +++ b/src/uu/readlink/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_readlink" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "readlink ~ (uutils) display resolved path of PATHNAME" @@ -15,8 +15,8 @@ edition = "2021" path = "src/readlink.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true, features=["fs"] } +clap = { workspace = true } +uucore = { workspace = true, features = ["fs"] } [[bin]] name = "readlink" diff --git a/src/uu/readlink/readlink.md b/src/uu/readlink/readlink.md new file mode 100644 index 00000000000..7215acbec09 --- /dev/null +++ b/src/uu/readlink/readlink.md @@ -0,0 +1,7 @@ +# readlink + +``` +readlink [OPTION]... [FILE]... +``` + +Print value of a symbolic link or canonical file name. diff --git a/src/uu/readlink/src/readlink.rs b/src/uu/readlink/src/readlink.rs index 8afb184be98..e247c614695 100644 --- a/src/uu/readlink/src/readlink.rs +++ b/src/uu/readlink/src/readlink.rs @@ -14,10 +14,10 @@ use std::path::{Path, PathBuf}; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError, UUsageError}; use uucore::fs::{canonicalize, MissingHandling, ResolveMode}; -use uucore::{format_usage, show_error}; +use uucore::{format_usage, help_about, help_usage, show_error}; -const ABOUT: &str = "Print value of a symbolic link or canonical file name."; -const USAGE: &str = "{} [OPTION]... [FILE]..."; +const ABOUT: &str = help_about!("readlink.md"); +const USAGE: &str = help_usage!("readlink.md"); const OPT_CANONICALIZE: &str = "canonicalize"; const OPT_CANONICALIZE_MISSING: &str = "canonicalize-missing"; const OPT_CANONICALIZE_EXISTING: &str = "canonicalize-existing"; @@ -99,7 +99,7 @@ pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) - .override_help(format_usage(USAGE)) + .override_usage(format_usage(USAGE)) .infer_long_args(true) .arg( Arg::new(OPT_CANONICALIZE) diff --git a/src/uu/realpath/Cargo.toml b/src/uu/realpath/Cargo.toml index cf23d60af7b..9b2b5352c4d 100644 --- a/src/uu/realpath/Cargo.toml +++ b/src/uu/realpath/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_realpath" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "realpath ~ (uutils) display resolved absolute path of PATHNAME" @@ -15,8 +15,8 @@ edition = "2021" path = "src/realpath.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true, features=["fs"] } +clap = { workspace = true } +uucore = { workspace = true, features = ["fs"] } [[bin]] name = "realpath" diff --git a/src/uu/relpath/Cargo.toml b/src/uu/relpath/Cargo.toml index 99e37f5f883..4108d612c01 100644 --- a/src/uu/relpath/Cargo.toml +++ b/src/uu/relpath/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_relpath" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "relpath ~ (uutils) display relative path of PATHNAME_TO from PATHNAME_FROM" @@ -15,8 +15,8 @@ edition = "2021" path = "src/relpath.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true, features=["fs"] } +clap = { workspace = true } +uucore = { workspace = true, features = ["fs"] } [[bin]] name = "relpath" diff --git a/src/uu/rm/Cargo.toml b/src/uu/rm/Cargo.toml index af63aee7848..ec46031d063 100644 --- a/src/uu/rm/Cargo.toml +++ b/src/uu/rm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_rm" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "rm ~ (uutils) remove PATHNAME" @@ -15,15 +15,15 @@ edition = "2021" path = "src/rm.rs" [dependencies] -clap = { workspace=true } -walkdir = { workspace=true } -uucore = { workspace=true, features=["fs"] } +clap = { workspace = true } +walkdir = { workspace = true } +uucore = { workspace = true, features = ["fs"] } [target.'cfg(unix)'.dependencies] -libc = { workspace=true } +libc = { workspace = true } [target.'cfg(windows)'.dependencies] -windows-sys = { workspace=true, features = ["Win32_Storage_FileSystem"] } +windows-sys = { workspace = true, features = ["Win32_Storage_FileSystem"] } [[bin]] name = "rm" diff --git a/src/uu/rm/src/rm.rs b/src/uu/rm/src/rm.rs index 05d03cee2b2..f2e2050d9f9 100644 --- a/src/uu/rm/src/rm.rs +++ b/src/uu/rm/src/rm.rs @@ -7,8 +7,9 @@ // spell-checker:ignore (path) eacces -use clap::{crate_version, parser::ValueSource, Arg, ArgAction, Command}; +use clap::{builder::ValueParser, crate_version, parser::ValueSource, Arg, ArgAction, Command}; use std::collections::VecDeque; +use std::ffi::{OsStr, OsString}; use std::fs::{self, File, Metadata}; use std::io::ErrorKind; use std::ops::BitOr; @@ -59,9 +60,9 @@ static ARG_FILES: &str = "files"; pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().after_help(AFTER_HELP).try_get_matches_from(args)?; - let files: Vec = matches - .get_many::(ARG_FILES) - .map(|v| v.map(ToString::to_string).collect()) + let files: Vec<&OsStr> = matches + .get_many::(ARG_FILES) + .map(|v| v.map(OsString::as_os_str).collect()) .unwrap_or_default(); let force_flag = matches.get_flag(OPT_FORCE); @@ -114,11 +115,20 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { verbose: matches.get_flag(OPT_VERBOSE), }; if options.interactive == InteractiveMode::Once && (options.recursive || files.len() > 3) { - let msg = if options.recursive { - "Remove all arguments recursively?" - } else { - "Remove all arguments?" - }; + let msg: String = format!( + "remove {} {}{}", + files.len(), + if files.len() > 1 { + "arguments" + } else { + "argument" + }, + if options.recursive { + " recursively?" + } else { + "?" + } + ); if !prompt_yes!("{}", msg) { return Ok(()); } @@ -168,6 +178,9 @@ pub fn uu_app() -> Command { prompts always", ) .value_name("WHEN") + .num_args(0..=1) + .require_equals(true) + .default_missing_value("always") .overrides_with_all([OPT_PROMPT, OPT_PROMPT_MORE]), ) .arg( @@ -231,13 +244,14 @@ pub fn uu_app() -> Command { .arg( Arg::new(ARG_FILES) .action(ArgAction::Append) + .value_parser(ValueParser::os_string()) .num_args(1..) .value_hint(clap::ValueHint::AnyPath), ) } // TODO: implement one-file-system (this may get partially implemented in walkdir) -fn remove(files: &[String], options: &Options) -> bool { +fn remove(files: &[&OsStr], options: &Options) -> bool { let mut had_err = false; for filename in files { @@ -257,14 +271,14 @@ fn remove(files: &[String], options: &Options) -> bool { // TODO: When the error is not about missing files // (e.g., permission), even rm -f should fail with // outputting the error, but there's no easy eay. - if !options.force { + if options.force { + false + } else { show_error!( "cannot remove {}: No such file or directory", filename.quote() ); true - } else { - false } } } @@ -274,6 +288,7 @@ fn remove(files: &[String], options: &Options) -> bool { had_err } +#[allow(clippy::cognitive_complexity)] fn handle_dir(path: &Path, options: &Options) -> bool { let mut had_err = false; @@ -422,6 +437,7 @@ fn remove_file(path: &Path, options: &Options) -> bool { false } +#[allow(clippy::cognitive_complexity)] fn prompt_file(path: &Path, options: &Options, is_dir: bool) -> bool { // If interactive is Never we never want to send prompts if options.interactive == InteractiveMode::Never { diff --git a/src/uu/rmdir/Cargo.toml b/src/uu/rmdir/Cargo.toml index 6e6bbcb154d..6c152a82a18 100644 --- a/src/uu/rmdir/Cargo.toml +++ b/src/uu/rmdir/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_rmdir" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "rmdir ~ (uutils) remove empty DIRECTORY" @@ -15,9 +15,9 @@ edition = "2021" path = "src/rmdir.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true, features=["fs"] } -libc = { workspace=true } +clap = { workspace = true } +uucore = { workspace = true, features = ["fs"] } +libc = { workspace = true } [[bin]] name = "rmdir" diff --git a/src/uu/runcon/Cargo.toml b/src/uu/runcon/Cargo.toml index 46d5652233a..191d9f01374 100644 --- a/src/uu/runcon/Cargo.toml +++ b/src/uu/runcon/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_runcon" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "runcon ~ (uutils) run command with specified security context" @@ -14,11 +14,11 @@ edition = "2021" path = "src/runcon.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true, features=["entries", "fs", "perms"] } -selinux = { workspace=true } -thiserror = { workspace=true } -libc = { workspace=true } +clap = { workspace = true } +uucore = { workspace = true, features = ["entries", "fs", "perms"] } +selinux = { workspace = true } +thiserror = { workspace = true } +libc = { workspace = true } [[bin]] name = "runcon" diff --git a/src/uu/runcon/runcon.md b/src/uu/runcon/runcon.md new file mode 100644 index 00000000000..1911c50447c --- /dev/null +++ b/src/uu/runcon/runcon.md @@ -0,0 +1,18 @@ +# runcon + +``` +runcon [CONTEXT COMMAND [ARG...]] +runcon [-c] [-u USER] [-r ROLE] [-t TYPE] [-l RANGE] COMMAND [ARG...]"; +``` + +Run command with specified security context under SELinux enabled systems. + +## After Help + +Run COMMAND with completely-specified CONTEXT, or with current or transitioned security context modified by one or more of LEVEL, ROLE, TYPE, and USER. + +If none of --compute, --type, --user, --role or --range is specified, then the first argument is used as the complete context. + +Note that only carefully-chosen contexts are likely to successfully run. + +With neither CONTEXT nor COMMAND are specified, then this prints the current security context. diff --git a/src/uu/runcon/src/runcon.rs b/src/uu/runcon/src/runcon.rs index 4ee3c6eba9a..a22bff47020 100644 --- a/src/uu/runcon/src/runcon.rs +++ b/src/uu/runcon/src/runcon.rs @@ -5,7 +5,7 @@ use uucore::error::{UResult, UUsageError}; use clap::{crate_version, Arg, ArgAction, Command}; use selinux::{OpaqueSecurityContext, SecurityClass, SecurityContext}; -use uucore::format_usage; +use uucore::{format_usage, help_about, help_section, help_usage}; use std::borrow::Cow; use std::ffi::{CStr, CString, OsStr, OsString}; @@ -18,18 +18,9 @@ mod errors; use errors::error_exit_status; use errors::{Error, Result, RunconError}; -const ABOUT: &str = "Run command with specified security context."; -const USAGE: &str = "\ - {} [CONTEXT COMMAND [ARG...]] - {} [-c] [-u USER] [-r ROLE] [-t TYPE] [-l RANGE] COMMAND [ARG...]"; -const DESCRIPTION: &str = "Run COMMAND with completely-specified CONTEXT, or with current or \ - transitioned security context modified by one or more of \ - LEVEL, ROLE, TYPE, and USER.\n\n\ - If none of --compute, --type, --user, --role or --range is specified, \ - then the first argument is used as the complete context.\n\n\ - Note that only carefully-chosen contexts are likely to successfully run.\n\n\ - With neither CONTEXT nor COMMAND are specified, \ - then this prints the current security context."; +const ABOUT: &str = help_about!("runcon.md"); +const USAGE: &str = help_usage!("runcon.md"); +const DESCRIPTION: &str = help_section!("after help", "runcon.md"); pub mod options { pub const COMPUTE: &str = "compute"; diff --git a/src/uu/seq/Cargo.toml b/src/uu/seq/Cargo.toml index d8c861aaeb2..e327b3eba0c 100644 --- a/src/uu/seq/Cargo.toml +++ b/src/uu/seq/Cargo.toml @@ -1,7 +1,7 @@ # spell-checker:ignore bigdecimal [package] name = "uu_seq" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "seq ~ (uutils) display a sequence of numbers" @@ -16,11 +16,11 @@ edition = "2021" path = "src/seq.rs" [dependencies] -bigdecimal = { workspace=true } -clap = { workspace=true } -num-bigint = { workspace=true } -num-traits = { workspace=true } -uucore = { workspace=true, features=["memo"] } +bigdecimal = { workspace = true } +clap = { workspace = true } +num-bigint = { workspace = true } +num-traits = { workspace = true } +uucore = { workspace = true, features = ["memo"] } [[bin]] name = "seq" diff --git a/src/uu/seq/seq.md b/src/uu/seq/seq.md new file mode 100644 index 00000000000..065404e2001 --- /dev/null +++ b/src/uu/seq/seq.md @@ -0,0 +1,9 @@ +# seq + +Display numbers from FIRST to LAST, in steps of INCREMENT. + +``` +seq [OPTION]... LAST +seq [OPTION]... FIRST LAST +seq [OPTION]... FIRST INCREMENT LAST"; +``` diff --git a/src/uu/seq/src/seq.rs b/src/uu/seq/src/seq.rs index 40bac84496c..97382ed1b05 100644 --- a/src/uu/seq/src/seq.rs +++ b/src/uu/seq/src/seq.rs @@ -2,7 +2,6 @@ // * // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -// TODO: Support -f flag // spell-checker:ignore (ToDO) istr chiter argptr ilen extendedbigdecimal extendedbigint numberparse use std::io::{stdout, ErrorKind, Write}; use std::process::exit; @@ -12,9 +11,9 @@ use num_traits::Zero; use uucore::error::FromIo; use uucore::error::UResult; -use uucore::format_usage; use uucore::memo::printf; use uucore::show; +use uucore::{format_usage, help_about, help_usage}; mod error; mod extendedbigdecimal; @@ -27,17 +26,15 @@ use crate::extendedbigint::ExtendedBigInt; use crate::number::Number; use crate::number::PreciseNumber; -static ABOUT: &str = "Display numbers from FIRST to LAST, in steps of INCREMENT."; -const USAGE: &str = "\ - {} [OPTION]... LAST - {} [OPTION]... FIRST LAST - {} [OPTION]... FIRST INCREMENT LAST"; -static OPT_SEPARATOR: &str = "separator"; -static OPT_TERMINATOR: &str = "terminator"; -static OPT_WIDTHS: &str = "widths"; -static OPT_FORMAT: &str = "format"; +const ABOUT: &str = help_about!("seq.md"); +const USAGE: &str = help_usage!("seq.md"); -static ARG_NUMBERS: &str = "numbers"; +const OPT_SEPARATOR: &str = "separator"; +const OPT_TERMINATOR: &str = "terminator"; +const OPT_WIDTHS: &str = "widths"; +const OPT_FORMAT: &str = "format"; + +const ARG_NUMBERS: &str = "numbers"; #[derive(Clone)] struct SeqOptions<'a> { diff --git a/src/uu/shred/Cargo.toml b/src/uu/shred/Cargo.toml index ee129122a8d..3da23ff2b88 100644 --- a/src/uu/shred/Cargo.toml +++ b/src/uu/shred/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_shred" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "shred ~ (uutils) hide former FILE contents with repeated overwrites" @@ -15,10 +15,10 @@ edition = "2021" path = "src/shred.rs" [dependencies] -clap = { workspace=true } -rand = { workspace=true } -uucore = { workspace=true } -libc = { workspace=true } +clap = { workspace = true } +rand = { workspace = true } +uucore = { workspace = true } +libc = { workspace = true } [[bin]] name = "shred" diff --git a/src/uu/shred/src/shred.rs b/src/uu/shred/src/shred.rs index 1c7d6ab136c..fd14a324596 100644 --- a/src/uu/shred/src/shred.rs +++ b/src/uu/shred/src/shred.rs @@ -19,6 +19,7 @@ use std::os::unix::prelude::PermissionsExt; use std::path::{Path, PathBuf}; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError, UUsageError}; +use uucore::parse_size::parse_size; use uucore::{format_usage, help_about, help_section, help_usage, show, show_error, show_if_err}; const ABOUT: &str = help_about!("shred.md"); @@ -238,7 +239,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .get_one::(options::SIZE) .map(|s| s.to_string()); let size = get_size(size_arg); - let exact = matches.get_flag(options::EXACT) && size.is_none(); // if -s is given, ignore -x + let exact = matches.get_flag(options::EXACT) || size.is_some(); let zero = matches.get_flag(options::ZERO); let verbose = matches.get_flag(options::VERBOSE); @@ -318,38 +319,17 @@ pub fn uu_app() -> Command { ) } -// TODO: Add support for all postfixes here up to and including EiB -// http://www.gnu.org/software/coreutils/manual/coreutils.html#Block-size fn get_size(size_str_opt: Option) -> Option { - size_str_opt.as_ref()?; - - let mut size_str = size_str_opt.as_ref().unwrap().clone(); - // Immutably look at last character of size string - let unit = match size_str.chars().last().unwrap() { - 'K' => { - size_str.pop(); - 1024u64 - } - 'M' => { - size_str.pop(); - (1024 * 1024) as u64 - } - 'G' => { - size_str.pop(); - (1024 * 1024 * 1024) as u64 - } - _ => 1u64, - }; - - let coefficient = match size_str.parse::() { - Ok(u) => u, - Err(_) => { - show_error!("{}: Invalid file size", size_str_opt.unwrap().maybe_quote()); - std::process::exit(1); - } - }; - - Some(coefficient * unit) + size_str_opt + .as_ref() + .and_then(|size| parse_size(size.as_str()).ok()) + .or_else(|| { + if let Some(size) = size_str_opt { + show_error!("invalid file size: {}", size.quote()); + std::process::exit(1); + } + None + }) } fn pass_name(pass_type: &PassType) -> String { @@ -361,6 +341,7 @@ fn pass_name(pass_type: &PassType) -> String { } #[allow(clippy::too_many_arguments)] +#[allow(clippy::cognitive_complexity)] fn wipe_file( path_str: &str, n_passes: usize, @@ -551,7 +532,9 @@ fn wipe_name(orig_path: &Path, verbose: bool) -> Option { } // Sync every file rename - let new_file = File::open(new_path.clone()) + let new_file = OpenOptions::new() + .write(true) + .open(new_path.clone()) .expect("Failed to open renamed file for syncing"); new_file.sync_all().expect("Failed to sync renamed file"); diff --git a/src/uu/shuf/Cargo.toml b/src/uu/shuf/Cargo.toml index eed9065c7e6..974791ee1e2 100644 --- a/src/uu/shuf/Cargo.toml +++ b/src/uu/shuf/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_shuf" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "shuf ~ (uutils) display random permutations of input lines" @@ -15,11 +15,11 @@ edition = "2021" path = "src/shuf.rs" [dependencies] -clap = { workspace=true } -memchr = { workspace=true } -rand = { workspace=true } -rand_core = { workspace=true } -uucore = { workspace=true } +clap = { workspace = true } +memchr = { workspace = true } +rand = { workspace = true } +rand_core = { workspace = true } +uucore = { workspace = true } [[bin]] name = "shuf" diff --git a/src/uu/sleep/Cargo.toml b/src/uu/sleep/Cargo.toml index aa4bf0664ba..2cd38988e17 100644 --- a/src/uu/sleep/Cargo.toml +++ b/src/uu/sleep/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_sleep" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "sleep ~ (uutils) pause for DURATION" @@ -15,9 +15,9 @@ edition = "2021" path = "src/sleep.rs" [dependencies] -clap = { workspace=true } -fundu = { workspace=true } -uucore = { workspace=true } +clap = { workspace = true } +fundu = { workspace = true } +uucore = { workspace = true } [[bin]] name = "sleep" diff --git a/src/uu/sleep/src/sleep.rs b/src/uu/sleep/src/sleep.rs index 009986095f6..8acb7724f83 100644 --- a/src/uu/sleep/src/sleep.rs +++ b/src/uu/sleep/src/sleep.rs @@ -14,7 +14,7 @@ use uucore::{ }; use clap::{crate_version, Arg, ArgAction, Command}; -use fundu::{self, DurationParser, ParseError}; +use fundu::{self, DurationParser, ParseError, SaturatingInto}; static ABOUT: &str = help_about!("sleep.md"); const USAGE: &str = help_usage!("sleep.md"); @@ -63,7 +63,7 @@ pub fn uu_app() -> Command { fn sleep(args: &[&str]) -> UResult<()> { let mut arg_error = false; - use fundu::TimeUnit::*; + use fundu::TimeUnit::{Day, Hour, Minute, Second}; let parser = DurationParser::with_time_units(&[Second, Minute, Hour, Day]); let sleep_dur = args @@ -91,7 +91,9 @@ fn sleep(args: &[&str]) -> UResult<()> { None } }) - .fold(Duration::ZERO, |acc, n| acc.saturating_add(n)); + .fold(Duration::ZERO, |acc, n| { + acc.saturating_add(SaturatingInto::::saturating_into(n)) + }); if arg_error { return Err(UUsageError::new(1, "")); diff --git a/src/uu/sort/Cargo.toml b/src/uu/sort/Cargo.toml index e940d08e5f5..7540522ed29 100644 --- a/src/uu/sort/Cargo.toml +++ b/src/uu/sort/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_sort" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "sort ~ (uutils) sort input lines" @@ -15,19 +15,19 @@ edition = "2021" path = "src/sort.rs" [dependencies] -binary-heap-plus = { workspace=true } -clap = { workspace=true } -compare = { workspace=true } -ctrlc = { workspace=true } -fnv = { workspace=true } -itertools = { workspace=true } -memchr = { workspace=true } -ouroboros = { workspace=true } -rand = { workspace=true } -rayon = { workspace=true } -tempfile = { workspace=true } -unicode-width = { workspace=true } -uucore = { workspace=true, features=["fs"] } +binary-heap-plus = { workspace = true } +clap = { workspace = true } +compare = { workspace = true } +ctrlc = { workspace = true } +fnv = { workspace = true } +itertools = { workspace = true } +memchr = { workspace = true } +ouroboros = { workspace = true } +rand = { workspace = true } +rayon = { workspace = true } +tempfile = { workspace = true } +unicode-width = { workspace = true } +uucore = { workspace = true, features = ["fs"] } [[bin]] name = "sort" diff --git a/src/uu/sort/sort.md b/src/uu/sort/sort.md new file mode 100644 index 00000000000..1d1aa5d5ffe --- /dev/null +++ b/src/uu/sort/sort.md @@ -0,0 +1,21 @@ + + +# sort + +``` +sort [OPTION]... [FILE]... +``` + +Display sorted concatenation of all FILE(s). With no FILE, or when FILE is -, read standard input. + +## After help + +The key format is `FIELD[.CHAR][OPTIONS][,FIELD[.CHAR]][OPTIONS]`. + +Fields by default are separated by the first whitespace after a non-whitespace character. Use `-t` to specify a custom separator. +In the default case, whitespace is appended at the beginning of each field. Custom separators however are not included in fields. + +`FIELD` and `CHAR` both start at 1 (i.e. they are 1-indexed). If there is no end specified after a comma, the end will be the end of the line. +If `CHAR` is set 0, it means the end of the field. `CHAR` defaults to 1 for the start position and to 0 for the end position. + +Valid options are: `MbdfhnRrV`. They override the global options for this key. diff --git a/src/uu/sort/src/merge.rs b/src/uu/sort/src/merge.rs index a7b4417e346..f6da0ee329c 100644 --- a/src/uu/sort/src/merge.rs +++ b/src/uu/sort/src/merge.rs @@ -362,16 +362,16 @@ impl<'a> Compare for FileComparator<'a> { // Wait for the child to exit and check its exit code. fn check_child_success(mut child: Child, program: &str) -> UResult<()> { - if !matches!( + if matches!( child.wait().map(|e| e.code()), Ok(Some(0)) | Ok(None) | Err(_) ) { + Ok(()) + } else { Err(SortError::CompressProgTerminatedAbnormally { prog: program.to_owned(), } .into()) - } else { - Ok(()) } } diff --git a/src/uu/sort/src/numeric_str_cmp.rs b/src/uu/sort/src/numeric_str_cmp.rs index 70f8c2cd8eb..05f81c89781 100644 --- a/src/uu/sort/src/numeric_str_cmp.rs +++ b/src/uu/sort/src/numeric_str_cmp.rs @@ -52,6 +52,7 @@ impl NumInfo { /// an empty range (idx..idx) is returned so that idx is the char after the last zero. /// If the input is not a number (which has to be treated as zero), the returned empty range /// will be 0..0. + #[allow(clippy::cognitive_complexity)] pub fn parse(num: &str, parse_settings: &NumInfoParseSettings) -> (Self, Range) { let mut exponent = -1; let mut had_decimal_pt = false; @@ -198,15 +199,13 @@ pub fn human_numeric_str_cmp( let a_unit = get_unit(a.chars().next_back()); let b_unit = get_unit(b.chars().next_back()); let ordering = a_unit.cmp(&b_unit); - if ordering != Ordering::Equal { - if a_info.sign == Sign::Negative { - ordering.reverse() - } else { - ordering - } - } else { + if ordering == Ordering::Equal { // 3. Number numeric_str_cmp((a, a_info), (b, b_info)) + } else if a_info.sign == Sign::Negative { + ordering.reverse() + } else { + ordering } } diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index a681026f643..80c2275e969 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -45,26 +45,15 @@ use std::str::Utf8Error; use unicode_width::UnicodeWidthStr; use uucore::display::Quotable; use uucore::error::{set_exit_code, strip_errno, UError, UResult, USimpleError, UUsageError}; -use uucore::format_usage; use uucore::parse_size::{ParseSizeError, Parser}; use uucore::version_cmp::version_cmp; +use uucore::{format_usage, help_about, help_section, help_usage}; use crate::tmp_dir::TmpDirWrapper; -const ABOUT: &str = "\ - Display sorted concatenation of all FILE(s). \ - With no FILE, or when FILE is -, read standard input."; -const USAGE: &str = "{} [OPTION]... [FILE]..."; - -const LONG_HELP_KEYS: &str = "The key format is FIELD[.CHAR][OPTIONS][,FIELD[.CHAR]][OPTIONS]. - -Fields by default are separated by the first whitespace after a non-whitespace character. Use -t to specify a custom separator. -In the default case, whitespace is appended at the beginning of each field. Custom separators however are not included in fields. - -FIELD and CHAR both start at 1 (i.e. they are 1-indexed). If there is no end specified after a comma, the end will be the end of the line. -If CHAR is set 0, it means the end of the field. CHAR defaults to 1 for the start position and to 0 for the end position. - -Valid options are: MbdfhnRrV. They override the global options for this key."; +const ABOUT: &str = help_about!("sort.md"); +const USAGE: &str = help_usage!("sort.md"); +const AFTER_HELP: &str = help_section!("after help", "sort.md"); mod options { pub mod modes { @@ -185,7 +174,9 @@ impl Display for SortError { line, silent, } => { - if !silent { + if *silent { + Ok(()) + } else { write!( f, "{}:{}: disorder: {}", @@ -193,8 +184,6 @@ impl Display for SortError { line_number, line ) - } else { - Ok(()) } } Self::OpenFailed { path, error } => { @@ -582,7 +571,15 @@ impl<'a> Line<'a> { selection.start += num_range.start; selection.end = selection.start + num_range.len(); - if num_range != (0..0) { + if num_range == (0..0) { + // This was not a valid number. + // Report no match at the first non-whitespace character. + let leading_whitespace = self.line[selection.clone()] + .find(|c: char| !c.is_whitespace()) + .unwrap_or(0); + selection.start += leading_whitespace; + selection.end += leading_whitespace; + } else { // include a trailing si unit if selector.settings.mode == SortMode::HumanNumeric && self.line[selection.end..initial_selection.end] @@ -597,14 +594,6 @@ impl<'a> Line<'a> { { selection.start -= 1; } - } else { - // This was not a valid number. - // Report no match at the first non-whitespace character. - let leading_whitespace = self.line[selection.clone()] - .find(|c: char| !c.is_whitespace()) - .unwrap_or(0); - selection.start += leading_whitespace; - selection.end += leading_whitespace; } } SortMode::GeneralNumeric => { @@ -1044,6 +1033,7 @@ fn make_sort_mode_arg(mode: &'static str, short: char, help: &'static str) -> Ar } #[uucore::main] +#[allow(clippy::cognitive_complexity)] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let args = args.collect_ignore(); let mut settings = GlobalSettings::default(); @@ -1292,7 +1282,7 @@ pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) - .after_help(LONG_HELP_KEYS) + .after_help(AFTER_HELP) .override_usage(format_usage(USAGE)) .infer_long_args(true) .disable_help_flag(true) @@ -1572,10 +1562,7 @@ fn compare_by<'a>( let mut num_info_index = 0; let mut parsed_float_index = 0; for selector in &global_settings.selectors { - let (a_str, b_str) = if !selector.needs_selection { - // We can select the whole line. - (a.line, b.line) - } else { + let (a_str, b_str) = if selector.needs_selection { let selections = ( a_line_data.selections [a.index * global_settings.precomputed.selections_per_line + selection_index], @@ -1584,6 +1571,9 @@ fn compare_by<'a>( ); selection_index += 1; selections + } else { + // We can select the whole line. + (a.line, b.line) }; let settings = &selector.settings; @@ -1665,6 +1655,7 @@ fn compare_by<'a>( // In contrast to numeric compare, GNU general numeric/FP sort *should* recognize positive signs and // scientific notation, so we strip those lines only after the end of the following numeric string. // For example, 5e10KFD would be 5e10 or 5x10^10 and +10000HFKJFK would become 10000. +#[allow(clippy::cognitive_complexity)] fn get_leading_gen(input: &str) -> Range { let trimmed = input.trim_start(); let leading_whitespace_len = input.len() - trimmed.len(); diff --git a/src/uu/split/Cargo.toml b/src/uu/split/Cargo.toml index 2d379a3734c..32cfd6fda2c 100644 --- a/src/uu/split/Cargo.toml +++ b/src/uu/split/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_split" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "split ~ (uutils) split input into output files" @@ -15,9 +15,9 @@ edition = "2021" path = "src/split.rs" [dependencies] -clap = { workspace=true } -memchr = { workspace=true } -uucore = { workspace=true, features=["fs"] } +clap = { workspace = true } +memchr = { workspace = true } +uucore = { workspace = true, features = ["fs"] } [[bin]] name = "split" diff --git a/src/uu/split/split.md b/src/uu/split/split.md new file mode 100644 index 00000000000..d3a481fd307 --- /dev/null +++ b/src/uu/split/split.md @@ -0,0 +1,13 @@ + + +# split + +``` +split [OPTION]... [INPUT [PREFIX]] +``` + +Create output files containing consecutive or interleaved sections of input + +## After Help + +Output fixed-size pieces of INPUT to PREFIXaa, PREFIXab, ...; default size is 1000, and default PREFIX is 'x'. With no INPUT, or when INPUT is -, read standard input. diff --git a/src/uu/split/src/number.rs b/src/uu/split/src/number.rs index 778e24f7cf3..567526538a8 100644 --- a/src/uu/split/src/number.rs +++ b/src/uu/split/src/number.rs @@ -200,10 +200,10 @@ impl FixedWidthNumber { break; } } - if suffix_start != 0 { - Err(Overflow) - } else { + if suffix_start == 0 { Ok(Self { radix, digits }) + } else { + Err(Overflow) } } diff --git a/src/uu/split/src/split.rs b/src/uu/split/src/split.rs index d737c85134e..6e29e6f4b07 100644 --- a/src/uu/split/src/split.rs +++ b/src/uu/split/src/split.rs @@ -5,7 +5,7 @@ // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -// spell-checker:ignore (ToDO) PREFIXaa PREFIXab nbbbb ncccc +// spell-checker:ignore nbbbb ncccc mod filenames; mod number; @@ -23,9 +23,9 @@ use std::io::{stdin, BufRead, BufReader, BufWriter, ErrorKind, Read, Write}; use std::path::Path; use uucore::display::Quotable; use uucore::error::{FromIo, UIoError, UResult, USimpleError, UUsageError}; -use uucore::format_usage; use uucore::parse_size::{parse_size, ParseSizeError}; use uucore::uio_error; +use uucore::{format_usage, help_about, help_section, help_usage}; static OPT_BYTES: &str = "bytes"; static OPT_LINE_BYTES: &str = "line-bytes"; @@ -47,11 +47,9 @@ static OPT_ELIDE_EMPTY_FILES: &str = "elide-empty-files"; static ARG_INPUT: &str = "input"; static ARG_PREFIX: &str = "prefix"; -const USAGE: &str = "{} [OPTION]... [INPUT [PREFIX]]"; -const AFTER_HELP: &str = "\ - Output fixed-size pieces of INPUT to PREFIXaa, PREFIXab, ...; default \ - size is 1000, and default PREFIX is 'x'. With no INPUT, or when INPUT is \ - -, read standard input."; +const ABOUT: &str = help_about!("split.md"); +const USAGE: &str = help_usage!("split.md"); +const AFTER_HELP: &str = help_section!("after help", "split.md"); #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { @@ -66,7 +64,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(crate_version!()) - .about("Create output files containing consecutive or interleaved sections of input") + .about(ABOUT) .after_help(AFTER_HELP) .override_usage(format_usage(USAGE)) .infer_long_args(true) diff --git a/src/uu/stat/Cargo.toml b/src/uu/stat/Cargo.toml index 1c84eaf1ecf..3f165c35735 100644 --- a/src/uu/stat/Cargo.toml +++ b/src/uu/stat/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_stat" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "stat ~ (uutils) display FILE status" @@ -15,8 +15,8 @@ edition = "2021" path = "src/stat.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true, features=["entries", "libc", "fs", "fsext"] } +clap = { workspace = true } +uucore = { workspace = true, features = ["entries", "libc", "fs", "fsext"] } [[bin]] name = "stat" diff --git a/src/uu/stat/src/stat.rs b/src/uu/stat/src/stat.rs index fbbed8c2100..69f3c276084 100644 --- a/src/uu/stat/src/stat.rs +++ b/src/uu/stat/src/stat.rs @@ -205,7 +205,16 @@ struct Stater { default_dev_tokens: Vec, } -#[allow(clippy::cognitive_complexity)] +/// Prints a formatted output based on the provided output type, flags, width, and precision. +/// +/// # Arguments +/// +/// * `output` - A reference to the OutputType enum containing the value to be printed. +/// * `flags` - A Flags struct containing formatting flags. +/// * `width` - The width of the field for the printed output. +/// * `precision` - An Option containing the precision value. +/// +/// This function delegates the printing process to more specialized functions depending on the output type. fn print_it(output: &OutputType, flags: Flags, width: usize, precision: Option) { // If the precision is given as just '.', the precision is taken to be zero. // A negative precision is taken as if the precision were omitted. @@ -236,71 +245,166 @@ fn print_it(output: &OutputType, flags: Flags, width: usize, precision: Option { - let s = match precision { - Some(p) if p < s.len() => &s[..p], - _ => s, - }; - pad_and_print(s, flags.left, width, Padding::Space); - } - OutputType::Integer(num) => { - let num = num.to_string(); - let arg = if flags.group { - group_num(&num) - } else { - Cow::Borrowed(num.as_str()) - }; - let prefix = if flags.sign { - "+" - } else if flags.space { - " " - } else { - "" - }; - let extended = format!( - "{prefix}{arg:0>precision$}", - precision = precision.unwrap_or(0) - ); - pad_and_print(&extended, flags.left, width, padding_char); - } - OutputType::Unsigned(num) => { - let num = num.to_string(); - let s = if flags.group { - group_num(&num) - } else { - Cow::Borrowed(num.as_str()) - }; - let s = format!("{s:0>precision$}", precision = precision.unwrap_or(0)); - pad_and_print(&s, flags.left, width, padding_char); - } + OutputType::Str(s) => print_str(s, &flags, width, precision), + OutputType::Integer(num) => print_integer(*num, &flags, width, precision, padding_char), + OutputType::Unsigned(num) => print_unsigned(*num, &flags, width, precision, padding_char), OutputType::UnsignedOct(num) => { - let prefix = if flags.alter { "0" } else { "" }; - let s = format!( - "{prefix}{num:0>precision$o}", - precision = precision.unwrap_or(0) - ); - pad_and_print(&s, flags.left, width, padding_char); + print_unsigned_oct(*num, &flags, width, precision, padding_char); } OutputType::UnsignedHex(num) => { - let prefix = if flags.alter { "0x" } else { "" }; - let s = format!( - "{prefix}{num:0>precision$x}", - precision = precision.unwrap_or(0) - ); - pad_and_print(&s, flags.left, width, padding_char); + print_unsigned_hex(*num, &flags, width, precision, padding_char); } OutputType::Unknown => print!("?"), } } +/// Determines the padding character based on the provided flags and precision. +/// +/// # Arguments +/// +/// * `flags` - A reference to the Flags struct containing formatting flags. +/// * `precision` - An Option containing the precision value. +/// +/// # Returns +/// +/// * Padding - An instance of the Padding enum representing the padding character. +fn determine_padding_char(flags: &Flags, precision: &Option) -> Padding { + if flags.zero && !flags.left && precision.is_none() { + Padding::Zero + } else { + Padding::Space + } +} + +/// Prints a string value based on the provided flags, width, and precision. +/// +/// # Arguments +/// +/// * `s` - The string to be printed. +/// * `flags` - A reference to the Flags struct containing formatting flags. +/// * `width` - The width of the field for the printed string. +/// * `precision` - An Option containing the precision value. +fn print_str(s: &str, flags: &Flags, width: usize, precision: Option) { + let s = match precision { + Some(p) if p < s.len() => &s[..p], + _ => s, + }; + pad_and_print(s, flags.left, width, Padding::Space); +} + +/// Prints an integer value based on the provided flags, width, and precision. +/// +/// # Arguments +/// +/// * `num` - The integer value to be printed. +/// * `flags` - A reference to the Flags struct containing formatting flags. +/// * `width` - The width of the field for the printed integer. +/// * `precision` - An Option containing the precision value. +/// * `padding_char` - The padding character as determined by `determine_padding_char`. +fn print_integer( + num: i64, + flags: &Flags, + width: usize, + precision: Option, + padding_char: Padding, +) { + let num = num.to_string(); + let arg = if flags.group { + group_num(&num) + } else { + Cow::Borrowed(num.as_str()) + }; + let prefix = if flags.sign { + "+" + } else if flags.space { + " " + } else { + "" + }; + let extended = format!( + "{prefix}{arg:0>precision$}", + precision = precision.unwrap_or(0) + ); + pad_and_print(&extended, flags.left, width, padding_char); +} + +/// Prints an unsigned integer value based on the provided flags, width, and precision. +/// +/// # Arguments +/// +/// * `num` - The unsigned integer value to be printed. +/// * `flags` - A reference to the Flags struct containing formatting flags. +/// * `width` - The width of the field for the printed unsigned integer. +/// * `precision` - An Option containing the precision value. +/// * `padding_char` - The padding character as determined by `determine_padding_char`. +fn print_unsigned( + num: u64, + flags: &Flags, + width: usize, + precision: Option, + padding_char: Padding, +) { + let num = num.to_string(); + let s = if flags.group { + group_num(&num) + } else { + Cow::Borrowed(num.as_str()) + }; + let s = format!("{s:0>precision$}", precision = precision.unwrap_or(0)); + pad_and_print(&s, flags.left, width, padding_char); +} + +/// Prints an unsigned octal integer value based on the provided flags, width, and precision. +/// +/// # Arguments +/// +/// * `num` - The unsigned octal integer value to be printed. +/// * `flags` - A reference to the Flags struct containing formatting flags. +/// * `width` - The width of the field for the printed unsigned octal integer. +/// * `precision` - An Option containing the precision value. +/// * `padding_char` - The padding character as determined by `determine_padding_char`. +fn print_unsigned_oct( + num: u32, + flags: &Flags, + width: usize, + precision: Option, + padding_char: Padding, +) { + let prefix = if flags.alter { "0" } else { "" }; + let s = format!( + "{prefix}{num:0>precision$o}", + precision = precision.unwrap_or(0) + ); + pad_and_print(&s, flags.left, width, padding_char); +} + +/// Prints an unsigned hexadecimal integer value based on the provided flags, width, and precision. +/// +/// # Arguments +/// +/// * `num` - The unsigned hexadecimal integer value to be printed. +/// * `flags` - A reference to the Flags struct containing formatting flags. +/// * `width` - The width of the field for the printed unsigned hexadecimal integer. +/// * `precision` - An Option containing the precision value. +/// * `padding_char` - The padding character as determined by `determine_padding_char`. +fn print_unsigned_hex( + num: u64, + flags: &Flags, + width: usize, + precision: Option, + padding_char: Padding, +) { + let prefix = if flags.alter { "0x" } else { "" }; + let s = format!( + "{prefix}{num:0>precision$x}", + precision = precision.unwrap_or(0) + ); + pad_and_print(&s, flags.left, width, padding_char); +} + impl Stater { fn generate_tokens(format_str: &str, use_printf: bool) -> UResult> { let mut tokens = Vec::new(); @@ -375,9 +479,7 @@ impl Stater { }); } '\\' => { - if !use_printf { - tokens.push(Token::Char('\\')); - } else { + if use_printf { i += 1; if i >= bound { show_warning!("backslash at end of format"); @@ -414,6 +516,8 @@ impl Stater { tokens.push(Token::Char(c)); } } + } else { + tokens.push(Token::Char('\\')); } } @@ -519,7 +623,67 @@ impl Stater { OsString::from(file) }; - if !self.show_fs { + if self.show_fs { + #[cfg(unix)] + let p = file.as_bytes(); + #[cfg(not(unix))] + let p = file.into_string().unwrap(); + match statfs(p) { + Ok(meta) => { + let tokens = &self.default_tokens; + + for t in tokens.iter() { + match *t { + Token::Char(c) => print!("{c}"), + Token::Directive { + flag, + width, + precision, + format, + } => { + let output = match format { + // free blocks available to non-superuser + 'a' => OutputType::Unsigned(meta.avail_blocks()), + // total data blocks in file system + 'b' => OutputType::Unsigned(meta.total_blocks()), + // total file nodes in file system + 'c' => OutputType::Unsigned(meta.total_file_nodes()), + // free file nodes in file system + 'd' => OutputType::Unsigned(meta.free_file_nodes()), + // free blocks in file system + 'f' => OutputType::Unsigned(meta.free_blocks()), + // file system ID in hex + 'i' => OutputType::UnsignedHex(meta.fsid()), + // maximum length of filenames + 'l' => OutputType::Unsigned(meta.namelen()), + // file name + 'n' => OutputType::Str(display_name.to_string()), + // block size (for faster transfers) + 's' => OutputType::Unsigned(meta.io_size()), + // fundamental block size (for block counts) + 'S' => OutputType::Integer(meta.block_size()), + // file system type in hex + 't' => OutputType::UnsignedHex(meta.fs_type() as u64), + // file system type in human readable form + 'T' => OutputType::Str(pretty_fstype(meta.fs_type()).into()), + _ => OutputType::Unknown, + }; + + print_it(&output, flag, width, precision); + } + } + } + } + Err(e) => { + show_error!( + "cannot read file system information for {}: {}", + display_name.quote(), + e + ); + return 1; + } + } + } else { let result = if self.follow || stdin_is_fifo && display_name == "-" { fs::metadata(&file) } else { @@ -660,66 +824,6 @@ impl Stater { return 1; } } - } else { - #[cfg(unix)] - let p = file.as_bytes(); - #[cfg(not(unix))] - let p = file.into_string().unwrap(); - match statfs(p) { - Ok(meta) => { - let tokens = &self.default_tokens; - - for t in tokens.iter() { - match *t { - Token::Char(c) => print!("{c}"), - Token::Directive { - flag, - width, - precision, - format, - } => { - let output = match format { - // free blocks available to non-superuser - 'a' => OutputType::Unsigned(meta.avail_blocks()), - // total data blocks in file system - 'b' => OutputType::Unsigned(meta.total_blocks()), - // total file nodes in file system - 'c' => OutputType::Unsigned(meta.total_file_nodes()), - // free file nodes in file system - 'd' => OutputType::Unsigned(meta.free_file_nodes()), - // free blocks in file system - 'f' => OutputType::Unsigned(meta.free_blocks()), - // file system ID in hex - 'i' => OutputType::UnsignedHex(meta.fsid()), - // maximum length of filenames - 'l' => OutputType::Unsigned(meta.namelen()), - // file name - 'n' => OutputType::Str(display_name.to_string()), - // block size (for faster transfers) - 's' => OutputType::Unsigned(meta.io_size()), - // fundamental block size (for block counts) - 'S' => OutputType::Integer(meta.block_size()), - // file system type in hex - 't' => OutputType::UnsignedHex(meta.fs_type() as u64), - // file system type in human readable form - 'T' => OutputType::Str(pretty_fstype(meta.fs_type()).into()), - _ => OutputType::Unknown, - }; - - print_it(&output, flag, width, precision); - } - } - } - } - Err(e) => { - show_error!( - "cannot read file system information for {}: {}", - display_name.quote(), - e - ); - return 1; - } - } } 0 } diff --git a/src/uu/stat/stat.md b/src/uu/stat/stat.md index d91f2b01f0d..ac63edd18d2 100644 --- a/src/uu/stat/stat.md +++ b/src/uu/stat/stat.md @@ -25,7 +25,7 @@ Valid format sequences for files (without `--file-system`): - `%i`: inode number - `%m`: mount point - `%n`: file name -- `%N`: quoted file name with dereference if symbolic link +- `%N`: quoted file name with dereference (follow) if symbolic link - `%o`: optimal I/O transfer size hint - `%s`: total size, in bytes - `%t`: major device type in hex, for character/block device special files diff --git a/src/uu/stdbuf/Cargo.toml b/src/uu/stdbuf/Cargo.toml index 164188f9f28..bce2f8dba41 100644 --- a/src/uu/stdbuf/Cargo.toml +++ b/src/uu/stdbuf/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_stdbuf" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "stdbuf ~ (uutils) run COMMAND with modified standard stream buffering" @@ -15,12 +15,12 @@ edition = "2021" path = "src/stdbuf.rs" [dependencies] -clap = { workspace=true } -tempfile = { workspace=true } -uucore = { workspace=true } +clap = { workspace = true } +tempfile = { workspace = true } +uucore = { workspace = true } [build-dependencies] -libstdbuf = { version="0.0.17", package="uu_stdbuf_libstdbuf", path="src/libstdbuf" } +libstdbuf = { version = "0.0.19", package = "uu_stdbuf_libstdbuf", path = "src/libstdbuf" } [[bin]] name = "stdbuf" diff --git a/src/uu/stdbuf/src/libstdbuf/Cargo.toml b/src/uu/stdbuf/src/libstdbuf/Cargo.toml index 2498f7671a9..b3f18611899 100644 --- a/src/uu/stdbuf/src/libstdbuf/Cargo.toml +++ b/src/uu/stdbuf/src/libstdbuf/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_stdbuf_libstdbuf" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "stdbuf/libstdbuf ~ (uutils); dynamic library required for stdbuf" @@ -14,12 +14,15 @@ edition = "2021" [lib] name = "libstdbuf" path = "src/libstdbuf.rs" -crate-type = ["cdylib", "rlib"] # XXX: note: the rlib is just to prevent Cargo from spitting out a warning +crate-type = [ + "cdylib", + "rlib", +] # XXX: note: the rlib is just to prevent Cargo from spitting out a warning [dependencies] cpp = "0.5" -libc = { workspace=true } -uucore = { version=">=0.0.17", package="uucore", path="../../../../uucore" } +libc = { workspace = true } +uucore = { version = ">=0.0.19", package = "uucore", path = "../../../../uucore" } [build-dependencies] cpp_build = "0.5" diff --git a/src/uu/stdbuf/src/stdbuf.rs b/src/uu/stdbuf/src/stdbuf.rs index 2bfb6f908ec..e02dd28bc80 100644 --- a/src/uu/stdbuf/src/stdbuf.rs +++ b/src/uu/stdbuf/src/stdbuf.rs @@ -17,23 +17,11 @@ use tempfile::tempdir; use tempfile::TempDir; use uucore::error::{FromIo, UResult, USimpleError, UUsageError}; use uucore::parse_size::parse_size; -use uucore::{crash, format_usage}; - -static ABOUT: &str = - "Run COMMAND, with modified buffering operations for its standard streams.\n\n\ - Mandatory arguments to long options are mandatory for short options too."; -const USAGE: &str = "{} OPTION... COMMAND"; -static LONG_HELP: &str = "If MODE is 'L' the corresponding stream will be line buffered.\n\ - This option is invalid with standard input.\n\n\ - If MODE is '0' the corresponding stream will be unbuffered.\n\n\ - Otherwise MODE is a number which may be followed by one of the following:\n\n\ - KB 1000, K 1024, MB 1000*1000, M 1024*1024, and so on for G, T, P, E, Z, Y.\n\ - In this case the corresponding stream will be fully buffered with the buffer size set to \ - MODE bytes.\n\n\ - NOTE: If COMMAND adjusts the buffering of its standard streams ('tee' does for e.g.) then \ - that will override corresponding settings changed by 'stdbuf'.\n\ - Also some filters (like 'dd' and 'cat' etc.) don't use streams for I/O, \ - and are thus unaffected by 'stdbuf' settings.\n"; +use uucore::{crash, format_usage, help_about, help_section, help_usage}; + +const ABOUT: &str = help_about!("stdbuf.md"); +const USAGE: &str = help_usage!("stdbuf.md"); +const LONG_HELP: &str = help_section!("after help", "stdbuf.md"); mod options { pub const INPUT: &str = "input"; diff --git a/src/uu/stdbuf/stdbuf.md b/src/uu/stdbuf/stdbuf.md new file mode 100644 index 00000000000..e0062e6271a --- /dev/null +++ b/src/uu/stdbuf/stdbuf.md @@ -0,0 +1,24 @@ +# stdbuf + +``` +stdbuf [OPTION]... COMMAND +``` + +Run `COMMAND`, with modified buffering operations for its standard streams. + +Mandatory arguments to long options are mandatory for short options too. + +## After Help + +If `MODE` is 'L' the corresponding stream will be line buffered. +This option is invalid with standard input. + +If `MODE` is '0' the corresponding stream will be unbuffered. + +Otherwise, `MODE` is a number which may be followed by one of the following: + +KB 1000, K 1024, MB 1000*1000, M 1024*1024, and so on for G, T, P, E, Z, Y. +In this case the corresponding stream will be fully buffered with the buffer size set to `MODE` bytes. + +NOTE: If `COMMAND` adjusts the buffering of its standard streams (`tee` does for e.g.) then that will override corresponding settings changed by `stdbuf`. +Also some filters (like `dd` and `cat` etc.) don't use streams for I/O, and are thus unaffected by `stdbuf` settings. diff --git a/src/uu/stty/Cargo.toml b/src/uu/stty/Cargo.toml index 9c94f8cb16a..220651003ab 100644 --- a/src/uu/stty/Cargo.toml +++ b/src/uu/stty/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_stty" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "stty ~ (uutils) print or change terminal characteristics" @@ -15,9 +15,9 @@ edition = "2021" path = "src/stty.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true } -nix = { workspace=true, features = ["term", "ioctl"] } +clap = { workspace = true } +uucore = { workspace = true } +nix = { workspace = true, features = ["term", "ioctl"] } [[bin]] name = "stty" diff --git a/src/uu/stty/src/stty.rs b/src/uu/stty/src/stty.rs index 928c3f17749..e0966247133 100644 --- a/src/uu/stty/src/stty.rs +++ b/src/uu/stty/src/stty.rs @@ -18,7 +18,7 @@ use std::ops::ControlFlow; use std::os::unix::fs::OpenOptionsExt; use std::os::unix::io::{AsRawFd, IntoRawFd, RawFd}; use uucore::error::{UResult, USimpleError}; -use uucore::format_usage; +use uucore::{format_usage, help_about, help_usage}; #[cfg(not(any( target_os = "freebsd", @@ -31,11 +31,8 @@ use uucore::format_usage; use flags::BAUD_RATES; use flags::{CONTROL_FLAGS, INPUT_FLAGS, LOCAL_FLAGS, OUTPUT_FLAGS}; -const USAGE: &str = "\ - {} [-F DEVICE | --file=DEVICE] [SETTING]... - {} [-F DEVICE | --file=DEVICE] [-a|--all] - {} [-F DEVICE | --file=DEVICE] [-g|--save]"; -const SUMMARY: &str = "Print or change terminal characteristics."; +const USAGE: &str = help_usage!("stty.md"); +const SUMMARY: &str = help_about!("stty.md"); #[derive(Clone, Copy, Debug)] pub struct Flag { diff --git a/src/uu/stty/stty.md b/src/uu/stty/stty.md new file mode 100644 index 00000000000..6aa1decf5e7 --- /dev/null +++ b/src/uu/stty/stty.md @@ -0,0 +1,9 @@ +# stty + +``` +stty [-F DEVICE | --file=DEVICE] [SETTING]... +stty [-F DEVICE | --file=DEVICE] [-a|--all] +stty [-F DEVICE | --file=DEVICE] [-g|--save] +``` + +Print or change terminal characteristics. diff --git a/src/uu/sum/Cargo.toml b/src/uu/sum/Cargo.toml index 0997f22fc9a..37b4d21e0a0 100644 --- a/src/uu/sum/Cargo.toml +++ b/src/uu/sum/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_sum" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "sum ~ (uutils) display checksum and block counts for input" @@ -15,8 +15,8 @@ edition = "2021" path = "src/sum.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true } +clap = { workspace = true } +uucore = { workspace = true } [[bin]] name = "sum" diff --git a/src/uu/sum/src/sum.rs b/src/uu/sum/src/sum.rs index 1134f24448c..0ea415b1348 100644 --- a/src/uu/sum/src/sum.rs +++ b/src/uu/sum/src/sum.rs @@ -13,11 +13,10 @@ use std::io::{stdin, Read}; use std::path::Path; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError}; -use uucore::{format_usage, show}; +use uucore::{format_usage, help_about, help_usage, show}; -static USAGE: &str = "{} [OPTION]... [FILE]..."; -static ABOUT: &str = "Checksum and count the blocks in a file.\n\n\ - With no FILE, or when FILE is -, read standard input."; +const USAGE: &str = help_usage!("sum.md"); +const ABOUT: &str = help_about!("sum.md"); // This can be replaced with usize::div_ceil once it is stabilized. // This implementation approach is optimized for when `b` is a constant, diff --git a/src/uu/sum/sum.md b/src/uu/sum/sum.md new file mode 100644 index 00000000000..ca3adb81f36 --- /dev/null +++ b/src/uu/sum/sum.md @@ -0,0 +1,9 @@ +# sum + +``` +sum [OPTION]... [FILE]..." +``` + +Checksum and count the blocks in a file. + +With no FILE, or when FILE is -, read standard input. diff --git a/src/uu/sync/Cargo.toml b/src/uu/sync/Cargo.toml index 19696d881a8..36d110046a8 100644 --- a/src/uu/sync/Cargo.toml +++ b/src/uu/sync/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_sync" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "sync ~ (uutils) synchronize cache writes to storage" @@ -15,15 +15,19 @@ edition = "2021" path = "src/sync.rs" [dependencies] -clap = { workspace=true } -libc = { workspace=true } -uucore = { workspace=true, features=["wide"] } +clap = { workspace = true } +libc = { workspace = true } +uucore = { workspace = true, features = ["wide"] } [target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies] -nix = { workspace=true } +nix = { workspace = true } [target.'cfg(target_os = "windows")'.dependencies] -windows-sys = { workspace=true, features = ["Win32_Storage_FileSystem", "Win32_System_WindowsProgramming", "Win32_Foundation"] } +windows-sys = { workspace = true, features = [ + "Win32_Storage_FileSystem", + "Win32_System_WindowsProgramming", + "Win32_Foundation", +] } [[bin]] name = "sync" diff --git a/src/uu/sync/src/sync.rs b/src/uu/sync/src/sync.rs index d4bc1604d9a..821ad639b56 100644 --- a/src/uu/sync/src/sync.rs +++ b/src/uu/sync/src/sync.rs @@ -7,8 +7,6 @@ /* Last synced with: sync (GNU coreutils) 8.13 */ -extern crate libc; - use clap::{crate_version, Arg, ArgAction, Command}; #[cfg(any(target_os = "linux", target_os = "android"))] use nix::errno::Errno; @@ -21,10 +19,11 @@ use uucore::display::Quotable; #[cfg(any(target_os = "linux", target_os = "android"))] use uucore::error::FromIo; use uucore::error::{UResult, USimpleError}; -use uucore::format_usage; +use uucore::{format_usage, help_about, help_usage}; + +const ABOUT: &str = help_about!("sync.md"); +const USAGE: &str = help_usage!("sync.md"); -static ABOUT: &str = "Synchronize cached writes to persistent storage"; -const USAGE: &str = "{} [OPTION]... FILE..."; pub mod options { pub static FILE_SYSTEM: &str = "file-system"; pub static DATA: &str = "data"; @@ -34,7 +33,6 @@ static ARG_FILES: &str = "files"; #[cfg(unix)] mod platform { - use super::libc; #[cfg(any(target_os = "linux", target_os = "android"))] use std::fs::File; #[cfg(any(target_os = "linux", target_os = "android"))] @@ -175,7 +173,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let path = Path::new(&f); if let Err(e) = open(path, OFlag::O_NONBLOCK, Mode::empty()) { if e != Errno::EACCES || (e == Errno::EACCES && path.is_dir()) { - return e.map_err_context(|| format!("cannot stat {}", f.quote()))?; + return e.map_err_context(|| format!("error opening {}", f.quote()))?; } } } @@ -185,7 +183,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { if !Path::new(&f).exists() { return Err(USimpleError::new( 1, - format!("cannot stat {}: No such file or directory", f.quote()), + format!("error opening {}: No such file or directory", f.quote()), )); } } diff --git a/src/uu/sync/sync.md b/src/uu/sync/sync.md new file mode 100644 index 00000000000..2fdd363399f --- /dev/null +++ b/src/uu/sync/sync.md @@ -0,0 +1,7 @@ +# sync + +``` +sync [OPTION]... FILE... +``` + +Synchronize cached writes to persistent storage diff --git a/src/uu/tac/Cargo.toml b/src/uu/tac/Cargo.toml index 3df365bad19..4455ebe139a 100644 --- a/src/uu/tac/Cargo.toml +++ b/src/uu/tac/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "uu_tac" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "tac ~ (uutils) concatenate and display input lines in reverse order" @@ -17,11 +17,11 @@ edition = "2021" path = "src/tac.rs" [dependencies] -memchr = { workspace=true } -memmap2 = "0.5" -regex = { workspace=true } -clap = { workspace=true } -uucore = { workspace=true } +memchr = { workspace = true } +memmap2 = "0.7" +regex = { workspace = true } +clap = { workspace = true } +uucore = { workspace = true } [[bin]] name = "tac" diff --git a/src/uu/tac/src/tac.rs b/src/uu/tac/src/tac.rs index aef9932a27c..96bb82f1e1b 100644 --- a/src/uu/tac/src/tac.rs +++ b/src/uu/tac/src/tac.rs @@ -222,6 +222,7 @@ fn buffer_tac(data: &[u8], before: bool, separator: &str) -> std::io::Result<()> Ok(()) } +#[allow(clippy::cognitive_complexity)] fn tac(filenames: &[&str], before: bool, regex: bool, separator: &str) -> UResult<()> { // Compile the regular expression pattern if it is provided. let maybe_pattern = if regex { diff --git a/src/uu/tail/Cargo.toml b/src/uu/tail/Cargo.toml index 88b82a8e6fd..81213b58853 100644 --- a/src/uu/tail/Cargo.toml +++ b/src/uu/tail/Cargo.toml @@ -1,7 +1,7 @@ # spell-checker:ignore (libs) kqueue fundu [package] name = "uu_tail" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "tail ~ (uutils) display the last lines of input" @@ -16,18 +16,24 @@ edition = "2021" path = "src/tail.rs" [dependencies] -clap = { workspace=true } -libc = { workspace=true } -memchr = { workspace=true } -notify = { workspace=true } -uucore = { workspace=true } -same-file = { workspace=true } -is-terminal = { workspace=true } -fundu = { workspace=true } +clap = { workspace = true } +libc = { workspace = true } +memchr = { workspace = true } +notify = { workspace = true } +uucore = { workspace = true } +same-file = { workspace = true } +is-terminal = { workspace = true } +fundu = { workspace = true } [target.'cfg(windows)'.dependencies] -windows-sys = { workspace=true, features = ["Win32_System_Threading", "Win32_Foundation"] } -winapi-util = { workspace=true } +windows-sys = { workspace = true, features = [ + "Win32_System_Threading", + "Win32_Foundation", +] } +winapi-util = { workspace = true } + +[dev-dependencies] +rstest = { workspace = true } [[bin]] name = "tail" diff --git a/src/uu/tail/src/args.rs b/src/uu/tail/src/args.rs index 8e039b5f4b0..a7ddb077d43 100644 --- a/src/uu/tail/src/args.rs +++ b/src/uu/tail/src/args.rs @@ -8,8 +8,8 @@ use crate::paths::Input; use crate::{parse, platform, Quotable}; use clap::crate_version; -use clap::{parser::ValueSource, Arg, ArgAction, ArgMatches, Command}; -use fundu::DurationParser; +use clap::{Arg, ArgAction, ArgMatches, Command}; +use fundu::{DurationParser, SaturatingInto}; use is_terminal::IsTerminal; use same_file::Handle; use std::collections::VecDeque; @@ -24,22 +24,22 @@ const USAGE: &str = help_usage!("tail.md"); pub mod options { pub mod verbosity { - pub static QUIET: &str = "quiet"; - pub static VERBOSE: &str = "verbose"; + pub const QUIET: &str = "quiet"; + pub const VERBOSE: &str = "verbose"; } - pub static BYTES: &str = "bytes"; - pub static FOLLOW: &str = "follow"; - pub static LINES: &str = "lines"; - pub static PID: &str = "pid"; - pub static SLEEP_INT: &str = "sleep-interval"; - pub static ZERO_TERM: &str = "zero-terminated"; - pub static DISABLE_INOTIFY_TERM: &str = "-disable-inotify"; // NOTE: three hyphens is correct - pub static USE_POLLING: &str = "use-polling"; - pub static RETRY: &str = "retry"; - pub static FOLLOW_RETRY: &str = "F"; - pub static MAX_UNCHANGED_STATS: &str = "max-unchanged-stats"; - pub static ARG_FILES: &str = "files"; - pub static PRESUME_INPUT_PIPE: &str = "-presume-input-pipe"; // NOTE: three hyphens is correct + pub const BYTES: &str = "bytes"; + pub const FOLLOW: &str = "follow"; + pub const LINES: &str = "lines"; + pub const PID: &str = "pid"; + pub const SLEEP_INT: &str = "sleep-interval"; + pub const ZERO_TERM: &str = "zero-terminated"; + pub const DISABLE_INOTIFY_TERM: &str = "-disable-inotify"; // NOTE: three hyphens is correct + pub const USE_POLLING: &str = "use-polling"; + pub const RETRY: &str = "retry"; + pub const FOLLOW_RETRY: &str = "F"; + pub const MAX_UNCHANGED_STATS: &str = "max-unchanged-stats"; + pub const ARG_FILES: &str = "files"; + pub const PRESUME_INPUT_PIPE: &str = "-presume-input-pipe"; // NOTE: three hyphens is correct } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -80,7 +80,7 @@ impl FilterMode { Err(e) => { return Err(USimpleError::new( 1, - format!("invalid number of bytes: {e}"), + format!("invalid number of bytes: '{e}'"), )) } } @@ -182,19 +182,44 @@ impl Settings { } pub fn from(matches: &clap::ArgMatches) -> UResult { - let mut settings: Self = Self { - follow: if matches.get_flag(options::FOLLOW_RETRY) { - Some(FollowMode::Name) - } else if matches.value_source(options::FOLLOW) != Some(ValueSource::CommandLine) { - None - } else if matches.get_one::(options::FOLLOW) - == Some(String::from("name")).as_ref() + // We're parsing --follow, -F and --retry under the following conditions: + // * -F sets --retry and --follow=name + // * plain --follow or short -f is the same like specifying --follow=descriptor + // * All these options and flags can occur multiple times as command line arguments + let follow_retry = matches.get_flag(options::FOLLOW_RETRY); + // We don't need to check for occurrences of --retry if -F was specified which already sets + // retry + let retry = follow_retry || matches.get_flag(options::RETRY); + let follow = match ( + follow_retry, + matches + .get_one::(options::FOLLOW) + .map(|s| s.as_str()), + ) { + // -F and --follow if -F is specified after --follow. We don't need to care about the + // value of --follow. + (true, Some(_)) + // It's ok to use `index_of` instead of `indices_of` since -F and --follow + // overwrite themselves (not only the value but also the index). + if matches.index_of(options::FOLLOW_RETRY) > matches.index_of(options::FOLLOW) => { Some(FollowMode::Name) - } else { - Some(FollowMode::Descriptor) - }, - retry: matches.get_flag(options::RETRY) || matches.get_flag(options::FOLLOW_RETRY), + } + // * -F and --follow=name if --follow=name is specified after -F + // * No occurrences of -F but --follow=name + // * -F and no occurrences of --follow + (_, Some("name")) | (true, None) => Some(FollowMode::Name), + // * -F and --follow=descriptor (or plain --follow, -f) if --follow=descriptor is + // specified after -F + // * No occurrences of -F but --follow=descriptor, --follow, -f + (_, Some(_)) => Some(FollowMode::Descriptor), + // The default for no occurrences of -F or --follow + (false, None) => None, + }; + + let mut settings: Self = Self { + follow, + retry, use_polling: matches.get_flag(options::USE_POLLING), mode: FilterMode::from(matches)?, verbose: matches.get_flag(options::verbosity::VERBOSE), @@ -210,12 +235,15 @@ impl Settings { // `DURATION::MAX` or `infinity` was given // * not applied here but it supports customizable time units and provides better error // messages - settings.sleep_sec = - DurationParser::without_time_units() - .parse(source) - .map_err(|_| { - UUsageError::new(1, format!("invalid number of seconds: '{source}'")) - })?; + settings.sleep_sec = match DurationParser::without_time_units().parse(source) { + Ok(duration) => SaturatingInto::::saturating_into(duration), + Err(_) => { + return Err(UUsageError::new( + 1, + format!("invalid number of seconds: '{source}'"), + )) + } + } } if let Some(s) = matches.get_one::(options::MAX_UNCHANGED_STATS) { @@ -390,16 +418,17 @@ fn parse_num(src: &str) -> Result { starting_with = true; } } - } else { - return Err(ParseSizeError::ParseFailure(src.to_string())); } - parse_size(size_string).map(|n| match (n, starting_with) { - (0, true) => Signum::PlusZero, - (0, false) => Signum::MinusZero, - (n, true) => Signum::Positive(n), - (n, false) => Signum::Negative(n), - }) + match parse_size(size_string) { + Ok(n) => match (n, starting_with) { + (0, true) => Ok(Signum::PlusZero), + (0, false) => Ok(Signum::MinusZero), + (n, true) => Ok(Signum::Positive(n)), + (n, false) => Ok(Signum::Negative(n)), + }, + Err(_) => Err(ParseSizeError::ParseFailure(size_string.to_string())), + } } pub fn parse_args(args: impl uucore::Args) -> UResult { @@ -445,12 +474,11 @@ pub fn parse_args(args: impl uucore::Args) -> UResult { pub fn uu_app() -> Command { #[cfg(target_os = "linux")] - pub static POLLING_HELP: &str = "Disable 'inotify' support and use polling instead"; + const POLLING_HELP: &str = "Disable 'inotify' support and use polling instead"; #[cfg(all(unix, not(target_os = "linux")))] - pub static POLLING_HELP: &str = "Disable 'kqueue' support and use polling instead"; + const POLLING_HELP: &str = "Disable 'kqueue' support and use polling instead"; #[cfg(target_os = "windows")] - pub static POLLING_HELP: &str = - "Disable 'ReadDirectoryChanges' support and use polling instead"; + const POLLING_HELP: &str = "Disable 'ReadDirectoryChanges' support and use polling instead"; Command::new(uucore::util_name()) .version(crate_version!()) @@ -469,7 +497,7 @@ pub fn uu_app() -> Command { Arg::new(options::FOLLOW) .short('f') .long(options::FOLLOW) - .default_value("descriptor") + .default_missing_value("descriptor") .num_args(0..=1) .require_equals(true) .value_parser(["descriptor", "name"]) @@ -544,13 +572,14 @@ pub fn uu_app() -> Command { Arg::new(options::RETRY) .long(options::RETRY) .help("Keep trying to open a file if it is inaccessible") + .overrides_with(options::RETRY) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::FOLLOW_RETRY) .short('F') .help("Same as --follow=name --retry") - .overrides_with_all([options::RETRY, options::FOLLOW]) + .overrides_with(options::FOLLOW_RETRY) .action(ArgAction::SetTrue), ) .arg( @@ -570,6 +599,8 @@ pub fn uu_app() -> Command { #[cfg(test)] mod tests { + use rstest::rstest; + use crate::parse::ObsoleteArgs; use super::*; @@ -616,4 +647,39 @@ mod tests { let result = Settings::from_obsolete_args(&args, Some(&"file".into())); assert_eq!(result.follow, Some(FollowMode::Name)); } + + #[rstest] + #[case::default(vec![], None, false)] + #[case::retry(vec!["--retry"], None, true)] + #[case::multiple_retry(vec!["--retry", "--retry"], None, true)] + #[case::follow_long(vec!["--follow"], Some(FollowMode::Descriptor), false)] + #[case::follow_short(vec!["-f"], Some(FollowMode::Descriptor), false)] + #[case::follow_long_with_retry(vec!["--follow", "--retry"], Some(FollowMode::Descriptor), true)] + #[case::follow_short_with_retry(vec!["-f", "--retry"], Some(FollowMode::Descriptor), true)] + #[case::follow_overwrites_previous_selection_1(vec!["--follow=name", "--follow=descriptor"], Some(FollowMode::Descriptor), false)] + #[case::follow_overwrites_previous_selection_2(vec!["--follow=descriptor", "--follow=name"], Some(FollowMode::Name), false)] + #[case::big_f(vec!["-F"], Some(FollowMode::Name), true)] + #[case::multiple_big_f(vec!["-F", "-F"], Some(FollowMode::Name), true)] + #[case::big_f_with_retry_then_does_not_change(vec!["-F", "--retry"], Some(FollowMode::Name), true)] + #[case::big_f_with_follow_descriptor_then_change(vec!["-F", "--follow=descriptor"], Some(FollowMode::Descriptor), true)] + #[case::multiple_big_f_with_follow_descriptor_then_no_change(vec!["-F", "--follow=descriptor", "-F"], Some(FollowMode::Name), true)] + #[case::big_f_with_follow_short_then_change(vec!["-F", "-f"], Some(FollowMode::Descriptor), true)] + #[case::follow_descriptor_with_big_f_then_change(vec!["--follow=descriptor", "-F"], Some(FollowMode::Name), true)] + #[case::follow_short_with_big_f_then_change(vec!["-f", "-F"], Some(FollowMode::Name), true)] + #[case::big_f_with_follow_name_then_not_change(vec!["-F", "--follow=name"], Some(FollowMode::Name), true)] + #[case::follow_name_with_big_f_then_not_change(vec!["--follow=name", "-F"], Some(FollowMode::Name), true)] + #[case::big_f_with_multiple_long_follow(vec!["--follow=name", "-F", "--follow=descriptor"], Some(FollowMode::Descriptor), true)] + #[case::big_f_with_multiple_long_follow_name(vec!["--follow=name", "-F", "--follow=name"], Some(FollowMode::Name), true)] + #[case::big_f_with_multiple_short_follow(vec!["-f", "-F", "-f"], Some(FollowMode::Descriptor), true)] + #[case::multiple_big_f_with_multiple_short_follow(vec!["-f", "-F", "-f", "-F"], Some(FollowMode::Name), true)] + fn test_parse_settings_follow_mode_and_retry( + #[case] args: Vec<&str>, + #[case] expected_follow_mode: Option, + #[case] expected_retry: bool, + ) { + let settings = + Settings::from(&uu_app().no_binary_name(true).get_matches_from(args)).unwrap(); + assert_eq!(settings.follow, expected_follow_mode); + assert_eq!(settings.retry, expected_retry); + } } diff --git a/src/uu/tail/src/chunks.rs b/src/uu/tail/src/chunks.rs index 4b934c1d7d3..3aa380e20e0 100644 --- a/src/uu/tail/src/chunks.rs +++ b/src/uu/tail/src/chunks.rs @@ -579,16 +579,16 @@ impl LinesChunkBuffer { } } - if !&self.chunks.is_empty() { + if self.chunks.is_empty() { + // chunks is empty when a file is empty so quitting early here + return Ok(()); + } else { let length = &self.chunks.len(); let last = &mut self.chunks[length - 1]; if !last.get_buffer().ends_with(&[self.delimiter]) { last.lines += 1; self.lines += 1; } - } else { - // chunks is empty when a file is empty so quitting early here - return Ok(()); } // skip unnecessary chunks and save the first chunk which may hold some lines we have to diff --git a/src/uu/tail/src/follow/watch.rs b/src/uu/tail/src/follow/watch.rs index 2c3cf10b855..67ad8f0c2c6 100644 --- a/src/uu/tail/src/follow/watch.rs +++ b/src/uu/tail/src/follow/watch.rs @@ -303,6 +303,7 @@ impl Observer { Ok(()) } + #[allow(clippy::cognitive_complexity)] fn handle_event( &mut self, event: ¬ify::Event, @@ -471,6 +472,7 @@ impl Observer { } } +#[allow(clippy::cognitive_complexity)] pub fn follow(mut observer: Observer, settings: &Settings) -> UResult<()> { if observer.files.no_files_remaining(settings) && !observer.files.only_stdin_remaining() { return Err(USimpleError::new(1, text::NO_FILES_REMAINING.to_string())); diff --git a/src/uu/tail/src/tail.rs b/src/uu/tail/src/tail.rs index 9e3273638bb..e07616c6f59 100644 --- a/src/uu/tail/src/tail.rs +++ b/src/uu/tail/src/tail.rs @@ -125,10 +125,10 @@ fn tail_file( show_error!("error reading '{}': {}", input.display_name, err_msg); if settings.follow.is_some() { - let msg = if !settings.retry { - "; giving up on this name" - } else { + let msg = if settings.retry { "" + } else { + "; giving up on this name" }; show_error!( "{}: cannot follow end of this type of file{}", @@ -215,11 +215,7 @@ fn tail_stdin( // pipe None => { header_printer.print_input(input); - if !paths::stdin_is_bad_fd() { - let mut reader = BufReader::new(stdin()); - unbounded_tail(&mut reader, settings)?; - observer.add_stdin(input.display_name.as_str(), Some(Box::new(reader)), true)?; - } else { + if paths::stdin_is_bad_fd() { set_exit_code(1); show_error!( "cannot fstat {}: {}", @@ -233,6 +229,10 @@ fn tail_stdin( text::BAD_FD ); } + } else { + let mut reader = BufReader::new(stdin()); + unbounded_tail(&mut reader, settings)?; + observer.add_stdin(input.display_name.as_str(), Some(Box::new(reader)), true)?; } } }; diff --git a/src/uu/tail/src/text.rs b/src/uu/tail/src/text.rs index 0e4a26f39aa..e7686d301ed 100644 --- a/src/uu/tail/src/text.rs +++ b/src/uu/tail/src/text.rs @@ -5,20 +5,20 @@ // spell-checker:ignore (ToDO) kqueue -pub static DASH: &str = "-"; -pub static DEV_STDIN: &str = "/dev/stdin"; -pub static STDIN_HEADER: &str = "standard input"; -pub static NO_FILES_REMAINING: &str = "no files remaining"; -pub static NO_SUCH_FILE: &str = "No such file or directory"; -pub static BECOME_INACCESSIBLE: &str = "has become inaccessible"; -pub static BAD_FD: &str = "Bad file descriptor"; +pub const DASH: &str = "-"; +pub const DEV_STDIN: &str = "/dev/stdin"; +pub const STDIN_HEADER: &str = "standard input"; +pub const NO_FILES_REMAINING: &str = "no files remaining"; +pub const NO_SUCH_FILE: &str = "No such file or directory"; +pub const BECOME_INACCESSIBLE: &str = "has become inaccessible"; +pub const BAD_FD: &str = "Bad file descriptor"; #[cfg(target_os = "linux")] -pub static BACKEND: &str = "inotify"; +pub const BACKEND: &str = "inotify"; #[cfg(all(unix, not(target_os = "linux")))] -pub static BACKEND: &str = "kqueue"; +pub const BACKEND: &str = "kqueue"; #[cfg(target_os = "windows")] -pub static BACKEND: &str = "ReadDirectoryChanges"; -pub static FD0: &str = "/dev/fd/0"; -pub static IS_A_DIRECTORY: &str = "Is a directory"; -pub static DEV_TTY: &str = "/dev/tty"; -pub static DEV_PTMX: &str = "/dev/ptmx"; +pub const BACKEND: &str = "ReadDirectoryChanges"; +pub const FD0: &str = "/dev/fd/0"; +pub const IS_A_DIRECTORY: &str = "Is a directory"; +pub const DEV_TTY: &str = "/dev/tty"; +pub const DEV_PTMX: &str = "/dev/ptmx"; diff --git a/src/uu/tee/Cargo.toml b/src/uu/tee/Cargo.toml index 52a2acceac5..21d64c0c2e0 100644 --- a/src/uu/tee/Cargo.toml +++ b/src/uu/tee/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_tee" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "tee ~ (uutils) display input and copy to FILE" @@ -15,9 +15,9 @@ edition = "2021" path = "src/tee.rs" [dependencies] -clap = { workspace=true } -libc = { workspace=true } -uucore = { workspace=true, features=["libc"] } +clap = { workspace = true } +libc = { workspace = true } +uucore = { workspace = true, features = ["libc", "signals"] } [[bin]] name = "tee" diff --git a/src/uu/tee/src/tee.rs b/src/uu/tee/src/tee.rs index f8e6ebe4193..5c388dd0ee0 100644 --- a/src/uu/tee/src/tee.rs +++ b/src/uu/tee/src/tee.rs @@ -261,11 +261,11 @@ fn process_error( Err(f) } Some(OutputErrorMode::ExitNoPipe) => { - if f.kind() != ErrorKind::BrokenPipe { + if f.kind() == ErrorKind::BrokenPipe { + Ok(()) + } else { show_error!("{}: {}", writer.name.maybe_quote(), f); Err(f) - } else { - Ok(()) } } } diff --git a/src/uu/test/Cargo.toml b/src/uu/test/Cargo.toml index 5e7d2e23a10..3c2f2740199 100644 --- a/src/uu/test/Cargo.toml +++ b/src/uu/test/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_test" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "test ~ (uutils) evaluate comparison and file type expressions" @@ -15,12 +15,12 @@ edition = "2021" path = "src/test.rs" [dependencies] -clap = { workspace=true } -libc = { workspace=true } -uucore = { workspace=true } +clap = { workspace = true } +libc = { workspace = true } +uucore = { workspace = true } [target.'cfg(target_os = "redox")'.dependencies] -redox_syscall = { workspace=true } +redox_syscall = { workspace = true } [[bin]] name = "test" diff --git a/src/uu/test/src/error.rs b/src/uu/test/src/error.rs index e9ab1c08221..ced4b216af0 100644 --- a/src/uu/test/src/error.rs +++ b/src/uu/test/src/error.rs @@ -16,12 +16,12 @@ pub type ParseResult = Result; impl std::fmt::Display for ParseError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::Expected(s) => write!(f, "expected {}", s), + Self::Expected(s) => write!(f, "expected {s}"), Self::ExpectedValue => write!(f, "expected value"), - Self::MissingArgument(s) => write!(f, "missing argument after {}", s), - Self::ExtraArgument(s) => write!(f, "extra argument {}", s), - Self::UnknownOperator(s) => write!(f, "unknown operator {}", s), - Self::InvalidInteger(s) => write!(f, "invalid integer {}", s), + Self::MissingArgument(s) => write!(f, "missing argument after {s}"), + Self::ExtraArgument(s) => write!(f, "extra argument {s}"), + Self::UnknownOperator(s) => write!(f, "unknown operator {s}"), + Self::InvalidInteger(s) => write!(f, "invalid integer {s}"), } } } diff --git a/src/uu/test/src/test.rs b/src/uu/test/src/test.rs index 313ef04131e..b0a8fc61312 100644 --- a/src/uu/test/src/test.rs +++ b/src/uu/test/src/test.rs @@ -20,82 +20,27 @@ use std::fs; use std::os::unix::fs::MetadataExt; use uucore::display::Quotable; use uucore::error::{UResult, USimpleError}; -use uucore::format_usage; +use uucore::{format_usage, help_about, help_section}; +const ABOUT: &str = help_about!("test.md"); + +// The help_usage method replaces util name (the first word) with {}. +// And, The format_usage method replaces {} with execution_phrase ( e.g. test or [ ). +// However, This test command has two util names. +// So, we use test or [ instead of {} so that the usage string is correct. const USAGE: &str = "\ - {} EXPRESSION - {} - [ EXPRESSION ] - [ ] - [ OPTION"; +test EXPRESSION +test +[ EXPRESSION ] +[ ] +[ OPTION"; // We use after_help so that this comes after the usage string (it would come before if we used about) -const AFTER_HELP: &str = " -Exit with the status determined by EXPRESSION. - -An omitted EXPRESSION defaults to false. Otherwise, -EXPRESSION is true or false and sets exit status. It is one of: - - ( EXPRESSION ) EXPRESSION is true - ! EXPRESSION EXPRESSION is false - EXPRESSION1 -a EXPRESSION2 both EXPRESSION1 and EXPRESSION2 are true - EXPRESSION1 -o EXPRESSION2 either EXPRESSION1 or EXPRESSION2 is true - - -n STRING the length of STRING is nonzero - STRING equivalent to -n STRING - -z STRING the length of STRING is zero - STRING1 = STRING2 the strings are equal - STRING1 != STRING2 the strings are not equal - - INTEGER1 -eq INTEGER2 INTEGER1 is equal to INTEGER2 - INTEGER1 -ge INTEGER2 INTEGER1 is greater than or equal to INTEGER2 - INTEGER1 -gt INTEGER2 INTEGER1 is greater than INTEGER2 - INTEGER1 -le INTEGER2 INTEGER1 is less than or equal to INTEGER2 - INTEGER1 -lt INTEGER2 INTEGER1 is less than INTEGER2 - INTEGER1 -ne INTEGER2 INTEGER1 is not equal to INTEGER2 - - FILE1 -ef FILE2 FILE1 and FILE2 have the same device and inode numbers - FILE1 -nt FILE2 FILE1 is newer (modification date) than FILE2 - FILE1 -ot FILE2 FILE1 is older than FILE2 - - -b FILE FILE exists and is block special - -c FILE FILE exists and is character special - -d FILE FILE exists and is a directory - -e FILE FILE exists - -f FILE FILE exists and is a regular file - -g FILE FILE exists and is set-group-ID - -G FILE FILE exists and is owned by the effective group ID - -h FILE FILE exists and is a symbolic link (same as -L) - -k FILE FILE exists and has its sticky bit set - -L FILE FILE exists and is a symbolic link (same as -h) - -N FILE FILE exists and has been modified since it was last read - -O FILE FILE exists and is owned by the effective user ID - -p FILE FILE exists and is a named pipe - -r FILE FILE exists and read permission is granted - -s FILE FILE exists and has a size greater than zero - -S FILE FILE exists and is a socket - -t FD file descriptor FD is opened on a terminal - -u FILE FILE exists and its set-user-ID bit is set - -w FILE FILE exists and write permission is granted - -x FILE FILE exists and execute (or search) permission is granted - -Except for -h and -L, all FILE-related tests dereference symbolic links. -Beware that parentheses need to be escaped (e.g., by backslashes) for shells. -INTEGER may also be -l STRING, which evaluates to the length of STRING. - -NOTE: Binary -a and -o are inherently ambiguous. Use 'test EXPR1 && test -EXPR2' or 'test EXPR1 || test EXPR2' instead. - -NOTE: [ honors the --help and --version options, but test does not. -test treats each of those as it treats any other nonempty STRING. - -NOTE: your shell may have its own version of test and/or [, which usually supersedes -the version described here. Please refer to your shell's documentation -for details about the options it supports."; - -const ABOUT: &str = "Check file types and compare values."; +const AFTER_HELP: &str = help_section!("after help", "test.md"); pub fn uu_app() -> Command { + // Disable printing of -h and -v as valid alternatives for --help and --version, + // since we don't recognize -h and -v as help/version flags. Command::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) @@ -112,15 +57,7 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { if binary_name.ends_with('[') { // If invoked as [ we should recognize --help and --version (but not -h or -v) if args.len() == 1 && (args[0] == "--help" || args[0] == "--version") { - // Let clap pretty-print help and version - Command::new(binary_name) - .version(crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) - .after_help(AFTER_HELP) - // Disable printing of -h and -v as valid alternatives for --help and --version, - // since we don't recognize -h and -v as help/version flags. - .get_matches_from(std::iter::once(program).chain(args.into_iter())); + uu_app().get_matches_from(std::iter::once(program).chain(args.into_iter())); return Ok(()); } // If invoked via name '[', matching ']' must be in the last arg diff --git a/src/uu/test/test.md b/src/uu/test/test.md new file mode 100644 index 00000000000..e67eb1824ab --- /dev/null +++ b/src/uu/test/test.md @@ -0,0 +1,79 @@ +# test + +``` +test EXPRESSION +test +[ EXPRESSION ] +[ ] +[ OPTION +``` + +Check file types and compare values. + +## After Help + +Exit with the status determined by `EXPRESSION`. + +An omitted `EXPRESSION` defaults to false. +Otherwise, `EXPRESSION` is true or false and sets exit status. + +It is one of: + +* ( EXPRESSION ) `EXPRESSION` is true +* ! EXPRESSION `EXPRESSION` is false +* EXPRESSION1 -a EXPRESSION2 both `EXPRESSION1` and `EXPRESSION2` are true +* EXPRESSION1 -o EXPRESSION2 either `EXPRESSION1` or `EXPRESSION2` is true + +String operations: +* -n STRING the length of `STRING` is nonzero +* STRING equivalent to -n `STRING` +* -z STRING the length of `STRING` is zero +* STRING1 = STRING2 the strings are equal +* STRING1 != STRING2 the strings are not equal + +Integer comparisons: +* INTEGER1 -eq INTEGER2 `INTEGER1` is equal to `INTEGER2` +* INTEGER1 -ge INTEGER2 `INTEGER1` is greater than or equal to `INTEGER2` +* INTEGER1 -gt INTEGER2 `INTEGER1` is greater than `INTEGER2` +* INTEGER1 -le INTEGER2 `INTEGER1` is less than or equal to `INTEGER2` +* INTEGER1 -lt INTEGER2 `INTEGER1` is less than `INTEGER2` +* INTEGER1 -ne INTEGER2 `INTEGER1` is not equal to `INTEGER2` + +File operations: +* FILE1 -ef FILE2 `FILE1` and `FILE2` have the same device and inode numbers +* FILE1 -nt FILE2 `FILE1` is newer (modification date) than `FILE2` +* FILE1 -ot FILE2 `FILE1` is older than `FILE2` + +* -b FILE `FILE` exists and is block special +* -c FILE `FILE` exists and is character special +* -d FILE `FILE` exists and is a directory +* -e FILE `FILE` exists +* -f FILE `FILE` exists and is a regular file +* -g FILE `FILE` exists and is set-group-ID +* -G FILE `FILE` exists and is owned by the effective group ID +* -h FILE `FILE` exists and is a symbolic link (same as -L) +* -k FILE `FILE` exists and has its sticky bit set +* -L FILE `FILE` exists and is a symbolic link (same as -h) +* -N FILE `FILE` exists and has been modified since it was last read +* -O FILE `FILE` exists and is owned by the effective user ID +* -p FILE `FILE` exists and is a named pipe +* -r FILE `FILE` exists and read permission is granted +* -s FILE `FILE` exists and has a size greater than zero +* -S FILE `FILE` exists and is a socket +* -t FD `file` descriptor `FD` is opened on a terminal +* -u FILE `FILE` exists and its set-user-ID bit is set +* -w FILE `FILE` exists and write permission is granted +* -x FILE `FILE` exists and execute (or search) permission is granted + +Except for `-h` and `-L`, all FILE-related tests dereference (follow) symbolic links. +Beware that parentheses need to be escaped (e.g., by backslashes) for shells. +`INTEGER` may also be -l `STRING`, which evaluates to the length of `STRING`. + +NOTE: Binary `-a` and `-o` are inherently ambiguous. +Use `test EXPR1 && test EXPR2` or `test EXPR1 || test EXPR2` instead. + +NOTE: `[` honors the `--help` and `--version` options, but test does not. +test treats each of those as it treats any other nonempty `STRING`. + +NOTE: your shell may have its own version of `test` and/or `[`, which usually supersedes the version described here. +Please refer to your shell's documentation for details about the options it supports. diff --git a/src/uu/timeout/Cargo.toml b/src/uu/timeout/Cargo.toml index b19a2e9996f..43cadf3e5d8 100644 --- a/src/uu/timeout/Cargo.toml +++ b/src/uu/timeout/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_timeout" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "timeout ~ (uutils) run COMMAND with a DURATION time limit" @@ -15,10 +15,10 @@ edition = "2021" path = "src/timeout.rs" [dependencies] -clap = { workspace=true } -libc = { workspace=true } -nix = { workspace=true, features = ["signal"] } -uucore = { workspace=true, features=["process", "signals"] } +clap = { workspace = true } +libc = { workspace = true } +nix = { workspace = true, features = ["signal"] } +uucore = { workspace = true, features = ["process", "signals"] } [[bin]] name = "timeout" diff --git a/src/uu/timeout/src/timeout.rs b/src/uu/timeout/src/timeout.rs index 153beea3d66..531f29e8243 100644 --- a/src/uu/timeout/src/timeout.rs +++ b/src/uu/timeout/src/timeout.rs @@ -22,12 +22,12 @@ use uucore::process::ChildExt; use uucore::signals::enable_pipe_errors; use uucore::{ - format_usage, show_error, + format_usage, help_about, help_usage, show_error, signals::{signal_by_name_or_value, signal_name_by_value}, }; -static ABOUT: &str = "Start COMMAND, and kill it if still running after DURATION."; -const USAGE: &str = "{} [OPTION] DURATION COMMAND..."; +const ABOUT: &str = help_about!("timeout.md"); +const USAGE: &str = help_usage!("timeout.md"); pub mod options { pub static FOREGROUND: &str = "foreground"; diff --git a/src/uu/timeout/timeout.md b/src/uu/timeout/timeout.md new file mode 100644 index 00000000000..f992ab32772 --- /dev/null +++ b/src/uu/timeout/timeout.md @@ -0,0 +1,7 @@ +# timeout + +``` +timeout [OPTION] DURATION COMMAND... +``` + +Start `COMMAND`, and kill it if still running after `DURATION`. diff --git a/src/uu/touch/Cargo.toml b/src/uu/touch/Cargo.toml index 1fad22c0282..f90725197b8 100644 --- a/src/uu/touch/Cargo.toml +++ b/src/uu/touch/Cargo.toml @@ -1,6 +1,7 @@ +# spell-checker:ignore humantime [package] name = "uu_touch" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "touch ~ (uutils) change FILE timestamps" @@ -15,13 +16,23 @@ edition = "2021" path = "src/touch.rs" [dependencies] -filetime = { workspace=true } -clap = { workspace=true } -time = { workspace=true, features = ["parsing", "formatting", "local-offset", "macros"] } -uucore = { workspace=true, features=["libc"] } +filetime = { workspace = true } +clap = { workspace = true } +# TODO: use workspace dependency (0.3) when switching from time to chrono +humantime_to_duration = "0.2.1" +time = { workspace = true, features = [ + "parsing", + "formatting", + "local-offset", + "macros", +] } +uucore = { workspace = true, features = ["libc"] } [target.'cfg(target_os = "windows")'.dependencies] -windows-sys = { workspace=true, features = ["Win32_Storage_FileSystem", "Win32_Foundation"] } +windows-sys = { workspace = true, features = [ + "Win32_Storage_FileSystem", + "Win32_Foundation", +] } [[bin]] name = "touch" diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index ec88586c648..55663fdab21 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -6,12 +6,11 @@ // For the full copyright and license information, please view the LICENSE file // that was distributed with this source code. -// spell-checker:ignore (ToDO) filetime strptime utcoff strs datetime MMDDhhmm clapv PWSTR lpszfilepath hresult mktime YYYYMMDDHHMM YYMMDDHHMM DATETIME YYYYMMDDHHMMS subsecond -pub extern crate filetime; +// spell-checker:ignore (ToDO) filetime strptime utcoff strs datetime MMDDhhmm clapv PWSTR lpszfilepath hresult mktime YYYYMMDDHHMM YYMMDDHHMM DATETIME YYYYMMDDHHMMS subsecond humantime use clap::builder::ValueParser; use clap::{crate_version, Arg, ArgAction, ArgGroup, Command}; -use filetime::*; +use filetime::{set_symlink_file_times, FileTime}; use std::ffi::OsString; use std::fs::{self, File}; use std::path::{Path, PathBuf}; @@ -23,6 +22,7 @@ use uucore::{format_usage, help_about, help_usage, show}; const ABOUT: &str = help_about!("touch.md"); const USAGE: &str = help_usage!("touch.md"); + pub mod options { // Both SOURCES and sources are needed as we need to be able to refer to the ArgGroup. pub static SOURCES: &str = "sources"; @@ -65,6 +65,7 @@ fn dt_to_filename(tm: time::PrimitiveDateTime) -> FileTime { } #[uucore::main] +#[allow(clippy::cognitive_complexity)] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().try_get_matches_from(args)?; @@ -77,19 +78,57 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { ), ) })?; - let (mut atime, mut mtime) = - if let Some(reference) = matches.get_one::(options::sources::REFERENCE) { - stat(Path::new(reference), !matches.get_flag(options::NO_DEREF))? - } else { - let timestamp = if let Some(date) = matches.get_one::(options::sources::DATE) { - parse_date(date)? - } else if let Some(current) = matches.get_one::(options::sources::CURRENT) { - parse_timestamp(current)? + let (mut atime, mut mtime) = match ( + matches.get_one::(options::sources::REFERENCE), + matches.get_one::(options::sources::DATE), + ) { + (Some(reference), Some(date)) => { + let (atime, mtime) = stat(Path::new(reference), !matches.get_flag(options::NO_DEREF))?; + if let Ok(offset) = humantime_to_duration::from_str(date) { + let mut seconds = offset.whole_seconds(); + let mut nanos = offset.subsec_nanoseconds(); + if nanos < 0 { + nanos += 1_000_000_000; + seconds -= 1; + } + + let ref_atime_secs = atime.unix_seconds(); + let ref_atime_nanos = atime.nanoseconds(); + let atime = FileTime::from_unix_time( + ref_atime_secs + seconds, + ref_atime_nanos + nanos as u32, + ); + + let ref_mtime_secs = mtime.unix_seconds(); + let ref_mtime_nanos = mtime.nanoseconds(); + let mtime = FileTime::from_unix_time( + ref_mtime_secs + seconds, + ref_mtime_nanos + nanos as u32, + ); + + (atime, mtime) } else { - local_dt_to_filetime(time::OffsetDateTime::now_local().unwrap()) - }; + let timestamp = parse_date(date)?; + (timestamp, timestamp) + } + } + (Some(reference), None) => { + stat(Path::new(reference), !matches.get_flag(options::NO_DEREF))? + } + (None, Some(date)) => { + let timestamp = parse_date(date)?; (timestamp, timestamp) - }; + } + (None, None) => { + let timestamp = + if let Some(current) = matches.get_one::(options::sources::CURRENT) { + parse_timestamp(current)? + } else { + local_dt_to_filetime(time::OffsetDateTime::now_local().unwrap()) + }; + (timestamp, timestamp) + } + }; for filename in files { // FIXME: find a way to avoid having to clone the path @@ -101,7 +140,13 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let path = pathbuf.as_path(); - if let Err(e) = path.metadata() { + let metadata_result = if matches.get_flag(options::NO_DEREF) { + path.symlink_metadata() + } else { + path.metadata() + }; + + if let Err(e) = metadata_result { if e.kind() != std::io::ErrorKind::NotFound { return Err(e.map_err_context(|| format!("setting times of {}", filename.quote()))); } @@ -160,14 +205,21 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } } - if matches.get_flag(options::NO_DEREF) { + // sets the file access and modification times for a file or a symbolic link. + // The filename, access time (atime), and modification time (mtime) are provided as inputs. + + // If the filename is not "-", indicating a special case for touch -h -, + // the code checks if the NO_DEREF flag is set, which means the user wants to + // set the times for a symbolic link itself, rather than the file it points to. + if filename == "-" { + filetime::set_file_times(path, atime, mtime) + } else if matches.get_flag(options::NO_DEREF) { set_symlink_file_times(path, atime, mtime) } else { filetime::set_file_times(path, atime, mtime) } .map_err_context(|| format!("setting times of {}", path.quote()))?; } - Ok(()) } @@ -202,7 +254,8 @@ pub fn uu_app() -> Command { .long(options::sources::DATE) .allow_hyphen_values(true) .help("parse argument and use it instead of current time") - .value_name("STRING"), + .value_name("STRING") + .conflicts_with(options::sources::CURRENT), ) .arg( Arg::new(options::MODIFICATION) @@ -234,7 +287,8 @@ pub fn uu_app() -> Command { .help("use this file's times instead of the current time") .value_name("FILE") .value_parser(ValueParser::os_string()) - .value_hint(clap::ValueHint::AnyPath), + .value_hint(clap::ValueHint::AnyPath) + .conflicts_with(options::sources::CURRENT), ) .arg( Arg::new(options::TIME) @@ -254,20 +308,27 @@ pub fn uu_app() -> Command { .value_parser(ValueParser::os_string()) .value_hint(clap::ValueHint::AnyPath), ) - .group(ArgGroup::new(options::SOURCES).args([ - options::sources::CURRENT, - options::sources::DATE, - options::sources::REFERENCE, - ])) + .group( + ArgGroup::new(options::SOURCES) + .args([ + options::sources::CURRENT, + options::sources::DATE, + options::sources::REFERENCE, + ]) + .multiple(true), + ) } +// Get metadata of the provided path +// If `follow` is `true`, the function will try to follow symlinks +// If `follow` is `false` or the symlink is broken, the function will return metadata of the symlink itself fn stat(path: &Path, follow: bool) -> UResult<(FileTime, FileTime)> { - let metadata = if follow { - fs::symlink_metadata(path) - } else { - fs::metadata(path) - } - .map_err_context(|| format!("failed to get attributes of {}", path.quote()))?; + let metadata = match fs::metadata(path) { + Ok(metadata) => metadata, + Err(e) if e.kind() == std::io::ErrorKind::NotFound && !follow => fs::symlink_metadata(path) + .map_err_context(|| format!("failed to get attributes of {}", path.quote()))?, + Err(e) => return Err(e.into()), + }; Ok(( FileTime::from_last_access_time(&metadata), @@ -384,55 +445,7 @@ fn parse_date(s: &str) -> UResult { } } - // Relative day, like "today", "tomorrow", or "yesterday". - match s { - "now" | "today" => { - let now_local = time::OffsetDateTime::now_local().unwrap(); - return Ok(local_dt_to_filetime(now_local)); - } - "tomorrow" => { - let duration = time::Duration::days(1); - let now_local = time::OffsetDateTime::now_local().unwrap(); - let diff = now_local.checked_add(duration).unwrap(); - return Ok(local_dt_to_filetime(diff)); - } - "yesterday" => { - let duration = time::Duration::days(1); - let now_local = time::OffsetDateTime::now_local().unwrap(); - let diff = now_local.checked_sub(duration).unwrap(); - return Ok(local_dt_to_filetime(diff)); - } - _ => {} - } - - // Relative time, like "-1 hour" or "+3 days". - // - // TODO Add support for "year" and "month". - // TODO Add support for times without spaces like "-1hour". - let tokens: Vec<&str> = s.split_whitespace().collect(); - let maybe_duration = match &tokens[..] { - [num_str, "fortnight" | "fortnights"] => num_str - .parse::() - .ok() - .map(|n| time::Duration::weeks(2 * n)), - ["fortnight" | "fortnights"] => Some(time::Duration::weeks(2)), - [num_str, "week" | "weeks"] => num_str.parse::().ok().map(time::Duration::weeks), - ["week" | "weeks"] => Some(time::Duration::weeks(1)), - [num_str, "day" | "days"] => num_str.parse::().ok().map(time::Duration::days), - ["day" | "days"] => Some(time::Duration::days(1)), - [num_str, "hour" | "hours"] => num_str.parse::().ok().map(time::Duration::hours), - ["hour" | "hours"] => Some(time::Duration::hours(1)), - [num_str, "minute" | "minutes" | "min" | "mins"] => { - num_str.parse::().ok().map(time::Duration::minutes) - } - ["minute" | "minutes" | "min" | "mins"] => Some(time::Duration::minutes(1)), - [num_str, "second" | "seconds" | "sec" | "secs"] => { - num_str.parse::().ok().map(time::Duration::seconds) - } - ["second" | "seconds" | "sec" | "secs"] => Some(time::Duration::seconds(1)), - _ => None, - }; - if let Some(duration) = maybe_duration { + if let Ok(duration) = humantime_to_duration::from_str(s) { let now_local = time::OffsetDateTime::now_local().unwrap(); let diff = now_local.checked_add(duration).unwrap(); return Ok(local_dt_to_filetime(diff)); diff --git a/src/uu/tr/Cargo.toml b/src/uu/tr/Cargo.toml index eee6c7cd62a..e3eba0a1021 100644 --- a/src/uu/tr/Cargo.toml +++ b/src/uu/tr/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_tr" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "tr ~ (uutils) translate characters within input and display" @@ -15,9 +15,9 @@ edition = "2021" path = "src/tr.rs" [dependencies] -nom = { workspace=true } -clap = { workspace=true } -uucore = { workspace=true } +nom = { workspace = true } +clap = { workspace = true } +uucore = { workspace = true } [[bin]] name = "tr" diff --git a/src/uu/tr/src/tr.rs b/src/uu/tr/src/tr.rs index 8ccd0836608..9abbca63111 100644 --- a/src/uu/tr/src/tr.rs +++ b/src/uu/tr/src/tr.rs @@ -13,17 +13,15 @@ use clap::{crate_version, Arg, ArgAction, Command}; use nom::AsBytes; use operation::{translate_input, Sequence, SqueezeOperation, TranslateOperation}; use std::io::{stdin, stdout, BufReader, BufWriter}; -use uucore::{format_usage, show}; +use uucore::{format_usage, help_about, help_section, help_usage, show}; use crate::operation::DeleteOperation; use uucore::display::Quotable; use uucore::error::{UResult, USimpleError, UUsageError}; -const ABOUT: &str = "Translate or delete characters"; -const USAGE: &str = "{} [OPTION]... SET1 [SET2]"; -const LONG_USAGE: &str = "\ - Translate, squeeze, and/or delete characters from standard input, \ - writing to standard output."; +const ABOUT: &str = help_about!("tr.md"); +const AFTER_HELP: &str = help_section!("after help", "tr.md"); +const USAGE: &str = help_usage!("tr.md"); mod options { pub const COMPLEMENT: &str = "complement"; @@ -37,7 +35,7 @@ mod options { pub fn uumain(args: impl uucore::Args) -> UResult<()> { let args = args.collect_lossy(); - let matches = uu_app().after_help(LONG_USAGE).try_get_matches_from(args)?; + let matches = uu_app().after_help(AFTER_HELP).try_get_matches_from(args)?; let delete_flag = matches.get_flag(options::DELETE); let complement_flag = matches.get_flag(options::COMPLEMENT); diff --git a/src/uu/tr/tr.md b/src/uu/tr/tr.md new file mode 100644 index 00000000000..93349eeaa84 --- /dev/null +++ b/src/uu/tr/tr.md @@ -0,0 +1,11 @@ +# tr + +``` +tr [OPTION]... SET1 [SET2] +``` + +Translate or delete characters + +## After help + +Translate, squeeze, and/or delete characters from standard input, writing to standard output. diff --git a/src/uu/true/Cargo.toml b/src/uu/true/Cargo.toml index 9954c290634..f7f7e1a6ae5 100644 --- a/src/uu/true/Cargo.toml +++ b/src/uu/true/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_true" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "true ~ (uutils) do nothing and succeed" @@ -15,8 +15,8 @@ edition = "2021" path = "src/true.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true } +clap = { workspace = true } +uucore = { workspace = true } [[bin]] name = "true" diff --git a/src/uu/true/src/true.rs b/src/uu/true/src/true.rs index 7c373fb7abd..334652ce8d9 100644 --- a/src/uu/true/src/true.rs +++ b/src/uu/true/src/true.rs @@ -7,14 +7,9 @@ use clap::{Arg, ArgAction, Command}; use std::{ffi::OsString, io::Write}; use uucore::error::{set_exit_code, UResult}; +use uucore::help_about; -static ABOUT: &str = "\ -Returns true, a successful exit status. - -Immediately returns with the exit status `0`, except when invoked with one of the recognized -options. In those cases it will try to write the help or version text. Any IO error during this -operation causes the program to return `1` instead. -"; +const ABOUT: &str = help_about!("true.md"); #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { diff --git a/src/uu/true/true.md b/src/uu/true/true.md new file mode 100644 index 00000000000..b21c9616e14 --- /dev/null +++ b/src/uu/true/true.md @@ -0,0 +1,11 @@ +# true + +``` +true +``` + +Returns true, a successful exit status. + +Immediately returns with the exit status `0`, except when invoked with one of the recognized +options. In those cases it will try to write the help or version text. Any IO error during this +operation causes the program to return `1` instead. diff --git a/src/uu/truncate/Cargo.toml b/src/uu/truncate/Cargo.toml index e93751e89e5..bf36e8257c2 100644 --- a/src/uu/truncate/Cargo.toml +++ b/src/uu/truncate/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_truncate" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "truncate ~ (uutils) truncate (or extend) FILE to SIZE" @@ -15,8 +15,8 @@ edition = "2021" path = "src/truncate.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true } +clap = { workspace = true } +uucore = { workspace = true } [[bin]] name = "truncate" diff --git a/src/uu/tsort/Cargo.toml b/src/uu/tsort/Cargo.toml index 2737454eb5a..b7df32a1dd5 100644 --- a/src/uu/tsort/Cargo.toml +++ b/src/uu/tsort/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_tsort" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "tsort ~ (uutils) topologically sort input (partially ordered) pairs" @@ -15,8 +15,8 @@ edition = "2021" path = "src/tsort.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true } +clap = { workspace = true } +uucore = { workspace = true } [[bin]] name = "tsort" diff --git a/src/uu/tsort/src/tsort.rs b/src/uu/tsort/src/tsort.rs index cb179699db0..6e11a7f580f 100644 --- a/src/uu/tsort/src/tsort.rs +++ b/src/uu/tsort/src/tsort.rs @@ -6,18 +6,16 @@ // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. use clap::{crate_version, Arg, Command}; -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeMap, BTreeSet}; use std::fs::File; use std::io::{stdin, BufRead, BufReader, Read}; use std::path::Path; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError}; -use uucore::format_usage; +use uucore::{format_usage, help_about, help_usage}; -static ABOUT: &str = "Topological sort the strings in FILE. -Strings are defined as any sequence of tokens separated by whitespace (tab, space, or newline). -If FILE is not passed in, stdin is used instead."; -static USAGE: &str = "{} [OPTIONS] FILE"; +const ABOUT: &str = help_about!("tsort.md"); +const USAGE: &str = help_usage!("tsort.md"); mod options { pub const FILE: &str = "file"; @@ -105,8 +103,8 @@ pub fn uu_app() -> Command { // but using integer may improve performance. #[derive(Default)] struct Graph { - in_edges: HashMap>, - out_edges: HashMap>, + in_edges: BTreeMap>, + out_edges: BTreeMap>, result: Vec, } @@ -124,7 +122,7 @@ impl Graph { } fn init_node(&mut self, n: &str) { - self.in_edges.insert(n.to_string(), HashSet::new()); + self.in_edges.insert(n.to_string(), BTreeSet::new()); self.out_edges.insert(n.to_string(), vec![]); } diff --git a/src/uu/tsort/tsort.md b/src/uu/tsort/tsort.md new file mode 100644 index 00000000000..5effbf1e705 --- /dev/null +++ b/src/uu/tsort/tsort.md @@ -0,0 +1,10 @@ +# tsort + +``` +tsort [OPTIONS] FILE +``` + +Topological sort the strings in FILE. +Strings are defined as any sequence of tokens separated by whitespace (tab, space, or newline), ordering them based on dependencies in a directed acyclic graph (DAG). +Useful for scheduling and determining execution order. +If FILE is not passed in, stdin is used instead. diff --git a/src/uu/tty/Cargo.toml b/src/uu/tty/Cargo.toml index ee7fe559b08..d3d16d22a04 100644 --- a/src/uu/tty/Cargo.toml +++ b/src/uu/tty/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_tty" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "tty ~ (uutils) display the name of the terminal connected to standard input" @@ -15,10 +15,10 @@ edition = "2021" path = "src/tty.rs" [dependencies] -clap = { workspace=true } -nix = { workspace=true, features=["term"] } -is-terminal = { workspace=true } -uucore = { workspace=true, features=["fs"] } +clap = { workspace = true } +nix = { workspace = true, features = ["term"] } +is-terminal = { workspace = true } +uucore = { workspace = true, features = ["fs"] } [[bin]] name = "tty" diff --git a/src/uu/tty/src/tty.rs b/src/uu/tty/src/tty.rs index e3fc0451ef3..e2d9d184702 100644 --- a/src/uu/tty/src/tty.rs +++ b/src/uu/tty/src/tty.rs @@ -14,10 +14,10 @@ use is_terminal::IsTerminal; use std::io::Write; use std::os::unix::io::AsRawFd; use uucore::error::{set_exit_code, UResult}; -use uucore::format_usage; +use uucore::{format_usage, help_about, help_usage}; -static ABOUT: &str = "Print the file name of the terminal connected to standard input."; -const USAGE: &str = "{} [OPTION]..."; +const ABOUT: &str = help_about!("tty.md"); +const USAGE: &str = help_usage!("tty.md"); mod options { pub const SILENT: &str = "silent"; diff --git a/src/uu/tty/tty.md b/src/uu/tty/tty.md new file mode 100644 index 00000000000..230399a2069 --- /dev/null +++ b/src/uu/tty/tty.md @@ -0,0 +1,7 @@ +# tty + +``` +tty [OPTION]... +``` + +Print the file name of the terminal connected to standard input. diff --git a/src/uu/uname/Cargo.toml b/src/uu/uname/Cargo.toml index 818e6dd06d0..7b5f455a36f 100644 --- a/src/uu/uname/Cargo.toml +++ b/src/uu/uname/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_uname" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "uname ~ (uutils) display system information" @@ -15,9 +15,9 @@ edition = "2021" path = "src/uname.rs" [dependencies] -platform-info = { workspace=true } -clap = { workspace=true } -uucore = { workspace=true } +platform-info = { workspace = true } +clap = { workspace = true } +uucore = { workspace = true } [[bin]] name = "uname" diff --git a/src/uu/uname/src/uname.rs b/src/uu/uname/src/uname.rs index df744dd8509..8b867923454 100644 --- a/src/uu/uname/src/uname.rs +++ b/src/uu/uname/src/uname.rs @@ -13,7 +13,7 @@ use clap::{crate_version, Arg, ArgAction, Command}; use platform_info::*; use uucore::{ - error::{FromIo, UResult}, + error::{UResult, USimpleError}, format_usage, help_about, help_usage, }; @@ -36,8 +36,8 @@ pub mod options { pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().try_get_matches_from(args)?; - let uname = - PlatformInfo::new().map_err_context(|| "failed to create PlatformInfo".to_string())?; + let uname = PlatformInfo::new().map_err(|_e| USimpleError::new(1, "cannot get system name"))?; + let mut output = String::new(); let all = matches.get_flag(options::ALL); @@ -61,33 +61,32 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { || hardware_platform); if kernel_name || all || none { - output.push_str(&uname.sysname()); + output.push_str(&uname.sysname().to_string_lossy()); output.push(' '); } if nodename || all { - // maint: [2023-01-14; rivy] remove `.trim_end_matches('\0')` when platform-info nodename-NUL bug is fixed (see GH:uutils/platform-info/issues/32) - output.push_str(uname.nodename().trim_end_matches('\0')); + output.push_str(&uname.nodename().to_string_lossy()); output.push(' '); } if kernel_release || all { - output.push_str(&uname.release()); + output.push_str(&uname.release().to_string_lossy()); output.push(' '); } if kernel_version || all { - output.push_str(&uname.version()); + output.push_str(&uname.version().to_string_lossy()); output.push(' '); } if machine || all { - output.push_str(&uname.machine()); + output.push_str(&uname.machine().to_string_lossy()); output.push(' '); } if os || all { - output.push_str(&uname.osname()); + output.push_str(&uname.osname().to_string_lossy()); output.push(' '); } diff --git a/src/uu/unexpand/Cargo.toml b/src/uu/unexpand/Cargo.toml index a80bcdaf5db..b3d5e1b40b4 100644 --- a/src/uu/unexpand/Cargo.toml +++ b/src/uu/unexpand/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_unexpand" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "unexpand ~ (uutils) convert input spaces to tabs" @@ -15,9 +15,9 @@ edition = "2021" path = "src/unexpand.rs" [dependencies] -clap = { workspace=true } -unicode-width = { workspace=true } -uucore = { workspace=true } +clap = { workspace = true } +unicode-width = { workspace = true } +uucore = { workspace = true } [[bin]] name = "unexpand" diff --git a/src/uu/unexpand/src/unexpand.rs b/src/uu/unexpand/src/unexpand.rs index df2348a8c91..dd4471e2d6a 100644 --- a/src/uu/unexpand/src/unexpand.rs +++ b/src/uu/unexpand/src/unexpand.rs @@ -19,11 +19,10 @@ use std::str::from_utf8; use unicode_width::UnicodeWidthChar; use uucore::display::Quotable; use uucore::error::{FromIo, UError, UResult}; -use uucore::{crash, crash_if_err, format_usage}; +use uucore::{crash, crash_if_err, format_usage, help_about, help_usage}; -static USAGE: &str = "{} [OPTION]... [FILE]..."; -static ABOUT: &str = "Convert blanks in each FILE to tabs, writing to standard output.\n\n\ - With no FILE, or when FILE is -, read standard input."; +const USAGE: &str = help_usage!("unexpand.md"); +const ABOUT: &str = help_about!("unexpand.md"); const DEFAULT_TABSTOP: usize = 8; @@ -319,6 +318,7 @@ fn next_char_info(uflag: bool, buf: &[u8], byte: usize) -> (CharType, usize, usi (ctype, cwidth, nbytes) } +#[allow(clippy::cognitive_complexity)] fn unexpand(options: &Options) -> std::io::Result<()> { let mut output = BufWriter::new(stdout()); let ts = &options.tabstops[..]; diff --git a/src/uu/unexpand/unexpand.md b/src/uu/unexpand/unexpand.md new file mode 100644 index 00000000000..6fc69b93a3a --- /dev/null +++ b/src/uu/unexpand/unexpand.md @@ -0,0 +1,8 @@ +# unexpand + +``` +unexpand [OPTION]... [FILE]... +``` + +Convert blanks in each `FILE` to tabs, writing to standard output. +With no `FILE`, or when `FILE` is `-`, read standard input. diff --git a/src/uu/uniq/Cargo.toml b/src/uu/uniq/Cargo.toml index 272cc5a1b56..dec4bf2a49d 100644 --- a/src/uu/uniq/Cargo.toml +++ b/src/uu/uniq/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_uniq" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "uniq ~ (uutils) filter identical adjacent lines from input" @@ -15,8 +15,8 @@ edition = "2021" path = "src/uniq.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true } +clap = { workspace = true } +uucore = { workspace = true } [[bin]] name = "uniq" diff --git a/src/uu/uniq/src/uniq.rs b/src/uu/uniq/src/uniq.rs index cd4d728d9a1..89141f35fb4 100644 --- a/src/uu/uniq/src/uniq.rs +++ b/src/uu/uniq/src/uniq.rs @@ -5,22 +5,18 @@ // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -use clap::{crate_version, Arg, ArgAction, ArgMatches, Command}; +use clap::{builder::ValueParser, crate_version, Arg, ArgAction, ArgMatches, Command}; +use std::ffi::{OsStr, OsString}; use std::fs::File; -use std::io::{self, stdin, stdout, BufRead, BufReader, BufWriter, Read, Write}; -use std::path::Path; +use std::io::{self, stdin, stdout, BufRead, BufReader, BufWriter, Write}; use std::str::FromStr; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError, UUsageError}; -use uucore::format_usage; +use uucore::{format_usage, help_about, help_section, help_usage}; -const ABOUT: &str = "Report or omit repeated lines."; -const USAGE: &str = "{} [OPTION]... [INPUT [OUTPUT]]..."; -const LONG_USAGE: &str = "\ - Filter adjacent matching lines from INPUT (or standard input),\n\ - writing to OUTPUT (or standard output).\n\n\ - Note: 'uniq' does not detect repeated lines unless they are adjacent.\n\ - You may want to sort the input first, or use 'sort -u' without 'uniq'."; +const ABOUT: &str = help_about!("uniq.md"); +const USAGE: &str = help_usage!("uniq.md"); +const AFTER_HELP: &str = help_section!("after help", "uniq.md"); pub mod options { pub static ALL_REPEATED: &str = "all-repeated"; @@ -68,11 +64,7 @@ macro_rules! write_line_terminator { } impl Uniq { - pub fn print_uniq( - &self, - reader: &mut BufReader, - writer: &mut BufWriter, - ) -> UResult<()> { + pub fn print_uniq(&self, reader: impl BufRead, mut writer: impl Write) -> UResult<()> { let mut first_line_printed = false; let mut group_count = 1; let line_terminator = self.get_line_terminator(); @@ -82,6 +74,8 @@ impl Uniq { None => return Ok(()), }; + let writer = &mut writer; + // compare current `line` with consecutive lines (`next_line`) of the input // and if needed, print `line` based on the command line options provided for next_line in lines { @@ -126,7 +120,6 @@ impl Uniq { } match char_indices.find(|(_, c)| c.is_whitespace()) { None => return "", - Some((next_field_i, _)) => i = next_field_i, } } @@ -199,9 +192,9 @@ impl Uniq { || self.delimiters == Delimiters::Both) } - fn print_line( + fn print_line( &self, - writer: &mut BufWriter, + writer: &mut impl Write, line: &str, count: usize, first_line_printed: bool, @@ -213,7 +206,7 @@ impl Uniq { } if self.show_counts { - writer.write_all(format!("{count:7} {line}").as_bytes()) + write!(writer, "{count:7} {line}") } else { writer.write_all(line.as_bytes()) } @@ -247,21 +240,14 @@ fn opt_parsed(opt_name: &str, matches: &ArgMatches) -> UResult UResult<()> { - let matches = uu_app().after_help(LONG_USAGE).try_get_matches_from(args)?; + let matches = uu_app().after_help(AFTER_HELP).try_get_matches_from(args)?; - let files: Vec = matches - .get_many::(ARG_FILES) - .map(|v| v.map(ToString::to_string).collect()) - .unwrap_or_default(); + let files = matches.get_many::(ARG_FILES); - let (in_file_name, out_file_name) = match files.len() { - 0 => ("-".to_owned(), "-".to_owned()), - 1 => (files[0].clone(), "-".to_owned()), - 2 => (files[0].clone(), files[1].clone()), - _ => { - unreachable!() // Cannot happen as clap will fail earlier - } - }; + let (in_file_name, out_file_name) = files + .map(|fi| fi.map(AsRef::as_ref)) + .map(|mut fi| (fi.next(), fi.next())) + .unwrap_or_default(); let uniq = Uniq { repeats_only: matches.get_flag(options::REPEATED) @@ -286,8 +272,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } uniq.print_uniq( - &mut open_input_file(&in_file_name)?, - &mut open_output_file(&out_file_name)?, + open_input_file(in_file_name)?, + open_output_file(out_file_name)?, ) } @@ -391,6 +377,7 @@ pub fn uu_app() -> Command { .arg( Arg::new(ARG_FILES) .action(ArgAction::Append) + .value_parser(ValueParser::os_string()) .num_args(0..=2) .value_hint(clap::ValueHint::FilePath), ) @@ -416,26 +403,26 @@ fn get_delimiter(matches: &ArgMatches) -> Delimiters { } } -fn open_input_file(in_file_name: &str) -> UResult>> { - let in_file = if in_file_name == "-" { - Box::new(stdin()) as Box - } else { - let path = Path::new(in_file_name); - let in_file = File::open(path) - .map_err_context(|| format!("Could not open {}", in_file_name.maybe_quote()))?; - Box::new(in_file) as Box - }; - Ok(BufReader::new(in_file)) +// None or "-" means stdin. +fn open_input_file(in_file_name: Option<&OsStr>) -> UResult> { + Ok(match in_file_name { + Some(path) if path != "-" => { + let in_file = File::open(path) + .map_err_context(|| format!("Could not open {}", path.maybe_quote()))?; + Box::new(BufReader::new(in_file)) + } + _ => Box::new(stdin().lock()), + }) } -fn open_output_file(out_file_name: &str) -> UResult>> { - let out_file = if out_file_name == "-" { - Box::new(stdout()) as Box - } else { - let path = Path::new(out_file_name); - let out_file = File::create(path) - .map_err_context(|| format!("Could not create {}", out_file_name.maybe_quote()))?; - Box::new(out_file) as Box - }; - Ok(BufWriter::new(out_file)) +// None or "-" means stdout. +fn open_output_file(out_file_name: Option<&OsStr>) -> UResult> { + Ok(match out_file_name { + Some(path) if path != "-" => { + let out_file = File::create(path) + .map_err_context(|| format!("Could not open {}", path.maybe_quote()))?; + Box::new(BufWriter::new(out_file)) + } + _ => Box::new(stdout().lock()), + }) } diff --git a/src/uu/uniq/uniq.md b/src/uu/uniq/uniq.md new file mode 100644 index 00000000000..dea57081b77 --- /dev/null +++ b/src/uu/uniq/uniq.md @@ -0,0 +1,15 @@ +# uniq + +``` +uniq [OPTION]... [INPUT [OUTPUT]]... +``` + +Report or omit repeated lines. + +## After help + +Filter adjacent matching lines from `INPUT` (or standard input), +writing to `OUTPUT` (or standard output). + +Note: `uniq` does not detect repeated lines unless they are adjacent. +You may want to sort the input first, or use `sort -u` without `uniq`. diff --git a/src/uu/unlink/Cargo.toml b/src/uu/unlink/Cargo.toml index 6b300dab060..10ec571d19b 100644 --- a/src/uu/unlink/Cargo.toml +++ b/src/uu/unlink/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_unlink" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "unlink ~ (uutils) remove a (file system) link to FILE" @@ -15,8 +15,8 @@ edition = "2021" path = "src/unlink.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true } +clap = { workspace = true } +uucore = { workspace = true } [[bin]] name = "unlink" diff --git a/src/uu/unlink/src/unlink.rs b/src/uu/unlink/src/unlink.rs index 99dd5579182..5d1594a05cd 100644 --- a/src/uu/unlink/src/unlink.rs +++ b/src/uu/unlink/src/unlink.rs @@ -16,8 +16,10 @@ use clap::{crate_version, Arg, Command}; use uucore::display::Quotable; use uucore::error::{FromIo, UResult}; +use uucore::{format_usage, help_about, help_usage}; -static ABOUT: &str = "Unlink the file at FILE."; +const ABOUT: &str = help_about!("unlink.md"); +const USAGE: &str = help_usage!("unlink.md"); static OPT_PATH: &str = "FILE"; #[uucore::main] @@ -33,6 +35,7 @@ pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) + .override_usage(format_usage(USAGE)) .infer_long_args(true) .arg( Arg::new(OPT_PATH) diff --git a/src/uu/unlink/unlink.md b/src/uu/unlink/unlink.md new file mode 100644 index 00000000000..4468fb20450 --- /dev/null +++ b/src/uu/unlink/unlink.md @@ -0,0 +1,7 @@ +# unlink + +``` +unlink [FILE] +``` + +Unlink the file at `FILE`. diff --git a/src/uu/uptime/Cargo.toml b/src/uu/uptime/Cargo.toml index a38a030e8b9..b92254cda2a 100644 --- a/src/uu/uptime/Cargo.toml +++ b/src/uu/uptime/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_uptime" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "uptime ~ (uutils) display dynamic system information" @@ -15,9 +15,9 @@ edition = "2021" path = "src/uptime.rs" [dependencies] -chrono = { workspace=true } -clap = { workspace=true } -uucore = { workspace=true, features=["libc", "utmpx"] } +chrono = { workspace = true } +clap = { workspace = true } +uucore = { workspace = true, features = ["libc", "utmpx"] } [[bin]] name = "uptime" diff --git a/src/uu/users/Cargo.toml b/src/uu/users/Cargo.toml index 32abd158197..81af586291a 100644 --- a/src/uu/users/Cargo.toml +++ b/src/uu/users/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_users" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "users ~ (uutils) display names of currently logged-in users" @@ -15,8 +15,8 @@ edition = "2021" path = "src/users.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true, features=["utmpx"] } +clap = { workspace = true } +uucore = { workspace = true, features = ["utmpx"] } [[bin]] name = "users" diff --git a/src/uu/users/src/users.rs b/src/uu/users/src/users.rs index 05d656576ae..6a5e54f99ff 100644 --- a/src/uu/users/src/users.rs +++ b/src/uu/users/src/users.rs @@ -41,10 +41,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .map(|v| v.map(AsRef::as_ref).collect()) .unwrap_or_default(); - let filename = if !files.is_empty() { - files[0] - } else { + let filename = if files.is_empty() { utmpx::DEFAULT_FILE.as_ref() + } else { + files[0] }; let mut users = Utmpx::iter_all_records_from(filename) diff --git a/src/uu/vdir/Cargo.toml b/src/uu/vdir/Cargo.toml index 569807af6c5..68d0c34aebc 100644 --- a/src/uu/vdir/Cargo.toml +++ b/src/uu/vdir/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_vdir" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "shortcut to ls -l -b" @@ -15,9 +15,9 @@ edition = "2021" path = "src/vdir.rs" [dependencies] -clap = { workspace=true, features = ["env"] } -uucore = { workspace=true, features=["entries", "fs"] } -uu_ls = { workspace=true } +clap = { workspace = true, features = ["env"] } +uucore = { workspace = true, features = ["entries", "fs"] } +uu_ls = { workspace = true } [[bin]] name = "vdir" diff --git a/src/uu/wc/Cargo.toml b/src/uu/wc/Cargo.toml index 40e0fd03b42..363483bf8af 100644 --- a/src/uu/wc/Cargo.toml +++ b/src/uu/wc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_wc" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "wc ~ (uutils) display newline, word, and byte counts for input" @@ -15,15 +15,15 @@ edition = "2021" path = "src/wc.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true, features=["pipes"] } -bytecount = { workspace=true } -utf-8 = { workspace=true } -unicode-width = { workspace=true } +clap = { workspace = true } +uucore = { workspace = true, features = ["pipes"] } +bytecount = { workspace = true } +thiserror = { workspace = true } +unicode-width = { workspace = true } [target.'cfg(unix)'.dependencies] -nix = { workspace=true } -libc = { workspace=true } +nix = { workspace = true } +libc = { workspace = true } [[bin]] name = "wc" diff --git a/src/uu/wc/src/countable.rs b/src/uu/wc/src/countable.rs index 5596decb3e4..b86b96fa2f1 100644 --- a/src/uu/wc/src/countable.rs +++ b/src/uu/wc/src/countable.rs @@ -28,6 +28,7 @@ impl WordCountable for StdinLock<'_> { self } } + impl WordCountable for File { type Buffered = BufReader; diff --git a/src/uu/wc/src/utf8/LICENSE b/src/uu/wc/src/utf8/LICENSE new file mode 100644 index 00000000000..6f3c83e6872 --- /dev/null +++ b/src/uu/wc/src/utf8/LICENSE @@ -0,0 +1,26 @@ +// spell-checker:ignore Sapin +Copyright (c) Simon Sapin and many others + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/src/uu/wc/src/utf8/mod.rs b/src/uu/wc/src/utf8/mod.rs new file mode 100644 index 00000000000..31638e7589c --- /dev/null +++ b/src/uu/wc/src/utf8/mod.rs @@ -0,0 +1,93 @@ +// spell-checker:ignore Sapin +mod read; + +pub use read::{BufReadDecoder, BufReadDecoderError}; + +use std::cmp; +use std::str; + +/// +/// Incremental, zero-copy UTF-8 decoding with error handling +/// +/// The original implementation was written by Simon Sapin in the utf-8 crate . +/// uu_wc used to depend on that crate. +/// The author archived the repository . +/// They suggested incorporating the source directly into uu_wc . +/// + +#[derive(Debug, Copy, Clone)] +pub struct Incomplete { + pub buffer: [u8; 4], + pub buffer_len: u8, +} + +impl Incomplete { + pub fn empty() -> Self { + Self { + buffer: [0, 0, 0, 0], + buffer_len: 0, + } + } + + pub fn is_empty(&self) -> bool { + self.buffer_len == 0 + } + + pub fn new(bytes: &[u8]) -> Self { + let mut buffer = [0, 0, 0, 0]; + let len = bytes.len(); + buffer[..len].copy_from_slice(bytes); + Self { + buffer, + buffer_len: len as u8, + } + } + + fn take_buffer(&mut self) -> &[u8] { + let len = self.buffer_len as usize; + self.buffer_len = 0; + &self.buffer[..len] + } + + /// (consumed_from_input, None): not enough input + /// (consumed_from_input, Some(Err(()))): error bytes in buffer + /// (consumed_from_input, Some(Ok(()))): UTF-8 string in buffer + fn try_complete_offsets(&mut self, input: &[u8]) -> (usize, Option>) { + let initial_buffer_len = self.buffer_len as usize; + let copied_from_input; + { + let unwritten = &mut self.buffer[initial_buffer_len..]; + copied_from_input = cmp::min(unwritten.len(), input.len()); + unwritten[..copied_from_input].copy_from_slice(&input[..copied_from_input]); + } + let spliced = &self.buffer[..initial_buffer_len + copied_from_input]; + match str::from_utf8(spliced) { + Ok(_) => { + self.buffer_len = spliced.len() as u8; + (copied_from_input, Some(Ok(()))) + } + Err(error) => { + let valid_up_to = error.valid_up_to(); + if valid_up_to > 0 { + let consumed = valid_up_to.checked_sub(initial_buffer_len).unwrap(); + self.buffer_len = valid_up_to as u8; + (consumed, Some(Ok(()))) + } else { + match error.error_len() { + Some(invalid_sequence_length) => { + let consumed = invalid_sequence_length + .checked_sub(initial_buffer_len) + .unwrap(); + self.buffer_len = invalid_sequence_length as u8; + (consumed, Some(Err(()))) + } + None => { + self.buffer_len = spliced.len() as u8; + (copied_from_input, None) + } + } + } + } + } + } +} diff --git a/src/uu/wc/src/utf8/read.rs b/src/uu/wc/src/utf8/read.rs new file mode 100644 index 00000000000..1b5bbbe7f76 --- /dev/null +++ b/src/uu/wc/src/utf8/read.rs @@ -0,0 +1,138 @@ +// spell-checker:ignore bytestream +use super::*; +use std::error::Error; +use std::fmt; +use std::io::{self, BufRead}; +use std::str; + +/// Wraps a `std::io::BufRead` buffered byte stream and decode it as UTF-8. +pub struct BufReadDecoder { + buf_read: B, + bytes_consumed: usize, + incomplete: Incomplete, +} + +#[derive(Debug)] +pub enum BufReadDecoderError<'a> { + /// Represents one UTF-8 error in the byte stream. + /// + /// In lossy decoding, each such error should be replaced with U+FFFD. + /// (See `BufReadDecoder::next_lossy` and `BufReadDecoderError::lossy`.) + InvalidByteSequence(&'a [u8]), + + /// An I/O error from the underlying byte stream + Io(io::Error), +} + +impl<'a> fmt::Display for BufReadDecoderError<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + BufReadDecoderError::InvalidByteSequence(bytes) => { + write!(f, "invalid byte sequence: {bytes:02x?}") + } + BufReadDecoderError::Io(ref err) => write!(f, "underlying bytestream error: {err}"), + } + } +} + +impl<'a> Error for BufReadDecoderError<'a> { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match *self { + BufReadDecoderError::InvalidByteSequence(_) => None, + BufReadDecoderError::Io(ref err) => Some(err), + } + } +} + +impl BufReadDecoder { + pub fn new(buf_read: B) -> Self { + Self { + buf_read, + bytes_consumed: 0, + incomplete: Incomplete::empty(), + } + } + + /// Decode and consume the next chunk of UTF-8 input. + /// + /// This method is intended to be called repeatedly until it returns `None`, + /// which represents EOF from the underlying byte stream. + /// This is similar to `Iterator::next`, + /// except that decoded chunks borrow the decoder (~iterator) + /// so they need to be handled or copied before the next chunk can start decoding. + #[allow(clippy::cognitive_complexity)] + pub fn next_strict(&mut self) -> Option> { + enum BytesSource { + BufRead(usize), + Incomplete, + } + macro_rules! try_io { + ($io_result: expr) => { + match $io_result { + Ok(value) => value, + Err(error) => return Some(Err(BufReadDecoderError::Io(error))), + } + }; + } + let (source, result) = loop { + if self.bytes_consumed > 0 { + self.buf_read.consume(self.bytes_consumed); + self.bytes_consumed = 0; + } + let buf = try_io!(self.buf_read.fill_buf()); + + // Force loop iteration to go through an explicit `continue` + enum Unreachable {} + let _: Unreachable = if self.incomplete.is_empty() { + if buf.is_empty() { + return None; // EOF + } + match str::from_utf8(buf) { + Ok(_) => break (BytesSource::BufRead(buf.len()), Ok(())), + Err(error) => { + let valid_up_to = error.valid_up_to(); + if valid_up_to > 0 { + break (BytesSource::BufRead(valid_up_to), Ok(())); + } + match error.error_len() { + Some(invalid_sequence_length) => { + break (BytesSource::BufRead(invalid_sequence_length), Err(())) + } + None => { + self.bytes_consumed = buf.len(); + self.incomplete = Incomplete::new(buf); + // need more input bytes + continue; + } + } + } + } + } else { + if buf.is_empty() { + break (BytesSource::Incomplete, Err(())); // EOF with incomplete code point + } + let (consumed, opt_result) = self.incomplete.try_complete_offsets(buf); + self.bytes_consumed = consumed; + match opt_result { + None => { + // need more input bytes + continue; + } + Some(result) => break (BytesSource::Incomplete, result), + } + }; + }; + let bytes = match source { + BytesSource::BufRead(byte_count) => { + self.bytes_consumed = byte_count; + let buf = try_io!(self.buf_read.fill_buf()); + &buf[..byte_count] + } + BytesSource::Incomplete => self.incomplete.take_buffer(), + }; + match result { + Ok(()) => Some(Ok(unsafe { str::from_utf8_unchecked(bytes) })), + Err(()) => Some(Err(BufReadDecoderError::InvalidByteSequence(bytes))), + } + } +} diff --git a/src/uu/wc/src/wc.rs b/src/uu/wc/src/wc.rs index 0b7b164a810..9fb8ca7a637 100644 --- a/src/uu/wc/src/wc.rs +++ b/src/uu/wc/src/wc.rs @@ -5,57 +5,79 @@ // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -// cSpell:ignore wc wc's +// cSpell:ignore ilog wc wc's mod count_fast; mod countable; +mod utf8; mod word_count; -use clap::builder::ValueParser; -use count_fast::{count_bytes_chars_and_lines_fast, count_bytes_fast}; -use countable::WordCountable; + +use std::{ + borrow::{Borrow, Cow}, + cmp::max, + ffi::OsString, + fs::{self, File}, + io::{self, Write}, + iter, + path::{Path, PathBuf}, +}; + +use clap::{builder::ValueParser, crate_version, Arg, ArgAction, ArgMatches, Command}; +use thiserror::Error; use unicode_width::UnicodeWidthChar; use utf8::{BufReadDecoder, BufReadDecoderError}; -use uucore::{format_usage, help_about, help_usage, show}; -use word_count::{TitledWordCount, WordCount}; - -use clap::{crate_version, Arg, ArgAction, ArgMatches, Command}; -use std::cmp::max; -use std::error::Error; -use std::ffi::{OsStr, OsString}; -use std::fmt::Display; -use std::fs::{self, File}; -use std::io::{self, Read, Write}; -use std::path::PathBuf; +use uucore::{ + error::{FromIo, UError, UResult}, + format_usage, help_about, help_usage, + quoting_style::{escape_name, QuotingStyle}, + show, +}; -use uucore::error::{UError, UResult, USimpleError}; -use uucore::quoting_style::{escape_name, QuotingStyle}; +use crate::{ + count_fast::{count_bytes_chars_and_lines_fast, count_bytes_fast}, + countable::WordCountable, + word_count::WordCount, +}; /// The minimum character width for formatting counts when reading from stdin. const MINIMUM_WIDTH: usize = 7; -struct Settings { +struct Settings<'a> { show_bytes: bool, show_chars: bool, show_lines: bool, show_words: bool, show_max_line_length: bool, - files0_from_stdin_mode: bool, - title_quoting_style: QuotingStyle, + files0_from: Option>, + total_when: TotalWhen, } -impl Settings { - fn new(matches: &ArgMatches) -> Self { - let title_quoting_style = QuotingStyle::Shell { - escape: true, - always_quote: false, - show_control: false, - }; +impl Default for Settings<'_> { + fn default() -> Self { + // Defaults if none of -c, -m, -l, -w, nor -L are specified. + Self { + show_bytes: true, + show_chars: false, + show_lines: true, + show_words: true, + show_max_line_length: false, + files0_from: None, + total_when: TotalWhen::default(), + } + } +} - let files0_from_stdin_mode = match matches.get_one::(options::FILES0_FROM) { - Some(files_0_from) => files_0_from == STDIN_REPR, - None => false, - }; +impl<'a> Settings<'a> { + fn new(matches: &'a ArgMatches) -> Self { + let files0_from = matches + .get_one::(options::FILES0_FROM) + .map(Into::into); + + let total_when = matches + .get_one::(options::TOTAL) + .map(Into::into) + .unwrap_or_default(); let settings = Self { show_bytes: matches.get_flag(options::BYTES), @@ -63,140 +85,302 @@ impl Settings { show_lines: matches.get_flag(options::LINES), show_words: matches.get_flag(options::WORDS), show_max_line_length: matches.get_flag(options::MAX_LINE_LENGTH), - files0_from_stdin_mode, - title_quoting_style, + files0_from, + total_when, }; - if settings.show_bytes - || settings.show_chars - || settings.show_lines - || settings.show_words - || settings.show_max_line_length - { - return settings; - } - - Self { - show_bytes: true, - show_chars: false, - show_lines: true, - show_words: true, - show_max_line_length: false, - files0_from_stdin_mode, - title_quoting_style: settings.title_quoting_style, + if settings.number_enabled() > 0 { + settings + } else { + Self { + files0_from: settings.files0_from, + total_when, + ..Default::default() + } } } fn number_enabled(&self) -> u32 { - let mut result = 0; - result += self.show_bytes as u32; - result += self.show_chars as u32; - result += self.show_lines as u32; - result += self.show_max_line_length as u32; - result += self.show_words as u32; - result + [ + self.show_bytes, + self.show_chars, + self.show_lines, + self.show_max_line_length, + self.show_words, + ] + .into_iter() + .map(Into::::into) + .sum() } } const ABOUT: &str = help_about!("wc.md"); const USAGE: &str = help_usage!("wc.md"); -pub mod options { +mod options { pub static BYTES: &str = "bytes"; pub static CHAR: &str = "chars"; pub static FILES0_FROM: &str = "files0-from"; pub static LINES: &str = "lines"; pub static MAX_LINE_LENGTH: &str = "max-line-length"; + pub static TOTAL: &str = "total"; pub static WORDS: &str = "words"; } - static ARG_FILES: &str = "files"; static STDIN_REPR: &str = "-"; +static QS_ESCAPE: &QuotingStyle = &QuotingStyle::Shell { + escape: true, + always_quote: false, + show_control: false, +}; +static QS_QUOTE_ESCAPE: &QuotingStyle = &QuotingStyle::Shell { + escape: true, + always_quote: true, + show_control: false, +}; + +/// Supported inputs. +#[derive(Debug)] +enum Inputs<'a> { + /// Default Standard input, i.e. no arguments. + Stdin, + /// Files; "-" means stdin, possibly multiple times! + Paths(Vec>), + /// --files0-from; "-" means stdin. + Files0From(Input<'a>), +} + +impl<'a> Inputs<'a> { + fn new(matches: &'a ArgMatches) -> UResult { + let arg_files = matches.get_many::(ARG_FILES); + let files0_from = matches.get_one::(options::FILES0_FROM); + + match (arg_files, files0_from) { + (None, None) => Ok(Self::Stdin), + (Some(files), None) => Ok(Self::Paths(files.map(Into::into).collect())), + (None, Some(path)) => { + // If path is a file, and the file isn't too large, we'll load it ahead + // of time. Every path within the file will have its length checked to + // hopefully better align the output columns. + let input = Input::from(path); + match input.try_as_files0()? { + Some(paths) => Ok(Self::Paths(paths)), + None => Ok(Self::Files0From(input)), + } + } + (Some(_), Some(_)) => Err(WcError::FilesDisabled.into()), + } + } + + // Creates an iterator which yields values borrowed from the command line arguments. + // Returns an error if the file specified in --files0-from cannot be opened. + fn try_iter( + &'a self, + settings: &'a Settings<'a>, + ) -> UResult>> { + let base: Box> = match self { + Self::Stdin => Box::new(iter::once(Ok(Input::Stdin(StdinKind::Implicit)))), + Self::Paths(inputs) => Box::new(inputs.iter().map(|i| Ok(i.as_borrowed()))), + Self::Files0From(input) => match input { + Input::Path(path) => Box::new(files0_iter_file(path)?), + Input::Stdin(_) => Box::new(files0_iter_stdin()), + }, + }; + + // The 1-based index of each yielded item must be tracked for error reporting. + let mut with_idx = base.enumerate().map(|(i, v)| (i + 1, v)); + let files0_from_path = settings.files0_from.as_ref().map(|p| p.as_borrowed()); + + let iter = iter::from_fn(move || { + let (idx, next) = with_idx.next()?; + match next { + // filter zero length file names... + Ok(Input::Path(p)) if p.as_os_str().is_empty() => Some(Err({ + let maybe_ctx = files0_from_path.as_ref().map(|p| (p, idx)); + WcError::zero_len(maybe_ctx).into() + })), + _ => Some(next), + } + }); + Ok(iter) + } +} + +#[derive(Clone, Copy, Debug)] enum StdinKind { - /// Stdin specified on command-line with "-". + /// Specified on command-line with "-" (STDIN_REPR) Explicit, - - /// Stdin implicitly specified on command-line by not passing any positional argument. + /// Implied by the lack of any arguments Implicit, } -/// Supported inputs. -enum Input { - /// A regular file. - Path(PathBuf), - - /// Standard input. +/// Represents a single input, either to be counted or processed for other files names via +/// --files0-from. +#[derive(Debug)] +enum Input<'a> { + Path(Cow<'a, Path>), Stdin(StdinKind), } -impl From<&OsStr> for Input { - fn from(input: &OsStr) -> Self { - if input == STDIN_REPR { +impl From for Input<'_> { + fn from(p: PathBuf) -> Self { + if p.as_os_str() == STDIN_REPR { + Self::Stdin(StdinKind::Explicit) + } else { + Self::Path(Cow::Owned(p)) + } + } +} + +impl<'a, T: AsRef + ?Sized> From<&'a T> for Input<'a> { + fn from(p: &'a T) -> Self { + let p = p.as_ref(); + if p.as_os_str() == STDIN_REPR { Self::Stdin(StdinKind::Explicit) } else { - Self::Path(input.into()) + Self::Path(Cow::Borrowed(p)) } } } -impl Input { +impl<'a> Input<'a> { + /// Translates Path(Cow::Owned(_)) to Path(Cow::Borrowed(_)). + fn as_borrowed(&'a self) -> Self { + match self { + Self::Path(p) => Self::Path(Cow::Borrowed(p.borrow())), + Self::Stdin(k) => Self::Stdin(*k), + } + } + /// Converts input to title that appears in stats. - fn to_title(&self, quoting_style: &QuotingStyle) -> Option { + fn to_title(&self) -> Option> { match self { - Self::Path(path) => Some(escape_name(&path.clone().into_os_string(), quoting_style)), - Self::Stdin(StdinKind::Explicit) => { - Some(escape_name(OsStr::new(STDIN_REPR), quoting_style)) - } + Self::Path(path) => Some(match path.to_str() { + Some(s) if !s.contains('\n') => Cow::Borrowed(s), + _ => Cow::Owned(escape_name(path.as_os_str(), QS_ESCAPE)), + }), + Self::Stdin(StdinKind::Explicit) => Some(Cow::Borrowed(STDIN_REPR)), Self::Stdin(StdinKind::Implicit) => None, } } - fn path_display(&self, quoting_style: &QuotingStyle) -> String { + /// Converts input into the form that appears in errors. + fn path_display(&self) -> String { match self { - Self::Path(path) => escape_name(&path.clone().into_os_string(), quoting_style), - Self::Stdin(_) => escape_name(OsStr::new("standard input"), quoting_style), + Self::Path(path) => escape_name(path.as_os_str(), QS_ESCAPE), + Self::Stdin(_) => String::from("standard input"), + } + } + + /// When given --files0-from, we may be given a path or stdin. Either may be a stream or + /// a regular file. If given a file less than 10 MiB, it will be consumed and turned into + /// a Vec of Input::Paths which can be scanned to determine the widths of the columns that + /// will ultimately be printed. + fn try_as_files0(&self) -> UResult>>> { + match self { + Self::Path(path) => match fs::metadata(path) { + Ok(meta) if meta.is_file() && meta.len() <= (10 << 20) => Ok(Some( + files0_iter_file(path)?.collect::, _>>()?, + )), + _ => Ok(None), + }, + Self::Stdin(_) if is_stdin_small_file() => { + Ok(Some(files0_iter_stdin().collect::, _>>()?)) + } + Self::Stdin(_) => Ok(None), } } } -#[derive(Debug)] -enum WcError { - FilesDisabled(String), - StdinReprNotAllowed(String), +#[cfg(unix)] +fn is_stdin_small_file() -> bool { + use std::os::unix::io::{AsRawFd, FromRawFd}; + // Safety: we'll rely on Rust to give us a valid RawFd for stdin with which we can attempt to + // open a File, but only for the sake of fetching .metadata(). ManuallyDrop will ensure we + // don't do anything else to the FD if anything unexpected happens. + let f = std::mem::ManuallyDrop::new(unsafe { File::from_raw_fd(io::stdin().as_raw_fd()) }); + matches!(f.metadata(), Ok(meta) if meta.is_file() && meta.len() <= (10 << 20)) } -impl UError for WcError { - fn code(&self) -> i32 { - match self { - Self::FilesDisabled(_) | Self::StdinReprNotAllowed(_) => 1, +#[cfg(not(unix))] +// Windows presents a piped stdin as a "normal file" with a length equal to however many bytes +// have been buffered at the time it's checked. To be safe, we must never assume it's a file. +fn is_stdin_small_file() -> bool { + false +} + +/// When to show the "total" line +#[derive(Clone, Copy, Default, PartialEq)] +enum TotalWhen { + #[default] + Auto, + Always, + Only, + Never, +} + +impl> From for TotalWhen { + fn from(s: T) -> Self { + match s.as_ref() { + "auto" => Self::Auto, + "always" => Self::Always, + "only" => Self::Only, + "never" => Self::Never, + _ => unreachable!("Should have been caught by clap"), } } +} - fn usage(&self) -> bool { - matches!(self, Self::FilesDisabled(_)) +impl TotalWhen { + fn is_total_row_visible(&self, num_inputs: usize) -> bool { + match self { + Self::Auto => num_inputs > 1, + Self::Always | Self::Only => true, + Self::Never => false, + } } } -impl Error for WcError {} +#[derive(Debug, Error)] +enum WcError { + #[error("file operands cannot be combined with --files0-from")] + FilesDisabled, + #[error("when reading file names from stdin, no file name of '-' allowed")] + StdinReprNotAllowed, + #[error("invalid zero-length file name")] + ZeroLengthFileName, + #[error("{path}:{idx}: invalid zero-length file name")] + ZeroLengthFileNameCtx { path: Cow<'static, str>, idx: usize }, +} -impl Display for WcError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::FilesDisabled(message) | Self::StdinReprNotAllowed(message) => { - write!(f, "{message}") +impl WcError { + fn zero_len(ctx: Option<(&Input, usize)>) -> Self { + match ctx { + Some((input, idx)) => { + let path = match input { + Input::Stdin(_) => STDIN_REPR.into(), + Input::Path(path) => escape_name(path.as_os_str(), QS_ESCAPE).into(), + }; + Self::ZeroLengthFileNameCtx { path, idx } } + None => Self::ZeroLengthFileName, } } } +impl UError for WcError { + fn usage(&self) -> bool { + matches!(self, Self::FilesDisabled) + } +} + #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().try_get_matches_from(args)?; - let inputs = inputs(&matches)?; - let settings = Settings::new(&matches); + let inputs = Inputs::new(&matches)?; wc(&inputs, &settings) } @@ -225,11 +409,12 @@ pub fn uu_app() -> Command { Arg::new(options::FILES0_FROM) .long(options::FILES0_FROM) .value_name("F") - .help( - "read input from the files specified by - NUL-terminated names in file F; - If F is - then read names from standard input", - ) + .help(concat!( + "read input from the files specified by\n", + " NUL-terminated names in file F;\n", + " If F is - then read names from standard input" + )) + .value_parser(ValueParser::os_string()) .value_hint(clap::ValueHint::FilePath), ) .arg( @@ -246,6 +431,17 @@ pub fn uu_app() -> Command { .help("print the length of the longest line") .action(ArgAction::SetTrue), ) + .arg( + Arg::new(options::TOTAL) + .long(options::TOTAL) + .value_parser(["auto", "always", "only", "never"]) + .value_name("WHEN") + .hide_possible_values(true) + .help(concat!( + "when to print a line with total counts;\n", + " WHEN can be: auto, always, only, never" + )), + ) .arg( Arg::new(options::WORDS) .short('w') @@ -261,47 +457,6 @@ pub fn uu_app() -> Command { ) } -fn inputs(matches: &ArgMatches) -> UResult> { - match matches.get_many::(ARG_FILES) { - Some(os_values) => { - if matches.contains_id(options::FILES0_FROM) { - return Err(WcError::FilesDisabled( - "file operands cannot be combined with --files0-from".into(), - ) - .into()); - } - - Ok(os_values.map(|s| Input::from(s.as_os_str())).collect()) - } - None => match matches.get_one::(options::FILES0_FROM) { - Some(files_0_from) => create_paths_from_files0(files_0_from), - None => Ok(vec![Input::Stdin(StdinKind::Implicit)]), - }, - } -} - -fn create_paths_from_files0(files_0_from: &str) -> UResult> { - let mut paths = String::new(); - let read_from_stdin = files_0_from == STDIN_REPR; - - if read_from_stdin { - io::stdin().lock().read_to_string(&mut paths)?; - } else { - File::open(files_0_from)?.read_to_string(&mut paths)?; - } - - let paths: Vec<&str> = paths.split_terminator('\0').collect(); - - if read_from_stdin && paths.contains(&STDIN_REPR) { - return Err(WcError::StdinReprNotAllowed( - "when reading file names from stdin, no file name of '-' allowed".into(), - ) - .into()); - } - - Ok(paths.iter().map(OsStr::new).map(Input::from).collect()) -} - fn word_count_from_reader( mut reader: T, settings: &Settings, @@ -405,6 +560,7 @@ fn word_count_from_reader( } } +#[allow(clippy::cognitive_complexity)] fn word_count_from_reader_specialized< T: WordCountable, const SHOW_CHARS: bool, @@ -482,171 +638,250 @@ enum CountResult { Failure(io::Error), } -/// If we fail opening a file we only show the error. If we fail reading it -/// we show a count for what we managed to read. +/// If we fail opening a file, we only show the error. If we fail reading the +/// file, we show a count for what we managed to read. /// -/// Therefore the reading implementations always return a total and sometimes +/// Therefore, the reading implementations always return a total and sometimes /// return an error: (WordCount, Option). -fn word_count_from_input(input: &Input, settings: &Settings) -> CountResult { - match input { - Input::Stdin(_) => { - let stdin = io::stdin(); - let stdin_lock = stdin.lock(); - let count = word_count_from_reader(stdin_lock, settings); - match count { - (total, Some(error)) => CountResult::Interrupted(total, error), - (total, None) => CountResult::Success(total), - } - } +fn word_count_from_input(input: &Input<'_>, settings: &Settings) -> CountResult { + let (total, maybe_err) = match input { + Input::Stdin(_) => word_count_from_reader(io::stdin().lock(), settings), Input::Path(path) => match File::open(path) { - Err(error) => CountResult::Failure(error), - Ok(file) => match word_count_from_reader(file, settings) { - (total, Some(error)) => CountResult::Interrupted(total, error), - (total, None) => CountResult::Success(total), - }, + Ok(f) => word_count_from_reader(f, settings), + Err(err) => return CountResult::Failure(err), }, + }; + match maybe_err { + None => CountResult::Success(total), + Some(err) => CountResult::Interrupted(total, err), } } /// Compute the number of digits needed to represent all counts in all inputs. /// -/// `inputs` may include zero or more [`Input::Stdin`] entries, each of -/// which represents reading from `stdin`. The presence of any such -/// entry causes this function to return a width that is at least -/// [`MINIMUM_WIDTH`]. +/// For [`Inputs::Stdin`], [`MINIMUM_WIDTH`] is returned, unless there is only one counter number +/// to be printed, in which case 1 is returned. /// -/// If `input` is empty, or if only one number needs to be printed (for just -/// one file) then this function is optimized to return 1 without making any -/// calls to get file metadata. +/// For [`Inputs::Files0From`], [`MINIMUM_WIDTH`] is returned. /// -/// If file metadata could not be read from any of the [`Input::Path`] input, -/// that input does not affect number width computation +/// An [`Inputs::Paths`] may include zero or more "-" entries, each of which represents reading +/// from `stdin`. The presence of any such entry causes this function to return a width that is at +/// least [`MINIMUM_WIDTH`]. /// -/// Otherwise, the file sizes in the file metadata are summed and the number of -/// digits in that total size is returned as the number width +/// If an [`Inputs::Paths`] contains only one path and only one number needs to be printed then +/// this function is optimized to return 1 without making any calls to get file metadata. /// -/// To mirror GNU wc's behavior a special case is added. If --files0-from is -/// used and input is read from stdin and there is only one calculation enabled -/// columns will not be aligned. This is not exactly GNU wc's behavior, but it -/// is close enough to pass the GNU test suite. -fn compute_number_width(inputs: &[Input], settings: &Settings) -> usize { - if inputs.is_empty() - || (inputs.len() == 1 && settings.number_enabled() == 1) - || (settings.files0_from_stdin_mode && settings.number_enabled() == 1) - { - return 1; - } - - let mut minimum_width = 1; - let mut total = 0; - - for input in inputs { - match input { - Input::Stdin(_) => { - minimum_width = MINIMUM_WIDTH; +/// If file metadata could not be read from any of the [`Input::Path`] input, that input does not +/// affect number width computation. Otherwise, the file sizes from the files' metadata are summed +/// and the number of digits in that total size is returned. +fn compute_number_width(inputs: &Inputs, settings: &Settings) -> usize { + match inputs { + Inputs::Stdin if settings.number_enabled() == 1 => 1, + Inputs::Stdin => MINIMUM_WIDTH, + Inputs::Files0From(_) => 1, + Inputs::Paths(inputs) => { + if settings.number_enabled() == 1 && inputs.len() == 1 { + return 1; } - Input::Path(path) => { - if let Ok(meta) = fs::metadata(path) { - if meta.is_file() { - total += meta.len(); - } else { - minimum_width = MINIMUM_WIDTH; + + let mut minimum_width = 1; + let mut total: u64 = 0; + for input in inputs.iter() { + match input { + Input::Stdin(_) => minimum_width = MINIMUM_WIDTH, + Input::Path(path) => { + if let Ok(meta) = fs::metadata(path) { + if meta.is_file() { + total += meta.len(); + } else { + minimum_width = MINIMUM_WIDTH; + } + } } } } + + if total == 0 { + minimum_width + } else { + let total_width = (1 + ilog10_u64(total)) + .try_into() + .expect("ilog of a u64 should fit into a usize"); + max(total_width, minimum_width) + } } } +} + +type InputIterItem<'a> = Result, Box>; - max(minimum_width, total.to_string().len()) +/// To be used with `--files0-from=-`, this applies a filter on the results of files0_iter to +/// translate '-' into the appropriate error. +fn files0_iter_stdin<'a>() -> impl Iterator> { + files0_iter(io::stdin().lock(), STDIN_REPR.into()).map(|i| match i { + Ok(Input::Stdin(_)) => Err(WcError::StdinReprNotAllowed.into()), + _ => i, + }) +} + +fn files0_iter_file<'a>(path: &Path) -> UResult>> { + match File::open(path) { + Ok(f) => Ok(files0_iter(f, path.into())), + Err(e) => Err(e.map_err_context(|| { + format!( + "cannot open {} for reading", + escape_name(path.as_os_str(), QS_QUOTE_ESCAPE) + ) + })), + } } -fn wc(inputs: &[Input], settings: &Settings) -> UResult<()> { - let number_width = compute_number_width(inputs, settings); +fn files0_iter<'a>( + r: impl io::Read + 'static, + err_path: OsString, +) -> impl Iterator> { + use std::io::BufRead; + let mut i = Some( + io::BufReader::new(r) + .split(b'\0') + .map(move |res| match res { + Ok(p) if p == STDIN_REPR.as_bytes() => Ok(Input::Stdin(StdinKind::Explicit)), + Ok(p) => { + // On Unix systems, OsStrings are just strings of bytes, not necessarily UTF-8. + #[cfg(unix)] + { + use std::os::unix::ffi::OsStringExt; + Ok(Input::Path(PathBuf::from(OsString::from_vec(p)).into())) + } + + // ...Windows does not, we must go through Strings. + #[cfg(not(unix))] + { + let s = String::from_utf8(p) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + Ok(Input::Path(PathBuf::from(s).into())) + } + } + Err(e) => Err(e.map_err_context(|| { + format!("{}: read error", escape_name(&err_path, QS_ESCAPE)) + }) as Box), + }), + ); + // Loop until there is an error; yield that error and then nothing else. + std::iter::from_fn(move || { + let next = i.as_mut().and_then(Iterator::next); + if matches!(next, Some(Err(_)) | None) { + i = None; + } + next + }) +} +fn wc(inputs: &Inputs, settings: &Settings) -> UResult<()> { let mut total_word_count = WordCount::default(); + let mut num_inputs: usize = 0; - let num_inputs = inputs.len(); + let (number_width, are_stats_visible) = match settings.total_when { + TotalWhen::Only => (1, false), + _ => (compute_number_width(inputs, settings), true), + }; - for input in inputs { - let word_count = match word_count_from_input(input, settings) { + for maybe_input in inputs.try_iter(settings)? { + num_inputs += 1; + + let input = match maybe_input { + Ok(input) => input, + Err(err) => { + show!(err); + continue; + } + }; + + let word_count = match word_count_from_input(&input, settings) { CountResult::Success(word_count) => word_count, - CountResult::Interrupted(word_count, error) => { - show!(USimpleError::new( - 1, - format!( - "{}: {}", - input.path_display(&settings.title_quoting_style), - error - ) - )); + CountResult::Interrupted(word_count, err) => { + show!(err.map_err_context(|| input.path_display())); word_count } - CountResult::Failure(error) => { - show!(USimpleError::new( - 1, - format!( - "{}: {}", - input.path_display(&settings.title_quoting_style), - error - ) - )); + CountResult::Failure(err) => { + show!(err.map_err_context(|| input.path_display())); continue; } }; total_word_count += word_count; - let result = word_count.with_title(input.to_title(&settings.title_quoting_style)); - if let Err(err) = print_stats(settings, &result, number_width) { - show!(USimpleError::new( - 1, - format!( - "failed to print result for {}: {}", - &result.title.unwrap_or_else(|| String::from("")), - err, - ), - )); + if are_stats_visible { + let maybe_title = input.to_title(); + let maybe_title_str = maybe_title.as_deref(); + if let Err(err) = print_stats(settings, &word_count, maybe_title_str, number_width) { + let title = maybe_title_str.unwrap_or(""); + show!(err.map_err_context(|| format!("failed to print result for {title}"))); + } } } - if num_inputs > 1 { - let total_result = total_word_count.with_title(Some(String::from("total"))); - if let Err(err) = print_stats(settings, &total_result, number_width) { - show!(USimpleError::new( - 1, - format!("failed to print total: {err}") - )); + if settings.total_when.is_total_row_visible(num_inputs) { + let title = are_stats_visible.then_some("total"); + if let Err(err) = print_stats(settings, &total_word_count, title, number_width) { + show!(err.map_err_context(|| "failed to print total".into())); } } - // Although this appears to be returning `Ok`, the exit code may - // have been set to a non-zero value by a call to `show!()` above. + // Although this appears to be returning `Ok`, the exit code may have been set to a non-zero + // value by a call to `record_error!()` above. Ok(()) } fn print_stats( settings: &Settings, - result: &TitledWordCount, + result: &WordCount, + title: Option<&str>, number_width: usize, ) -> io::Result<()> { - let mut columns = Vec::new(); - - if settings.show_lines { - columns.push(format!("{:1$}", result.count.lines, number_width)); + let mut stdout = io::stdout().lock(); + + let maybe_cols = [ + (settings.show_lines, result.lines), + (settings.show_words, result.words), + (settings.show_chars, result.chars), + (settings.show_bytes, result.bytes), + (settings.show_max_line_length, result.max_line_length), + ]; + + let mut space = ""; + for (_, num) in maybe_cols.iter().filter(|(show, _)| *show) { + write!(stdout, "{space}{num:number_width$}")?; + space = " "; } - if settings.show_words { - columns.push(format!("{:1$}", result.count.words, number_width)); + + if let Some(title) = title { + writeln!(stdout, "{space}{title}") + } else { + writeln!(stdout) } - if settings.show_chars { - columns.push(format!("{:1$}", result.count.chars, number_width)); +} + +// TODO: remove and just use usize::ilog10 once the MSRV is >= 1.67. +fn ilog10_u64(mut u: u64) -> u32 { + if u == 0 { + panic!("cannot compute log of 0") } - if settings.show_bytes { - columns.push(format!("{:1$}", result.count.bytes, number_width)); + let mut log = 0; + if u >= 10_000_000_000 { + log += 10; + u /= 10_000_000_000; } - if settings.show_max_line_length { - columns.push(format!("{:1$}", result.count.max_line_length, number_width)); + if u >= 100_000 { + log += 5; + u /= 100_000; } - if let Some(title) = &result.title { - columns.push(title.clone()); + // Rust's standard library in versions >= 1.67 does something even more clever than this, but + // this should work just fine for the time being. + log + match u { + 1..=9 => 0, + 10..=99 => 1, + 100..=999 => 2, + 1000..=9999 => 3, + 10000..=99999 => 4, + _ => unreachable!(), } - - writeln!(io::stdout().lock(), "{}", columns.join(" ")) } diff --git a/src/uu/wc/src/word_count.rs b/src/uu/wc/src/word_count.rs index b2dd7045024..cf839175f96 100644 --- a/src/uu/wc/src/word_count.rs +++ b/src/uu/wc/src/word_count.rs @@ -29,19 +29,3 @@ impl AddAssign for WordCount { *self = *self + other; } } - -impl WordCount { - pub fn with_title(self, title: Option) -> TitledWordCount { - TitledWordCount { title, count: self } - } -} - -/// This struct supplements the actual word count with an optional title that is -/// displayed to the user at the end of the program. -/// The reason we don't simply include title in the `WordCount` struct is that -/// it would result in unnecessary copying of `String`. -#[derive(Debug, Default, Clone)] -pub struct TitledWordCount { - pub title: Option, - pub count: WordCount, -} diff --git a/src/uu/who/Cargo.toml b/src/uu/who/Cargo.toml index 31c55750850..bfbd7909d81 100644 --- a/src/uu/who/Cargo.toml +++ b/src/uu/who/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_who" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "who ~ (uutils) display information about currently logged-in users" @@ -15,8 +15,8 @@ edition = "2021" path = "src/who.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true, features=["utmpx"] } +clap = { workspace = true } +uucore = { workspace = true, features = ["utmpx"] } [[bin]] name = "who" diff --git a/src/uu/who/src/who.rs b/src/uu/who/src/who.rs index 26248b12b8a..fbfff80d731 100644 --- a/src/uu/who/src/who.rs +++ b/src/uu/who/src/who.rs @@ -316,13 +316,13 @@ fn time_string(ut: &Utmpx) -> String { fn current_tty() -> String { unsafe { let res = ttyname(STDIN_FILENO); - if !res.is_null() { + if res.is_null() { + String::new() + } else { CStr::from_ptr(res as *const _) .to_string_lossy() .trim_start_matches("/dev/") .to_owned() - } else { - String::new() } } } @@ -402,7 +402,7 @@ impl Who { &time_string(ut), "", "", - if !last.is_control() { &comment } else { "" }, + if last.is_control() { "" } else { &comment }, "", ); } @@ -482,7 +482,7 @@ impl Who { let iwgrp = S_IWGRP; #[cfg(any(target_os = "android", target_os = "freebsd", target_vendor = "apple"))] let iwgrp = S_IWGRP as u32; - mesg = if meta.mode() & iwgrp != 0 { '+' } else { '-' }; + mesg = if meta.mode() & iwgrp == 0 { '-' } else { '+' }; last_change = meta.atime(); } _ => { @@ -491,10 +491,10 @@ impl Who { } } - let idle = if last_change != 0 { - idle_string(last_change, 0) - } else { + let idle = if last_change == 0 { " ?".into() + } else { + idle_string(last_change, 0) }; let s = if self.do_lookup { diff --git a/src/uu/whoami/Cargo.toml b/src/uu/whoami/Cargo.toml index 15a7ad4ce46..fe06a0a7e62 100644 --- a/src/uu/whoami/Cargo.toml +++ b/src/uu/whoami/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_whoami" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "whoami ~ (uutils) display user name of current effective user ID" @@ -15,14 +15,18 @@ edition = "2021" path = "src/whoami.rs" [dependencies] -clap = { workspace=true } -uucore = { workspace=true, features=["entries"] } +clap = { workspace = true } +uucore = { workspace = true, features = ["entries"] } [target.'cfg(target_os = "windows")'.dependencies] -windows-sys = { workspace=true, features = ["Win32_NetworkManagement_NetManagement", "Win32_System_WindowsProgramming", "Win32_Foundation"] } +windows-sys = { workspace = true, features = [ + "Win32_NetworkManagement_NetManagement", + "Win32_System_WindowsProgramming", + "Win32_Foundation", +] } [target.'cfg(unix)'.dependencies] -libc = { workspace=true } +libc = { workspace = true } [[bin]] name = "whoami" diff --git a/src/uu/yes/Cargo.toml b/src/uu/yes/Cargo.toml index fd2aca5b498..0e2b934f3a3 100644 --- a/src/uu/yes/Cargo.toml +++ b/src/uu/yes/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_yes" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "yes ~ (uutils) repeatedly display a line with STRING (or 'y')" @@ -15,14 +15,15 @@ edition = "2021" path = "src/yes.rs" [dependencies] -clap = { workspace=true } +clap = { workspace = true } +itertools = { workspace = true } [target.'cfg(unix)'.dependencies] -uucore = { workspace=true, features=["pipes", "signals"] } -nix = { workspace=true } +uucore = { workspace = true, features = ["pipes", "signals"] } +nix = { workspace = true } [target.'cfg(not(unix))'.dependencies] -uucore = { workspace=true, features=["pipes"] } +uucore = { workspace = true, features = ["pipes"] } [[bin]] name = "yes" diff --git a/src/uu/yes/src/yes.rs b/src/uu/yes/src/yes.rs index 41bfeddca62..72c19b87281 100644 --- a/src/uu/yes/src/yes.rs +++ b/src/uu/yes/src/yes.rs @@ -7,8 +7,11 @@ /* last synced with: yes (GNU coreutils) 8.13 */ -use clap::{Arg, ArgAction, Command}; -use std::borrow::Cow; +// cSpell:ignore strs + +use clap::{builder::ValueParser, crate_version, Arg, ArgAction, Command}; +use std::error::Error; +use std::ffi::OsString; use std::io::{self, Write}; use uucore::error::{UResult, USimpleError}; #[cfg(unix)] @@ -28,19 +31,11 @@ const BUF_SIZE: usize = 16 * 1024; pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().try_get_matches_from(args)?; - let string = if let Some(values) = matches.get_many::("STRING") { - let mut result = values.fold(String::new(), |res, s| res + s + " "); - result.pop(); - result.push('\n'); - Cow::from(result) - } else { - Cow::from("y\n") - }; + let mut buffer = Vec::with_capacity(BUF_SIZE); + args_into_buffer(&mut buffer, matches.get_many::("STRING")).unwrap(); + prepare_buffer(&mut buffer); - let mut buffer = [0; BUF_SIZE]; - let bytes = prepare_buffer(&string, &mut buffer); - - match exec(bytes) { + match exec(&buffer) { Ok(()) => Ok(()), Err(err) if err.kind() == io::ErrorKind::BrokenPipe => Ok(()), Err(err) => Err(USimpleError::new(1, format!("standard output: {err}"))), @@ -49,23 +44,76 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) + .version(crate_version!()) .about(ABOUT) .override_usage(format_usage(USAGE)) - .arg(Arg::new("STRING").action(ArgAction::Append)) + .arg( + Arg::new("STRING") + .value_parser(ValueParser::os_string()) + .action(ArgAction::Append), + ) .infer_long_args(true) } -fn prepare_buffer<'a>(input: &'a str, buffer: &'a mut [u8; BUF_SIZE]) -> &'a [u8] { - if input.len() < BUF_SIZE / 2 { - let mut size = 0; - while size < BUF_SIZE - input.len() { - let (_, right) = buffer.split_at_mut(size); - right[..input.len()].copy_from_slice(input.as_bytes()); - size += input.len(); - } - &buffer[..size] +// Copies words from `i` into `buf`, separated by spaces. +fn args_into_buffer<'a>( + buf: &mut Vec, + i: Option>, +) -> Result<(), Box> { + // TODO: this should be replaced with let/else once available in the MSRV. + let i = if let Some(i) = i { + i } else { - input.as_bytes() + buf.extend_from_slice(b"y\n"); + return Ok(()); + }; + + // On Unix (and wasi), OsStrs are just &[u8]'s underneath... + #[cfg(any(unix, target_os = "wasi"))] + { + #[cfg(unix)] + use std::os::unix::ffi::OsStrExt; + #[cfg(target_os = "wasi")] + use std::os::wasi::ffi::OsStrExt; + + for part in itertools::intersperse(i.map(|a| a.as_bytes()), b" ") { + buf.extend_from_slice(part); + } + } + + // But, on Windows, we must hop through a String. + #[cfg(not(any(unix, target_os = "wasi")))] + { + for part in itertools::intersperse(i.map(|a| a.to_str()), Some(" ")) { + let bytes = match part { + Some(part) => part.as_bytes(), + None => return Err("arguments contain invalid UTF-8".into()), + }; + buf.extend_from_slice(bytes); + } + } + + buf.push(b'\n'); + + Ok(()) +} + +// Assumes buf holds a single output line forged from the command line arguments, copies it +// repeatedly until the buffer holds as many copies as it can under BUF_SIZE. +fn prepare_buffer(buf: &mut Vec) { + if buf.len() * 2 > BUF_SIZE { + return; + } + + assert!(!buf.is_empty()); + + let line_len = buf.len(); + let target_size = line_len * (BUF_SIZE / line_len); + + while buf.len() < target_size { + let to_copy = std::cmp::min(target_size - buf.len(), buf.len()); + debug_assert_eq!(to_copy % line_len, 0); + buf.extend_from_within(..to_copy); } } @@ -88,3 +136,67 @@ pub fn exec(bytes: &[u8]) -> io::Result<()> { stdout.write_all(bytes)?; } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_prepare_buffer() { + let tests = [ + (150, 16350), + (1000, 16000), + (4093, 16372), + (4099, 12297), + (4111, 12333), + (2, 16384), + (3, 16383), + (4, 16384), + (5, 16380), + (8192, 16384), + (8191, 16382), + (8193, 8193), + (10000, 10000), + (15000, 15000), + (25000, 25000), + ]; + + for (line, final_len) in tests { + let mut v = std::iter::repeat(b'a').take(line).collect::>(); + prepare_buffer(&mut v); + assert_eq!(v.len(), final_len); + } + } + + #[test] + fn test_args_into_buf() { + { + let mut v = Vec::with_capacity(BUF_SIZE); + args_into_buffer(&mut v, None::>).unwrap(); + assert_eq!(String::from_utf8(v).unwrap(), "y\n"); + } + + { + let mut v = Vec::with_capacity(BUF_SIZE); + args_into_buffer(&mut v, Some([OsString::from("foo")].iter())).unwrap(); + assert_eq!(String::from_utf8(v).unwrap(), "foo\n"); + } + + { + let mut v = Vec::with_capacity(BUF_SIZE); + args_into_buffer( + &mut v, + Some( + [ + OsString::from("foo"), + OsString::from("bar baz"), + OsString::from("qux"), + ] + .iter(), + ), + ) + .unwrap(); + assert_eq!(String::from_utf8(v).unwrap(), "foo bar baz qux\n"); + } + } +} diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index 4ca3941db7d..7fa4aa3445a 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "uucore" -version = "0.0.17" +version = "0.0.19" authors = ["uutils developers"] license = "MIT" description = "uutils ~ 'core' uutils code library (cross-platform)" @@ -15,49 +15,58 @@ categories = ["command-line-utilities"] edition = "2021" [lib] -path="src/lib/lib.rs" +path = "src/lib/lib.rs" [dependencies] -clap = { workspace=true } -uucore_procs = { workspace=true } -dns-lookup = { version="1.0.5", optional=true } -dunce = "1.0.3" -wild = "2.0" -glob = "0.3.0" +clap = { workspace = true } +uucore_procs = { workspace = true } +dns-lookup = { version = "2.0.2", optional = true } +dunce = "1.0.4" +wild = "2.1" +glob = "0.3.1" # * optional -itertools = { version="0.10.0", optional=true } -thiserror = { workspace=true, optional=true } -time = { workspace=true, optional=true, features = ["formatting", "local-offset", "macros"] } +itertools = { version = "0.10.5", optional = true } +thiserror = { workspace = true, optional = true } +time = { workspace = true, optional = true, features = [ + "formatting", + "local-offset", + "macros", +] } # * "problem" dependencies (pinned) -data-encoding = { version="2.1", optional=true } -data-encoding-macro = { version="0.1.12", optional=true } -z85 = { version="3.0.5", optional=true } -libc = { version="0.2.137", optional=true } -once_cell = { workspace=true } +data-encoding = { version = "2.4", optional = true } +data-encoding-macro = { version = "0.1.13", optional = true } +z85 = { version = "3.0.5", optional = true } +libc = { version = "0.2.146", optional = true } +once_cell = { workspace = true } os_display = "0.1.3" -digest = { workspace=true } -hex = { workspace=true } -memchr = { workspace=true } -md-5 = { workspace=true } -sha1 = { workspace=true } -sha2 = { workspace=true } -sha3 = { workspace=true } -blake2b_simd = { workspace=true } -blake3 = { workspace=true } -sm3 = { workspace=true } +digest = { workspace = true } +hex = { workspace = true } +memchr = { workspace = true } +md-5 = { workspace = true } +sha1 = { workspace = true } +sha2 = { workspace = true } +sha3 = { workspace = true } +blake2b_simd = { workspace = true } +blake3 = { workspace = true } +sm3 = { workspace = true } [target.'cfg(unix)'.dependencies] -walkdir = { workspace=true, optional=true } -nix = { workspace=true, features = ["fs", "uio", "zerocopy", "signal"] } +walkdir = { workspace = true, optional = true } +nix = { workspace = true, features = ["fs", "uio", "zerocopy", "signal"] } [dev-dependencies] -clap = { workspace=true } -once_cell = { workspace=true } +clap = { workspace = true } +once_cell = { workspace = true } +tempfile = { workspace = true } [target.'cfg(target_os = "windows")'.dependencies] -winapi-util = { version= "0.1.5", optional=true } -windows-sys = { version = "0.42.0", optional = true, default-features = false, features = ["Win32_Storage_FileSystem", "Win32_Foundation", "Win32_System_WindowsProgramming"] } +winapi-util = { version = "0.1.5", optional = true } +windows-sys = { version = "0.48.0", optional = true, default-features = false, features = [ + "Win32_Storage_FileSystem", + "Win32_Foundation", + "Win32_System_WindowsProgramming", +] } [features] default = [] diff --git a/src/uucore/src/lib/features/encoding.rs b/src/uucore/src/lib/features/encoding.rs index db4c4c63588..a42044eea78 100644 --- a/src/uucore/src/lib/features/encoding.rs +++ b/src/uucore/src/lib/features/encoding.rs @@ -67,10 +67,10 @@ pub fn encode(f: Format, input: &[u8]) -> Result { Z85 => { // According to the spec we should not accept inputs whose len is not a multiple of 4. // However, the z85 crate implements a padded encoding and accepts such inputs. We have to manually check for them. - if input.len() % 4 != 0 { - return Err(EncodeError::Z85InputLenNotMultipleOf4); - } else { + if input.len() % 4 == 0 { z85::encode(input) + } else { + return Err(EncodeError::Z85InputLenNotMultipleOf4); } } }) diff --git a/src/uucore/src/lib/features/entries.rs b/src/uucore/src/lib/features/entries.rs index 561de888bd2..c0229aa3e96 100644 --- a/src/uucore/src/lib/features/entries.rs +++ b/src/uucore/src/lib/features/entries.rs @@ -168,10 +168,10 @@ pub struct Passwd { /// SAFETY: ptr must point to a valid C string. /// Returns None if ptr is null. unsafe fn cstr2string(ptr: *const c_char) -> Option { - if !ptr.is_null() { - Some(CStr::from_ptr(ptr).to_string_lossy().into_owned()) - } else { + if ptr.is_null() { None + } else { + Some(CStr::from_ptr(ptr).to_string_lossy().into_owned()) } } @@ -321,7 +321,8 @@ macro_rules! f { } else { // SAFETY: We're holding PW_LOCK. unsafe { - let data = $fnam(CString::new(k).unwrap().as_ptr()); + let cstring = CString::new(k).unwrap(); + let data = $fnam(cstring.as_ptr()); if !data.is_null() { Ok($st::from_raw(ptr::read(data as *const _))) } else { diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index 43c21aa8d7b..e92d0977f50 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -326,6 +326,7 @@ impl<'a> From> for OwningComponent { /// * [`ResolveMode::Logical`] makes this function resolve '..' components /// before symlinks /// +#[allow(clippy::cognitive_complexity)] pub fn canonicalize>( original: P, miss_mode: MissingHandling, @@ -455,6 +456,9 @@ pub fn display_permissions(metadata: &fs::Metadata, display_file_type: bool) -> display_permissions_unix(mode, display_file_type) } +// The logic below is more readable written this way. +#[allow(clippy::if_not_else)] +#[allow(clippy::cognitive_complexity)] #[cfg(unix)] /// Display the permissions of a file on a unix like system pub fn display_permissions_unix(mode: mode_t, display_file_type: bool) -> String { @@ -582,10 +586,108 @@ pub fn make_path_relative_to, P2: AsRef>(path: P1, to: P2) components.iter().collect() } +/// Checks if there is a symlink loop in the given path. +/// +/// A symlink loop is a chain of symlinks where the last symlink points back to one of the previous symlinks in the chain. +/// +/// # Arguments +/// +/// * `path` - A reference to a `Path` representing the starting path to check for symlink loops. +/// +/// # Returns +/// +/// * `bool` - Returns `true` if a symlink loop is detected, `false` otherwise. +pub fn is_symlink_loop(path: &Path) -> bool { + let mut visited_symlinks = HashSet::new(); + let mut current_path = path.to_path_buf(); + + while let (Ok(metadata), Ok(link)) = ( + current_path.symlink_metadata(), + fs::read_link(¤t_path), + ) { + if !metadata.file_type().is_symlink() { + return false; + } + if !visited_symlinks.insert(current_path.clone()) { + return true; + } + current_path = link; + } + + false +} + +#[cfg(not(unix))] +// Hard link comparison is not supported on non-Unix platforms +pub fn are_hardlinks_to_same_file(_source: &Path, _target: &Path) -> bool { + false +} + +/// Checks if two paths are hard links to the same file. +/// +/// # Arguments +/// +/// * `source` - A reference to a `Path` representing the source path. +/// * `target` - A reference to a `Path` representing the target path. +/// +/// # Returns +/// +/// * `bool` - Returns `true` if the paths are hard links to the same file, and `false` otherwise. +#[cfg(unix)] +pub fn are_hardlinks_to_same_file(source: &Path, target: &Path) -> bool { + let source_metadata = match fs::symlink_metadata(source) { + Ok(metadata) => metadata, + Err(_) => return false, + }; + + let target_metadata = match fs::symlink_metadata(target) { + Ok(metadata) => metadata, + Err(_) => return false, + }; + + source_metadata.ino() == target_metadata.ino() && source_metadata.dev() == target_metadata.dev() +} + +#[cfg(not(unix))] +pub fn are_hardlinks_or_one_way_symlink_to_same_file(_source: &Path, _target: &Path) -> bool { + false +} + +/// Checks if either two paths are hard links to the same file or if the source path is a symbolic link which when fully resolved points to target path +/// +/// # Arguments +/// +/// * `source` - A reference to a `Path` representing the source path. +/// * `target` - A reference to a `Path` representing the target path. +/// +/// # Returns +/// +/// * `bool` - Returns `true` if either of above conditions are true, and `false` otherwise. +#[cfg(unix)] +pub fn are_hardlinks_or_one_way_symlink_to_same_file(source: &Path, target: &Path) -> bool { + let source_metadata = match fs::metadata(source) { + Ok(metadata) => metadata, + Err(_) => return false, + }; + + let target_metadata = match fs::symlink_metadata(target) { + Ok(metadata) => metadata, + Err(_) => return false, + }; + + source_metadata.ino() == target_metadata.ino() && source_metadata.dev() == target_metadata.dev() +} + #[cfg(test)] mod tests { // Note this useful idiom: importing names from outer (for mod tests) scope. use super::*; + #[cfg(unix)] + use std::io::Write; + #[cfg(unix)] + use std::os::unix; + #[cfg(unix)] + use tempfile::{tempdir, NamedTempFile}; struct NormalizePathTestCase<'a> { path: &'a str, @@ -693,4 +795,81 @@ mod tests { display_permissions_unix(S_IFCHR | S_ISVTX as mode_t | 0o054, true) ); } + + #[cfg(unix)] + #[test] + fn test_is_symlink_loop_no_loop() { + let temp_dir = tempdir().unwrap(); + let file_path = temp_dir.path().join("file.txt"); + let symlink_path = temp_dir.path().join("symlink"); + + fs::write(&file_path, "test content").unwrap(); + unix::fs::symlink(&file_path, &symlink_path).unwrap(); + + assert!(!is_symlink_loop(&symlink_path)); + } + + #[cfg(unix)] + #[test] + fn test_is_symlink_loop_direct_loop() { + let temp_dir = tempdir().unwrap(); + let symlink_path = temp_dir.path().join("loop"); + + unix::fs::symlink(&symlink_path, &symlink_path).unwrap(); + + assert!(is_symlink_loop(&symlink_path)); + } + + #[cfg(unix)] + #[test] + fn test_is_symlink_loop_indirect_loop() { + let temp_dir = tempdir().unwrap(); + let symlink1_path = temp_dir.path().join("symlink1"); + let symlink2_path = temp_dir.path().join("symlink2"); + + unix::fs::symlink(&symlink1_path, &symlink2_path).unwrap(); + unix::fs::symlink(&symlink2_path, &symlink1_path).unwrap(); + + assert!(is_symlink_loop(&symlink1_path)); + } + + #[cfg(unix)] + #[test] + fn test_are_hardlinks_to_same_file_same_file() { + let mut temp_file = NamedTempFile::new().unwrap(); + writeln!(temp_file, "Test content").unwrap(); + + let path1 = temp_file.path(); + let path2 = temp_file.path(); + + assert_eq!(are_hardlinks_to_same_file(&path1, &path2), true); + } + + #[cfg(unix)] + #[test] + fn test_are_hardlinks_to_same_file_different_files() { + let mut temp_file1 = NamedTempFile::new().unwrap(); + writeln!(temp_file1, "Test content 1").unwrap(); + + let mut temp_file2 = NamedTempFile::new().unwrap(); + writeln!(temp_file2, "Test content 2").unwrap(); + + let path1 = temp_file1.path(); + let path2 = temp_file2.path(); + + assert_eq!(are_hardlinks_to_same_file(&path1, &path2), false); + } + + #[cfg(unix)] + #[test] + fn test_are_hardlinks_to_same_file_hard_link() { + let mut temp_file = NamedTempFile::new().unwrap(); + writeln!(temp_file, "Test content").unwrap(); + let path1 = temp_file.path(); + + let path2 = temp_file.path().with_extension("hardlink"); + fs::hard_link(&path1, &path2).unwrap(); + + assert_eq!(are_hardlinks_to_same_file(&path1, &path2), true); + } } diff --git a/src/uucore/src/lib/features/fsext.rs b/src/uucore/src/lib/features/fsext.rs index 927ae0f7ad0..89f1d6e7165 100644 --- a/src/uucore/src/lib/features/fsext.rs +++ b/src/uucore/src/lib/features/fsext.rs @@ -11,7 +11,6 @@ // spell-checker:ignore DATETIME subsecond (arch) bitrig ; (fs) cifs smbfs -extern crate time; use time::macros::format_description; use time::UtcOffset; @@ -295,10 +294,10 @@ impl MountInfo { fs_type_buf.len() as u32, ) }; - let fs_type = if 0 != success { - Some(LPWSTR2String(&fs_type_buf)) - } else { + let fs_type = if 0 == success { None + } else { + Some(LPWSTR2String(&fs_type_buf)) }; let mut mn_info = Self { dev_id: volume_name, @@ -410,7 +409,7 @@ pub fn read_fs_list() -> Result, std::io::Error> { let reader = BufReader::new(f); Ok(reader .lines() - .filter_map(|line| line.ok()) + .map_while(Result::ok) .filter_map(|line| { let raw_data = line.split_whitespace().collect::>(); MountInfo::new(file_name, &raw_data) @@ -862,10 +861,10 @@ pub fn pretty_time(sec: i64, nsec: i64) -> String { pub fn pretty_filetype<'a>(mode: mode_t, size: u64) -> &'a str { match mode & S_IFMT { S_IFREG => { - if size != 0 { - "regular file" - } else { + if size == 0 { "regular empty file" + } else { + "regular file" } } S_IFDIR => "directory", diff --git a/src/uucore/src/lib/features/mode.rs b/src/uucore/src/lib/features/mode.rs index a54824d18e8..9435e3201a6 100644 --- a/src/uucore/src/lib/features/mode.rs +++ b/src/uucore/src/lib/features/mode.rs @@ -209,7 +209,7 @@ mod test { assert_eq!(super::parse_mode("u+x").unwrap(), 0o766); assert_eq!( super::parse_mode("+x").unwrap(), - if !crate::os::is_wsl_1() { 0o777 } else { 0o776 } + if crate::os::is_wsl_1() { 0o776 } else { 0o777 } ); assert_eq!(super::parse_mode("a-w").unwrap(), 0o444); assert_eq!(super::parse_mode("g-r").unwrap(), 0o626); diff --git a/src/uucore/src/lib/features/perms.rs b/src/uucore/src/lib/features/perms.rs index 07203a31816..5384b52a18f 100644 --- a/src/uucore/src/lib/features/perms.rs +++ b/src/uucore/src/lib/features/perms.rs @@ -182,6 +182,7 @@ pub enum TraverseSymlinks { pub struct ChownExecutor { pub dest_uid: Option, pub dest_gid: Option, + pub raw_owner: String, // The owner of the file as input by the user in the command line. pub traverse_symlinks: TraverseSymlinks, pub verbosity: Verbosity, pub filter: IfFrom, @@ -203,11 +204,21 @@ impl ChownExecutor { Ok(()) } + #[allow(clippy::cognitive_complexity)] fn traverse>(&self, root: P) -> i32 { let path = root.as_ref(); let meta = match self.obtain_meta(path, self.dereference) { Some(m) => m, - _ => return 1, + _ => { + if self.verbosity.level == VerbosityLevel::Verbose { + println!( + "failed to change ownership of {} to {}", + path.quote(), + self.raw_owner + ); + } + return 1; + } }; // Prohibit only if: @@ -260,16 +271,22 @@ impl ChownExecutor { } } } else { + self.print_verbose_ownership_retained_as( + path, + meta.uid(), + self.dest_gid.map(|_| meta.gid()), + ); 0 }; - if !self.recursive { - ret - } else { + if self.recursive { ret | self.dive_into(&root) + } else { + ret } } + #[allow(clippy::cognitive_complexity)] fn dive_into>(&self, root: P) -> i32 { let root = root.as_ref(); @@ -320,6 +337,11 @@ impl ChownExecutor { }; if !self.matched(meta.uid(), meta.gid()) { + self.print_verbose_ownership_retained_as( + path, + meta.uid(), + self.dest_gid.map(|_| meta.gid()), + ); continue; } @@ -381,6 +403,35 @@ impl ChownExecutor { IfFrom::UserGroup(u, g) => u == uid && g == gid, } } + + fn print_verbose_ownership_retained_as(&self, path: &Path, uid: u32, gid: Option) { + if self.verbosity.level == VerbosityLevel::Verbose { + match (self.dest_uid, self.dest_gid, gid) { + (Some(_), Some(_), Some(gid)) => { + println!( + "ownership of {} retained as {}:{}", + path.quote(), + entries::uid2usr(uid).unwrap_or_else(|_| uid.to_string()), + entries::gid2grp(gid).unwrap_or_else(|_| gid.to_string()), + ); + } + (None, Some(_), Some(gid)) => { + println!( + "ownership of {} retained as {}", + path.quote(), + entries::gid2grp(gid).unwrap_or_else(|_| gid.to_string()), + ); + } + (_, _, _) => { + println!( + "ownership of {} retained as {}", + path.quote(), + entries::uid2usr(uid).unwrap_or_else(|_| uid.to_string()), + ); + } + } + } + } } pub mod options { @@ -412,7 +463,13 @@ pub mod options { pub const ARG_FILES: &str = "FILE"; } -type GidUidFilterParser = fn(&ArgMatches) -> UResult<(Option, Option, IfFrom)>; +pub struct GidUidOwnerFilter { + pub dest_gid: Option, + pub dest_uid: Option, + pub raw_owner: String, + pub filter: IfFrom, +} +type GidUidFilterOwnerParser = fn(&ArgMatches) -> UResult; /// Base implementation for `chgrp` and `chown`. /// @@ -421,11 +478,12 @@ type GidUidFilterParser = fn(&ArgMatches) -> UResult<(Option, Option, /// `parse_gid_uid_and_filter` will be called to obtain the target gid and uid, and the filter, /// from `ArgMatches`. /// `groups_only` determines whether verbose output will only mention the group. +#[allow(clippy::cognitive_complexity)] pub fn chown_base( mut command: Command, args: impl crate::Args, add_arg_if_not_reference: &'static str, - parse_gid_uid_and_filter: GidUidFilterParser, + parse_gid_uid_and_filter: GidUidFilterOwnerParser, groups_only: bool, ) -> UResult<()> { let args: Vec<_> = args.collect(); @@ -508,12 +566,18 @@ pub fn chown_base( } else { VerbosityLevel::Normal }; - let (dest_gid, dest_uid, filter) = parse_gid_uid_and_filter(&matches)?; + let GidUidOwnerFilter { + dest_gid, + dest_uid, + raw_owner, + filter, + } = parse_gid_uid_and_filter(&matches)?; let executor = ChownExecutor { traverse_symlinks, dest_gid, dest_uid, + raw_owner, verbosity: Verbosity { groups_only, level: verbosity_level, diff --git a/src/uucore/src/lib/features/process.rs b/src/uucore/src/lib/features/process.rs index b8b2f178f1b..4a52f0fc4fa 100644 --- a/src/uucore/src/lib/features/process.rs +++ b/src/uucore/src/lib/features/process.rs @@ -58,10 +58,10 @@ pub trait ChildExt { impl ChildExt for Child { fn send_signal(&mut self, signal: usize) -> io::Result<()> { - if unsafe { libc::kill(self.id() as pid_t, signal as i32) } != 0 { - Err(io::Error::last_os_error()) - } else { + if unsafe { libc::kill(self.id() as pid_t, signal as i32) } == 0 { Ok(()) + } else { + Err(io::Error::last_os_error()) } } @@ -70,10 +70,10 @@ impl ChildExt for Child { 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 { + if unsafe { libc::kill(0, signal as i32) } == 0 { Ok(()) + } else { + Err(io::Error::last_os_error()) } } diff --git a/src/uucore/src/lib/features/sum.rs b/src/uucore/src/lib/features/sum.rs index 3394146308c..c1cfaf5f84d 100644 --- a/src/uucore/src/lib/features/sum.rs +++ b/src/uucore/src/lib/features/sum.rs @@ -38,10 +38,25 @@ pub trait Digest { } } -pub struct Blake2b(blake2b_simd::State); +/// first element of the tuple is the blake2b state +/// second is the number of output bits +pub struct Blake2b(blake2b_simd::State, usize); + +impl Blake2b { + /// Return a new Blake2b instance with a custom output bytes length + pub fn with_output_bytes(output_bytes: usize) -> Self { + let mut params = blake2b_simd::Params::new(); + params.hash_length(output_bytes); + + let state = params.to_state(); + Self(state, output_bytes * 8) + } +} + impl Digest for Blake2b { fn new() -> Self { - Self(blake2b_simd::State::new()) + // by default, Blake2b output is 512 bits long (= 64B) + Self::with_output_bytes(64) } fn hash_update(&mut self, input: &[u8]) { @@ -54,11 +69,11 @@ impl Digest for Blake2b { } fn reset(&mut self) { - *self = Self::new(); + *self = Self::with_output_bytes(self.output_bytes()); } fn output_bits(&self) -> usize { - 512 + self.1 } } diff --git a/src/uucore/src/lib/features/tokenize/num_format/formatters/float_common.rs b/src/uucore/src/lib/features/tokenize/num_format/formatters/float_common.rs index c277e60a699..e0a29217c4a 100644 --- a/src/uucore/src/lib/features/tokenize/num_format/formatters/float_common.rs +++ b/src/uucore/src/lib/features/tokenize/num_format/formatters/float_common.rs @@ -40,6 +40,7 @@ fn has_enough_digits( } impl FloatAnalysis { + #[allow(clippy::cognitive_complexity)] pub fn analyze( str_in: &str, initial_prefix: &InitialPrefix, @@ -219,6 +220,7 @@ fn round_terminal_digit( (before_dec, after_dec, false) } +#[allow(clippy::cognitive_complexity)] pub fn get_primitive_dec( initial_prefix: &InitialPrefix, str_in: &str, diff --git a/src/uucore/src/lib/features/tokenize/num_format/formatters/intf.rs b/src/uucore/src/lib/features/tokenize/num_format/formatters/intf.rs index e0eb0ffd338..0f6e78de6f6 100644 --- a/src/uucore/src/lib/features/tokenize/num_format/formatters/intf.rs +++ b/src/uucore/src/lib/features/tokenize/num_format/formatters/intf.rs @@ -40,6 +40,7 @@ impl Intf { // is_zero: true if number is zero, false otherwise // len_digits: length of digits used to create the int // important, for example, if we run into a non-valid character + #[allow(clippy::cognitive_complexity)] fn analyze(str_in: &str, signed_out: bool, initial_prefix: &InitialPrefix) -> IntAnalysis { // the maximum number of digits we could conceivably // have before the decimal point without exceeding the diff --git a/src/uucore/src/lib/features/tokenize/num_format/num_format.rs b/src/uucore/src/lib/features/tokenize/num_format/num_format.rs index 9aa97f8112d..c9b1178b6ac 100644 --- a/src/uucore/src/lib/features/tokenize/num_format/num_format.rs +++ b/src/uucore/src/lib/features/tokenize/num_format/num_format.rs @@ -87,6 +87,7 @@ fn get_provided(str_in_opt: Option<&String>) -> Option { // a base, // and an offset for index after all // initial spacing, sign, base prefix, and leading zeroes +#[allow(clippy::cognitive_complexity)] fn get_initial_prefix(str_in: &str, field_type: &FieldType) -> InitialPrefix { let mut str_it = str_in.chars(); let mut ret = InitialPrefix { diff --git a/src/uucore/src/lib/features/tokenize/sub.rs b/src/uucore/src/lib/features/tokenize/sub.rs index 9312040e32e..5bdb24dc633 100644 --- a/src/uucore/src/lib/features/tokenize/sub.rs +++ b/src/uucore/src/lib/features/tokenize/sub.rs @@ -173,6 +173,7 @@ impl SubParser { prefix_char, )) } + #[allow(clippy::cognitive_complexity)] fn sub_vals_retrieved(&mut self, it: &mut PutBackN) -> UResult { if !Self::successfully_eat_prefix(it, &mut self.text_so_far)? { return Ok(false); @@ -197,22 +198,24 @@ impl SubParser { self.text_so_far.push(ch); match ch { '-' | '*' | '0'..='9' => { - if !self.past_decimal { - if self.min_width_is_asterisk || self.specifiers_found { + if self.past_decimal { + // second field should never have a + // negative value + if self.second_field_is_asterisk || ch == '-' || self.specifiers_found { return Err(SubError::InvalidSpec(self.text_so_far.clone()).into()); } - if self.min_width_tmp.is_none() { - self.min_width_tmp = Some(String::new()); + if self.second_field_tmp.is_none() { + self.second_field_tmp = Some(String::new()); } - match self.min_width_tmp.as_mut() { + match self.second_field_tmp.as_mut() { Some(x) => { - if (ch == '-' || ch == '*') && !x.is_empty() { + if ch == '*' && !x.is_empty() { return Err( SubError::InvalidSpec(self.text_so_far.clone()).into() ); } if ch == '*' { - self.min_width_is_asterisk = true; + self.second_field_is_asterisk = true; } x.push(ch); } @@ -221,23 +224,21 @@ impl SubParser { } } } else { - // second field should never have a - // negative value - if self.second_field_is_asterisk || ch == '-' || self.specifiers_found { + if self.min_width_is_asterisk || self.specifiers_found { return Err(SubError::InvalidSpec(self.text_so_far.clone()).into()); } - if self.second_field_tmp.is_none() { - self.second_field_tmp = Some(String::new()); + if self.min_width_tmp.is_none() { + self.min_width_tmp = Some(String::new()); } - match self.second_field_tmp.as_mut() { + match self.min_width_tmp.as_mut() { Some(x) => { - if ch == '*' && !x.is_empty() { + if (ch == '-' || ch == '*') && !x.is_empty() { return Err( SubError::InvalidSpec(self.text_so_far.clone()).into() ); } if ch == '*' { - self.second_field_is_asterisk = true; + self.min_width_is_asterisk = true; } x.push(ch); } @@ -248,10 +249,10 @@ impl SubParser { } } '.' => { - if !self.past_decimal { - self.past_decimal = true; - } else { + if self.past_decimal { return Err(SubError::InvalidSpec(self.text_so_far.clone()).into()); + } else { + self.past_decimal = true; } } x if legal_fields.binary_search(&x).is_ok() => { @@ -342,6 +343,7 @@ impl SubParser { } impl Sub { + #[allow(clippy::cognitive_complexity)] pub(crate) fn write(&self, writer: &mut W, pf_args_it: &mut Peekable>) where W: Write, diff --git a/src/uucore/src/lib/features/tokenize/unescaped_text.rs b/src/uucore/src/lib/features/tokenize/unescaped_text.rs index e659b11b569..29c657ed863 100644 --- a/src/uucore/src/lib/features/tokenize/unescaped_text.rs +++ b/src/uucore/src/lib/features/tokenize/unescaped_text.rs @@ -135,13 +135,13 @@ impl UnescapedText { } _ => {} } - if !ignore { + if ignore { + byte_vec.push(ch as u8); + } else { let val = (Self::base_to_u32(min_len, max_len, base, it) % 256) as u8; byte_vec.push(val); let bvec = [val]; flush_bytes(writer, &bvec); - } else { - byte_vec.push(ch as u8); } } e => { @@ -197,6 +197,7 @@ impl UnescapedText { // and return a wrapper around a Vec of unescaped bytes // break on encounter of sub symbol ('%[^%]') unless called // through %b subst. + #[allow(clippy::cognitive_complexity)] pub fn from_it_core( writer: &mut W, it: &mut PutBackN, diff --git a/src/uucore/src/lib/features/utmpx.rs b/src/uucore/src/lib/features/utmpx.rs index 599a027786d..35c5ac5b02e 100644 --- a/src/uucore/src/lib/features/utmpx.rs +++ b/src/uucore/src/lib/features/utmpx.rs @@ -42,13 +42,10 @@ use std::ptr; use std::sync::{Mutex, MutexGuard}; pub use self::ut::*; -use libc::utmpx; -// pub use libc::getutxid; -// pub use libc::getutxline; -// pub use libc::pututxline; pub use libc::endutxent; pub use libc::getutxent; pub use libc::setutxent; +use libc::utmpx; #[cfg(any(target_vendor = "apple", target_os = "linux", target_os = "netbsd"))] pub use libc::utmpxname; @@ -230,7 +227,6 @@ impl Utmpx { let (hostname, display) = host.split_once(':').unwrap_or((&host, "")); if !hostname.is_empty() { - extern crate dns_lookup; use dns_lookup::{getaddrinfo, AddrInfoHints}; const AI_CANONNAME: i32 = 0x2; @@ -336,7 +332,9 @@ impl Iterator for UtmpxIter { fn next(&mut self) -> Option { unsafe { let res = getutxent(); - if !res.is_null() { + if res.is_null() { + None + } else { // The data behind this pointer will be replaced by the next // call to getutxent(), so we have to read it now. // All the strings live inline in the struct as arrays, which @@ -344,8 +342,6 @@ impl Iterator for UtmpxIter { Some(Utmpx { inner: ptr::read(res as *const _), }) - } else { - None } } } diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index 00162ddbba5..e76e540c8d8 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -26,6 +26,7 @@ pub use crate::mods::os; pub use crate::mods::panic; pub use crate::mods::quoting_style; pub use crate::mods::ranges; +pub use crate::mods::update_control; pub use crate::mods::version_cmp; // * string parsing modules diff --git a/src/uucore/src/lib/mods.rs b/src/uucore/src/lib/mods.rs index 4b6c53f9531..71d288c69a5 100644 --- a/src/uucore/src/lib/mods.rs +++ b/src/uucore/src/lib/mods.rs @@ -6,6 +6,7 @@ pub mod error; pub mod os; pub mod panic; pub mod ranges; +pub mod update_control; pub mod version_cmp; // dir and vdir also need access to the quoting_style module pub mod quoting_style; diff --git a/src/uucore/src/lib/mods/backup_control.rs b/src/uucore/src/lib/mods/backup_control.rs index 52b2771c6a7..2d161c43fa0 100644 --- a/src/uucore/src/lib/mods/backup_control.rs +++ b/src/uucore/src/lib/mods/backup_control.rs @@ -200,8 +200,6 @@ impl Display for BackupError { pub mod arguments { use clap::ArgAction; - extern crate clap; - pub static OPT_BACKUP: &str = "backupopt_backup"; pub static OPT_BACKUP_NO_ARG: &str = "backupopt_b"; pub static OPT_SUFFIX: &str = "backupopt_suffix"; diff --git a/src/uucore/src/lib/mods/quoting_style.rs b/src/uucore/src/lib/mods/quoting_style.rs index 3c8bf686a8d..a6efb2898cd 100644 --- a/src/uucore/src/lib/mods/quoting_style.rs +++ b/src/uucore/src/lib/mods/quoting_style.rs @@ -259,13 +259,13 @@ fn shell_with_escape(name: &str, quotes: Quotes) -> (String, bool) { pub fn escape_name(name: &OsStr, style: &QuotingStyle) -> String { match style { QuotingStyle::Literal { show_control } => { - if !show_control { + if *show_control { + name.to_string_lossy().into_owned() + } else { name.to_string_lossy() .chars() .flat_map(|c| EscapedChar::new_literal(c).hide_control()) .collect() - } else { - name.to_string_lossy().into_owned() } } QuotingStyle::C { quotes } => { diff --git a/src/uucore/src/lib/mods/update_control.rs b/src/uucore/src/lib/mods/update_control.rs new file mode 100644 index 00000000000..e46afd18522 --- /dev/null +++ b/src/uucore/src/lib/mods/update_control.rs @@ -0,0 +1,139 @@ +// This file is part of the uutils coreutils package. +// +// (c) John Shin +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +//! Implement GNU-style update functionality. +//! +//! - pre-defined [`clap`-Arguments][1] for inclusion in utilities that +//! implement updates +//! - determination of the [update mode][2] +//! +//! Update-functionality is implemented by the following utilities: +//! +//! - `cp` +//! - `mv` +//! +//! +//! [1]: arguments +//! [2]: `determine_update_mode()` +//! +//! +//! # Usage example +//! +//! ``` +//! #[macro_use] +//! extern crate uucore; +//! +//! use clap::{Command, Arg, ArgMatches}; +//! use uucore::update_control::{self, UpdateMode}; +//! +//! fn main() { +//! let matches = Command::new("command") +//! .arg(update_control::arguments::update()) +//! .arg(update_control::arguments::update_no_args()) +//! .get_matches_from(vec![ +//! "command", "--update=older" +//! ]); +//! +//! let update_mode = update_control::determine_update_mode(&matches); +//! +//! // handle cases +//! if update_mode == UpdateMode::ReplaceIfOlder { +//! // do +//! } else { +//! unreachable!() +//! } +//! } +//! ``` +use clap::ArgMatches; + +// Available update mode +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum UpdateMode { + // --update=`all`, `` + ReplaceAll, + // --update=`none` + ReplaceNone, + // --update=`older` + // -u + ReplaceIfOlder, +} + +pub mod arguments { + use clap::ArgAction; + + pub static OPT_UPDATE: &str = "update"; + pub static OPT_UPDATE_NO_ARG: &str = "u"; + + // `--update` argument, defaults to `older` if no values are provided + pub fn update() -> clap::Arg { + clap::Arg::new(OPT_UPDATE) + .long("update") + .help("move only when the SOURCE file is newer than the destination file or when the destination file is missing") + .value_parser(["none", "all", "older"]) + .num_args(0..=1) + .default_missing_value("older") + .require_equals(true) + .overrides_with("update") + .action(clap::ArgAction::Set) + } + + // `-u` argument + pub fn update_no_args() -> clap::Arg { + clap::Arg::new(OPT_UPDATE_NO_ARG) + .short('u') + .help("like --update but does not accept an argument") + .action(ArgAction::SetTrue) + } +} + +/// Determine the "mode" for the update operation to perform, if any. +/// +/// Parses the backup options and converts them to an instance of +/// `UpdateMode` for further processing. +/// +/// Takes [`clap::ArgMatches`] as argument which **must** contain the options +/// from [`arguments::update()`] or [`arguments::update_no_args()`]. Otherwise +/// the `ReplaceAll` mode is returned unconditionally. +/// +/// # Examples +/// +/// Here's how one would integrate the update mode determination into an +/// application. +/// +/// ``` +/// #[macro_use] +/// extern crate uucore; +/// use uucore::update_control::{self, UpdateMode}; +/// use clap::{Command, Arg, ArgMatches}; +/// +/// fn main() { +/// let matches = Command::new("command") +/// .arg(update_control::arguments::update()) +/// .arg(update_control::arguments::update_no_args()) +/// .get_matches_from(vec![ +/// "command", "--update=all" +/// ]); +/// +/// let update_mode = update_control::determine_update_mode(&matches); +/// assert_eq!(update_mode, UpdateMode::ReplaceAll) +/// } +pub fn determine_update_mode(matches: &ArgMatches) -> UpdateMode { + if let Some(mode) = matches.get_one::(arguments::OPT_UPDATE) { + match mode.as_str() { + "all" => UpdateMode::ReplaceAll, + "none" => UpdateMode::ReplaceNone, + "older" => UpdateMode::ReplaceIfOlder, + _ => unreachable!("other args restricted by clap"), + } + } else if matches.get_flag(arguments::OPT_UPDATE_NO_ARG) { + // short form of this option is equivalent to using --update=older + UpdateMode::ReplaceIfOlder + } else { + // no option was present + UpdateMode::ReplaceAll + } +} diff --git a/src/uucore/src/lib/parser/parse_size.rs b/src/uucore/src/lib/parser/parse_size.rs index d1e571e0694..2ea84e389b9 100644 --- a/src/uucore/src/lib/parser/parse_size.rs +++ b/src/uucore/src/lib/parser/parse_size.rs @@ -3,7 +3,7 @@ // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -// spell-checker:ignore (ToDO) hdsf ghead gtail +// spell-checker:ignore (ToDO) hdsf ghead gtail ACDBK hexdigit use std::error::Error; use std::fmt; @@ -25,6 +25,12 @@ pub struct Parser<'parser> { pub default_unit: Option<&'parser str>, } +enum NumberSystem { + Decimal, + Octal, + Hexadecimal, +} + impl<'parser> Parser<'parser> { pub fn with_allow_list(&mut self, allow_list: &'parser [&str]) -> &mut Self { self.allow_list = Some(allow_list); @@ -62,31 +68,26 @@ impl<'parser> Parser<'parser> { /// assert_eq!(Ok(123), parse_size("123")); /// assert_eq!(Ok(9 * 1000), parse_size("9kB")); // kB is 1000 /// assert_eq!(Ok(2 * 1024), parse_size("2K")); // K is 1024 + /// assert_eq!(Ok(44251 * 1024), parse_size("0xACDBK")); /// ``` pub fn parse(&self, size: &str) -> Result { if size.is_empty() { return Err(ParseSizeError::parse_failure(size)); } - // Get the numeric part of the size argument. For example, if the - // argument is "123K", then the numeric part is "123". - let numeric_string: String = size.chars().take_while(|c| c.is_ascii_digit()).collect(); - let number: u64 = if !numeric_string.is_empty() { - match numeric_string.parse() { - Ok(n) => n, - Err(_) => return Err(ParseSizeError::parse_failure(size)), - } - } else { - 1 - }; - // Get the alphabetic units part of the size argument and compute - // the factor it represents. For example, if the argument is "123K", - // then the unit part is "K" and the factor is 1024. This may be the - // empty string, in which case, the factor is 1. - // - // The lowercase "b" (used by `od`, `head`, `tail`, etc.) means - // "block" and the Posix block size is 512. The uppercase "B" - // means "byte". + let number_system: NumberSystem = self.determine_number_system(size); + + // Split the size argument into numeric and unit parts + // For example, if the argument is "123K", the numeric part is "123", and + // the unit is "K" + let numeric_string: String = match number_system { + NumberSystem::Hexadecimal => size + .chars() + .take(2) + .chain(size.chars().skip(2).take_while(|c| c.is_ascii_hexdigit())) + .collect(), + _ => size.chars().take_while(|c| c.is_ascii_digit()).collect(), + }; let mut unit: &str = &size[numeric_string.len()..]; if let Some(default_unit) = self.default_unit { @@ -115,6 +116,12 @@ impl<'parser> Parser<'parser> { } } + // Compute the factor the unit represents. + // empty string means the factor is 1. + // + // The lowercase "b" (used by `od`, `head`, `tail`, etc.) means + // "block" and the Posix block size is 512. The uppercase "B" + // means "byte". let (base, exponent): (u128, u32) = match unit { "" => (1, 0), "B" if self.capital_b_bytes => (1, 0), @@ -142,10 +149,62 @@ impl<'parser> Parser<'parser> { Ok(n) => n, Err(_) => return Err(ParseSizeError::size_too_big(size)), }; + + // parse string into u64 + let number: u64 = match number_system { + NumberSystem::Decimal => { + if numeric_string.is_empty() { + 1 + } else { + self.parse_number(&numeric_string, 10, size)? + } + } + NumberSystem::Octal => { + let trimmed_string = numeric_string.trim_start_matches('0'); + self.parse_number(trimmed_string, 8, size)? + } + NumberSystem::Hexadecimal => { + let trimmed_string = numeric_string.trim_start_matches("0x"); + self.parse_number(trimmed_string, 16, size)? + } + }; + number .checked_mul(factor) .ok_or_else(|| ParseSizeError::size_too_big(size)) } + + fn determine_number_system(&self, size: &str) -> NumberSystem { + if size.len() <= 1 { + return NumberSystem::Decimal; + } + + if size.starts_with("0x") { + return NumberSystem::Hexadecimal; + } + + let num_digits: usize = size + .chars() + .take_while(|c| c.is_ascii_digit()) + .collect::() + .len(); + let all_zeros = size.chars().all(|c| c == '0'); + if size.starts_with('0') && num_digits > 1 && !all_zeros { + return NumberSystem::Octal; + } + + NumberSystem::Decimal + } + + fn parse_number( + &self, + numeric_string: &str, + radix: u32, + original_size: &str, + ) -> Result { + u64::from_str_radix(numeric_string, radix) + .map_err(|_| ParseSizeError::ParseFailure(original_size.to_string())) + } } /// Parse a size string into a number of bytes. @@ -336,7 +395,7 @@ mod tests { #[test] fn invalid_suffix() { - let test_strings = ["328hdsf3290", "5mib", "1e2", "1H", "1.2"]; + let test_strings = ["5mib", "1eb", "1H"]; for &test_string in &test_strings { assert_eq!( parse_size(test_string).unwrap_err(), @@ -450,4 +509,18 @@ mod tests { assert!(parser.parse("1B").is_err()); assert!(parser.parse("B").is_err()); } + + #[test] + fn parse_octal_size() { + assert_eq!(Ok(63), parse_size("077")); + assert_eq!(Ok(528), parse_size("01020")); + assert_eq!(Ok(668 * 1024), parse_size("01234K")); + } + + #[test] + fn parse_hex_size() { + assert_eq!(Ok(10), parse_size("0xA")); + assert_eq!(Ok(94722), parse_size("0x17202")); + assert_eq!(Ok(44251 * 1024), parse_size("0xACDBK")); + } } diff --git a/src/uucore_procs/Cargo.toml b/src/uucore_procs/Cargo.toml index c61d6367343..a83baf1d42d 100644 --- a/src/uucore_procs/Cargo.toml +++ b/src/uucore_procs/Cargo.toml @@ -1,6 +1,7 @@ +# spell-checker:ignore uuhelp [package] name = "uucore_procs" -version = "0.0.17" +version = "0.0.19" authors = ["Roy Ivy III "] license = "MIT" description = "uutils ~ 'uucore' proc-macros" @@ -18,3 +19,4 @@ proc-macro = true [dependencies] proc-macro2 = "1.0" quote = "1.0" +uuhelp_parser = { path = "../uuhelp_parser", version = "0.0.19" } diff --git a/src/uucore_procs/src/lib.rs b/src/uucore_procs/src/lib.rs index ab2458cebb5..b78da782210 100644 --- a/src/uucore_procs/src/lib.rs +++ b/src/uucore_procs/src/lib.rs @@ -1,14 +1,11 @@ // Copyright (C) ~ Roy Ivy III ; MIT license -// spell-checker:ignore backticks +// spell-checker:ignore backticks uuhelp -extern crate proc_macro; use std::{fs::File, io::Read, path::PathBuf}; use proc_macro::{Literal, TokenStream, TokenTree}; use quote::quote; -const MARKDOWN_CODE_FENCES: &str = "```"; - //## rust proc-macro background info //* ref: @@ //* ref: [path construction from LitStr](https://oschwald.github.io/maxminddb-rust/syn/struct.LitStr.html) @@ @@ -61,7 +58,7 @@ fn render_markdown(s: &str) -> String { pub fn help_about(input: TokenStream) -> TokenStream { let input: Vec = input.into_iter().collect(); let filename = get_argument(&input, 0, "filename"); - let text: String = parse_about(&read_help(&filename)); + let text: String = uuhelp_parser::parse_about(&read_help(&filename)); TokenTree::Literal(Literal::string(&text)).into() } @@ -75,7 +72,7 @@ pub fn help_about(input: TokenStream) -> TokenStream { pub fn help_usage(input: TokenStream) -> TokenStream { let input: Vec = input.into_iter().collect(); let filename = get_argument(&input, 0, "filename"); - let text: String = parse_usage(&read_help(&filename)); + let text: String = uuhelp_parser::parse_usage(&read_help(&filename)); TokenTree::Literal(Literal::string(&text)).into() } @@ -108,9 +105,15 @@ pub fn help_section(input: TokenStream) -> TokenStream { let input: Vec = input.into_iter().collect(); let section = get_argument(&input, 0, "section"); let filename = get_argument(&input, 1, "filename"); - let text = parse_help_section(§ion, &read_help(&filename)); - let rendered = render_markdown(&text); - TokenTree::Literal(Literal::string(&rendered)).into() + + if let Some(text) = uuhelp_parser::parse_section(§ion, &read_help(&filename)) { + let rendered = render_markdown(&text); + TokenTree::Literal(Literal::string(&rendered)).into() + } else { + panic!( + "The section '{section}' could not be found in the help file. Maybe it is spelled wrong?" + ) + } } /// Get an argument from the input vector of `TokenTree`. @@ -149,214 +152,3 @@ fn read_help(filename: &str) -> String { content } - -/// Get a single section from content -/// -/// The section must be a second level section (i.e. start with `##`). -fn parse_help_section(section: &str, content: &str) -> String { - fn is_section_header(line: &str, section: &str) -> bool { - line.strip_prefix("##") - .map_or(false, |l| l.trim().to_lowercase() == section) - } - - let section = §ion.to_lowercase(); - - // We cannot distinguish between an empty or non-existing section below, - // so we do a quick test to check whether the section exists to provide - // a nice error message. - if content.lines().all(|l| !is_section_header(l, section)) { - panic!( - "The section '{section}' could not be found in the help file. Maybe it is spelled wrong?" - ) - } - - // Prefix includes space to allow processing of section with level 3-6 headers - let section_header_prefix = "## "; - - content - .lines() - .skip_while(|&l| !is_section_header(l, section)) - .skip(1) - .take_while(|l| !l.starts_with(section_header_prefix)) - .collect::>() - .join("\n") - .trim() - .to_string() -} - -/// Parses the first markdown code block into a usage string -/// -/// The code fences are removed and the name of the util is replaced -/// with `{}` so that it can be replaced with the appropriate name -/// at runtime. -fn parse_usage(content: &str) -> String { - content - .lines() - .skip_while(|l| !l.starts_with(MARKDOWN_CODE_FENCES)) - .skip(1) - .take_while(|l| !l.starts_with(MARKDOWN_CODE_FENCES)) - .map(|l| { - // Replace the util name (assumed to be the first word) with "{}" - // to be replaced with the runtime value later. - if let Some((_util, args)) = l.split_once(' ') { - format!("{{}} {args}\n") - } else { - "{}\n".to_string() - } - }) - .collect::>() - .join("") - .trim() - .to_string() -} - -/// Parses the text between the first markdown code block and the next header, if any, -/// into an about string. -fn parse_about(content: &str) -> String { - content - .lines() - .skip_while(|l| !l.starts_with(MARKDOWN_CODE_FENCES)) - .skip(1) - .skip_while(|l| !l.starts_with(MARKDOWN_CODE_FENCES)) - .skip(1) - .take_while(|l| !l.starts_with('#')) - .collect::>() - .join("\n") - .trim() - .to_string() -} - -#[cfg(test)] -mod tests { - use super::{parse_about, parse_help_section, parse_usage}; - - #[test] - fn section_parsing() { - let input = "\ - # ls\n\ - ## some section\n\ - This is some section\n\ - \n\ - ## ANOTHER SECTION - This is the other section\n\ - with multiple lines\n"; - - assert_eq!( - parse_help_section("some section", input), - "This is some section" - ); - assert_eq!( - parse_help_section("SOME SECTION", input), - "This is some section" - ); - assert_eq!( - parse_help_section("another section", input), - "This is the other section\nwith multiple lines" - ); - } - - #[test] - fn section_parsing_with_additional_headers() { - let input = "\ - # ls\n\ - ## after section\n\ - This is some section\n\ - \n\ - ### level 3 header\n\ - \n\ - Additional text under the section.\n\ - \n\ - #### level 4 header\n\ - \n\ - Yet another paragraph\n"; - - assert_eq!( - parse_help_section("after section", input), - "This is some section\n\n\ - ### level 3 header\n\n\ - Additional text under the section.\n\n\ - #### level 4 header\n\n\ - Yet another paragraph" - ); - } - - #[test] - #[should_panic] - fn section_parsing_panic() { - let input = "\ - # ls\n\ - ## some section\n\ - This is some section\n\ - \n\ - ## ANOTHER SECTION - This is the other section\n\ - with multiple lines\n"; - parse_help_section("non-existent section", input); - } - - #[test] - fn usage_parsing() { - let input = "\ - # ls\n\ - ```\n\ - ls -l\n\ - ```\n\ - ## some section\n\ - This is some section\n\ - \n\ - ## ANOTHER SECTION - This is the other section\n\ - with multiple lines\n"; - - assert_eq!(parse_usage(input), "{} -l"); - } - - #[test] - fn multi_line_usage_parsing() { - let input = "\ - # ls\n\ - ```\n\ - ls -a\n\ - ls -b\n\ - ls -c\n\ - ```\n\ - ## some section\n\ - This is some section\n"; - - assert_eq!(parse_usage(input), "{} -a\n{} -b\n{} -c"); - } - - #[test] - fn about_parsing() { - let input = "\ - # ls\n\ - ```\n\ - ls -l\n\ - ```\n\ - \n\ - This is the about section\n\ - \n\ - ## some section\n\ - This is some section\n"; - - assert_eq!(parse_about(input), "This is the about section"); - } - - #[test] - fn multi_line_about_parsing() { - let input = "\ - # ls\n\ - ```\n\ - ls -l\n\ - ```\n\ - \n\ - about a\n\ - \n\ - about b\n\ - \n\ - ## some section\n\ - This is some section\n"; - - assert_eq!(parse_about(input), "about a\n\nabout b"); - } -} diff --git a/src/uuhelp_parser/Cargo.toml b/src/uuhelp_parser/Cargo.toml new file mode 100644 index 00000000000..888d0753442 --- /dev/null +++ b/src/uuhelp_parser/Cargo.toml @@ -0,0 +1,7 @@ +# spell-checker:ignore uuhelp +[package] +name = "uuhelp_parser" +version = "0.0.19" +edition = "2021" +license = "MIT" +description = "A collection of functions to parse the markdown code of help files" diff --git a/src/uuhelp_parser/src/lib.rs b/src/uuhelp_parser/src/lib.rs new file mode 100644 index 00000000000..8faa4e6ce4d --- /dev/null +++ b/src/uuhelp_parser/src/lib.rs @@ -0,0 +1,236 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +//! A collection of functions to parse the markdown code of help files. +//! +//! The structure of the markdown code is assumed to be: +//! +//! # util name +//! +//! ```text +//! usage info +//! ``` +//! +//! About text +//! +//! ## Section 1 +//! +//! Some content +//! +//! ## Section 2 +//! +//! Some content + +const MARKDOWN_CODE_FENCES: &str = "```"; + +/// Parses the text between the first markdown code block and the next header, if any, +/// into an about string. +pub fn parse_about(content: &str) -> String { + content + .lines() + .skip_while(|l| !l.starts_with(MARKDOWN_CODE_FENCES)) + .skip(1) + .skip_while(|l| !l.starts_with(MARKDOWN_CODE_FENCES)) + .skip(1) + .take_while(|l| !l.starts_with('#')) + .collect::>() + .join("\n") + .trim() + .to_string() +} + +/// Parses the first markdown code block into a usage string +/// +/// The code fences are removed and the name of the util is replaced +/// with `{}` so that it can be replaced with the appropriate name +/// at runtime. +pub fn parse_usage(content: &str) -> String { + content + .lines() + .skip_while(|l| !l.starts_with(MARKDOWN_CODE_FENCES)) + .skip(1) + .take_while(|l| !l.starts_with(MARKDOWN_CODE_FENCES)) + .map(|l| { + // Replace the util name (assumed to be the first word) with "{}" + // to be replaced with the runtime value later. + if let Some((_util, args)) = l.split_once(' ') { + format!("{{}} {args}\n") + } else { + "{}\n".to_string() + } + }) + .collect::>() + .join("") + .trim() + .to_string() +} + +/// Get a single section from content +/// +/// The section must be a second level section (i.e. start with `##`). +pub fn parse_section(section: &str, content: &str) -> Option { + fn is_section_header(line: &str, section: &str) -> bool { + line.strip_prefix("##") + .map_or(false, |l| l.trim().to_lowercase() == section) + } + + let section = §ion.to_lowercase(); + + // We cannot distinguish between an empty or non-existing section below, + // so we do a quick test to check whether the section exists + if content.lines().all(|l| !is_section_header(l, section)) { + return None; + } + + // Prefix includes space to allow processing of section with level 3-6 headers + let section_header_prefix = "## "; + + Some( + content + .lines() + .skip_while(|&l| !is_section_header(l, section)) + .skip(1) + .take_while(|l| !l.starts_with(section_header_prefix)) + .collect::>() + .join("\n") + .trim() + .to_string(), + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_section() { + let input = "\ + # ls\n\ + ## some section\n\ + This is some section\n\ + \n\ + ## ANOTHER SECTION + This is the other section\n\ + with multiple lines\n"; + + assert_eq!( + parse_section("some section", input).unwrap(), + "This is some section" + ); + assert_eq!( + parse_section("SOME SECTION", input).unwrap(), + "This is some section" + ); + assert_eq!( + parse_section("another section", input).unwrap(), + "This is the other section\nwith multiple lines" + ); + } + + #[test] + fn test_parse_section_with_sub_headers() { + let input = "\ + # ls\n\ + ## after section\n\ + This is some section\n\ + \n\ + ### level 3 header\n\ + \n\ + Additional text under the section.\n\ + \n\ + #### level 4 header\n\ + \n\ + Yet another paragraph\n"; + + assert_eq!( + parse_section("after section", input).unwrap(), + "This is some section\n\n\ + ### level 3 header\n\n\ + Additional text under the section.\n\n\ + #### level 4 header\n\n\ + Yet another paragraph" + ); + } + + #[test] + fn test_parse_non_existing_section() { + let input = "\ + # ls\n\ + ## some section\n\ + This is some section\n\ + \n\ + ## ANOTHER SECTION + This is the other section\n\ + with multiple lines\n"; + + assert!(parse_section("non-existing section", input).is_none()); + } + + #[test] + fn test_parse_usage() { + let input = "\ + # ls\n\ + ```\n\ + ls -l\n\ + ```\n\ + ## some section\n\ + This is some section\n\ + \n\ + ## ANOTHER SECTION + This is the other section\n\ + with multiple lines\n"; + + assert_eq!(parse_usage(input), "{} -l"); + } + + #[test] + fn test_parse_multi_line_usage() { + let input = "\ + # ls\n\ + ```\n\ + ls -a\n\ + ls -b\n\ + ls -c\n\ + ```\n\ + ## some section\n\ + This is some section\n"; + + assert_eq!(parse_usage(input), "{} -a\n{} -b\n{} -c"); + } + + #[test] + fn test_parse_about() { + let input = "\ + # ls\n\ + ```\n\ + ls -l\n\ + ```\n\ + \n\ + This is the about section\n\ + \n\ + ## some section\n\ + This is some section\n"; + + assert_eq!(parse_about(input), "This is the about section"); + } + + #[test] + fn test_parse_multi_line_about() { + let input = "\ + # ls\n\ + ```\n\ + ls -l\n\ + ```\n\ + \n\ + about a\n\ + \n\ + about b\n\ + \n\ + ## some section\n\ + This is some section\n"; + + assert_eq!(parse_about(input), "about a\n\nabout b"); + } +} diff --git a/tests/benches/factor/Cargo.toml b/tests/benches/factor/Cargo.toml index efada4b00ee..26900e78a91 100644 --- a/tests/benches/factor/Cargo.toml +++ b/tests/benches/factor/Cargo.toml @@ -7,6 +7,8 @@ description = "Benchmarks for the uu_factor integer factorization tool" homepage = "https://github.com/uutils/coreutils" edition = "2021" +[workspace] + [dependencies] uu_factor = { path = "../../../src/uu/factor" } @@ -14,7 +16,7 @@ uu_factor = { path = "../../../src/uu/factor" } array-init = "2.0.0" criterion = "0.3" rand = "0.8" -rand_chacha = "0.2.2" +rand_chacha = "0.3.1" [[bench]] name = "gcd" diff --git a/tests/benches/factor/benches/gcd.rs b/tests/benches/factor/benches/gcd.rs index f2bae51c74a..a9d2a8c55e8 100644 --- a/tests/benches/factor/benches/gcd.rs +++ b/tests/benches/factor/benches/gcd.rs @@ -6,7 +6,7 @@ fn gcd(c: &mut Criterion) { // Deterministic RNG; use an explicitly-named RNG to guarantee stability use rand::{RngCore, SeedableRng}; use rand_chacha::ChaCha8Rng; - const SEED: u64 = 0xa_b4d_1dea_dead_cafe; + const SEED: u64 = 0xab4d_1dea_dead_cafe; let mut rng = ChaCha8Rng::seed_from_u64(SEED); std::iter::repeat_with(move || (rng.next_u64(), rng.next_u64())) @@ -22,7 +22,7 @@ fn gcd(c: &mut Criterion) { }, ); } - group.finish() + group.finish(); } criterion_group!(benches, gcd); diff --git a/tests/benches/factor/benches/table.rs b/tests/benches/factor/benches/table.rs index 7f749a10ffb..59e8db1f3bc 100644 --- a/tests/benches/factor/benches/table.rs +++ b/tests/benches/factor/benches/table.rs @@ -7,12 +7,7 @@ fn table(c: &mut Criterion) { check_personality(); const INPUT_SIZE: usize = 128; - assert!( - INPUT_SIZE % CHUNK_SIZE == 0, - "INPUT_SIZE ({}) is not divisible by CHUNK_SIZE ({})", - INPUT_SIZE, - CHUNK_SIZE - ); + let inputs = { // Deterministic RNG; use an explicitly-named RNG to guarantee stability use rand::{RngCore, SeedableRng}; @@ -29,42 +24,40 @@ fn table(c: &mut Criterion) { let a_str = format!("{:?}", a); group.bench_with_input(BenchmarkId::new("factor_chunk", &a_str), &a, |b, &a| { b.iter(|| { - let mut n_s = a.clone(); + let mut n_s = a; let mut f_s: [_; INPUT_SIZE] = array_init(|_| Factors::one()); for (n_s, f_s) in n_s.chunks_mut(CHUNK_SIZE).zip(f_s.chunks_mut(CHUNK_SIZE)) { - factor_chunk(n_s.try_into().unwrap(), f_s.try_into().unwrap()) + factor_chunk(n_s.try_into().unwrap(), f_s.try_into().unwrap()); } - }) + }); }); group.bench_with_input(BenchmarkId::new("factor", &a_str), &a, |b, &a| { b.iter(|| { - let mut n_s = a.clone(); + let mut n_s = a; let mut f_s: [_; INPUT_SIZE] = array_init(|_| Factors::one()); for (n, f) in n_s.iter_mut().zip(f_s.iter_mut()) { - factor(n, f) + factor(n, f); } - }) + }); }); } - group.finish() + group.finish(); } #[cfg(target_os = "linux")] fn check_personality() { use std::fs; const ADDR_NO_RANDOMIZE: u64 = 0x0040000; - const PERSONALITY_PATH: &'static str = "/proc/self/personality"; + const PERSONALITY_PATH: &str = "/proc/self/personality"; let p_string = fs::read_to_string(PERSONALITY_PATH) - .expect(&format!("Couldn't read '{}'", PERSONALITY_PATH)) - .strip_suffix("\n") + .unwrap_or_else(|_| panic!("Couldn't read '{}'", PERSONALITY_PATH)) + .strip_suffix('\n') .unwrap() .to_owned(); - let personality = u64::from_str_radix(&p_string, 16).expect(&format!( - "Expected a hex value for personality, got '{:?}'", - p_string - )); + let personality = u64::from_str_radix(&p_string, 16) + .unwrap_or_else(|_| panic!("Expected a hex value for personality, got '{:?}'", p_string)); if personality & ADDR_NO_RANDOMIZE == 0 { eprintln!( "WARNING: Benchmarking with ASLR enabled (personality is {:x}), results might not be reproducible.", diff --git a/tests/by-util/test_chgrp.rs b/tests/by-util/test_chgrp.rs index 69224b0bdc6..cf6c62ff7f8 100644 --- a/tests/by-util/test_chgrp.rs +++ b/tests/by-util/test_chgrp.rs @@ -1,7 +1,7 @@ // spell-checker:ignore (words) nosuchgroup groupname use crate::common::util::TestScenario; -use rust_users::get_effective_gid; +use uucore::process::getegid; #[test] fn test_invalid_option() { @@ -53,7 +53,7 @@ fn test_invalid_group() { #[test] fn test_1() { - if get_effective_gid() != 0 { + if getegid() != 0 { new_ucmd!().arg("bin").arg(DIR).fails().stderr_contains( // linux fails with "Operation not permitted (os error 1)" // because of insufficient permissions, @@ -66,7 +66,7 @@ fn test_1() { #[test] fn test_fail_silently() { - if get_effective_gid() != 0 { + if getegid() != 0 { for opt in ["-f", "--silent", "--quiet", "--sil", "--qui"] { new_ucmd!() .arg(opt) @@ -137,7 +137,7 @@ fn test_reference() { // skip for root or MS-WSL // * MS-WSL is bugged (as of 2019-12-25), allowing non-root accounts su-level privileges for `chgrp` // * for MS-WSL, succeeds and stdout == 'group of /etc retained as root' - if !(get_effective_gid() == 0 || uucore::os::is_wsl_1()) { + if !(getegid() == 0 || uucore::os::is_wsl_1()) { new_ucmd!() .arg("-v") .arg("--reference=/etc/passwd") @@ -203,7 +203,7 @@ fn test_missing_files() { #[test] #[cfg(target_os = "linux")] fn test_big_p() { - if get_effective_gid() != 0 { + if getegid() != 0 { new_ucmd!() .arg("-RP") .arg("bin") @@ -218,7 +218,7 @@ fn test_big_p() { #[test] #[cfg(any(target_os = "linux", target_os = "android"))] fn test_big_h() { - if get_effective_gid() != 0 { + if getegid() != 0 { assert!( new_ucmd!() .arg("-RH") diff --git a/tests/by-util/test_chmod.rs b/tests/by-util/test_chmod.rs index 379df494417..9e3c7d2da7f 100644 --- a/tests/by-util/test_chmod.rs +++ b/tests/by-util/test_chmod.rs @@ -4,9 +4,7 @@ use std::fs::{metadata, set_permissions, OpenOptions, Permissions}; use std::os::unix::fs::{OpenOptionsExt, PermissionsExt}; use std::sync::Mutex; -extern crate chmod; -extern crate libc; -use self::libc::umask; +use libc::umask; static TEST_FILE: &str = "file"; static REFERENCE_FILE: &str = "reference"; @@ -540,6 +538,7 @@ fn test_invalid_arg() { } #[test] +#[cfg(not(target_os = "android"))] fn test_mode_after_dash_dash() { let (at, ucmd) = at_and_ucmd!(); run_single_test( @@ -651,6 +650,7 @@ fn test_gnu_invalid_mode() { } #[test] +#[cfg(not(target_os = "android"))] fn test_gnu_options() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; diff --git a/tests/by-util/test_chown.rs b/tests/by-util/test_chown.rs index 9bd6382a691..7a1a4a6bd7f 100644 --- a/tests/by-util/test_chown.rs +++ b/tests/by-util/test_chown.rs @@ -2,9 +2,7 @@ use crate::common::util::{is_ci, run_ucmd_as_root, CmdResult, TestScenario}; #[cfg(any(target_os = "linux", target_os = "android"))] -use rust_users::get_effective_uid; - -extern crate chown; +use uucore::process::geteuid; // Apparently some CI environments have configuration issues, e.g. with 'whoami' and 'id'. // If we are running inside the CI and "needle" is in "stderr" skipping this test is @@ -36,7 +34,7 @@ fn skipping_test_is_okay(result: &CmdResult, needle: &str) -> bool { #[cfg(test)] mod test_passgrp { - use super::chown::entries::{gid2grp, grp2gid, uid2usr, usr2uid}; + use chown::entries::{gid2grp, grp2gid, uid2usr, usr2uid}; #[test] fn test_usr2uid() { @@ -703,7 +701,7 @@ fn test_root_preserve() { #[cfg(any(target_os = "linux", target_os = "android"))] #[test] fn test_big_p() { - if get_effective_uid() != 0 { + if geteuid() != 0 { new_ucmd!() .arg("-RP") .arg("bin") @@ -732,15 +730,275 @@ fn test_chown_file_notexisting() { let user_name = String::from(result.stdout_str().trim()); assert!(!user_name.is_empty()); - let _result = scene + scene .ucmd() - .arg(user_name) + .arg(&user_name) .arg("--verbose") .arg("not_existing") - .fails(); - - // TODO: uncomment once "failed to change ownership of '{}' to {}" added to stdout - // result.stderr_contains("retained as"); + .fails() + .stdout_contains(format!( + "failed to change ownership of 'not_existing' to {user_name}" + )); // TODO: uncomment once message changed from "cannot dereference" to "cannot access" // result.stderr_contains("cannot access 'not_existing': No such file or directory"); } + +#[test] +fn test_chown_no_change_to_user_from_user() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let result = scene.cmd("whoami").run(); + if skipping_test_is_okay(&result, "whoami: cannot find name for user ID") { + return; + } + let user_name = String::from(result.stdout_str().trim()); + assert!(!user_name.is_empty()); + + let file = "f"; + at.touch(file); + scene + .ucmd() + .arg("-v") + .arg("--from=42") + .arg("43") + .arg(file) + .succeeds() + .stdout_only(format!("ownership of '{file}' retained as {user_name}\n")); +} + +#[test] +fn test_chown_no_change_to_user_from_group() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let result = scene.cmd("whoami").run(); + if skipping_test_is_okay(&result, "whoami: cannot find name for user ID") { + return; + } + let user_name = String::from(result.stdout_str().trim()); + assert!(!user_name.is_empty()); + + let file = "f"; + at.touch(file); + scene + .ucmd() + .arg("-v") + .arg("--from=:42") + .arg("43") + .arg(file) + .succeeds() + .stdout_only(format!("ownership of '{file}' retained as {user_name}\n")); +} + +#[test] +fn test_chown_no_change_to_user_from_user_group() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let result = scene.cmd("whoami").run(); + if skipping_test_is_okay(&result, "whoami: cannot find name for user ID") { + return; + } + let user_name = String::from(result.stdout_str().trim()); + assert!(!user_name.is_empty()); + + let file = "f"; + at.touch(file); + scene + .ucmd() + .arg("-v") + .arg("--from=42:42") + .arg("43") + .arg(file) + .succeeds() + .stdout_only(format!("ownership of '{file}' retained as {user_name}\n")); +} + +#[test] +fn test_chown_no_change_to_group_from_user() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let result = scene.cmd("whoami").run(); + if skipping_test_is_okay(&result, "whoami: cannot find name for user ID") { + return; + } + let user_name = String::from(result.stdout_str().trim()); + assert!(!user_name.is_empty()); + + let result = scene.cmd("id").arg("-ng").run(); + if skipping_test_is_okay(&result, "id: cannot find name for group ID") { + return; + } + let group_name = String::from(result.stdout_str().trim()); + assert!(!group_name.is_empty()); + + let file = "f"; + at.touch(file); + scene + .ucmd() + .arg("-v") + .arg("--from=42") + .arg(":43") + .arg(file) + .succeeds() + .stdout_only(format!("ownership of '{file}' retained as {group_name}\n")); +} + +#[test] +fn test_chown_no_change_to_group_from_group() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let result = scene.cmd("whoami").run(); + if skipping_test_is_okay(&result, "whoami: cannot find name for user ID") { + return; + } + let user_name = String::from(result.stdout_str().trim()); + assert!(!user_name.is_empty()); + let result = scene.cmd("id").arg("-ng").run(); + if skipping_test_is_okay(&result, "id: cannot find name for group ID") { + return; + } + let group_name = String::from(result.stdout_str().trim()); + assert!(!group_name.is_empty()); + + let file = "f"; + at.touch(file); + scene + .ucmd() + .arg("-v") + .arg("--from=:42") + .arg(":43") + .arg(file) + .succeeds() + .stdout_only(format!("ownership of '{file}' retained as {group_name}\n")); +} + +#[test] +fn test_chown_no_change_to_group_from_user_group() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let result = scene.cmd("whoami").run(); + if skipping_test_is_okay(&result, "whoami: cannot find name for user ID") { + return; + } + let user_name = String::from(result.stdout_str().trim()); + assert!(!user_name.is_empty()); + let result = scene.cmd("id").arg("-ng").run(); + if skipping_test_is_okay(&result, "id: cannot find name for group ID") { + return; + } + let group_name = String::from(result.stdout_str().trim()); + assert!(!group_name.is_empty()); + + let file = "f"; + at.touch(file); + scene + .ucmd() + .arg("-v") + .arg("--from=42:42") + .arg(":43") + .arg(file) + .succeeds() + .stdout_only(format!("ownership of '{file}' retained as {group_name}\n")); +} + +#[test] +fn test_chown_no_change_to_user_group_from_user() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let result = scene.cmd("whoami").run(); + if skipping_test_is_okay(&result, "whoami: cannot find name for user ID") { + return; + } + let user_name = String::from(result.stdout_str().trim()); + assert!(!user_name.is_empty()); + + let result = scene.cmd("id").arg("-ng").run(); + if skipping_test_is_okay(&result, "id: cannot find name for group ID") { + return; + } + let group_name = String::from(result.stdout_str().trim()); + assert!(!group_name.is_empty()); + + let file = "f"; + at.touch(file); + scene + .ucmd() + .arg("-v") + .arg("--from=42") + .arg("43:43") + .arg(file) + .succeeds() + .stdout_only(format!( + "ownership of '{file}' retained as {user_name}:{group_name}\n" + )); +} + +#[test] +fn test_chown_no_change_to_user_group_from_group() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let result = scene.cmd("whoami").run(); + if skipping_test_is_okay(&result, "whoami: cannot find name for user ID") { + return; + } + let user_name = String::from(result.stdout_str().trim()); + assert!(!user_name.is_empty()); + let result = scene.cmd("id").arg("-ng").run(); + if skipping_test_is_okay(&result, "id: cannot find name for group ID") { + return; + } + let group_name = String::from(result.stdout_str().trim()); + assert!(!group_name.is_empty()); + + let file = "f"; + at.touch(file); + scene + .ucmd() + .arg("-v") + .arg("--from=:42") + .arg("43:43") + .arg(file) + .succeeds() + .stdout_only(format!( + "ownership of '{file}' retained as {user_name}:{group_name}\n" + )); +} + +#[test] +fn test_chown_no_change_to_user_group_from_user_group() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let result = scene.cmd("whoami").run(); + if skipping_test_is_okay(&result, "whoami: cannot find name for user ID") { + return; + } + let user_name = String::from(result.stdout_str().trim()); + assert!(!user_name.is_empty()); + let result = scene.cmd("id").arg("-ng").run(); + if skipping_test_is_okay(&result, "id: cannot find name for group ID") { + return; + } + let group_name = String::from(result.stdout_str().trim()); + assert!(!group_name.is_empty()); + + let file = "f"; + at.touch(file); + scene + .ucmd() + .arg("-v") + .arg("--from=42:42") + .arg("43:43") + .arg(file) + .succeeds() + .stdout_only(format!( + "ownership of '{file}' retained as {user_name}:{group_name}\n" + )); +} diff --git a/tests/by-util/test_cksum.rs b/tests/by-util/test_cksum.rs index 814d4c03a68..41ddc2ee0b1 100644 --- a/tests/by-util/test_cksum.rs +++ b/tests/by-util/test_cksum.rs @@ -1,7 +1,11 @@ -// spell-checker:ignore (words) asdf +// spell-checker:ignore (words) asdf algo algos use crate::common::util::TestScenario; +const ALGOS: [&str; 11] = [ + "sysv", "bsd", "crc", "md5", "sha1", "sha224", "sha256", "sha384", "sha512", "blake2b", "sm3", +]; + #[test] fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails().code_is(1); @@ -12,7 +16,7 @@ fn test_single_file() { new_ucmd!() .arg("lorem_ipsum.txt") .succeeds() - .stdout_is_fixture("single_file.expected"); + .stdout_is_fixture("crc_single_file.expected"); } #[test] @@ -21,7 +25,7 @@ fn test_multiple_files() { .arg("lorem_ipsum.txt") .arg("alice_in_wonderland.txt") .succeeds() - .stdout_is_fixture("multiple_files.expected"); + .stdout_is_fixture("crc_multiple_files.expected"); } #[test] @@ -29,11 +33,11 @@ fn test_stdin() { new_ucmd!() .pipe_in_fixture("lorem_ipsum.txt") .succeeds() - .stdout_is_fixture("stdin.expected"); + .stdout_is_fixture("crc_stdin.expected"); } #[test] -fn test_empty() { +fn test_empty_file() { let (at, mut ucmd) = at_and_ucmd!(); at.touch("a"); @@ -62,25 +66,26 @@ fn test_arg_overrides_stdin() { } #[test] -fn test_invalid_file() { - let ts = TestScenario::new(util_name!()); - let at = ts.fixtures.clone(); - - let folder_name = "asdf"; +fn test_nonexisting_file() { + let file_name = "asdf"; - // First check when file doesn't exist - ts.ucmd() - .arg(folder_name) + new_ucmd!() + .arg(file_name) .fails() .no_stdout() - .stderr_contains("cksum: asdf: No such file or directory"); + .stderr_contains(format!("cksum: {file_name}: No such file or directory")); +} + +#[test] +fn test_folder() { + let (at, mut ucmd) = at_and_ucmd!(); - // Then check when the file is of an invalid type + let folder_name = "a_folder"; at.mkdir(folder_name); - ts.ucmd() - .arg(folder_name) + + ucmd.arg(folder_name) .succeeds() - .stdout_only("4294967295 0 asdf\n"); + .stdout_only(format!("4294967295 0 {folder_name}\n")); } // Make sure crc is correct for files larger than 32 bytes @@ -116,77 +121,106 @@ fn test_stdin_larger_than_128_bytes() { } #[test] -fn test_sha1_single_file() { - new_ucmd!() - .arg("-a=sha1") - .arg("lorem_ipsum.txt") - .succeeds() - .stdout_is("ab1dd0bae1d8883a3d18a66de6afbd28252cfbef 772 lorem_ipsum.txt\n"); +fn test_algorithm_single_file() { + for algo in ALGOS { + for option in ["-a", "--algorithm"] { + new_ucmd!() + .arg(format!("{option}={algo}")) + .arg("lorem_ipsum.txt") + .succeeds() + .stdout_is_fixture(format!("{algo}_single_file.expected")); + } + } } #[test] -fn test_sm3_single_file() { - new_ucmd!() - .arg("-a=sm3") - .arg("lorem_ipsum.txt") - .succeeds() - .stdout_is( - "6d296b805d060bfed22808df308dbb9b4317794dd4ed6740a10770a782699bc2 772 lorem_ipsum.txt\n", - ); +fn test_algorithm_multiple_files() { + for algo in ALGOS { + for option in ["-a", "--algorithm"] { + new_ucmd!() + .arg(format!("{option}={algo}")) + .arg("lorem_ipsum.txt") + .arg("alice_in_wonderland.txt") + .succeeds() + .stdout_is_fixture(format!("{algo}_multiple_files.expected")); + } + } } #[test] -fn test_bsd_single_file() { +fn test_algorithm_stdin() { + for algo in ALGOS { + for option in ["-a", "--algorithm"] { + new_ucmd!() + .arg(format!("{option}={algo}")) + .pipe_in_fixture("lorem_ipsum.txt") + .succeeds() + .stdout_is_fixture(format!("{algo}_stdin.expected")); + } + } +} + +#[test] +fn test_untagged_single_file() { new_ucmd!() - .arg("-a=bsd") + .arg("--untagged") .arg("lorem_ipsum.txt") .succeeds() - .stdout_only_fixture("bsd_single_file.expected"); + .stdout_is_fixture("untagged/crc_single_file.expected"); } #[test] -fn test_bsd_multiple_files() { +fn test_untagged_multiple_files() { new_ucmd!() - .arg("-a=bsd") + .arg("--untagged") .arg("lorem_ipsum.txt") .arg("alice_in_wonderland.txt") .succeeds() - .stdout_only_fixture("bsd_multiple_files.expected"); + .stdout_is_fixture("untagged/crc_multiple_files.expected"); } #[test] -fn test_bsd_stdin() { +fn test_untagged_stdin() { new_ucmd!() - .arg("-a=bsd") + .arg("--untagged") .pipe_in_fixture("lorem_ipsum.txt") .succeeds() - .stdout_only_fixture("bsd_stdin.expected"); + .stdout_is_fixture("untagged/crc_stdin.expected"); } #[test] -fn test_sysv_single_file() { - new_ucmd!() - .arg("-a=sysv") - .arg("lorem_ipsum.txt") - .succeeds() - .stdout_only_fixture("sysv_single_file.expected"); +fn test_untagged_algorithm_single_file() { + for algo in ALGOS { + new_ucmd!() + .arg("--untagged") + .arg(format!("--algorithm={algo}")) + .arg("lorem_ipsum.txt") + .succeeds() + .stdout_is_fixture(format!("untagged/{algo}_single_file.expected")); + } } #[test] -fn test_sysv_multiple_files() { - new_ucmd!() - .arg("-a=sysv") - .arg("lorem_ipsum.txt") - .arg("alice_in_wonderland.txt") - .succeeds() - .stdout_only_fixture("sysv_multiple_files.expected"); +fn test_untagged_algorithm_multiple_files() { + for algo in ALGOS { + new_ucmd!() + .arg("--untagged") + .arg(format!("--algorithm={algo}")) + .arg("lorem_ipsum.txt") + .arg("alice_in_wonderland.txt") + .succeeds() + .stdout_is_fixture(format!("untagged/{algo}_multiple_files.expected")); + } } #[test] -fn test_sysv_stdin() { - new_ucmd!() - .arg("-a=sysv") - .pipe_in_fixture("lorem_ipsum.txt") - .succeeds() - .stdout_only_fixture("sysv_stdin.expected"); +fn test_untagged_algorithm_stdin() { + for algo in ALGOS { + new_ucmd!() + .arg("--untagged") + .arg(format!("--algorithm={algo}")) + .pipe_in_fixture("lorem_ipsum.txt") + .succeeds() + .stdout_is_fixture(format!("untagged/{algo}_stdin.expected")); + } } diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index dfbbc1473a5..fa5845eac35 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -244,6 +244,192 @@ fn test_cp_arg_update_interactive_error() { .no_stdout(); } +#[test] +fn test_cp_arg_update_none() { + let (at, mut ucmd) = at_and_ucmd!(); + + ucmd.arg(TEST_HELLO_WORLD_SOURCE) + .arg(TEST_HOW_ARE_YOU_SOURCE) + .arg("--update=none") + .succeeds() + .no_stderr() + .no_stdout(); + + assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "How are you?\n"); +} + +#[test] +fn test_cp_arg_update_all() { + let (at, mut ucmd) = at_and_ucmd!(); + + ucmd.arg(TEST_HELLO_WORLD_SOURCE) + .arg(TEST_HOW_ARE_YOU_SOURCE) + .arg("--update=all") + .succeeds() + .no_stderr() + .no_stdout(); + + assert_eq!( + at.read(TEST_HOW_ARE_YOU_SOURCE), + at.read(TEST_HELLO_WORLD_SOURCE) + ); +} + +#[test] +fn test_cp_arg_update_older_dest_not_older_than_src() { + let (at, mut ucmd) = at_and_ucmd!(); + + let old = "test_cp_arg_update_dest_not_older_file1"; + let new = "test_cp_arg_update_dest_not_older_file2"; + let old_content = "old content\n"; + let new_content = "new content\n"; + + at.write(old, old_content); + at.write(new, new_content); + + ucmd.arg(old) + .arg(new) + .arg("--update=older") + .succeeds() + .no_stderr() + .no_stdout(); + + assert_eq!(at.read(new), "new content\n"); +} + +#[test] +fn test_cp_arg_update_older_dest_older_than_src() { + let (at, mut ucmd) = at_and_ucmd!(); + + let old = "test_cp_arg_update_dest_older_file1"; + let new = "test_cp_arg_update_dest_older_file2"; + let old_content = "old content\n"; + let new_content = "new content\n"; + + at.write(old, old_content); + + sleep(Duration::from_secs(1)); + + at.write(new, new_content); + + ucmd.arg(new) + .arg(old) + .arg("--update=older") + .succeeds() + .no_stderr() + .no_stdout(); + + assert_eq!(at.read(old), "new content\n"); +} + +#[test] +fn test_cp_arg_update_short_no_overwrite() { + // same as --update=older + let (at, mut ucmd) = at_and_ucmd!(); + + let old = "test_cp_arg_update_short_no_overwrite_file1"; + let new = "test_cp_arg_update_short_no_overwrite_file2"; + let old_content = "old content\n"; + let new_content = "new content\n"; + + at.write(old, old_content); + + sleep(Duration::from_secs(1)); + + at.write(new, new_content); + + ucmd.arg(old) + .arg(new) + .arg("-u") + .succeeds() + .no_stderr() + .no_stdout(); + + assert_eq!(at.read(new), "new content\n"); +} + +#[test] +fn test_cp_arg_update_short_overwrite() { + // same as --update=older + let (at, mut ucmd) = at_and_ucmd!(); + + let old = "test_cp_arg_update_short_overwrite_file1"; + let new = "test_cp_arg_update_short_overwrite_file2"; + let old_content = "old content\n"; + let new_content = "new content\n"; + + at.write(old, old_content); + + sleep(Duration::from_secs(1)); + + at.write(new, new_content); + + ucmd.arg(new) + .arg(old) + .arg("-u") + .succeeds() + .no_stderr() + .no_stdout(); + + assert_eq!(at.read(old), "new content\n"); +} + +#[test] +fn test_cp_arg_update_none_then_all() { + // take last if multiple update args are supplied, + // update=all wins in this case + let (at, mut ucmd) = at_and_ucmd!(); + + let old = "test_cp_arg_update_none_then_all_file1"; + let new = "test_cp_arg_update_none_then_all_file2"; + let old_content = "old content\n"; + let new_content = "new content\n"; + + at.write(old, old_content); + + sleep(Duration::from_secs(1)); + + at.write(new, new_content); + + ucmd.arg(old) + .arg(new) + .arg("--update=none") + .arg("--update=all") + .succeeds() + .no_stderr() + .no_stdout(); + + assert_eq!(at.read(new), "old content\n"); +} + +#[test] +fn test_cp_arg_update_all_then_none() { + // take last if multiple update args are supplied, + // update=none wins in this case + let (at, mut ucmd) = at_and_ucmd!(); + + let old = "test_cp_arg_update_all_then_none_file1"; + let new = "test_cp_arg_update_all_then_none_file2"; + let old_content = "old content\n"; + let new_content = "new content\n"; + + at.write(old, old_content); + + sleep(Duration::from_secs(1)); + + at.write(new, new_content); + + ucmd.arg(old) + .arg(new) + .arg("--update=all") + .arg("--update=none") + .succeeds() + .no_stderr() + .no_stdout(); + + assert_eq!(at.read(new), "new content\n"); +} + #[test] fn test_cp_arg_interactive() { let (at, mut ucmd) = at_and_ucmd!(); @@ -257,6 +443,7 @@ fn test_cp_arg_interactive() { } #[test] +#[cfg(not(target_os = "android"))] fn test_cp_arg_interactive_update() { // -u -i won't show the prompt to validate the override or not // Therefore, the error code will be 0 @@ -269,6 +456,29 @@ fn test_cp_arg_interactive_update() { .no_stdout(); } +#[test] +#[cfg(not(target_os = "android"))] +fn test_cp_arg_interactive_verbose() { + let (at, mut ucmd) = at_and_ucmd!(); + at.touch("a"); + at.touch("b"); + ucmd.args(&["-vi", "a", "b"]) + .pipe_in("N\n") + .fails() + .stdout_is("skipped 'b'\n"); +} + +#[test] +#[cfg(not(target_os = "android"))] +fn test_cp_arg_interactive_verbose_clobber() { + let (at, mut ucmd) = at_and_ucmd!(); + at.touch("a"); + at.touch("b"); + ucmd.args(&["-vin", "a", "b"]) + .fails() + .stdout_is("skipped 'b'\n"); +} + #[test] #[cfg(target_os = "linux")] fn test_cp_arg_link() { @@ -300,7 +510,8 @@ fn test_cp_arg_no_clobber() { ucmd.arg(TEST_HELLO_WORLD_SOURCE) .arg(TEST_HOW_ARE_YOU_SOURCE) .arg("--no-clobber") - .succeeds(); + .fails() + .stderr_contains("not replacing"); assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "How are you?\n"); } @@ -311,7 +522,7 @@ fn test_cp_arg_no_clobber_inferred_arg() { ucmd.arg(TEST_HELLO_WORLD_SOURCE) .arg(TEST_HOW_ARE_YOU_SOURCE) .arg("--no-clob") - .succeeds(); + .fails(); assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "How are you?\n"); } @@ -338,7 +549,7 @@ fn test_cp_arg_no_clobber_twice() { .arg("--no-clobber") .arg("source.txt") .arg("dest.txt") - .succeeds(); + .fails(); assert_eq!(at.read("source.txt"), "some-content"); // Should be empty as the "no-clobber" should keep @@ -888,6 +1099,88 @@ fn test_cp_parents_dest_not_directory() { .stderr_contains("with --parents, the destination must be a directory"); } +#[test] +fn test_cp_parents_with_permissions_copy_file() { + let (at, mut ucmd) = at_and_ucmd!(); + + let dir = "dir"; + let file = "p1/p2/file"; + + at.mkdir(dir); + at.mkdir_all("p1/p2"); + at.touch(file); + + let p1_mode = 0o0777; + let p2_mode = 0o0711; + let file_mode = 0o0702; + + #[cfg(unix)] + { + at.set_mode("p1", p1_mode); + at.set_mode("p1/p2", p2_mode); + at.set_mode(file, file_mode); + } + + ucmd.arg("-p") + .arg("--parents") + .arg(file) + .arg(dir) + .succeeds(); + + #[cfg(all(unix, not(target_os = "freebsd")))] + { + let p1_metadata = at.metadata("p1"); + let p2_metadata = at.metadata("p1/p2"); + let file_metadata = at.metadata(file); + + assert_metadata_eq!(p1_metadata, at.metadata("dir/p1")); + assert_metadata_eq!(p2_metadata, at.metadata("dir/p1/p2")); + assert_metadata_eq!(file_metadata, at.metadata("dir/p1/p2/file")); + } +} + +#[test] +fn test_cp_parents_with_permissions_copy_dir() { + let (at, mut ucmd) = at_and_ucmd!(); + + let dir1 = "dir"; + let dir2 = "p1/p2"; + let file = "p1/p2/file"; + + at.mkdir(dir1); + at.mkdir_all(dir2); + at.touch(file); + + let p1_mode = 0o0777; + let p2_mode = 0o0711; + let file_mode = 0o0702; + + #[cfg(unix)] + { + at.set_mode("p1", p1_mode); + at.set_mode("p1/p2", p2_mode); + at.set_mode(file, file_mode); + } + + ucmd.arg("-p") + .arg("--parents") + .arg("-r") + .arg(dir2) + .arg(dir1) + .succeeds(); + + #[cfg(all(unix, not(target_os = "freebsd")))] + { + let p1_metadata = at.metadata("p1"); + let p2_metadata = at.metadata("p1/p2"); + let file_metadata = at.metadata(file); + + assert_metadata_eq!(p1_metadata, at.metadata("dir/p1")); + assert_metadata_eq!(p2_metadata, at.metadata("dir/p1/p2")); + assert_metadata_eq!(file_metadata, at.metadata("dir/p1/p2/file")); + } +} + #[test] #[cfg(unix)] fn test_cp_writable_special_file_permissions() { @@ -930,6 +1223,33 @@ fn test_cp_preserve_no_args() { } } +#[test] +fn test_cp_preserve_no_args_before_opts() { + let (at, mut ucmd) = at_and_ucmd!(); + let src_file = "a"; + let dst_file = "b"; + + // Prepare the source file + at.touch(src_file); + #[cfg(unix)] + at.set_mode(src_file, 0o0500); + + // Copy + ucmd.arg("--preserve") + .arg(src_file) + .arg(dst_file) + .succeeds(); + + #[cfg(all(unix, not(target_os = "freebsd")))] + { + // Assert that the mode, ownership, and timestamps are preserved + // NOTICE: the ownership is not modified on the src file, because that requires root permissions + let metadata_src = at.metadata(src_file); + let metadata_dst = at.metadata(dst_file); + assert_metadata_eq!(metadata_src, metadata_dst); + } +} + #[test] fn test_cp_preserve_all() { let (at, mut ucmd) = at_and_ucmd!(); @@ -1394,13 +1714,13 @@ fn test_cp_one_file_system() { use walkdir::WalkDir; let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; // Test must be run as root (or with `sudo -E`) if scene.cmd("whoami").run().stdout_str() != "root\n" { return; } - let at = scene.fixtures.clone(); let at_src = AtPath::new(&at.plus(TEST_MOUNT_COPY_FROM_FOLDER)); let at_dst = AtPath::new(&at.plus(TEST_COPY_TO_FOLDER_NEW)); @@ -1542,7 +1862,7 @@ fn test_cp_reflink_insufficient_permission() { .stderr_only("cp: 'unreadable' -> 'existing_file.txt': Permission denied (os error 13)\n"); } -#[cfg(any(target_os = "linux", target_os = "android"))] +#[cfg(target_os = "linux")] #[test] fn test_closes_file_descriptors() { use procfs::process::Process; @@ -1902,6 +2222,17 @@ fn test_copy_through_dangling_symlink() { .stderr_only("cp: not writing through dangling symlink 'target'\n"); } +#[test] +fn test_copy_through_dangling_symlink_posixly_correct() { + let (at, mut ucmd) = at_and_ucmd!(); + at.touch("file"); + at.symlink_file("nonexistent", "target"); + ucmd.arg("file") + .arg("target") + .env("POSIXLY_CORRECT", "1") + .succeeds(); +} + #[test] fn test_copy_through_dangling_symlink_no_dereference() { let (at, mut ucmd) = at_and_ucmd!(); @@ -2297,6 +2628,20 @@ fn test_remove_destination_symbolic_link_loop() { assert!(at.file_exists("loop")); } +#[test] +#[cfg(not(windows))] +fn test_cp_symbolic_link_loop() { + let (at, mut ucmd) = at_and_ucmd!(); + at.symlink_file("loop", "loop"); + at.plus("loop"); + at.touch("f"); + ucmd.args(&["-f", "f", "loop"]) + .succeeds() + .no_stdout() + .no_stderr(); + assert!(at.file_exists("loop")); +} + /// Test that copying a directory to itself is disallowed. #[test] fn test_copy_directory_to_itself_disallowed() { @@ -2615,3 +2960,246 @@ fn test_cp_archive_on_directory_ending_dot() { ucmd.args(&["-a", "dir1/.", "dir2"]).succeeds(); assert!(at.file_exists("dir2/file")); } + +#[test] +fn test_cp_debug_default() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.touch("a"); + let result = ts.ucmd().arg("--debug").arg("a").arg("b").succeeds(); + + let stdout_str = result.stdout_str(); + #[cfg(target_os = "macos")] + if !stdout_str + .contains("copy offload: unknown, reflink: unsupported, sparse detection: unsupported") + { + panic!("Failure: stdout was \n{stdout_str}"); + } + #[cfg(target_os = "linux")] + if !stdout_str.contains("copy offload: unknown, reflink: unsupported, sparse detection: no") { + panic!("Failure: stdout was \n{stdout_str}"); + } + + #[cfg(windows)] + if !stdout_str + .contains("copy offload: unsupported, reflink: unsupported, sparse detection: unsupported") + { + panic!("Failure: stdout was \n{stdout_str}"); + } +} + +#[test] +fn test_cp_debug_multiple_default() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + let dir = "dir"; + at.touch("a"); + at.touch("b"); + at.mkdir(dir); + let result = ts + .ucmd() + .arg("--debug") + .arg("a") + .arg("b") + .arg(dir) + .succeeds(); + + let stdout_str = result.stdout_str(); + + #[cfg(target_os = "macos")] + { + if !stdout_str + .contains("copy offload: unknown, reflink: unsupported, sparse detection: unsupported") + { + panic!("Failure: stdout was \n{stdout_str}"); + } + + // two files, two occurrences + assert_eq!( + result + .stdout_str() + .matches( + "copy offload: unknown, reflink: unsupported, sparse detection: unsupported" + ) + .count(), + 2 + ); + } + + #[cfg(target_os = "linux")] + { + if !stdout_str.contains("copy offload: unknown, reflink: unsupported, sparse detection: no") + { + panic!("Failure: stdout was \n{stdout_str}"); + } + + // two files, two occurrences + assert_eq!( + result + .stdout_str() + .matches("copy offload: unknown, reflink: unsupported, sparse detection: no") + .count(), + 2 + ); + } + + #[cfg(target_os = "windows")] + { + if !stdout_str.contains( + "copy offload: unsupported, reflink: unsupported, sparse detection: unsupported", + ) { + panic!("Failure: stdout was \n{stdout_str}"); + } + + // two files, two occurrences + assert_eq!( + result + .stdout_str() + .matches("copy offload: unsupported, reflink: unsupported, sparse detection: unsupported") + .count(), + 2 + ); + } +} + +#[test] +#[cfg(target_os = "linux")] +fn test_cp_debug_sparse_reflink() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.touch("a"); + let result = ts + .ucmd() + .arg("--debug") + .arg("--sparse=always") + .arg("--reflink=never") + .arg("a") + .arg("b") + .succeeds(); + + let stdout_str = result.stdout_str(); + if !stdout_str.contains("copy offload: avoided, reflink: no, sparse detection: zeros") { + panic!("Failure: stdout was \n{stdout_str}"); + } +} + +#[test] +#[cfg(target_os = "linux")] +fn test_cp_debug_sparse_always() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.touch("a"); + let result = ts + .ucmd() + .arg("--debug") + .arg("--sparse=always") + .arg("a") + .arg("b") + .succeeds(); + let stdout_str = result.stdout_str(); + if !stdout_str.contains("copy offload: avoided, reflink: unsupported, sparse detection: zeros") + { + panic!("Failure: stdout was \n{stdout_str}"); + } +} + +#[test] +#[cfg(target_os = "linux")] +fn test_cp_debug_sparse_never() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.touch("a"); + let result = ts + .ucmd() + .arg("--debug") + .arg("--sparse=never") + .arg("a") + .arg("b") + .succeeds(); + let stdout_str = result.stdout_str(); + if !stdout_str.contains("copy offload: unknown, reflink: unsupported, sparse detection: no") { + panic!("Failure: stdout was \n{stdout_str}"); + } +} + +#[test] +fn test_cp_debug_sparse_auto() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.touch("a"); + let result = ts + .ucmd() + .arg("--debug") + .arg("--sparse=auto") + .arg("a") + .arg("b") + .succeeds(); + let stdout_str = result.stdout_str(); + + #[cfg(target_os = "macos")] + if !stdout_str + .contains("copy offload: unknown, reflink: unsupported, sparse detection: unsupported") + { + panic!("Failure: stdout was \n{stdout_str}"); + } + + #[cfg(target_os = "linux")] + if !stdout_str.contains("copy offload: unknown, reflink: unsupported, sparse detection: no") { + panic!("Failure: stdout was \n{stdout_str}"); + } +} + +#[test] +#[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))] +fn test_cp_debug_reflink_auto() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.touch("a"); + let result = ts + .ucmd() + .arg("--debug") + .arg("--reflink=auto") + .arg("a") + .arg("b") + .succeeds(); + + #[cfg(target_os = "linux")] + { + let stdout_str = result.stdout_str(); + if !stdout_str.contains("copy offload: unknown, reflink: unsupported, sparse detection: no") + { + panic!("Failure: stdout was \n{stdout_str}"); + } + } + + #[cfg(target_os = "macos")] + { + let stdout_str = result.stdout_str(); + if !stdout_str + .contains("copy offload: unknown, reflink: unsupported, sparse detection: unsupported") + { + panic!("Failure: stdout was \n{stdout_str}"); + } + } +} + +#[test] +#[cfg(target_os = "linux")] +fn test_cp_debug_sparse_always_reflink_auto() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.touch("a"); + let result = ts + .ucmd() + .arg("--debug") + .arg("--sparse=always") + .arg("--reflink=auto") + .arg("a") + .arg("b") + .succeeds(); + let stdout_str = result.stdout_str(); + if !stdout_str.contains("copy offload: avoided, reflink: unsupported, sparse detection: zeros") + { + panic!("Failure: stdout was \n{stdout_str}"); + } +} diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 7e9c52af5c2..669f02e331d 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -1,9 +1,7 @@ -extern crate regex; - -use self::regex::Regex; use crate::common::util::TestScenario; +use regex::Regex; #[cfg(all(unix, not(target_os = "macos")))] -use rust_users::get_effective_uid; +use uucore::process::geteuid; #[test] fn test_invalid_arg() { @@ -215,7 +213,7 @@ fn test_date_format_literal() { #[test] #[cfg(all(unix, not(target_os = "macos")))] fn test_date_set_valid() { - if get_effective_uid() == 0 { + if geteuid() == 0 { new_ucmd!() .arg("--set") .arg("2020-03-12 13:30:00+08:00") @@ -236,7 +234,7 @@ fn test_date_set_invalid() { #[test] #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] fn test_date_set_permissions_error() { - if !(get_effective_uid() == 0 || uucore::os::is_wsl_1()) { + if !(geteuid() == 0 || uucore::os::is_wsl_1()) { let result = new_ucmd!() .arg("--set") .arg("2020-03-11 21:45:00+08:00") @@ -263,7 +261,7 @@ fn test_date_set_mac_unavailable() { #[cfg(all(unix, not(target_os = "macos")))] /// TODO: expected to fail currently; change to succeeds() when required. fn test_date_set_valid_2() { - if get_effective_uid() == 0 { + if geteuid() == 0 { let result = new_ucmd!() .arg("--set") .arg("Sat 20 Mar 2021 14:53:01 AWST") // spell-checker:disable-line @@ -283,6 +281,38 @@ fn test_date_for_invalid_file() { ); } +#[test] +#[cfg(unix)] +fn test_date_for_no_permission_file() { + let (at, mut ucmd) = at_and_ucmd!(); + const FILE: &str = "file-no-perm-1"; + + use std::os::unix::fs::PermissionsExt; + let file = std::fs::OpenOptions::new() + .create(true) + .write(true) + .open(at.plus(FILE)) + .unwrap(); + file.set_permissions(std::fs::Permissions::from_mode(0o222)) + .unwrap(); + let result = ucmd.arg("--file").arg(FILE).fails(); + result.no_stdout(); + assert_eq!( + result.stderr_str().trim(), + format!("date: {FILE}: Permission denied") + ); +} + +#[test] +fn test_date_for_dir_as_file() { + let result = new_ucmd!().arg("--file").arg("/").fails(); + result.no_stdout(); + assert_eq!( + result.stderr_str().trim(), + "date: expected file, got directory '/'", + ); +} + #[test] fn test_date_for_file() { let (at, mut ucmd) = at_and_ucmd!(); @@ -295,7 +325,7 @@ fn test_date_for_file() { #[cfg(all(unix, not(target_os = "macos")))] /// TODO: expected to fail currently; change to succeeds() when required. fn test_date_set_valid_3() { - if get_effective_uid() == 0 { + if geteuid() == 0 { let result = new_ucmd!() .arg("--set") .arg("Sat 20 Mar 2021 14:53:01") // Local timezone @@ -309,7 +339,7 @@ fn test_date_set_valid_3() { #[cfg(all(unix, not(target_os = "macos")))] /// TODO: expected to fail currently; change to succeeds() when required. fn test_date_set_valid_4() { - if get_effective_uid() == 0 { + if geteuid() == 0 { let result = new_ucmd!() .arg("--set") .arg("2020-03-11 21:45:00") // Local timezone @@ -326,6 +356,36 @@ fn test_invalid_format_string() { assert!(result.stderr_str().starts_with("date: invalid format ")); } +#[test] +fn test_unsupported_format() { + let result = new_ucmd!().arg("+%#z").fails(); + result.no_stdout(); + assert!(result.stderr_str().starts_with("date: invalid format %#z")); +} + +#[test] +fn test_date_string_human() { + let date_formats = vec![ + "1 year ago", + "1 year", + "2 months ago", + "15 days ago", + "1 week ago", + "5 hours ago", + "30 minutes ago", + "10 seconds", + ]; + let re = Regex::new(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}\n$").unwrap(); + for date_format in date_formats { + new_ucmd!() + .arg("-d") + .arg(date_format) + .arg("+%Y-%m-%d %S:%M") + .succeeds() + .stdout_matches(&re); + } +} + #[test] fn test_invalid_date_string() { new_ucmd!() diff --git a/tests/by-util/test_dd.rs b/tests/by-util/test_dd.rs index 99eae480945..b43f32c240a 100644 --- a/tests/by-util/test_dd.rs +++ b/tests/by-util/test_dd.rs @@ -1,7 +1,7 @@ // spell-checker:ignore fname, tname, fpath, specfile, testfile, unspec, ifile, ofile, outfile, fullblock, urand, fileio, atoe, atoibm, availible, behaviour, bmax, bremain, btotal, cflags, creat, ctable, ctty, datastructures, doesnt, etoa, fileout, fname, gnudd, iconvflags, iseek, nocache, noctty, noerror, nofollow, nolinks, nonblock, oconvflags, oseek, outfile, parseargs, rlen, rmax, rposition, rremain, rsofar, rstat, sigusr, sigval, wlen, wstat abcdefghijklm abcdefghi nabcde nabcdefg abcdefg use crate::common::util::TestScenario; -#[cfg(not(windows))] +#[cfg(all(not(windows), feature = "printf"))] use crate::common::util::{UCommand, TESTS_BINARY}; use regex::Regex; @@ -1536,3 +1536,29 @@ fn test_multiple_processes_reading_stdin() { .succeeds() .stdout_only("def\n"); } + +/// Test that discarding system file cache fails for stdin. +#[test] +#[cfg(target_os = "linux")] +fn test_nocache_stdin_error() { + #[cfg(not(target_env = "musl"))] + let detail = "Illegal seek"; + #[cfg(target_env = "musl")] + let detail = "Invalid seek"; + new_ucmd!() + .args(&["iflag=nocache", "count=0", "status=noxfer"]) + .fails() + .code_is(1) + .stderr_only(format!("dd: failed to discard cache for: 'standard input': {detail}\n0+0 records in\n0+0 records out\n")); +} + +/// Test for discarding system file cache. +#[test] +#[cfg(target_os = "linux")] +fn test_nocache_file() { + let (at, mut ucmd) = at_and_ucmd!(); + at.write_bytes("f", b"a".repeat(1 << 20).as_slice()); + ucmd.args(&["if=f", "of=/dev/null", "iflag=nocache", "status=noxfer"]) + .succeeds() + .stderr_only("2048+0 records in\n2048+0 records out\n"); +} diff --git a/tests/by-util/test_dir.rs b/tests/by-util/test_dir.rs index 5905edff184..fd94f3a8f5d 100644 --- a/tests/by-util/test_dir.rs +++ b/tests/by-util/test_dir.rs @@ -1,11 +1,5 @@ -#[cfg(not(windows))] -extern crate libc; -extern crate regex; -#[cfg(not(windows))] -extern crate tempfile; - -use self::regex::Regex; use crate::common::util::TestScenario; +use regex::Regex; /* * As dir use the same functions than ls, we don't have to retest them here. diff --git a/tests/by-util/test_dircolors.rs b/tests/by-util/test_dircolors.rs index 629a10912dc..2d83c76b53d 100644 --- a/tests/by-util/test_dircolors.rs +++ b/tests/by-util/test_dircolors.rs @@ -1,8 +1,7 @@ // spell-checker:ignore overridable use crate::common::util::TestScenario; -extern crate dircolors; -use self::dircolors::{guess_syntax, OutputFmt, StrUtils}; +use dircolors::{guess_syntax, OutputFmt, StrUtils}; #[test] fn test_invalid_arg() { @@ -221,3 +220,13 @@ fn test_helper(file_name: &str, term: &str) { .run() .stdout_is_fixture(format!("{file_name}.sh.expected")); } + +#[test] +fn test_dircolors_for_dir_as_file() { + let result = new_ucmd!().args(&["-c", "/"]).fails(); + result.no_stdout(); + assert_eq!( + result.stderr_str().trim(), + "dircolors: expected file, got directory '/'", + ); +} diff --git a/tests/by-util/test_du.rs b/tests/by-util/test_du.rs index d69eaaf9919..699746f03f4 100644 --- a/tests/by-util/test_du.rs +++ b/tests/by-util/test_du.rs @@ -3,10 +3,9 @@ // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -// spell-checker:ignore (paths) sublink subwords azerty azeaze xcwww azeaz amaz azea qzerty tazerty +// spell-checker:ignore (paths) sublink subwords azerty azeaze xcwww azeaz amaz azea qzerty tazerty tsublink #[cfg(not(windows))] use regex::Regex; -#[cfg(not(windows))] use std::io::Write; #[cfg(any(target_os = "linux", target_os = "android"))] @@ -122,7 +121,7 @@ fn test_du_invalid_size() { fn test_du_basics_bad_name() { new_ucmd!() .arg("bad_name") - .succeeds() // TODO: replace with ".fails()" once `du` is fixed + .fails() .stderr_only("du: bad_name: No such file or directory\n"); } @@ -286,6 +285,30 @@ fn test_du_dereference() { _du_dereference(result.stdout_str()); } +#[cfg(not(windows))] +#[test] +fn test_du_dereference_args() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + at.mkdir_all("subdir"); + let mut file1 = at.make_file("subdir/file-ignore1"); + file1.write_all(b"azeaze").unwrap(); + let mut file2 = at.make_file("subdir/file-ignore1"); + file2.write_all(b"amaz?ng").unwrap(); + at.symlink_dir("subdir", "sublink"); + + let result = ts.ucmd().arg("-D").arg("-s").arg("sublink").succeeds(); + let stdout = result.stdout_str(); + + assert!(!stdout.starts_with('0')); + assert!(stdout.contains("sublink")); + + // Without the option + let result = ts.ucmd().arg("-s").arg("sublink").succeeds(); + result.stdout_contains("0\tsublink\n"); +} + #[cfg(target_vendor = "apple")] fn _du_dereference(s: &str) { assert_eq!(s, "4\tsubdir/links/deeper_dir\n16\tsubdir/links\n"); @@ -851,3 +874,29 @@ fn test_du_exclude_invalid_syntax() { .fails() .stderr_contains("du: Invalid exclude syntax"); } + +#[cfg(not(windows))] +#[test] +fn test_du_symlink_fail() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + at.symlink_file("non-existing.txt", "target.txt"); + + ts.ucmd().arg("-L").arg("target.txt").fails().code_is(1); +} + +#[cfg(not(windows))] +#[test] +fn test_du_symlink_multiple_fail() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + at.symlink_file("non-existing.txt", "target.txt"); + let mut file1 = at.make_file("file1"); + file1.write_all(b"azeaze").unwrap(); + + let result = ts.ucmd().arg("-L").arg("target.txt").arg("file1").fails(); + assert_eq!(result.code(), 1); + result.stdout_contains("4\tfile1\n"); +} diff --git a/tests/by-util/test_factor.rs b/tests/by-util/test_factor.rs index 23da2ea69f7..1c75413702f 100644 --- a/tests/by-util/test_factor.rs +++ b/tests/by-util/test_factor.rs @@ -8,19 +8,16 @@ // spell-checker:ignore (methods) hexdigest -use crate::common::util::{AtPath, TestScenario}; +use crate::common::util::TestScenario; use std::time::{Duration, SystemTime}; #[path = "../../src/uu/factor/sieve.rs"] mod sieve; -extern crate conv; -extern crate rand; - -use self::rand::distributions::{Distribution, Uniform}; -use self::rand::{rngs::SmallRng, Rng, SeedableRng}; use self::sieve::Sieve; +use rand::distributions::{Distribution, Uniform}; +use rand::{rngs::SmallRng, Rng, SeedableRng}; const NUM_PRIMES: usize = 10000; const NUM_TESTS: usize = 100; @@ -30,9 +27,17 @@ fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails().code_is(1); } +#[test] +fn test_valid_arg_exponents() { + new_ucmd!().arg("-h").succeeds().code_is(0); + new_ucmd!().arg("--exponents").succeeds().code_is(0); +} + #[test] #[cfg(feature = "sort")] +#[cfg(not(target_os = "android"))] fn test_parallel() { + use crate::common::util::AtPath; use hex_literal::hex; use sha1::{Digest, Sha1}; use std::{fs::OpenOptions, time::Duration}; @@ -80,7 +85,6 @@ fn test_parallel() { #[test] fn test_first_1000_integers() { - extern crate sha1; use hex_literal::hex; use sha1::{Digest, Sha1}; @@ -103,6 +107,33 @@ fn test_first_1000_integers() { ); } +#[test] +fn test_first_1000_integers_with_exponents() { + use hex_literal::hex; + use sha1::{Digest, Sha1}; + + let n_integers = 1000; + let mut input_string = String::new(); + for i in 0..=n_integers { + input_string.push_str(&(format!("{i} "))[..]); + } + + println!("STDIN='{input_string}'"); + let result = new_ucmd!() + .arg("-h") + .pipe_in(input_string.as_bytes()) + .succeeds(); + + // Using factor from GNU Coreutils 9.2 + // `seq 0 1000 | factor -h | sha1sum` => "45f5f758a9319870770bd1fec2de23d54331944d" + let mut hasher = Sha1::new(); + hasher.update(result.stdout()); + let hash_check = hasher.finalize(); + assert_eq!( + hash_check[..], + hex!("45f5f758a9319870770bd1fec2de23d54331944d") + ); +} #[test] fn test_cli_args() { // Make sure that factor works with CLI arguments as well. @@ -117,7 +148,7 @@ fn test_cli_args() { #[test] fn test_random() { - use conv::prelude::*; + use conv::prelude::ValueFrom; let log_num_primes = f64::value_from(NUM_PRIMES).unwrap().log2().ceil(); let primes = Sieve::primes().take(NUM_PRIMES).collect::>(); @@ -282,6 +313,35 @@ fn run(input_string: &[u8], output_string: &[u8]) { .stdout_is(String::from_utf8(output_string.to_owned()).unwrap()); } +#[test] +fn test_primes_with_exponents() { + let mut input_string = String::new(); + let mut output_string = String::new(); + for primes in PRIMES_BY_BITS.iter() { + for &prime in *primes { + input_string.push_str(&(format!("{prime} "))[..]); + output_string.push_str(&(format!("{prime}: {prime}\n"))[..]); + } + } + + println!( + "STDIN='{}'", + String::from_utf8_lossy(input_string.as_bytes()) + ); + println!( + "STDOUT(expected)='{}'", + String::from_utf8_lossy(output_string.as_bytes()) + ); + + // run factor with --exponents + new_ucmd!() + .timeout(Duration::from_secs(240)) + .arg("--exponents") + .pipe_in(input_string) + .run() + .stdout_is(String::from_utf8(output_string.as_bytes().to_owned()).unwrap()); +} + const PRIMES_BY_BITS: &[&[u64]] = &[ PRIMES14, PRIMES15, PRIMES16, PRIMES17, PRIMES18, PRIMES19, PRIMES20, PRIMES21, PRIMES22, PRIMES23, PRIMES24, PRIMES25, PRIMES26, PRIMES27, PRIMES28, PRIMES29, PRIMES30, PRIMES31, diff --git a/tests/by-util/test_fmt.rs b/tests/by-util/test_fmt.rs index 316add0386f..afce9acd8db 100644 --- a/tests/by-util/test_fmt.rs +++ b/tests/by-util/test_fmt.rs @@ -7,47 +7,39 @@ fn test_invalid_arg() { #[test] fn test_fmt() { - let result = new_ucmd!().arg("one-word-per-line.txt").run(); - //.stdout_is_fixture("call_graph.expected"); - assert_eq!( - result.stdout_str().trim(), - "this is a file with one word per line" - ); + new_ucmd!() + .arg("one-word-per-line.txt") + .succeeds() + .stdout_is("this is a file with one word per line\n"); } #[test] -fn test_fmt_q() { - let result = new_ucmd!().arg("-q").arg("one-word-per-line.txt").run(); - //.stdout_is_fixture("call_graph.expected"); - assert_eq!( - result.stdout_str().trim(), - "this is a file with one word per line" - ); +fn test_fmt_quick() { + for param in ["-q", "--quick"] { + new_ucmd!() + .args(&["one-word-per-line.txt", param]) + .succeeds() + .stdout_is("this is a file with one word per line\n"); + } } #[test] -fn test_fmt_w_too_big() { - let result = new_ucmd!() - .arg("-w") - .arg("2501") - .arg("one-word-per-line.txt") - .run(); - //.stdout_is_fixture("call_graph.expected"); - assert_eq!( - result.stderr_str().trim(), - "fmt: invalid width: '2501': Numerical result out of range" - ); +fn test_fmt_width() { + for param in ["-w", "--width"] { + new_ucmd!() + .args(&["one-word-per-line.txt", param, "10"]) + .succeeds() + .stdout_is("this is\na file\nwith one\nword per\nline\n"); + } } + #[test] -fn test_fmt_w() { - let result = new_ucmd!() - .arg("-w") - .arg("10") - .arg("one-word-per-line.txt") - .run(); - //.stdout_is_fixture("call_graph.expected"); - assert_eq!( - result.stdout_str().trim(), - "this is\na file\nwith one\nword per\nline" - ); +fn test_fmt_width_too_big() { + for param in ["-w", "--width"] { + new_ucmd!() + .args(&["one-word-per-line.txt", param, "2501"]) + .fails() + .code_is(1) + .stderr_is("fmt: invalid width: '2501': Numerical result out of range\n"); + } } diff --git a/tests/by-util/test_hashsum.rs b/tests/by-util/test_hashsum.rs index 3d3d72c4ca7..3650047b21a 100644 --- a/tests/by-util/test_hashsum.rs +++ b/tests/by-util/test_hashsum.rs @@ -53,6 +53,14 @@ macro_rules! test_digest { .stdout_is("input.txt: OK\n"); } + #[test] + fn test_zero() { + let ts = TestScenario::new("hashsum"); + assert_eq!(ts.fixtures.read(EXPECTED_FILE), + get_hash!(ts.ucmd().arg(DIGEST_ARG).arg(BITS_ARG).arg("--zero").arg("input.txt").succeeds().no_stderr().stdout_str())); + } + + #[cfg(windows)] #[test] fn test_text_mode() { @@ -117,6 +125,70 @@ fn test_check_sha1() { .stderr_is(""); } +#[test] +fn test_check_b2sum_length_option_0() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.write("testf", "foobar\n"); + at.write("testf.b2sum", "9e2bf63e933e610efee4a8d6cd4a9387e80860edee97e27db3b37a828d226ab1eb92a9cdd8ca9ca67a753edaf8bd89a0558496f67a30af6f766943839acf0110 testf\n"); + + scene + .ccmd("b2sum") + .arg("--length=0") + .arg("-c") + .arg(at.subdir.join("testf.b2sum")) + .succeeds() + .stdout_only("testf: OK\n"); +} + +#[test] +fn test_check_b2sum_length_option_8() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.write("testf", "foobar\n"); + at.write("testf.b2sum", "6a testf\n"); + + scene + .ccmd("b2sum") + .arg("--length=8") + .arg("-c") + .arg(at.subdir.join("testf.b2sum")) + .succeeds() + .stdout_only("testf: OK\n"); +} + +#[test] +fn test_invalid_b2sum_length_option_not_multiple_of_8() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.write("testf", "foobar\n"); + + scene + .ccmd("b2sum") + .arg("--length=9") + .arg(at.subdir.join("testf")) + .fails() + .code_is(1); +} + +#[test] +fn test_invalid_b2sum_length_option_too_large() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.write("testf", "foobar\n"); + + scene + .ccmd("b2sum") + .arg("--length=513") + .arg(at.subdir.join("testf")) + .fails() + .code_is(1); +} + #[test] fn test_check_file_not_found_warning() { let scene = TestScenario::new(util_name!()); @@ -137,6 +209,144 @@ fn test_check_file_not_found_warning() { .stderr_is("sha1sum: warning: 1 listed file could not be read\n"); } +// Asterisk `*` is a reserved paths character on win32, nor the path can end with a whitespace. +// ref: https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions +#[test] +fn test_check_md5sum() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + #[cfg(not(windows))] + { + for f in &["a", " b", "*c", "dd", " "] { + at.write(f, &format!("{f}\n")); + } + at.write( + "check.md5sum", + "60b725f10c9c85c70d97880dfe8191b3 a\n\ + bf35d7536c785cf06730d5a40301eba2 b\n\ + f5b61709718c1ecf8db1aea8547d4698 *c\n\ + b064a020db8018f18ff5ae367d01b212 dd\n\ + d784fa8b6d98d27699781bd9a7cf19f0 ", + ); + scene + .ccmd("md5sum") + .arg("--strict") + .arg("-c") + .arg("check.md5sum") + .succeeds() + .stdout_is("a: OK\n b: OK\n*c: OK\ndd: OK\n : OK\n") + .stderr_is(""); + } + #[cfg(windows)] + { + for f in &["a", " b", "dd"] { + at.write(f, &format!("{f}\n")); + } + at.write( + "check.md5sum", + "60b725f10c9c85c70d97880dfe8191b3 a\n\ + bf35d7536c785cf06730d5a40301eba2 b\n\ + b064a020db8018f18ff5ae367d01b212 dd", + ); + scene + .ccmd("md5sum") + .arg("--strict") + .arg("-c") + .arg("check.md5sum") + .succeeds() + .stdout_is("a: OK\n b: OK\ndd: OK\n") + .stderr_is(""); + } +} + +#[test] +fn test_check_md5sum_reverse_bsd() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + #[cfg(not(windows))] + { + for f in &["a", " b", "*c", "dd", " "] { + at.write(f, &format!("{f}\n")); + } + at.write( + "check.md5sum", + "60b725f10c9c85c70d97880dfe8191b3 a\n\ + bf35d7536c785cf06730d5a40301eba2 b\n\ + f5b61709718c1ecf8db1aea8547d4698 *c\n\ + b064a020db8018f18ff5ae367d01b212 dd\n\ + d784fa8b6d98d27699781bd9a7cf19f0 ", + ); + scene + .ccmd("md5sum") + .arg("--strict") + .arg("-c") + .arg("check.md5sum") + .succeeds() + .stdout_is("a: OK\n b: OK\n*c: OK\ndd: OK\n : OK\n") + .stderr_is(""); + } + #[cfg(windows)] + { + for f in &["a", " b", "dd"] { + at.write(f, &format!("{f}\n")); + } + at.write( + "check.md5sum", + "60b725f10c9c85c70d97880dfe8191b3 a\n\ + bf35d7536c785cf06730d5a40301eba2 b\n\ + b064a020db8018f18ff5ae367d01b212 dd", + ); + scene + .ccmd("md5sum") + .arg("--strict") + .arg("-c") + .arg("check.md5sum") + .succeeds() + .stdout_is("a: OK\n b: OK\ndd: OK\n") + .stderr_is(""); + } +} + +#[test] +fn test_check_md5sum_mixed_format() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + #[cfg(not(windows))] + { + for f in &[" b", "*c", "dd", " "] { + at.write(f, &format!("{f}\n")); + } + at.write( + "check.md5sum", + "bf35d7536c785cf06730d5a40301eba2 b\n\ + f5b61709718c1ecf8db1aea8547d4698 *c\n\ + b064a020db8018f18ff5ae367d01b212 dd\n\ + d784fa8b6d98d27699781bd9a7cf19f0 ", + ); + } + #[cfg(windows)] + { + for f in &[" b", "dd"] { + at.write(f, &format!("{f}\n")); + } + at.write( + "check.md5sum", + "bf35d7536c785cf06730d5a40301eba2 b\n\ + b064a020db8018f18ff5ae367d01b212 dd", + ); + } + scene + .ccmd("md5sum") + .arg("--strict") + .arg("-c") + .arg("check.md5sum") + .fails() + .code_is(1); +} + #[test] fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails().code_is(1); diff --git a/tests/by-util/test_head.rs b/tests/by-util/test_head.rs index 571bfb3a89d..0e1eafc8602 100644 --- a/tests/by-util/test_head.rs +++ b/tests/by-util/test_head.rs @@ -189,6 +189,15 @@ fn test_no_such_file_or_directory() { .stderr_contains("cannot open 'no_such_file.toml' for reading: No such file or directory"); } +#[test] +fn test_lines_leading_zeros() { + new_ucmd!() + .arg("--lines=010") + .pipe_in("\n\n\n\n\n\n\n\n\n\n\n\n") + .succeeds() + .stdout_is("\n\n\n\n\n\n\n\n\n\n"); +} + /// Test that each non-existent files gets its own error message printed. #[test] fn test_multiple_nonexistent_files() { diff --git a/tests/by-util/test_hostid.rs b/tests/by-util/test_hostid.rs index 3ea818480b6..b42ec211d0c 100644 --- a/tests/by-util/test_hostid.rs +++ b/tests/by-util/test_hostid.rs @@ -1,6 +1,5 @@ -use crate::common::util::*; -extern crate regex; -use self::regex::Regex; +use crate::common::util::TestScenario; +use regex::Regex; #[test] fn test_normal() { diff --git a/tests/by-util/test_install.rs b/tests/by-util/test_install.rs index a30737f0598..d76ce1e0147 100644 --- a/tests/by-util/test_install.rs +++ b/tests/by-util/test_install.rs @@ -2,12 +2,12 @@ use crate::common::util::{is_ci, TestScenario}; use filetime::FileTime; -use rust_users::{get_effective_gid, get_effective_uid}; use std::os::unix::fs::PermissionsExt; #[cfg(not(any(windows, target_os = "freebsd")))] use std::process::Command; #[cfg(any(target_os = "linux", target_os = "android"))] use std::thread::sleep; +use uucore::process::{getegid, geteuid}; #[test] fn test_invalid_arg() { @@ -322,7 +322,7 @@ fn test_install_target_new_file_with_group() { let (at, mut ucmd) = at_and_ucmd!(); let file = "file"; let dir = "target_dir"; - let gid = get_effective_gid(); + let gid = getegid(); at.touch(file); at.mkdir(dir); @@ -349,7 +349,7 @@ fn test_install_target_new_file_with_owner() { let (at, mut ucmd) = at_and_ucmd!(); let file = "file"; let dir = "target_dir"; - let uid = get_effective_uid(); + let uid = geteuid(); at.touch(file); at.mkdir(dir); diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index ba328d8957f..1266a7cab92 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -1,18 +1,16 @@ -// spell-checker:ignore (words) READMECAREFULLY birthtime doesntexist oneline somebackup lrwx somefile somegroup somehiddenbackup somehiddenfile tabsize aaaaaaaa bbbb cccc dddddddd ncccc +// spell-checker:ignore (words) READMECAREFULLY birthtime doesntexist oneline somebackup lrwx somefile somegroup somehiddenbackup somehiddenfile tabsize aaaaaaaa bbbb cccc dddddddd ncccc neee naaaaa nbcdef nfffff -#[cfg(not(windows))] -extern crate libc; -extern crate regex; -#[cfg(not(windows))] -extern crate tempfile; - -use self::regex::Regex; -#[cfg(feature = "feat_selinux")] +#[cfg(any(unix, feature = "feat_selinux"))] use crate::common::util::expected_result; use crate::common::util::TestScenario; #[cfg(all(unix, feature = "chmod"))] use nix::unistd::{close, dup}; +use regex::Regex; use std::collections::HashMap; +#[cfg(target_os = "linux")] +use std::ffi::OsStr; +#[cfg(target_os = "linux")] +use std::os::unix::ffi::OsStrExt; #[cfg(all(unix, feature = "chmod"))] use std::os::unix::io::IntoRawFd; use std::path::Path; @@ -1245,7 +1243,7 @@ fn test_ls_long_total_size() { ("long_si", "total 8.2k"), ] .iter() - .cloned() + .copied() .collect() } else { [ @@ -1254,7 +1252,7 @@ fn test_ls_long_total_size() { ("long_si", "total 2"), ] .iter() - .cloned() + .copied() .collect() }; @@ -1282,7 +1280,7 @@ fn test_ls_long_formats() { // Zero or one "." for indicating a file with security context // Regex for three names, so all of author, group and owner - let re_three = Regex::new(r"[xrw-]{9}\.? \d ([-0-9_a-z_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)] let re_three_num = Regex::new(r"[xrw-]{9}\.? \d (\d+ ){3}0").unwrap(); @@ -1291,13 +1289,13 @@ fn test_ls_long_formats() { // - group and owner // - author and owner // - author and group - let re_two = Regex::new(r"[xrw-]{9}\.? \d ([-0-9_a-z_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)] let re_two_num = Regex::new(r"[xrw-]{9}\.? \d (\d+ ){2}0").unwrap(); // Regex for one name: author, group or owner - let re_one = Regex::new(r"[xrw-]{9}\.? \d [-0-9_a-z_A-Z]+ 0").unwrap(); + let re_one = Regex::new(r"[xrw-]{9}\.? \d [-0-9_a-z.A-Z]+ 0").unwrap(); #[cfg(unix)] let re_one_num = Regex::new(r"[xrw-]{9}\.? \d \d+ 0").unwrap(); @@ -1566,6 +1564,28 @@ fn test_ls_sort_name() { .stdout_is(".a\n.b\na\nb\n"); } +#[test] +fn test_ls_sort_width() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("aaaaa"); + at.touch("bbb"); + at.touch("cccc"); + at.touch("eee"); + at.touch("d"); + at.touch("fffff"); + at.touch("abc"); + at.touch("zz"); + at.touch("bcdef"); + + scene + .ucmd() + .arg("--sort=width") + .succeeds() + .stdout_is("d\nzz\nabc\nbbb\neee\ncccc\naaaaa\nbcdef\nfffff\n"); +} + #[test] fn test_ls_order_size() { let scene = TestScenario::new(util_name!()); @@ -1646,88 +1666,103 @@ fn test_ls_styles() { at.touch("test"); let re_full = Regex::new( - r"[a-z-]* \d* \w* \w* \d* \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d* (\+|\-)\d{4} test\n", + r"[a-z-]* \d* [\w.]* [\w.]* \d* \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d* (\+|\-)\d{4} test\n", ) .unwrap(); let re_long = - Regex::new(r"[a-z-]* \d* \w* \w* \d* \d{4}-\d{2}-\d{2} \d{2}:\d{2} test\n").unwrap(); - let re_iso = Regex::new(r"[a-z-]* \d* \w* \w* \d* \d{2}-\d{2} \d{2}:\d{2} test\n").unwrap(); + Regex::new(r"[a-z-]* \d* [\w.]* [\w.]* \d* \d{4}-\d{2}-\d{2} \d{2}:\d{2} test\n").unwrap(); + let re_iso = + Regex::new(r"[a-z-]* \d* [\w.]* [\w.]* \d* \d{2}-\d{2} \d{2}:\d{2} test\n").unwrap(); let re_locale = - Regex::new(r"[a-z-]* \d* \w* \w* \d* [A-Z][a-z]{2} ( |\d)\d \d{2}:\d{2} test\n").unwrap(); - let re_custom_format = Regex::new(r"[a-z-]* \d* \w* \w* \d* \d{4}__\d{2} test\n").unwrap(); + Regex::new(r"[a-z-]* \d* [\w.]* [\w.]* \d* [A-Z][a-z]{2} ( |\d)\d \d{2}:\d{2} test\n") + .unwrap(); + let re_custom_format = + Regex::new(r"[a-z-]* \d* [\w.]* [\w.]* \d* \d{4}__\d{2} test\n").unwrap(); //full-iso - let result = scene + scene .ucmd() .arg("-l") .arg("--time-style=full-iso") - .succeeds(); - assert!(re_full.is_match(result.stdout_str())); + .succeeds() + .stdout_matches(&re_full); //long-iso - let result = scene + scene .ucmd() .arg("-l") .arg("--time-style=long-iso") - .succeeds(); - assert!(re_long.is_match(result.stdout_str())); + .succeeds() + .stdout_matches(&re_long); //iso - let result = scene.ucmd().arg("-l").arg("--time-style=iso").succeeds(); - assert!(re_iso.is_match(result.stdout_str())); + scene + .ucmd() + .arg("-l") + .arg("--time-style=iso") + .succeeds() + .stdout_matches(&re_iso); //locale - let result = scene.ucmd().arg("-l").arg("--time-style=locale").succeeds(); - assert!(re_locale.is_match(result.stdout_str())); + scene + .ucmd() + .arg("-l") + .arg("--time-style=locale") + .succeeds() + .stdout_matches(&re_locale); //+FORMAT - let result = scene + scene .ucmd() .arg("-l") .arg("--time-style=+%Y__%M") - .succeeds(); - assert!(re_custom_format.is_match(result.stdout_str())); + .succeeds() + .stdout_matches(&re_custom_format); // Also fails due to not having full clap support for time_styles scene.ucmd().arg("-l").arg("-time-style=invalid").fails(); //Overwrite options tests - let result = scene + scene .ucmd() .arg("-l") .arg("--time-style=long-iso") .arg("--time-style=iso") - .succeeds(); - assert!(re_iso.is_match(result.stdout_str())); - let result = scene + .succeeds() + .stdout_matches(&re_iso); + scene .ucmd() .arg("--time-style=iso") .arg("--full-time") - .succeeds(); - assert!(re_full.is_match(result.stdout_str())); - let result = scene + .succeeds() + .stdout_matches(&re_full); + scene .ucmd() .arg("--full-time") .arg("--time-style=iso") - .succeeds(); - assert!(re_iso.is_match(result.stdout_str())); + .succeeds() + .stdout_matches(&re_iso); - let result = scene + scene .ucmd() .arg("--full-time") .arg("--time-style=iso") .arg("--full-time") - .succeeds(); - assert!(re_full.is_match(result.stdout_str())); + .succeeds() + .stdout_matches(&re_full); - let result = scene + scene .ucmd() .arg("--full-time") .arg("-x") .arg("-l") - .succeeds(); - assert!(re_full.is_match(result.stdout_str())); + .succeeds() + .stdout_matches(&re_full); at.touch("test2"); - let result = scene.ucmd().arg("--full-time").arg("-x").succeeds(); - assert_eq!(result.stdout_str(), "test test2\n"); + scene + .ucmd() + .arg("--full-time") + .arg("-x") + .succeeds() + .stdout_is("test test2\n"); } #[test] @@ -1782,9 +1817,15 @@ fn test_ls_order_time() { at.open("test-4").metadata().unwrap().accessed().unwrap(); // It seems to be dependent on the platform whether the access time is actually set - #[cfg(all(unix, not(target_os = "android")))] - result.stdout_only("test-3\ntest-4\ntest-2\ntest-1\n"); - #[cfg(any(windows, target_os = "android"))] + #[cfg(unix)] + { + let expected = unwrap_or_return!(expected_result(&scene, &["-t", arg])); + at.open("test-3").metadata().unwrap().accessed().unwrap(); + at.open("test-4").metadata().unwrap().accessed().unwrap(); + + result.stdout_only(expected.stdout_str()); + } + #[cfg(windows)] result.stdout_only("test-4\ntest-3\ntest-2\ntest-1\n"); } @@ -3378,3 +3419,54 @@ fn test_tabsize_formatting() { .succeeds() .stdout_is("aaaaaaaa bbbb\ncccc dddddddd"); } + +#[cfg(any( + target_os = "linux", + target_os = "macos", + target_os = "ios", + target_os = "freebsd", + target_os = "dragonfly", + target_os = "netbsd", + target_os = "openbsd", + target_os = "illumos", + target_os = "solaris" +))] +#[test] +fn test_device_number() { + use std::fs::{metadata, read_dir}; + use std::os::unix::fs::{FileTypeExt, MetadataExt}; + use uucore::libc::{dev_t, major, minor}; + + let dev_dir = read_dir("/dev").unwrap(); + // let's use the first device for test + let blk_dev = dev_dir + .map(|res_entry| res_entry.unwrap()) + .find(|entry| { + entry.file_type().unwrap().is_block_device() + || entry.file_type().unwrap().is_char_device() + }) + .expect("Expect a block/char device"); + let blk_dev_path = blk_dev.path(); + let blk_dev_meta = metadata(blk_dev_path.as_path()).unwrap(); + let blk_dev_number = blk_dev_meta.rdev() as dev_t; + let (major, minor) = unsafe { (major(blk_dev_number), minor(blk_dev_number)) }; + let major_minor_str = format!("{}, {}", major, minor); + + let scene = TestScenario::new(util_name!()); + scene + .ucmd() + .arg("-l") + .arg(blk_dev_path.to_str().expect("should be UTF-8 encoded")) + .succeeds() + .stdout_contains(major_minor_str); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_invalid_utf8() { + let (at, mut ucmd) = at_and_ucmd!(); + + let filename = OsStr::from_bytes(b"-\xE0-foo"); + at.touch(filename); + ucmd.succeeds(); +} diff --git a/tests/by-util/test_mkdir.rs b/tests/by-util/test_mkdir.rs index 2f09be6c20e..11a860d5a03 100644 --- a/tests/by-util/test_mkdir.rs +++ b/tests/by-util/test_mkdir.rs @@ -1,40 +1,35 @@ use crate::common::util::TestScenario; #[cfg(not(windows))] -use std::os::unix::fs::PermissionsExt; -#[cfg(not(windows))] -extern crate libc; -#[cfg(not(windows))] -use self::libc::{mode_t, umask}; - -static TEST_DIR1: &str = "mkdir_test1"; -static TEST_DIR2: &str = "mkdir_test2"; -static TEST_DIR3: &str = "mkdir_test3"; -static TEST_DIR4: &str = "mkdir_test4/mkdir_test4_1"; -static TEST_DIR5: &str = "mkdir_test5/mkdir_test5_1"; -static TEST_DIR6: &str = "mkdir_test6"; -static TEST_FILE7: &str = "mkdir_test7"; -static TEST_DIR8: &str = "mkdir_test8/mkdir_test8_1/mkdir_test8_2"; -static TEST_DIR9: &str = "mkdir_test9/../mkdir_test9_1/../mkdir_test9_2"; -static TEST_DIR10: &str = "mkdir_test10/."; -static TEST_DIR11: &str = "mkdir_test11/.."; +use libc::{mode_t, umask}; +use once_cell::sync::Lazy; #[cfg(not(windows))] -static TEST_DIR12: &str = "mkdir_test12"; +use std::os::unix::fs::PermissionsExt; +use std::sync::Mutex; + +// tests in `test_mkdir.rs` cannot run in parallel since some tests alter the umask. This may cause +// other tests to run under a wrong set of permissions +// +// when writing a test case, acquire this mutex before proceeding with the main logic of the test +static TEST_MUTEX: Lazy> = Lazy::new(|| Mutex::new(())); #[test] fn test_invalid_arg() { + let _guard = TEST_MUTEX.lock(); new_ucmd!().arg("--definitely-invalid").fails().code_is(1); } #[test] fn test_mkdir_mkdir() { - new_ucmd!().arg(TEST_DIR1).succeeds(); + let _guard = TEST_MUTEX.lock(); + new_ucmd!().arg("test_dir").succeeds(); } #[test] fn test_mkdir_verbose() { - let expected = "mkdir: created directory 'mkdir_test1'\n"; + let _guard = TEST_MUTEX.lock(); + let expected = "mkdir: created directory 'test_dir'\n"; new_ucmd!() - .arg(TEST_DIR1) + .arg("test_dir") .arg("-v") .run() .stdout_is(expected); @@ -42,124 +37,223 @@ fn test_mkdir_verbose() { #[test] fn test_mkdir_dup_dir() { + let _guard = TEST_MUTEX.lock(); + let scene = TestScenario::new(util_name!()); - scene.ucmd().arg(TEST_DIR2).succeeds(); - scene.ucmd().arg(TEST_DIR2).fails(); + let test_dir = "test_dir"; + + scene.ucmd().arg(test_dir).succeeds(); + scene.ucmd().arg(test_dir).fails(); } #[test] fn test_mkdir_mode() { - new_ucmd!().arg("-m").arg("755").arg(TEST_DIR3).succeeds(); + let _guard = TEST_MUTEX.lock(); + new_ucmd!().arg("-m").arg("755").arg("test_dir").succeeds(); } #[test] fn test_mkdir_parent() { + let _guard = TEST_MUTEX.lock(); let scene = TestScenario::new(util_name!()); - scene.ucmd().arg("-p").arg(TEST_DIR4).succeeds(); - scene.ucmd().arg("-p").arg(TEST_DIR4).succeeds(); - scene.ucmd().arg("--parent").arg(TEST_DIR4).succeeds(); - scene.ucmd().arg("--parents").arg(TEST_DIR4).succeeds(); + let test_dir = "parent_dir/child_dir"; + + scene.ucmd().arg("-p").arg(test_dir).succeeds(); + scene.ucmd().arg("-p").arg(test_dir).succeeds(); + scene.ucmd().arg("--parent").arg(test_dir).succeeds(); + scene.ucmd().arg("--parents").arg(test_dir).succeeds(); } #[test] fn test_mkdir_no_parent() { - new_ucmd!().arg(TEST_DIR5).fails(); + let _guard = TEST_MUTEX.lock(); + new_ucmd!().arg("parent_dir/child_dir").fails(); } #[test] fn test_mkdir_dup_dir_parent() { + let _guard = TEST_MUTEX.lock(); + let scene = TestScenario::new(util_name!()); - scene.ucmd().arg(TEST_DIR6).succeeds(); - scene.ucmd().arg("-p").arg(TEST_DIR6).succeeds(); + let test_dir = "test_dir"; + + scene.ucmd().arg(test_dir).succeeds(); + scene.ucmd().arg("-p").arg(test_dir).succeeds(); +} + +#[cfg(not(windows))] +#[test] +fn test_mkdir_parent_mode() { + let _guard = TEST_MUTEX.lock(); + let (at, mut ucmd) = at_and_ucmd!(); + + let default_umask: mode_t = 0o160; + let original_umask = unsafe { umask(default_umask) }; + + ucmd.arg("-p").arg("a/b").succeeds().no_stderr().no_stdout(); + + assert!(at.dir_exists("a")); + // parents created by -p have permissions set to "=rwx,u+wx" + assert_eq!( + at.metadata("a").permissions().mode() as mode_t, + ((!default_umask & 0o777) | 0o300) + 0o40000 + ); + assert!(at.dir_exists("a/b")); + // sub directory's permission is determined only by the umask + assert_eq!( + at.metadata("a/b").permissions().mode() as mode_t, + (!default_umask & 0o777) + 0o40000 + ); + + unsafe { + umask(original_umask); + } +} + +#[cfg(not(windows))] +#[test] +fn test_mkdir_parent_mode_check_existing_parent() { + let _guard = TEST_MUTEX.lock(); + let (at, mut ucmd) = at_and_ucmd!(); + + at.mkdir("a"); + + let default_umask: mode_t = 0o160; + let original_umask = unsafe { umask(default_umask) }; + + ucmd.arg("-p") + .arg("a/b/c") + .succeeds() + .no_stderr() + .no_stdout(); + + assert!(at.dir_exists("a")); + // parent dirs that already exist do not get their permissions modified + assert_eq!( + at.metadata("a").permissions().mode() as mode_t, + (!original_umask & 0o777) + 0o40000 + ); + assert!(at.dir_exists("a/b")); + assert_eq!( + at.metadata("a/b").permissions().mode() as mode_t, + ((!default_umask & 0o777) | 0o300) + 0o40000 + ); + assert!(at.dir_exists("a/b/c")); + assert_eq!( + at.metadata("a/b/c").permissions().mode() as mode_t, + (!default_umask & 0o777) + 0o40000 + ); + + unsafe { + umask(original_umask); + } } #[test] fn test_mkdir_dup_file() { + let _guard = TEST_MUTEX.lock(); + let scene = TestScenario::new(util_name!()); - scene.fixtures.touch(TEST_FILE7); - scene.ucmd().arg(TEST_FILE7).fails(); + let test_file = "test_file.txt"; + + scene.fixtures.touch(test_file); + + scene.ucmd().arg(test_file).fails(); // mkdir should fail for a file even if -p is specified. - scene.ucmd().arg("-p").arg(TEST_FILE7).fails(); + scene.ucmd().arg("-p").arg(test_file).fails(); } #[test] #[cfg(not(windows))] fn test_symbolic_mode() { + let _guard = TEST_MUTEX.lock(); let (at, mut ucmd) = at_and_ucmd!(); + let test_dir = "test_dir"; - ucmd.arg("-m").arg("a=rwx").arg(TEST_DIR1).succeeds(); - let perms = at.metadata(TEST_DIR1).permissions().mode(); + ucmd.arg("-m").arg("a=rwx").arg(test_dir).succeeds(); + let perms = at.metadata(test_dir).permissions().mode(); assert_eq!(perms, 0o40777); } #[test] #[cfg(not(windows))] fn test_symbolic_alteration() { + let _guard = TEST_MUTEX.lock(); let (at, mut ucmd) = at_and_ucmd!(); + let test_dir = "test_dir"; - ucmd.arg("-m").arg("-w").arg(TEST_DIR1).succeeds(); - let perms = at.metadata(TEST_DIR1).permissions().mode(); - assert_eq!(perms, 0o40555); + let default_umask = 0o022; + let original_umask = unsafe { umask(default_umask) }; + + ucmd.arg("-m").arg("-w").arg(test_dir).succeeds(); + let perms = at.metadata(test_dir).permissions().mode(); + assert_eq!(perms, 0o40577); + + unsafe { umask(original_umask) }; } #[test] #[cfg(not(windows))] fn test_multi_symbolic() { + let _guard = TEST_MUTEX.lock(); let (at, mut ucmd) = at_and_ucmd!(); + let test_dir = "test_dir"; - ucmd.arg("-m") - .arg("u=rwx,g=rx,o=") - .arg(TEST_DIR1) - .succeeds(); - let perms = at.metadata(TEST_DIR1).permissions().mode(); + ucmd.arg("-m").arg("u=rwx,g=rx,o=").arg(test_dir).succeeds(); + let perms = at.metadata(test_dir).permissions().mode(); assert_eq!(perms, 0o40750); } #[test] fn test_recursive_reporting() { + let _guard = TEST_MUTEX.lock(); + let test_dir = "test_dir/test_dir_a/test_dir_b"; + new_ucmd!() .arg("-p") .arg("-v") - .arg(TEST_DIR8) + .arg(test_dir) .succeeds() - .stdout_contains("created directory 'mkdir_test8'") - .stdout_contains("created directory 'mkdir_test8/mkdir_test8_1'") - .stdout_contains("created directory 'mkdir_test8/mkdir_test8_1/mkdir_test8_2'"); - new_ucmd!().arg("-v").arg(TEST_DIR8).fails().no_stdout(); + .stdout_contains("created directory 'test_dir'") + .stdout_contains("created directory 'test_dir/test_dir_a'") + .stdout_contains("created directory 'test_dir/test_dir_a/test_dir_b'"); + new_ucmd!().arg("-v").arg(test_dir).fails().no_stdout(); + + let test_dir = "test_dir/../test_dir_a/../test_dir_b"; + new_ucmd!() .arg("-p") .arg("-v") - .arg(TEST_DIR9) + .arg(test_dir) .succeeds() - .stdout_contains("created directory 'mkdir_test9'") - .stdout_contains("created directory 'mkdir_test9/../mkdir_test9_1'") - .stdout_contains("created directory 'mkdir_test9/../mkdir_test9_1/../mkdir_test9_2'"); + .stdout_contains("created directory 'test_dir'") + .stdout_contains("created directory 'test_dir/../test_dir_a'") + .stdout_contains("created directory 'test_dir/../test_dir_a/../test_dir_b'"); } #[test] fn test_mkdir_trailing_dot() { - let scene2 = TestScenario::new("ls"); - new_ucmd!() - .arg("-p") - .arg("-v") - .arg("mkdir_test10-2") - .succeeds(); + let _guard = TEST_MUTEX.lock(); + + new_ucmd!().arg("-p").arg("-v").arg("test_dir").succeeds(); new_ucmd!() .arg("-p") .arg("-v") - .arg(TEST_DIR10) + .arg("test_dir_a/.") .succeeds() - .stdout_contains("created directory 'mkdir_test10'"); + .stdout_contains("created directory 'test_dir_a'"); new_ucmd!() .arg("-p") .arg("-v") - .arg(TEST_DIR11) + .arg("test_dir_b/..") .succeeds() - .stdout_contains("created directory 'mkdir_test11'"); - let result = scene2.ucmd().arg("-al").run(); + .stdout_contains("created directory 'test_dir_b'"); + + let scene = TestScenario::new("ls"); + let result = scene.ucmd().arg("-al").run(); println!("ls dest {}", result.stdout_str()); } @@ -167,12 +261,15 @@ fn test_mkdir_trailing_dot() { #[cfg(not(windows))] fn test_umask_compliance() { fn test_single_case(umask_set: mode_t) { + let _guard = TEST_MUTEX.lock(); + + let test_dir = "test_dir"; let (at, mut ucmd) = at_and_ucmd!(); let original_umask = unsafe { umask(umask_set) }; - ucmd.arg(TEST_DIR12).succeeds(); - let perms = at.metadata(TEST_DIR12).permissions().mode() as mode_t; + ucmd.arg(test_dir).succeeds(); + let perms = at.metadata(test_dir).permissions().mode() as mode_t; assert_eq!(perms, (!umask_set & 0o0777) + 0o40000); // before compare, add the set GUID, UID bits unsafe { diff --git a/tests/by-util/test_mktemp.rs b/tests/by-util/test_mktemp.rs index e80f61a2842..6bcb37f4592 100644 --- a/tests/by-util/test_mktemp.rs +++ b/tests/by-util/test_mktemp.rs @@ -569,6 +569,32 @@ fn test_template_path_separator() { )); } +/// Test that a prefix with a point is valid. +#[test] +fn test_prefix_template_separator() { + new_ucmd!().args(&["-p", ".", "-t", "a.XXXX"]).succeeds(); +} + +#[test] +fn test_prefix_template_with_path_separator() { + #[cfg(not(windows))] + new_ucmd!() + .args(&["-t", "a/XXX"]) + .fails() + .stderr_only(format!( + "mktemp: invalid template, {}, contains directory separator\n", + "a/XXX".quote() + )); + #[cfg(windows)] + new_ucmd!() + .args(&["-t", r"a\XXX"]) + .fails() + .stderr_only(format!( + "mktemp: invalid template, {}, contains directory separator\n", + r"a\XXX".quote() + )); +} + /// Test that a suffix with a path separator is invalid. #[test] fn test_suffix_path_separator() { @@ -702,16 +728,18 @@ fn test_tmpdir_env_var() { assert_suffix_matches_template!("tmp.XXXXXXXXXX", filename); assert!(at.file_exists(filename)); - // FIXME This is not working because --tmpdir is configured to - // require a value. - // - // // `TMPDIR=. mktemp --tmpdir` - // let (at, mut ucmd) = at_and_ucmd!(); - // let result = ucmd.env(TMPDIR, ".").arg("--tmpdir").succeeds(); - // let filename = result.no_stderr().stdout_str().trim_end(); - // let template = format!(".{}tmp.XXXXXXXXXX", MAIN_SEPARATOR); - // assert_matches_template!(&template, filename); - // assert!(at.file_exists(filename)); + // `TMPDIR=. mktemp --tmpdir` + let (at, mut ucmd) = at_and_ucmd!(); + let result = ucmd.env(TMPDIR, ".").arg("--tmpdir").succeeds(); + let filename = result.no_stderr().stdout_str().trim_end(); + #[cfg(not(windows))] + { + let template = format!(".{MAIN_SEPARATOR}tmp.XXXXXXXXXX"); + assert_matches_template!(&template, filename); + } + #[cfg(windows)] + assert_suffix_matches_template!("tmp.XXXXXXXXXX", filename); + assert!(at.file_exists(filename)); // `TMPDIR=. mktemp --tmpdir XXX` let (at, mut ucmd) = at_and_ucmd!(); @@ -823,6 +851,53 @@ fn test_nonexistent_dir_prefix() { #[test] fn test_default_missing_value() { + new_ucmd!().arg("-d").arg("--tmpdir").succeeds(); +} + +#[test] +fn test_default_issue_4821_t_tmpdir() { let scene = TestScenario::new(util_name!()); - scene.ucmd().arg("-d").arg("--tmpdir").succeeds(); + let pathname = scene.fixtures.as_string(); + let result = scene + .ucmd() + .env(TMPDIR, &pathname) + .arg("-t") + .arg("foo.XXXX") + .succeeds(); + let stdout = result.stdout_str(); + println!("stdout = {stdout}"); + assert!(stdout.contains(&pathname)); +} + +#[test] +fn test_default_issue_4821_t_tmpdir_p() { + let scene = TestScenario::new(util_name!()); + let pathname = scene.fixtures.as_string(); + let result = scene + .ucmd() + .arg("-t") + .arg("-p") + .arg(&pathname) + .arg("foo.XXXX") + .succeeds(); + let stdout = result.stdout_str(); + println!("stdout = {stdout}"); + assert!(stdout.contains(&pathname)); +} + +#[test] +fn test_t_ensure_tmpdir_has_higher_priority_than_p() { + let scene = TestScenario::new(util_name!()); + let pathname = scene.fixtures.as_string(); + let result = scene + .ucmd() + .env(TMPDIR, &pathname) + .arg("-t") + .arg("-p") + .arg("should_not_attempt_to_write_in_this_nonexisting_dir") + .arg("foo.XXXX") + .succeeds(); + let stdout = result.stdout_str(); + println!("stdout = {stdout}"); + assert!(stdout.contains(&pathname)); } diff --git a/tests/by-util/test_more.rs b/tests/by-util/test_more.rs index bdf80a27c10..95a4818b529 100644 --- a/tests/by-util/test_more.rs +++ b/tests/by-util/test_more.rs @@ -6,7 +6,34 @@ fn test_more_no_arg() { // Reading from stdin is now supported, so this must succeed if std::io::stdout().is_terminal() { new_ucmd!().succeeds(); - } else { + } +} + +#[test] +fn test_valid_arg() { + if std::io::stdout().is_terminal() { + new_ucmd!().arg("-c").succeeds(); + new_ucmd!().arg("--print-over").succeeds(); + + new_ucmd!().arg("-p").succeeds(); + new_ucmd!().arg("--clean-print").succeeds(); + + new_ucmd!().arg("-s").succeeds(); + new_ucmd!().arg("--squeeze").succeeds(); + + new_ucmd!().arg("-n").arg("10").succeeds(); + new_ucmd!().arg("--lines").arg("0").succeeds(); + new_ucmd!().arg("--number").arg("0").succeeds(); + } +} + +#[test] +fn test_invalid_arg() { + if std::io::stdout().is_terminal() { + new_ucmd!().arg("--invalid").fails(); + + new_ucmd!().arg("--lines").arg("-10").fails(); + new_ucmd!().arg("--number").arg("-10").fails(); } } @@ -20,6 +47,23 @@ fn test_more_dir_arg() { .arg(".") .fails() .usage_error("'.' is a directory."); - } else { + } +} + +#[test] +#[cfg(target_family = "unix")] +fn test_more_invalid_file_perms() { + use std::fs::{set_permissions, Permissions}; + use std::os::unix::fs::PermissionsExt; + + if std::io::stdout().is_terminal() { + let (at, mut ucmd) = at_and_ucmd!(); + let permissions = Permissions::from_mode(0o244); + at.make_file("invalid-perms.txt"); + set_permissions(at.plus("invalid-perms.txt"), permissions).unwrap(); + ucmd.arg("invalid-perms.txt") + .fails() + .code_is(1) + .stderr_contains("permission denied"); } } diff --git a/tests/by-util/test_mv.rs b/tests/by-util/test_mv.rs index 54bf53002cc..0c292c50d85 100644 --- a/tests/by-util/test_mv.rs +++ b/tests/by-util/test_mv.rs @@ -1,8 +1,7 @@ -extern crate filetime; -extern crate time; - -use self::filetime::FileTime; use crate::common::util::TestScenario; +use filetime::FileTime; +use std::thread::sleep; +use std::time::Duration; #[test] fn test_invalid_arg() { @@ -58,6 +57,63 @@ fn test_mv_move_file_into_dir() { assert!(at.file_exists(format!("{dir}/{file}"))); } +#[test] +fn test_mv_move_file_into_dir_with_target_arg() { + let (at, mut ucmd) = at_and_ucmd!(); + let dir = "test_mv_move_file_into_dir_with_target_arg_dir"; + let file = "test_mv_move_file_into_dir_with_target_arg_file"; + + at.mkdir(dir); + at.touch(file); + + ucmd.arg("--target") + .arg(dir) + .arg(file) + .succeeds() + .no_stderr(); + + assert!(at.file_exists(format!("{dir}/{file}"))); +} + +#[test] +fn test_mv_move_file_into_file_with_target_arg() { + let (at, mut ucmd) = at_and_ucmd!(); + let file1 = "test_mv_move_file_into_file_with_target_arg_file1"; + let file2 = "test_mv_move_file_into_file_with_target_arg_file2"; + + at.touch(file1); + at.touch(file2); + + ucmd.arg("--target") + .arg(file1) + .arg(file2) + .fails() + .stderr_is(format!("mv: target directory '{file1}': Not a directory\n")); + + assert!(at.file_exists(file1)); +} + +#[test] +fn test_mv_move_multiple_files_into_file() { + let (at, mut ucmd) = at_and_ucmd!(); + let file1 = "test_mv_move_multiple_files_into_file1"; + let file2 = "test_mv_move_multiple_files_into_file2"; + let file3 = "test_mv_move_multiple_files_into_file3"; + + at.touch(file1); + at.touch(file2); + at.touch(file3); + + ucmd.arg(file1) + .arg(file2) + .arg(file3) + .fails() + .stderr_is(format!("mv: target '{file3}': Not a directory\n")); + + assert!(at.file_exists(file1)); + assert!(at.file_exists(file2)); +} + #[test] fn test_mv_move_file_between_dirs() { let (at, mut ucmd) = at_and_ucmd!(); @@ -184,6 +240,81 @@ fn test_mv_interactive() { assert!(at.file_exists(file_b)); } +#[test] +fn test_mv_interactive_with_dir_as_target() { + let (at, mut ucmd) = at_and_ucmd!(); + + let file = "test_mv_interactive_file"; + let target_dir = "target"; + + at.mkdir(target_dir); + at.touch(file); + at.touch(format!("{target_dir}/{file}")); + + ucmd.arg(file) + .arg(target_dir) + .arg("-i") + .pipe_in("n") + .fails() + .stderr_does_not_contain("cannot move") + .no_stdout(); +} + +#[test] +fn test_mv_interactive_dir_to_file_not_affirmative() { + let (at, mut ucmd) = at_and_ucmd!(); + + let dir = "test_mv_interactive_dir_to_file_not_affirmative_dir"; + let file = "test_mv_interactive_dir_to_file_not_affirmative_file"; + + at.mkdir(dir); + at.touch(file); + + ucmd.arg(dir) + .arg(file) + .arg("-i") + .pipe_in("n") + .fails() + .no_stdout(); + + assert!(at.dir_exists(dir)); +} + +#[test] +fn test_mv_interactive_no_clobber_force_last_arg_wins() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let file_a = "a.txt"; + let file_b = "b.txt"; + + at.touch(file_a); + at.touch(file_b); + + scene + .ucmd() + .args(&[file_a, file_b, "-f", "-i", "-n"]) + .fails() + .stderr_is(format!("mv: not replacing '{file_b}'\n")); + + scene + .ucmd() + .args(&[file_a, file_b, "-n", "-f", "-i"]) + .fails() + .stderr_is(format!("mv: overwrite '{file_b}'? ")); + + at.write(file_a, "aa"); + + scene + .ucmd() + .args(&[file_a, file_b, "-i", "-n", "-f"]) + .succeeds() + .no_output(); + + assert!(!at.file_exists(file_a)); + assert_eq!("aa", at.read(file_b)); +} + #[test] fn test_mv_arg_update_interactive() { let (at, mut ucmd) = at_and_ucmd!(); @@ -215,8 +346,9 @@ fn test_mv_no_clobber() { ucmd.arg("-n") .arg(file_a) .arg(file_b) - .succeeds() - .no_stderr(); + .fails() + .code_is(1) + .stderr_only(format!("mv: not replacing '{file_b}'\n")); assert!(at.file_exists(file_a)); assert!(at.file_exists(file_b)); @@ -268,6 +400,116 @@ fn test_mv_same_file() { .stderr_is(format!("mv: '{file_a}' and '{file_a}' are the same file\n",)); } +#[test] +#[cfg(all(unix, not(target_os = "android")))] +fn test_mv_same_hardlink() { + let (at, mut ucmd) = at_and_ucmd!(); + let file_a = "test_mv_same_file_a"; + let file_b = "test_mv_same_file_b"; + at.touch(file_a); + + at.hard_link(file_a, file_b); + + at.touch(file_a); + ucmd.arg(file_a) + .arg(file_b) + .fails() + .stderr_is(format!("mv: '{file_a}' and '{file_b}' are the same file\n",)); +} + +#[test] +#[cfg(all(unix, not(target_os = "android")))] +fn test_mv_same_symlink() { + let (at, mut ucmd) = at_and_ucmd!(); + let file_a = "test_mv_same_file_a"; + let file_b = "test_mv_same_file_b"; + let file_c = "test_mv_same_file_c"; + + at.touch(file_a); + + at.symlink_file(file_a, file_b); + + ucmd.arg(file_b) + .arg(file_a) + .fails() + .stderr_is(format!("mv: '{file_b}' and '{file_a}' are the same file\n",)); + + let (at2, mut ucmd2) = at_and_ucmd!(); + at2.touch(file_a); + + at2.symlink_file(file_a, file_b); + ucmd2.arg(file_a).arg(file_b).succeeds(); + assert!(at2.file_exists(file_b)); + assert!(!at2.file_exists(file_a)); + + let (at3, mut ucmd3) = at_and_ucmd!(); + at3.touch(file_a); + + at3.symlink_file(file_a, file_b); + at3.symlink_file(file_b, file_c); + + ucmd3.arg(file_c).arg(file_b).succeeds(); + assert!(!at3.symlink_exists(file_c)); + assert!(at3.symlink_exists(file_b)); + + let (at4, mut ucmd4) = at_and_ucmd!(); + at4.touch(file_a); + + at4.symlink_file(file_a, file_b); + at4.symlink_file(file_b, file_c); + + ucmd4 + .arg(file_c) + .arg(file_a) + .fails() + .stderr_is(format!("mv: '{file_c}' and '{file_a}' are the same file\n",)); +} + +#[test] +#[cfg(all(unix, not(target_os = "android")))] +fn test_mv_hardlink_to_symlink() { + let (at, mut ucmd) = at_and_ucmd!(); + let file = "file"; + let symlink_file = "symlink"; + let hardlink_to_symlink_file = "hardlink_to_symlink"; + + at.touch(file); + at.symlink_file(file, symlink_file); + at.hard_link(symlink_file, hardlink_to_symlink_file); + + ucmd.arg(symlink_file).arg(hardlink_to_symlink_file).fails(); + + let (at2, mut ucmd2) = at_and_ucmd!(); + + at2.touch(file); + at2.symlink_file(file, symlink_file); + at2.hard_link(symlink_file, hardlink_to_symlink_file); + + ucmd2 + .arg("--backup") + .arg(symlink_file) + .arg(hardlink_to_symlink_file) + .succeeds(); + assert!(!at2.symlink_exists(symlink_file)); + assert!(at2.symlink_exists(&format!("{hardlink_to_symlink_file}~"))); +} + +#[test] +#[cfg(all(unix, not(target_os = "android")))] +fn test_mv_same_hardlink_backup_simple() { + let (at, mut ucmd) = at_and_ucmd!(); + let file_a = "test_mv_same_file_a"; + let file_b = "test_mv_same_file_b"; + at.touch(file_a); + + at.hard_link(file_a, file_b); + + ucmd.arg(file_a) + .arg(file_b) + .arg("--backup=simple") + .succeeds(); +} + #[test] fn test_mv_same_file_not_dot_dir() { let (at, mut ucmd) = at_and_ucmd!(); @@ -641,6 +883,208 @@ fn test_mv_update_option() { assert!(!at.file_exists(file_b)); } +#[test] +fn test_mv_arg_update_none() { + let (at, mut ucmd) = at_and_ucmd!(); + + let file1 = "test_mv_arg_update_none_file1"; + let file2 = "test_mv_arg_update_none_file2"; + let file1_content = "file1 content\n"; + let file2_content = "file2 content\n"; + + at.write(file1, file1_content); + at.write(file2, file2_content); + + ucmd.arg(file1) + .arg(file2) + .arg("--update=none") + .succeeds() + .no_stderr() + .no_stdout(); + + assert_eq!(at.read(file2), file2_content); +} + +#[test] +fn test_mv_arg_update_all() { + let (at, mut ucmd) = at_and_ucmd!(); + + let file1 = "test_mv_arg_update_none_file1"; + let file2 = "test_mv_arg_update_none_file2"; + let file1_content = "file1 content\n"; + let file2_content = "file2 content\n"; + + at.write(file1, file1_content); + at.write(file2, file2_content); + + ucmd.arg(file1) + .arg(file2) + .arg("--update=all") + .succeeds() + .no_stderr() + .no_stdout(); + + assert_eq!(at.read(file2), file1_content); +} + +#[test] +fn test_mv_arg_update_older_dest_not_older() { + let (at, mut ucmd) = at_and_ucmd!(); + + let old = "test_mv_arg_update_none_file1"; + let new = "test_mv_arg_update_none_file2"; + let old_content = "file1 content\n"; + let new_content = "file2 content\n"; + + at.write(old, old_content); + + sleep(Duration::from_secs(1)); + + at.write(new, new_content); + + ucmd.arg(old) + .arg(new) + .arg("--update=older") + .succeeds() + .no_stderr() + .no_stdout(); + + assert_eq!(at.read(new), new_content); +} + +#[test] +fn test_mv_arg_update_none_then_all() { + // take last if multiple update args are supplied, + // update=all wins in this case + let (at, mut ucmd) = at_and_ucmd!(); + + let old = "test_mv_arg_update_none_then_all_file1"; + let new = "test_mv_arg_update_none_then_all_file2"; + let old_content = "old content\n"; + let new_content = "new content\n"; + + at.write(old, old_content); + + sleep(Duration::from_secs(1)); + + at.write(new, new_content); + + ucmd.arg(old) + .arg(new) + .arg("--update=none") + .arg("--update=all") + .succeeds() + .no_stderr() + .no_stdout(); + + assert_eq!(at.read(new), "old content\n"); +} + +#[test] +fn test_mv_arg_update_all_then_none() { + // take last if multiple update args are supplied, + // update=none wins in this case + let (at, mut ucmd) = at_and_ucmd!(); + + let old = "test_mv_arg_update_all_then_none_file1"; + let new = "test_mv_arg_update_all_then_none_file2"; + let old_content = "old content\n"; + let new_content = "new content\n"; + + at.write(old, old_content); + + sleep(Duration::from_secs(1)); + + at.write(new, new_content); + + ucmd.arg(old) + .arg(new) + .arg("--update=all") + .arg("--update=none") + .succeeds() + .no_stderr() + .no_stdout(); + + assert_eq!(at.read(new), "new content\n"); +} + +#[test] +fn test_mv_arg_update_older_dest_older() { + let (at, mut ucmd) = at_and_ucmd!(); + + let old = "test_mv_arg_update_none_file1"; + let new = "test_mv_arg_update_none_file2"; + let old_content = "file1 content\n"; + let new_content = "file2 content\n"; + + at.write(old, old_content); + + sleep(Duration::from_secs(1)); + + at.write(new, new_content); + + ucmd.arg(new) + .arg(old) + .arg("--update=all") + .succeeds() + .no_stderr() + .no_stdout(); + + assert_eq!(at.read(old), new_content); +} + +#[test] +fn test_mv_arg_update_short_overwrite() { + // same as --update=older + let (at, mut ucmd) = at_and_ucmd!(); + + let old = "test_mv_arg_update_none_file1"; + let new = "test_mv_arg_update_none_file2"; + let old_content = "file1 content\n"; + let new_content = "file2 content\n"; + + at.write(old, old_content); + + sleep(Duration::from_secs(1)); + + at.write(new, new_content); + + ucmd.arg(new) + .arg(old) + .arg("-u") + .succeeds() + .no_stderr() + .no_stdout(); + + assert_eq!(at.read(old), new_content); +} + +#[test] +fn test_mv_arg_update_short_no_overwrite() { + // same as --update=older + let (at, mut ucmd) = at_and_ucmd!(); + + let old = "test_mv_arg_update_none_file1"; + let new = "test_mv_arg_update_none_file2"; + let old_content = "file1 content\n"; + let new_content = "file2 content\n"; + + at.write(old, old_content); + + sleep(Duration::from_secs(1)); + + at.write(new, new_content); + + ucmd.arg(old) + .arg(new) + .arg("-u") + .succeeds() + .no_stderr() + .no_stdout(); + + assert_eq!(at.read(new), new_content); +} + #[test] fn test_mv_target_dir() { let (at, mut ucmd) = at_and_ucmd!(); @@ -728,7 +1172,9 @@ fn test_mv_backup_dir() { .arg(dir_a) .arg(dir_b) .succeeds() - .stdout_only(format!("'{dir_a}' -> '{dir_b}' (backup: '{dir_b}~')\n")); + .stdout_only(format!( + "renamed '{dir_a}' -> '{dir_b}' (backup: '{dir_b}~')\n" + )); assert!(!at.dir_exists(dir_a)); assert!(at.dir_exists(dir_b)); @@ -800,7 +1246,7 @@ fn test_mv_verbose() { .arg(file_a) .arg(file_b) .succeeds() - .stdout_only(format!("'{file_a}' -> '{file_b}'\n")); + .stdout_only(format!("renamed '{file_a}' -> '{file_b}'\n")); at.touch(file_a); scene @@ -809,7 +1255,9 @@ fn test_mv_verbose() { .arg(file_a) .arg(file_b) .succeeds() - .stdout_only(format!("'{file_a}' -> '{file_b}' (backup: '{file_b}~')\n")); + .stdout_only(format!( + "renamed '{file_a}' -> '{file_b}' (backup: '{file_b}~')\n" + )); } #[test] @@ -872,6 +1320,29 @@ fn test_mv_info_self() { .stderr_contains("mv: cannot move 'dir2' to a subdirectory of itself, 'dir2/dir2'"); } +#[test] +fn test_mv_arg_interactive_skipped() { + let (at, mut ucmd) = at_and_ucmd!(); + at.touch("a"); + at.touch("b"); + ucmd.args(&["-vi", "a", "b"]) + .pipe_in("N\n") + .ignore_stdin_write_error() + .fails() + .stderr_is("mv: overwrite 'b'? ") + .stdout_is("skipped 'b'\n"); +} + +#[test] +fn test_mv_arg_interactive_skipped_vin() { + let (at, mut ucmd) = at_and_ucmd!(); + at.touch("a"); + at.touch("b"); + ucmd.args(&["-vin", "a", "b"]) + .fails() + .stdout_is("skipped 'b'\n"); +} + #[test] fn test_mv_into_self_data() { let scene = TestScenario::new(util_name!()); diff --git a/tests/by-util/test_nice.rs b/tests/by-util/test_nice.rs index e2af88dfd92..4e8d5a2ee35 100644 --- a/tests/by-util/test_nice.rs +++ b/tests/by-util/test_nice.rs @@ -1,11 +1,14 @@ +// spell-checker:ignore libc's use crate::common::util::TestScenario; #[test] #[cfg(not(target_os = "android"))] fn test_get_current_niceness() { - // NOTE: this assumes the test suite is being run with a default niceness - // of 0, which may not necessarily be true - new_ucmd!().run().stdout_is("0\n"); + // Test that the nice command with no arguments returns the default nice + // value, which we determine by querying libc's `nice` in our own process. + new_ucmd!() + .run() + .stdout_is(format!("{}\n", unsafe { libc::nice(0) })); } #[test] diff --git a/tests/by-util/test_od.rs b/tests/by-util/test_od.rs index da30a5d971e..54ac06384a8 100644 --- a/tests/by-util/test_od.rs +++ b/tests/by-util/test_od.rs @@ -4,15 +4,13 @@ // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -extern crate unindent; - -use self::unindent::unindent; use crate::common::util::TestScenario; use std::env; use std::fs::remove_file; use std::fs::File; use std::io::Write; use std::path::Path; +use unindent::unindent; // octal dump of 'abcdefghijklmnopqrstuvwxyz\n' // spell-checker:disable-line static ALPHA_OUT: &str = " @@ -629,6 +627,35 @@ fn test_skip_bytes() { )); } +#[test] +fn test_skip_bytes_hex() { + let input = "abcdefghijklmnopq"; // spell-checker:disable-line + new_ucmd!() + .arg("-c") + .arg("--skip-bytes=0xB") + .run_piped_stdin(input.as_bytes()) + .no_stderr() + .success() + .stdout_is(unindent( + " + 0000013 l m n o p q + 0000021 + ", + )); + new_ucmd!() + .arg("-c") + .arg("--skip-bytes=0xE") + .run_piped_stdin(input.as_bytes()) + .no_stderr() + .success() + .stdout_is(unindent( + " + 0000016 o p q + 0000021 + ", + )); +} + #[test] fn test_skip_bytes_error() { let input = "12345"; diff --git a/tests/by-util/test_paste.rs b/tests/by-util/test_paste.rs index 2e119846edf..da92daa32f1 100644 --- a/tests/by-util/test_paste.rs +++ b/tests/by-util/test_paste.rs @@ -156,6 +156,39 @@ fn test_multi_stdin() { } } +#[test] +// TODO: make this test work on Windows +#[cfg(not(windows))] +fn test_delimiter_list_ending_with_escaped_backslash() { + for d in ["-d", "--delimiters"] { + let (at, mut ucmd) = at_and_ucmd!(); + let mut ins = vec![]; + for (i, _in) in ["a\n", "b\n"].iter().enumerate() { + let file = format!("in{}", i); + at.write(&file, _in); + ins.push(file); + } + ucmd.args(&[d, "\\\\"]) + .args(&ins) + .succeeds() + .stdout_is("a\\b\n"); + } +} + +#[test] +fn test_delimiter_list_ending_with_unescaped_backslash() { + for d in ["-d", "--delimiters"] { + new_ucmd!() + .args(&[d, "\\"]) + .fails() + .stderr_contains("delimiter list ends with an unescaped backslash: \\"); + new_ucmd!() + .args(&[d, "_\\"]) + .fails() + .stderr_contains("delimiter list ends with an unescaped backslash: _\\"); + } +} + #[test] fn test_data() { for example in EXAMPLE_DATA { diff --git a/tests/by-util/test_pinky.rs b/tests/by-util/test_pinky.rs index ba142c905ce..f266175f524 100644 --- a/tests/by-util/test_pinky.rs +++ b/tests/by-util/test_pinky.rs @@ -3,14 +3,9 @@ // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -extern crate uucore; - use crate::common::util::{expected_result, TestScenario}; - -use self::uucore::entries::{Locate, Passwd}; - -extern crate pinky; -pub use self::pinky::*; +use pinky::Capitalize; +use uucore::entries::{Locate, Passwd}; #[test] fn test_invalid_arg() { diff --git a/tests/by-util/test_pr.rs b/tests/by-util/test_pr.rs index 66f4f130921..b62fa4a9683 100644 --- a/tests/by-util/test_pr.rs +++ b/tests/by-util/test_pr.rs @@ -1,5 +1,4 @@ // spell-checker:ignore (ToDO) Sdivide -extern crate time; use crate::common::util::{TestScenario, UCommand}; use std::fs::metadata; diff --git a/tests/by-util/test_pwd.rs b/tests/by-util/test_pwd.rs index 5719e87d24b..1e43f5be6ba 100644 --- a/tests/by-util/test_pwd.rs +++ b/tests/by-util/test_pwd.rs @@ -27,7 +27,7 @@ fn test_deleted_dir() { use std::process::Command; let ts = TestScenario::new(util_name!()); - let at = ts.fixtures.clone(); + let at = ts.fixtures; let output = Command::new("sh") .arg("-c") .arg(format!( @@ -116,7 +116,7 @@ fn test_symlinked_default_posix_p() { .env("POSIXLY_CORRECT", "1") .arg("-P") .succeeds() - .stdout_is(env.symdir + "\n"); + .stdout_is(env.subdir + "\n"); } #[cfg(not(windows))] diff --git a/tests/by-util/test_rm.rs b/tests/by-util/test_rm.rs index 3a2ee0b44db..737c4fa79b0 100644 --- a/tests/by-util/test_rm.rs +++ b/tests/by-util/test_rm.rs @@ -54,6 +54,8 @@ fn test_rm_interactive() { at.touch(file_a); at.touch(file_b); + assert!(at.file_exists(file_a)); + assert!(at.file_exists(file_b)); scene .ucmd() @@ -84,6 +86,11 @@ fn test_rm_force() { let file_a = "test_rm_force_a"; let file_b = "test_rm_force_b"; + at.touch(file_a); + at.touch(file_b); + assert!(at.file_exists(file_a)); + assert!(at.file_exists(file_b)); + ucmd.arg("-f") .arg(file_a) .arg(file_b) @@ -100,6 +107,11 @@ fn test_rm_force_multiple() { let file_a = "test_rm_force_a"; let file_b = "test_rm_force_b"; + at.touch(file_a); + at.touch(file_b); + assert!(at.file_exists(file_a)); + assert!(at.file_exists(file_b)); + ucmd.arg("-f") .arg("-f") .arg("-f") @@ -349,6 +361,74 @@ fn test_rm_interactive_never() { assert!(!at.file_exists(file_2)); } +#[test] +fn test_rm_interactive_missing_value() { + // `--interactive` is equivalent to `--interactive=always` or `-i` + let (at, mut ucmd) = at_and_ucmd!(); + + let file1 = "test_rm_interactive_missing_value_file1"; + let file2 = "test_rm_interactive_missing_value_file2"; + + at.touch(file1); + at.touch(file2); + + ucmd.arg("--interactive") + .arg(file1) + .arg(file2) + .pipe_in("y\ny") + .succeeds(); + + assert!(!at.file_exists(file1)); + assert!(!at.file_exists(file2)); +} + +#[test] +fn test_rm_interactive_once_prompt() { + let (at, mut ucmd) = at_and_ucmd!(); + + let file1 = "test_rm_interactive_once_recursive_prompt_file1"; + let file2 = "test_rm_interactive_once_recursive_prompt_file2"; + let file3 = "test_rm_interactive_once_recursive_prompt_file3"; + let file4 = "test_rm_interactive_once_recursive_prompt_file4"; + + at.touch(file1); + at.touch(file2); + at.touch(file3); + at.touch(file4); + + ucmd.arg("--interactive=once") + .arg(file1) + .arg(file2) + .arg(file3) + .arg(file4) + .pipe_in("y") + .succeeds() + .stderr_contains("remove 4 arguments?"); + + assert!(!at.file_exists(file1)); + assert!(!at.file_exists(file2)); + assert!(!at.file_exists(file3)); + assert!(!at.file_exists(file4)); +} + +#[test] +fn test_rm_interactive_once_recursive_prompt() { + let (at, mut ucmd) = at_and_ucmd!(); + + let file1 = "test_rm_interactive_once_recursive_prompt_file1"; + + at.touch(file1); + + ucmd.arg("--interactive=once") + .arg("-r") + .arg(file1) + .pipe_in("y") + .succeeds() + .stderr_contains("remove 1 argument recursively?"); + + assert!(!at.file_exists(file1)); +} + #[test] fn test_rm_descend_directory() { // This test descends into each directory and deletes the files and folders inside of them @@ -578,3 +658,24 @@ fn test_fifo_removal() { .timeout(Duration::from_secs(2)) .succeeds(); } + +#[test] +#[cfg(any(unix, target_os = "wasi"))] +#[cfg(not(target_os = "macos"))] +fn test_non_utf8() { + use std::ffi::OsStr; + #[cfg(unix)] + use std::os::unix::ffi::OsStrExt; + #[cfg(target_os = "wasi")] + use std::os::wasi::ffi::OsStrExt; + + let file = OsStr::from_bytes(b"not\xffutf8"); // spell-checker:disable-line + + let (at, mut ucmd) = at_and_ucmd!(); + + at.touch(file); + assert!(at.file_exists(file)); + + ucmd.arg(file).succeeds(); + assert!(!at.file_exists(file)); +} diff --git a/tests/by-util/test_shred.rs b/tests/by-util/test_shred.rs index 58db09cbd65..d98b840c47f 100644 --- a/tests/by-util/test_shred.rs +++ b/tests/by-util/test_shred.rs @@ -18,7 +18,7 @@ fn test_shred_remove() { at.touch(file_b); // Shred file_a. - scene.ucmd().arg("-u").arg(file_a).run(); + scene.ucmd().arg("-u").arg(file_a).succeeds(); // file_a was deleted, file_b exists. assert!(!at.file_exists(file_a)); @@ -51,3 +51,14 @@ fn test_shred_force() { // file_a was deleted. assert!(!at.file_exists(file)); } + +#[test] +fn test_hex() { + let (at, mut ucmd) = at_and_ucmd!(); + + let file = "test_hex"; + + at.touch(file); + + ucmd.arg("--size=0x10").arg(file).succeeds(); +} diff --git a/tests/by-util/test_sleep.rs b/tests/by-util/test_sleep.rs index 06a22deae07..13aa882b9ba 100644 --- a/tests/by-util/test_sleep.rs +++ b/tests/by-util/test_sleep.rs @@ -10,7 +10,7 @@ fn test_invalid_time_interval() { new_ucmd!() .arg("xyz") .fails() - .usage_error("invalid time interval 'xyz': Invalid character: 'x' at position 1"); + .usage_error("invalid time interval 'xyz': Invalid input: 'xyz' at position 1"); new_ucmd!() .args(&["--", "-1"]) .fails() @@ -211,7 +211,7 @@ fn test_sleep_when_input_has_only_whitespace_then_error(#[case] input: &str) { #[test] fn test_sleep_when_multiple_input_some_with_error_then_shows_all_errors() { - let expected = "invalid time interval 'abc': Invalid character: 'a' at position 1\n\ + let expected = "invalid time interval 'abc': Invalid input: 'abc' at position 1\n\ sleep: invalid time interval '1years': Invalid time unit: 'years' at position 2\n\ sleep: invalid time interval ' ': Found only whitespace in input"; diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index a61db56b0f3..e66a405abbb 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -915,6 +915,7 @@ fn test_compress_merge() { } #[test] +#[cfg(not(target_os = "android"))] fn test_compress_fail() { #[cfg(not(windows))] TestScenario::new(util_name!()) diff --git a/tests/by-util/test_split.rs b/tests/by-util/test_split.rs index 5ed96ed3518..1395a4fa28b 100644 --- a/tests/by-util/test_split.rs +++ b/tests/by-util/test_split.rs @@ -3,13 +3,10 @@ // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. // spell-checker:ignore xzaaa sixhundredfiftyonebytes ninetyonebytes threebytes asciilowercase fghij klmno pqrst uvwxyz fivelines twohundredfortyonebytes onehundredlines nbbbb -extern crate rand; -extern crate regex; -use self::rand::{thread_rng, Rng}; -use self::regex::Regex; use crate::common::util::{AtPath, TestScenario}; -use rand::SeedableRng; +use rand::{thread_rng, Rng, SeedableRng}; +use regex::Regex; #[cfg(not(windows))] use std::env; use std::path::Path; diff --git a/tests/by-util/test_stat.rs b/tests/by-util/test_stat.rs index 6dbe940f836..2527dc7cddc 100644 --- a/tests/by-util/test_stat.rs +++ b/tests/by-util/test_stat.rs @@ -3,8 +3,6 @@ // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -extern crate regex; - use crate::common::util::{expected_result, TestScenario}; #[test] diff --git a/tests/by-util/test_sync.rs b/tests/by-util/test_sync.rs index 961b2d2163a..9cae3b8a0cf 100644 --- a/tests/by-util/test_sync.rs +++ b/tests/by-util/test_sync.rs @@ -1,5 +1,4 @@ use crate::common::util::TestScenario; -extern crate tempfile; use std::fs; use tempfile::tempdir; @@ -42,7 +41,7 @@ fn test_sync_no_existing_files() { .arg("--data") .arg("do-no-exist") .fails() - .stderr_contains("cannot stat"); + .stderr_contains("error opening"); } #[test] @@ -64,9 +63,9 @@ fn test_sync_no_permission_dir() { ts.ccmd("chmod").arg("0").arg(dir).succeeds(); let result = ts.ucmd().arg("--data").arg(dir).fails(); - result.stderr_contains("sync: cannot stat 'foo': Permission denied"); + result.stderr_contains("sync: error opening 'foo': Permission denied"); let result = ts.ucmd().arg(dir).fails(); - result.stderr_contains("sync: cannot stat 'foo': Permission denied"); + result.stderr_contains("sync: error opening 'foo': Permission denied"); } #[cfg(not(target_os = "windows"))] diff --git a/tests/by-util/test_tail.rs b/tests/by-util/test_tail.rs index a8039cef665..b7740f6140e 100644 --- a/tests/by-util/test_tail.rs +++ b/tests/by-util/test_tail.rs @@ -7,8 +7,6 @@ // spell-checker:ignore (libs) kqueue // spell-checker:ignore (jargon) tailable untailable datasame runneradmin tmpi -extern crate tail; - use crate::common::random::{AlphanumericNewline, RandomString}; #[cfg(unix)] use crate::common::util::expected_result; @@ -38,15 +36,15 @@ use tail::chunks::BUFFER_SIZE as CHUNK_BUFFER_SIZE; ))] use tail::text; -static FOOBAR_TXT: &str = "foobar.txt"; -static FOOBAR_2_TXT: &str = "foobar2.txt"; -static FOOBAR_WITH_NULL_TXT: &str = "foobar_with_null.txt"; +const FOOBAR_TXT: &str = "foobar.txt"; +const FOOBAR_2_TXT: &str = "foobar2.txt"; +const FOOBAR_WITH_NULL_TXT: &str = "foobar_with_null.txt"; #[allow(dead_code)] -static FOLLOW_NAME_TXT: &str = "follow_name.txt"; +const FOLLOW_NAME_TXT: &str = "follow_name.txt"; #[allow(dead_code)] -static FOLLOW_NAME_SHORT_EXP: &str = "follow_name_short.expected"; +const FOLLOW_NAME_SHORT_EXP: &str = "follow_name_short.expected"; #[allow(dead_code)] -static FOLLOW_NAME_EXP: &str = "follow_name.expected"; +const FOLLOW_NAME_EXP: &str = "follow_name.expected"; #[cfg(not(windows))] const DEFAULT_SLEEP_INTERVAL_MILLIS: u64 = 1000; @@ -1538,6 +1536,7 @@ fn test_retry8() { #[test] #[cfg(all( not(target_vendor = "apple"), + not(target_os = "android"), not(target_os = "windows"), not(target_os = "freebsd") ))] // FIXME: for currently not working platforms @@ -1618,6 +1617,7 @@ fn test_retry9() { #[test] #[cfg(all( not(target_vendor = "apple"), + not(target_os = "android"), not(target_os = "windows"), not(target_os = "freebsd") ))] // FIXME: for currently not working platforms @@ -1680,6 +1680,7 @@ fn test_follow_descriptor_vs_rename1() { #[test] #[cfg(all( not(target_vendor = "apple"), + not(target_os = "android"), not(target_os = "windows"), not(target_os = "freebsd") ))] // FIXME: for currently not working platforms @@ -2120,6 +2121,7 @@ fn test_follow_name_move_create1() { #[test] #[cfg(all( not(target_vendor = "apple"), + not(target_os = "android"), not(target_os = "windows"), not(target_os = "freebsd") ))] // FIXME: for currently not working platforms @@ -4360,14 +4362,7 @@ fn test_follow_when_file_and_symlink_are_pointing_to_same_file_and_append_data() .stdout_only(expected_stdout); } -// Fails with: -// 'Assertion failed. Expected 'tail' to be running but exited with status=exit status: 1. -// stdout: -// stderr: tail: warning: --retry ignored; --retry is useful only when following -// tail: error reading 'dir': Is a directory -// ' #[test] -#[cfg(disabled_until_fixed)] fn test_args_when_directory_given_shorthand_big_f_together_with_retry() { let scene = TestScenario::new(util_name!()); let fixtures = &scene.fixtures; @@ -4379,9 +4374,17 @@ fn test_args_when_directory_given_shorthand_big_f_together_with_retry() { tail: {0}: cannot follow end of this type of file\n", dirname ); - let mut child = scene.ucmd().args(&["-F", "--retry", "dir"]).run_no_wait(); + child.make_assertion_with_delay(500).is_alive(); + child + .kill() + .make_assertion() + .with_current_output() + .stderr_only(&expected_stderr); + + let mut child = scene.ucmd().args(&["--retry", "-F", "dir"]).run_no_wait(); + child.make_assertion_with_delay(500).is_alive(); child .kill() diff --git a/tests/by-util/test_tee.rs b/tests/by-util/test_tee.rs index 946c60d0ae6..51d552d6726 100644 --- a/tests/by-util/test_tee.rs +++ b/tests/by-util/test_tee.rs @@ -118,9 +118,10 @@ mod linux_only { use std::os::unix::io::FromRawFd; let mut fds: [c_int; 2] = [0, 0]; - if unsafe { libc::pipe(&mut fds as *mut c_int) } != 0 { - panic!("Failed to create pipe"); - } + assert!( + (unsafe { libc::pipe(&mut fds as *mut c_int) } == 0), + "Failed to create pipe" + ); // Drop the read end of the pipe let _ = unsafe { File::from_raw_fd(fds[0]) }; diff --git a/tests/by-util/test_touch.rs b/tests/by-util/test_touch.rs index 80fb267108f..0e4eade3d25 100644 --- a/tests/by-util/test_touch.rs +++ b/tests/by-util/test_touch.rs @@ -6,10 +6,8 @@ // See https://github.com/time-rs/time/issues/293#issuecomment-946382614= // Defined in .cargo/config -extern crate touch; -use self::touch::filetime::{self, FileTime}; +use filetime::FileTime; -extern crate time; use time::macros::format_description; use crate::common::util::{AtPath, TestScenario}; @@ -251,14 +249,39 @@ fn test_touch_set_both_date_and_reference() { let ref_file = "test_touch_reference"; let file = "test_touch_set_both_date_and_reference"; - let start_of_year = str_to_filetime("%Y%m%d%H%M", "201501010000"); + let start_of_year = str_to_filetime("%Y%m%d%H%M", "201501011234"); at.touch(ref_file); set_file_times(&at, ref_file, start_of_year, start_of_year); assert!(at.file_exists(ref_file)); ucmd.args(&["-d", "Thu Jan 01 12:34:00 2015", "-r", ref_file, file]) - .fails(); + .succeeds() + .no_stderr(); + let (atime, mtime) = get_file_times(&at, file); + assert_eq!(atime, start_of_year); + assert_eq!(mtime, start_of_year); +} + +#[test] +fn test_touch_set_both_offset_date_and_reference() { + let (at, mut ucmd) = at_and_ucmd!(); + let ref_file = "test_touch_reference"; + let file = "test_touch_set_both_date_and_reference"; + + let start_of_year = str_to_filetime("%Y%m%d%H%M", "201501011234"); + let five_days_later = str_to_filetime("%Y%m%d%H%M", "201501061234"); + + at.touch(ref_file); + set_file_times(&at, ref_file, start_of_year, start_of_year); + assert!(at.file_exists(ref_file)); + + ucmd.args(&["-d", "+5 days", "-r", ref_file, file]) + .succeeds() + .no_stderr(); + let (atime, mtime) = get_file_times(&at, file); + assert_eq!(atime, five_days_later); + assert_eq!(mtime, five_days_later); } #[test] @@ -585,7 +608,15 @@ fn test_touch_set_date_relative_smoke() { // > (equivalent to ‘day’), the string ‘yesterday’ is worth one day // > in the past (equivalent to ‘day ago’). // - let times = ["yesterday", "tomorrow", "now"]; + let times = [ + "yesterday", + "tomorrow", + "now", + "2 seconds", + "2 years 1 week", + "2 days ago", + "2 months and 1 second", + ]; for time in times { let (at, mut ucmd) = at_and_ucmd!(); at.touch("f"); @@ -594,6 +625,11 @@ fn test_touch_set_date_relative_smoke() { .no_stderr() .no_stdout(); } + let (at, mut ucmd) = at_and_ucmd!(); + at.touch("f"); + ucmd.args(&["-d", "a", "f"]) + .fails() + .stderr_contains("touch: Unable to parse date"); } #[test] @@ -793,3 +829,27 @@ fn test_touch_trailing_slash_no_create() { at.relative_symlink_dir("dir2", "link2"); ucmd.args(&["-c", "link2/"]).succeeds(); } + +#[test] +fn test_touch_no_dereference_ref_dangling() { + let (at, mut ucmd) = at_and_ucmd!(); + at.touch("file"); + at.relative_symlink_file("nowhere", "dangling"); + + ucmd.args(&["-h", "-r", "dangling", "file"]).succeeds(); +} + +#[test] +fn test_touch_no_dereference_dangling() { + let (at, mut ucmd) = at_and_ucmd!(); + at.relative_symlink_file("nowhere", "dangling"); + + ucmd.args(&["-h", "dangling"]).succeeds(); +} + +#[test] +fn test_touch_dash() { + let (_, mut ucmd) = at_and_ucmd!(); + + ucmd.args(&["-h", "-"]).succeeds().no_stderr().no_stdout(); +} diff --git a/tests/by-util/test_tsort.rs b/tests/by-util/test_tsort.rs index 8b01e2a2d65..62a74c31d0e 100644 --- a/tests/by-util/test_tsort.rs +++ b/tests/by-util/test_tsort.rs @@ -20,6 +20,14 @@ fn test_sort_self_loop() { .stdout_only("first\nsecond\n"); } +#[test] +fn test_sort_floating_nodes() { + new_ucmd!() + .pipe_in("d d\nc c\na a\nb b") + .succeeds() + .stdout_only("a\nb\nc\nd\n"); +} + #[test] fn test_no_such_file() { new_ucmd!() diff --git a/tests/by-util/test_uptime.rs b/tests/by-util/test_uptime.rs index 46613ef5656..628f4cead6d 100644 --- a/tests/by-util/test_uptime.rs +++ b/tests/by-util/test_uptime.rs @@ -1,6 +1,5 @@ -extern crate regex; -use self::regex::Regex; use crate::common::util::TestScenario; +use regex::Regex; #[test] fn test_invalid_arg() { diff --git a/tests/by-util/test_vdir.rs b/tests/by-util/test_vdir.rs index d80247450c7..f6498567fbf 100644 --- a/tests/by-util/test_vdir.rs +++ b/tests/by-util/test_vdir.rs @@ -1,11 +1,5 @@ -#[cfg(not(windows))] -extern crate libc; -extern crate regex; -#[cfg(not(windows))] -extern crate tempfile; - -use self::regex::Regex; use crate::common::util::TestScenario; +use regex::Regex; /* * As vdir use the same functions than ls, we don't have to retest them here. diff --git a/tests/by-util/test_wc.rs b/tests/by-util/test_wc.rs index 3cda99270f3..aba5ed350a6 100644 --- a/tests/by-util/test_wc.rs +++ b/tests/by-util/test_wc.rs @@ -268,12 +268,16 @@ fn test_multiple_default() { "lorem_ipsum.txt", "moby_dick.txt", "alice_in_wonderland.txt", + "alice in wonderland.txt", ]) .run() - .stdout_is( - " 13 109 772 lorem_ipsum.txt\n 18 204 1115 moby_dick.txt\n 5 57 302 \ - alice_in_wonderland.txt\n 36 370 2189 total\n", - ); + .stdout_is(concat!( + " 13 109 772 lorem_ipsum.txt\n", + " 18 204 1115 moby_dick.txt\n", + " 5 57 302 alice_in_wonderland.txt\n", + " 5 57 302 alice in wonderland.txt\n", + " 41 427 2491 total\n", + )); } /// Test for an empty file. @@ -352,17 +356,24 @@ fn test_file_bytes_dictate_width() { new_ucmd!() .args(&["-lwc", "alice_in_wonderland.txt", "lorem_ipsum.txt"]) .run() - .stdout_is( - " 5 57 302 alice_in_wonderland.txt\n 13 109 772 \ - lorem_ipsum.txt\n 18 166 1074 total\n", - ); + .stdout_is(concat!( + " 5 57 302 alice_in_wonderland.txt\n", + " 13 109 772 lorem_ipsum.txt\n", + " 18 166 1074 total\n", + )); // . is a directory, so minimum_width should get set to 7 #[cfg(not(windows))] - const STDOUT: &str = " 0 0 0 emptyfile.txt\n 0 0 0 \ - .\n 0 0 0 total\n"; + const STDOUT: &str = concat!( + " 0 0 0 emptyfile.txt\n", + " 0 0 0 .\n", + " 0 0 0 total\n", + ); #[cfg(windows)] - const STDOUT: &str = " 0 0 0 emptyfile.txt\n 0 0 0 total\n"; + const STDOUT: &str = concat!( + " 0 0 0 emptyfile.txt\n", + " 0 0 0 total\n", + ); new_ucmd!() .args(&["-lwc", "emptyfile.txt", "."]) .run() @@ -375,7 +386,7 @@ fn test_read_from_directory_error() { #[cfg(not(windows))] const STDERR: &str = ".: Is a directory"; #[cfg(windows)] - const STDERR: &str = ".: Access is denied"; + const STDERR: &str = ".: Permission denied"; #[cfg(not(windows))] const STDOUT: &str = " 0 0 0 .\n"; @@ -392,15 +403,10 @@ fn test_read_from_directory_error() { /// Test that getting counts from nonexistent file is an error. #[test] fn test_read_from_nonexistent_file() { - #[cfg(not(windows))] - const MSG: &str = "bogusfile: No such file or directory"; - #[cfg(windows)] - const MSG: &str = "bogusfile: The system cannot find the file specified"; new_ucmd!() .args(&["bogusfile"]) .fails() - .stderr_contains(MSG) - .stdout_is(""); + .stderr_only("wc: bogusfile: No such file or directory\n"); } #[test] @@ -424,13 +430,30 @@ fn test_files0_disabled_files_argument() { #[test] fn test_files0_from() { + // file new_ucmd!() .args(&["--files0-from=files0_list.txt"]) .run() - .stdout_is( - " 13 109 772 lorem_ipsum.txt\n 18 204 1115 moby_dick.txt\n 5 57 302 \ - alice_in_wonderland.txt\n 36 370 2189 total\n", - ); + .success() + .stdout_is(concat!( + " 13 109 772 lorem_ipsum.txt\n", + " 18 204 1115 moby_dick.txt\n", + " 5 57 302 alice_in_wonderland.txt\n", + " 36 370 2189 total\n", + )); + + // stream + new_ucmd!() + .args(&["--files0-from=-"]) + .pipe_in_fixture("files0_list.txt") + .run() + .success() + .stdout_is(concat!( + "13 109 772 lorem_ipsum.txt\n", + "18 204 1115 moby_dick.txt\n", + "5 57 302 alice_in_wonderland.txt\n", + "36 370 2189 total\n", + )); } #[test] @@ -439,7 +462,7 @@ fn test_files0_from_with_stdin() { .args(&["--files0-from=-"]) .pipe_in("lorem_ipsum.txt") .run() - .stdout_is(" 13 109 772 lorem_ipsum.txt\n"); + .stdout_is("13 109 772 lorem_ipsum.txt\n"); } #[test] @@ -448,10 +471,12 @@ fn test_files0_from_with_stdin_in_file() { .args(&["--files0-from=files0_list_with_stdin.txt"]) .pipe_in_fixture("alice_in_wonderland.txt") .run() - .stdout_is( - " 13 109 772 lorem_ipsum.txt\n 18 204 1115 moby_dick.txt\n 5 57 302 \ - -\n 36 370 2189 total\n", - ); + .stdout_is(concat!( + " 13 109 772 lorem_ipsum.txt\n", + " 18 204 1115 moby_dick.txt\n", + " 5 57 302 -\n", // alice_in_wonderland.txt + " 36 370 2189 total\n", + )); } #[test] @@ -464,3 +489,218 @@ fn test_files0_from_with_stdin_try_read_from_stdin() { .stderr_contains(MSG) .stdout_is(""); } + +#[test] +fn test_total_auto() { + new_ucmd!() + .args(&["lorem_ipsum.txt", "--total=auto"]) + .run() + .stdout_is(" 13 109 772 lorem_ipsum.txt\n"); + + new_ucmd!() + .args(&["lorem_ipsum.txt", "moby_dick.txt", "--total=auto"]) + .run() + .stdout_is(concat!( + " 13 109 772 lorem_ipsum.txt\n", + " 18 204 1115 moby_dick.txt\n", + " 31 313 1887 total\n", + )); +} + +#[test] +fn test_total_always() { + new_ucmd!() + .args(&["lorem_ipsum.txt", "--total=always"]) + .run() + .stdout_is(concat!( + " 13 109 772 lorem_ipsum.txt\n", + " 13 109 772 total\n", + )); + + new_ucmd!() + .args(&["lorem_ipsum.txt", "moby_dick.txt", "--total=always"]) + .run() + .stdout_is(concat!( + " 13 109 772 lorem_ipsum.txt\n", + " 18 204 1115 moby_dick.txt\n", + " 31 313 1887 total\n", + )); +} + +#[test] +fn test_total_never() { + new_ucmd!() + .args(&["lorem_ipsum.txt", "--total=never"]) + .run() + .stdout_is(" 13 109 772 lorem_ipsum.txt\n"); + + new_ucmd!() + .args(&["lorem_ipsum.txt", "moby_dick.txt", "--total=never"]) + .run() + .stdout_is(concat!( + " 13 109 772 lorem_ipsum.txt\n", + " 18 204 1115 moby_dick.txt\n", + )); +} + +#[test] +fn test_total_only() { + new_ucmd!() + .args(&["lorem_ipsum.txt", "--total=only"]) + .run() + .stdout_is("13 109 772\n"); + + new_ucmd!() + .args(&["lorem_ipsum.txt", "moby_dick.txt", "--total=only"]) + .run() + .stdout_is("31 313 1887\n"); +} + +#[test] +fn test_zero_length_files() { + // A trailing zero is ignored, but otherwise empty file names are an error... + const LIST: &str = "\0moby_dick.txt\0\0alice_in_wonderland.txt\0\0lorem_ipsum.txt\0"; + + // Try with and without the last \0 + for l in [LIST.len(), LIST.len() - 1] { + new_ucmd!() + .args(&["--files0-from=-"]) + .pipe_in(&LIST[..l]) + .run() + .failure() + .stdout_is(concat!( + "18 204 1115 moby_dick.txt\n", + "5 57 302 alice_in_wonderland.txt\n", + "13 109 772 lorem_ipsum.txt\n", + "36 370 2189 total\n", + )) + .stderr_is(concat!( + "wc: -:1: invalid zero-length file name\n", + "wc: -:3: invalid zero-length file name\n", + "wc: -:5: invalid zero-length file name\n", + )); + } + + // But, just as important, a zero-length file name may still be at the end... + new_ucmd!() + .args(&["--files0-from=-"]) + .pipe_in( + LIST.as_bytes() + .iter() + .chain(b"\0") + .copied() + .collect::>(), + ) + .run() + .failure() + .stdout_is(concat!( + "18 204 1115 moby_dick.txt\n", + "5 57 302 alice_in_wonderland.txt\n", + "13 109 772 lorem_ipsum.txt\n", + "36 370 2189 total\n", + )) + .stderr_is(concat!( + "wc: -:1: invalid zero-length file name\n", + "wc: -:3: invalid zero-length file name\n", + "wc: -:5: invalid zero-length file name\n", + "wc: -:7: invalid zero-length file name\n", + )); +} + +#[test] +fn test_files0_errors_quoting() { + new_ucmd!() + .args(&["--files0-from=files0 with nonexistent.txt"]) + .run() + .failure() + .stderr_is(concat!( + "wc: this_file_does_not_exist.txt: No such file or directory\n", + "wc: 'files0 with nonexistent.txt':2: invalid zero-length file name\n", + "wc: 'this file does not exist.txt': No such file or directory\n", + "wc: \"this files doesn't exist either.txt\": No such file or directory\n", + )) + .stdout_is("0 0 0 total\n"); +} + +#[test] +fn test_files0_progressive_stream() { + use std::process::Stdio; + // You should be able to run wc and have a back-and-forth exchange with wc... + let mut child = new_ucmd!() + .args(&["--files0-from=-"]) + .set_stdin(Stdio::piped()) + .set_stdout(Stdio::piped()) + .set_stderr(Stdio::piped()) + .run_no_wait(); + + macro_rules! chk { + ($fn:ident, $exp:literal) => { + assert_eq!(child.$fn($exp.len()), $exp.as_bytes()); + }; + } + + // File in, count out... + child.write_in("moby_dick.txt\0"); + chk!(stdout_exact_bytes, "18 204 1115 moby_dick.txt\n"); + child.write_in("lorem_ipsum.txt\0"); + chk!(stdout_exact_bytes, "13 109 772 lorem_ipsum.txt\n"); + + // Introduce an error! + child.write_in("\0"); + chk!( + stderr_exact_bytes, + "wc: -:3: invalid zero-length file name\n" + ); + + // wc is quick to forgive, let's move on... + child.write_in("alice_in_wonderland.txt\0"); + chk!(stdout_exact_bytes, "5 57 302 alice_in_wonderland.txt\n"); + + // Fin. + child + .wait() + .expect("wc should finish") + .failure() + .stdout_only("36 370 2189 total\n"); +} + +#[test] +fn files0_from_dir() { + // On Unix, `read(open("."))` fails. On Windows, `open(".")` fails. Thus, the errors happen in + // different contexts. + #[cfg(not(windows))] + macro_rules! dir_err { + ($p:literal) => { + concat!("wc: ", $p, ": read error: Is a directory\n") + }; + } + #[cfg(windows)] + macro_rules! dir_err { + ($p:literal) => { + concat!("wc: cannot open ", $p, " for reading: Permission denied\n") + }; + } + + new_ucmd!() + .args(&["--files0-from=dir with spaces"]) + .fails() + .stderr_only(dir_err!("'dir with spaces'")); + + // Those contexts have different rules about quoting in errors... + #[cfg(windows)] + const DOT_ERR: &str = dir_err!("'.'"); + #[cfg(not(windows))] + const DOT_ERR: &str = dir_err!("."); + new_ucmd!() + .args(&["--files0-from=."]) + .fails() + .stderr_only(DOT_ERR); + + // That also means you cannot `< . wc --files0-from=-` on Windows. + #[cfg(not(windows))] + new_ucmd!() + .args(&["--files0-from=-"]) + .set_stdin(std::fs::File::open(".").unwrap()) + .fails() + .stderr_only(dir_err!("-")); +} diff --git a/tests/by-util/test_whoami.rs b/tests/by-util/test_whoami.rs index cbcf86028a1..9e6c35be6e2 100644 --- a/tests/by-util/test_whoami.rs +++ b/tests/by-util/test_whoami.rs @@ -50,3 +50,8 @@ fn test_normal_compare_env() { println!("test skipped:"); } } + +#[test] +fn test_succeeds_on_all_platforms() { + new_ucmd!().succeeds().no_stderr(); +} diff --git a/tests/by-util/test_yes.rs b/tests/by-util/test_yes.rs index 9f03780b741..89a68e7e1c9 100644 --- a/tests/by-util/test_yes.rs +++ b/tests/by-util/test_yes.rs @@ -1,3 +1,4 @@ +use std::ffi::OsStr; use std::process::{ExitStatus, Stdio}; #[cfg(unix)] @@ -6,24 +7,26 @@ use std::os::unix::process::ExitStatusExt; use crate::common::util::TestScenario; #[cfg(unix)] -fn check_termination(result: &ExitStatus) { +fn check_termination(result: ExitStatus) { assert_eq!(result.signal(), Some(libc::SIGPIPE)); } #[cfg(not(unix))] -fn check_termination(result: &ExitStatus) { +fn check_termination(result: ExitStatus) { assert!(result.success(), "yes did not exit successfully"); } +const NO_ARGS: &[&str] = &[]; + /// Run `yes`, capture some of the output, close the pipe, and verify it. -fn run(args: &[&str], expected: &[u8]) { +fn run(args: &[impl AsRef], expected: &[u8]) { let mut cmd = new_ucmd!(); let mut child = cmd.args(args).set_stdout(Stdio::piped()).run_no_wait(); let buf = child.stdout_exact_bytes(expected.len()); child.close_stdout(); #[allow(deprecated)] - check_termination(&child.wait_with_output().unwrap().status); + check_termination(child.wait_with_output().unwrap().status); assert_eq!(buf.as_slice(), expected); } @@ -34,7 +37,7 @@ fn test_invalid_arg() { #[test] fn test_simple() { - run(&[], b"y\ny\ny\ny\n"); + run(NO_ARGS, b"y\ny\ny\ny\n"); } #[test] @@ -44,7 +47,7 @@ fn test_args() { #[test] fn test_long_output() { - run(&[], "y\n".repeat(512 * 1024).as_bytes()); + run(NO_ARGS, "y\n".repeat(512 * 1024).as_bytes()); } /// Test with an output that seems likely to get mangled in case of incomplete writes. @@ -88,3 +91,20 @@ fn test_piped_to_dev_full() { } } } + +#[test] +#[cfg(any(unix, target_os = "wasi"))] +fn test_non_utf8() { + #[cfg(unix)] + use std::os::unix::ffi::OsStrExt; + #[cfg(target_os = "wasi")] + use std::os::wasi::ffi::OsStrExt; + + run( + &[ + OsStr::from_bytes(b"\xbf\xff\xee"), + OsStr::from_bytes(b"bar"), + ], + &b"\xbf\xff\xee bar\n".repeat(5000), + ); +} diff --git a/tests/common/util.rs b/tests/common/util.rs index 5d72a7abfd2..0898a4ad762 100644 --- a/tests/common/util.rs +++ b/tests/common/util.rs @@ -2016,7 +2016,7 @@ impl UChild { /// Read, consume and return the output as [`String`] from [`Child`]'s stdout. /// - /// See also [`UChild::stdout_bytes] for side effects. + /// See also [`UChild::stdout_bytes`] for side effects. pub fn stdout(&mut self) -> String { String::from_utf8(self.stdout_bytes()).unwrap() } diff --git a/tests/fixtures/cksum/blake2b_multiple_files.expected b/tests/fixtures/cksum/blake2b_multiple_files.expected new file mode 100644 index 00000000000..97d06eb6fea --- /dev/null +++ b/tests/fixtures/cksum/blake2b_multiple_files.expected @@ -0,0 +1,2 @@ +BLAKE2b (lorem_ipsum.txt) = 0e97a09189e560c3789c0bff1f020166861ef857d1fbfe4574de1842e3c06cabb9575e4af6309a166158c2b408d3c038c1b49d828b35158142cdc0396d1195c3 +BLAKE2b (alice_in_wonderland.txt) = 91b8b0f0868e905ad18b8ac35e4a1dacd289857b19258ab5d1e071761af758b0134ec152d4f011fe1825ca889c80c2e072ca70eb50548c25fc49a98937515af4 diff --git a/tests/fixtures/cksum/blake2b_single_file.expected b/tests/fixtures/cksum/blake2b_single_file.expected new file mode 100644 index 00000000000..64ede7ca040 --- /dev/null +++ b/tests/fixtures/cksum/blake2b_single_file.expected @@ -0,0 +1 @@ +BLAKE2b (lorem_ipsum.txt) = 0e97a09189e560c3789c0bff1f020166861ef857d1fbfe4574de1842e3c06cabb9575e4af6309a166158c2b408d3c038c1b49d828b35158142cdc0396d1195c3 diff --git a/tests/fixtures/cksum/blake2b_stdin.expected b/tests/fixtures/cksum/blake2b_stdin.expected new file mode 100644 index 00000000000..15c34ea775a --- /dev/null +++ b/tests/fixtures/cksum/blake2b_stdin.expected @@ -0,0 +1 @@ +BLAKE2b (-) = 0e97a09189e560c3789c0bff1f020166861ef857d1fbfe4574de1842e3c06cabb9575e4af6309a166158c2b408d3c038c1b49d828b35158142cdc0396d1195c3 diff --git a/tests/fixtures/cksum/multiple_files.expected b/tests/fixtures/cksum/crc_multiple_files.expected similarity index 100% rename from tests/fixtures/cksum/multiple_files.expected rename to tests/fixtures/cksum/crc_multiple_files.expected diff --git a/tests/fixtures/cksum/single_file.expected b/tests/fixtures/cksum/crc_single_file.expected similarity index 100% rename from tests/fixtures/cksum/single_file.expected rename to tests/fixtures/cksum/crc_single_file.expected diff --git a/tests/fixtures/cksum/stdin.expected b/tests/fixtures/cksum/crc_stdin.expected similarity index 100% rename from tests/fixtures/cksum/stdin.expected rename to tests/fixtures/cksum/crc_stdin.expected diff --git a/tests/fixtures/cksum/md5_multiple_files.expected b/tests/fixtures/cksum/md5_multiple_files.expected new file mode 100644 index 00000000000..54023e7615f --- /dev/null +++ b/tests/fixtures/cksum/md5_multiple_files.expected @@ -0,0 +1,2 @@ +MD5 (lorem_ipsum.txt) = cd724690f7dc61775dfac400a71f2caa +MD5 (alice_in_wonderland.txt) = f6fa7033e16166a9589aa1c0388ffd58 diff --git a/tests/fixtures/cksum/md5_single_file.expected b/tests/fixtures/cksum/md5_single_file.expected new file mode 100644 index 00000000000..5975053d1e2 --- /dev/null +++ b/tests/fixtures/cksum/md5_single_file.expected @@ -0,0 +1 @@ +MD5 (lorem_ipsum.txt) = cd724690f7dc61775dfac400a71f2caa diff --git a/tests/fixtures/cksum/md5_stdin.expected b/tests/fixtures/cksum/md5_stdin.expected new file mode 100644 index 00000000000..b3b1090bccc --- /dev/null +++ b/tests/fixtures/cksum/md5_stdin.expected @@ -0,0 +1 @@ +MD5 (-) = cd724690f7dc61775dfac400a71f2caa diff --git a/tests/fixtures/cksum/sha1_multiple_files.expected b/tests/fixtures/cksum/sha1_multiple_files.expected new file mode 100644 index 00000000000..f20ea947153 --- /dev/null +++ b/tests/fixtures/cksum/sha1_multiple_files.expected @@ -0,0 +1,2 @@ +SHA1 (lorem_ipsum.txt) = ab1dd0bae1d8883a3d18a66de6afbd28252cfbef +SHA1 (alice_in_wonderland.txt) = 22b54b2520e8b4fa59eb10719028a4e587c12d1e diff --git a/tests/fixtures/cksum/sha1_single_file.expected b/tests/fixtures/cksum/sha1_single_file.expected new file mode 100644 index 00000000000..1b820559f61 --- /dev/null +++ b/tests/fixtures/cksum/sha1_single_file.expected @@ -0,0 +1 @@ +SHA1 (lorem_ipsum.txt) = ab1dd0bae1d8883a3d18a66de6afbd28252cfbef diff --git a/tests/fixtures/cksum/sha1_stdin.expected b/tests/fixtures/cksum/sha1_stdin.expected new file mode 100644 index 00000000000..ced9ce80d49 --- /dev/null +++ b/tests/fixtures/cksum/sha1_stdin.expected @@ -0,0 +1 @@ +SHA1 (-) = ab1dd0bae1d8883a3d18a66de6afbd28252cfbef diff --git a/tests/fixtures/cksum/sha224_multiple_files.expected b/tests/fixtures/cksum/sha224_multiple_files.expected new file mode 100644 index 00000000000..0d6b45b102d --- /dev/null +++ b/tests/fixtures/cksum/sha224_multiple_files.expected @@ -0,0 +1,2 @@ +SHA224 (lorem_ipsum.txt) = 3de66fbcad106e1b40ab391be56c51d2007eb1f9c655d0f4e29bfc01 +SHA224 (alice_in_wonderland.txt) = 54c9c7d78458886418ce0845111fc49fe1c628ffd4bf3da14226ffd9 diff --git a/tests/fixtures/cksum/sha224_single_file.expected b/tests/fixtures/cksum/sha224_single_file.expected new file mode 100644 index 00000000000..a08d66bb4ed --- /dev/null +++ b/tests/fixtures/cksum/sha224_single_file.expected @@ -0,0 +1 @@ +SHA224 (lorem_ipsum.txt) = 3de66fbcad106e1b40ab391be56c51d2007eb1f9c655d0f4e29bfc01 diff --git a/tests/fixtures/cksum/sha224_stdin.expected b/tests/fixtures/cksum/sha224_stdin.expected new file mode 100644 index 00000000000..1bd13869885 --- /dev/null +++ b/tests/fixtures/cksum/sha224_stdin.expected @@ -0,0 +1 @@ +SHA224 (-) = 3de66fbcad106e1b40ab391be56c51d2007eb1f9c655d0f4e29bfc01 diff --git a/tests/fixtures/cksum/sha256_multiple_files.expected b/tests/fixtures/cksum/sha256_multiple_files.expected new file mode 100644 index 00000000000..e6fd8beb520 --- /dev/null +++ b/tests/fixtures/cksum/sha256_multiple_files.expected @@ -0,0 +1,2 @@ +SHA256 (lorem_ipsum.txt) = f7c420501c50e00b309250100d67ea5e910981536b4582fe9c435bd92b3f1f02 +SHA256 (alice_in_wonderland.txt) = 14ab7e5a0aa3a670222744714bc96961d51012cb216225d965db71824a46e5fe diff --git a/tests/fixtures/cksum/sha256_single_file.expected b/tests/fixtures/cksum/sha256_single_file.expected new file mode 100644 index 00000000000..e16abcb0f37 --- /dev/null +++ b/tests/fixtures/cksum/sha256_single_file.expected @@ -0,0 +1 @@ +SHA256 (lorem_ipsum.txt) = f7c420501c50e00b309250100d67ea5e910981536b4582fe9c435bd92b3f1f02 diff --git a/tests/fixtures/cksum/sha256_stdin.expected b/tests/fixtures/cksum/sha256_stdin.expected new file mode 100644 index 00000000000..87bd8419531 --- /dev/null +++ b/tests/fixtures/cksum/sha256_stdin.expected @@ -0,0 +1 @@ +SHA256 (-) = f7c420501c50e00b309250100d67ea5e910981536b4582fe9c435bd92b3f1f02 diff --git a/tests/fixtures/cksum/sha384_multiple_files.expected b/tests/fixtures/cksum/sha384_multiple_files.expected new file mode 100644 index 00000000000..a5a3324a254 --- /dev/null +++ b/tests/fixtures/cksum/sha384_multiple_files.expected @@ -0,0 +1,2 @@ +SHA384 (lorem_ipsum.txt) = 4be4b90a0d0d32966992921019f24abc824dcfb8b1c408102f1f6788fb80ba9a9a4c5a7b575a3353a90a8ee719481dcb +SHA384 (alice_in_wonderland.txt) = b7966c97ef84ab5858db2e0cdd33fbaf4fa8346d84de65aba001e738c242598a43272854d0073ad1099404eaa1d93766 diff --git a/tests/fixtures/cksum/sha384_single_file.expected b/tests/fixtures/cksum/sha384_single_file.expected new file mode 100644 index 00000000000..8d673e60b06 --- /dev/null +++ b/tests/fixtures/cksum/sha384_single_file.expected @@ -0,0 +1 @@ +SHA384 (lorem_ipsum.txt) = 4be4b90a0d0d32966992921019f24abc824dcfb8b1c408102f1f6788fb80ba9a9a4c5a7b575a3353a90a8ee719481dcb diff --git a/tests/fixtures/cksum/sha384_stdin.expected b/tests/fixtures/cksum/sha384_stdin.expected new file mode 100644 index 00000000000..3c0d5c8189e --- /dev/null +++ b/tests/fixtures/cksum/sha384_stdin.expected @@ -0,0 +1 @@ +SHA384 (-) = 4be4b90a0d0d32966992921019f24abc824dcfb8b1c408102f1f6788fb80ba9a9a4c5a7b575a3353a90a8ee719481dcb diff --git a/tests/fixtures/cksum/sha512_multiple_files.expected b/tests/fixtures/cksum/sha512_multiple_files.expected new file mode 100644 index 00000000000..0f533b27c8f --- /dev/null +++ b/tests/fixtures/cksum/sha512_multiple_files.expected @@ -0,0 +1,2 @@ +SHA512 (lorem_ipsum.txt) = 965464ab2556aad58ebc73d89ad221e559797529ecafc0f466c11795cff6d6e2c60f96a07c542cfd1f426e5e4fe0a48aa15667ba44096b213d0813cd038dfa05 +SHA512 (alice_in_wonderland.txt) = 251646d5a7eb481e0f3aced7839d78dd5e97153f822dc55938e17059c485990d85d602e2881b528b565ab6262584a69c97b068b26bda81acc9356c53c7c1c96d diff --git a/tests/fixtures/cksum/sha512_single_file.expected b/tests/fixtures/cksum/sha512_single_file.expected new file mode 100644 index 00000000000..e9a02c96a0d --- /dev/null +++ b/tests/fixtures/cksum/sha512_single_file.expected @@ -0,0 +1 @@ +SHA512 (lorem_ipsum.txt) = 965464ab2556aad58ebc73d89ad221e559797529ecafc0f466c11795cff6d6e2c60f96a07c542cfd1f426e5e4fe0a48aa15667ba44096b213d0813cd038dfa05 diff --git a/tests/fixtures/cksum/sha512_stdin.expected b/tests/fixtures/cksum/sha512_stdin.expected new file mode 100644 index 00000000000..8321c4cc3e9 --- /dev/null +++ b/tests/fixtures/cksum/sha512_stdin.expected @@ -0,0 +1 @@ +SHA512 (-) = 965464ab2556aad58ebc73d89ad221e559797529ecafc0f466c11795cff6d6e2c60f96a07c542cfd1f426e5e4fe0a48aa15667ba44096b213d0813cd038dfa05 diff --git a/tests/fixtures/cksum/sm3_multiple_files.expected b/tests/fixtures/cksum/sm3_multiple_files.expected new file mode 100644 index 00000000000..eae2cde2f3d --- /dev/null +++ b/tests/fixtures/cksum/sm3_multiple_files.expected @@ -0,0 +1,2 @@ +SM3 (lorem_ipsum.txt) = 6d296b805d060bfed22808df308dbb9b4317794dd4ed6740a10770a782699bc2 +SM3 (alice_in_wonderland.txt) = d66617ae3c4e87828298dcd836f79efbab488c53b84e09c3e8e83a16c902418d diff --git a/tests/fixtures/cksum/sm3_single_file.expected b/tests/fixtures/cksum/sm3_single_file.expected new file mode 100644 index 00000000000..cf4b8304a3d --- /dev/null +++ b/tests/fixtures/cksum/sm3_single_file.expected @@ -0,0 +1 @@ +SM3 (lorem_ipsum.txt) = 6d296b805d060bfed22808df308dbb9b4317794dd4ed6740a10770a782699bc2 diff --git a/tests/fixtures/cksum/sm3_stdin.expected b/tests/fixtures/cksum/sm3_stdin.expected new file mode 100644 index 00000000000..436fcfb4142 --- /dev/null +++ b/tests/fixtures/cksum/sm3_stdin.expected @@ -0,0 +1 @@ +SM3 (-) = 6d296b805d060bfed22808df308dbb9b4317794dd4ed6740a10770a782699bc2 diff --git a/tests/fixtures/cksum/untagged/blake2b_multiple_files.expected b/tests/fixtures/cksum/untagged/blake2b_multiple_files.expected new file mode 100644 index 00000000000..7c68c857332 --- /dev/null +++ b/tests/fixtures/cksum/untagged/blake2b_multiple_files.expected @@ -0,0 +1,2 @@ +0e97a09189e560c3789c0bff1f020166861ef857d1fbfe4574de1842e3c06cabb9575e4af6309a166158c2b408d3c038c1b49d828b35158142cdc0396d1195c3 lorem_ipsum.txt +91b8b0f0868e905ad18b8ac35e4a1dacd289857b19258ab5d1e071761af758b0134ec152d4f011fe1825ca889c80c2e072ca70eb50548c25fc49a98937515af4 alice_in_wonderland.txt diff --git a/tests/fixtures/cksum/untagged/blake2b_single_file.expected b/tests/fixtures/cksum/untagged/blake2b_single_file.expected new file mode 100644 index 00000000000..1f5444c87cd --- /dev/null +++ b/tests/fixtures/cksum/untagged/blake2b_single_file.expected @@ -0,0 +1 @@ +0e97a09189e560c3789c0bff1f020166861ef857d1fbfe4574de1842e3c06cabb9575e4af6309a166158c2b408d3c038c1b49d828b35158142cdc0396d1195c3 lorem_ipsum.txt diff --git a/tests/fixtures/cksum/untagged/blake2b_stdin.expected b/tests/fixtures/cksum/untagged/blake2b_stdin.expected new file mode 100644 index 00000000000..0892bff36dc --- /dev/null +++ b/tests/fixtures/cksum/untagged/blake2b_stdin.expected @@ -0,0 +1 @@ +0e97a09189e560c3789c0bff1f020166861ef857d1fbfe4574de1842e3c06cabb9575e4af6309a166158c2b408d3c038c1b49d828b35158142cdc0396d1195c3 - diff --git a/tests/fixtures/cksum/untagged/bsd_multiple_files.expected b/tests/fixtures/cksum/untagged/bsd_multiple_files.expected new file mode 100644 index 00000000000..941a2a512f8 --- /dev/null +++ b/tests/fixtures/cksum/untagged/bsd_multiple_files.expected @@ -0,0 +1,2 @@ +08109 1 lorem_ipsum.txt +01814 1 alice_in_wonderland.txt diff --git a/tests/fixtures/cksum/untagged/bsd_single_file.expected b/tests/fixtures/cksum/untagged/bsd_single_file.expected new file mode 100644 index 00000000000..293ada3bd61 --- /dev/null +++ b/tests/fixtures/cksum/untagged/bsd_single_file.expected @@ -0,0 +1 @@ +08109 1 lorem_ipsum.txt diff --git a/tests/fixtures/cksum/untagged/bsd_stdin.expected b/tests/fixtures/cksum/untagged/bsd_stdin.expected new file mode 100644 index 00000000000..4843ba08246 --- /dev/null +++ b/tests/fixtures/cksum/untagged/bsd_stdin.expected @@ -0,0 +1 @@ +08109 1 diff --git a/tests/fixtures/cksum/untagged/crc_multiple_files.expected b/tests/fixtures/cksum/untagged/crc_multiple_files.expected new file mode 100644 index 00000000000..d7a4f5b4f3b --- /dev/null +++ b/tests/fixtures/cksum/untagged/crc_multiple_files.expected @@ -0,0 +1,2 @@ +378294376 772 lorem_ipsum.txt +3805907707 302 alice_in_wonderland.txt diff --git a/tests/fixtures/cksum/untagged/crc_single_file.expected b/tests/fixtures/cksum/untagged/crc_single_file.expected new file mode 100644 index 00000000000..e9fc1ca7cf4 --- /dev/null +++ b/tests/fixtures/cksum/untagged/crc_single_file.expected @@ -0,0 +1 @@ +378294376 772 lorem_ipsum.txt diff --git a/tests/fixtures/cksum/untagged/crc_stdin.expected b/tests/fixtures/cksum/untagged/crc_stdin.expected new file mode 100644 index 00000000000..28b37d0bed1 --- /dev/null +++ b/tests/fixtures/cksum/untagged/crc_stdin.expected @@ -0,0 +1 @@ +378294376 772 diff --git a/tests/fixtures/cksum/untagged/md5_multiple_files.expected b/tests/fixtures/cksum/untagged/md5_multiple_files.expected new file mode 100644 index 00000000000..4b63cbff7fd --- /dev/null +++ b/tests/fixtures/cksum/untagged/md5_multiple_files.expected @@ -0,0 +1,2 @@ +cd724690f7dc61775dfac400a71f2caa lorem_ipsum.txt +f6fa7033e16166a9589aa1c0388ffd58 alice_in_wonderland.txt diff --git a/tests/fixtures/cksum/untagged/md5_single_file.expected b/tests/fixtures/cksum/untagged/md5_single_file.expected new file mode 100644 index 00000000000..ca9eb678596 --- /dev/null +++ b/tests/fixtures/cksum/untagged/md5_single_file.expected @@ -0,0 +1 @@ +cd724690f7dc61775dfac400a71f2caa lorem_ipsum.txt diff --git a/tests/fixtures/cksum/untagged/md5_stdin.expected b/tests/fixtures/cksum/untagged/md5_stdin.expected new file mode 100644 index 00000000000..f4094dcc7e0 --- /dev/null +++ b/tests/fixtures/cksum/untagged/md5_stdin.expected @@ -0,0 +1 @@ +cd724690f7dc61775dfac400a71f2caa - diff --git a/tests/fixtures/cksum/untagged/sha1_multiple_files.expected b/tests/fixtures/cksum/untagged/sha1_multiple_files.expected new file mode 100644 index 00000000000..1712c740911 --- /dev/null +++ b/tests/fixtures/cksum/untagged/sha1_multiple_files.expected @@ -0,0 +1,2 @@ +ab1dd0bae1d8883a3d18a66de6afbd28252cfbef lorem_ipsum.txt +22b54b2520e8b4fa59eb10719028a4e587c12d1e alice_in_wonderland.txt diff --git a/tests/fixtures/cksum/untagged/sha1_single_file.expected b/tests/fixtures/cksum/untagged/sha1_single_file.expected new file mode 100644 index 00000000000..c8e8ddca6f9 --- /dev/null +++ b/tests/fixtures/cksum/untagged/sha1_single_file.expected @@ -0,0 +1 @@ +ab1dd0bae1d8883a3d18a66de6afbd28252cfbef lorem_ipsum.txt diff --git a/tests/fixtures/cksum/untagged/sha1_stdin.expected b/tests/fixtures/cksum/untagged/sha1_stdin.expected new file mode 100644 index 00000000000..2396ed481ec --- /dev/null +++ b/tests/fixtures/cksum/untagged/sha1_stdin.expected @@ -0,0 +1 @@ +ab1dd0bae1d8883a3d18a66de6afbd28252cfbef - diff --git a/tests/fixtures/cksum/untagged/sha224_multiple_files.expected b/tests/fixtures/cksum/untagged/sha224_multiple_files.expected new file mode 100644 index 00000000000..3eb2b500a00 --- /dev/null +++ b/tests/fixtures/cksum/untagged/sha224_multiple_files.expected @@ -0,0 +1,2 @@ +3de66fbcad106e1b40ab391be56c51d2007eb1f9c655d0f4e29bfc01 lorem_ipsum.txt +54c9c7d78458886418ce0845111fc49fe1c628ffd4bf3da14226ffd9 alice_in_wonderland.txt diff --git a/tests/fixtures/cksum/untagged/sha224_single_file.expected b/tests/fixtures/cksum/untagged/sha224_single_file.expected new file mode 100644 index 00000000000..c5ab1f7d143 --- /dev/null +++ b/tests/fixtures/cksum/untagged/sha224_single_file.expected @@ -0,0 +1 @@ +3de66fbcad106e1b40ab391be56c51d2007eb1f9c655d0f4e29bfc01 lorem_ipsum.txt diff --git a/tests/fixtures/cksum/untagged/sha224_stdin.expected b/tests/fixtures/cksum/untagged/sha224_stdin.expected new file mode 100644 index 00000000000..691e451b373 --- /dev/null +++ b/tests/fixtures/cksum/untagged/sha224_stdin.expected @@ -0,0 +1 @@ +3de66fbcad106e1b40ab391be56c51d2007eb1f9c655d0f4e29bfc01 - diff --git a/tests/fixtures/cksum/untagged/sha256_multiple_files.expected b/tests/fixtures/cksum/untagged/sha256_multiple_files.expected new file mode 100644 index 00000000000..a57fa2eaf1b --- /dev/null +++ b/tests/fixtures/cksum/untagged/sha256_multiple_files.expected @@ -0,0 +1,2 @@ +f7c420501c50e00b309250100d67ea5e910981536b4582fe9c435bd92b3f1f02 lorem_ipsum.txt +14ab7e5a0aa3a670222744714bc96961d51012cb216225d965db71824a46e5fe alice_in_wonderland.txt diff --git a/tests/fixtures/cksum/untagged/sha256_single_file.expected b/tests/fixtures/cksum/untagged/sha256_single_file.expected new file mode 100644 index 00000000000..1b1be9516be --- /dev/null +++ b/tests/fixtures/cksum/untagged/sha256_single_file.expected @@ -0,0 +1 @@ +f7c420501c50e00b309250100d67ea5e910981536b4582fe9c435bd92b3f1f02 lorem_ipsum.txt diff --git a/tests/fixtures/cksum/untagged/sha256_stdin.expected b/tests/fixtures/cksum/untagged/sha256_stdin.expected new file mode 100644 index 00000000000..998db6c664b --- /dev/null +++ b/tests/fixtures/cksum/untagged/sha256_stdin.expected @@ -0,0 +1 @@ +f7c420501c50e00b309250100d67ea5e910981536b4582fe9c435bd92b3f1f02 - diff --git a/tests/fixtures/cksum/untagged/sha384_multiple_files.expected b/tests/fixtures/cksum/untagged/sha384_multiple_files.expected new file mode 100644 index 00000000000..d309034b4af --- /dev/null +++ b/tests/fixtures/cksum/untagged/sha384_multiple_files.expected @@ -0,0 +1,2 @@ +4be4b90a0d0d32966992921019f24abc824dcfb8b1c408102f1f6788fb80ba9a9a4c5a7b575a3353a90a8ee719481dcb lorem_ipsum.txt +b7966c97ef84ab5858db2e0cdd33fbaf4fa8346d84de65aba001e738c242598a43272854d0073ad1099404eaa1d93766 alice_in_wonderland.txt diff --git a/tests/fixtures/cksum/untagged/sha384_single_file.expected b/tests/fixtures/cksum/untagged/sha384_single_file.expected new file mode 100644 index 00000000000..88d4da57726 --- /dev/null +++ b/tests/fixtures/cksum/untagged/sha384_single_file.expected @@ -0,0 +1 @@ +4be4b90a0d0d32966992921019f24abc824dcfb8b1c408102f1f6788fb80ba9a9a4c5a7b575a3353a90a8ee719481dcb lorem_ipsum.txt diff --git a/tests/fixtures/cksum/untagged/sha384_stdin.expected b/tests/fixtures/cksum/untagged/sha384_stdin.expected new file mode 100644 index 00000000000..cde20b78b93 --- /dev/null +++ b/tests/fixtures/cksum/untagged/sha384_stdin.expected @@ -0,0 +1 @@ +4be4b90a0d0d32966992921019f24abc824dcfb8b1c408102f1f6788fb80ba9a9a4c5a7b575a3353a90a8ee719481dcb - diff --git a/tests/fixtures/cksum/untagged/sha512_multiple_files.expected b/tests/fixtures/cksum/untagged/sha512_multiple_files.expected new file mode 100644 index 00000000000..a5dafa7c329 --- /dev/null +++ b/tests/fixtures/cksum/untagged/sha512_multiple_files.expected @@ -0,0 +1,2 @@ +965464ab2556aad58ebc73d89ad221e559797529ecafc0f466c11795cff6d6e2c60f96a07c542cfd1f426e5e4fe0a48aa15667ba44096b213d0813cd038dfa05 lorem_ipsum.txt +251646d5a7eb481e0f3aced7839d78dd5e97153f822dc55938e17059c485990d85d602e2881b528b565ab6262584a69c97b068b26bda81acc9356c53c7c1c96d alice_in_wonderland.txt diff --git a/tests/fixtures/cksum/untagged/sha512_single_file.expected b/tests/fixtures/cksum/untagged/sha512_single_file.expected new file mode 100644 index 00000000000..adea498d609 --- /dev/null +++ b/tests/fixtures/cksum/untagged/sha512_single_file.expected @@ -0,0 +1 @@ +965464ab2556aad58ebc73d89ad221e559797529ecafc0f466c11795cff6d6e2c60f96a07c542cfd1f426e5e4fe0a48aa15667ba44096b213d0813cd038dfa05 lorem_ipsum.txt diff --git a/tests/fixtures/cksum/untagged/sha512_stdin.expected b/tests/fixtures/cksum/untagged/sha512_stdin.expected new file mode 100644 index 00000000000..dd9c968438a --- /dev/null +++ b/tests/fixtures/cksum/untagged/sha512_stdin.expected @@ -0,0 +1 @@ +965464ab2556aad58ebc73d89ad221e559797529ecafc0f466c11795cff6d6e2c60f96a07c542cfd1f426e5e4fe0a48aa15667ba44096b213d0813cd038dfa05 - diff --git a/tests/fixtures/cksum/untagged/sm3_multiple_files.expected b/tests/fixtures/cksum/untagged/sm3_multiple_files.expected new file mode 100644 index 00000000000..de12ab0b9b6 --- /dev/null +++ b/tests/fixtures/cksum/untagged/sm3_multiple_files.expected @@ -0,0 +1,2 @@ +6d296b805d060bfed22808df308dbb9b4317794dd4ed6740a10770a782699bc2 lorem_ipsum.txt +d66617ae3c4e87828298dcd836f79efbab488c53b84e09c3e8e83a16c902418d alice_in_wonderland.txt diff --git a/tests/fixtures/cksum/untagged/sm3_single_file.expected b/tests/fixtures/cksum/untagged/sm3_single_file.expected new file mode 100644 index 00000000000..54d5f40d233 --- /dev/null +++ b/tests/fixtures/cksum/untagged/sm3_single_file.expected @@ -0,0 +1 @@ +6d296b805d060bfed22808df308dbb9b4317794dd4ed6740a10770a782699bc2 lorem_ipsum.txt diff --git a/tests/fixtures/cksum/untagged/sm3_stdin.expected b/tests/fixtures/cksum/untagged/sm3_stdin.expected new file mode 100644 index 00000000000..6ba002b45b6 --- /dev/null +++ b/tests/fixtures/cksum/untagged/sm3_stdin.expected @@ -0,0 +1 @@ +6d296b805d060bfed22808df308dbb9b4317794dd4ed6740a10770a782699bc2 - diff --git a/tests/fixtures/cksum/untagged/sysv_multiple_files.expected b/tests/fixtures/cksum/untagged/sysv_multiple_files.expected new file mode 100644 index 00000000000..83a6d6d839f --- /dev/null +++ b/tests/fixtures/cksum/untagged/sysv_multiple_files.expected @@ -0,0 +1,2 @@ +6985 2 lorem_ipsum.txt +27441 1 alice_in_wonderland.txt diff --git a/tests/fixtures/cksum/untagged/sysv_single_file.expected b/tests/fixtures/cksum/untagged/sysv_single_file.expected new file mode 100644 index 00000000000..e0f7252cbe8 --- /dev/null +++ b/tests/fixtures/cksum/untagged/sysv_single_file.expected @@ -0,0 +1 @@ +6985 2 lorem_ipsum.txt diff --git a/tests/fixtures/cksum/untagged/sysv_stdin.expected b/tests/fixtures/cksum/untagged/sysv_stdin.expected new file mode 100644 index 00000000000..f0fba8c815c --- /dev/null +++ b/tests/fixtures/cksum/untagged/sysv_stdin.expected @@ -0,0 +1 @@ +6985 2 diff --git a/tests/fixtures/wc/alice in wonderland.txt b/tests/fixtures/wc/alice in wonderland.txt new file mode 100644 index 00000000000..a95562a1ce6 --- /dev/null +++ b/tests/fixtures/wc/alice in wonderland.txt @@ -0,0 +1,5 @@ +Alice was beginning to get very tired of sitting by +her sister on the bank, and of having nothing to do: once or twice +she had peeped into the book her sister was reading, but it had no +pictures or conversations in it, "and what is the use of a book," +thought Alice "without pictures or conversation?" diff --git a/tests/fixtures/wc/dir with spaces/.keep b/tests/fixtures/wc/dir with spaces/.keep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/fixtures/wc/files0 with nonexistent.txt b/tests/fixtures/wc/files0 with nonexistent.txt new file mode 100644 index 00000000000..00c00b705b1 Binary files /dev/null and b/tests/fixtures/wc/files0 with nonexistent.txt differ diff --git a/tests/tests.rs b/tests/tests.rs index 17581799dd7..02c3bfdaba2 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -1,9 +1,6 @@ #[macro_use] mod common; -#[cfg(unix)] -extern crate rust_users; - #[cfg(feature = "arch")] #[path = "by-util/test_arch.rs"] mod test_arch; diff --git a/util/android-commands.sh b/util/android-commands.sh index 42e27ad8868..4b504de5649 100755 --- a/util/android-commands.sh +++ b/util/android-commands.sh @@ -1,5 +1,5 @@ #!/bin/bash -# spell-checker:ignore termux keyevent sdcard binutils unmatch adb's dumpsys logcat pkill +# spell-checker:ignore termux keyevent sdcard binutils unmatch adb's dumpsys logcat pkill nextest logfile # There are three shells: the host's, adb, and termux. Only adb lets us run # commands directly on the emulated device, only termux provides a GNU @@ -8,30 +8,36 @@ # This means that the commands sent to termux are first parsed as arguments in # this shell, then as arguments in the adb shell, before finally being used as # text inputs to the app. Hence, the "'wrapping'" on those commands. -# There's no way to get any feedback from termux, so every time we run a -# command on it, we make sure it ends by creating a unique *.probe file at the -# end of the command. The contents of the file are used as a return code: 0 on -# success, some other number for errors (an empty file is basically the same as -# 0). Note that the return codes are text, not raw bytes. +# There's no way to get any direct feedback from termux, so every time we run a +# command on it, we make sure it creates a unique *.probe file which is polled +# every 30 seconds together with the current output of the command in a *.log file. +# The contents of the probe file are used as a return code: 0 on success, some +# other number for errors (an empty file is basically the same as 0). Note that +# the return codes are text, not raw bytes. +this_repo="$(dirname "$(dirname -- "$(readlink -- "${0}")")")" +cache_dir_name="__rust_cache__" -this_repo="$(dirname $(dirname -- "$(readlink -- "${0}")"))" - -help () { +help() { echo \ -"Usage: $0 COMMAND [ARG] + "Usage: $0 COMMAND [ARG] where COMMAND is one of: + init download termux and initialize the emulator image snapshot APK install APK and dependencies on an emulator to prep a snapshot (you can, but probably don't want to, run this for physical devices -- just set up termux and the dependencies yourself) - sync [REPO] push the repo at REPO to the device, deleting and restoring all - symlinks (locally) in the process; by default, REPO is: + sync_host [REPO] + push the repo at REPO to the device, deleting and restoring all symlinks (locally) + in the process; The cached rust directories are restored, too; by default, REPO is: $this_repo + sync_image [REPO] + copy the repo/target and the HOME/.cargo directories from the device back to the + host; by default, REPO is: $this_repo build run \`cargo build --features feat_os_unix_android\` on the - device, then pull the output as build.log + device tests run \`cargo test --features feat_os_unix_android\` on the - device, then pull the output as tests.log + device If you have multiple devices, use the ANDROID_SERIAL environment variable to specify which to connect to." @@ -41,9 +47,13 @@ hit_enter() { adb shell input keyevent 66 } +exit_termux() { + adb shell input text "exit" && hit_enter && hit_enter +} + launch_termux() { echo "launching termux" - if ! adb shell 'am start -n com.termux/.HomeActivity' ; then + if ! adb shell 'am start -n com.termux/.HomeActivity'; then echo "failed to launch termux" exit 1 fi @@ -57,76 +67,296 @@ launch_termux() { adb shell 'rm /sdcard/launch.probe' && echo "removed launch.probe" } +# Usage: run_termux_command +# +# Runs the command specified in $1 in a termux shell, polling for the probe specified in $2 (and the +# current output). If polling the probe succeeded the command is considered to have finished. This +# method prints the current stdout and stderr of the command every SLEEP_INTERVAL seconds and +# finishes a command run with a summary. It returns with the exit code of the probe if specified as +# file content of the probe. +# +# Positional arguments +# $1 The command to execute in the termux shell +# $2 The path to the probe. The file name must end with `.probe` +# +# It's possible to overwrite settings by specifying the setting the variable before calling this +# method (Default in parentheses): +# keep_log 0|1 Keeps the logs after running the command if set to 1. The log file name is +# derived from the probe file name (the last component of the path) and +# `.probe` replaced with `.log. (0) +# debug 0|1 Adds additional debugging output to the log file if set to 1. (1) +# timeout SECONDS The timeout in full SECONDS for the command to complete before giving up. (3600) +# retries RETRIES The number of retries for trying to fix possible issues when we're not receiving +# any progress from the emulator. (3) +# sleep_interval +# SECONDS The time interval in full SECONDS between polls for the probe and the current +# output. (5) run_termux_command() { - command="$1" # text of the escaped command, including creating the probe! - probe="$2" # unique file that indicates the command is complete + # shellcheck disable=SC2155 + local command="$(echo "$1" | sed -E "s/^['](.*)[']$/\1/")" # text of the escaped command, including creating the probe! + local probe="$2" # unique file that indicates the command is complete + local keep_log=${keep_log:-0} + local debug=${debug:-1} + + log_name="$(basename -s .probe "${probe}").log" # probe name must have suffix .probe + log_file="/sdcard/${log_name}" + log_read="${log_name}.read" + echo 0 >"${log_read}" + if [[ $debug -eq 1 ]]; then + shell_command="'set -x; { ${command}; } &> ${log_file}; set +x'" + else + shell_command="'{ ${command}; } &> ${log_file}'" + fi + launch_termux - adb shell input text "$command" && hit_enter - while ! adb shell "ls $probe" 2>/dev/null; do echo "waiting for $probe"; sleep 30; done - return_code=$(adb shell "cat $probe") - adb shell "rm $probe" - echo "return code: $return_code" + echo "Running command: ${command}" + start=$(date +%s) + adb shell input text "$shell_command" && sleep 3 && hit_enter + # just for safety wait a little bit before polling for the probe and the log file + sleep 5 + + local timeout=${timeout:-3600} + local retries=${retries:-10} + local sleep_interval=${sleep_interval:-10} + try_fix=3 + echo "run_termux_command with timeout=$timeout / retries=$retries / sleep_interval=$sleep_interval" + while ! adb shell "ls $probe" 2>/dev/null; do + echo -n "Waiting for $probe: " + + if [[ -e "$log_name" ]]; then + rm "$log_name" + fi + + adb pull "$log_file" . || try_fix=$((try_fix - 1)) + if [[ -e "$log_name" ]]; then + tail -n +"$(<"$log_read")" "$log_name" + echo + wc -l <"${log_name}" | tr -d "[:space:]" >"$log_read" + fi + + if [[ retries -le 0 ]]; then + echo "Maximum retries reached running command. Aborting ..." + return 1 + elif [[ try_fix -le 0 ]]; then + retries=$((retries - 1)) + try_fix=3 + # Since there is no output, there is no way to know what is happening inside. See if + # hitting the enter key solves the issue, sometimes the github runner is just a little + # bit slow. + echo "No output received. Trying to fix the issue ... (${retries} retries left)" + hit_enter + fi + + sleep "$sleep_interval" + timeout=$((timeout - sleep_interval)) + + if [[ $timeout -le 0 ]]; then + echo "Timeout reached running command. Aborting ..." + return 1 + fi + done + end=$(date +%s) + + return_code=$(adb shell "cat $probe") || return_code=0 + adb shell "rm ${probe}" + + adb pull "$log_file" . + echo "==================================== SUMMARY ===================================" + echo "Command: ${command}" + echo "Finished in $((end - start)) seconds." + echo "Output was:" + cat "$log_name" + echo "Return code: $return_code" + echo "================================================================================" + + adb shell "rm ${log_file}" + [[ $keep_log -ne 1 ]] && rm -f "$log_name" + rm -f "$log_read" "$probe" + + # shellcheck disable=SC2086 return $return_code } -snapshot () { +init() { + arch="$1" + api_level="$2" + termux="$3" + + # shellcheck disable=SC2015 + wget "https://github.com/termux/termux-app/releases/download/${termux}/termux-app_${termux}+github-debug_${arch}.apk" && + snapshot "termux-app_${termux}+github-debug_${arch}.apk" && + hash_rustc && + exit_termux && + adb -s emulator-5554 emu avd snapshot save "${api_level}-${arch}+termux-${termux}" && + echo "Emulator image created." || { + pkill -9 qemu-system-x86_64 + return 1 + } + pkill -9 qemu-system-x86_64 || true +} + +snapshot() { apk="$1" - echo "running snapshot" + echo "Running snapshot" adb install -g "$apk" + + echo "Prepare and install system packages" probe='/sdcard/pkg.probe' - command="'yes | pkg install rust binutils openssl -y; touch $probe'" + command="'mkdir -vp ~/.cargo/bin; yes | pkg install rust binutils openssl tar -y; echo \$? > $probe'" + run_termux_command "$command" "$probe" || return + + echo "Installing cargo-nextest" + probe='/sdcard/nextest.probe' + # We need to install nextest via cargo currently, since there is no pre-built binary for android x86 + command="'\ +export CARGO_TERM_COLOR=always; \ +cargo install cargo-nextest; \ +echo \$? > $probe'" run_termux_command "$command" "$probe" - echo "snapshot complete" - adb shell input text "exit" && hit_enter && hit_enter + return_code=$? + + echo "Info about cargo and rust" + probe='/sdcard/info.probe' + command="'echo \$HOME; \ +PATH=\$HOME/.cargo/bin:\$PATH; \ +export PATH; \ +echo \$PATH; \ +pwd; \ +command -v rustc && rustc -Vv; \ +ls -la ~/.cargo/bin; \ +cargo --list; \ +cargo nextest --version; \ +touch $probe'" + run_termux_command "$command" "$probe" + + echo "Snapshot complete" + # shellcheck disable=SC2086 + return $return_code } -sync () { +sync_host() { repo="$1" - echo "running sync $1" + cache_home="${HOME}/${cache_dir_name}" + cache_dest="/sdcard/${cache_dir_name}" + + echo "Running sync host -> image: ${repo}" + # android doesn't allow symlinks on shared dirs, and adb can't selectively push files symlinks=$(find "$repo" -type l) # dash doesn't support process substitution :( - echo $symlinks | sort >symlinks + echo "$symlinks" | sort >symlinks + git -C "$repo" diff --name-status | cut -f 2 >modified modified_links=$(join symlinks modified) - if [ ! -z "$modified_links" ]; then + if [ -n "$modified_links" ]; then echo "You have modified symlinks. Either stash or commit them, then try again: $modified_links" exit 1 fi + #shellcheck disable=SC2086 if ! git ls-files --error-unmatch $symlinks >/dev/null; then echo "You have untracked symlinks. Either remove or commit them, then try again." exit 1 fi + + #shellcheck disable=SC2086 rm $symlinks # adb's shell user only has access to shared dirs... - adb push "$repo" /sdcard/coreutils + adb push -a "$repo" /sdcard/coreutils + [[ -e "$cache_home" ]] && adb push -a "$cache_home" "$cache_dest" + + #shellcheck disable=SC2086 git -C "$repo" checkout $symlinks + # ...but shared dirs can't build, so move it home as termux - probe='/sdcard/mv.probe' - command="'cp -r /sdcard/coreutils ~/; touch $probe'" - run_termux_command "$command" "$probe" + probe='/sdcard/sync.probe' + command="'mv /sdcard/coreutils ~/; \ +cd ~/coreutils; \ +if [[ -e ${cache_dest} ]]; then \ +rm -rf ~/.cargo ./target; \ +tar xzf ${cache_dest}/cargo.tgz -C ~/; \ +ls -la ~/.cargo; \ +tar xzf ${cache_dest}/target.tgz; \ +ls -la ./target; \ +rm -rf ${cache_dest}; \ +fi; \ +touch $probe'" + run_termux_command "$command" "$probe" || return + + echo "Finished sync host -> image: ${repo}" +} + +sync_image() { + repo="$1" + cache_home="${HOME}/${cache_dir_name}" + cache_dest="/sdcard/${cache_dir_name}" + + echo "Running sync image -> host: ${repo}" + + probe='/sdcard/cache.probe' + command="'rm -rf /sdcard/coreutils ${cache_dest}; \ +mkdir -p ${cache_dest}; \ +cd ${cache_dest}; \ +tar czf cargo.tgz -C ~/ .cargo; \ +tar czf target.tgz -C ~/coreutils target; \ +ls -la ${cache_dest}; \ +echo \$? > ${probe}'" + run_termux_command "$command" "$probe" || return + + rm -rf "$cache_home" + adb pull -a "$cache_dest" "$cache_home" || return + + echo "Finished sync image -> host: ${repo}" } -build () { +build() { + echo "Running build" + probe='/sdcard/build.probe' - command="'cd ~/coreutils && cargo build --features feat_os_unix_android 2>/sdcard/build.log; echo \$? >$probe'" - echo "running build" - run_termux_command "$command" "$probe" - return_code=$? - adb pull /sdcard/build.log . - cat build.log - return $return_code + command="'export CARGO_TERM_COLOR=always; \ +export CARGO_INCREMENTAL=0; \ +cd ~/coreutils && cargo build --features feat_os_unix_android; \ +echo \$? >$probe'" + run_termux_command "$command" "$probe" || return + + echo "Finished build" } -tests () { +tests() { + echo "Running tests" + probe='/sdcard/tests.probe' - export RUST_BACKTRACE=1 - command="'cd ~/coreutils && timeout --preserve-status --verbose -k 1m 60m cargo test --features feat_os_unix_android --no-fail-fast >/sdcard/tests.log 2>&1; echo \$? >$probe'" - run_termux_command "$command" "$probe" - return_code=$? - adb pull /sdcard/tests.log . - cat tests.log - return $return_code + command="'export PATH=\$HOME/.cargo/bin:\$PATH; \ +export RUST_BACKTRACE=1; \ +export CARGO_TERM_COLOR=always; \ +export CARGO_INCREMENTAL=0; \ +cd ~/coreutils; \ +timeout --preserve-status --verbose -k 1m 60m \ +cargo nextest run --profile ci --hide-progress-bar --features feat_os_unix_android; \ +echo \$? >$probe'" + run_termux_command "$command" "$probe" || return + + echo "Finished tests" +} + +hash_rustc() { + probe='/sdcard/rustc.probe' + tmp_hash="__rustc_hash__.tmp" + hash="__rustc_hash__" + + echo "Hashing rustc version: ${HOME}/${hash}" + + command="'rustc -Vv; echo \$? > ${probe}'" + keep_log=1 + debug=0 + run_termux_command "$command" "$probe" || return + rm -f "$tmp_hash" + mv "rustc.log" "$tmp_hash" || return + # sha256sum is not available. shasum is the macos native program. + shasum -a 256 "$tmp_hash" | cut -f 1 -d ' ' | tr -d '[:space:]' >"${HOME}/${hash}" || return + + rm -f "$tmp_hash" + + echo "Finished hashing rustc version: ${HOME}/${hash}" } #adb logcat & @@ -134,16 +364,54 @@ exit_code=0 if [ $# -eq 1 ]; then case "$1" in - sync) sync "$this_repo"; exit_code=$?;; - build) build; exit_code=$?;; - tests) tests; exit_code=$?;; - *) help;; + sync_host) + sync_host "$this_repo" + exit_code=$? + ;; + sync_image) + sync_image "$this_repo" + exit_code=$? + ;; + build) + build + exit_code=$? + ;; + tests) + tests + exit_code=$? + ;; + *) help ;; esac elif [ $# -eq 2 ]; then case "$1" in - snapshot) snapshot "$2"; exit_code=$?;; - sync) sync "$2"; exit_code=$?;; - *) help; exit 1;; + snapshot) + snapshot "$2" + exit_code=$? + ;; + sync_host) + sync_host "$2" + exit_code=$? + ;; + sync_image) + sync_image "$2" + exit_code=$? + ;; + *) + help + exit 1 + ;; + esac +elif [ $# -eq 4 ]; then + case "$1" in + init) + shift + init "$@" + exit_code=$? + ;; + *) + help + exit 1 + ;; esac else help diff --git a/util/build-gnu.sh b/util/build-gnu.sh index dad97086636..13fef7bb921 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -1,7 +1,7 @@ #!/bin/bash # `build-gnu.bash` ~ builds GNU coreutils (from supplied sources) # -# UU_MAKE_PROFILE == 'debug' | 'release' ## build profile for *uutils* build; may be supplied by caller, defaults to 'debug' +# UU_MAKE_PROFILE == 'debug' | 'release' ## build profile for *uutils* build; may be supplied by caller, defaults to 'release' # spell-checker:ignore (paths) abmon deref discrim eacces getlimits getopt ginstall inacc infloop inotify reflink ; (misc) INT_OFLOW OFLOW baddecode submodules ; (vars/env) SRCDIR vdir rcexp xpart @@ -63,11 +63,13 @@ for binary in $(./build-aux/gen-lists-of-programs.sh --list-progs); do done if test -f gnu-built; then + # Change the PATH in the Makefile to test the uutils coreutils instead of the GNU coreutils + sed -i "s/^[[:blank:]]*PATH=.*/ PATH='${UU_BUILD_DIR//\//\\/}\$(PATH_SEPARATOR)'\"\$\$PATH\" \\\/" Makefile echo "GNU build already found. Skip" echo "'rm -f $(pwd)/gnu-built' to force the build" echo "Note: the customization of the tests will still happen" else - ./bootstrap + ./bootstrap --skip-po ./configure --quiet --disable-gcc-warnings #Add timeout to to protect against hangs sed -i 's|^"\$@|/usr/bin/timeout 600 "\$@|' build-aux/test-driver @@ -164,6 +166,9 @@ sed -i -e "s|rm: cannot remove 'a/1'|rm: cannot remove 'a'|g" tests/rm/rm2.sh sed -i -e "s|removed directory 'a/'|removed directory 'a'|g" tests/rm/v-slash.sh +# 'rel' doesn't exist. Our implementation is giving a better message. +sed -i -e "s|rm: cannot remove 'rel': Permission denied|rm: cannot remove 'rel': No such file or directory|g" tests/rm/inaccessible.sh + # overlay-headers.sh test intends to check for inotify events, # however there's a bug because `---dis` is an alias for: `---disable-inotify` sed -i -e "s|---dis ||g" tests/tail-2/overlay-headers.sh @@ -220,7 +225,10 @@ sed -i -Ez "s/\n([^\n#]*pad-3\.2[^\n]*)\n([^\n]*)\n([^\n]*)/\n# uutils\/numfmt s # Update the GNU error message to match the one generated by clap sed -i -e "s/\$prog: multiple field specifications/error: The argument '--field ' was provided more than once, but cannot be used multiple times\n\nUsage: numfmt [OPTION]... [NUMBER]...\n\n\nFor more information try '--help'/g" tests/misc/numfmt.pl +sed -i -e "s/Try 'mv --help' for more information/For more information, try '--help'/g" -e "s/mv: missing file operand/error: the following required arguments were not provided:\n ...\n\nUsage: mv [OPTION]... [-T] SOURCE DEST\n mv [OPTION]... SOURCE... DIRECTORY\n mv [OPTION]... -t DIRECTORY SOURCE...\n/g" -e "s/mv: missing destination file operand after 'no-file'/error: The argument '...' requires at least 2 values, but only 1 was provided\n\nUsage: mv [OPTION]... [-T] SOURCE DEST\n mv [OPTION]... SOURCE... DIRECTORY\n mv [OPTION]... -t DIRECTORY SOURCE...\n/g" tests/mv/diag.sh # GNU doesn't support width > INT_MAX # disable these test cases sed -i -E "s|^([^#]*2_31.*)$|#\1|g" tests/misc/printf-cov.pl + +sed -i -e "s/du: invalid -t argument/du: invalid --threshold argument/" -e "s/du: option requires an argument/error: a value is required for '--threshold ' but none was supplied/" -e "/Try 'du --help' for more information./d" tests/du/threshold.sh diff --git a/util/publish.sh b/util/publish.sh index d9039fa606d..71830f1f915 100755 --- a/util/publish.sh +++ b/util/publish.sh @@ -1,4 +1,5 @@ #!/bin/sh +# spell-checker:ignore uuhelp ARG="" if test "$1" != "--do-it"; then ARG="--dry-run --allow-dirty" @@ -35,7 +36,7 @@ TOTAL_ORDER=$(echo -e $PARTIAL_ORDER | tsort | tac) TOTAL_ORDER=${TOTAL_ORDER#ROOT} set -e -for dir in src/uucore_procs/ src/uucore/ src/uu/stdbuf/src/libstdbuf/; do +for dir in src/uuhelp_parser/ src/uucore_procs/ src/uucore/ src/uu/stdbuf/src/libstdbuf/; do ( cd "$dir" #shellcheck disable=SC2086 diff --git a/util/remaining-gnu-error.py b/util/remaining-gnu-error.py index c64ee974ef5..5fd47300ab2 100755 --- a/util/remaining-gnu-error.py +++ b/util/remaining-gnu-error.py @@ -8,6 +8,7 @@ import os import glob import json +import sys base = "../gnu/tests/" urllib.request.urlretrieve( @@ -18,7 +19,8 @@ types = ("/*/*.sh", "/*/*.pl", "/*/*.xpl") tests = [] -error_or_skip_tests = [] +error_tests = [] +skip_tests = [] for files in types: tests.extend(glob.glob(base + files)) @@ -51,16 +53,29 @@ def show_list(l): # the tests pass, we don't care anymore if data[d][e] == "PASS": - list_of_files.remove(a) + try: + list_of_files.remove(a) + except ValueError: + print("Could not find test '%s'. Maybe update the GNU repo?" % a) + sys.exit(1) - # if it is SKIP or ERROR, show it - if data[d][e] == "SKIP" or data[d][e] == "ERROR": + # if it is SKIP, show it + if data[d][e] == "SKIP": list_of_files.remove(a) - error_or_skip_tests.append(a) + skip_tests.append(a) + # if it is ERROR, show it + if data[d][e] == "ERROR": + list_of_files.remove(a) + error_tests.append(a) -print("SKIP and ERROR tests:") -show_list(error_or_skip_tests) +print("===============") +print("SKIP tests:") +show_list(skip_tests) +print("") +print("===============") +print("ERROR tests:") +show_list(error_tests) print("") print("===============") print("FAIL tests:") diff --git a/util/run-gnu-test.sh b/util/run-gnu-test.sh index f5c47e6450a..ac736abe193 100755 --- a/util/run-gnu-test.sh +++ b/util/run-gnu-test.sh @@ -1,8 +1,6 @@ #!/bin/sh # `run-gnu-test.bash [TEST]` # run GNU test (or all tests if TEST is missing/null) -# -# UU_MAKE_PROFILE == 'debug' | 'release' ## build profile used for *uutils* build; may be supplied by caller, defaults to 'release' # spell-checker:ignore (env/vars) GNULIB SRCDIR SUBDIRS ; (utils) shellcheck @@ -62,4 +60,4 @@ else sudo make -j "$(nproc)" check-root SUBDIRS=. RUN_EXPENSIVE_TESTS=yes RUN_VERY_EXPENSIVE_TESTS=yes VERBOSE=no gl_public_submodule_commit="" srcdir="${path_GNU}" TEST_SUITE_LOG="tests/test-suite-root.log" || : fi fi -fi \ No newline at end of file +fi diff --git a/util/update-version.sh b/util/update-version.sh index 4b7b6d554e4..0fd15422ade 100755 --- a/util/update-version.sh +++ b/util/update-version.sh @@ -1,4 +1,6 @@ #!/bin/sh +# spell-checker:ignore uuhelp + # This is a stupid helper. I will mass replace all versions (including other crates) # So, it should be triple-checked @@ -7,12 +9,13 @@ # 2) run it: sh util/update-version.sh # 3) Do a spot check with "git diff" # 4) cargo test --release --features unix -# 5) Run util/publish.sh in dry mode (it will fail as packages needs more recent version of uucore) -# 6) Run util/publish.sh --do-it -# 7) In some cases, you might have to fix dependencies and run import +# 5) git commit -m "New release" +# 6) Run util/publish.sh in dry mode (it will fail as packages needs more recent version of uucore) +# 7) Run util/publish.sh --do-it +# 8) In some cases, you might have to fix dependencies and run import -FROM="0.0.16" -TO="0.0.17" +FROM="0.0.18" +TO="0.0.19" PROGS=$(ls -1d src/uu/*/Cargo.toml src/uu/stdbuf/src/libstdbuf/Cargo.toml src/uucore/Cargo.toml Cargo.toml) @@ -23,6 +26,9 @@ sed -i -e "s|version = \"$FROM\"|version = \"$TO\"|" $PROGS # Update uucore_procs sed -i -e "s|version = \"$FROM\"|version = \"$TO\"|" src/uucore_procs/Cargo.toml +# Update uuhelp_parser +sed -i -e "s|version = \"$FROM\"|version = \"$TO\"|" src/uuhelp_parser/Cargo.toml + # Update the stdbuf stuff sed -i -e "s|libstdbuf = { version=\"$FROM\"|libstdbuf = { version=\"$TO\"|" src/uu/stdbuf/Cargo.toml sed -i -e "s|= { optional=true, version=\"$FROM\", package=\"uu_|= { optional=true, version=\"$TO\", package=\"uu_|g" Cargo.toml diff --git a/util/why-skip.txt b/util/why-skip.txt new file mode 100644 index 00000000000..c790311c13d --- /dev/null +++ b/util/why-skip.txt @@ -0,0 +1,98 @@ +# spell-checker:ignore epipe readdir restorecon SIGALRM capget bigtime rootfs enotsup + += trapping SIGPIPE is not supported = +tests/tail-2/pipe-f.sh +tests/misc/seq-epipe.sh +tests/misc/printf-surprise.sh +tests/misc/env-signal-handler.sh + += skipped test: breakpoint not hit = +tests/tail-2/inotify-race2.sh +tail-2/inotify-race.sh + += internal test failure: maybe LD_PRELOAD doesn't work? = +tests/rm/rm-readdir-fail.sh +tests/rm/r-root.sh +tests/df/skip-duplicates.sh +tests/df/no-mtab-status.sh + += LD_PRELOAD was ineffective? = +tests/cp/nfs-removal-race.sh + += failed to create hfs file system = +tests/mv/hardlink-case.sh + += temporarily disabled = +tests/mkdir/writable-under-readonly.sh + += this system lacks SMACK support = +tests/mkdir/smack-root.sh +tests/mkdir/smack-no-root.sh +tests/id/smack.sh + += this system lacks SELinux support = +tests/mkdir/selinux.sh +tests/mkdir/restorecon.sh +tests/misc/selinux.sh +tests/misc/chcon.sh +tests/install/install-Z-selinux.sh +tests/install/install-C-selinux.sh +tests/id/no-context.sh +tests/id/context.sh +tests/cp/no-ctx.sh +tests/cp/cp-a-selinux.sh + += failed to set xattr of file = +tests/misc/xattr.sh + += timeout returned 142. SIGALRM not handled? = +tests/misc/timeout-group.sh + += FULL_PARTITION_TMPDIR not defined = +tests/misc/tac-continue.sh + += can't get window size = +tests/misc/stty-row-col.sh + += The Swedish locale with blank thousands separator is unavailable. = +tests/misc/sort-h-thousands-sep.sh + += this shell lacks ulimit support = +tests/misc/csplit-heap.sh + += multicall binary is disabled = +tests/misc/coreutils.sh + += your ls doesn't call capget = +tests/ls/no-cap.sh + + += not running on GNU/Hurd = +tests/id/gnu-zero-uids.sh + += file system cannot represent big timestamps = +tests/du/bigtime.sh + += no rootfs in mtab = +tests/df/skip-rootfs.sh + += insufficient mount/ext2 support = +tests/df/problematic-chars.sh +tests/cp/cp-mv-enotsup-xattr.sh + += 512 byte aligned O_DIRECT is not supported on this (file) system = +tests/dd/direct.sh + += skipped test: /usr/bin/touch -m -d '1998-01-15 23:00' didn't work = +tests/misc/ls-time.sh + += requires controlling input terminal = +tests/misc/stty-pairs.sh +tests/misc/stty.sh +tests/misc/stty-invalid.sh + += insufficient SEEK_DATA support = +tests/cp/sparse-perf.sh +tests/cp/sparse-extents.sh +tests/cp/sparse-extents-2.sh +tests/cp/sparse-2.sh