diff --git a/.cargo/config b/.cargo/config.toml similarity index 87% rename from .cargo/config rename to .cargo/config.toml index a4dc4b423fe..d7db3025da8 100644 --- a/.cargo/config +++ b/.cargo/config.toml @@ -1,7 +1,7 @@ [target.x86_64-unknown-redox] linker = "x86_64-unknown-redox-gcc" -[target.'cfg(feature = "cargo-clippy")'] +[target.'cfg(clippy)'] rustflags = [ "-Wclippy::use_self", "-Wclippy::needless_pass_by_value", diff --git a/.clippy.toml b/.clippy.toml index 814e40b6960..b1552463edd 100644 --- a/.clippy.toml +++ b/.clippy.toml @@ -1,2 +1,2 @@ msrv = "1.70.0" -cognitive-complexity-threshold = 10 +cognitive-complexity-threshold = 24 diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index ad12ede6833..f61b49ea27e 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -18,6 +18,7 @@ env: on: pull_request: push: + tags: branches: - main @@ -110,7 +111,7 @@ jobs: components: clippy - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.3 + uses: mozilla-actions/sccache-action@v0.0.4 - name: Initialize workflow variables id: vars shell: bash @@ -164,7 +165,7 @@ jobs: - uses: taiki-e/install-action@nextest - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.3 + uses: mozilla-actions/sccache-action@v0.0.4 - name: Initialize workflow variables id: vars shell: bash @@ -252,7 +253,7 @@ jobs: - uses: taiki-e/install-action@nextest - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.3 + uses: mozilla-actions/sccache-action@v0.0.4 - name: "`make build`" shell: bash run: | @@ -306,7 +307,7 @@ jobs: - uses: taiki-e/install-action@nextest - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.3 + uses: mozilla-actions/sccache-action@v0.0.4 - name: Test run: cargo nextest run --hide-progress-bar --profile ci ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} env: @@ -333,7 +334,7 @@ jobs: - uses: taiki-e/install-action@nextest - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.3 + uses: mozilla-actions/sccache-action@v0.0.4 - name: Test run: cargo nextest run --hide-progress-bar --profile ci ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} env: @@ -356,7 +357,7 @@ jobs: - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.3 + uses: mozilla-actions/sccache-action@v0.0.4 - name: Install dependencies shell: bash run: | @@ -466,16 +467,16 @@ jobs: matrix: job: # - { os , target , cargo-options , features , use-cross , toolchain, skip-tests } - - { os: ubuntu-latest , target: arm-unknown-linux-gnueabihf, features: feat_os_unix_gnueabihf, use-cross: use-cross, } - - { os: ubuntu-latest , target: aarch64-unknown-linux-gnu , features: feat_os_unix_gnueabihf , use-cross: use-cross } - - { os: ubuntu-latest , target: aarch64-unknown-linux-musl , features: feat_os_unix_musl , use-cross: use-cross } + - { os: ubuntu-latest , target: arm-unknown-linux-gnueabihf, features: feat_os_unix_gnueabihf, use-cross: use-cross, skip-tests: true } + - { os: ubuntu-latest , target: aarch64-unknown-linux-gnu , features: feat_os_unix_gnueabihf , use-cross: use-cross , skip-tests: true } + - { os: ubuntu-latest , target: aarch64-unknown-linux-musl , features: feat_os_unix_musl , use-cross: use-cross , skip-tests: true } # - { os: ubuntu-latest , target: x86_64-unknown-linux-gnu , features: feat_selinux , use-cross: use-cross } - { os: ubuntu-latest , target: i686-unknown-linux-gnu , features: feat_os_unix , use-cross: use-cross } - { os: ubuntu-latest , target: i686-unknown-linux-musl , features: feat_os_unix_musl , use-cross: use-cross } - { os: ubuntu-latest , target: x86_64-unknown-linux-gnu , features: feat_os_unix , use-cross: use-cross } - { os: ubuntu-latest , target: x86_64-unknown-linux-musl , features: feat_os_unix_musl , use-cross: use-cross } - - { os: ubuntu-latest , target: x86_64-unknown-redox , features: feat_os_unix_redox , use-cross: redoxer , skip-tests: true } - - { os: macos-latest , target: aarch64-apple-darwin , features: feat_os_macos , use-cross: use-cross, skip-tests: true} # Hopefully github provides free M1 runners soon... + - { os: ubuntu-latest , target: x86_64-unknown-redox , features: feat_os_unix_redox , use-cross: redoxer , skip-tests: true } + - { os: macos-14 , target: aarch64-apple-darwin , features: feat_os_macos } # M1 CPU - { os: macos-latest , target: x86_64-apple-darwin , features: feat_os_macos } - { os: windows-latest , target: i686-pc-windows-msvc , features: feat_os_windows } - { os: windows-latest , target: x86_64-pc-windows-gnu , features: feat_os_windows } @@ -490,7 +491,7 @@ jobs: with: key: "${{ matrix.job.os }}_${{ matrix.job.target }}" - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.3 + uses: mozilla-actions/sccache-action@v0.0.4 - name: Initialize workflow variables id: vars shell: bash @@ -585,9 +586,6 @@ jobs: if [ "${CARGO_CMD}" = 'cross' ] && [ ! -e "Cross.toml" ] ; then 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; - outputs CARGO_TEST_OPTIONS # * executable for `strip`? STRIP="strip" case ${{ matrix.job.target }} in @@ -612,15 +610,15 @@ jobs: run: | ## Install/setup prerequisites case '${{ matrix.job.target }}' in - arm-unknown-linux-gnueabihf) + arm-unknown-linux-gnueabihf) sudo apt-get -y update sudo apt-get -y install gcc-arm-linux-gnueabihf ;; - aarch64-unknown-linux-*) + aarch64-unknown-linux-*) sudo apt-get -y update sudo apt-get -y install gcc-aarch64-linux-gnu ;; - *-redox*) + *-redox*) sudo apt-get -y update sudo apt-get -y install fuse3 libfuse-dev ;; @@ -707,7 +705,7 @@ jobs: run: | ## Test individual utilities ${{ steps.vars.outputs.CARGO_CMD }} ${{ steps.vars.outputs.CARGO_CMD_OPTIONS }} test --target=${{ matrix.job.target }} \ - ${{ steps.vars.outputs.CARGO_TEST_OPTIONS}} ${{ matrix.job.cargo-options }} ${{ steps.dep_vars.outputs.CARGO_UTILITY_LIST_OPTIONS }} + ${{ matrix.job.cargo-options }} ${{ steps.dep_vars.outputs.CARGO_UTILITY_LIST_OPTIONS }} env: RUST_BACKTRACE: "1" - name: Archive executable artifacts @@ -750,7 +748,7 @@ jobs: fakeroot dpkg-deb --build "${DPKG_DIR}" "${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.DPKG_NAME }}" fi - name: Publish - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 if: steps.vars.outputs.DEPLOY with: files: | @@ -781,7 +779,7 @@ jobs: - uses: actions/checkout@v4 - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.3 + uses: mozilla-actions/sccache-action@v0.0.4 - name: Install/setup prerequisites shell: bash run: | @@ -865,7 +863,7 @@ jobs: components: rustfmt - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.3 + uses: mozilla-actions/sccache-action@v0.0.4 - name: Build coreutils as multiple binaries shell: bash run: | @@ -949,7 +947,7 @@ jobs: - uses: taiki-e/install-action@grcov - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.3 + uses: mozilla-actions/sccache-action@v0.0.4 # - name: Reattach HEAD ## may be needed for accurate code coverage info # run: git checkout ${{ github.head_ref }} - name: Initialize workflow variables @@ -1044,10 +1042,9 @@ jobs: ~/.cargo/bin/grcov . --output-type lcov --output-path "${COVERAGE_REPORT_FILE}" --branch --ignore build.rs --ignore "vendor/*" --ignore "/*" --ignore "[a-zA-Z]:/*" --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()" echo "report=${COVERAGE_REPORT_FILE}" >> $GITHUB_OUTPUT - name: Upload coverage results (to Codecov.io) - uses: codecov/codecov-action@v3 - # if: steps.vars.outputs.HAS_CODECOV_TOKEN + uses: codecov/codecov-action@v4 with: - # token: ${{ secrets.CODECOV_TOKEN }} + token: ${{ secrets.CODECOV_TOKEN }} file: ${{ steps.coverage.outputs.report }} ## flags: IntegrationTests, UnitTests, ${{ steps.vars.outputs.CODECOV_FLAGS }} flags: ${{ steps.vars.outputs.CODECOV_FLAGS }} diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index 89fa5a7da50..6a4676a7913 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -397,8 +397,9 @@ jobs: grcov . --output-type lcov --output-path "${COVERAGE_REPORT_FILE}" --branch --ignore build.rs --ignore "vendor/*" --ignore "/*" --ignore "[a-zA-Z]:/*" --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()" echo "report=${COVERAGE_REPORT_FILE}" >> $GITHUB_OUTPUT - name: Upload coverage results (to Codecov.io) - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: + token: ${{ secrets.CODECOV_TOKEN }} file: ${{ steps.coverage.outputs.report }} flags: gnutests name: gnutests diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 86b5a89e83a..528c2ad496c 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -17,42 +17,113 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} +env: + TERMUX: v0.118.0 + KEY_POSTFIX: nextest+rustc-hash+adb+sshd+upgrade+XGB+inc18 + COMMON_EMULATOR_OPTIONS: -no-window -noaudio -no-boot-anim -camera-back none -gpu swiftshader_indirect + EMULATOR_DISK_SIZE: 12GB + EMULATOR_HEAP_SIZE: 2048M + EMULATOR_BOOT_TIMEOUT: 1200 # 20min + jobs: test_android: name: Test builds - runs-on: macos-latest timeout-minutes: 90 strategy: fail-fast: false matrix: + os: [ubuntu-latest] # , macos-latest + cores: [4] # , 6 + ram: [4096, 8192] api-level: [28] - target: [default] - arch: [x86] # , arm64-v8a + target: [google_apis_playstore] + arch: [x86, x86_64] # , arm64-v8a + exclude: + - ram: 8192 + arch: x86 + - ram: 4096 + arch: x86_64 + runs-on: ${{ matrix.os }} env: - TERMUX: v0.118.0 + EMULATOR_RAM_SIZE: ${{ matrix.ram }} + EMULATOR_CORES: ${{ matrix.cores }} + RUNNER_OS: ${{ matrix.os }} + AVD_CACHE_KEY: "set later due to limitations of github actions not able to concatenate env variables" steps: + - name: Concatenate values to environment file + run: | + echo "AVD_CACHE_KEY=${{ matrix.os }}-${{ matrix.cores }}-${{ matrix.ram }}-${{ matrix.api-level }}-${{ matrix.target }}-${{ matrix.arch }}+termux-${{ env.TERMUX }}+${{ env.KEY_POSTFIX }}" >> $GITHUB_ENV + - name: Collect information about runner + if: always() + continue-on-error: true + run: | + hostname + uname -a + free -mh + df -Th + cat /proc/cpuinfo + - name: (Linux) create links from home to data partition + if: ${{ runner.os == 'Linux' }} + continue-on-error: true + run: | + ls -lah /mnt/ + cat /mnt/DATALOSS_WARNING_README.txt + sudo mkdir /mnt/data + sudo chmod a+rwx /mnt/data + mkdir /mnt/data/.android && ln -s /mnt/data/.android ~/.android + mkdir /mnt/data/work && ln -s /mnt/data/work ~/work + - name: Enable KVM group perms (linux hardware acceleration) + if: ${{ runner.os == 'Linux' }} + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm - uses: actions/checkout@v4 + - name: Collect information about runner + if: always() + continue-on-error: true + run: | + free -mh + df -Th - name: Restore AVD cache uses: actions/cache/restore@v4 id: avd-cache + continue-on-error: true with: path: | ~/.android/avd/* ~/.android/avd/*/snapshots/* ~/.android/adb* ~/__rustc_hash__ - key: avd-${{ matrix.api-level }}-${{ matrix.arch }}+termux-${{ env.TERMUX }}+nextest+rustc-hash + key: avd-${{ env.AVD_CACHE_KEY }} + - name: Collect information about runner after AVD cache + if: always() + continue-on-error: true + run: | + free -mh + df -Th + ls -lah /mnt/data + du -sch /mnt/data + - name: Delete AVD Lockfile when run from cache + if: steps.avd-cache.outputs.cache-hit == 'true' + run: | + rm -f \ + ~/.android/avd/*.avd/*.lock \ + ~/.android/avd/*/*.lock - name: Create and cache emulator image if: steps.avd-cache.outputs.cache-hit != 'true' - uses: reactivecircus/android-emulator-runner@v2 + uses: reactivecircus/android-emulator-runner@v2.30.1 with: api-level: ${{ matrix.api-level }} target: ${{ matrix.target }} arch: ${{ matrix.arch }} - ram-size: 2048M - disk-size: 7GB + ram-size: ${{ env.EMULATOR_RAM_SIZE }} + heap-size: ${{ env.EMULATOR_HEAP_SIZE }} + disk-size: ${{ env.EMULATOR_DISK_SIZE }} + cores: ${{ env.EMULATOR_CORES }} force-avd-creation: true - emulator-options: -no-window -no-snapshot-load -noaudio -no-boot-anim -camera-back none + emulator-options: ${{ env.COMMON_EMULATOR_OPTIONS }} -no-snapshot-load + emulator-boot-timeout: ${{ env.EMULATOR_BOOT_TIMEOUT }} script: | util/android-commands.sh init "${{ matrix.arch }}" "${{ matrix.api-level }}" "${{ env.TERMUX }}" - name: Save AVD cache @@ -64,12 +135,12 @@ jobs: ~/.android/avd/*/snapshots/* ~/.android/adb* ~/__rustc_hash__ - key: avd-${{ matrix.api-level }}-${{ matrix.arch }}+termux-${{ env.TERMUX }}+nextest+rustc-hash + key: avd-${{ env.AVD_CACHE_KEY }} - uses: juliangruber/read-file-action@v1 id: read_rustc_hash with: # ~ expansion didn't work - path: /Users/runner/__rustc_hash__ + path: ${{ runner.os == 'Linux' && '/home/runner/__rustc_hash__' || '/Users/runner/__rustc_hash__' }} trim: true - name: Restore rust cache id: rust-cache @@ -79,16 +150,25 @@ jobs: # 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: Collect information about runner ressources + if: always() + continue-on-error: true + run: | + free -mh + df -Th - name: Build and Test - uses: reactivecircus/android-emulator-runner@v2 + uses: reactivecircus/android-emulator-runner@v2.30.1 with: api-level: ${{ matrix.api-level }} target: ${{ matrix.target }} arch: ${{ matrix.arch }} - ram-size: 2048M - disk-size: 7GB + ram-size: ${{ env.EMULATOR_RAM_SIZE }} + heap-size: ${{ env.EMULATOR_HEAP_SIZE }} + disk-size: ${{ env.EMULATOR_DISK_SIZE }} + cores: ${{ env.EMULATOR_CORES }} force-avd-creation: false - emulator-options: -no-window -no-snapshot-save -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -snapshot ${{ matrix.api-level }}-${{ matrix.arch }}+termux-${{ env.TERMUX }} + emulator-options: ${{ env.COMMON_EMULATOR_OPTIONS }} -no-snapshot-save -snapshot ${{ env.AVD_CACHE_KEY }} + emulator-boot-timeout: ${{ env.EMULATOR_BOOT_TIMEOUT }} # 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. @@ -97,9 +177,27 @@ jobs: 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: Collect information about runner ressources + if: always() + continue-on-error: true + run: | + free -mh + df -Th - name: Save rust cache if: steps.rust-cache.outputs.cache-hit != 'true' uses: actions/cache/save@v4 with: path: ~/__rust_cache__ key: ${{ matrix.arch }}_${{ matrix.target}}_${{ steps.read_rustc_hash.outputs.content }}_${{ hashFiles('**/Cargo.toml', '**/Cargo.lock') }}_v3 + - name: archive any output (error screenshots) + if: always() + uses: actions/upload-artifact@v4 + with: + name: test_output_${{ env.AVD_CACHE_KEY }} + path: output + - name: Collect information about runner ressources + if: always() + continue-on-error: true + run: | + free -mh + df -Th diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 289830f8171..1879bfc78f6 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -86,7 +86,7 @@ jobs: components: clippy - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.3 + uses: mozilla-actions/sccache-action@v0.0.4 - name: Initialize workflow variables id: vars shell: bash @@ -121,7 +121,7 @@ jobs: run: | ## `cargo clippy` lint testing unset fault - CLIPPY_FLAGS="-W clippy::default_trait_access -W clippy::manual_string_new -W clippy::cognitive_complexity -W clippy::implicit_clone" + CLIPPY_FLAGS="-W clippy::default_trait_access -W clippy::manual_string_new -W clippy::cognitive_complexity -W clippy::implicit_clone -W clippy::range-plus-one -W clippy::redundant-clone" fault_type="${{ steps.vars.outputs.FAULT_TYPE }}" fault_prefix=$(echo "$fault_type" | tr '[:lower:]' '[:upper:]') # * convert any warnings to GHA UI annotations; ref: @@ -152,10 +152,7 @@ jobs: - name: Install/setup prerequisites shell: bash run: | - ## Install/setup prerequisites - # * pin installed cspell to v4.2.8 (cspell v5+ is broken for NodeJS < v12) - ## maint: [2021-11-10; rivy] `cspell` version may be advanced to v5 when used with NodeJS >= v12 - sudo apt-get -y update ; sudo apt-get -y install npm ; sudo npm install cspell@4.2.8 -g ; + sudo apt-get -y update ; sudo apt-get -y install npm ; sudo npm install cspell -g ; - name: Run `cspell` shell: bash run: | @@ -167,10 +164,7 @@ jobs: cfg_files=($(shopt -s nullglob ; echo {.vscode,.}/{,.}c[sS]pell{.json,.config{.js,.cjs,.json,.yaml,.yml},.yaml,.yml} ;)) cfg_file=${cfg_files[0]} unset CSPELL_CFG_OPTION ; if [ -n "$cfg_file" ]; then CSPELL_CFG_OPTION="--config $cfg_file" ; fi - # * `cspell` - ## maint: [2021-11-10; rivy] the `--no-progress` option for `cspell` is a `cspell` v5+ option - # S=$(cspell ${CSPELL_CFG_OPTION} --no-summary --no-progress "**/*") && printf "%s\n" "$S" || { printf "%s\n" "$S" ; printf "%s" "$S" | sed -E -n "s/${PWD//\//\\/}\/(.*):(.*):(.*) - (.*)/::${fault_type} file=\1,line=\2,col=\3::${fault_type^^}: \4 (file:'\1', line:\2)/p" ; fault=true ; true ; } - S=$(cspell ${CSPELL_CFG_OPTION} --no-summary "**/*") && printf "%s\n" "$S" || { printf "%s\n" "$S" ; printf "%s" "$S" | sed -E -n "s/${PWD//\//\\/}\/(.*):(.*):(.*) - (.*)/::${fault_type} file=\1,line=\2,col=\3::${fault_type^^}: \4 (file:'\1', line:\2)/p" ; fault=true ; true ; } + S=$(cspell ${CSPELL_CFG_OPTION} --no-summary --no-progress "**/*") && printf "%s\n" "$S" || { printf "%s\n" "$S" ; printf "%s" "$S" | sed -E -n "s/${PWD//\//\\/}\/(.*):(.*):(.*) - (.*)/::${fault_type} file=\1,line=\2,col=\3::${fault_type^^}: \4 (file:'\1', line:\2)/p" ; fault=true ; true ; } if [ -n "${{ steps.vars.outputs.FAIL_ON_FAULT }}" ] && [ -n "$fault" ]; then exit 1 ; fi toml_format: diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index 25655f0917f..a2b3562ca3a 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -37,9 +37,9 @@ jobs: - uses: actions/checkout@v4 - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.3 + uses: mozilla-actions/sccache-action@v0.0.4 - name: Prepare, build and test - uses: vmactions/freebsd-vm@v1.0.5 + uses: vmactions/freebsd-vm@v1.0.6 with: usesh: true sync: rsync @@ -129,9 +129,9 @@ jobs: - uses: actions/checkout@v4 - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.3 + uses: mozilla-actions/sccache-action@v0.0.4 - name: Prepare, build and test - uses: vmactions/freebsd-vm@v1.0.5 + uses: vmactions/freebsd-vm@v1.0.6 with: usesh: true sync: rsync diff --git a/.vscode/cspell.dictionaries/shell.wordlist.txt b/.vscode/cspell.dictionaries/shell.wordlist.txt index 95dea94a7cd..11ce341addf 100644 --- a/.vscode/cspell.dictionaries/shell.wordlist.txt +++ b/.vscode/cspell.dictionaries/shell.wordlist.txt @@ -25,6 +25,7 @@ sudoedit tcsh tzselect urandom +VARNAME wtmp zsh diff --git a/Cargo.lock b/Cargo.lock index e8c0ce0ca28..f4d616dc518 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -149,9 +149,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.0" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" [[package]] name = "blake2b_simd" @@ -166,9 +166,9 @@ dependencies = [ [[package]] name = "blake3" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0231f06152bf547e9c2b5194f247cd97aacf6dcd8b15d8e5ec0663f64580da87" +checksum = "30cca6d3674597c30ddf2c587bf8d9d65c9a84d2326d941cc79c9842dfe0ef52" dependencies = [ "arrayref", "arrayvec", @@ -188,9 +188,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.9.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c48f0051a4b4c5e0b6d365cd04af53aeaa209e3cc15ec2cdb69e73cc87fbd0dc" +checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" dependencies = [ "memchr", "regex-automata", @@ -236,11 +236,17 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "chrono" -version = "0.4.32" +version = "0.4.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41daef31d7a747c5c847246f36de49ced6f7403b4cdabc807a97b5cc184cda7a" +checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" dependencies = [ "android-tzdata", "iana-time-zone", @@ -374,7 +380,7 @@ checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" [[package]] name = "coreutils" -version = "0.0.24" +version = "0.0.25" dependencies = [ "chrono", "clap", @@ -541,7 +547,7 @@ dependencies = [ "lazy_static", "proc-macro2", "regex", - "syn 2.0.23", + "syn 2.0.32", "unicode-xid", ] @@ -553,7 +559,7 @@ checksum = "3e1a2532e4ed4ea13031c13bc7bc0dbca4aae32df48e9d77f0d1e743179f2ea1" dependencies = [ "lazy_static", "proc-macro2", - "syn 2.0.23", + "syn 2.0.32", ] [[package]] @@ -568,7 +574,7 @@ dependencies = [ "lazy_static", "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.32", ] [[package]] @@ -636,7 +642,7 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.2", "crossterm_winapi", "libc", "mio", @@ -673,12 +679,12 @@ dependencies = [ [[package]] name = "ctrlc" -version = "3.4.1" +version = "3.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e95fbd621905b854affdc67943b043a0fbb6ed7385fd5a25650d19a8a6cfdf" +checksum = "672465ae37dc1bc6380a6547a8883d5dd397b0f1faaad4f265726cc7042a5345" dependencies = [ "nix", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -790,11 +796,11 @@ dependencies = [ [[package]] name = "exacl" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c695152c1c2777163ea93fff517edc6dd1f8fc226c14b0d60cdcde0beb316d9f" +checksum = "22be12de19decddab85d09f251ec8363f060ccb22ec9c81bc157c0c8433946d8" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.2", "log", "scopeguard", "uuid", @@ -802,9 +808,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "file_diff" @@ -820,7 +826,7 @@ checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.4.1", "windows-sys 0.52.0", ] @@ -936,7 +942,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.32", ] [[package]] @@ -1010,9 +1016,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "half" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc52e53916c08643f1b56ec082790d1e86a32e58dc5268f897f313fbae7b4872" +checksum = "b5eceaaeec696539ddaf7b333340f1af35a5aa87ae3e4f3ead0532f72affab2e" dependencies = [ "cfg-if", "crunchy", @@ -1121,9 +1127,9 @@ dependencies = [ [[package]] name = "itertools" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ "either", ] @@ -1186,9 +1192,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.152" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libloading" @@ -1230,12 +1236,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "lscolors" @@ -1294,9 +1297,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "log", @@ -1306,12 +1309,13 @@ dependencies = [ [[package]] name = "nix" -version = "0.27.1" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.2", "cfg-if", + "cfg_aliases", "libc", ] @@ -1375,9 +1379,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", ] @@ -1462,7 +1466,7 @@ checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.4.1", "smallvec", "windows-targets 0.48.0", ] @@ -1586,11 +1590,11 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "731e0d9356b0c25f16f33b5be79b1c57b562f141ebfcdb0ad8ac2c13a24293b4" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.2", "hex", "lazy_static", "procfs-core", - "rustix 0.38.30", + "rustix 0.38.31", ] [[package]] @@ -1599,7 +1603,7 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d3554923a69f4ce04c4a754260c338f505ce22642d3830e049a399fc2059a29" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.2", "hex", ] @@ -1670,9 +1674,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" +checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd" dependencies = [ "either", "rayon-core", @@ -1680,9 +1684,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -1697,6 +1701,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c178f952cc7eac391f3124bd9851d1ac0bdbc4c9de2d892ccd5f0d8b160e96" +dependencies = [ + "bitflags 2.4.2", +] + [[package]] name = "reference-counted-singleton" version = "0.1.2" @@ -1705,9 +1718,9 @@ checksum = "f1bfbf25d7eb88ddcbb1ec3d755d0634da8f7657b2cb8b74089121409ab8228f" [[package]] name = "regex" -version = "1.10.3" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", @@ -1778,7 +1791,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.23", + "syn 2.0.32", "unicode-ident", ] @@ -1823,11 +1836,11 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.30" +version = "0.38.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" +checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.2", "errno", "libc", "linux-raw-sys 0.4.12", @@ -1845,9 +1858,9 @@ dependencies = [ [[package]] name = "scopeguard" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "self_cell" @@ -1889,9 +1902,23 @@ checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" [[package]] name = "serde" -version = "1.0.147" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.32", +] [[package]] name = "sha1" @@ -1927,9 +1954,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.1.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook" @@ -1987,9 +2014,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b187f0231d56fe41bfb12034819dd2bf336422a5866de41bc3fec4b2e3883e8" +checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" [[package]] name = "smawk" @@ -2026,9 +2053,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.23" +version = "2.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59fb7d6d8281a51045d62b8eb3a7d1ce347b76f312af50cd3dc0af39c87c1737" +checksum = "239814284fd6f1a4ffe4ca893952cdd93c224b6a1571c9a9eadd670295c0c9e2" dependencies = [ "proc-macro2", "quote", @@ -2037,14 +2064,13 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.9.0" +version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", - "redox_syscall", - "rustix 0.38.30", + "rustix 0.38.31", "windows-sys 0.52.0", ] @@ -2064,15 +2090,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" dependencies = [ - "rustix 0.38.30", + "rustix 0.38.31", "windows-sys 0.48.0", ] [[package]] name = "textwrap" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" dependencies = [ "smawk", "terminal_size 0.2.6", @@ -2158,9 +2184,9 @@ checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-segmentation" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode-width" @@ -2188,7 +2214,7 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uu_arch" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "platform-info", @@ -2197,7 +2223,7 @@ dependencies = [ [[package]] name = "uu_base32" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "uucore", @@ -2205,7 +2231,7 @@ dependencies = [ [[package]] name = "uu_base64" -version = "0.0.24" +version = "0.0.25" dependencies = [ "uu_base32", "uucore", @@ -2213,7 +2239,7 @@ dependencies = [ [[package]] name = "uu_basename" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "uucore", @@ -2221,7 +2247,7 @@ dependencies = [ [[package]] name = "uu_basenc" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "uu_base32", @@ -2230,7 +2256,7 @@ dependencies = [ [[package]] name = "uu_cat" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "nix", @@ -2240,7 +2266,7 @@ dependencies = [ [[package]] name = "uu_chcon" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "fts-sys", @@ -2252,7 +2278,7 @@ dependencies = [ [[package]] name = "uu_chgrp" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "uucore", @@ -2260,7 +2286,7 @@ dependencies = [ [[package]] name = "uu_chmod" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "libc", @@ -2269,7 +2295,7 @@ dependencies = [ [[package]] name = "uu_chown" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "uucore", @@ -2277,7 +2303,7 @@ dependencies = [ [[package]] name = "uu_chroot" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "uucore", @@ -2285,7 +2311,7 @@ dependencies = [ [[package]] name = "uu_cksum" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "hex", @@ -2294,7 +2320,7 @@ dependencies = [ [[package]] name = "uu_comm" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "uucore", @@ -2302,7 +2328,7 @@ dependencies = [ [[package]] name = "uu_cp" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "exacl", @@ -2318,7 +2344,7 @@ dependencies = [ [[package]] name = "uu_csplit" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "regex", @@ -2328,7 +2354,7 @@ dependencies = [ [[package]] name = "uu_cut" -version = "0.0.24" +version = "0.0.25" dependencies = [ "bstr", "clap", @@ -2338,7 +2364,7 @@ dependencies = [ [[package]] name = "uu_date" -version = "0.0.24" +version = "0.0.25" dependencies = [ "chrono", "clap", @@ -2350,7 +2376,7 @@ dependencies = [ [[package]] name = "uu_dd" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "gcd", @@ -2362,7 +2388,7 @@ dependencies = [ [[package]] name = "uu_df" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "tempfile", @@ -2372,7 +2398,7 @@ dependencies = [ [[package]] name = "uu_dir" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "uu_ls", @@ -2381,7 +2407,7 @@ dependencies = [ [[package]] name = "uu_dircolors" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "uucore", @@ -2389,7 +2415,7 @@ dependencies = [ [[package]] name = "uu_dirname" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "uucore", @@ -2397,7 +2423,7 @@ dependencies = [ [[package]] name = "uu_du" -version = "0.0.24" +version = "0.0.25" dependencies = [ "chrono", "clap", @@ -2408,7 +2434,7 @@ dependencies = [ [[package]] name = "uu_echo" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "uucore", @@ -2416,7 +2442,7 @@ dependencies = [ [[package]] name = "uu_env" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "nix", @@ -2426,7 +2452,7 @@ dependencies = [ [[package]] name = "uu_expand" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "unicode-width", @@ -2435,7 +2461,7 @@ dependencies = [ [[package]] name = "uu_expr" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "num-bigint", @@ -2446,7 +2472,7 @@ dependencies = [ [[package]] name = "uu_factor" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "coz", @@ -2459,7 +2485,7 @@ dependencies = [ [[package]] name = "uu_false" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "uucore", @@ -2467,7 +2493,7 @@ dependencies = [ [[package]] name = "uu_fmt" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "unicode-width", @@ -2476,7 +2502,7 @@ dependencies = [ [[package]] name = "uu_fold" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "uucore", @@ -2484,7 +2510,7 @@ dependencies = [ [[package]] name = "uu_groups" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "uucore", @@ -2492,7 +2518,7 @@ dependencies = [ [[package]] name = "uu_hashsum" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "hex", @@ -2503,7 +2529,7 @@ dependencies = [ [[package]] name = "uu_head" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "memchr", @@ -2512,7 +2538,7 @@ dependencies = [ [[package]] name = "uu_hostid" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "libc", @@ -2521,7 +2547,7 @@ dependencies = [ [[package]] name = "uu_hostname" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "hostname", @@ -2531,7 +2557,7 @@ dependencies = [ [[package]] name = "uu_id" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "selinux", @@ -2540,7 +2566,7 @@ dependencies = [ [[package]] name = "uu_install" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "file_diff", @@ -2551,7 +2577,7 @@ dependencies = [ [[package]] name = "uu_join" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "memchr", @@ -2560,7 +2586,7 @@ dependencies = [ [[package]] name = "uu_kill" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "nix", @@ -2569,7 +2595,7 @@ dependencies = [ [[package]] name = "uu_link" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "uucore", @@ -2577,7 +2603,7 @@ dependencies = [ [[package]] name = "uu_ln" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "uucore", @@ -2585,7 +2611,7 @@ dependencies = [ [[package]] name = "uu_logname" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "libc", @@ -2594,7 +2620,7 @@ dependencies = [ [[package]] name = "uu_ls" -version = "0.0.24" +version = "0.0.25" dependencies = [ "chrono", "clap", @@ -2612,7 +2638,7 @@ dependencies = [ [[package]] name = "uu_mkdir" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "uucore", @@ -2620,7 +2646,7 @@ dependencies = [ [[package]] name = "uu_mkfifo" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "libc", @@ -2629,7 +2655,7 @@ dependencies = [ [[package]] name = "uu_mknod" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "libc", @@ -2638,7 +2664,7 @@ dependencies = [ [[package]] name = "uu_mktemp" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "rand", @@ -2648,7 +2674,7 @@ dependencies = [ [[package]] name = "uu_more" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "crossterm", @@ -2660,7 +2686,7 @@ dependencies = [ [[package]] name = "uu_mv" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "fs_extra", @@ -2670,7 +2696,7 @@ dependencies = [ [[package]] name = "uu_nice" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "libc", @@ -2680,7 +2706,7 @@ dependencies = [ [[package]] name = "uu_nl" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "regex", @@ -2689,7 +2715,7 @@ dependencies = [ [[package]] name = "uu_nohup" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "libc", @@ -2698,7 +2724,7 @@ dependencies = [ [[package]] name = "uu_nproc" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "libc", @@ -2707,7 +2733,7 @@ dependencies = [ [[package]] name = "uu_numfmt" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "uucore", @@ -2715,7 +2741,7 @@ dependencies = [ [[package]] name = "uu_od" -version = "0.0.24" +version = "0.0.25" dependencies = [ "byteorder", "clap", @@ -2725,7 +2751,7 @@ dependencies = [ [[package]] name = "uu_paste" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "uucore", @@ -2733,7 +2759,7 @@ dependencies = [ [[package]] name = "uu_pathchk" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "libc", @@ -2742,7 +2768,7 @@ dependencies = [ [[package]] name = "uu_pinky" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "uucore", @@ -2750,7 +2776,7 @@ dependencies = [ [[package]] name = "uu_pr" -version = "0.0.24" +version = "0.0.25" dependencies = [ "chrono", "clap", @@ -2762,7 +2788,7 @@ dependencies = [ [[package]] name = "uu_printenv" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "uucore", @@ -2770,7 +2796,7 @@ dependencies = [ [[package]] name = "uu_printf" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "uucore", @@ -2778,7 +2804,7 @@ dependencies = [ [[package]] name = "uu_ptx" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "regex", @@ -2787,7 +2813,7 @@ dependencies = [ [[package]] name = "uu_pwd" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "uucore", @@ -2795,7 +2821,7 @@ dependencies = [ [[package]] name = "uu_readlink" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "uucore", @@ -2803,7 +2829,7 @@ dependencies = [ [[package]] name = "uu_realpath" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "uucore", @@ -2811,7 +2837,7 @@ dependencies = [ [[package]] name = "uu_rm" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "libc", @@ -2822,7 +2848,7 @@ dependencies = [ [[package]] name = "uu_rmdir" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "libc", @@ -2831,7 +2857,7 @@ dependencies = [ [[package]] name = "uu_runcon" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "libc", @@ -2842,7 +2868,7 @@ dependencies = [ [[package]] name = "uu_seq" -version = "0.0.24" +version = "0.0.25" dependencies = [ "bigdecimal", "clap", @@ -2853,7 +2879,7 @@ dependencies = [ [[package]] name = "uu_shred" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "libc", @@ -2863,7 +2889,7 @@ dependencies = [ [[package]] name = "uu_shuf" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "memchr", @@ -2874,7 +2900,7 @@ dependencies = [ [[package]] name = "uu_sleep" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "fundu", @@ -2883,7 +2909,7 @@ dependencies = [ [[package]] name = "uu_sort" -version = "0.0.24" +version = "0.0.25" dependencies = [ "binary-heap-plus", "clap", @@ -2902,7 +2928,7 @@ dependencies = [ [[package]] name = "uu_split" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "memchr", @@ -2911,15 +2937,16 @@ dependencies = [ [[package]] name = "uu_stat" -version = "0.0.24" +version = "0.0.25" dependencies = [ + "chrono", "clap", "uucore", ] [[package]] name = "uu_stdbuf" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "tempfile", @@ -2929,7 +2956,7 @@ dependencies = [ [[package]] name = "uu_stdbuf_libstdbuf" -version = "0.0.24" +version = "0.0.25" dependencies = [ "cpp", "cpp_build", @@ -2938,7 +2965,7 @@ dependencies = [ [[package]] name = "uu_stty" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "nix", @@ -2947,7 +2974,7 @@ dependencies = [ [[package]] name = "uu_sum" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "uucore", @@ -2955,7 +2982,7 @@ dependencies = [ [[package]] name = "uu_sync" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "libc", @@ -2966,7 +2993,7 @@ dependencies = [ [[package]] name = "uu_tac" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "memchr", @@ -2977,7 +3004,7 @@ dependencies = [ [[package]] name = "uu_tail" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "fundu", @@ -2993,7 +3020,7 @@ dependencies = [ [[package]] name = "uu_tee" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "libc", @@ -3002,17 +3029,17 @@ dependencies = [ [[package]] name = "uu_test" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "libc", - "redox_syscall", + "redox_syscall 0.5.0", "uucore", ] [[package]] name = "uu_timeout" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "libc", @@ -3022,7 +3049,7 @@ dependencies = [ [[package]] name = "uu_touch" -version = "0.0.24" +version = "0.0.25" dependencies = [ "chrono", "clap", @@ -3034,7 +3061,7 @@ dependencies = [ [[package]] name = "uu_tr" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "nom", @@ -3043,7 +3070,7 @@ dependencies = [ [[package]] name = "uu_true" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "uucore", @@ -3051,7 +3078,7 @@ dependencies = [ [[package]] name = "uu_truncate" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "uucore", @@ -3059,7 +3086,7 @@ dependencies = [ [[package]] name = "uu_tsort" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "uucore", @@ -3067,7 +3094,7 @@ dependencies = [ [[package]] name = "uu_tty" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "nix", @@ -3076,7 +3103,7 @@ dependencies = [ [[package]] name = "uu_uname" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "platform-info", @@ -3085,7 +3112,7 @@ dependencies = [ [[package]] name = "uu_unexpand" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "unicode-width", @@ -3094,7 +3121,7 @@ dependencies = [ [[package]] name = "uu_uniq" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "uucore", @@ -3102,7 +3129,7 @@ dependencies = [ [[package]] name = "uu_unlink" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "uucore", @@ -3110,7 +3137,7 @@ dependencies = [ [[package]] name = "uu_uptime" -version = "0.0.24" +version = "0.0.25" dependencies = [ "chrono", "clap", @@ -3119,7 +3146,7 @@ dependencies = [ [[package]] name = "uu_users" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "uucore", @@ -3127,7 +3154,7 @@ dependencies = [ [[package]] name = "uu_vdir" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "uu_ls", @@ -3136,7 +3163,7 @@ dependencies = [ [[package]] name = "uu_wc" -version = "0.0.24" +version = "0.0.25" dependencies = [ "bytecount", "clap", @@ -3149,7 +3176,7 @@ dependencies = [ [[package]] name = "uu_who" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "uucore", @@ -3157,7 +3184,7 @@ dependencies = [ [[package]] name = "uu_whoami" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "libc", @@ -3167,7 +3194,7 @@ dependencies = [ [[package]] name = "uu_yes" -version = "0.0.24" +version = "0.0.25" dependencies = [ "clap", "itertools", @@ -3177,7 +3204,7 @@ dependencies = [ [[package]] name = "uucore" -version = "0.0.24" +version = "0.0.25" dependencies = [ "blake2b_simd", "blake3", @@ -3214,7 +3241,7 @@ dependencies = [ [[package]] name = "uucore_procs" -version = "0.0.24" +version = "0.0.25" dependencies = [ "proc-macro2", "quote", @@ -3223,13 +3250,13 @@ dependencies = [ [[package]] name = "uuhelp_parser" -version = "0.0.24" +version = "0.0.25" [[package]] name = "uuid" -version = "1.2.2" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" +checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" [[package]] name = "uutils_term_grid" @@ -3248,9 +3275,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "walkdir" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", @@ -3283,7 +3310,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.32", "wasm-bindgen-shared", ] @@ -3305,7 +3332,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.32", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3329,9 +3356,9 @@ dependencies = [ [[package]] name = "wild" -version = "2.2.0" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10d01931a94d5a115a53f95292f51d316856b68a035618eb831bbba593a30b67" +checksum = "a3131afc8c575281e1e80f36ed6a092aa502c08b18ed7524e86fbbb12bb410e1" dependencies = [ "glob", ] @@ -3573,7 +3600,7 @@ checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" dependencies = [ "libc", "linux-raw-sys 0.4.12", - "rustix 0.38.30", + "rustix 0.38.31", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 757cacf3492..68bdf8faed9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ [package] name = "coreutils" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "coreutils ~ GNU coreutils (updated); implemented as universal (cross-platform) utils, written in Rust" @@ -260,10 +260,10 @@ test = ["uu_test"] [workspace.dependencies] bigdecimal = "0.4" binary-heap-plus = "0.5.0" -bstr = "1.9" +bstr = "1.9.1" bytecount = "0.6.7" byteorder = "1.5.0" -chrono = { version = "^0.4.32", default-features = false, features = [ +chrono = { version = "^0.4.35", default-features = false, features = [ "std", "alloc", "clock", @@ -275,7 +275,7 @@ compare = "0.1.0" coz = { version = "0.1.3" } crossterm = ">=0.27.0" ctrlc = { version = "3.4", features = ["termination"] } -exacl = "0.11.0" +exacl = "0.12.0" file_diff = "1.0.0" filetime = "0.2" fnv = "1.0.7" @@ -284,21 +284,21 @@ fts-sys = "0.2" fundu = "2.0.0" gcd = "2.3" glob = "0.3.1" -half = "2.3" +half = "2.4" hostname = "0.3" indicatif = "0.17" -itertools = "0.12.0" -libc = "0.2.152" +itertools = "0.12.1" +libc = "0.2.153" lscolors = { version = "0.16.0", default-features = false, features = [ "gnu_legacy", ] } memchr = "2" memmap2 = "0.9" -nix = { version = "0.27", default-features = false } +nix = { version = "0.28", default-features = false } nom = "7.1.3" notify = { version = "=6.0.1", features = ["macos_kqueue"] } num-bigint = "0.4.4" -num-traits = "0.2.17" +num-traits = "0.2.18" number_prefix = "0.4" once_cell = "1.19.0" onig = { version = "~6.4", default-features = false } @@ -309,9 +309,9 @@ platform-info = "2.0.2" quick-error = "2.0.1" rand = { version = "0.8", features = ["small_rng"] } rand_core = "0.6" -rayon = "1.8" -redox_syscall = "0.4" -regex = "1.10.3" +rayon = "1.9" +redox_syscall = "0.5" +regex = "1.10.4" rstest = "0.18.2" rust-ini = "0.19.0" same-file = "1.0.6" @@ -319,16 +319,16 @@ self_cell = "1.0.3" selinux = "0.4" signal-hook = "0.3.17" smallvec = { version = "1.13", features = ["union"] } -tempfile = "3.9.0" +tempfile = "3.10.1" uutils_term_grid = "0.3" terminal_size = "0.3.0" -textwrap = { version = "0.16.0", features = ["terminal_size"] } +textwrap = { version = "0.16.1", features = ["terminal_size"] } thiserror = "1.0" time = { version = "0.3" } -unicode-segmentation = "1.10.1" +unicode-segmentation = "1.11.0" unicode-width = "0.1.11" utf-8 = "0.7.6" -walkdir = "2.4" +walkdir = "2.5" winapi-util = "0.1.6" windows-sys = { version = "0.48.0", default-features = false } xattr = "1.3.1" @@ -340,7 +340,7 @@ sha1 = "0.10.6" sha2 = "0.10.8" sha3 = "0.10.8" blake2b_simd = "1.0.2" -blake3 = "1.5.0" +blake3 = "1.5.1" sm3 = "0.4.2" digest = "0.10.7" @@ -363,109 +363,109 @@ 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.24", package = "uu_test", path = "src/uu/test" } +uu_test = { optional = true, version = "0.0.25", package = "uu_test", path = "src/uu/test" } # -arch = { optional = true, version = "0.0.24", package = "uu_arch", path = "src/uu/arch" } -base32 = { optional = true, version = "0.0.24", package = "uu_base32", path = "src/uu/base32" } -base64 = { optional = true, version = "0.0.24", package = "uu_base64", path = "src/uu/base64" } -basename = { optional = true, version = "0.0.24", package = "uu_basename", path = "src/uu/basename" } -basenc = { optional = true, version = "0.0.24", package = "uu_basenc", path = "src/uu/basenc" } -cat = { optional = true, version = "0.0.24", package = "uu_cat", path = "src/uu/cat" } -chcon = { optional = true, version = "0.0.24", package = "uu_chcon", path = "src/uu/chcon" } -chgrp = { optional = true, version = "0.0.24", package = "uu_chgrp", path = "src/uu/chgrp" } -chmod = { optional = true, version = "0.0.24", package = "uu_chmod", path = "src/uu/chmod" } -chown = { optional = true, version = "0.0.24", package = "uu_chown", path = "src/uu/chown" } -chroot = { optional = true, version = "0.0.24", package = "uu_chroot", path = "src/uu/chroot" } -cksum = { optional = true, version = "0.0.24", package = "uu_cksum", path = "src/uu/cksum" } -comm = { optional = true, version = "0.0.24", package = "uu_comm", path = "src/uu/comm" } -cp = { optional = true, version = "0.0.24", package = "uu_cp", path = "src/uu/cp" } -csplit = { optional = true, version = "0.0.24", package = "uu_csplit", path = "src/uu/csplit" } -cut = { optional = true, version = "0.0.24", package = "uu_cut", path = "src/uu/cut" } -date = { optional = true, version = "0.0.24", package = "uu_date", path = "src/uu/date" } -dd = { optional = true, version = "0.0.24", package = "uu_dd", path = "src/uu/dd" } -df = { optional = true, version = "0.0.24", package = "uu_df", path = "src/uu/df" } -dir = { optional = true, version = "0.0.24", package = "uu_dir", path = "src/uu/dir" } -dircolors = { optional = true, version = "0.0.24", package = "uu_dircolors", path = "src/uu/dircolors" } -dirname = { optional = true, version = "0.0.24", package = "uu_dirname", path = "src/uu/dirname" } -du = { optional = true, version = "0.0.24", package = "uu_du", path = "src/uu/du" } -echo = { optional = true, version = "0.0.24", package = "uu_echo", path = "src/uu/echo" } -env = { optional = true, version = "0.0.24", package = "uu_env", path = "src/uu/env" } -expand = { optional = true, version = "0.0.24", package = "uu_expand", path = "src/uu/expand" } -expr = { optional = true, version = "0.0.24", package = "uu_expr", path = "src/uu/expr" } -factor = { optional = true, version = "0.0.24", package = "uu_factor", path = "src/uu/factor" } -false = { optional = true, version = "0.0.24", package = "uu_false", path = "src/uu/false" } -fmt = { optional = true, version = "0.0.24", package = "uu_fmt", path = "src/uu/fmt" } -fold = { optional = true, version = "0.0.24", package = "uu_fold", path = "src/uu/fold" } -groups = { optional = true, version = "0.0.24", package = "uu_groups", path = "src/uu/groups" } -hashsum = { optional = true, version = "0.0.24", package = "uu_hashsum", path = "src/uu/hashsum" } -head = { optional = true, version = "0.0.24", package = "uu_head", path = "src/uu/head" } -hostid = { optional = true, version = "0.0.24", package = "uu_hostid", path = "src/uu/hostid" } -hostname = { optional = true, version = "0.0.24", package = "uu_hostname", path = "src/uu/hostname" } -id = { optional = true, version = "0.0.24", package = "uu_id", path = "src/uu/id" } -install = { optional = true, version = "0.0.24", package = "uu_install", path = "src/uu/install" } -join = { optional = true, version = "0.0.24", package = "uu_join", path = "src/uu/join" } -kill = { optional = true, version = "0.0.24", package = "uu_kill", path = "src/uu/kill" } -link = { optional = true, version = "0.0.24", package = "uu_link", path = "src/uu/link" } -ln = { optional = true, version = "0.0.24", package = "uu_ln", path = "src/uu/ln" } -ls = { optional = true, version = "0.0.24", package = "uu_ls", path = "src/uu/ls" } -logname = { optional = true, version = "0.0.24", package = "uu_logname", path = "src/uu/logname" } -mkdir = { optional = true, version = "0.0.24", package = "uu_mkdir", path = "src/uu/mkdir" } -mkfifo = { optional = true, version = "0.0.24", package = "uu_mkfifo", path = "src/uu/mkfifo" } -mknod = { optional = true, version = "0.0.24", package = "uu_mknod", path = "src/uu/mknod" } -mktemp = { optional = true, version = "0.0.24", package = "uu_mktemp", path = "src/uu/mktemp" } -more = { optional = true, version = "0.0.24", package = "uu_more", path = "src/uu/more" } -mv = { optional = true, version = "0.0.24", package = "uu_mv", path = "src/uu/mv" } -nice = { optional = true, version = "0.0.24", package = "uu_nice", path = "src/uu/nice" } -nl = { optional = true, version = "0.0.24", package = "uu_nl", path = "src/uu/nl" } -nohup = { optional = true, version = "0.0.24", package = "uu_nohup", path = "src/uu/nohup" } -nproc = { optional = true, version = "0.0.24", package = "uu_nproc", path = "src/uu/nproc" } -numfmt = { optional = true, version = "0.0.24", package = "uu_numfmt", path = "src/uu/numfmt" } -od = { optional = true, version = "0.0.24", package = "uu_od", path = "src/uu/od" } -paste = { optional = true, version = "0.0.24", package = "uu_paste", path = "src/uu/paste" } -pathchk = { optional = true, version = "0.0.24", package = "uu_pathchk", path = "src/uu/pathchk" } -pinky = { optional = true, version = "0.0.24", package = "uu_pinky", path = "src/uu/pinky" } -pr = { optional = true, version = "0.0.24", package = "uu_pr", path = "src/uu/pr" } -printenv = { optional = true, version = "0.0.24", package = "uu_printenv", path = "src/uu/printenv" } -printf = { optional = true, version = "0.0.24", package = "uu_printf", path = "src/uu/printf" } -ptx = { optional = true, version = "0.0.24", package = "uu_ptx", path = "src/uu/ptx" } -pwd = { optional = true, version = "0.0.24", package = "uu_pwd", path = "src/uu/pwd" } -readlink = { optional = true, version = "0.0.24", package = "uu_readlink", path = "src/uu/readlink" } -realpath = { optional = true, version = "0.0.24", package = "uu_realpath", path = "src/uu/realpath" } -rm = { optional = true, version = "0.0.24", package = "uu_rm", path = "src/uu/rm" } -rmdir = { optional = true, version = "0.0.24", package = "uu_rmdir", path = "src/uu/rmdir" } -runcon = { optional = true, version = "0.0.24", package = "uu_runcon", path = "src/uu/runcon" } -seq = { optional = true, version = "0.0.24", package = "uu_seq", path = "src/uu/seq" } -shred = { optional = true, version = "0.0.24", package = "uu_shred", path = "src/uu/shred" } -shuf = { optional = true, version = "0.0.24", package = "uu_shuf", path = "src/uu/shuf" } -sleep = { optional = true, version = "0.0.24", package = "uu_sleep", path = "src/uu/sleep" } -sort = { optional = true, version = "0.0.24", package = "uu_sort", path = "src/uu/sort" } -split = { optional = true, version = "0.0.24", package = "uu_split", path = "src/uu/split" } -stat = { optional = true, version = "0.0.24", package = "uu_stat", path = "src/uu/stat" } -stdbuf = { optional = true, version = "0.0.24", package = "uu_stdbuf", path = "src/uu/stdbuf" } -stty = { optional = true, version = "0.0.24", package = "uu_stty", path = "src/uu/stty" } -sum = { optional = true, version = "0.0.24", package = "uu_sum", path = "src/uu/sum" } -sync = { optional = true, version = "0.0.24", package = "uu_sync", path = "src/uu/sync" } -tac = { optional = true, version = "0.0.24", package = "uu_tac", path = "src/uu/tac" } -tail = { optional = true, version = "0.0.24", package = "uu_tail", path = "src/uu/tail" } -tee = { optional = true, version = "0.0.24", package = "uu_tee", path = "src/uu/tee" } -timeout = { optional = true, version = "0.0.24", package = "uu_timeout", path = "src/uu/timeout" } -touch = { optional = true, version = "0.0.24", package = "uu_touch", path = "src/uu/touch" } -tr = { optional = true, version = "0.0.24", package = "uu_tr", path = "src/uu/tr" } -true = { optional = true, version = "0.0.24", package = "uu_true", path = "src/uu/true" } -truncate = { optional = true, version = "0.0.24", package = "uu_truncate", path = "src/uu/truncate" } -tsort = { optional = true, version = "0.0.24", package = "uu_tsort", path = "src/uu/tsort" } -tty = { optional = true, version = "0.0.24", package = "uu_tty", path = "src/uu/tty" } -uname = { optional = true, version = "0.0.24", package = "uu_uname", path = "src/uu/uname" } -unexpand = { optional = true, version = "0.0.24", package = "uu_unexpand", path = "src/uu/unexpand" } -uniq = { optional = true, version = "0.0.24", package = "uu_uniq", path = "src/uu/uniq" } -unlink = { optional = true, version = "0.0.24", package = "uu_unlink", path = "src/uu/unlink" } -uptime = { optional = true, version = "0.0.24", package = "uu_uptime", path = "src/uu/uptime" } -users = { optional = true, version = "0.0.24", package = "uu_users", path = "src/uu/users" } -vdir = { optional = true, version = "0.0.24", package = "uu_vdir", path = "src/uu/vdir" } -wc = { optional = true, version = "0.0.24", package = "uu_wc", path = "src/uu/wc" } -who = { optional = true, version = "0.0.24", package = "uu_who", path = "src/uu/who" } -whoami = { optional = true, version = "0.0.24", package = "uu_whoami", path = "src/uu/whoami" } -yes = { optional = true, version = "0.0.24", package = "uu_yes", path = "src/uu/yes" } +arch = { optional = true, version = "0.0.25", package = "uu_arch", path = "src/uu/arch" } +base32 = { optional = true, version = "0.0.25", package = "uu_base32", path = "src/uu/base32" } +base64 = { optional = true, version = "0.0.25", package = "uu_base64", path = "src/uu/base64" } +basename = { optional = true, version = "0.0.25", package = "uu_basename", path = "src/uu/basename" } +basenc = { optional = true, version = "0.0.25", package = "uu_basenc", path = "src/uu/basenc" } +cat = { optional = true, version = "0.0.25", package = "uu_cat", path = "src/uu/cat" } +chcon = { optional = true, version = "0.0.25", package = "uu_chcon", path = "src/uu/chcon" } +chgrp = { optional = true, version = "0.0.25", package = "uu_chgrp", path = "src/uu/chgrp" } +chmod = { optional = true, version = "0.0.25", package = "uu_chmod", path = "src/uu/chmod" } +chown = { optional = true, version = "0.0.25", package = "uu_chown", path = "src/uu/chown" } +chroot = { optional = true, version = "0.0.25", package = "uu_chroot", path = "src/uu/chroot" } +cksum = { optional = true, version = "0.0.25", package = "uu_cksum", path = "src/uu/cksum" } +comm = { optional = true, version = "0.0.25", package = "uu_comm", path = "src/uu/comm" } +cp = { optional = true, version = "0.0.25", package = "uu_cp", path = "src/uu/cp" } +csplit = { optional = true, version = "0.0.25", package = "uu_csplit", path = "src/uu/csplit" } +cut = { optional = true, version = "0.0.25", package = "uu_cut", path = "src/uu/cut" } +date = { optional = true, version = "0.0.25", package = "uu_date", path = "src/uu/date" } +dd = { optional = true, version = "0.0.25", package = "uu_dd", path = "src/uu/dd" } +df = { optional = true, version = "0.0.25", package = "uu_df", path = "src/uu/df" } +dir = { optional = true, version = "0.0.25", package = "uu_dir", path = "src/uu/dir" } +dircolors = { optional = true, version = "0.0.25", package = "uu_dircolors", path = "src/uu/dircolors" } +dirname = { optional = true, version = "0.0.25", package = "uu_dirname", path = "src/uu/dirname" } +du = { optional = true, version = "0.0.25", package = "uu_du", path = "src/uu/du" } +echo = { optional = true, version = "0.0.25", package = "uu_echo", path = "src/uu/echo" } +env = { optional = true, version = "0.0.25", package = "uu_env", path = "src/uu/env" } +expand = { optional = true, version = "0.0.25", package = "uu_expand", path = "src/uu/expand" } +expr = { optional = true, version = "0.0.25", package = "uu_expr", path = "src/uu/expr" } +factor = { optional = true, version = "0.0.25", package = "uu_factor", path = "src/uu/factor" } +false = { optional = true, version = "0.0.25", package = "uu_false", path = "src/uu/false" } +fmt = { optional = true, version = "0.0.25", package = "uu_fmt", path = "src/uu/fmt" } +fold = { optional = true, version = "0.0.25", package = "uu_fold", path = "src/uu/fold" } +groups = { optional = true, version = "0.0.25", package = "uu_groups", path = "src/uu/groups" } +hashsum = { optional = true, version = "0.0.25", package = "uu_hashsum", path = "src/uu/hashsum" } +head = { optional = true, version = "0.0.25", package = "uu_head", path = "src/uu/head" } +hostid = { optional = true, version = "0.0.25", package = "uu_hostid", path = "src/uu/hostid" } +hostname = { optional = true, version = "0.0.25", package = "uu_hostname", path = "src/uu/hostname" } +id = { optional = true, version = "0.0.25", package = "uu_id", path = "src/uu/id" } +install = { optional = true, version = "0.0.25", package = "uu_install", path = "src/uu/install" } +join = { optional = true, version = "0.0.25", package = "uu_join", path = "src/uu/join" } +kill = { optional = true, version = "0.0.25", package = "uu_kill", path = "src/uu/kill" } +link = { optional = true, version = "0.0.25", package = "uu_link", path = "src/uu/link" } +ln = { optional = true, version = "0.0.25", package = "uu_ln", path = "src/uu/ln" } +ls = { optional = true, version = "0.0.25", package = "uu_ls", path = "src/uu/ls" } +logname = { optional = true, version = "0.0.25", package = "uu_logname", path = "src/uu/logname" } +mkdir = { optional = true, version = "0.0.25", package = "uu_mkdir", path = "src/uu/mkdir" } +mkfifo = { optional = true, version = "0.0.25", package = "uu_mkfifo", path = "src/uu/mkfifo" } +mknod = { optional = true, version = "0.0.25", package = "uu_mknod", path = "src/uu/mknod" } +mktemp = { optional = true, version = "0.0.25", package = "uu_mktemp", path = "src/uu/mktemp" } +more = { optional = true, version = "0.0.25", package = "uu_more", path = "src/uu/more" } +mv = { optional = true, version = "0.0.25", package = "uu_mv", path = "src/uu/mv" } +nice = { optional = true, version = "0.0.25", package = "uu_nice", path = "src/uu/nice" } +nl = { optional = true, version = "0.0.25", package = "uu_nl", path = "src/uu/nl" } +nohup = { optional = true, version = "0.0.25", package = "uu_nohup", path = "src/uu/nohup" } +nproc = { optional = true, version = "0.0.25", package = "uu_nproc", path = "src/uu/nproc" } +numfmt = { optional = true, version = "0.0.25", package = "uu_numfmt", path = "src/uu/numfmt" } +od = { optional = true, version = "0.0.25", package = "uu_od", path = "src/uu/od" } +paste = { optional = true, version = "0.0.25", package = "uu_paste", path = "src/uu/paste" } +pathchk = { optional = true, version = "0.0.25", package = "uu_pathchk", path = "src/uu/pathchk" } +pinky = { optional = true, version = "0.0.25", package = "uu_pinky", path = "src/uu/pinky" } +pr = { optional = true, version = "0.0.25", package = "uu_pr", path = "src/uu/pr" } +printenv = { optional = true, version = "0.0.25", package = "uu_printenv", path = "src/uu/printenv" } +printf = { optional = true, version = "0.0.25", package = "uu_printf", path = "src/uu/printf" } +ptx = { optional = true, version = "0.0.25", package = "uu_ptx", path = "src/uu/ptx" } +pwd = { optional = true, version = "0.0.25", package = "uu_pwd", path = "src/uu/pwd" } +readlink = { optional = true, version = "0.0.25", package = "uu_readlink", path = "src/uu/readlink" } +realpath = { optional = true, version = "0.0.25", package = "uu_realpath", path = "src/uu/realpath" } +rm = { optional = true, version = "0.0.25", package = "uu_rm", path = "src/uu/rm" } +rmdir = { optional = true, version = "0.0.25", package = "uu_rmdir", path = "src/uu/rmdir" } +runcon = { optional = true, version = "0.0.25", package = "uu_runcon", path = "src/uu/runcon" } +seq = { optional = true, version = "0.0.25", package = "uu_seq", path = "src/uu/seq" } +shred = { optional = true, version = "0.0.25", package = "uu_shred", path = "src/uu/shred" } +shuf = { optional = true, version = "0.0.25", package = "uu_shuf", path = "src/uu/shuf" } +sleep = { optional = true, version = "0.0.25", package = "uu_sleep", path = "src/uu/sleep" } +sort = { optional = true, version = "0.0.25", package = "uu_sort", path = "src/uu/sort" } +split = { optional = true, version = "0.0.25", package = "uu_split", path = "src/uu/split" } +stat = { optional = true, version = "0.0.25", package = "uu_stat", path = "src/uu/stat" } +stdbuf = { optional = true, version = "0.0.25", package = "uu_stdbuf", path = "src/uu/stdbuf" } +stty = { optional = true, version = "0.0.25", package = "uu_stty", path = "src/uu/stty" } +sum = { optional = true, version = "0.0.25", package = "uu_sum", path = "src/uu/sum" } +sync = { optional = true, version = "0.0.25", package = "uu_sync", path = "src/uu/sync" } +tac = { optional = true, version = "0.0.25", package = "uu_tac", path = "src/uu/tac" } +tail = { optional = true, version = "0.0.25", package = "uu_tail", path = "src/uu/tail" } +tee = { optional = true, version = "0.0.25", package = "uu_tee", path = "src/uu/tee" } +timeout = { optional = true, version = "0.0.25", package = "uu_timeout", path = "src/uu/timeout" } +touch = { optional = true, version = "0.0.25", package = "uu_touch", path = "src/uu/touch" } +tr = { optional = true, version = "0.0.25", package = "uu_tr", path = "src/uu/tr" } +true = { optional = true, version = "0.0.25", package = "uu_true", path = "src/uu/true" } +truncate = { optional = true, version = "0.0.25", package = "uu_truncate", path = "src/uu/truncate" } +tsort = { optional = true, version = "0.0.25", package = "uu_tsort", path = "src/uu/tsort" } +tty = { optional = true, version = "0.0.25", package = "uu_tty", path = "src/uu/tty" } +uname = { optional = true, version = "0.0.25", package = "uu_uname", path = "src/uu/uname" } +unexpand = { optional = true, version = "0.0.25", package = "uu_unexpand", path = "src/uu/unexpand" } +uniq = { optional = true, version = "0.0.25", package = "uu_uniq", path = "src/uu/uniq" } +unlink = { optional = true, version = "0.0.25", package = "uu_unlink", path = "src/uu/unlink" } +uptime = { optional = true, version = "0.0.25", package = "uu_uptime", path = "src/uu/uptime" } +users = { optional = true, version = "0.0.25", package = "uu_users", path = "src/uu/users" } +vdir = { optional = true, version = "0.0.25", package = "uu_vdir", path = "src/uu/vdir" } +wc = { optional = true, version = "0.0.25", package = "uu_wc", path = "src/uu/wc" } +who = { optional = true, version = "0.0.25", package = "uu_who", path = "src/uu/who" } +whoami = { optional = true, version = "0.0.25", package = "uu_whoami", path = "src/uu/whoami" } +yes = { optional = true, version = "0.0.25", 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" } @@ -495,10 +495,10 @@ rstest = { workspace = true } [target.'cfg(any(target_os = "linux", target_os = "android"))'.dev-dependencies] procfs = { version = "0.16", default-features = false } -rlimit = "0.10.1" [target.'cfg(unix)'.dev-dependencies] -nix = { workspace = true, features = ["process", "signal", "user"] } +nix = { workspace = true, features = ["process", "signal", "user", "term"] } +rlimit = "0.10.1" rand_pcg = "0.3" xattr = { workspace = true } diff --git a/README.md b/README.md index c8ca0d8a316..dbcd7d86021 100644 --- a/README.md +++ b/README.md @@ -51,11 +51,10 @@ that scripts can be easily transferred between platforms.
## Documentation - uutils has both user and developer documentation available: - [User Manual](https://uutils.github.io/coreutils/book/) -- [Developer Documentation](https://uutils.github.io/dev/coreutils/) (currently offline, you can use docs.rs in the meantime) +- [Developer Documentation](https://docs.rs/crate/coreutils/) Both can also be generated locally, the instructions for that can be found in the [coreutils docs](https://github.com/uutils/uutils.github.io) repository. diff --git a/deny.toml b/deny.toml index d7c04ad2d5b..943fcdfa918 100644 --- a/deny.toml +++ b/deny.toml @@ -6,10 +6,8 @@ [advisories] db-path = "~/.cargo/advisory-db" db-urls = ["https://github.com/rustsec/advisory-db"] -vulnerability = "warn" -unmaintained = "warn" +version = 2 yanked = "warn" -notice = "warn" ignore = [ #"RUSTSEC-0000-0000", ] @@ -18,7 +16,7 @@ ignore = [ # More documentation for the licenses section can be found here: # https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html [licenses] -unlicensed = "deny" +version = 2 allow = [ "MIT", "Apache-2.0", @@ -29,9 +27,6 @@ allow = [ "CC0-1.0", "Unicode-DFS-2016", ] -copyleft = "deny" -allow-osi-fsf-free = "neither" -default = "deny" confidence-threshold = 0.8 [[licenses.clarify]] @@ -104,6 +99,8 @@ skip = [ { name = "bitflags", version = "1.3.2" }, # clap_builder, textwrap { name = "terminal_size", version = "0.2.6" }, + # filetime, parking_lot_core + { name = "redox_syscall", version = "0.4.1" }, ] # spell-checker: enable diff --git a/docs/compiles_table.py b/docs/compiles_table.py index 83c98bed1de..884b0bc39e1 100644 --- a/docs/compiles_table.py +++ b/docs/compiles_table.py @@ -10,7 +10,7 @@ # third party dependencies from tqdm import tqdm -# spell-checker:ignore (libs) tqdm imap ; (shell/mac) xcrun ; (vars) nargs +# spell-checker:ignore (libs) tqdm imap ; (shell/mac) xcrun ; (vars) nargs retcode csvfile BINS_PATH = Path("../src/uu") CACHE_PATH = Path("compiles_table.csv") diff --git a/docs/src/installation.md b/docs/src/installation.md index dc631d240ca..80afdda53f7 100644 --- a/docs/src/installation.md +++ b/docs/src/installation.md @@ -1,4 +1,4 @@ - + # Installation @@ -151,6 +151,17 @@ scoop install uutils-coreutils conda install -c conda-forge uutils-coreutils ``` +### Yocto + +[Yocto recipe](https://github.com/openembedded/meta-openembedded/tree/master/meta-oe/recipes-core/uutils-coreutils) + +The uutils-coreutils recipe is provided as part of the meta-openembedded yocto layer. +Clone [poky](https://github.com/yoctoproject/poky) and [meta-openembedded](https://github.com/openembedded/meta-openembedded/tree/master), add +`meta-openembedded/meta-oe` as layer in your `build/conf/bblayers.conf` file, +and then either call `bitbake uutils-coreutils`, or use +`PREFERRED_PROVIDER_coreutils = "uutils-coreutils"` in your `build/conf/local.conf` file and +then build your usual yocto image. + ## Non-standard packages ### `coreutils-hybrid` (AUR) diff --git a/fuzz/fuzz_targets/fuzz_printf.rs b/fuzz/fuzz_targets/fuzz_printf.rs index 72fac540b17..cb2d90ed531 100644 --- a/fuzz/fuzz_targets/fuzz_printf.rs +++ b/fuzz/fuzz_targets/fuzz_printf.rs @@ -10,6 +10,7 @@ use uu_printf::uumain; use rand::seq::SliceRandom; use rand::Rng; +use std::env; use std::ffi::OsString; mod fuzz_common; @@ -82,6 +83,8 @@ fuzz_target!(|_data: &[u8]| { args.extend(printf_input.split_whitespace().map(OsString::from)); let rust_result = generate_and_run_uumain(&args, uumain, None); + // TODO remove once uutils printf supports localization + env::set_var("LC_ALL", "C"); let gnu_result = match run_gnu_cmd(CMD_PATH, &args[1..], false, None) { Ok(result) => result, Err(error_result) => { diff --git a/fuzz/fuzz_targets/fuzz_sort.rs b/fuzz/fuzz_targets/fuzz_sort.rs index 3520bbaefed..9bb7df35767 100644 --- a/fuzz/fuzz_targets/fuzz_sort.rs +++ b/fuzz/fuzz_targets/fuzz_sort.rs @@ -60,7 +60,7 @@ fuzz_target!(|_data: &[u8]| { let rust_result = generate_and_run_uumain(&args, uumain, Some(&input_lines)); // TODO remove once uutils sort supports localization - env::set_var("LC_COLLATE", "C"); + env::set_var("LC_ALL", "C"); let gnu_result = match run_gnu_cmd(CMD_PATH, &args[1..], false, Some(&input_lines)) { Ok(result) => result, Err(error_result) => { diff --git a/src/uu/arch/Cargo.toml b/src/uu/arch/Cargo.toml index b4d07b26c59..fa2940acde1 100644 --- a/src/uu/arch/Cargo.toml +++ b/src/uu/arch/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_arch" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "arch ~ (uutils) display machine architecture" diff --git a/src/uu/base32/Cargo.toml b/src/uu/base32/Cargo.toml index 1c27e14cf3f..5c8b23cba1f 100644 --- a/src/uu/base32/Cargo.toml +++ b/src/uu/base32/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_base32" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "base32 ~ (uutils) decode/encode input (base32-encoding)" diff --git a/src/uu/base32/src/base_common.rs b/src/uu/base32/src/base_common.rs index 68c40287db7..897722dd36e 100644 --- a/src/uu/base32/src/base_common.rs +++ b/src/uu/base32/src/base_common.rs @@ -102,23 +102,24 @@ pub fn base_app(about: &'static str, usage: &str) -> Command { .short('d') .long(options::DECODE) .help("decode data") - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .overrides_with(options::DECODE), ) .arg( Arg::new(options::IGNORE_GARBAGE) .short('i') .long(options::IGNORE_GARBAGE) .help("when decoding, ignore non-alphabetic characters") - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .overrides_with(options::IGNORE_GARBAGE), ) .arg( Arg::new(options::WRAP) .short('w') .long(options::WRAP) .value_name("COLS") - .help( - "wrap encoded lines after COLS character (default 76, 0 to disable wrapping)", - ), + .help("wrap encoded lines after COLS character (default 76, 0 to disable wrapping)") + .overrides_with(options::WRAP), ) // "multiple" arguments are used to check whether there is more than one // file passed in. diff --git a/src/uu/base64/Cargo.toml b/src/uu/base64/Cargo.toml index 204b880bf72..5df285f89a5 100644 --- a/src/uu/base64/Cargo.toml +++ b/src/uu/base64/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_base64" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "base64 ~ (uutils) decode/encode input (base64-encoding)" diff --git a/src/uu/basename/Cargo.toml b/src/uu/basename/Cargo.toml index 51202235b15..9262b483a3f 100644 --- a/src/uu/basename/Cargo.toml +++ b/src/uu/basename/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_basename" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "basename ~ (uutils) display PATHNAME with leading directory components removed" diff --git a/src/uu/basename/basename.md b/src/uu/basename/basename.md index b17cac74a00..ee87fa76d4e 100644 --- a/src/uu/basename/basename.md +++ b/src/uu/basename/basename.md @@ -1,7 +1,7 @@ # basename ``` -basename NAME [SUFFIX] +basename [-z] NAME [SUFFIX] basename OPTION... NAME... ``` diff --git a/src/uu/basename/src/basename.rs b/src/uu/basename/src/basename.rs index 6c9baca6fce..f502fb23466 100644 --- a/src/uu/basename/src/basename.rs +++ b/src/uu/basename/src/basename.rs @@ -27,86 +27,48 @@ pub mod options { pub fn uumain(args: impl uucore::Args) -> UResult<()> { let args = args.collect_lossy(); - // Since options have to go before names, - // if the first argument is not an option, then there is no option, - // and that implies there is exactly one name (no option => no -a option), - // so simple format is used - if args.len() > 1 && !args[1].starts_with('-') { - if args.len() > 3 { - return Err(UUsageError::new( - 1, - format!("extra operand {}", args[3].to_string().quote()), - )); - } - let suffix = if args.len() > 2 { args[2].as_ref() } else { "" }; - println!("{}", basename(&args[1], suffix)); - return Ok(()); - } - // // Argument parsing // let matches = uu_app().try_get_matches_from(args)?; - // too few arguments - if !matches.contains_id(options::NAME) { - return Err(UUsageError::new(1, "missing operand".to_string())); - } - let line_ending = LineEnding::from_zero_flag(matches.get_flag(options::ZERO)); - let opt_suffix = matches.get_one::(options::SUFFIX).is_some(); - let opt_multiple = matches.get_flag(options::MULTIPLE); - let multiple_paths = opt_suffix || opt_multiple; - let name_args_count = matches + let mut name_args = matches .get_many::(options::NAME) - .map(|n| n.len()) - .unwrap_or(0); - - // too many arguments - if !multiple_paths && name_args_count > 2 { - return Err(UUsageError::new( - 1, - format!( - "extra operand {}", - matches - .get_many::(options::NAME) - .unwrap() - .nth(2) - .unwrap() - .quote() - ), - )); + .unwrap_or_default() + .collect::>(); + if name_args.is_empty() { + return Err(UUsageError::new(1, "missing operand".to_string())); } - - let suffix = if opt_suffix { - matches.get_one::(options::SUFFIX).unwrap() - } else if !opt_multiple && name_args_count > 1 { + let multiple_paths = + matches.get_one::(options::SUFFIX).is_some() || matches.get_flag(options::MULTIPLE); + let suffix = if multiple_paths { matches - .get_many::(options::NAME) - .unwrap() - .nth(1) - .unwrap() + .get_one::(options::SUFFIX) + .cloned() + .unwrap_or_default() } else { - "" + // "simple format" + match name_args.len() { + 0 => panic!("already checked"), + 1 => String::default(), + 2 => name_args.pop().unwrap().clone(), + _ => { + return Err(UUsageError::new( + 1, + format!("extra operand {}", name_args[2].quote(),), + )); + } + } }; // // Main Program Processing // - let paths: Vec<_> = if multiple_paths { - matches.get_many::(options::NAME).unwrap().collect() - } else { - matches - .get_many::(options::NAME) - .unwrap() - .take(1) - .collect() - }; - - for path in paths { - print!("{}{}", basename(path, suffix), line_ending); + for path in name_args { + print!("{}{}", basename(path, &suffix), line_ending); } Ok(()) @@ -123,27 +85,31 @@ pub fn uu_app() -> Command { .short('a') .long(options::MULTIPLE) .help("support multiple arguments and treat each as a NAME") - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .overrides_with(options::MULTIPLE), ) .arg( Arg::new(options::NAME) .action(clap::ArgAction::Append) .value_hint(clap::ValueHint::AnyPath) - .hide(true), + .hide(true) + .trailing_var_arg(true), ) .arg( Arg::new(options::SUFFIX) .short('s') .long(options::SUFFIX) .value_name("SUFFIX") - .help("remove a trailing SUFFIX; implies -a"), + .help("remove a trailing SUFFIX; implies -a") + .overrides_with(options::SUFFIX), ) .arg( Arg::new(options::ZERO) .short('z') .long(options::ZERO) .help("end each output line with NUL, not newline") - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .overrides_with(options::ZERO), ) } diff --git a/src/uu/basenc/Cargo.toml b/src/uu/basenc/Cargo.toml index 26a2364282f..842d80876d2 100644 --- a/src/uu/basenc/Cargo.toml +++ b/src/uu/basenc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_basenc" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "basenc ~ (uutils) decode/encode input" diff --git a/src/uu/basenc/src/basenc.rs b/src/uu/basenc/src/basenc.rs index ff512b17652..ed117b22a0d 100644 --- a/src/uu/basenc/src/basenc.rs +++ b/src/uu/basenc/src/basenc.rs @@ -53,12 +53,14 @@ const ENCODINGS: &[(&str, Format, &str)] = &[ pub fn uu_app() -> Command { let mut command = base_common::base_app(ABOUT, USAGE); for encoding in ENCODINGS { - command = command.arg( - Arg::new(encoding.0) - .long(encoding.0) - .help(encoding.2) - .action(ArgAction::SetTrue), - ); + let raw_arg = Arg::new(encoding.0) + .long(encoding.0) + .help(encoding.2) + .action(ArgAction::SetTrue); + let overriding_arg = ENCODINGS + .iter() + .fold(raw_arg, |arg, enc| arg.overrides_with(enc.0)); + command = command.arg(overriding_arg); } command } diff --git a/src/uu/cat/Cargo.toml b/src/uu/cat/Cargo.toml index cce6561c084..79609c1cedd 100644 --- a/src/uu/cat/Cargo.toml +++ b/src/uu/cat/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_cat" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "cat ~ (uutils) concatenate and display input" diff --git a/src/uu/cat/src/cat.rs b/src/uu/cat/src/cat.rs index af55442ca5e..b239dc87a41 100644 --- a/src/uu/cat/src/cat.rs +++ b/src/uu/cat/src/cat.rs @@ -170,6 +170,7 @@ mod options { pub static SHOW_NONPRINTING_TABS: &str = "t"; pub static SHOW_TABS: &str = "show-tabs"; pub static SHOW_NONPRINTING: &str = "show-nonprinting"; + pub static IGNORED_U: &str = "ignored-u"; } #[uucore::main] @@ -231,6 +232,7 @@ pub fn uu_app() -> Command { .override_usage(format_usage(USAGE)) .about(ABOUT) .infer_long_args(true) + .args_override_self(true) .arg( Arg::new(options::FILE) .hide(true) @@ -249,7 +251,8 @@ pub fn uu_app() -> Command { .short('b') .long(options::NUMBER_NONBLANK) .help("number nonempty output lines, overrides -n") - .overrides_with(options::NUMBER) + // Note: This MUST NOT .overrides_with(options::NUMBER)! + // In clap, overriding is symmetric, so "-b -n" counts as "-n", which is not what we want. .action(ArgAction::SetTrue), ) .arg( @@ -299,6 +302,12 @@ pub fn uu_app() -> Command { .help("use ^ and M- notation, except for LF (\\n) and TAB (\\t)") .action(ArgAction::SetTrue), ) + .arg( + Arg::new(options::IGNORED_U) + .short('u') + .help("(ignored)") + .action(ArgAction::SetTrue), + ) } fn cat_handle( diff --git a/src/uu/cat/src/splice.rs b/src/uu/cat/src/splice.rs index 6c2b6d3dac3..13daae84d7f 100644 --- a/src/uu/cat/src/splice.rs +++ b/src/uu/cat/src/splice.rs @@ -5,7 +5,10 @@ use super::{CatResult, FdReadable, InputHandle}; use nix::unistd; -use std::os::unix::io::{AsRawFd, RawFd}; +use std::os::{ + fd::AsFd, + unix::io::{AsRawFd, RawFd}, +}; use uucore::pipes::{pipe, splice, splice_exact}; @@ -20,9 +23,9 @@ const BUF_SIZE: usize = 1024 * 16; /// The `bool` in the result value indicates if we need to fall back to normal /// copying or not. False means we don't have to. #[inline] -pub(super) fn write_fast_using_splice( +pub(super) fn write_fast_using_splice( handle: &InputHandle, - write_fd: &impl AsRawFd, + write_fd: &S, ) -> CatResult { let (pipe_rd, pipe_wr) = pipe()?; @@ -38,7 +41,7 @@ pub(super) fn write_fast_using_splice( // we can recover by copying the data that we have from the // intermediate pipe to stdout using normal read/write. Then // we tell the caller to fall back. - copy_exact(pipe_rd.as_raw_fd(), write_fd.as_raw_fd(), n)?; + copy_exact(pipe_rd.as_raw_fd(), write_fd, n)?; return Ok(true); } } @@ -52,7 +55,7 @@ pub(super) fn write_fast_using_splice( /// Move exactly `num_bytes` bytes from `read_fd` to `write_fd`. /// /// Panics if not enough bytes can be read. -fn copy_exact(read_fd: RawFd, write_fd: RawFd, num_bytes: usize) -> nix::Result<()> { +fn copy_exact(read_fd: RawFd, write_fd: &impl AsFd, num_bytes: usize) -> nix::Result<()> { let mut left = num_bytes; let mut buf = [0; BUF_SIZE]; while left > 0 { diff --git a/src/uu/chcon/Cargo.toml b/src/uu/chcon/Cargo.toml index 021e435823a..7498b7cc99d 100644 --- a/src/uu/chcon/Cargo.toml +++ b/src/uu/chcon/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_chcon" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "chcon ~ (uutils) change file security context" diff --git a/src/uu/chcon/src/chcon.rs b/src/uu/chcon/src/chcon.rs index ec111c853fd..1a804bd3bbf 100644 --- a/src/uu/chcon/src/chcon.rs +++ b/src/uu/chcon/src/chcon.rs @@ -154,6 +154,7 @@ pub fn uu_app() -> Command { .override_usage(format_usage(USAGE)) .infer_long_args(true) .disable_help_flag(true) + .args_override_self(true) .arg( Arg::new(options::HELP) .long(options::HELP) @@ -163,7 +164,7 @@ pub fn uu_app() -> Command { .arg( Arg::new(options::dereference::DEREFERENCE) .long(options::dereference::DEREFERENCE) - .conflicts_with(options::dereference::NO_DEREFERENCE) + .overrides_with(options::dereference::NO_DEREFERENCE) .help( "Affect the referent of each symbolic link (this is the default), \ rather than the symbolic link itself.", @@ -180,7 +181,7 @@ pub fn uu_app() -> Command { .arg( Arg::new(options::preserve_root::PRESERVE_ROOT) .long(options::preserve_root::PRESERVE_ROOT) - .conflicts_with(options::preserve_root::NO_PRESERVE_ROOT) + .overrides_with(options::preserve_root::NO_PRESERVE_ROOT) .help("Fail to operate recursively on '/'.") .action(ArgAction::SetTrue), ) diff --git a/src/uu/chgrp/Cargo.toml b/src/uu/chgrp/Cargo.toml index bbad8d31e6e..21745317e9c 100644 --- a/src/uu/chgrp/Cargo.toml +++ b/src/uu/chgrp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_chgrp" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "chgrp ~ (uutils) change the group ownership of FILE" diff --git a/src/uu/chmod/Cargo.toml b/src/uu/chmod/Cargo.toml index e779469deda..2a6a46474cd 100644 --- a/src/uu/chmod/Cargo.toml +++ b/src/uu/chmod/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_chmod" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "chmod ~ (uutils) change mode of FILE" diff --git a/src/uu/chmod/src/chmod.rs b/src/uu/chmod/src/chmod.rs index 31663b1af9c..d1325743782 100644 --- a/src/uu/chmod/src/chmod.rs +++ b/src/uu/chmod/src/chmod.rs @@ -101,7 +101,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let recursive = matches.get_flag(options::RECURSIVE); let fmode = match matches.get_one::(options::REFERENCE) { Some(fref) => match fs::metadata(fref) { - Ok(meta) => Some(meta.mode()), + Ok(meta) => Some(meta.mode() & 0o7777), Err(err) => { return Err(USimpleError::new( 1, @@ -267,7 +267,7 @@ impl Chmoder { return Err(USimpleError::new( 1, format!( - "it is dangerous to operate recursively on {}\nuse --no-preserve-root to override this failsafe", + "it is dangerous to operate recursively on {}\nchmod: use --no-preserve-root to override this failsafe", filename.quote() ) )); diff --git a/src/uu/chown/Cargo.toml b/src/uu/chown/Cargo.toml index dfa9dba32a1..ae4c969c3c9 100644 --- a/src/uu/chown/Cargo.toml +++ b/src/uu/chown/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_chown" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "chown ~ (uutils) change the ownership of FILE" diff --git a/src/uu/chroot/Cargo.toml b/src/uu/chroot/Cargo.toml index 12533b6542a..d6090ec8c18 100644 --- a/src/uu/chroot/Cargo.toml +++ b/src/uu/chroot/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_chroot" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "chroot ~ (uutils) run COMMAND under a new root directory" diff --git a/src/uu/cksum/Cargo.toml b/src/uu/cksum/Cargo.toml index 47b4d73592a..406aa75df77 100644 --- a/src/uu/cksum/Cargo.toml +++ b/src/uu/cksum/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_cksum" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "cksum ~ (uutils) display CRC and size of input" @@ -16,7 +16,7 @@ path = "src/cksum.rs" [dependencies] clap = { workspace = true } -uucore = { workspace = true, features = ["sum"] } +uucore = { workspace = true, features = ["encoding", "sum"] } hex = { workspace = true } [[bin]] diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index 36dfbbe1e3d..c5c362c5936 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -15,6 +15,7 @@ use std::io::{self, stdin, stdout, BufReader, Read, Write}; use std::iter; use std::path::Path; use uucore::{ + encoding, error::{FromIo, UError, UResult, USimpleError}, format_usage, help_about, help_section, help_usage, show, sum::{ @@ -44,6 +45,13 @@ enum CkSumError { RawMultipleFiles, } +#[derive(Debug, PartialEq)] +enum OutputFormat { + Hexadecimal, + Raw, + Base64, +} + impl UError for CkSumError { fn code(&self) -> i32 { match self { @@ -138,7 +146,7 @@ struct Options { output_bits: usize, untagged: bool, length: Option, - raw: bool, + output_format: OutputFormat, } /// Calculate checksum @@ -153,7 +161,7 @@ where I: Iterator, { let files: Vec<_> = files.collect(); - if options.raw && files.len() > 1 { + if options.output_format == OutputFormat::Raw && files.len() > 1 { return Err(Box::new(CkSumError::RawMultipleFiles)); } @@ -177,7 +185,7 @@ where }; Box::new(file_buf) as Box }); - let (sum, sz) = digest_read(&mut options.digest, &mut file, options.output_bits) + let (sum_hex, sz) = digest_read(&mut options.digest, &mut file, options.output_bits) .map_err_context(|| "failed to read input".to_string())?; if filename.is_dir() { show!(USimpleError::new( @@ -186,17 +194,25 @@ where )); continue; } - if options.raw { - let bytes = match options.algo_name { - ALGORITHM_OPTIONS_CRC => sum.parse::().unwrap().to_be_bytes().to_vec(), - ALGORITHM_OPTIONS_SYSV | ALGORITHM_OPTIONS_BSD => { - sum.parse::().unwrap().to_be_bytes().to_vec() - } - _ => decode(sum).unwrap(), - }; - stdout().write_all(&bytes)?; - return Ok(()); - } + let sum = match options.output_format { + OutputFormat::Raw => { + let bytes = match options.algo_name { + ALGORITHM_OPTIONS_CRC => sum_hex.parse::().unwrap().to_be_bytes().to_vec(), + ALGORITHM_OPTIONS_SYSV | ALGORITHM_OPTIONS_BSD => { + sum_hex.parse::().unwrap().to_be_bytes().to_vec() + } + _ => decode(sum_hex).unwrap(), + }; + // Cannot handle multiple files anyway, output immediately. + stdout().write_all(&bytes)?; + return Ok(()); + } + OutputFormat::Hexadecimal => sum_hex, + OutputFormat::Base64 => match options.algo_name { + ALGORITHM_OPTIONS_CRC | ALGORITHM_OPTIONS_SYSV | ALGORITHM_OPTIONS_BSD => sum_hex, + _ => encoding::encode(encoding::Format::Base64, &decode(sum_hex).unwrap()).unwrap(), + }, + }; // The BSD checksum output is 5 digit integer let bsd_width = 5; match (options.algo_name, not_file) { @@ -286,8 +302,10 @@ mod options { pub const ALGORITHM: &str = "algorithm"; pub const FILE: &str = "file"; pub const UNTAGGED: &str = "untagged"; + pub const TAG: &str = "tag"; pub const LENGTH: &str = "length"; pub const RAW: &str = "raw"; + pub const BASE64: &str = "base64"; } #[uucore::main] @@ -342,13 +360,21 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let (name, algo, bits) = detect_algo(algo_name, length); + let output_format = if matches.get_flag(options::RAW) { + OutputFormat::Raw + } else if matches.get_flag(options::BASE64) { + OutputFormat::Base64 + } else { + OutputFormat::Hexadecimal + }; + let opts = Options { algo_name: name, digest: algo, output_bits: bits, length, untagged: matches.get_flag(options::UNTAGGED), - raw: matches.get_flag(options::RAW), + output_format, }; match matches.get_many::(options::FILE) { @@ -365,6 +391,7 @@ pub fn uu_app() -> Command { .about(ABOUT) .override_usage(format_usage(USAGE)) .infer_long_args(true) + .args_override_self(true) .arg( Arg::new(options::FILE) .hide(true) @@ -395,6 +422,13 @@ pub fn uu_app() -> Command { Arg::new(options::UNTAGGED) .long(options::UNTAGGED) .help("create a reversed style checksum, without digest type") + .action(ArgAction::SetTrue) + .overrides_with(options::TAG), + ) + .arg( + Arg::new(options::TAG) + .long(options::TAG) + .help("create a BSD style checksum, undo --untagged (default)") .action(ArgAction::SetTrue), ) .arg( @@ -411,5 +445,14 @@ pub fn uu_app() -> Command { .help("emit a raw binary digest, not hexadecimal") .action(ArgAction::SetTrue), ) + .arg( + Arg::new(options::BASE64) + .long(options::BASE64) + .help("emit a base64 digest, not hexadecimal") + .action(ArgAction::SetTrue) + // Even though this could easily just override an earlier '--raw', + // GNU cksum does not permit these flags to be combined: + .conflicts_with(options::RAW), + ) .after_help(AFTER_HELP) } diff --git a/src/uu/comm/Cargo.toml b/src/uu/comm/Cargo.toml index cd759aad246..41e2e3b7459 100644 --- a/src/uu/comm/Cargo.toml +++ b/src/uu/comm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_comm" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "comm ~ (uutils) compare sorted inputs" diff --git a/src/uu/cp/Cargo.toml b/src/uu/cp/Cargo.toml index 7db6b202bb2..1e379b72f9f 100644 --- a/src/uu/cp/Cargo.toml +++ b/src/uu/cp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_cp" -version = "0.0.24" +version = "0.0.25" authors = [ "Jordy Dickinson ", "Joshua S. Miller ", diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 28e9b678471..778ddf843b6 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -3,8 +3,6 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. // spell-checker:ignore (ToDO) copydir ficlone fiemap ftruncate linkgs lstat nlink nlinks pathbuf pwrite reflink strs xattrs symlinked deduplicated advcpmv nushell IRWXG IRWXO IRWXU IRWXUGO IRWXU IRWXG IRWXO IRWXUGO -#![allow(clippy::missing_safety_doc)] -#![allow(clippy::extra_unused_lifetimes)] use quick_error::quick_error; use std::cmp::Ordering; @@ -12,14 +10,13 @@ use std::collections::{HashMap, HashSet}; use std::env; #[cfg(not(windows))] use std::ffi::CString; -use std::fs::{self, File, OpenOptions}; +use std::fs::{self, File, Metadata, OpenOptions, Permissions}; use std::io; #[cfg(unix)] use std::os::unix::ffi::OsStrExt; #[cfg(unix)] use std::os::unix::fs::{FileTypeExt, PermissionsExt}; use std::path::{Path, PathBuf, StripPrefixError}; -use std::string::ToString; use clap::{builder::ValueParser, crate_version, Arg, ArgAction, ArgMatches, Command}; use filetime::FileTime; @@ -783,7 +780,11 @@ impl CopyMode { { Self::Update } else if matches.get_flag(options::ATTRIBUTES_ONLY) { - Self::AttrOnly + if matches.get_flag(options::REMOVE_DESTINATION) { + Self::Copy + } else { + Self::AttrOnly + } } else { Self::Copy } @@ -1624,7 +1625,7 @@ fn aligned_ancestors<'a>(source: &'a Path, dest: &'a Path) -> Vec<(&'a Path, &'a // Get the matching number of elements from the ancestors of the // destination path (for example, get "d/a" and "d/a/b"). let k = source_ancestors.len(); - let dest_ancestors = &dest_ancestors[1..1 + k]; + let dest_ancestors = &dest_ancestors[1..=k]; // Now we have two slices of the same length, so we zip them. let mut result = vec![]; @@ -1638,153 +1639,59 @@ fn aligned_ancestors<'a>(source: &'a Path, dest: &'a Path) -> Vec<(&'a Path, &'a result } -/// Copy the a file from `source` to `dest`. `source` will be dereferenced if -/// `options.dereference` is set to true. `dest` will be dereferenced only if -/// the source was not a symlink. -/// -/// Behavior when copying to existing files is contingent on the -/// `options.overwrite` mode. If a file is skipped, the return type -/// should be `Error:Skipped` -/// -/// The original permissions of `source` will be copied to `dest` -/// after a successful copy. -#[allow(clippy::cognitive_complexity)] -fn copy_file( +fn print_verbose_output( + parents: bool, progress_bar: &Option, source: &Path, dest: &Path, - options: &Options, - symlinked_files: &mut HashSet, - copied_files: &mut HashMap, - source_in_command_line: bool, -) -> CopyResult<()> { - 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(()); - } - - // Fail if dest is a dangling symlink or a symlink this program created previously - if dest.is_symlink() { - if FileInformation::from_path(dest, false) - .map(|info| symlinked_files.contains(&info)) - .unwrap_or(false) - { - return Err(Error::Error(format!( - "will not copy '{}' through just-created symlink '{}'", - source.display(), - dest.display() - ))); - } - let copy_contents = options.dereference(source_in_command_line) || !source.is_symlink(); - if copy_contents - && !dest.exists() - && !matches!( - 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 '{}'", - dest.display() - ))); - } - if paths_refer_to_same_file(source, dest, true) - && matches!( - options.overwrite, - OverwriteMode::Clobber(ClobberMode::RemoveDestination) - ) - { - fs::remove_file(dest)?; - } - } - - if are_hardlinks_to_same_file(source, dest) - && matches!( - options.overwrite, - OverwriteMode::Clobber(ClobberMode::RemoveDestination) - ) - { - fs::remove_file(dest)?; - } - - if file_or_link_exists(dest) { - if are_hardlinks_to_same_file(source, dest) - && !options.force() - && options.backup == BackupMode::NoBackup - && source != dest - || (source == dest && options.copy_mode == CopyMode::Link) - { - return Ok(()); - } - handle_existing_dest(source, dest, options, source_in_command_line)?; - } - - if options.preserve_hard_links() { - // if we encounter a matching device/inode pair in the source tree - // we can arrange to create a hard link between the corresponding names - // in the destination tree. - if let Some(new_source) = copied_files.get( - &FileInformation::from_path(source, options.dereference(source_in_command_line)) - .context(format!("cannot stat {}", source.quote()))?, - ) { - std::fs::hard_link(new_source, dest)?; - return Ok(()); - }; +) { + if let Some(pb) = progress_bar { + // Suspend (hide) the progress bar so the println won't overlap with the progress bar. + pb.suspend(|| { + print_paths(parents, source, dest); + }); + } else { + print_paths(parents, source, dest); } +} - if options.verbose { - if let Some(pb) = progress_bar { - // Suspend (hide) the progress bar so the println won't overlap with the progress bar. - pb.suspend(|| { - if options.parents { - // For example, if copying file `a/b/c` and its parents - // to directory `d/`, then print - // - // a -> d/a - // a/b -> d/a/b - // - for (x, y) in aligned_ancestors(source, dest) { - println!("{} -> {}", x.display(), y.display()); - } - } - - println!("{}", context_for(source, dest)); - }); - } else { - if options.parents { - // For example, if copying file `a/b/c` and its parents - // to directory `d/`, then print - // - // a -> d/a - // a/b -> d/a/b - // - for (x, y) in aligned_ancestors(source, dest) { - println!("{} -> {}", x.display(), y.display()); - } - } - - println!("{}", context_for(source, dest)); +fn print_paths(parents: bool, source: &Path, dest: &Path) { + if parents { + // For example, if copying file `a/b/c` and its parents + // to directory `d/`, then print + // + // a -> d/a + // a/b -> d/a/b + // + for (x, y) in aligned_ancestors(source, dest) { + println!("{} -> {}", x.display(), y.display()); } } - // Calculate the context upfront before canonicalizing the path - let context = context_for(source, dest); - let context = context.as_str(); + println!("{}", context_for(source, dest)); +} - let source_metadata = { - let result = if options.dereference(source_in_command_line) { - fs::metadata(source) - } else { - fs::symlink_metadata(source) - }; - result.context(context)? - }; +/// Handles the copy mode for a file copy operation. +/// +/// This function determines how to copy a file based on the provided options. +/// It supports different copy modes, including hard linking, copying, symbolic linking, updating, and attribute-only copying. +/// It also handles file backups, overwriting, and dereferencing based on the provided options. +/// +/// # Returns +/// +/// * `Ok(())` - The file was copied successfully. +/// * `Err(CopyError)` - An error occurred while copying the file. +fn handle_copy_mode( + source: &Path, + dest: &Path, + options: &Options, + context: &str, + source_metadata: Metadata, + symlinked_files: &mut HashSet, + source_in_command_line: bool, +) -> CopyResult<()> { let source_file_type = source_metadata.file_type(); + let source_is_symlink = source_file_type.is_symlink(); #[cfg(unix)] @@ -1792,24 +1699,6 @@ fn copy_file( #[cfg(not(unix))] let source_is_fifo = false; - let dest_permissions = if dest.exists() { - dest.symlink_metadata().context(context)?.permissions() - } else { - #[allow(unused_mut)] - let mut permissions = source_metadata.permissions(); - #[cfg(unix)] - { - let mut mode = handle_no_preserve_mode(options, permissions.mode()); - - // apply umask - use uucore::mode::get_umask; - mode &= !get_umask(); - - permissions.set_mode(mode); - } - permissions - }; - match options.copy_mode { CopyMode::Link => { if dest.exists() { @@ -1912,6 +1801,196 @@ fn copy_file( } }; + Ok(()) +} + +/// Calculates the permissions for the destination file in a copy operation. +/// +/// If the destination file already exists, its current permissions are returned. +/// If the destination file does not exist, the source file's permissions are used, +/// with the `no-preserve` option and the umask taken into account on Unix platforms. +/// # Returns +/// +/// * `Ok(Permissions)` - The calculated permissions for the destination file. +/// * `Err(CopyError)` - An error occurred while getting the metadata of the destination file. +/// Allow unused variables for Windows (on options) +#[allow(unused_variables)] +fn calculate_dest_permissions( + dest: &Path, + source_metadata: &Metadata, + options: &Options, + context: &str, +) -> CopyResult { + if dest.exists() { + Ok(dest.symlink_metadata().context(context)?.permissions()) + } else { + #[cfg(unix)] + { + let mut permissions = source_metadata.permissions(); + let mode = handle_no_preserve_mode(options, permissions.mode()); + + // Apply umask + use uucore::mode::get_umask; + let mode = mode & !get_umask(); + permissions.set_mode(mode); + Ok(permissions) + } + #[cfg(not(unix))] + { + let permissions = source_metadata.permissions(); + Ok(permissions) + } + } +} + +/// Copy the a file from `source` to `dest`. `source` will be dereferenced if +/// `options.dereference` is set to true. `dest` will be dereferenced only if +/// the source was not a symlink. +/// +/// Behavior when copying to existing files is contingent on the +/// `options.overwrite` mode. If a file is skipped, the return type +/// should be `Error:Skipped` +/// +/// 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, + dest: &Path, + options: &Options, + symlinked_files: &mut HashSet, + copied_files: &mut HashMap, + source_in_command_line: bool, +) -> CopyResult<()> { + 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(()); + } + + // Fail if dest is a dangling symlink or a symlink this program created previously + if dest.is_symlink() { + if FileInformation::from_path(dest, false) + .map(|info| symlinked_files.contains(&info)) + .unwrap_or(false) + { + return Err(Error::Error(format!( + "will not copy '{}' through just-created symlink '{}'", + source.display(), + dest.display() + ))); + } + let copy_contents = options.dereference(source_in_command_line) || !source.is_symlink(); + if copy_contents + && !dest.exists() + && !matches!( + 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 '{}'", + dest.display() + ))); + } + if paths_refer_to_same_file(source, dest, true) + && matches!( + options.overwrite, + OverwriteMode::Clobber(ClobberMode::RemoveDestination) + ) + { + fs::remove_file(dest)?; + } + } + + if are_hardlinks_to_same_file(source, dest) + && matches!( + options.overwrite, + OverwriteMode::Clobber(ClobberMode::RemoveDestination) + ) + { + fs::remove_file(dest)?; + } + + if file_or_link_exists(dest) + && (!options.attributes_only + || matches!( + options.overwrite, + OverwriteMode::Clobber(ClobberMode::RemoveDestination) + )) + { + if are_hardlinks_to_same_file(source, dest) + && !options.force() + && options.backup == BackupMode::NoBackup + && source != dest + || (source == dest && options.copy_mode == CopyMode::Link) + { + return Ok(()); + } + handle_existing_dest(source, dest, options, source_in_command_line)?; + } + + if options.attributes_only + && source.is_symlink() + && !matches!( + options.overwrite, + OverwriteMode::Clobber(ClobberMode::RemoveDestination) + ) + { + return Err(format!( + "cannot change attribute {}: Source file is a non regular file", + dest.quote() + ) + .into()); + } + + if options.preserve_hard_links() { + // if we encounter a matching device/inode pair in the source tree + // we can arrange to create a hard link between the corresponding names + // in the destination tree. + if let Some(new_source) = copied_files.get( + &FileInformation::from_path(source, options.dereference(source_in_command_line)) + .context(format!("cannot stat {}", source.quote()))?, + ) { + std::fs::hard_link(new_source, dest)?; + return Ok(()); + }; + } + + if options.verbose { + print_verbose_output(options.parents, progress_bar, source, dest); + } + + // Calculate the context upfront before canonicalizing the path + let context = context_for(source, dest); + let context = context.as_str(); + + let source_metadata = { + let result = if options.dereference(source_in_command_line) { + fs::metadata(source) + } else { + fs::symlink_metadata(source) + }; + result.context(context)? + }; + + let dest_permissions = calculate_dest_permissions(dest, &source_metadata, options, context)?; + + handle_copy_mode( + source, + dest, + options, + context, + source_metadata, + symlinked_files, + source_in_command_line, + )?; + // TODO: implement something similar to gnu's lchown if !dest.is_symlink() { // Here, to match GNU semantics, we quietly ignore an error diff --git a/src/uu/csplit/Cargo.toml b/src/uu/csplit/Cargo.toml index 5e2f310cb51..8f06524de12 100644 --- a/src/uu/csplit/Cargo.toml +++ b/src/uu/csplit/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_csplit" -version = "0.0.24" +version = "0.0.25" 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" @@ -18,7 +18,7 @@ path = "src/csplit.rs" clap = { workspace = true } thiserror = { workspace = true } regex = { workspace = true } -uucore = { workspace = true, features = ["entries", "fs"] } +uucore = { workspace = true, features = ["entries", "fs", "format"] } [[bin]] name = "csplit" diff --git a/src/uu/csplit/src/csplit.rs b/src/uu/csplit/src/csplit.rs index 00bebbf4dcb..e4d7c243c23 100644 --- a/src/uu/csplit/src/csplit.rs +++ b/src/uu/csplit/src/csplit.rs @@ -2,7 +2,6 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -#![crate_name = "uu_csplit"] // spell-checker:ignore rustdoc #![allow(rustdoc::private_intra_doc_links)] @@ -89,7 +88,7 @@ impl CsplitOptions { /// more than once. pub fn csplit( options: &CsplitOptions, - patterns: Vec, + patterns: Vec, input: T, ) -> Result<(), CsplitError> where @@ -97,6 +96,7 @@ where { let mut input_iter = InputSplitter::new(input.lines().enumerate()); let mut split_writer = SplitWriter::new(options); + let patterns: Vec = patterns::get_patterns(&patterns[..])?; let ret = do_csplit(&mut split_writer, patterns, &mut input_iter); // consume the rest @@ -563,7 +563,6 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .unwrap() .map(|s| s.to_string()) .collect(); - let patterns = patterns::get_patterns(&patterns[..])?; let options = CsplitOptions::new(&matches); if file_name == "-" { let stdin = io::stdin(); diff --git a/src/uu/csplit/src/csplit_error.rs b/src/uu/csplit/src/csplit_error.rs index 1559a29f8bc..4a83b637b07 100644 --- a/src/uu/csplit/src/csplit_error.rs +++ b/src/uu/csplit/src/csplit_error.rs @@ -21,7 +21,7 @@ pub enum CsplitError { MatchNotFound(String), #[error("{}: match not found on repetition {}", ._0.quote(), _1)] MatchNotFoundOnRepetition(String, usize), - #[error("line number must be greater than zero")] + #[error("0: line number must be greater than zero")] LineNumberIsZero, #[error("line number '{}' is smaller than preceding line number, {}", _0, _1)] LineNumberSmallerThanPrevious(usize, usize), diff --git a/src/uu/csplit/src/split_name.rs b/src/uu/csplit/src/split_name.rs index 4d94b56a923..e2432f3ce10 100644 --- a/src/uu/csplit/src/split_name.rs +++ b/src/uu/csplit/src/split_name.rs @@ -4,14 +4,15 @@ // file that was distributed with this source code. // spell-checker:ignore (regex) diuox -use regex::Regex; +use uucore::format::{num_format::UnsignedInt, Format, FormatError}; use crate::csplit_error::CsplitError; /// Computes the filename of a split, taking into consideration a possible user-defined suffix /// format. pub struct SplitName { - fn_split_name: Box String>, + prefix: Vec, + format: Format, } impl SplitName { @@ -36,6 +37,7 @@ impl SplitName { ) -> Result { // get the prefix let prefix = prefix_opt.unwrap_or_else(|| "xx".to_string()); + // the width for the split offset let n_digits = n_digits_opt .map(|opt| { @@ -44,120 +46,29 @@ impl SplitName { }) .transpose()? .unwrap_or(2); - // translate the custom format into a function - let fn_split_name: Box String> = match format_opt { - None => Box::new(move |n: usize| -> String { format!("{prefix}{n:0n_digits$}") }), - Some(custom) => { - let spec = - Regex::new(r"(?P%((?P[0#-])(?P\d+)?)?(?P[diuoxX]))") - .unwrap(); - let mut captures_iter = spec.captures_iter(&custom); - let custom_fn: Box String> = match captures_iter.next() { - Some(captures) => { - let all = captures.name("ALL").unwrap(); - let before = custom[0..all.start()].to_owned(); - let after = custom[all.end()..].to_owned(); - let width = match captures.name("WIDTH") { - None => 0, - Some(m) => m.as_str().parse::().unwrap(), - }; - match (captures.name("FLAG"), captures.name("TYPE")) { - (None, Some(ref t)) => match t.as_str() { - "d" | "i" | "u" => Box::new(move |n: usize| -> String { - format!("{prefix}{before}{n}{after}") - }), - "o" => Box::new(move |n: usize| -> String { - format!("{prefix}{before}{n:o}{after}") - }), - "x" => Box::new(move |n: usize| -> String { - format!("{prefix}{before}{n:x}{after}") - }), - "X" => Box::new(move |n: usize| -> String { - format!("{prefix}{before}{n:X}{after}") - }), - _ => return Err(CsplitError::SuffixFormatIncorrect), - }, - (Some(ref f), Some(ref t)) => { - match (f.as_str(), t.as_str()) { - /* - * zero padding - */ - // decimal - ("0", "d" | "i" | "u") => Box::new(move |n: usize| -> String { - format!("{prefix}{before}{n:0width$}{after}") - }), - // octal - ("0", "o") => Box::new(move |n: usize| -> String { - format!("{prefix}{before}{n:0width$o}{after}") - }), - // lower hexadecimal - ("0", "x") => Box::new(move |n: usize| -> String { - format!("{prefix}{before}{n:0width$x}{after}") - }), - // upper hexadecimal - ("0", "X") => Box::new(move |n: usize| -> String { - format!("{prefix}{before}{n:0width$X}{after}") - }), - - /* - * Alternate form - */ - // octal - ("#", "o") => Box::new(move |n: usize| -> String { - format!("{prefix}{before}{n:>#width$o}{after}") - }), - // lower hexadecimal - ("#", "x") => Box::new(move |n: usize| -> String { - format!("{prefix}{before}{n:>#width$x}{after}") - }), - // upper hexadecimal - ("#", "X") => Box::new(move |n: usize| -> String { - format!("{prefix}{before}{n:>#width$X}{after}") - }), - - /* - * Left adjusted - */ - // decimal - ("-", "d" | "i" | "u") => Box::new(move |n: usize| -> String { - format!("{prefix}{before}{n:<#width$}{after}") - }), - // octal - ("-", "o") => Box::new(move |n: usize| -> String { - format!("{prefix}{before}{n:<#width$o}{after}") - }), - // lower hexadecimal - ("-", "x") => Box::new(move |n: usize| -> String { - format!("{prefix}{before}{n:<#width$x}{after}") - }), - // upper hexadecimal - ("-", "X") => Box::new(move |n: usize| -> String { - format!("{prefix}{before}{n:<#width$X}{after}") - }), - - _ => return Err(CsplitError::SuffixFormatIncorrect), - } - } - _ => return Err(CsplitError::SuffixFormatIncorrect), - } - } - None => return Err(CsplitError::SuffixFormatIncorrect), - }; - - // there cannot be more than one format pattern - if captures_iter.next().is_some() { - return Err(CsplitError::SuffixFormatTooManyPercents); - } - custom_fn - } + + let format_string = match format_opt { + Some(f) => f, + None => format!("%0{n_digits}u"), }; - Ok(Self { fn_split_name }) + let format = match Format::::parse(format_string) { + Ok(format) => Ok(format), + Err(FormatError::TooManySpecs(_)) => Err(CsplitError::SuffixFormatTooManyPercents), + Err(_) => Err(CsplitError::SuffixFormatIncorrect), + }?; + + Ok(Self { + prefix: prefix.as_bytes().to_owned(), + format, + }) } /// Returns the filename of the i-th split. pub fn get(&self, n: usize) -> String { - (self.fn_split_name)(n) + let mut v = self.prefix.clone(); + self.format.fmt(&mut v, n as u64).unwrap(); + String::from_utf8_lossy(&v).to_string() } } @@ -279,7 +190,7 @@ mod tests { #[test] fn alternate_form_octal() { let split_name = SplitName::new(None, Some(String::from("cst-%#10o-")), None).unwrap(); - assert_eq!(split_name.get(42), "xxcst- 0o52-"); + assert_eq!(split_name.get(42), "xxcst- 052-"); } #[test] @@ -291,7 +202,7 @@ mod tests { #[test] fn alternate_form_upper_hex() { let split_name = SplitName::new(None, Some(String::from("cst-%#10X-")), None).unwrap(); - assert_eq!(split_name.get(42), "xxcst- 0x2A-"); + assert_eq!(split_name.get(42), "xxcst- 0X2A-"); } #[test] @@ -315,19 +226,19 @@ mod tests { #[test] fn left_adjusted_octal() { let split_name = SplitName::new(None, Some(String::from("cst-%-10o-")), None).unwrap(); - assert_eq!(split_name.get(42), "xxcst-0o52 -"); + assert_eq!(split_name.get(42), "xxcst-52 -"); } #[test] fn left_adjusted_lower_hex() { let split_name = SplitName::new(None, Some(String::from("cst-%-10x-")), None).unwrap(); - assert_eq!(split_name.get(42), "xxcst-0x2a -"); + assert_eq!(split_name.get(42), "xxcst-2a -"); } #[test] fn left_adjusted_upper_hex() { let split_name = SplitName::new(None, Some(String::from("cst-%-10X-")), None).unwrap(); - assert_eq!(split_name.get(42), "xxcst-0x2A -"); + assert_eq!(split_name.get(42), "xxcst-2A -"); } #[test] diff --git a/src/uu/cut/Cargo.toml b/src/uu/cut/Cargo.toml index c98bec5bc42..17ec0c52d87 100644 --- a/src/uu/cut/Cargo.toml +++ b/src/uu/cut/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_cut" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "cut ~ (uutils) display byte/field columns of input lines" diff --git a/src/uu/cut/src/cut.rs b/src/uu/cut/src/cut.rs index 2a3196d002e..1b9194c170b 100644 --- a/src/uu/cut/src/cut.rs +++ b/src/uu/cut/src/cut.rs @@ -6,12 +6,15 @@ // spell-checker:ignore (ToDO) delim sourcefiles use bstr::io::BufReadExt; -use clap::{crate_version, Arg, ArgAction, Command}; +use clap::{builder::ValueParser, crate_version, Arg, ArgAction, ArgMatches, Command}; +use std::ffi::OsString; use std::fs::File; use std::io::{stdin, stdout, BufReader, BufWriter, IsTerminal, Read, Write}; +#[cfg(unix)] +use std::os::unix::ffi::OsStrExt; use std::path::Path; use uucore::display::Quotable; -use uucore::error::{FromIo, UResult, USimpleError}; +use uucore::error::{set_exit_code, FromIo, UResult, USimpleError}; use uucore::line_ending::LineEnding; use self::searcher::Searcher; @@ -26,27 +29,38 @@ const USAGE: &str = help_usage!("cut.md"); const ABOUT: &str = help_about!("cut.md"); const AFTER_HELP: &str = help_section!("after help", "cut.md"); -struct Options { - out_delim: Option, +struct Options<'a> { + out_delimiter: Option<&'a [u8]>, line_ending: LineEnding, + field_opts: Option>, } -enum Delimiter { +enum Delimiter<'a> { Whitespace, - String(String), // FIXME: use char? + Slice(&'a [u8]), } -struct FieldOptions { - delimiter: Delimiter, - out_delimiter: Option, +struct FieldOptions<'a> { + delimiter: Delimiter<'a>, only_delimited: bool, - line_ending: LineEnding, } -enum Mode { - Bytes(Vec, Options), - Characters(Vec, Options), - Fields(Vec, FieldOptions), +enum Mode<'a> { + Bytes(Vec, Options<'a>), + Characters(Vec, Options<'a>), + Fields(Vec, Options<'a>), +} + +impl Default for Delimiter<'_> { + fn default() -> Self { + Self::Slice(b"\t") + } +} + +impl<'a> From<&'a OsString> for Delimiter<'a> { + fn from(s: &'a OsString) -> Self { + Self::Slice(os_string_as_bytes(s).unwrap()) + } } fn stdout_writer() -> Box { @@ -69,11 +83,7 @@ fn cut_bytes(reader: R, ranges: &[Range], opts: &Options) -> UResult<() let newline_char = opts.line_ending.into(); let mut buf_in = BufReader::new(reader); let mut out = stdout_writer(); - let delim = opts - .out_delim - .as_ref() - .map_or("", String::as_str) - .as_bytes(); + let out_delim = opts.out_delimiter.unwrap_or(b"\t"); let result = buf_in.for_byte_record(newline_char, |line| { let mut print_delim = false; @@ -82,8 +92,8 @@ fn cut_bytes(reader: R, ranges: &[Range], opts: &Options) -> UResult<() break; } if print_delim { - out.write_all(delim)?; - } else if opts.out_delim.is_some() { + out.write_all(out_delim)?; + } else if opts.out_delimiter.is_some() { print_delim = true; } // change `low` from 1-indexed value to 0-index value @@ -109,7 +119,7 @@ fn cut_fields_explicit_out_delim( ranges: &[Range], only_delimited: bool, newline_char: u8, - out_delim: &str, + out_delim: &[u8], ) -> UResult<()> { let mut buf_in = BufReader::new(reader); let mut out = stdout_writer(); @@ -145,7 +155,7 @@ fn cut_fields_explicit_out_delim( for _ in 0..=high - low { // skip printing delimiter if this is the first matching field for this line if print_delim { - out.write_all(out_delim.as_bytes())?; + out.write_all(out_delim)?; } else { print_delim = true; } @@ -256,17 +266,18 @@ fn cut_fields_implicit_out_delim( Ok(()) } -fn cut_fields(reader: R, ranges: &[Range], opts: &FieldOptions) -> UResult<()> { +fn cut_fields(reader: R, ranges: &[Range], opts: &Options) -> UResult<()> { let newline_char = opts.line_ending.into(); - match opts.delimiter { - Delimiter::String(ref delim) => { - let matcher = ExactMatcher::new(delim.as_bytes()); + let field_opts = opts.field_opts.as_ref().unwrap(); // it is safe to unwrap() here - field_opts will always be Some() for cut_fields() call + match field_opts.delimiter { + Delimiter::Slice(delim) => { + let matcher = ExactMatcher::new(delim); match opts.out_delimiter { - Some(ref out_delim) => cut_fields_explicit_out_delim( + Some(out_delim) => cut_fields_explicit_out_delim( reader, &matcher, ranges, - opts.only_delimited, + field_opts.only_delimited, newline_char, out_delim, ), @@ -274,21 +285,20 @@ fn cut_fields(reader: R, ranges: &[Range], opts: &FieldOptions) -> URes reader, &matcher, ranges, - opts.only_delimited, + field_opts.only_delimited, newline_char, ), } } Delimiter::Whitespace => { let matcher = WhitespaceMatcher {}; - let out_delim = opts.out_delimiter.as_deref().unwrap_or("\t"); cut_fields_explicit_out_delim( reader, &matcher, ranges, - opts.only_delimited, + field_opts.only_delimited, newline_char, - out_delim, + opts.out_delimiter.unwrap_or(b"\t"), ) } } @@ -319,6 +329,7 @@ fn cut_files(mut filenames: Vec, mode: &Mode) { if path.is_dir() { show_error!("{}: Is a directory", filename.maybe_quote()); + set_exit_code(1); continue; } @@ -336,6 +347,84 @@ fn cut_files(mut filenames: Vec, mode: &Mode) { } } +// Helper function for processing delimiter values (which could be non UTF-8) +// It converts OsString to &[u8] for unix targets only +// On non-unix (i.e. Windows) it will just return an error if delimiter value is not UTF-8 +fn os_string_as_bytes(os_string: &OsString) -> UResult<&[u8]> { + #[cfg(unix)] + let bytes = os_string.as_bytes(); + + #[cfg(not(unix))] + let bytes = os_string + .to_str() + .ok_or_else(|| { + uucore::error::UUsageError::new( + 1, + "invalid UTF-8 was detected in one or more arguments", + ) + })? + .as_bytes(); + + Ok(bytes) +} + +// Get delimiter and output delimiter from `-d`/`--delimiter` and `--output-delimiter` options respectively +// Allow either delimiter to have a value that is neither UTF-8 nor ASCII to align with GNU behavior +fn get_delimiters( + matches: &ArgMatches, + delimiter_is_equal: bool, +) -> UResult<(Delimiter, Option<&[u8]>)> { + let whitespace_delimited = matches.get_flag(options::WHITESPACE_DELIMITED); + let delim_opt = matches.get_one::(options::DELIMITER); + let delim = match delim_opt { + Some(_) if whitespace_delimited => { + return Err(USimpleError::new( + 1, + "invalid input: Only one of --delimiter (-d) or -w option can be specified", + )); + } + Some(os_string) => { + // GNU's `cut` supports `-d=` to set the delimiter to `=`. + // Clap parsing is limited in this situation, see: + // https://github.com/uutils/coreutils/issues/2424#issuecomment-863825242 + if delimiter_is_equal { + Delimiter::Slice(b"=") + } else if os_string == "''" || os_string.is_empty() { + // treat `''` as empty delimiter + Delimiter::Slice(b"\0") + } else { + // For delimiter `-d` option value - allow both UTF-8 (possibly multi-byte) characters + // and Non UTF-8 (and not ASCII) single byte "characters", like `b"\xAD"` to align with GNU behavior + let bytes = os_string_as_bytes(os_string)?; + if os_string.to_str().is_some_and(|s| s.chars().count() > 1) + || os_string.to_str().is_none() && bytes.len() > 1 + { + return Err(USimpleError::new( + 1, + "the delimiter must be a single character", + )); + } else { + Delimiter::from(os_string) + } + } + } + None => match whitespace_delimited { + true => Delimiter::Whitespace, + false => Delimiter::default(), + }, + }; + let out_delim = matches + .get_one::(options::OUTPUT_DELIMITER) + .map(|os_string| { + if os_string.is_empty() || os_string == "''" { + b"\0" + } else { + os_string_as_bytes(os_string).unwrap() + } + }); + Ok((delim, out_delim)) +} + mod options { pub const BYTES: &str = "bytes"; pub const CHARACTERS: &str = "characters"; @@ -351,116 +440,68 @@ mod options { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let args = args.collect_ignore(); + let args = args.collect::>(); - let delimiter_is_equal = args.contains(&"-d=".to_string()); // special case + let delimiter_is_equal = args.contains(&OsString::from("-d=")); // special case let matches = uu_app().try_get_matches_from(args)?; let complement = matches.get_flag(options::COMPLEMENT); + let only_delimited = matches.get_flag(options::ONLY_DELIMITED); + + let (delimiter, out_delimiter) = get_delimiters(&matches, delimiter_is_equal)?; + let line_ending = LineEnding::from_zero_flag(matches.get_flag(options::ZERO_TERMINATED)); + + // Only one, and only one of cutting mode arguments, i.e. `-b`, `-c`, `-f`, + // is expected. The number of those arguments is used for parsing a cutting + // mode and handling the error cases. + let mode_args_count = [ + matches.indices_of(options::BYTES), + matches.indices_of(options::CHARACTERS), + matches.indices_of(options::FIELDS), + ] + .into_iter() + .map(|indices| indices.unwrap_or_default().count()) + .sum(); let mode_parse = match ( + mode_args_count, matches.get_one::(options::BYTES), matches.get_one::(options::CHARACTERS), matches.get_one::(options::FIELDS), ) { - (Some(byte_ranges), None, None) => list_to_ranges(byte_ranges, complement).map(|ranges| { + (1, Some(byte_ranges), None, None) => list_to_ranges(byte_ranges, complement).map(|ranges| { Mode::Bytes( ranges, Options { - out_delim: Some( - matches - .get_one::(options::OUTPUT_DELIMITER) - .map(|s| s.as_str()) - .unwrap_or_default() - .to_owned(), - ), - line_ending: LineEnding::from_zero_flag(matches.get_flag(options::ZERO_TERMINATED)), + out_delimiter, + line_ending, + field_opts: None, }, ) }), - (None, Some(char_ranges), None) => list_to_ranges(char_ranges, complement).map(|ranges| { + (1, None, Some(char_ranges), None) => list_to_ranges(char_ranges, complement).map(|ranges| { Mode::Characters( ranges, Options { - out_delim: Some( - matches - .get_one::(options::OUTPUT_DELIMITER) - .map(|s| s.as_str()) - .unwrap_or_default() - .to_owned(), - ), - line_ending: LineEnding::from_zero_flag(matches.get_flag(options::ZERO_TERMINATED)), + out_delimiter, + line_ending, + field_opts: None, }, ) }), - (None, None, Some(field_ranges)) => { - list_to_ranges(field_ranges, complement).and_then(|ranges| { - let out_delim = match matches.get_one::(options::OUTPUT_DELIMITER) { - Some(s) => { - if s.is_empty() { - Some("\0".to_owned()) - } else { - Some(s.clone()) - } - } - None => None, - }; - - let only_delimited = matches.get_flag(options::ONLY_DELIMITED); - let whitespace_delimited = matches.get_flag(options::WHITESPACE_DELIMITED); - let zero_terminated = matches.get_flag(options::ZERO_TERMINATED); - let line_ending = LineEnding::from_zero_flag(zero_terminated); - - match matches.get_one::(options::DELIMITER).map(|s| s.as_str()) { - Some(_) if whitespace_delimited => { - Err("invalid input: Only one of --delimiter (-d) or -w option can be specified".into()) - } - Some(mut delim) => { - // GNU's `cut` supports `-d=` to set the delimiter to `=`. - // Clap parsing is limited in this situation, see: - // https://github.com/uutils/coreutils/issues/2424#issuecomment-863825242 - if delimiter_is_equal { - delim = "="; - } else if delim == "''" { - // treat `''` as empty delimiter - delim = ""; - } - if delim.chars().count() > 1 { - Err("the delimiter must be a single character".into()) - } else { - let delim = if delim.is_empty() { - "\0".to_owned() - } else { - delim.to_owned() - }; - - Ok(Mode::Fields( - ranges, - FieldOptions { - delimiter: Delimiter::String(delim), - out_delimiter: out_delim, - only_delimited, - line_ending, - }, - )) - } - } - None => Ok(Mode::Fields( - ranges, - FieldOptions { - delimiter: match whitespace_delimited { - true => Delimiter::Whitespace, - false => Delimiter::String("\t".to_owned()), - }, - out_delimiter: out_delim, - only_delimited, - line_ending, - }, - )), - } - }) - } - (ref b, ref c, ref f) if b.is_some() || c.is_some() || f.is_some() => Err( + (1, None, None, Some(field_ranges)) => list_to_ranges(field_ranges, complement).map(|ranges| { + Mode::Fields( + ranges, + Options { + out_delimiter, + line_ending, + field_opts: Some(FieldOptions { + only_delimited, + delimiter, + })}, + ) + }), + (2.., _, _, _) => Err( "invalid usage: expects no more than one of --fields (-f), --chars (-c) or --bytes (-b)".into() ), _ => Err("invalid usage: expects one of --fields (-f), --chars (-c) or --bytes (-b)".into()), @@ -510,6 +551,13 @@ pub fn uu_app() -> Command { .about(ABOUT) .after_help(AFTER_HELP) .infer_long_args(true) + // While `args_override_self(true)` for some arguments, such as `-d` + // and `--output-delimiter`, is consistent to the behavior of GNU cut, + // arguments related to cutting mode, i.e. `-b`, `-c`, `-f`, should + // cause an error when there is more than one of them, as described in + // the manual of GNU cut: "Use one, and only one of -b, -c or -f". + // `ArgAction::Append` is used on `-b`, `-c`, `-f` arguments, so that + // the occurrences of those could be counted and be handled accordingly. .args_override_self(true) .arg( Arg::new(options::BYTES) @@ -517,7 +565,8 @@ pub fn uu_app() -> Command { .long(options::BYTES) .help("filter byte columns from the input source") .allow_hyphen_values(true) - .value_name("LIST"), + .value_name("LIST") + .action(ArgAction::Append), ) .arg( Arg::new(options::CHARACTERS) @@ -525,12 +574,14 @@ pub fn uu_app() -> Command { .long(options::CHARACTERS) .help("alias for character mode") .allow_hyphen_values(true) - .value_name("LIST"), + .value_name("LIST") + .action(ArgAction::Append), ) .arg( Arg::new(options::DELIMITER) .short('d') .long(options::DELIMITER) + .value_parser(ValueParser::os_string()) .help("specify the delimiter character that separates fields in the input source. Defaults to Tab.") .value_name("DELIM"), ) @@ -547,7 +598,8 @@ pub fn uu_app() -> Command { .long(options::FIELDS) .help("filter field columns from the input source") .allow_hyphen_values(true) - .value_name("LIST"), + .value_name("LIST") + .action(ArgAction::Append), ) .arg( Arg::new(options::COMPLEMENT) @@ -572,6 +624,7 @@ pub fn uu_app() -> Command { .arg( Arg::new(options::OUTPUT_DELIMITER) .long(options::OUTPUT_DELIMITER) + .value_parser(ValueParser::os_string()) .help("in field mode, replace the delimiter in output lines with this option's argument") .value_name("NEW_DELIM"), ) diff --git a/src/uu/date/Cargo.toml b/src/uu/date/Cargo.toml index 11ad64bbef7..bd7dd254271 100644 --- a/src/uu/date/Cargo.toml +++ b/src/uu/date/Cargo.toml @@ -1,7 +1,7 @@ # spell-checker:ignore datetime [package] name = "uu_date" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "date ~ (uutils) display or set the current time" diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index ee3c7bfdfae..bc50a8a2c58 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -6,7 +6,7 @@ // spell-checker:ignore (chrono) Datelike Timelike ; (format) DATEFILE MMDDhhmm ; (vars) datetime datetimes use chrono::format::{Item, StrftimeItems}; -use chrono::{DateTime, Duration, FixedOffset, Local, Offset, Utc}; +use chrono::{DateTime, FixedOffset, Local, Offset, TimeDelta, Utc}; #[cfg(windows)] use chrono::{Datelike, Timelike}; use clap::{crate_version, Arg, ArgAction, Command}; @@ -91,7 +91,7 @@ enum DateSource { Now, Custom(String), File(PathBuf), - Human(Duration), + Human(TimeDelta), } enum Iso8601Format { diff --git a/src/uu/dd/Cargo.toml b/src/uu/dd/Cargo.toml index 1dbb37bde55..2e32722f6fc 100644 --- a/src/uu/dd/Cargo.toml +++ b/src/uu/dd/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_dd" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "dd ~ (uutils) copy and convert files" @@ -20,11 +20,9 @@ gcd = { workspace = true } libc = { workspace = true } uucore = { workspace = true, features = ["format", "quoting-style"] } -[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 } +nix = { workspace = true, features = ["fs"] } [[bin]] name = "dd" diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index 07a754deb51..b1c6b563017 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.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 fname, ftype, 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 bufferedoutput +// spell-checker:ignore fname, ftype, 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 bufferedoutput, SETFL mod blocks; mod bufferedoutput; @@ -16,8 +16,13 @@ mod progress; use crate::bufferedoutput::BufferedOutput; use blocks::conv_block_unblock_helper; use datastructures::*; +#[cfg(any(target_os = "linux", target_os = "android"))] +use nix::fcntl::FcntlArg::F_SETFL; +#[cfg(any(target_os = "linux", target_os = "android"))] +use nix::fcntl::OFlag; use parseargs::Parser; use progress::{gen_prog_updater, ProgUpdate, ReadStat, StatusLevel, WriteStat}; +use uucore::io::OwnedFileDescriptorOrHandle; use std::cmp; use std::env; @@ -31,6 +36,8 @@ use std::os::unix::{ fs::FileTypeExt, io::{AsRawFd, FromRawFd}, }; +#[cfg(windows)] +use std::os::windows::{fs::MetadataExt, io::AsHandle}; use std::path::Path; use std::sync::{ atomic::{AtomicBool, Ordering::Relaxed}, @@ -227,7 +234,7 @@ impl Source { Err(e) => Err(e), } } - Self::File(f) => f.seek(io::SeekFrom::Start(n)), + Self::File(f) => f.seek(io::SeekFrom::Current(n.try_into().unwrap())), #[cfg(unix)] Self::Fifo(f) => io::copy(&mut f.take(n), &mut io::sink()), } @@ -283,9 +290,35 @@ impl<'a> Input<'a> { /// Instantiate this struct with stdin as a source. fn new_stdin(settings: &'a Settings) -> UResult { #[cfg(not(unix))] - let mut src = Source::Stdin(io::stdin()); + let mut src = { + let f = File::from(io::stdin().as_handle().try_clone_to_owned()?); + let is_file = if let Ok(metadata) = f.metadata() { + // this hack is needed as there is no other way on windows + // to differentiate between the case where `seek` works + // on a file handle or not. i.e. when the handle is no real + // file but a pipe, `seek` is still successful, but following + // `read`s are not affected by the seek. + metadata.creation_time() != 0 + } else { + false + }; + if is_file { + Source::File(f) + } else { + Source::Stdin(io::stdin()) + } + }; #[cfg(unix)] let mut src = Source::stdin_as_file(); + #[cfg(unix)] + if let Source::StdinFile(f) = &src { + // GNU compatibility: + // this will check whether stdin points to a folder or not + if f.metadata()?.is_file() && settings.iflags.directory { + show_error!("standard input: not a directory"); + return Err(1.into()); + } + }; if settings.skip > 0 { src.skip(settings.skip)?; } @@ -557,7 +590,7 @@ impl Dest { return Ok(len); } } - f.seek(io::SeekFrom::Start(n)) + f.seek(io::SeekFrom::Current(n.try_into().unwrap())) } #[cfg(unix)] Self::Fifo(f) => { @@ -699,6 +732,11 @@ impl<'a> Output<'a> { if !settings.oconv.notrunc { dst.set_len(settings.seek).ok(); } + + Self::prepare_file(dst, settings) + } + + fn prepare_file(dst: File, settings: &'a Settings) -> UResult { let density = if settings.oconv.sparse { Density::Sparse } else { @@ -710,6 +748,24 @@ impl<'a> Output<'a> { Ok(Self { dst, settings }) } + /// Instantiate this struct with file descriptor as a destination. + /// + /// This is useful e.g. for the case when the file descriptor was + /// already opened by the system (stdout) and has a state + /// (current position) that shall be used. + fn new_file_from_stdout(settings: &'a Settings) -> UResult { + let fx = OwnedFileDescriptorOrHandle::from(io::stdout())?; + #[cfg(any(target_os = "linux", target_os = "android"))] + if let Some(libc_flags) = make_linux_oflags(&settings.oflags) { + nix::fcntl::fcntl( + fx.as_raw().as_raw_fd(), + F_SETFL(OFlag::from_bits_retain(libc_flags)), + )?; + } + + Self::prepare_file(fx.into_file(), settings) + } + /// Instantiate this struct with the given named pipe as a destination. #[cfg(unix)] fn new_fifo(filename: &Path, settings: &'a Settings) -> UResult { @@ -1287,9 +1343,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { #[cfg(unix)] Some(ref outfile) if is_fifo(outfile) => Output::new_fifo(Path::new(&outfile), &settings)?, Some(ref outfile) => Output::new_file(Path::new(&outfile), &settings)?, - None if is_stdout_redirected_to_seekable_file() => { - Output::new_file(Path::new(&stdout_canonicalized()), &settings)? - } + None if is_stdout_redirected_to_seekable_file() => Output::new_file_from_stdout(&settings)?, None => Output::new_stdout(&settings)?, }; dd_copy(i, o).map_err_context(|| "IO error".to_string()) diff --git a/src/uu/dd/src/parseargs.rs b/src/uu/dd/src/parseargs.rs index 60ce9a6971f..93d6c63a97d 100644 --- a/src/uu/dd/src/parseargs.rs +++ b/src/uu/dd/src/parseargs.rs @@ -275,7 +275,7 @@ impl Parser { fn parse_n(val: &str) -> Result { let n = parse_bytes_with_opt_multiplier(val)?; - Ok(if val.ends_with('B') { + Ok(if val.contains('B') { Num::Bytes(n) } else { Num::Blocks(n) @@ -490,6 +490,8 @@ fn parse_bytes_only(s: &str) -> Result { /// 512. You can also use standard block size suffixes like `'k'` for /// 1024. /// +/// If the number would be too large, return [`std::u64::MAX`] instead. +/// /// # Errors /// /// If a number cannot be parsed or if the multiplication would cause @@ -507,17 +509,16 @@ fn parse_bytes_only(s: &str) -> Result { fn parse_bytes_no_x(full: &str, s: &str) -> Result { let parser = SizeParser { capital_b_bytes: true, + no_empty_numeric: true, ..Default::default() }; let (num, multiplier) = match (s.find('c'), s.rfind('w'), s.rfind('b')) { (None, None, None) => match parser.parse_u64(s) { Ok(n) => (n, 1), + Err(ParseSizeError::SizeTooBig(_)) => (u64::MAX, 1), Err(ParseSizeError::InvalidSuffix(_) | ParseSizeError::ParseFailure(_)) => { return Err(ParseError::InvalidNumber(full.to_string())) } - Err(ParseSizeError::SizeTooBig(_)) => { - return Err(ParseError::MultiplierStringOverflow(full.to_string())) - } }, (Some(i), None, None) => (parse_bytes_only(&s[..i])?, 1), (None, Some(i), None) => (parse_bytes_only(&s[..i])?, 2), @@ -630,22 +631,42 @@ fn conversion_mode( #[cfg(test)] mod tests { - use crate::parseargs::parse_bytes_with_opt_multiplier; + use crate::parseargs::{parse_bytes_with_opt_multiplier, Parser}; + use crate::Num; + use std::matches; + const BIG: &str = "9999999999999999999999999999999999999999999999999999999999999"; #[test] - fn test_parse_bytes_with_opt_multiplier() { + fn test_parse_bytes_with_opt_multiplier_invalid() { + assert!(parse_bytes_with_opt_multiplier("123asdf").is_err()); + } + + #[test] + fn test_parse_bytes_with_opt_multiplier_without_x() { assert_eq!(parse_bytes_with_opt_multiplier("123").unwrap(), 123); assert_eq!(parse_bytes_with_opt_multiplier("123c").unwrap(), 123); // 123 * 1 assert_eq!(parse_bytes_with_opt_multiplier("123w").unwrap(), 123 * 2); assert_eq!(parse_bytes_with_opt_multiplier("123b").unwrap(), 123 * 512); - assert_eq!(parse_bytes_with_opt_multiplier("123x3").unwrap(), 123 * 3); assert_eq!(parse_bytes_with_opt_multiplier("123k").unwrap(), 123 * 1024); - assert_eq!(parse_bytes_with_opt_multiplier("1x2x3").unwrap(), 6); // 1 * 2 * 3 + assert_eq!(parse_bytes_with_opt_multiplier(BIG).unwrap(), u64::MAX); + } + #[test] + fn test_parse_bytes_with_opt_multiplier_with_x() { + assert_eq!(parse_bytes_with_opt_multiplier("123x3").unwrap(), 123 * 3); + assert_eq!(parse_bytes_with_opt_multiplier("1x2x3").unwrap(), 6); // 1 * 2 * 3 assert_eq!( parse_bytes_with_opt_multiplier("1wx2cx3w").unwrap(), 2 * 2 * (3 * 2) // (1 * 2) * (2 * 1) * (3 * 2) ); - assert!(parse_bytes_with_opt_multiplier("123asdf").is_err()); + } + #[test] + fn test_parse_n() { + for arg in ["1x8x4", "1c", "123b", "123w"] { + assert!(matches!(Parser::parse_n(arg), Ok(Num::Blocks(_)))); + } + for arg in ["1Bx8x4", "2Bx8", "2Bx8B", "2x8B"] { + assert!(matches!(Parser::parse_n(arg), Ok(Num::Bytes(_)))); + } } } diff --git a/src/uu/dd/src/parseargs/unit_tests.rs b/src/uu/dd/src/parseargs/unit_tests.rs index 51b0933e926..baeafdd56ae 100644 --- a/src/uu/dd/src/parseargs/unit_tests.rs +++ b/src/uu/dd/src/parseargs/unit_tests.rs @@ -13,6 +13,7 @@ use crate::parseargs::Parser; use crate::StatusLevel; #[cfg(not(any(target_os = "linux", target_os = "android")))] +#[allow(clippy::useless_vec)] #[test] fn unimplemented_flags_should_error_non_linux() { let mut succeeded = Vec::new(); @@ -55,6 +56,7 @@ fn unimplemented_flags_should_error_non_linux() { } #[test] +#[allow(clippy::useless_vec)] fn unimplemented_flags_should_error() { let mut succeeded = Vec::new(); @@ -506,14 +508,6 @@ mod test_64bit_arch { ); } -#[test] -#[should_panic] -fn test_overflow_panic() { - let bs_str = format!("{}KiB", u64::MAX); - - parse_bytes_with_opt_multiplier(&bs_str).unwrap(); -} - #[test] #[should_panic] fn test_neg_panic() { diff --git a/src/uu/df/Cargo.toml b/src/uu/df/Cargo.toml index e9aa192e820..788db50406d 100644 --- a/src/uu/df/Cargo.toml +++ b/src/uu/df/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_df" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "df ~ (uutils) display file system information" diff --git a/src/uu/df/src/table.rs b/src/uu/df/src/table.rs index f6e09420482..daa30d1eef5 100644 --- a/src/uu/df/src/table.rs +++ b/src/uu/df/src/table.rs @@ -58,13 +58,13 @@ pub(crate) struct Row { bytes_capacity: Option, /// Total number of inodes in the filesystem. - inodes: u64, + inodes: u128, /// Number of used inodes. - inodes_used: u64, + inodes_used: u128, /// Number of free inodes. - inodes_free: u64, + inodes_free: u128, /// Percentage of inodes that are used, given as a float between 0 and 1. /// @@ -178,9 +178,9 @@ impl From for Row { } else { Some(bavail as f64 / ((bused + bavail) as f64)) }, - inodes: files, - inodes_used: fused, - inodes_free: ffree, + inodes: files as u128, + inodes_used: fused as u128, + inodes_free: ffree as u128, inodes_usage: if files == 0 { None } else { @@ -235,9 +235,9 @@ impl<'a> RowFormatter<'a> { /// Get a string giving the scaled version of the input number. /// /// The scaling factor is defined in the `options` field. - fn scaled_inodes(&self, size: u64) -> String { + fn scaled_inodes(&self, size: u128) -> String { if let Some(h) = self.options.human_readable { - to_magnitude_and_suffix(size.into(), SuffixType::HumanReadable(h)) + to_magnitude_and_suffix(size, SuffixType::HumanReadable(h)) } else { size.to_string() } @@ -395,12 +395,6 @@ impl Table { let values = fmt.get_values(); total += row; - for (i, value) in values.iter().enumerate() { - if UnicodeWidthStr::width(value.as_str()) > widths[i] { - widths[i] = UnicodeWidthStr::width(value.as_str()); - } - } - rows.push(values); } } @@ -410,6 +404,16 @@ impl Table { rows.push(total_row.get_values()); } + // extend the column widths (in chars) for long values in rows + // do it here, after total row was added to the list of rows + for row in &rows { + for (i, value) in row.iter().enumerate() { + if UnicodeWidthStr::width(value.as_str()) > widths[i] { + widths[i] = UnicodeWidthStr::width(value.as_str()); + } + } + } + Self { rows, widths, @@ -466,9 +470,11 @@ impl fmt::Display for Table { #[cfg(test)] mod tests { + use std::vec; + use crate::blocks::HumanReadable; use crate::columns::Column; - use crate::table::{Header, HeaderMode, Row, RowFormatter}; + use crate::table::{Header, HeaderMode, Row, RowFormatter, Table}; use crate::{BlockSize, Options}; const COLUMNS_WITH_FS_TYPE: [Column; 7] = [ @@ -848,4 +854,89 @@ mod tests { assert_eq!(row.inodes_used, 0); } + + #[test] + fn test_table_column_width_computation_include_total_row() { + let d1 = crate::Filesystem { + file: None, + mount_info: crate::MountInfo { + dev_id: "28".to_string(), + dev_name: "none".to_string(), + fs_type: "9p".to_string(), + mount_dir: "/usr/lib/wsl/drivers".to_string(), + mount_option: "ro,nosuid,nodev,noatime".to_string(), + mount_root: "/".to_string(), + remote: false, + dummy: false, + }, + usage: crate::table::FsUsage { + blocksize: 4096, + blocks: 244029695, + bfree: 125085030, + bavail: 125085030, + bavail_top_bit_set: false, + files: 99999999999, + ffree: 999999, + }, + }; + + let filesystems = vec![d1.clone(), d1]; + + let mut options = Options { + show_total: true, + columns: vec![ + Column::Source, + Column::Itotal, + Column::Iused, + Column::Iavail, + ], + ..Default::default() + }; + + let table_w_total = Table::new(&options, filesystems.clone()); + assert_eq!( + table_w_total.to_string(), + "Filesystem Inodes IUsed IFree\n\ + none 99999999999 99999000000 999999\n\ + none 99999999999 99999000000 999999\n\ + total 199999999998 199998000000 1999998" + ); + + options.show_total = false; + + let table_w_o_total = Table::new(&options, filesystems); + assert_eq!( + table_w_o_total.to_string(), + "Filesystem Inodes IUsed IFree\n\ + none 99999999999 99999000000 999999\n\ + none 99999999999 99999000000 999999" + ); + } + + #[test] + fn test_row_accumulation_u64_overflow() { + let total = u64::MAX as u128; + let used1 = 3000u128; + let used2 = 50000u128; + + let mut row1 = Row { + inodes: total, + inodes_used: used1, + inodes_free: total - used1, + ..Default::default() + }; + + let row2 = Row { + inodes: total, + inodes_used: used2, + inodes_free: total - used2, + ..Default::default() + }; + + row1 += row2; + + assert_eq!(row1.inodes, total * 2); + assert_eq!(row1.inodes_used, used1 + used2); + assert_eq!(row1.inodes_free, total * 2 - used1 - used2); + } } diff --git a/src/uu/dir/Cargo.toml b/src/uu/dir/Cargo.toml index b82298ce319..24da984af03 100644 --- a/src/uu/dir/Cargo.toml +++ b/src/uu/dir/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_dir" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "shortcut to ls -C -b" diff --git a/src/uu/dircolors/Cargo.toml b/src/uu/dircolors/Cargo.toml index 1bf87c22cc1..15f943a4637 100644 --- a/src/uu/dircolors/Cargo.toml +++ b/src/uu/dircolors/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_dircolors" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "dircolors ~ (uutils) display commands to set LS_COLORS" diff --git a/src/uu/dirname/Cargo.toml b/src/uu/dirname/Cargo.toml index 85391859663..49ebcb653c3 100644 --- a/src/uu/dirname/Cargo.toml +++ b/src/uu/dirname/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_dirname" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "dirname ~ (uutils) display parent directory of PATHNAME" diff --git a/src/uu/du/Cargo.toml b/src/uu/du/Cargo.toml index 8b9eb062e48..03c5e1f8f20 100644 --- a/src/uu/du/Cargo.toml +++ b/src/uu/du/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_du" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "du ~ (uutils) display disk usage" diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index 62fcfceda01..b71d5f44c13 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -785,8 +785,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .send(Err(USimpleError::new( 1, format!( - "{}: No such file or directory", - path.to_string_lossy().maybe_quote() + "cannot access {}: No such file or directory", + path.to_string_lossy().quote() ), ))) .map_err(|e| USimpleError::new(1, e.to_string()))?; diff --git a/src/uu/echo/Cargo.toml b/src/uu/echo/Cargo.toml index ddb896c3389..bd70c5eee58 100644 --- a/src/uu/echo/Cargo.toml +++ b/src/uu/echo/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_echo" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "echo ~ (uutils) display TEXT" diff --git a/src/uu/echo/src/echo.rs b/src/uu/echo/src/echo.rs index a34c99bc91d..c94443822e0 100644 --- a/src/uu/echo/src/echo.rs +++ b/src/uu/echo/src/echo.rs @@ -132,13 +132,15 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { + // Note: echo is different from the other utils in that it should **not** + // have `infer_long_args(true)`, because, for example, `--ver` should be + // printed as `--ver` and not show the version text. Command::new(uucore::util_name()) // TrailingVarArg specifies the final positional argument is a VarArg // and it doesn't attempts the parse any further args. // Final argument must have multiple(true) or the usage string equivalent. .trailing_var_arg(true) .allow_hyphen_values(true) - .infer_long_args(true) .version(crate_version!()) .about(ABOUT) .after_help(AFTER_HELP) diff --git a/src/uu/env/Cargo.toml b/src/uu/env/Cargo.toml index 26fa1ebbec8..03bebdde86d 100644 --- a/src/uu/env/Cargo.toml +++ b/src/uu/env/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_env" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "env ~ (uutils) set each NAME to VALUE in the environment and run COMMAND" diff --git a/src/uu/env/src/env.rs b/src/uu/env/src/env.rs index 608357f5050..4f2790dc8b4 100644 --- a/src/uu/env/src/env.rs +++ b/src/uu/env/src/env.rs @@ -5,19 +5,32 @@ // spell-checker:ignore (ToDO) chdir execvp progname subcommand subcommands unsets setenv putenv spawnp SIGSEGV SIGBUS sigaction +pub mod native_int_str; +pub mod parse_error; +pub mod split_iterator; +pub mod string_expander; +pub mod string_parser; +pub mod variable_parser; + +use clap::builder::ValueParser; use clap::{crate_name, crate_version, Arg, ArgAction, Command}; use ini::Ini; +use native_int_str::{ + from_native_int_representation_owned, Convert, NCvt, NativeIntStr, NativeIntString, NativeStr, +}; #[cfg(unix)] use nix::sys::signal::{raise, sigaction, SaFlags, SigAction, SigHandler, SigSet, Signal}; use std::borrow::Cow; use std::env; +use std::ffi::{OsStr, OsString}; use std::io::{self, Write}; -use std::iter::Iterator; +use std::ops::Deref; + #[cfg(unix)] use std::os::unix::process::ExitStatusExt; -use std::process; +use std::process::{self}; use uucore::display::Quotable; -use uucore::error::{UClapError, UResult, USimpleError, UUsageError}; +use uucore::error::{ExitCode, UError, UResult, USimpleError, UUsageError}; use uucore::line_ending::LineEnding; use uucore::{format_usage, help_about, help_section, help_usage, show_warning}; @@ -25,14 +38,16 @@ const ABOUT: &str = help_about!("env.md"); const USAGE: &str = help_usage!("env.md"); const AFTER_HELP: &str = help_section!("after help", "env.md"); +const ERROR_MSG_S_SHEBANG: &str = "use -[v]S to pass options in shebang lines"; + struct Options<'a> { ignore_env: bool, line_ending: LineEnding, - running_directory: Option<&'a str>, - files: Vec<&'a str>, - unsets: Vec<&'a str>, - sets: Vec<(&'a str, &'a str)>, - program: Vec<&'a str>, + running_directory: Option<&'a OsStr>, + files: Vec<&'a OsStr>, + unsets: Vec<&'a OsStr>, + sets: Vec<(Cow<'a, OsStr>, Cow<'a, OsStr>)>, + program: Vec<&'a OsStr>, } // print name=value env pairs on screen @@ -45,13 +60,13 @@ fn print_env(line_ending: LineEnding) { } } -fn parse_name_value_opt<'a>(opts: &mut Options<'a>, opt: &'a str) -> UResult { +fn parse_name_value_opt<'a>(opts: &mut Options<'a>, opt: &'a OsStr) -> UResult { // is it a NAME=VALUE like opt ? - if let Some(idx) = opt.find('=') { + let wrap = NativeStr::<'a>::new(opt); + let split_o = wrap.split_once(&'='); + if let Some((name, value)) = split_o { // yes, so push name, value pair - let (name, value) = opt.split_at(idx); - opts.sets.push((name, &value['='.len_utf8()..])); - + opts.sets.push((name, value)); Ok(false) } else { // no, it's a program-like opt @@ -59,7 +74,7 @@ fn parse_name_value_opt<'a>(opts: &mut Options<'a>, opt: &'a str) -> UResult(opts: &mut Options<'a>, opt: &'a str) -> UResult<()> { +fn parse_program_opt<'a>(opts: &mut Options<'a>, opt: &'a OsStr) -> UResult<()> { if opts.line_ending == LineEnding::Nul { Err(UUsageError::new( 125, @@ -97,23 +112,6 @@ fn load_config_file(opts: &mut Options) -> UResult<()> { Ok(()) } -#[cfg(not(windows))] -#[allow(clippy::ptr_arg)] -fn build_command<'a, 'b>(args: &'a Vec<&'b str>) -> (Cow<'b, str>, &'a [&'b str]) { - let progname = Cow::from(args[0]); - (progname, &args[1..]) -} - -#[cfg(windows)] -fn build_command<'a, 'b>(args: &'a mut Vec<&'b str>) -> (Cow<'b, str>, &'a [&'b str]) { - args.insert(0, "/d/c"); - let progname = env::var("ComSpec") - .map(Cow::from) - .unwrap_or_else(|_| Cow::from("cmd")); - - (progname, &args[..]) -} - pub fn uu_app() -> Command { Command::new(crate_name!()) .version(crate_version!()) @@ -135,6 +133,7 @@ pub fn uu_app() -> Command { .long("chdir") .number_of_values(1) .value_name("DIR") + .value_parser(ValueParser::os_string()) .value_hint(clap::ValueHint::DirPath) .help("change working directory to DIR"), ) @@ -154,6 +153,7 @@ pub fn uu_app() -> Command { .long("file") .value_name("PATH") .value_hint(clap::ValueHint::FilePath) + .value_parser(ValueParser::os_string()) .action(ArgAction::Append) .help( "read and set variables from a \".env\"-style configuration file \ @@ -166,25 +166,281 @@ pub fn uu_app() -> Command { .long("unset") .value_name("NAME") .action(ArgAction::Append) + .value_parser(ValueParser::os_string()) .help("remove variable from the environment"), ) - .arg(Arg::new("vars").action(ArgAction::Append)) + .arg( + Arg::new("debug") + .short('v') + .long("debug") + .action(ArgAction::SetTrue) + .help("print verbose information for each processing step"), + ) + .arg( + Arg::new("split-string") // split string handling is implemented directly, not using CLAP. But this entry here is needed for the help information output. + .short('S') + .long("split-string") + .value_name("S") + .action(ArgAction::Set) + .value_parser(ValueParser::os_string()) + .help("process and split S into separate arguments; used to pass multiple arguments on shebang lines") + ) + .arg( + Arg::new("vars") + .action(ArgAction::Append) + .value_parser(ValueParser::os_string()) + ) +} + +pub fn parse_args_from_str(text: &NativeIntStr) -> UResult> { + split_iterator::split(text).map_err(|e| match e { + parse_error::ParseError::BackslashCNotAllowedInDoubleQuotes { pos: _ } => { + USimpleError::new(125, "'\\c' must not appear in double-quoted -S string") + } + parse_error::ParseError::InvalidBackslashAtEndOfStringInMinusS { pos: _, quoting: _ } => { + USimpleError::new(125, "invalid backslash at end of string in -S") + } + parse_error::ParseError::InvalidSequenceBackslashXInMinusS { pos: _, c } => { + USimpleError::new(125, format!("invalid sequence '\\{}' in -S", c)) + } + parse_error::ParseError::MissingClosingQuote { pos: _, c: _ } => { + USimpleError::new(125, "no terminating quote in -S string") + } + parse_error::ParseError::ParsingOfVariableNameFailed { pos, msg } => { + USimpleError::new(125, format!("variable name issue (at {}): {}", pos, msg,)) + } + _ => USimpleError::new(125, format!("Error: {:?}", e)), + }) +} + +fn debug_print_args(args: &[OsString]) { + eprintln!("input args:"); + for (i, arg) in args.iter().enumerate() { + eprintln!("arg[{}]: {}", i, arg.quote()); + } +} + +fn check_and_handle_string_args( + arg: &OsString, + prefix_to_test: &str, + all_args: &mut Vec, + do_debug_print_args: Option<&Vec>, +) -> UResult { + let native_arg = NCvt::convert(arg); + if let Some(remaining_arg) = native_arg.strip_prefix(&*NCvt::convert(prefix_to_test)) { + if let Some(input_args) = do_debug_print_args { + debug_print_args(input_args); // do it here, such that its also printed when we get an error/panic during parsing + } + + let arg_strings = parse_args_from_str(remaining_arg)?; + all_args.extend( + arg_strings + .into_iter() + .map(from_native_int_representation_owned), + ); + + Ok(true) + } else { + Ok(false) + } +} + +#[derive(Default)] +struct EnvAppData { + do_debug_printing: bool, + had_string_argument: bool, } -#[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)?; +impl EnvAppData { + fn make_error_no_such_file_or_dir(&self, prog: &OsStr) -> Box { + uucore::show_error!("{}: No such file or directory", prog.quote()); + if !self.had_string_argument { + uucore::show_error!("{}", ERROR_MSG_S_SHEBANG); + } + ExitCode::new(127) + } + + fn process_all_string_arguments( + &mut self, + original_args: &Vec, + ) -> UResult> { + let mut all_args: Vec = Vec::new(); + for arg in original_args { + match arg { + b if check_and_handle_string_args(b, "--split-string", &mut all_args, None)? => { + self.had_string_argument = true; + } + b if check_and_handle_string_args(b, "-S", &mut all_args, None)? => { + self.had_string_argument = true; + } + b if check_and_handle_string_args( + b, + "-vS", + &mut all_args, + Some(original_args), + )? => + { + self.do_debug_printing = true; + self.had_string_argument = true; + } + _ => { + all_args.push(arg.clone()); + } + } + } + + Ok(all_args) + } + + fn parse_arguments( + &mut self, + original_args: impl uucore::Args, + ) -> Result<(Vec, clap::ArgMatches), Box> { + let original_args: Vec = original_args.collect(); + let args = self.process_all_string_arguments(&original_args)?; + let app = uu_app(); + let matches = app + .try_get_matches_from(args) + .map_err(|e| -> Box { + match e.kind() { + clap::error::ErrorKind::DisplayHelp + | clap::error::ErrorKind::DisplayVersion => e.into(), + _ => { + // extent any real issue with parameter parsing by the ERROR_MSG_S_SHEBANG + let s = format!("{}", e); + if !s.is_empty() { + let s = s.trim_end(); + uucore::show_error!("{}", s); + } + uucore::show_error!("{}", ERROR_MSG_S_SHEBANG); + uucore::error::ExitCode::new(125) + } + } + })?; + Ok((original_args, matches)) + } + + fn run_env(&mut self, original_args: impl uucore::Args) -> UResult<()> { + let (original_args, matches) = self.parse_arguments(original_args)?; + + let did_debug_printing_before = self.do_debug_printing; // could have been done already as part of the "-vS" string parsing + let do_debug_printing = self.do_debug_printing || matches.get_flag("debug"); + if do_debug_printing && !did_debug_printing_before { + debug_print_args(&original_args); + } + + let mut opts = make_options(&matches)?; + + apply_change_directory(&opts)?; + // NOTE: we manually set and unset the env vars below rather than using Command::env() to more + // easily handle the case where no command is given + + apply_removal_of_all_env_vars(&opts); + + // load .env-style config file prior to those given on the command-line + load_config_file(&mut opts)?; + + apply_unset_env_vars(&opts)?; + + apply_specified_env_vars(&opts); + + if opts.program.is_empty() { + // no program provided, so just dump all env vars to stdout + print_env(opts.line_ending); + } else { + return self.run_program(opts, do_debug_printing); + } + + Ok(()) + } + + fn run_program( + &mut self, + opts: Options<'_>, + do_debug_printing: bool, + ) -> Result<(), Box> { + let prog = Cow::from(opts.program[0]); + let args = &opts.program[1..]; + if do_debug_printing { + eprintln!("executable: {}", prog.quote()); + for (i, arg) in args.iter().enumerate() { + eprintln!("arg[{}]: {}", i, arg.quote()); + } + } + // we need to execute a command + + /* + * On Unix-like systems Command::status either ends up calling either fork or posix_spawnp + * (which ends up calling clone). Keep using the current process would be ideal, but the + * standard library contains many checks and fail-safes to ensure the process ends up being + * created. This is much simpler than dealing with the hassles of calling execvp directly. + */ + match process::Command::new(&*prog).args(args).status() { + Ok(exit) if !exit.success() => { + #[cfg(unix)] + if let Some(exit_code) = exit.code() { + return Err(exit_code.into()); + } else { + // `exit.code()` returns `None` on Unix when the process is terminated by a signal. + // See std::os::unix::process::ExitStatusExt for more information. This prints out + // the interrupted process and the signal it received. + let signal_code = exit.signal().unwrap(); + let signal = Signal::try_from(signal_code).unwrap(); + + // We have to disable any handler that's installed by default. + // This ensures that we exit on this signal. + // For example, `SIGSEGV` and `SIGBUS` have default handlers installed in Rust. + // We ignore the errors because there is not much we can do if that fails anyway. + // SAFETY: The function is unsafe because installing functions is unsafe, but we are + // just defaulting to default behavior and not installing a function. Hence, the call + // is safe. + let _ = unsafe { + sigaction( + signal, + &SigAction::new(SigHandler::SigDfl, SaFlags::empty(), SigSet::all()), + ) + }; + + let _ = raise(signal); + } + return Err(exit.code().unwrap().into()); + } + Err(ref err) + if (err.kind() == io::ErrorKind::NotFound) + || (err.kind() == io::ErrorKind::InvalidInput) => + { + return Err(self.make_error_no_such_file_or_dir(prog.deref())); + } + Err(e) => { + uucore::show_error!("unknown error: {:?}", e); + return Err(126.into()); + } + Ok(_) => (), + } + Ok(()) + } +} + +fn apply_removal_of_all_env_vars(opts: &Options<'_>) { + // remove all env vars if told to ignore presets + if opts.ignore_env { + for (ref name, _) in env::vars_os() { + env::remove_var(name); + } + } +} + +fn make_options(matches: &clap::ArgMatches) -> UResult> { let ignore_env = matches.get_flag("ignore-environment"); let line_ending = LineEnding::from_zero_flag(matches.get_flag("null")); - let running_directory = matches.get_one::("chdir").map(|s| s.as_str()); - let files = match matches.get_many::("file") { - Some(v) => v.map(|s| s.as_str()).collect(), + let running_directory = matches.get_one::("chdir").map(|s| s.as_os_str()); + let files = match matches.get_many::("file") { + Some(v) => v.map(|s| s.as_os_str()).collect(), None => Vec::with_capacity(0), }; - let unsets = match matches.get_many::("unset") { - Some(v) => v.map(|s| s.as_str()).collect(), + let unsets = match matches.get_many::("unset") { + Some(v) => v.map(|s| s.as_os_str()).collect(), None => Vec::with_capacity(0), }; @@ -198,21 +454,8 @@ fn run_env(args: impl uucore::Args) -> UResult<()> { program: vec![], }; - // change directory - if let Some(d) = opts.running_directory { - match env::set_current_dir(d) { - Ok(()) => d, - Err(error) => { - return Err(USimpleError::new( - 125, - format!("cannot change directory to \"{d}\": {error}"), - )); - } - }; - } - let mut begin_prog_opts = false; - if let Some(mut iter) = matches.get_many::("vars") { + if let Some(mut iter) = matches.get_many::("vars") { // read NAME=VALUE arguments (and up to a single program argument) while !begin_prog_opts { if let Some(opt) = iter.next() { @@ -232,30 +475,16 @@ fn run_env(args: impl uucore::Args) -> UResult<()> { } } - // GNU env tests this behavior - if opts.program.is_empty() && running_directory.is_some() { - return Err(UUsageError::new( - 125, - "must specify command with --chdir (-C)".to_string(), - )); - } - - // NOTE: we manually set and unset the env vars below rather than using Command::env() to more - // easily handle the case where no command is given - - // remove all env vars if told to ignore presets - if opts.ignore_env { - for (ref name, _) in env::vars() { - env::remove_var(name); - } - } - - // load .env-style config file prior to those given on the command-line - load_config_file(&mut opts)?; + Ok(opts) +} - // unset specified env vars +fn apply_unset_env_vars(opts: &Options<'_>) -> Result<(), Box> { for name in &opts.unsets { - if name.is_empty() || name.contains(0 as char) || name.contains('=') { + let native_name = NativeStr::new(name); + if name.is_empty() + || native_name.contains(&'\0').unwrap() + || native_name.contains(&'=').unwrap() + { return Err(USimpleError::new( 125, format!("cannot unset {}: Invalid argument", name.quote()), @@ -264,9 +493,35 @@ fn run_env(args: impl uucore::Args) -> UResult<()> { env::remove_var(name); } + Ok(()) +} + +fn apply_change_directory(opts: &Options<'_>) -> Result<(), Box> { + // GNU env tests this behavior + if opts.program.is_empty() && opts.running_directory.is_some() { + return Err(UUsageError::new( + 125, + "must specify command with --chdir (-C)".to_string(), + )); + } + + if let Some(d) = opts.running_directory { + match env::set_current_dir(d) { + Ok(()) => d, + Err(error) => { + return Err(USimpleError::new( + 125, + format!("cannot change directory to {}: {error}", d.quote()), + )); + } + }; + } + Ok(()) +} +fn apply_specified_env_vars(opts: &Options<'_>) { // set specified env vars - for &(name, val) in &opts.sets { + for (name, val) in &opts.sets { /* * set_var panics if name is an empty string * set_var internally calls setenv (on unix at least), while GNU env calls putenv instead. @@ -295,64 +550,9 @@ fn run_env(args: impl uucore::Args) -> UResult<()> { } env::set_var(name, val); } - - if opts.program.is_empty() { - // no program provided, so just dump all env vars to stdout - print_env(opts.line_ending); - } else { - // we need to execute a command - #[cfg(windows)] - let (prog, args) = build_command(&mut opts.program); - #[cfg(not(windows))] - let (prog, args) = build_command(&opts.program); - - /* - * On Unix-like systems Command::status either ends up calling either fork or posix_spawnp - * (which ends up calling clone). Keep using the current process would be ideal, but the - * standard library contains many checks and fail-safes to ensure the process ends up being - * created. This is much simpler than dealing with the hassles of calling execvp directly. - */ - match process::Command::new(&*prog).args(args).status() { - Ok(exit) if !exit.success() => { - #[cfg(unix)] - if let Some(exit_code) = exit.code() { - return Err(exit_code.into()); - } else { - // `exit.code()` returns `None` on Unix when the process is terminated by a signal. - // See std::os::unix::process::ExitStatusExt for more information. This prints out - // the interrupted process and the signal it received. - let signal_code = exit.signal().unwrap(); - let signal = Signal::try_from(signal_code).unwrap(); - - // We have to disable any handler that's installed by default. - // This ensures that we exit on this signal. - // For example, `SIGSEGV` and `SIGBUS` have default handlers installed in Rust. - // We ignore the errors because there is not much we can do if that fails anyway. - // SAFETY: The function is unsafe because installing functions is unsafe, but we are - // just defaulting to default behavior and not installing a function. Hence, the call - // is safe. - let _ = unsafe { - sigaction( - signal, - &SigAction::new(SigHandler::SigDfl, SaFlags::empty(), SigSet::all()), - ) - }; - - let _ = raise(signal); - } - #[cfg(not(unix))] - return Err(exit.code().unwrap().into()); - } - Err(ref err) if err.kind() == io::ErrorKind::NotFound => return Err(127.into()), - Err(_) => return Err(126.into()), - Ok(_) => (), - } - } - - Ok(()) } #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - run_env(args) + EnvAppData::default().run_env(args) } diff --git a/src/uu/env/src/native_int_str.rs b/src/uu/env/src/native_int_str.rs new file mode 100644 index 00000000000..dc1e741e1e1 --- /dev/null +++ b/src/uu/env/src/native_int_str.rs @@ -0,0 +1,325 @@ +// 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. + +// This module contains classes and functions for dealing with the differences +// between operating systems regarding the lossless processing of OsStr/OsString. +// In contrast to existing crates with similar purpose, this module does not use any +// `unsafe` features or functions. +// Due to a suboptimal design aspect of OsStr/OsString on windows, we need to +// encode/decode to wide chars on windows operating system. +// This prevents borrowing from OsStr on windows. Anyway, if optimally used,# +// this conversion needs to be done only once in the beginning and at the end. + +use std::ffi::OsString; +#[cfg(not(target_os = "windows"))] +use std::os::unix::ffi::{OsStrExt, OsStringExt}; +#[cfg(target_os = "windows")] +use std::os::windows::prelude::*; +use std::{borrow::Cow, ffi::OsStr}; + +#[cfg(target_os = "windows")] +use u16 as NativeIntCharU; +#[cfg(not(target_os = "windows"))] +use u8 as NativeIntCharU; + +pub type NativeCharInt = NativeIntCharU; +pub type NativeIntStr = [NativeCharInt]; +pub type NativeIntString = Vec; + +pub struct NCvt; + +pub trait Convert { + fn convert(f: From) -> To; +} + +// ================ str/String ================= + +impl<'a> Convert<&'a str, Cow<'a, NativeIntStr>> for NCvt { + fn convert(f: &'a str) -> Cow<'a, NativeIntStr> { + #[cfg(target_os = "windows")] + { + Cow::Owned(f.encode_utf16().collect()) + } + + #[cfg(not(target_os = "windows"))] + { + Cow::Borrowed(f.as_bytes()) + } + } +} + +impl<'a> Convert<&'a String, Cow<'a, NativeIntStr>> for NCvt { + fn convert(f: &'a String) -> Cow<'a, NativeIntStr> { + #[cfg(target_os = "windows")] + { + Cow::Owned(f.encode_utf16().collect()) + } + + #[cfg(not(target_os = "windows"))] + { + Cow::Borrowed(f.as_bytes()) + } + } +} + +impl<'a> Convert> for NCvt { + fn convert(f: String) -> Cow<'a, NativeIntStr> { + #[cfg(target_os = "windows")] + { + Cow::Owned(f.encode_utf16().collect()) + } + + #[cfg(not(target_os = "windows"))] + { + Cow::Owned(f.into_bytes()) + } + } +} + +// ================ OsStr/OsString ================= + +impl<'a> Convert<&'a OsStr, Cow<'a, NativeIntStr>> for NCvt { + fn convert(f: &'a OsStr) -> Cow<'a, NativeIntStr> { + to_native_int_representation(f) + } +} + +impl<'a> Convert<&'a OsString, Cow<'a, NativeIntStr>> for NCvt { + fn convert(f: &'a OsString) -> Cow<'a, NativeIntStr> { + to_native_int_representation(f) + } +} + +impl<'a> Convert> for NCvt { + fn convert(f: OsString) -> Cow<'a, NativeIntStr> { + #[cfg(target_os = "windows")] + { + Cow::Owned(f.encode_wide().collect()) + } + + #[cfg(not(target_os = "windows"))] + { + Cow::Owned(f.into_vec()) + } + } +} + +// ================ Vec ================= + +impl<'a> Convert<&'a Vec<&'a str>, Vec>> for NCvt { + fn convert(f: &'a Vec<&'a str>) -> Vec> { + f.iter().map(|x| Self::convert(*x)).collect() + } +} + +impl<'a> Convert, Vec>> for NCvt { + fn convert(f: Vec<&'a str>) -> Vec> { + f.iter().map(|x| Self::convert(*x)).collect() + } +} + +impl<'a> Convert<&'a Vec, Vec>> for NCvt { + fn convert(f: &'a Vec) -> Vec> { + f.iter().map(Self::convert).collect() + } +} + +impl<'a> Convert, Vec>> for NCvt { + fn convert(f: Vec) -> Vec> { + f.into_iter().map(Self::convert).collect() + } +} + +pub fn to_native_int_representation(input: &OsStr) -> Cow<'_, NativeIntStr> { + #[cfg(target_os = "windows")] + { + Cow::Owned(input.encode_wide().collect()) + } + + #[cfg(not(target_os = "windows"))] + { + Cow::Borrowed(input.as_bytes()) + } +} + +#[allow(clippy::needless_pass_by_value)] // needed on windows +pub fn from_native_int_representation(input: Cow<'_, NativeIntStr>) -> Cow<'_, OsStr> { + #[cfg(target_os = "windows")] + { + Cow::Owned(OsString::from_wide(&input)) + } + + #[cfg(not(target_os = "windows"))] + { + match input { + Cow::Borrowed(borrow) => Cow::Borrowed(OsStr::from_bytes(borrow)), + Cow::Owned(own) => Cow::Owned(OsString::from_vec(own)), + } + } +} + +#[allow(clippy::needless_pass_by_value)] // needed on windows +pub fn from_native_int_representation_owned(input: NativeIntString) -> OsString { + #[cfg(target_os = "windows")] + { + OsString::from_wide(&input) + } + + #[cfg(not(target_os = "windows"))] + { + OsString::from_vec(input) + } +} + +pub fn get_single_native_int_value(c: &char) -> Option { + #[cfg(target_os = "windows")] + { + let mut buf = [0u16, 0]; + let s = c.encode_utf16(&mut buf); + if s.len() == 1 { + Some(buf[0]) + } else { + None + } + } + + #[cfg(not(target_os = "windows"))] + { + let mut buf = [0u8, 0, 0, 0]; + let s = c.encode_utf8(&mut buf); + if s.len() == 1 { + Some(buf[0]) + } else { + None + } + } +} + +pub fn get_char_from_native_int(ni: NativeCharInt) -> Option<(char, NativeCharInt)> { + let c_opt; + #[cfg(target_os = "windows")] + { + c_opt = char::decode_utf16([ni; 1]).next().unwrap().ok(); + }; + + #[cfg(not(target_os = "windows"))] + { + c_opt = std::str::from_utf8(&[ni; 1]) + .ok() + .map(|x| x.chars().next().unwrap()); + }; + + if let Some(c) = c_opt { + return Some((c, ni)); + } + + None +} + +pub struct NativeStr<'a> { + native: Cow<'a, NativeIntStr>, +} + +impl<'a> NativeStr<'a> { + pub fn new(str: &'a OsStr) -> Self { + Self { + native: to_native_int_representation(str), + } + } + + pub fn native(&self) -> Cow<'a, NativeIntStr> { + self.native.clone() + } + + pub fn into_native(self) -> Cow<'a, NativeIntStr> { + self.native + } + + pub fn contains(&self, x: &char) -> Option { + let n_c = get_single_native_int_value(x)?; + Some(self.native.contains(&n_c)) + } + + pub fn slice(&self, from: usize, to: usize) -> Cow<'a, OsStr> { + let result = self.match_cow(|b| Ok::<_, ()>(&b[from..to]), |o| Ok(o[from..to].to_vec())); + result.unwrap() + } + + pub fn split_once(&self, pred: &char) -> Option<(Cow<'a, OsStr>, Cow<'a, OsStr>)> { + let n_c = get_single_native_int_value(pred)?; + let p = self.native.iter().position(|&x| x == n_c)?; + let before = self.slice(0, p); + let after = self.slice(p + 1, self.native.len()); + Some((before, after)) + } + + pub fn split_at(&self, pos: usize) -> (Cow<'a, OsStr>, Cow<'a, OsStr>) { + let before = self.slice(0, pos); + let after = self.slice(pos, self.native.len()); + (before, after) + } + + pub fn strip_prefix(&self, prefix: &OsStr) -> Option> { + let n_prefix = to_native_int_representation(prefix); + let result = self.match_cow( + |b| b.strip_prefix(&*n_prefix).ok_or(()), + |o| o.strip_prefix(&*n_prefix).map(|x| x.to_vec()).ok_or(()), + ); + result.ok() + } + + pub fn strip_prefix_native(&self, prefix: &OsStr) -> Option> { + let n_prefix = to_native_int_representation(prefix); + let result = self.match_cow_native( + |b| b.strip_prefix(&*n_prefix).ok_or(()), + |o| o.strip_prefix(&*n_prefix).map(|x| x.to_vec()).ok_or(()), + ); + result.ok() + } + + fn match_cow( + &self, + f_borrow: FnBorrow, + f_owned: FnOwned, + ) -> Result, Err> + where + FnBorrow: FnOnce(&'a [NativeCharInt]) -> Result<&'a [NativeCharInt], Err>, + FnOwned: FnOnce(&Vec) -> Result, Err>, + { + match &self.native { + Cow::Borrowed(b) => { + let slice = f_borrow(b); + let os_str = slice.map(|x| from_native_int_representation(Cow::Borrowed(x))); + os_str + } + Cow::Owned(o) => { + let slice = f_owned(o); + let os_str = slice.map(from_native_int_representation_owned); + os_str.map(Cow::Owned) + } + } + } + + fn match_cow_native( + &self, + f_borrow: FnBorrow, + f_owned: FnOwned, + ) -> Result, Err> + where + FnBorrow: FnOnce(&'a [NativeCharInt]) -> Result<&'a [NativeCharInt], Err>, + FnOwned: FnOnce(&Vec) -> Result, Err>, + { + match &self.native { + Cow::Borrowed(b) => { + let slice = f_borrow(b); + slice.map(Cow::Borrowed) + } + Cow::Owned(o) => { + let slice = f_owned(o); + slice.map(Cow::Owned) + } + } + } +} diff --git a/src/uu/env/src/parse_error.rs b/src/uu/env/src/parse_error.rs new file mode 100644 index 00000000000..cbdba99ed90 --- /dev/null +++ b/src/uu/env/src/parse_error.rs @@ -0,0 +1,55 @@ +// 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. + +use std::fmt; + +use crate::string_parser; + +/// An error returned when string arg splitting fails. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum ParseError { + MissingClosingQuote { + pos: usize, + c: char, + }, + InvalidBackslashAtEndOfStringInMinusS { + pos: usize, + quoting: String, + }, + BackslashCNotAllowedInDoubleQuotes { + pos: usize, + }, + InvalidSequenceBackslashXInMinusS { + pos: usize, + c: char, + }, + ParsingOfVariableNameFailed { + pos: usize, + msg: String, + }, + InternalError { + pos: usize, + sub_err: string_parser::Error, + }, + ReachedEnd, + ContinueWithDelimiter, +} + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(format!("{:?}", self).as_str()) + } +} + +impl std::error::Error for ParseError {} + +impl From for ParseError { + fn from(value: string_parser::Error) -> Self { + Self::InternalError { + pos: value.peek_position, + sub_err: value, + } + } +} diff --git a/src/uu/env/src/split_iterator.rs b/src/uu/env/src/split_iterator.rs new file mode 100644 index 00000000000..0af7a78ad8a --- /dev/null +++ b/src/uu/env/src/split_iterator.rs @@ -0,0 +1,375 @@ +// 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. +// +// This file is based on work from Tomasz MiÄ…sko who published it as "shell_words" crate, +// licensed under the Apache License, Version 2.0 +// or the MIT license , at your option. +// +//! Process command line according to parsing rules of original GNU env. +//! Even though it looks quite like a POSIX syntax, the original +//! "shell_words" implementation had to be adapted significantly. +//! +//! Apart from the grammar differences, there is a new feature integrated: $VARIABLE expansion. +//! +//! [GNU env] +// spell-checker:ignore (words) Tomasz MiÄ…sko rntfv FFFD varname + +#![forbid(unsafe_code)] + +use std::borrow::Cow; + +use crate::native_int_str::from_native_int_representation; +use crate::native_int_str::NativeCharInt; +use crate::native_int_str::NativeIntStr; +use crate::native_int_str::NativeIntString; +use crate::parse_error::ParseError; +use crate::string_expander::StringExpander; +use crate::string_parser::StringParser; +use crate::variable_parser::VariableParser; + +const BACKSLASH: char = '\\'; +const DOUBLE_QUOTES: char = '\"'; +const SINGLE_QUOTES: char = '\''; +const NEW_LINE: char = '\n'; +const DOLLAR: char = '$'; + +const REPLACEMENTS: [(char, char); 9] = [ + ('r', '\r'), + ('n', '\n'), + ('t', '\t'), + ('f', '\x0C'), + ('v', '\x0B'), + ('_', ' '), + ('#', '#'), + ('$', '$'), + ('"', '"'), +]; + +const ASCII_WHITESPACE_CHARS: [char; 6] = [' ', '\t', '\r', '\n', '\x0B', '\x0C']; + +pub struct SplitIterator<'a> { + expander: StringExpander<'a>, + words: Vec>, +} + +impl<'a> SplitIterator<'a> { + pub fn new(s: &'a NativeIntStr) -> Self { + Self { + expander: StringExpander::new(s), + words: Vec::new(), + } + } + + fn skip_one(&mut self) -> Result<(), ParseError> { + self.expander + .get_parser_mut() + .consume_one_ascii_or_all_non_ascii()?; + Ok(()) + } + + fn take_one(&mut self) -> Result<(), ParseError> { + Ok(self.expander.take_one()?) + } + + fn get_current_char(&self) -> Option { + self.expander.peek().ok() + } + + fn push_char_to_word(&mut self, c: char) { + self.expander.put_one_char(c); + } + + fn push_word_to_words(&mut self) { + let word = self.expander.take_collected_output(); + self.words.push(word); + } + + fn get_parser(&self) -> &StringParser<'a> { + self.expander.get_parser() + } + + fn get_parser_mut(&mut self) -> &mut StringParser<'a> { + self.expander.get_parser_mut() + } + + fn substitute_variable<'x>(&'x mut self) -> Result<(), ParseError> { + let mut var_parse = VariableParser::<'a, '_> { + parser: self.get_parser_mut(), + }; + + let (name, default) = var_parse.parse_variable()?; + + let varname_os_str_cow = from_native_int_representation(Cow::Borrowed(name)); + let value = std::env::var_os(varname_os_str_cow); + match (&value, default) { + (None, None) => {} // do nothing, just replace it with "" + (Some(value), _) => { + self.expander.put_string(value); + } + (None, Some(default)) => { + self.expander.put_native_string(default); + } + }; + + Ok(()) + } + + fn check_and_replace_ascii_escape_code(&mut self, c: char) -> Result { + if let Some(replace) = REPLACEMENTS.iter().find(|&x| x.0 == c) { + self.skip_one()?; + self.push_char_to_word(replace.1); + return Ok(true); + } + + Ok(false) + } + + fn make_invalid_sequence_backslash_xin_minus_s(&self, c: char) -> ParseError { + ParseError::InvalidSequenceBackslashXInMinusS { + pos: self.expander.get_parser().get_peek_position(), + c, + } + } + + fn state_root(&mut self) -> Result<(), ParseError> { + loop { + match self.state_delimiter() { + Err(ParseError::ContinueWithDelimiter) => {} + Err(ParseError::ReachedEnd) => return Ok(()), + result => return result, + } + } + } + + fn state_delimiter(&mut self) -> Result<(), ParseError> { + loop { + match self.get_current_char() { + None => return Ok(()), + Some('#') => { + self.skip_one()?; + self.state_comment()?; + } + Some(BACKSLASH) => { + self.skip_one()?; + self.state_delimiter_backslash()?; + } + Some(c) if ASCII_WHITESPACE_CHARS.contains(&c) => { + self.skip_one()?; + } + Some(_) => { + // Don't consume char. Will be done in unquoted state. + self.state_unquoted()?; + } + } + } + } + + fn state_delimiter_backslash(&mut self) -> Result<(), ParseError> { + match self.get_current_char() { + None => Err(ParseError::InvalidBackslashAtEndOfStringInMinusS { + pos: self.get_parser().get_peek_position(), + quoting: "Delimiter".into(), + }), + Some('_') | Some(NEW_LINE) => { + self.skip_one()?; + Ok(()) + } + Some(DOLLAR) | Some(BACKSLASH) | Some('#') | Some(SINGLE_QUOTES) + | Some(DOUBLE_QUOTES) => { + self.take_one()?; + self.state_unquoted() + } + Some('c') => Err(ParseError::ReachedEnd), + Some(c) if self.check_and_replace_ascii_escape_code(c)? => self.state_unquoted(), + Some(c) => Err(self.make_invalid_sequence_backslash_xin_minus_s(c)), + } + } + + fn state_unquoted(&mut self) -> Result<(), ParseError> { + loop { + match self.get_current_char() { + None => { + self.push_word_to_words(); + return Err(ParseError::ReachedEnd); + } + Some(DOLLAR) => { + self.substitute_variable()?; + } + Some(SINGLE_QUOTES) => { + self.skip_one()?; + self.state_single_quoted()?; + } + Some(DOUBLE_QUOTES) => { + self.skip_one()?; + self.state_double_quoted()?; + } + Some(BACKSLASH) => { + self.skip_one()?; + self.state_unquoted_backslash()?; + } + Some(c) if ASCII_WHITESPACE_CHARS.contains(&c) => { + self.push_word_to_words(); + self.skip_one()?; + return Ok(()); + } + Some(_) => { + self.take_one()?; + } + } + } + } + + fn state_unquoted_backslash(&mut self) -> Result<(), ParseError> { + match self.get_current_char() { + None => Err(ParseError::InvalidBackslashAtEndOfStringInMinusS { + pos: self.get_parser().get_peek_position(), + quoting: "Unquoted".into(), + }), + Some(NEW_LINE) => { + self.skip_one()?; + Ok(()) + } + Some('_') => { + self.skip_one()?; + self.push_word_to_words(); + Err(ParseError::ContinueWithDelimiter) + } + Some('c') => { + self.push_word_to_words(); + Err(ParseError::ReachedEnd) + } + Some(DOLLAR) | Some(BACKSLASH) | Some(SINGLE_QUOTES) | Some(DOUBLE_QUOTES) => { + self.take_one()?; + Ok(()) + } + Some(c) if self.check_and_replace_ascii_escape_code(c)? => Ok(()), + Some(c) => Err(self.make_invalid_sequence_backslash_xin_minus_s(c)), + } + } + + fn state_single_quoted(&mut self) -> Result<(), ParseError> { + loop { + match self.get_current_char() { + None => { + return Err(ParseError::MissingClosingQuote { + pos: self.get_parser().get_peek_position(), + c: '\'', + }) + } + Some(SINGLE_QUOTES) => { + self.skip_one()?; + return Ok(()); + } + Some(BACKSLASH) => { + self.skip_one()?; + self.split_single_quoted_backslash()?; + } + Some(_) => { + self.take_one()?; + } + } + } + } + + fn split_single_quoted_backslash(&mut self) -> Result<(), ParseError> { + match self.get_current_char() { + None => Err(ParseError::MissingClosingQuote { + pos: self.get_parser().get_peek_position(), + c: '\'', + }), + Some(NEW_LINE) => { + self.skip_one()?; + Ok(()) + } + Some(SINGLE_QUOTES) | Some(BACKSLASH) => { + self.take_one()?; + Ok(()) + } + Some(c) if REPLACEMENTS.iter().any(|&x| x.0 == c) => { + // See GNU test-suite e11: In single quotes, \t remains as it is. + // Comparing with GNU behavior: \a is not accepted and issues an error. + // So apparently only known sequences are allowed, even though they are not expanded.... bug of GNU? + self.push_char_to_word(BACKSLASH); + self.take_one()?; + Ok(()) + } + Some(c) => Err(self.make_invalid_sequence_backslash_xin_minus_s(c)), + } + } + + fn state_double_quoted(&mut self) -> Result<(), ParseError> { + loop { + match self.get_current_char() { + None => { + return Err(ParseError::MissingClosingQuote { + pos: self.get_parser().get_peek_position(), + c: '"', + }) + } + Some(DOLLAR) => { + self.substitute_variable()?; + } + Some(DOUBLE_QUOTES) => { + self.skip_one()?; + return Ok(()); + } + Some(BACKSLASH) => { + self.skip_one()?; + self.state_double_quoted_backslash()?; + } + Some(_) => { + self.take_one()?; + } + } + } + } + + fn state_double_quoted_backslash(&mut self) -> Result<(), ParseError> { + match self.get_current_char() { + None => Err(ParseError::MissingClosingQuote { + pos: self.get_parser().get_peek_position(), + c: '"', + }), + Some(NEW_LINE) => { + self.skip_one()?; + Ok(()) + } + Some(DOUBLE_QUOTES) | Some(DOLLAR) | Some(BACKSLASH) => { + self.take_one()?; + Ok(()) + } + Some('c') => Err(ParseError::BackslashCNotAllowedInDoubleQuotes { + pos: self.get_parser().get_peek_position(), + }), + Some(c) if self.check_and_replace_ascii_escape_code(c)? => Ok(()), + Some(c) => Err(self.make_invalid_sequence_backslash_xin_minus_s(c)), + } + } + + fn state_comment(&mut self) -> Result<(), ParseError> { + loop { + match self.get_current_char() { + None => return Err(ParseError::ReachedEnd), + Some(NEW_LINE) => { + self.skip_one()?; + return Ok(()); + } + Some(_) => { + self.get_parser_mut().skip_until_char_or_end(NEW_LINE); + } + } + } + } + + pub fn split(mut self) -> Result, ParseError> { + self.state_root()?; + Ok(self.words) + } +} + +pub fn split(s: &NativeIntStr) -> Result, ParseError> { + let splitted_args = SplitIterator::new(s).split()?; + Ok(splitted_args) +} diff --git a/src/uu/env/src/string_expander.rs b/src/uu/env/src/string_expander.rs new file mode 100644 index 00000000000..06e4699269f --- /dev/null +++ b/src/uu/env/src/string_expander.rs @@ -0,0 +1,92 @@ +// 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. + +use std::{ + ffi::{OsStr, OsString}, + mem, + ops::Deref, +}; + +use crate::{ + native_int_str::{to_native_int_representation, NativeCharInt, NativeIntStr}, + string_parser::{Chunk, Error, StringParser}, +}; + +/// This class makes parsing and word collection more convenient. +/// +/// It manages an "output" buffer that is automatically filled. +/// It provides "skip_one" and "take_one" that focus on +/// working with ASCII separators. Thus they will skip or take +/// all consecutive non-ascii char sequences at once. +pub struct StringExpander<'a> { + parser: StringParser<'a>, + output: Vec, +} + +impl<'a> StringExpander<'a> { + pub fn new(input: &'a NativeIntStr) -> Self { + Self { + parser: StringParser::new(input), + output: Vec::default(), + } + } + + pub fn new_at(input: &'a NativeIntStr, pos: usize) -> Self { + Self { + parser: StringParser::new_at(input, pos), + output: Vec::default(), + } + } + + pub fn get_parser(&self) -> &StringParser<'a> { + &self.parser + } + + pub fn get_parser_mut(&mut self) -> &mut StringParser<'a> { + &mut self.parser + } + + pub fn peek(&self) -> Result { + self.parser.peek() + } + + pub fn skip_one(&mut self) -> Result<(), Error> { + self.get_parser_mut().consume_one_ascii_or_all_non_ascii()?; + Ok(()) + } + + pub fn get_peek_position(&self) -> usize { + self.get_parser().get_peek_position() + } + + pub fn take_one(&mut self) -> Result<(), Error> { + let chunks = self.parser.consume_one_ascii_or_all_non_ascii()?; + for chunk in chunks { + match chunk { + Chunk::InvalidEncoding(invalid) => self.output.extend(invalid), + Chunk::ValidSingleIntChar((_c, ni)) => self.output.push(ni), + } + } + Ok(()) + } + + pub fn put_one_char(&mut self, c: char) { + let os_str = OsString::from(c.to_string()); + self.put_string(os_str); + } + + pub fn put_string>(&mut self, os_str: S) { + let native = to_native_int_representation(os_str.as_ref()); + self.output.extend(native.deref()); + } + + pub fn put_native_string(&mut self, n_str: &NativeIntStr) { + self.output.extend(n_str); + } + + pub fn take_collected_output(&mut self) -> Vec { + mem::take(&mut self.output) + } +} diff --git a/src/uu/env/src/string_parser.rs b/src/uu/env/src/string_parser.rs new file mode 100644 index 00000000000..6f8d550883b --- /dev/null +++ b/src/uu/env/src/string_parser.rs @@ -0,0 +1,182 @@ +// 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. +// +// spell-checker:ignore (words) splitted FFFD +#![forbid(unsafe_code)] + +use std::{borrow::Cow, ffi::OsStr}; + +use crate::native_int_str::{ + from_native_int_representation, get_char_from_native_int, get_single_native_int_value, + NativeCharInt, NativeIntStr, +}; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Error { + pub peek_position: usize, + pub err_type: ErrorType, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum ErrorType { + EndOfInput, + InternalError, +} + +/// Provides a valid char or a invalid sequence of bytes. +/// +/// Invalid byte sequences can't be splitted in any meaningful way. +/// Thus, they need to be consumed as one piece. +pub enum Chunk<'a> { + InvalidEncoding(&'a NativeIntStr), + ValidSingleIntChar((char, NativeCharInt)), +} + +/// This class makes parsing a OsString char by char more convenient. +/// +/// It also allows to capturing of intermediate positions for later splitting. +pub struct StringParser<'a> { + input: &'a NativeIntStr, + pointer: usize, + remaining: &'a NativeIntStr, +} + +impl<'a> StringParser<'a> { + pub fn new(input: &'a NativeIntStr) -> Self { + let mut instance = Self { + input, + pointer: 0, + remaining: input, + }; + instance.set_pointer(0); + instance + } + + pub fn new_at(input: &'a NativeIntStr, pos: usize) -> Self { + let mut instance = Self::new(input); + instance.set_pointer(pos); + instance + } + + pub fn get_input(&self) -> &'a NativeIntStr { + self.input + } + + pub fn get_peek_position(&self) -> usize { + self.pointer + } + + pub fn peek(&self) -> Result { + self.peek_char_at_pointer(self.pointer) + } + + fn make_err(&self, err_type: ErrorType) -> Error { + Error { + peek_position: self.get_peek_position(), + err_type, + } + } + + pub fn peek_char_at_pointer(&self, at_pointer: usize) -> Result { + let split = self.input.split_at(at_pointer).1; + if split.is_empty() { + return Err(self.make_err(ErrorType::EndOfInput)); + } + if let Some((c, _ni)) = get_char_from_native_int(split[0]) { + Ok(c) + } else { + Ok('\u{FFFD}') + } + } + + fn get_chunk_with_length_at(&self, pointer: usize) -> Result<(Chunk<'a>, usize), Error> { + let (_before, after) = self.input.split_at(pointer); + if after.is_empty() { + return Err(self.make_err(ErrorType::EndOfInput)); + } + + if let Some(c_ni) = get_char_from_native_int(after[0]) { + Ok((Chunk::ValidSingleIntChar(c_ni), 1)) + } else { + let mut i = 1; + while i < after.len() { + if let Some(_c) = get_char_from_native_int(after[i]) { + break; + } + i += 1; + } + + let chunk = &after[0..i]; + Ok((Chunk::InvalidEncoding(chunk), chunk.len())) + } + } + + pub fn peek_chunk(&self) -> Option> { + return self + .get_chunk_with_length_at(self.pointer) + .ok() + .map(|(chunk, _)| chunk); + } + + pub fn consume_chunk(&mut self) -> Result, Error> { + let (chunk, len) = self.get_chunk_with_length_at(self.pointer)?; + self.set_pointer(self.pointer + len); + Ok(chunk) + } + + pub fn consume_one_ascii_or_all_non_ascii(&mut self) -> Result>, Error> { + let mut result = Vec::>::new(); + loop { + let data = self.consume_chunk()?; + let was_ascii = if let Chunk::ValidSingleIntChar((c, _ni)) = &data { + c.is_ascii() + } else { + false + }; + result.push(data); + if was_ascii { + return Ok(result); + } + + match self.peek_chunk() { + Some(Chunk::ValidSingleIntChar((c, _ni))) if c.is_ascii() => return Ok(result), + None => return Ok(result), + _ => {} + } + } + } + + pub fn skip_multiple(&mut self, skip_byte_count: usize) { + let end_ptr = self.pointer + skip_byte_count; + self.set_pointer(end_ptr); + } + + pub fn skip_until_char_or_end(&mut self, c: char) { + let native_rep = get_single_native_int_value(&c).unwrap(); + let pos = self.remaining.iter().position(|x| *x == native_rep); + + if let Some(pos) = pos { + self.set_pointer(self.pointer + pos); + } else { + self.set_pointer(self.input.len()); + } + } + + pub fn substring(&self, range: &std::ops::Range) -> &'a NativeIntStr { + let (_before1, after1) = self.input.split_at(range.start); + let (middle, _after2) = after1.split_at(range.end - range.start); + middle + } + + pub fn peek_remaining(&self) -> Cow<'a, OsStr> { + from_native_int_representation(Cow::Borrowed(self.remaining)) + } + + pub fn set_pointer(&mut self, new_pointer: usize) { + self.pointer = new_pointer; + let (_before, after) = self.input.split_at(self.pointer); + self.remaining = after; + } +} diff --git a/src/uu/env/src/variable_parser.rs b/src/uu/env/src/variable_parser.rs new file mode 100644 index 00000000000..ef80ff801a6 --- /dev/null +++ b/src/uu/env/src/variable_parser.rs @@ -0,0 +1,158 @@ +// 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. + +use std::ops::Range; + +use crate::{native_int_str::NativeIntStr, parse_error::ParseError, string_parser::StringParser}; + +pub struct VariableParser<'a, 'b> { + pub parser: &'b mut StringParser<'a>, +} + +impl<'a, 'b> VariableParser<'a, 'b> { + fn get_current_char(&self) -> Option { + self.parser.peek().ok() + } + + fn check_variable_name_start(&self) -> Result<(), ParseError> { + if let Some(c) = self.get_current_char() { + if c.is_ascii_digit() { + return Err(ParseError::ParsingOfVariableNameFailed { + pos: self.parser.get_peek_position(), + msg: format!("Unexpected character: '{}', expected variable name must not start with 0..9", c) }); + } + } + Ok(()) + } + + fn skip_one(&mut self) -> Result<(), ParseError> { + self.parser.consume_chunk()?; + Ok(()) + } + + fn parse_braced_variable_name( + &mut self, + ) -> Result<(&'a NativeIntStr, Option<&'a NativeIntStr>), ParseError> { + let pos_start = self.parser.get_peek_position(); + + self.check_variable_name_start()?; + + let (varname_end, default_end); + loop { + match self.get_current_char() { + None => { + return Err(ParseError::ParsingOfVariableNameFailed { + pos: self.parser.get_peek_position(), msg: "Missing closing brace".into() }) + }, + Some(c) if !c.is_ascii() || c.is_ascii_alphanumeric() || c == '_' => { + self.skip_one()?; + } + Some(':') => { + varname_end = self.parser.get_peek_position(); + loop { + match self.get_current_char() { + None => { + return Err(ParseError::ParsingOfVariableNameFailed { + pos: self.parser.get_peek_position(), + msg: "Missing closing brace after default value".into() }) + }, + Some('}') => { + default_end = Some(self.parser.get_peek_position()); + self.skip_one()?; + break + }, + Some(_) => { + self.skip_one()?; + }, + } + } + break; + }, + Some('}') => { + varname_end = self.parser.get_peek_position(); + default_end = None; + self.skip_one()?; + break; + }, + Some(c) => { + return Err(ParseError::ParsingOfVariableNameFailed { + pos: self.parser.get_peek_position(), + msg: format!("Unexpected character: '{}', expected a closing brace ('}}') or colon (':')", c) + }) + }, + }; + } + + let default_opt = if let Some(default_end) = default_end { + Some(self.parser.substring(&Range { + start: varname_end + 1, + end: default_end, + })) + } else { + None + }; + + let varname = self.parser.substring(&Range { + start: pos_start, + end: varname_end, + }); + + Ok((varname, default_opt)) + } + + fn parse_unbraced_variable_name(&mut self) -> Result<&'a NativeIntStr, ParseError> { + let pos_start = self.parser.get_peek_position(); + + self.check_variable_name_start()?; + + loop { + match self.get_current_char() { + None => break, + Some(c) if c.is_ascii_alphanumeric() || c == '_' => { + self.skip_one()?; + } + Some(_) => break, + }; + } + + let pos_end = self.parser.get_peek_position(); + + if pos_end == pos_start { + return Err(ParseError::ParsingOfVariableNameFailed { + pos: pos_start, + msg: "Missing variable name".into(), + }); + } + + let varname = self.parser.substring(&Range { + start: pos_start, + end: pos_end, + }); + + Ok(varname) + } + + pub fn parse_variable( + &mut self, + ) -> Result<(&'a NativeIntStr, Option<&'a NativeIntStr>), ParseError> { + self.skip_one()?; + + let (name, default) = match self.get_current_char() { + None => { + return Err(ParseError::ParsingOfVariableNameFailed { + pos: self.parser.get_peek_position(), + msg: "missing variable name".into(), + }) + } + Some('{') => { + self.skip_one()?; + self.parse_braced_variable_name()? + } + Some(_) => (self.parse_unbraced_variable_name()?, None), + }; + + Ok((name, default)) + } +} diff --git a/src/uu/expand/Cargo.toml b/src/uu/expand/Cargo.toml index 87f8f8f7716..703552476eb 100644 --- a/src/uu/expand/Cargo.toml +++ b/src/uu/expand/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_expand" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "expand ~ (uutils) convert input tabs to spaces" diff --git a/src/uu/expand/src/expand.rs b/src/uu/expand/src/expand.rs index 1efb36c654c..6df282de23a 100644 --- a/src/uu/expand/src/expand.rs +++ b/src/uu/expand/src/expand.rs @@ -7,6 +7,7 @@ use clap::{crate_version, Arg, ArgAction, ArgMatches, Command}; use std::error::Error; +use std::ffi::OsString; use std::fmt; use std::fs::File; use std::io::{stdin, stdout, BufRead, BufReader, BufWriter, Read, Write}; @@ -243,18 +244,20 @@ impl Options { /// Preprocess command line arguments and expand shortcuts. For example, "-7" is expanded to /// "--tabs=7" and "-1,3" to "--tabs=1 --tabs=3". -fn expand_shortcuts(args: &[String]) -> Vec { +fn expand_shortcuts(args: Vec) -> Vec { let mut processed_args = Vec::with_capacity(args.len()); for arg in args { - if arg.starts_with('-') && arg[1..].chars().all(is_digit_or_comma) { - arg[1..] - .split(',') - .filter(|s| !s.is_empty()) - .for_each(|s| processed_args.push(format!("--tabs={s}"))); - } else { - processed_args.push(arg.to_string()); + if let Some(arg) = arg.to_str() { + if arg.starts_with('-') && arg[1..].chars().all(is_digit_or_comma) { + arg[1..] + .split(',') + .filter(|s| !s.is_empty()) + .for_each(|s| processed_args.push(OsString::from(format!("--tabs={s}")))); + continue; + } } + processed_args.push(arg); } processed_args @@ -262,9 +265,7 @@ fn expand_shortcuts(args: &[String]) -> Vec { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let args = args.collect_ignore(); - - let matches = uu_app().try_get_matches_from(expand_shortcuts(&args))?; + let matches = uu_app().try_get_matches_from(expand_shortcuts(args.collect()))?; expand(&Options::new(&matches)?) } @@ -471,15 +472,21 @@ fn expand(options: &Options) -> UResult<()> { set_exit_code(1); continue; } - - let mut fh = open(file)?; - - while match fh.read_until(b'\n', &mut buf) { - Ok(s) => s > 0, - Err(_) => buf.is_empty(), - } { - expand_line(&mut buf, &mut output, ts, options) - .map_err_context(|| "failed to write output".to_string())?; + match open(file) { + Ok(mut fh) => { + while match fh.read_until(b'\n', &mut buf) { + Ok(s) => s > 0, + Err(_) => buf.is_empty(), + } { + expand_line(&mut buf, &mut output, ts, options) + .map_err_context(|| "failed to write output".to_string())?; + } + } + Err(e) => { + show_error!("{}", e); + set_exit_code(1); + continue; + } } } Ok(()) diff --git a/src/uu/expr/Cargo.toml b/src/uu/expr/Cargo.toml index 17d37413e26..efd6c166543 100644 --- a/src/uu/expr/Cargo.toml +++ b/src/uu/expr/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_expr" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "expr ~ (uutils) display the value of EXPRESSION" diff --git a/src/uu/factor/Cargo.toml b/src/uu/factor/Cargo.toml index a149056888a..ea42131f27d 100644 --- a/src/uu/factor/Cargo.toml +++ b/src/uu/factor/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_factor" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "factor ~ (uutils) display the prime factors of each NUMBER" diff --git a/src/uu/factor/build.rs b/src/uu/factor/build.rs index 8de0605a292..ac22a5566d5 100644 --- a/src/uu/factor/build.rs +++ b/src/uu/factor/build.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -//! Generate a table of the multiplicative inverses of p_i mod 2^64 +//! Generate a table of the multiplicative inverses of `p_i` mod 2^64 //! for the first 1027 odd primes (all 13 bit and smaller primes). //! You can supply a command line argument to override the default //! value of 1027 for the number of entries in the table. @@ -87,7 +87,7 @@ fn test_generator_10001() { } const MAX_WIDTH: usize = 102; -const PREAMBLE: &str = r##"/* +const PREAMBLE: &str = r"/* * This file is part of the uutils coreutils package. * * For the full copyright and license information, please view the LICENSE file @@ -100,4 +100,4 @@ const PREAMBLE: &str = r##"/* #[allow(clippy::unreadable_literal)] pub const PRIME_INVERSIONS_U64: &[(u64, u64, u64)] = &[ - "##; + "; diff --git a/src/uu/factor/src/factor.rs b/src/uu/factor/src/factor.rs index d7899a7e6ee..7c5097b5522 100644 --- a/src/uu/factor/src/factor.rs +++ b/src/uu/factor/src/factor.rs @@ -217,7 +217,7 @@ mod tests { // This is a strong pseudoprime (wrt. miller_rabin::BASIS) // and triggered a bug in rho::factor's code path handling // miller_rabbin::Result::Composite - let pseudoprime = 17179869183; + let pseudoprime = 17_179_869_183; for _ in 0..20 { // Repeat the test 20 times, as it only fails some fraction // of the time. diff --git a/src/uu/factor/src/miller_rabin.rs b/src/uu/factor/src/miller_rabin.rs index a06b20cd3b0..13a05806a9c 100644 --- a/src/uu/factor/src/miller_rabin.rs +++ b/src/uu/factor/src/miller_rabin.rs @@ -14,19 +14,17 @@ pub(crate) trait Basis { impl Basis for Montgomery { // Small set of bases for the Miller-Rabin prime test, valid for all 64b integers; // discovered by Jim Sinclair on 2011-04-20, see miller-rabin.appspot.com - #[allow(clippy::unreadable_literal)] - const BASIS: &'static [u64] = &[2, 325, 9375, 28178, 450775, 9780504, 1795265022]; + const BASIS: &'static [u64] = &[2, 325, 9375, 28178, 450_775, 9_780_504, 1_795_265_022]; } impl Basis for Montgomery { // spell-checker:ignore (names) Steve Worley // Small set of bases for the Miller-Rabin prime test, valid for all 32b integers; // discovered by Steve Worley on 2013-05-27, see miller-rabin.appspot.com - #[allow(clippy::unreadable_literal)] const BASIS: &'static [u64] = &[ - 4230279247111683200, - 14694767155120705706, - 16641139526367750375, + 4_230_279_247_111_683_200, + 14_694_767_155_120_705_706, + 16_641_139_526_367_750_375, ]; } @@ -112,7 +110,7 @@ mod tests { use crate::numeric::{traits::DoubleInt, Arithmetic, Montgomery}; use quickcheck::quickcheck; use std::iter; - const LARGEST_U64_PRIME: u64 = 0xFFFFFFFFFFFFFFC5; + const LARGEST_U64_PRIME: u64 = 0xFFFF_FFFF_FFFF_FFC5; fn primes() -> impl Iterator { iter::once(2).chain(odd_primes()) diff --git a/src/uu/factor/src/numeric/montgomery.rs b/src/uu/factor/src/numeric/montgomery.rs index 10c6dd2d906..135b22e39a0 100644 --- a/src/uu/factor/src/numeric/montgomery.rs +++ b/src/uu/factor/src/numeric/montgomery.rs @@ -31,14 +31,11 @@ pub(crate) trait Arithmetic: Copy + Sized { // Check that r (reduced back to the usual representation) equals // a^b % n, unless the latter computation overflows - // Temporarily commented-out, as there u64::checked_pow is not available - // on the minimum supported Rust version, nor is an appropriate method - // for compiling the check conditionally. - //debug_assert!(self - // .to_u64(_a) - // .checked_pow(_b as u32) - // .map(|r| r % self.modulus() == self.to_u64(result)) - // .unwrap_or(true)); + debug_assert!(self + .to_u64(_a) + .checked_pow(_b as u32) + .map(|r| r % self.modulus() == self.to_u64(result)) + .unwrap_or(true)); result } @@ -111,7 +108,7 @@ impl Arithmetic for Montgomery { } fn to_mod(&self, x: u64) -> Self::ModInt { - // TODO: optimise! + // TODO: optimize! debug_assert!(x < self.n.as_u64()); let r = T::from_double_width( ((T::DoubleWidth::from_u64(x)) << T::zero().count_zeros() as usize) diff --git a/src/uu/factor/src/rho.rs b/src/uu/factor/src/rho.rs index 2af0f685564..9375aa822f2 100644 --- a/src/uu/factor/src/rho.rs +++ b/src/uu/factor/src/rho.rs @@ -10,15 +10,14 @@ use std::cmp::{max, min}; use crate::numeric::*; -pub(crate) fn find_divisor(n: A) -> u64 { - #![allow(clippy::many_single_char_names)] +pub(crate) fn find_divisor(input: A) -> u64 { let mut rand = { - let range = Uniform::new(1, n.modulus()); + let range = Uniform::new(1, input.modulus()); let mut rng = SmallRng::from_rng(&mut thread_rng()).unwrap(); - move || n.to_mod(range.sample(&mut rng)) + move || input.to_mod(range.sample(&mut rng)) }; - let quadratic = |a, b| move |x| n.add(n.mul(a, n.mul(x, x)), b); + let quadratic = |a, b| move |x| input.add(input.mul(a, input.mul(x, x)), b); loop { let f = quadratic(rand(), rand()); @@ -29,11 +28,11 @@ pub(crate) fn find_divisor(n: A) -> u64 { x = f(x); y = f(f(y)); let d = { - let _x = n.to_u64(x); - let _y = n.to_u64(y); - gcd(n.modulus(), max(_x, _y) - min(_x, _y)) + let _x = input.to_u64(x); + let _y = input.to_u64(y); + gcd(input.modulus(), max(_x, _y) - min(_x, _y)) }; - if d == n.modulus() { + if d == input.modulus() { // Failure, retry with a different quadratic break; } else if d > 1 { diff --git a/src/uu/false/Cargo.toml b/src/uu/false/Cargo.toml index e88fa99783f..332dffe96cd 100644 --- a/src/uu/false/Cargo.toml +++ b/src/uu/false/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_false" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "false ~ (uutils) do nothing and fail" diff --git a/src/uu/fmt/Cargo.toml b/src/uu/fmt/Cargo.toml index de415310f2f..e628408cf28 100644 --- a/src/uu/fmt/Cargo.toml +++ b/src/uu/fmt/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_fmt" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "fmt ~ (uutils) reformat each paragraph of input" diff --git a/src/uu/fmt/src/fmt.rs b/src/uu/fmt/src/fmt.rs index 4380487814b..7e10c41e7eb 100644 --- a/src/uu/fmt/src/fmt.rs +++ b/src/uu/fmt/src/fmt.rs @@ -21,6 +21,10 @@ mod parasplit; const ABOUT: &str = help_about!("fmt.md"); const USAGE: &str = help_usage!("fmt.md"); const MAX_WIDTH: usize = 2500; +const DEFAULT_GOAL: usize = 70; +const DEFAULT_WIDTH: usize = 75; +// by default, goal is 93% of width +const DEFAULT_GOAL_TO_WIDTH_RATIO: usize = 93; mod options { pub const CROWN_MARGIN: &str = "crown-margin"; @@ -39,9 +43,6 @@ mod options { pub const FILES: &str = "files"; } -// by default, goal is 93% of width -const DEFAULT_GOAL_TO_WIDTH_RATIO: usize = 93; - pub type FileOrStdReader = BufReader>; pub struct FmtOptions { crown: bool, @@ -95,15 +96,20 @@ impl FmtOptions { (w, g) } (Some(&w), None) => { - let g = (w * DEFAULT_GOAL_TO_WIDTH_RATIO / 100).min(w - 3); + // Only allow a goal of zero if the width is set to be zero + let g = (w * DEFAULT_GOAL_TO_WIDTH_RATIO / 100).max(if w == 0 { 0 } else { 1 }); (w, g) } (None, Some(&g)) => { + if g > DEFAULT_WIDTH { + return Err(USimpleError::new(1, "GOAL cannot be greater than WIDTH.")); + } let w = (g * 100 / DEFAULT_GOAL_TO_WIDTH_RATIO).max(g + 3); (w, g) } - (None, None) => (75, 70), + (None, None) => (DEFAULT_WIDTH, DEFAULT_GOAL), }; + debug_assert!(width >= goal, "GOAL {goal} should not be greater than WIDTH {width} when given {width_opt:?} and {goal_opt:?}."); if width > MAX_WIDTH { return Err(USimpleError::new( @@ -331,7 +337,7 @@ pub fn uu_app() -> Command { Arg::new(options::GOAL) .short('g') .long("goal") - .help("Goal width, default of 93% of WIDTH. Must be less than WIDTH.") + .help("Goal width, default of 93% of WIDTH. Must be less than or equal to WIDTH.") .value_name("GOAL") .value_parser(clap::value_parser!(usize)), ) diff --git a/src/uu/fmt/src/linebreak.rs b/src/uu/fmt/src/linebreak.rs index 7393589d0b3..b8c592d3f42 100644 --- a/src/uu/fmt/src/linebreak.rs +++ b/src/uu/fmt/src/linebreak.rs @@ -6,7 +6,7 @@ // spell-checker:ignore (ToDO) INFTY MULT accum breakwords linebreak linebreaking linebreaks linelen maxlength minlength nchars ostream overlen parasplit plass posn powf punct signum slen sstart tabwidth tlen underlen winfo wlen wordlen use std::io::{BufWriter, Stdout, Write}; -use std::{cmp, i64, mem}; +use std::{cmp, mem}; use crate::parasplit::{ParaWords, Paragraph, WordInfo}; use crate::FmtOptions; @@ -238,8 +238,8 @@ fn find_kp_breakpoints<'a, T: Iterator>>( let mut active_breaks = vec![0]; let mut next_active_breaks = vec![]; - let stretch = (args.opts.width - args.opts.goal) as isize; - let minlength = args.opts.goal - stretch as usize; + let stretch = args.opts.width - args.opts.goal; + let minlength = args.opts.goal - stretch; let mut new_linebreaks = vec![]; let mut is_sentence_start = false; let mut least_demerits = 0; @@ -300,7 +300,7 @@ fn find_kp_breakpoints<'a, T: Iterator>>( compute_demerits( args.opts.goal as isize - tlen as isize, stretch, - w.word_nchars as isize, + w.word_nchars, active.prev_rat, ) }; @@ -393,7 +393,7 @@ const DR_MULT: f32 = 600.0; // DL_MULT is penalty multiplier for short words at end of line const DL_MULT: f32 = 300.0; -fn compute_demerits(delta_len: isize, stretch: isize, wlen: isize, prev_rat: f32) -> (i64, f32) { +fn compute_demerits(delta_len: isize, stretch: usize, wlen: usize, prev_rat: f32) -> (i64, f32) { // how much stretch are we using? let ratio = if delta_len == 0 { 0.0f32 @@ -419,7 +419,7 @@ fn compute_demerits(delta_len: isize, stretch: isize, wlen: isize, prev_rat: f32 }; // we penalize lines that have very different ratios from previous lines - let bad_delta_r = (DR_MULT * (((ratio - prev_rat) / 2.0).powi(3)).abs()) as i64; + let bad_delta_r = (DR_MULT * ((ratio - prev_rat) / 2.0).powi(3).abs()) as i64; let demerits = i64::pow(1 + bad_linelen + bad_wordlen + bad_delta_r, 2); @@ -440,8 +440,8 @@ fn restart_active_breaks<'a>( } else { // choose the lesser evil: breaking too early, or breaking too late let wlen = w.word_nchars + args.compute_width(w, active.length, active.fresh); - let underlen = (min - active.length) as isize; - let overlen = ((wlen + slen + active.length) - args.opts.width) as isize; + let underlen = min as isize - active.length as isize; + let overlen = (wlen + slen + active.length) as isize - args.opts.width as isize; if overlen > underlen { // break early, put this word on the next line (true, args.indent_len + w.word_nchars) diff --git a/src/uu/fold/Cargo.toml b/src/uu/fold/Cargo.toml index 02cf1115ec2..7a45d25b9c2 100644 --- a/src/uu/fold/Cargo.toml +++ b/src/uu/fold/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_fold" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "fold ~ (uutils) wrap each line of input" diff --git a/src/uu/groups/Cargo.toml b/src/uu/groups/Cargo.toml index dac52fc5864..8d0a9d310d2 100644 --- a/src/uu/groups/Cargo.toml +++ b/src/uu/groups/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_groups" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "groups ~ (uutils) display group memberships for USERNAME" diff --git a/src/uu/hashsum/Cargo.toml b/src/uu/hashsum/Cargo.toml index a20fe0d7826..668d8a1f385 100644 --- a/src/uu/hashsum/Cargo.toml +++ b/src/uu/hashsum/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_hashsum" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "hashsum ~ (uutils) display or check input digests" diff --git a/src/uu/hashsum/src/hashsum.rs b/src/uu/hashsum/src/hashsum.rs index 5e439853a58..8f57522d625 100644 --- a/src/uu/hashsum/src/hashsum.rs +++ b/src/uu/hashsum/src/hashsum.rs @@ -393,13 +393,15 @@ pub fn uu_app_common() -> Command { .short('c') .long("check") .help("read hashsums from the FILEs and check them") - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .conflicts_with("tag"), ) .arg( Arg::new("tag") .long("tag") .help("create a BSD-style checksum") - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .conflicts_with("text"), ) .arg( Arg::new("text") @@ -630,7 +632,8 @@ where } 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})", + // it can start with \ + r"^(|\\){algorithm} \((?P.*)\) = (?P[a-fA-F0-9]{digest_size})", algorithm = options.algoname, digest_size = bytes_marker, )) @@ -699,7 +702,8 @@ where } }, }; - let f = match File::open(ck_filename.clone()) { + let (ck_filename_unescaped, prefix) = unescape_filename(&ck_filename); + let f = match File::open(ck_filename_unescaped) { Err(_) => { failed_open_file += 1; println!( @@ -732,11 +736,11 @@ where // easier (and more important) on Unix than on Windows. if sum == real_sum { if !options.quiet { - println!("{ck_filename}: OK"); + println!("{prefix}{ck_filename}: OK"); } } else { if !options.status { - println!("{ck_filename}: FAILED"); + println!("{prefix}{ck_filename}: FAILED"); } failed_cksum += 1; } @@ -749,16 +753,19 @@ where options.output_bits, ) .map_err_context(|| "failed to read input".to_string())?; + let (escaped_filename, prefix) = escape_filename(filename); if options.tag { - println!("{} ({}) = {}", options.algoname, filename.display(), sum); + println!( + "{}{} ({}) = {}", + prefix, options.algoname, escaped_filename, sum + ); } else if options.nonames { println!("{sum}"); } else if options.zero { + // with zero, we don't escape the filename print!("{} {}{}\0", sum, binary_marker, filename.display()); } else { - let (filename, has_prefix) = escape_filename(filename); - let prefix = if has_prefix { "\\" } else { "" }; - println!("{}{} {}{}", prefix, sum, binary_marker, filename); + println!("{}{} {}{}", prefix, sum, binary_marker, escaped_filename); } } } @@ -783,14 +790,23 @@ where Ok(()) } -fn escape_filename(filename: &Path) -> (String, bool) { +fn unescape_filename(filename: &str) -> (String, &'static str) { + let unescaped = filename + .replace("\\\\", "\\") + .replace("\\n", "\n") + .replace("\\r", "\r"); + let prefix = if unescaped == filename { "" } else { "\\" }; + (unescaped, prefix) +} + +fn escape_filename(filename: &Path) -> (String, &'static str) { let original = filename.as_os_str().to_string_lossy(); let escaped = original .replace('\\', "\\\\") .replace('\n', "\\n") .replace('\r', "\\r"); - let has_changed = escaped != original; - (escaped, has_changed) + let prefix = if escaped == original { "" } else { "\\" }; + (escaped, prefix) } fn digest_reader( diff --git a/src/uu/head/Cargo.toml b/src/uu/head/Cargo.toml index 45e51b881b2..34b064ef6a9 100644 --- a/src/uu/head/Cargo.toml +++ b/src/uu/head/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_head" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "head ~ (uutils) display the first lines of input" diff --git a/src/uu/head/src/head.rs b/src/uu/head/src/head.rs index 3f6fd218507..dc5c0a258a1 100644 --- a/src/uu/head/src/head.rs +++ b/src/uu/head/src/head.rs @@ -7,10 +7,7 @@ use clap::{crate_version, Arg, ArgAction, ArgMatches, Command}; use std::ffi::OsString; -use std::fs::Metadata; use std::io::{self, BufWriter, ErrorKind, Read, Seek, SeekFrom, Write}; -#[cfg(not(target_os = "windows"))] -use std::os::unix::fs::MetadataExt; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError}; use uucore::line_ending::LineEnding; @@ -401,30 +398,10 @@ fn is_seekable(input: &mut std::fs::File) -> bool { && input.seek(SeekFrom::Start(current_pos.unwrap())).is_ok() } -fn sanity_limited_blksize(_st: &Metadata) -> u64 { - #[cfg(not(target_os = "windows"))] - { - const DEFAULT: u64 = 512; - const MAX: u64 = usize::MAX as u64 / 8 + 1; - - let st_blksize: u64 = _st.blksize(); - match st_blksize { - 0 => DEFAULT, - 1..=MAX => st_blksize, - _ => DEFAULT, - } - } - - #[cfg(target_os = "windows")] - { - 512 - } -} - fn head_backwards_file(input: &mut std::fs::File, options: &HeadOptions) -> std::io::Result<()> { let st = input.metadata()?; let seekable = is_seekable(input); - let blksize_limit = sanity_limited_blksize(&st); + let blksize_limit = uucore::fs::sane_blksize::sane_blksize_from_metadata(&st); if !seekable || st.len() <= blksize_limit { return head_backwards_without_seek_file(input, options); } diff --git a/src/uu/hostid/Cargo.toml b/src/uu/hostid/Cargo.toml index 8e551befb4a..7df3107d1d0 100644 --- a/src/uu/hostid/Cargo.toml +++ b/src/uu/hostid/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_hostid" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "hostid ~ (uutils) display the numeric identifier of the current host" diff --git a/src/uu/hostname/Cargo.toml b/src/uu/hostname/Cargo.toml index 7dd6eabe5ad..95343b5d3f9 100644 --- a/src/uu/hostname/Cargo.toml +++ b/src/uu/hostname/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_hostname" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "hostname ~ (uutils) display or set the host name of the current host" diff --git a/src/uu/id/Cargo.toml b/src/uu/id/Cargo.toml index 6d332acfc3c..b15e9909ee8 100644 --- a/src/uu/id/Cargo.toml +++ b/src/uu/id/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_id" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "id ~ (uutils) display user and group information for USER" diff --git a/src/uu/install/Cargo.toml b/src/uu/install/Cargo.toml index 647e0958ee0..e8754929970 100644 --- a/src/uu/install/Cargo.toml +++ b/src/uu/install/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_install" -version = "0.0.24" +version = "0.0.25" authors = ["Ben Eills ", "uutils developers"] license = "MIT" description = "install ~ (uutils) copy files from SOURCE to DESTINATION (with specified attributes)" diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index 9955be7b292..331a50f6741 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -748,6 +748,18 @@ fn perform_backup(to: &Path, b: &Behavior) -> UResult> { /// Returns an empty Result or an error in case of failure. /// fn copy_file(from: &Path, to: &Path) -> UResult<()> { + // fs::copy fails if destination is a invalid symlink. + // so lets just remove all existing files at destination before copy. + if let Err(e) = fs::remove_file(to) { + if e.kind() != std::io::ErrorKind::NotFound { + show_error!( + "Failed to remove existing file {}. Error: {:?}", + to.display(), + e + ); + } + } + if from.as_os_str() == "/dev/null" { /* workaround a limitation of fs::copy * https://github.com/rust-lang/rust/issues/79390 diff --git a/src/uu/join/Cargo.toml b/src/uu/join/Cargo.toml index 759b04af712..54dc547ce8f 100644 --- a/src/uu/join/Cargo.toml +++ b/src/uu/join/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_join" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "join ~ (uutils) merge lines from inputs with matching join fields" diff --git a/src/uu/join/src/join.rs b/src/uu/join/src/join.rs index 423af983ec9..3b0c8dfb958 100644 --- a/src/uu/join/src/join.rs +++ b/src/uu/join/src/join.rs @@ -9,7 +9,6 @@ use clap::builder::ValueParser; use clap::{crate_version, Arg, ArgAction, Command}; use memchr::{memchr3_iter, memchr_iter}; use std::cmp::Ordering; -use std::convert::From; use std::error::Error; use std::ffi::OsString; use std::fmt::Display; diff --git a/src/uu/kill/Cargo.toml b/src/uu/kill/Cargo.toml index e60faa1c455..3425bff55b4 100644 --- a/src/uu/kill/Cargo.toml +++ b/src/uu/kill/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_kill" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "kill ~ (uutils) send a signal to a process" diff --git a/src/uu/link/Cargo.toml b/src/uu/link/Cargo.toml index d3643c5dcf6..56025f96c10 100644 --- a/src/uu/link/Cargo.toml +++ b/src/uu/link/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_link" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "link ~ (uutils) create a hard (file system) link to FILE" diff --git a/src/uu/ln/Cargo.toml b/src/uu/ln/Cargo.toml index ba97c84e365..33e4588b81e 100644 --- a/src/uu/ln/Cargo.toml +++ b/src/uu/ln/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_ln" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "ln ~ (uutils) create a (file system) link to TARGET" diff --git a/src/uu/ln/ln.md b/src/uu/ln/ln.md index b2320d6c427..6bd6ee01619 100644 --- a/src/uu/ln/ln.md +++ b/src/uu/ln/ln.md @@ -7,7 +7,7 @@ ln [OPTION]... TARGET... DIRECTORY ln [OPTION]... -t DIRECTORY TARGET... ``` -Change file owner and group +Make links between files. ## After Help diff --git a/src/uu/logname/Cargo.toml b/src/uu/logname/Cargo.toml index d9870b1d99c..fcb5af8822d 100644 --- a/src/uu/logname/Cargo.toml +++ b/src/uu/logname/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_logname" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "logname ~ (uutils) display the login name of the current user" diff --git a/src/uu/ls/Cargo.toml b/src/uu/ls/Cargo.toml index 49c64ba0937..56f76849464 100644 --- a/src/uu/ls/Cargo.toml +++ b/src/uu/ls/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_ls" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "ls ~ (uutils) display directory contents" diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index ed100477f42..333360f50ec 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -2935,7 +2935,7 @@ fn display_group(metadata: &Metadata, config: &Config) -> String { } #[cfg(target_os = "redox")] -fn display_group(metadata: &Metadata, config: &Config) -> String { +fn display_group(metadata: &Metadata, _config: &Config) -> String { metadata.gid().to_string() } @@ -2981,7 +2981,8 @@ fn display_date(metadata: &Metadata, config: &Config) -> String { Some(time) => { //Date is recent if from past 6 months //According to GNU a Gregorian year has 365.2425 * 24 * 60 * 60 == 31556952 seconds on the average. - let recent = time + chrono::Duration::seconds(31_556_952 / 2) > chrono::Local::now(); + let recent = time + chrono::TimeDelta::try_seconds(31_556_952 / 2).unwrap() + > chrono::Local::now(); match &config.time_style { TimeStyle::FullIso => time.format("%Y-%m-%d %H:%M:%S.%f %z"), diff --git a/src/uu/mkdir/Cargo.toml b/src/uu/mkdir/Cargo.toml index a0ba24b42e8..f9ab373a286 100644 --- a/src/uu/mkdir/Cargo.toml +++ b/src/uu/mkdir/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_mkdir" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "mkdir ~ (uutils) create DIRECTORY" diff --git a/src/uu/mkfifo/Cargo.toml b/src/uu/mkfifo/Cargo.toml index 10ff3661686..3d6247a906e 100644 --- a/src/uu/mkfifo/Cargo.toml +++ b/src/uu/mkfifo/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_mkfifo" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "mkfifo ~ (uutils) create FIFOs (named pipes)" diff --git a/src/uu/mknod/Cargo.toml b/src/uu/mknod/Cargo.toml index 000fe4d01a5..7af4f74c369 100644 --- a/src/uu/mknod/Cargo.toml +++ b/src/uu/mknod/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_mknod" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "mknod ~ (uutils) create special file NAME of TYPE" diff --git a/src/uu/mktemp/Cargo.toml b/src/uu/mktemp/Cargo.toml index 504c46350d7..25703caf464 100644 --- a/src/uu/mktemp/Cargo.toml +++ b/src/uu/mktemp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_mktemp" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "mktemp ~ (uutils) create and display a temporary file or directory from TEMPLATE" diff --git a/src/uu/more/Cargo.toml b/src/uu/more/Cargo.toml index 58957a08c74..81dc0a289d2 100644 --- a/src/uu/more/Cargo.toml +++ b/src/uu/more/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_more" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "more ~ (uutils) input perusal filter" diff --git a/src/uu/more/src/more.rs b/src/uu/more/src/more.rs index f651a033b54..0b8c838f29d 100644 --- a/src/uu/more/src/more.rs +++ b/src/uu/more/src/more.rs @@ -3,11 +3,10 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (methods) isnt - use std::{ fs::File, - io::{stdin, stdout, BufReader, IsTerminal, Read, Stdout, Write}, + io::{stdin, stdout, BufReader, Read, Stdout, Write}, + panic::set_hook, path::Path, time::Duration, }; @@ -24,8 +23,8 @@ use crossterm::{ use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; -use uucore::display::Quotable; use uucore::error::{UResult, USimpleError, UUsageError}; +use uucore::{display::Quotable, show}; use uucore::{format_usage, help_about, help_usage}; const ABOUT: &str = help_about!("more.md"); @@ -53,6 +52,7 @@ struct Options { clean_print: bool, from_line: usize, lines: Option, + pattern: Option, print_over: bool, silent: bool, squeeze: bool, @@ -74,10 +74,14 @@ impl Options { Some(number) if number > 1 => number - 1, _ => 0, }; + let pattern = matches + .get_one::(options::PATTERN) + .map(|s| s.to_owned()); Self { clean_print: matches.get_flag(options::CLEAN_PRINT), from_line, lines, + pattern, print_over: matches.get_flag(options::PRINT_OVER), silent: matches.get_flag(options::SILENT), squeeze: matches.get_flag(options::SQUEEZE), @@ -87,6 +91,13 @@ impl Options { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { + // Disable raw mode before exiting if a panic occurs + set_hook(Box::new(|panic_info| { + terminal::disable_raw_mode().unwrap(); + print!("\r"); + println!("{panic_info}"); + })); + let matches = match uu_app().try_get_matches_from(args) { Ok(m) => m, Err(e) => return Err(e.into()), @@ -105,25 +116,31 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let file = Path::new(file); if file.is_dir() { terminal::disable_raw_mode().unwrap(); - return Err(UUsageError::new( - 1, + show!(UUsageError::new( + 0, format!("{} is a directory.", file.quote()), )); + terminal::enable_raw_mode().unwrap(); + continue; } if !file.exists() { terminal::disable_raw_mode().unwrap(); - return Err(USimpleError::new( - 1, + show!(USimpleError::new( + 0, format!("cannot open {}: No such file or directory", file.quote()), )); + terminal::enable_raw_mode().unwrap(); + continue; } let opened_file = match File::open(file) { Err(why) => { terminal::disable_raw_mode().unwrap(); - return Err(USimpleError::new( - 1, + show!(USimpleError::new( + 0, format!("cannot open {}: {}", file.quote(), why.kind()), )); + terminal::enable_raw_mode().unwrap(); + continue; } Ok(opened_file) => opened_file, }; @@ -140,13 +157,14 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { buff.clear(); } reset_term(&mut stdout); - } else if !std::io::stdin().is_terminal() { + } else { stdin().read_to_string(&mut buff).unwrap(); + if buff.is_empty() { + return Err(UUsageError::new(1, "bad usage")); + } let mut stdout = setup_term(); more(&buff, &mut stdout, false, None, None, &mut options)?; reset_term(&mut stdout); - } else { - return Err(UUsageError::new(1, "bad usage")); } Ok(()) } @@ -192,6 +210,15 @@ pub fn uu_app() -> Command { .action(ArgAction::SetTrue) .hide(true), ) + .arg( + Arg::new(options::PATTERN) + .short('P') + .long(options::PATTERN) + .allow_hyphen_values(true) + .required(false) + .value_name("pattern") + .help("Display file beginning from pattern match"), + ) .arg( Arg::new(options::FROM_LINE) .short('F') @@ -231,14 +258,6 @@ pub fn uu_app() -> Command { .long(options::NO_PAUSE) .help("Suppress pause after form feed"), ) - .arg( - Arg::new(options::PATTERN) - .short('P') - .allow_hyphen_values(true) - .required(false) - .takes_value(true) - .help("Display file beginning from pattern match"), - ) */ .arg( Arg::new(options::FILES) @@ -293,6 +312,17 @@ fn more( let mut pager = Pager::new(rows, lines, next_file, options); + if options.pattern.is_some() { + match search_pattern_in_file(&pager.lines, &options.pattern) { + Some(number) => pager.upper_mark = number, + None => { + execute!(stdout, terminal::Clear(terminal::ClearType::CurrentLine))?; + stdout.write_all("\rPattern not found\n".as_bytes())?; + pager.content_rows -= 1; + } + } + } + if multiple_file { execute!(stdout, terminal::Clear(terminal::ClearType::CurrentLine)).unwrap(); stdout.write_all( @@ -578,6 +608,19 @@ impl<'a> Pager<'a> { } } +fn search_pattern_in_file(lines: &[String], pattern: &Option) -> Option { + let pattern = pattern.clone().unwrap_or_default(); + if lines.is_empty() || pattern.is_empty() { + return None; + } + for (line_number, line) in lines.iter().enumerate() { + if line.contains(pattern.as_str()) { + return Some(line_number); + } + } + None +} + fn paging_add_back_message(options: &Options, stdout: &mut std::io::Stdout) -> UResult<()> { if options.lines.is_some() { execute!(stdout, MoveUp(1))?; @@ -626,7 +669,7 @@ fn break_line(line: &str, cols: usize) -> Vec { #[cfg(test)] mod tests { - use super::break_line; + use super::{break_line, search_pattern_in_file}; use unicode_width::UnicodeWidthStr; #[test] @@ -674,4 +717,53 @@ mod tests { // Each 👩ðŸ»â€ðŸ”¬ is 6 character width it break line to the closest number to 80 => 6 * 13 = 78 assert_eq!((78, 42), (widths[0], widths[1])); } + + #[test] + fn test_search_pattern_empty_lines() { + let lines = vec![]; + let pattern = Some(String::from("pattern")); + assert_eq!(None, search_pattern_in_file(&lines, &pattern)); + } + + #[test] + fn test_search_pattern_empty_pattern() { + let lines = vec![String::from("line1"), String::from("line2")]; + let pattern = None; + assert_eq!(None, search_pattern_in_file(&lines, &pattern)); + } + + #[test] + fn test_search_pattern_found_pattern() { + let lines = vec![ + String::from("line1"), + String::from("line2"), + String::from("pattern"), + ]; + let lines2 = vec![ + String::from("line1"), + String::from("line2"), + String::from("pattern"), + String::from("pattern2"), + ]; + let lines3 = vec![ + String::from("line1"), + String::from("line2"), + String::from("other_pattern"), + ]; + let pattern = Some(String::from("pattern")); + assert_eq!(2, search_pattern_in_file(&lines, &pattern).unwrap()); + assert_eq!(2, search_pattern_in_file(&lines2, &pattern).unwrap()); + assert_eq!(2, search_pattern_in_file(&lines3, &pattern).unwrap()); + } + + #[test] + fn test_search_pattern_not_found_pattern() { + let lines = vec![ + String::from("line1"), + String::from("line2"), + String::from("something"), + ]; + let pattern = Some(String::from("pattern")); + assert_eq!(None, search_pattern_in_file(&lines, &pattern)); + } } diff --git a/src/uu/mv/Cargo.toml b/src/uu/mv/Cargo.toml index 4193f9595cb..7ee7dcac07f 100644 --- a/src/uu/mv/Cargo.toml +++ b/src/uu/mv/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_mv" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "mv ~ (uutils) move (rename) SOURCE to DESTINATION" diff --git a/src/uu/nice/Cargo.toml b/src/uu/nice/Cargo.toml index 7285318ef63..b0ad0186420 100644 --- a/src/uu/nice/Cargo.toml +++ b/src/uu/nice/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_nice" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "nice ~ (uutils) run PROGRAM with modified scheduling priority" diff --git a/src/uu/nl/Cargo.toml b/src/uu/nl/Cargo.toml index 35b0f3d848b..8f0507a0284 100644 --- a/src/uu/nl/Cargo.toml +++ b/src/uu/nl/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_nl" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "nl ~ (uutils) display input with added line numbers" diff --git a/src/uu/nl/src/helper.rs b/src/uu/nl/src/helper.rs index e617010c147..ee3f3a0a95d 100644 --- a/src/uu/nl/src/helper.rs +++ b/src/uu/nl/src/helper.rs @@ -36,7 +36,7 @@ pub fn parse_options(settings: &mut crate::Settings, opts: &clap::ArgMatches) -> { None => {} Some(Ok(style)) => settings.header_numbering = style, - Some(Err(message)) => errs.push(message.to_string()), + Some(Err(message)) => errs.push(message), } match opts .get_one::(options::BODY_NUMBERING) @@ -45,7 +45,7 @@ pub fn parse_options(settings: &mut crate::Settings, opts: &clap::ArgMatches) -> { None => {} Some(Ok(style)) => settings.body_numbering = style, - Some(Err(message)) => errs.push(message.to_string()), + Some(Err(message)) => errs.push(message), } match opts .get_one::(options::FOOTER_NUMBERING) @@ -54,7 +54,7 @@ pub fn parse_options(settings: &mut crate::Settings, opts: &clap::ArgMatches) -> { None => {} Some(Ok(style)) => settings.footer_numbering = style, - Some(Err(message)) => errs.push(message.to_string()), + Some(Err(message)) => errs.push(message), } match opts.get_one::(options::NUMBER_WIDTH) { None => {} diff --git a/src/uu/nohup/Cargo.toml b/src/uu/nohup/Cargo.toml index 8e8bd50cb4d..9f4c8943589 100644 --- a/src/uu/nohup/Cargo.toml +++ b/src/uu/nohup/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_nohup" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "nohup ~ (uutils) run COMMAND, ignoring hangup signals" diff --git a/src/uu/nohup/src/nohup.rs b/src/uu/nohup/src/nohup.rs index 74dfa71c5f8..48651834233 100644 --- a/src/uu/nohup/src/nohup.rs +++ b/src/uu/nohup/src/nohup.rs @@ -148,7 +148,6 @@ fn find_stdout() -> UResult { }; match OpenOptions::new() - .write(true) .create(true) .append(true) .open(Path::new(NOHUP_OUT)) @@ -168,12 +167,7 @@ fn find_stdout() -> UResult { let mut homeout = PathBuf::from(home); homeout.push(NOHUP_OUT); let homeout_str = homeout.to_str().unwrap(); - match OpenOptions::new() - .write(true) - .create(true) - .append(true) - .open(&homeout) - { + match OpenOptions::new().create(true).append(true).open(&homeout) { Ok(t) => { show_error!( "ignoring input and appending output to {}", diff --git a/src/uu/nproc/Cargo.toml b/src/uu/nproc/Cargo.toml index 9e6efc559c1..76c58d566be 100644 --- a/src/uu/nproc/Cargo.toml +++ b/src/uu/nproc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_nproc" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "nproc ~ (uutils) display the number of processing units available" diff --git a/src/uu/numfmt/Cargo.toml b/src/uu/numfmt/Cargo.toml index 71505d4b7db..4fe4db0fdf3 100644 --- a/src/uu/numfmt/Cargo.toml +++ b/src/uu/numfmt/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_numfmt" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "numfmt ~ (uutils) reformat NUMBER" diff --git a/src/uu/numfmt/src/format.rs b/src/uu/numfmt/src/format.rs index 034d900e92d..5933092f62f 100644 --- a/src/uu/numfmt/src/format.rs +++ b/src/uu/numfmt/src/format.rs @@ -53,11 +53,7 @@ impl<'a> Iterator for WhitespaceSplitter<'a> { .unwrap_or(haystack.len()), ); - let (field, rest) = field.split_at( - field - .find(|c: char| c.is_whitespace()) - .unwrap_or(field.len()), - ); + let (field, rest) = field.split_at(field.find(char::is_whitespace).unwrap_or(field.len())); self.s = if rest.is_empty() { None } else { Some(rest) }; @@ -107,7 +103,7 @@ fn parse_implicit_precision(s: &str) -> usize { match s.split_once('.') { Some((_, decimal_part)) => decimal_part .chars() - .take_while(|c| c.is_ascii_digit()) + .take_while(char::is_ascii_digit) .count(), None => 0, } diff --git a/src/uu/numfmt/src/numfmt.rs b/src/uu/numfmt/src/numfmt.rs index d158072fbb4..80a2051bd44 100644 --- a/src/uu/numfmt/src/numfmt.rs +++ b/src/uu/numfmt/src/numfmt.rs @@ -229,29 +229,9 @@ fn parse_options(args: &ArgMatches) -> Result { }) } -// If the --format argument and its value are provided separately, they are concatenated to avoid a -// potential clap error. For example: "--format --%f--" is changed to "--format=--%f--". -fn concat_format_arg_and_value(args: &[String]) -> Vec { - let mut processed_args: Vec = Vec::with_capacity(args.len()); - let mut iter = args.iter().peekable(); - - while let Some(arg) = iter.next() { - if arg == "--format" && iter.peek().is_some() { - processed_args.push(format!("--format={}", iter.peek().unwrap())); - iter.next(); - } else { - processed_args.push(arg.to_string()); - } - } - - processed_args -} - #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let args = args.collect_ignore(); - - let matches = uu_app().try_get_matches_from(concat_format_arg_and_value(&args))?; + let matches = uu_app().try_get_matches_from(args)?; let options = parse_options(&matches).map_err(NumfmtError::IllegalArgument)?; @@ -300,7 +280,8 @@ pub fn uu_app() -> Command { Arg::new(options::FORMAT) .long(options::FORMAT) .help("use printf style floating-point FORMAT; see FORMAT below for details") - .value_name("FORMAT"), + .value_name("FORMAT") + .allow_hyphen_values(true), ) .arg( Arg::new(options::FROM) diff --git a/src/uu/od/Cargo.toml b/src/uu/od/Cargo.toml index e2c0a236b93..7a80de290ca 100644 --- a/src/uu/od/Cargo.toml +++ b/src/uu/od/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_od" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "od ~ (uutils) display formatted representation of input" diff --git a/src/uu/od/src/mockstream.rs b/src/uu/od/src/mockstream.rs index 925d52f7e58..9904fa9c1da 100644 --- a/src/uu/od/src/mockstream.rs +++ b/src/uu/od/src/mockstream.rs @@ -10,7 +10,7 @@ use std::io::{Cursor, Error, ErrorKind, Read, Result}; /// /// # Examples /// -/// ``` +/// ```no_run /// use std::io::{Cursor, Read}; /// /// struct CountIo {} diff --git a/src/uu/od/src/multifilereader.rs b/src/uu/od/src/multifilereader.rs index f7575e975de..813ef029f37 100644 --- a/src/uu/od/src/multifilereader.rs +++ b/src/uu/od/src/multifilereader.rs @@ -5,9 +5,7 @@ // spell-checker:ignore (ToDO) multifile curr fnames fname xfrd fillloop mockstream use std::fs::File; -use std::io; -use std::io::BufReader; -use std::vec::Vec; +use std::io::{self, BufReader}; use uucore::display::Quotable; use uucore::show_error; diff --git a/src/uu/paste/Cargo.toml b/src/uu/paste/Cargo.toml index 0ab1015b9df..afc001141ce 100644 --- a/src/uu/paste/Cargo.toml +++ b/src/uu/paste/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_paste" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "paste ~ (uutils) merge lines from inputs" diff --git a/src/uu/pathchk/Cargo.toml b/src/uu/pathchk/Cargo.toml index eeb667ac349..c24ad4b8f50 100644 --- a/src/uu/pathchk/Cargo.toml +++ b/src/uu/pathchk/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_pathchk" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "pathchk ~ (uutils) diagnose invalid or non-portable PATHNAME" diff --git a/src/uu/pinky/Cargo.toml b/src/uu/pinky/Cargo.toml index ba1dbc09688..e29155ffc74 100644 --- a/src/uu/pinky/Cargo.toml +++ b/src/uu/pinky/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_pinky" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "pinky ~ (uutils) display user information" diff --git a/src/uu/pr/Cargo.toml b/src/uu/pr/Cargo.toml index 4d09ad7afc8..629e07c2f32 100644 --- a/src/uu/pr/Cargo.toml +++ b/src/uu/pr/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_pr" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "pr ~ (uutils) convert text files for printing" diff --git a/src/uu/pr/src/pr.rs b/src/uu/pr/src/pr.rs index ef178a888cf..010183d3192 100644 --- a/src/uu/pr/src/pr.rs +++ b/src/uu/pr/src/pr.rs @@ -11,7 +11,6 @@ use clap::{crate_version, Arg, ArgAction, ArgMatches, Command}; use itertools::Itertools; use quick_error::ResultExt; use regex::Regex; -use std::convert::From; use std::fs::{metadata, File}; use std::io::{stdin, stdout, BufRead, BufReader, Lines, Read, Write}; #[cfg(unix)] @@ -577,18 +576,19 @@ fn build_options( // +page option is less priority than --pages let page_plus_re = Regex::new(r"\s*\+(\d+:*\d*)\s*").unwrap(); - let start_page_in_plus_option = match page_plus_re.captures(free_args).map(|i| { + let res = page_plus_re.captures(free_args).map(|i| { let unparsed_num = i.get(1).unwrap().as_str().trim(); let x: Vec<_> = unparsed_num.split(':').collect(); x[0].to_string().parse::().map_err(|_e| { PrError::EncounteredErrors(format!("invalid {} argument {}", "+", unparsed_num.quote())) }) - }) { + }); + let start_page_in_plus_option = match res { Some(res) => res?, None => 1, }; - let end_page_in_plus_option = match page_plus_re + let res = page_plus_re .captures(free_args) .map(|i| i.get(1).unwrap().as_str().trim()) .filter(|i| i.contains(':')) @@ -601,7 +601,8 @@ fn build_options( unparsed_num.quote() )) }) - }) { + }); + let end_page_in_plus_option = match res { Some(res) => Some(res?), None => None, }; @@ -616,27 +617,27 @@ fn build_options( }) }; - let start_page = match matches + let res = matches .get_one::(options::PAGES) .map(|i| { let x: Vec<_> = i.split(':').collect(); x[0].to_string() }) - .map(invalid_pages_map) - { + .map(invalid_pages_map); + let start_page = match res { Some(res) => res?, None => start_page_in_plus_option, }; - let end_page = match matches + let res = matches .get_one::(options::PAGES) .filter(|i| i.contains(':')) .map(|i| { let x: Vec<_> = i.split(':').collect(); x[1].to_string() }) - .map(invalid_pages_map) - { + .map(invalid_pages_map); + let end_page = match res { Some(res) => Some(res?), None => end_page_in_plus_option, }; @@ -707,12 +708,13 @@ fn build_options( let re_col = Regex::new(r"\s*-(\d+)\s*").unwrap(); - let start_column_option = match re_col.captures(free_args).map(|i| { + let res = re_col.captures(free_args).map(|i| { let unparsed_num = i.get(1).unwrap().as_str().trim(); unparsed_num.parse::().map_err(|_e| { PrError::EncounteredErrors(format!("invalid {} argument {}", "-", unparsed_num.quote())) }) - }) { + }); + let start_column_option = match res { Some(res) => Some(res?), None => None, }; diff --git a/src/uu/printenv/Cargo.toml b/src/uu/printenv/Cargo.toml index 32765a1f1e9..613eaa47325 100644 --- a/src/uu/printenv/Cargo.toml +++ b/src/uu/printenv/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_printenv" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "printenv ~ (uutils) display value of environment VAR" diff --git a/src/uu/printf/Cargo.toml b/src/uu/printf/Cargo.toml index 0033c31494b..7f98554db8f 100644 --- a/src/uu/printf/Cargo.toml +++ b/src/uu/printf/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_printf" -version = "0.0.24" +version = "0.0.25" authors = ["Nathan Ross", "uutils developers"] license = "MIT" description = "printf ~ (uutils) FORMAT and display ARGUMENTS" diff --git a/src/uu/ptx/Cargo.toml b/src/uu/ptx/Cargo.toml index 30bee8642b1..112f49ecf84 100644 --- a/src/uu/ptx/Cargo.toml +++ b/src/uu/ptx/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_ptx" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "ptx ~ (uutils) display a permuted index of input" diff --git a/src/uu/ptx/src/ptx.rs b/src/uu/ptx/src/ptx.rs index 7caa8f4a5d4..b952da92931 100644 --- a/src/uu/ptx/src/ptx.rs +++ b/src/uu/ptx/src/ptx.rs @@ -9,7 +9,6 @@ use clap::{crate_version, Arg, ArgAction, Command}; use regex::Regex; use std::cmp; use std::collections::{BTreeSet, HashMap, HashSet}; -use std::default::Default; use std::error::Error; use std::fmt::{Display, Formatter, Write as FmtWrite}; use std::fs::File; diff --git a/src/uu/pwd/Cargo.toml b/src/uu/pwd/Cargo.toml index 82cd4cf8ee1..7bfbb36678f 100644 --- a/src/uu/pwd/Cargo.toml +++ b/src/uu/pwd/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_pwd" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "pwd ~ (uutils) display current working directory" diff --git a/src/uu/readlink/Cargo.toml b/src/uu/readlink/Cargo.toml index cb17296129f..52f20a52213 100644 --- a/src/uu/readlink/Cargo.toml +++ b/src/uu/readlink/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_readlink" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "readlink ~ (uutils) display resolved path of PATHNAME" diff --git a/src/uu/realpath/Cargo.toml b/src/uu/realpath/Cargo.toml index 942f9601640..c4cc02bdd8c 100644 --- a/src/uu/realpath/Cargo.toml +++ b/src/uu/realpath/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_realpath" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "realpath ~ (uutils) display resolved absolute path of PATHNAME" diff --git a/src/uu/rm/Cargo.toml b/src/uu/rm/Cargo.toml index a36251838d5..1eafc3a30f9 100644 --- a/src/uu/rm/Cargo.toml +++ b/src/uu/rm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_rm" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "rm ~ (uutils) remove PATHNAME" diff --git a/src/uu/rmdir/Cargo.toml b/src/uu/rmdir/Cargo.toml index e567b98b69d..8c1807019ca 100644 --- a/src/uu/rmdir/Cargo.toml +++ b/src/uu/rmdir/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_rmdir" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "rmdir ~ (uutils) remove empty DIRECTORY" diff --git a/src/uu/runcon/Cargo.toml b/src/uu/runcon/Cargo.toml index e22f2e9d1ca..0b73e0b6c25 100644 --- a/src/uu/runcon/Cargo.toml +++ b/src/uu/runcon/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_runcon" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "runcon ~ (uutils) run command with specified security context" diff --git a/src/uu/seq/Cargo.toml b/src/uu/seq/Cargo.toml index dca08f5d064..3016c72c2da 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.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "seq ~ (uutils) display a sequence of numbers" diff --git a/src/uu/seq/seq.md b/src/uu/seq/seq.md index 8e67391e487..d747e4a0263 100644 --- a/src/uu/seq/seq.md +++ b/src/uu/seq/seq.md @@ -1,9 +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 ``` + +Display numbers from FIRST to LAST, in steps of INCREMENT. diff --git a/src/uu/shred/Cargo.toml b/src/uu/shred/Cargo.toml index 97fab64e1f6..24c5fe5eb1e 100644 --- a/src/uu/shred/Cargo.toml +++ b/src/uu/shred/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_shred" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "shred ~ (uutils) hide former FILE contents with repeated overwrites" diff --git a/src/uu/shred/src/shred.rs b/src/uu/shred/src/shred.rs index b142e2e94e0..d023b62107a 100644 --- a/src/uu/shred/src/shred.rs +++ b/src/uu/shred/src/shred.rs @@ -365,6 +365,7 @@ fn get_size(size_str_opt: Option) -> Option { .or_else(|| { if let Some(size) = size_str_opt { show_error!("invalid file size: {}", size.quote()); + // TODO: replace with our error management std::process::exit(1); } None @@ -406,9 +407,10 @@ fn wipe_file( )); } + let metadata = fs::metadata(path).map_err_context(String::new)?; + // If force is true, set file permissions to not-readonly. if force { - let metadata = fs::metadata(path).map_err_context(String::new)?; let mut perms = metadata.permissions(); #[cfg(unix)] #[allow(clippy::useless_conversion, clippy::unnecessary_cast)] @@ -428,40 +430,43 @@ fn wipe_file( // Fill up our pass sequence let mut pass_sequence = Vec::new(); + if metadata.len() != 0 { + // Only add passes if the file is non-empty - if n_passes <= 3 { - // Only random passes if n_passes <= 3 - for _ in 0..n_passes { - pass_sequence.push(PassType::Random); - } - } else { - // First fill it with Patterns, shuffle it, then evenly distribute Random - let n_full_arrays = n_passes / PATTERNS.len(); // How many times can we go through all the patterns? - let remainder = n_passes % PATTERNS.len(); // How many do we get through on our last time through? + if n_passes <= 3 { + // Only random passes if n_passes <= 3 + for _ in 0..n_passes { + pass_sequence.push(PassType::Random); + } + } else { + // First fill it with Patterns, shuffle it, then evenly distribute Random + let n_full_arrays = n_passes / PATTERNS.len(); // How many times can we go through all the patterns? + let remainder = n_passes % PATTERNS.len(); // How many do we get through on our last time through? + + for _ in 0..n_full_arrays { + for p in PATTERNS { + pass_sequence.push(PassType::Pattern(p)); + } + } + for pattern in PATTERNS.into_iter().take(remainder) { + pass_sequence.push(PassType::Pattern(pattern)); + } + let mut rng = rand::thread_rng(); + pass_sequence.shuffle(&mut rng); // randomize the order of application - for _ in 0..n_full_arrays { - for p in PATTERNS { - pass_sequence.push(PassType::Pattern(p)); + let n_random = 3 + n_passes / 10; // Minimum 3 random passes; ratio of 10 after + // Evenly space random passes; ensures one at the beginning and end + for i in 0..n_random { + pass_sequence[i * (n_passes - 1) / (n_random - 1)] = PassType::Random; } } - for pattern in PATTERNS.into_iter().take(remainder) { - pass_sequence.push(PassType::Pattern(pattern)); - } - let mut rng = rand::thread_rng(); - pass_sequence.shuffle(&mut rng); // randomize the order of application - let n_random = 3 + n_passes / 10; // Minimum 3 random passes; ratio of 10 after - // Evenly space random passes; ensures one at the beginning and end - for i in 0..n_random { - pass_sequence[i * (n_passes - 1) / (n_random - 1)] = PassType::Random; + // --zero specifies whether we want one final pass of 0x00 on our file + if zero { + pass_sequence.push(PassType::Pattern(PATTERNS[0])); } } - // --zero specifies whether we want one final pass of 0x00 on our file - if zero { - pass_sequence.push(PassType::Pattern(PATTERNS[0])); - } - let total_passes = pass_sequence.len(); let mut file = OpenOptions::new() .write(true) @@ -471,29 +476,19 @@ fn wipe_file( let size = match size { Some(size) => size, - None => get_file_size(path)?, + None => metadata.len(), }; for (i, pass_type) in pass_sequence.into_iter().enumerate() { if verbose { let pass_name = pass_name(&pass_type); - if total_passes < 10 { - show_error!( - "{}: pass {}/{} ({})...", - path.maybe_quote(), - i + 1, - total_passes, - pass_name - ); - } else { - show_error!( - "{}: pass {:2.0}/{:2.0} ({})...", - path.maybe_quote(), - i + 1, - total_passes, - pass_name - ); - } + show_error!( + "{}: pass {:2}/{} ({})...", + path.maybe_quote(), + i + 1, + total_passes, + pass_name + ); } // size is an optional argument for exactly how many bytes we want to shred // Ignore failed writes; just keep trying @@ -539,10 +534,6 @@ fn do_pass( Ok(()) } -fn get_file_size(path: &Path) -> Result { - Ok(fs::metadata(path)?.len()) -} - // Repeatedly renames the file with strings of decreasing length (most likely all 0s) // Return the path of the file after its last renaming or None if error fn wipe_name(orig_path: &Path, verbose: bool, remove_method: RemoveMethod) -> Option { @@ -589,7 +580,8 @@ fn wipe_name(orig_path: &Path, verbose: bool, remove_method: RemoveMethod) -> Op new_path.quote(), e ); - return None; + // TODO: replace with our error management + std::process::exit(1); } } } diff --git a/src/uu/shuf/BENCHMARKING.md b/src/uu/shuf/BENCHMARKING.md index 6fa9028afce..d16b1afb03b 100644 --- a/src/uu/shuf/BENCHMARKING.md +++ b/src/uu/shuf/BENCHMARKING.md @@ -3,7 +3,7 @@ # Benchmarking shuf `shuf` is a simple utility, but there are at least two important cases -benchmark: with and without repetition. +to benchmark: with and without repetition. When benchmarking changes, make sure to always build with the `--release` flag. You can compare with another branch by compiling on that branch and then @@ -28,11 +28,11 @@ a range of numbers to randomly sample from. An example of a command that works well for testing: ```shell -hyperfine --warmup 10 "target/release/shuf -i 0-10000000" +hyperfine --warmup 10 "target/release/shuf -i 0-10000000 > /dev/null" ``` To measure the time taken by shuffling an input file, the following command can -be used:: +be used: ```shell hyperfine --warmup 10 "target/release/shuf input.txt > /dev/null" @@ -49,5 +49,14 @@ should be benchmarked separately. In this case, we have to pass the `-n` flag or the command will run forever. An example of a hyperfine command is ```shell -hyperfine --warmup 10 "target/release/shuf -r -n 10000000 -i 0-1000" +hyperfine --warmup 10 "target/release/shuf -r -n 10000000 -i 0-1000 > /dev/null" +``` + +## With huge interval ranges + +When `shuf` runs with huge interval ranges, special care must be taken, so it +should be benchmarked separately also. An example of a hyperfine command is + +```shell +hyperfine --warmup 10 "target/release/shuf -n 100 -i 1000-2000000000 > /dev/null" ``` diff --git a/src/uu/shuf/Cargo.toml b/src/uu/shuf/Cargo.toml index c1d2de7c8bd..80696189a63 100644 --- a/src/uu/shuf/Cargo.toml +++ b/src/uu/shuf/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_shuf" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "shuf ~ (uutils) display random permutations of input lines" diff --git a/src/uu/shuf/src/shuf.rs b/src/uu/shuf/src/shuf.rs index de302435cb6..40028c2fb5e 100644 --- a/src/uu/shuf/src/shuf.rs +++ b/src/uu/shuf/src/shuf.rs @@ -3,16 +3,18 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (ToDO) cmdline evec seps rvec fdata +// spell-checker:ignore (ToDO) cmdline evec nonrepeating seps shufable rvec fdata use clap::{crate_version, Arg, ArgAction, Command}; use memchr::memchr_iter; use rand::prelude::SliceRandom; -use rand::RngCore; +use rand::{Rng, RngCore}; +use std::collections::HashSet; use std::fs::File; -use std::io::{stdin, stdout, BufReader, BufWriter, Read, Write}; +use std::io::{stdin, stdout, BufReader, BufWriter, Error, Read, Write}; +use std::ops::RangeInclusive; use uucore::display::Quotable; -use uucore::error::{FromIo, UResult, USimpleError}; +use uucore::error::{FromIo, UResult, USimpleError, UUsageError}; use uucore::{format_usage, help_about, help_usage}; mod rand_read_adapter; @@ -20,7 +22,7 @@ mod rand_read_adapter; enum Mode { Default(String), Echo(Vec), - InputRange((usize, usize)), + InputRange(RangeInclusive), } static USAGE: &str = help_usage!("shuf.md"); @@ -42,15 +44,21 @@ mod options { pub static RANDOM_SOURCE: &str = "random-source"; pub static REPEAT: &str = "repeat"; pub static ZERO_TERMINATED: &str = "zero-terminated"; - pub static FILE: &str = "file"; + pub static FILE_OR_ARGS: &str = "file-or-args"; } #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().try_get_matches_from(args)?; - let mode = if let Some(args) = matches.get_many::(options::ECHO) { - Mode::Echo(args.map(String::from).collect()) + let mode = if matches.get_flag(options::ECHO) { + Mode::Echo( + matches + .get_many::(options::FILE_OR_ARGS) + .unwrap_or_default() + .map(String::from) + .collect(), + ) } else if let Some(range) = matches.get_one::(options::INPUT_RANGE) { match parse_range(range) { Ok(m) => Mode::InputRange(m), @@ -59,13 +67,17 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } } } else { - Mode::Default( - matches - .get_one::(options::FILE) - .map(|s| s.as_str()) - .unwrap_or("-") - .to_string(), - ) + let mut operands = matches + .get_many::(options::FILE_OR_ARGS) + .unwrap_or_default(); + let file = operands.next().cloned().unwrap_or("-".into()); + if let Some(second_file) = operands.next() { + return Err(UUsageError::new( + 1, + format!("unexpected argument '{second_file}' found"), + )); + }; + Mode::Default(file) }; let options = Options { @@ -92,22 +104,30 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { }, }; + if options.head_count == 0 { + // Do not attempt to read the random source or the input file. + // However, we must touch the output file, if given: + if let Some(s) = options.output { + File::create(&s[..]) + .map_err_context(|| format!("failed to open {} for writing", s.quote()))?; + } + return Ok(()); + } + match mode { Mode::Echo(args) => { let mut evec = args.iter().map(String::as_bytes).collect::>(); find_seps(&mut evec, options.sep); - shuf_bytes(&mut evec, options)?; + shuf_exec(&mut evec, options)?; } - Mode::InputRange((b, e)) => { - let rvec = (b..e).map(|x| format!("{x}")).collect::>(); - let mut rvec = rvec.iter().map(String::as_bytes).collect::>(); - shuf_bytes(&mut rvec, options)?; + Mode::InputRange(mut range) => { + shuf_exec(&mut range, options)?; } Mode::Default(filename) => { let fdata = read_input_file(&filename)?; let mut fdata = vec![&fdata[..]]; find_seps(&mut fdata, options.sep); - shuf_bytes(&mut fdata, options)?; + shuf_exec(&mut fdata, options)?; } } @@ -120,15 +140,13 @@ pub fn uu_app() -> Command { .version(crate_version!()) .override_usage(format_usage(USAGE)) .infer_long_args(true) - .args_override_self(true) .arg( Arg::new(options::ECHO) .short('e') .long(options::ECHO) - .value_name("ARG") .help("treat each ARG as an input line") - .use_value_delimiter(false) - .num_args(0..) + .action(clap::ArgAction::SetTrue) + .overrides_with(options::ECHO) .conflicts_with(options::INPUT_RANGE), ) .arg( @@ -137,13 +155,14 @@ pub fn uu_app() -> Command { .long(options::INPUT_RANGE) .value_name("LO-HI") .help("treat each number LO through HI as an input line") - .conflicts_with(options::FILE), + .conflicts_with(options::FILE_OR_ARGS), ) .arg( Arg::new(options::HEAD_COUNT) .short('n') .long(options::HEAD_COUNT) .value_name("COUNT") + .action(clap::ArgAction::Append) .help("output at most COUNT lines"), ) .arg( @@ -166,16 +185,22 @@ pub fn uu_app() -> Command { .short('r') .long(options::REPEAT) .help("output lines can be repeated") - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .overrides_with(options::REPEAT), ) .arg( Arg::new(options::ZERO_TERMINATED) .short('z') .long(options::ZERO_TERMINATED) .help("line delimiter is NUL, not newline") - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .overrides_with(options::ZERO_TERMINATED), + ) + .arg( + Arg::new(options::FILE_OR_ARGS) + .action(clap::ArgAction::Append) + .value_hint(clap::ValueHint::FilePath), ) - .arg(Arg::new(options::FILE).value_hint(clap::ValueHint::FilePath)) } fn read_input_file(filename: &str) -> UResult> { @@ -195,6 +220,13 @@ fn read_input_file(filename: &str) -> UResult> { } fn find_seps(data: &mut Vec<&[u8]>, sep: u8) { + // Special case: If data is empty (and does not even contain a single 'sep' + // to indicate the presence of the empty element), then behave as if the input contained no elements at all. + if data.len() == 1 && data[0].is_empty() { + data.clear(); + return; + } + // need to use for loop so we don't borrow the vector as we modify it in place // basic idea: // * We don't care about the order of the result. This lets us slice the slices @@ -219,7 +251,173 @@ fn find_seps(data: &mut Vec<&[u8]>, sep: u8) { } } -fn shuf_bytes(input: &mut Vec<&[u8]>, opts: Options) -> UResult<()> { +trait Shufable { + type Item: Writable; + fn is_empty(&self) -> bool; + fn choose(&self, rng: &mut WrappedRng) -> Self::Item; + // This type shouldn't even be known. However, because we want to support + // Rust 1.70, it is not possible to return "impl Iterator". + // TODO: When the MSRV is raised, rewrite this to return "impl Iterator". + type PartialShuffleIterator<'b>: Iterator + where + Self: 'b; + fn partial_shuffle<'b>( + &'b mut self, + rng: &'b mut WrappedRng, + amount: usize, + ) -> Self::PartialShuffleIterator<'b>; +} + +impl<'a> Shufable for Vec<&'a [u8]> { + type Item = &'a [u8]; + fn is_empty(&self) -> bool { + (**self).is_empty() + } + fn choose(&self, rng: &mut WrappedRng) -> Self::Item { + // Note: "copied()" only copies the reference, not the entire [u8]. + // Returns None if the slice is empty. We checked this before, so + // this is safe. + (**self).choose(rng).unwrap() + } + type PartialShuffleIterator<'b> = std::iter::Copied> where Self: 'b; + fn partial_shuffle<'b>( + &'b mut self, + rng: &'b mut WrappedRng, + amount: usize, + ) -> Self::PartialShuffleIterator<'b> { + // Note: "copied()" only copies the reference, not the entire [u8]. + (**self).partial_shuffle(rng, amount).0.iter().copied() + } +} + +impl Shufable for RangeInclusive { + type Item = usize; + fn is_empty(&self) -> bool { + self.is_empty() + } + fn choose(&self, rng: &mut WrappedRng) -> usize { + rng.gen_range(self.clone()) + } + type PartialShuffleIterator<'b> = NonrepeatingIterator<'b> where Self: 'b; + fn partial_shuffle<'b>( + &'b mut self, + rng: &'b mut WrappedRng, + amount: usize, + ) -> Self::PartialShuffleIterator<'b> { + NonrepeatingIterator::new(self.clone(), rng, amount) + } +} + +enum NumberSet { + AlreadyListed(HashSet), + Remaining(Vec), +} + +struct NonrepeatingIterator<'a> { + range: RangeInclusive, + rng: &'a mut WrappedRng, + remaining_count: usize, + buf: NumberSet, +} + +impl<'a> NonrepeatingIterator<'a> { + fn new( + range: RangeInclusive, + rng: &'a mut WrappedRng, + amount: usize, + ) -> NonrepeatingIterator { + let capped_amount = if range.start() > range.end() { + 0 + } else if *range.start() == 0 && *range.end() == std::usize::MAX { + amount + } else { + amount.min(range.end() - range.start() + 1) + }; + NonrepeatingIterator { + range, + rng, + remaining_count: capped_amount, + buf: NumberSet::AlreadyListed(HashSet::default()), + } + } + + fn produce(&mut self) -> usize { + debug_assert!(self.range.start() <= self.range.end()); + match &mut self.buf { + NumberSet::AlreadyListed(already_listed) => { + let chosen = loop { + let guess = self.rng.gen_range(self.range.clone()); + let newly_inserted = already_listed.insert(guess); + if newly_inserted { + break guess; + } + }; + // Once a significant fraction of the interval has already been enumerated, + // the number of attempts to find a number that hasn't been chosen yet increases. + // Therefore, we need to switch at some point from "set of already returned values" to "list of remaining values". + let range_size = (self.range.end() - self.range.start()).saturating_add(1); + if number_set_should_list_remaining(already_listed.len(), range_size) { + let mut remaining = self + .range + .clone() + .filter(|n| !already_listed.contains(n)) + .collect::>(); + assert!(remaining.len() >= self.remaining_count); + remaining.partial_shuffle(&mut self.rng, self.remaining_count); + remaining.truncate(self.remaining_count); + self.buf = NumberSet::Remaining(remaining); + } + chosen + } + NumberSet::Remaining(remaining_numbers) => { + debug_assert!(!remaining_numbers.is_empty()); + // We only enter produce() when there is at least one actual element remaining, so popping must always return an element. + remaining_numbers.pop().unwrap() + } + } + } +} + +impl<'a> Iterator for NonrepeatingIterator<'a> { + type Item = usize; + + fn next(&mut self) -> Option { + if self.range.is_empty() || self.remaining_count == 0 { + return None; + } + self.remaining_count -= 1; + Some(self.produce()) + } +} + +// This could be a method, but it is much easier to test as a stand-alone function. +fn number_set_should_list_remaining(listed_count: usize, range_size: usize) -> bool { + // Arbitrarily determine the switchover point to be around 25%. This is because: + // - HashSet has a large space overhead for the hash table load factor. + // - This means that somewhere between 25-40%, the memory required for a "positive" HashSet and a "negative" Vec should be the same. + // - HashSet has a small but non-negligible overhead for each lookup, so we have a slight preference for Vec anyway. + // - At 25%, on average 1.33 attempts are needed to find a number that hasn't been taken yet. + // - Finally, "24%" is computationally the simplest: + listed_count >= range_size / 4 +} + +trait Writable { + fn write_all_to(&self, output: &mut impl Write) -> Result<(), Error>; +} + +impl<'a> Writable for &'a [u8] { + fn write_all_to(&self, output: &mut impl Write) -> Result<(), Error> { + output.write_all(self) + } +} + +impl Writable for usize { + fn write_all_to(&self, output: &mut impl Write) -> Result<(), Error> { + output.write_all(format!("{self}").as_bytes()) + } +} + +fn shuf_exec(input: &mut impl Shufable, opts: Options) -> UResult<()> { let mut output = BufWriter::new(match opts.output { None => Box::new(stdout()) as Box, Some(s) => { @@ -238,28 +436,23 @@ fn shuf_bytes(input: &mut Vec<&[u8]>, opts: Options) -> UResult<()> { None => WrappedRng::RngDefault(rand::thread_rng()), }; - if input.is_empty() { - return Ok(()); - } - if opts.repeat { + if input.is_empty() { + return Err(USimpleError::new(1, "no lines to repeat")); + } for _ in 0..opts.head_count { - // Returns None is the slice is empty. We checked this before, so - // this is safe. - let r = input.choose(&mut rng).unwrap(); + let r = input.choose(&mut rng); - output - .write_all(r) + r.write_all_to(&mut output) .map_err_context(|| "write failed".to_string())?; output .write_all(&[opts.sep]) .map_err_context(|| "write failed".to_string())?; } } else { - let (shuffled, _) = input.partial_shuffle(&mut rng, opts.head_count); + let shuffled = input.partial_shuffle(&mut rng, opts.head_count); for r in shuffled { - output - .write_all(r) + r.write_all_to(&mut output) .map_err_context(|| "write failed".to_string())?; output .write_all(&[opts.sep]) @@ -270,7 +463,7 @@ fn shuf_bytes(input: &mut Vec<&[u8]>, opts: Options) -> UResult<()> { Ok(()) } -fn parse_range(input_range: &str) -> Result<(usize, usize), String> { +fn parse_range(input_range: &str) -> Result, String> { if let Some((from, to)) = input_range.split_once('-') { let begin = from .parse::() @@ -278,7 +471,11 @@ fn parse_range(input_range: &str) -> Result<(usize, usize), String> { let end = to .parse::() .map_err(|_| format!("invalid input range: {}", to.quote()))?; - Ok((begin, end + 1)) + if begin <= end || begin == end + 1 { + Ok(begin..=end) + } else { + Err(format!("invalid input range: {}", input_range.quote())) + } } else { Err(format!("invalid input range: {}", input_range.quote())) } @@ -329,3 +526,88 @@ impl RngCore for WrappedRng { } } } + +#[cfg(test)] +// Since the computed value is a bool, it is more readable to write the expected value out: +#[allow(clippy::bool_assert_comparison)] +mod test_number_set_decision { + use super::number_set_should_list_remaining; + + #[test] + fn test_stay_positive_large_remaining_first() { + assert_eq!(false, number_set_should_list_remaining(0, std::usize::MAX)); + } + + #[test] + fn test_stay_positive_large_remaining_second() { + assert_eq!(false, number_set_should_list_remaining(1, std::usize::MAX)); + } + + #[test] + fn test_stay_positive_large_remaining_tenth() { + assert_eq!(false, number_set_should_list_remaining(9, std::usize::MAX)); + } + + #[test] + fn test_stay_positive_smallish_range_first() { + assert_eq!(false, number_set_should_list_remaining(0, 12345)); + } + + #[test] + fn test_stay_positive_smallish_range_second() { + assert_eq!(false, number_set_should_list_remaining(1, 12345)); + } + + #[test] + fn test_stay_positive_smallish_range_tenth() { + assert_eq!(false, number_set_should_list_remaining(9, 12345)); + } + + #[test] + fn test_stay_positive_small_range_not_too_early() { + assert_eq!(false, number_set_should_list_remaining(1, 10)); + } + + // Don't want to test close to the border, in case we decide to change the threshold. + // However, at 50% coverage, we absolutely should switch: + #[test] + fn test_switch_half() { + assert_eq!(true, number_set_should_list_remaining(1234, 2468)); + } + + // Ensure that the decision is monotonous: + #[test] + fn test_switch_late1() { + assert_eq!(true, number_set_should_list_remaining(12340, 12345)); + } + + #[test] + fn test_switch_late2() { + assert_eq!(true, number_set_should_list_remaining(12344, 12345)); + } + + // Ensure that we are overflow-free: + #[test] + fn test_no_crash_exceed_max_size1() { + assert_eq!( + false, + number_set_should_list_remaining(12345, std::usize::MAX) + ); + } + + #[test] + fn test_no_crash_exceed_max_size2() { + assert_eq!( + true, + number_set_should_list_remaining(std::usize::MAX - 1, std::usize::MAX) + ); + } + + #[test] + fn test_no_crash_exceed_max_size3() { + assert_eq!( + true, + number_set_should_list_remaining(std::usize::MAX, std::usize::MAX) + ); + } +} diff --git a/src/uu/sleep/Cargo.toml b/src/uu/sleep/Cargo.toml index 94789f31312..4b9214fafce 100644 --- a/src/uu/sleep/Cargo.toml +++ b/src/uu/sleep/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_sleep" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "sleep ~ (uutils) pause for DURATION" diff --git a/src/uu/sleep/src/sleep.rs b/src/uu/sleep/src/sleep.rs index b1d6bd89958..36e3adfee1e 100644 --- a/src/uu/sleep/src/sleep.rs +++ b/src/uu/sleep/src/sleep.rs @@ -12,7 +12,7 @@ use uucore::{ }; use clap::{crate_version, Arg, ArgAction, Command}; -use fundu::{self, DurationParser, ParseError, SaturatingInto}; +use fundu::{DurationParser, ParseError, SaturatingInto}; static ABOUT: &str = help_about!("sleep.md"); const USAGE: &str = help_usage!("sleep.md"); diff --git a/src/uu/sort/Cargo.toml b/src/uu/sort/Cargo.toml index 7f5938c7034..2454138d569 100644 --- a/src/uu/sort/Cargo.toml +++ b/src/uu/sort/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_sort" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "sort ~ (uutils) sort input lines" diff --git a/src/uu/sort/src/numeric_str_cmp.rs b/src/uu/sort/src/numeric_str_cmp.rs index c6af856c2eb..54950f2dbfe 100644 --- a/src/uu/sort/src/numeric_str_cmp.rs +++ b/src/uu/sort/src/numeric_str_cmp.rs @@ -221,8 +221,8 @@ pub fn numeric_str_cmp((a, a_info): (&str, &NumInfo), (b, b_info): (&str, &NumIn a_info.exponent.cmp(&b_info.exponent) } else { // walk the characters from the front until we find a difference - let mut a_chars = a.chars().filter(|c| c.is_ascii_digit()); - let mut b_chars = b.chars().filter(|c| c.is_ascii_digit()); + let mut a_chars = a.chars().filter(char::is_ascii_digit); + let mut b_chars = b.chars().filter(char::is_ascii_digit); loop { let a_next = a_chars.next(); let b_next = b_chars.next(); diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index 5fcfe2c82da..07420d9c1d4 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -251,6 +251,7 @@ impl Output { let file = if let Some(name) = name { // This is different from `File::create()` because we don't truncate the output yet. // This allows using the output file as an input file. + #[allow(clippy::suspicious_open_options)] let file = OpenOptions::new() .write(true) .create(true) @@ -985,7 +986,7 @@ impl FieldSelector { let mut range = match to { Some(Resolution::StartOfChar(mut to)) => { // We need to include the character at `to`. - to += line[to..].chars().next().map_or(1, |c| c.len_utf8()); + to += line[to..].chars().next().map_or(1, char::len_utf8); from..to } Some(Resolution::EndOfChar(to)) => from..to, diff --git a/src/uu/split/Cargo.toml b/src/uu/split/Cargo.toml index 5a7ded4bc8b..4a421ea2f7d 100644 --- a/src/uu/split/Cargo.toml +++ b/src/uu/split/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_split" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "split ~ (uutils) split input into output files" diff --git a/src/uu/split/src/split.rs b/src/uu/split/src/split.rs index a837bcb21ec..7712e510124 100644 --- a/src/uu/split/src/split.rs +++ b/src/uu/split/src/split.rs @@ -43,18 +43,6 @@ static OPT_VERBOSE: &str = "verbose"; static OPT_SEPARATOR: &str = "separator"; static OPT_ELIDE_EMPTY_FILES: &str = "elide-empty-files"; static OPT_IO_BLKSIZE: &str = "-io-blksize"; -// Cap ---io-blksize value -// For 64bit systems the max value is the same as in GNU -// and is equivalent of `i32::MAX >> 20 << 20` operation. -// On 32bit systems however, even though it fits within `u32` and `i32`, -// it causes rust-lang `library/alloc/src/raw_vec.rs` to panic with 'capacity overflow' error. -// Could be due to how `std::io::BufReader` handles internal buffers. -// So we use much smaller value for those -static OPT_IO_BLKSIZE_MAX: usize = if usize::BITS >= 64 { - 2_146_435_072 -} else { - 1_000_000_000 -}; static ARG_INPUT: &str = "input"; static ARG_PREFIX: &str = "prefix"; @@ -421,7 +409,7 @@ struct Settings { /// chunks. If this is `false`, then empty files will not be /// created. elide_empty_files: bool, - io_blksize: Option, + io_blksize: Option, } /// An error when parsing settings from command-line arguments. @@ -512,17 +500,10 @@ impl Settings { None => b'\n', }; - let io_blksize: Option = if let Some(s) = matches.get_one::(OPT_IO_BLKSIZE) { + let io_blksize: Option = if let Some(s) = matches.get_one::(OPT_IO_BLKSIZE) { match parse_size_u64(s) { - Ok(n) => { - let n: usize = n - .try_into() - .map_err(|_| SettingsError::InvalidIOBlockSize(s.to_string()))?; - if n > OPT_IO_BLKSIZE_MAX { - return Err(SettingsError::InvalidIOBlockSize(s.to_string())); - } - Some(n) - } + Ok(0) => return Err(SettingsError::InvalidIOBlockSize(s.to_string())), + Ok(n) if n <= uucore::fs::sane_blksize::MAX => Some(n), _ => return Err(SettingsError::InvalidIOBlockSize(s.to_string())), } } else { @@ -645,14 +626,18 @@ fn get_input_size( input: &String, reader: &mut R, buf: &mut Vec, - io_blksize: &Option, + io_blksize: &Option, ) -> std::io::Result where R: BufRead, { // Set read limit to io_blksize if specified - // Otherwise to OPT_IO_BLKSIZE_MAX - let read_limit = io_blksize.unwrap_or(OPT_IO_BLKSIZE_MAX) as u64; + let read_limit: u64 = if let Some(custom_blksize) = io_blksize { + *custom_blksize + } else { + // otherwise try to get it from filesystem, or use default + uucore::fs::sane_blksize::sane_blksize_from_path(Path::new(input)) + }; // Try to read into buffer up to a limit let num_bytes = reader @@ -918,8 +903,7 @@ impl<'a> Write for LineChunkWriter<'a> { // Write the line, starting from *after* the previous // separator character and ending *after* the current // separator character. - let num_bytes_written = - custom_write(&buf[prev..i + 1], &mut self.inner, self.settings)?; + let num_bytes_written = custom_write(&buf[prev..=i], &mut self.inner, self.settings)?; total_bytes_written += num_bytes_written; prev = i + 1; self.num_lines_remaining_in_current_chunk -= 1; @@ -1090,7 +1074,7 @@ impl<'a> Write for LineBytesChunkWriter<'a> { // example comment above.) Some(i) if i < self.num_bytes_remaining_in_current_chunk => { let num_bytes_written = - custom_write(&buf[..i + 1], &mut self.inner, self.settings)?; + custom_write(&buf[..=i], &mut self.inner, self.settings)?; self.num_bytes_remaining_in_current_chunk -= num_bytes_written; total_bytes_written += num_bytes_written; buf = &buf[num_bytes_written..]; @@ -1146,6 +1130,11 @@ struct OutFile { /// and [`n_chunks_by_line_round_robin`] functions. type OutFiles = Vec; trait ManageOutFiles { + fn instantiate_writer( + &mut self, + idx: usize, + settings: &Settings, + ) -> UResult<&mut BufWriter>>; /// Initialize a new set of output files /// Each OutFile is generated with filename, while the writer for it could be /// optional, to be instantiated later by the calling function as needed. @@ -1210,44 +1199,63 @@ impl ManageOutFiles for OutFiles { Ok(out_files) } - fn get_writer( + fn instantiate_writer( &mut self, idx: usize, settings: &Settings, ) -> UResult<&mut BufWriter>> { - if self[idx].maybe_writer.is_some() { - Ok(self[idx].maybe_writer.as_mut().unwrap()) - } else { - // Writer was not instantiated upfront or was temporarily closed due to system resources constraints. - // Instantiate it and record for future use. + let mut count = 0; + // Use-case for doing multiple tries of closing fds: + // E.g. split running in parallel to other processes (e.g. another split) doing similar stuff, + // sharing the same limits. In this scenario, after closing one fd, the other process + // might "steel" the freed fd and open a file on its side. Then it would be beneficial + // if split would be able to close another fd before cancellation. + 'loop1: loop { + let filename_to_open = self[idx].filename.as_str(); + let file_to_open_is_new = self[idx].is_new; let maybe_writer = - settings.instantiate_current_writer(self[idx].filename.as_str(), self[idx].is_new); + settings.instantiate_current_writer(filename_to_open, file_to_open_is_new); if let Ok(writer) = maybe_writer { self[idx].maybe_writer = Some(writer); - Ok(self[idx].maybe_writer.as_mut().unwrap()) - } else if settings.filter.is_some() { + return Ok(self[idx].maybe_writer.as_mut().unwrap()); + } + + if settings.filter.is_some() { // Propagate error if in `--filter` mode - Err(maybe_writer.err().unwrap().into()) - } else { - // Could have hit system limit for open files. - // Try to close one previously instantiated writer first - for (i, out_file) in self.iter_mut().enumerate() { - if i != idx && out_file.maybe_writer.is_some() { - out_file.maybe_writer.as_mut().unwrap().flush()?; - out_file.maybe_writer = None; - out_file.is_new = false; - break; - } + return Err(maybe_writer.err().unwrap().into()); + } + + // Could have hit system limit for open files. + // Try to close one previously instantiated writer first + for (i, out_file) in self.iter_mut().enumerate() { + if i != idx && out_file.maybe_writer.is_some() { + out_file.maybe_writer.as_mut().unwrap().flush()?; + out_file.maybe_writer = None; + out_file.is_new = false; + count += 1; + + // And then try to instantiate the writer again + continue 'loop1; } - // And then try to instantiate the writer again - // If this fails - give up and propagate the error - self[idx].maybe_writer = - Some(settings.instantiate_current_writer( - self[idx].filename.as_str(), - self[idx].is_new, - )?); - Ok(self[idx].maybe_writer.as_mut().unwrap()) } + + // If this fails - give up and propagate the error + uucore::show_error!("at file descriptor limit, but no file descriptor left to close. Closed {count} writers before."); + return Err(maybe_writer.err().unwrap().into()); + } + } + + fn get_writer( + &mut self, + idx: usize, + settings: &Settings, + ) -> UResult<&mut BufWriter>> { + if self[idx].maybe_writer.is_some() { + Ok(self[idx].maybe_writer.as_mut().unwrap()) + } else { + // Writer was not instantiated upfront or was temporarily closed due to system resources constraints. + // Instantiate it and record for future use. + self.instantiate_writer(idx, settings) } } } @@ -1627,16 +1635,12 @@ fn split(settings: &Settings) -> UResult<()> { let r_box = if settings.input == "-" { Box::new(stdin()) as Box } else { - let r = File::open(Path::new(&settings.input)).map_err_context(|| { - format!( - "cannot open {} for reading: No such file or directory", - settings.input.quote() - ) - })?; + let r = File::open(Path::new(&settings.input)) + .map_err_context(|| format!("cannot open {} for reading", settings.input.quote()))?; Box::new(r) as Box }; let mut reader = if let Some(c) = settings.io_blksize { - BufReader::with_capacity(c, r_box) + BufReader::with_capacity(c.try_into().unwrap(), r_box) } else { BufReader::new(r_box) }; diff --git a/src/uu/stat/Cargo.toml b/src/uu/stat/Cargo.toml index dd28fc5f382..181e82531da 100644 --- a/src/uu/stat/Cargo.toml +++ b/src/uu/stat/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_stat" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "stat ~ (uutils) display FILE status" @@ -17,6 +17,7 @@ path = "src/stat.rs" [dependencies] clap = { workspace = true } uucore = { workspace = true, features = ["entries", "libc", "fs", "fsext"] } +chrono = { workspace = true } [[bin]] name = "stat" diff --git a/src/uu/stat/src/stat.rs b/src/uu/stat/src/stat.rs index 7d1fd574c25..fe007397d18 100644 --- a/src/uu/stat/src/stat.rs +++ b/src/uu/stat/src/stat.rs @@ -2,22 +2,21 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +// spell-checker:ignore datetime use clap::builder::ValueParser; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError}; use uucore::fs::display_permissions; -use uucore::fsext::{ - pretty_filetype, pretty_fstype, pretty_time, read_fs_list, statfs, BirthTime, FsMeta, -}; +use uucore::fsext::{pretty_filetype, pretty_fstype, read_fs_list, statfs, BirthTime, FsMeta}; use uucore::libc::mode_t; use uucore::{ entries, format_usage, help_about, help_section, help_usage, show_error, show_warning, }; +use chrono::{DateTime, Local}; use clap::{crate_version, Arg, ArgAction, ArgMatches, Command}; use std::borrow::Cow; -use std::convert::AsRef; use std::ffi::{OsStr, OsString}; use std::fs; use std::os::unix::fs::{FileTypeExt, MetadataExt}; @@ -404,122 +403,137 @@ fn print_unsigned_hex( } impl Stater { - #[allow(clippy::cognitive_complexity)] - fn generate_tokens(format_str: &str, use_printf: bool) -> UResult> { - let mut tokens = Vec::new(); - let bound = format_str.len(); - let chars = format_str.chars().collect::>(); - let mut i = 0; - while i < bound { - match chars[i] { - '%' => { - let old = i; + fn handle_percent_case( + chars: &[char], + i: &mut usize, + bound: usize, + format_str: &str, + ) -> UResult { + let old = *i; + + *i += 1; + if *i >= bound { + return Ok(Token::Char('%')); + } + if chars[*i] == '%' { + *i += 1; + return Ok(Token::Char('%')); + } - i += 1; - if i >= bound { - tokens.push(Token::Char('%')); - continue; - } - if chars[i] == '%' { - tokens.push(Token::Char('%')); - i += 1; - continue; - } + let mut flag = Flags::default(); + + while *i < bound { + match chars[*i] { + '#' => flag.alter = true, + '0' => flag.zero = true, + '-' => flag.left = true, + ' ' => flag.space = true, + '+' => flag.sign = true, + '\'' => flag.group = true, + 'I' => unimplemented!(), + _ => break, + } + *i += 1; + } + check_bound(format_str, bound, old, *i)?; - let mut flag = Flags::default(); - - while i < bound { - match chars[i] { - '#' => flag.alter = true, - '0' => flag.zero = true, - '-' => flag.left = true, - ' ' => flag.space = true, - '+' => flag.sign = true, - '\'' => flag.group = true, - 'I' => unimplemented!(), - _ => break, - } - i += 1; - } - check_bound(format_str, bound, old, i)?; + let mut width = 0; + let mut precision = None; + let mut j = *i; - let mut width = 0; - let mut precision = None; - let mut j = i; + if let Some((field_width, offset)) = format_str[j..].scan_num::() { + width = field_width; + j += offset; + } + check_bound(format_str, bound, old, j)?; - if let Some((field_width, offset)) = format_str[j..].scan_num::() { - width = field_width; - j += offset; - } - check_bound(format_str, bound, old, j)?; - - if chars[j] == '.' { - j += 1; - check_bound(format_str, bound, old, j)?; - - match format_str[j..].scan_num::() { - Some((value, offset)) => { - if value >= 0 { - precision = Some(value as usize); - } - j += offset; - } - None => precision = Some(0), - } - check_bound(format_str, bound, old, j)?; + if chars[j] == '.' { + j += 1; + check_bound(format_str, bound, old, j)?; + + match format_str[j..].scan_num::() { + Some((value, offset)) => { + if value >= 0 { + precision = Some(value as usize); } + j += offset; + } + None => precision = Some(0), + } + check_bound(format_str, bound, old, j)?; + } + + *i = j; + Ok(Token::Directive { + width, + flag, + precision, + format: chars[*i], + }) + } - i = j; - tokens.push(Token::Directive { - width, - flag, - precision, - format: chars[i], - }); + fn handle_escape_sequences( + chars: &[char], + i: &mut usize, + bound: usize, + format_str: &str, + ) -> Token { + *i += 1; + if *i >= bound { + show_warning!("backslash at end of format"); + return Token::Char('\\'); + } + match chars[*i] { + 'x' if *i + 1 < bound => { + if let Some((c, offset)) = format_str[*i + 1..].scan_char(16) { + *i += offset; + Token::Char(c) + } else { + show_warning!("unrecognized escape '\\x'"); + Token::Char('x') } + } + '0'..='7' => { + let (c, offset) = format_str[*i..].scan_char(8).unwrap(); + *i += offset - 1; + Token::Char(c) + } + '"' => Token::Char('"'), + '\\' => Token::Char('\\'), + 'a' => Token::Char('\x07'), + 'b' => Token::Char('\x08'), + 'e' => Token::Char('\x1B'), + 'f' => Token::Char('\x0C'), + 'n' => Token::Char('\n'), + 'r' => Token::Char('\r'), + 't' => Token::Char('\t'), + 'v' => Token::Char('\x0B'), + c => { + show_warning!("unrecognized escape '\\{}'", c); + Token::Char(c) + } + } + } + + fn generate_tokens(format_str: &str, use_printf: bool) -> UResult> { + let mut tokens = Vec::new(); + let bound = format_str.len(); + let chars = format_str.chars().collect::>(); + let mut i = 0; + while i < bound { + match chars[i] { + '%' => tokens.push(Self::handle_percent_case( + &chars, &mut i, bound, format_str, + )?), '\\' => { if use_printf { - i += 1; - if i >= bound { - show_warning!("backslash at end of format"); - tokens.push(Token::Char('\\')); - continue; - } - match chars[i] { - 'x' if i + 1 < bound => { - if let Some((c, offset)) = format_str[i + 1..].scan_char(16) { - tokens.push(Token::Char(c)); - i += offset; - } else { - show_warning!("unrecognized escape '\\x'"); - tokens.push(Token::Char('x')); - } - } - '0'..='7' => { - let (c, offset) = format_str[i..].scan_char(8).unwrap(); - tokens.push(Token::Char(c)); - i += offset - 1; - } - '"' => tokens.push(Token::Char('"')), - '\\' => tokens.push(Token::Char('\\')), - 'a' => tokens.push(Token::Char('\x07')), - 'b' => tokens.push(Token::Char('\x08')), - 'e' => tokens.push(Token::Char('\x1B')), - 'f' => tokens.push(Token::Char('\x0C')), - 'n' => tokens.push(Token::Char('\n')), - 'r' => tokens.push(Token::Char('\r')), - 't' => tokens.push(Token::Char('\t')), - 'v' => tokens.push(Token::Char('\x0B')), - c => { - show_warning!("unrecognized escape '\\{}'", c); - tokens.push(Token::Char(c)); - } - } + tokens.push(Self::handle_escape_sequences( + &chars, &mut i, bound, format_str, + )); } else { tokens.push(Token::Char('\\')); } } - c => tokens.push(Token::Char(c)), } i += 1; @@ -531,10 +545,16 @@ impl Stater { } fn new(matches: &ArgMatches) -> UResult { - let files = matches + let files: Vec = matches .get_many::(options::FILES) .map(|v| v.map(OsString::from).collect()) .unwrap_or_default(); + if files.is_empty() { + return Err(Box::new(USimpleError { + code: 1, + message: "missing operand\nTry 'stat --help' for more information.".to_string(), + })); + } let format_str = if matches.contains_id(options::PRINTF) { matches .get_one::(options::PRINTF) @@ -788,10 +808,14 @@ impl Stater { } // time of file birth, human-readable; - if unknown - 'w' => OutputType::Str(meta.pretty_birth()), + 'w' => OutputType::Str( + meta.birth() + .map(|(sec, nsec)| pretty_time(sec as i64, nsec as i64)) + .unwrap_or(String::from("-")), + ), // time of file birth, seconds since Epoch; 0 if unknown - 'W' => OutputType::Unsigned(meta.birth()), + 'W' => OutputType::Unsigned(meta.birth().unwrap_or_default().0), // time of last access, human-readable 'x' => OutputType::Str(pretty_time( @@ -929,6 +953,16 @@ pub fn uu_app() -> Command { ) } +const PRETTY_DATETIME_FORMAT: &str = "%Y-%m-%d %H:%M:%S.%f %z"; + +fn pretty_time(sec: i64, nsec: i64) -> String { + // Return the date in UTC + let tm = chrono::DateTime::from_timestamp(sec, nsec as u32).unwrap_or_default(); + let tm: DateTime = tm.into(); + + tm.format(PRETTY_DATETIME_FORMAT).to_string() +} + #[cfg(test)] mod tests { use super::{group_num, Flags, ScanUtil, Stater, Token}; diff --git a/src/uu/stdbuf/Cargo.toml b/src/uu/stdbuf/Cargo.toml index fdee4f68433..218b9826288 100644 --- a/src/uu/stdbuf/Cargo.toml +++ b/src/uu/stdbuf/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_stdbuf" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "stdbuf ~ (uutils) run COMMAND with modified standard stream buffering" @@ -20,7 +20,7 @@ tempfile = { workspace = true } uucore = { workspace = true } [build-dependencies] -libstdbuf = { version = "0.0.24", package = "uu_stdbuf_libstdbuf", path = "src/libstdbuf" } +libstdbuf = { version = "0.0.25", 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 eab74dc089e..9eda03b1855 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.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "stdbuf/libstdbuf ~ (uutils); dynamic library required for stdbuf" diff --git a/src/uu/stty/Cargo.toml b/src/uu/stty/Cargo.toml index 51863220d5f..8dc47d008ca 100644 --- a/src/uu/stty/Cargo.toml +++ b/src/uu/stty/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_stty" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "stty ~ (uutils) print or change terminal characteristics" diff --git a/src/uu/stty/src/flags.rs b/src/uu/stty/src/flags.rs index 2c8e154e8b1..eac57151be9 100644 --- a/src/uu/stty/src/flags.rs +++ b/src/uu/stty/src/flags.rs @@ -5,7 +5,7 @@ // spell-checker:ignore parenb parodd cmspar hupcl cstopb cread clocal crtscts CSIZE // spell-checker:ignore ignbrk brkint ignpar parmrk inpck istrip inlcr igncr icrnl ixoff ixon iuclc ixany imaxbel iutf -// spell-checker:ignore opost olcuc ocrnl onlcr onocr onlret ofill ofdel nldly crdly tabdly bsdly vtdly ffdly +// spell-checker:ignore opost olcuc ocrnl onlcr onocr onlret ofdel nldly crdly tabdly bsdly vtdly ffdly // spell-checker:ignore isig icanon iexten echoe crterase echok echonl noflsh xcase tostop echoprt prterase echoctl ctlecho echoke crtkill flusho extproc // spell-checker:ignore lnext rprnt susp swtch vdiscard veof veol verase vintr vkill vlnext vquit vreprint vstart vstop vsusp vswtc vwerase werase // spell-checker:ignore sigquit sigtstp @@ -86,14 +86,6 @@ pub const OUTPUT_FLAGS: &[Flag] = &[ target_os = "linux", target_os = "macos" ))] - Flag::new("ofill", O::OFILL), - #[cfg(any( - target_os = "android", - target_os = "haiku", - target_os = "ios", - target_os = "linux", - target_os = "macos" - ))] Flag::new("ofdel", O::OFDEL), #[cfg(any( target_os = "android", diff --git a/src/uu/sum/Cargo.toml b/src/uu/sum/Cargo.toml index 56711b6fd29..e59072fb8b6 100644 --- a/src/uu/sum/Cargo.toml +++ b/src/uu/sum/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_sum" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "sum ~ (uutils) display checksum and block counts for input" diff --git a/src/uu/sync/Cargo.toml b/src/uu/sync/Cargo.toml index 0c93433928c..5859ba33bb6 100644 --- a/src/uu/sync/Cargo.toml +++ b/src/uu/sync/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_sync" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "sync ~ (uutils) synchronize cache writes to storage" diff --git a/src/uu/tac/Cargo.toml b/src/uu/tac/Cargo.toml index a89f179e388..646ecaa0fa3 100644 --- a/src/uu/tac/Cargo.toml +++ b/src/uu/tac/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "uu_tac" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "tac ~ (uutils) concatenate and display input lines in reverse order" diff --git a/src/uu/tail/Cargo.toml b/src/uu/tail/Cargo.toml index 826f7f83daf..d711fe37451 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.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "tail ~ (uutils) display the last lines of input" diff --git a/src/uu/tee/Cargo.toml b/src/uu/tee/Cargo.toml index b84e7cb314a..14011a763d4 100644 --- a/src/uu/tee/Cargo.toml +++ b/src/uu/tee/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_tee" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "tee ~ (uutils) display input and copy to FILE" diff --git a/src/uu/test/Cargo.toml b/src/uu/test/Cargo.toml index 35132cd27b2..d58ee83e2c0 100644 --- a/src/uu/test/Cargo.toml +++ b/src/uu/test/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_test" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "test ~ (uutils) evaluate comparison and file type expressions" diff --git a/src/uu/timeout/Cargo.toml b/src/uu/timeout/Cargo.toml index 3a9500a2e96..9f6a51486ef 100644 --- a/src/uu/timeout/Cargo.toml +++ b/src/uu/timeout/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_timeout" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "timeout ~ (uutils) run COMMAND with a DURATION time limit" diff --git a/src/uu/timeout/src/status.rs b/src/uu/timeout/src/status.rs index 10103ab9b29..7a94c7f9441 100644 --- a/src/uu/timeout/src/status.rs +++ b/src/uu/timeout/src/status.rs @@ -3,7 +3,6 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. //! Exit status codes produced by `timeout`. -use std::convert::From; use uucore::error::UError; /// Enumerates the exit statuses produced by `timeout`. diff --git a/src/uu/timeout/src/timeout.rs b/src/uu/timeout/src/timeout.rs index 958bc647e10..ccc97403d51 100644 --- a/src/uu/timeout/src/timeout.rs +++ b/src/uu/timeout/src/timeout.rs @@ -202,14 +202,15 @@ fn send_signal(process: &mut Child, signal: usize, foreground: bool) { // NOTE: GNU timeout doesn't check for errors of signal. // The subprocess might have exited just after the timeout. // Sending a signal now would return "No such process", but we should still try to kill the children. - _ = process.send_signal(signal); - if !foreground { - _ = process.send_signal_group(signal); - let kill_signal = signal_by_name_or_value("KILL").unwrap(); - let continued_signal = signal_by_name_or_value("CONT").unwrap(); - if signal != kill_signal && signal != continued_signal { - _ = process.send_signal(continued_signal); - _ = process.send_signal_group(continued_signal); + match foreground { + true => _ = process.send_signal(signal), + false => { + _ = process.send_signal_group(signal); + let kill_signal = signal_by_name_or_value("KILL").unwrap(); + let continued_signal = signal_by_name_or_value("CONT").unwrap(); + if signal != kill_signal && signal != continued_signal { + _ = process.send_signal_group(continued_signal); + } } } } @@ -342,8 +343,15 @@ fn timeout( send_signal(process, signal, foreground); match kill_after { None => { + let status = process.wait()?; if preserve_status { - Err(ExitStatus::SignalSent(signal).into()) + if let Some(ec) = status.code() { + Err(ec.into()) + } else if let Some(sc) = status.signal() { + Err(ExitStatus::SignalSent(sc.try_into().unwrap()).into()) + } else { + Err(ExitStatus::CommandTimedOut.into()) + } } else { Err(ExitStatus::CommandTimedOut.into()) } diff --git a/src/uu/touch/Cargo.toml b/src/uu/touch/Cargo.toml index 51cc4ac3f41..aa763e598f8 100644 --- a/src/uu/touch/Cargo.toml +++ b/src/uu/touch/Cargo.toml @@ -1,7 +1,7 @@ # spell-checker:ignore datetime [package] name = "uu_touch" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "touch ~ (uutils) change FILE timestamps" diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index ebdff8d2116..fe1783b214a 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -326,12 +326,12 @@ fn update_times( // 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 = 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()), - }; + let metadata = if follow { + fs::metadata(path).or_else(|_| fs::symlink_metadata(path)) + } else { + fs::symlink_metadata(path) + } + .map_err_context(|| format!("failed to get attributes of {}", path.quote()))?; Ok(( FileTime::from_last_access_time(&metadata), @@ -433,7 +433,7 @@ fn parse_timestamp(s: &str) -> UResult { // only care about the timestamp anyway. // Tested in gnu/tests/touch/60-seconds if local.second() == 59 && ts.ends_with(".60") { - local += Duration::seconds(1); + local += Duration::try_seconds(1).unwrap(); } // Due to daylight saving time switch, local time can jump from 1:59 AM to @@ -441,7 +441,7 @@ fn parse_timestamp(s: &str) -> UResult { // valid. If we are within this jump, chrono takes the offset from before // the jump. If we then jump forward an hour, we get the new corrected // offset. Jumping back will then now correctly take the jump into account. - let local2 = local + Duration::hours(1) - Duration::hours(1); + let local2 = local + Duration::try_hours(1).unwrap() - Duration::try_hours(1).unwrap(); if local.hour() != local2.hour() { return Err(USimpleError::new( 1, diff --git a/src/uu/tr/Cargo.toml b/src/uu/tr/Cargo.toml index ff1efbe921f..aa7fa9d8bfa 100644 --- a/src/uu/tr/Cargo.toml +++ b/src/uu/tr/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_tr" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "tr ~ (uutils) translate characters within input and display" diff --git a/src/uu/tr/src/operation.rs b/src/uu/tr/src/operation.rs index 5565de6a16d..77ace6b40d8 100644 --- a/src/uu/tr/src/operation.rs +++ b/src/uu/tr/src/operation.rs @@ -122,18 +122,17 @@ impl Sequence { } // Hide all the nasty sh*t in here - // TODO: Make the 2 set lazily generate the character mapping as necessary. pub fn solve_set_characters( set1_str: &[u8], set2_str: &[u8], truncate_set1_flag: bool, ) -> Result<(Vec, Vec), BadSequence> { let set1 = Self::from_str(set1_str)?; - let set2 = Self::from_str(set2_str)?; let is_char_star = |s: &&Self| -> bool { matches!(s, Self::CharStar(_)) }; let set1_star_count = set1.iter().filter(is_char_star).count(); if set1_star_count == 0 { + let set2 = Self::from_str(set2_str)?; let set2_star_count = set2.iter().filter(is_char_star).count(); if set2_star_count < 2 { let char_star = set2.iter().find_map(|s| match s { @@ -339,6 +338,32 @@ impl Sequence { pub trait SymbolTranslator { fn translate(&mut self, current: u8) -> Option; + + /// Takes two SymbolTranslators and creates a new SymbolTranslator over both in sequence. + /// + /// This behaves pretty much identical to [`Iterator::chain`]. + fn chain(self, other: T) -> ChainedSymbolTranslator + where + Self: Sized, + { + ChainedSymbolTranslator:: { + stage_a: self, + stage_b: other, + } + } +} + +pub struct ChainedSymbolTranslator { + stage_a: A, + stage_b: B, +} + +impl SymbolTranslator for ChainedSymbolTranslator { + fn translate(&mut self, current: u8) -> Option { + self.stage_a + .translate(current) + .and_then(|c| self.stage_b.translate(c)) + } } #[derive(Debug)] diff --git a/src/uu/tr/src/tr.rs b/src/uu/tr/src/tr.rs index 28b99a10f46..6f78f13db94 100644 --- a/src/uu/tr/src/tr.rs +++ b/src/uu/tr/src/tr.rs @@ -9,9 +9,10 @@ mod operation; mod unicode_table; 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 operation::{ + translate_input, Sequence, SqueezeOperation, SymbolTranslator, TranslateOperation, +}; +use std::io::{stdin, stdout, BufWriter}; use uucore::{format_usage, help_about, help_section, help_usage, show}; use crate::operation::DeleteOperation; @@ -57,10 +58,44 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { if !(delete_flag || squeeze_flag) && sets_len < 2 { return Err(UUsageError::new( 1, - format!("missing operand after {}", sets[0].quote()), + format!( + "missing operand after {}\nTwo strings must be given when translating.", + sets[0].quote() + ), )); } + if delete_flag & squeeze_flag && sets_len < 2 { + return Err(UUsageError::new( + 1, + format!( + "missing operand after {}\nTwo strings must be given when deleting and squeezing.", + sets[0].quote() + ), + )); + } + + if sets_len > 1 { + let start = "extra operand"; + if delete_flag && !squeeze_flag { + let op = sets[1].quote(); + let msg = if sets_len == 2 { + format!( + "{} {}\nOnly one string may be given when deleting without squeezing repeats.", + start, op, + ) + } else { + format!("{} {}", start, op,) + }; + return Err(UUsageError::new(1, msg)); + } + if sets_len > 2 { + let op = sets[2].quote(); + let msg = format!("{} {}", start, op); + return Err(UUsageError::new(1, msg)); + } + } + if let Some(first) = sets.first() { if first.ends_with('\\') { show!(USimpleError::new( @@ -83,19 +118,13 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { truncate_set1_flag, )?; + // '*_op' are the operations that need to be applied, in order. if delete_flag { if squeeze_flag { - let mut delete_buffer = vec![]; - { - let mut delete_writer = BufWriter::new(&mut delete_buffer); - let delete_op = DeleteOperation::new(set1, complement_flag); - translate_input(&mut locked_stdin, &mut delete_writer, delete_op); - } - { - let mut squeeze_reader = BufReader::new(delete_buffer.as_bytes()); - let op = SqueezeOperation::new(set2, complement_flag); - translate_input(&mut squeeze_reader, &mut buffered_stdout, op); - } + let delete_op = DeleteOperation::new(set1, complement_flag); + let squeeze_op = SqueezeOperation::new(set2, false); + let op = delete_op.chain(squeeze_op); + translate_input(&mut locked_stdin, &mut buffered_stdout, op); } else { let op = DeleteOperation::new(set1, complement_flag); translate_input(&mut locked_stdin, &mut buffered_stdout, op); @@ -105,17 +134,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let op = SqueezeOperation::new(set1, complement_flag); translate_input(&mut locked_stdin, &mut buffered_stdout, op); } else { - let mut translate_buffer = vec![]; - { - let mut writer = BufWriter::new(&mut translate_buffer); - let op = TranslateOperation::new(set1, set2.clone(), complement_flag)?; - translate_input(&mut locked_stdin, &mut writer, op); - } - { - let mut reader = BufReader::new(translate_buffer.as_bytes()); - let squeeze_op = SqueezeOperation::new(set2, false); - translate_input(&mut reader, &mut buffered_stdout, squeeze_op); - } + let translate_op = TranslateOperation::new(set1, set2.clone(), complement_flag)?; + let squeeze_op = SqueezeOperation::new(set2, false); + let op = translate_op.chain(squeeze_op); + translate_input(&mut locked_stdin, &mut buffered_stdout, op); } } else { let op = TranslateOperation::new(set1, set2, complement_flag)?; @@ -130,20 +152,23 @@ pub fn uu_app() -> Command { .about(ABOUT) .override_usage(format_usage(USAGE)) .infer_long_args(true) + .trailing_var_arg(true) .arg( Arg::new(options::COMPLEMENT) .visible_short_alias('C') .short('c') .long(options::COMPLEMENT) .help("use the complement of SET1") - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .overrides_with(options::COMPLEMENT), ) .arg( Arg::new(options::DELETE) .short('d') .long(options::DELETE) .help("delete characters in SET1, do not translate") - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .overrides_with(options::DELETE), ) .arg( Arg::new(options::SQUEEZE) @@ -154,14 +179,16 @@ pub fn uu_app() -> Command { listed in the last specified SET, with a single occurrence \ of that character", ) - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .overrides_with(options::SQUEEZE), ) .arg( Arg::new(options::TRUNCATE_SET1) .long(options::TRUNCATE_SET1) .short('t') .help("first truncate SET1 to length of SET2") - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .overrides_with(options::TRUNCATE_SET1), ) - .arg(Arg::new(options::SETS).num_args(1..=2)) + .arg(Arg::new(options::SETS).num_args(1..)) } diff --git a/src/uu/true/Cargo.toml b/src/uu/true/Cargo.toml index eae893d2176..6d4947aa5d7 100644 --- a/src/uu/true/Cargo.toml +++ b/src/uu/true/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_true" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "true ~ (uutils) do nothing and succeed" diff --git a/src/uu/truncate/Cargo.toml b/src/uu/truncate/Cargo.toml index 2ea443ce7f4..151af3eb36a 100644 --- a/src/uu/truncate/Cargo.toml +++ b/src/uu/truncate/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_truncate" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "truncate ~ (uutils) truncate (or extend) FILE to SIZE" diff --git a/src/uu/truncate/src/truncate.rs b/src/uu/truncate/src/truncate.rs index 9368ce9b170..7af25085f49 100644 --- a/src/uu/truncate/src/truncate.rs +++ b/src/uu/truncate/src/truncate.rs @@ -178,10 +178,26 @@ pub fn uu_app() -> Command { /// /// If the file could not be opened, or there was a problem setting the /// size of the file. -fn file_truncate(filename: &str, create: bool, size: u64) -> std::io::Result<()> { +fn file_truncate(filename: &str, create: bool, size: u64) -> UResult<()> { + #[cfg(unix)] + if let Ok(metadata) = std::fs::metadata(filename) { + if metadata.file_type().is_fifo() { + return Err(USimpleError::new( + 1, + format!( + "cannot open {} for writing: No such device or address", + filename.quote() + ), + )); + } + } let path = Path::new(filename); - let f = OpenOptions::new().write(true).create(create).open(path)?; - f.set_len(size) + match OpenOptions::new().write(true).create(create).open(path) { + Ok(file) => file.set_len(size), + Err(e) if e.kind() == ErrorKind::NotFound && !create => Ok(()), + Err(e) => Err(e), + } + .map_err_context(|| format!("cannot open {} for writing", filename.quote())) } /// Truncate files to a size relative to a given file. @@ -233,19 +249,7 @@ fn truncate_reference_and_size( let fsize = metadata.len(); let tsize = mode.to_size(fsize); for filename in filenames { - #[cfg(unix)] - if std::fs::metadata(filename)?.file_type().is_fifo() { - return Err(USimpleError::new( - 1, - format!( - "cannot open {} for writing: No such device or address", - filename.quote() - ), - )); - } - - file_truncate(filename, create, tsize) - .map_err_context(|| format!("cannot open {} for writing", filename.quote()))?; + file_truncate(filename, create, tsize)?; } Ok(()) } @@ -280,18 +284,7 @@ fn truncate_reference_file_only( })?; let tsize = metadata.len(); for filename in filenames { - #[cfg(unix)] - if std::fs::metadata(filename)?.file_type().is_fifo() { - return Err(USimpleError::new( - 1, - format!( - "cannot open {} for writing: No such device or address", - filename.quote() - ), - )); - } - file_truncate(filename, create, tsize) - .map_err_context(|| format!("cannot open {} for writing", filename.quote()))?; + file_truncate(filename, create, tsize)?; } Ok(()) } @@ -337,15 +330,8 @@ fn truncate_size_only(size_string: &str, filenames: &[String], create: bool) -> Err(_) => 0, }; let tsize = mode.to_size(fsize); - match file_truncate(filename, create, tsize) { - Ok(_) => continue, - Err(e) if e.kind() == ErrorKind::NotFound && !create => continue, - Err(e) => { - return Err( - e.map_err_context(|| format!("cannot open {} for writing", filename.quote())) - ) - } - } + // TODO: Fix duplicate call to stat + file_truncate(filename, create, tsize)?; } Ok(()) } diff --git a/src/uu/tsort/Cargo.toml b/src/uu/tsort/Cargo.toml index f0430b41b3b..398a6e163bf 100644 --- a/src/uu/tsort/Cargo.toml +++ b/src/uu/tsort/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_tsort" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "tsort ~ (uutils) topologically sort input (partially ordered) pairs" diff --git a/src/uu/tsort/src/tsort.rs b/src/uu/tsort/src/tsort.rs index 2bc9d317576..cd0b2030ae3 100644 --- a/src/uu/tsort/src/tsort.rs +++ b/src/uu/tsort/src/tsort.rs @@ -5,7 +5,7 @@ use clap::{crate_version, Arg, Command}; use std::collections::{BTreeMap, BTreeSet}; use std::fs::File; -use std::io::{stdin, BufRead, BufReader, Read}; +use std::io::{stdin, BufReader, Read}; use std::path::Path; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError}; @@ -43,31 +43,28 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { &mut file_buf as &mut dyn Read }); + let mut input_buffer = String::new(); + reader.read_to_string(&mut input_buffer)?; let mut g = Graph::new(); - loop { - let mut line = String::new(); - match reader.read_line(&mut line) { - Ok(_) => { - let tokens: Vec = line.split_whitespace().map(|s| s.to_owned()).collect(); - if tokens.is_empty() { - break; - } - for ab in tokens.chunks(2) { - match ab.len() { - 2 => g.add_edge(&ab[0], &ab[1]), - _ => { - return Err(USimpleError::new( - 1, - format!( - "{}: input contains an odd number of tokens", - input.maybe_quote() - ), - )) - } - } + + for line in input_buffer.lines() { + let tokens: Vec<_> = line.split_whitespace().collect(); + if tokens.is_empty() { + break; + } + for ab in tokens.chunks(2) { + match ab.len() { + 2 => g.add_edge(ab[0], ab[1]), + _ => { + return Err(USimpleError::new( + 1, + format!( + "{}: input contains an odd number of tokens", + input.maybe_quote() + ), + )) } } - _ => break, } } @@ -104,13 +101,13 @@ pub fn uu_app() -> Command { // We use String as a representation of node here // but using integer may improve performance. #[derive(Default)] -struct Graph { - in_edges: BTreeMap>, - out_edges: BTreeMap>, - result: Vec, +struct Graph<'input> { + in_edges: BTreeMap<&'input str, BTreeSet<&'input str>>, + out_edges: BTreeMap<&'input str, Vec<&'input str>>, + result: Vec<&'input str>, } -impl Graph { +impl<'input> Graph<'input> { fn new() -> Self { Self::default() } @@ -123,12 +120,12 @@ impl Graph { self.in_edges[to].contains(from) } - fn init_node(&mut self, n: &str) { - self.in_edges.insert(n.to_string(), BTreeSet::new()); - self.out_edges.insert(n.to_string(), vec![]); + fn init_node(&mut self, n: &'input str) { + self.in_edges.insert(n, BTreeSet::new()); + self.out_edges.insert(n, vec![]); } - fn add_edge(&mut self, from: &str, to: &str) { + fn add_edge(&mut self, from: &'input str, to: &'input str) { if !self.has_node(to) { self.init_node(to); } @@ -138,8 +135,8 @@ impl Graph { } if from != to && !self.has_edge(from, to) { - self.in_edges.get_mut(to).unwrap().insert(from.to_string()); - self.out_edges.get_mut(from).unwrap().push(to.to_string()); + self.in_edges.get_mut(to).unwrap().insert(from); + self.out_edges.get_mut(from).unwrap().push(to); } } @@ -149,14 +146,14 @@ impl Graph { let mut start_nodes = vec![]; for (n, edges) in &self.in_edges { if edges.is_empty() { - start_nodes.push(n.clone()); + start_nodes.push(*n); } } while !start_nodes.is_empty() { let n = start_nodes.remove(0); - self.result.push(n.clone()); + self.result.push(n); let n_out_edges = self.out_edges.get_mut(&n).unwrap(); #[allow(clippy::explicit_iter_loop)] @@ -166,7 +163,7 @@ impl Graph { // If m doesn't have other in-coming edges add it to start_nodes if m_in_edges.is_empty() { - start_nodes.push(m.clone()); + start_nodes.push(m); } } n_out_edges.clear(); diff --git a/src/uu/tty/Cargo.toml b/src/uu/tty/Cargo.toml index d6065d4679e..4cefb9267ae 100644 --- a/src/uu/tty/Cargo.toml +++ b/src/uu/tty/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_tty" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "tty ~ (uutils) display the name of the terminal connected to standard input" diff --git a/src/uu/tty/src/tty.rs b/src/uu/tty/src/tty.rs index efda4a7becc..b7d3aedcd22 100644 --- a/src/uu/tty/src/tty.rs +++ b/src/uu/tty/src/tty.rs @@ -9,7 +9,6 @@ use clap::{crate_version, Arg, ArgAction, Command}; use std::io::{IsTerminal, Write}; -use std::os::unix::io::AsRawFd; use uucore::error::{set_exit_code, UResult}; use uucore::{format_usage, help_about, help_usage}; @@ -37,8 +36,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let mut stdout = std::io::stdout(); - // Get the ttyname via nix - let name = nix::unistd::ttyname(std::io::stdin().as_raw_fd()); + let name = nix::unistd::ttyname(std::io::stdin()); let write_result = match name { Ok(name) => writeln!(stdout, "{}", name.display()), diff --git a/src/uu/uname/Cargo.toml b/src/uu/uname/Cargo.toml index f9883eef93c..53f1b916c99 100644 --- a/src/uu/uname/Cargo.toml +++ b/src/uu/uname/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_uname" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "uname ~ (uutils) display system information" diff --git a/src/uu/uname/src/uname.rs b/src/uu/uname/src/uname.rs index e6d5c3a0a32..4a7c3f460c6 100644 --- a/src/uu/uname/src/uname.rs +++ b/src/uu/uname/src/uname.rs @@ -27,80 +27,119 @@ pub mod options { pub static OS: &str = "operating-system"; } -#[uucore::main] -pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; - - 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); - let kernel_name = matches.get_flag(options::KERNEL_NAME); - let nodename = matches.get_flag(options::NODENAME); - let kernel_release = matches.get_flag(options::KERNEL_RELEASE); - let kernel_version = matches.get_flag(options::KERNEL_VERSION); - let machine = matches.get_flag(options::MACHINE); - let processor = matches.get_flag(options::PROCESSOR); - let hardware_platform = matches.get_flag(options::HARDWARE_PLATFORM); - let os = matches.get_flag(options::OS); - - let none = !(all - || kernel_name - || nodename - || kernel_release - || kernel_version - || machine - || os - || processor - || hardware_platform); - - if kernel_name || all || none { - output.push_str(&uname.sysname().to_string_lossy()); - output.push(' '); - } - - if nodename || all { - output.push_str(&uname.nodename().to_string_lossy()); - output.push(' '); - } - - if kernel_release || all { - output.push_str(&uname.release().to_string_lossy()); - output.push(' '); - } - - if kernel_version || all { - output.push_str(&uname.version().to_string_lossy()); - output.push(' '); - } - - if machine || all { - output.push_str(&uname.machine().to_string_lossy()); - output.push(' '); - } +pub struct UNameOutput { + pub kernel_name: Option, + pub nodename: Option, + pub kernel_release: Option, + pub kernel_version: Option, + pub machine: Option, + pub os: Option, + pub processor: Option, + pub hardware_platform: Option, +} - if os || all { - output.push_str(&uname.osname().to_string_lossy()); - output.push(' '); +impl UNameOutput { + fn display(&self) -> String { + let mut output = String::new(); + for name in [ + self.kernel_name.as_ref(), + self.nodename.as_ref(), + self.kernel_release.as_ref(), + self.kernel_version.as_ref(), + self.machine.as_ref(), + self.os.as_ref(), + self.processor.as_ref(), + self.hardware_platform.as_ref(), + ] + .into_iter() + .flatten() + { + output.push_str(name); + output.push(' '); + } + output } - // This option is unsupported on modern Linux systems - // See: https://lists.gnu.org/archive/html/bug-coreutils/2005-09/msg00063.html - if processor { - output.push_str("unknown"); - output.push(' '); + pub fn new(opts: &Options) -> UResult { + let uname = + PlatformInfo::new().map_err(|_e| USimpleError::new(1, "cannot get system name"))?; + let none = !(opts.all + || opts.kernel_name + || opts.nodename + || opts.kernel_release + || opts.kernel_version + || opts.machine + || opts.os + || opts.processor + || opts.hardware_platform); + + let kernel_name = (opts.kernel_name || opts.all || none) + .then(|| uname.sysname().to_string_lossy().to_string()); + + let nodename = + (opts.nodename || opts.all).then(|| uname.nodename().to_string_lossy().to_string()); + + let kernel_release = (opts.kernel_release || opts.all) + .then(|| uname.release().to_string_lossy().to_string()); + + let kernel_version = (opts.kernel_version || opts.all) + .then(|| uname.version().to_string_lossy().to_string()); + + let machine = + (opts.machine || opts.all).then(|| uname.machine().to_string_lossy().to_string()); + + let os = (opts.os || opts.all).then(|| uname.osname().to_string_lossy().to_string()); + + // This option is unsupported on modern Linux systems + // See: https://lists.gnu.org/archive/html/bug-coreutils/2005-09/msg00063.html + let processor = opts.processor.then(|| "unknown".to_string()); + + // This option is unsupported on modern Linux systems + // See: https://lists.gnu.org/archive/html/bug-coreutils/2005-09/msg00063.html + let hardware_platform = opts.hardware_platform.then(|| "unknown".to_string()); + + Ok(Self { + kernel_name, + nodename, + kernel_release, + kernel_version, + machine, + os, + processor, + hardware_platform, + }) } +} - // This option is unsupported on modern Linux systems - // See: https://lists.gnu.org/archive/html/bug-coreutils/2005-09/msg00063.html - if hardware_platform { - output.push_str("unknown"); - output.push(' '); - } +pub struct Options { + pub all: bool, + pub kernel_name: bool, + pub nodename: bool, + pub kernel_version: bool, + pub kernel_release: bool, + pub machine: bool, + pub processor: bool, + pub hardware_platform: bool, + pub os: bool, +} - println!("{}", output.trim_end()); +#[uucore::main] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let matches = uu_app().try_get_matches_from(args)?; + let options = Options { + all: matches.get_flag(options::ALL), + kernel_name: matches.get_flag(options::KERNEL_NAME), + nodename: matches.get_flag(options::NODENAME), + kernel_release: matches.get_flag(options::KERNEL_RELEASE), + kernel_version: matches.get_flag(options::KERNEL_VERSION), + machine: matches.get_flag(options::MACHINE), + processor: matches.get_flag(options::PROCESSOR), + hardware_platform: matches.get_flag(options::HARDWARE_PLATFORM), + os: matches.get_flag(options::OS), + }; + let output = UNameOutput::new(&options)?; + println!("{}", output.display().trim_end()); Ok(()) } diff --git a/src/uu/unexpand/Cargo.toml b/src/uu/unexpand/Cargo.toml index d10239b960e..a0644c04dfd 100644 --- a/src/uu/unexpand/Cargo.toml +++ b/src/uu/unexpand/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_unexpand" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "unexpand ~ (uutils) convert input spaces to tabs" diff --git a/src/uu/uniq/Cargo.toml b/src/uu/uniq/Cargo.toml index 52d1d22e16a..cc2fed4eeea 100644 --- a/src/uu/uniq/Cargo.toml +++ b/src/uu/uniq/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_uniq" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "uniq ~ (uutils) filter identical adjacent lines from input" diff --git a/src/uu/uniq/src/uniq.rs b/src/uu/uniq/src/uniq.rs index 72338bf9602..e074ebe42d7 100644 --- a/src/uu/uniq/src/uniq.rs +++ b/src/uu/uniq/src/uniq.rs @@ -2,14 +2,18 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. - -use clap::{builder::ValueParser, crate_version, Arg, ArgAction, ArgGroup, ArgMatches, Command}; +// spell-checker:ignore badoption +use clap::{ + builder::ValueParser, crate_version, error::ContextKind, error::Error, error::ErrorKind, Arg, + ArgAction, ArgMatches, Command, +}; use std::ffi::{OsStr, OsString}; use std::fs::File; -use std::io::{self, stdin, stdout, BufRead, BufReader, BufWriter, Write}; -use std::str::FromStr; +use std::io::{stdin, stdout, BufRead, BufReader, BufWriter, Write}; +use std::num::IntErrorKind; use uucore::display::Quotable; -use uucore::error::{FromIo, UResult, USimpleError, UUsageError}; +use uucore::error::{FromIo, UError, UResult, USimpleError}; +use uucore::posix::{posix_version, OBSOLETE}; use uucore::{format_usage, help_about, help_section, help_usage}; const ABOUT: &str = help_about!("uniq.md"); @@ -23,7 +27,6 @@ pub mod options { pub static IGNORE_CASE: &str = "ignore-case"; pub static REPEATED: &str = "repeated"; pub static SKIP_FIELDS: &str = "skip-fields"; - pub static OBSOLETE_SKIP_FIELDS: &str = "obsolete_skip_field"; pub static SKIP_CHARS: &str = "skip-chars"; pub static UNIQUE: &str = "unique"; pub static ZERO_TERMINATED: &str = "zero-terminated"; @@ -54,8 +57,6 @@ struct Uniq { zero_terminated: bool, } -const OBSOLETE_SKIP_FIELDS_DIGITS: [&str; 10] = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]; - macro_rules! write_line_terminator { ($writer:expr, $line_terminator:expr) => { $writer @@ -69,7 +70,7 @@ impl Uniq { let mut first_line_printed = false; let mut group_count = 1; let line_terminator = self.get_line_terminator(); - let mut lines = reader.split(line_terminator).map(get_line_string); + let mut lines = reader.split(line_terminator); let mut line = match lines.next() { Some(l) => l?, None => return Ok(()), @@ -111,22 +112,28 @@ impl Uniq { Ok(()) } - fn skip_fields<'a>(&self, line: &'a str) -> &'a str { + fn skip_fields(&self, line: &[u8]) -> Vec { if let Some(skip_fields) = self.skip_fields { - let mut i = 0; - let mut char_indices = line.char_indices(); + let mut line = line.iter(); + let mut line_after_skipped_field: Vec; for _ in 0..skip_fields { - if char_indices.all(|(_, c)| c.is_whitespace()) { - return ""; + if line.all(|u| u.is_ascii_whitespace()) { + return Vec::new(); } - match char_indices.find(|(_, c)| c.is_whitespace()) { - None => return "", - Some((next_field_i, _)) => i = next_field_i, + line_after_skipped_field = line + .by_ref() + .skip_while(|u| !u.is_ascii_whitespace()) + .copied() + .collect::>(); + + if line_after_skipped_field.is_empty() { + return Vec::new(); } + line = line_after_skipped_field.iter(); } - &line[i..] + line.copied().collect::>() } else { - line + line.to_vec() } } @@ -138,15 +145,15 @@ impl Uniq { } } - fn cmp_keys(&self, first: &str, second: &str) -> bool { + fn cmp_keys(&self, first: &[u8], second: &[u8]) -> bool { self.cmp_key(first, |first_iter| { self.cmp_key(second, |second_iter| first_iter.ne(second_iter)) }) } - fn cmp_key(&self, line: &str, mut closure: F) -> bool + fn cmp_key(&self, line: &[u8], mut closure: F) -> bool where - F: FnMut(&mut dyn Iterator) -> bool, + F: FnMut(&mut dyn Iterator) -> bool, { let fields_to_check = self.skip_fields(line); let len = fields_to_check.len(); @@ -155,28 +162,34 @@ impl Uniq { if len > 0 { // fast path: avoid doing any work if there is no need to skip or map to lower-case if !self.ignore_case && slice_start == 0 && slice_stop == len { - return closure(&mut fields_to_check.chars()); + return closure(&mut fields_to_check.iter().copied()); } // fast path: avoid skipping if self.ignore_case && slice_start == 0 && slice_stop == len { - return closure(&mut fields_to_check.chars().flat_map(|c| c.to_uppercase())); + return closure(&mut fields_to_check.iter().map(|u| u.to_ascii_lowercase())); } - // fast path: we can avoid mapping chars to upper-case, if we don't want to ignore the case + // fast path: we can avoid mapping chars to lower-case, if we don't want to ignore the case if !self.ignore_case { - return closure(&mut fields_to_check.chars().skip(slice_start).take(slice_stop)); + return closure( + &mut fields_to_check + .iter() + .skip(slice_start) + .take(slice_stop) + .copied(), + ); } closure( &mut fields_to_check - .chars() + .iter() .skip(slice_start) .take(slice_stop) - .flat_map(|c| c.to_uppercase()), + .map(|u| u.to_ascii_lowercase()), ) } else { - closure(&mut fields_to_check.chars()) + closure(&mut fields_to_check.iter().copied()) } } @@ -196,7 +209,7 @@ impl Uniq { fn print_line( &self, writer: &mut impl Write, - line: &str, + line: &[u8], count: usize, first_line_printed: bool, ) -> UResult<()> { @@ -207,9 +220,16 @@ impl Uniq { } if self.show_counts { - write!(writer, "{count:7} {line}") + let prefix = format!("{count:7} "); + let out = prefix + .as_bytes() + .iter() + .chain(line.iter()) + .copied() + .collect::>(); + writer.write_all(out.as_slice()) } else { - writer.write_all(line.as_bytes()) + writer.write_all(line) } .map_err_context(|| "Failed to write line".to_string())?; @@ -217,66 +237,328 @@ impl Uniq { } } -fn get_line_string(io_line: io::Result>) -> UResult { - let line_bytes = io_line.map_err_context(|| "failed to split lines".to_string())?; - String::from_utf8(line_bytes) - .map_err(|e| USimpleError::new(1, format!("failed to convert line to utf8: {e}"))) +fn opt_parsed(opt_name: &str, matches: &ArgMatches) -> UResult> { + match matches.get_one::(opt_name) { + Some(arg_str) => match arg_str.parse::() { + Ok(v) => Ok(Some(v)), + Err(e) => match e.kind() { + IntErrorKind::PosOverflow => Ok(Some(usize::MAX)), + _ => Err(USimpleError::new( + 1, + format!( + "Invalid argument for {}: {}", + opt_name, + arg_str.maybe_quote() + ), + )), + }, + }, + None => Ok(None), + } } -fn opt_parsed(opt_name: &str, matches: &ArgMatches) -> UResult> { - Ok(match matches.get_one::(opt_name) { - Some(arg_str) => Some(arg_str.parse().map_err(|_| { - USimpleError::new( - 1, - format!( - "Invalid argument for {}: {}", - opt_name, - arg_str.maybe_quote() - ), +/// Extract obsolete shorthands (if any) for skip fields and skip chars options +/// following GNU `uniq` behavior +/// +/// Examples for obsolete skip fields option +/// `uniq -1 file` would equal `uniq -f1 file` +/// `uniq -1 -2 -3 file` would equal `uniq -f123 file` +/// `uniq -1 -2 -f5 file` would equal `uniq -f5 file` +/// `uniq -u20s4 file` would equal `uniq -u -f20 -s4 file` +/// `uniq -D1w3 -3 file` would equal `uniq -D -f3 -w3 file` +/// +/// Examples for obsolete skip chars option +/// `uniq +1 file` would equal `uniq -s1 file` +/// `uniq +1 -s2 file` would equal `uniq -s2 file` +/// `uniq -s2 +3 file` would equal `uniq -s3 file` +/// +fn handle_obsolete(args: impl uucore::Args) -> (Vec, Option, Option) { + let mut skip_fields_old = None; + let mut skip_chars_old = None; + let mut preceding_long_opt_req_value = false; + let mut preceding_short_opt_req_value = false; + + let filtered_args = args + .filter_map(|os_slice| { + filter_args( + os_slice, + &mut skip_fields_old, + &mut skip_chars_old, + &mut preceding_long_opt_req_value, + &mut preceding_short_opt_req_value, ) - })?), - None => None, - }) + }) + .collect(); + + // exacted String values (if any) for skip_fields_old and skip_chars_old + // are guaranteed to consist of ascii digit chars only at this point + // so, it is safe to parse into usize and collapse Result into Option + let skip_fields_old: Option = skip_fields_old.and_then(|v| v.parse::().ok()); + let skip_chars_old: Option = skip_chars_old.and_then(|v| v.parse::().ok()); + + (filtered_args, skip_fields_old, skip_chars_old) } -/// Gets number of fields to be skipped from the shorthand option `-N` -/// -/// ```bash -/// uniq -12345 -/// ``` -/// the first digit isn't interpreted by clap as part of the value -/// so `get_one()` would return `2345`, then to get the actual value -/// we loop over every possible first digit, only one of which can be -/// found in the command line because they conflict with each other, -/// append the value to it and parse the resulting string as usize, -/// an error at this point means that a character that isn't a digit was given -fn obsolete_skip_field(matches: &ArgMatches) -> UResult> { - for opt_text in OBSOLETE_SKIP_FIELDS_DIGITS { - let argument = matches.get_one::(opt_text); - if matches.contains_id(opt_text) { - let mut full = opt_text.to_owned(); - if let Some(ar) = argument { - full.push_str(ar); +fn filter_args( + os_slice: OsString, + skip_fields_old: &mut Option, + skip_chars_old: &mut Option, + preceding_long_opt_req_value: &mut bool, + preceding_short_opt_req_value: &mut bool, +) -> Option { + let filter: Option; + if let Some(slice) = os_slice.to_str() { + if should_extract_obs_skip_fields( + slice, + preceding_long_opt_req_value, + preceding_short_opt_req_value, + ) { + // start of the short option string + // that can have obsolete skip fields option value in it + filter = handle_extract_obs_skip_fields(slice, skip_fields_old); + } else if should_extract_obs_skip_chars( + slice, + preceding_long_opt_req_value, + preceding_short_opt_req_value, + ) { + // the obsolete skip chars option + filter = handle_extract_obs_skip_chars(slice, skip_chars_old); + } else { + // either not a short option + // or a short option that cannot have obsolete lines value in it + filter = Some(OsString::from(slice)); + // Check and reset to None obsolete values extracted so far + // if corresponding new/documented options are encountered next. + // NOTE: For skip fields - occurrences of corresponding new/documented options + // inside combined short options ike '-u20s4' or '-D1w3', etc + // are also covered in `handle_extract_obs_skip_fields()` function + if slice.starts_with("-f") { + *skip_fields_old = None; } - let value = full.parse::(); + if slice.starts_with("-s") { + *skip_chars_old = None; + } + } + handle_preceding_options( + slice, + preceding_long_opt_req_value, + preceding_short_opt_req_value, + ); + } else { + // Cannot cleanly convert os_slice to UTF-8 + // Do not process and return as-is + // This will cause failure later on, but we should not handle it here + // and let clap panic on invalid UTF-8 argument + filter = Some(os_slice); + } + filter +} + +/// Helper function to [`filter_args`] +/// Checks if the slice is a true short option (and not hyphen prefixed value of an option) +/// and if so, a short option that can contain obsolete skip fields value +fn should_extract_obs_skip_fields( + slice: &str, + preceding_long_opt_req_value: &bool, + preceding_short_opt_req_value: &bool, +) -> bool { + slice.starts_with('-') + && !slice.starts_with("--") + && !preceding_long_opt_req_value + && !preceding_short_opt_req_value + && !slice.starts_with("-s") + && !slice.starts_with("-f") + && !slice.starts_with("-w") +} + +/// Helper function to [`filter_args`] +/// Checks if the slice is a true obsolete skip chars short option +fn should_extract_obs_skip_chars( + slice: &str, + preceding_long_opt_req_value: &bool, + preceding_short_opt_req_value: &bool, +) -> bool { + slice.starts_with('+') + && posix_version().is_some_and(|v| v <= OBSOLETE) + && !preceding_long_opt_req_value + && !preceding_short_opt_req_value + && slice.chars().nth(1).map_or(false, |c| c.is_ascii_digit()) +} - if let Ok(val) = value { - return Ok(Some(val)); +/// Helper function to [`filter_args`] +/// Captures if current slice is a preceding option +/// that requires value +fn handle_preceding_options( + slice: &str, + preceding_long_opt_req_value: &mut bool, + preceding_short_opt_req_value: &mut bool, +) { + // capture if current slice is a preceding long option that requires value and does not use '=' to assign that value + // following slice should be treaded as value for this option + // even if it starts with '-' (which would be treated as hyphen prefixed value) + if slice.starts_with("--") { + use options as O; + *preceding_long_opt_req_value = &slice[2..] == O::SKIP_CHARS + || &slice[2..] == O::SKIP_FIELDS + || &slice[2..] == O::CHECK_CHARS + || &slice[2..] == O::GROUP + || &slice[2..] == O::ALL_REPEATED; + } + // capture if current slice is a preceding short option that requires value and does not have value in the same slice (value separated by whitespace) + // following slice should be treaded as value for this option + // even if it starts with '-' (which would be treated as hyphen prefixed value) + *preceding_short_opt_req_value = slice == "-s" || slice == "-f" || slice == "-w"; + // slice is a value + // reset preceding option flags + if !slice.starts_with('-') { + *preceding_short_opt_req_value = false; + *preceding_long_opt_req_value = false; + } +} + +/// Helper function to [`filter_args`] +/// Extracts obsolete skip fields numeric part from argument slice +/// and filters it out +fn handle_extract_obs_skip_fields( + slice: &str, + skip_fields_old: &mut Option, +) -> Option { + let mut obs_extracted: Vec = vec![]; + let mut obs_end_reached = false; + let mut obs_overwritten_by_new = false; + let filtered_slice: Vec = slice + .chars() + .filter(|c| { + if c.eq(&'f') { + // any extracted obsolete skip fields value up to this point should be discarded + // as the new/documented option for skip fields was used after it + // i.e. in situation like `-u12f3` + // The obsolete skip fields value should still be extracted, filtered out + // but the skip_fields_old should be set to None instead of Some(String) later on + obs_overwritten_by_new = true; + } + // To correctly process scenario like '-u20s4' or '-D1w3', etc + // we need to stop extracting digits once alphabetic character is encountered + // after we already have something in obs_extracted + if c.is_ascii_digit() && !obs_end_reached { + obs_extracted.push(*c); + false } else { - return Err(USimpleError { - code: 1, - message: format!("Invalid argument for skip-fields: {}", full), + if !obs_extracted.is_empty() { + obs_end_reached = true; } - .into()); + true + } + }) + .collect(); + + if obs_extracted.is_empty() { + // no obsolete value found/extracted + Some(OsString::from(slice)) + } else { + // obsolete value was extracted + // unless there was new/documented option for skip fields used after it + // set the skip_fields_old value (concatenate to it if there was a value there already) + if obs_overwritten_by_new { + *skip_fields_old = None; + } else { + let mut extracted: String = obs_extracted.iter().collect(); + if let Some(val) = skip_fields_old { + extracted.push_str(val); } + *skip_fields_old = Some(extracted); + } + if filtered_slice.get(1).is_some() { + // there were some short options in front of or after obsolete lines value + // i.e. '-u20s4' or '-D1w3' or similar, which after extraction of obsolete lines value + // would look like '-us4' or '-Dw3' or similar + let filtered_slice: String = filtered_slice.iter().collect(); + Some(OsString::from(filtered_slice)) + } else { + None } } - Ok(None) +} + +/// Helper function to [`filter_args`] +/// Extracts obsolete skip chars numeric part from argument slice +fn handle_extract_obs_skip_chars( + slice: &str, + skip_chars_old: &mut Option, +) -> Option { + let mut obs_extracted: Vec = vec![]; + let mut slice_chars = slice.chars(); + slice_chars.next(); // drop leading '+' character + for c in slice_chars { + if c.is_ascii_digit() { + obs_extracted.push(c); + } else { + // for obsolete skip chars option the whole value after '+' should be numeric + // so, if any non-digit characters are encountered in the slice (i.e. `+1q`, etc) + // set skip_chars_old to None and return whole slice back. + // It will be parsed by clap and panic with appropriate error message + *skip_chars_old = None; + return Some(OsString::from(slice)); + } + } + if obs_extracted.is_empty() { + // no obsolete value found/extracted + // i.e. it was just '+' character alone + Some(OsString::from(slice)) + } else { + // successfully extracted numeric value + // capture it and return None to filter out the whole slice + *skip_chars_old = Some(obs_extracted.iter().collect()); + None + } +} + +/// Maps Clap errors to USimpleError and overrides 3 specific ones +/// to meet requirements of GNU tests for `uniq`. +/// Unfortunately these overrides are necessary, since several GNU tests +/// for `uniq` hardcode and require the exact wording of the error message +/// and it is not compatible with how Clap formats and displays those error messages. +fn map_clap_errors(clap_error: &Error) -> Box { + let footer = "Try 'uniq --help' for more information."; + let override_arg_conflict = + "--group is mutually exclusive with -c/-d/-D/-u\n".to_string() + footer; + let override_group_badoption = "invalid argument 'badoption' for '--group'\nValid arguments are:\n - 'prepend'\n - 'append'\n - 'separate'\n - 'both'\n".to_string() + footer; + let override_all_repeated_badoption = "invalid argument 'badoption' for '--all-repeated'\nValid arguments are:\n - 'none'\n - 'prepend'\n - 'separate'\n".to_string() + footer; + + let error_message = match clap_error.kind() { + ErrorKind::ArgumentConflict => override_arg_conflict, + ErrorKind::InvalidValue + if clap_error + .get(ContextKind::InvalidValue) + .is_some_and(|v| v.to_string() == "badoption") + && clap_error + .get(ContextKind::InvalidArg) + .is_some_and(|v| v.to_string().starts_with("--group")) => + { + override_group_badoption + } + ErrorKind::InvalidValue + if clap_error + .get(ContextKind::InvalidValue) + .is_some_and(|v| v.to_string() == "badoption") + && clap_error + .get(ContextKind::InvalidArg) + .is_some_and(|v| v.to_string().starts_with("--all-repeated")) => + { + override_all_repeated_badoption + } + _ => clap_error.to_string(), + }; + USimpleError::new(1, error_message) } #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().after_help(AFTER_HELP).try_get_matches_from(args)?; + let (args, skip_fields_old, skip_chars_old) = handle_obsolete(args); + + let matches = uu_app() + .try_get_matches_from(args) + .map_err(|e| map_clap_errors(&e))?; let files = matches.get_many::(ARG_FILES); @@ -286,8 +568,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .unwrap_or_default(); let skip_fields_modern: Option = opt_parsed(options::SKIP_FIELDS, &matches)?; - - let skip_fields_old: Option = obsolete_skip_field(&matches)?; + let skip_chars_modern: Option = opt_parsed(options::SKIP_CHARS, &matches)?; let uniq = Uniq { repeats_only: matches.get_flag(options::REPEATED) @@ -298,16 +579,16 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { delimiters: get_delimiter(&matches), show_counts: matches.get_flag(options::COUNT), skip_fields: skip_fields_modern.or(skip_fields_old), - slice_start: opt_parsed(options::SKIP_CHARS, &matches)?, + slice_start: skip_chars_modern.or(skip_chars_old), slice_stop: opt_parsed(options::CHECK_CHARS, &matches)?, ignore_case: matches.get_flag(options::IGNORE_CASE), zero_terminated: matches.get_flag(options::ZERO_TERMINATED), }; if uniq.show_counts && uniq.all_repeated { - return Err(UUsageError::new( + return Err(USimpleError::new( 1, - "printing all duplicated lines and repeat counts is meaningless", + "printing all duplicated lines and repeat counts is meaningless\nTry 'uniq --help' for more information.", )); } @@ -318,11 +599,12 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { - let mut cmd = Command::new(uucore::util_name()) + Command::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .override_usage(format_usage(USAGE)) .infer_long_args(true) + .after_help(AFTER_HELP) .arg( Arg::new(options::ALL_REPEATED) .short('D') @@ -356,6 +638,7 @@ pub fn uu_app() -> Command { options::REPEATED, options::ALL_REPEATED, options::UNIQUE, + options::COUNT ]), ) .arg( @@ -397,7 +680,6 @@ pub fn uu_app() -> Command { Arg::new(options::SKIP_FIELDS) .short('f') .long(options::SKIP_FIELDS) - .overrides_with_all(OBSOLETE_SKIP_FIELDS_DIGITS) .help("avoid comparing the first N fields") .value_name("N"), ) @@ -415,42 +697,14 @@ pub fn uu_app() -> Command { .help("end lines with 0 byte, not newline") .action(ArgAction::SetTrue), ) - .group( - // in GNU `uniq` every every digit of these arguments - // would be interpreted as a simple flag, - // these flags then are concatenated to get - // the number of fields to skip. - // in this way `uniq -1 -z -2` would be - // equal to `uniq -12 -q`, since this behavior - // is counterintuitive and it's hard to do in clap - // we handle it more like GNU `fold`: we have a flag - // for each possible initial digit, that takes the - // rest of the value as argument. - // we disallow explicitly multiple occurrences - // because then it would have a different behavior - // from GNU - ArgGroup::new(options::OBSOLETE_SKIP_FIELDS) - .multiple(false) - .args(OBSOLETE_SKIP_FIELDS_DIGITS) - ) .arg( Arg::new(ARG_FILES) .action(ArgAction::Append) .value_parser(ValueParser::os_string()) .num_args(0..=2) + .hide(true) .value_hint(clap::ValueHint::FilePath), - ); - - for i in OBSOLETE_SKIP_FIELDS_DIGITS { - cmd = cmd.arg( - Arg::new(i) - .short(i.chars().next().unwrap()) - .num_args(0..=1) - .hide(true), - ); - } - - cmd + ) } fn get_delimiter(matches: &ArgMatches) -> Delimiters { diff --git a/src/uu/unlink/Cargo.toml b/src/uu/unlink/Cargo.toml index 854df092c1b..c94a73ac027 100644 --- a/src/uu/unlink/Cargo.toml +++ b/src/uu/unlink/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_unlink" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "unlink ~ (uutils) remove a (file system) link to FILE" diff --git a/src/uu/uptime/Cargo.toml b/src/uu/uptime/Cargo.toml index 0013e980a67..922d566c0de 100644 --- a/src/uu/uptime/Cargo.toml +++ b/src/uu/uptime/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_uptime" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "uptime ~ (uutils) display dynamic system information" diff --git a/src/uu/users/Cargo.toml b/src/uu/users/Cargo.toml index 8e29b119e8a..d483d5d2661 100644 --- a/src/uu/users/Cargo.toml +++ b/src/uu/users/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_users" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "users ~ (uutils) display names of currently logged-in users" diff --git a/src/uu/vdir/Cargo.toml b/src/uu/vdir/Cargo.toml index 3c7e55eda69..26df48c109a 100644 --- a/src/uu/vdir/Cargo.toml +++ b/src/uu/vdir/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_vdir" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "shortcut to ls -l -b" diff --git a/src/uu/wc/BENCHMARKING.md b/src/uu/wc/BENCHMARKING.md index 0a34c8cfad2..953e9038c81 100644 --- a/src/uu/wc/BENCHMARKING.md +++ b/src/uu/wc/BENCHMARKING.md @@ -86,8 +86,7 @@ If you want to get fancy and exhaustive, generate a table: |------------------------|--------------|------------------|-----------------|-------------------| | `wc ` | 1.3965 | 1.6182 | 5.2967 | 2.2294 | | `wc -c ` | 0.8134 | 1.2774 | 0.7732 | 0.9106 | - -| `uucat | wc -c` | 2.7760 | 2.5565 | 2.3769 | 2.3982 | +| `uucat \| wc -c` | 2.7760 | 2.5565 | 2.3769 | 2.3982 | | `wc -l ` | 1.1441 | 1.2854 | 2.9681 | 1.1493 | | `wc -L ` | 2.1087 | 1.2551 | 5.4577 | 2.1490 | | `wc -m ` | 2.7272 | 2.1704 | 7.3371 | 3.4347 | diff --git a/src/uu/wc/Cargo.toml b/src/uu/wc/Cargo.toml index 516e3be46a3..7ca801884c4 100644 --- a/src/uu/wc/Cargo.toml +++ b/src/uu/wc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_wc" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "wc ~ (uutils) display newline, word, and byte counts for input" diff --git a/src/uu/wc/src/utf8/read.rs b/src/uu/wc/src/utf8/read.rs index 4a92d85e688..819b0a6891c 100644 --- a/src/uu/wc/src/utf8/read.rs +++ b/src/uu/wc/src/utf8/read.rs @@ -7,7 +7,6 @@ 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 { diff --git a/src/uu/wc/src/wc.rs b/src/uu/wc/src/wc.rs index d69647c3804..b7600cb2090 100644 --- a/src/uu/wc/src/wc.rs +++ b/src/uu/wc/src/wc.rs @@ -190,7 +190,7 @@ impl<'a> Inputs<'a> { // 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 files0_from_path = settings.files0_from.as_ref().map(Input::as_borrowed); let iter = iter::from_fn(move || { let (idx, next) = with_idx.next()?; @@ -526,11 +526,11 @@ fn word_count_from_reader( (_, false, false, true, true) => { word_count_from_reader_specialized::<_, false, false, true, true>(reader) } - // show_chars, show_words + // show_lines, show_words (_, false, true, false, true) => { word_count_from_reader_specialized::<_, false, true, false, true>(reader) } - // show_chars, show_lines + // show_lines, show_max_line_length (_, false, true, true, false) => { word_count_from_reader_specialized::<_, false, true, true, false>(reader) } @@ -565,7 +565,65 @@ fn word_count_from_reader( } } -#[allow(clippy::cognitive_complexity)] +fn process_chunk< + const SHOW_CHARS: bool, + const SHOW_LINES: bool, + const SHOW_MAX_LINE_LENGTH: bool, + const SHOW_WORDS: bool, +>( + total: &mut WordCount, + text: &str, + current_len: &mut usize, + in_word: &mut bool, +) { + for ch in text.chars() { + if SHOW_WORDS { + if ch.is_whitespace() { + *in_word = false; + } else if ch.is_ascii_control() { + // These count as characters but do not affect the word state + } else if !(*in_word) { + *in_word = true; + total.words += 1; + } + } + if SHOW_MAX_LINE_LENGTH { + match ch { + '\n' | '\r' | '\x0c' => { + total.max_line_length = max(*current_len, total.max_line_length); + *current_len = 0; + } + '\t' => { + *current_len -= *current_len % 8; + *current_len += 8; + } + _ => { + *current_len += ch.width().unwrap_or(0); + } + } + } + if SHOW_LINES && ch == '\n' { + total.lines += 1; + } + if SHOW_CHARS { + total.chars += 1; + } + } + total.bytes += text.len(); + + total.max_line_length = max(*current_len, total.max_line_length); +} + +fn handle_error(error: BufReadDecoderError<'_>, total: &mut WordCount) -> Option { + match error { + BufReadDecoderError::InvalidByteSequence(bytes) => { + total.bytes += bytes.len(); + } + BufReadDecoderError::Io(e) => return Some(e), + } + None +} + fn word_count_from_reader_specialized< T: WordCountable, const SHOW_CHARS: bool, @@ -579,58 +637,24 @@ fn word_count_from_reader_specialized< let mut reader = BufReadDecoder::new(reader.buffered()); let mut in_word = false; let mut current_len = 0; - while let Some(chunk) = reader.next_strict() { match chunk { Ok(text) => { - for ch in text.chars() { - if SHOW_WORDS { - if ch.is_whitespace() { - in_word = false; - } else if ch.is_ascii_control() { - // These count as characters but do not affect the word state - } else if !in_word { - in_word = true; - total.words += 1; - } - } - if SHOW_MAX_LINE_LENGTH { - match ch { - '\n' | '\r' | '\x0c' => { - total.max_line_length = max(current_len, total.max_line_length); - current_len = 0; - } - '\t' => { - current_len -= current_len % 8; - current_len += 8; - } - _ => { - current_len += ch.width().unwrap_or(0); - } - } - } - if SHOW_LINES && ch == '\n' { - total.lines += 1; - } - if SHOW_CHARS { - total.chars += 1; - } - } - total.bytes += text.len(); - } - Err(BufReadDecoderError::InvalidByteSequence(bytes)) => { - // GNU wc treats invalid data as neither word nor char nor whitespace, - // so no other counters are affected - total.bytes += bytes.len(); + process_chunk::( + &mut total, + text, + &mut current_len, + &mut in_word, + ); } - Err(BufReadDecoderError::Io(e)) => { - return (total, Some(e)); + Err(e) => { + if let Some(e) = handle_error(e, &mut total) { + return (total, Some(e)); + } } } } - total.max_line_length = max(current_len, total.max_line_length); - (total, None) } diff --git a/src/uu/who/Cargo.toml b/src/uu/who/Cargo.toml index f343942fc4b..867ad3ee02b 100644 --- a/src/uu/who/Cargo.toml +++ b/src/uu/who/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_who" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "who ~ (uutils) display information about currently logged-in users" diff --git a/src/uu/whoami/Cargo.toml b/src/uu/whoami/Cargo.toml index 7e47898efa2..74fb37cc30a 100644 --- a/src/uu/whoami/Cargo.toml +++ b/src/uu/whoami/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_whoami" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "whoami ~ (uutils) display user name of current effective user ID" diff --git a/src/uu/yes/Cargo.toml b/src/uu/yes/Cargo.toml index 1f59bdb16f5..1d1bbcbafa0 100644 --- a/src/uu/yes/Cargo.toml +++ b/src/uu/yes/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_yes" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "yes ~ (uutils) repeatedly display a line with STRING (or 'y')" diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index 78c01cd071c..53e2aa1e2a5 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "uucore" -version = "0.0.24" +version = "0.0.25" authors = ["uutils developers"] license = "MIT" description = "uutils ~ 'core' uutils code library (cross-platform)" @@ -77,10 +77,10 @@ colors = [] encoding = ["data-encoding", "data-encoding-macro", "z85", "thiserror"] entries = ["libc"] fs = ["dunce", "libc", "winapi-util", "windows-sys"] -fsext = ["libc", "time", "windows-sys"] +fsext = ["libc", "windows-sys"] fsxattr = ["xattr"] lines = [] -format = ["itertools"] +format = ["itertools", "quoting-style"] mode = ["libc"] perms = ["libc", "walkdir"] pipes = [] diff --git a/src/uucore/src/lib/features/backup_control.rs b/src/uucore/src/lib/features/backup_control.rs index fedbb375cb7..99889a6fff3 100644 --- a/src/uucore/src/lib/features/backup_control.rs +++ b/src/uucore/src/lib/features/backup_control.rs @@ -474,7 +474,6 @@ pub fn source_is_target_backup(source: &Path, target: &Path, suffix: &str) -> bo #[cfg(test)] mod tests { use super::*; - use std::env; // Required to instantiate mutex in shared context use clap::Command; use once_cell::sync::Lazy; diff --git a/src/uucore/src/lib/features/encoding.rs b/src/uucore/src/lib/features/encoding.rs index db218d5f061..dd89c1f5201 100644 --- a/src/uucore/src/lib/features/encoding.rs +++ b/src/uucore/src/lib/features/encoding.rs @@ -6,11 +6,9 @@ // spell-checker:ignore (strings) ABCDEFGHIJKLMNOPQRSTUVWXYZ ABCDEFGHIJKLMNOPQRSTUV // spell-checker:ignore (encodings) lsbf msbf hexupper -use data_encoding::{self, BASE32, BASE64}; - use std::io::{self, Read, Write}; -use data_encoding::{Encoding, BASE32HEX, BASE64URL, HEXUPPER}; +use data_encoding::{Encoding, BASE32, BASE32HEX, BASE64, BASE64URL, HEXUPPER}; use data_encoding_macro::new_encoding; #[cfg(feature = "thiserror")] use thiserror::Error; @@ -25,6 +23,7 @@ pub enum DecodeError { Io(#[from] io::Error), } +#[derive(Debug)] pub enum EncodeError { Z85InputLenNotMultipleOf4, InvalidInput, diff --git a/src/uucore/src/lib/features/entries.rs b/src/uucore/src/lib/features/entries.rs index fa10ba2decb..e2cbafa59c5 100644 --- a/src/uucore/src/lib/features/entries.rs +++ b/src/uucore/src/lib/features/entries.rs @@ -124,7 +124,7 @@ pub fn get_groups_gnu(arg_id: Option) -> IOResult> { Ok(sort_groups(groups, egid)) } -#[cfg(all(unix, feature = "process"))] +#[cfg(all(unix, not(target_os = "redox"), feature = "process"))] fn sort_groups(mut groups: Vec, egid: gid_t) -> Vec { if let Some(index) = groups.iter().position(|&x| x == egid) { groups[..=index].rotate_right(1); diff --git a/src/uucore/src/lib/features/format/argument.rs b/src/uucore/src/lib/features/format/argument.rs index ef81fc3533b..75851049895 100644 --- a/src/uucore/src/lib/features/format/argument.rs +++ b/src/uucore/src/lib/features/format/argument.rs @@ -31,7 +31,7 @@ pub enum FormatArgument { } pub trait ArgumentIter<'a>: Iterator { - fn get_char(&mut self) -> char; + fn get_char(&mut self) -> u8; fn get_i64(&mut self) -> i64; fn get_u64(&mut self) -> u64; fn get_f64(&mut self) -> f64; @@ -39,14 +39,14 @@ pub trait ArgumentIter<'a>: Iterator { } impl<'a, T: Iterator> ArgumentIter<'a> for T { - fn get_char(&mut self) -> char { + fn get_char(&mut self) -> u8 { let Some(next) = self.next() else { - return '\0'; + return b'\0'; }; match next { - FormatArgument::Char(c) => *c, - FormatArgument::Unparsed(s) => s.bytes().next().map_or('\0', char::from), - _ => '\0', + FormatArgument::Char(c) => *c as u8, + FormatArgument::Unparsed(s) => s.bytes().next().unwrap_or(b'\0'), + _ => b'\0', } } diff --git a/src/uucore/src/lib/features/format/num_format.rs b/src/uucore/src/lib/features/format/num_format.rs index 607c028c32c..52551f10b86 100644 --- a/src/uucore/src/lib/features/format/num_format.rs +++ b/src/uucore/src/lib/features/format/num_format.rs @@ -60,7 +60,7 @@ pub enum PositiveSign { Space, } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum NumberAlignment { Left, RightSpace, @@ -140,43 +140,25 @@ impl Formatter for UnsignedInt { fn fmt(&self, mut writer: impl Write, x: Self::Input) -> std::io::Result<()> { let mut s = match self.variant { UnsignedIntVariant::Decimal => format!("{x}"), - UnsignedIntVariant::Octal(Prefix::No) => format!("{x:o}"), - UnsignedIntVariant::Octal(Prefix::Yes) => { - // The prefix that rust uses is `0o`, but GNU uses `0`. - // We also need to take into account that 0 should not be 00 - // Since this is an unsigned int, we do not need to take the minus - // sign into account. - if x == 0 { - format!("{x:o}") - } else { - format!("0{x:o}") - } - } - UnsignedIntVariant::Hexadecimal(Case::Lowercase, Prefix::No) => { + UnsignedIntVariant::Octal(_) => format!("{x:o}"), + UnsignedIntVariant::Hexadecimal(Case::Lowercase, _) => { format!("{x:x}") } - UnsignedIntVariant::Hexadecimal(Case::Lowercase, Prefix::Yes) => { - if x == 0 { - "0".to_string() - } else { - format!("{x:#x}") - } - } - UnsignedIntVariant::Hexadecimal(Case::Uppercase, Prefix::No) => { + UnsignedIntVariant::Hexadecimal(Case::Uppercase, _) => { format!("{x:X}") } - UnsignedIntVariant::Hexadecimal(Case::Uppercase, Prefix::Yes) => { - if x == 0 { - "0".to_string() - } else { - format!("{x:#X}") - } - } }; - if self.precision > s.len() { - s = format!("{:0width$}", s, width = self.precision); - } + // Zeroes do not get a prefix. An octal value does also not get a + // prefix if the padded value will not start with a zero. + let prefix = match (x, self.variant) { + (1.., UnsignedIntVariant::Hexadecimal(Case::Lowercase, Prefix::Yes)) => "0x", + (1.., UnsignedIntVariant::Hexadecimal(Case::Uppercase, Prefix::Yes)) => "0X", + (1.., UnsignedIntVariant::Octal(Prefix::Yes)) if s.len() >= self.precision => "0", + _ => "", + }; + + s = format!("{prefix}{s:0>width$}", width = self.precision); match self.alignment { NumberAlignment::Left => write!(writer, "{s: Result { + // A signed int spec might be mapped to an unsigned int spec if no sign is specified + let s = if let Spec::SignedInt { + width, + precision, + positive_sign: PositiveSign::None, + alignment, + } = s + { + Spec::UnsignedInt { + variant: UnsignedIntVariant::Decimal, + width, + precision, + alignment, + } + } else { + s + }; + let Spec::UnsignedInt { variant, width, diff --git a/src/uucore/src/lib/features/format/spec.rs b/src/uucore/src/lib/features/format/spec.rs index 7c0d0236764..7c173a3a9b6 100644 --- a/src/uucore/src/lib/features/format/spec.rs +++ b/src/uucore/src/lib/features/format/spec.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 (vars) intmax ptrdiff +// spell-checker:ignore (vars) intmax ptrdiff padlen use crate::quoting_style::{escape_name, QuotingStyle}; @@ -14,7 +14,7 @@ use super::{ }, parse_escape_only, ArgumentIter, FormatChar, FormatError, }; -use std::{fmt::Display, io::Write, ops::ControlFlow}; +use std::{io::Write, ops::ControlFlow}; /// A parsed specification for formatting a value /// @@ -87,6 +87,40 @@ enum Length { LongDouble, } +#[derive(Default, PartialEq, Eq)] +struct Flags { + minus: bool, + plus: bool, + space: bool, + hash: bool, + zero: bool, +} + +impl Flags { + pub fn parse(rest: &mut &[u8], index: &mut usize) -> Self { + let mut flags = Self::default(); + + while let Some(x) = rest.get(*index) { + match x { + b'-' => flags.minus = true, + b'+' => flags.plus = true, + b' ' => flags.space = true, + b'#' => flags.hash = true, + b'0' => flags.zero = true, + _ => break, + } + *index += 1; + } + + flags + } + + /// Whether any of the flags is set to true + fn any(&self) -> bool { + self != &Self::default() + } +} + impl Spec { pub fn parse<'a>(rest: &mut &'a [u8]) -> Result { // Based on the C++ reference, the spec format looks like: @@ -97,34 +131,12 @@ impl Spec { let mut index = 0; let start = *rest; - let mut minus = false; - let mut plus = false; - let mut space = false; - let mut hash = false; - let mut zero = false; + let flags = Flags::parse(rest, &mut index); - while let Some(x) = rest.get(index) { - match x { - b'-' => minus = true, - b'+' => plus = true, - b' ' => space = true, - b'#' => hash = true, - b'0' => zero = true, - _ => break, - } - index += 1; - } - - let alignment = match (minus, zero) { - (true, _) => NumberAlignment::Left, - (false, true) => NumberAlignment::RightZero, - (false, false) => NumberAlignment::RightSpace, - }; - - let positive_sign = match (plus, space) { - (true, _) => PositiveSign::Plus, - (false, true) => PositiveSign::Space, - (false, false) => PositiveSign::None, + let positive_sign = match flags { + Flags { plus: true, .. } => PositiveSign::Plus, + Flags { space: true, .. } => PositiveSign::Space, + _ => PositiveSign::None, }; let width = eat_asterisk_or_number(rest, &mut index); @@ -136,6 +148,17 @@ impl Spec { None }; + // The `0` flag is ignored if `-` is given or a precision is specified. + // So the only case for RightZero, is when `-` is not given and the + // precision is none. + let alignment = if flags.minus { + NumberAlignment::Left + } else if flags.zero && precision.is_none() { + NumberAlignment::RightZero + } else { + NumberAlignment::RightSpace + }; + // We ignore the length. It's not really relevant to printf let _ = Self::parse_length(rest, &mut index); @@ -148,38 +171,38 @@ impl Spec { Ok(match type_spec { // GNU accepts minus, plus and space even though they are not used b'c' => { - if hash || precision.is_some() { + if flags.zero || flags.hash || precision.is_some() { return Err(&start[..index]); } Self::Char { width, - align_left: minus, + align_left: flags.minus, } } b's' => { - if hash { + if flags.zero || flags.hash { return Err(&start[..index]); } Self::String { precision, width, - align_left: minus, + align_left: flags.minus, } } b'b' => { - if hash || minus || plus || space || width.is_some() || precision.is_some() { + if flags.any() || width.is_some() || precision.is_some() { return Err(&start[..index]); } Self::EscapedString } b'q' => { - if hash || minus || plus || space || width.is_some() || precision.is_some() { + if flags.any() || width.is_some() || precision.is_some() { return Err(&start[..index]); } Self::QuotedString } b'd' | b'i' => { - if hash { + if flags.hash { return Err(&start[..index]); } Self::SignedInt { @@ -191,10 +214,10 @@ impl Spec { } c @ (b'u' | b'o' | b'x' | b'X') => { // Normal unsigned integer cannot have a prefix - if *c == b'u' && hash { + if *c == b'u' && flags.hash { return Err(&start[..index]); } - let prefix = match hash { + let prefix = match flags.hash { false => Prefix::No, true => Prefix::Yes, }; @@ -222,7 +245,7 @@ impl Spec { b'a' | b'A' => FloatVariant::Hexadecimal, _ => unreachable!(), }, - force_decimal: match hash { + force_decimal: match flags.hash { false => ForceDecimal::No, true => ForceDecimal::Yes, }, @@ -289,7 +312,7 @@ impl Spec { match self { Self::Char { width, align_left } => { let width = resolve_asterisk(*width, &mut args)?.unwrap_or(0); - write_padded(writer, args.get_char(), width, false, *align_left) + write_padded(writer, &[args.get_char()], width, *align_left) } Self::String { width, @@ -310,7 +333,7 @@ impl Spec { Some(p) if p < s.len() => &s[..p], _ => s, }; - write_padded(writer, truncated, width, false, *align_left) + write_padded(writer, truncated.as_bytes(), width, *align_left) } Self::EscapedString => { let s = args.get_str(); @@ -422,16 +445,17 @@ fn resolve_asterisk<'a>( fn write_padded( mut writer: impl Write, - text: impl Display, + text: &[u8], width: usize, - pad_zero: bool, left: bool, ) -> Result<(), FormatError> { - match (left, pad_zero) { - (false, false) => write!(writer, "{text: >width$}"), - (false, true) => write!(writer, "{text:0>width$}"), - // 0 is ignored if we pad left. - (true, _) => write!(writer, "{text: padlen$}", "")?; + writer.write_all(text) } .map_err(FormatError::IoError) } diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index 3b9170bc309..c7fb1f2fcb8 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -13,7 +13,6 @@ use libc::{ S_IROTH, S_IRUSR, S_ISGID, S_ISUID, S_ISVTX, S_IWGRP, S_IWOTH, S_IWUSR, S_IXGRP, S_IXOTH, S_IXUSR, }; -use std::borrow::Cow; use std::collections::HashSet; use std::collections::VecDeque; use std::env; @@ -195,30 +194,6 @@ impl Hash for FileInformation { } } -/// resolve a relative path -pub fn resolve_relative_path(path: &Path) -> Cow { - if path.components().all(|e| e != Component::ParentDir) { - return path.into(); - } - let root = Component::RootDir.as_os_str(); - let mut result = env::current_dir().unwrap_or_else(|_| PathBuf::from(root)); - for comp in path.components() { - match comp { - Component::ParentDir => { - if let Ok(p) = result.read_link() { - result = p; - } - result.pop(); - } - Component::CurDir => (), - Component::RootDir | Component::Normal(_) | Component::Prefix(_) => { - result.push(comp.as_os_str()); - } - } - } - result.into() -} - /// Controls how symbolic links should be handled when canonicalizing a path. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum MissingHandling { @@ -743,6 +718,55 @@ pub fn path_ends_with_terminator(path: &Path) -> bool { .map_or(false, |wide| wide == b'/'.into() || wide == b'\\'.into()) } +pub mod sane_blksize { + + #[cfg(not(target_os = "windows"))] + use std::os::unix::fs::MetadataExt; + use std::{fs::metadata, path::Path}; + + pub const DEFAULT: u64 = 512; + pub const MAX: u64 = (u32::MAX / 8 + 1) as u64; + + /// Provides sanity checked blksize value from the provided value. + /// + /// If the provided value is a invalid values a meaningful adaption + /// of that value is done. + pub fn sane_blksize(st_blksize: u64) -> u64 { + match st_blksize { + 0 => DEFAULT, + 1..=MAX => st_blksize, + _ => DEFAULT, + } + } + + /// Provides the blksize information from the provided metadata. + /// + /// If the metadata contain invalid values a meaningful adaption + /// of that value is done. + pub fn sane_blksize_from_metadata(_metadata: &std::fs::Metadata) -> u64 { + #[cfg(not(target_os = "windows"))] + { + sane_blksize(_metadata.blksize()) + } + + #[cfg(target_os = "windows")] + { + DEFAULT + } + } + + /// Provides the blksize information from given file path's filesystem. + /// + /// If the metadata can't be fetched or contain invalid values a + /// meaningful adaption of that value is done. + pub fn sane_blksize_from_path(path: &Path) -> u64 { + match metadata(path) { + Ok(metadata) => sane_blksize_from_metadata(&metadata), + Err(_) => DEFAULT, + } + } +} + #[cfg(test)] mod tests { // Note this useful idiom: importing names from outer (for mod tests) scope. @@ -970,4 +994,13 @@ mod tests { assert!(path_ends_with_terminator(Path::new("/"))); assert!(path_ends_with_terminator(Path::new("C:\\"))); } + + #[test] + fn test_sane_blksize() { + assert_eq!(512, sane_blksize::sane_blksize(0)); + assert_eq!(512, sane_blksize::sane_blksize(512)); + assert_eq!(4096, sane_blksize::sane_blksize(4096)); + assert_eq!(0x2000_0000, sane_blksize::sane_blksize(0x2000_0000)); + assert_eq!(512, sane_blksize::sane_blksize(0x2000_0001)); + } } diff --git a/src/uucore/src/lib/features/fsext.rs b/src/uucore/src/lib/features/fsext.rs index d02d74babbc..0acedb4e1bb 100644 --- a/src/uucore/src/lib/features/fsext.rs +++ b/src/uucore/src/lib/features/fsext.rs @@ -7,14 +7,11 @@ // spell-checker:ignore DATETIME getmntinfo subsecond (arch) bitrig ; (fs) cifs smbfs -use time::macros::format_description; -use time::UtcOffset; - #[cfg(any(target_os = "linux", target_os = "android"))] const LINUX_MTAB: &str = "/etc/mtab"; #[cfg(any(target_os = "linux", target_os = "android"))] const LINUX_MOUNTINFO: &str = "/proc/self/mountinfo"; -#[cfg(unix)] +#[cfg(all(unix, not(target_os = "redox")))] static MOUNT_OPT_BIND: &str = "bind"; #[cfg(windows)] const MAX_PATH: usize = 266; @@ -66,7 +63,6 @@ use libc::{ mode_t, strerror, S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFMT, S_IFREG, S_IFSOCK, }; use std::borrow::Cow; -use std::convert::From; #[cfg(unix)] use std::ffi::CStr; #[cfg(unix)] @@ -115,26 +111,16 @@ pub use libc::statfs as statfs_fn; pub use libc::statvfs as statfs_fn; pub trait BirthTime { - fn pretty_birth(&self) -> String; - fn birth(&self) -> u64; + fn birth(&self) -> Option<(u64, u32)>; } use std::fs::Metadata; impl BirthTime for Metadata { - fn pretty_birth(&self) -> String { - self.created() - .ok() - .and_then(|t| t.duration_since(UNIX_EPOCH).ok()) - .map(|e| pretty_time(e.as_secs() as i64, i64::from(e.subsec_nanos()))) - .unwrap_or_else(|| "-".to_owned()) - } - - fn birth(&self) -> u64 { + fn birth(&self) -> Option<(u64, u32)> { self.created() .ok() .and_then(|t| t.duration_since(UNIX_EPOCH).ok()) - .map(|e| e.as_secs()) - .unwrap_or_default() + .map(|e| (e.as_secs(), e.subsec_nanos())) } } @@ -318,7 +304,7 @@ impl From for MountInfo { } } -#[cfg(unix)] +#[cfg(all(unix, not(target_os = "redox")))] fn is_dummy_filesystem(fs_type: &str, mount_option: &str) -> bool { // spell-checker:disable match fs_type { @@ -337,14 +323,14 @@ fn is_dummy_filesystem(fs_type: &str, mount_option: &str) -> bool { // spell-checker:enable } -#[cfg(unix)] +#[cfg(all(unix, not(target_os = "redox")))] fn is_remote_filesystem(dev_name: &str, fs_type: &str) -> bool { dev_name.find(':').is_some() || (dev_name.starts_with("//") && fs_type == "smbfs" || fs_type == "cifs") || dev_name == "-hosts" } -#[cfg(unix)] +#[cfg(all(unix, not(target_os = "redox")))] fn mount_dev_id(mount_dir: &str) -> String { use std::os::unix::fs::MetadataExt; @@ -664,7 +650,7 @@ impl FsMeta for StatFs { any( target_arch = "s390x", target_vendor = "apple", - target_os = "android", + all(target_os = "android", target_pointer_width = "32"), target_os = "openbsd", not(target_pointer_width = "64") ) @@ -675,7 +661,8 @@ impl FsMeta for StatFs { target_os = "freebsd", target_os = "illumos", target_os = "solaris", - target_os = "redox" + target_os = "redox", + all(target_os = "android", target_pointer_width = "64"), ))] return self.f_bsize.try_into().unwrap(); } @@ -741,14 +728,17 @@ impl FsMeta for StatFs { not(target_env = "musl"), any( target_vendor = "apple", - target_os = "android", + all(target_os = "android", target_pointer_width = "32"), target_os = "freebsd", target_arch = "s390x", not(target_pointer_width = "64") ) ))] return self.f_type.into(); - #[cfg(target_env = "musl")] + #[cfg(any( + target_env = "musl", + all(target_os = "android", target_pointer_width = "64"), + ))] return self.f_type.try_into().unwrap(); } #[cfg(not(any( @@ -865,50 +855,6 @@ where } } -// match strftime "%Y-%m-%d %H:%M:%S.%f %z" -const PRETTY_DATETIME_FORMAT: &[time::format_description::FormatItem] = format_description!( - "\ -[year]-[month]-[day padding:zero] \ -[hour]:[minute]:[second].[subsecond digits:9] \ -[offset_hour sign:mandatory][offset_minute]" -); - -pub fn pretty_time(sec: i64, nsec: i64) -> String { - // sec == seconds since UNIX_EPOCH - // nsec == nanoseconds since (UNIX_EPOCH + sec) - let ts_nanos: i128 = (sec * 1_000_000_000 + nsec).into(); - - // Return the date in UTC - let tm = match time::OffsetDateTime::from_unix_timestamp_nanos(ts_nanos) { - Ok(tm) => tm, - Err(e) => { - panic!("error: {e}"); - } - }; - - // Get the offset to convert to local time - // Because of DST (daylight saving), we get the local time back when - // the date was set - let local_offset = match UtcOffset::local_offset_at(tm) { - Ok(lo) => lo, - Err(_) if cfg!(target_os = "redox") => UtcOffset::UTC, - Err(e) => { - panic!("error: {e}"); - } - }; - - // Include the conversion to local time - let res = tm - .to_offset(local_offset) - .format(&PRETTY_DATETIME_FORMAT) - .unwrap(); - if res.ends_with(" -0000") { - res.replace(" -0000", " +0000") - } else { - res - } -} - #[cfg(unix)] pub fn pretty_filetype<'a>(mode: mode_t, size: u64) -> &'a str { match mode & S_IFMT { @@ -1048,6 +994,7 @@ pub fn pretty_fstype<'a>(fstype: i64) -> Cow<'a, str> { 0x5846_5342 => "xfs".into(), 0x012F_D16D => "xia".into(), 0x2FC1_2FC1 => "zfs".into(), + 0xDE => "zfs".into(), other => format!("UNKNOWN ({other:#x})").into(), } // spell-checker:enable diff --git a/src/uucore/src/lib/features/perms.rs b/src/uucore/src/lib/features/perms.rs index 5384b52a18f..880620c7ab1 100644 --- a/src/uucore/src/lib/features/perms.rs +++ b/src/uucore/src/lib/features/perms.rs @@ -5,17 +5,14 @@ //! Common functions to manage permissions +// spell-checker:ignore (jargon) TOCTOU + use crate::display::Quotable; -use crate::error::strip_errno; -use crate::error::UResult; -use crate::error::USimpleError; +use crate::error::{strip_errno, UResult, USimpleError}; pub use crate::features::entries; -use crate::fs::resolve_relative_path; use crate::show_error; -use clap::Arg; -use clap::ArgMatches; -use clap::Command; -use libc::{self, gid_t, uid_t}; +use clap::{Arg, ArgMatches, Command}; +use libc::{gid_t, uid_t}; use walkdir::WalkDir; use std::io::Error as IOError; @@ -26,7 +23,7 @@ use std::fs::Metadata; use std::os::unix::fs::MetadataExt; use std::os::unix::ffi::OsStrExt; -use std::path::Path; +use std::path::{Path, MAIN_SEPARATOR_STR}; /// The various level of verbosity #[derive(PartialEq, Eq, Clone, Debug)] @@ -192,6 +189,75 @@ pub struct ChownExecutor { pub dereference: bool, } +#[cfg(test)] +pub fn check_root(path: &Path, would_recurse_symlink: bool) -> bool { + is_root(path, would_recurse_symlink) +} + +/// In the context of chown and chgrp, check whether we are in a "preserve-root" scenario. +/// +/// In particular, we want to prohibit further traversal only if: +/// (--preserve-root and -R present) && +/// (path canonicalizes to "/") && +/// ( +/// (path is a symlink && would traverse/recurse this symlink) || +/// (path is not a symlink) +/// ) +/// The first clause is checked by the caller, the second and third clause is checked here. +/// The caller has to evaluate -P/-H/-L into 'would_recurse_symlink'. +/// Recall that canonicalization resolves both relative paths (e.g. "..") and symlinks. +fn is_root(path: &Path, would_traverse_symlink: bool) -> bool { + // The third clause can be evaluated without any syscalls, so we do that first. + // If we would_recurse_symlink, then the clause is true no matter whether the path is a symlink + // or not. Otherwise, we only need to check here if the path can syntactically be a symlink: + if !would_traverse_symlink { + // We cannot check path.is_dir() here, as this would resolve symlinks, + // which we need to avoid here. + // All directory-ish paths match "*/", except ".", "..", "*/.", and "*/..". + let looks_like_dir = match path.as_os_str().to_str() { + // If it contains special character, prefer to err on the side of safety, i.e. forbidding the chown operation: + None => false, + Some(".") | Some("..") => true, + Some(path_str) => { + (path_str.ends_with(MAIN_SEPARATOR_STR)) + || (path_str.ends_with(&format!("{}.", MAIN_SEPARATOR_STR))) + || (path_str.ends_with(&format!("{}..", MAIN_SEPARATOR_STR))) + } + }; + // TODO: Once we reach MSRV 1.74.0, replace this abomination by something simpler, e.g. this: + // let path_bytes = path.as_os_str().as_encoded_bytes(); + // let looks_like_dir = path_bytes == [b'.'] + // || path_bytes == [b'.', b'.'] + // || path_bytes.ends_with(&[MAIN_SEPARATOR as u8]) + // || path_bytes.ends_with(&[MAIN_SEPARATOR as u8, b'.']) + // || path_bytes.ends_with(&[MAIN_SEPARATOR as u8, b'.', b'.']); + if !looks_like_dir { + return false; + } + } + + // FIXME: TOCTOU bug! canonicalize() runs at a different time than WalkDir's recursion decision. + // However, we're forced to make the decision whether to warn about --preserve-root + // *before* even attempting to chown the path, let alone doing the stat inside WalkDir. + if let Ok(p) = path.canonicalize() { + let path_buf = path.to_path_buf(); + if p.parent().is_none() { + if path_buf.as_os_str() == "/" { + show_error!("it is dangerous to operate recursively on '/'"); + } else { + show_error!( + "it is dangerous to operate recursively on {} (same as '/')", + path_buf.quote() + ); + } + show_error!("use --no-preserve-root to override this failsafe"); + return true; + } + } + + false +} + impl ChownExecutor { pub fn exec(&self) -> UResult<()> { let mut ret = 0; @@ -221,31 +287,12 @@ impl ChownExecutor { } }; - // Prohibit only if: - // (--preserve-root and -R present) && - // ( - // (argument is not symlink && resolved to be '/') || - // (argument is symlink && should follow argument && resolved to be '/') - // ) - if self.recursive && self.preserve_root { - let may_exist = if self.dereference { - path.canonicalize().ok() - } else { - let real = resolve_relative_path(path); - if real.is_dir() { - Some(real.canonicalize().expect("failed to get real path")) - } else { - Some(real.into_owned()) - } - }; - - if let Some(p) = may_exist { - if p.parent().is_none() { - show_error!("it is dangerous to operate recursively on '/'"); - show_error!("use --no-preserve-root to override this failsafe"); - return 1; - } - } + if self.recursive + && self.preserve_root + && is_root(path, self.traverse_symlinks != TraverseSymlinks::None) + { + // Fail-fast, do not attempt to recurse. + return 1; } let ret = if self.matched(meta.uid(), meta.gid()) { @@ -336,6 +383,12 @@ impl ChownExecutor { } }; + if self.preserve_root && is_root(path, self.traverse_symlinks == TraverseSymlinks::All) + { + // Fail-fast, do not recurse further. + return 1; + } + if !self.matched(meta.uid(), meta.gid()) { self.print_verbose_ownership_retained_as( path, @@ -590,3 +643,74 @@ pub fn chown_base( }; executor.exec() } + +#[cfg(test)] +mod tests { + // Note this useful idiom: importing names from outer (for mod tests) scope. + use super::*; + #[cfg(unix)] + use std::os::unix; + use std::path::{Component, PathBuf}; + #[cfg(unix)] + use tempfile::tempdir; + + #[test] + fn test_empty_string() { + let path = PathBuf::new(); + assert_eq!(path.to_str(), Some("")); + // The main point to test here is that we don't crash. + // The result should be 'false', to avoid unnecessary and confusing warnings. + assert!(!is_root(&path, false)); + assert!(!is_root(&path, true)); + } + + #[allow(clippy::needless_borrow)] + #[cfg(unix)] + #[test] + fn test_literal_root() { + let component = Component::RootDir; + let path: &Path = component.as_ref(); + assert_eq!( + path.to_str(), + Some("/"), + "cfg(unix) but using non-unix path delimiters?!" + ); + // Must return true, this is the main scenario that --preserve-root shall prevent. + assert!(is_root(&path, false)); + assert!(is_root(&path, true)); + } + + #[cfg(unix)] + #[test] + fn test_symlink_slash() { + let temp_dir = tempdir().unwrap(); + let symlink_path = temp_dir.path().join("symlink"); + unix::fs::symlink(PathBuf::from("/"), symlink_path).unwrap(); + let symlink_path_slash = temp_dir.path().join("symlink/"); + // Must return true, we're about to "accidentally" recurse on "/", + // since "symlink/" always counts as an already-entered directory + // Output from GNU: + // $ chown --preserve-root -RH --dereference $(id -u) slink-to-root/ + // chown: it is dangerous to operate recursively on 'slink-to-root/' (same as '/') + // chown: use --no-preserve-root to override this failsafe + // [$? = 1] + // $ chown --preserve-root -RH --no-dereference $(id -u) slink-to-root/ + // chown: it is dangerous to operate recursively on 'slink-to-root/' (same as '/') + // chown: use --no-preserve-root to override this failsafe + // [$? = 1] + assert!(is_root(&symlink_path_slash, false)); + assert!(is_root(&symlink_path_slash, true)); + } + + #[cfg(unix)] + #[test] + fn test_symlink_no_slash() { + // This covers both the commandline-argument case and the recursion case. + let temp_dir = tempdir().unwrap(); + let symlink_path = temp_dir.path().join("symlink"); + unix::fs::symlink(PathBuf::from("/"), &symlink_path).unwrap(); + // Only return true we're about to "accidentally" recurse on "/". + assert!(!is_root(&symlink_path, false)); + assert!(is_root(&symlink_path, true)); + } +} diff --git a/src/uucore/src/lib/features/pipes.rs b/src/uucore/src/lib/features/pipes.rs index 75749f72148..17c5b1b3245 100644 --- a/src/uucore/src/lib/features/pipes.rs +++ b/src/uucore/src/lib/features/pipes.rs @@ -8,7 +8,6 @@ use std::fs::File; use std::io::IoSlice; #[cfg(any(target_os = "linux", target_os = "android"))] use std::os::unix::io::AsRawFd; -use std::os::unix::io::FromRawFd; #[cfg(any(target_os = "linux", target_os = "android"))] use nix::fcntl::SpliceFFlags; @@ -21,8 +20,7 @@ pub use nix::{Error, Result}; /// from the first. pub fn pipe() -> Result<(File, File)> { let (read, write) = nix::unistd::pipe()?; - // SAFETY: The file descriptors do not have other owners. - unsafe { Ok((File::from_raw_fd(read), File::from_raw_fd(write))) } + Ok((File::from(read), File::from(write))) } /// Less noisy wrapper around [`nix::fcntl::splice`]. diff --git a/src/uucore/src/lib/features/utmpx.rs b/src/uucore/src/lib/features/utmpx.rs index 1b6ecbcf5c8..bdc3544b2bc 100644 --- a/src/uucore/src/lib/features/utmpx.rs +++ b/src/uucore/src/lib/features/utmpx.rs @@ -54,8 +54,6 @@ pub unsafe extern "C" fn utmpxname(_file: *const libc::c_char) -> libc::c_int { 0 } -use once_cell::sync::Lazy; - use crate::*; // import macros from `../../macros.rs` // In case the c_char array doesn't end with NULL diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index 6f8400589ef..e891cc40410 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -22,9 +22,11 @@ pub use uucore_procs::*; // * cross-platform modules pub use crate::mods::display; pub use crate::mods::error; +pub use crate::mods::io; pub use crate::mods::line_ending; pub use crate::mods::os; pub use crate::mods::panic; +pub use crate::mods::posix; // * string parsing modules pub use crate::parser::parse_glob; diff --git a/src/uucore/src/lib/mods.rs b/src/uucore/src/lib/mods.rs index 986536d6dd5..29508e31a89 100644 --- a/src/uucore/src/lib/mods.rs +++ b/src/uucore/src/lib/mods.rs @@ -6,6 +6,8 @@ pub mod display; pub mod error; +pub mod io; pub mod line_ending; pub mod os; pub mod panic; +pub mod posix; diff --git a/src/uucore/src/lib/mods/error.rs b/src/uucore/src/lib/mods/error.rs index 82644ae8a50..5720a6bef96 100644 --- a/src/uucore/src/lib/mods/error.rs +++ b/src/uucore/src/lib/mods/error.rs @@ -56,7 +56,6 @@ // spell-checker:ignore uioerror rustdoc -use clap; use std::{ error::Error, fmt::{Display, Formatter}, diff --git a/src/uucore/src/lib/mods/io.rs b/src/uucore/src/lib/mods/io.rs new file mode 100644 index 00000000000..0d2240e9b1c --- /dev/null +++ b/src/uucore/src/lib/mods/io.rs @@ -0,0 +1,98 @@ +// 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. + +/// Encapsulates differences between OSs regarding the access to +/// file handles / descriptors. +/// This is useful when dealing with lower level stdin/stdout access. +/// +/// In detail: +/// On unix like OSs, file _descriptors_ are used in this context. +/// On windows OSs, file _handles_ are used. +/// +/// Even though they are distinct classes, they share common functionality. +/// Access to this common functionality is provided in `OwnedFileDescriptorOrHandle`. + +#[cfg(not(windows))] +use std::os::fd::{AsFd, OwnedFd}; +#[cfg(windows)] +use std::os::windows::io::{AsHandle, OwnedHandle}; +use std::{ + fs::{File, OpenOptions}, + io, + path::Path, + process::Stdio, +}; + +#[cfg(windows)] +type NativeType = OwnedHandle; +#[cfg(not(windows))] +type NativeType = OwnedFd; + +/// abstraction wrapper for native file handle / file descriptor +pub struct OwnedFileDescriptorOrHandle { + fx: NativeType, +} + +impl OwnedFileDescriptorOrHandle { + /// create from underlying native type + pub fn new(x: NativeType) -> Self { + Self { fx: x } + } + + /// create by opening a file + pub fn open_file(options: &OpenOptions, path: &Path) -> io::Result { + let f = options.open(path)?; + Self::from(f) + } + + /// conversion from borrowed native type + /// + /// e.g. `std::io::stdout()`, `std::fs::File`, ... + #[cfg(windows)] + pub fn from(t: T) -> io::Result { + Ok(Self { + fx: t.as_handle().try_clone_to_owned()?, + }) + } + + /// conversion from borrowed native type + /// + /// e.g. `std::io::stdout()`, `std::fs::File`, ... + #[cfg(not(windows))] + pub fn from(t: T) -> io::Result { + Ok(Self { + fx: t.as_fd().try_clone_to_owned()?, + }) + } + + /// instantiates a corresponding `File` + pub fn into_file(self) -> File { + File::from(self.fx) + } + + /// instantiates a corresponding `Stdio` + pub fn into_stdio(self) -> Stdio { + Stdio::from(self.fx) + } + + /// clones self. useful when needing another + /// owned reference to same file + pub fn try_clone(&self) -> io::Result { + self.fx.try_clone().map(Self::new) + } + + /// provides native type to be used with + /// OS specific functions without abstraction + pub fn as_raw(&self) -> &NativeType { + &self.fx + } +} + +/// instantiates a corresponding `Stdio` +impl From for Stdio { + fn from(value: OwnedFileDescriptorOrHandle) -> Self { + value.into_stdio() + } +} diff --git a/src/uucore/src/lib/mods/posix.rs b/src/uucore/src/lib/mods/posix.rs new file mode 100644 index 00000000000..662880f8466 --- /dev/null +++ b/src/uucore/src/lib/mods/posix.rs @@ -0,0 +1,52 @@ +// 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. +// spell-checker:ignore (vars) +//! Iterate over lines, including the line ending character(s). +//! +//! This module provides the [`posix_version`] function, that returns +//! Some(usize) if the `_POSIX2_VERSION` environment variable is defined +//! and has value that can be parsed. +//! Otherwise returns None, so the calling utility would assume default behavior. +//! +//! NOTE: GNU (as of v9.4) recognizes three distinct values for POSIX version: +//! '199209' for POSIX 1003.2-1992, which would define Obsolete mode +//! '200112' for POSIX 1003.1-2001, which is the minimum version for Traditional mode +//! '200809' for POSIX 1003.1-2008, which is the minimum version for Modern mode +//! +//! Utilities that rely on this module: +//! `sort` (TBD) +//! `tail` (TBD) +//! `touch` (TBD) +//! `uniq` +//! +use std::env; + +pub const OBSOLETE: usize = 199209; +pub const TRADITIONAL: usize = 200112; +pub const MODERN: usize = 200809; + +pub fn posix_version() -> Option { + env::var("_POSIX2_VERSION") + .ok() + .and_then(|v| v.parse::().ok()) +} + +#[cfg(test)] +mod tests { + use crate::posix::*; + + #[test] + fn test_posix_version() { + // default + assert_eq!(posix_version(), None); + // set specific version + env::set_var("_POSIX2_VERSION", OBSOLETE.to_string()); + assert_eq!(posix_version(), Some(OBSOLETE)); + env::set_var("_POSIX2_VERSION", TRADITIONAL.to_string()); + assert_eq!(posix_version(), Some(TRADITIONAL)); + env::set_var("_POSIX2_VERSION", MODERN.to_string()); + assert_eq!(posix_version(), Some(MODERN)); + } +} diff --git a/src/uucore/src/lib/parser/parse_glob.rs b/src/uucore/src/lib/parser/parse_glob.rs index 9215dd7bf7d..100d2edf561 100644 --- a/src/uucore/src/lib/parser/parse_glob.rs +++ b/src/uucore/src/lib/parser/parse_glob.rs @@ -18,7 +18,11 @@ fn fix_negation(glob: &str) -> String { while i + 3 < chars.len() { if chars[i] == '[' && chars[i + 1] == '^' { match chars[i + 3..].iter().position(|x| *x == ']') { - None => (), + None => { + // if closing square bracket not found, stop looking for it + // again + break; + } Some(j) => { chars[i + 1] = '!'; i += j + 4; @@ -90,6 +94,11 @@ mod tests { assert_eq!(fix_negation("[[]] [^a]"), "[[]] [!a]"); assert_eq!(fix_negation("[[] [^a]"), "[[] [!a]"); assert_eq!(fix_negation("[]] [^a]"), "[]] [!a]"); + + // test that we don't look for closing square brackets unnecessarily + // Verifies issue #5584 + let chars = "^[".repeat(174571); + assert_eq!(fix_negation(chars.as_str()), chars); } #[test] diff --git a/src/uucore/src/lib/parser/parse_size.rs b/src/uucore/src/lib/parser/parse_size.rs index 163c8942fb2..b963a713159 100644 --- a/src/uucore/src/lib/parser/parse_size.rs +++ b/src/uucore/src/lib/parser/parse_size.rs @@ -16,6 +16,8 @@ use crate::display::Quotable; /// The [`Parser::parse`] function performs the parse. #[derive(Default)] pub struct Parser<'parser> { + /// Whether to allow empty numeric strings. + pub no_empty_numeric: bool, /// Whether to treat the suffix "B" as meaning "bytes". pub capital_b_bytes: bool, /// Whether to treat "b" as a "byte count" instead of "block" @@ -48,6 +50,10 @@ impl<'parser> Parser<'parser> { self } + pub fn with_allow_empty_numeric(&mut self, value: bool) -> &mut Self { + self.no_empty_numeric = value; + self + } /// Parse a size string into a number of bytes. /// /// A size string comprises an integer and an optional unit. The unit @@ -90,9 +96,9 @@ impl<'parser> Parser<'parser> { NumberSystem::Hexadecimal => size .chars() .take(2) - .chain(size.chars().skip(2).take_while(|c| c.is_ascii_hexdigit())) + .chain(size.chars().skip(2).take_while(char::is_ascii_hexdigit)) .collect(), - _ => size.chars().take_while(|c| c.is_ascii_digit()).collect(), + _ => size.chars().take_while(char::is_ascii_digit).collect(), }; let mut unit: &str = &size[numeric_string.len()..]; @@ -160,7 +166,7 @@ impl<'parser> Parser<'parser> { // parse string into u128 let number: u128 = match number_system { NumberSystem::Decimal => { - if numeric_string.is_empty() { + if numeric_string.is_empty() && !self.no_empty_numeric { 1 } else { Self::parse_number(&numeric_string, 10, size)? @@ -243,7 +249,7 @@ impl<'parser> Parser<'parser> { let num_digits: usize = size .chars() - .take_while(|c| c.is_ascii_digit()) + .take_while(char::is_ascii_digit) .collect::() .len(); let all_zeros = size.chars().all(|c| c == '0'); diff --git a/src/uucore/src/lib/parser/shortcut_value_parser.rs b/src/uucore/src/lib/parser/shortcut_value_parser.rs index 2b94f9b75bc..bdc96b8ef59 100644 --- a/src/uucore/src/lib/parser/shortcut_value_parser.rs +++ b/src/uucore/src/lib/parser/shortcut_value_parser.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 abcdefgh +// spell-checker:ignore abcdefgh abef use clap::{ builder::{PossibleValue, TypedValueParser}, error::{ContextKind, ContextValue, ErrorKind}, diff --git a/src/uucore_procs/Cargo.toml b/src/uucore_procs/Cargo.toml index 2411bb6c166..f1817d2a2d9 100644 --- a/src/uucore_procs/Cargo.toml +++ b/src/uucore_procs/Cargo.toml @@ -1,7 +1,7 @@ # spell-checker:ignore uuhelp [package] name = "uucore_procs" -version = "0.0.24" +version = "0.0.25" authors = ["Roy Ivy III "] license = "MIT" description = "uutils ~ 'uucore' proc-macros" @@ -19,4 +19,4 @@ proc-macro = true [dependencies] proc-macro2 = "1.0" quote = "1.0" -uuhelp_parser = { path = "../uuhelp_parser", version = "0.0.24" } +uuhelp_parser = { path = "../uuhelp_parser", version = "0.0.25" } diff --git a/src/uucore_procs/src/lib.rs b/src/uucore_procs/src/lib.rs index cbe915936d3..dfd8b88f631 100644 --- a/src/uucore_procs/src/lib.rs +++ b/src/uucore_procs/src/lib.rs @@ -63,6 +63,9 @@ pub fn help_about(input: TokenStream) -> TokenStream { let input: Vec = input.into_iter().collect(); let filename = get_argument(&input, 0, "filename"); let text: String = uuhelp_parser::parse_about(&read_help(&filename)); + if text.is_empty() { + panic!("About text not found! Make sure the markdown format is correct"); + } TokenTree::Literal(Literal::string(&text)).into() } @@ -77,6 +80,9 @@ pub fn help_usage(input: TokenStream) -> TokenStream { let input: Vec = input.into_iter().collect(); let filename = get_argument(&input, 0, "filename"); let text: String = uuhelp_parser::parse_usage(&read_help(&filename)); + if text.is_empty() { + panic!("Usage text not found! Make sure the markdown format is correct"); + } TokenTree::Literal(Literal::string(&text)).into() } diff --git a/src/uuhelp_parser/Cargo.toml b/src/uuhelp_parser/Cargo.toml index 018aa1d898f..f3948e13c06 100644 --- a/src/uuhelp_parser/Cargo.toml +++ b/src/uuhelp_parser/Cargo.toml @@ -1,7 +1,7 @@ # spell-checker:ignore uuhelp [package] name = "uuhelp_parser" -version = "0.0.24" +version = "0.0.25" edition = "2021" license = "MIT" description = "A collection of functions to parse the markdown code of help files" diff --git a/tests/by-util/test_base32.rs b/tests/by-util/test_base32.rs index 8bb5bda5415..785db388be2 100644 --- a/tests/by-util/test_base32.rs +++ b/tests/by-util/test_base32.rs @@ -22,6 +22,26 @@ fn test_encode() { .stdout_only("JBSWY3DPFQQFO33SNRSCC===\n"); // spell-checker:disable-line } +#[test] +fn test_encode_repeat_flags_later_wrap_10() { + let input = "Hello, World!\n"; + new_ucmd!() + .args(&["-ii", "-w17", "-w10"]) + .pipe_in(input) + .succeeds() + .stdout_only("JBSWY3DPFQ\nQFO33SNRSC\nCCQ=\n"); // spell-checker:disable-line +} + +#[test] +fn test_encode_repeat_flags_later_wrap_17() { + let input = "Hello, World!\n"; + new_ucmd!() + .args(&["-ii", "-w10", "-w17"]) + .pipe_in(input) + .succeeds() + .stdout_only("JBSWY3DPFQQFO33SN\nRSCCCQ=\n"); // spell-checker:disable-line +} + #[test] fn test_base32_encode_file() { new_ucmd!() @@ -42,6 +62,16 @@ fn test_decode() { } } +#[test] +fn test_decode_repeat_flags() { + let input = "JBSWY3DPFQQFO33SNRSCC===\n"; // spell-checker:disable-line + new_ucmd!() + .args(&["-didiw80", "--wrap=17", "--wrap", "8"]) // spell-checker:disable-line + .pipe_in(input) + .succeeds() + .stdout_only("Hello, World!"); +} + #[test] fn test_garbage() { let input = "aGVsbG8sIHdvcmxkIQ==\0"; // spell-checker:disable-line diff --git a/tests/by-util/test_base64.rs b/tests/by-util/test_base64.rs index b46507faeff..403fd7db86a 100644 --- a/tests/by-util/test_base64.rs +++ b/tests/by-util/test_base64.rs @@ -20,6 +20,26 @@ fn test_encode() { .stdout_only("aGVsbG8sIHdvcmxkIQ==\n"); // spell-checker:disable-line } +#[test] +fn test_encode_repeat_flags_later_wrap_10() { + let input = "hello, world!"; + new_ucmd!() + .args(&["-ii", "-w15", "-w10"]) + .pipe_in(input) + .succeeds() + .stdout_only("aGVsbG8sIH\ndvcmxkIQ==\n"); // spell-checker:disable-line +} + +#[test] +fn test_encode_repeat_flags_later_wrap_15() { + let input = "hello, world!"; + new_ucmd!() + .args(&["-ii", "-w10", "-w15"]) + .pipe_in(input) + .succeeds() + .stdout_only("aGVsbG8sIHdvcmx\nkIQ==\n"); // spell-checker:disable-line +} + #[test] fn test_base64_encode_file() { new_ucmd!() @@ -40,6 +60,16 @@ fn test_decode() { } } +#[test] +fn test_decode_repeat_flags() { + let input = "aGVsbG8sIHdvcmxkIQ==\n"; // spell-checker:disable-line + new_ucmd!() + .args(&["-didiw80", "--wrap=17", "--wrap", "8"]) // spell-checker:disable-line + .pipe_in(input) + .succeeds() + .stdout_only("hello, world!"); +} + #[test] fn test_garbage() { let input = "aGVsbG8sIHdvcmxkIQ==\0"; // spell-checker:disable-line diff --git a/tests/by-util/test_basename.rs b/tests/by-util/test_basename.rs index 73b44ff7522..b9cee2863ac 100644 --- a/tests/by-util/test_basename.rs +++ b/tests/by-util/test_basename.rs @@ -201,3 +201,67 @@ fn test_simple_format() { fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails().code_is(1); } + +#[test] +fn test_repeated_multiple() { + new_ucmd!() + .args(&["-aa", "-a", "foo"]) + .succeeds() + .stdout_is("foo\n"); +} + +#[test] +fn test_repeated_multiple_many() { + new_ucmd!() + .args(&["-aa", "-a", "1/foo", "q/bar", "x/y/baz"]) + .succeeds() + .stdout_is("foo\nbar\nbaz\n"); +} + +#[test] +fn test_repeated_suffix_last() { + new_ucmd!() + .args(&["-s", ".h", "-s", ".c", "foo.c"]) + .succeeds() + .stdout_is("foo\n"); +} + +#[test] +fn test_repeated_suffix_not_first() { + new_ucmd!() + .args(&["-s", ".h", "-s", ".c", "foo.h"]) + .succeeds() + .stdout_is("foo.h\n"); +} + +#[test] +fn test_repeated_suffix_multiple() { + new_ucmd!() + .args(&["-as", ".h", "-a", "-s", ".c", "foo.c", "bar.c", "bar.h"]) + .succeeds() + .stdout_is("foo\nbar\nbar.h\n"); +} + +#[test] +fn test_repeated_zero() { + new_ucmd!() + .args(&["-zz", "-z", "foo/bar"]) + .succeeds() + .stdout_is("bar\0"); +} + +#[test] +fn test_zero_does_not_imply_multiple() { + new_ucmd!() + .args(&["-z", "foo.c", "c"]) + .succeeds() + .stdout_is("foo.\0"); +} + +#[test] +fn test_suffix_implies_multiple() { + new_ucmd!() + .args(&["-s", ".c", "foo.c", "o.c"]) + .succeeds() + .stdout_is("foo\no\n"); +} diff --git a/tests/by-util/test_basenc.rs b/tests/by-util/test_basenc.rs index c9e15ef1f61..2976d609974 100644 --- a/tests/by-util/test_basenc.rs +++ b/tests/by-util/test_basenc.rs @@ -2,6 +2,8 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. + +//spell-checker: ignore (encodings) lsbf msbf use crate::common::util::TestScenario; #[test] @@ -31,3 +33,221 @@ fn test_invalid_input() { .fails() .stderr_only(error_message); } + +#[test] +fn test_base64() { + new_ucmd!() + .arg("--base64") + .pipe_in("to>be?") + .succeeds() + .no_stderr() + .stdout_only("dG8+YmU/\n"); // spell-checker:disable-line +} + +#[test] +fn test_base64_decode() { + new_ucmd!() + .args(&["--base64", "-d"]) + .pipe_in("dG8+YmU/") // spell-checker:disable-line + .succeeds() + .no_stderr() + .stdout_only("to>be?"); +} + +#[test] +fn test_base64url() { + new_ucmd!() + .arg("--base64url") + .pipe_in("to>be?") + .succeeds() + .no_stderr() + .stdout_only("dG8-YmU_\n"); // spell-checker:disable-line +} + +#[test] +fn test_base64url_decode() { + new_ucmd!() + .args(&["--base64url", "-d"]) + .pipe_in("dG8-YmU_") // spell-checker:disable-line + .succeeds() + .no_stderr() + .stdout_only("to>be?"); +} + +#[test] +fn test_base32() { + new_ucmd!() + .arg("--base32") + .pipe_in("nice>base?") + .succeeds() + .no_stderr() + .stdout_only("NZUWGZJ6MJQXGZJ7\n"); // spell-checker:disable-line +} + +#[test] +fn test_base32_decode() { + new_ucmd!() + .args(&["--base32", "-d"]) + .pipe_in("NZUWGZJ6MJQXGZJ7") // spell-checker:disable-line + .succeeds() + .no_stderr() + .stdout_only("nice>base?"); +} + +#[test] +fn test_base32hex() { + new_ucmd!() + .arg("--base32hex") + .pipe_in("nice>base?") + .succeeds() + .no_stderr() + .stdout_only("DPKM6P9UC9GN6P9V\n"); // spell-checker:disable-line +} + +#[test] +fn test_base32hex_decode() { + new_ucmd!() + .args(&["--base32hex", "-d"]) + .pipe_in("DPKM6P9UC9GN6P9V") // spell-checker:disable-line + .succeeds() + .no_stderr() + .stdout_only("nice>base?"); +} + +#[test] +fn test_base16() { + new_ucmd!() + .arg("--base16") + .pipe_in("Hello, World!") + .succeeds() + .no_stderr() + .stdout_only("48656C6C6F2C20576F726C6421\n"); +} + +#[test] +fn test_base16_decode() { + new_ucmd!() + .args(&["--base16", "-d"]) + .pipe_in("48656C6C6F2C20576F726C6421") + .succeeds() + .no_stderr() + .stdout_only("Hello, World!"); +} + +#[test] +fn test_base2msbf() { + new_ucmd!() + .arg("--base2msbf") + .pipe_in("msbf") + .succeeds() + .no_stderr() + .stdout_only("01101101011100110110001001100110\n"); +} + +#[test] +fn test_base2msbf_decode() { + new_ucmd!() + .args(&["--base2msbf", "-d"]) + .pipe_in("01101101011100110110001001100110") + .succeeds() + .no_stderr() + .stdout_only("msbf"); +} + +#[test] +fn test_base2lsbf() { + new_ucmd!() + .arg("--base2lsbf") + .pipe_in("lsbf") + .succeeds() + .no_stderr() + .stdout_only("00110110110011100100011001100110\n"); +} + +#[test] +fn test_base2lsbf_decode() { + new_ucmd!() + .args(&["--base2lsbf", "-d"]) + .pipe_in("00110110110011100100011001100110") + .succeeds() + .no_stderr() + .stdout_only("lsbf"); +} + +#[test] +fn test_choose_last_encoding_z85() { + new_ucmd!() + .args(&[ + "--base2lsbf", + "--base2msbf", + "--base16", + "--base32hex", + "--base64url", + "--base32", + "--base64", + "--z85", + ]) + .pipe_in("Hello, World") + .succeeds() + .no_stderr() + .stdout_only("nm=QNz.92jz/PV8\n"); +} + +#[test] +fn test_choose_last_encoding_base64() { + new_ucmd!() + .args(&[ + "--base2msbf", + "--base2lsbf", + "--base64url", + "--base32hex", + "--base32", + "--base16", + "--z85", + "--base64", + ]) + .pipe_in("Hello, World!") + .succeeds() + .no_stderr() + .stdout_only("SGVsbG8sIFdvcmxkIQ==\n"); // spell-checker:disable-line +} + +#[test] +fn test_choose_last_encoding_base2lsbf() { + new_ucmd!() + .args(&[ + "--base64url", + "--base16", + "--base2msbf", + "--base32", + "--base64", + "--z85", + "--base32hex", + "--base2lsbf", + ]) + .pipe_in("lsbf") + .succeeds() + .no_stderr() + .stdout_only("00110110110011100100011001100110\n"); +} + +#[test] +fn test_base32_decode_repeated() { + new_ucmd!() + .args(&[ + "--ignore", + "--wrap=80", + "--base32hex", + "--z85", + "--ignore", + "--decode", + "--z85", + "--base32", + "-w", + "10", + ]) + .pipe_in("NZUWGZJ6MJQXGZJ7") // spell-checker:disable-line + .succeeds() + .no_stderr() + .stdout_only("nice>base?"); +} diff --git a/tests/by-util/test_cat.rs b/tests/by-util/test_cat.rs index aa86ab06652..b7b19fd3228 100644 --- a/tests/by-util/test_cat.rs +++ b/tests/by-util/test_cat.rs @@ -175,6 +175,7 @@ fn test_piped_to_dev_full() { s.ucmd() .set_stdout(dev_full) .pipe_in_fixture("alpha.txt") + .ignore_stdin_write_error() .fails() .stderr_contains("No space left on device"); } @@ -224,6 +225,7 @@ fn test_three_directories_and_file_and_stdin() { "test_directory3", ]) .pipe_in("stdout bytes") + .ignore_stdin_write_error() .fails() .stderr_is_fixture("three_directories_and_file_and_stdin.stderr.expected") .stdout_is( @@ -253,6 +255,28 @@ fn test_output_multi_files_print_all_chars() { // spell-checker:enable } +#[test] +fn test_output_multi_files_print_all_chars_repeated() { + // spell-checker:disable + new_ucmd!() + .args(&["alpha.txt", "256.txt", "-A", "-n", "-A", "-n"]) + .succeeds() + .stdout_only( + " 1\tabcde$\n 2\tfghij$\n 3\tklmno$\n 4\tpqrst$\n \ + 5\tuvwxyz$\n 6\t^@^A^B^C^D^E^F^G^H^I$\n \ + 7\t^K^L^M^N^O^P^Q^R^S^T^U^V^W^X^Y^Z^[^\\^]^^^_ \ + !\"#$%&\'()*+,-./0123456789:;\ + <=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~^?M-^@M-^AM-^\ + BM-^CM-^DM-^EM-^FM-^GM-^HM-^IM-^JM-^KM-^LM-^MM-^NM-^OM-^PM-^QM-^RM-^SM-^TM-^UM-^V\ + M-^WM-^XM-^YM-^ZM-^[M-^\\M-^]M-^^M-^_M- \ + M-!M-\"M-#M-$M-%M-&M-\'M-(M-)M-*M-+M-,M--M-.M-/M-0M-1M-2M-3M-4M-5M-6M-7M-8M-9M-:\ + M-;M-M-?M-@M-AM-BM-CM-DM-EM-FM-GM-HM-IM-JM-KM-LM-MM-NM-OM-PM-QM-RM-SM-TM-U\ + M-VM-WM-XM-YM-ZM-[M-\\M-]M-^M-_M-`M-aM-bM-cM-dM-eM-fM-gM-hM-iM-jM-kM-lM-mM-nM-oM-\ + pM-qM-rM-sM-tM-uM-vM-wM-xM-yM-zM-{M-|M-}M-~M-^?", + ); + // spell-checker:enable +} + #[test] fn test_numbered_lines_no_trailing_newline() { // spell-checker:disable @@ -268,7 +292,7 @@ fn test_numbered_lines_no_trailing_newline() { #[test] fn test_stdin_show_nonprinting() { - for same_param in ["-v", "--show-nonprinting", "--show-non"] { + for same_param in ["-v", "-vv", "--show-nonprinting", "--show-non"] { new_ucmd!() .args(&[same_param]) .pipe_in("\t\0\n") @@ -279,7 +303,7 @@ fn test_stdin_show_nonprinting() { #[test] fn test_stdin_show_tabs() { - for same_param in ["-T", "--show-tabs", "--show-ta"] { + for same_param in ["-T", "-TT", "--show-tabs", "--show-ta"] { new_ucmd!() .args(&[same_param]) .pipe_in("\t\0\n") @@ -290,7 +314,7 @@ fn test_stdin_show_tabs() { #[test] fn test_stdin_show_ends() { - for same_param in ["-E", "--show-ends", "--show-e"] { + for same_param in ["-E", "-EE", "--show-ends", "--show-e"] { new_ucmd!() .args(&[same_param, "-"]) .pipe_in("\t\0\n\t") @@ -300,7 +324,7 @@ fn test_stdin_show_ends() { } #[test] -fn squeeze_all_files() { +fn test_squeeze_all_files() { // empty lines at the end of a file are "squeezed" together with empty lines at the beginning let (at, mut ucmd) = at_and_ucmd!(); at.write("input1", "a\n\n"); @@ -310,6 +334,17 @@ fn squeeze_all_files() { .stdout_only("a\n\nb"); } +#[test] +fn test_squeeze_all_files_repeated() { + // empty lines at the end of a file are "squeezed" together with empty lines at the beginning + let (at, mut ucmd) = at_and_ucmd!(); + at.write("input1", "a\n\n"); + at.write("input2", "\n\nb"); + ucmd.args(&["-s", "input1", "input2", "-s"]) + .succeeds() + .stdout_only("a\n\nb"); +} + #[test] fn test_show_ends_crlf() { new_ucmd!() @@ -339,6 +374,15 @@ fn test_stdin_nonprinting_and_endofline() { .stdout_only("\t^@$\n"); } +#[test] +fn test_stdin_nonprinting_and_endofline_repeated() { + new_ucmd!() + .args(&["-ee", "-e"]) + .pipe_in("\t\0\n") + .succeeds() + .stdout_only("\t^@$\n"); +} + #[test] fn test_stdin_nonprinting_and_tabs() { new_ucmd!() @@ -348,6 +392,15 @@ fn test_stdin_nonprinting_and_tabs() { .stdout_only("^I^@\n"); } +#[test] +fn test_stdin_nonprinting_and_tabs_repeated() { + new_ucmd!() + .args(&["-tt", "-t"]) + .pipe_in("\t\0\n") + .succeeds() + .stdout_only("^I^@\n"); +} + #[test] fn test_stdin_squeeze_blank() { for same_param in ["-s", "--squeeze-blank", "--squeeze"] { @@ -362,7 +415,7 @@ fn test_stdin_squeeze_blank() { #[test] fn test_stdin_number_non_blank() { // spell-checker:disable-next-line - for same_param in ["-b", "--number-nonblank", "--number-non"] { + for same_param in ["-b", "-bb", "--number-nonblank", "--number-non"] { new_ucmd!() .arg(same_param) .arg("-") @@ -384,6 +437,15 @@ fn test_non_blank_overrides_number() { } } +#[test] +fn test_non_blank_overrides_number_even_when_present() { + new_ucmd!() + .args(&["-n", "-b", "-n"]) + .pipe_in("\na\nb\n\n\nc") + .succeeds() + .stdout_only("\n 1\ta\n 2\tb\n\n\n 3\tc"); +} + #[test] fn test_squeeze_blank_before_numbering() { for same_param in ["-s", "--squeeze-blank"] { @@ -503,7 +565,6 @@ fn test_write_to_self_empty() { let file = OpenOptions::new() .create_new(true) - .write(true) .append(true) .open(&file_path) .unwrap(); @@ -519,7 +580,6 @@ fn test_write_to_self() { let file = OpenOptions::new() .create_new(true) - .write(true) .append(true) .open(file_path) .unwrap(); @@ -552,3 +612,14 @@ fn test_error_loop() { .fails() .stderr_is("cat: 1: Too many levels of symbolic links\n"); } + +#[test] +fn test_u_ignored() { + for same_param in ["-u", "-uu"] { + new_ucmd!() + .arg(same_param) + .pipe_in("hello") + .succeeds() + .stdout_only("hello"); + } +} diff --git a/tests/by-util/test_chcon.rs b/tests/by-util/test_chcon.rs index 71405e451d0..a8dae9aed06 100644 --- a/tests/by-util/test_chcon.rs +++ b/tests/by-util/test_chcon.rs @@ -88,6 +88,33 @@ fn valid_context_on_valid_symlink() { assert_eq!(get_file_context(dir.plus("a.tmp")).unwrap(), a_context); } +#[test] +fn valid_context_on_valid_symlink_override_dereference() { + let (dir, mut cmd) = at_and_ucmd!(); + dir.touch("a.tmp"); + dir.symlink_file("a.tmp", "la.tmp"); + + let a_context = get_file_context(dir.plus("a.tmp")).unwrap(); + let la_context = get_file_context(dir.plus("la.tmp")).unwrap(); + let new_a_context = "guest_u:object_r:etc_t:s0:c42"; + assert_ne!(a_context.as_deref(), Some(new_a_context)); + assert_ne!(la_context.as_deref(), Some(new_a_context)); + + cmd.args(&[ + "--verbose", + "--no-dereference", + "--dereference", + new_a_context, + ]) + .arg(dir.plus("la.tmp")) + .succeeds(); + assert_eq!( + get_file_context(dir.plus("a.tmp")).unwrap().as_deref(), + Some(new_a_context) + ); + assert_eq!(get_file_context(dir.plus("la.tmp")).unwrap(), la_context); +} + #[test] fn valid_context_on_broken_symlink() { let (dir, mut cmd) = at_and_ucmd!(); @@ -104,6 +131,29 @@ fn valid_context_on_broken_symlink() { ); } +#[test] +fn valid_context_on_broken_symlink_after_deref() { + let (dir, mut cmd) = at_and_ucmd!(); + dir.symlink_file("a.tmp", "la.tmp"); + + let la_context = get_file_context(dir.plus("la.tmp")).unwrap(); + let new_la_context = "guest_u:object_r:etc_t:s0:c42"; + assert_ne!(la_context.as_deref(), Some(new_la_context)); + + cmd.args(&[ + "--verbose", + "--dereference", + "--no-dereference", + new_la_context, + ]) + .arg(dir.plus("la.tmp")) + .succeeds(); + assert_eq!( + get_file_context(dir.plus("la.tmp")).unwrap().as_deref(), + Some(new_la_context) + ); +} + #[test] fn valid_context_with_prior_xattributes() { let (dir, mut cmd) = at_and_ucmd!(); @@ -324,6 +374,30 @@ fn user_change() { ); } +#[test] +fn user_change_repeated() { + let (dir, mut cmd) = at_and_ucmd!(); + + dir.touch("a.tmp"); + let a_context = get_file_context(dir.plus("a.tmp")).unwrap(); + let new_a_context = if let Some(a_context) = a_context { + let mut components: Vec<_> = a_context.split(':').collect(); + components[0] = "guest_u"; + components.join(":") + } else { + set_file_context(dir.plus("a.tmp"), "unconfined_u:object_r:user_tmp_t:s0").unwrap(); + String::from("guest_u:object_r:user_tmp_t:s0") + }; + + cmd.args(&["--verbose", "--user=wrong", "--user=guest_u"]) + .arg(dir.plus("a.tmp")) + .succeeds(); + assert_eq!( + get_file_context(dir.plus("a.tmp")).unwrap(), + Some(new_a_context) + ); +} + #[test] fn role_change() { let (dir, mut cmd) = at_and_ucmd!(); @@ -421,6 +495,99 @@ fn valid_reference() { ); } +#[test] +fn valid_reference_repeat_flags() { + let (dir, mut cmd) = at_and_ucmd!(); + + dir.touch("a.tmp"); + let new_a_context = "guest_u:object_r:etc_t:s0:c42"; + set_file_context(dir.plus("a.tmp"), new_a_context).unwrap(); + + dir.touch("b.tmp"); + let b_context = get_file_context(dir.plus("b.tmp")).unwrap(); + assert_ne!(b_context.as_deref(), Some(new_a_context)); + + cmd.arg("--verbose") + .arg("-vvRRHHLLPP") // spell-checker:disable-line + .arg("--no-preserve-root") + .arg("--no-preserve-root") + .arg("--preserve-root") + .arg("--preserve-root") + .arg("--dereference") + .arg("--dereference") + .arg("--no-dereference") + .arg("--no-dereference") + .arg(format!("--reference={}", dir.plus_as_string("a.tmp"))) + .arg(dir.plus("b.tmp")) + .succeeds(); + assert_eq!( + get_file_context(dir.plus("b.tmp")).unwrap().as_deref(), + Some(new_a_context) + ); +} + +#[test] +fn valid_reference_repeated_reference() { + let (dir, mut cmd) = at_and_ucmd!(); + + dir.touch("a.tmp"); + let new_a_context = "guest_u:object_r:etc_t:s0:c42"; + set_file_context(dir.plus("a.tmp"), new_a_context).unwrap(); + + dir.touch("wrong.tmp"); + let new_wrong_context = "guest_u:object_r:etc_t:s42:c0"; + set_file_context(dir.plus("wrong.tmp"), new_wrong_context).unwrap(); + + dir.touch("b.tmp"); + let b_context = get_file_context(dir.plus("b.tmp")).unwrap(); + assert_ne!(b_context.as_deref(), Some(new_a_context)); + + cmd.arg("--verbose") + .arg(format!("--reference={}", dir.plus_as_string("wrong.tmp"))) + .arg(format!("--reference={}", dir.plus_as_string("a.tmp"))) + .arg(dir.plus("b.tmp")) + .succeeds(); + assert_eq!( + get_file_context(dir.plus("b.tmp")).unwrap().as_deref(), + Some(new_a_context) + ); + assert_eq!( + get_file_context(dir.plus("wrong.tmp")).unwrap().as_deref(), + Some(new_wrong_context) + ); +} + +#[test] +fn valid_reference_multi() { + let (dir, mut cmd) = at_and_ucmd!(); + + dir.touch("a.tmp"); + let new_a_context = "guest_u:object_r:etc_t:s0:c42"; + set_file_context(dir.plus("a.tmp"), new_a_context).unwrap(); + + dir.touch("b1.tmp"); + let b1_context = get_file_context(dir.plus("b1.tmp")).unwrap(); + assert_ne!(b1_context.as_deref(), Some(new_a_context)); + + dir.touch("b2.tmp"); + let b2_context = get_file_context(dir.plus("b2.tmp")).unwrap(); + assert_ne!(b2_context.as_deref(), Some(new_a_context)); + + cmd.arg("--verbose") + .arg(format!("--reference={}", dir.plus_as_string("a.tmp"))) + .arg(dir.plus("b1.tmp")) + .arg(dir.plus("b2.tmp")) + .succeeds(); + assert_eq!( + get_file_context(dir.plus("b1.tmp")).unwrap().as_deref(), + Some(new_a_context) + ); + assert_eq!( + get_file_context(dir.plus("b2.tmp")).unwrap().as_deref(), + Some(new_a_context) + ); +} + fn get_file_context(path: impl AsRef) -> Result, selinux::errors::Error> { let path = path.as_ref(); match selinux::SecurityContext::of_path(path, false, false) { diff --git a/tests/by-util/test_chgrp.rs b/tests/by-util/test_chgrp.rs index 07966b67b33..eca5ba0edff 100644 --- a/tests/by-util/test_chgrp.rs +++ b/tests/by-util/test_chgrp.rs @@ -85,18 +85,29 @@ fn test_fail_silently() { #[test] fn test_preserve_root() { // It's weird that on OS X, `realpath /etc/..` returns '/private' + new_ucmd!() + .arg("--preserve-root") + .arg("-R") + .arg("bin") + .arg("/") + .fails() + .stderr_is("chgrp: it is dangerous to operate recursively on '/'\nchgrp: use --no-preserve-root to override this failsafe\n"); for d in [ - "/", "/////dev///../../../../", "../../../../../../../../../../../../../../", "./../../../../../../../../../../../../../../", ] { + let expected_error = format!( + "chgrp: it is dangerous to operate recursively on '{}' (same as '/')\nchgrp: use --no-preserve-root to override this failsafe\n", + d, + ); new_ucmd!() .arg("--preserve-root") .arg("-R") - .arg("bin").arg(d) + .arg("bin") + .arg(d) .fails() - .stderr_is("chgrp: it is dangerous to operate recursively on '/'\nchgrp: use --no-preserve-root to override this failsafe\n"); + .stderr_is(expected_error); } } @@ -105,17 +116,22 @@ fn test_preserve_root_symlink() { let file = "test_chgrp_symlink2root"; for d in [ "/", + "//", + "///", "////dev//../../../../", "..//../../..//../..//../../../../../../../../", ".//../../../../../../..//../../../../../../../", ] { let (at, mut ucmd) = at_and_ucmd!(); at.symlink_file(d, file); + let expected_error = + "chgrp: it is dangerous to operate recursively on 'test_chgrp_symlink2root' (same as '/')\nchgrp: use --no-preserve-root to override this failsafe\n"; ucmd.arg("--preserve-root") .arg("-HR") - .arg("bin").arg(file) + .arg("bin") + .arg(file) .fails() - .stderr_is("chgrp: it is dangerous to operate recursively on '/'\nchgrp: use --no-preserve-root to override this failsafe\n"); + .stderr_is(expected_error); } let (at, mut ucmd) = at_and_ucmd!(); @@ -124,7 +140,7 @@ fn test_preserve_root_symlink() { .arg("-HR") .arg("bin").arg(format!(".//{file}/..//..//../../")) .fails() - .stderr_is("chgrp: it is dangerous to operate recursively on '/'\nchgrp: use --no-preserve-root to override this failsafe\n"); + .stderr_is("chgrp: it is dangerous to operate recursively on './/test_chgrp_symlink2root/..//..//../../' (same as '/')\nchgrp: use --no-preserve-root to override this failsafe\n"); let (at, mut ucmd) = at_and_ucmd!(); at.symlink_file("/", "__root__"); @@ -132,7 +148,47 @@ fn test_preserve_root_symlink() { .arg("-R") .arg("bin").arg("__root__/.") .fails() - .stderr_is("chgrp: it is dangerous to operate recursively on '/'\nchgrp: use --no-preserve-root to override this failsafe\n"); + .stderr_is("chgrp: it is dangerous to operate recursively on '__root__/.' (same as '/')\nchgrp: use --no-preserve-root to override this failsafe\n"); +} + +#[test] +fn test_preserve_root_symlink_cwd_root() { + new_ucmd!() + .current_dir("/") + .arg("--preserve-root") + .arg("-R") + .arg("bin").arg(".") + .fails() + .stderr_is("chgrp: it is dangerous to operate recursively on '.' (same as '/')\nchgrp: use --no-preserve-root to override this failsafe\n"); + new_ucmd!() + .current_dir("/") + .arg("--preserve-root") + .arg("-R") + .arg("bin").arg("/.") + .fails() + .stderr_is("chgrp: it is dangerous to operate recursively on '/.' (same as '/')\nchgrp: use --no-preserve-root to override this failsafe\n"); + new_ucmd!() + .current_dir("/") + .arg("--preserve-root") + .arg("-R") + .arg("bin").arg("..") + .fails() + .stderr_is("chgrp: it is dangerous to operate recursively on '..' (same as '/')\nchgrp: use --no-preserve-root to override this failsafe\n"); + new_ucmd!() + .current_dir("/") + .arg("--preserve-root") + .arg("-R") + .arg("bin").arg("/..") + .fails() + .stderr_is("chgrp: it is dangerous to operate recursively on '/..' (same as '/')\nchgrp: use --no-preserve-root to override this failsafe\n"); + new_ucmd!() + .current_dir("/") + .arg("--preserve-root") + .arg("-R") + .arg("bin") + .arg("...") + .fails() + .stderr_is("chgrp: cannot access '...': No such file or directory\n"); } #[test] diff --git a/tests/by-util/test_chmod.rs b/tests/by-util/test_chmod.rs index be730a8c0ad..35197f85ee4 100644 --- a/tests/by-util/test_chmod.rs +++ b/tests/by-util/test_chmod.rs @@ -645,6 +645,20 @@ fn test_quiet_n_verbose_used_multiple_times() { .succeeds(); } +#[test] +fn test_changes_from_identical_reference() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch("file"); + scene + .ucmd() + .arg("-c") + .arg("--reference=file") + .arg("file") + .succeeds() + .no_stdout(); +} + #[test] fn test_gnu_invalid_mode() { let scene = TestScenario::new(util_name!()); diff --git a/tests/by-util/test_cksum.rs b/tests/by-util/test_cksum.rs index 464de947479..818b7e79929 100644 --- a/tests/by-util/test_cksum.rs +++ b/tests/by-util/test_cksum.rs @@ -128,6 +128,29 @@ fn test_stdin_larger_than_128_bytes() { assert_eq!(bytes_cnt, 2058); } +#[test] +fn test_repeated_flags() { + new_ucmd!() + .arg("-a") + .arg("sha1") + .arg("--algo=sha256") + .arg("-a=md5") + .arg("lorem_ipsum.txt") + .succeeds() + .stdout_is_fixture("md5_single_file.expected"); +} + +#[test] +fn test_tag_after_untagged() { + new_ucmd!() + .arg("--untagged") + .arg("--tag") + .arg("-a=md5") + .arg("lorem_ipsum.txt") + .succeeds() + .stdout_is_fixture("md5_single_file.expected"); +} + #[test] fn test_algorithm_single_file() { for algo in ALGOS { @@ -208,6 +231,17 @@ fn test_untagged_algorithm_single_file() { } } +#[test] +fn test_untagged_algorithm_after_tag() { + new_ucmd!() + .arg("--tag") + .arg("--untagged") + .arg("--algorithm=md5") + .arg("lorem_ipsum.txt") + .succeeds() + .stdout_is_fixture("untagged/md5_single_file.expected"); +} + #[test] fn test_untagged_algorithm_multiple_files() { for algo in ALGOS { @@ -291,6 +325,20 @@ fn test_length_is_zero() { .stdout_is_fixture("length_is_zero.expected"); } +#[test] +fn test_length_repeated() { + new_ucmd!() + .arg("--length=10") + .arg("--length=123456") + .arg("--length=0") + .arg("--algorithm=blake2b") + .arg("lorem_ipsum.txt") + .arg("alice_in_wonderland.txt") + .succeeds() + .no_stderr() + .stdout_is_fixture("length_is_zero.expected"); +} + #[test] fn test_raw_single_file() { for algo in ALGOS { @@ -315,6 +363,43 @@ fn test_raw_multiple_files() { .code_is(1); } +#[test] +fn test_base64_raw_conflicts() { + new_ucmd!() + .arg("--base64") + .arg("--raw") + .arg("lorem_ipsum.txt") + .fails() + .no_stdout() + .stderr_contains("--base64") + .stderr_contains("cannot be used with") + .stderr_contains("--raw"); +} + +#[test] +fn test_base64_single_file() { + for algo in ALGOS { + new_ucmd!() + .arg("--base64") + .arg("lorem_ipsum.txt") + .arg(format!("--algorithm={algo}")) + .succeeds() + .no_stderr() + .stdout_is_fixture_bytes(format!("base64/{algo}_single_file.expected")); + } +} +#[test] +fn test_base64_multiple_files() { + new_ucmd!() + .arg("--base64") + .arg("--algorithm=md5") + .arg("lorem_ipsum.txt") + .arg("alice_in_wonderland.txt") + .succeeds() + .no_stderr() + .stdout_is_fixture_bytes("base64/md5_multiple_files.expected"); +} + #[test] fn test_fail_on_folder() { let (at, mut ucmd) = at_and_ucmd!(); diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index c0d81d9a915..e3b373da19d 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -505,9 +505,31 @@ fn test_cp_arg_interactive_update() { at.touch("a"); at.touch("b"); ucmd.args(&["-i", "-u", "a", "b"]) - .pipe_in("N\n") + .pipe_in("") .succeeds() .no_stdout(); + // Make extra sure that closing stdin behaves identically to piping-in nothing. + let (at, mut ucmd) = at_and_ucmd!(); + at.touch("a"); + at.touch("b"); + ucmd.args(&["-i", "-u", "a", "b"]).succeeds().no_stdout(); +} + +#[test] +#[cfg(not(any(target_os = "android", target_os = "freebsd")))] +#[ignore = "known issue #6019"] +fn test_cp_arg_interactive_update_newer() { + // -u -i *WILL* show the prompt to validate the override. + // Therefore, the error code depends on the prompt response. + let (at, mut ucmd) = at_and_ucmd!(); + at.touch("b"); + at.touch("a"); + ucmd.args(&["-i", "-u", "a", "b"]) + .pipe_in("N\n") + .fails() + .code_is(1) + .no_stdout() + .stderr_is("cp: overwrite 'b'? "); } #[test] @@ -1455,6 +1477,7 @@ fn test_cp_preserve_all_context_fails_on_non_selinux() { #[test] #[cfg(target_os = "android")] +#[cfg(disabled_until_fixed)] // FIXME: the test looks to .succeed on android fn test_cp_preserve_xattr_fails_on_android() { // Because of the SELinux extended attributes used on Android, trying to copy extended // attributes has to fail in this case, since we specify `--preserve=xattr` and this puts it @@ -3283,7 +3306,7 @@ fn test_symbolic_link_file() { #[test] fn test_src_base_dot() { let ts = TestScenario::new(util_name!()); - let at = ts.fixtures.clone(); + let at = &ts.fixtures; at.mkdir("x"); at.mkdir("y"); ts.ucmd() @@ -3758,7 +3781,7 @@ fn test_acl_preserve() { // calling the command directly. xattr requires some dev packages to be installed // and it adds a complex dependency just for a test match Command::new("setfacl") - .args(["-m", "group::rwx", &path1]) + .args(["-m", "group::rwx", path1]) .status() .map(|status| status.code()) { @@ -3777,3 +3800,58 @@ fn test_acl_preserve() { assert!(compare_xattrs(&file, &file_target)); } + +#[test] +fn test_cp_force_remove_destination_attributes_only_with_symlink() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.write("file1", "1"); + at.write("file2", "2"); + at.symlink_file("file1", "sym1"); + + scene + .ucmd() + .args(&[ + "-a", + "--remove-destination", + "--attributes-only", + "sym1", + "file2", + ]) + .succeeds(); + + assert!( + at.symlink_exists("file2"), + "file2 is not a symbolic link as expected" + ); + + assert_eq!( + at.read("file1"), + at.read("file2"), + "Contents of file1 and file2 do not match" + ); +} + +#[test] +fn test_cp_no_dereference_attributes_only_with_symlink() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("file1", "1"); + at.write("file2", "2"); + at.write("file2.exp", "2"); + at.symlink_file("file1", "sym1"); + + let result = scene + .ucmd() + .args(&["--no-dereference", "--attributes-only", "sym1", "file2"]) + .fails(); + + assert_eq!(result.code(), 1, "cp command did not fail"); + + assert_eq!( + at.read("file2"), + at.read("file2.exp"), + "file2 content does not match expected" + ); +} diff --git a/tests/by-util/test_csplit.rs b/tests/by-util/test_csplit.rs index b83d5e0eede..b52c44b0e47 100644 --- a/tests/by-util/test_csplit.rs +++ b/tests/by-util/test_csplit.rs @@ -1342,3 +1342,37 @@ fn test_line_num_range_with_up_to_match3() { assert_eq!(at.read("xx01"), ""); assert_eq!(at.read("xx02"), generate(10, 51)); } + +#[test] +fn precision_format() { + for f in ["%#6.3x", "%0#6.3x"] { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "10", "--suffix-format", f]) + .succeeds() + .stdout_only("18\n123\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 2); + assert_eq!(at.read("xx 000"), generate(1, 10)); + assert_eq!(at.read("xx 0x001"), generate(10, 51)); + } +} + +#[test] +fn zero_error() { + let (at, mut ucmd) = at_and_ucmd!(); + at.touch("in"); + ucmd.args(&["in", "0"]) + .fails() + .stderr_contains("0: line number must be greater"); +} + +#[test] +fn no_such_file() { + let (_, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["in", "0"]) + .fails() + .stderr_contains("cannot access 'in': No such file or directory"); +} diff --git a/tests/by-util/test_cut.rs b/tests/by-util/test_cut.rs index 57e6666d304..50d158f966b 100644 --- a/tests/by-util/test_cut.rs +++ b/tests/by-util/test_cut.rs @@ -212,20 +212,25 @@ fn test_zero_terminated_only_delimited() { } #[test] -fn test_directory_and_no_such_file() { +fn test_is_a_directory() { let (at, mut ucmd) = at_and_ucmd!(); at.mkdir("some"); ucmd.arg("-b1") .arg("some") - .run() + .fails() + .code_is(1) .stderr_is("cut: some: Is a directory\n"); +} +#[test] +fn test_no_such_file() { new_ucmd!() .arg("-b1") .arg("some") - .run() + .fails() + .code_is(1) .stderr_is("cut: some: No such file or directory\n"); } @@ -265,3 +270,35 @@ fn test_multiple() { assert_eq!(result.stdout_str(), "b\n"); assert_eq!(result.stderr_str(), ""); } + +#[test] +fn test_multiple_mode_args() { + for args in [ + vec!["-b1", "-b2"], + vec!["-c1", "-c2"], + vec!["-f1", "-f2"], + vec!["-b1", "-c2"], + vec!["-b1", "-f2"], + vec!["-c1", "-f2"], + vec!["-b1", "-c2", "-f3"], + ] { + new_ucmd!() + .args(&args) + .fails() + .stderr_is("cut: invalid usage: expects no more than one of --fields (-f), --chars (-c) or --bytes (-b)\n"); + } +} + +#[test] +#[cfg(unix)] +fn test_8bit_non_utf8_delimiter() { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + let delim = OsStr::from_bytes(b"\xAD".as_slice()); + new_ucmd!() + .arg("-d") + .arg(delim) + .args(&["--out=_", "-f2,3", "8bit-delim.txt"]) + .succeeds() + .stdout_check(|out| out == "b_c\n".as_bytes()); +} diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index a65f02fa4c7..def3fa8af00 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -294,6 +294,7 @@ fn test_date_for_no_permission_file() { use std::os::unix::fs::PermissionsExt; let file = std::fs::OpenOptions::new() .create(true) + .truncate(true) .write(true) .open(at.plus(FILE)) .unwrap(); diff --git a/tests/by-util/test_dd.rs b/tests/by-util/test_dd.rs index 93cfee06718..a3b007909e5 100644 --- a/tests/by-util/test_dd.rs +++ b/tests/by-util/test_dd.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 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 fifoname +// 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 fifoname seekable #[cfg(unix)] use crate::common::util::run_ucmd_as_root_with_stdin_stdout; @@ -11,6 +11,7 @@ use crate::common::util::TestScenario; use crate::common::util::{UCommand, TESTS_BINARY}; use regex::Regex; +use uucore::io::OwnedFileDescriptorOrHandle; use std::fs::{File, OpenOptions}; use std::io::{BufReader, Read, Write}; @@ -277,7 +278,7 @@ fn test_final_stats_unspec() { new_ucmd!() .run() .stderr_contains("0+0 records in\n0+0 records out\n0 bytes copied, ") - .stderr_matches(&Regex::new(r"\d\.\d+(e-\d\d)? s, ").unwrap()) + .stderr_matches(&Regex::new(r"\d(\.\d+)?(e-\d\d)? s, ").unwrap()) .stderr_contains("0.0 B/s") .success(); } @@ -1405,6 +1406,36 @@ fn test_bytes_suffix() { .stdout_only("\0\0\0abcdef"); } +#[test] +// the recursive nature of the suffix allows any string with a 'B' in it treated as bytes. +fn test_bytes_suffix_recursive() { + new_ucmd!() + .args(&["count=2Bx2", "status=none"]) + .pipe_in("abcdef") + .succeeds() + .stdout_only("abcd"); + new_ucmd!() + .args(&["skip=2Bx2", "status=none"]) + .pipe_in("abcdef") + .succeeds() + .stdout_only("ef"); + new_ucmd!() + .args(&["iseek=2Bx2", "status=none"]) + .pipe_in("abcdef") + .succeeds() + .stdout_only("ef"); + new_ucmd!() + .args(&["seek=2Bx2", "status=none"]) + .pipe_in("abcdef") + .succeeds() + .stdout_only("\0\0\0\0abcdef"); + new_ucmd!() + .args(&["oseek=2Bx2", "status=none"]) + .pipe_in("abcdef") + .succeeds() + .stdout_only("\0\0\0\0abcdef"); +} + /// Test for "conv=sync" with a slow reader. #[cfg(not(windows))] #[test] @@ -1537,6 +1568,16 @@ fn test_nocache_stdin_error() { .stderr_only(format!("dd: failed to discard cache for: 'standard input': {detail}\n0+0 records in\n0+0 records out\n")); } +/// Test that dd fails when no number in count. +#[test] +fn test_empty_count_number() { + new_ucmd!() + .args(&["count=B"]) + .fails() + .code_is(1) + .stderr_only("dd: invalid number: ‘B’\n"); +} + /// Test for discarding system file cache. #[test] #[cfg(target_os = "linux")] @@ -1600,7 +1641,7 @@ fn test_seek_past_dev() { fn test_reading_partial_blocks_from_fifo() { // Create the FIFO. let ts = TestScenario::new(util_name!()); - let at = ts.fixtures.clone(); + let at = &ts.fixtures; at.mkfifo("fifo"); let fifoname = at.plus_as_string("fifo"); @@ -1641,7 +1682,7 @@ fn test_reading_partial_blocks_from_fifo() { fn test_reading_partial_blocks_from_fifo_unbuffered() { // Create the FIFO. let ts = TestScenario::new(util_name!()); - let at = ts.fixtures.clone(); + let at = ts.fixtures; at.mkfifo("fifo"); let fifoname = at.plus_as_string("fifo"); @@ -1673,3 +1714,64 @@ fn test_reading_partial_blocks_from_fifo_unbuffered() { let expected = b"0+2 records in\n0+2 records out\n4 bytes copied"; assert!(output.stderr.starts_with(expected)); } + +#[test] +#[cfg(any(target_os = "linux", target_os = "android"))] +fn test_iflag_directory_fails_when_file_is_passed_via_std_in() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.make_file("input"); + let filename = at.plus_as_string("input"); + new_ucmd!() + .args(&["iflag=directory", "count=0"]) + .set_stdin(std::process::Stdio::from(File::open(filename).unwrap())) + .fails() + .stderr_contains("standard input: not a directory"); +} + +#[test] +fn test_stdin_stdout_not_rewound_even_when_connected_to_seekable_file() { + use std::process::Stdio; + + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + at.write("in", "abcde"); + + let stdin = OwnedFileDescriptorOrHandle::open_file( + OpenOptions::new().read(true), + at.plus("in").as_path(), + ) + .unwrap(); + let stdout = OwnedFileDescriptorOrHandle::open_file( + OpenOptions::new().create(true).write(true), + at.plus("out").as_path(), + ) + .unwrap(); + let stderr = OwnedFileDescriptorOrHandle::open_file( + OpenOptions::new().create(true).write(true), + at.plus("err").as_path(), + ) + .unwrap(); + + ts.ucmd() + .args(&["bs=1", "skip=1", "count=1"]) + .set_stdin(Stdio::from(stdin.try_clone().unwrap())) + .set_stdout(Stdio::from(stdout.try_clone().unwrap())) + .set_stderr(Stdio::from(stderr.try_clone().unwrap())) + .succeeds(); + + ts.ucmd() + .args(&["bs=1", "skip=1"]) + .set_stdin(stdin) + .set_stdout(stdout) + .set_stderr(stderr) + .succeeds(); + + let err_file_content = std::fs::read_to_string(at.plus_as_string("err")).unwrap(); + println!("stderr:\n{}", err_file_content); + + let out_file_content = std::fs::read_to_string(at.plus_as_string("out")).unwrap(); + println!("stdout:\n{}", out_file_content); + assert_eq!(out_file_content, "bde"); +} diff --git a/tests/by-util/test_du.rs b/tests/by-util/test_du.rs index 9e4622c31a8..92469b6f52d 100644 --- a/tests/by-util/test_du.rs +++ b/tests/by-util/test_du.rs @@ -173,11 +173,15 @@ fn test_du_with_posixly_correct() { } #[test] -fn test_du_basics_bad_name() { +fn test_du_non_existing_files() { new_ucmd!() - .arg("bad_name") + .arg("non_existing_a") + .arg("non_existing_b") .fails() - .stderr_only("du: bad_name: No such file or directory\n"); + .stderr_only(concat!( + "du: cannot access 'non_existing_a': No such file or directory\n", + "du: cannot access 'non_existing_b': No such file or directory\n" + )); } #[test] diff --git a/tests/by-util/test_echo.rs b/tests/by-util/test_echo.rs index ac6bd74d1e0..4ae623f2f6f 100644 --- a/tests/by-util/test_echo.rs +++ b/tests/by-util/test_echo.rs @@ -293,3 +293,13 @@ fn old_octal_syntax() { .succeeds() .stdout_is("A1\n"); } + +#[test] +fn partial_version_argument() { + new_ucmd!().arg("--ver").succeeds().stdout_is("--ver\n"); +} + +#[test] +fn partial_help_argument() { + new_ucmd!().arg("--he").succeeds().stdout_is("--he\n"); +} diff --git a/tests/by-util/test_env.rs b/tests/by-util/test_env.rs index 8ce55a1d3a2..13535e4161f 100644 --- a/tests/by-util/test_env.rs +++ b/tests/by-util/test_env.rs @@ -2,9 +2,12 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (words) bamf chdir rlimit prlimit COMSPEC +// spell-checker:ignore (words) bamf chdir rlimit prlimit COMSPEC cout cerr FFFD +#[cfg(target_os = "linux")] +use crate::common::util::expected_result; use crate::common::util::TestScenario; +use ::env::native_int_str::{Convert, NCvt}; use std::env; use std::path::Path; use tempfile::tempdir; @@ -34,11 +37,24 @@ fn test_env_version() { #[test] fn test_echo() { - let result = new_ucmd!().arg("echo").arg("FOO-bar").succeeds(); + #[cfg(target_os = "windows")] + let args = ["cmd", "/d/c", "echo"]; + #[cfg(not(target_os = "windows"))] + let args = ["echo"]; + + let result = new_ucmd!().args(&args).arg("FOO-bar").succeeds(); assert_eq!(result.stdout_str().trim(), "FOO-bar"); } +#[cfg(target_os = "windows")] +#[test] +fn test_if_windows_batch_files_can_be_executed() { + let result = new_ucmd!().arg("./runBat.bat").succeeds(); + + assert!(result.stdout_str().contains("Hello Windows World!")); +} + #[test] fn test_file_option() { let out = new_ucmd!() @@ -245,3 +261,936 @@ fn test_fail_change_directory() { .stderr_move_str(); assert!(out.contains("env: cannot change directory to ")); } + +#[cfg(not(target_os = "windows"))] // windows has no executable "echo", its only supported as part of a batch-file +#[test] +fn test_split_string_into_args_one_argument_no_quotes() { + let scene = TestScenario::new(util_name!()); + + let out = scene + .ucmd() + .arg("-S echo hello world") + .succeeds() + .stdout_move_str(); + assert_eq!(out, "hello world\n"); +} + +#[cfg(not(target_os = "windows"))] // windows has no executable "echo", its only supported as part of a batch-file +#[test] +fn test_split_string_into_args_one_argument() { + let scene = TestScenario::new(util_name!()); + + let out = scene + .ucmd() + .arg("-S echo \"hello world\"") + .succeeds() + .stdout_move_str(); + assert_eq!(out, "hello world\n"); +} + +#[cfg(not(target_os = "windows"))] // windows has no executable "echo", its only supported as part of a batch-file +#[test] +fn test_split_string_into_args_s_escaping_challenge() { + let scene = TestScenario::new(util_name!()); + + let out = scene + .ucmd() + .args(&[r#"-S echo "hello \"great\" world""#]) + .succeeds() + .stdout_move_str(); + assert_eq!(out, "hello \"great\" world\n"); +} + +#[test] +fn test_split_string_into_args_s_escaped_c_not_allowed() { + let scene = TestScenario::new(util_name!()); + + let out = scene.ucmd().args(&[r#"-S"\c""#]).fails().stderr_move_str(); + assert_eq!( + out, + "env: '\\c' must not appear in double-quoted -S string\n" + ); +} + +#[cfg(not(target_os = "windows"))] // no printf available +#[test] +fn test_split_string_into_args_s_whitespace_handling() { + let scene = TestScenario::new(util_name!()); + + let out = scene + .ucmd() + .args(&["-Sprintf x%sx\\n A \t B \x0B\x0C\r\n"]) + .succeeds() + .stdout_move_str(); + assert_eq!(out, "xAx\nxBx\n"); +} + +#[cfg(not(target_os = "windows"))] // no printf available +#[test] +fn test_split_string_into_args_long_option_whitespace_handling() { + let scene = TestScenario::new(util_name!()); + + let out = scene + .ucmd() + .args(&["--split-string printf x%sx\\n A \t B \x0B\x0C\r\n"]) + .succeeds() + .stdout_move_str(); + assert_eq!(out, "xAx\nxBx\n"); +} + +#[cfg(not(target_os = "windows"))] // no printf available +#[test] +fn test_split_string_into_args_debug_output_whitespace_handling() { + let scene = TestScenario::new(util_name!()); + + let out = scene + .ucmd() + .args(&["-vS printf x%sx\\n A \t B \x0B\x0C\r\n"]) + .succeeds(); + assert_eq!(out.stdout_str(), "xAx\nxBx\n"); + assert_eq!(out.stderr_str(), "input args:\narg[0]: 'env'\narg[1]: $'-vS printf x%sx\\\\n A \\t B \\x0B\\x0C\\r\\n'\nexecutable: 'printf'\narg[0]: $'x%sx\\n'\narg[1]: 'A'\narg[2]: 'B'\n"); +} + +// FixMe: This test fails on MACOS: +// thread 'test_env::test_gnu_e20' panicked at 'assertion failed: `(left == right)` +// left: `"A=B C=D\n__CF_USER_TEXT_ENCODING=0x1F5:0x0:0x0\n"`, +// right: `"A=B C=D\n"`', tests/by-util/test_env.rs:369:5 +#[cfg(not(target_os = "macos"))] +#[test] +fn test_gnu_e20() { + let scene = TestScenario::new(util_name!()); + + let env_bin = String::from(crate::common::util::TESTS_BINARY) + " " + util_name!(); + + let (input, output) = ( + [ + String::from("-i"), + String::from(r#"-SA="B\_C=D" "#) + env_bin.escape_default().to_string().as_str() + "", + ], + "A=B C=D\n", + ); + + let out = scene.ucmd().args(&input).succeeds(); + assert_eq!(out.stdout_str(), output); +} + +#[test] +fn test_split_string_misc() { + use ::env::native_int_str::NCvt; + use ::env::parse_args_from_str; + + assert_eq!( + NCvt::convert(vec!["A=B", "FOO=AR", "sh", "-c", "echo $A$FOO"]), + parse_args_from_str(&NCvt::convert(r#"A=B FOO=AR sh -c "echo \$A\$FOO""#)).unwrap(), + ); + assert_eq!( + NCvt::convert(vec!["A=B", "FOO=AR", "sh", "-c", "echo $A$FOO"]), + parse_args_from_str(&NCvt::convert(r#"A=B FOO=AR sh -c 'echo $A$FOO'"#)).unwrap() + ); + assert_eq!( + NCvt::convert(vec!["A=B", "FOO=AR", "sh", "-c", "echo $A$FOO"]), + parse_args_from_str(&NCvt::convert(r#"A=B FOO=AR sh -c 'echo $A$FOO'"#)).unwrap() + ); + + assert_eq!( + NCvt::convert(vec!["-i", "A=B ' C"]), + parse_args_from_str(&NCvt::convert(r#"-i A='B \' C'"#)).unwrap() + ); +} + +#[test] +fn test_split_string_environment_vars_test() { + std::env::set_var("FOO", "BAR"); + assert_eq!( + NCvt::convert(vec!["FOO=bar", "sh", "-c", "echo xBARx =$FOO="]), + ::env::parse_args_from_str(&NCvt::convert(r#"FOO=bar sh -c "echo x${FOO}x =\$FOO=""#)) + .unwrap(), + ); +} + +#[macro_export] +macro_rules! compare_with_gnu { + ( $ts:expr, $args:expr ) => {{ + println!("=========================================================================="); + let result = $ts.ucmd().args($args).run(); + + #[cfg(target_os = "linux")] + { + let reference = expected_result(&$ts, $args); + if let Ok(reference) = reference { + let success = result.code() == reference.code() + && result.stdout_str() == reference.stdout_str() + && result.stderr_str() == reference.stderr_str(); + if !success { + println!("reference.code: {}", reference.code()); + println!(" result.code: {}", result.code()); + println!("reference.cout: {}", reference.stdout_str()); + println!(" result.cout: {}", result.stdout_str()); + println!("reference.cerr: {}", reference.stderr_str_lossy()); + println!(" result.cerr: {}", result.stderr_str_lossy()); + } + assert_eq!(result.code(), reference.code()); + assert_eq!(result.stdout_str(), reference.stdout_str()); + assert_eq!(result.stderr_str(), reference.stderr_str()); + } else { + println!( + "gnu reference test skipped. Reason: {:?}", + reference.unwrap_err() + ); + } + } + + result + }}; +} + +#[test] +#[allow(clippy::cognitive_complexity)] // Ignore clippy lint of too long function sign +fn test_env_with_gnu_reference_parsing_errors() { + let ts = TestScenario::new(util_name!()); + + compare_with_gnu!(ts, &["-S\\|echo hallo"]) // no quotes, invalid escape sequence | + .failure() + .code_is(125) + .no_stdout() + .stderr_is("env: invalid sequence '\\|' in -S\n"); + + compare_with_gnu!(ts, &["-S\\a"]) // no quotes, invalid escape sequence a + .failure() + .code_is(125) + .no_stdout() + .stderr_is("env: invalid sequence '\\a' in -S\n"); + + compare_with_gnu!(ts, &["-S\"\\a\""]) // double quotes, invalid escape sequence a + .failure() + .code_is(125) + .no_stdout() + .stderr_is("env: invalid sequence '\\a' in -S\n"); + + compare_with_gnu!(ts, &[r#"-S"\a""#]) // same as before, just using r#""# + .failure() + .code_is(125) + .no_stdout() + .stderr_is("env: invalid sequence '\\a' in -S\n"); + + compare_with_gnu!(ts, &["-S'\\a'"]) // single quotes, invalid escape sequence a + .failure() + .code_is(125) + .no_stdout() + .stderr_is("env: invalid sequence '\\a' in -S\n"); + + compare_with_gnu!(ts, &[r#"-S\|\&\;"#]) // no quotes, invalid escape sequence | + .failure() + .code_is(125) + .no_stdout() + .stderr_is("env: invalid sequence '\\|' in -S\n"); + + compare_with_gnu!(ts, &[r#"-S\<\&\;"#]) // no quotes, invalid escape sequence < + .failure() + .code_is(125) + .no_stdout() + .stderr_is("env: invalid sequence '\\<' in -S\n"); + + compare_with_gnu!(ts, &[r#"-S\>\&\;"#]) // no quotes, invalid escape sequence > + .failure() + .code_is(125) + .no_stdout() + .stderr_is("env: invalid sequence '\\>' in -S\n"); + + compare_with_gnu!(ts, &[r#"-S\`\&\;"#]) // no quotes, invalid escape sequence ` + .failure() + .code_is(125) + .no_stdout() + .stderr_is("env: invalid sequence '\\`' in -S\n"); + + compare_with_gnu!(ts, &[r#"-S"\`\&\;""#]) // double quotes, invalid escape sequence ` + .failure() + .code_is(125) + .no_stdout() + .stderr_is("env: invalid sequence '\\`' in -S\n"); + + compare_with_gnu!(ts, &[r#"-S'\`\&\;'"#]) // single quotes, invalid escape sequence ` + .failure() + .code_is(125) + .no_stdout() + .stderr_is("env: invalid sequence '\\`' in -S\n"); + + compare_with_gnu!(ts, &[r#"-S\`"#]) // ` escaped without quotes + .failure() + .code_is(125) + .no_stdout() + .stderr_is("env: invalid sequence '\\`' in -S\n"); + + compare_with_gnu!(ts, &[r#"-S"\`""#]) // ` escaped in double quotes + .failure() + .code_is(125) + .no_stdout() + .stderr_is("env: invalid sequence '\\`' in -S\n"); + + compare_with_gnu!(ts, &[r#"-S'\`'"#]) // ` escaped in single quotes + .failure() + .code_is(125) + .no_stdout() + .stderr_is("env: invalid sequence '\\`' in -S\n"); + + ts.ucmd() + .args(&[r#"-S\🦉"#]) // ` escaped in single quotes + .fails() + .code_is(125) + .no_stdout() + .stderr_is("env: invalid sequence '\\\u{FFFD}' in -S\n"); // gnu doesn't show the owl. Instead a invalid unicode ? +} + +#[test] +fn test_env_with_gnu_reference_empty_executable_single_quotes() { + let ts = TestScenario::new(util_name!()); + + ts.ucmd() + .args(&["-S''"]) // empty single quotes, considered as program name + .fails() + .code_is(127) + .no_stdout() + .stderr_is("env: '': No such file or directory\n"); // gnu version again adds escaping here +} + +#[test] +fn test_env_with_gnu_reference_empty_executable_double_quotes() { + let ts = TestScenario::new(util_name!()); + + compare_with_gnu!(ts, &["-S\"\""]) // empty double quotes, considered as program name + .failure() + .code_is(127) + .no_stdout() + .stderr_is("env: '': No such file or directory\n"); +} + +#[cfg(test)] +mod tests_split_iterator { + + enum EscapeStyle { + /// No escaping. + None, + /// Wrap in single quotes. + SingleQuoted, + /// Single quotes combined with backslash. + Mixed, + } + + /// Determines escaping style to use. + fn escape_style(s: &str) -> EscapeStyle { + if s.is_empty() { + return EscapeStyle::SingleQuoted; + } + + let mut special = false; + let mut newline = false; + let mut single_quote = false; + + for c in s.chars() { + match c { + '\n' => { + newline = true; + special = true; + } + '\'' => { + single_quote = true; + special = true; + } + '|' | '&' | ';' | '<' | '>' | '(' | ')' | '$' | '`' | '\\' | '"' | ' ' | '\t' + | '*' | '?' | '[' | '#' | 'Ëœ' | '=' | '%' => { + special = true; + } + _ => continue, + } + } + + if !special { + EscapeStyle::None + } else if newline && !single_quote { + EscapeStyle::SingleQuoted + } else { + EscapeStyle::Mixed + } + } + + /// Escapes special characters in a string, so that it will retain its literal + /// meaning when used as a part of command in Unix shell. + /// + /// It tries to avoid introducing any unnecessary quotes or escape characters, + /// but specifics regarding quoting style are left unspecified. + pub fn quote(s: &str) -> std::borrow::Cow { + // We are going somewhat out of the way to provide + // minimal amount of quoting in typical cases. + match escape_style(s) { + EscapeStyle::None => s.into(), + EscapeStyle::SingleQuoted => format!("'{}'", s).into(), + EscapeStyle::Mixed => { + let mut quoted = String::new(); + quoted.push('\''); + for c in s.chars() { + if c == '\'' { + quoted.push_str("'\\''"); + } else { + quoted.push(c); + } + } + quoted.push('\''); + quoted.into() + } + } + } + + /// Joins arguments into a single command line suitable for execution in Unix + /// shell. + /// + /// Each argument is quoted using [`quote`] to preserve its literal meaning when + /// parsed by Unix shell. + /// + /// Note: This function is essentially an inverse of [`split`]. + /// + /// # Examples + /// + /// Logging executed commands in format that can be easily copied and pasted + /// into an actual shell: + /// + /// ```rust,no_run + /// fn execute(args: &[&str]) { + /// use std::process::Command; + /// println!("Executing: {}", shell_words::join(args)); + /// Command::new(&args[0]) + /// .args(&args[1..]) + /// .spawn() + /// .expect("failed to start subprocess") + /// .wait() + /// .expect("failed to wait for subprocess"); + /// } + /// + /// execute(&["python", "-c", "print('Hello world!')"]); + /// ``` + /// + /// [`quote`]: fn.quote.html + /// [`split`]: fn.split.html + pub fn join(words: I) -> String + where + I: IntoIterator, + S: AsRef, + { + let mut line = words.into_iter().fold(String::new(), |mut line, word| { + let quoted = quote(word.as_ref()); + line.push_str(quoted.as_ref()); + line.push(' '); + line + }); + line.pop(); + line + } + + use std::ffi::OsString; + + use ::env::parse_error::ParseError; + use env::native_int_str::{from_native_int_representation_owned, Convert, NCvt}; + + fn split(input: &str) -> Result, ParseError> { + ::env::split_iterator::split(&NCvt::convert(input)).map(|vec| { + vec.into_iter() + .map(from_native_int_representation_owned) + .collect() + }) + } + + fn split_ok(cases: &[(&str, &[&str])]) { + for (i, &(input, expected)) in cases.iter().enumerate() { + match split(input) { + Err(actual) => { + panic!( + "[{i}] calling split({:?}):\nexpected: Ok({:?})\n actual: Err({:?})\n", + input, expected, actual + ); + } + Ok(actual) => { + assert!( + expected == actual.as_slice(), + "[{i}] After split({:?}).unwrap()\nexpected: {:?}\n actual: {:?}\n", + input, + expected, + actual + ); + } + } + } + } + + #[test] + fn split_empty() { + split_ok(&[("", &[])]); + } + + #[test] + fn split_initial_whitespace_is_removed() { + split_ok(&[ + (" a", &["a"]), + ("\t\t\t\tbar", &["bar"]), + ("\t \nc", &["c"]), + ]); + } + + #[test] + fn split_trailing_whitespace_is_removed() { + split_ok(&[ + ("a ", &["a"]), + ("b\t", &["b"]), + ("c\t \n \n \n", &["c"]), + ("d\n\n", &["d"]), + ]); + } + + #[test] + fn split_carriage_return() { + split_ok(&[("c\ra\r'\r'\r", &["c", "a", "\r"])]); + } + + #[test] + fn split_() { + split_ok(&[("\\'\\'", &["''"])]); + } + + #[test] + fn split_single_quotes() { + split_ok(&[ + (r#"''"#, &[r#""#]), + (r#"'a'"#, &[r#"a"#]), + (r#"'\\'"#, &[r#"\"#]), + (r#"' \\ '"#, &[r#" \ "#]), + (r#"'#'"#, &[r#"#"#]), + ]); + } + + #[test] + fn split_double_quotes() { + split_ok(&[ + (r#""""#, &[""]), + (r#""""""#, &[""]), + (r#""a b c' d""#, &["a b c' d"]), + (r#""\$""#, &["$"]), + (r#""`""#, &["`"]), + (r#""\"""#, &["\""]), + (r#""\\""#, &["\\"]), + ("\"\n\"", &["\n"]), + ("\"\\\n\"", &[""]), + ]); + } + + #[test] + fn split_unquoted() { + split_ok(&[ + (r#"\\|\\&\\;"#, &[r#"\|\&\;"#]), + (r#"\\<\\>"#, &[r#"\<\>"#]), + (r#"\\(\\)"#, &[r#"\(\)"#]), + (r#"\$"#, &[r#"$"#]), + (r#"\""#, &[r#"""#]), + (r#"\'"#, &[r#"'"#]), + ("\\\n", &[]), + (" \\\n \n", &[]), + ("a\nb\nc", &["a", "b", "c"]), + ("a\\\nb\\\nc", &["abc"]), + ("foo bar baz", &["foo", "bar", "baz"]), + ]); + } + + #[test] + fn split_trailing_backslash() { + assert_eq!( + split("\\"), + Err(ParseError::InvalidBackslashAtEndOfStringInMinusS { + pos: 1, + quoting: "Delimiter".into() + }) + ); + assert_eq!( + split(" \\"), + Err(ParseError::InvalidBackslashAtEndOfStringInMinusS { + pos: 2, + quoting: "Delimiter".into() + }) + ); + assert_eq!( + split("a\\"), + Err(ParseError::InvalidBackslashAtEndOfStringInMinusS { + pos: 2, + quoting: "Unquoted".into() + }) + ); + } + + #[test] + fn split_errors() { + assert_eq!( + split("'abc"), + Err(ParseError::MissingClosingQuote { pos: 4, c: '\'' }) + ); + assert_eq!( + split("\""), + Err(ParseError::MissingClosingQuote { pos: 1, c: '"' }) + ); + assert_eq!( + split("'\\"), + Err(ParseError::MissingClosingQuote { pos: 2, c: '\'' }) + ); + assert_eq!( + split("'\\"), + Err(ParseError::MissingClosingQuote { pos: 2, c: '\'' }) + ); + assert_eq!( + split(r#""$""#), + Err(ParseError::ParsingOfVariableNameFailed { + pos: 2, + msg: "Missing variable name".into() + }), + ); + } + + #[test] + fn split_error_fail_with_unknown_escape_sequences() { + assert_eq!( + split("\\a"), + Err(ParseError::InvalidSequenceBackslashXInMinusS { pos: 1, c: 'a' }) + ); + assert_eq!( + split("\"\\a\""), + Err(ParseError::InvalidSequenceBackslashXInMinusS { pos: 2, c: 'a' }) + ); + assert_eq!( + split("'\\a'"), + Err(ParseError::InvalidSequenceBackslashXInMinusS { pos: 2, c: 'a' }) + ); + assert_eq!( + split(r#""\a""#), + Err(ParseError::InvalidSequenceBackslashXInMinusS { pos: 2, c: 'a' }) + ); + assert_eq!( + split(r#"\🦉"#), + Err(ParseError::InvalidSequenceBackslashXInMinusS { + pos: 1, + c: '\u{FFFD}' + }) + ); + } + + #[test] + fn split_comments() { + split_ok(&[ + (r#" x # comment "#, &["x"]), + (r#" w1#w2 "#, &["w1#w2"]), + (r#"'not really a # comment'"#, &["not really a # comment"]), + (" a # very long comment \n b # another comment", &["a", "b"]), + ]); + } + + #[test] + fn test_quote() { + assert_eq!(quote(""), "''"); + assert_eq!(quote("'"), "''\\'''"); + assert_eq!(quote("abc"), "abc"); + assert_eq!(quote("a \n b"), "'a \n b'"); + assert_eq!(quote("X'\nY"), "'X'\\''\nY'"); + } + + #[test] + fn test_join() { + assert_eq!(join(["a", "b", "c"]), "a b c"); + assert_eq!(join([" ", "$", "\n"]), "' ' '$' '\n'"); + } + + #[test] + fn join_followed_by_split_is_identity() { + let cases: Vec<&[&str]> = vec![ + &["a"], + &["python", "-c", "print('Hello world!')"], + &["echo", " arg with spaces ", "arg \' with \" quotes"], + &["even newlines are quoted correctly\n", "\n", "\n\n\t "], + &["$", "`test`"], + &["cat", "~user/log*"], + &["test", "'a \"b", "\"X'"], + &["empty", "", "", ""], + ]; + for argv in cases { + let args = join(argv); + assert_eq!(split(&args).unwrap(), argv); + } + } +} + +mod test_raw_string_parser { + use std::{ + borrow::Cow, + ffi::{OsStr, OsString}, + }; + + use env::{ + native_int_str::{ + from_native_int_representation, from_native_int_representation_owned, + to_native_int_representation, NativeStr, + }, + string_expander::StringExpander, + string_parser, + }; + + const LEN_OWL: usize = if cfg!(target_os = "windows") { 2 } else { 4 }; + + #[test] + fn test_ascii_only_take_one_look_at_correct_data_and_end_behavior() { + let input = "hello"; + let cow = to_native_int_representation(OsStr::new(input)); + let mut uut = StringExpander::new(&cow); + for c in input.chars() { + assert_eq!(c, uut.get_parser().peek().unwrap()); + uut.take_one().unwrap(); + } + assert_eq!( + uut.get_parser().peek(), + Err(string_parser::Error { + peek_position: 5, + err_type: string_parser::ErrorType::EndOfInput + }) + ); + uut.take_one().unwrap_err(); + uut.take_one().unwrap_err(); + assert_eq!( + from_native_int_representation_owned(uut.take_collected_output()), + input + ); + uut.take_one().unwrap_err(); + assert_eq!( + from_native_int_representation_owned(uut.take_collected_output()), + "" + ); + } + + #[test] + fn test_multi_byte_codes_take_one_look_at_correct_data_and_end_behavior() { + let input = OsString::from("🦉🦉🦉x🦉🦉x🦉x🦉🦉🦉🦉"); + let cow = to_native_int_representation(input.as_os_str()); + let mut uut = StringExpander::new(&cow); + for _i in 0..3 { + assert_eq!(uut.get_parser().peek().unwrap(), '\u{FFFD}'); + uut.take_one().unwrap(); + assert_eq!(uut.get_parser().peek().unwrap(), 'x'); + uut.take_one().unwrap(); + } + assert_eq!(uut.get_parser().peek().unwrap(), '\u{FFFD}'); + uut.take_one().unwrap(); + assert_eq!( + uut.get_parser().peek(), + Err(string_parser::Error { + peek_position: 10 * LEN_OWL + 3, + err_type: string_parser::ErrorType::EndOfInput + }) + ); + uut.take_one().unwrap_err(); + uut.take_one().unwrap_err(); + assert_eq!( + from_native_int_representation_owned(uut.take_collected_output()), + input + ); + uut.take_one().unwrap_err(); + assert_eq!( + from_native_int_representation_owned(uut.take_collected_output()), + "" + ); + } + + #[test] + fn test_multi_byte_codes_put_one_ascii_start_middle_end_try_invalid_ascii() { + let input = OsString::from("🦉🦉🦉x🦉🦉x🦉x🦉🦉🦉🦉"); + let cow = to_native_int_representation(input.as_os_str()); + let owl: char = '🦉'; + let mut uut = StringExpander::new(&cow); + uut.put_one_char('a'); + for _i in 0..3 { + assert_eq!(uut.get_parser().peek().unwrap(), '\u{FFFD}'); + uut.take_one().unwrap(); + uut.put_one_char('a'); + assert_eq!(uut.get_parser().peek().unwrap(), 'x'); + uut.take_one().unwrap(); + uut.put_one_char('a'); + } + assert_eq!(uut.get_parser().peek().unwrap(), '\u{FFFD}'); + uut.take_one().unwrap(); + uut.put_one_char(owl); + uut.put_one_char('a'); + assert_eq!( + uut.get_parser().peek(), + Err(string_parser::Error { + peek_position: LEN_OWL * 10 + 3, + err_type: string_parser::ErrorType::EndOfInput + }) + ); + uut.take_one().unwrap_err(); + uut.put_one_char('a'); + uut.take_one().unwrap_err(); + assert_eq!( + from_native_int_representation_owned(uut.take_collected_output()), + "a🦉🦉🦉axa🦉🦉axa🦉axa🦉🦉🦉🦉🦉aa" + ); + uut.take_one().unwrap_err(); + assert_eq!( + from_native_int_representation_owned(uut.take_collected_output()), + "" + ); + } + + #[test] + fn test_multi_byte_codes_skip_one_take_one_skip_until_ascii_char_or_end() { + let input = OsString::from("🦉🦉🦉x🦉🦉x🦉x🦉🦉🦉🦉"); + let cow = to_native_int_representation(input.as_os_str()); + let mut uut = StringExpander::new(&cow); + + uut.skip_one().unwrap(); // skip 🦉🦉🦉 + let p = LEN_OWL * 3; + assert_eq!(uut.get_peek_position(), p); + + uut.skip_one().unwrap(); // skip x + assert_eq!(uut.get_peek_position(), p + 1); + uut.take_one().unwrap(); // take 🦉🦉 + let p = p + 1 + LEN_OWL * 2; + assert_eq!(uut.get_peek_position(), p); + + uut.skip_one().unwrap(); // skip x + assert_eq!(uut.get_peek_position(), p + 1); + uut.get_parser_mut().skip_until_char_or_end('x'); // skip 🦉 + let p = p + 1 + LEN_OWL; + assert_eq!(uut.get_peek_position(), p); + uut.take_one().unwrap(); // take x + uut.get_parser_mut().skip_until_char_or_end('x'); // skip 🦉🦉🦉🦉 till end + let p = p + 1 + LEN_OWL * 4; + assert_eq!(uut.get_peek_position(), p); + + uut.take_one().unwrap_err(); + assert_eq!( + from_native_int_representation_owned(uut.take_collected_output()), + "🦉🦉x" + ); + } + + #[test] + fn test_multi_byte_codes_skip_multiple_ascii_bounded_good_and_bad() { + let input = OsString::from("🦉🦉🦉x🦉🦉x🦉x🦉🦉🦉🦉"); + let cow = to_native_int_representation(input.as_os_str()); + let mut uut = StringExpander::new(&cow); + + uut.get_parser_mut().skip_multiple(0); + assert_eq!(uut.get_peek_position(), 0); + let p = LEN_OWL * 3; + uut.get_parser_mut().skip_multiple(p); // skips 🦉🦉🦉 + assert_eq!(uut.get_peek_position(), p); + + uut.take_one().unwrap(); // take x + assert_eq!(uut.get_peek_position(), p + 1); + let step = LEN_OWL * 3 + 1; + uut.get_parser_mut().skip_multiple(step); // skips 🦉🦉x🦉 + let p = p + 1 + step; + assert_eq!(uut.get_peek_position(), p); + uut.take_one().unwrap(); // take x + + assert_eq!(uut.get_peek_position(), p + 1); + let step = 4 * LEN_OWL; + uut.get_parser_mut().skip_multiple(step); // skips 🦉🦉🦉🦉 + let p = p + 1 + step; + assert_eq!(uut.get_peek_position(), p); + + uut.take_one().unwrap_err(); + assert_eq!( + from_native_int_representation_owned(uut.take_collected_output()), + "xx" + ); + } + + #[test] + fn test_multi_byte_codes_put_string_utf8_start_middle_end() { + let input = OsString::from("🦉🦉🦉x🦉🦉x🦉x🦉🦉🦉🦉"); + let cow = to_native_int_representation(input.as_os_str()); + let mut uut = StringExpander::new(&cow); + + uut.put_string("🦔oo"); + uut.take_one().unwrap(); // takes 🦉🦉🦉 + uut.put_string("oo🦔"); + uut.take_one().unwrap(); // take x + uut.get_parser_mut().skip_until_char_or_end('\n'); // skips till end + uut.put_string("o🦔o"); + + uut.take_one().unwrap_err(); + assert_eq!( + from_native_int_representation_owned(uut.take_collected_output()), + "🦔oo🦉🦉🦉oo🦔xo🦔o" + ); + } + + #[test] + fn test_multi_byte_codes_look_at_remaining_start_middle_end() { + let input = "🦉🦉🦉x🦉🦉x🦉x🦉🦉🦉🦉"; + let cow = to_native_int_representation(OsStr::new(input)); + let mut uut = StringExpander::new(&cow); + + assert_eq!(uut.get_parser().peek_remaining(), OsStr::new(input)); + uut.take_one().unwrap(); // takes 🦉🦉🦉 + assert_eq!(uut.get_parser().peek_remaining(), OsStr::new(&input[12..])); + uut.get_parser_mut().skip_until_char_or_end('\n'); // skips till end + assert_eq!(uut.get_parser().peek_remaining(), OsStr::new("")); + + uut.take_one().unwrap_err(); + assert_eq!( + from_native_int_representation_owned(uut.take_collected_output()), + "🦉🦉🦉" + ); + } + + #[test] + fn test_deal_with_invalid_encoding() { + let owl_invalid_part; + let (brace_1, brace_2); + #[cfg(target_os = "windows")] + { + let mut buffer = [0u16; 2]; + let owl = '🦉'.encode_utf16(&mut buffer); + owl_invalid_part = owl[0]; + brace_1 = '<'.encode_utf16(&mut buffer).to_vec(); + brace_2 = '>'.encode_utf16(&mut buffer).to_vec(); + } + #[cfg(not(target_os = "windows"))] + { + let mut buffer = [0u8; 4]; + let owl = '🦉'.encode_utf8(&mut buffer); + owl_invalid_part = owl.bytes().next().unwrap(); + brace_1 = [b'<'].to_vec(); + brace_2 = [b'>'].to_vec(); + } + let mut input_ux = brace_1; + input_ux.push(owl_invalid_part); + input_ux.extend(brace_2); + let input_str = from_native_int_representation(Cow::Borrowed(&input_ux)); + let mut uut = StringExpander::new(&input_ux); + + assert_eq!(uut.get_parser().peek_remaining(), input_str); + assert_eq!(uut.get_parser().peek().unwrap(), '<'); + uut.take_one().unwrap(); // takes "<" + assert_eq!( + uut.get_parser().peek_remaining(), + NativeStr::new(&input_str).split_at(1).1 + ); + assert_eq!(uut.get_parser().peek().unwrap(), '\u{FFFD}'); + uut.take_one().unwrap(); // takes owl_b + assert_eq!( + uut.get_parser().peek_remaining(), + NativeStr::new(&input_str).split_at(2).1 + ); + assert_eq!(uut.get_parser().peek().unwrap(), '>'); + uut.get_parser_mut().skip_until_char_or_end('\n'); + assert_eq!(uut.get_parser().peek_remaining(), OsStr::new("")); + + uut.take_one().unwrap_err(); + assert_eq!( + from_native_int_representation_owned(uut.take_collected_output()), + NativeStr::new(&input_str).split_at(2).0 + ); + } +} diff --git a/tests/by-util/test_expand.rs b/tests/by-util/test_expand.rs index c420f5ad5b9..ce105e78c7a 100644 --- a/tests/by-util/test_expand.rs +++ b/tests/by-util/test_expand.rs @@ -403,7 +403,7 @@ fn test_args_override() { // * indentation uses characters int main() { // * next line has both a leading & trailing tab - // with tabs=> + // with tabs=>\t return 0; } ", @@ -417,3 +417,12 @@ fn test_expand_directory() { .fails() .stderr_contains("expand: .: Is a directory"); } + +#[test] +fn test_nonexisting_file() { + new_ucmd!() + .args(&["nonexistent", "with-spaces.txt"]) + .fails() + .stderr_contains("expand: nonexistent: No such file or directory") + .stdout_contains_line("// !note: file contains significant whitespace"); +} diff --git a/tests/by-util/test_factor.rs b/tests/by-util/test_factor.rs index 57a2dae0998..bcab7d8c967 100644 --- a/tests/by-util/test_factor.rs +++ b/tests/by-util/test_factor.rs @@ -2,7 +2,6 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -#![allow(clippy::unreadable_literal)] // spell-checker:ignore (methods) hexdigest diff --git a/tests/by-util/test_fmt.rs b/tests/by-util/test_fmt.rs index 4fd05908019..8d50023f38e 100644 --- a/tests/by-util/test_fmt.rs +++ b/tests/by-util/test_fmt.rs @@ -33,7 +33,19 @@ fn test_fmt_width() { new_ucmd!() .args(&["one-word-per-line.txt", param, "10"]) .succeeds() - .stdout_is("this is\na file\nwith one\nword per\nline\n"); + .stdout_is("this is a\nfile with\none word\nper line\n"); + } +} + +#[test] +fn test_small_width() { + for width in ["0", "1", "2", "3"] { + for param in ["-w", "--width"] { + new_ucmd!() + .args(&[param, width, "one-word-per-line.txt"]) + .succeeds() + .stdout_is("this\nis\na\nfile\nwith\none\nword\nper\nline\n"); + } } } @@ -81,6 +93,17 @@ fn test_fmt_goal_too_big() { } } +#[test] +fn test_fmt_goal_bigger_than_default_width_of_75() { + for param in ["-g", "--goal"] { + new_ucmd!() + .args(&["one-word-per-line.txt", param, "76"]) + .fails() + .code_is(1) + .stderr_is("fmt: GOAL cannot be greater than WIDTH.\n"); + } +} + #[test] fn test_fmt_invalid_goal() { for param in ["-g", "--goal"] { diff --git a/tests/by-util/test_hashsum.rs b/tests/by-util/test_hashsum.rs index 7edd387c326..22a028a320a 100644 --- a/tests/by-util/test_hashsum.rs +++ b/tests/by-util/test_hashsum.rs @@ -356,6 +356,22 @@ fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails().code_is(1); } +#[test] +fn test_conflicting_arg() { + new_ucmd!() + .arg("--tag") + .arg("--check") + .arg("--md5") + .fails() + .code_is(1); + new_ucmd!() + .arg("--tag") + .arg("--text") + .arg("--md5") + .fails() + .code_is(1); +} + #[test] fn test_tag() { let scene = TestScenario::new(util_name!()); @@ -386,3 +402,47 @@ fn test_with_escape_filename() { assert!(stdout.starts_with('\\')); assert!(stdout.trim().ends_with("a\\nb")); } + +#[test] +#[cfg(not(windows))] +fn test_with_escape_filename_zero_text() { + let scene = TestScenario::new(util_name!()); + + let at = &scene.fixtures; + let filename = "a\nb"; + at.touch(filename); + let result = scene + .ccmd("md5sum") + .arg("--text") + .arg("--zero") + .arg(filename) + .succeeds(); + let stdout = result.stdout_str(); + println!("stdout {}", stdout); + assert!(!stdout.starts_with('\\')); + assert!(stdout.contains("a\nb")); +} + +#[test] +#[cfg(not(windows))] +fn test_check_with_escape_filename() { + let scene = TestScenario::new(util_name!()); + + let at = &scene.fixtures; + + let filename = "a\nb"; + at.touch(filename); + let result = scene.ccmd("md5sum").arg("--tag").arg(filename).succeeds(); + let stdout = result.stdout_str(); + println!("stdout {}", stdout); + assert!(stdout.starts_with("\\MD5")); + assert!(stdout.contains("a\\nb")); + at.write("check.md5", stdout); + let result = scene + .ccmd("md5sum") + .arg("--strict") + .arg("-c") + .arg("check.md5") + .succeeds(); + result.stdout_is("\\a\\nb: OK\n"); +} diff --git a/tests/by-util/test_head.rs b/tests/by-util/test_head.rs index 0b0e98aa122..b72e77281ad 100644 --- a/tests/by-util/test_head.rs +++ b/tests/by-util/test_head.rs @@ -99,10 +99,8 @@ fn test_verbose() { } #[test] -#[ignore] fn test_spams_newline() { - //this test is does not mirror what GNU does - new_ucmd!().pipe_in("a").succeeds().stdout_is("a\n"); + new_ucmd!().pipe_in("a").succeeds().stdout_is("a"); } #[test] diff --git a/tests/by-util/test_install.rs b/tests/by-util/test_install.rs index 6b1d76e5527..5790c685fc2 100644 --- a/tests/by-util/test_install.rs +++ b/tests/by-util/test_install.rs @@ -8,7 +8,7 @@ use crate::common::util::{is_ci, run_ucmd_as_root, TestScenario}; use filetime::FileTime; use std::fs; use std::os::unix::fs::{MetadataExt, PermissionsExt}; -#[cfg(not(any(windows, target_os = "freebsd")))] +#[cfg(not(windows))] use std::process::Command; #[cfg(any(target_os = "linux", target_os = "android"))] use std::thread::sleep; @@ -610,13 +610,17 @@ fn test_install_copy_then_compare_file_with_extra_mode() { } const STRIP_TARGET_FILE: &str = "helloworld_installed"; -#[cfg(not(any(windows, target_os = "freebsd")))] +#[cfg(all(not(windows), not(target_os = "freebsd")))] const SYMBOL_DUMP_PROGRAM: &str = "objdump"; -#[cfg(not(any(windows, target_os = "freebsd")))] +#[cfg(target_os = "freebsd")] +const SYMBOL_DUMP_PROGRAM: &str = "llvm-objdump"; +#[cfg(not(windows))] const STRIP_SOURCE_FILE_SYMBOL: &str = "main"; fn strip_source_file() -> &'static str { - if cfg!(target_os = "macos") { + if cfg!(target_os = "freebsd") { + "helloworld_freebsd" + } else if cfg!(target_os = "macos") { "helloworld_macos" } else if cfg!(target_arch = "arm") || cfg!(target_arch = "aarch64") { "helloworld_android" @@ -626,8 +630,7 @@ fn strip_source_file() -> &'static str { } #[test] -// FixME: Freebsd fails on 'No such file or directory' -#[cfg(not(any(windows, target_os = "freebsd")))] +#[cfg(not(windows))] fn test_install_and_strip() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; @@ -650,8 +653,7 @@ fn test_install_and_strip() { } #[test] -// FixME: Freebsd fails on 'No such file or directory' -#[cfg(not(any(windows, target_os = "freebsd")))] +#[cfg(not(windows))] fn test_install_and_strip_with_program() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; @@ -677,8 +679,6 @@ fn test_install_and_strip_with_program() { #[cfg(all(unix, feature = "chmod"))] #[test] -// FixME: Freebsd fails on 'No such file or directory' -#[cfg(not(target_os = "freebsd"))] fn test_install_and_strip_with_program_hyphen() { let scene = TestScenario::new(util_name!()); @@ -715,6 +715,64 @@ fn test_install_and_strip_with_program_hyphen() { .stdout_is("./-dest\n"); } +#[cfg(all(unix, feature = "chmod"))] +#[test] +fn test_install_on_invalid_link_at_destination() { + let scene = TestScenario::new(util_name!()); + + let at = &scene.fixtures; + at.mkdir("src"); + at.mkdir("dest"); + let src_dir = at.plus("src"); + let dst_dir = at.plus("dest"); + + at.touch("test.sh"); + at.symlink_file( + "/opt/FakeDestination", + &dst_dir.join("test.sh").to_string_lossy(), + ); + scene.ccmd("chmod").arg("+x").arg("test.sh").succeeds(); + at.symlink_file("test.sh", &src_dir.join("test.sh").to_string_lossy()); + + scene + .ucmd() + .current_dir(&src_dir) + .arg(src_dir.join("test.sh")) + .arg(dst_dir.join("test.sh")) + .succeeds() + .no_stderr() + .no_stdout(); +} + +#[cfg(all(unix, feature = "chmod"))] +#[test] +fn test_install_on_invalid_link_at_destination_and_dev_null_at_source() { + let scene = TestScenario::new(util_name!()); + + let at = &scene.fixtures; + at.mkdir("src"); + at.mkdir("dest"); + let src_dir = at.plus("src"); + let dst_dir = at.plus("dest"); + + at.touch("test.sh"); + at.symlink_file( + "/opt/FakeDestination", + &dst_dir.join("test.sh").to_string_lossy(), + ); + scene.ccmd("chmod").arg("+x").arg("test.sh").succeeds(); + at.symlink_file("test.sh", &src_dir.join("test.sh").to_string_lossy()); + + scene + .ucmd() + .current_dir(&src_dir) + .arg("/dev/null") + .arg(dst_dir.join("test.sh")) + .succeeds() + .no_stderr() + .no_stdout(); +} + #[test] #[cfg(not(windows))] fn test_install_and_strip_with_invalid_program() { diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index 1262c2ab9ec..3f0154f7af9 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.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 (words) READMECAREFULLY birthtime doesntexist oneline somebackup lrwx somefile somegroup somehiddenbackup somehiddenfile tabsize aaaaaaaa bbbb cccc dddddddd ncccc neee naaaaa nbcdef nfffff dired subdired tmpfs mdir COLORTERM mexe +// spell-checker:ignore (words) READMECAREFULLY birthtime doesntexist oneline somebackup lrwx somefile somegroup somehiddenbackup somehiddenfile tabsize aaaaaaaa bbbb cccc dddddddd ncccc neee naaaaa nbcdef nfffff dired subdired tmpfs mdir COLORTERM mexe bcdef #[cfg(any(unix, feature = "feat_selinux"))] use crate::common::util::expected_result; @@ -73,6 +73,21 @@ fn test_ls_ordering() { .stdout_matches(&Regex::new("some-dir1:\\ntotal 0").unwrap()); } +#[cfg(all(unix, feature = "df", not(target_os = "freebsd")))] +fn get_filesystem_type(scene: &TestScenario, path: &Path) -> String { + let mut cmd = scene.ccmd("df"); + cmd.args(&["-PT"]).arg(path); + let output = cmd.succeeds(); + let stdout_str = String::from_utf8_lossy(output.stdout()); + println!("output of stat call ({:?}):\n{}", cmd, stdout_str); + let regex_str = r#"Filesystem\s+Type\s+.+[\r\n]+([^\s]+)\s+(?[^\s]+)\s+"#; + let regex = Regex::new(regex_str).unwrap(); + let m = regex.captures(&stdout_str).unwrap(); + let fstype = m["fstype"].to_owned(); + println!("detected fstype: {}", fstype); + fstype +} + #[cfg(all(feature = "truncate", feature = "dd"))] #[test] // FIXME: fix this test for FreeBSD fn test_ls_allocation_size() { @@ -81,7 +96,7 @@ fn test_ls_allocation_size() { at.mkdir("some-dir1"); at.touch("some-dir1/empty-file"); - #[cfg(unix)] + #[cfg(all(unix, feature = "df"))] { scene .ccmd("truncate") @@ -115,13 +130,24 @@ fn test_ls_allocation_size() { .succeeds() .stdout_matches(&Regex::new("[^ ] 2 [^ ]").unwrap()); + #[cfg(not(target_os = "freebsd"))] + let (zero_file_size_4k, zero_file_size_1k, zero_file_size_8k, zero_file_size_4m) = + match get_filesystem_type(&scene, &scene.fixtures.subdir).as_str() { + // apparently f2fs (flash friendly fs) accepts small overhead for better performance + "f2fs" => (4100, 1025, 8200, "4.1M"), + _ => (4096, 1024, 8192, "4.0M"), + }; + #[cfg(not(target_os = "freebsd"))] scene .ucmd() .arg("-s1") .arg("some-dir1") .succeeds() - .stdout_is("total 4096\n 0 empty-file\n 0 file-with-holes\n4096 zero-file\n"); + .stdout_is(format!( + "total {zero_file_size_4k}\n 0 empty-file\n 0 file-with-holes\n\ + {zero_file_size_4k} zero-file\n" + )); scene .ucmd() @@ -138,7 +164,7 @@ fn test_ls_allocation_size() { .arg("some-dir1") .succeeds() .stdout_contains("0 empty-file") - .stdout_contains("4096 zero-file"); + .stdout_contains(format!("{zero_file_size_4k} zero-file")); // Test alignment of different block sized files let res = scene.ucmd().arg("-si1").arg("some-dir1").succeeds(); @@ -185,10 +211,10 @@ fn test_ls_allocation_size() { .arg("-s1") .arg("some-dir1") .succeeds() - .stdout_contains("total 1024") + .stdout_contains(format!("total {zero_file_size_1k}")) .stdout_contains("0 empty-file") .stdout_contains("0 file-with-holes") - .stdout_contains("1024 zero-file"); + .stdout_contains(format!("{zero_file_size_1k} zero-file")); #[cfg(not(target_os = "freebsd"))] scene @@ -210,10 +236,10 @@ fn test_ls_allocation_size() { .arg("-s1") .arg("some-dir1") .succeeds() - .stdout_contains("total 1024") + .stdout_contains(format!("total {zero_file_size_1k}")) .stdout_contains("0 empty-file") .stdout_contains("0 file-with-holes") - .stdout_contains("1024 zero-file"); + .stdout_contains(format!("{zero_file_size_1k} zero-file")); #[cfg(not(target_os = "freebsd"))] scene @@ -222,10 +248,10 @@ fn test_ls_allocation_size() { .arg("-s1") .arg("some-dir1") .succeeds() - .stdout_contains("total 8192") + .stdout_contains(format!("total {zero_file_size_8k}")) .stdout_contains("0 empty-file") .stdout_contains("0 file-with-holes") - .stdout_contains("8192 zero-file"); + .stdout_contains(format!("{zero_file_size_8k} zero-file")); // -k should make 'ls' ignore the env var #[cfg(not(target_os = "freebsd"))] @@ -235,10 +261,10 @@ fn test_ls_allocation_size() { .arg("-s1k") .arg("some-dir1") .succeeds() - .stdout_contains("total 4096") + .stdout_contains(format!("total {zero_file_size_4k}")) .stdout_contains("0 empty-file") .stdout_contains("0 file-with-holes") - .stdout_contains("4096 zero-file"); + .stdout_contains(format!("{zero_file_size_4k} zero-file")); // but manually specified blocksize overrides -k #[cfg(not(target_os = "freebsd"))] @@ -248,10 +274,10 @@ fn test_ls_allocation_size() { .arg("--block-size=4K") .arg("some-dir1") .succeeds() - .stdout_contains("total 1024") + .stdout_contains(format!("total {zero_file_size_1k}")) .stdout_contains("0 empty-file") .stdout_contains("0 file-with-holes") - .stdout_contains("1024 zero-file"); + .stdout_contains(format!("{zero_file_size_1k} zero-file")); #[cfg(not(target_os = "freebsd"))] scene @@ -260,10 +286,10 @@ fn test_ls_allocation_size() { .arg("--block-size=4K") .arg("some-dir1") .succeeds() - .stdout_contains("total 1024") + .stdout_contains(format!("total {zero_file_size_1k}")) .stdout_contains("0 empty-file") .stdout_contains("0 file-with-holes") - .stdout_contains("1024 zero-file"); + .stdout_contains(format!("{zero_file_size_1k} zero-file")); // si option should always trump the human-readable option #[cfg(not(target_os = "freebsd"))] @@ -285,10 +311,10 @@ fn test_ls_allocation_size() { .arg("--block-size=human-readable") .arg("some-dir1") .succeeds() - .stdout_contains("total 4.0M") + .stdout_contains(format!("total {zero_file_size_4m}")) .stdout_contains("0 empty-file") .stdout_contains("0 file-with-holes") - .stdout_contains("4.0M zero-file"); + .stdout_contains(format!("{zero_file_size_4m} zero-file")); #[cfg(not(target_os = "freebsd"))] scene diff --git a/tests/by-util/test_more.rs b/tests/by-util/test_more.rs index e80020d3996..3941dc1dad9 100644 --- a/tests/by-util/test_more.rs +++ b/tests/by-util/test_more.rs @@ -7,33 +7,46 @@ use std::io::IsTerminal; #[test] fn test_more_no_arg() { - // Reading from stdin is now supported, so this must succeed if std::io::stdout().is_terminal() { - new_ucmd!().succeeds(); + new_ucmd!().fails().stderr_contains("more: bad usage"); } } #[test] fn test_valid_arg() { if std::io::stdout().is_terminal() { - new_ucmd!().arg("-c").succeeds(); - new_ucmd!().arg("--print-over").succeeds(); + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let file = "test_file"; + at.touch(file); - new_ucmd!().arg("-p").succeeds(); - new_ucmd!().arg("--clean-print").succeeds(); + scene.ucmd().arg(file).arg("-c").succeeds(); + scene.ucmd().arg(file).arg("--print-over").succeeds(); - new_ucmd!().arg("-s").succeeds(); - new_ucmd!().arg("--squeeze").succeeds(); + scene.ucmd().arg(file).arg("-p").succeeds(); + scene.ucmd().arg(file).arg("--clean-print").succeeds(); - new_ucmd!().arg("-u").succeeds(); - new_ucmd!().arg("--plain").succeeds(); + scene.ucmd().arg(file).arg("-s").succeeds(); + scene.ucmd().arg(file).arg("--squeeze").succeeds(); - new_ucmd!().arg("-n").arg("10").succeeds(); - new_ucmd!().arg("--lines").arg("0").succeeds(); - new_ucmd!().arg("--number").arg("0").succeeds(); + scene.ucmd().arg(file).arg("-u").succeeds(); + scene.ucmd().arg(file).arg("--plain").succeeds(); - new_ucmd!().arg("-F").arg("10").succeeds(); - new_ucmd!().arg("--from-line").arg("0").succeeds(); + scene.ucmd().arg(file).arg("-n").arg("10").succeeds(); + scene.ucmd().arg(file).arg("--lines").arg("0").succeeds(); + scene.ucmd().arg(file).arg("--number").arg("0").succeeds(); + + scene.ucmd().arg(file).arg("-F").arg("10").succeeds(); + scene + .ucmd() + .arg(file) + .arg("--from-line") + .arg("0") + .succeeds(); + + scene.ucmd().arg(file).arg("-P").arg("something").succeeds(); + scene.ucmd().arg(file).arg("--pattern").arg("-1").succeeds(); } } @@ -91,8 +104,8 @@ fn test_more_dir_arg() { if std::io::stdout().is_terminal() { new_ucmd!() .arg(".") - .fails() - .usage_error("'.' is a directory."); + .succeeds() + .stderr_contains("'.' is a directory."); } } @@ -108,8 +121,93 @@ fn test_more_invalid_file_perms() { at.make_file("invalid-perms.txt"); set_permissions(at.plus("invalid-perms.txt"), permissions).unwrap(); ucmd.arg("invalid-perms.txt") - .fails() - .code_is(1) + .succeeds() .stderr_contains("permission denied"); } } + +#[test] +fn test_more_error_on_single_arg() { + if std::io::stdout().is_terminal() { + let ts = TestScenario::new("more"); + ts.fixtures.mkdir_all("folder"); + ts.ucmd() + .arg("folder") + .succeeds() + .stderr_contains("is a directory"); + ts.ucmd() + .arg("file1") + .succeeds() + .stderr_contains("No such file or directory"); + } +} + +#[test] +fn test_more_error_on_multiple_files() { + if std::io::stdout().is_terminal() { + let ts = TestScenario::new("more"); + ts.fixtures.mkdir_all("folder"); + ts.fixtures.make_file("file1"); + ts.ucmd() + .arg("folder") + .arg("file2") + .arg("file1") + .succeeds() + .stderr_contains("folder") + .stderr_contains("file2") + .stdout_contains("file1"); + ts.ucmd() + .arg("file2") + .arg("file3") + .succeeds() + .stderr_contains("file2") + .stderr_contains("file3"); + } +} + +#[test] +fn test_more_pattern_found() { + if std::io::stdout().is_terminal() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let file = "test_file"; + + at.write(file, "line1\nline2"); + + // output only the second line "line2" + scene + .ucmd() + .arg("-P") + .arg("line2") + .arg(file) + .succeeds() + .no_stderr() + .stdout_does_not_contain("line1") + .stdout_contains("line2"); + } +} + +#[test] +fn test_more_pattern_not_found() { + if std::io::stdout().is_terminal() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let file = "test_file"; + + let file_content = "line1\nline2"; + at.write(file, file_content); + + scene + .ucmd() + .arg("-P") + .arg("something") + .arg(file) + .succeeds() + .no_stderr() + .stdout_contains("Pattern not found") + .stdout_contains("line1") + .stdout_contains("line2"); + } +} diff --git a/tests/by-util/test_mv.rs b/tests/by-util/test_mv.rs index dd05ffbcd0a..a53d7277bd9 100644 --- a/tests/by-util/test_mv.rs +++ b/tests/by-util/test_mv.rs @@ -1590,7 +1590,7 @@ fn test_acl() { // calling the command directly. xattr requires some dev packages to be installed // and it adds a complex dependency just for a test match Command::new("setfacl") - .args(["-m", "group::rwx", &path1]) + .args(["-m", "group::rwx", path1]) .status() .map(|status| status.code()) { diff --git a/tests/by-util/test_nl.rs b/tests/by-util/test_nl.rs index a00e37a4767..78d18bcb41f 100644 --- a/tests/by-util/test_nl.rs +++ b/tests/by-util/test_nl.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 binvalid finvalid hinvalid iinvalid linvalid nabcabc nabcabcabc ninvalid vinvalid winvalid +// spell-checker:ignore binvalid finvalid hinvalid iinvalid linvalid nabcabc nabcabcabc ninvalid vinvalid winvalid dabc näää use crate::common::util::TestScenario; #[test] diff --git a/tests/by-util/test_nohup.rs b/tests/by-util/test_nohup.rs index b014c31aa08..db0a22a5e42 100644 --- a/tests/by-util/test_nohup.rs +++ b/tests/by-util/test_nohup.rs @@ -2,6 +2,7 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +// spell-checker:ignore winsize Openpty openpty xpixel ypixel ptyprocess use crate::common::util::TestScenario; use std::thread::sleep; @@ -31,3 +32,32 @@ fn test_nohup_multiple_args_and_flags() { assert!(at.file_exists("file1")); assert!(at.file_exists("file2")); } + +#[test] +#[cfg(any( + target_os = "linux", + target_os = "android", + target_os = "freebsd", + target_vendor = "apple" +))] +fn test_nohup_with_pseudo_terminal_emulation_on_stdin_stdout_stderr_get_replaced() { + let ts = TestScenario::new(util_name!()); + let result = ts + .ucmd() + .terminal_simulation(true) + .args(&["sh", "is_atty.sh"]) + .succeeds(); + + assert_eq!( + String::from_utf8_lossy(result.stderr()).trim(), + "nohup: ignoring input and appending output to 'nohup.out'" + ); + + sleep(std::time::Duration::from_millis(10)); + + // this proves that nohup was exchanging the stdio file descriptors + assert_eq!( + std::fs::read_to_string(ts.fixtures.plus_as_string("nohup.out")).unwrap(), + "stdin is not atty\nstdout is not atty\nstderr is not atty\n" + ); +} diff --git a/tests/by-util/test_numfmt.rs b/tests/by-util/test_numfmt.rs index 2c2e95d0bb1..bb80502d584 100644 --- a/tests/by-util/test_numfmt.rs +++ b/tests/by-util/test_numfmt.rs @@ -666,7 +666,12 @@ fn test_invalid_stdin_number_returns_status_2() { #[test] fn test_invalid_stdin_number_in_middle_of_input() { - new_ucmd!().pipe_in("100\nhello\n200").fails().code_is(2); + new_ucmd!() + .pipe_in("100\nhello\n200") + .ignore_stdin_write_error() + .fails() + .stdout_is("100\n") + .code_is(2); } #[test] diff --git a/tests/by-util/test_pr.rs b/tests/by-util/test_pr.rs index 195d4056798..823f0718f4b 100644 --- a/tests/by-util/test_pr.rs +++ b/tests/by-util/test_pr.rs @@ -5,13 +5,10 @@ // spell-checker:ignore (ToDO) Sdivide use crate::common::util::{TestScenario, UCommand}; +use chrono::{DateTime, Duration, Utc}; use std::fs::metadata; -use time::macros::format_description; -use time::Duration; -use time::OffsetDateTime; -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"; fn file_last_modified_time(ucmd: &UCommand, path: &str) -> String { let tmp_dir_path = ucmd.get_full_fixture_path(path); @@ -20,28 +17,27 @@ fn file_last_modified_time(ucmd: &UCommand, path: &str) -> String { .map(|i| { i.modified() .map(|x| { - let date_time: OffsetDateTime = x.into(); - date_time.format(&DATE_TIME_FORMAT).unwrap() + let date_time: DateTime = x.into(); + date_time.format(DATE_TIME_FORMAT).to_string() }) .unwrap_or_default() }) .unwrap_or_default() } -fn all_minutes(from: OffsetDateTime, to: OffsetDateTime) -> Vec { - let to = to + Duration::minutes(1); - // const FORMAT: &str = "%b %d %H:%M %Y"; +fn all_minutes(from: DateTime, to: DateTime) -> Vec { + let to = to + Duration::try_minutes(1).unwrap(); let mut vec = vec![]; let mut current = from; while current < to { - vec.push(current.format(&DATE_TIME_FORMAT).unwrap()); - current += Duration::minutes(1); + vec.push(current.format(DATE_TIME_FORMAT).to_string()); + current += Duration::try_minutes(1).unwrap(); } vec } -fn valid_last_modified_template_vars(from: OffsetDateTime) -> Vec> { - all_minutes(from, OffsetDateTime::now_utc()) +fn valid_last_modified_template_vars(from: DateTime) -> Vec> { + all_minutes(from, Utc::now()) .into_iter() .map(|time| vec![("{last_modified_time}".to_string(), time)]) .collect() @@ -257,7 +253,7 @@ fn test_with_suppress_error_option() { fn test_with_stdin() { let expected_file_path = "stdin.log.expected"; let mut scenario = new_ucmd!(); - let start = OffsetDateTime::now_utc(); + let start = Utc::now(); scenario .pipe_in_fixture("stdin.log") .args(&["--pages=1:2", "-n", "-"]) @@ -320,7 +316,7 @@ fn test_with_mpr() { let expected_test_file_path = "mpr.log.expected"; let expected_test_file_path1 = "mpr1.log.expected"; let expected_test_file_path2 = "mpr2.log.expected"; - let start = OffsetDateTime::now_utc(); + let start = Utc::now(); new_ucmd!() .args(&["--pages=1:2", "-m", "-n", test_file_path, test_file_path1]) .succeeds() @@ -329,7 +325,7 @@ fn test_with_mpr() { &valid_last_modified_template_vars(start), ); - let start = OffsetDateTime::now_utc(); + let start = Utc::now(); new_ucmd!() .args(&["--pages=2:4", "-m", "-n", test_file_path, test_file_path1]) .succeeds() @@ -338,7 +334,7 @@ fn test_with_mpr() { &valid_last_modified_template_vars(start), ); - let start = OffsetDateTime::now_utc(); + let start = Utc::now(); new_ucmd!() .args(&[ "--pages=1:2", @@ -413,7 +409,7 @@ fn test_with_pr_core_utils_tests() { let mut scenario = new_ucmd!(); let input_file_path = input_file.first().unwrap(); let test_file_path = expected_file.first().unwrap(); - let value = file_last_modified_time(&scenario, test_file_path); + let value = file_last_modified_time(&scenario, input_file_path); let mut arguments: Vec<&str> = flags .split(' ') .filter(|i| i.trim() != "") @@ -445,7 +441,7 @@ fn test_with_join_lines_option() { let test_file_2 = "test.log"; let expected_file_path = "joined.log.expected"; let mut scenario = new_ucmd!(); - let start = OffsetDateTime::now_utc(); + let start = Utc::now(); scenario .args(&["+1:2", "-J", "-m", test_file_1, test_file_2]) .run() diff --git a/tests/by-util/test_printf.rs b/tests/by-util/test_printf.rs index db4c5aa7f8d..0cb603da4a4 100644 --- a/tests/by-util/test_printf.rs +++ b/tests/by-util/test_printf.rs @@ -668,15 +668,112 @@ fn sub_alternative_upper_hex() { new_ucmd!() .args(&["%#X", "42"]) .succeeds() - .stdout_only("0x2A"); + .stdout_only("0X2A"); } #[test] fn char_as_byte() { - new_ucmd!().args(&["%c", "🙃"]).succeeds().stdout_only("ð"); + new_ucmd!() + .args(&["%c", "🙃"]) + .succeeds() + .no_stderr() + .stdout_is_bytes(b"\xf0"); } #[test] fn no_infinite_loop() { new_ucmd!().args(&["a", "b"]).succeeds().stdout_only("a"); } + +#[test] +fn pad_octal_with_prefix() { + new_ucmd!() + .args(&[">%#15.6o<", "0"]) + .succeeds() + .stdout_only("> 000000<"); + + new_ucmd!() + .args(&[">%#15.6o<", "01"]) + .succeeds() + .stdout_only("> 000001<"); + + new_ucmd!() + .args(&[">%#15.6o<", "01234"]) + .succeeds() + .stdout_only("> 001234<"); + + new_ucmd!() + .args(&[">%#15.6o<", "012345"]) + .succeeds() + .stdout_only("> 012345<"); + + new_ucmd!() + .args(&[">%#15.6o<", "0123456"]) + .succeeds() + .stdout_only("> 0123456<"); +} + +#[test] +fn pad_unsigned_zeroes() { + for format in ["%.3u", "%.3x", "%.3X", "%.3o"] { + new_ucmd!() + .args(&[format, "0"]) + .succeeds() + .stdout_only("000"); + } +} + +#[test] +fn pad_unsigned_three() { + for (format, expected) in [ + ("%.3u", "003"), + ("%.3x", "003"), + ("%.3X", "003"), + ("%.3o", "003"), + ("%#.3x", "0x003"), + ("%#.3X", "0X003"), + ("%#.3o", "003"), + ] { + new_ucmd!() + .args(&[format, "3"]) + .succeeds() + .stdout_only(expected); + } +} + +#[test] +fn pad_char() { + for (format, expected) in [("%3c", " X"), ("%1c", "X"), ("%-1c", "X"), ("%-3c", "X ")] { + new_ucmd!() + .args(&[format, "X"]) + .succeeds() + .stdout_only(expected); + } +} + +#[test] +fn pad_string() { + for (format, expected) in [ + ("%8s", " bottle"), + ("%-8s", "bottle "), + ("%6s", "bottle"), + ("%-6s", "bottle"), + ] { + new_ucmd!() + .args(&[format, "bottle"]) + .succeeds() + .stdout_only(expected); + } +} + +#[test] +fn format_spec_zero_char_fails() { + // It is invalid to have the format spec '%0c' + new_ucmd!().args(&["%0c", "3"]).fails().code_is(1); +} + +#[test] +fn format_spec_zero_string_fails() { + // It is invalid to have the format spec '%0s' + new_ucmd!().args(&["%0s", "3"]).fails().code_is(1); +} diff --git a/tests/by-util/test_rm.rs b/tests/by-util/test_rm.rs index 5125c746da7..65d631e4cd8 100644 --- a/tests/by-util/test_rm.rs +++ b/tests/by-util/test_rm.rs @@ -226,6 +226,19 @@ fn test_rm_directory_without_flag() { .stderr_contains(&format!("cannot remove '{dir}': Is a directory")); } +#[test] +#[cfg(windows)] +// https://github.com/uutils/coreutils/issues/3200 +fn test_rm_directory_with_trailing_backslash() { + let (at, mut ucmd) = at_and_ucmd!(); + let dir = "dir"; + + at.mkdir(dir); + + ucmd.arg(".\\dir\\").arg("-rf").succeeds(); + assert!(!at.dir_exists(dir)); +} + #[test] fn test_rm_verbose() { let (at, mut ucmd) = at_and_ucmd!(); diff --git a/tests/by-util/test_shred.rs b/tests/by-util/test_shred.rs index d5de7882f57..1ff847afcd8 100644 --- a/tests/by-util/test_shred.rs +++ b/tests/by-util/test_shred.rs @@ -143,3 +143,60 @@ fn test_hex() { ucmd.arg("--size=0x10").arg(file).succeeds(); } + +#[test] +fn test_shred_empty() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let file_a = "test_shred_remove_a"; + + at.touch(file_a); + + // Shred file_a and verify that, as it is empty, it doesn't have "pass 1/3 (random)" + scene + .ucmd() + .arg("-uv") + .arg(file_a) + .succeeds() + .stderr_does_not_contain("1/3 (random)"); + + assert!(!at.file_exists(file_a)); + + // if the file isn't empty, we should have random + at.touch(file_a); + at.write(file_a, "1"); + scene + .ucmd() + .arg("-uv") + .arg(file_a) + .succeeds() + .stderr_contains("1/3 (random)"); + + assert!(!at.file_exists(file_a)); +} + +#[test] +#[cfg(all(unix, feature = "chmod"))] +fn test_shred_fail_no_perm() { + use std::path::Path; + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + let dir = "dir"; + + let file = "test_shred_remove_a"; + + let binding = Path::new("dir").join(file); + let path = binding.to_str().unwrap(); + at.mkdir(dir); + at.touch(path); + scene.ccmd("chmod").arg("a-w").arg(dir).succeeds(); + + scene + .ucmd() + .arg("-uv") + .arg(path) + .fails() + .stderr_contains("Couldn't rename to"); +} diff --git a/tests/by-util/test_shuf.rs b/tests/by-util/test_shuf.rs index 13df0fa483f..8a991e43509 100644 --- a/tests/by-util/test_shuf.rs +++ b/tests/by-util/test_shuf.rs @@ -2,6 +2,8 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. + +// spell-checker:ignore (ToDO) unwritable use crate::common::util::TestScenario; #[test] @@ -32,6 +34,28 @@ fn test_output_is_random_permutation() { assert_eq!(result_seq, input_seq, "Output is not a permutation"); } +#[test] +fn test_explicit_stdin_file() { + let input_seq = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + let input = input_seq + .iter() + .map(ToString::to_string) + .collect::>() + .join("\n"); + + let result = new_ucmd!().arg("-").pipe_in(input.as_bytes()).succeeds(); + result.no_stderr(); + + let mut result_seq: Vec = result + .stdout_str() + .split('\n') + .filter(|x| !x.is_empty()) + .map(|x| x.parse().unwrap()) + .collect(); + result_seq.sort_unstable(); + assert_eq!(result_seq, input_seq, "Output is not a permutation"); +} + #[test] fn test_zero_termination() { let input_seq = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; @@ -48,6 +72,215 @@ fn test_zero_termination() { assert_eq!(result_seq, input_seq, "Output is not a permutation"); } +#[test] +fn test_zero_termination_multi() { + let input_seq = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + let result = new_ucmd!().arg("-z").arg("-z").arg("-i1-10").succeeds(); + result.no_stderr(); + + let mut result_seq: Vec = result + .stdout_str() + .split('\0') + .filter(|x| !x.is_empty()) + .map(|x| x.parse().unwrap()) + .collect(); + result_seq.sort_unstable(); + assert_eq!(result_seq, input_seq, "Output is not a permutation"); +} + +#[test] +fn test_very_large_range() { + let num_samples = 10; + let result = new_ucmd!() + .arg("-n") + .arg(&num_samples.to_string()) + .arg("-i0-1234567890") + .succeeds(); + result.no_stderr(); + + let result_seq: Vec = result + .stdout_str() + .split('\n') + .filter(|x| !x.is_empty()) + .map(|x| x.parse().unwrap()) + .collect(); + assert_eq!(result_seq.len(), num_samples, "Miscounted output length!"); + assert!( + result_seq.iter().all(|x| (0..=1234567890).contains(x)), + "Output includes element not from range: {}", + result.stdout_str() + ); +} + +#[test] +fn test_very_large_range_offset() { + let num_samples = 10; + let result = new_ucmd!() + .arg("-n") + .arg(&num_samples.to_string()) + .arg("-i1234567890-2147483647") + .succeeds(); + result.no_stderr(); + + let result_seq: Vec = result + .stdout_str() + .split('\n') + .filter(|x| !x.is_empty()) + .map(|x| x.parse().unwrap()) + .collect(); + assert_eq!(result_seq.len(), num_samples, "Miscounted output length!"); + assert!( + result_seq + .iter() + .all(|x| (1234567890..=2147483647).contains(x)), + "Output includes element not from range: {}", + result.stdout_str() + ); +} + +#[test] +fn test_range_repeat_no_overflow_1_max() { + let upper_bound = std::usize::MAX; + let result = new_ucmd!() + .arg("-rn1") + .arg(&format!("-i1-{upper_bound}")) + .succeeds(); + result.no_stderr(); + + let result_seq: Vec = result + .stdout_str() + .split('\n') + .filter(|x| !x.is_empty()) + .map(|x| x.parse().unwrap()) + .collect(); + assert_eq!(result_seq.len(), 1, "Miscounted output length!"); +} + +#[test] +fn test_range_repeat_no_overflow_0_max_minus_1() { + let upper_bound = std::usize::MAX - 1; + let result = new_ucmd!() + .arg("-rn1") + .arg(&format!("-i0-{upper_bound}")) + .succeeds(); + result.no_stderr(); + + let result_seq: Vec = result + .stdout_str() + .split('\n') + .filter(|x| !x.is_empty()) + .map(|x| x.parse().unwrap()) + .collect(); + assert_eq!(result_seq.len(), 1, "Miscounted output length!"); +} + +#[test] +fn test_range_permute_no_overflow_1_max() { + let upper_bound = std::usize::MAX; + let result = new_ucmd!() + .arg("-n1") + .arg(&format!("-i1-{upper_bound}")) + .succeeds(); + result.no_stderr(); + + let result_seq: Vec = result + .stdout_str() + .split('\n') + .filter(|x| !x.is_empty()) + .map(|x| x.parse().unwrap()) + .collect(); + assert_eq!(result_seq.len(), 1, "Miscounted output length!"); +} + +#[test] +fn test_range_permute_no_overflow_0_max_minus_1() { + let upper_bound = std::usize::MAX - 1; + let result = new_ucmd!() + .arg("-n1") + .arg(&format!("-i0-{upper_bound}")) + .succeeds(); + result.no_stderr(); + + let result_seq: Vec = result + .stdout_str() + .split('\n') + .filter(|x| !x.is_empty()) + .map(|x| x.parse().unwrap()) + .collect(); + assert_eq!(result_seq.len(), 1, "Miscounted output length!"); +} + +#[test] +fn test_range_permute_no_overflow_0_max() { + // NOTE: This is different from GNU shuf! + // GNU shuf accepts -i0-MAX-1 and -i1-MAX, but not -i0-MAX. + // This feels like a bug in GNU shuf. + let upper_bound = std::usize::MAX; + let result = new_ucmd!() + .arg("-n1") + .arg(&format!("-i0-{upper_bound}")) + .succeeds(); + result.no_stderr(); + + let result_seq: Vec = result + .stdout_str() + .split('\n') + .filter(|x| !x.is_empty()) + .map(|x| x.parse().unwrap()) + .collect(); + assert_eq!(result_seq.len(), 1, "Miscounted output length!"); +} + +#[test] +fn test_very_high_range_full() { + let input_seq = vec![ + 2147483641, 2147483642, 2147483643, 2147483644, 2147483645, 2147483646, 2147483647, + ]; + let result = new_ucmd!().arg("-i2147483641-2147483647").succeeds(); + result.no_stderr(); + + let mut result_seq: Vec = result + .stdout_str() + .split('\n') + .filter(|x| !x.is_empty()) + .map(|x| x.parse().unwrap()) + .collect(); + result_seq.sort_unstable(); + assert_eq!(result_seq, input_seq, "Output is not a permutation"); +} + +#[test] +fn test_range_repeat() { + let num_samples = 500; + let result = new_ucmd!() + .arg("-r") + .arg("-n") + .arg(&num_samples.to_string()) + .arg("-i12-34") + .succeeds(); + result.no_stderr(); + + let result_seq: Vec = result + .stdout_str() + .split('\n') + .filter(|x| !x.is_empty()) + .map(|x| x.parse().unwrap()) + .collect(); + assert_eq!(result_seq.len(), num_samples, "Miscounted output length!"); + assert!( + result_seq.iter().all(|x| (12..=34).contains(x)), + "Output includes element not from range: {}", + result.stdout_str() + ); +} + +#[test] +fn test_empty_input() { + let result = new_ucmd!().pipe_in(vec![]).succeeds(); + result.no_stderr(); + result.no_stdout(); +} + #[test] fn test_echo() { let input_seq = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; @@ -72,6 +305,57 @@ fn test_echo() { assert_eq!(result_seq, input_seq, "Output is not a permutation"); } +#[test] +fn test_echo_multi() { + let result = new_ucmd!() + .arg("-e") + .arg("a") + .arg("b") + .arg("-e") + .arg("c") + .succeeds(); + result.no_stderr(); + + let mut result_seq: Vec = result + .stdout_str() + .split('\n') + .filter(|x| !x.is_empty()) + .map(|x| x.into()) + .collect(); + result_seq.sort_unstable(); + assert_eq!(result_seq, ["a", "b", "c"], "Output is not a permutation"); +} + +#[test] +fn test_echo_postfix() { + let result = new_ucmd!().arg("a").arg("b").arg("c").arg("-e").succeeds(); + result.no_stderr(); + + let mut result_seq: Vec = result + .stdout_str() + .split('\n') + .filter(|x| !x.is_empty()) + .map(|x| x.into()) + .collect(); + result_seq.sort_unstable(); + assert_eq!(result_seq, ["a", "b", "c"], "Output is not a permutation"); +} + +#[test] +fn test_echo_short_collapsed_zero() { + let result = new_ucmd!().arg("-ez").arg("a").arg("b").arg("c").succeeds(); + result.no_stderr(); + + let mut result_seq: Vec = result + .stdout_str() + .split('\0') + .filter(|x| !x.is_empty()) + .map(|x| x.parse().unwrap()) + .collect(); + result_seq.sort_unstable(); + assert_eq!(result_seq, ["a", "b", "c"], "Output is not a permutation"); +} + #[test] fn test_head_count() { let repeat_limit = 5; @@ -103,6 +387,145 @@ fn test_head_count() { ); } +#[test] +fn test_zero_head_count_pipe() { + let result = new_ucmd!().arg("-n0").pipe_in(vec![]).succeeds(); + // Output must be completely empty, not even a newline! + result.no_output(); +} + +#[test] +fn test_zero_head_count_pipe_explicit() { + let result = new_ucmd!().arg("-n0").arg("-").pipe_in(vec![]).succeeds(); + result.no_output(); +} + +#[test] +fn test_zero_head_count_file_unreadable() { + new_ucmd!() + .arg("-n0") + .arg("/invalid/unreadable") + .pipe_in(vec![]) + .succeeds() + .no_output(); +} + +#[test] +fn test_zero_head_count_file_touch_output_negative() { + new_ucmd!() + .arg("-n0") + .arg("-o") + .arg("/invalid/unwritable") + .pipe_in(vec![]) + .fails() + .stderr_contains("failed to open '/invalid/unwritable' for writing:"); +} + +#[test] +fn test_zero_head_count_file_touch_output_positive_new() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["-n0", "-o", "file"]).succeeds().no_output(); + assert_eq!( + at.read_bytes("file"), + Vec::new(), + "Output file must exist and be completely empty" + ); +} + +#[test] +fn test_zero_head_count_file_touch_output_positive_existing() { + let (at, mut ucmd) = at_and_ucmd!(); + at.touch("file"); + ucmd.args(&["-n0", "-o", "file"]).succeeds().no_output(); + assert_eq!( + at.read_bytes("file"), + Vec::new(), + "Output file must exist and be completely empty" + ); +} + +#[test] +fn test_zero_head_count_echo() { + new_ucmd!() + .arg("-n0") + .arg("-e") + .arg("hello") + .pipe_in(vec![]) + .succeeds() + .no_output(); +} + +#[test] +fn test_zero_head_count_range() { + new_ucmd!().arg("-n0").arg("-i4-8").succeeds().no_output(); +} + +#[test] +fn test_head_count_multi_big_then_small() { + let repeat_limit = 5; + let input_seq = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + let input = input_seq + .iter() + .map(ToString::to_string) + .collect::>() + .join("\n"); + + let result = new_ucmd!() + .arg("-n") + .arg(&(repeat_limit + 1).to_string()) + .arg("-n") + .arg(&repeat_limit.to_string()) + .pipe_in(input.as_bytes()) + .succeeds(); + result.no_stderr(); + + let result_seq: Vec = result + .stdout_str() + .split('\n') + .filter(|x| !x.is_empty()) + .map(|x| x.parse().unwrap()) + .collect(); + assert_eq!(result_seq.len(), repeat_limit, "Output is not limited"); + assert!( + result_seq.iter().all(|x| input_seq.contains(x)), + "Output includes element not from input: {}", + result.stdout_str() + ); +} + +#[test] +fn test_head_count_multi_small_then_big() { + let repeat_limit = 5; + let input_seq = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + let input = input_seq + .iter() + .map(ToString::to_string) + .collect::>() + .join("\n"); + + let result = new_ucmd!() + .arg("-n") + .arg(&repeat_limit.to_string()) + .arg("-n") + .arg(&(repeat_limit + 1).to_string()) + .pipe_in(input.as_bytes()) + .succeeds(); + result.no_stderr(); + + let result_seq: Vec = result + .stdout_str() + .split('\n') + .filter(|x| !x.is_empty()) + .map(|x| x.parse().unwrap()) + .collect(); + assert_eq!(result_seq.len(), repeat_limit, "Output is not limited"); + assert!( + result_seq.iter().all(|x| input_seq.contains(x)), + "Output includes element not from input: {}", + result.stdout_str() + ); +} + #[test] fn test_repeat() { let repeat_limit = 15000; @@ -141,6 +564,45 @@ fn test_repeat() { ); } +#[test] +fn test_repeat_multi() { + let repeat_limit = 15000; + let input_seq = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + let input = input_seq + .iter() + .map(ToString::to_string) + .collect::>() + .join("\n"); + + let result = new_ucmd!() + .arg("-r") + .arg("-r") // The only difference to test_repeat() + .args(&["-n", &repeat_limit.to_string()]) + .pipe_in(input.as_bytes()) + .succeeds(); + result.no_stderr(); + + let result_seq: Vec = result + .stdout_str() + .split('\n') + .filter(|x| !x.is_empty()) + .map(|x| x.parse().unwrap()) + .collect(); + assert_eq!( + result_seq.len(), + repeat_limit, + "Output is not repeating forever" + ); + assert!( + result_seq.iter().all(|x| input_seq.contains(x)), + "Output includes element not from input: {:?}", + result_seq + .iter() + .filter(|x| !input_seq.contains(x)) + .collect::>() + ); +} + #[test] fn test_file_input() { let expected_seq = vec![11, 12, 13, 14, 15, 16, 17, 18, 19, 20]; @@ -198,6 +660,40 @@ fn test_shuf_invalid_input_range_three() { .stderr_contains("invalid input range: 'b'"); } +#[test] +fn test_shuf_multiple_input_ranges() { + new_ucmd!() + .args(&["-i", "2-9", "-i", "2-9"]) + .fails() + .stderr_contains("--input-range") + .stderr_contains("cannot be used multiple times"); +} + +#[test] +fn test_shuf_multiple_outputs() { + new_ucmd!() + .args(&["-o", "file_a", "-o", "file_b"]) + .fails() + .stderr_contains("--output") + .stderr_contains("cannot be used multiple times"); +} + +#[test] +fn test_shuf_two_input_files() { + new_ucmd!() + .args(&["file_a", "file_b"]) + .fails() + .stderr_contains("unexpected argument 'file_b' found"); +} + +#[test] +fn test_shuf_three_input_files() { + new_ucmd!() + .args(&["file_a", "file_b", "file_c"]) + .fails() + .stderr_contains("unexpected argument 'file_b' found"); +} + #[test] fn test_shuf_invalid_input_line_count() { new_ucmd!() @@ -221,3 +717,84 @@ fn test_shuf_multiple_input_line_count() { .count(); assert_eq!(result_count, 5, "Output should have 5 items"); } + +#[test] +fn test_shuf_repeat_empty_range() { + new_ucmd!() + .arg("-ri4-3") + .fails() + .no_stdout() + .stderr_only("shuf: no lines to repeat\n"); +} + +#[test] +fn test_shuf_repeat_empty_echo() { + new_ucmd!() + .arg("-re") + .fails() + .no_stdout() + .stderr_only("shuf: no lines to repeat\n"); +} + +#[test] +fn test_shuf_repeat_empty_input() { + new_ucmd!() + .arg("-r") + .pipe_in("") + .fails() + .no_stdout() + .stderr_only("shuf: no lines to repeat\n"); +} + +#[test] +fn test_range_one_elem() { + new_ucmd!() + .arg("-i5-5") + .succeeds() + .no_stderr() + .stdout_only("5\n"); +} + +#[test] +fn test_range_empty() { + new_ucmd!().arg("-i5-4").succeeds().no_output(); +} + +#[test] +fn test_range_empty_minus_one() { + new_ucmd!() + .arg("-i5-3") + .fails() + .no_stdout() + .stderr_only("shuf: invalid input range: '5-3'\n"); +} + +#[test] +fn test_range_repeat_one_elem() { + new_ucmd!() + .arg("-n1") + .arg("-ri5-5") + .succeeds() + .no_stderr() + .stdout_only("5\n"); +} + +#[test] +fn test_range_repeat_empty() { + new_ucmd!() + .arg("-n1") + .arg("-ri5-4") + .fails() + .no_stdout() + .stderr_only("shuf: no lines to repeat\n"); +} + +#[test] +fn test_range_repeat_empty_minus_one() { + new_ucmd!() + .arg("-n1") + .arg("-ri5-3") + .fails() + .no_stdout() + .stderr_only("shuf: invalid input range: '5-3'\n"); +} diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index 97c72c7b19b..2e114348b46 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -38,20 +38,20 @@ fn test_buffer_sizes() { .arg("ext_sort.txt") .succeeds() .stdout_is_fixture("ext_sort.expected"); + } - #[cfg(not(target_pointer_width = "32"))] - { - let buffer_sizes = ["1000G", "10T"]; - for buffer_size in &buffer_sizes { - TestScenario::new(util_name!()) - .ucmd() - .arg("-n") - .arg("-S") - .arg(buffer_size) - .arg("ext_sort.txt") - .succeeds() - .stdout_is_fixture("ext_sort.expected"); - } + #[cfg(not(target_pointer_width = "32"))] + { + let buffer_sizes = ["1000G", "10T"]; + for buffer_size in &buffer_sizes { + TestScenario::new(util_name!()) + .ucmd() + .arg("-n") + .arg("-S") + .arg(buffer_size) + .arg("ext_sort.txt") + .succeeds() + .stdout_is_fixture("ext_sort.expected"); } } } @@ -813,8 +813,6 @@ fn test_check_silent() { #[test] fn test_check_unique() { - // Due to a clap bug the combination "-cu" does not work. "-c -u" works. - // See https://github.com/clap-rs/clap/issues/2624 new_ucmd!() .args(&["-c", "-u"]) .pipe_in("A\nA\n") @@ -823,6 +821,16 @@ fn test_check_unique() { .stderr_only("sort: -:2: disorder: A\n"); } +#[test] +fn test_check_unique_combined() { + new_ucmd!() + .args(&["-cu"]) + .pipe_in("A\nA\n") + .fails() + .code_is(1) + .stderr_only("sort: -:2: disorder: A\n"); +} + #[test] fn test_dictionary_and_nonprinting_conflicts() { let conflicting_args = ["n", "h", "g", "M"]; diff --git a/tests/by-util/test_split.rs b/tests/by-util/test_split.rs index acb8ab56140..64eccf81ad8 100644 --- a/tests/by-util/test_split.rs +++ b/tests/by-util/test_split.rs @@ -123,6 +123,15 @@ fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails().code_is(1); } +#[test] +fn test_split_non_existing_file() { + new_ucmd!() + .arg("non-existing") + .fails() + .code_is(1) + .stderr_is("split: cannot open 'non-existing' for reading: No such file or directory\n"); +} + #[test] fn test_split_default() { let (at, mut ucmd) = at_and_ucmd!(); @@ -1068,6 +1077,7 @@ fn test_split_number_oversized_stdin() { new_ucmd!() .args(&["--number=3", "---io-blksize=600"]) .pipe_in_fixture("sixhundredfiftyonebytes.txt") + .ignore_stdin_write_error() .fails() .stderr_only("split: -: cannot determine input size\n"); } diff --git a/tests/by-util/test_stat.rs b/tests/by-util/test_stat.rs index e918d54e9c7..189b1d44109 100644 --- a/tests/by-util/test_stat.rs +++ b/tests/by-util/test_stat.rs @@ -338,3 +338,10 @@ fn test_stdin_redirect() { .stdout_contains("File: -") .succeeded(); } + +#[test] +fn test_without_argument() { + new_ucmd!() + .fails() + .stderr_contains("missing operand\nTry 'stat --help' for more information."); +} diff --git a/tests/by-util/test_tee.rs b/tests/by-util/test_tee.rs index 34076bbf9b1..c186682785a 100644 --- a/tests/by-util/test_tee.rs +++ b/tests/by-util/test_tee.rs @@ -77,7 +77,7 @@ fn test_tee_no_more_writeable_1() { // equals to 'tee /dev/full out2 FileTime { let tm = chrono::NaiveDateTime::parse_from_str(s, format).unwrap(); - FileTime::from_unix_time(tm.timestamp(), tm.timestamp_subsec_nanos()) + FileTime::from_unix_time(tm.and_utc().timestamp(), tm.timestamp_subsec_nanos()) } #[test] @@ -846,13 +848,48 @@ fn test_touch_dash() { } #[test] -// Chrono panics for now -#[ignore] fn test_touch_invalid_date_format() { let (_at, mut ucmd) = at_and_ucmd!(); let file = "test_touch_invalid_date_format"; ucmd.args(&["-m", "-t", "+1000000000000 years", file]) .fails() - .stderr_contains("touch: invalid date format ‘+1000000000000 years’"); + .stderr_contains("touch: invalid date format '+1000000000000 years'"); +} + +#[test] +#[cfg(not(target_os = "freebsd"))] +fn test_touch_symlink_with_no_deref() { + let (at, mut ucmd) = at_and_ucmd!(); + let target = "foo.txt"; + let symlink = "bar.txt"; + let time = FileTime::from_unix_time(123, 0); + + at.touch(target); + at.relative_symlink_file(target, symlink); + set_symlink_file_times(at.plus(symlink), time, time).unwrap(); + + ucmd.args(&["-a", "--no-dereference", symlink]).succeeds(); + // Modification time shouldn't be set to the destination's modification time + assert_eq!(time, get_symlink_times(&at, symlink).1); +} + +#[test] +#[cfg(not(target_os = "freebsd"))] +fn test_touch_reference_symlink_with_no_deref() { + let (at, mut ucmd) = at_and_ucmd!(); + let target = "foo.txt"; + let symlink = "bar.txt"; + let arg = "baz.txt"; + let time = FileTime::from_unix_time(123, 0); + + at.touch(target); + at.relative_symlink_file(target, symlink); + set_symlink_file_times(at.plus(symlink), time, time).unwrap(); + at.touch(arg); + + ucmd.args(&["--reference", symlink, "--no-dereference", arg]) + .succeeds(); + // Times should be taken from the symlink, not the destination + assert_eq!((time, time), get_symlink_times(&at, arg)); } diff --git a/tests/by-util/test_tr.rs b/tests/by-util/test_tr.rs index 01d062cab7b..6adbc4022a0 100644 --- a/tests/by-util/test_tr.rs +++ b/tests/by-util/test_tr.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 aabbaa aabbcc aabc abbb abcc abcdefabcdef abcdefghijk abcdefghijklmn abcdefghijklmnop ABCDEFGHIJKLMNOPQRS abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ ABCDEFZZ abcxyz ABCXYZ abcxyzabcxyz ABCXYZABCXYZ acbdef alnum amzamz AMZXAMZ bbbd cclass cefgm cntrl compl dabcdef dncase Gzabcdefg PQRST upcase wxyzz xdigit xycde xyyye xyyz xyzzzzxyzzzz ZABCDEF Zamz Cdefghijkl Cdefghijklmn +// spell-checker:ignore aabbaa aabbcc aabc abbb abbbcddd abcc abcdefabcdef abcdefghijk abcdefghijklmn abcdefghijklmnop ABCDEFGHIJKLMNOPQRS abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ ABCDEFZZ abcxyz ABCXYZ abcxyzabcxyz ABCXYZABCXYZ acbdef alnum amzamz AMZXAMZ bbbd cclass cefgm cntrl compl dabcdef dncase Gzabcdefg PQRST upcase wxyzz xdigit XXXYYY xycde xyyye xyyz xyzzzzxyzzzz ZABCDEF Zamz Cdefghijkl Cdefghijklmn use crate::common::util::TestScenario; #[test] @@ -46,6 +46,32 @@ fn test_delete() { .stdout_is("BD"); } +#[test] +fn test_delete_afterwards_is_not_flag() { + new_ucmd!() + .args(&["a-z", "-d"]) + .pipe_in("aBcD") + .succeeds() + .stdout_is("-BdD"); +} + +#[test] +fn test_delete_multi() { + new_ucmd!() + .args(&["-d", "-d", "a-z"]) + .pipe_in("aBcD") + .succeeds() + .stdout_is("BD"); +} + +#[test] +fn test_delete_late() { + new_ucmd!() + .args(&["-d", "a-z", "-d"]) + .fails() + .stderr_contains("extra operand '-d'"); +} + #[test] fn test_delete_complement() { new_ucmd!() @@ -78,6 +104,14 @@ fn test_complement1() { .stdout_is("aX"); } +#[test] +fn test_complement_afterwards_is_not_flag() { + new_ucmd!() + .args(&["a", "X", "-c"]) + .fails() + .stderr_contains("extra operand '-c'"); +} + #[test] fn test_complement2() { new_ucmd!() @@ -118,12 +152,46 @@ fn test_complement5() { .stdout_is("0a1b2c3"); } +#[test] +fn test_complement_multi_early() { + new_ucmd!() + .args(&["-c", "-c", "a", "X"]) + .pipe_in("ab") + .succeeds() + .stdout_is("aX"); +} + +#[test] +fn test_complement_multi_middle() { + new_ucmd!() + .args(&["-c", "a", "-c", "X"]) + .fails() + .stderr_contains("tr: extra operand 'X'"); +} + +#[test] +fn test_complement_multi_late() { + new_ucmd!() + .args(&["-c", "a", "X", "-c"]) + .fails() + .stderr_contains("tr: extra operand '-c'"); +} + #[test] fn test_squeeze() { new_ucmd!() .args(&["-s", "a-z"]) .pipe_in("aaBBcDcc") - .run() + .succeeds() + .stdout_is("aBBcDc"); +} + +#[test] +fn test_squeeze_multi() { + new_ucmd!() + .args(&["-ss", "-s", "a-z"]) + .pipe_in("aaBBcDcc") + .succeeds() .stdout_is("aBBcDc"); } @@ -132,7 +200,16 @@ fn test_squeeze_complement() { new_ucmd!() .args(&["-sc", "a-z"]) .pipe_in("aaBBcDcc") - .run() + .succeeds() + .stdout_is("aaBcDcc"); +} + +#[test] +fn test_squeeze_complement_multi() { + new_ucmd!() + .args(&["-scsc", "a-z"]) // spell-checker:disable-line + .pipe_in("aaBBcDcc") + .succeeds() .stdout_is("aaBcDcc"); } @@ -163,6 +240,15 @@ fn test_translate_and_squeeze_multiple_lines() { .stdout_is("yaay\nyaay"); // spell-checker:disable-line } +#[test] +fn test_delete_and_squeeze_one_set() { + new_ucmd!() + .args(&["-ds", "a-z"]) + .fails() + .stderr_contains("missing operand after 'a-z'") + .stderr_contains("Two strings must be given when deleting and squeezing."); +} + #[test] fn test_delete_and_squeeze() { new_ucmd!() @@ -181,6 +267,15 @@ fn test_delete_and_squeeze_complement() { .stdout_is("abc"); } +#[test] +fn test_delete_and_squeeze_complement_squeeze_set2() { + new_ucmd!() + .args(&["-dsc", "abX", "XYZ"]) + .pipe_in("abbbcdddXXXYYY") + .succeeds() + .stdout_is("abbbX"); +} + #[test] fn test_set1_longer_than_set2() { new_ucmd!() @@ -205,7 +300,16 @@ fn test_truncate() { new_ucmd!() .args(&["-t", "abc", "xy"]) .pipe_in("abcde") - .run() + .succeeds() + .stdout_is("xycde"); // spell-checker:disable-line +} + +#[test] +fn test_truncate_multi() { + new_ucmd!() + .args(&["-tt", "-t", "abc", "xy"]) + .pipe_in("abcde") + .succeeds() .stdout_is("xycde"); // spell-checker:disable-line } @@ -1147,3 +1251,35 @@ fn check_against_gnu_tr_tests_no_abort_1() { .succeeds() .stdout_is("abb"); } + +#[test] +fn test_delete_flag_takes_only_one_operand() { + // gnu tr -d fails with more than 1 argument + new_ucmd!().args(&["-d", "a", "p"]).fails().stderr_contains( + "extra operand 'p'\nOnly one string may be given when deleting without squeezing repeats.", + ); +} + +#[test] +fn test_truncate_flag_fails_with_more_than_two_operand() { + new_ucmd!() + .args(&["-t", "a", "b", "c"]) + .fails() + .stderr_contains("extra operand 'c'"); +} + +#[test] +fn test_squeeze_flag_fails_with_more_than_two_operand() { + new_ucmd!() + .args(&["-s", "a", "b", "c"]) + .fails() + .stderr_contains("extra operand 'c'"); +} + +#[test] +fn test_complement_flag_fails_with_more_than_two_operand() { + new_ucmd!() + .args(&["-c", "a", "b", "c"]) + .fails() + .stderr_contains("extra operand 'c'"); +} diff --git a/tests/by-util/test_truncate.rs b/tests/by-util/test_truncate.rs index 81b87ed2e3c..e6a128186e9 100644 --- a/tests/by-util/test_truncate.rs +++ b/tests/by-util/test_truncate.rs @@ -43,9 +43,6 @@ fn test_reference() { let mut file = at.make_file(FILE2); // manpage: "A FILE argument that does not exist is created." - // TODO: 'truncate' does not create the file in this case, - // but should because '--no-create' wasn't specified. - at.touch(FILE1); // TODO: remove this when 'no-create' is fixed scene.ucmd().arg("-s").arg("+5KB").arg(FILE1).succeeds(); scene @@ -240,10 +237,9 @@ fn test_reference_with_size_file_not_found() { #[test] fn test_truncate_bytes_size() { - // TODO: this should succeed without error, uncomment when '--no-create' is fixed - // new_ucmd!() - // .args(&["--no-create", "--size", "K", "file"]) - // .succeeds(); + new_ucmd!() + .args(&["--no-create", "--size", "K", "file"]) + .succeeds(); new_ucmd!() .args(&["--size", "1024R", "file"]) .fails() @@ -270,9 +266,39 @@ fn test_new_file() { assert_eq!(at.read_bytes(filename), vec![b'\0'; 8]); } +/// Test that truncating a non-existent file creates that file, even in reference-mode. +#[test] +fn test_new_file_reference() { + let (at, mut ucmd) = at_and_ucmd!(); + let mut old_file = at.make_file(FILE1); + old_file.write_all(b"1234567890").unwrap(); + let filename = "new_file_that_does_not_exist_yet"; + ucmd.args(&["-r", FILE1, filename]) + .succeeds() + .no_stdout() + .no_stderr(); + assert!(at.file_exists(filename)); + assert_eq!(at.read_bytes(filename), vec![b'\0'; 10]); +} + +/// Test that truncating a non-existent file creates that file, even in size-and-reference-mode. +#[test] +fn test_new_file_size_and_reference() { + let (at, mut ucmd) = at_and_ucmd!(); + let mut old_file = at.make_file(FILE1); + old_file.write_all(b"1234567890").unwrap(); + let filename = "new_file_that_does_not_exist_yet"; + ucmd.args(&["-s", "+3", "-r", FILE1, filename]) + .succeeds() + .no_stdout() + .no_stderr(); + assert!(at.file_exists(filename)); + assert_eq!(at.read_bytes(filename), vec![b'\0'; 13]); +} + /// Test for not creating a non-existent file. #[test] -fn test_new_file_no_create() { +fn test_new_file_no_create_size_only() { let (at, mut ucmd) = at_and_ucmd!(); let filename = "new_file_that_does_not_exist_yet"; ucmd.args(&["-s", "8", "-c", filename]) @@ -282,6 +308,34 @@ fn test_new_file_no_create() { assert!(!at.file_exists(filename)); } +/// Test for not creating a non-existent file. +#[test] +fn test_new_file_no_create_reference_only() { + let (at, mut ucmd) = at_and_ucmd!(); + let mut old_file = at.make_file(FILE1); + old_file.write_all(b"1234567890").unwrap(); + let filename = "new_file_that_does_not_exist_yet"; + ucmd.args(&["-r", FILE1, "-c", filename]) + .succeeds() + .no_stdout() + .no_stderr(); + assert!(!at.file_exists(filename)); +} + +/// Test for not creating a non-existent file. +#[test] +fn test_new_file_no_create_size_and_reference() { + let (at, mut ucmd) = at_and_ucmd!(); + let mut old_file = at.make_file(FILE1); + old_file.write_all(b"1234567890").unwrap(); + let filename = "new_file_that_does_not_exist_yet"; + ucmd.args(&["-r", FILE1, "-s", "+8", "-c", filename]) + .succeeds() + .no_stdout() + .no_stderr(); + assert!(!at.file_exists(filename)); +} + #[test] fn test_division_by_zero_size_only() { new_ucmd!() diff --git a/tests/by-util/test_uniq.rs b/tests/by-util/test_uniq.rs index aa41de8274d..dd055402f2f 100644 --- a/tests/by-util/test_uniq.rs +++ b/tests/by-util/test_uniq.rs @@ -2,10 +2,10 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use std::io::Write; -// spell-checker:ignore nabcd +// spell-checker:ignore nabcd badoption schar use crate::common::util::TestScenario; +use uucore::posix::OBSOLETE; static INPUT: &str = "sorted.txt"; static OUTPUT: &str = "sorted-output.txt"; @@ -118,10 +118,10 @@ fn test_stdin_skip_21_fields_obsolete() { #[test] fn test_stdin_skip_invalid_fields_obsolete() { new_ucmd!() - .args(&["-5deadbeef"]) + .args(&["-5q"]) .run() .failure() - .stderr_only("uniq: Invalid argument for skip-fields: 5deadbeef\n"); + .stderr_contains("error: unexpected argument '-q' found\n"); } #[test] @@ -138,8 +138,7 @@ fn test_all_repeated_followed_by_filename() { let filename = "test.txt"; let (at, mut ucmd) = at_and_ucmd!(); - let mut file = at.make_file(filename); - file.write_all(b"a\na\n").unwrap(); + at.write(filename, "a\na\n"); ucmd.args(&["--all-repeated", filename]) .run() @@ -202,14 +201,13 @@ fn test_stdin_zero_terminated() { } #[test] -fn test_invalid_utf8() { +fn test_gnu_locale_fr_schar() { new_ucmd!() - .arg("not-utf8-sequence.txt") + .args(&["-f1", "locale-fr-schar.txt"]) + .env("LC_ALL", "C") .run() - .failure() - .stderr_only( - "uniq: failed to convert line to utf8: invalid utf-8 sequence of 1 bytes from index 0\n", - ); + .success() + .stdout_is_fixture_bytes("locale-fr-schar.txt"); } #[test] @@ -226,8 +224,7 @@ fn test_group_followed_by_filename() { let filename = "test.txt"; let (at, mut ucmd) = at_and_ucmd!(); - let mut file = at.make_file(filename); - file.write_all(b"a\na\n").unwrap(); + at.write(filename, "a\na\n"); ucmd.args(&["--group", filename]) .run() @@ -521,23 +518,23 @@ fn gnu_tests() { stderr: None, exit: None, }, - // // Obsolete syntax for "-s 1" - // TestCase { - // name: "obs-plus40", - // args: &["+1"], - // input: "aaa\naaa\n", - // stdout: Some("aaa\n"), - // stderr: None, - // exit: None, - // }, - // TestCase { - // name: "obs-plus41", - // args: &["+1"], - // input: "baa\naaa\n", - // stdout: Some("baa\n"), - // stderr: None, - // exit: None, - // }, + // Obsolete syntax for "-s 1" + TestCase { + name: "obs-plus40", + args: &["+1"], + input: "aaa\naaa\n", + stdout: Some("aaa\n"), + stderr: None, + exit: None, + }, + TestCase { + name: "obs-plus41", + args: &["+1"], + input: "baa\naaa\n", + stdout: Some("baa\n"), + stderr: None, + exit: None, + }, TestCase { name: "42", args: &["-s", "1"], @@ -554,7 +551,6 @@ fn gnu_tests() { stderr: None, exit: None, }, - /* // Obsolete syntax for "-s 1" TestCase { name: "obs-plus44", @@ -572,7 +568,6 @@ fn gnu_tests() { stderr: None, exit: None, }, - */ TestCase { name: "50", args: &["-f", "1", "-s", "1"], @@ -757,17 +752,17 @@ fn gnu_tests() { stderr: None, exit: None, }, - /* - Disable as it fails too often. See: - https://github.com/uutils/coreutils/issues/3509 TestCase { name: "112", args: &["-D", "-c"], - input: "a a\na b\n", + input: "", // Note: Different from GNU test, but should not matter stdout: Some(""), - stderr: Some("uniq: printing all duplicated lines and repeat counts is meaningless"), + stderr: Some(concat!( + "uniq: printing all duplicated lines and repeat counts is meaningless\n", + "Try 'uniq --help' for more information.\n" + )), exit: Some(1), - },*/ + }, TestCase { name: "113", args: &["--all-repeated=separate"], @@ -816,6 +811,21 @@ fn gnu_tests() { stderr: None, exit: None, }, + TestCase { + name: "119", + args: &["--all-repeated=badoption"], + input: "", // Note: Different from GNU test, but should not matter + stdout: Some(""), + stderr: Some(concat!( + "uniq: invalid argument 'badoption' for '--all-repeated'\n", + "Valid arguments are:\n", + " - 'none'\n", + " - 'prepend'\n", + " - 'separate'\n", + "Try 'uniq --help' for more information.\n" + )), + exit: Some(1), + }, // \x08 is the backspace char TestCase { name: "120", @@ -825,6 +835,16 @@ fn gnu_tests() { stderr: None, exit: None, }, + // u128::MAX = 340282366920938463463374607431768211455 + TestCase { + name: "121", + args: &["-d", "-u", "-w340282366920938463463374607431768211456"], + input: "a\na\n\x08", + stdout: Some(""), + stderr: None, + exit: None, + }, + // Test 122 is the same as 121, just different big int overflow number TestCase { name: "123", args: &["--zero-terminated"], @@ -969,16 +989,108 @@ fn gnu_tests() { stderr: None, exit: None, }, + TestCase { + name: "141", + args: &["--group", "-c"], + input: "", + stdout: Some(""), + stderr: Some(concat!( + "uniq: --group is mutually exclusive with -c/-d/-D/-u\n", + "Try 'uniq --help' for more information.\n" + )), + exit: Some(1), + }, + TestCase { + name: "142", + args: &["--group", "-d"], + input: "", + stdout: Some(""), + stderr: Some(concat!( + "uniq: --group is mutually exclusive with -c/-d/-D/-u\n", + "Try 'uniq --help' for more information.\n" + )), + exit: Some(1), + }, + TestCase { + name: "143", + args: &["--group", "-u"], + input: "", + stdout: Some(""), + stderr: Some(concat!( + "uniq: --group is mutually exclusive with -c/-d/-D/-u\n", + "Try 'uniq --help' for more information.\n" + )), + exit: Some(1), + }, + TestCase { + name: "144", + args: &["--group", "-D"], + input: "", + stdout: Some(""), + stderr: Some(concat!( + "uniq: --group is mutually exclusive with -c/-d/-D/-u\n", + "Try 'uniq --help' for more information.\n" + )), + exit: Some(1), + }, + TestCase { + name: "145", + args: &["--group=badoption"], + input: "", + stdout: Some(""), + stderr: Some(concat!( + "uniq: invalid argument 'badoption' for '--group'\n", + "Valid arguments are:\n", + " - 'prepend'\n", + " - 'append'\n", + " - 'separate'\n", + " - 'both'\n", + "Try 'uniq --help' for more information.\n" + )), + exit: Some(1), + }, ]; + // run regular version of tests with regular file as input for case in cases { + // prep input file + let (at, mut ucmd) = at_and_ucmd!(); + at.write("input-file", case.input); + + // first - run a version of tests with regular file as input eprintln!("Test {}", case.name); - let result = new_ucmd!().args(case.args).run_piped_stdin(case.input); + // set environment variable for obsolete skip char option tests + if case.name.starts_with("obs-plus") { + ucmd.env("_POSIX2_VERSION", OBSOLETE.to_string()); + } + let result = ucmd.args(case.args).arg("input-file").run(); + if let Some(stdout) = case.stdout { + result.stdout_is(stdout); + } + if let Some(stderr) = case.stderr { + result.stderr_is(stderr); + } + if let Some(exit) = case.exit { + result.code_is(exit); + } + + // then - ".stdin" version of tests with input piped in + // NOTE: GNU has another variant for stdin redirect from a file + // as in `uniq < input-file` + // For now we treat it as equivalent of piped in stdin variant + // as in `cat input-file | uniq` + eprintln!("Test {}.stdin", case.name); + // set environment variable for obsolete skip char option tests + let mut ucmd = new_ucmd!(); + if case.name.starts_with("obs-plus") { + ucmd.env("_POSIX2_VERSION", OBSOLETE.to_string()); + } + let result = ucmd.args(case.args).run_piped_stdin(case.input); if let Some(stdout) = case.stdout { result.stdout_is(stdout); } if let Some(stderr) = case.stderr { - result.stderr_contains(stderr); + result.stderr_is(stderr); } if let Some(exit) = case.exit { result.code_is(exit); diff --git a/tests/common/random.rs b/tests/common/random.rs index 42b6eaa7703..82a58578eef 100644 --- a/tests/common/random.rs +++ b/tests/common/random.rs @@ -208,6 +208,7 @@ mod tests { } #[test] + #[allow(clippy::cognitive_complexity)] // Ignore clippy lint of too long function sign fn test_random_string_generate_with_delimiter_should_end_with_delimiter() { let random_string = RandomString::generate_with_delimiter(Alphanumeric, 0, 1, true, 1); assert_eq!(1, random_string.len()); @@ -251,6 +252,7 @@ mod tests { } #[test] + #[allow(clippy::cognitive_complexity)] // Ignore clippy lint of too long function sign fn test_random_string_generate_with_delimiter_should_not_end_with_delimiter() { let random_string = RandomString::generate_with_delimiter(Alphanumeric, 0, 0, false, 1); assert_eq!(1, random_string.len()); diff --git a/tests/common/util.rs b/tests/common/util.rs index 9055c238e08..19ef8317af2 100644 --- a/tests/common/util.rs +++ b/tests/common/util.rs @@ -3,16 +3,18 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -//spell-checker: ignore (linux) rlimit prlimit coreutil ggroups uchild uncaptured scmd SHLVL canonicalized +//spell-checker: ignore (linux) rlimit prlimit coreutil ggroups uchild uncaptured scmd SHLVL canonicalized openpty +//spell-checker: ignore (linux) winsize xpixel ypixel setrlimit FSIZE #![allow(dead_code)] +#[cfg(unix)] +use nix::pty::OpenptyResult; use pretty_assertions::assert_eq; -#[cfg(any(target_os = "linux", target_os = "android"))] -use rlimit::prlimit; +#[cfg(unix)] +use rlimit::setrlimit; #[cfg(feature = "sleep")] use rstest::rstest; -#[cfg(unix)] use std::borrow::Cow; use std::collections::VecDeque; #[cfg(not(windows))] @@ -21,20 +23,24 @@ use std::ffi::{OsStr, OsString}; use std::fs::{self, hard_link, remove_file, File, OpenOptions}; use std::io::{self, BufWriter, Read, Result, Write}; #[cfg(unix)] +use std::os::fd::OwnedFd; +#[cfg(unix)] use std::os::unix::fs::{symlink as symlink_dir, symlink as symlink_file, PermissionsExt}; #[cfg(unix)] +use std::os::unix::process::CommandExt; +#[cfg(unix)] use std::os::unix::process::ExitStatusExt; #[cfg(windows)] use std::os::windows::fs::{symlink_dir, symlink_file}; #[cfg(windows)] -use std::path::MAIN_SEPARATOR; +use std::path::MAIN_SEPARATOR_STR; use std::path::{Path, PathBuf}; use std::process::{Child, Command, ExitStatus, Output, Stdio}; use std::rc::Rc; use std::sync::mpsc::{self, RecvTimeoutError}; use std::thread::{sleep, JoinHandle}; use std::time::{Duration, Instant}; -use std::{env, hint, thread}; +use std::{env, hint, mem, thread}; use tempfile::{Builder, TempDir}; static TESTS_DIR: &str = "tests"; @@ -46,6 +52,7 @@ static ALREADY_RUN: &str = " you have already run this UCommand, if you want to static MULTIPLE_STDIN_MEANINGLESS: &str = "Ucommand is designed around a typical use case of: provide args and input stream -> spawn process -> block until completion -> return output streams. For verifying that a particular section of the input stream is what causes a particular behavior, use the Command type directly."; static NO_STDIN_MEANINGLESS: &str = "Setting this flag has no effect if there is no stdin"; +static END_OF_TRANSMISSION_SEQUENCE: &[u8] = &[b'\n', 0x04]; pub const TESTS_BINARY: &str = env!("CARGO_BIN_EXE_coreutils"); pub const PATH: &str = env!("PATH"); @@ -344,6 +351,11 @@ impl CmdResult { std::str::from_utf8(&self.stderr).unwrap() } + /// Returns the program's standard error as a string slice, automatically handling invalid utf8 + pub fn stderr_str_lossy(&self) -> Cow<'_, str> { + String::from_utf8_lossy(&self.stderr) + } + /// Returns the program's standard error as a string /// consumes self pub fn stderr_move_str(self) -> String { @@ -364,6 +376,14 @@ impl CmdResult { #[track_caller] pub fn code_is(&self, expected_code: i32) -> &Self { + let fails = self.code() != expected_code; + if fails { + eprintln!( + "stdout:\n{}\nstderr:\n{}", + self.stdout_str(), + self.stderr_str() + ); + } assert_eq!(self.code(), expected_code); self } @@ -387,7 +407,8 @@ impl CmdResult { pub fn success(&self) -> &Self { assert!( self.succeeded(), - "Command was expected to succeed.\nstdout = {}\n stderr = {}", + "Command was expected to succeed. code: {}\nstdout = {}\n stderr = {}", + self.code(), self.stdout_str(), self.stderr_str() ); @@ -864,7 +885,6 @@ impl AtPath { pub fn append(&self, name: &str, contents: &str) { log_info("write(append)", self.plus_as_string(name)); let mut f = OpenOptions::new() - .write(true) .append(true) .create(true) .open(self.plus(name)) @@ -876,7 +896,6 @@ impl AtPath { pub fn append_bytes(&self, name: &str, contents: &[u8]) { log_info("write(append)", self.plus_as_string(name)); let mut f = OpenOptions::new() - .write(true) .append(true) .create(true) .open(self.plus(name)) @@ -997,7 +1016,7 @@ impl AtPath { pub fn relative_symlink_file(&self, original: &str, link: &str) { #[cfg(windows)] - let original = original.replace('/', &MAIN_SEPARATOR.to_string()); + let original = original.replace('/', MAIN_SEPARATOR_STR); log_info( "symlink", format!("{},{}", &original, &self.plus_as_string(link)), @@ -1019,7 +1038,7 @@ impl AtPath { pub fn relative_symlink_dir(&self, original: &str, link: &str) { #[cfg(windows)] - let original = original.replace('/', &MAIN_SEPARATOR.to_string()); + let original = original.replace('/', MAIN_SEPARATOR_STR); log_info( "symlink", format!("{},{}", &original, &self.plus_as_string(link)), @@ -1218,10 +1237,14 @@ pub struct UCommand { stdout: Option, stderr: Option, bytes_into_stdin: Option>, - #[cfg(any(target_os = "linux", target_os = "android"))] + #[cfg(unix)] limits: Vec<(rlimit::Resource, u64, u64)>, stderr_to_stdout: bool, timeout: Option, + #[cfg(unix)] + terminal_simulation: bool, + #[cfg(unix)] + terminal_size: Option, tmpd: Option>, // drop last } @@ -1377,7 +1400,7 @@ impl UCommand { self } - #[cfg(any(target_os = "linux", target_os = "android"))] + #[cfg(unix)] pub fn limit( &mut self, resource: rlimit::Resource, @@ -1400,6 +1423,68 @@ impl UCommand { self } + /// Set if process should be run in a simulated terminal + /// + /// This is useful to test behavior that is only active if [`stdout.is_terminal()`] is [`true`]. + /// (unix: pty, windows: ConPTY[not yet supported]) + #[cfg(unix)] + pub fn terminal_simulation(&mut self, enable: bool) -> &mut Self { + self.terminal_simulation = enable; + self + } + + /// Set if process should be run in a simulated terminal with specific size + /// + /// This is useful to test behavior that is only active if [`stdout.is_terminal()`] is [`true`]. + /// And the size of the terminal matters additionally. + #[cfg(unix)] + pub fn terminal_size(&mut self, win_size: libc::winsize) -> &mut Self { + self.terminal_simulation(true); + self.terminal_size = Some(win_size); + self + } + + #[cfg(unix)] + fn read_from_pty(pty_fd: std::os::fd::OwnedFd, out: File) { + let read_file = std::fs::File::from(pty_fd); + let mut reader = std::io::BufReader::new(read_file); + let mut writer = std::io::BufWriter::new(out); + let result = std::io::copy(&mut reader, &mut writer); + match result { + Ok(_) => {} + // Input/output error (os error 5) is returned due to pipe closes. Buffer gets content anyway. + Err(e) if e.raw_os_error().unwrap_or_default() == 5 => {} + Err(e) => { + eprintln!("Unexpected error: {:?}", e); + panic!("error forwarding output of pty"); + } + } + } + + #[cfg(unix)] + fn spawn_reader_thread( + &self, + captured_output: Option, + pty_fd_master: OwnedFd, + name: String, + ) -> Option { + if let Some(mut captured_output_i) = captured_output { + let fd = captured_output_i.try_clone().unwrap(); + + let handle = std::thread::Builder::new() + .name(name) + .spawn(move || { + Self::read_from_pty(pty_fd_master, fd); + }) + .unwrap(); + + captured_output_i.reader_thread_handle = Some(handle); + Some(captured_output_i) + } else { + None + } + } + /// Build the `std::process::Command` and apply the defaults on fields which were not specified /// by the user. /// @@ -1419,7 +1504,14 @@ impl UCommand { /// * `stderr_to_stdout`: `false` /// * `bytes_into_stdin`: `None` /// * `limits`: `None`. - fn build(&mut self) -> (Command, Option, Option) { + fn build( + &mut self, + ) -> ( + Command, + Option, + Option, + Option, + ) { if self.bin_path.is_some() { if let Some(util_name) = &self.util_name { self.args.push_front(util_name.into()); @@ -1498,6 +1590,10 @@ impl UCommand { let mut captured_stdout = None; let mut captured_stderr = None; + #[cfg(unix)] + let mut stdin_pty: Option = None; + #[cfg(not(unix))] + let stdin_pty: Option = None; if self.stderr_to_stdout { let mut output = CapturedOutput::default(); @@ -1531,7 +1627,58 @@ impl UCommand { .stderr(stderr); }; - (command, captured_stdout, captured_stderr) + #[cfg(unix)] + if self.terminal_simulation { + let terminal_size = self.terminal_size.unwrap_or(libc::winsize { + ws_col: 80, + ws_row: 30, + ws_xpixel: 80 * 8, + ws_ypixel: 30 * 10, + }); + + let OpenptyResult { + slave: pi_slave, + master: pi_master, + } = nix::pty::openpty(&terminal_size, None).unwrap(); + let OpenptyResult { + slave: po_slave, + master: po_master, + } = nix::pty::openpty(&terminal_size, None).unwrap(); + let OpenptyResult { + slave: pe_slave, + master: pe_master, + } = nix::pty::openpty(&terminal_size, None).unwrap(); + + stdin_pty = Some(File::from(pi_master)); + + captured_stdout = + self.spawn_reader_thread(captured_stdout, po_master, "stdout_reader".to_string()); + captured_stderr = + self.spawn_reader_thread(captured_stderr, pe_master, "stderr_reader".to_string()); + + command.stdin(pi_slave).stdout(po_slave).stderr(pe_slave); + } + + #[cfg(unix)] + if !self.limits.is_empty() { + // just to be safe: move a copy of the limits list into the closure. + // this way the closure is fully self-contained. + let limits_copy = self.limits.clone(); + let closure = move || -> Result<()> { + for &(resource, soft_limit, hard_limit) in &limits_copy { + setrlimit(resource, soft_limit, hard_limit)?; + } + Ok(()) + }; + // SAFETY: the closure is self-contained and doesn't do any memory + // writes that would need to be propagated back to the parent process. + // also, the closure doesn't access stdin, stdout and stderr. + unsafe { + command.pre_exec(closure); + } + } + + (command, captured_stdout, captured_stderr, stdin_pty) } /// Spawns the command, feeds the stdin if any, and returns the @@ -1540,23 +1687,12 @@ impl UCommand { assert!(!self.has_run, "{}", ALREADY_RUN); self.has_run = true; - let (mut command, captured_stdout, captured_stderr) = self.build(); + let (mut command, captured_stdout, captured_stderr, stdin_pty) = self.build(); log_info("run", self.to_string()); let child = command.spawn().unwrap(); - #[cfg(any(target_os = "linux", target_os = "android"))] - for &(resource, soft_limit, hard_limit) in &self.limits { - prlimit( - child.id() as i32, - resource, - Some((soft_limit, hard_limit)), - None, - ) - .unwrap(); - } - - let mut child = UChild::from(self, child, captured_stdout, captured_stderr); + let mut child = UChild::from(self, child, captured_stdout, captured_stderr, stdin_pty); if let Some(input) = self.bytes_into_stdin.take() { child.pipe_in(input); @@ -1621,6 +1757,7 @@ impl std::fmt::Display for UCommand { struct CapturedOutput { current_file: File, output: tempfile::NamedTempFile, // drop last + reader_thread_handle: Option>, } impl CapturedOutput { @@ -1629,6 +1766,7 @@ impl CapturedOutput { Self { current_file: output.reopen().unwrap(), output, + reader_thread_handle: None, } } @@ -1705,6 +1843,7 @@ impl Default for CapturedOutput { Self { current_file: file.reopen().unwrap(), output: file, + reader_thread_handle: None, } } } @@ -1838,6 +1977,7 @@ pub struct UChild { util_name: Option, captured_stdout: Option, captured_stderr: Option, + stdin_pty: Option, ignore_stdin_write_error: bool, stderr_to_stdout: bool, join_handle: Option>>, @@ -1851,6 +1991,7 @@ impl UChild { child: Child, captured_stdout: Option, captured_stderr: Option, + stdin_pty: Option, ) -> Self { Self { raw: child, @@ -1858,6 +1999,7 @@ impl UChild { util_name: ucommand.util_name.clone(), captured_stdout, captured_stderr, + stdin_pty, ignore_stdin_write_error: ucommand.ignore_stdin_write_error, stderr_to_stdout: ucommand.stderr_to_stdout, join_handle: None, @@ -1998,11 +2140,19 @@ impl UChild { /// error. #[deprecated = "Please use wait() -> io::Result instead."] pub fn wait_with_output(mut self) -> io::Result { + // some apps do not stop execution until their stdin gets closed. + // to prevent a endless waiting here, we close the stdin. + self.join(); // ensure that all pending async input is piped in + self.close_stdin(); + let output = if let Some(timeout) = self.timeout { let child = self.raw; let (sender, receiver) = mpsc::channel(); - let handle = thread::spawn(move || sender.send(child.wait_with_output())); + let handle = thread::Builder::new() + .name("wait_with_output".to_string()) + .spawn(move || sender.send(child.wait_with_output())) + .unwrap(); match receiver.recv_timeout(timeout) { Ok(result) => { @@ -2034,9 +2184,15 @@ impl UChild { }; if let Some(stdout) = self.captured_stdout.as_mut() { + if let Some(handle) = stdout.reader_thread_handle.take() { + handle.join().unwrap(); + } output.stdout = stdout.output_bytes(); } if let Some(stderr) = self.captured_stderr.as_mut() { + if let Some(handle) = stderr.reader_thread_handle.take() { + handle.join().unwrap(); + } output.stderr = stderr.output_bytes(); } @@ -2198,6 +2354,29 @@ impl UChild { } } + fn access_stdin_as_writer<'a>(&'a mut self) -> Box { + if let Some(stdin_fd) = &self.stdin_pty { + Box::new(BufWriter::new(stdin_fd.try_clone().unwrap())) + } else { + let stdin: &mut std::process::ChildStdin = self.raw.stdin.as_mut().unwrap(); + Box::new(BufWriter::new(stdin)) + } + } + + fn take_stdin_as_writer(&mut self) -> Box { + if let Some(stdin_fd) = mem::take(&mut self.stdin_pty) { + Box::new(BufWriter::new(stdin_fd)) + } else { + let stdin = self + .raw + .stdin + .take() + .expect("Could not pipe into child process. Was it set to Stdio::null()?"); + + Box::new(BufWriter::new(stdin)) + } + } + /// Pipe data into [`Child`] stdin in a separate thread to avoid deadlocks. /// /// In contrast to [`UChild::write_in`], this method is designed to simulate a pipe on the @@ -2219,24 +2398,24 @@ impl UChild { /// [`JoinHandle`]: std::thread::JoinHandle pub fn pipe_in>>(&mut self, content: T) -> &mut Self { let ignore_stdin_write_error = self.ignore_stdin_write_error; - let content = content.into(); - let stdin = self - .raw - .stdin - .take() - .expect("Could not pipe into child process. Was it set to Stdio::null()?"); - - let join_handle = thread::spawn(move || { - let mut writer = BufWriter::new(stdin); - - match writer.write_all(&content).and_then(|()| writer.flush()) { - Err(error) if !ignore_stdin_write_error => Err(io::Error::new( - io::ErrorKind::Other, - format!("failed to write to stdin of child: {error}"), - )), - Ok(()) | Err(_) => Ok(()), - } - }); + let mut content: Vec = content.into(); + if self.stdin_pty.is_some() { + content.append(&mut END_OF_TRANSMISSION_SEQUENCE.to_vec()); + } + let mut writer = self.take_stdin_as_writer(); + + let join_handle = std::thread::Builder::new() + .name("pipe_in".to_string()) + .spawn( + move || match writer.write_all(&content).and_then(|()| writer.flush()) { + Err(error) if !ignore_stdin_write_error => Err(io::Error::new( + io::ErrorKind::Other, + format!("failed to write to stdin of child: {error}"), + )), + Ok(()) | Err(_) => Ok(()), + }, + ) + .unwrap(); self.join_handle = Some(join_handle); self @@ -2279,10 +2458,11 @@ impl UChild { /// # Errors /// If [`ChildStdin::write_all`] or [`ChildStdin::flush`] returned an error pub fn try_write_in>>(&mut self, data: T) -> io::Result<()> { - let stdin = self.raw.stdin.as_mut().unwrap(); + let ignore_stdin_write_error = self.ignore_stdin_write_error; + let mut writer = self.access_stdin_as_writer(); - match stdin.write_all(&data.into()).and_then(|()| stdin.flush()) { - Err(error) if !self.ignore_stdin_write_error => Err(io::Error::new( + match writer.write_all(&data.into()).and_then(|()| writer.flush()) { + Err(error) if !ignore_stdin_write_error => Err(io::Error::new( io::ErrorKind::Other, format!("failed to write to stdin of child: {error}"), )), @@ -2319,6 +2499,11 @@ impl UChild { /// Note, this does not have any effect if using the [`UChild::pipe_in`] method. pub fn close_stdin(&mut self) -> &mut Self { self.raw.stdin.take(); + if self.stdin_pty.is_some() { + // a pty can not be closed. We need to send a EOT: + let _ = self.try_write_in(END_OF_TRANSMISSION_SEQUENCE); + self.stdin_pty.take(); + } self } } @@ -2499,7 +2684,7 @@ pub fn expected_result(ts: &TestScenario, args: &[&str]) -> std::result::Result< let (stdout, stderr): (String, String) = if cfg!(target_os = "linux") { ( result.stdout_str().to_string(), - result.stderr_str().to_string(), + result.stderr_str_lossy().to_string(), ) } else { // `host_name_for` added prefix, strip 'g' prefix from results: @@ -2507,7 +2692,7 @@ pub fn expected_result(ts: &TestScenario, args: &[&str]) -> std::result::Result< let to = &from[1..]; ( result.stdout_str().replace(&from, to), - result.stderr_str().replace(&from, to), + result.stderr_str_lossy().replace(&from, to), ) }; @@ -3417,4 +3602,158 @@ mod tests { xattr::set(&file_path2, test_attr, test_value).unwrap(); assert!(compare_xattrs(&file_path1, &file_path2)); } + + #[cfg(unix)] + #[cfg(feature = "env")] + #[test] + fn test_simulation_of_terminal_false() { + let scene = TestScenario::new("util"); + + let out = scene.ccmd("env").arg("sh").arg("is_atty.sh").succeeds(); + std::assert_eq!( + String::from_utf8_lossy(out.stdout()), + "stdin is not atty\nstdout is not atty\nstderr is not atty\n" + ); + std::assert_eq!( + String::from_utf8_lossy(out.stderr()), + "This is an error message.\n" + ); + } + + #[cfg(unix)] + #[cfg(feature = "env")] + #[test] + fn test_simulation_of_terminal_true() { + let scene = TestScenario::new("util"); + + let out = scene + .ccmd("env") + .arg("sh") + .arg("is_atty.sh") + .terminal_simulation(true) + .succeeds(); + std::assert_eq!( + String::from_utf8_lossy(out.stdout()), + "stdin is atty\r\nstdout is atty\r\nstderr is atty\r\nterminal size: 30 80\r\n" + ); + std::assert_eq!( + String::from_utf8_lossy(out.stderr()), + "This is an error message.\r\n" + ); + } + + #[cfg(unix)] + #[cfg(feature = "env")] + #[test] + fn test_simulation_of_terminal_size_information() { + let scene = TestScenario::new("util"); + + let out = scene + .ccmd("env") + .arg("sh") + .arg("is_atty.sh") + .terminal_size(libc::winsize { + ws_col: 40, + ws_row: 10, + ws_xpixel: 40 * 8, + ws_ypixel: 10 * 10, + }) + .succeeds(); + std::assert_eq!( + String::from_utf8_lossy(out.stdout()), + "stdin is atty\r\nstdout is atty\r\nstderr is atty\r\nterminal size: 10 40\r\n" + ); + std::assert_eq!( + String::from_utf8_lossy(out.stderr()), + "This is an error message.\r\n" + ); + } + + #[cfg(unix)] + #[cfg(feature = "env")] + #[test] + fn test_simulation_of_terminal_pty_sends_eot_automatically() { + let scene = TestScenario::new("util"); + + let mut cmd = scene.ccmd("env"); + cmd.timeout(std::time::Duration::from_secs(10)); + cmd.args(&["cat", "-"]); + cmd.terminal_simulation(true); + let child = cmd.run_no_wait(); + let out = child.wait().unwrap(); // cat would block if there is no eot + + std::assert_eq!(String::from_utf8_lossy(out.stderr()), ""); + std::assert_eq!(String::from_utf8_lossy(out.stdout()), "\r\n"); + } + + #[cfg(unix)] + #[cfg(feature = "env")] + #[test] + fn test_simulation_of_terminal_pty_pipes_into_data_and_sends_eot_automatically() { + let scene = TestScenario::new("util"); + + let message = "Hello stdin forwarding!"; + + let mut cmd = scene.ccmd("env"); + cmd.args(&["cat", "-"]); + cmd.terminal_simulation(true); + cmd.pipe_in(message); + let child = cmd.run_no_wait(); + let out = child.wait().unwrap(); + + std::assert_eq!( + String::from_utf8_lossy(out.stdout()), + format!("{}\r\n", message) + ); + std::assert_eq!(String::from_utf8_lossy(out.stderr()), ""); + } + + #[cfg(unix)] + #[cfg(feature = "env")] + #[test] + fn test_simulation_of_terminal_pty_write_in_data_and_sends_eot_automatically() { + let scene = TestScenario::new("util"); + + let mut cmd = scene.ccmd("env"); + cmd.args(&["cat", "-"]); + cmd.terminal_simulation(true); + let mut child = cmd.run_no_wait(); + child.write_in("Hello stdin forwarding via write_in!"); + let out = child.wait().unwrap(); + + std::assert_eq!( + String::from_utf8_lossy(out.stdout()), + "Hello stdin forwarding via write_in!\r\n" + ); + std::assert_eq!(String::from_utf8_lossy(out.stderr()), ""); + } + + #[cfg(unix)] + #[test] + fn test_application_of_process_resource_limits_unlimited_file_size() { + let ts = TestScenario::new("util"); + ts.cmd("sh") + .args(&["-c", "ulimit -Sf; ulimit -Hf"]) + .succeeds() + .no_stderr() + .stdout_is("unlimited\nunlimited\n"); + } + + #[cfg(unix)] + #[test] + fn test_application_of_process_resource_limits_limited_file_size() { + let unit_size_bytes = if cfg!(target_os = "macos") { 1024 } else { 512 }; + + let ts = TestScenario::new("util"); + ts.cmd("sh") + .args(&["-c", "ulimit -Sf; ulimit -Hf"]) + .limit( + rlimit::Resource::FSIZE, + 8 * unit_size_bytes, + 16 * unit_size_bytes, + ) + .succeeds() + .no_stderr() + .stdout_is("8\n16\n"); + } } diff --git a/tests/fixtures/cksum/base64/blake2b_single_file.expected b/tests/fixtures/cksum/base64/blake2b_single_file.expected new file mode 100644 index 00000000000..18bec38f017 --- /dev/null +++ b/tests/fixtures/cksum/base64/blake2b_single_file.expected @@ -0,0 +1 @@ +BLAKE2b (lorem_ipsum.txt) = DpegkYnlYMN4nAv/HwIBZoYe+FfR+/5FdN4YQuPAbKu5V15K9jCaFmFYwrQI08A4wbSdgos1FYFCzcA5bRGVww== diff --git a/tests/fixtures/cksum/base64/bsd_single_file.expected b/tests/fixtures/cksum/base64/bsd_single_file.expected new file mode 100644 index 00000000000..293ada3bd61 --- /dev/null +++ b/tests/fixtures/cksum/base64/bsd_single_file.expected @@ -0,0 +1 @@ +08109 1 lorem_ipsum.txt diff --git a/tests/fixtures/cksum/base64/crc_single_file.expected b/tests/fixtures/cksum/base64/crc_single_file.expected new file mode 100644 index 00000000000..e9fc1ca7cf4 --- /dev/null +++ b/tests/fixtures/cksum/base64/crc_single_file.expected @@ -0,0 +1 @@ +378294376 772 lorem_ipsum.txt diff --git a/tests/fixtures/cksum/base64/md5_multiple_files.expected b/tests/fixtures/cksum/base64/md5_multiple_files.expected new file mode 100644 index 00000000000..e7f844b9ac7 --- /dev/null +++ b/tests/fixtures/cksum/base64/md5_multiple_files.expected @@ -0,0 +1,2 @@ +MD5 (lorem_ipsum.txt) = zXJGkPfcYXdd+sQApx8sqg== +MD5 (alice_in_wonderland.txt) = 9vpwM+FhZqlYmqHAOI/9WA== diff --git a/tests/fixtures/cksum/base64/md5_single_file.expected b/tests/fixtures/cksum/base64/md5_single_file.expected new file mode 100644 index 00000000000..8a12f79278c --- /dev/null +++ b/tests/fixtures/cksum/base64/md5_single_file.expected @@ -0,0 +1 @@ +MD5 (lorem_ipsum.txt) = zXJGkPfcYXdd+sQApx8sqg== diff --git a/tests/fixtures/cksum/base64/sha1_single_file.expected b/tests/fixtures/cksum/base64/sha1_single_file.expected new file mode 100644 index 00000000000..b12608e77c7 --- /dev/null +++ b/tests/fixtures/cksum/base64/sha1_single_file.expected @@ -0,0 +1 @@ +SHA1 (lorem_ipsum.txt) = qx3QuuHYiDo9GKZt5q+9KCUs++8= diff --git a/tests/fixtures/cksum/base64/sha224_single_file.expected b/tests/fixtures/cksum/base64/sha224_single_file.expected new file mode 100644 index 00000000000..69d85abdb86 --- /dev/null +++ b/tests/fixtures/cksum/base64/sha224_single_file.expected @@ -0,0 +1 @@ +SHA224 (lorem_ipsum.txt) = PeZvvK0QbhtAqzkb5WxR0gB+sfnGVdD04pv8AQ== diff --git a/tests/fixtures/cksum/base64/sha256_single_file.expected b/tests/fixtures/cksum/base64/sha256_single_file.expected new file mode 100644 index 00000000000..d9a8814a30b --- /dev/null +++ b/tests/fixtures/cksum/base64/sha256_single_file.expected @@ -0,0 +1 @@ +SHA256 (lorem_ipsum.txt) = 98QgUBxQ4AswklAQDWfqXpEJgVNrRYL+nENb2Ss/HwI= diff --git a/tests/fixtures/cksum/base64/sha384_single_file.expected b/tests/fixtures/cksum/base64/sha384_single_file.expected new file mode 100644 index 00000000000..8a8333d3553 --- /dev/null +++ b/tests/fixtures/cksum/base64/sha384_single_file.expected @@ -0,0 +1 @@ +SHA384 (lorem_ipsum.txt) = S+S5Cg0NMpZpkpIQGfJKvIJNz7ixxAgQLx9niPuAupqaTFp7V1ozU6kKjucZSB3L diff --git a/tests/fixtures/cksum/base64/sha512_single_file.expected b/tests/fixtures/cksum/base64/sha512_single_file.expected new file mode 100644 index 00000000000..da36c4295fb --- /dev/null +++ b/tests/fixtures/cksum/base64/sha512_single_file.expected @@ -0,0 +1 @@ +SHA512 (lorem_ipsum.txt) = llRkqyVWqtWOvHPYmtIh5Vl5dSnsr8D0ZsEXlc/21uLGD5agfFQs/R9Cbl5P4KSKoVZnukQJayE9CBPNA436BQ== diff --git a/tests/fixtures/cksum/base64/sm3_single_file.expected b/tests/fixtures/cksum/base64/sm3_single_file.expected new file mode 100644 index 00000000000..e4ca582c938 --- /dev/null +++ b/tests/fixtures/cksum/base64/sm3_single_file.expected @@ -0,0 +1 @@ +SM3 (lorem_ipsum.txt) = bSlrgF0GC/7SKAjfMI27m0MXeU3U7WdAoQdwp4Jpm8I= diff --git a/tests/fixtures/cksum/base64/sysv_single_file.expected b/tests/fixtures/cksum/base64/sysv_single_file.expected new file mode 100644 index 00000000000..e0f7252cbe8 --- /dev/null +++ b/tests/fixtures/cksum/base64/sysv_single_file.expected @@ -0,0 +1 @@ +6985 2 lorem_ipsum.txt diff --git a/tests/fixtures/cut/8bit-delim.txt b/tests/fixtures/cut/8bit-delim.txt new file mode 100644 index 00000000000..2312c916aef --- /dev/null +++ b/tests/fixtures/cut/8bit-delim.txt @@ -0,0 +1 @@ +a­b­c diff --git a/tests/fixtures/env/runBat.bat b/tests/fixtures/env/runBat.bat new file mode 100644 index 00000000000..63ab744d3ab --- /dev/null +++ b/tests/fixtures/env/runBat.bat @@ -0,0 +1 @@ +echo Hello Windows World! diff --git a/tests/fixtures/install/helloworld_freebsd b/tests/fixtures/install/helloworld_freebsd new file mode 100755 index 00000000000..bfd78863030 Binary files /dev/null and b/tests/fixtures/install/helloworld_freebsd differ diff --git a/tests/fixtures/nohup/is_atty.sh b/tests/fixtures/nohup/is_atty.sh new file mode 100644 index 00000000000..30c07ecee71 --- /dev/null +++ b/tests/fixtures/nohup/is_atty.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +if [ -t 0 ] ; then + echo "stdin is atty" +else + echo "stdin is not atty" +fi + +if [ -t 1 ] ; then + echo "stdout is atty" +else + echo "stdout is not atty" +fi + +if [ -t 2 ] ; then + echo "stderr is atty" +else + echo "stderr is not atty" +fi + +true diff --git a/tests/fixtures/uniq/locale-fr-schar.txt b/tests/fixtures/uniq/locale-fr-schar.txt new file mode 100644 index 00000000000..4e285f37aef --- /dev/null +++ b/tests/fixtures/uniq/locale-fr-schar.txt @@ -0,0 +1,2 @@ + y z +  y z diff --git a/tests/fixtures/util/is_atty.sh b/tests/fixtures/util/is_atty.sh new file mode 100644 index 00000000000..30f8caf32e8 --- /dev/null +++ b/tests/fixtures/util/is_atty.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +if [ -t 0 ] ; then + echo "stdin is atty" +else + echo "stdin is not atty" +fi + +if [ -t 1 ] ; then + echo "stdout is atty" +else + echo "stdout is not atty" +fi + +if [ -t 2 ] ; then + echo "stderr is atty" + echo "terminal size: $(stty size)" +else + echo "stderr is not atty" +fi + +>&2 echo "This is an error message." + +true diff --git a/util/android-commands.sh b/util/android-commands.sh index 652e3d992bf..48fdd86d844 100755 --- a/util/android-commands.sh +++ b/util/android-commands.sh @@ -1,22 +1,72 @@ #!/usr/bin/env bash # 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 -# environment on the emulated device (to e.g. run cargo). So we use adb to -# launch termux, then to send keystrokes to it while it's running. -# This means that the commands sent to termux are first parsed as arguments in +# spell-checker:ignore screencap reinit PIPESTATUS keygen sourceslist + +# There are four shells: the host's, adb, termux and termux via ssh. +# But only termux and termux via ssh provides a GNU environment on the +# emulated device (to e.g. run cargo). +# Initially, only adb lets us run commands directly on the emulated device. +# Thus we first establish a ssh connection which then can be used to access +# the termux shell directly, getting output and return code as usual. +# So we use adb to launch termux, then to send keystrokes to it while it's running. +# This way we install sshd and a public key from the host. After that we can +# use ssh to directly run commands in termux environment. + +# Before ssh, we need to consider some inconvenient, limiting specialties: +# The commands sent to termux via adb keystrokes 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 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. +# Using this approach 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. + +# Additionally, we can use adb screenshot functionality to investigate issues +# when there is no feedback arriving from the android device. this_repo="$(dirname "$(dirname -- "$(readlink -- "${0}")")")" cache_dir_name="__rust_cache__" +dev_probe_dir=/sdcard +dev_home_dir=/data/data/com.termux/files/home + +# This is a list of termux package mirrors approved to be used. +# The default mirror list contains entries that do not function properly anymore. +# To avoid failures due to broken mirrors, we use our own list. +# Choose only reliable mirrors here: +repo_url_list=( + "deb https://packages-cf.termux.org/apt/termux-main/ stable main" + "deb https://packages-cf.termux.dev/apt/termux-main/ stable main" +# "deb https://grimler.se/termux/termux-main stable main" # slow + "deb https://ftp.fau.de/termux/termux-main stable main" +) +number_repo_urls=${#repo_url_list[@]} +repo_url_round_robin=$RANDOM + +move_to_next_repo_url() { + repo_url_round_robin=$(((repo_url_round_robin + 1) % number_repo_urls)) + echo "next round robin repo_url: $repo_url_round_robin" +} +move_to_next_repo_url # first call needed for modulo + +get_current_repo_url() { + echo "${repo_url_list[$repo_url_round_robin]}" +} + +# dump some information about the runners system for debugging purposes: +echo "====== runner information ======" +echo "hostname: $(hostname)" +echo "uname -a: $(uname -a)" +echo "pwd: $(pwd)" +echo "\$*: $*" +echo "\$0: $0" +# shellcheck disable=SC2140 +echo "\$(readlink -- "\$\{0\}"): $(readlink -- "${0}")" +echo "\$this_repo: $this_repo" +echo "readlink -f \$this_repo: $(readlink -f "$this_repo")" +this_repo=$(readlink -f "$this_repo") +echo "====== runner info end =========" help() { echo \ @@ -48,23 +98,82 @@ hit_enter() { } exit_termux() { - adb shell input text "exit" && hit_enter && hit_enter + adb shell input text \"exit\" && hit_enter && hit_enter +} + +timestamp() { + date +"%H%M%S%Z" +} + +add_timestamp_to_lines() { + while IFS= read -r line; do printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$line"; done +} + +# takes a screenshot with given name from the android device. Filename is prefixed with timestamp. +# screenshots are collected at the end of the github workflow and provided as download link. +take_screen_shot() { + filename_prefix="$1" + filename="$this_repo/output/$(timestamp)_${filename_prefix}_screen.png" + echo "take screenshot: $filename" + mkdir -p "$this_repo/output" + adb exec-out screencap -p > "$filename" } launch_termux() { echo "launching termux" + take_screen_shot "launch_termux_enter" + + adb shell input tap 120 380 # close potential dialog "System UI isn't responding" with "wait". + # should not cause side effects when dialog is not there as there are + # no relevant GUI elements at this position otherwise. + if ! adb shell 'am start -n com.termux/.HomeActivity'; then echo "failed to launch termux" exit 1 fi + + take_screen_shot "launch_termux_after_start_activity" + # the emulator can sometimes be a little slow to launch the app - while ! adb shell 'ls /sdcard/launch.probe' 2>/dev/null; do - echo "waiting for launch.probe" - sleep 5 - adb shell input text 'touch\ /sdcard/launch.probe' && hit_enter + loop_count=0 + while ! adb shell "dumpsys window windows" | \ + grep -E "imeInputTarget in display# 0 Window{[^}]+com.termux\/com\.termux\.HomeActivity}" + do + sleep 1 + loop_count=$((loop_count + 1)) + if [[ loop_count -ge 20 ]]; then + break + fi + done + + take_screen_shot "launch_termux_after_wait_activity" + + touch_cmd() { + adb shell input text "\"touch $dev_probe_dir/launch.probe\"" && hit_enter + sleep 1 + } + + local timeout_start=120 + local timeout=$timeout_start + touch_cmd + while ! adb shell "ls $dev_probe_dir/launch.probe" 2>/dev/null + do + echo "waiting for launch.probe - ($timeout / $timeout_start seconds)" + take_screen_shot "launch_termux_touch_probe" + sleep 4 + touch_cmd + + timeout=$((timeout - 4)) + if [[ timeout -le 0 ]]; then + take_screen_shot "error_launch_termux" + echo "timeout waiting for termux to start up" + return 1 + fi + done echo "found launch.probe" - adb shell 'rm /sdcard/launch.probe' && echo "removed launch.probe" + take_screen_shot "launch_termux_found_probe" + adb shell "rm $dev_probe_dir/launch.probe" && echo "removed launch.probe" } # Usage: run_termux_command @@ -99,7 +208,7 @@ run_termux_command() { local debug=${debug:-1} log_name="$(basename -s .probe "${probe}").log" # probe name must have suffix .probe - log_file="/sdcard/${log_name}" + log_file="$dev_probe_dir/${log_name}" log_read="${log_name}.read" echo 0 >"${log_read}" if [[ $debug -eq 1 ]]; then @@ -108,12 +217,22 @@ run_termux_command() { shell_command="'{ ${command}; } &> ${log_file}'" fi - launch_termux + launch_termux || return + + take_screen_shot "run_termux_command_before_input_of_shell_command" + + # remove artificial quoting + shell_command="${shell_command%\'}" + shell_command="${shell_command#\'}" + echo "Running command: ${command}" + echo "Running shell-command: ${shell_command}" start=$(date +%s) - adb shell input text "$shell_command" && sleep 3 && hit_enter + adb_input_text_long "$shell_command" && sleep 1 && hit_enter # just for safety wait a little bit before polling for the probe and the log file - sleep 5 + sleep 1 + + take_screen_shot "run_termux_command_after_input_of_shell_command" local timeout=${timeout:-3600} local retries=${retries:-10} @@ -136,6 +255,7 @@ run_termux_command() { if [[ retries -le 0 ]]; then echo "Maximum retries reached running command. Aborting ..." + take_screen_shot "run_termux_command_maximum_tries_reached" return 1 elif [[ try_fix -le 0 ]]; then retries=$((retries - 1)) @@ -144,7 +264,10 @@ run_termux_command() { # 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)" + take_screen_shot "run_termux_command_before_trying_to_fix" hit_enter + sleep 1 + take_screen_shot "run_termux_command_after_trying_to_fix" fi sleep "$sleep_interval" @@ -152,6 +275,7 @@ run_termux_command() { if [[ $timeout -le 0 ]]; then echo "Timeout reached running command. Aborting ..." + take_screen_shot "run_termux_command_timeout_reached" return 1 fi done @@ -160,7 +284,7 @@ run_termux_command() { return_code=$(adb shell "cat $probe") || return_code=0 adb shell "rm ${probe}" - adb pull "$log_file" . + adb shell "cat $log_file" > "$log_name" echo "==================================== SUMMARY ===================================" echo "Command: ${command}" echo "Finished in $((end - start)) seconds." @@ -173,61 +297,230 @@ run_termux_command() { [[ $keep_log -ne 1 ]] && rm -f "$log_name" rm -f "$log_read" "$probe" + take_screen_shot "run_termux_command_finished_normally" + # shellcheck disable=SC2086 return $return_code } init() { arch="$1" + # shellcheck disable=SC2034 api_level="$2" termux="$3" + snapshot_name="${AVD_CACHE_KEY}" + # shellcheck disable=SC2015 - wget "https://github.com/termux/termux-app/releases/download/${termux}/termux-app_${termux}+github-debug_${arch}.apk" && + wget -nv "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." || { + adb -s emulator-5554 emu avd snapshot save "$snapshot_name" && + echo "Emulator image created. Name: $snapshot_name" || { pkill -9 qemu-system-x86_64 return 1 } pkill -9 qemu-system-x86_64 || true } +reinit_ssh_connection() { + setup_ssh_forwarding + test_ssh_connection && return + + start_sshd_via_adb_shell && ( + test_ssh_connection && return + generate_and_install_public_key && test_ssh_connection && return + ) || ( + install_packages_via_adb_shell openssh openssl + generate_and_install_public_key + start_sshd_via_adb_shell + test_ssh_connection && return + ) || ( + echo "failed to setup ssh connection" + return 1 + ) +} + +start_sshd_via_adb_shell() { + echo "start sshd via adb shell" + probe="$dev_probe_dir/sshd.probe" + command="'sshd; echo \$? > $probe'" + run_termux_command "$command" "$probe" +} + +setup_ssh_forwarding() { + echo "setup ssh forwarding" + adb forward tcp:9022 tcp:8022 +} + +copy_file_or_dir_to_device_via_ssh() { + scp -r "$1" "scp://termux@127.0.0.1:9022/$2" +} + +copy_file_or_dir_from_device_via_ssh() { + scp -r "scp://termux@127.0.0.1:9022/$1" "$2" +} + +# runs the in args provided command on android side via ssh. forwards return code. +# adds a timestamp to every line to be able to see where delays are +run_command_via_ssh() { + ssh -p 9022 termux:@127.0.0.1 -o StrictHostKeyChecking=accept-new "$@" 2>&1 | add_timestamp_to_lines + return "${PIPESTATUS[0]}" +} + +test_ssh_connection() { + run_command_via_ssh echo ssh connection is working +} + +# takes a local (on runner side) script file and runs it via ssh on the virtual android device. forwards return code. +# adds a timestamp to every line to be able to see where delays are +run_script_file_via_ssh() { + ssh -p 9022 termux:@127.0.0.1 -o StrictHostKeyChecking=accept-new "bash -s" < "$1" 2>&1 | add_timestamp_to_lines + return "${PIPESTATUS[0]}" +} + +# Experiments showed that the adb shell input text functionality has a limitation for the input length. +# If input length is too big, the input is not fully provided to the android device. +# To avoid this, we divide large inputs into smaller chunks and put them one-by-one. +adb_input_text_long() { + string=$1 + length=${#string} + step=20 + p=0 + for ((i = 0; i < length-step; i = i + step)); do + chunk="${string:i:$step}" + adb shell input text "'$chunk'" + p=$((i+step)) + done + + remaining="${string:p}" + adb shell input text "'$remaining'" +} + +generate_rsa_key_local() { + yes "" | ssh-keygen -t rsa -b 4096 -C "Github Action" -N "" +} + +install_rsa_pub() { + + run_command_via_ssh "echo hello" && return # if this works, we are already fine. Skipping + + # remove old host identity: + ssh-keygen -f ~/.ssh/known_hosts -R "[127.0.0.1]:9022" + + rsa_pub_key=$(cat ~/.ssh/id_rsa.pub) + echo "=====================================" + echo "$rsa_pub_key" + echo "=====================================" + + adb shell input text \"echo \" + + adb_input_text_long "$rsa_pub_key" + + adb shell input text "\" >> ~/.ssh/authorized_keys\"" && hit_enter + sleep 1 +} + +install_packages_via_adb_shell() { + install_package_list="$*" + + install_packages_via_adb_shell_using_apt "$install_package_list" + if [[ $? -ne 0 ]]; then + echo "apt failed. Now try install with pkg as fallback." + probe="$dev_probe_dir/pkg.probe" + command="'mkdir -vp ~/.cargo/bin; yes | pkg install $install_package_list -y; echo \$? > $probe'" + run_termux_command "$command" "$probe" || return 1 + fi + + return 0 +} + +# We use apt to install the packages as pkg doesn't respect any pre-defined mirror list. +# Its important to have a defined mirror list to avoid issues with broken mirrors. +install_packages_via_adb_shell_using_apt() { + install_package_list="$*" + + repo_url=$(get_current_repo_url) + move_to_next_repo_url + echo "set apt repository url: $repo_url" + probe="$dev_probe_dir/sourceslist.probe" + command="'echo $repo_url | dd of=\$PREFIX/etc/apt/sources.list; echo \$? > $probe'" + run_termux_command "$command" "$probe" + + probe="$dev_probe_dir/adb_install.probe" + command="'mkdir -vp ~/.cargo/bin; apt update; yes | apt install $install_package_list -y; echo \$? > $probe'" + run_termux_command "$command" "$probe" +} + +install_packages_via_ssh_using_apt() { + install_package_list="$*" + + repo_url=$(get_current_repo_url) + move_to_next_repo_url + echo "set apt repository url: $repo_url" + run_command_via_ssh "echo $repo_url | dd of=\$PREFIX/etc/apt/sources.list" + + run_command_via_ssh "apt update; yes | apt install $install_package_list -y" +} + +apt_upgrade_all_packages() { + repo_url=$(get_current_repo_url) + move_to_next_repo_url + echo "set apt repository url: $repo_url" + run_command_via_ssh "echo $repo_url | dd of=\$PREFIX/etc/apt/sources.list" + + run_command_via_ssh "apt update; yes | apt upgrade -y" +} + +generate_and_install_public_key() { + echo "generate local public private key pair" + generate_rsa_key_local + echo "install public key via 'adb shell input'" + install_rsa_pub + echo "installed ssh public key on device" +} + +run_with_retry() { + tries=$1 + shift 1 + + for i in $(seq 1 $tries); do + echo "Try #$i of $tries: run $*" + "$@" && echo "Done in try#$i" && return 0 + done + + exit_code=$? + + echo "Still failing after $tries. Code: $exit_code" + + return $exit_code +} + snapshot() { apk="$1" echo "Running snapshot" adb install -g "$apk" echo "Prepare and install system packages" - probe='/sdcard/pkg.probe' - command="'mkdir -vp ~/.cargo/bin; yes | pkg install rust binutils openssl tar -y; echo \$? > $probe'" - run_termux_command "$command" "$probe" || return + + reinit_ssh_connection || return 1 + + apt_upgrade_all_packages + + install_packages_via_ssh_using_apt "rust binutils openssl tar mount-utils" + + echo "Read /proc/cpuinfo" + run_command_via_ssh "cat /proc/cpuinfo" 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" + command="export CARGO_TERM_COLOR=always && cargo install cargo-nextest" + run_with_retry 3 run_command_via_ssh "$command" 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 "Info about cargo and rust - via SSH Script" + run_script_file_via_ssh "$this_repo/util/android-scripts/collect-info.sh" echo "Snapshot complete" # shellcheck disable=SC2086 @@ -237,50 +530,16 @@ touch $probe'" sync_host() { repo="$1" cache_home="${HOME}/${cache_dir_name}" - cache_dest="/sdcard/${cache_dir_name}" + cache_dest="$dev_home_dir/${cache_dir_name}" - echo "Running sync host -> image: ${repo}" + reinit_ssh_connection - # 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 "Running sync host -> image: ${repo}" - git -C "$repo" diff --name-status | cut -f 2 >modified - modified_links=$(join symlinks modified) - 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 + # run_command_via_ssh "mkdir $dev_home_dir/coreutils" - #shellcheck disable=SC2086 - rm $symlinks - # adb's shell user only has access to shared dirs... - 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/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 + copy_file_or_dir_to_device_via_ssh "$repo" "$dev_home_dir" + [[ -e "$cache_home" ]] && copy_file_or_dir_to_device_via_ssh "$cache_home" "$cache_dest" echo "Finished sync host -> image: ${repo}" } @@ -288,22 +547,22 @@ touch $probe'" sync_image() { repo="$1" cache_home="${HOME}/${cache_dir_name}" - cache_dest="/sdcard/${cache_dir_name}" + cache_dest="$dev_probe_dir/${cache_dir_name}" + + reinit_ssh_connection echo "Running sync image -> host: ${repo}" - probe='/sdcard/cache.probe' - command="'rm -rf /sdcard/coreutils ${cache_dest}; \ + command="rm -rf $dev_probe_dir/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 +ls -la ${cache_dest}" + run_command_via_ssh "$command" || return rm -rf "$cache_home" - adb pull -a "$cache_dest" "$cache_home" || return + copy_file_or_dir_from_device_via_ssh "$cache_dest" "$cache_home" || return echo "Finished sync image -> host: ${repo}" } @@ -311,12 +570,14 @@ echo \$? > ${probe}'" build() { echo "Running build" - probe='/sdcard/build.probe' - 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 + reinit_ssh_connection + + run_script_file_via_ssh "$this_repo/util/android-scripts/collect-info.sh" + + command="export CARGO_TERM_COLOR=always; + export CARGO_INCREMENTAL=0; \ + cd ~/coreutils && cargo build --features feat_os_unix_android" + run_with_retry 3 run_command_via_ssh "$command" || return echo "Finished build" } @@ -324,31 +585,24 @@ echo \$? >$probe'" tests() { echo "Running tests" - probe='/sdcard/tests.probe' - 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 + reinit_ssh_connection + + run_script_file_via_ssh "$this_repo/util/android-scripts/collect-info.sh" + + run_script_file_via_ssh "$this_repo/util/android-scripts/run-tests.sh" || return echo "Finished tests" } hash_rustc() { - probe='/sdcard/rustc.probe' tmp_hash="__rustc_hash__.tmp" hash="__rustc_hash__" + reinit_ssh_connection + echo "Hashing rustc version: ${HOME}/${hash}" - command="'rustc -Vv; echo \$? > ${probe}'" - keep_log=1 - debug=0 - run_termux_command "$command" "$probe" || return + run_command_via_ssh "rustc -Vv" > rustc.log || return rm -f "$tmp_hash" mv "rustc.log" "$tmp_hash" || return # sha256sum is not available. shasum is the macos native program. diff --git a/util/android-scripts/collect-info.sh b/util/android-scripts/collect-info.sh new file mode 100644 index 00000000000..412423bd135 --- /dev/null +++ b/util/android-scripts/collect-info.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# spell-checker:ignore nextest watchplus PIPESTATUS + +echo "system resources - RAM:" +free -hm +echo "system resources - CPU:" +lscpu +echo "system resources - file systems:" +mount + +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 diff --git a/util/android-scripts/run-tests.sh b/util/android-scripts/run-tests.sh new file mode 100644 index 00000000000..17eed8808e0 --- /dev/null +++ b/util/android-scripts/run-tests.sh @@ -0,0 +1,68 @@ +#!/bin/bash + +# spell-checker:ignore nextest watchplus PIPESTATUS + +echo "PATH: $PATH" + +export PATH=$HOME/.cargo/bin:$PATH +export RUST_BACKTRACE=full +export CARGO_TERM_COLOR=always +export CARGO_INCREMENTAL=0 + +echo "PATH: $PATH" + +run_with_retry() { + tries=$1 + shift 1 + + for i in $(seq 1 $tries); do + echo "Try #$i of $tries: run $*" + "$@" && echo "Done in try#$i" && return 0 + done + + exit_code=$? + + echo "Still failing after $tries. Code: $exit_code" + + return $exit_code +} + +run_tests_in_subprocess() ( + + # limit virtual memory to 3GB to avoid that OS kills sshd + ulimit -v $((1024 * 1024 * 3)) + + watchplus() { + # call: watchplus + while true; do + "${@:2}" + sleep "$1" + done + } + + kill_all_background_jobs() { + jobs -p | xargs -I{} kill -- {} + } + + # observe (log) every 2 seconds the system resource usage to judge if we are at a limit + watchplus 2 df -h & + watchplus 2 free -hm & + + nextest_params=(--profile ci --hide-progress-bar --features feat_os_unix_android) + + # run tests + cd ~/coreutils && \ + run_with_retry 3 timeout --preserve-status --verbose -k 1m 10m \ + cargo nextest run --no-run "${nextest_params[@]}" && + timeout --preserve-status --verbose -k 1m 60m \ + cargo nextest run "${nextest_params[@]}" + + result=$? + + kill_all_background_jobs + + return $result +) + +# run sub-shell to be able to use ulimit without affecting the sshd +run_tests_in_subprocess diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 9fdb3079d9d..876e645faed 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -2,7 +2,7 @@ # `build-gnu.bash` ~ builds GNU coreutils (from supplied sources) # -# 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 dired OSTYPE ; (utils) gnproc greadlink gsed +# spell-checker:ignore (paths) abmon deref discrim eacces getlimits getopt ginstall inacc infloop inotify reflink ; (misc) INT_OFLOW OFLOW baddecode submodules xstrtol ; (vars/env) SRCDIR vdir rcexp xpart dired OSTYPE ; (utils) gnproc greadlink gsed set -e @@ -100,9 +100,9 @@ cp "${UU_BUILD_DIR}/install" "${UU_BUILD_DIR}/ginstall" # The GNU tests rename t # Create *sum binaries for sum in b2sum b3sum md5sum sha1sum sha224sum sha256sum sha384sum sha512sum; do sum_path="${UU_BUILD_DIR}/${sum}" - test -f "${sum_path}" || cp "${UU_BUILD_DIR}/hashsum" "${sum_path}" + test -f "${sum_path}" || (cd ${UU_BUILD_DIR} && ln -s "hashsum" "${sum}") done -test -f "${UU_BUILD_DIR}/[" || cp "${UU_BUILD_DIR}/test" "${UU_BUILD_DIR}/[" +test -f "${UU_BUILD_DIR}/[" || (cd ${UU_BUILD_DIR} && ln -s "test" "[") ## @@ -221,6 +221,8 @@ grep -rlE '/usr/local/bin/\s?/usr/local/bin' init.cfg tests/* | xargs -r sed -Ei # we should not regress our project just to match what GNU is going. # So, do some changes on the fly +patch -N -r - -d "$path_GNU" -p 1 -i "`realpath \"$path_UUTILS/util/gnu-patches/tests_env_env-S.pl.patch\"`" || true + sed -i -e "s|rm: cannot remove 'e/slink'|rm: cannot remove 'e'|g" tests/rm/fail-eacces.sh sed -i -e "s|rm: cannot remove 'a/b'|rm: cannot remove 'a'|g" tests/rm/fail-2eperm.sh @@ -249,6 +251,11 @@ test -f "${UU_BUILD_DIR}/getlimits" || cp src/getlimits "${UU_BUILD_DIR}" # SKIP for now sed -i -e "s|my \$prog = 'pr';$|my \$prog = 'pr';CuSkip::skip \"\$prog: SKIP for producing too long logs\";|" tests/pr/pr-tests.pl +# We don't have the same error message and no need to be that specific +sed -i -e "s|invalid suffix in --pages argument|invalid --pages argument|" \ + -e "s|--pages argument '\$too_big' too large|invalid --pages argument '\$too_big'|" \ + -e "s|invalid page range|invalid --pages argument|" tests/misc/xstrtol.pl + # When decoding an invalid base32/64 string, gnu writes everything it was able to decode until # it hit the decode error, while we don't write anything if the input is invalid. sed -i "s/\(baddecode.*OUT=>\"\).*\"/\1\"/g" tests/misc/base64.pl @@ -313,6 +320,7 @@ sed -i -e "s/du: invalid -t argument/du: invalid --threshold argument/" -e "s/du # Remove the extra output check sed -i -e "s|Try '\$prog --help' for more information.\\\n||" tests/du/files0-from.pl sed -i -e "s|when reading file names from stdin, no file name of\"|-: No such file or directory\n\"|" -e "s| '-' allowed\\\n||" tests/du/files0-from.pl +sed -i -e "s|-: No such file or directory|cannot access '-': No such file or directory|g" tests/du/files0-from.pl awk 'BEGIN {count=0} /compare exp out2/ && count < 6 {sub(/compare exp out2/, "grep -q \"cannot be used with\" out2"); count++} 1' tests/df/df-output.sh > tests/df/df-output.sh.tmp && mv tests/df/df-output.sh.tmp tests/df/df-output.sh @@ -329,6 +337,9 @@ ls: invalid --time-style argument 'XX'\nPossible values are: [\"full-iso\", \"lo # "hostid BEFORE --help AFTER " same for this sed -i -e "s/env \$prog \$BEFORE \$opt > out2/env \$prog \$BEFORE \$opt > out2 #/" -e "s/env \$prog \$BEFORE \$opt AFTER > out3/env \$prog \$BEFORE \$opt AFTER > out3 #/" -e "s/compare exp out2/compare exp out2 #/" -e "s/compare exp out3/compare exp out3 #/" tests/help/help-version-getopt.sh +# The case doesn't really matter here +sed -i -e "s|WARNING: 1 line is improperly formatted|warning: 1 line is improperly formatted|" tests/cksum/md5sum-bsd.sh + # Add debug info + we have less syscall then GNU's. Adjust our check. # Use GNU sed for /c command "${SED}" -i -e '/test \$n_stat1 = \$n_stat2 \\/c\ diff --git a/util/compare_gnu_result.py b/util/compare_gnu_result.py index 704951863b5..0ea55210d11 100755 --- a/util/compare_gnu_result.py +++ b/util/compare_gnu_result.py @@ -1,4 +1,4 @@ -#! /usr/bin/python +#!/usr/bin/env python3 """ Compare the current results to the last results gathered from the main branch to highlight diff --git a/util/gnu-patches/tests_env_env-S.pl.patch b/util/gnu-patches/tests_env_env-S.pl.patch new file mode 100644 index 00000000000..404a00ca60e --- /dev/null +++ b/util/gnu-patches/tests_env_env-S.pl.patch @@ -0,0 +1,47 @@ +diff --git a/tests/env/env-S.pl b/tests/env/env-S.pl +index 710ca82cf..af7cf6efa 100755 +--- a/tests/env/env-S.pl ++++ b/tests/env/env-S.pl +@@ -209,27 +209,28 @@ my @Tests = + {ERR=>"$prog: no terminating quote in -S string\n"}], + ['err5', q[-S'A=B\\q'], {EXIT=>125}, + {ERR=>"$prog: invalid sequence '\\q' in -S\n"}], +- ['err6', q[-S'A=$B'], {EXIT=>125}, +- {ERR=>"$prog: only \${VARNAME} expansion is supported, error at: \$B\n"}], ++ ['err6', q[-S'A=$B echo hello'], {EXIT=>0}, ++ {OUT=>"hello"}], + ['err7', q[-S'A=${B'], {EXIT=>125}, +- {ERR=>"$prog: only \${VARNAME} expansion is supported, " . +- "error at: \${B\n"}], ++ {ERR=>"$prog" . qq[: variable name issue (at 5): Missing closing brace\n]}], + ['err8', q[-S'A=${B%B}'], {EXIT=>125}, +- {ERR=>"$prog: only \${VARNAME} expansion is supported, " . +- "error at: \${B%B}\n"}], ++ {ERR=>"$prog" . qq[: variable name issue (at 5): Unexpected character: '%', expected a closing brace ('}') or colon (':')\n]}], + ['err9', q[-S'A=${9B}'], {EXIT=>125}, +- {ERR=>"$prog: only \${VARNAME} expansion is supported, " . +- "error at: \${9B}\n"}], ++ {ERR=>"$prog" . qq[: variable name issue (at 4): Unexpected character: '9', expected variable name must not start with 0..9\n]}], + + # Test incorrect shebang usage (extraneous whitespace). + ['err_sp2', q['-v -S cat -n'], {EXIT=>125}, +- {ERR=>"env: invalid option -- ' '\n" . +- "env: use -[v]S to pass options in shebang lines\n" . +- "Try 'env --help' for more information.\n"}], ++ {ERR=>"$prog: error: unexpected argument '- ' found\n\n" . ++ " tip: to pass '- ' as a value, use '-- - '\n\n" . ++ "Usage: $prog [OPTION]... [-] [NAME=VALUE]... [COMMAND [ARG]...]\n\n" . ++ "For more information, try '--help'.\n" . ++ "$prog: use -[v]S to pass options in shebang lines\n"}], + ['err_sp3', q['-v -S cat -n'], {EXIT=>125}, # embedded tab after -v +- {ERR=>"env: invalid option -- '\t'\n" . +- "env: use -[v]S to pass options in shebang lines\n" . +- "Try 'env --help' for more information.\n"}], ++ {ERR=>"$prog: error: unexpected argument '-\t' found\n\n" . ++ " tip: to pass '-\t' as a value, use '-- -\t'\n\n" . ++ "Usage: $prog [OPTION]... [-] [NAME=VALUE]... [COMMAND [ARG]...]\n\n" . ++ "For more information, try '--help'.\n" . ++ "$prog: use -[v]S to pass options in shebang lines\n"}], + + # Also diagnose incorrect shebang usage when failing to exec. + # This typically happens with: diff --git a/util/size-experiment.py b/util/size-experiment.py index 8acacf9d108..2b1ec0fce74 100644 --- a/util/size-experiment.py +++ b/util/size-experiment.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python3 +# spell-checker:ignore debuginfo import subprocess from itertools import product import shutil diff --git a/util/update-version.sh b/util/update-version.sh index 7f785f1d7af..5127677da3a 100755 --- a/util/update-version.sh +++ b/util/update-version.sh @@ -17,8 +17,8 @@ # 10) Create the release on github https://github.com/uutils/coreutils/releases/new # 11) Make sure we have good release notes -FROM="0.0.23" -TO="0.0.24" +FROM="0.0.24" +TO="0.0.25" PROGS=$(ls -1d src/uu/*/Cargo.toml src/uu/stdbuf/src/libstdbuf/Cargo.toml src/uucore/Cargo.toml Cargo.toml)