diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 32c3537c2..b2d77a5a4 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -5,7 +5,9 @@ name: CICD # spell-checker:ignore (jargon) SHAs deps softprops toolchain # spell-checker:ignore (names) CodeCOV MacOS MinGW Peltoche rivy # spell-checker:ignore (shell/tools) choco clippy dmake dpkg esac fakeroot gmake grcov halium lcov libssl mkdir popd printf pushd rustc rustfmt rustup shopt xargs -# spell-checker:ignore (misc) aarch alnum armhf bindir busytest coreutils gnueabihf issuecomment maint nullglob onexitbegin onexitend tempfile testsuite uutils +# spell-checker:ignore (misc) aarch alnum armhf bindir busytest coreutils gnueabihf issuecomment maint nullglob onexitbegin onexitend runtest tempfile testsuite uutils + +# ToDO: [2021-06; rivy] change from `cargo-tree` to `cargo tree` once MSRV is >= 1.45 env: PROJECT_NAME: coreutils @@ -17,6 +19,40 @@ env: on: [push, pull_request] jobs: + code_deps: + name: Style/dependencies + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + - { os: ubuntu-latest , features: feat_os_unix } + steps: + - uses: actions/checkout@v2 + - name: Initialize workflow variables + id: vars + shell: bash + run: | + ## VARs setup + outputs() { step_id="vars"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo ::set-output name=${var}::${!var}; done; } + # target-specific options + # * CARGO_FEATURES_OPTION + CARGO_FEATURES_OPTION='' ; + if [ -n "${{ matrix.job.features }}" ]; then CARGO_FEATURES_OPTION='--features "${{ matrix.job.features }}"' ; fi + outputs CARGO_FEATURES_OPTION + - name: Install `rust` toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + default: true + profile: minimal # minimal component installation (ie, no documentation) + - name: "`cargo update` testing" + shell: bash + run: | + ## `cargo update` testing + # * convert any warnings to GHA UI annotations; ref: + cargo fetch --locked --quiet || { echo "::error file=Cargo.lock::'Cargo.lock' file requires update (use \`cargo +${{ env.RUST_MIN_SRV }} update\`)" ; exit 1 ; } + code_format: name: Style/format runs-on: ${{ matrix.job.os }} @@ -26,18 +62,18 @@ jobs: job: - { os: ubuntu-latest , features: feat_os_unix } steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - name: Initialize workflow variables id: vars shell: bash run: | ## VARs setup + outputs() { step_id="vars"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo ::set-output name=${var}::${!var}; done; } # target-specific options # * CARGO_FEATURES_OPTION CARGO_FEATURES_OPTION='' ; if [ -n "${{ matrix.job.features }}" ]; then CARGO_FEATURES_OPTION='--features "${{ matrix.job.features }}"' ; fi - echo set-output name=CARGO_FEATURES_OPTION::${CARGO_FEATURES_OPTION} - echo ::set-output name=CARGO_FEATURES_OPTION::${CARGO_FEATURES_OPTION} + outputs CARGO_FEATURES_OPTION - name: Install `rust` toolchain uses: actions-rs/toolchain@v1 with: @@ -48,36 +84,19 @@ jobs: - name: "`fmt` testing" shell: bash run: | - # `fmt` testing + ## `fmt` testing # * convert any warnings to GHA UI annotations; ref: - S=$(cargo fmt -- --check) && printf "%s\n" "$S" || { printf "%s\n" "$S" | sed -E -n -e "s/^Diff[[:space:]]+in[[:space:]]+${PWD//\//\\/}\/(.*)[[:space:]]+at[[:space:]]+[^0-9]+([0-9]+).*$/::warning file=\1,line=\2::WARNING: \`cargo fmt\`: style violation/p" ; } + S=$(cargo fmt -- --check) && printf "%s\n" "$S" || { printf "%s\n" "$S" ; printf "%s\n" "$S" | sed -E -n -e "s/^Diff[[:space:]]+in[[:space:]]+${PWD//\//\\/}\/(.*)[[:space:]]+at[[:space:]]+[^0-9]+([0-9]+).*$/::error file=\1,line=\2::ERROR: \`cargo fmt\`: style violation (file:'\1', line:\2; use \`cargo fmt \"\1\"\`)/p" ; exit 1 ; } - name: "`fmt` testing of tests" + if: success() || failure() # run regardless of prior step success/failure shell: bash run: | - # `fmt` testing of tests + ## `fmt` testing of tests # * convert any warnings to GHA UI annotations; ref: - S=$(find tests -name "*.rs" -print0 | xargs -0 cargo fmt -- --check) && printf "%s\n" "$S" || { printf "%s\n" "$S" | sed -E -n "s/^Diff[[:space:]]+in[[:space:]]+${PWD//\//\\/}\/(.*)[[:space:]]+at[[:space:]]+[^0-9]+([0-9]+).*$/::warning file=\1,line=\2::WARNING: \`cargo fmt\`: style violation/p" ; } + S=$(find tests -name "*.rs" -print0 | xargs -0 cargo fmt -- --check) && printf "%s\n" "$S" || { printf "%s\n" "$S" ; printf "%s\n" "$S" | sed -E -n "s/^Diff[[:space:]]+in[[:space:]]+${PWD//\//\\/}\/(.*)[[:space:]]+at[[:space:]]+[^0-9]+([0-9]+).*$/::error file=\1,line=\2::ERROR: \`cargo fmt\`: style violation (file:'\1', line:\2; use \`cargo fmt \"\1\"\`)/p" ; exit 1 ; } - code_spellcheck: - name: Style/spelling - runs-on: ${{ matrix.job.os }} - strategy: - matrix: - job: - - { os: ubuntu-latest } - steps: - - uses: actions/checkout@v1 - - name: Install/setup prerequisites - shell: bash - run: | - sudo apt-get -y update ; sudo apt-get -y install npm ; sudo npm install cspell -g; - - name: Run `cspell` - shell: bash - run: | - cspell --config .vscode/cSpell.json --no-summary --no-progress "**/*" | sed "s/\(.*\):\(.*\):\(.*\) - \(.*\)/::warning file=\1,line=\2,col=\3::cspell: \4/" || true - - code_warnings: - name: Style/warnings + code_lint: + name: Style/lint runs-on: ${{ matrix.job.os }} strategy: fail-fast: false @@ -87,18 +106,18 @@ jobs: - { os: macos-latest , features: feat_os_macos } - { os: windows-latest , features: feat_os_windows } steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - name: Initialize workflow variables id: vars shell: bash run: | ## VARs setup + outputs() { step_id="vars"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo ::set-output name=${var}::${!var}; done; } # target-specific options # * CARGO_FEATURES_OPTION CARGO_FEATURES_OPTION='' ; if [ -n "${{ matrix.job.features }}" ]; then CARGO_FEATURES_OPTION='--features "${{ matrix.job.features }}"' ; fi - echo set-output name=CARGO_FEATURES_OPTION::${CARGO_FEATURES_OPTION} - echo ::set-output name=CARGO_FEATURES_OPTION::${CARGO_FEATURES_OPTION} + outputs CARGO_FEATURES_OPTION - name: Install `rust` toolchain uses: actions-rs/toolchain@v1 with: @@ -106,13 +125,32 @@ jobs: default: true profile: minimal # minimal component installation (ie, no documentation) components: clippy - - name: "`clippy` testing" - if: success() || failure() # run regardless of prior step success/failure + - name: "`clippy` lint testing" shell: bash run: | - # `clippy` testing + ## `clippy` lint testing # * convert any warnings to GHA UI annotations; ref: - S=$(cargo +nightly clippy --all-targets ${{ matrix.job.cargo-options }} ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} -- -D warnings 2>&1) && printf "%s\n" "$S" || { printf "%s\n" "$S" ; printf "%s" "$S" | sed -E -n -e '/^error:/{' -e "N; s/^error:[[:space:]]+(.*)\\n[[:space:]]+-->[[:space:]]+(.*):([0-9]+):([0-9]+).*$/::warning file=\2,line=\3,col=\4::WARNING: \`cargo clippy\`: \1/p;" -e '}' ; } + S=$(cargo +nightly clippy --all-targets ${{ matrix.job.cargo-options }} ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} -- -D warnings 2>&1) && printf "%s\n" "$S" || { printf "%s\n" "$S" ; printf "%s" "$S" | sed -E -n -e '/^error:/{' -e "N; s/^error:[[:space:]]+(.*)\\n[[:space:]]+-->[[:space:]]+${PWD//\//\\/}\/(.*):([0-9]+):([0-9]+).*$/::error file=\2,line=\3,col=\4::ERROR: \`cargo clippy\`: \1 (file:'\2', line:\3)/p;" -e '}' ; exit 1 ; } + + code_spellcheck: + name: Style/spelling + runs-on: ${{ matrix.job.os }} + strategy: + matrix: + job: + - { os: ubuntu-latest } + steps: + - uses: actions/checkout@v2 + - name: Install/setup prerequisites + shell: bash + run: | + ## Install/setup prerequisites + sudo apt-get -y update ; sudo apt-get -y install npm ; sudo npm install cspell -g ; + - name: Run `cspell` + shell: bash + run: | + ## Run `cspell` + cspell --config .vscode/cSpell.json --no-summary --no-progress "**/*" | sed -E -n "s/${PWD//\//\\/}\/(.*):(.*):(.*) - (.*)/::error file=\1,line=\2,col=\3::ERROR: \4 (file:'\1', line:\2)/p" min_version: name: MinRustV # Minimum supported rust version @@ -122,7 +160,7 @@ jobs: job: - { os: ubuntu-latest , features: feat_os_unix } steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - name: Install `rust` toolchain (v${{ env.RUST_MIN_SRV }}) uses: actions-rs/toolchain@v1 with: @@ -137,33 +175,32 @@ jobs: use-tool-cache: true env: RUSTUP_TOOLCHAIN: stable - - name: Confirm compatible 'Cargo.lock' + - name: Confirm MinSRV compatible 'Cargo.lock' shell: bash run: | - # Confirm compatible 'Cargo.lock' + ## Confirm MinSRV compatible 'Cargo.lock' # * 'Cargo.lock' is required to be in a format that `cargo` of MinSRV can interpret (eg, v1-format for MinSRV < v1.38) - cargo fetch --locked --quiet || { echo "::error file=Cargo.lock::Incompatible 'Cargo.lock' format; try \`cargo +${{ env.RUST_MIN_SRV }} update\`" ; exit 1 ; } + cargo fetch --locked --quiet || { echo "::error file=Cargo.lock::Incompatible (or out-of-date) 'Cargo.lock' file; update using \`cargo +${{ env.RUST_MIN_SRV }} update\`" ; exit 1 ; } - name: Info shell: bash run: | - # Info - ## environment + ## Info + # environment echo "## environment" echo "CI='${CI}'" - ## tooling info display + # tooling info display echo "## tooling" which gcc >/dev/null 2>&1 && (gcc --version | head -1) || true - rustup -V + rustup -V 2>/dev/null rustup show active-toolchain cargo -V rustc -V cargo-tree tree -V - ## dependencies + # dependencies echo "## dependency list" cargo fetch --locked --quiet ## * using the 'stable' toolchain is necessary to avoid "unexpected '--filter-platform'" errors - RUSTUP_TOOLCHAIN=stable cargo-tree tree --frozen --all --no-dev-dependencies --no-indent --features ${{ matrix.job.features }} | grep -vE "$PWD" | sort --unique - + RUSTUP_TOOLCHAIN=stable cargo-tree tree --locked --all --no-dev-dependencies --no-indent --features ${{ matrix.job.features }} | grep -vE "$PWD" | sort --unique - name: Test uses: actions-rs/cargo@v1 with: @@ -172,8 +209,8 @@ jobs: env: RUSTFLAGS: '-Awarnings' - busybox_test: - name: Busybox test suite + build_makefile: + name: Build/Makefile runs-on: ${{ matrix.job.os }} strategy: fail-fast: false @@ -181,45 +218,26 @@ jobs: job: - { os: ubuntu-latest } steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - name: Install `rust` toolchain uses: actions-rs/toolchain@v1 with: toolchain: stable default: true profile: minimal # minimal component installation (ie, no documentation) - - name: "prepare busytest" + - name: Install/setup prerequisites shell: bash run: | - make prepare-busytest - - name: "run busybox testsuite" + ## Install/setup prerequisites + sudo apt-get -y update ; sudo apt-get -y install python3-sphinx ; + - name: "`make build`" shell: bash run: | - bindir=$(pwd)/target/debug - cd tmp/busybox-*/testsuite - S=$(bindir=$bindir ./runtest) && printf "%s\n" "$S" || { printf "%s\n" "$S" | grep "FAIL:" | sed -e "s/FAIL: /::warning ::Test failure:/g" ; } - - makefile_build: - name: Test the build target of the Makefile - runs-on: ${{ matrix.job.os }} - strategy: - fail-fast: false - matrix: - job: - - { os: ubuntu-latest } - steps: - - uses: actions/checkout@v1 - - name: Install `rust` toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - default: true - profile: minimal # minimal component installation (ie, no documentation) - - name: "Run make build" - shell: bash - run: | - sudo apt-get -y update ; sudo apt-get -y install python3-sphinx; make build + - name: "`make test`" + shell: bash + run: | + make test build: name: Build @@ -231,7 +249,6 @@ jobs: # { os, target, cargo-options, features, use-cross, toolchain } - { 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-16.04 , target: x86_64-unknown-linux-gnu , features: feat_os_unix , use-cross: use-cross } # - { os: ubuntu-18.04 , target: i586-unknown-linux-gnu , features: feat_os_unix , use-cross: use-cross } ## note: older windows platform; not required, dev-FYI only # - { os: ubuntu-18.04 , target: i586-unknown-linux-gnu , features: feat_os_unix , use-cross: use-cross } ## note: older windows platform; not required, dev-FYI only - { os: ubuntu-18.04 , target: i686-unknown-linux-gnu , features: feat_os_unix , use-cross: use-cross } @@ -244,11 +261,11 @@ jobs: - { os: windows-latest , target: x86_64-pc-windows-gnu , features: feat_os_windows } ## note: requires rust >= 1.43.0 to link correctly - { os: windows-latest , target: x86_64-pc-windows-msvc , features: feat_os_windows } steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - name: Install/setup prerequisites shell: bash run: | - ## install/setup prerequisites + ## Install/setup prerequisites case '${{ matrix.job.target }}' in arm-unknown-linux-gnueabihf) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;; aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;; @@ -261,22 +278,20 @@ jobs: shell: bash run: | ## VARs setup + outputs() { step_id="vars"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo ::set-output name=${var}::${!var}; done; } # toolchain TOOLCHAIN="stable" ## default to "stable" toolchain # * specify alternate/non-default TOOLCHAIN for *-pc-windows-gnu targets; gnu targets on Windows are broken for the standard *-pc-windows-msvc toolchain (refs: GH:rust-lang/rust#47048, GH:rust-lang/rust#53454, GH:rust-lang/cargo#6754) case ${{ matrix.job.target }} in *-pc-windows-gnu) TOOLCHAIN="stable-${{ matrix.job.target }}" ;; esac; # * use requested TOOLCHAIN if specified if [ -n "${{ matrix.job.toolchain }}" ]; then TOOLCHAIN="${{ matrix.job.toolchain }}" ; fi - echo set-output name=TOOLCHAIN::${TOOLCHAIN:-/false} - echo ::set-output name=TOOLCHAIN::${TOOLCHAIN} + outputs TOOLCHAIN # staging directory STAGING='_staging' - echo set-output name=STAGING::${STAGING} - echo ::set-output name=STAGING::${STAGING} + outputs STAGING # determine EXE suffix EXE_suffix="" ; case '${{ matrix.job.target }}' in *-pc-windows-*) EXE_suffix=".exe" ;; esac; - echo set-output name=EXE_suffix::${EXE_suffix} - echo ::set-output name=EXE_suffix::${EXE_suffix} + outputs EXE_suffix # parse commit reference info echo GITHUB_REF=${GITHUB_REF} echo GITHUB_SHA=${GITHUB_SHA} @@ -284,14 +299,7 @@ jobs: unset REF_BRANCH ; case "${GITHUB_REF}" in refs/heads/*) REF_BRANCH=${GITHUB_REF#refs/heads/} ;; esac; unset REF_TAG ; case "${GITHUB_REF}" in refs/tags/*) REF_TAG=${GITHUB_REF#refs/tags/} ;; esac; REF_SHAS=${GITHUB_SHA:0:8} - echo set-output name=REF_NAME::${REF_NAME} - echo set-output name=REF_BRANCH::${REF_BRANCH} - echo set-output name=REF_TAG::${REF_TAG} - echo set-output name=REF_SHAS::${REF_SHAS} - echo ::set-output name=REF_NAME::${REF_NAME} - echo ::set-output name=REF_BRANCH::${REF_BRANCH} - echo ::set-output name=REF_TAG::${REF_TAG} - echo ::set-output name=REF_SHAS::${REF_SHAS} + outputs REF_NAME REF_BRANCH REF_TAG REF_SHAS # parse target unset TARGET_ARCH case '${{ matrix.job.target }}' in @@ -301,68 +309,50 @@ jobs: i686-*) TARGET_ARCH=i686 ;; x86_64-*) TARGET_ARCH=x86_64 ;; esac; - echo set-output name=TARGET_ARCH::${TARGET_ARCH} - echo ::set-output name=TARGET_ARCH::${TARGET_ARCH} unset TARGET_OS ; case '${{ matrix.job.target }}' in *-linux-*) TARGET_OS=linux ;; *-apple-*) TARGET_OS=macos ;; *-windows-*) TARGET_OS=windows ;; esac; - echo set-output name=TARGET_OS::${TARGET_OS} - echo ::set-output name=TARGET_OS::${TARGET_OS} + outputs TARGET_ARCH TARGET_OS # package name PKG_suffix=".tar.gz" ; case '${{ matrix.job.target }}' in *-pc-windows-*) PKG_suffix=".zip" ;; esac; PKG_BASENAME=${PROJECT_NAME}-${REF_TAG:-$REF_SHAS}-${{ matrix.job.target }} PKG_NAME=${PKG_BASENAME}${PKG_suffix} - echo set-output name=PKG_suffix::${PKG_suffix} - echo set-output name=PKG_BASENAME::${PKG_BASENAME} - echo set-output name=PKG_NAME::${PKG_NAME} - echo ::set-output name=PKG_suffix::${PKG_suffix} - echo ::set-output name=PKG_BASENAME::${PKG_BASENAME} - echo ::set-output name=PKG_NAME::${PKG_NAME} + outputs PKG_suffix PKG_BASENAME PKG_NAME # deployable tag? (ie, leading "vM" or "M"; M == version number) unset DEPLOY ; if [[ $REF_TAG =~ ^[vV]?[0-9].* ]]; then DEPLOY='true' ; fi - echo set-output name=DEPLOY::${DEPLOY:-/false} - echo ::set-output name=DEPLOY::${DEPLOY} + outputs DEPLOY # DPKG architecture? unset DPKG_ARCH case ${{ matrix.job.target }} in x86_64-*-linux-*) DPKG_ARCH=amd64 ;; *-linux-*) DPKG_ARCH=${TARGET_ARCH} ;; esac - echo set-output name=DPKG_ARCH::${DPKG_ARCH} - echo ::set-output name=DPKG_ARCH::${DPKG_ARCH} + outputs DPKG_ARCH # DPKG version? unset DPKG_VERSION ; if [[ $REF_TAG =~ ^[vV]?[0-9].* ]]; then DPKG_VERSION=${REF_TAG/#[vV]/} ; fi - echo set-output name=DPKG_VERSION::${DPKG_VERSION} - echo ::set-output name=DPKG_VERSION::${DPKG_VERSION} + outputs DPKG_VERSION # DPKG base name/conflicts? DPKG_BASENAME=${PROJECT_NAME} DPKG_CONFLICTS=${PROJECT_NAME}-musl case ${{ matrix.job.target }} in *-musl) DPKG_BASENAME=${PROJECT_NAME}-musl ; DPKG_CONFLICTS=${PROJECT_NAME} ;; esac; - echo set-output name=DPKG_BASENAME::${DPKG_BASENAME} - echo set-output name=DPKG_CONFLICTS::${DPKG_CONFLICTS} - echo ::set-output name=DPKG_BASENAME::${DPKG_BASENAME} - echo ::set-output name=DPKG_CONFLICTS::${DPKG_CONFLICTS} + outputs DPKG_BASENAME DPKG_CONFLICTS # DPKG name unset DPKG_NAME; if [[ -n $DPKG_ARCH && -n $DPKG_VERSION ]]; then DPKG_NAME="${DPKG_BASENAME}_${DPKG_VERSION}_${DPKG_ARCH}.deb" ; fi - echo set-output name=DPKG_NAME::${DPKG_NAME} - echo ::set-output name=DPKG_NAME::${DPKG_NAME} + outputs DPKG_NAME # target-specific options # * CARGO_FEATURES_OPTION CARGO_FEATURES_OPTION='' ; if [ -n "${{ matrix.job.features }}" ]; then CARGO_FEATURES_OPTION='--features "${{ matrix.job.features }}"' ; fi - echo set-output name=CARGO_FEATURES_OPTION::${CARGO_FEATURES_OPTION} - echo ::set-output name=CARGO_FEATURES_OPTION::${CARGO_FEATURES_OPTION} + outputs CARGO_FEATURES_OPTION # * CARGO_USE_CROSS (truthy) CARGO_USE_CROSS='true' ; case '${{ matrix.job.use-cross }}' in ''|0|f|false|n|no) unset CARGO_USE_CROSS ;; esac; - echo set-output name=CARGO_USE_CROSS::${CARGO_USE_CROSS:-/false} - echo ::set-output name=CARGO_USE_CROSS::${CARGO_USE_CROSS} + outputs CARGO_USE_CROSS # ** pass needed environment into `cross` container (iff `cross` not already configured via "Cross.toml") if [ -n "${CARGO_USE_CROSS}" ] && [ ! -e "Cross.toml" ] ; then printf "[build.env]\npassthrough = [\"CI\"]\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; - echo set-output name=CARGO_TEST_OPTIONS::${CARGO_TEST_OPTIONS} - echo ::set-output name=CARGO_TEST_OPTIONS::${CARGO_TEST_OPTIONS} + outputs CARGO_TEST_OPTIONS # * executable for `strip`? STRIP="strip" case ${{ matrix.job.target }} in @@ -370,12 +360,11 @@ jobs: arm-*-linux-gnueabihf) STRIP="arm-linux-gnueabihf-strip" ;; *-pc-windows-msvc) STRIP="" ;; esac; - echo set-output name=STRIP::${STRIP:-/false} - echo ::set-output name=STRIP::${STRIP} + outputs STRIP - name: Create all needed build/work directories shell: bash run: | - ## create build/work space + ## Create build/work space mkdir -p '${{ steps.vars.outputs.STAGING }}' mkdir -p '${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.PKG_BASENAME }}' mkdir -p '${{ steps.vars.outputs.STAGING }}/dpkg' @@ -395,11 +384,12 @@ jobs: shell: bash run: | ## Dependent VARs setup + outputs() { step_id="dep_vars"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo ::set-output name=${var}::${!var}; done; } # * determine sub-crate utility list UTILITY_LIST="$(./util/show-utils.sh ${CARGO_FEATURES_OPTION})" + echo UTILITY_LIST=${UTILITY_LIST} CARGO_UTILITY_LIST_OPTIONS="$(for u in ${UTILITY_LIST}; do echo "-puu_${u}"; done;)" - echo set-output name=UTILITY_LIST::${UTILITY_LIST} - echo ::set-output name=CARGO_UTILITY_LIST_OPTIONS::${CARGO_UTILITY_LIST_OPTIONS} + outputs CARGO_UTILITY_LIST_OPTIONS - name: Install `cargo-tree` # for dependency information uses: actions-rs/install@v0.1 with: @@ -411,26 +401,26 @@ jobs: - name: Info shell: bash run: | - # Info - ## commit info + ## Info + # commit info echo "## commit" echo GITHUB_REF=${GITHUB_REF} echo GITHUB_SHA=${GITHUB_SHA} - ## environment + # environment echo "## environment" echo "CI='${CI}'" - ## tooling info display + # tooling info display echo "## tooling" which gcc >/dev/null 2>&1 && (gcc --version | head -1) || true - rustup -V + rustup -V 2>/dev/null rustup show active-toolchain cargo -V rustc -V cargo-tree tree -V - ## dependencies + # dependencies echo "## dependency list" cargo fetch --locked --quiet - cargo-tree tree --target=${{ matrix.job.target }} ${{ matrix.job.cargo-options }} ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} --all --no-dev-dependencies --no-indent | grep -vE "$PWD" | sort --unique + cargo-tree tree --locked --target=${{ matrix.job.target }} ${{ matrix.job.cargo-options }} ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} --all --no-dev-dependencies --no-indent | grep -vE "$PWD" | sort --unique - name: Build uses: actions-rs/cargo@v1 with: @@ -457,7 +447,7 @@ jobs: - name: Package shell: bash run: | - ## package artifact(s) + ## Package artifact(s) # binary cp 'target/${{ matrix.job.target }}/release/${{ env.PROJECT_NAME }}${{ steps.vars.outputs.EXE_suffix }}' '${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.PKG_BASENAME }}/' # `strip` binary (if needed) @@ -498,6 +488,37 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + test_busybox: + name: Tests/BusyBox test suite + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + - { os: ubuntu-latest } + steps: + - uses: actions/checkout@v2 + - name: Install `rust` toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + default: true + profile: minimal # minimal component installation (ie, no documentation) + - name: Install/setup prerequisites + shell: bash + run: | + make prepare-busytest + - name: "Run BusyBox test suite" + shell: bash + run: | + ## Run BusyBox test suite + bindir=$(pwd)/target/debug + cd tmp/busybox-*/testsuite + output=$(bindir=$bindir ./runtest 2>&1 || true) + printf "%s\n" "${output}" + n_fails=$(echo "$output" | grep "^FAIL:\s" | wc --lines) + if [ $n_fails -gt 0 ] ; then echo "::warning ::${n_fails}+ test failures" ; fi + coverage: name: Code Coverage runs-on: ${{ matrix.job.os }} @@ -510,11 +531,11 @@ jobs: - { os: macos-latest , features: macos } - { os: windows-latest , features: windows } steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - name: Install/setup prerequisites shell: bash run: | - ## install/setup prerequisites + ## Install/setup prerequisites case '${{ matrix.job.os }}' in macos-latest) brew install coreutils ;; # needed for testing esac @@ -524,34 +545,31 @@ jobs: id: vars shell: bash run: | + ## VARs setup + outputs() { step_id="vars"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo ::set-output name=${var}::${!var}; done; } # toolchain TOOLCHAIN="nightly-${{ env.RUST_COV_SRV }}" ## default to "nightly" toolchain (required for certain required unstable compiler flags) ## !maint: refactor when stable channel has needed support # * specify gnu-type TOOLCHAIN for windows; `grcov` requires gnu-style code coverage data files case ${{ matrix.job.os }} in windows-*) TOOLCHAIN="$TOOLCHAIN-x86_64-pc-windows-gnu" ;; esac; # * use requested TOOLCHAIN if specified if [ -n "${{ matrix.job.toolchain }}" ]; then TOOLCHAIN="${{ matrix.job.toolchain }}" ; fi - echo set-output name=TOOLCHAIN::${TOOLCHAIN} - echo ::set-output name=TOOLCHAIN::${TOOLCHAIN} + outputs TOOLCHAIN # staging directory STAGING='_staging' - echo set-output name=STAGING::${STAGING} - echo ::set-output name=STAGING::${STAGING} + outputs STAGING ## # check for CODECOV_TOKEN availability (work-around for inaccessible 'secrets' object for 'if'; see ) ## # note: CODECOV_TOKEN / HAS_CODECOV_TOKEN is not needed for public repositories when using AppVeyor, Azure Pipelines, CircleCI, GitHub Actions, Travis (see ) ## unset HAS_CODECOV_TOKEN ## if [ -n $CODECOV_TOKEN ]; then HAS_CODECOV_TOKEN='true' ; fi - ## echo set-output name=HAS_CODECOV_TOKEN::${HAS_CODECOV_TOKEN} - ## echo ::set-output name=HAS_CODECOV_TOKEN::${HAS_CODECOV_TOKEN} + ## outputs HAS_CODECOV_TOKEN # target-specific options # * CARGO_FEATURES_OPTION CARGO_FEATURES_OPTION='--all-features' ; ## default to '--all-features' for code coverage if [ -n "${{ matrix.job.features }}" ]; then CARGO_FEATURES_OPTION='--features "${{ matrix.job.features }}"' ; fi - echo set-output name=CARGO_FEATURES_OPTION::${CARGO_FEATURES_OPTION} - echo ::set-output name=CARGO_FEATURES_OPTION::${CARGO_FEATURES_OPTION} + outputs CARGO_FEATURES_OPTION # * CODECOV_FLAGS CODECOV_FLAGS=$( echo "${{ matrix.job.os }}" | sed 's/[^[:alnum:]]/_/g' ) - echo set-output name=CODECOV_FLAGS::${CODECOV_FLAGS} - echo ::set-output name=CODECOV_FLAGS::${CODECOV_FLAGS} + outputs CODECOV_FLAGS - name: rust toolchain ~ install uses: actions-rs/toolchain@v1 with: @@ -563,11 +581,11 @@ jobs: shell: bash run: | ## Dependent VARs setup + outputs() { step_id="dep_vars"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo ::set-output name=${var}::${!var}; done; } # * determine sub-crate utility list UTILITY_LIST="$(./util/show-utils.sh ${CARGO_FEATURES_OPTION})" CARGO_UTILITY_LIST_OPTIONS="$(for u in ${UTILITY_LIST}; do echo "-puu_${u}"; done;)" - echo set-output name=UTILITY_LIST::${UTILITY_LIST} - echo ::set-output name=CARGO_UTILITY_LIST_OPTIONS::${CARGO_UTILITY_LIST_OPTIONS} + outputs CARGO_UTILITY_LIST_OPTIONS - name: Test uucore uses: actions-rs/cargo@v1 with: @@ -606,12 +624,12 @@ jobs: with: crate: grcov version: latest - use-tool-cache: true + use-tool-cache: false - name: Generate coverage data (via `grcov`) id: coverage shell: bash run: | - # generate coverage data + ## Generate coverage data COVERAGE_REPORT_DIR="target/debug" COVERAGE_REPORT_FILE="${COVERAGE_REPORT_DIR}/lcov.info" # GRCOV_IGNORE_OPTION='--ignore build.rs --ignore "/*" --ignore "[a-zA-Z]:/*"' ## `grcov` ignores these params when passed as an environment variable (why?) diff --git a/.github/workflows/FixPR.yml b/.github/workflows/FixPR.yml new file mode 100644 index 000000000..d3f8a86b8 --- /dev/null +++ b/.github/workflows/FixPR.yml @@ -0,0 +1,135 @@ +name: FixPR + +# Trigger automated fixes for PRs being merged (with associated commits) + +# ToDO: [2021-06; rivy] change from `cargo-tree` to `cargo tree` once MSRV is >= 1.45 + +env: + BRANCH_TARGET: master + +on: + # * only trigger on pull request closed to specific branches + # ref: https://github.community/t/trigger-workflow-only-on-pull-request-merge/17359/9 + pull_request: + branches: + - master # == env.BRANCH_TARGET ## unfortunately, env context variables are only available in jobs/steps (see ) + types: [ closed ] + +jobs: + code_deps: + # Refresh dependencies (ie, 'Cargo.lock') and show updated dependency tree + if: github.event.pull_request.merged == true ## only for PR merges + name: Update/dependencies + runs-on: ${{ matrix.job.os }} + strategy: + matrix: + job: + - { os: ubuntu-latest , features: feat_os_unix } + steps: + - uses: actions/checkout@v2 + - name: Initialize job variables + id: vars + shell: bash + run: | + ## VARs setup + outputs() { step_id="vars"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo ::set-output name=${var}::${!var}; done; } + # surface MSRV from CICD workflow + RUST_MIN_SRV=$(grep -P "^\s+RUST_MIN_SRV:" .github/workflows/CICD.yml | grep -Po "(?<=\x22)\d+[.]\d+(?:[.]\d+)?(?=\x22)" ) + outputs RUST_MIN_SRV + - name: Install `rust` toolchain (v${{ steps.vars.outputs.RUST_MIN_SRV }}) + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ steps.vars.outputs.RUST_MIN_SRV }} + default: true + profile: minimal # minimal component installation (ie, no documentation) + - name: Install `cargo-tree` # for dependency information + uses: actions-rs/install@v0.1 + with: + crate: cargo-tree + version: latest + use-tool-cache: true + env: + RUSTUP_TOOLCHAIN: stable + - name: Ensure updated 'Cargo.lock' + shell: bash + run: | + # Ensure updated 'Cargo.lock' + # * 'Cargo.lock' is required to be in a format that `cargo` of MinSRV can interpret (eg, v1-format for MinSRV < v1.38) + cargo fetch --locked --quiet || cargo +${{ steps.vars.outputs.RUST_MIN_SRV }} update + - name: Info + shell: bash + run: | + # Info + ## environment + echo "## environment" + echo "CI='${CI}'" + ## tooling info display + echo "## tooling" + which gcc >/dev/null 2>&1 && (gcc --version | head -1) || true + rustup -V 2>/dev/null + rustup show active-toolchain + cargo -V + rustc -V + cargo-tree tree -V + ## dependencies + echo "## dependency list" + cargo fetch --locked --quiet + ## * using the 'stable' toolchain is necessary to avoid "unexpected '--filter-platform'" errors + RUSTUP_TOOLCHAIN=stable cargo-tree tree --locked --all --no-dev-dependencies --no-indent --features ${{ matrix.job.features }} | grep -vE "$PWD" | sort --unique + - name: Commit any changes (to '${{ env.BRANCH_TARGET }}') + uses: EndBug/add-and-commit@v7 + with: + branch: ${{ env.BRANCH_TARGET }} + default_author: github_actions + message: "maint ~ refresh 'Cargo.lock'" + add: Cargo.lock + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + code_format: + # Recheck/refresh code formatting + if: github.event.pull_request.merged == true ## only for PR merges + name: Update/format + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + - { os: ubuntu-latest , features: feat_os_unix } + steps: + - uses: actions/checkout@v2 + - name: Initialize job variables + id: vars + shell: bash + run: | + ## VARs setup + outputs() { step_id="vars"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo ::set-output name=${var}::${!var}; done; } + # target-specific options + # * CARGO_FEATURES_OPTION + CARGO_FEATURES_OPTION='' ; + if [ -n "${{ matrix.job.features }}" ]; then CARGO_FEATURES_OPTION='--features "${{ matrix.job.features }}"' ; fi + outputs CARGO_FEATURES_OPTION + - name: Install `rust` toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + default: true + profile: minimal # minimal component installation (ie, no documentation) + components: rustfmt + - name: "`cargo fmt`" + shell: bash + run: | + cargo fmt + - name: "`cargo fmt` tests" + shell: bash + run: | + # `cargo fmt` of tests + find tests -name "*.rs" -print0 | xargs -0 cargo fmt -- + - name: Commit any changes (to '${{ env.BRANCH_TARGET }}') + uses: EndBug/add-and-commit@v7 + with: + branch: ${{ env.BRANCH_TARGET }} + default_author: github_actions + message: "maint ~ rustfmt (`cargo fmt`)" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/GNU.yml b/.github/workflows/GnuTests.yml similarity index 77% rename from .github/workflows/GNU.yml rename to .github/workflows/GnuTests.yml index 1f9250900..8bf6c091b 100644 --- a/.github/workflows/GNU.yml +++ b/.github/workflows/GnuTests.yml @@ -1,4 +1,6 @@ -name: GNU +name: GnuTests + +# spell-checker:ignore (names) gnulib ; (utils) autopoint gperf pyinotify texinfo ; (vars) XPASS on: [push, pull_request] @@ -7,7 +9,6 @@ jobs: name: Run GNU tests runs-on: ubuntu-latest steps: - # Checks out a copy of your repository on the ubuntu-latest machine - name: Checkout code uutil uses: actions/checkout@v2 with: @@ -18,7 +19,7 @@ jobs: repository: 'coreutils/coreutils' path: 'gnu' ref: v8.32 - - name: Checkout GNU corelib + - name: Checkout GNU coreutils library (gnulib) uses: actions/checkout@v2 with: repository: 'coreutils/gnulib' @@ -32,23 +33,26 @@ jobs: default: true profile: minimal # minimal component installation (ie, no documentation) components: rustfmt - - name: Install deps + - name: Install dependencies shell: bash run: | + ## Install dependencies sudo apt-get update sudo apt-get install autoconf autopoint bison texinfo gperf gcc g++ gdb python-pyinotify python3-sphinx jq - name: Build binaries shell: bash run: | - cd uutils - bash util/build-gnu.sh + ## Build binaries + cd uutils + bash util/build-gnu.sh - name: Run GNU tests shell: bash run: | bash uutils/util/run-gnu-test.sh - - name: Extract tests info + - name: Extract testing info shell: bash run: | + ## Extract testing info LOG_FILE=gnu/tests/test-suite.log if test -f "$LOG_FILE" then @@ -58,7 +62,13 @@ jobs: FAIL=$(sed -n "s/.*# FAIL: \(.*\)/\1/p" "$LOG_FILE"|tr -d '\r'|head -n1) XPASS=$(sed -n "s/.*# XPASS: \(.*\)/\1/p" "$LOG_FILE"|tr -d '\r'|head -n1) ERROR=$(sed -n "s/.*# ERROR: \(.*\)/\1/p" "$LOG_FILE"|tr -d '\r'|head -n1) - echo "::warning ::GNU testsuite = TOTAL: $TOTAL / PASS: $PASS / FAIL: $FAIL / ERROR: $ERROR" + if [[ "$TOTAL" -eq 0 || "$TOTAL" -eq 1 ]]; then + echo "Error in the execution, failing early" + exit 1 + fi + output="GNU tests summary = TOTAL: $TOTAL / PASS: $PASS / FAIL: $FAIL / ERROR: $ERROR" + echo "${output}" + if [[ "$FAIL" -gt 0 || "$ERROR" -gt 0 ]]; then echo "::warning ::${output}" ; fi jq -n \ --arg date "$(date --rfc-email)" \ --arg sha "$GITHUB_SHA" \ @@ -72,12 +82,10 @@ jobs: else echo "::error ::Failed to get summary of test results" fi - - uses: actions/upload-artifact@v2 with: name: test-report path: gnu/tests/**/*.log - - uses: actions/upload-artifact@v2 with: name: gnu-result diff --git a/.vscode/cspell.dictionaries/acronyms+names.wordlist.txt b/.vscode/cspell.dictionaries/acronyms+names.wordlist.txt index 3956d1d8a..a46448a32 100644 --- a/.vscode/cspell.dictionaries/acronyms+names.wordlist.txt +++ b/.vscode/cspell.dictionaries/acronyms+names.wordlist.txt @@ -12,6 +12,7 @@ FIFOs FQDN # fully qualified domain name GID # group ID GIDs +GNU GNUEABI GNUEABIhf JFS @@ -45,6 +46,7 @@ Deno EditorConfig FreeBSD Gmail +GNU Irix MS-DOS MSDOS diff --git a/.vscode/cspell.dictionaries/jargon.wordlist.txt b/.vscode/cspell.dictionaries/jargon.wordlist.txt index 89af1b153..c2e2c29f3 100644 --- a/.vscode/cspell.dictionaries/jargon.wordlist.txt +++ b/.vscode/cspell.dictionaries/jargon.wordlist.txt @@ -78,6 +78,7 @@ symlinks syscall syscalls tokenize +toolchain truthy unbuffered unescape diff --git a/.vscode/cspell.dictionaries/people.wordlist.txt b/.vscode/cspell.dictionaries/people.wordlist.txt index 01cfa4a3e..d7665585b 100644 --- a/.vscode/cspell.dictionaries/people.wordlist.txt +++ b/.vscode/cspell.dictionaries/people.wordlist.txt @@ -58,6 +58,10 @@ Haitao Li Inokentiy Babushkin Inokentiy Babushkin +Jan Scheer * jhscheer + Jan + Scheer + jhscheer Jeremiah Peschka Jeremiah Peschka @@ -97,6 +101,9 @@ Michael Debertol Michael Gehring Michael Gehring +Mitchell Mebane + Mitchell + Mebane Morten Olsen Lysgaard Morten Olsen diff --git a/.vscode/cspell.dictionaries/workspace.wordlist.txt b/.vscode/cspell.dictionaries/workspace.wordlist.txt index e2a864f9c..7242199a5 100644 --- a/.vscode/cspell.dictionaries/workspace.wordlist.txt +++ b/.vscode/cspell.dictionaries/workspace.wordlist.txt @@ -7,6 +7,7 @@ advapi advapi32-sys aho-corasick backtrace +blake2b_simd bstr byteorder chacha @@ -47,16 +48,19 @@ xattr # * rust/rustc RUSTDOCFLAGS RUSTFLAGS -bitxor # BitXor trait function clippy +rustc +rustfmt +rustup +# +bitor # BitOr trait function +bitxor # BitXor trait function concat fract powi println repr rfind -rustc -rustfmt struct structs substr diff --git a/Cargo.lock b/Cargo.lock index 0455d7118..993fe3e39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6,16 +6,6 @@ version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" -[[package]] -name = "advapi32-sys" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e06588080cb19d0acb6739808aafa5f26bfb2ca015b2b6370028b44cf7cb8a9a" -dependencies = [ - "winapi 0.2.8", - "winapi-build", -] - [[package]] name = "aho-corasick" version = "0.7.18" @@ -44,13 +34,16 @@ dependencies = [ ] [[package]] -name = "arrayvec" -version = "0.4.12" +name = "arrayref" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9" -dependencies = [ - "nodrop", -] +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" [[package]] name = "atty" @@ -100,11 +93,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" [[package]] -name = "blake2-rfc" -version = "0.2.18" +name = "blake2b_simd" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d6d530bdd2d52966a6d03b7a964add7ae1a288d25214066fd4b600f0f796400" +checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" dependencies = [ + "arrayref", "arrayvec", "constant_time_eq", ] @@ -183,6 +177,7 @@ dependencies = [ "atty", "bitflags", "strsim", + "term_size", "textwrap", "unicode-width", "vec_map", @@ -224,6 +219,7 @@ version = "0.0.6" dependencies = [ "atty", "chrono", + "clap", "conv", "filetime", "glob 0.3.0", @@ -700,9 +696,9 @@ checksum = "62aca2aba2d62b4a7f5b33f3712cb1b0692779a56fb510499d5c0aa594daeaf3" [[package]] name = "heck" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" dependencies = [ "unicode-segmentation", ] @@ -1383,12 +1379,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae1ded71d66a4a97f5e961fd0cb25a5f366a42a41570d16a763a69c092c26ae4" -dependencies = [ - "byteorder", -] +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" [[package]] name = "regex-syntax" @@ -1442,21 +1435,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" -[[package]] -name = "semver" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" -dependencies = [ - "semver-parser", -] - -[[package]] -name = "semver-parser" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" - [[package]] name = "sha1" version = "0.6.0" @@ -1501,9 +1479,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16f1d0fef1604ba8f7a073c7e701f213e056707210e9020af4528e0101ce11a6" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" dependencies = [ "libc", ] @@ -1771,6 +1749,7 @@ dependencies = [ name = "uu_cat" version = "0.0.6" dependencies = [ + "atty", "clap", "nix 0.20.0", "thiserror", @@ -1783,6 +1762,7 @@ dependencies = [ name = "uu_chgrp" version = "0.0.6" dependencies = [ + "clap", "uucore", "uucore_procs", "walkdir", @@ -1871,6 +1851,7 @@ dependencies = [ name = "uu_cut" version = "0.0.6" dependencies = [ + "atty", "bstr", "clap", "memchr 2.4.0", @@ -1904,6 +1885,7 @@ dependencies = [ name = "uu_dircolors" version = "0.0.6" dependencies = [ + "clap", "glob 0.3.0", "uucore", "uucore_procs", @@ -1964,6 +1946,7 @@ dependencies = [ name = "uu_expr" version = "0.0.6" dependencies = [ + "clap", "libc", "num-bigint", "num-traits", @@ -1991,6 +1974,7 @@ dependencies = [ name = "uu_false" version = "0.0.6" dependencies = [ + "clap", "uucore", "uucore_procs", ] @@ -2028,7 +2012,7 @@ dependencies = [ name = "uu_hashsum" version = "0.0.6" dependencies = [ - "blake2-rfc", + "blake2b_simd", "clap", "digest", "hex", @@ -2056,6 +2040,7 @@ dependencies = [ name = "uu_hostid" version = "0.0.6" dependencies = [ + "clap", "libc", "uucore", "uucore_procs", @@ -2215,6 +2200,8 @@ dependencies = [ "nix 0.13.1", "redox_syscall 0.1.57", "redox_termios", + "unicode-segmentation", + "unicode-width", "uucore", "uucore_procs", ] @@ -2258,6 +2245,7 @@ dependencies = [ name = "uu_nohup" version = "0.0.6" dependencies = [ + "atty", "clap", "libc", "uucore", @@ -2329,6 +2317,7 @@ name = "uu_pr" version = "0.0.6" dependencies = [ "chrono", + "clap", "getopts", "itertools 0.10.0", "quick-error 2.0.1", @@ -2351,6 +2340,7 @@ dependencies = [ name = "uu_printf" version = "0.0.6" dependencies = [ + "clap", "itertools 0.8.2", "uucore", "uucore_procs", @@ -2416,6 +2406,7 @@ dependencies = [ "uucore", "uucore_procs", "walkdir", + "winapi 0.3.9", ] [[package]] @@ -2483,7 +2474,6 @@ dependencies = [ "ouroboros", "rand 0.7.3", "rayon", - "semver", "tempfile", "unicode-width", "uucore", @@ -2586,6 +2576,7 @@ dependencies = [ name = "uu_test" version = "0.0.6" dependencies = [ + "clap", "libc", "redox_syscall 0.1.57", "uucore", @@ -2597,8 +2588,8 @@ name = "uu_timeout" version = "0.0.6" dependencies = [ "clap", - "getopts", "libc", + "nix 0.20.0", "uucore", "uucore_procs", ] @@ -2629,6 +2620,7 @@ dependencies = [ name = "uu_true" version = "0.0.6" dependencies = [ + "clap", "uucore", "uucore_procs", ] @@ -2655,6 +2647,7 @@ dependencies = [ name = "uu_tty" version = "0.0.6" dependencies = [ + "atty", "clap", "libc", "uucore", @@ -2746,7 +2739,6 @@ dependencies = [ name = "uu_whoami" version = "0.0.6" dependencies = [ - "advapi32-sys", "clap", "uucore", "uucore_procs", diff --git a/Cargo.toml b/Cargo.toml index 5f89a4077..02d0abb88 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,6 +63,7 @@ feat_common_core = [ "more", "mv", "nl", + "numfmt", "od", "paste", "pr", @@ -160,7 +161,6 @@ feat_require_unix = [ "mkfifo", "mknod", "nice", - "numfmt", "nohup", "pathchk", "stat", @@ -225,6 +225,7 @@ test = [ "uu_test" ] [workspace] [dependencies] +clap = { version = "2.33", features = ["wrap_help"] } lazy_static = { version="1.3" } textwrap = { version="=0.11.0", features=["term_size"] } # !maint: [2020-05-10; rivy] unstable crate using undocumented features; pinned currently, will review uucore = { version=">=0.0.8", package="uucore", path="src/uucore" } @@ -349,9 +350,9 @@ sha1 = { version="0.6", features=["std"] } tempfile = "3.2.0" time = "0.1" unindent = "0.1" -uucore = { version=">=0.0.8", package="uucore", path="src/uucore", features=["entries"] } +uucore = { version=">=0.0.8", package="uucore", path="src/uucore", features=["entries", "process"] } walkdir = "2.2" -atty = "0.2.14" +atty = "0.2" [target.'cfg(unix)'.dev-dependencies] rlimit = "0.4.0" diff --git a/GNUmakefile b/GNUmakefile index 102856b66..89a4dca80 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -268,11 +268,11 @@ test: ${CARGO} test ${CARGOFLAGS} --features "$(TESTS) $(TEST_SPEC_FEATURE)" --no-default-features $(TEST_NO_FAIL_FAST) busybox-src: - if [ ! -e $(BUSYBOX_SRC) ]; then \ - mkdir -p $(BUSYBOX_ROOT); \ - wget https://busybox.net/downloads/busybox-$(BUSYBOX_VER).tar.bz2 -P $(BUSYBOX_ROOT); \ - tar -C $(BUSYBOX_ROOT) -xf $(BUSYBOX_ROOT)/busybox-$(BUSYBOX_VER).tar.bz2; \ - fi; \ + if [ ! -e "$(BUSYBOX_SRC)" ] ; then \ + mkdir -p "$(BUSYBOX_ROOT)" ; \ + wget "https://busybox.net/downloads/busybox-$(BUSYBOX_VER).tar.bz2" -P "$(BUSYBOX_ROOT)" ; \ + tar -C "$(BUSYBOX_ROOT)" -xf "$(BUSYBOX_ROOT)/busybox-$(BUSYBOX_VER).tar.bz2" ; \ + fi ; # This is a busybox-specific config file their test suite wants to parse. $(BUILDDIR)/.config: $(BASEDIR)/.busybox-config @@ -280,10 +280,12 @@ $(BUILDDIR)/.config: $(BASEDIR)/.busybox-config # Test under the busybox test suite $(BUILDDIR)/busybox: busybox-src build-coreutils $(BUILDDIR)/.config - cp $(BUILDDIR)/coreutils $(BUILDDIR)/busybox; \ - chmod +x $@; + cp "$(BUILDDIR)/coreutils" "$(BUILDDIR)/busybox" + chmod +x $@ prepare-busytest: $(BUILDDIR)/busybox + # disable inapplicable tests + -( cd "$(BUSYBOX_SRC)/testsuite" ; if [ -e "busybox.tests" ] ; then mv busybox.tests busybox.tests- ; fi ; ) ifeq ($(EXES),) busytest: @@ -312,6 +314,11 @@ else endif $(foreach man, $(filter $(INSTALLEES), $(basename $(notdir $(wildcard $(DOCSDIR)/_build/man/*)))), \ cat $(DOCSDIR)/_build/man/$(man).1 | gzip > $(INSTALLDIR_MAN)/$(PROG_PREFIX)$(man).1.gz &&) : + $(foreach prog, $(INSTALLEES), \ + $(BUILDDIR)/coreutils completion $(prog) zsh > $(DESTDIR)$(PREFIX)/share/zsh/site-functions/_$(PROG_PREFIX)$(prog); \ + $(BUILDDIR)/coreutils completion $(prog) bash > $(DESTDIR)$(PREFIX)/share/bash-completion/completions/$(PROG_PREFIX)$(prog); \ + $(BUILDDIR)/coreutils completion $(prog) fish > $(DESTDIR)$(PREFIX)/share/fish/vendor_completions.d/$(PROG_PREFIX)$(prog).fish; \ + ) uninstall: ifeq (${MULTICALL}, y) @@ -319,6 +326,9 @@ ifeq (${MULTICALL}, y) endif rm -f $(addprefix $(INSTALLDIR_MAN)/,$(PROG_PREFIX)coreutils.1.gz) rm -f $(addprefix $(INSTALLDIR_BIN)/$(PROG_PREFIX),$(PROGS)) + rm -f $(addprefix $(DESTDIR)$(PREFIX)/share/zsh/site-functions/_$(PROG_PREFIX),$(PROGS)) + rm -f $(addprefix $(DESTDIR)$(PREFIX)/share/bash-completion/completions/$(PROG_PREFIX),$(PROGS)) + rm -f $(addprefix $(DESTDIR)$(PREFIX)/share/fish/vendor_completions.d/$(PROG_PREFIX),$(addsuffix .fish,$(PROGS))) rm -f $(addprefix $(INSTALLDIR_MAN)/$(PROG_PREFIX),$(addsuffix .1.gz,$(PROGS))) .PHONY: all build build-coreutils build-pkgs build-docs test distclean clean busytest install uninstall diff --git a/README.md b/README.md index fde01d64a..083320ac0 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,9 @@ $ cargo install --path . This command will install uutils into Cargo's *bin* folder (*e.g.* `$HOME/.cargo/bin`). +This does not install files necessary for shell completion. For shell completion to work, +use `GNU Make` or see `Manually install shell completions`. + ### GNU Make To install all available utilities: @@ -179,6 +182,10 @@ Set install parent directory (default value is /usr/local): $ make PREFIX=/my/path install ``` +Installing with `make` installs shell completions for all installed utilities +for `bash`, `fish` and `zsh`. Completions for `elvish` and `powershell` can also +be generated; See `Manually install shell completions`. + ### NixOS The [standard package set](https://nixos.org/nixpkgs/manual/) of [NixOS](https://nixos.org/) @@ -188,6 +195,23 @@ provides this package out of the box since 18.03: $ nix-env -iA nixos.uutils-coreutils ``` +### Manually install shell completions + +The `coreutils` binary can generate completions for the `bash`, `elvish`, `fish`, `powershell` +and `zsh` shells. It prints the result to stdout. + +The syntax is: +```bash +cargo run completion +``` + +So, to install completions for `ls` on `bash` to `/usr/local/share/bash-completion/completions/ls`, +run: + +```bash +cargo run completion ls bash > /usr/local/share/bash-completion/completions/ls +``` + ## Un-installation Instructions Un-installation differs depending on how you have installed uutils. If you used @@ -342,22 +366,22 @@ To contribute to uutils, please see [CONTRIBUTING](CONTRIBUTING.md). | Done | Semi-Done | To Do | |-----------|-----------|--------| | arch | cp | chcon | -| base32 | expr | csplit | -| base64 | install | dd | -| basename | ls | df | -| cat | more | numfmt | -| chgrp | od (`--strings` and 128-bit data types missing) | runcon | -| chmod | printf | stty | -| chown | sort | | -| chroot | split | | -| cksum | tail | | -| comm | test | | -| csplit | date | | -| cut | join | | -| dircolors | df | | +| base32 | date | dd | +| base64 | df | runcon | +| basename | expr | stty | +| cat | install | | +| chgrp | join | | +| chmod | ls | | +| chown | more | | +| chroot | numfmt | | +| cksum | od (`--strings` and 128-bit data types missing) | | +| comm | pr | | +| csplit | printf | | +| cut | sort | | +| dircolors | split | | | dirname | tac | | -| du | pr | | -| echo | | | +| du | tail | | +| echo | test | | | env | | | | expand | | | | factor | | | @@ -374,12 +398,12 @@ To contribute to uutils, please see [CONTRIBUTING](CONTRIBUTING.md). | link | | | | ln | | | | logname | | | -| ~~md5sum~~ (replaced by [hashsum](https://github.com/uutils/coreutils/blob/master/src/uu/hashsum/src/hashsum.rs)) | | -| ~~sha1sum~~ (replaced by [hashsum](https://github.com/uutils/coreutils/blob/master/src/uu/hashsum/src/hashsum.rs)) | | -| ~~sha224sum~~ (replaced by [hashsum](https://github.com/uutils/coreutils/blob/master/src/uu/hashsum/src/hashsum.rs)) | | -| ~~sha256sum~~ (replaced by [hashsum](https://github.com/uutils/coreutils/blob/master/src/uu/hashsum/src/hashsum.rs)) | | -| ~~sha384sum~~ (replaced by [hashsum](https://github.com/uutils/coreutils/blob/master/src/uu/hashsum/src/hashsum.rs)) | | -| ~~sha512sum~~ (replaced by [hashsum](https://github.com/uutils/coreutils/blob/master/src/uu/hashsum/src/hashsum.rs)) | | +| ~~md5sum~~ (replaced by [hashsum](https://github.com/uutils/coreutils/blob/master/src/uu/hashsum/src/hashsum.rs)) | | | +| ~~sha1sum~~ (replaced by [hashsum](https://github.com/uutils/coreutils/blob/master/src/uu/hashsum/src/hashsum.rs)) | | | +| ~~sha224sum~~ (replaced by [hashsum](https://github.com/uutils/coreutils/blob/master/src/uu/hashsum/src/hashsum.rs)) | | | +| ~~sha256sum~~ (replaced by [hashsum](https://github.com/uutils/coreutils/blob/master/src/uu/hashsum/src/hashsum.rs)) | | | +| ~~sha384sum~~ (replaced by [hashsum](https://github.com/uutils/coreutils/blob/master/src/uu/hashsum/src/hashsum.rs)) | | | +| ~~sha512sum~~ (replaced by [hashsum](https://github.com/uutils/coreutils/blob/master/src/uu/hashsum/src/hashsum.rs)) | | | | mkdir | | | | mkfifo | | | | mknod | | | diff --git a/build.rs b/build.rs index ae38177b0..e9fe129eb 100644 --- a/build.rs +++ b/build.rs @@ -43,7 +43,7 @@ pub fn main() { let mut tf = File::create(Path::new(&out_dir).join("test_modules.rs")).unwrap(); mf.write_all( - "type UtilityMap = HashMap<&'static str, fn(T) -> i32>;\n\ + "type UtilityMap = HashMap<&'static str, (fn(T) -> i32, fn() -> App<'static, 'static>)>;\n\ \n\ fn util_map() -> UtilityMap {\n\ \tlet mut map = UtilityMap::new();\n\ @@ -54,10 +54,33 @@ pub fn main() { for krate in crates { match krate.as_ref() { + // 'test' is named uu_test to avoid collision with rust core crate 'test'. + // It can also be invoked by name '[' for the '[ expr ] syntax'. + "uu_test" => { + mf.write_all( + format!( + "\ + \tmap.insert(\"test\", ({krate}::uumain, {krate}::uu_app));\n\ + \t\tmap.insert(\"[\", ({krate}::uumain, {krate}::uu_app));\n\ + ", + krate = krate + ) + .as_bytes(), + ) + .unwrap(); + tf.write_all( + format!( + "#[path=\"{dir}/test_test.rs\"]\nmod test_test;\n", + dir = util_tests_dir, + ) + .as_bytes(), + ) + .unwrap() + } k if k.starts_with(override_prefix) => { mf.write_all( format!( - "\tmap.insert(\"{k}\", {krate}::uumain);\n", + "\tmap.insert(\"{k}\", ({krate}::uumain, {krate}::uu_app));\n", k = krate[override_prefix.len()..].to_string(), krate = krate ) @@ -77,7 +100,7 @@ pub fn main() { "false" | "true" => { mf.write_all( format!( - "\tmap.insert(\"{krate}\", r#{krate}::uumain);\n", + "\tmap.insert(\"{krate}\", (r#{krate}::uumain, r#{krate}::uu_app));\n", krate = krate ) .as_bytes(), @@ -97,20 +120,20 @@ pub fn main() { mf.write_all( format!( "\ - \tmap.insert(\"{krate}\", {krate}::uumain);\n\ - \t\tmap.insert(\"md5sum\", {krate}::uumain);\n\ - \t\tmap.insert(\"sha1sum\", {krate}::uumain);\n\ - \t\tmap.insert(\"sha224sum\", {krate}::uumain);\n\ - \t\tmap.insert(\"sha256sum\", {krate}::uumain);\n\ - \t\tmap.insert(\"sha384sum\", {krate}::uumain);\n\ - \t\tmap.insert(\"sha512sum\", {krate}::uumain);\n\ - \t\tmap.insert(\"sha3sum\", {krate}::uumain);\n\ - \t\tmap.insert(\"sha3-224sum\", {krate}::uumain);\n\ - \t\tmap.insert(\"sha3-256sum\", {krate}::uumain);\n\ - \t\tmap.insert(\"sha3-384sum\", {krate}::uumain);\n\ - \t\tmap.insert(\"sha3-512sum\", {krate}::uumain);\n\ - \t\tmap.insert(\"shake128sum\", {krate}::uumain);\n\ - \t\tmap.insert(\"shake256sum\", {krate}::uumain);\n\ + \tmap.insert(\"{krate}\", ({krate}::uumain, {krate}::uu_app_custom));\n\ + \t\tmap.insert(\"md5sum\", ({krate}::uumain, {krate}::uu_app_common));\n\ + \t\tmap.insert(\"sha1sum\", ({krate}::uumain, {krate}::uu_app_common));\n\ + \t\tmap.insert(\"sha224sum\", ({krate}::uumain, {krate}::uu_app_common));\n\ + \t\tmap.insert(\"sha256sum\", ({krate}::uumain, {krate}::uu_app_common));\n\ + \t\tmap.insert(\"sha384sum\", ({krate}::uumain, {krate}::uu_app_common));\n\ + \t\tmap.insert(\"sha512sum\", ({krate}::uumain, {krate}::uu_app_common));\n\ + \t\tmap.insert(\"sha3sum\", ({krate}::uumain, {krate}::uu_app_common));\n\ + \t\tmap.insert(\"sha3-224sum\", ({krate}::uumain, {krate}::uu_app_common));\n\ + \t\tmap.insert(\"sha3-256sum\", ({krate}::uumain, {krate}::uu_app_common));\n\ + \t\tmap.insert(\"sha3-384sum\", ({krate}::uumain, {krate}::uu_app_common));\n\ + \t\tmap.insert(\"sha3-512sum\", ({krate}::uumain, {krate}::uu_app_common));\n\ + \t\tmap.insert(\"shake128sum\", ({krate}::uumain, {krate}::uu_app_common));\n\ + \t\tmap.insert(\"shake256sum\", ({krate}::uumain, {krate}::uu_app_common));\n\ ", krate = krate ) @@ -130,7 +153,7 @@ pub fn main() { _ => { mf.write_all( format!( - "\tmap.insert(\"{krate}\", {krate}::uumain);\n", + "\tmap.insert(\"{krate}\", ({krate}::uumain, {krate}::uu_app));\n", krate = krate ) .as_bytes(), diff --git a/docs/uutils.rst b/docs/uutils.rst index 19af87fef..e3b8c6a1a 100644 --- a/docs/uutils.rst +++ b/docs/uutils.rst @@ -16,7 +16,7 @@ Synopsis Description ----------- -``uutils`` is a program that contains that other coreutils commands, somewhat +``uutils`` is a program that contains other coreutils commands, somewhat similar to Busybox. --help, -h print a help menu for PROGRAM displaying accepted options and diff --git a/src/bin/coreutils.rs b/src/bin/coreutils.rs index 2e703b682..3e8df57f7 100644 --- a/src/bin/coreutils.rs +++ b/src/bin/coreutils.rs @@ -5,6 +5,9 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +use clap::App; +use clap::Arg; +use clap::Shell; use std::cmp; use std::collections::hash_map::HashMap; use std::ffi::OsString; @@ -52,7 +55,7 @@ fn main() { let binary_as_util = name(&binary); // binary name equals util name? - if let Some(&uumain) = utils.get(binary_as_util) { + if let Some(&(uumain, _)) = utils.get(binary_as_util) { process::exit(uumain((vec![binary.into()].into_iter()).chain(args))); } @@ -74,8 +77,12 @@ fn main() { if let Some(util_os) = util_name { let util = util_os.as_os_str().to_string_lossy(); + if util == "completion" { + gen_completions(args, utils); + } + match utils.get(&util[..]) { - Some(&uumain) => { + Some(&(uumain, _)) => { process::exit(uumain((vec![util_os].into_iter()).chain(args))); } None => { @@ -85,7 +92,7 @@ fn main() { let util = util_os.as_os_str().to_string_lossy(); match utils.get(&util[..]) { - Some(&uumain) => { + Some(&(uumain, _)) => { let code = uumain( (vec![util_os, OsString::from("--help")].into_iter()) .chain(args), @@ -113,3 +120,50 @@ fn main() { process::exit(0); } } + +/// Prints completions for the utility in the first parameter for the shell in the second parameter to stdout +fn gen_completions( + args: impl Iterator, + util_map: UtilityMap, +) -> ! { + let all_utilities: Vec<_> = std::iter::once("coreutils") + .chain(util_map.keys().copied()) + .collect(); + + let matches = App::new("completion") + .about("Prints completions to stdout") + .arg( + Arg::with_name("utility") + .possible_values(&all_utilities) + .required(true), + ) + .arg( + Arg::with_name("shell") + .possible_values(&Shell::variants()) + .required(true), + ) + .get_matches_from(std::iter::once(OsString::from("completion")).chain(args)); + + let utility = matches.value_of("utility").unwrap(); + let shell = matches.value_of("shell").unwrap(); + + let mut app = if utility == "coreutils" { + gen_coreutils_app(util_map) + } else { + util_map.get(utility).unwrap().1() + }; + let shell: Shell = shell.parse().unwrap(); + let bin_name = std::env::var("PROG_PREFIX").unwrap_or_default() + utility; + + app.gen_completions_to(bin_name, shell, &mut io::stdout()); + io::stdout().flush().unwrap(); + process::exit(0); +} + +fn gen_coreutils_app(util_map: UtilityMap) -> App<'static, 'static> { + let mut app = App::new("coreutils"); + for (_, (_, sub_app)) in util_map { + app = app.subcommand(sub_app()); + } + app +} diff --git a/src/uu/arch/Cargo.toml b/src/uu/arch/Cargo.toml index b3fe1f8cb..855b577d6 100644 --- a/src/uu/arch/Cargo.toml +++ b/src/uu/arch/Cargo.toml @@ -16,7 +16,7 @@ path = "src/arch.rs" [dependencies] platform-info = "0.1" -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/arch/src/arch.rs b/src/uu/arch/src/arch.rs index eddd24502..94ec97e98 100644 --- a/src/uu/arch/src/arch.rs +++ b/src/uu/arch/src/arch.rs @@ -12,18 +12,23 @@ extern crate uucore; use platform_info::*; use clap::{crate_version, App}; +use uucore::error::{FromIo, UResult}; static ABOUT: &str = "Display machine architecture"; static SUMMARY: &str = "Determine architecture name for current machine."; -pub fn uumain(args: impl uucore::Args) -> i32 { +#[uucore_procs::gen_uumain] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + uu_app().get_matches_from(args); + + let uts = PlatformInfo::new().map_err_context(|| "cannot get system name".to_string())?; + println!("{}", uts.machine().trim()); + Ok(()) +} + +pub fn uu_app() -> App<'static, 'static> { App::new(executable!()) .version(crate_version!()) .about(ABOUT) .after_help(SUMMARY) - .get_matches_from(args); - - let uts = return_if_err!(1, PlatformInfo::new()); - println!("{}", uts.machine().trim()); - 0 } diff --git a/src/uu/base32/Cargo.toml b/src/uu/base32/Cargo.toml index 1b448af0a..a024c49db 100644 --- a/src/uu/base32/Cargo.toml +++ b/src/uu/base32/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/base32.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features = ["encoding"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/base32/src/base32.rs b/src/uu/base32/src/base32.rs index f0e187c31..9a29717ac 100644 --- a/src/uu/base32/src/base32.rs +++ b/src/uu/base32/src/base32.rs @@ -10,6 +10,7 @@ extern crate uucore; use std::io::{stdin, Read}; +use clap::App; use uucore::encoding::Format; pub mod base_common; @@ -38,18 +39,11 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let config_result: Result = base_common::parse_base_cmd_args(args, name, VERSION, ABOUT, &usage); - - if config_result.is_err() { - match config_result { - Ok(_) => panic!(), - Err(s) => crash!(BASE_CMD_PARSE_ERROR, "{}", s), - } - } + let config = config_result.unwrap_or_else(|s| crash!(BASE_CMD_PARSE_ERROR, "{}", s)); // Create a reference to stdin so we can return a locked stdin from // parse_base_cmd_args let stdin_raw = stdin(); - let config = config_result.unwrap(); let mut input: Box = base_common::get_input(&config, &stdin_raw); base_common::handle_input( @@ -63,3 +57,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { 0 } + +pub fn uu_app() -> App<'static, 'static> { + base_common::base_app(executable!(), VERSION, ABOUT) +} diff --git a/src/uu/base32/src/base_common.rs b/src/uu/base32/src/base_common.rs index ee5fe8675..4fc8b495b 100644 --- a/src/uu/base32/src/base_common.rs +++ b/src/uu/base32/src/base_common.rs @@ -39,7 +39,7 @@ impl Config { Some(mut values) => { let name = values.next().unwrap(); if values.len() != 0 { - return Err(format!("extra operand ‘{}’", name)); + return Err(format!("extra operand '{}'", name)); } if name == "-" { @@ -54,15 +54,13 @@ impl Config { None => None, }; - let cols = match options.value_of(options::WRAP) { - Some(num) => match num.parse::() { - Ok(n) => Some(n), - Err(e) => { - return Err(format!("Invalid wrap size: ‘{}’: {}", num, e)); - } - }, - None => None, - }; + let cols = options + .value_of(options::WRAP) + .map(|num| { + num.parse::() + .map_err(|e| format!("Invalid wrap size: '{}': {}", num, e)) + }) + .transpose()?; Ok(Config { decode: options.is_present(options::DECODE), @@ -80,10 +78,17 @@ pub fn parse_base_cmd_args( about: &str, usage: &str, ) -> Result { - let app = App::new(name) + let app = base_app(name, version, about).usage(usage); + let arg_list = args + .collect_str(InvalidEncodingHandling::ConvertLossy) + .accept_any(); + Config::from(app.get_matches_from(arg_list)) +} + +pub fn base_app<'a>(name: &str, version: &'a str, about: &'a str) -> App<'static, 'a> { + App::new(name) .version(version) .about(about) - .usage(usage) // Format arguments. .arg( Arg::with_name(options::DECODE) @@ -108,11 +113,7 @@ pub fn parse_base_cmd_args( ) // "multiple" arguments are used to check whether there is more than one // file passed in. - .arg(Arg::with_name(options::FILE).index(1).multiple(true)); - let arg_list = args - .collect_str(InvalidEncodingHandling::ConvertLossy) - .accept_any(); - Config::from(app.get_matches_from(arg_list)) + .arg(Arg::with_name(options::FILE).index(1).multiple(true)) } pub fn get_input<'a>(config: &Config, stdin_ref: &'a Stdin) -> Box { diff --git a/src/uu/base64/Cargo.toml b/src/uu/base64/Cargo.toml index d4ee69f03..202c6511b 100644 --- a/src/uu/base64/Cargo.toml +++ b/src/uu/base64/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/base64.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features = ["encoding"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } uu_base32 = { version=">=0.0.6", package="uu_base32", path="../base32"} diff --git a/src/uu/base64/src/base64.rs b/src/uu/base64/src/base64.rs index 810df4fe8..71ed44e6e 100644 --- a/src/uu/base64/src/base64.rs +++ b/src/uu/base64/src/base64.rs @@ -10,6 +10,7 @@ extern crate uucore; use uu_base32::base_common; +pub use uu_base32::uu_app; use uucore::encoding::Format; @@ -38,18 +39,11 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let name = executable!(); let config_result: Result = base_common::parse_base_cmd_args(args, name, VERSION, ABOUT, &usage); - - if config_result.is_err() { - match config_result { - Ok(_) => panic!(), - Err(s) => crash!(BASE_CMD_PARSE_ERROR, "{}", s), - } - } + let config = config_result.unwrap_or_else(|s| crash!(BASE_CMD_PARSE_ERROR, "{}", s)); // Create a reference to stdin so we can return a locked stdin from // parse_base_cmd_args let stdin_raw = stdin(); - let config = config_result.unwrap(); let mut input: Box = base_common::get_input(&config, &stdin_raw); base_common::handle_input( diff --git a/src/uu/basename/Cargo.toml b/src/uu/basename/Cargo.toml index 0072619b7..9912dfd87 100644 --- a/src/uu/basename/Cargo.toml +++ b/src/uu/basename/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/basename.rs" [dependencies] -clap = "2.33.2" +clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/basename/src/basename.rs b/src/uu/basename/src/basename.rs index a0eed93f1..5450ee3f2 100644 --- a/src/uu/basename/src/basename.rs +++ b/src/uu/basename/src/basename.rs @@ -40,31 +40,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { // // Argument parsing // - let matches = App::new(executable!()) - .version(crate_version!()) - .about(SUMMARY) - .usage(&usage[..]) - .arg( - Arg::with_name(options::MULTIPLE) - .short("a") - .long(options::MULTIPLE) - .help("support multiple arguments and treat each as a NAME"), - ) - .arg(Arg::with_name(options::NAME).multiple(true).hidden(true)) - .arg( - Arg::with_name(options::SUFFIX) - .short("s") - .long(options::SUFFIX) - .value_name("SUFFIX") - .help("remove a trailing SUFFIX; implies -a"), - ) - .arg( - Arg::with_name(options::ZERO) - .short("z") - .long(options::ZERO) - .help("end each output line with NUL, not newline"), - ) - .get_matches_from(args); + let matches = uu_app().usage(&usage[..]).get_matches_from(args); // too few arguments if !matches.is_present(options::NAME) { @@ -110,22 +86,41 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let line_ending = if opt_zero { "\0" } else { "\n" }; for path in paths { - print!("{}{}", basename(&path, &suffix), line_ending); + print!("{}{}", basename(path, suffix), line_ending); } 0 } +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about(SUMMARY) + .arg( + Arg::with_name(options::MULTIPLE) + .short("a") + .long(options::MULTIPLE) + .help("support multiple arguments and treat each as a NAME"), + ) + .arg(Arg::with_name(options::NAME).multiple(true).hidden(true)) + .arg( + Arg::with_name(options::SUFFIX) + .short("s") + .long(options::SUFFIX) + .value_name("SUFFIX") + .help("remove a trailing SUFFIX; implies -a"), + ) + .arg( + Arg::with_name(options::ZERO) + .short("z") + .long(options::ZERO) + .help("end each output line with NUL, not newline"), + ) +} + fn basename(fullname: &str, suffix: &str) -> String { // Remove all platform-specific path separators from the end - let mut path: String = fullname - .chars() - .rev() - .skip_while(|&ch| is_separator(ch)) - .collect(); - - // Undo reverse - path = path.chars().rev().collect(); + let path = fullname.trim_end_matches(is_separator); // Convert to path buffer and get last path component let pb = PathBuf::from(path); diff --git a/src/uu/cat/Cargo.toml b/src/uu/cat/Cargo.toml index 09b289253..f20cddcf9 100644 --- a/src/uu/cat/Cargo.toml +++ b/src/uu/cat/Cargo.toml @@ -15,8 +15,9 @@ edition = "2018" path = "src/cat.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } thiserror = "1.0" +atty = "0.2" uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["fs"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/cat/src/cat.rs b/src/uu/cat/src/cat.rs index 1f2f441d8..35a5308ed 100644 --- a/src/uu/cat/src/cat.rs +++ b/src/uu/cat/src/cat.rs @@ -20,7 +20,6 @@ use clap::{crate_version, App, Arg}; use std::fs::{metadata, File}; use std::io::{self, Read, Write}; use thiserror::Error; -use uucore::fs::is_stdin_interactive; /// Linux splice support #[cfg(any(target_os = "linux", target_os = "android"))] @@ -170,7 +169,65 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .collect_str(InvalidEncodingHandling::Ignore) .accept_any(); - let matches = App::new(executable!()) + let matches = uu_app().get_matches_from(args); + + let number_mode = if matches.is_present(options::NUMBER_NONBLANK) { + NumberingMode::NonEmpty + } else if matches.is_present(options::NUMBER) { + NumberingMode::All + } else { + NumberingMode::None + }; + + let show_nonprint = vec![ + options::SHOW_ALL.to_owned(), + options::SHOW_NONPRINTING_ENDS.to_owned(), + options::SHOW_NONPRINTING_TABS.to_owned(), + options::SHOW_NONPRINTING.to_owned(), + ] + .iter() + .any(|v| matches.is_present(v)); + + let show_ends = vec![ + options::SHOW_ENDS.to_owned(), + options::SHOW_ALL.to_owned(), + options::SHOW_NONPRINTING_ENDS.to_owned(), + ] + .iter() + .any(|v| matches.is_present(v)); + + let show_tabs = vec![ + options::SHOW_ALL.to_owned(), + options::SHOW_TABS.to_owned(), + options::SHOW_NONPRINTING_TABS.to_owned(), + ] + .iter() + .any(|v| matches.is_present(v)); + + let squeeze_blank = matches.is_present(options::SQUEEZE_BLANK); + let files: Vec = match matches.values_of(options::FILE) { + Some(v) => v.clone().map(|v| v.to_owned()).collect(), + None => vec!["-".to_owned()], + }; + + let options = OutputOptions { + show_ends, + number: number_mode, + show_nonprint, + show_tabs, + squeeze_blank, + }; + let success = cat_files(files, &options).is_ok(); + + if success { + 0 + } else { + 1 + } +} + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) .name(NAME) .version(crate_version!()) .usage(SYNTAX) @@ -230,61 +287,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .long(options::SHOW_NONPRINTING) .help("use ^ and M- notation, except for LF (\\n) and TAB (\\t)"), ) - .get_matches_from(args); - - let number_mode = if matches.is_present(options::NUMBER_NONBLANK) { - NumberingMode::NonEmpty - } else if matches.is_present(options::NUMBER) { - NumberingMode::All - } else { - NumberingMode::None - }; - - let show_nonprint = vec![ - options::SHOW_ALL.to_owned(), - options::SHOW_NONPRINTING_ENDS.to_owned(), - options::SHOW_NONPRINTING_TABS.to_owned(), - options::SHOW_NONPRINTING.to_owned(), - ] - .iter() - .any(|v| matches.is_present(v)); - - let show_ends = vec![ - options::SHOW_ENDS.to_owned(), - options::SHOW_ALL.to_owned(), - options::SHOW_NONPRINTING_ENDS.to_owned(), - ] - .iter() - .any(|v| matches.is_present(v)); - - let show_tabs = vec![ - options::SHOW_ALL.to_owned(), - options::SHOW_TABS.to_owned(), - options::SHOW_NONPRINTING_TABS.to_owned(), - ] - .iter() - .any(|v| matches.is_present(v)); - - let squeeze_blank = matches.is_present(options::SQUEEZE_BLANK); - let files: Vec = match matches.values_of(options::FILE) { - Some(v) => v.clone().map(|v| v.to_owned()).collect(), - None => vec!["-".to_owned()], - }; - - let options = OutputOptions { - show_ends, - number: number_mode, - show_nonprint, - show_tabs, - squeeze_blank, - }; - let success = cat_files(files, &options).is_ok(); - - if success { - 0 - } else { - 1 - } } fn cat_handle( @@ -295,7 +297,7 @@ fn cat_handle( if options.can_write_fast() { write_fast(handle) } else { - write_lines(handle, &options, state) + write_lines(handle, options, state) } } @@ -306,9 +308,9 @@ fn cat_path(path: &str, options: &OutputOptions, state: &mut OutputState) -> Cat #[cfg(any(target_os = "linux", target_os = "android"))] file_descriptor: stdin.as_raw_fd(), reader: stdin, - is_interactive: is_stdin_interactive(), + is_interactive: atty::is(atty::Stream::Stdin), }; - return cat_handle(&mut handle, &options, state); + return cat_handle(&mut handle, options, state); } match get_input_type(path)? { InputType::Directory => Err(CatError::IsDirectory), @@ -322,7 +324,7 @@ fn cat_path(path: &str, options: &OutputOptions, state: &mut OutputState) -> Cat reader: socket, is_interactive: false, }; - cat_handle(&mut handle, &options, state) + cat_handle(&mut handle, options, state) } _ => { let file = File::open(path)?; @@ -332,7 +334,7 @@ fn cat_path(path: &str, options: &OutputOptions, state: &mut OutputState) -> Cat reader: file, is_interactive: false, }; - cat_handle(&mut handle, &options, state) + cat_handle(&mut handle, options, state) } } } @@ -345,7 +347,7 @@ fn cat_files(files: Vec, options: &OutputOptions) -> Result<(), u32> { }; for path in &files { - if let Err(err) = cat_path(path, &options, &mut state) { + if let Err(err) = cat_path(path, options, &mut state) { show_error!("{}: {}", path, err); error_count += 1; } diff --git a/src/uu/chgrp/Cargo.toml b/src/uu/chgrp/Cargo.toml index 9424ad35e..619bdaaad 100644 --- a/src/uu/chgrp/Cargo.toml +++ b/src/uu/chgrp/Cargo.toml @@ -15,6 +15,7 @@ edition = "2018" path = "src/chgrp.rs" [dependencies] +clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["entries", "fs", "perms"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } walkdir = "2.2" diff --git a/src/uu/chgrp/src/chgrp.rs b/src/uu/chgrp/src/chgrp.rs index f6afc2805..489be59eb 100644 --- a/src/uu/chgrp/src/chgrp.rs +++ b/src/uu/chgrp/src/chgrp.rs @@ -14,6 +14,8 @@ use uucore::fs::resolve_relative_path; use uucore::libc::gid_t; use uucore::perms::{wrap_chgrp, Verbosity}; +use clap::{App, Arg}; + extern crate walkdir; use walkdir::WalkDir; @@ -24,76 +26,117 @@ use std::os::unix::fs::MetadataExt; use std::path::Path; use uucore::InvalidEncodingHandling; -static SYNTAX: &str = - "chgrp [OPTION]... GROUP FILE...\n or : chgrp [OPTION]... --reference=RFILE FILE..."; -static SUMMARY: &str = "Change the group of each FILE to GROUP."; +static ABOUT: &str = "Change the group of each FILE to GROUP."; +static VERSION: &str = env!("CARGO_PKG_VERSION"); + +pub mod options { + pub mod verbosity { + pub static CHANGES: &str = "changes"; + pub static QUIET: &str = "quiet"; + pub static SILENT: &str = "silent"; + pub static VERBOSE: &str = "verbose"; + } + pub mod preserve_root { + pub static PRESERVE: &str = "preserve-root"; + pub static NO_PRESERVE: &str = "no-preserve-root"; + } + pub mod dereference { + pub static DEREFERENCE: &str = "dereference"; + pub static NO_DEREFERENCE: &str = "no-dereference"; + } + pub static RECURSIVE: &str = "recursive"; + pub mod traverse { + pub static TRAVERSE: &str = "H"; + pub static NO_TRAVERSE: &str = "P"; + pub static EVERY: &str = "L"; + } + pub static REFERENCE: &str = "reference"; + pub static ARG_GROUP: &str = "GROUP"; + pub static ARG_FILES: &str = "FILE"; +} const FTS_COMFOLLOW: u8 = 1; const FTS_PHYSICAL: u8 = 1 << 1; const FTS_LOGICAL: u8 = 1 << 2; +fn get_usage() -> String { + format!( + "{0} [OPTION]... GROUP FILE...\n {0} [OPTION]... --reference=RFILE FILE...", + executable!() + ) +} + pub fn uumain(args: impl uucore::Args) -> i32 { let args = args .collect_str(InvalidEncodingHandling::ConvertLossy) .accept_any(); - let mut opts = app!(SYNTAX, SUMMARY, ""); - opts.optflag("c", - "changes", - "like verbose but report only when a change is made") - .optflag("f", "silent", "") - .optflag("", "quiet", "suppress most error messages") - .optflag("v", - "verbose", - "output a diagnostic for every file processed") - .optflag("", "dereference", "affect the referent of each symbolic link (this is the default), rather than the symbolic link itself") - .optflag("h", "no-dereference", "affect symbolic links instead of any referenced file (useful only on systems that can change the ownership of a symlink)") - .optflag("", - "no-preserve-root", - "do not treat '/' specially (the default)") - .optflag("", "preserve-root", "fail to operate recursively on '/'") - .optopt("", - "reference", - "use RFILE's owner and group rather than specifying OWNER:GROUP values", - "RFILE") - .optflag("R", - "recursive", - "operate on files and directories recursively") - .optflag("H", - "", - "if a command line argument is a symbolic link to a directory, traverse it") - .optflag("L", - "", - "traverse every symbolic link to a directory encountered") - .optflag("P", "", "do not traverse any symbolic links (default)"); + let usage = get_usage(); - let mut bit_flag = FTS_PHYSICAL; - let mut preserve_root = false; - let mut derefer = -1; - let flags: &[char] = &['H', 'L', 'P']; - for opt in &args { - match opt.as_str() { - // If more than one is specified, only the final one takes effect. - s if s.contains(flags) => { - if let Some(idx) = s.rfind(flags) { - match s.chars().nth(idx).unwrap() { - 'H' => bit_flag = FTS_COMFOLLOW | FTS_PHYSICAL, - 'L' => bit_flag = FTS_LOGICAL, - 'P' => bit_flag = FTS_PHYSICAL, - _ => (), - } - } - } - "--no-preserve-root" => preserve_root = false, - "--preserve-root" => preserve_root = true, - "--dereference" => derefer = 1, - "--no-dereference" => derefer = 0, - _ => (), + let mut app = uu_app().usage(&usage[..]); + + // we change the positional args based on whether + // --reference was used. + let mut reference = false; + let mut help = false; + // stop processing options on -- + for arg in args.iter().take_while(|s| *s != "--") { + if arg.starts_with("--reference=") || arg == "--reference" { + reference = true; + } else if arg == "--help" { + // we stop processing once we see --help, + // as it doesn't matter if we've seen reference or not + help = true; + break; } } - let matches = opts.parse(args); - let recursive = matches.opt_present("recursive"); + if help || !reference { + // add both positional arguments + app = app.arg( + Arg::with_name(options::ARG_GROUP) + .value_name(options::ARG_GROUP) + .required(true) + .takes_value(true) + .multiple(false), + ) + } + app = app.arg( + Arg::with_name(options::ARG_FILES) + .value_name(options::ARG_FILES) + .multiple(true) + .takes_value(true) + .required(true) + .min_values(1), + ); + + let matches = app.get_matches_from(args); + + /* Get the list of files */ + let files: Vec = matches + .values_of(options::ARG_FILES) + .map(|v| v.map(ToString::to_string).collect()) + .unwrap_or_default(); + + let preserve_root = matches.is_present(options::preserve_root::PRESERVE); + + let mut derefer = if matches.is_present(options::dereference::DEREFERENCE) { + 1 + } else if matches.is_present(options::dereference::NO_DEREFERENCE) { + 0 + } else { + -1 + }; + + let mut bit_flag = if matches.is_present(options::traverse::TRAVERSE) { + FTS_COMFOLLOW | FTS_PHYSICAL + } else if matches.is_present(options::traverse::EVERY) { + FTS_LOGICAL + } else { + FTS_PHYSICAL + }; + + let recursive = matches.is_present(options::RECURSIVE); if recursive { if bit_flag == FTS_PHYSICAL { if derefer == 1 { @@ -106,27 +149,20 @@ pub fn uumain(args: impl uucore::Args) -> i32 { bit_flag = FTS_PHYSICAL; } - let verbosity = if matches.opt_present("changes") { + let verbosity = if matches.is_present(options::verbosity::CHANGES) { Verbosity::Changes - } else if matches.opt_present("silent") || matches.opt_present("quiet") { + } else if matches.is_present(options::verbosity::SILENT) + || matches.is_present(options::verbosity::QUIET) + { Verbosity::Silent - } else if matches.opt_present("verbose") { + } else if matches.is_present(options::verbosity::VERBOSE) { Verbosity::Verbose } else { Verbosity::Normal }; - if matches.free.is_empty() { - show_usage_error!("missing operand"); - return 1; - } else if matches.free.len() < 2 && !matches.opt_present("reference") { - show_usage_error!("missing operand after ‘{}’", matches.free[0]); - return 1; - } - - let dest_gid: gid_t; - let mut files; - if let Some(file) = matches.opt_str("reference") { + let dest_gid: u32; + if let Some(file) = matches.value_of(options::REFERENCE) { match fs::metadata(&file) { Ok(meta) => { dest_gid = meta.gid(); @@ -136,19 +172,17 @@ pub fn uumain(args: impl uucore::Args) -> i32 { return 1; } } - files = matches.free; } else { - match entries::grp2gid(&matches.free[0]) { + let group = matches.value_of(options::ARG_GROUP).unwrap_or_default(); + match entries::grp2gid(group) { Ok(g) => { dest_gid = g; } _ => { - show_error!("invalid group: {}", matches.free[0].as_str()); + show_error!("invalid group: {}", group); return 1; } } - files = matches.free; - files.remove(0); } let executor = Chgrper { @@ -163,6 +197,86 @@ pub fn uumain(args: impl uucore::Args) -> i32 { executor.exec() } +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .arg( + Arg::with_name(options::verbosity::CHANGES) + .short("c") + .long(options::verbosity::CHANGES) + .help("like verbose but report only when a change is made"), + ) + .arg( + Arg::with_name(options::verbosity::SILENT) + .short("f") + .long(options::verbosity::SILENT), + ) + .arg( + Arg::with_name(options::verbosity::QUIET) + .long(options::verbosity::QUIET) + .help("suppress most error messages"), + ) + .arg( + Arg::with_name(options::verbosity::VERBOSE) + .short("v") + .long(options::verbosity::VERBOSE) + .help("output a diagnostic for every file processed"), + ) + .arg( + Arg::with_name(options::dereference::DEREFERENCE) + .long(options::dereference::DEREFERENCE), + ) + .arg( + Arg::with_name(options::dereference::NO_DEREFERENCE) + .short("h") + .long(options::dereference::NO_DEREFERENCE) + .help( + "affect symbolic links instead of any referenced file (useful only on systems that can change the ownership of a symlink)", + ), + ) + .arg( + Arg::with_name(options::preserve_root::PRESERVE) + .long(options::preserve_root::PRESERVE) + .help("fail to operate recursively on '/'"), + ) + .arg( + Arg::with_name(options::preserve_root::NO_PRESERVE) + .long(options::preserve_root::NO_PRESERVE) + .help("do not treat '/' specially (the default)"), + ) + .arg( + Arg::with_name(options::REFERENCE) + .long(options::REFERENCE) + .value_name("RFILE") + .help("use RFILE's group rather than specifying GROUP values") + .takes_value(true) + .multiple(false), + ) + .arg( + Arg::with_name(options::RECURSIVE) + .short("R") + .long(options::RECURSIVE) + .help("operate on files and directories recursively"), + ) + .arg( + Arg::with_name(options::traverse::TRAVERSE) + .short(options::traverse::TRAVERSE) + .help("if a command line argument is a symbolic link to a directory, traverse it"), + ) + .arg( + Arg::with_name(options::traverse::NO_TRAVERSE) + .short(options::traverse::NO_TRAVERSE) + .help("do not traverse any symbolic links (default)") + .overrides_with_all(&[options::traverse::TRAVERSE, options::traverse::EVERY]), + ) + .arg( + Arg::with_name(options::traverse::EVERY) + .short(options::traverse::EVERY) + .help("traverse every symbolic link to a directory encountered"), + ) +} + struct Chgrper { dest_gid: gid_t, bit_flag: u8, diff --git a/src/uu/chmod/Cargo.toml b/src/uu/chmod/Cargo.toml index ac7030b62..c523829f3 100644 --- a/src/uu/chmod/Cargo.toml +++ b/src/uu/chmod/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/chmod.rs" [dependencies] -clap = "2.33.3" +clap = { version = "2.33", features = ["wrap_help"] } libc = "0.2.42" uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["fs", "mode"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/chmod/src/chmod.rs b/src/uu/chmod/src/chmod.rs index 9cdabc7d6..d89827c97 100644 --- a/src/uu/chmod/src/chmod.rs +++ b/src/uu/chmod/src/chmod.rs @@ -61,11 +61,64 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); let after_help = get_long_usage(); - let matches = App::new(executable!()) - .version(crate_version!()) - .about(ABOUT) + let matches = uu_app() .usage(&usage[..]) .after_help(&after_help[..]) + .get_matches_from(args); + + let changes = matches.is_present(options::CHANGES); + let quiet = matches.is_present(options::QUIET); + let verbose = matches.is_present(options::VERBOSE); + let preserve_root = matches.is_present(options::PRESERVE_ROOT); + let recursive = matches.is_present(options::RECURSIVE); + let fmode = matches + .value_of(options::REFERENCE) + .and_then(|fref| match fs::metadata(fref) { + Ok(meta) => Some(meta.mode()), + Err(err) => crash!(1, "cannot stat attributes of '{}': {}", fref, err), + }); + let modes = matches.value_of(options::MODE).unwrap(); // should always be Some because required + let cmode = if mode_had_minus_prefix { + // clap parsing is finished, now put prefix back + format!("-{}", modes) + } else { + modes.to_string() + }; + let mut files: Vec = matches + .values_of(options::FILE) + .map(|v| v.map(ToString::to_string).collect()) + .unwrap_or_default(); + let cmode = if fmode.is_some() { + // "--reference" and MODE are mutually exclusive + // if "--reference" was used MODE needs to be interpreted as another FILE + // it wasn't possible to implement this behavior directly with clap + files.push(cmode); + None + } else { + Some(cmode) + }; + + let chmoder = Chmoder { + changes, + quiet, + verbose, + preserve_root, + recursive, + fmode, + cmode, + }; + match chmoder.chmod(files) { + Ok(()) => {} + Err(e) => return e, + } + + 0 +} + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about(ABOUT) .arg( Arg::with_name(options::CHANGES) .long(options::CHANGES) @@ -120,54 +173,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .required_unless(options::MODE) .multiple(true), ) - .get_matches_from(args); - - let changes = matches.is_present(options::CHANGES); - let quiet = matches.is_present(options::QUIET); - let verbose = matches.is_present(options::VERBOSE); - let preserve_root = matches.is_present(options::PRESERVE_ROOT); - let recursive = matches.is_present(options::RECURSIVE); - let fmode = - matches - .value_of(options::REFERENCE) - .and_then(|ref fref| match fs::metadata(fref) { - Ok(meta) => Some(meta.mode()), - Err(err) => crash!(1, "cannot stat attributes of '{}': {}", fref, err), - }); - let modes = matches.value_of(options::MODE).unwrap(); // should always be Some because required - let mut cmode = if mode_had_minus_prefix { - // clap parsing is finished, now put prefix back - Some(format!("-{}", modes)) - } else { - Some(modes.to_string()) - }; - let mut files: Vec = matches - .values_of(options::FILE) - .map(|v| v.map(ToString::to_string).collect()) - .unwrap_or_default(); - if fmode.is_some() { - // "--reference" and MODE are mutually exclusive - // if "--reference" was used MODE needs to be interpreted as another FILE - // it wasn't possible to implement this behavior directly with clap - files.push(cmode.unwrap()); - cmode = None; - } - - let chmoder = Chmoder { - changes, - quiet, - verbose, - preserve_root, - recursive, - fmode, - cmode, - }; - match chmoder.chmod(files) { - Ok(()) => {} - Err(e) => return e, - } - - 0 } // Iterate 'args' and delete the first occurrence @@ -230,11 +235,11 @@ impl Chmoder { return Err(1); } if !self.recursive { - r = self.chmod_file(&file).and(r); + r = self.chmod_file(file).and(r); } else { for entry in WalkDir::new(&filename).into_iter().filter_map(|e| e.ok()) { let file = entry.path(); - r = self.chmod_file(&file).and(r); + r = self.chmod_file(file).and(r); } } } diff --git a/src/uu/chown/Cargo.toml b/src/uu/chown/Cargo.toml index 74533af04..f19ed39a8 100644 --- a/src/uu/chown/Cargo.toml +++ b/src/uu/chown/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/chown.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } glob = "0.3.0" uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["entries", "fs", "perms"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/chown/src/chown.rs b/src/uu/chown/src/chown.rs index 2bb5133fe..e1d3ff22b 100644 --- a/src/uu/chown/src/chown.rs +++ b/src/uu/chown/src/chown.rs @@ -73,10 +73,116 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); - let matches = App::new(executable!()) + let matches = uu_app().usage(&usage[..]).get_matches_from(args); + + /* First arg is the owner/group */ + let owner = matches.value_of(ARG_OWNER).unwrap(); + + /* Then the list of files */ + let files: Vec = matches + .values_of(ARG_FILES) + .map(|v| v.map(ToString::to_string).collect()) + .unwrap_or_default(); + + let preserve_root = matches.is_present(options::preserve_root::PRESERVE); + + let mut derefer = if matches.is_present(options::dereference::NO_DEREFERENCE) { + 1 + } else { + 0 + }; + + let mut bit_flag = if matches.is_present(options::traverse::TRAVERSE) { + FTS_COMFOLLOW | FTS_PHYSICAL + } else if matches.is_present(options::traverse::EVERY) { + FTS_LOGICAL + } else { + FTS_PHYSICAL + }; + + let recursive = matches.is_present(options::RECURSIVE); + if recursive { + if bit_flag == FTS_PHYSICAL { + if derefer == 1 { + show_error!("-R --dereference requires -H or -L"); + return 1; + } + derefer = 0; + } + } else { + bit_flag = FTS_PHYSICAL; + } + + let verbosity = if matches.is_present(options::verbosity::CHANGES) { + Verbosity::Changes + } else if matches.is_present(options::verbosity::SILENT) + || matches.is_present(options::verbosity::QUIET) + { + Verbosity::Silent + } else if matches.is_present(options::verbosity::VERBOSE) { + Verbosity::Verbose + } else { + Verbosity::Normal + }; + + let filter = if let Some(spec) = matches.value_of(options::FROM) { + match parse_spec(spec) { + Ok((Some(uid), None)) => IfFrom::User(uid), + Ok((None, Some(gid))) => IfFrom::Group(gid), + Ok((Some(uid), Some(gid))) => IfFrom::UserGroup(uid, gid), + Ok((None, None)) => IfFrom::All, + Err(e) => { + show_error!("{}", e); + return 1; + } + } + } else { + IfFrom::All + }; + + let dest_uid: Option; + let dest_gid: Option; + if let Some(file) = matches.value_of(options::REFERENCE) { + match fs::metadata(&file) { + Ok(meta) => { + dest_gid = Some(meta.gid()); + dest_uid = Some(meta.uid()); + } + Err(e) => { + show_error!("failed to get attributes of '{}': {}", file, e); + return 1; + } + } + } else { + match parse_spec(owner) { + Ok((u, g)) => { + dest_uid = u; + dest_gid = g; + } + Err(e) => { + show_error!("{}", e); + return 1; + } + } + } + let executor = Chowner { + bit_flag, + dest_uid, + dest_gid, + verbosity, + recursive, + dereference: derefer != 0, + filter, + preserve_root, + files, + }; + executor.exec() +} + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) .version(crate_version!()) .about(ABOUT) - .usage(&usage[..]) .arg( Arg::with_name(options::verbosity::CHANGES) .short("c") @@ -167,110 +273,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .required(true) .min_values(1), ) - .get_matches_from(args); - - /* First arg is the owner/group */ - let owner = matches.value_of(ARG_OWNER).unwrap(); - - /* Then the list of files */ - let files: Vec = matches - .values_of(ARG_FILES) - .map(|v| v.map(ToString::to_string).collect()) - .unwrap_or_default(); - - let preserve_root = matches.is_present(options::preserve_root::PRESERVE); - - let mut derefer = if matches.is_present(options::dereference::NO_DEREFERENCE) { - 1 - } else { - 0 - }; - - let mut bit_flag = if matches.is_present(options::traverse::TRAVERSE) { - FTS_COMFOLLOW | FTS_PHYSICAL - } else if matches.is_present(options::traverse::EVERY) { - FTS_LOGICAL - } else { - FTS_PHYSICAL - }; - - let recursive = matches.is_present(options::RECURSIVE); - if recursive { - if bit_flag == FTS_PHYSICAL { - if derefer == 1 { - show_error!("-R --dereference requires -H or -L"); - return 1; - } - derefer = 0; - } - } else { - bit_flag = FTS_PHYSICAL; - } - - let verbosity = if matches.is_present(options::verbosity::CHANGES) { - Verbosity::Changes - } else if matches.is_present(options::verbosity::SILENT) - || matches.is_present(options::verbosity::QUIET) - { - Verbosity::Silent - } else if matches.is_present(options::verbosity::VERBOSE) { - Verbosity::Verbose - } else { - Verbosity::Normal - }; - - let filter = if let Some(spec) = matches.value_of(options::FROM) { - match parse_spec(&spec) { - Ok((Some(uid), None)) => IfFrom::User(uid), - Ok((None, Some(gid))) => IfFrom::Group(gid), - Ok((Some(uid), Some(gid))) => IfFrom::UserGroup(uid, gid), - Ok((None, None)) => IfFrom::All, - Err(e) => { - show_error!("{}", e); - return 1; - } - } - } else { - IfFrom::All - }; - - let dest_uid: Option; - let dest_gid: Option; - if let Some(file) = matches.value_of(options::REFERENCE) { - match fs::metadata(&file) { - Ok(meta) => { - dest_gid = Some(meta.gid()); - dest_uid = Some(meta.uid()); - } - Err(e) => { - show_error!("failed to get attributes of '{}': {}", file, e); - return 1; - } - } - } else { - match parse_spec(&owner) { - Ok((u, g)) => { - dest_uid = u; - dest_gid = g; - } - Err(e) => { - show_error!("{}", e); - return 1; - } - } - } - let executor = Chowner { - bit_flag, - dest_uid, - dest_gid, - verbosity, - recursive, - dereference: derefer != 0, - filter, - preserve_root, - files, - }; - executor.exec() } fn parse_spec(spec: &str) -> Result<(Option, Option), String> { @@ -278,37 +280,25 @@ fn parse_spec(spec: &str) -> Result<(Option, Option), String> { let usr_only = args.len() == 1 && !args[0].is_empty(); let grp_only = args.len() == 2 && args[0].is_empty(); let usr_grp = args.len() == 2 && !args[0].is_empty() && !args[1].is_empty(); - - if usr_only { - Ok(( - Some(match Passwd::locate(args[0]) { - Ok(v) => v.uid(), - _ => return Err(format!("invalid user: ‘{}’", spec)), - }), - None, - )) - } else if grp_only { - Ok(( - None, - Some(match Group::locate(args[1]) { - Ok(v) => v.gid(), - _ => return Err(format!("invalid group: ‘{}’", spec)), - }), - )) - } else if usr_grp { - Ok(( - Some(match Passwd::locate(args[0]) { - Ok(v) => v.uid(), - _ => return Err(format!("invalid user: ‘{}’", spec)), - }), - Some(match Group::locate(args[1]) { - Ok(v) => v.gid(), - _ => return Err(format!("invalid group: ‘{}’", spec)), - }), - )) + let uid = if usr_only || usr_grp { + Some( + Passwd::locate(args[0]) + .map_err(|_| format!("invalid user: '{}'", spec))? + .uid(), + ) } else { - Ok((None, None)) - } + None + }; + let gid = if grp_only || usr_grp { + Some( + Group::locate(args[1]) + .map_err(|_| format!("invalid group: '{}'", spec))? + .gid(), + ) + } else { + None + }; + Ok((uid, gid)) } enum IfFrom { @@ -497,3 +487,17 @@ impl Chowner { } } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_parse_spec() { + assert_eq!(parse_spec(":"), Ok((None, None))); + assert!(parse_spec("::") + .err() + .unwrap() + .starts_with("invalid group: ")); + } +} diff --git a/src/uu/chroot/src/chroot.rs b/src/uu/chroot/src/chroot.rs index a05bd4494..2c0f8522c 100644 --- a/src/uu/chroot/src/chroot.rs +++ b/src/uu/chroot/src/chroot.rs @@ -28,6 +28,7 @@ mod options { pub const GROUP: &str = "group"; pub const GROUPS: &str = "groups"; pub const USERSPEC: &str = "userspec"; + pub const COMMAND: &str = "command"; } pub fn uumain(args: impl uucore::Args) -> i32 { @@ -35,11 +36,72 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .collect_str(InvalidEncodingHandling::ConvertLossy) .accept_any(); - let matches = App::new(executable!()) + let matches = uu_app().get_matches_from(args); + + let default_shell: &'static str = "/bin/sh"; + let default_option: &'static str = "-i"; + let user_shell = std::env::var("SHELL"); + + let newroot: &Path = match matches.value_of(options::NEWROOT) { + Some(v) => Path::new(v), + None => crash!( + 1, + "Missing operand: NEWROOT\nTry '{} --help' for more information.", + NAME + ), + }; + + if !newroot.is_dir() { + crash!( + 1, + "cannot change root directory to `{}`: no such directory", + newroot.display() + ); + } + + let commands = match matches.values_of(options::COMMAND) { + Some(v) => v.collect(), + None => vec![], + }; + + // TODO: refactor the args and command matching + // See: https://github.com/uutils/coreutils/pull/2365#discussion_r647849967 + let command: Vec<&str> = match commands.len() { + 1 => { + let shell: &str = match user_shell { + Err(_) => default_shell, + Ok(ref s) => s.as_ref(), + }; + vec![shell, default_option] + } + _ => commands, + }; + + set_context(newroot, &matches); + + let pstatus = Command::new(command[0]) + .args(&command[1..]) + .status() + .unwrap_or_else(|e| crash!(1, "Cannot exec: {}", e)); + + if pstatus.success() { + 0 + } else { + pstatus.code().unwrap_or(-1) + } +} + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) .version(crate_version!()) .about(ABOUT) .usage(SYNTAX) - .arg(Arg::with_name(options::NEWROOT).hidden(true).required(true)) + .arg( + Arg::with_name(options::NEWROOT) + .hidden(true) + .required(true) + .index(1), + ) .arg( Arg::with_name(options::USER) .short("u") @@ -71,59 +133,12 @@ pub fn uumain(args: impl uucore::Args) -> i32 { ) .value_name("USER:GROUP"), ) - .get_matches_from(args); - - let default_shell: &'static str = "/bin/sh"; - let default_option: &'static str = "-i"; - let user_shell = std::env::var("SHELL"); - - let newroot: &Path = match matches.value_of(options::NEWROOT) { - Some(v) => Path::new(v), - None => crash!( - 1, - "Missing operand: NEWROOT\nTry '{} --help' for more information.", - NAME - ), - }; - - if !newroot.is_dir() { - crash!( - 1, - "cannot change root directory to `{}`: no such directory", - newroot.display() - ); - } - - let command: Vec<&str> = match matches.args.len() { - 1 => { - let shell: &str = match user_shell { - Err(_) => default_shell, - Ok(ref s) => s.as_ref(), - }; - vec![shell, default_option] - } - _ => { - let mut vector: Vec<&str> = Vec::new(); - for (&k, v) in matches.args.iter() { - vector.push(k); - vector.push(&v.vals[0].to_str().unwrap()); - } - vector - } - }; - - set_context(&newroot, &matches); - - let pstatus = Command::new(command[0]) - .args(&command[1..]) - .status() - .unwrap_or_else(|e| crash!(1, "Cannot exec: {}", e)); - - if pstatus.success() { - 0 - } else { - pstatus.code().unwrap_or(-1) - } + .arg( + Arg::with_name(options::COMMAND) + .hidden(true) + .multiple(true) + .index(2), + ) } fn set_context(root: &Path, options: &clap::ArgMatches) { @@ -132,7 +147,7 @@ fn set_context(root: &Path, options: &clap::ArgMatches) { let group_str = options.value_of(options::GROUP).unwrap_or_default(); let groups_str = options.value_of(options::GROUPS).unwrap_or_default(); let userspec = match userspec_str { - Some(ref u) => { + Some(u) => { let s: Vec<&str> = u.split(':').collect(); if s.len() != 2 || s.iter().any(|&spec| spec.is_empty()) { crash!(1, "invalid userspec: `{}`", u) diff --git a/src/uu/cksum/Cargo.toml b/src/uu/cksum/Cargo.toml index 0332efbf8..792c6c0c7 100644 --- a/src/uu/cksum/Cargo.toml +++ b/src/uu/cksum/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/cksum.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } libc = "0.2.42" uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index 49c0536f5..e88cc78b3 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -160,18 +160,14 @@ fn cksum(fname: &str) -> io::Result<(u32, usize)> { let mut bytes = init_byte_array(); loop { - match rd.read(&mut bytes) { - Ok(num_bytes) => { - if num_bytes == 0 { - return Ok((crc_final(crc, size), size)); - } - for &b in bytes[..num_bytes].iter() { - crc = crc_update(crc, b); - } - size += num_bytes; - } - Err(err) => return Err(err), + let num_bytes = rd.read(&mut bytes)?; + if num_bytes == 0 { + return Ok((crc_final(crc, size), size)); } + for &b in bytes[..num_bytes].iter() { + crc = crc_update(crc, b); + } + size += num_bytes; } } @@ -184,13 +180,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .collect_str(InvalidEncodingHandling::Ignore) .accept_any(); - let matches = App::new(executable!()) - .name(NAME) - .version(crate_version!()) - .about(SUMMARY) - .usage(SYNTAX) - .arg(Arg::with_name(options::FILE).hidden(true).multiple(true)) - .get_matches_from(args); + let matches = uu_app().get_matches_from(args); let files: Vec = match matches.values_of(options::FILE) { Some(v) => v.clone().map(|v| v.to_owned()).collect(), @@ -221,3 +211,12 @@ pub fn uumain(args: impl uucore::Args) -> i32 { exit_code } + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .name(NAME) + .version(crate_version!()) + .about(SUMMARY) + .usage(SYNTAX) + .arg(Arg::with_name(options::FILE).hidden(true).multiple(true)) +} diff --git a/src/uu/comm/Cargo.toml b/src/uu/comm/Cargo.toml index f02217790..b1f8948e7 100644 --- a/src/uu/comm/Cargo.toml +++ b/src/uu/comm/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/comm.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } libc = "0.2.42" uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/comm/src/comm.rs b/src/uu/comm/src/comm.rs index f7190fb73..aa10432a2 100644 --- a/src/uu/comm/src/comm.rs +++ b/src/uu/comm/src/comm.rs @@ -50,9 +50,8 @@ fn mkdelim(col: usize, opts: &ArgMatches) -> String { } fn ensure_nl(line: &mut String) { - match line.chars().last() { - Some('\n') => (), - _ => line.push('\n'), + if !line.ends_with('\n') { + line.push('\n'); } } @@ -138,10 +137,20 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .collect_str(InvalidEncodingHandling::ConvertLossy) .accept_any(); - let matches = App::new(executable!()) + let matches = uu_app().usage(&usage[..]).get_matches_from(args); + + let mut f1 = open_file(matches.value_of(options::FILE_1).unwrap()).unwrap(); + let mut f2 = open_file(matches.value_of(options::FILE_2).unwrap()).unwrap(); + + comm(&mut f1, &mut f2, &matches); + + 0 +} + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) .version(crate_version!()) .about(ABOUT) - .usage(&usage[..]) .after_help(LONG_HELP) .arg( Arg::with_name(options::COLUMN_1) @@ -168,12 +177,4 @@ pub fn uumain(args: impl uucore::Args) -> i32 { ) .arg(Arg::with_name(options::FILE_1).required(true)) .arg(Arg::with_name(options::FILE_2).required(true)) - .get_matches_from(args); - - let mut f1 = open_file(matches.value_of(options::FILE_1).unwrap()).unwrap(); - let mut f2 = open_file(matches.value_of(options::FILE_2).unwrap()).unwrap(); - - comm(&mut f1, &mut f2, &matches); - - 0 } diff --git a/src/uu/cp/Cargo.toml b/src/uu/cp/Cargo.toml index 9d582adae..76990863d 100644 --- a/src/uu/cp/Cargo.toml +++ b/src/uu/cp/Cargo.toml @@ -19,7 +19,7 @@ edition = "2018" path = "src/cp.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } filetime = "0.2" libc = "0.2.85" quick-error = "1.2.3" diff --git a/src/uu/cp/README.md b/src/uu/cp/README.md index 91753eb66..13399c3b1 100644 --- a/src/uu/cp/README.md +++ b/src/uu/cp/README.md @@ -7,37 +7,37 @@ ### To Do -- [ ] archive -- [ ] attributes-only -- [ ] copy-contents -- [ ] no-dereference-preserve-linkgs -- [ ] dereference -- [ ] no-dereference -- [ ] preserve-default-attributes -- [ ] preserve -- [ ] no-preserve -- [ ] parents -- [ ] reflink -- [ ] sparse -- [ ] strip-trailing-slashes -- [ ] update -- [ ] one-file-system -- [ ] context - [ ] cli-symbolic-links +- [ ] context +- [ ] copy-contents +- [ ] sparse ### Completed +- [x] archive +- [x] attributes-only - [x] backup +- [x] dereference - [x] force (Not implemented on Windows) - [x] interactive - [x] link - [x] no-clobber +- [x] no-dereference +- [x] no-dereference-preserve-links +- [x] no-preserve - [x] no-target-directory +- [x] one-file-system +- [x] parents - [x] paths +- [x] preserve +- [x] preserve-default-attributes - [x] recursive +- [x] reflink - [x] remove-destination (On Windows, current only works for writeable files) +- [x] strip-trailing-slashes - [x] suffix - [x] symbolic-link - [x] target-directory +- [x] update - [x] verbose - [x] version diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 6a114cf44..4deaefa98 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -48,7 +48,6 @@ use std::path::{Path, PathBuf, StripPrefixError}; use std::str::FromStr; use std::string::ToString; use uucore::backup_control::{self, BackupMode}; -use uucore::fs::resolve_relative_path; use uucore::fs::{canonicalize, CanonicalizeMode}; use walkdir::WalkDir; @@ -198,7 +197,6 @@ pub struct Options { copy_contents: bool, copy_mode: CopyMode, dereference: bool, - no_dereference: bool, no_target_dir: bool, one_file_system: bool, overwrite: OverwriteMode, @@ -228,39 +226,41 @@ fn get_usage() -> String { } // Argument constants -static OPT_ARCHIVE: &str = "archive"; -static OPT_ATTRIBUTES_ONLY: &str = "attributes-only"; -static OPT_BACKUP: &str = "backup"; -static OPT_BACKUP_NO_ARG: &str = "b"; -static OPT_CLI_SYMBOLIC_LINKS: &str = "cli-symbolic-links"; -static OPT_CONTEXT: &str = "context"; -static OPT_COPY_CONTENTS: &str = "copy-contents"; -static OPT_DEREFERENCE: &str = "dereference"; -static OPT_FORCE: &str = "force"; -static OPT_INTERACTIVE: &str = "interactive"; -static OPT_LINK: &str = "link"; -static OPT_NO_CLOBBER: &str = "no-clobber"; -static OPT_NO_DEREFERENCE: &str = "no-dereference"; -static OPT_NO_DEREFERENCE_PRESERVE_LINKS: &str = "no-dereference-preserve-linkgs"; -static OPT_NO_PRESERVE: &str = "no-preserve"; -static OPT_NO_TARGET_DIRECTORY: &str = "no-target-directory"; -static OPT_ONE_FILE_SYSTEM: &str = "one-file-system"; -static OPT_PARENT: &str = "parent"; -static OPT_PARENTS: &str = "parents"; -static OPT_PATHS: &str = "paths"; -static OPT_PRESERVE: &str = "preserve"; -static OPT_PRESERVE_DEFAULT_ATTRIBUTES: &str = "preserve-default-attributes"; -static OPT_RECURSIVE: &str = "recursive"; -static OPT_RECURSIVE_ALIAS: &str = "recursive_alias"; -static OPT_REFLINK: &str = "reflink"; -static OPT_REMOVE_DESTINATION: &str = "remove-destination"; -static OPT_SPARSE: &str = "sparse"; -static OPT_STRIP_TRAILING_SLASHES: &str = "strip-trailing-slashes"; -static OPT_SUFFIX: &str = "suffix"; -static OPT_SYMBOLIC_LINK: &str = "symbolic-link"; -static OPT_TARGET_DIRECTORY: &str = "target-directory"; -static OPT_UPDATE: &str = "update"; -static OPT_VERBOSE: &str = "verbose"; +mod options { + pub const ARCHIVE: &str = "archive"; + pub const ATTRIBUTES_ONLY: &str = "attributes-only"; + pub const BACKUP: &str = "backup"; + pub const BACKUP_NO_ARG: &str = "b"; + pub const CLI_SYMBOLIC_LINKS: &str = "cli-symbolic-links"; + pub const CONTEXT: &str = "context"; + pub const COPY_CONTENTS: &str = "copy-contents"; + pub const DEREFERENCE: &str = "dereference"; + pub const FORCE: &str = "force"; + pub const INTERACTIVE: &str = "interactive"; + pub const LINK: &str = "link"; + pub const NO_CLOBBER: &str = "no-clobber"; + pub const NO_DEREFERENCE: &str = "no-dereference"; + pub const NO_DEREFERENCE_PRESERVE_LINKS: &str = "no-dereference-preserve-linkgs"; + pub const NO_PRESERVE: &str = "no-preserve"; + pub const NO_TARGET_DIRECTORY: &str = "no-target-directory"; + pub const ONE_FILE_SYSTEM: &str = "one-file-system"; + pub const PARENT: &str = "parent"; + pub const PARENTS: &str = "parents"; + pub const PATHS: &str = "paths"; + pub const PRESERVE: &str = "preserve"; + pub const PRESERVE_DEFAULT_ATTRIBUTES: &str = "preserve-default-attributes"; + pub const RECURSIVE: &str = "recursive"; + pub const RECURSIVE_ALIAS: &str = "recursive_alias"; + pub const REFLINK: &str = "reflink"; + pub const REMOVE_DESTINATION: &str = "remove-destination"; + pub const SPARSE: &str = "sparse"; + pub const STRIP_TRAILING_SLASHES: &str = "strip-trailing-slashes"; + pub const SUFFIX: &str = "suffix"; + pub const SYMBOLIC_LINK: &str = "symbolic-link"; + pub const TARGET_DIRECTORY: &str = "target-directory"; + pub const UPDATE: &str = "update"; + pub const VERBOSE: &str = "verbose"; +} #[cfg(unix)] static PRESERVABLE_ATTRIBUTES: &[&str] = &[ @@ -290,74 +290,71 @@ static DEFAULT_ATTRIBUTES: &[Attribute] = &[ Attribute::Timestamps, ]; -pub fn uumain(args: impl uucore::Args) -> i32 { - let usage = get_usage(); - let matches = App::new(executable!()) +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) .version(crate_version!()) .about(ABOUT) - .after_help(&*format!("{}\n{}", LONG_HELP, backup_control::BACKUP_CONTROL_LONG_HELP)) - .usage(&usage[..]) - .arg(Arg::with_name(OPT_TARGET_DIRECTORY) + .arg(Arg::with_name(options::TARGET_DIRECTORY) .short("t") - .conflicts_with(OPT_NO_TARGET_DIRECTORY) - .long(OPT_TARGET_DIRECTORY) - .value_name(OPT_TARGET_DIRECTORY) + .conflicts_with(options::NO_TARGET_DIRECTORY) + .long(options::TARGET_DIRECTORY) + .value_name(options::TARGET_DIRECTORY) .takes_value(true) .help("copy all SOURCE arguments into target-directory")) - .arg(Arg::with_name(OPT_NO_TARGET_DIRECTORY) + .arg(Arg::with_name(options::NO_TARGET_DIRECTORY) .short("T") - .long(OPT_NO_TARGET_DIRECTORY) - .conflicts_with(OPT_TARGET_DIRECTORY) + .long(options::NO_TARGET_DIRECTORY) + .conflicts_with(options::TARGET_DIRECTORY) .help("Treat DEST as a regular file and not a directory")) - .arg(Arg::with_name(OPT_INTERACTIVE) + .arg(Arg::with_name(options::INTERACTIVE) .short("i") - .long(OPT_INTERACTIVE) - .conflicts_with(OPT_NO_CLOBBER) + .long(options::INTERACTIVE) + .conflicts_with(options::NO_CLOBBER) .help("ask before overwriting files")) - .arg(Arg::with_name(OPT_LINK) + .arg(Arg::with_name(options::LINK) .short("l") - .long(OPT_LINK) - .overrides_with(OPT_REFLINK) + .long(options::LINK) + .overrides_with(options::REFLINK) .help("hard-link files instead of copying")) - .arg(Arg::with_name(OPT_NO_CLOBBER) + .arg(Arg::with_name(options::NO_CLOBBER) .short("n") - .long(OPT_NO_CLOBBER) - .conflicts_with(OPT_INTERACTIVE) + .long(options::NO_CLOBBER) + .conflicts_with(options::INTERACTIVE) .help("don't overwrite a file that already exists")) - .arg(Arg::with_name(OPT_RECURSIVE) + .arg(Arg::with_name(options::RECURSIVE) .short("r") - .long(OPT_RECURSIVE) + .long(options::RECURSIVE) // --archive sets this option .help("copy directories recursively")) - .arg(Arg::with_name(OPT_RECURSIVE_ALIAS) + .arg(Arg::with_name(options::RECURSIVE_ALIAS) .short("R") .help("same as -r")) - .arg(Arg::with_name(OPT_STRIP_TRAILING_SLASHES) - .long(OPT_STRIP_TRAILING_SLASHES) + .arg(Arg::with_name(options::STRIP_TRAILING_SLASHES) + .long(options::STRIP_TRAILING_SLASHES) .help("remove any trailing slashes from each SOURCE argument")) - .arg(Arg::with_name(OPT_VERBOSE) + .arg(Arg::with_name(options::VERBOSE) .short("v") - .long(OPT_VERBOSE) + .long(options::VERBOSE) .help("explicitly state what is being done")) - .arg(Arg::with_name(OPT_SYMBOLIC_LINK) + .arg(Arg::with_name(options::SYMBOLIC_LINK) .short("s") - .long(OPT_SYMBOLIC_LINK) - .conflicts_with(OPT_LINK) - .overrides_with(OPT_REFLINK) + .long(options::SYMBOLIC_LINK) + .conflicts_with(options::LINK) + .overrides_with(options::REFLINK) .help("make symbolic links instead of copying")) - .arg(Arg::with_name(OPT_FORCE) + .arg(Arg::with_name(options::FORCE) .short("f") - .long(OPT_FORCE) + .long(options::FORCE) .help("if an existing destination file cannot be opened, remove it and \ try again (this option is ignored when the -n option is also used). \ Currently not implemented for Windows.")) - .arg(Arg::with_name(OPT_REMOVE_DESTINATION) - .long(OPT_REMOVE_DESTINATION) - .conflicts_with(OPT_FORCE) + .arg(Arg::with_name(options::REMOVE_DESTINATION) + .long(options::REMOVE_DESTINATION) + .conflicts_with(options::FORCE) .help("remove each existing destination file before attempting to open it \ (contrast with --force). On Windows, current only works for writeable files.")) - .arg(Arg::with_name(OPT_BACKUP) - .long(OPT_BACKUP) + .arg(Arg::with_name(options::BACKUP) + .long(options::BACKUP) .help("make a backup of each existing destination file") .takes_value(true) .require_equals(true) @@ -365,104 +362,116 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .possible_values(backup_control::BACKUP_CONTROL_VALUES) .value_name("CONTROL") ) - .arg(Arg::with_name(OPT_BACKUP_NO_ARG) - .short(OPT_BACKUP_NO_ARG) + .arg(Arg::with_name(options::BACKUP_NO_ARG) + .short(options::BACKUP_NO_ARG) .help("like --backup but does not accept an argument") ) - .arg(Arg::with_name(OPT_SUFFIX) + .arg(Arg::with_name(options::SUFFIX) .short("S") - .long(OPT_SUFFIX) + .long(options::SUFFIX) .takes_value(true) .value_name("SUFFIX") .help("override the usual backup suffix")) - .arg(Arg::with_name(OPT_UPDATE) + .arg(Arg::with_name(options::UPDATE) .short("u") - .long(OPT_UPDATE) - .help("copy only when the SOURCE file is newer than the destination file\ + .long(options::UPDATE) + .help("copy only when the SOURCE file is newer than the destination file \ or when the destination file is missing")) - .arg(Arg::with_name(OPT_REFLINK) - .long(OPT_REFLINK) + .arg(Arg::with_name(options::REFLINK) + .long(options::REFLINK) .takes_value(true) .value_name("WHEN") .help("control clone/CoW copies. See below")) - .arg(Arg::with_name(OPT_ATTRIBUTES_ONLY) - .long(OPT_ATTRIBUTES_ONLY) - .conflicts_with(OPT_COPY_CONTENTS) - .overrides_with(OPT_REFLINK) + .arg(Arg::with_name(options::ATTRIBUTES_ONLY) + .long(options::ATTRIBUTES_ONLY) + .conflicts_with(options::COPY_CONTENTS) + .overrides_with(options::REFLINK) .help("Don't copy the file data, just the attributes")) - .arg(Arg::with_name(OPT_PRESERVE) - .long(OPT_PRESERVE) + .arg(Arg::with_name(options::PRESERVE) + .long(options::PRESERVE) .takes_value(true) .multiple(true) .use_delimiter(true) .possible_values(PRESERVABLE_ATTRIBUTES) + .min_values(0) .value_name("ATTR_LIST") - .conflicts_with_all(&[OPT_PRESERVE_DEFAULT_ATTRIBUTES, OPT_NO_PRESERVE]) + .conflicts_with_all(&[options::PRESERVE_DEFAULT_ATTRIBUTES, options::NO_PRESERVE]) // -d sets this option // --archive sets this option - .help("Preserve the specified attributes (default: mode(unix only),ownership,timestamps),\ + .help("Preserve the specified attributes (default: mode (unix only), ownership, timestamps), \ if possible additional attributes: context, links, xattr, all")) - .arg(Arg::with_name(OPT_PRESERVE_DEFAULT_ATTRIBUTES) + .arg(Arg::with_name(options::PRESERVE_DEFAULT_ATTRIBUTES) .short("-p") - .long(OPT_PRESERVE_DEFAULT_ATTRIBUTES) - .conflicts_with_all(&[OPT_PRESERVE, OPT_NO_PRESERVE, OPT_ARCHIVE]) + .long(options::PRESERVE_DEFAULT_ATTRIBUTES) + .conflicts_with_all(&[options::PRESERVE, options::NO_PRESERVE, options::ARCHIVE]) .help("same as --preserve=mode(unix only),ownership,timestamps")) - .arg(Arg::with_name(OPT_NO_PRESERVE) - .long(OPT_NO_PRESERVE) + .arg(Arg::with_name(options::NO_PRESERVE) + .long(options::NO_PRESERVE) .takes_value(true) .value_name("ATTR_LIST") - .conflicts_with_all(&[OPT_PRESERVE_DEFAULT_ATTRIBUTES, OPT_PRESERVE, OPT_ARCHIVE]) + .conflicts_with_all(&[options::PRESERVE_DEFAULT_ATTRIBUTES, options::PRESERVE, options::ARCHIVE]) .help("don't preserve the specified attributes")) - .arg(Arg::with_name(OPT_PARENTS) - .long(OPT_PARENTS) - .alias(OPT_PARENT) + .arg(Arg::with_name(options::PARENTS) + .long(options::PARENTS) + .alias(options::PARENT) .help("use full source file name under DIRECTORY")) - .arg(Arg::with_name(OPT_NO_DEREFERENCE) + .arg(Arg::with_name(options::NO_DEREFERENCE) .short("-P") - .long(OPT_NO_DEREFERENCE) - .conflicts_with(OPT_DEREFERENCE) + .long(options::NO_DEREFERENCE) + .conflicts_with(options::DEREFERENCE) // -d sets this option .help("never follow symbolic links in SOURCE")) - .arg(Arg::with_name(OPT_DEREFERENCE) + .arg(Arg::with_name(options::DEREFERENCE) .short("L") - .long(OPT_DEREFERENCE) - .conflicts_with(OPT_NO_DEREFERENCE) + .long(options::DEREFERENCE) + .conflicts_with(options::NO_DEREFERENCE) .help("always follow symbolic links in SOURCE")) - .arg(Arg::with_name(OPT_ARCHIVE) + .arg(Arg::with_name(options::ARCHIVE) .short("a") - .long(OPT_ARCHIVE) - .conflicts_with_all(&[OPT_PRESERVE_DEFAULT_ATTRIBUTES, OPT_PRESERVE, OPT_NO_PRESERVE]) + .long(options::ARCHIVE) + .conflicts_with_all(&[options::PRESERVE_DEFAULT_ATTRIBUTES, options::PRESERVE, options::NO_PRESERVE]) .help("Same as -dR --preserve=all")) - .arg(Arg::with_name(OPT_NO_DEREFERENCE_PRESERVE_LINKS) + .arg(Arg::with_name(options::NO_DEREFERENCE_PRESERVE_LINKS) .short("d") .help("same as --no-dereference --preserve=links")) - .arg(Arg::with_name(OPT_ONE_FILE_SYSTEM) + .arg(Arg::with_name(options::ONE_FILE_SYSTEM) .short("x") - .long(OPT_ONE_FILE_SYSTEM) + .long(options::ONE_FILE_SYSTEM) .help("stay on this file system")) // TODO: implement the following args - .arg(Arg::with_name(OPT_COPY_CONTENTS) - .long(OPT_COPY_CONTENTS) - .conflicts_with(OPT_ATTRIBUTES_ONLY) + .arg(Arg::with_name(options::COPY_CONTENTS) + .long(options::COPY_CONTENTS) + .conflicts_with(options::ATTRIBUTES_ONLY) .help("NotImplemented: copy contents of special files when recursive")) - .arg(Arg::with_name(OPT_SPARSE) - .long(OPT_SPARSE) + .arg(Arg::with_name(options::SPARSE) + .long(options::SPARSE) .takes_value(true) .value_name("WHEN") .help("NotImplemented: control creation of sparse files. See below")) - .arg(Arg::with_name(OPT_CONTEXT) - .long(OPT_CONTEXT) + .arg(Arg::with_name(options::CONTEXT) + .long(options::CONTEXT) .takes_value(true) .value_name("CTX") .help("NotImplemented: set SELinux security context of destination file to default type")) - .arg(Arg::with_name(OPT_CLI_SYMBOLIC_LINKS) + .arg(Arg::with_name(options::CLI_SYMBOLIC_LINKS) .short("H") .help("NotImplemented: follow command-line symbolic links in SOURCE")) // END TODO - .arg(Arg::with_name(OPT_PATHS) + .arg(Arg::with_name(options::PATHS) .multiple(true)) +} + +pub fn uumain(args: impl uucore::Args) -> i32 { + let usage = get_usage(); + let matches = uu_app() + .after_help(&*format!( + "{}\n{}", + LONG_HELP, + backup_control::BACKUP_CONTROL_LONG_HELP + )) + .usage(&usage[..]) .get_matches_from(args); let options = crash_if_err!(EXIT_ERR, Options::from_matches(&matches)); @@ -473,7 +482,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } let paths: Vec = matches - .values_of(OPT_PATHS) + .values_of(options::PATHS) .map(|v| v.map(ToString::to_string).collect()) .unwrap_or_default(); @@ -495,9 +504,9 @@ pub fn uumain(args: impl uucore::Args) -> i32 { impl ClobberMode { fn from_matches(matches: &ArgMatches) -> ClobberMode { - if matches.is_present(OPT_FORCE) { + if matches.is_present(options::FORCE) { ClobberMode::Force - } else if matches.is_present(OPT_REMOVE_DESTINATION) { + } else if matches.is_present(options::REMOVE_DESTINATION) { ClobberMode::RemoveDestination } else { ClobberMode::Standard @@ -507,9 +516,9 @@ impl ClobberMode { impl OverwriteMode { fn from_matches(matches: &ArgMatches) -> OverwriteMode { - if matches.is_present(OPT_INTERACTIVE) { + if matches.is_present(options::INTERACTIVE) { OverwriteMode::Interactive(ClobberMode::from_matches(matches)) - } else if matches.is_present(OPT_NO_CLOBBER) { + } else if matches.is_present(options::NO_CLOBBER) { OverwriteMode::NoClobber } else { OverwriteMode::Clobber(ClobberMode::from_matches(matches)) @@ -519,15 +528,15 @@ impl OverwriteMode { impl CopyMode { fn from_matches(matches: &ArgMatches) -> CopyMode { - if matches.is_present(OPT_LINK) { + if matches.is_present(options::LINK) { CopyMode::Link - } else if matches.is_present(OPT_SYMBOLIC_LINK) { + } else if matches.is_present(options::SYMBOLIC_LINK) { CopyMode::SymLink - } else if matches.is_present(OPT_SPARSE) { + } else if matches.is_present(options::SPARSE) { CopyMode::Sparse - } else if matches.is_present(OPT_UPDATE) { + } else if matches.is_present(options::UPDATE) { CopyMode::Update - } else if matches.is_present(OPT_ATTRIBUTES_ONLY) { + } else if matches.is_present(options::ATTRIBUTES_ONLY) { CopyMode::AttrOnly } else { CopyMode::Copy @@ -575,13 +584,13 @@ fn add_all_attributes() -> Vec { impl Options { fn from_matches(matches: &ArgMatches) -> CopyResult { let not_implemented_opts = vec![ - OPT_COPY_CONTENTS, - OPT_SPARSE, + options::COPY_CONTENTS, + options::SPARSE, #[cfg(not(any(windows, unix)))] - OPT_ONE_FILE_SYSTEM, - OPT_CONTEXT, + options::ONE_FILE_SYSTEM, + options::CONTEXT, #[cfg(windows)] - OPT_FORCE, + options::FORCE, ]; for not_implemented_opt in not_implemented_opts { @@ -590,27 +599,28 @@ impl Options { } } - let recursive = matches.is_present(OPT_RECURSIVE) - || matches.is_present(OPT_RECURSIVE_ALIAS) - || matches.is_present(OPT_ARCHIVE); + let recursive = matches.is_present(options::RECURSIVE) + || matches.is_present(options::RECURSIVE_ALIAS) + || matches.is_present(options::ARCHIVE); let backup_mode = backup_control::determine_backup_mode( - matches.is_present(OPT_BACKUP_NO_ARG) || matches.is_present(OPT_BACKUP), - matches.value_of(OPT_BACKUP), + matches.is_present(options::BACKUP_NO_ARG) || matches.is_present(options::BACKUP), + matches.value_of(options::BACKUP), ); - let backup_suffix = backup_control::determine_backup_suffix(matches.value_of(OPT_SUFFIX)); + let backup_suffix = + backup_control::determine_backup_suffix(matches.value_of(options::SUFFIX)); let overwrite = OverwriteMode::from_matches(matches); // Parse target directory options - let no_target_dir = matches.is_present(OPT_NO_TARGET_DIRECTORY); + let no_target_dir = matches.is_present(options::NO_TARGET_DIRECTORY); let target_dir = matches - .value_of(OPT_TARGET_DIRECTORY) + .value_of(options::TARGET_DIRECTORY) .map(ToString::to_string); // Parse attributes to preserve - let preserve_attributes: Vec = if matches.is_present(OPT_PRESERVE) { - match matches.values_of(OPT_PRESERVE) { + let preserve_attributes: Vec = if matches.is_present(options::PRESERVE) { + match matches.values_of(options::PRESERVE) { None => DEFAULT_ATTRIBUTES.to_vec(), Some(attribute_strs) => { let mut attributes = Vec::new(); @@ -625,33 +635,34 @@ impl Options { attributes } } - } else if matches.is_present(OPT_ARCHIVE) { + } else if matches.is_present(options::ARCHIVE) { // --archive is used. Same as --preserve=all add_all_attributes() - } else if matches.is_present(OPT_NO_DEREFERENCE_PRESERVE_LINKS) { + } else if matches.is_present(options::NO_DEREFERENCE_PRESERVE_LINKS) { vec![Attribute::Links] - } else if matches.is_present(OPT_PRESERVE_DEFAULT_ATTRIBUTES) { + } else if matches.is_present(options::PRESERVE_DEFAULT_ATTRIBUTES) { DEFAULT_ATTRIBUTES.to_vec() } else { vec![] }; let options = Options { - attributes_only: matches.is_present(OPT_ATTRIBUTES_ONLY), - copy_contents: matches.is_present(OPT_COPY_CONTENTS), + attributes_only: matches.is_present(options::ATTRIBUTES_ONLY), + copy_contents: matches.is_present(options::COPY_CONTENTS), copy_mode: CopyMode::from_matches(matches), - dereference: matches.is_present(OPT_DEREFERENCE), // No dereference is set with -p, -d and --archive - no_dereference: matches.is_present(OPT_NO_DEREFERENCE) - || matches.is_present(OPT_NO_DEREFERENCE_PRESERVE_LINKS) - || matches.is_present(OPT_ARCHIVE), - one_file_system: matches.is_present(OPT_ONE_FILE_SYSTEM), - parents: matches.is_present(OPT_PARENTS), - update: matches.is_present(OPT_UPDATE), - verbose: matches.is_present(OPT_VERBOSE), - strip_trailing_slashes: matches.is_present(OPT_STRIP_TRAILING_SLASHES), + dereference: !(matches.is_present(options::NO_DEREFERENCE) + || matches.is_present(options::NO_DEREFERENCE_PRESERVE_LINKS) + || matches.is_present(options::ARCHIVE) + || recursive) + || matches.is_present(options::DEREFERENCE), + one_file_system: matches.is_present(options::ONE_FILE_SYSTEM), + parents: matches.is_present(options::PARENTS), + update: matches.is_present(options::UPDATE), + verbose: matches.is_present(options::VERBOSE), + strip_trailing_slashes: matches.is_present(options::STRIP_TRAILING_SLASHES), reflink_mode: { - if let Some(reflink) = matches.value_of(OPT_REFLINK) { + if let Some(reflink) = matches.value_of(options::REFLINK) { match reflink { "always" => ReflinkMode::Always, "auto" => ReflinkMode::Auto, @@ -664,7 +675,14 @@ impl Options { } } } else { - ReflinkMode::Never + #[cfg(any(target_os = "linux", target_os = "macos"))] + { + ReflinkMode::Auto + } + #[cfg(not(any(target_os = "linux", target_os = "macos")))] + { + ReflinkMode::Never + } } }, backup: backup_mode, @@ -709,27 +727,26 @@ fn parse_path_args(path_args: &[String], options: &Options) -> CopyResult<(Vec { // All path args are sources, and the target dir was // specified separately - (paths, PathBuf::from(target)) + PathBuf::from(target) } None => { // If there was no explicit target-dir, then use the last // path_arg - let target = paths.pop().unwrap(); - (paths, target) + paths.pop().unwrap() } }; if options.strip_trailing_slashes { - for source in sources.iter_mut() { + for source in paths.iter_mut() { *source = source.components().as_path().to_owned() } } - Ok((sources, target)) + Ok((paths, target)) } fn preserve_hardlinks( @@ -937,7 +954,15 @@ fn copy_directory(root: &Path, target: &TargetSlice, options: &Options) -> CopyR return Err(format!("omitting directory '{}'", root.display()).into()); } - let root_path = Path::new(&root).canonicalize()?; + // if no-dereference is enabled and this is a symlink, copy it as a file + if !options.dereference && fs::symlink_metadata(root).unwrap().file_type().is_symlink() { + return copy_file(root, target, options); + } + + let current_dir = + env::current_dir().unwrap_or_else(|e| crash!(1, "failed to get current directory {}", e)); + + let root_path = current_dir.join(root); let root_parent = if target.exists() { root_path.parent() @@ -958,18 +983,13 @@ fn copy_directory(root: &Path, target: &TargetSlice, options: &Options) -> CopyR #[cfg(any(windows, target_os = "redox"))] let mut hard_links: Vec<(String, u64)> = vec![]; - for path in WalkDir::new(root).same_file_system(options.one_file_system) { + for path in WalkDir::new(root) + .same_file_system(options.one_file_system) + .follow_links(options.dereference) + { let p = or_continue!(path); let is_symlink = fs::symlink_metadata(p.path())?.file_type().is_symlink(); - let path = if (options.no_dereference || options.dereference) && is_symlink { - // we are dealing with a symlink. Don't follow it - match env::current_dir() { - Ok(cwd) => cwd.join(resolve_relative_path(p.path())), - Err(e) => crash!(1, "failed to get current directory {}", e), - } - } else { - or_continue!(p.path().canonicalize()) - }; + let path = current_dir.join(&p.path()); let local_to_root_parent = match root_parent { Some(parent) => { @@ -992,9 +1012,10 @@ fn copy_directory(root: &Path, target: &TargetSlice, options: &Options) -> CopyR }; let local_to_target = target.join(&local_to_root_parent); - - if path.is_dir() && !local_to_target.exists() { - or_continue!(fs::create_dir_all(local_to_target.clone())); + if is_symlink && !options.dereference { + copy_link(&path, &local_to_target)?; + } else if path.is_dir() && !local_to_target.exists() { + or_continue!(fs::create_dir_all(local_to_target)); } else if !path.is_dir() { if preserve_hard_links { let mut found_hard_link = false; @@ -1088,7 +1109,7 @@ fn copy_attribute(source: &Path, dest: &Path, attribute: &Attribute) -> CopyResu } #[cfg(not(windows))] -#[allow(clippy::unnecessary_unwrap)] // needed for windows version +#[allow(clippy::unnecessary_wraps)] // needed for windows version fn symlink_file(source: &Path, dest: &Path, context: &str) -> CopyResult<()> { match std::os::unix::fs::symlink(source, dest).context(context) { Ok(_) => Ok(()), @@ -1177,7 +1198,7 @@ fn copy_file(source: &Path, dest: &Path, options: &Options) -> CopyResult<()> { CopyMode::SymLink => { symlink_file(source, dest, &*context_for(source, dest))?; } - CopyMode::Sparse => return Err(Error::NotImplemented(OPT_SPARSE.to_string())), + CopyMode::Sparse => return Err(Error::NotImplemented(options::SPARSE.to_string())), CopyMode::Update => { if dest.exists() { let src_metadata = fs::metadata(source)?; @@ -1212,49 +1233,68 @@ fn copy_file(source: &Path, dest: &Path, options: &Options) -> CopyResult<()> { /// Copy the file from `source` to `dest` either using the normal `fs::copy` or a /// copy-on-write scheme if --reflink is specified and the filesystem supports it. fn copy_helper(source: &Path, dest: &Path, options: &Options) -> CopyResult<()> { - if options.reflink_mode != ReflinkMode::Never { - #[cfg(not(any(target_os = "linux", target_os = "macos")))] - return Err("--reflink is only supported on linux and macOS" - .to_string() - .into()); - - #[cfg(target_os = "macos")] - copy_on_write_macos(source, dest, options.reflink_mode)?; - - #[cfg(target_os = "linux")] - copy_on_write_linux(source, dest, options.reflink_mode)?; - } else if options.no_dereference && fs::symlink_metadata(&source)?.file_type().is_symlink() { - // Here, we will copy the symlink itself (actually, just recreate it) - let link = fs::read_link(&source)?; - let dest: Cow<'_, Path> = if dest.is_dir() { - match source.file_name() { - Some(name) => dest.join(name).into(), - None => crash!( - EXIT_ERR, - "cannot stat ‘{}’: No such file or directory", - source.display() - ), - } - } else { - dest.into() - }; - symlink_file(&link, &dest, &*context_for(&link, &dest))?; - } else if source.to_string_lossy() == "/dev/null" { + if options.parents { + let parent = dest.parent().unwrap_or(dest); + fs::create_dir_all(parent)?; + } + let is_symlink = fs::symlink_metadata(&source)?.file_type().is_symlink(); + if source.to_string_lossy() == "/dev/null" { /* workaround a limitation of fs::copy * https://github.com/rust-lang/rust/issues/79390 */ File::create(dest)?; - } else { - if options.parents { - let parent = dest.parent().unwrap_or(dest); - fs::create_dir_all(parent)?; + } else if !options.dereference && is_symlink { + copy_link(source, dest)?; + } else if options.reflink_mode != ReflinkMode::Never { + #[cfg(not(any(target_os = "linux", target_os = "macos")))] + return Err("--reflink is only supported on linux and macOS" + .to_string() + .into()); + #[cfg(any(target_os = "linux", target_os = "macos"))] + if is_symlink { + assert!(options.dereference); + let real_path = std::fs::read_link(source)?; + + #[cfg(target_os = "macos")] + copy_on_write_macos(&real_path, dest, options.reflink_mode)?; + #[cfg(target_os = "linux")] + copy_on_write_linux(&real_path, dest, options.reflink_mode)?; + } else { + #[cfg(target_os = "macos")] + copy_on_write_macos(source, dest, options.reflink_mode)?; + #[cfg(target_os = "linux")] + copy_on_write_linux(source, dest, options.reflink_mode)?; } + } else { fs::copy(source, dest).context(&*context_for(source, dest))?; } Ok(()) } +fn copy_link(source: &Path, dest: &Path) -> CopyResult<()> { + // Here, we will copy the symlink itself (actually, just recreate it) + let link = fs::read_link(&source)?; + let dest: Cow<'_, Path> = if dest.is_dir() { + match source.file_name() { + Some(name) => dest.join(name).into(), + None => crash!( + EXIT_ERR, + "cannot stat '{}': No such file or directory", + source.display() + ), + } + } else { + // we always need to remove the file to be able to create a symlink, + // even if it is writeable. + if dest.exists() { + fs::remove_file(dest)?; + } + dest.into() + }; + symlink_file(&link, &dest, &*context_for(&link, &dest)) +} + /// Copies `source` to `dest` using copy-on-write if possible. #[cfg(target_os = "linux")] fn copy_on_write_linux(source: &Path, dest: &Path, mode: ReflinkMode) -> CopyResult<()> { @@ -1271,15 +1311,15 @@ fn copy_on_write_linux(source: &Path, dest: &Path, mode: ReflinkMode) -> CopyRes ReflinkMode::Always => unsafe { let result = ficlone(dst_file.as_raw_fd(), src_file.as_raw_fd() as *const i32); if result != 0 { - return Err(format!( + Err(format!( "failed to clone {:?} from {:?}: {}", source, dest, std::io::Error::last_os_error() ) - .into()); + .into()) } else { - return Ok(()); + Ok(()) } }, ReflinkMode::Auto => unsafe { @@ -1287,11 +1327,10 @@ fn copy_on_write_linux(source: &Path, dest: &Path, mode: ReflinkMode) -> CopyRes if result != 0 { fs::copy(source, dest).context(&*context_for(source, dest))?; } + Ok(()) }, ReflinkMode::Never => unreachable!(), } - - Ok(()) } /// Copies `source` to `dest` using copy-on-write if possible. @@ -1392,9 +1431,9 @@ pub fn paths_refer_to_same_file(p1: &Path, p2: &Path) -> io::Result { fn test_cp_localize_to_target() { assert!( localize_to_target( - &Path::new("a/source/"), - &Path::new("a/source/c.txt"), - &Path::new("target/") + Path::new("a/source/"), + Path::new("a/source/c.txt"), + Path::new("target/") ) .unwrap() == Path::new("target/c.txt") diff --git a/src/uu/csplit/Cargo.toml b/src/uu/csplit/Cargo.toml index 7687991b0..48655316f 100644 --- a/src/uu/csplit/Cargo.toml +++ b/src/uu/csplit/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/csplit.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } thiserror = "1.0" regex = "1.0.0" glob = "0.2.11" diff --git a/src/uu/csplit/src/csplit.rs b/src/uu/csplit/src/csplit.rs index e3b2069ab..048ec80d8 100644 --- a/src/uu/csplit/src/csplit.rs +++ b/src/uu/csplit/src/csplit.rs @@ -92,7 +92,7 @@ where T: BufRead, { let mut input_iter = InputSplitter::new(input.lines().enumerate()); - let mut split_writer = SplitWriter::new(&options); + let mut split_writer = SplitWriter::new(options); let ret = do_csplit(&mut split_writer, patterns, &mut input_iter); // consume the rest @@ -711,10 +711,37 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .collect_str(InvalidEncodingHandling::Ignore) .accept_any(); - let matches = App::new(executable!()) + let matches = uu_app().usage(&usage[..]).get_matches_from(args); + + // get the file to split + let file_name = matches.value_of(options::FILE).unwrap(); + + // get the patterns to split on + let patterns: Vec = matches + .values_of(options::PATTERN) + .unwrap() + .map(str::to_string) + .collect(); + let patterns = return_if_err!(1, patterns::get_patterns(&patterns[..])); + let options = CsplitOptions::new(&matches); + if file_name == "-" { + let stdin = io::stdin(); + crash_if_err!(1, csplit(&options, patterns, stdin.lock())); + } else { + let file = return_if_err!(1, File::open(file_name)); + let file_metadata = return_if_err!(1, file.metadata()); + if !file_metadata.is_file() { + crash!(1, "'{}' is not a regular file", file_name); + } + crash_if_err!(1, csplit(&options, patterns, BufReader::new(file))); + }; + 0 +} + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) .version(crate_version!()) .about(SUMMARY) - .usage(&usage[..]) .arg( Arg::with_name(options::SUFFIX_FORMAT) .short("b") @@ -768,29 +795,4 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .required(true), ) .after_help(LONG_HELP) - .get_matches_from(args); - - // get the file to split - let file_name = matches.value_of(options::FILE).unwrap(); - - // get the patterns to split on - let patterns: Vec = matches - .values_of(options::PATTERN) - .unwrap() - .map(str::to_string) - .collect(); - let patterns = return_if_err!(1, patterns::get_patterns(&patterns[..])); - let options = CsplitOptions::new(&matches); - if file_name == "-" { - let stdin = io::stdin(); - crash_if_err!(1, csplit(&options, patterns, stdin.lock())); - } else { - let file = return_if_err!(1, File::open(file_name)); - let file_metadata = return_if_err!(1, file.metadata()); - if !file_metadata.is_file() { - crash!(1, "'{}' is not a regular file", file_name); - } - crash_if_err!(1, csplit(&options, patterns, BufReader::new(file))); - }; - 0 } diff --git a/src/uu/csplit/src/patterns.rs b/src/uu/csplit/src/patterns.rs index 5621d18a3..4ab7862ac 100644 --- a/src/uu/csplit/src/patterns.rs +++ b/src/uu/csplit/src/patterns.rs @@ -133,20 +133,12 @@ fn extract_patterns(args: &[String]) -> Result, CsplitError> { Some(m) => m.as_str().parse().unwrap(), }; if let Some(up_to_match) = captures.name("UPTO") { - let pattern = match Regex::new(up_to_match.as_str()) { - Err(_) => { - return Err(CsplitError::InvalidPattern(arg.to_string())); - } - Ok(reg) => reg, - }; + let pattern = Regex::new(up_to_match.as_str()) + .map_err(|_| CsplitError::InvalidPattern(arg.to_string()))?; patterns.push(Pattern::UpToMatch(pattern, offset, execute_ntimes)); } else if let Some(skip_to_match) = captures.name("SKIPTO") { - let pattern = match Regex::new(skip_to_match.as_str()) { - Err(_) => { - return Err(CsplitError::InvalidPattern(arg.to_string())); - } - Ok(reg) => reg, - }; + let pattern = Regex::new(skip_to_match.as_str()) + .map_err(|_| CsplitError::InvalidPattern(arg.to_string()))?; patterns.push(Pattern::SkipToMatch(pattern, offset, execute_ntimes)); } } else if let Ok(line_number) = arg.parse::() { diff --git a/src/uu/csplit/src/split_name.rs b/src/uu/csplit/src/split_name.rs index 6db781e9b..758216414 100644 --- a/src/uu/csplit/src/split_name.rs +++ b/src/uu/csplit/src/split_name.rs @@ -33,13 +33,13 @@ impl SplitName { // get the prefix let prefix = prefix_opt.unwrap_or_else(|| "xx".to_string()); // the width for the split offset - let n_digits = match n_digits_opt { - None => 2, - Some(opt) => match opt.parse::() { - Ok(digits) => digits, - Err(_) => return Err(CsplitError::InvalidNumber(opt)), - }, - }; + let n_digits = n_digits_opt + .map(|opt| { + opt.parse::() + .map_err(|_| CsplitError::InvalidNumber(opt)) + }) + .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 { diff --git a/src/uu/cut/Cargo.toml b/src/uu/cut/Cargo.toml index c863c1772..9a83ff554 100644 --- a/src/uu/cut/Cargo.toml +++ b/src/uu/cut/Cargo.toml @@ -15,11 +15,12 @@ edition = "2018" path = "src/cut.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } memchr = "2" bstr = "0.2" +atty = "0.2" [[bin]] name = "cut" diff --git a/src/uu/cut/src/cut.rs b/src/uu/cut/src/cut.rs index 819cbb989..e33b8a2fe 100644 --- a/src/uu/cut/src/cut.rs +++ b/src/uu/cut/src/cut.rs @@ -17,7 +17,6 @@ use std::io::{stdin, stdout, BufReader, BufWriter, Read, Write}; use std::path::Path; use self::searcher::Searcher; -use uucore::fs::is_stdout_interactive; use uucore::ranges::Range; use uucore::InvalidEncodingHandling; @@ -127,7 +126,7 @@ enum Mode { } fn stdout_writer() -> Box { - if is_stdout_interactive() { + if atty::is(atty::Stream::Stdout) { Box::new(stdout()) } else { Box::new(BufWriter::new(stdout())) as Box @@ -397,88 +396,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .collect_str(InvalidEncodingHandling::Ignore) .accept_any(); - let matches = App::new(executable!()) - .name(NAME) - .version(crate_version!()) - .usage(SYNTAX) - .about(SUMMARY) - .after_help(LONG_HELP) - .arg( - Arg::with_name(options::BYTES) - .short("b") - .long(options::BYTES) - .takes_value(true) - .help("filter byte columns from the input source") - .allow_hyphen_values(true) - .value_name("LIST") - .display_order(1), - ) - .arg( - Arg::with_name(options::CHARACTERS) - .short("c") - .long(options::CHARACTERS) - .help("alias for character mode") - .takes_value(true) - .allow_hyphen_values(true) - .value_name("LIST") - .display_order(2), - ) - .arg( - Arg::with_name(options::DELIMITER) - .short("d") - .long(options::DELIMITER) - .help("specify the delimiter character that separates fields in the input source. Defaults to Tab.") - .takes_value(true) - .value_name("DELIM") - .display_order(3), - ) - .arg( - Arg::with_name(options::FIELDS) - .short("f") - .long(options::FIELDS) - .help("filter field columns from the input source") - .takes_value(true) - .allow_hyphen_values(true) - .value_name("LIST") - .display_order(4), - ) - .arg( - Arg::with_name(options::COMPLEMENT) - .long(options::COMPLEMENT) - .help("invert the filter - instead of displaying only the filtered columns, display all but those columns") - .takes_value(false) - .display_order(5), - ) - .arg( - Arg::with_name(options::ONLY_DELIMITED) - .short("s") - .long(options::ONLY_DELIMITED) - .help("in field mode, only print lines which contain the delimiter") - .takes_value(false) - .display_order(6), - ) - .arg( - Arg::with_name(options::ZERO_TERMINATED) - .short("z") - .long(options::ZERO_TERMINATED) - .help("instead of filtering columns based on line, filter columns based on \\0 (NULL character)") - .takes_value(false) - .display_order(8), - ) - .arg( - Arg::with_name(options::OUTPUT_DELIMITER) - .long(options::OUTPUT_DELIMITER) - .help("in field mode, replace the delimiter in output lines with this option's argument") - .takes_value(true) - .value_name("NEW_DELIM") - .display_order(7), - ) - .arg( - Arg::with_name(options::FILE) - .hidden(true) - .multiple(true) - ) - .get_matches_from(args); + let matches = uu_app().get_matches_from(args); let complement = matches.is_present(options::COMPLEMENT); @@ -532,7 +450,16 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let zero_terminated = matches.is_present(options::ZERO_TERMINATED); match matches.value_of(options::DELIMITER) { - Some(delim) => { + 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 + // Since clap parsing handles `-d=` as delimiter explicitly set to "" and + // an empty delimiter is not accepted by GNU's `cut` (and makes no sense), + // we can use this as basis for a simple workaround: + if delim.is_empty() { + delim = "="; + } if delim.chars().count() > 1 { Err(msg_opt_invalid_should_be!( "empty or 1 character long", @@ -619,3 +546,87 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } } } + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .name(NAME) + .version(crate_version!()) + .usage(SYNTAX) + .about(SUMMARY) + .after_help(LONG_HELP) + .arg( + Arg::with_name(options::BYTES) + .short("b") + .long(options::BYTES) + .takes_value(true) + .help("filter byte columns from the input source") + .allow_hyphen_values(true) + .value_name("LIST") + .display_order(1), + ) + .arg( + Arg::with_name(options::CHARACTERS) + .short("c") + .long(options::CHARACTERS) + .help("alias for character mode") + .takes_value(true) + .allow_hyphen_values(true) + .value_name("LIST") + .display_order(2), + ) + .arg( + Arg::with_name(options::DELIMITER) + .short("d") + .long(options::DELIMITER) + .help("specify the delimiter character that separates fields in the input source. Defaults to Tab.") + .takes_value(true) + .value_name("DELIM") + .display_order(3), + ) + .arg( + Arg::with_name(options::FIELDS) + .short("f") + .long(options::FIELDS) + .help("filter field columns from the input source") + .takes_value(true) + .allow_hyphen_values(true) + .value_name("LIST") + .display_order(4), + ) + .arg( + Arg::with_name(options::COMPLEMENT) + .long(options::COMPLEMENT) + .help("invert the filter - instead of displaying only the filtered columns, display all but those columns") + .takes_value(false) + .display_order(5), + ) + .arg( + Arg::with_name(options::ONLY_DELIMITED) + .short("s") + .long(options::ONLY_DELIMITED) + .help("in field mode, only print lines which contain the delimiter") + .takes_value(false) + .display_order(6), + ) + .arg( + Arg::with_name(options::ZERO_TERMINATED) + .short("z") + .long(options::ZERO_TERMINATED) + .help("instead of filtering columns based on line, filter columns based on \\0 (NULL character)") + .takes_value(false) + .display_order(8), + ) + .arg( + Arg::with_name(options::OUTPUT_DELIMITER) + .long(options::OUTPUT_DELIMITER) + .help("in field mode, replace the delimiter in output lines with this option's argument") + .takes_value(true) + .value_name("NEW_DELIM") + .display_order(7), + ) + .arg( + Arg::with_name(options::FILE) + .hidden(true) + .multiple(true) + ) +} diff --git a/src/uu/date/Cargo.toml b/src/uu/date/Cargo.toml index db6c077bd..3751e071e 100644 --- a/src/uu/date/Cargo.toml +++ b/src/uu/date/Cargo.toml @@ -16,7 +16,7 @@ path = "src/date.rs" [dependencies] chrono = "0.4.4" -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index 8a0e3ef3a..0071b5e8c 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -142,75 +142,11 @@ pub fn uumain(args: impl uucore::Args) -> i32 { {0} [OPTION]... [MMDDhhmm[[CC]YY][.ss]]", NAME ); - let matches = App::new(executable!()) - .version(crate_version!()) - .about(ABOUT) - .usage(&syntax[..]) - .arg( - Arg::with_name(OPT_DATE) - .short("d") - .long(OPT_DATE) - .takes_value(true) - .help("display time described by STRING, not 'now'"), - ) - .arg( - Arg::with_name(OPT_FILE) - .short("f") - .long(OPT_FILE) - .takes_value(true) - .help("like --date; once for each line of DATEFILE"), - ) - .arg( - Arg::with_name(OPT_ISO_8601) - .short("I") - .long(OPT_ISO_8601) - .takes_value(true) - .help(ISO_8601_HELP_STRING), - ) - .arg( - Arg::with_name(OPT_RFC_EMAIL) - .short("R") - .long(OPT_RFC_EMAIL) - .help(RFC_5322_HELP_STRING), - ) - .arg( - Arg::with_name(OPT_RFC_3339) - .long(OPT_RFC_3339) - .takes_value(true) - .help(RFC_3339_HELP_STRING), - ) - .arg( - Arg::with_name(OPT_DEBUG) - .long(OPT_DEBUG) - .help("annotate the parsed date, and warn about questionable usage to stderr"), - ) - .arg( - Arg::with_name(OPT_REFERENCE) - .short("r") - .long(OPT_REFERENCE) - .takes_value(true) - .help("display the last modification time of FILE"), - ) - .arg( - Arg::with_name(OPT_SET) - .short("s") - .long(OPT_SET) - .takes_value(true) - .help(OPT_SET_HELP_STRING), - ) - .arg( - Arg::with_name(OPT_UNIVERSAL) - .short("u") - .long(OPT_UNIVERSAL) - .alias(OPT_UNIVERSAL_2) - .help("print or set Coordinated Universal Time (UTC)"), - ) - .arg(Arg::with_name(OPT_FORMAT).multiple(false)) - .get_matches_from(args); + let matches = uu_app().usage(&syntax[..]).get_matches_from(args); let format = if let Some(form) = matches.value_of(OPT_FORMAT) { if !form.starts_with('+') { - eprintln!("date: invalid date ‘{}’", form); + eprintln!("date: invalid date '{}'", form); return 1; } let form = form[1..].to_string(); @@ -239,7 +175,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let set_to = match matches.value_of(OPT_SET).map(parse_date) { None => None, Some(Err((input, _err))) => { - eprintln!("date: invalid date ‘{}’", input); + eprintln!("date: invalid date '{}'", input); return 1; } Some(Ok(date)) => Some(date), @@ -305,7 +241,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { println!("{}", formatted); } Err((input, _err)) => { - println!("date: invalid date ‘{}’", input); + println!("date: invalid date '{}'", input); } } } @@ -314,6 +250,72 @@ pub fn uumain(args: impl uucore::Args) -> i32 { 0 } +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about(ABOUT) + .arg( + Arg::with_name(OPT_DATE) + .short("d") + .long(OPT_DATE) + .takes_value(true) + .help("display time described by STRING, not 'now'"), + ) + .arg( + Arg::with_name(OPT_FILE) + .short("f") + .long(OPT_FILE) + .takes_value(true) + .help("like --date; once for each line of DATEFILE"), + ) + .arg( + Arg::with_name(OPT_ISO_8601) + .short("I") + .long(OPT_ISO_8601) + .takes_value(true) + .help(ISO_8601_HELP_STRING), + ) + .arg( + Arg::with_name(OPT_RFC_EMAIL) + .short("R") + .long(OPT_RFC_EMAIL) + .help(RFC_5322_HELP_STRING), + ) + .arg( + Arg::with_name(OPT_RFC_3339) + .long(OPT_RFC_3339) + .takes_value(true) + .help(RFC_3339_HELP_STRING), + ) + .arg( + Arg::with_name(OPT_DEBUG) + .long(OPT_DEBUG) + .help("annotate the parsed date, and warn about questionable usage to stderr"), + ) + .arg( + Arg::with_name(OPT_REFERENCE) + .short("r") + .long(OPT_REFERENCE) + .takes_value(true) + .help("display the last modification time of FILE"), + ) + .arg( + Arg::with_name(OPT_SET) + .short("s") + .long(OPT_SET) + .takes_value(true) + .help(OPT_SET_HELP_STRING), + ) + .arg( + Arg::with_name(OPT_UNIVERSAL) + .short("u") + .long(OPT_UNIVERSAL) + .alias(OPT_UNIVERSAL_2) + .help("print or set Coordinated Universal Time (UTC)"), + ) + .arg(Arg::with_name(OPT_FORMAT).multiple(false)) +} + /// Return the appropriate format string for the given settings. fn make_format_string(settings: &Settings) -> &str { match settings.format { diff --git a/src/uu/df/Cargo.toml b/src/uu/df/Cargo.toml index 0e65fdb32..4700d419a 100644 --- a/src/uu/df/Cargo.toml +++ b/src/uu/df/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/df.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } number_prefix = "0.4" uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["libc", "fsext"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/df/src/df.rs b/src/uu/df/src/df.rs index 0836aa43d..1092938df 100644 --- a/src/uu/df/src/df.rs +++ b/src/uu/df/src/df.rs @@ -258,120 +258,7 @@ fn use_size(free_size: u64, total_size: u64) -> String { pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); - let matches = App::new(executable!()) - .version(crate_version!()) - .about(ABOUT) - .usage(&usage[..]) - .arg( - Arg::with_name(OPT_ALL) - .short("a") - .long("all") - .help("include dummy file systems"), - ) - .arg( - Arg::with_name(OPT_BLOCKSIZE) - .short("B") - .long("block-size") - .takes_value(true) - .help( - "scale sizes by SIZE before printing them; e.g.\ - '-BM' prints sizes in units of 1,048,576 bytes", - ), - ) - .arg( - Arg::with_name(OPT_DIRECT) - .long("direct") - .help("show statistics for a file instead of mount point"), - ) - .arg( - Arg::with_name(OPT_TOTAL) - .long("total") - .help("produce a grand total"), - ) - .arg( - Arg::with_name(OPT_HUMAN_READABLE) - .short("h") - .long("human-readable") - .conflicts_with(OPT_HUMAN_READABLE_2) - .help("print sizes in human readable format (e.g., 1K 234M 2G)"), - ) - .arg( - Arg::with_name(OPT_HUMAN_READABLE_2) - .short("H") - .long("si") - .conflicts_with(OPT_HUMAN_READABLE) - .help("likewise, but use powers of 1000 not 1024"), - ) - .arg( - Arg::with_name(OPT_INODES) - .short("i") - .long("inodes") - .help("list inode information instead of block usage"), - ) - .arg( - Arg::with_name(OPT_KILO) - .short("k") - .help("like --block-size=1K"), - ) - .arg( - Arg::with_name(OPT_LOCAL) - .short("l") - .long("local") - .help("limit listing to local file systems"), - ) - .arg( - Arg::with_name(OPT_NO_SYNC) - .long("no-sync") - .conflicts_with(OPT_SYNC) - .help("do not invoke sync before getting usage info (default)"), - ) - .arg( - Arg::with_name(OPT_OUTPUT) - .long("output") - .takes_value(true) - .use_delimiter(true) - .help( - "use the output format defined by FIELD_LIST,\ - or print all fields if FIELD_LIST is omitted.", - ), - ) - .arg( - Arg::with_name(OPT_PORTABILITY) - .short("P") - .long("portability") - .help("use the POSIX output format"), - ) - .arg( - Arg::with_name(OPT_SYNC) - .long("sync") - .conflicts_with(OPT_NO_SYNC) - .help("invoke sync before getting usage info"), - ) - .arg( - Arg::with_name(OPT_TYPE) - .short("t") - .long("type") - .takes_value(true) - .use_delimiter(true) - .help("limit listing to file systems of type TYPE"), - ) - .arg( - Arg::with_name(OPT_PRINT_TYPE) - .short("T") - .long("print-type") - .help("print file system type"), - ) - .arg( - Arg::with_name(OPT_EXCLUDE_TYPE) - .short("x") - .long("exclude-type") - .takes_value(true) - .use_delimiter(true) - .help("limit listing to file systems not of type TYPE"), - ) - .arg(Arg::with_name(OPT_PATHS).multiple(true)) - .help("Filesystem(s) to list") - .get_matches_from(args); + let matches = uu_app().usage(&usage[..]).get_matches_from(args); let paths: Vec = matches .values_of(OPT_PATHS) @@ -511,3 +398,118 @@ pub fn uumain(args: impl uucore::Args) -> i32 { EXIT_OK } + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about(ABOUT) + .arg( + Arg::with_name(OPT_ALL) + .short("a") + .long("all") + .help("include dummy file systems"), + ) + .arg( + Arg::with_name(OPT_BLOCKSIZE) + .short("B") + .long("block-size") + .takes_value(true) + .help( + "scale sizes by SIZE before printing them; e.g.\ + '-BM' prints sizes in units of 1,048,576 bytes", + ), + ) + .arg( + Arg::with_name(OPT_DIRECT) + .long("direct") + .help("show statistics for a file instead of mount point"), + ) + .arg( + Arg::with_name(OPT_TOTAL) + .long("total") + .help("produce a grand total"), + ) + .arg( + Arg::with_name(OPT_HUMAN_READABLE) + .short("h") + .long("human-readable") + .conflicts_with(OPT_HUMAN_READABLE_2) + .help("print sizes in human readable format (e.g., 1K 234M 2G)"), + ) + .arg( + Arg::with_name(OPT_HUMAN_READABLE_2) + .short("H") + .long("si") + .conflicts_with(OPT_HUMAN_READABLE) + .help("likewise, but use powers of 1000 not 1024"), + ) + .arg( + Arg::with_name(OPT_INODES) + .short("i") + .long("inodes") + .help("list inode information instead of block usage"), + ) + .arg( + Arg::with_name(OPT_KILO) + .short("k") + .help("like --block-size=1K"), + ) + .arg( + Arg::with_name(OPT_LOCAL) + .short("l") + .long("local") + .help("limit listing to local file systems"), + ) + .arg( + Arg::with_name(OPT_NO_SYNC) + .long("no-sync") + .conflicts_with(OPT_SYNC) + .help("do not invoke sync before getting usage info (default)"), + ) + .arg( + Arg::with_name(OPT_OUTPUT) + .long("output") + .takes_value(true) + .use_delimiter(true) + .help( + "use the output format defined by FIELD_LIST,\ + or print all fields if FIELD_LIST is omitted.", + ), + ) + .arg( + Arg::with_name(OPT_PORTABILITY) + .short("P") + .long("portability") + .help("use the POSIX output format"), + ) + .arg( + Arg::with_name(OPT_SYNC) + .long("sync") + .conflicts_with(OPT_NO_SYNC) + .help("invoke sync before getting usage info"), + ) + .arg( + Arg::with_name(OPT_TYPE) + .short("t") + .long("type") + .takes_value(true) + .use_delimiter(true) + .help("limit listing to file systems of type TYPE"), + ) + .arg( + Arg::with_name(OPT_PRINT_TYPE) + .short("T") + .long("print-type") + .help("print file system type"), + ) + .arg( + Arg::with_name(OPT_EXCLUDE_TYPE) + .short("x") + .long("exclude-type") + .takes_value(true) + .use_delimiter(true) + .help("limit listing to file systems not of type TYPE"), + ) + .arg(Arg::with_name(OPT_PATHS).multiple(true)) + .help("Filesystem(s) to list") +} diff --git a/src/uu/dircolors/Cargo.toml b/src/uu/dircolors/Cargo.toml index 5e822820e..a97c78c78 100644 --- a/src/uu/dircolors/Cargo.toml +++ b/src/uu/dircolors/Cargo.toml @@ -15,6 +15,7 @@ edition = "2018" path = "src/dircolors.rs" [dependencies] +clap = { version = "2.33", features = ["wrap_help"] } glob = "0.3.0" uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/dircolors/src/dircolors.rs b/src/uu/dircolors/src/dircolors.rs index b6942c2d2..70b609e31 100644 --- a/src/uu/dircolors/src/dircolors.rs +++ b/src/uu/dircolors/src/dircolors.rs @@ -1,6 +1,7 @@ // This file is part of the uutils coreutils package. // // (c) Jian Zeng +// (c) Mitchell Mebane // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. @@ -15,6 +16,15 @@ use std::env; use std::fs::File; use std::io::{BufRead, BufReader}; +use clap::{crate_version, App, Arg}; + +mod options { + pub const BOURNE_SHELL: &str = "bourne-shell"; + pub const C_SHELL: &str = "c-shell"; + pub const PRINT_DATABASE: &str = "print-database"; + pub const FILE: &str = "FILE"; +} + static SYNTAX: &str = "[OPTION]... [FILE]"; static SUMMARY: &str = "Output commands to set the LS_COLORS environment variable."; static LONG_HELP: &str = " @@ -52,28 +62,27 @@ pub fn guess_syntax() -> OutputFmt { } } +fn get_usage() -> String { + format!("{0} {1}", executable!(), SYNTAX) +} + pub fn uumain(args: impl uucore::Args) -> i32 { let args = args .collect_str(InvalidEncodingHandling::Ignore) .accept_any(); - let matches = app!(SYNTAX, SUMMARY, LONG_HELP) - .optflag("b", "sh", "output Bourne shell code to set LS_COLORS") - .optflag( - "", - "bourne-shell", - "output Bourne shell code to set LS_COLORS", - ) - .optflag("c", "csh", "output C shell code to set LS_COLORS") - .optflag("", "c-shell", "output C shell code to set LS_COLORS") - .optflag("p", "print-database", "print the byte counts") - .parse(args); + let usage = get_usage(); - if (matches.opt_present("csh") - || matches.opt_present("c-shell") - || matches.opt_present("sh") - || matches.opt_present("bourne-shell")) - && matches.opt_present("print-database") + let matches = uu_app().usage(&usage[..]).get_matches_from(&args); + + let files = matches + .values_of(options::FILE) + .map_or(vec![], |file_values| file_values.collect()); + + // clap provides .conflicts_with / .conflicts_with_all, but we want to + // manually handle conflicts so we can match the output of GNU coreutils + if (matches.is_present(options::C_SHELL) || matches.is_present(options::BOURNE_SHELL)) + && matches.is_present(options::PRINT_DATABASE) { show_usage_error!( "the options to output dircolors' internal database and\nto select a shell \ @@ -82,12 +91,12 @@ pub fn uumain(args: impl uucore::Args) -> i32 { return 1; } - if matches.opt_present("print-database") { - if !matches.free.is_empty() { + if matches.is_present(options::PRINT_DATABASE) { + if !files.is_empty() { show_usage_error!( - "extra operand ‘{}’\nfile operands cannot be combined with \ + "extra operand '{}'\nfile operands cannot be combined with \ --print-database (-p)", - matches.free[0] + files[0] ); return 1; } @@ -96,9 +105,9 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } let mut out_format = OutputFmt::Unknown; - if matches.opt_present("csh") || matches.opt_present("c-shell") { + if matches.is_present(options::C_SHELL) { out_format = OutputFmt::CShell; - } else if matches.opt_present("sh") || matches.opt_present("bourne-shell") { + } else if matches.is_present(options::BOURNE_SHELL) { out_format = OutputFmt::Shell; } @@ -113,24 +122,20 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } let result; - if matches.free.is_empty() { + if files.is_empty() { result = parse(INTERNAL_DB.lines(), out_format, "") } else { - if matches.free.len() > 1 { - show_usage_error!("extra operand ‘{}’", matches.free[1]); + if files.len() > 1 { + show_usage_error!("extra operand '{}'", files[1]); return 1; } - match File::open(matches.free[0].as_str()) { + match File::open(files[0]) { Ok(f) => { let fin = BufReader::new(f); - result = parse( - fin.lines().filter_map(Result::ok), - out_format, - matches.free[0].as_str(), - ) + result = parse(fin.lines().filter_map(Result::ok), out_format, files[0]) } Err(e) => { - show_error!("{}: {}", matches.free[0], e); + show_error!("{}: {}", files[0], e); return 1; } } @@ -147,6 +152,37 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } } +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about(SUMMARY) + .after_help(LONG_HELP) + .arg( + Arg::with_name(options::BOURNE_SHELL) + .long("sh") + .short("b") + .visible_alias("bourne-shell") + .help("output Bourne shell code to set LS_COLORS") + .display_order(1), + ) + .arg( + Arg::with_name(options::C_SHELL) + .long("csh") + .short("c") + .visible_alias("c-shell") + .help("output C shell code to set LS_COLORS") + .display_order(2), + ) + .arg( + Arg::with_name(options::PRINT_DATABASE) + .long("print-database") + .short("p") + .help("print the byte counts") + .display_order(3), + ) + .arg(Arg::with_name(options::FILE).hidden(true).multiple(true)) +} + pub trait StrUtils { /// Remove comments and trim whitespace fn purify(&self) -> &Self; @@ -158,21 +194,25 @@ pub trait StrUtils { impl StrUtils for str { fn purify(&self) -> &Self { let mut line = self; - for (n, c) in self.chars().enumerate() { - if c != '#' { - continue; - } - - // Ignore if '#' is at the beginning of line - if n == 0 { - line = &self[..0]; - break; - } - + for (n, _) in self + .as_bytes() + .iter() + .enumerate() + .filter(|(_, c)| **c == b'#') + { // Ignore the content after '#' // only if it is preceded by at least one whitespace - if self.chars().nth(n - 1).unwrap().is_whitespace() { - line = &self[..n]; + match self[..n].chars().last() { + Some(c) if c.is_whitespace() => { + line = &self[..n - c.len_utf8()]; + break; + } + None => { + // n == 0 + line = &self[..0]; + break; + } + _ => (), } } line.trim() diff --git a/src/uu/dirname/Cargo.toml b/src/uu/dirname/Cargo.toml index 0975f33bb..2375d66c9 100644 --- a/src/uu/dirname/Cargo.toml +++ b/src/uu/dirname/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/dirname.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } libc = "0.2.42" uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/dirname/src/dirname.rs b/src/uu/dirname/src/dirname.rs index ad42517d4..356f2e6b1 100644 --- a/src/uu/dirname/src/dirname.rs +++ b/src/uu/dirname/src/dirname.rs @@ -38,18 +38,9 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); let after_help = get_long_usage(); - let matches = App::new(executable!()) - .about(ABOUT) + let matches = uu_app() .usage(&usage[..]) .after_help(&after_help[..]) - .version(crate_version!()) - .arg( - Arg::with_name(options::ZERO) - .long(options::ZERO) - .short("z") - .help("separate output with NUL rather than newline"), - ) - .arg(Arg::with_name(options::DIR).hidden(true).multiple(true)) .get_matches_from(args); let separator = if matches.is_present(options::ZERO) { @@ -92,3 +83,16 @@ pub fn uumain(args: impl uucore::Args) -> i32 { 0 } + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .about(ABOUT) + .version(crate_version!()) + .arg( + Arg::with_name(options::ZERO) + .long(options::ZERO) + .short("z") + .help("separate output with NUL rather than newline"), + ) + .arg(Arg::with_name(options::DIR).hidden(true).multiple(true)) +} diff --git a/src/uu/du/Cargo.toml b/src/uu/du/Cargo.toml index 023c0a021..60f37db06 100644 --- a/src/uu/du/Cargo.toml +++ b/src/uu/du/Cargo.toml @@ -15,10 +15,12 @@ edition = "2018" path = "src/du.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } chrono = "0.4" uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } + +[target.'cfg(target_os = "windows")'.dependencies] winapi = { version="0.3", features=[] } [[bin]] diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index a46f74100..05167853c 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -1,17 +1,19 @@ -// This file is part of the uutils coreutils package. -// -// (c) Derek Chiang -// -// For the full copyright and license information, please view the LICENSE -// file that was distributed with this source code. +// * This file is part of the uutils coreutils package. +// * +// * (c) Derek Chiang +// * +// * For the full copyright and license information, please view the LICENSE +// * file that was distributed with this source code. #[macro_use] extern crate uucore; use chrono::prelude::DateTime; use chrono::Local; +use clap::ArgMatches; use clap::{crate_version, App, Arg}; use std::collections::HashSet; +use std::convert::TryFrom; use std::env; use std::fs; #[cfg(not(windows))] @@ -24,8 +26,12 @@ use std::os::unix::fs::MetadataExt; use std::os::windows::fs::MetadataExt; #[cfg(windows)] use std::os::windows::io::AsRawHandle; +#[cfg(windows)] +use std::path::Path; use std::path::PathBuf; +use std::str::FromStr; use std::time::{Duration, UNIX_EPOCH}; +use uucore::parse_size::{parse_size, ParseSizeError}; use uucore::InvalidEncodingHandling; #[cfg(windows)] use winapi::shared::minwindef::{DWORD, LPVOID}; @@ -42,7 +48,7 @@ mod options { pub const NULL: &str = "0"; pub const ALL: &str = "all"; pub const APPARENT_SIZE: &str = "apparent-size"; - pub const BLOCK_SIZE: &str = "B"; + pub const BLOCK_SIZE: &str = "block-size"; pub const BYTES: &str = "b"; pub const TOTAL: &str = "c"; pub const MAX_DEPTH: &str = "d"; @@ -52,9 +58,13 @@ mod options { pub const BLOCK_SIZE_1M: &str = "m"; pub const SEPARATE_DIRS: &str = "S"; pub const SUMMARIZE: &str = "s"; + pub const THRESHOLD: &str = "threshold"; pub const SI: &str = "si"; pub const TIME: &str = "time"; pub const TIME_STYLE: &str = "time-style"; + pub const ONE_FILE_SYSTEM: &str = "one-file-system"; + pub const DEREFERENCE: &str = "dereference"; + pub const INODES: &str = "inodes"; pub const FILE: &str = "FILE"; } @@ -79,6 +89,9 @@ struct Options { max_depth: Option, total: bool, separate_dirs: bool, + one_file_system: bool, + dereference: bool, + inodes: bool, } #[derive(PartialEq, Eq, Hash, Clone, Copy)] @@ -92,6 +105,7 @@ struct Stat { is_dir: bool, size: u64, blocks: u64, + inodes: u64, inode: Option, created: Option, accessed: u64, @@ -99,8 +113,12 @@ struct Stat { } impl Stat { - fn new(path: PathBuf) -> Result { - let metadata = fs::symlink_metadata(&path)?; + fn new(path: PathBuf, options: &Options) -> Result { + let metadata = if options.dereference { + fs::metadata(&path)? + } else { + fs::symlink_metadata(&path)? + }; #[cfg(not(windows))] let file_info = FileInfo { @@ -113,6 +131,7 @@ impl Stat { is_dir: metadata.is_dir(), size: metadata.len(), blocks: metadata.blocks() as u64, + inodes: 1, inode: Some(file_info), created: birth_u64(&metadata), accessed: metadata.atime() as u64, @@ -130,6 +149,7 @@ impl Stat { size: metadata.len(), blocks: size_on_disk / 1024 * 2, inode: file_info, + inodes: 1, created: windows_creation_time_to_unix_time(metadata.creation_time()), accessed: windows_time_to_unix_time(metadata.last_access_time()), modified: windows_time_to_unix_time(metadata.last_write_time()), @@ -159,7 +179,7 @@ fn birth_u64(meta: &Metadata) -> Option { } #[cfg(windows)] -fn get_size_on_disk(path: &PathBuf) -> u64 { +fn get_size_on_disk(path: &Path) -> u64 { let mut size_on_disk = 0; // bind file so it stays in scope until end of function @@ -191,7 +211,7 @@ fn get_size_on_disk(path: &PathBuf) -> u64 { } #[cfg(windows)] -fn get_file_info(path: &PathBuf) -> Option { +fn get_file_info(path: &Path) -> Option { let mut result = None; let file = match fs::File::open(path) { @@ -223,65 +243,35 @@ fn get_file_info(path: &PathBuf) -> Option { result } -fn unit_string_to_number(s: &str) -> Option { - let mut offset = 0; - let mut s_chars = s.chars().rev(); - - let (mut ch, multiple) = match s_chars.next() { - Some('B') | Some('b') => ('B', 1000u64), - Some(ch) => (ch, 1024u64), - None => return None, - }; - if ch == 'B' { - ch = s_chars.next()?; - offset += 1; - } - ch = ch.to_ascii_uppercase(); - - let unit = UNITS - .iter() - .rev() - .find(|&&(unit_ch, _)| unit_ch == ch) - .map(|&(_, val)| { - // we found a match, so increment offset - offset += 1; - val - }) - .or_else(|| if multiple == 1024 { Some(0) } else { None })?; - - let number = s[..s.len() - offset].parse::().ok()?; - - Some(number * multiple.pow(unit)) -} - -fn translate_to_pure_number(s: &Option<&str>) -> Option { - match *s { - Some(ref s) => unit_string_to_number(s), - None => None, - } -} - -fn read_block_size(s: Option<&str>) -> u64 { - match translate_to_pure_number(&s) { - Some(v) => v, - None => { - if let Some(value) = s { - show_error!("invalid --block-size argument '{}'", value); - }; - - for env_var in &["DU_BLOCK_SIZE", "BLOCK_SIZE", "BLOCKSIZE"] { - let env_size = env::var(env_var).ok(); - if let Some(quantity) = translate_to_pure_number(&env_size.as_deref()) { - return quantity; +fn read_block_size(s: Option<&str>) -> usize { + if let Some(s) = s { + parse_size(s) + .unwrap_or_else(|e| crash!(1, "{}", format_error_message(e, s, options::BLOCK_SIZE))) + } else { + for env_var in &["DU_BLOCK_SIZE", "BLOCK_SIZE", "BLOCKSIZE"] { + if let Ok(env_size) = env::var(env_var) { + if let Ok(v) = parse_size(&env_size) { + return v; } } - - if env::var("POSIXLY_CORRECT").is_ok() { - 512 - } else { - 1024 - } } + if env::var("POSIXLY_CORRECT").is_ok() { + 512 + } else { + 1024 + } + } +} + +fn choose_size(matches: &ArgMatches, stat: &Stat) -> u64 { + if matches.is_present(options::INODES) { + stat.inodes + } else if matches.is_present(options::APPARENT_SIZE) || matches.is_present(options::BYTES) { + stat.size + } else { + // The st_blocks field indicates the number of blocks allocated to the file, 512-byte units. + // See: http://linux.die.net/man/2/stat + stat.blocks * 512 } } @@ -302,7 +292,7 @@ fn du( Err(e) => { safe_writeln!( stderr(), - "{}: cannot read directory ‘{}‘: {}", + "{}: cannot read directory '{}': {}", options.program_name, my_stat.path.display(), e @@ -313,20 +303,29 @@ fn du( for f in read { match f { - Ok(entry) => match Stat::new(entry.path()) { + Ok(entry) => match Stat::new(entry.path(), options) { Ok(this_stat) => { + if let Some(inode) = this_stat.inode { + if inodes.contains(&inode) { + continue; + } + inodes.insert(inode); + } if this_stat.is_dir { + if options.one_file_system { + if let (Some(this_inode), Some(my_inode)) = + (this_stat.inode, my_stat.inode) + { + if this_inode.dev_id != my_inode.dev_id { + continue; + } + } + } futures.push(du(this_stat, options, depth + 1, inodes)); } else { - if this_stat.inode.is_some() { - let inode = this_stat.inode.unwrap(); - if inodes.contains(&inode) { - continue; - } - inodes.insert(inode); - } my_stat.size += this_stat.size; my_stat.blocks += this_stat.blocks; + my_stat.inodes += 1; if options.all { stats.push(this_stat); } @@ -334,18 +333,11 @@ fn du( } Err(error) => match error.kind() { ErrorKind::PermissionDenied => { - let description = format!( - "cannot access '{}'", - entry - .path() - .as_os_str() - .to_str() - .unwrap_or("") - ); + let description = format!("cannot access '{}'", entry.path().display()); let error_message = "Permission denied"; show_error_custom_description!(description, "{}", error_message) } - _ => show_error!("{}", error), + _ => show_error!("cannot access '{}': {}", entry.path().display(), error), }, }, Err(error) => show_error!("{}", error), @@ -353,12 +345,15 @@ fn du( } } - stats.extend(futures.into_iter().flatten().rev().filter(|stat| { + stats.extend(futures.into_iter().flatten().filter(|stat| { if !options.separate_dirs && stat.path.parent().unwrap() == my_stat.path { my_stat.size += stat.size; my_stat.blocks += stat.blocks; + my_stat.inodes += stat.inodes; } - options.max_depth == None || depth < options.max_depth.unwrap() + options + .max_depth + .map_or(true, |max_depth| depth < max_depth) })); stats.push(my_stat); Box::new(stats.into_iter()) @@ -412,182 +407,19 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); - let matches = App::new(executable!()) - .version(crate_version!()) - .about(SUMMARY) - .usage(&usage[..]) - .after_help(LONG_HELP) - .arg( - Arg::with_name(options::ALL) - .short("a") - .long(options::ALL) - .help("write counts for all files, not just directories"), - ) - .arg( - Arg::with_name(options::APPARENT_SIZE) - .long(options::APPARENT_SIZE) - .help( - "print apparent sizes, rather than disk usage \ - although the apparent size is usually smaller, it may be larger due to holes \ - in ('sparse') files, internal fragmentation, indirect blocks, and the like" - ) - ) - .arg( - Arg::with_name(options::BLOCK_SIZE) - .short("B") - .long("block-size") - .value_name("SIZE") - .help( - "scale sizes by SIZE before printing them. \ - E.g., '-BM' prints sizes in units of 1,048,576 bytes. See SIZE format below." - ) - ) - .arg( - Arg::with_name(options::BYTES) - .short("b") - .long("bytes") - .help("equivalent to '--apparent-size --block-size=1'") - ) - .arg( - Arg::with_name(options::TOTAL) - .long("total") - .short("c") - .help("produce a grand total") - ) - .arg( - Arg::with_name(options::MAX_DEPTH) - .short("d") - .long("max-depth") - .value_name("N") - .help( - "print the total for a directory (or file, with --all) \ - only if it is N or fewer levels below the command \ - line argument; --max-depth=0 is the same as --summarize" - ) - ) - .arg( - Arg::with_name(options::HUMAN_READABLE) - .long("human-readable") - .short("h") - .help("print sizes in human readable format (e.g., 1K 234M 2G)") - ) - .arg( - Arg::with_name("inodes") - .long("inodes") - .help( - "list inode usage information instead of block usage like --block-size=1K" - ) - ) - .arg( - Arg::with_name(options::BLOCK_SIZE_1K) - .short("k") - .help("like --block-size=1K") - ) - .arg( - Arg::with_name(options::COUNT_LINKS) - .short("l") - .long("count-links") - .help("count sizes many times if hard linked") - ) - // .arg( - // Arg::with_name("dereference") - // .short("L") - // .long("dereference") - // .help("dereference all symbolic links") - // ) - // .arg( - // Arg::with_name("no-dereference") - // .short("P") - // .long("no-dereference") - // .help("don't follow any symbolic links (this is the default)") - // ) - .arg( - Arg::with_name(options::BLOCK_SIZE_1M) - .short("m") - .help("like --block-size=1M") - ) - .arg( - Arg::with_name(options::NULL) - .short("0") - .long("null") - .help("end each output line with 0 byte rather than newline") - ) - .arg( - Arg::with_name(options::SEPARATE_DIRS) - .short("S") - .long("separate-dirs") - .help("do not include size of subdirectories") - ) - .arg( - Arg::with_name(options::SUMMARIZE) - .short("s") - .long("summarize") - .help("display only a total for each argument") - ) - .arg( - Arg::with_name(options::SI) - .long(options::SI) - .help("like -h, but use powers of 1000 not 1024") - ) - // .arg( - // Arg::with_name("one-file-system") - // .short("x") - // .long("one-file-system") - // .help("skip directories on different file systems") - // ) - // .arg( - // Arg::with_name("") - // .short("x") - // .long("exclude-from") - // .value_name("FILE") - // .help("exclude files that match any pattern in FILE") - // ) - // .arg( - // Arg::with_name("exclude") - // .long("exclude") - // .value_name("PATTERN") - // .help("exclude files that match PATTERN") - // ) - .arg( - Arg::with_name(options::TIME) - .long(options::TIME) - .value_name("WORD") - .require_equals(true) - .min_values(0) - .possible_values(&["atime", "access", "use", "ctime", "status", "birth", "creation"]) - .help( - "show time of the last modification of any file in the \ - directory, or any of its subdirectories. If WORD is given, show time as WORD instead \ - of modification time: atime, access, use, ctime, status, birth or creation" - ) - ) - .arg( - Arg::with_name(options::TIME_STYLE) - .long(options::TIME_STYLE) - .value_name("STYLE") - .help( - "show times using style STYLE: \ - full-iso, long-iso, iso, +FORMAT FORMAT is interpreted like 'date'" - ) - ) - .arg( - Arg::with_name(options::FILE) - .hidden(true) - .multiple(true) - ) - .get_matches_from(args); + let matches = uu_app().usage(&usage[..]).get_matches_from(args); let summarize = matches.is_present(options::SUMMARIZE); let max_depth_str = matches.value_of(options::MAX_DEPTH); let max_depth = max_depth_str.as_ref().and_then(|s| s.parse::().ok()); match (max_depth_str, max_depth) { - (Some(ref s), _) if summarize => { - show_error!("summarizing conflicts with --max-depth={}", *s); + (Some(s), _) if summarize => { + show_error!("summarizing conflicts with --max-depth={}", s); return 1; } - (Some(ref s), None) => { - show_error!("invalid maximum depth '{}'", *s); + (Some(s), None) => { + show_error!("invalid maximum depth '{}'", s); return 1; } (Some(_), Some(_)) | (None, _) => { /* valid */ } @@ -599,16 +431,28 @@ pub fn uumain(args: impl uucore::Args) -> i32 { max_depth, total: matches.is_present(options::TOTAL), separate_dirs: matches.is_present(options::SEPARATE_DIRS), + one_file_system: matches.is_present(options::ONE_FILE_SYSTEM), + dereference: matches.is_present(options::DEREFERENCE), + inodes: matches.is_present(options::INODES), }; let files = match matches.value_of(options::FILE) { Some(_) => matches.values_of(options::FILE).unwrap().collect(), - None => { - vec!["./"] // TODO: gnu `du` doesn't use trailing "/" here - } + None => vec!["."], }; - let block_size = read_block_size(matches.value_of(options::BLOCK_SIZE)); + if options.inodes + && (matches.is_present(options::APPARENT_SIZE) || matches.is_present(options::BYTES)) + { + show_warning!("options --apparent-size and -b are ineffective with --inodes") + } + + let block_size = u64::try_from(read_block_size(matches.value_of(options::BLOCK_SIZE))).unwrap(); + + let threshold = matches.value_of(options::THRESHOLD).map(|s| { + Threshold::from_str(s) + .unwrap_or_else(|e| crash!(1, "{}", format_error_message(e, s, options::THRESHOLD))) + }); let multiplier: u64 = if matches.is_present(options::SI) { 1000 @@ -628,7 +472,13 @@ pub fn uumain(args: impl uucore::Args) -> i32 { convert_size_other } }; - let convert_size = |size| convert_size_fn(size, multiplier, block_size); + let convert_size = |size: u64| { + if options.inodes { + size.to_string() + } else { + convert_size_fn(size, multiplier, block_size) + } + }; let time_format_str = match matches.value_of("time-style") { Some(s) => match s { @@ -661,23 +511,22 @@ Try '{} --help' for more information.", let mut grand_total = 0; for path_string in files { let path = PathBuf::from(&path_string); - match Stat::new(path) { + match Stat::new(path, &options) { Ok(stat) => { let mut inodes: HashSet = HashSet::new(); - + if let Some(inode) = stat.inode { + inodes.insert(inode); + } let iter = du(stat, &options, 0, &mut inodes); let (_, len) = iter.size_hint(); let len = len.unwrap(); for (index, stat) in iter.enumerate() { - let size = if matches.is_present(options::APPARENT_SIZE) - || matches.is_present(options::BYTES) - { - stat.size - } else { - // C's stat is such that each block is assume to be 512 bytes - // See: http://linux.die.net/man/2/stat - stat.blocks * 512 - }; + let size = choose_size(&matches, &stat); + + if threshold.map_or(false, |threshold| threshold.should_exclude(size)) { + continue; + } + if matches.is_present(options::TIME) { let tm = { let secs = { @@ -690,8 +539,8 @@ Try '{} --help' for more information.", time } else { show_error!( - "Invalid argument ‘{}‘ for --time. -‘birth‘ and ‘creation‘ arguments are not supported on this platform.", + "Invalid argument '{}' for --time. +'birth' and 'creation' arguments are not supported on this platform.", s ); return 1; @@ -744,31 +593,235 @@ Try '{} --help' for more information.", 0 } +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about(SUMMARY) + .after_help(LONG_HELP) + .arg( + Arg::with_name(options::ALL) + .short("a") + .long(options::ALL) + .help("write counts for all files, not just directories"), + ) + .arg( + Arg::with_name(options::APPARENT_SIZE) + .long(options::APPARENT_SIZE) + .help( + "print apparent sizes, rather than disk usage \ + although the apparent size is usually smaller, it may be larger due to holes \ + in ('sparse') files, internal fragmentation, indirect blocks, and the like" + ) + .alias("app") // The GNU test suite uses this alias + ) + .arg( + Arg::with_name(options::BLOCK_SIZE) + .short("B") + .long(options::BLOCK_SIZE) + .value_name("SIZE") + .help( + "scale sizes by SIZE before printing them. \ + E.g., '-BM' prints sizes in units of 1,048,576 bytes. See SIZE format below." + ) + ) + .arg( + Arg::with_name(options::BYTES) + .short("b") + .long("bytes") + .help("equivalent to '--apparent-size --block-size=1'") + ) + .arg( + Arg::with_name(options::TOTAL) + .long("total") + .short("c") + .help("produce a grand total") + ) + .arg( + Arg::with_name(options::MAX_DEPTH) + .short("d") + .long("max-depth") + .value_name("N") + .help( + "print the total for a directory (or file, with --all) \ + only if it is N or fewer levels below the command \ + line argument; --max-depth=0 is the same as --summarize" + ) + ) + .arg( + Arg::with_name(options::HUMAN_READABLE) + .long("human-readable") + .short("h") + .help("print sizes in human readable format (e.g., 1K 234M 2G)") + ) + .arg( + Arg::with_name(options::INODES) + .long(options::INODES) + .help( + "list inode usage information instead of block usage like --block-size=1K" + ) + ) + .arg( + Arg::with_name(options::BLOCK_SIZE_1K) + .short("k") + .help("like --block-size=1K") + ) + .arg( + Arg::with_name(options::COUNT_LINKS) + .short("l") + .long("count-links") + .help("count sizes many times if hard linked") + ) + .arg( + Arg::with_name(options::DEREFERENCE) + .short("L") + .long(options::DEREFERENCE) + .help("dereference all symbolic links") + ) + // .arg( + // Arg::with_name("no-dereference") + // .short("P") + // .long("no-dereference") + // .help("don't follow any symbolic links (this is the default)") + // ) + .arg( + Arg::with_name(options::BLOCK_SIZE_1M) + .short("m") + .help("like --block-size=1M") + ) + .arg( + Arg::with_name(options::NULL) + .short("0") + .long("null") + .help("end each output line with 0 byte rather than newline") + ) + .arg( + Arg::with_name(options::SEPARATE_DIRS) + .short("S") + .long("separate-dirs") + .help("do not include size of subdirectories") + ) + .arg( + Arg::with_name(options::SUMMARIZE) + .short("s") + .long("summarize") + .help("display only a total for each argument") + ) + .arg( + Arg::with_name(options::SI) + .long(options::SI) + .help("like -h, but use powers of 1000 not 1024") + ) + .arg( + Arg::with_name(options::ONE_FILE_SYSTEM) + .short("x") + .long(options::ONE_FILE_SYSTEM) + .help("skip directories on different file systems") + ) + .arg( + Arg::with_name(options::THRESHOLD) + .short("t") + .long(options::THRESHOLD) + .alias("th") + .value_name("SIZE") + .number_of_values(1) + .allow_hyphen_values(true) + .help("exclude entries smaller than SIZE if positive, \ + or entries greater than SIZE if negative") + ) + // .arg( + // Arg::with_name("") + // .short("x") + // .long("exclude-from") + // .value_name("FILE") + // .help("exclude files that match any pattern in FILE") + // ) + // .arg( + // Arg::with_name("exclude") + // .long("exclude") + // .value_name("PATTERN") + // .help("exclude files that match PATTERN") + // ) + .arg( + Arg::with_name(options::TIME) + .long(options::TIME) + .value_name("WORD") + .require_equals(true) + .min_values(0) + .possible_values(&["atime", "access", "use", "ctime", "status", "birth", "creation"]) + .help( + "show time of the last modification of any file in the \ + directory, or any of its subdirectories. If WORD is given, show time as WORD instead \ + of modification time: atime, access, use, ctime, status, birth or creation" + ) + ) + .arg( + Arg::with_name(options::TIME_STYLE) + .long(options::TIME_STYLE) + .value_name("STYLE") + .help( + "show times using style STYLE: \ + full-iso, long-iso, iso, +FORMAT FORMAT is interpreted like 'date'" + ) + ) + .arg( + Arg::with_name(options::FILE) + .hidden(true) + .multiple(true) + ) +} + +#[derive(Clone, Copy)] +enum Threshold { + Lower(u64), + Upper(u64), +} + +impl FromStr for Threshold { + type Err = ParseSizeError; + + fn from_str(s: &str) -> std::result::Result { + let offset = if s.starts_with(&['-', '+'][..]) { 1 } else { 0 }; + + let size = u64::try_from(parse_size(&s[offset..])?).unwrap(); + + if s.starts_with('-') { + Ok(Threshold::Upper(size)) + } else { + Ok(Threshold::Lower(size)) + } + } +} + +impl Threshold { + fn should_exclude(&self, size: u64) -> bool { + match *self { + Threshold::Upper(threshold) => size > threshold, + Threshold::Lower(threshold) => size < threshold, + } + } +} + +fn format_error_message(error: ParseSizeError, s: &str, option: &str) -> String { + // NOTE: + // GNU's du echos affected flag, -B or --block-size (-t or --threshold), depending user's selection + // GNU's du does distinguish between "invalid (suffix in) argument" + match error { + ParseSizeError::ParseFailure(_) => format!("invalid --{} argument '{}'", option, s), + ParseSizeError::SizeTooBig(_) => format!("--{} argument '{}' too large", option, s), + } +} + #[cfg(test)] mod test_du { #[allow(unused_imports)] use super::*; - #[test] - fn test_translate_to_pure_number() { - let test_data = [ - (Some("10".to_string()), Some(10)), - (Some("10K".to_string()), Some(10 * 1024)), - (Some("5M".to_string()), Some(5 * 1024 * 1024)), - (Some("900KB".to_string()), Some(900 * 1000)), - (Some("BAD_STRING".to_string()), None), - ]; - for it in test_data.iter() { - assert_eq!(translate_to_pure_number(&it.0.as_deref()), it.1); - } - } - #[test] fn test_read_block_size() { let test_data = [ - (Some("10".to_string()), 10), + (Some("1024".to_string()), 1024), + (Some("K".to_string()), 1024), (None, 1024), - (Some("BAD_STRING".to_string()), 1024), ]; for it in test_data.iter() { assert_eq!(read_block_size(it.0.as_deref()), it.1); diff --git a/src/uu/echo/Cargo.toml b/src/uu/echo/Cargo.toml index 15f189030..5ba44d4a8 100644 --- a/src/uu/echo/Cargo.toml +++ b/src/uu/echo/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/echo.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/echo/src/echo.rs b/src/uu/echo/src/echo.rs index 56cd967f4..8c976c2b4 100644 --- a/src/uu/echo/src/echo.rs +++ b/src/uu/echo/src/echo.rs @@ -117,7 +117,26 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let args = args .collect_str(InvalidEncodingHandling::ConvertLossy) .accept_any(); - let matches = App::new(executable!()) + let matches = uu_app().get_matches_from(args); + + let no_newline = matches.is_present(options::NO_NEWLINE); + let escaped = matches.is_present(options::ENABLE_BACKSLASH_ESCAPE); + let values: Vec = match matches.values_of(options::STRING) { + Some(s) => s.map(|s| s.to_string()).collect(), + None => vec!["".to_string()], + }; + + match execute(no_newline, escaped, values) { + Ok(_) => 0, + Err(f) => { + show_error!("{}", f); + 1 + } + } +} + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) .name(NAME) // TrailingVarArg specifies the final positional argument is a VarArg // and it doesn't attempts the parse any further args. @@ -154,22 +173,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .multiple(true) .allow_hyphen_values(true), ) - .get_matches_from(args); - - let no_newline = matches.is_present(options::NO_NEWLINE); - let escaped = matches.is_present(options::ENABLE_BACKSLASH_ESCAPE); - let values: Vec = match matches.values_of(options::STRING) { - Some(s) => s.map(|s| s.to_string()).collect(), - None => vec!["".to_string()], - }; - - match execute(no_newline, escaped, values) { - Ok(_) => 0, - Err(f) => { - show_error!("{}", f); - 1 - } - } } fn execute(no_newline: bool, escaped: bool, free: Vec) -> io::Result<()> { @@ -181,7 +184,7 @@ fn execute(no_newline: bool, escaped: bool, free: Vec) -> io::Result<()> write!(output, " ")?; } if escaped { - let should_stop = print_escaped(&input, &mut output)?; + let should_stop = print_escaped(input, &mut output)?; if should_stop { break; } diff --git a/src/uu/env/Cargo.toml b/src/uu/env/Cargo.toml index ef0017e02..7cbd812c2 100644 --- a/src/uu/env/Cargo.toml +++ b/src/uu/env/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/env.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } libc = "0.2.42" rust-ini = "0.13.0" uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } diff --git a/src/uu/env/src/env.rs b/src/uu/env/src/env.rs index 50a327260..51ff92801 100644 --- a/src/uu/env/src/env.rs +++ b/src/uu/env/src/env.rs @@ -82,13 +82,10 @@ fn load_config_file(opts: &mut Options) -> Result<(), i32> { Ini::load_from_file(file) }; - let conf = match conf { - Ok(config) => config, - Err(error) => { - eprintln!("env: error: \"{}\": {}", file, error); - return Err(1); - } - }; + let conf = conf.map_err(|error| { + eprintln!("env: error: \"{}\": {}", file, error); + 1 + })?; for (_, prop) in &conf { // ignore all INI section lines (treat them as comments) @@ -117,7 +114,7 @@ fn build_command<'a, 'b>(args: &'a mut Vec<&'b str>) -> (Cow<'b, str>, &'a [&'b (progname, &args[..]) } -fn create_app() -> App<'static, 'static> { +pub fn uu_app() -> App<'static, 'static> { App::new(crate_name!()) .version(crate_version!()) .author(crate_authors!()) @@ -161,7 +158,7 @@ fn create_app() -> App<'static, 'static> { } fn run_env(args: impl uucore::Args) -> Result<(), i32> { - let app = create_app(); + let app = uu_app(); let matches = app.get_matches_from(args); let ignore_env = matches.is_present("ignore-environment"); @@ -245,7 +242,7 @@ fn run_env(args: impl uucore::Args) -> Result<(), i32> { } // set specified env vars - for &(ref name, ref val) in &opts.sets { + for &(name, val) in &opts.sets { // FIXME: set_var() panics if name is an empty string env::set_var(name, val); } @@ -256,13 +253,10 @@ fn run_env(args: impl uucore::Args) -> Result<(), i32> { // FIXME: this should just use execvp() (no fork()) on Unix-like systems match Command::new(&*prog).args(args).status() { - Ok(exit) => { - if !exit.success() { - return Err(exit.code().unwrap()); - } - } + Ok(exit) if !exit.success() => return Err(exit.code().unwrap()), Err(ref err) if err.kind() == io::ErrorKind::NotFound => return Err(127), Err(_) => return Err(126), + Ok(_) => (), } } else { // no program provided, so just dump all env vars to stdout diff --git a/src/uu/expand/Cargo.toml b/src/uu/expand/Cargo.toml index 4931cf53c..2119897b4 100644 --- a/src/uu/expand/Cargo.toml +++ b/src/uu/expand/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/expand.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } unicode-width = "0.1.5" uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/expand/src/expand.rs b/src/uu/expand/src/expand.rs index 08a514dbf..66c3eb259 100644 --- a/src/uu/expand/src/expand.rs +++ b/src/uu/expand/src/expand.rs @@ -15,7 +15,6 @@ extern crate uucore; use clap::{crate_version, App, Arg, ArgMatches}; use std::fs::File; use std::io::{stdin, stdout, BufRead, BufReader, BufWriter, Read, Write}; -use std::iter::repeat; use std::str::from_utf8; use unicode_width::UnicodeWidthChar; @@ -90,7 +89,7 @@ impl Options { }) .max() .unwrap(); // length of tabstops is guaranteed >= 1 - let tspaces = repeat(' ').take(nspaces).collect(); + let tspaces = " ".repeat(nspaces); let files: Vec = match matches.values_of(options::FILES) { Some(s) => s.map(|v| v.to_string()).collect(), @@ -109,10 +108,16 @@ impl Options { pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); - let matches = App::new(executable!()) + let matches = uu_app().usage(&usage[..]).get_matches_from(args); + + expand(Options::new(&matches)); + 0 +} + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) .version(crate_version!()) .about(ABOUT) - .usage(&usage[..]) .after_help(LONG_HELP) .arg( Arg::with_name(options::INITIAL) @@ -139,10 +144,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .hidden(true) .takes_value(true) ) - .get_matches_from(args); - - expand(Options::new(&matches)); - 0 } fn open(path: String) -> BufReader> { @@ -236,7 +237,7 @@ fn expand(options: Options) { // now dump out either spaces if we're expanding, or a literal tab if we're not if init || !options.iflag { - safe_unwrap!(output.write_all(&options.tspaces[..nts].as_bytes())); + safe_unwrap!(output.write_all(options.tspaces[..nts].as_bytes())); } else { safe_unwrap!(output.write_all(&buf[byte..byte + nbytes])); } diff --git a/src/uu/expr/Cargo.toml b/src/uu/expr/Cargo.toml index ed992bf71..4211a2d25 100644 --- a/src/uu/expr/Cargo.toml +++ b/src/uu/expr/Cargo.toml @@ -15,6 +15,7 @@ edition = "2018" path = "src/expr.rs" [dependencies] +clap = { version = "2.33", features = ["wrap_help"] } libc = "0.2.42" num-bigint = "0.4.0" num-traits = "0.2.14" diff --git a/src/uu/expr/src/expr.rs b/src/uu/expr/src/expr.rs index 5d63bed80..92c15565d 100644 --- a/src/uu/expr/src/expr.rs +++ b/src/uu/expr/src/expr.rs @@ -8,13 +8,20 @@ #[macro_use] extern crate uucore; +use clap::{crate_version, App, Arg}; use uucore::InvalidEncodingHandling; mod syntax_tree; mod tokens; -static NAME: &str = "expr"; -static VERSION: &str = env!("CARGO_PKG_VERSION"); +const VERSION: &str = "version"; +const HELP: &str = "help"; + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .arg(Arg::with_name(VERSION).long(VERSION)) + .arg(Arg::with_name(HELP).long(HELP)) +} pub fn uumain(args: impl uucore::Args) -> i32 { let args = args @@ -37,7 +44,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } fn process_expr(token_strings: &[String]) -> Result { - let maybe_tokens = tokens::strings_to_tokens(&token_strings); + let maybe_tokens = tokens::strings_to_tokens(token_strings); let maybe_ast = syntax_tree::tokens_to_ast(maybe_tokens); evaluate_ast(maybe_ast) } @@ -56,11 +63,7 @@ fn print_expr_error(expr_error: &str) -> ! { } fn evaluate_ast(maybe_ast: Result, String>) -> Result { - if maybe_ast.is_err() { - Err(maybe_ast.err().unwrap()) - } else { - maybe_ast.ok().unwrap().evaluate() - } + maybe_ast.and_then(|ast| ast.evaluate()) } fn maybe_handle_help_or_version(args: &[String]) -> bool { @@ -137,5 +140,5 @@ Environment variables: } fn print_version() { - println!("{} {}", NAME, VERSION); + println!("{} {}", executable!(), crate_version!()); } diff --git a/src/uu/expr/src/syntax_tree.rs b/src/uu/expr/src/syntax_tree.rs index b72d78729..ff49ea57e 100644 --- a/src/uu/expr/src/syntax_tree.rs +++ b/src/uu/expr/src/syntax_tree.rs @@ -160,10 +160,8 @@ impl AstNode { if let AstNode::Node { operands, .. } = self { let mut out = Vec::with_capacity(operands.len()); for operand in operands { - match operand.evaluate() { - Ok(value) => out.push(value), - Err(reason) => return Err(reason), - } + let value = operand.evaluate()?; + out.push(value); } Ok(out) } else { @@ -175,23 +173,14 @@ impl AstNode { pub fn tokens_to_ast( maybe_tokens: Result, String>, ) -> Result, String> { - if maybe_tokens.is_err() { - Err(maybe_tokens.err().unwrap()) - } else { - let tokens = maybe_tokens.ok().unwrap(); + maybe_tokens.and_then(|tokens| { let mut out_stack: TokenStack = Vec::new(); let mut op_stack: TokenStack = Vec::new(); for (token_idx, token) in tokens { - if let Err(reason) = - push_token_to_either_stack(token_idx, &token, &mut out_stack, &mut op_stack) - { - return Err(reason); - } - } - if let Err(reason) = move_rest_of_ops_to_out(&mut out_stack, &mut op_stack) { - return Err(reason); + push_token_to_either_stack(token_idx, &token, &mut out_stack, &mut op_stack)?; } + move_rest_of_ops_to_out(&mut out_stack, &mut op_stack)?; assert!(op_stack.is_empty()); maybe_dump_rpn(&out_stack); @@ -205,7 +194,7 @@ pub fn tokens_to_ast( maybe_dump_ast(&result); result } - } + }) } fn maybe_dump_ast(result: &Result, String>) { @@ -261,10 +250,8 @@ fn maybe_ast_node( ) -> Result, String> { let mut operands = Vec::with_capacity(arity); for _ in 0..arity { - match ast_from_rpn(rpn) { - Err(reason) => return Err(reason), - Ok(operand) => operands.push(operand), - } + let operand = ast_from_rpn(rpn)?; + operands.push(operand); } operands.reverse(); Ok(AstNode::new_node(token_idx, op_type, operands)) @@ -408,10 +395,12 @@ fn move_till_match_paren( op_stack: &mut TokenStack, ) -> Result<(), String> { loop { - match op_stack.pop() { - None => return Err("syntax error (Mismatched close-parenthesis)".to_string()), - Some((_, Token::ParOpen)) => return Ok(()), - Some(other) => out_stack.push(other), + let op = op_stack + .pop() + .ok_or_else(|| "syntax error (Mismatched close-parenthesis)".to_string())?; + match op { + (_, Token::ParOpen) => return Ok(()), + other => out_stack.push(other), } } } @@ -471,22 +460,17 @@ fn infix_operator_and(values: &[String]) -> String { fn operator_match(values: &[String]) -> Result { assert!(values.len() == 2); - let re = match Regex::with_options(&values[1], RegexOptions::REGEX_OPTION_NONE, Syntax::grep()) - { - Ok(m) => m, - Err(err) => return Err(err.description().to_string()), - }; - if re.captures_len() > 0 { - Ok(match re.captures(&values[0]) { - Some(captures) => captures.at(1).unwrap().to_string(), - None => "".to_string(), - }) + let re = Regex::with_options(&values[1], RegexOptions::REGEX_OPTION_NONE, Syntax::grep()) + .map_err(|err| err.description().to_string())?; + Ok(if re.captures_len() > 0 { + re.captures(&values[0]) + .map(|captures| captures.at(1).unwrap()) + .unwrap_or("") + .to_string() } else { - Ok(match re.find(&values[0]) { - Some((start, end)) => (end - start).to_string(), - None => "0".to_string(), - }) - } + re.find(&values[0]) + .map_or("0".to_string(), |(start, end)| (end - start).to_string()) + }) } fn prefix_operator_length(values: &[String]) -> String { diff --git a/src/uu/expr/src/tokens.rs b/src/uu/expr/src/tokens.rs index 6f2795588..748960bc3 100644 --- a/src/uu/expr/src/tokens.rs +++ b/src/uu/expr/src/tokens.rs @@ -78,27 +78,27 @@ pub fn strings_to_tokens(strings: &[String]) -> Result, Stri "(" => Token::ParOpen, ")" => Token::ParClose, - "^" => Token::new_infix_op(&s, false, 7), + "^" => Token::new_infix_op(s, false, 7), - ":" => Token::new_infix_op(&s, true, 6), + ":" => Token::new_infix_op(s, true, 6), - "*" => Token::new_infix_op(&s, true, 5), - "/" => Token::new_infix_op(&s, true, 5), - "%" => Token::new_infix_op(&s, true, 5), + "*" => Token::new_infix_op(s, true, 5), + "/" => Token::new_infix_op(s, true, 5), + "%" => Token::new_infix_op(s, true, 5), - "+" => Token::new_infix_op(&s, true, 4), - "-" => Token::new_infix_op(&s, true, 4), + "+" => Token::new_infix_op(s, true, 4), + "-" => Token::new_infix_op(s, true, 4), - "=" => Token::new_infix_op(&s, true, 3), - "!=" => Token::new_infix_op(&s, true, 3), - "<" => Token::new_infix_op(&s, true, 3), - ">" => Token::new_infix_op(&s, true, 3), - "<=" => Token::new_infix_op(&s, true, 3), - ">=" => Token::new_infix_op(&s, true, 3), + "=" => Token::new_infix_op(s, true, 3), + "!=" => Token::new_infix_op(s, true, 3), + "<" => Token::new_infix_op(s, true, 3), + ">" => Token::new_infix_op(s, true, 3), + "<=" => Token::new_infix_op(s, true, 3), + ">=" => Token::new_infix_op(s, true, 3), - "&" => Token::new_infix_op(&s, true, 2), + "&" => Token::new_infix_op(s, true, 2), - "|" => Token::new_infix_op(&s, true, 1), + "|" => Token::new_infix_op(s, true, 1), "match" => Token::PrefixOp { arity: 2, @@ -117,9 +117,9 @@ pub fn strings_to_tokens(strings: &[String]) -> Result, Stri value: s.clone(), }, - _ => Token::new_value(&s), + _ => Token::new_value(s), }; - push_token_if_not_escaped(&mut tokens_acc, tok_idx, token_if_not_escaped, &s); + push_token_if_not_escaped(&mut tokens_acc, tok_idx, token_if_not_escaped, s); tok_idx += 1; } maybe_dump_tokens_acc(&tokens_acc); diff --git a/src/uu/factor/Cargo.toml b/src/uu/factor/Cargo.toml index eb977760f..c9cfe78ab 100644 --- a/src/uu/factor/Cargo.toml +++ b/src/uu/factor/Cargo.toml @@ -21,7 +21,7 @@ rand = { version = "0.7", features = ["small_rng"] } smallvec = { version = "0.6.14, < 1.0" } uucore = { version = ">=0.0.8", package = "uucore", path = "../../uucore" } uucore_procs = { version = ">=0.0.5", package = "uucore_procs", path = "../../uucore_procs" } -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } [dev-dependencies] paste = "0.1.18" diff --git a/src/uu/factor/src/cli.rs b/src/uu/factor/src/cli.rs index af5e3cdb0..0f5d21362 100644 --- a/src/uu/factor/src/cli.rs +++ b/src/uu/factor/src/cli.rs @@ -36,11 +36,7 @@ fn print_factors_str(num_str: &str, w: &mut impl io::Write) -> Result<(), Box i32 { - let matches = App::new(executable!()) - .version(crate_version!()) - .about(SUMMARY) - .arg(Arg::with_name(options::NUMBER).multiple(true)) - .get_matches_from(args); + let matches = uu_app().get_matches_from(args); let stdout = stdout(); let mut w = io::BufWriter::new(stdout.lock()); @@ -68,3 +64,10 @@ pub fn uumain(args: impl uucore::Args) -> i32 { 0 } + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about(SUMMARY) + .arg(Arg::with_name(options::NUMBER).multiple(true)) +} diff --git a/src/uu/false/Cargo.toml b/src/uu/false/Cargo.toml index d7cbcd13a..93913b7e2 100644 --- a/src/uu/false/Cargo.toml +++ b/src/uu/false/Cargo.toml @@ -15,6 +15,7 @@ edition = "2018" path = "src/false.rs" [dependencies] +clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/false/src/false.rs b/src/uu/false/src/false.rs index 917c43fa0..17c681129 100644 --- a/src/uu/false/src/false.rs +++ b/src/uu/false/src/false.rs @@ -5,6 +5,14 @@ // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -pub fn uumain(_: impl uucore::Args) -> i32 { +use clap::App; +use uucore::executable; + +pub fn uumain(args: impl uucore::Args) -> i32 { + uu_app().get_matches_from(args); 1 } + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) +} diff --git a/src/uu/fmt/Cargo.toml b/src/uu/fmt/Cargo.toml index 24ee13b35..fdb1f8ca4 100644 --- a/src/uu/fmt/Cargo.toml +++ b/src/uu/fmt/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/fmt.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } libc = "0.2.42" unicode-width = "0.1.5" uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } diff --git a/src/uu/fmt/src/fmt.rs b/src/uu/fmt/src/fmt.rs index 91f59e076..8c2c8d9d9 100644 --- a/src/uu/fmt/src/fmt.rs +++ b/src/uu/fmt/src/fmt.rs @@ -77,129 +77,7 @@ pub struct FmtOptions { pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); - let matches = App::new(executable!()) - .version(crate_version!()) - .about(ABOUT) - .usage(&usage[..]) - .arg( - Arg::with_name(OPT_CROWN_MARGIN) - .short("c") - .long(OPT_CROWN_MARGIN) - .help( - "First and second line of paragraph - may have different indentations, in which - case the first line's indentation is preserved, - and each subsequent line's indentation matches the second line.", - ), - ) - .arg( - Arg::with_name(OPT_TAGGED_PARAGRAPH) - .short("t") - .long("tagged-paragraph") - .help( - "Like -c, except that the first and second line of a paragraph *must* - have different indentation or they are treated as separate paragraphs.", - ), - ) - .arg( - Arg::with_name(OPT_PRESERVE_HEADERS) - .short("m") - .long("preserve-headers") - .help( - "Attempt to detect and preserve mail headers in the input. - Be careful when combining this flag with -p.", - ), - ) - .arg( - Arg::with_name(OPT_SPLIT_ONLY) - .short("s") - .long("split-only") - .help("Split lines only, do not reflow."), - ) - .arg( - Arg::with_name(OPT_UNIFORM_SPACING) - .short("u") - .long("uniform-spacing") - .help( - "Insert exactly one - space between words, and two between sentences. - Sentence breaks in the input are detected as [?!.] - followed by two spaces or a newline; other punctuation - is not interpreted as a sentence break.", - ), - ) - .arg( - Arg::with_name(OPT_PREFIX) - .short("p") - .long("prefix") - .help( - "Reformat only lines - beginning with PREFIX, reattaching PREFIX to reformatted lines. - Unless -x is specified, leading whitespace will be ignored - when matching PREFIX.", - ) - .value_name("PREFIX"), - ) - .arg( - Arg::with_name(OPT_SKIP_PREFIX) - .short("P") - .long("skip-prefix") - .help( - "Do not reformat lines - beginning with PSKIP. Unless -X is specified, leading whitespace - will be ignored when matching PSKIP", - ) - .value_name("PSKIP"), - ) - .arg( - Arg::with_name(OPT_EXACT_PREFIX) - .short("x") - .long("exact-prefix") - .help( - "PREFIX must match at the - beginning of the line with no preceding whitespace.", - ), - ) - .arg( - Arg::with_name(OPT_EXACT_SKIP_PREFIX) - .short("X") - .long("exact-skip-prefix") - .help( - "PSKIP must match at the - beginning of the line with no preceding whitespace.", - ), - ) - .arg( - Arg::with_name(OPT_WIDTH) - .short("w") - .long("width") - .help("Fill output lines up to a maximum of WIDTH columns, default 79.") - .value_name("WIDTH"), - ) - .arg( - Arg::with_name(OPT_GOAL) - .short("g") - .long("goal") - .help("Goal width, default ~0.94*WIDTH. Must be less than WIDTH.") - .value_name("GOAL"), - ) - .arg(Arg::with_name(OPT_QUICK).short("q").long("quick").help( - "Break lines more quickly at the - expense of a potentially more ragged appearance.", - )) - .arg( - Arg::with_name(OPT_TAB_WIDTH) - .short("T") - .long("tab-width") - .help( - "Treat tabs as TABWIDTH spaces for - determining line length, default 8. Note that this is used only for - calculating line lengths; tabs are preserved in the output.", - ) - .value_name("TABWIDTH"), - ) - .arg(Arg::with_name(ARG_FILES).multiple(true).takes_value(true)) - .get_matches_from(args); + let matches = uu_app().usage(&usage[..]).get_matches_from(args); let mut files: Vec = matches .values_of(ARG_FILES) @@ -331,3 +209,127 @@ pub fn uumain(args: impl uucore::Args) -> i32 { 0 } + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about(ABOUT) + .arg( + Arg::with_name(OPT_CROWN_MARGIN) + .short("c") + .long(OPT_CROWN_MARGIN) + .help( + "First and second line of paragraph \ + may have different indentations, in which \ + case the first line's indentation is preserved, \ + and each subsequent line's indentation matches the second line.", + ), + ) + .arg( + Arg::with_name(OPT_TAGGED_PARAGRAPH) + .short("t") + .long("tagged-paragraph") + .help( + "Like -c, except that the first and second line of a paragraph *must* \ + have different indentation or they are treated as separate paragraphs.", + ), + ) + .arg( + Arg::with_name(OPT_PRESERVE_HEADERS) + .short("m") + .long("preserve-headers") + .help( + "Attempt to detect and preserve mail headers in the input. \ + Be careful when combining this flag with -p.", + ), + ) + .arg( + Arg::with_name(OPT_SPLIT_ONLY) + .short("s") + .long("split-only") + .help("Split lines only, do not reflow."), + ) + .arg( + Arg::with_name(OPT_UNIFORM_SPACING) + .short("u") + .long("uniform-spacing") + .help( + "Insert exactly one \ + space between words, and two between sentences. \ + Sentence breaks in the input are detected as [?!.] \ + followed by two spaces or a newline; other punctuation \ + is not interpreted as a sentence break.", + ), + ) + .arg( + Arg::with_name(OPT_PREFIX) + .short("p") + .long("prefix") + .help( + "Reformat only lines \ + beginning with PREFIX, reattaching PREFIX to reformatted lines. \ + Unless -x is specified, leading whitespace will be ignored \ + when matching PREFIX.", + ) + .value_name("PREFIX"), + ) + .arg( + Arg::with_name(OPT_SKIP_PREFIX) + .short("P") + .long("skip-prefix") + .help( + "Do not reformat lines \ + beginning with PSKIP. Unless -X is specified, leading whitespace \ + will be ignored when matching PSKIP", + ) + .value_name("PSKIP"), + ) + .arg( + Arg::with_name(OPT_EXACT_PREFIX) + .short("x") + .long("exact-prefix") + .help( + "PREFIX must match at the \ + beginning of the line with no preceding whitespace.", + ), + ) + .arg( + Arg::with_name(OPT_EXACT_SKIP_PREFIX) + .short("X") + .long("exact-skip-prefix") + .help( + "PSKIP must match at the \ + beginning of the line with no preceding whitespace.", + ), + ) + .arg( + Arg::with_name(OPT_WIDTH) + .short("w") + .long("width") + .help("Fill output lines up to a maximum of WIDTH columns, default 79.") + .value_name("WIDTH"), + ) + .arg( + Arg::with_name(OPT_GOAL) + .short("g") + .long("goal") + .help("Goal width, default ~0.94*WIDTH. Must be less than WIDTH.") + .value_name("GOAL"), + ) + .arg(Arg::with_name(OPT_QUICK).short("q").long("quick").help( + "Break lines more quickly at the \ + expense of a potentially more ragged appearance.", + )) + .arg( + Arg::with_name(OPT_TAB_WIDTH) + .short("T") + .long("tab-width") + .help( + "Treat tabs as TABWIDTH spaces for \ + determining line length, default 8. Note that this is used only for \ + calculating line lengths; tabs are preserved in the output.", + ) + .value_name("TABWIDTH"), + ) + .arg(Arg::with_name(ARG_FILES).multiple(true).takes_value(true)) +} diff --git a/src/uu/fold/Cargo.toml b/src/uu/fold/Cargo.toml index c5578384e..50ed34388 100644 --- a/src/uu/fold/Cargo.toml +++ b/src/uu/fold/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/fold.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/fold/src/fold.rs b/src/uu/fold/src/fold.rs index e476fed5b..1dbc8cdc7 100644 --- a/src/uu/fold/src/fold.rs +++ b/src/uu/fold/src/fold.rs @@ -36,7 +36,35 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .accept_any(); let (args, obs_width) = handle_obsolete(&args[..]); - let matches = App::new(executable!()) + let matches = uu_app().get_matches_from(args); + + let bytes = matches.is_present(options::BYTES); + let spaces = matches.is_present(options::SPACES); + let poss_width = match matches.value_of(options::WIDTH) { + Some(v) => Some(v.to_owned()), + None => obs_width, + }; + + let width = match poss_width { + Some(inp_width) => match inp_width.parse::() { + Ok(width) => width, + Err(e) => crash!(1, "illegal width value (\"{}\"): {}", inp_width, e), + }, + None => 80, + }; + + let files = match matches.values_of(options::FILE) { + Some(v) => v.map(|v| v.to_owned()).collect(), + None => vec!["-".to_owned()], + }; + + fold(files, bytes, spaces, width); + + 0 +} + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) .name(NAME) .version(crate_version!()) .usage(SYNTAX) @@ -68,37 +96,12 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .takes_value(true), ) .arg(Arg::with_name(options::FILE).hidden(true).multiple(true)) - .get_matches_from(args); - - let bytes = matches.is_present(options::BYTES); - let spaces = matches.is_present(options::SPACES); - let poss_width = match matches.value_of(options::WIDTH) { - Some(v) => Some(v.to_owned()), - None => obs_width, - }; - - let width = match poss_width { - Some(inp_width) => match inp_width.parse::() { - Ok(width) => width, - Err(e) => crash!(1, "illegal width value (\"{}\"): {}", inp_width, e), - }, - None => 80, - }; - - let files = match matches.values_of(options::FILE) { - Some(v) => v.map(|v| v.to_owned()).collect(), - None => vec!["-".to_owned()], - }; - - fold(files, bytes, spaces, width); - - 0 } fn handle_obsolete(args: &[String]) -> (Vec, Option) { for (i, arg) in args.iter().enumerate() { let slice = &arg; - if slice.starts_with('-') && slice.len() > 1 && slice.chars().nth(1).unwrap().is_digit(10) { + if slice.starts_with('-') && slice.chars().nth(1).map_or(false, |c| c.is_digit(10)) { let mut v = args.to_vec(); v.remove(i); return (v, Some(slice[1..].to_owned())); @@ -109,7 +112,7 @@ fn handle_obsolete(args: &[String]) -> (Vec, Option) { fn fold(filenames: Vec, bytes: bool, spaces: bool, width: usize) { for filename in &filenames { - let filename: &str = &filename; + let filename: &str = filename; let mut stdin_buf; let mut file_buf; let buffer = BufReader::new(if filename == "-" { diff --git a/src/uu/groups/Cargo.toml b/src/uu/groups/Cargo.toml index 1a56bc2ab..14ee44d18 100644 --- a/src/uu/groups/Cargo.toml +++ b/src/uu/groups/Cargo.toml @@ -15,9 +15,9 @@ edition = "2018" path = "src/groups.rs" [dependencies] -uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["entries"] } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["entries", "process"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } [[bin]] name = "groups" diff --git a/src/uu/groups/src/groups.rs b/src/uu/groups/src/groups.rs index 5b9cd948a..a40d1a490 100644 --- a/src/uu/groups/src/groups.rs +++ b/src/uu/groups/src/groups.rs @@ -5,59 +5,93 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +// +// ============================================================================ +// Test suite summary for GNU coreutils 8.32.162-4eda +// ============================================================================ +// PASS: tests/misc/groups-dash.sh +// PASS: tests/misc/groups-process-all.sh +// PASS: tests/misc/groups-version.sh // spell-checker:ignore (ToDO) passwd #[macro_use] extern crate uucore; -use uucore::entries::{get_groups, gid2grp, Locate, Passwd}; +use uucore::entries::{get_groups_gnu, gid2grp, Locate, Passwd}; use clap::{crate_version, App, Arg}; -static ABOUT: &str = "display current group names"; -static OPT_USER: &str = "user"; +mod options { + pub const USERS: &str = "USERNAME"; +} +static ABOUT: &str = "Print group memberships for each USERNAME or, \ + if no USERNAME is specified, for\nthe current process \ + (which may differ if the groups data‐base has changed)."; fn get_usage() -> String { - format!("{0} [USERNAME]", executable!()) + format!("{0} [OPTION]... [USERNAME]...", executable!()) } pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); - let matches = App::new(executable!()) - .version(crate_version!()) - .about(ABOUT) - .usage(&usage[..]) - .arg(Arg::with_name(OPT_USER)) - .get_matches_from(args); + let matches = uu_app().usage(&usage[..]).get_matches_from(args); - match matches.value_of(OPT_USER) { - None => { + let users: Vec = matches + .values_of(options::USERS) + .map(|v| v.map(ToString::to_string).collect()) + .unwrap_or_default(); + + let mut exit_code = 0; + + if users.is_empty() { + println!( + "{}", + get_groups_gnu(None) + .unwrap() + .iter() + .map(|&gid| gid2grp(gid).unwrap_or_else(|_| { + show_error!("cannot find name for group ID {}", gid); + exit_code = 1; + gid.to_string() + })) + .collect::>() + .join(" ") + ); + return exit_code; + } + + for user in users { + if let Ok(p) = Passwd::locate(user.as_str()) { println!( - "{}", - get_groups() - .unwrap() + "{} : {}", + user, + p.belongs_to() .iter() - .map(|&g| gid2grp(g).unwrap()) + .map(|&gid| gid2grp(gid).unwrap_or_else(|_| { + show_error!("cannot find name for group ID {}", gid); + exit_code = 1; + gid.to_string() + })) .collect::>() .join(" ") ); - 0 - } - Some(user) => { - if let Ok(p) = Passwd::locate(user) { - println!( - "{}", - p.belongs_to() - .iter() - .map(|&g| gid2grp(g).unwrap()) - .collect::>() - .join(" ") - ); - 0 - } else { - crash!(1, "unknown user {}", user); - } + } else { + show_error!("'{}': no such user", user); + exit_code = 1; } } + exit_code +} + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about(ABOUT) + .arg( + Arg::with_name(options::USERS) + .multiple(true) + .takes_value(true) + .value_name(options::USERS), + ) } diff --git a/src/uu/hashsum/BENCHMARKING.md b/src/uu/hashsum/BENCHMARKING.md new file mode 100644 index 000000000..cef710a19 --- /dev/null +++ b/src/uu/hashsum/BENCHMARKING.md @@ -0,0 +1,9 @@ +## Benchmarking hashsum + +### To bench blake2 + +Taken from: https://github.com/uutils/coreutils/pull/2296 + +With a large file: +$ hyperfine "./target/release/coreutils hashsum --b2sum large-file" "b2sum large-file" + diff --git a/src/uu/hashsum/Cargo.toml b/src/uu/hashsum/Cargo.toml index 04a22cac7..87a2b8aa1 100644 --- a/src/uu/hashsum/Cargo.toml +++ b/src/uu/hashsum/Cargo.toml @@ -16,7 +16,7 @@ path = "src/hashsum.rs" [dependencies] digest = "0.6.2" -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } hex = "0.2.0" libc = "0.2.42" md5 = "0.3.5" @@ -25,7 +25,7 @@ regex-syntax = "0.6.7" sha1 = "0.6.0" sha2 = "0.6.0" sha3 = "0.6.0" -blake2-rfc = "0.2.18" +blake2b_simd = "0.5.11" uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/hashsum/src/digest.rs b/src/uu/hashsum/src/digest.rs index 218de0a36..9093d94a7 100644 --- a/src/uu/hashsum/src/digest.rs +++ b/src/uu/hashsum/src/digest.rs @@ -1,4 +1,3 @@ -extern crate blake2_rfc; extern crate digest; extern crate md5; extern crate sha1; @@ -49,9 +48,9 @@ impl Digest for md5::Context { } } -impl Digest for blake2_rfc::blake2b::Blake2b { +impl Digest for blake2b_simd::State { fn new() -> Self { - blake2_rfc::blake2b::Blake2b::new(64) + Self::new() } fn input(&mut self, input: &[u8]) { @@ -59,12 +58,12 @@ impl Digest for blake2_rfc::blake2b::Blake2b { } fn result(&mut self, out: &mut [u8]) { - let hash_result = &self.clone().finalize(); - out.copy_from_slice(&hash_result.as_bytes()); + let hash_result = &self.finalize(); + out.copy_from_slice(hash_result.as_bytes()); } fn reset(&mut self) { - *self = blake2_rfc::blake2b::Blake2b::new(64); + *self = Self::new(); } fn output_bits(&self) -> usize { diff --git a/src/uu/hashsum/src/hashsum.rs b/src/uu/hashsum/src/hashsum.rs index b39b5788c..d9feb6648 100644 --- a/src/uu/hashsum/src/hashsum.rs +++ b/src/uu/hashsum/src/hashsum.rs @@ -19,7 +19,6 @@ mod digest; use self::digest::Digest; -use blake2_rfc::blake2b::Blake2b; use clap::{App, Arg, ArgMatches}; use hex::ToHex; use md5::Context as Md5; @@ -85,9 +84,13 @@ fn detect_algo<'a>( "sha256sum" => ("SHA256", Box::new(Sha256::new()) as Box, 256), "sha384sum" => ("SHA384", Box::new(Sha384::new()) as Box, 384), "sha512sum" => ("SHA512", Box::new(Sha512::new()) as Box, 512), - "b2sum" => ("BLAKE2", Box::new(Blake2b::new(64)) as Box, 512), + "b2sum" => ( + "BLAKE2", + Box::new(blake2b_simd::State::new()) as Box, + 512, + ), "sha3sum" => match matches.value_of("bits") { - Some(bits_str) => match (&bits_str).parse::() { + Some(bits_str) => match (bits_str).parse::() { Ok(224) => ( "SHA3-224", Box::new(Sha3_224::new()) as Box, @@ -137,7 +140,7 @@ fn detect_algo<'a>( 512, ), "shake128sum" => match matches.value_of("bits") { - Some(bits_str) => match (&bits_str).parse::() { + Some(bits_str) => match (bits_str).parse::() { Ok(bits) => ( "SHAKE128", Box::new(Shake128::new()) as Box, @@ -148,7 +151,7 @@ fn detect_algo<'a>( None => crash!(1, "--bits required for SHAKE-128"), }, "shake256sum" => match matches.value_of("bits") { - Some(bits_str) => match (&bits_str).parse::() { + Some(bits_str) => match (bits_str).parse::() { Ok(bits) => ( "SHAKE256", Box::new(Shake256::new()) as Box, @@ -187,11 +190,11 @@ fn detect_algo<'a>( set_or_crash("SHA512", Box::new(Sha512::new()), 512) } if matches.is_present("b2sum") { - set_or_crash("BLAKE2", Box::new(Blake2b::new(64)), 512) + set_or_crash("BLAKE2", Box::new(blake2b_simd::State::new()), 512) } if matches.is_present("sha3") { match matches.value_of("bits") { - Some(bits_str) => match (&bits_str).parse::() { + Some(bits_str) => match (bits_str).parse::() { Ok(224) => set_or_crash( "SHA3-224", Box::new(Sha3_224::new()) as Box, @@ -235,7 +238,7 @@ fn detect_algo<'a>( } if matches.is_present("shake128") { match matches.value_of("bits") { - Some(bits_str) => match (&bits_str).parse::() { + Some(bits_str) => match (bits_str).parse::() { Ok(bits) => set_or_crash("SHAKE128", Box::new(Shake128::new()), bits), Err(err) => crash!(1, "{}", err), }, @@ -244,7 +247,7 @@ fn detect_algo<'a>( } if matches.is_present("shake256") { match matches.value_of("bits") { - Some(bits_str) => match (&bits_str).parse::() { + Some(bits_str) => match (bits_str).parse::() { Ok(bits) => set_or_crash("SHAKE256", Box::new(Shake256::new()), bits), Err(err) => crash!(1, "{}", err), }, @@ -252,10 +255,8 @@ fn detect_algo<'a>( } } } - if alg.is_none() { - crash!(1, "You must specify hash algorithm!") - }; - (name, alg.unwrap(), output_bits) + let alg = alg.unwrap_or_else(|| crash!(1, "You must specify hash algorithm!")); + (name, alg, output_bits) } } } @@ -284,119 +285,7 @@ pub fn uumain(mut args: impl uucore::Args) -> i32 { // Default binary in Windows, text mode otherwise let binary_flag_default = cfg!(windows); - let binary_help = format!( - "read in binary mode{}", - if binary_flag_default { - " (default)" - } else { - "" - } - ); - - let text_help = format!( - "read in text mode{}", - if binary_flag_default { - "" - } else { - " (default)" - } - ); - - let mut app = App::new(executable!()) - .version(crate_version!()) - .about("Compute and check message digests.") - .arg( - Arg::with_name("binary") - .short("b") - .long("binary") - .help(&binary_help), - ) - .arg( - Arg::with_name("check") - .short("c") - .long("check") - .help("read hashsums from the FILEs and check them"), - ) - .arg( - Arg::with_name("tag") - .long("tag") - .help("create a BSD-style checksum"), - ) - .arg( - Arg::with_name("text") - .short("t") - .long("text") - .help(&text_help) - .conflicts_with("binary"), - ) - .arg( - Arg::with_name("quiet") - .short("q") - .long("quiet") - .help("don't print OK for each successfully verified file"), - ) - .arg( - Arg::with_name("status") - .short("s") - .long("status") - .help("don't output anything, status code shows success"), - ) - .arg( - Arg::with_name("strict") - .long("strict") - .help("exit non-zero for improperly formatted checksum lines"), - ) - .arg( - Arg::with_name("warn") - .short("w") - .long("warn") - .help("warn about improperly formatted checksum lines"), - ) - // Needed for variable-length output sums (e.g. SHAKE) - .arg( - Arg::with_name("bits") - .long("bits") - .help("set the size of the output (only for SHAKE)") - .takes_value(true) - .value_name("BITS") - // XXX: should we actually use validators? they're not particularly efficient - .validator(is_valid_bit_num), - ) - .arg( - Arg::with_name("FILE") - .index(1) - .multiple(true) - .value_name("FILE"), - ); - - if !is_custom_binary(&binary_name) { - let algorithms = &[ - ("md5", "work with MD5"), - ("sha1", "work with SHA1"), - ("sha224", "work with SHA224"), - ("sha256", "work with SHA256"), - ("sha384", "work with SHA384"), - ("sha512", "work with SHA512"), - ("sha3", "work with SHA3"), - ("sha3-224", "work with SHA3-224"), - ("sha3-256", "work with SHA3-256"), - ("sha3-384", "work with SHA3-384"), - ("sha3-512", "work with SHA3-512"), - ( - "shake128", - "work with SHAKE128 using BITS for the output size", - ), - ( - "shake256", - "work with SHAKE256 using BITS for the output size", - ), - ("b2sum", "work with BLAKE2"), - ]; - - for (name, desc) in algorithms { - app = app.arg(Arg::with_name(name).long(name).help(desc)); - } - } + let app = uu_app(&binary_name); // FIXME: this should use get_matches_from_safe() and crash!(), but at the moment that just // causes "error: " to be printed twice (once from crash!() and once from clap). With @@ -444,6 +333,124 @@ pub fn uumain(mut args: impl uucore::Args) -> i32 { } } +pub fn uu_app_common() -> App<'static, 'static> { + #[cfg(windows)] + const BINARY_HELP: &str = "read in binary mode (default)"; + #[cfg(not(windows))] + const BINARY_HELP: &str = "read in binary mode"; + #[cfg(windows)] + const TEXT_HELP: &str = "read in text mode"; + #[cfg(not(windows))] + const TEXT_HELP: &str = "read in text mode (default)"; + App::new(executable!()) + .version(crate_version!()) + .about("Compute and check message digests.") + .arg( + Arg::with_name("binary") + .short("b") + .long("binary") + .help(BINARY_HELP), + ) + .arg( + Arg::with_name("check") + .short("c") + .long("check") + .help("read hashsums from the FILEs and check them"), + ) + .arg( + Arg::with_name("tag") + .long("tag") + .help("create a BSD-style checksum"), + ) + .arg( + Arg::with_name("text") + .short("t") + .long("text") + .help(TEXT_HELP) + .conflicts_with("binary"), + ) + .arg( + Arg::with_name("quiet") + .short("q") + .long("quiet") + .help("don't print OK for each successfully verified file"), + ) + .arg( + Arg::with_name("status") + .short("s") + .long("status") + .help("don't output anything, status code shows success"), + ) + .arg( + Arg::with_name("strict") + .long("strict") + .help("exit non-zero for improperly formatted checksum lines"), + ) + .arg( + Arg::with_name("warn") + .short("w") + .long("warn") + .help("warn about improperly formatted checksum lines"), + ) + // Needed for variable-length output sums (e.g. SHAKE) + .arg( + Arg::with_name("bits") + .long("bits") + .help("set the size of the output (only for SHAKE)") + .takes_value(true) + .value_name("BITS") + // XXX: should we actually use validators? they're not particularly efficient + .validator(is_valid_bit_num), + ) + .arg( + Arg::with_name("FILE") + .index(1) + .multiple(true) + .value_name("FILE"), + ) +} + +pub fn uu_app_custom() -> App<'static, 'static> { + let mut app = uu_app_common(); + let algorithms = &[ + ("md5", "work with MD5"), + ("sha1", "work with SHA1"), + ("sha224", "work with SHA224"), + ("sha256", "work with SHA256"), + ("sha384", "work with SHA384"), + ("sha512", "work with SHA512"), + ("sha3", "work with SHA3"), + ("sha3-224", "work with SHA3-224"), + ("sha3-256", "work with SHA3-256"), + ("sha3-384", "work with SHA3-384"), + ("sha3-512", "work with SHA3-512"), + ( + "shake128", + "work with SHAKE128 using BITS for the output size", + ), + ( + "shake256", + "work with SHAKE256 using BITS for the output size", + ), + ("b2sum", "work with BLAKE2"), + ]; + + for (name, desc) in algorithms { + app = app.arg(Arg::with_name(name).long(name).help(desc)); + } + app +} + +// hashsum is handled differently in build.rs, therefore this is not the same +// as in other utilities. +fn uu_app(binary_name: &str) -> App<'static, 'static> { + if !is_custom_binary(binary_name) { + uu_app_custom() + } else { + uu_app_common() + } +} + #[allow(clippy::cognitive_complexity)] fn hashsum<'a, I>(mut options: Options, files: I) -> Result<(), i32> where diff --git a/src/uu/head/Cargo.toml b/src/uu/head/Cargo.toml index 661052f58..a0f1f9d95 100644 --- a/src/uu/head/Cargo.toml +++ b/src/uu/head/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/head.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["ringbuffer"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/head/src/head.rs b/src/uu/head/src/head.rs index 28710e1fe..e17e17034 100644 --- a/src/uu/head/src/head.rs +++ b/src/uu/head/src/head.rs @@ -1,3 +1,8 @@ +// * 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) zlines use clap::{crate_version, App, Arg}; @@ -35,7 +40,7 @@ mod take; use lines::zlines; use take::take_all_but; -fn app<'a>() -> App<'a, 'a> { +pub fn uu_app() -> App<'static, 'static> { App::new(executable!()) .version(crate_version!()) .about(ABOUT) @@ -75,7 +80,7 @@ fn app<'a>() -> App<'a, 'a> { .arg( Arg::with_name(options::QUIET_NAME) .short("q") - .long("--quiet") + .long("quiet") .visible_alias("silent") .help("never print headers giving file names") .overrides_with_all(&[options::VERBOSE_NAME, options::QUIET_NAME]), @@ -108,12 +113,7 @@ where { match parse::parse_num(src) { Ok((n, last)) => Ok((closure(n), last)), - Err(reason) => match reason { - parse::ParseError::Syntax => Err(format!("'{}'", src)), - parse::ParseError::Overflow => { - Err(format!("'{}': Value too large for defined datatype", src)) - } - }, + Err(e) => Err(e.to_string()), } } @@ -167,7 +167,7 @@ impl HeadOptions { ///Construct options from matches pub fn get_from(args: impl uucore::Args) -> Result { - let matches = app().get_matches_from(arg_iterate(args)?); + let matches = uu_app().get_matches_from(arg_iterate(args)?); let mut options = HeadOptions::new(); @@ -176,19 +176,11 @@ impl HeadOptions { options.zeroed = matches.is_present(options::ZERO_NAME); let mode_and_from_end = if let Some(v) = matches.value_of(options::BYTES_NAME) { - match parse_mode(v, Modes::Bytes) { - Ok(v) => v, - Err(err) => { - return Err(format!("invalid number of bytes: {}", err)); - } - } + parse_mode(v, Modes::Bytes) + .map_err(|err| format!("invalid number of bytes: {}", err))? } else if let Some(v) = matches.value_of(options::LINES_NAME) { - match parse_mode(v, Modes::Lines) { - Ok(v) => v, - Err(err) => { - return Err(format!("invalid number of lines: {}", err)); - } - } + parse_mode(v, Modes::Lines) + .map_err(|err| format!("invalid number of lines: {}", err))? } else { (Modes::Lines(10), false) }; @@ -474,7 +466,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let args = match HeadOptions::get_from(args) { Ok(o) => o, Err(s) => { - crash!(EXIT_FAILURE, "head: {}", s); + crash!(EXIT_FAILURE, "{}", s); } }; match uu_head(&args) { diff --git a/src/uu/head/src/parse.rs b/src/uu/head/src/parse.rs index f1c97561d..f6f291814 100644 --- a/src/uu/head/src/parse.rs +++ b/src/uu/head/src/parse.rs @@ -1,5 +1,10 @@ -use std::convert::TryFrom; +// * 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::OsString; +use uucore::parse_size::{parse_size, ParseSizeError}; #[derive(PartialEq, Debug)] pub enum ParseError { @@ -92,92 +97,25 @@ pub fn parse_obsolete(src: &str) -> Option } /// Parses an -c or -n argument, /// the bool specifies whether to read from the end -pub fn parse_num(src: &str) -> Result<(usize, bool), ParseError> { - let mut num_start = 0; - let mut chars = src.char_indices(); - let (mut chars, all_but_last) = match chars.next() { - Some((_, c)) => { +pub fn parse_num(src: &str) -> Result<(usize, bool), ParseSizeError> { + let mut size_string = src.trim(); + let mut all_but_last = false; + + if let Some(c) = size_string.chars().next() { + if c == '+' || c == '-' { + // head: '+' is not documented (8.32 man pages) + size_string = &size_string[1..]; if c == '-' { - num_start += 1; - (chars, true) - } else { - (src.char_indices(), false) + all_but_last = true; } } - None => return Err(ParseError::Syntax), - }; - let mut num_end = 0usize; - let mut last_char = 0 as char; - let mut num_count = 0usize; - for (n, c) in &mut chars { - if c.is_numeric() { - num_end = n; - num_count += 1; - } else { - last_char = c; - break; - } + } else { + return Err(ParseSizeError::ParseFailure(src.to_string())); } - let num = if num_count > 0 { - match src[num_start..=num_end].parse::() { - Ok(n) => Some(n), - Err(_) => return Err(ParseError::Overflow), - } - } else { - None - }; - - if last_char == 0 as char { - if let Some(n) = num { - Ok((n, all_but_last)) - } else { - Err(ParseError::Syntax) - } - } else { - let base: u128 = match chars.next() { - Some((_, c)) => { - let b = match c { - 'B' if last_char != 'b' => 1000, - 'i' if last_char != 'b' => { - if let Some((_, 'B')) = chars.next() { - 1024 - } else { - return Err(ParseError::Syntax); - } - } - _ => return Err(ParseError::Syntax), - }; - if chars.next().is_some() { - return Err(ParseError::Syntax); - } else { - b - } - } - None => 1024, - }; - let mul = match last_char.to_lowercase().next().unwrap() { - 'b' => 512, - 'k' => base.pow(1), - 'm' => base.pow(2), - 'g' => base.pow(3), - 't' => base.pow(4), - 'p' => base.pow(5), - 'e' => base.pow(6), - 'z' => base.pow(7), - 'y' => base.pow(8), - _ => return Err(ParseError::Syntax), - }; - let mul = match usize::try_from(mul) { - Ok(n) => n, - Err(_) => return Err(ParseError::Overflow), - }; - match num.unwrap_or(1).checked_mul(mul) { - Some(n) => Ok((n, all_but_last)), - None => Err(ParseError::Overflow), - } - } + parse_size(size_string).map(|n| (n, all_but_last)) } + #[cfg(test)] mod tests { use super::*; @@ -195,44 +133,6 @@ mod tests { Some(Ok(src.iter().map(|s| s.to_string()).collect())) } #[test] - #[cfg(not(target_pointer_width = "128"))] - fn test_parse_overflow_x64() { - assert_eq!(parse_num("1Y"), Err(ParseError::Overflow)); - assert_eq!(parse_num("1Z"), Err(ParseError::Overflow)); - assert_eq!(parse_num("100E"), Err(ParseError::Overflow)); - assert_eq!(parse_num("100000P"), Err(ParseError::Overflow)); - assert_eq!(parse_num("1000000000T"), Err(ParseError::Overflow)); - assert_eq!( - parse_num("10000000000000000000000"), - Err(ParseError::Overflow) - ); - } - #[test] - #[cfg(target_pointer_width = "32")] - fn test_parse_overflow_x32() { - assert_eq!(parse_num("1T"), Err(ParseError::Overflow)); - assert_eq!(parse_num("1000G"), Err(ParseError::Overflow)); - } - #[test] - fn test_parse_bad_syntax() { - assert_eq!(parse_num("5MiB nonsense"), Err(ParseError::Syntax)); - assert_eq!(parse_num("Nonsense string"), Err(ParseError::Syntax)); - assert_eq!(parse_num("5mib"), Err(ParseError::Syntax)); - assert_eq!(parse_num("biB"), Err(ParseError::Syntax)); - assert_eq!(parse_num("-"), Err(ParseError::Syntax)); - assert_eq!(parse_num(""), Err(ParseError::Syntax)); - } - #[test] - fn test_parse_numbers() { - assert_eq!(parse_num("k"), Ok((1024, false))); - assert_eq!(parse_num("MiB"), Ok((1024 * 1024, false))); - assert_eq!(parse_num("-5"), Ok((5, true))); - assert_eq!(parse_num("b"), Ok((512, false))); - assert_eq!(parse_num("-2GiB"), Ok((2 * 1024 * 1024 * 1024, true))); - assert_eq!(parse_num("5M"), Ok((5 * 1024 * 1024, false))); - assert_eq!(parse_num("5MB"), Ok((5 * 1000 * 1000, false))); - } - #[test] fn test_parse_numbers_obsolete() { assert_eq!(obsolete("-5"), obsolete_result(&["-n", "5"])); assert_eq!(obsolete("-100"), obsolete_result(&["-n", "100"])); diff --git a/src/uu/hostid/Cargo.toml b/src/uu/hostid/Cargo.toml index ab6954104..95e20db68 100644 --- a/src/uu/hostid/Cargo.toml +++ b/src/uu/hostid/Cargo.toml @@ -15,6 +15,7 @@ edition = "2018" path = "src/hostid.rs" [dependencies] +clap = { version = "2.33", features = ["wrap_help"] } libc = "0.2.42" uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/hostid/src/hostid.rs b/src/uu/hostid/src/hostid.rs index 551866521..e9fc08379 100644 --- a/src/uu/hostid/src/hostid.rs +++ b/src/uu/hostid/src/hostid.rs @@ -10,12 +10,10 @@ #[macro_use] extern crate uucore; +use clap::{crate_version, App}; use libc::c_long; -use uucore::InvalidEncodingHandling; static SYNTAX: &str = "[options]"; -static SUMMARY: &str = ""; -static LONG_HELP: &str = ""; // currently rust libc interface doesn't include gethostid extern "C" { @@ -23,14 +21,17 @@ extern "C" { } pub fn uumain(args: impl uucore::Args) -> i32 { - app!(SYNTAX, SUMMARY, LONG_HELP).parse( - args.collect_str(InvalidEncodingHandling::ConvertLossy) - .accept_any(), - ); + uu_app().get_matches_from(args); hostid(); 0 } +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .usage(SYNTAX) +} + fn hostid() { /* * POSIX says gethostid returns a "32-bit identifier" but is silent diff --git a/src/uu/hostname/Cargo.toml b/src/uu/hostname/Cargo.toml index fb1d00682..e4d78441c 100644 --- a/src/uu/hostname/Cargo.toml +++ b/src/uu/hostname/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/hostname.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } libc = "0.2.42" hostname = { version = "0.3", features = ["set"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["wide"] } diff --git a/src/uu/hostname/src/hostname.rs b/src/uu/hostname/src/hostname.rs index ff312fb58..fe477d7b5 100644 --- a/src/uu/hostname/src/hostname.rs +++ b/src/uu/hostname/src/hostname.rs @@ -52,10 +52,25 @@ fn get_usage() -> String { } fn execute(args: impl uucore::Args) -> i32 { let usage = get_usage(); - let matches = App::new(executable!()) + let matches = uu_app().usage(&usage[..]).get_matches_from(args); + + match matches.value_of(OPT_HOST) { + None => display_hostname(&matches), + Some(host) => { + if let Err(err) = hostname::set(host) { + show_error!("{}", err); + 1 + } else { + 0 + } + } + } +} + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) .version(crate_version!()) .about(ABOUT) - .usage(&usage[..]) .arg( Arg::with_name(OPT_DOMAIN) .short("d") @@ -80,19 +95,6 @@ fn execute(args: impl uucore::Args) -> i32 { possible", )) .arg(Arg::with_name(OPT_HOST)) - .get_matches_from(args); - - match matches.value_of(OPT_HOST) { - None => display_hostname(&matches), - Some(host) => { - if let Err(err) = hostname::set(host) { - show_error!("{}", err); - 1 - } else { - 0 - } - } - } } fn display_hostname(matches: &ArgMatches) -> i32 { diff --git a/src/uu/id/Cargo.toml b/src/uu/id/Cargo.toml index 308d6089d..97f478ef9 100644 --- a/src/uu/id/Cargo.toml +++ b/src/uu/id/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/id.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["entries", "process"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/id/src/id.rs b/src/uu/id/src/id.rs index 77b185f24..d5acc97f3 100644 --- a/src/uu/id/src/id.rs +++ b/src/uu/id/src/id.rs @@ -6,11 +6,27 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. // -// Synced with: +// This was originally based on BSD's `id` +// (noticeable in functionality, usage text, options text, etc.) +// and synced with: // http://ftp-archive.freebsd.org/mirror/FreeBSD-Archive/old-releases/i386/1.0-RELEASE/ports/shellutils/src/id.c // http://www.opensource.apple.com/source/shell_cmds/shell_cmds-118/id/id.c +// +// * This was partially rewritten in order for stdout/stderr/exit_code +// to be conform with GNU coreutils (8.32) test suite for `id`. +// +// * This supports multiple users (a feature that was introduced in coreutils 8.31) +// +// * This passes GNU's coreutils Test suite (8.32) +// for "tests/id/uid.sh" and "tests/id/zero/sh". +// +// * Option '--zero' does not exist for BSD's `id`, therefore '--zero' is only +// allowed together with other options that are available on GNU's `id`. +// +// * Help text based on BSD's `id` manpage and GNU's `id` manpage. +// -// spell-checker:ignore (ToDO) asid auditid auditinfo auid cstr egid emod euid getaudit getlogin gflag nflag pline rflag termid uflag +// spell-checker:ignore (ToDO) asid auditid auditinfo auid cstr egid emod euid getaudit getlogin gflag nflag pline rflag termid uflag gsflag zflag #![allow(non_camel_case_types)] #![allow(dead_code)] @@ -31,210 +47,346 @@ macro_rules! cstr2cow { }; } -#[cfg(not(target_os = "linux"))] -mod audit { - use super::libc::{c_int, c_uint, dev_t, pid_t, uid_t}; +static ABOUT: &str = "Print user and group information for each specified USER, +or (when USER omitted) for the current user."; - pub type au_id_t = uid_t; - pub type au_asid_t = pid_t; - pub type au_event_t = c_uint; - pub type au_emod_t = c_uint; - pub type au_class_t = c_int; - pub type au_flag_t = u64; - - #[repr(C)] - pub struct au_mask { - pub am_success: c_uint, - pub am_failure: c_uint, - } - pub type au_mask_t = au_mask; - - #[repr(C)] - pub struct au_tid_addr { - pub port: dev_t, - } - pub type au_tid_addr_t = au_tid_addr; - - #[repr(C)] - pub struct c_auditinfo_addr { - pub ai_auid: au_id_t, // Audit user ID - pub ai_mask: au_mask_t, // Audit masks. - pub ai_termid: au_tid_addr_t, // Terminal ID. - pub ai_asid: au_asid_t, // Audit session ID. - pub ai_flags: au_flag_t, // Audit session flags - } - pub type c_auditinfo_addr_t = c_auditinfo_addr; - - extern "C" { - pub fn getaudit(auditinfo_addr: *mut c_auditinfo_addr_t) -> c_int; - } +mod options { + pub const OPT_AUDIT: &str = "audit"; // GNU's id does not have this + pub const OPT_CONTEXT: &str = "context"; + pub const OPT_EFFECTIVE_USER: &str = "user"; + pub const OPT_GROUP: &str = "group"; + pub const OPT_GROUPS: &str = "groups"; + pub const OPT_HUMAN_READABLE: &str = "human-readable"; // GNU's id does not have this + pub const OPT_NAME: &str = "name"; + pub const OPT_PASSWORD: &str = "password"; // GNU's id does not have this + pub const OPT_REAL_ID: &str = "real"; + pub const OPT_ZERO: &str = "zero"; // BSD's id does not have this + pub const ARG_USERS: &str = "USER"; } -static ABOUT: &str = "Display user and group information for the specified USER,\n or (when USER omitted) for the current user."; - -static OPT_AUDIT: &str = "audit"; -static OPT_EFFECTIVE_USER: &str = "effective-user"; -static OPT_GROUP: &str = "group"; -static OPT_GROUPS: &str = "groups"; -static OPT_HUMAN_READABLE: &str = "human-readable"; -static OPT_NAME: &str = "name"; -static OPT_PASSWORD: &str = "password"; -static OPT_REAL_ID: &str = "real-id"; - -static ARG_USERS: &str = "users"; - fn get_usage() -> String { - format!("{0} [OPTION]... [USER]", executable!()) + format!("{0} [OPTION]... [USER]...", executable!()) +} + +fn get_description() -> String { + String::from( + "The id utility displays the user and group names and numeric IDs, of the \ + calling process, to the standard output. If the real and effective IDs are \ + different, both are displayed, otherwise only the real ID is displayed.\n\n\ + If a user (login name or user ID) is specified, the user and group IDs of \ + that user are displayed. In this case, the real and effective IDs are \ + assumed to be the same.", + ) +} + +struct Ids { + uid: u32, // user id + gid: u32, // group id + euid: u32, // effective uid + egid: u32, // effective gid +} + +struct State { + nflag: bool, // --name + uflag: bool, // --user + gflag: bool, // --group + gsflag: bool, // --groups + rflag: bool, // --real + zflag: bool, // --zero + ids: Option, + // The behavior for calling GNU's `id` and calling GNU's `id $USER` is similar but different. + // * The SELinux context is only displayed without a specified user. + // * The `getgroups` system call is only used without a specified user, this causes + // the order of the displayed groups to be different between `id` and `id $USER`. + // + // Example: + // $ strace -e getgroups id -G $USER + // 1000 10 975 968 + // +++ exited with 0 +++ + // $ strace -e getgroups id -G + // getgroups(0, NULL) = 4 + // getgroups(4, [10, 968, 975, 1000]) = 4 + // 1000 10 968 975 + // +++ exited with 0 +++ + user_specified: bool, } pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); + let after_help = get_description(); - let matches = App::new(executable!()) - .version(crate_version!()) - .about(ABOUT) + let matches = uu_app() .usage(&usage[..]) - .arg( - Arg::with_name(OPT_AUDIT) - .short("A") - .help("Display the process audit (not available on Linux)"), - ) - .arg( - Arg::with_name(OPT_EFFECTIVE_USER) - .short("u") - .long("user") - .help("Display the effective user ID as a number"), - ) - .arg( - Arg::with_name(OPT_GROUP) - .short("g") - .long(OPT_GROUP) - .help("Display the effective group ID as a number"), - ) - .arg( - Arg::with_name(OPT_GROUPS) - .short("G") - .long(OPT_GROUPS) - .help("Display the different group IDs"), - ) - .arg( - Arg::with_name(OPT_HUMAN_READABLE) - .short("p") - .help("Make the output human-readable"), - ) - .arg( - Arg::with_name(OPT_NAME) - .short("n") - .help("Display the name of the user or group ID for the -G, -g and -u options"), - ) - .arg( - Arg::with_name(OPT_PASSWORD) - .short("P") - .help("Display the id as a password file entry"), - ) - .arg( - Arg::with_name(OPT_REAL_ID) - .short("r") - .help("Display the real ID for the -g and -u options"), - ) - .arg(Arg::with_name(ARG_USERS).multiple(true).takes_value(true)) + .after_help(&after_help[..]) .get_matches_from(args); let users: Vec = matches - .values_of(ARG_USERS) + .values_of(options::ARG_USERS) .map(|v| v.map(ToString::to_string).collect()) .unwrap_or_default(); - if matches.is_present(OPT_AUDIT) { - auditid(); - return 0; + let mut state = State { + nflag: matches.is_present(options::OPT_NAME), + uflag: matches.is_present(options::OPT_EFFECTIVE_USER), + gflag: matches.is_present(options::OPT_GROUP), + gsflag: matches.is_present(options::OPT_GROUPS), + rflag: matches.is_present(options::OPT_REAL_ID), + zflag: matches.is_present(options::OPT_ZERO), + user_specified: !users.is_empty(), + ids: None, + }; + + let default_format = { + // "default format" is when none of '-ugG' was used + !(state.uflag || state.gflag || state.gsflag) + }; + + if (state.nflag || state.rflag) && default_format { + crash!(1, "cannot print only names or real IDs in default format"); + } + if (state.zflag) && default_format { + // NOTE: GNU test suite "id/zero.sh" needs this stderr output: + crash!(1, "option --zero not permitted in default format"); } - let possible_pw = if users.is_empty() { - None - } else { - match Passwd::locate(users[0].as_str()) { - Ok(p) => Some(p), - Err(_) => crash!(1, "No such user/group: {}", users[0]), + let delimiter = { + if state.zflag { + "\0".to_string() + } else { + " ".to_string() } }; - - let nflag = matches.is_present(OPT_NAME); - let uflag = matches.is_present(OPT_EFFECTIVE_USER); - let gflag = matches.is_present(OPT_GROUP); - let rflag = matches.is_present(OPT_REAL_ID); - - if gflag { - let id = possible_pw - .map(|p| p.gid()) - .unwrap_or(if rflag { getgid() } else { getegid() }); - println!( - "{}", - if nflag { - entries::gid2grp(id).unwrap_or_else(|_| id.to_string()) - } else { - id.to_string() - } - ); - return 0; - } - - if uflag { - let id = possible_pw - .map(|p| p.uid()) - .unwrap_or(if rflag { getuid() } else { geteuid() }); - println!( - "{}", - if nflag { - entries::uid2usr(id).unwrap_or_else(|_| id.to_string()) - } else { - id.to_string() - } - ); - return 0; - } - - if matches.is_present(OPT_GROUPS) { - println!( - "{}", - if nflag { - possible_pw - .map(|p| p.belongs_to()) - .unwrap_or_else(|| entries::get_groups().unwrap()) - .iter() - .map(|&id| entries::gid2grp(id).unwrap()) - .collect::>() - .join(" ") - } else { - possible_pw - .map(|p| p.belongs_to()) - .unwrap_or_else(|| entries::get_groups().unwrap()) - .iter() - .map(|&id| id.to_string()) - .collect::>() - .join(" ") - } - ); - return 0; - } - - if matches.is_present(OPT_PASSWORD) { - pline(possible_pw.map(|v| v.uid())); - return 0; + let line_ending = { + if state.zflag { + '\0' + } else { + '\n' + } }; + let mut exit_code = 0; - if matches.is_present(OPT_HUMAN_READABLE) { - pretty(possible_pw); - return 0; + for i in 0..=users.len() { + let possible_pw = if !state.user_specified { + None + } else { + match Passwd::locate(users[i].as_str()) { + Ok(p) => Some(p), + Err(_) => { + show_error!("'{}': no such user", users[i]); + exit_code = 1; + if i + 1 >= users.len() { + break; + } else { + continue; + } + } + } + }; + + // GNU's `id` does not support the flags: -p/-P/-A. + if matches.is_present(options::OPT_PASSWORD) { + // BSD's `id` ignores all but the first specified user + pline(possible_pw.map(|v| v.uid())); + return exit_code; + }; + if matches.is_present(options::OPT_HUMAN_READABLE) { + // BSD's `id` ignores all but the first specified user + pretty(possible_pw); + return exit_code; + } + if matches.is_present(options::OPT_AUDIT) { + // BSD's `id` ignores specified users + auditid(); + return exit_code; + } + + let (uid, gid) = possible_pw.map(|p| (p.uid(), p.gid())).unwrap_or(( + if state.rflag { getuid() } else { geteuid() }, + if state.rflag { getgid() } else { getegid() }, + )); + state.ids = Some(Ids { + uid, + gid, + euid: geteuid(), + egid: getegid(), + }); + + if state.gflag { + print!( + "{}", + if state.nflag { + entries::gid2grp(gid).unwrap_or_else(|_| { + show_error!("cannot find name for group ID {}", gid); + exit_code = 1; + gid.to_string() + }) + } else { + gid.to_string() + } + ); + } + + if state.uflag { + print!( + "{}", + if state.nflag { + entries::uid2usr(uid).unwrap_or_else(|_| { + show_error!("cannot find name for user ID {}", uid); + exit_code = 1; + uid.to_string() + }) + } else { + uid.to_string() + } + ); + } + + let groups = entries::get_groups_gnu(Some(gid)).unwrap(); + let groups = if state.user_specified { + possible_pw.map(|p| p.belongs_to()).unwrap() + } else { + groups.clone() + }; + + if state.gsflag { + print!( + "{}{}", + groups + .iter() + .map(|&id| { + if state.nflag { + entries::gid2grp(id).unwrap_or_else(|_| { + show_error!("cannot find name for group ID {}", id); + exit_code = 1; + id.to_string() + }) + } else { + id.to_string() + } + }) + .collect::>() + .join(&delimiter), + // NOTE: this is necessary to pass GNU's "tests/id/zero.sh": + if state.zflag && state.user_specified && users.len() > 1 { + "\0" + } else { + "" + } + ); + } + + if default_format { + id_print(&state, groups); + } + print!("{}", line_ending); + + if i + 1 >= users.len() { + break; + } } - if possible_pw.is_some() { - id_print(possible_pw, false, false) - } else { - id_print(possible_pw, true, true) - } + exit_code +} - 0 +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about(ABOUT) + .arg( + Arg::with_name(options::OPT_AUDIT) + .short("A") + .conflicts_with_all(&[ + options::OPT_GROUP, + options::OPT_EFFECTIVE_USER, + options::OPT_HUMAN_READABLE, + options::OPT_PASSWORD, + options::OPT_GROUPS, + options::OPT_ZERO, + ]) + .help( + "Display the process audit user ID and other process audit properties,\n\ + which requires privilege (not available on Linux).", + ), + ) + .arg( + Arg::with_name(options::OPT_EFFECTIVE_USER) + .short("u") + .long(options::OPT_EFFECTIVE_USER) + .conflicts_with(options::OPT_GROUP) + .help("Display only the effective user ID as a number."), + ) + .arg( + Arg::with_name(options::OPT_GROUP) + .short("g") + .long(options::OPT_GROUP) + .help("Display only the effective group ID as a number"), + ) + .arg( + Arg::with_name(options::OPT_GROUPS) + .short("G") + .long(options::OPT_GROUPS) + .conflicts_with_all(&[ + options::OPT_GROUP, + options::OPT_EFFECTIVE_USER, + options::OPT_HUMAN_READABLE, + options::OPT_PASSWORD, + options::OPT_AUDIT, + ]) + .help( + "Display only the different group IDs as white-space separated numbers, \ + in no particular order.", + ), + ) + .arg( + Arg::with_name(options::OPT_HUMAN_READABLE) + .short("p") + .help("Make the output human-readable. Each display is on a separate line."), + ) + .arg( + Arg::with_name(options::OPT_NAME) + .short("n") + .long(options::OPT_NAME) + .help( + "Display the name of the user or group ID for the -G, -g and -u options \ + instead of the number.\nIf any of the ID numbers cannot be mapped into \ + names, the number will be displayed as usual.", + ), + ) + .arg( + Arg::with_name(options::OPT_PASSWORD) + .short("P") + .help("Display the id as a password file entry."), + ) + .arg( + Arg::with_name(options::OPT_REAL_ID) + .short("r") + .long(options::OPT_REAL_ID) + .help( + "Display the real ID for the -G, -g and -u options instead of \ + the effective ID.", + ), + ) + .arg( + Arg::with_name(options::OPT_ZERO) + .short("z") + .long(options::OPT_ZERO) + .help( + "delimit entries with NUL characters, not whitespace;\n\ + not permitted in default format", + ), + ) + .arg( + Arg::with_name(options::OPT_CONTEXT) + .short("Z") + .long(options::OPT_CONTEXT) + .help("NotImplemented: print only the security context of the process"), + ) + .arg( + Arg::with_name(options::ARG_USERS) + .multiple(true) + .takes_value(true) + .value_name(options::ARG_USERS), + ) } fn pretty(possible_pw: Option) { @@ -280,7 +432,7 @@ fn pretty(possible_pw: Option) { println!( "groups\t{}", - entries::get_groups() + entries::get_groups_gnu(None) .unwrap() .iter() .map(|&gr| entries::gid2grp(gr).unwrap()) @@ -347,30 +499,21 @@ fn auditid() { println!("asid={}", auditinfo.ai_asid); } -fn id_print(possible_pw: Option, p_euid: bool, p_egid: bool) { - let (uid, gid) = possible_pw - .map(|p| (p.uid(), p.gid())) - .unwrap_or((getuid(), getgid())); - - let groups = match Passwd::locate(uid) { - Ok(p) => p.belongs_to(), - Err(e) => crash!(1, "Could not find uid {}: {}", uid, e), - }; +fn id_print(state: &State, groups: Vec) { + let uid = state.ids.as_ref().unwrap().uid; + let gid = state.ids.as_ref().unwrap().gid; + let euid = state.ids.as_ref().unwrap().euid; + let egid = state.ids.as_ref().unwrap().egid; print!("uid={}({})", uid, entries::uid2usr(uid).unwrap()); print!(" gid={}({})", gid, entries::gid2grp(gid).unwrap()); - - let euid = geteuid(); - if p_euid && (euid != uid) { + if !state.user_specified && (euid != uid) { print!(" euid={}({})", euid, entries::uid2usr(euid).unwrap()); } - - let egid = getegid(); - if p_egid && (egid != gid) { + if !state.user_specified && (egid != gid) { print!(" egid={}({})", euid, entries::gid2grp(egid).unwrap()); } - - println!( + print!( " groups={}", groups .iter() @@ -378,4 +521,49 @@ fn id_print(possible_pw: Option, p_euid: bool, p_egid: bool) { .collect::>() .join(",") ); + + // NOTE: (SELinux NotImplemented) placeholder: + // if !state.user_specified { + // // print SElinux context (does not depend on "-Z") + // print!(" context={}", get_selinux_contexts().join(":")); + // } +} + +#[cfg(not(target_os = "linux"))] +mod audit { + use super::libc::{c_int, c_uint, dev_t, pid_t, uid_t}; + + pub type au_id_t = uid_t; + pub type au_asid_t = pid_t; + pub type au_event_t = c_uint; + pub type au_emod_t = c_uint; + pub type au_class_t = c_int; + pub type au_flag_t = u64; + + #[repr(C)] + pub struct au_mask { + pub am_success: c_uint, + pub am_failure: c_uint, + } + pub type au_mask_t = au_mask; + + #[repr(C)] + pub struct au_tid_addr { + pub port: dev_t, + } + pub type au_tid_addr_t = au_tid_addr; + + #[repr(C)] + pub struct c_auditinfo_addr { + pub ai_auid: au_id_t, // Audit user ID + pub ai_mask: au_mask_t, // Audit masks. + pub ai_termid: au_tid_addr_t, // Terminal ID. + pub ai_asid: au_asid_t, // Audit session ID. + pub ai_flags: au_flag_t, // Audit session flags + } + pub type c_auditinfo_addr_t = c_auditinfo_addr; + + extern "C" { + pub fn getaudit(auditinfo_addr: *mut c_auditinfo_addr_t) -> c_int; + } } diff --git a/src/uu/install/Cargo.toml b/src/uu/install/Cargo.toml index 91463199a..5beef2b29 100644 --- a/src/uu/install/Cargo.toml +++ b/src/uu/install/Cargo.toml @@ -18,7 +18,7 @@ edition = "2018" path = "src/install.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } filetime = "0.2" file_diff = "1.0.0" libc = ">= 0.2" diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index 7aa6f95ff..bd227da56 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -15,6 +15,7 @@ extern crate uucore; use clap::{crate_version, App, Arg, ArgMatches}; use file_diff::diff; use filetime::{set_file_times, FileTime}; +use uucore::backup_control::{self, BackupMode}; use uucore::entries::{grp2gid, usr2uid}; use uucore::perms::{wrap_chgrp, wrap_chown, Verbosity}; @@ -33,6 +34,7 @@ const DEFAULT_STRIP_PROGRAM: &str = "strip"; pub struct Behavior { main_function: MainFunction, specified_mode: Option, + backup_mode: BackupMode, suffix: String, owner: String, group: String, @@ -42,6 +44,7 @@ pub struct Behavior { strip: bool, strip_program: String, create_leading: bool, + target_dir: Option, } #[derive(Clone, Eq, PartialEq)] @@ -67,7 +70,7 @@ static ABOUT: &str = "Copy SOURCE to DEST or multiple SOURCE(s) to the existing static OPT_COMPARE: &str = "compare"; static OPT_BACKUP: &str = "backup"; -static OPT_BACKUP_2: &str = "backup2"; +static OPT_BACKUP_NO_ARG: &str = "backup2"; static OPT_DIRECTORY: &str = "directory"; static OPT_IGNORED: &str = "ignored"; static OPT_CREATE_LEADING: &str = "create-leading"; @@ -97,21 +100,49 @@ fn get_usage() -> String { pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); - let matches = App::new(executable!()) + let matches = uu_app().usage(&usage[..]).get_matches_from(args); + + let paths: Vec = matches + .values_of(ARG_FILES) + .map(|v| v.map(ToString::to_string).collect()) + .unwrap_or_default(); + + if let Err(s) = check_unimplemented(&matches) { + show_error!("Unimplemented feature: {}", s); + return 2; + } + + let behavior = match behavior(&matches) { + Ok(x) => x, + Err(ret) => { + return ret; + } + }; + + match behavior.main_function { + MainFunction::Directory => directory(paths, behavior), + MainFunction::Standard => standard(paths, behavior), + } +} + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) .version(crate_version!()) .about(ABOUT) - .usage(&usage[..]) .arg( Arg::with_name(OPT_BACKUP) .long(OPT_BACKUP) - .help("(unimplemented) make a backup of each existing destination file") + .help("make a backup of each existing destination file") + .takes_value(true) + .require_equals(true) + .min_values(0) .value_name("CONTROL") ) .arg( // TODO implement flag - Arg::with_name(OPT_BACKUP_2) + Arg::with_name(OPT_BACKUP_NO_ARG) .short("b") - .help("(unimplemented) like --backup but does not accept an argument") + .help("like --backup but does not accept an argument") ) .arg( Arg::with_name(OPT_IGNORED) @@ -184,7 +215,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { Arg::with_name(OPT_SUFFIX) .short("S") .long(OPT_SUFFIX) - .help("(unimplemented) override the usual backup suffix") + .help("override the usual backup suffix") .value_name("SUFFIX") .takes_value(true) .min_values(1) @@ -194,7 +225,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { Arg::with_name(OPT_TARGET_DIRECTORY) .short("t") .long(OPT_TARGET_DIRECTORY) - .help("(unimplemented) move all SOURCE arguments into DIRECTORY") + .help("move all SOURCE arguments into DIRECTORY") .value_name("DIRECTORY") ) .arg( @@ -227,29 +258,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .value_name("CONTEXT") ) .arg(Arg::with_name(ARG_FILES).multiple(true).takes_value(true).min_values(1)) - .get_matches_from(args); - - let paths: Vec = matches - .values_of(ARG_FILES) - .map(|v| v.map(ToString::to_string).collect()) - .unwrap_or_default(); - - if let Err(s) = check_unimplemented(&matches) { - show_error!("Unimplemented feature: {}", s); - return 2; - } - - let behavior = match behavior(&matches) { - Ok(x) => x, - Err(ret) => { - return ret; - } - }; - - match behavior.main_function { - MainFunction::Directory => directory(paths, behavior), - MainFunction::Standard => standard(paths, behavior), - } } /// Check for unimplemented command line arguments. @@ -262,15 +270,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { /// /// fn check_unimplemented<'a>(matches: &ArgMatches) -> Result<(), &'a str> { - if matches.is_present(OPT_BACKUP) { - Err("--backup") - } else if matches.is_present(OPT_BACKUP_2) { - Err("-b") - } else if matches.is_present(OPT_SUFFIX) { - Err("--suffix, -S") - } else if matches.is_present(OPT_TARGET_DIRECTORY) { - Err("--target-directory, -t") - } else if matches.is_present(OPT_NO_TARGET_DIRECTORY) { + if matches.is_present(OPT_NO_TARGET_DIRECTORY) { Err("--no-target-directory, -T") } else if matches.is_present(OPT_PRESERVE_CONTEXT) { Err("--preserve-context, -P") @@ -299,37 +299,25 @@ fn behavior(matches: &ArgMatches) -> Result { let considering_dir: bool = MainFunction::Directory == main_function; let specified_mode: Option = if matches.is_present(OPT_MODE) { - match matches.value_of(OPT_MODE) { - Some(x) => match mode::parse(x, considering_dir) { - Ok(y) => Some(y), - Err(err) => { - show_error!("Invalid mode string: {}", err); - return Err(1); - } - }, - None => { - return Err(1); - } - } + let x = matches.value_of(OPT_MODE).ok_or(1)?; + Some(mode::parse(x, considering_dir).map_err(|err| { + show_error!("Invalid mode string: {}", err); + 1 + })?) } else { None }; - let backup_suffix = if matches.is_present(OPT_SUFFIX) { - match matches.value_of(OPT_SUFFIX) { - Some(x) => x, - None => { - return Err(1); - } - } - } else { - "~" - }; + let target_dir = matches.value_of(OPT_TARGET_DIRECTORY).map(|d| d.to_owned()); Ok(Behavior { main_function, specified_mode, - suffix: backup_suffix.to_string(), + backup_mode: backup_control::determine_backup_mode( + matches.is_present(OPT_BACKUP_NO_ARG) || matches.is_present(OPT_BACKUP), + matches.value_of(OPT_BACKUP), + ), + suffix: backup_control::determine_backup_suffix(matches.value_of(OPT_SUFFIX)), owner: matches.value_of(OPT_OWNER).unwrap_or("").to_string(), group: matches.value_of(OPT_GROUP).unwrap_or("").to_string(), verbose: matches.is_present(OPT_VERBOSE), @@ -342,6 +330,7 @@ fn behavior(matches: &ArgMatches) -> Result { .unwrap_or(DEFAULT_STRIP_PROGRAM), ), create_leading: matches.is_present(OPT_CREATE_LEADING), + target_dir, }) } @@ -379,7 +368,7 @@ fn directory(paths: Vec, b: Behavior) -> i32 { } } - if mode::chmod(&path, b.mode()).is_err() { + if mode::chmod(path, b.mode()).is_err() { all_successful = false; continue; } @@ -404,16 +393,17 @@ fn is_new_file_path(path: &Path) -> bool { /// /// Returns an integer intended as a program return code. /// -fn standard(paths: Vec, b: Behavior) -> i32 { - let sources = &paths[0..paths.len() - 1] - .iter() - .map(PathBuf::from) - .collect::>(); +fn standard(mut paths: Vec, b: Behavior) -> i32 { + let target: PathBuf = b + .target_dir + .clone() + .unwrap_or_else(|| paths.pop().unwrap()) + .into(); - let target = Path::new(paths.last().unwrap()); + let sources = &paths.iter().map(PathBuf::from).collect::>(); if sources.len() > 1 || (target.exists() && target.is_dir()) { - copy_files_into_dir(sources, &target.to_path_buf(), &b) + copy_files_into_dir(sources, &target, &b) } else { if let Some(parent) = target.parent() { if !parent.exists() && b.create_leading { @@ -422,15 +412,15 @@ fn standard(paths: Vec, b: Behavior) -> i32 { return 1; } - if mode::chmod(&parent, b.mode()).is_err() { + if mode::chmod(parent, b.mode()).is_err() { show_error!("failed to chmod {}", parent.display()); return 1; } } } - if target.is_file() || is_new_file_path(target) { - copy_file_to_file(&sources[0], &target.to_path_buf(), &b) + if target.is_file() || is_new_file_path(&target) { + copy_file_to_file(&sources[0], &target, &b) } else { show_error!( "invalid target {}: No such file or directory", @@ -501,7 +491,7 @@ fn copy_files_into_dir(files: &[PathBuf], target_dir: &Path, b: &Behavior) -> i3 /// _target_ must be a non-directory /// fn copy_file_to_file(file: &Path, target: &Path, b: &Behavior) -> i32 { - if copy(file, &target, b).is_err() { + if copy(file, target, b).is_err() { 1 } else { 0 @@ -524,6 +514,28 @@ fn copy(from: &Path, to: &Path, b: &Behavior) -> Result<(), ()> { if b.compare && !need_copy(from, to, b) { return Ok(()); } + // Declare the path here as we may need it for the verbose output below. + let mut backup_path = None; + + // Perform backup, if any, before overwriting 'to' + // + // The codes actually making use of the backup process don't seem to agree + // on how best to approach the issue. (mv and ln, for example) + if to.exists() { + backup_path = backup_control::get_backup_path(b.backup_mode, to, &b.suffix); + if let Some(ref backup_path) = backup_path { + // TODO!! + if let Err(err) = fs::rename(to, backup_path) { + show_error!( + "install: cannot backup file '{}' to '{}': {}", + to.display(), + backup_path.display(), + err + ); + return Err(()); + } + } + } if from.to_string_lossy() == "/dev/null" { /* workaround a limitation of fs::copy @@ -563,7 +575,7 @@ fn copy(from: &Path, to: &Path, b: &Behavior) -> Result<(), ()> { } } - if mode::chmod(&to, b.mode()).is_err() { + if mode::chmod(to, b.mode()).is_err() { return Err(()); } @@ -631,7 +643,11 @@ fn copy(from: &Path, to: &Path, b: &Behavior) -> Result<(), ()> { } if b.verbose { - show_error!("'{}' -> '{}'", from.display(), to.display()); + print!("'{}' -> '{}'", from.display(), to.display()); + match backup_path { + Some(path) => println!(" (backup: '{}')", path.display()), + None => println!(), + } } Ok(()) diff --git a/src/uu/join/Cargo.toml b/src/uu/join/Cargo.toml index 9371b7601..21284a6c3 100644 --- a/src/uu/join/Cargo.toml +++ b/src/uu/join/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/join.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/join/src/join.rs b/src/uu/join/src/join.rs index 7a044789f..60721f212 100644 --- a/src/uu/join/src/join.rs +++ b/src/uu/join/src/join.rs @@ -328,8 +328,8 @@ impl<'a> State<'a> { }); } else { repr.print_field(key); - repr.print_fields(&line1, self.key, self.max_fields); - repr.print_fields(&line2, other.key, other.max_fields); + repr.print_fields(line1, self.key, self.max_fields); + repr.print_fields(line2, other.key, other.max_fields); } println!(); @@ -442,7 +442,72 @@ impl<'a> State<'a> { } pub fn uumain(args: impl uucore::Args) -> i32 { - let matches = App::new(NAME) + let matches = uu_app().get_matches_from(args); + + let keys = parse_field_number_option(matches.value_of("j")); + let key1 = parse_field_number_option(matches.value_of("1")); + let key2 = parse_field_number_option(matches.value_of("2")); + + let mut settings: Settings = Default::default(); + + if let Some(value) = matches.value_of("v") { + settings.print_unpaired = parse_file_number(value); + settings.print_joined = false; + } else if let Some(value) = matches.value_of("a") { + settings.print_unpaired = parse_file_number(value); + } + + settings.ignore_case = matches.is_present("i"); + settings.key1 = get_field_number(keys, key1); + settings.key2 = get_field_number(keys, key2); + + if let Some(value) = matches.value_of("t") { + settings.separator = match value.len() { + 0 => Sep::Line, + 1 => Sep::Char(value.chars().next().unwrap()), + _ => crash!(1, "multi-character tab {}", value), + }; + } + + if let Some(format) = matches.value_of("o") { + if format == "auto" { + settings.autoformat = true; + } else { + settings.format = format + .split(|c| c == ' ' || c == ',' || c == '\t') + .map(Spec::parse) + .collect(); + } + } + + if let Some(empty) = matches.value_of("e") { + settings.empty = empty.to_string(); + } + + if matches.is_present("nocheck-order") { + settings.check_order = CheckOrder::Disabled; + } + + if matches.is_present("check-order") { + settings.check_order = CheckOrder::Enabled; + } + + if matches.is_present("header") { + settings.headers = true; + } + + let file1 = matches.value_of("file1").unwrap(); + let file2 = matches.value_of("file2").unwrap(); + + if file1 == "-" && file2 == "-" { + crash!(1, "both files cannot be standard input"); + } + + exec(file1, file2, &settings) +} + +pub fn uu_app() -> App<'static, 'static> { + App::new(NAME) .version(crate_version!()) .about( "For each pair of input lines with identical join fields, write a line to @@ -542,68 +607,6 @@ FILENUM is 1 or 2, corresponding to FILE1 or FILE2", .value_name("FILE2") .hidden(true), ) - .get_matches_from(args); - - let keys = parse_field_number_option(matches.value_of("j")); - let key1 = parse_field_number_option(matches.value_of("1")); - let key2 = parse_field_number_option(matches.value_of("2")); - - let mut settings: Settings = Default::default(); - - if let Some(value) = matches.value_of("v") { - settings.print_unpaired = parse_file_number(value); - settings.print_joined = false; - } else if let Some(value) = matches.value_of("a") { - settings.print_unpaired = parse_file_number(value); - } - - settings.ignore_case = matches.is_present("i"); - settings.key1 = get_field_number(keys, key1); - settings.key2 = get_field_number(keys, key2); - - if let Some(value) = matches.value_of("t") { - settings.separator = match value.len() { - 0 => Sep::Line, - 1 => Sep::Char(value.chars().next().unwrap()), - _ => crash!(1, "multi-character tab {}", value), - }; - } - - if let Some(format) = matches.value_of("o") { - if format == "auto" { - settings.autoformat = true; - } else { - settings.format = format - .split(|c| c == ' ' || c == ',' || c == '\t') - .map(Spec::parse) - .collect(); - } - } - - if let Some(empty) = matches.value_of("e") { - settings.empty = empty.to_string(); - } - - if matches.is_present("nocheck-order") { - settings.check_order = CheckOrder::Disabled; - } - - if matches.is_present("check-order") { - settings.check_order = CheckOrder::Enabled; - } - - if matches.is_present("header") { - settings.headers = true; - } - - let file1 = matches.value_of("file1").unwrap(); - let file2 = matches.value_of("file2").unwrap(); - - if file1 == "-" && file2 == "-" { - crash!(1, "both files cannot be standard input"); - } - - exec(file1, file2, &settings) } fn exec(file1: &str, file2: &str, settings: &Settings) -> i32 { @@ -611,7 +614,7 @@ fn exec(file1: &str, file2: &str, settings: &Settings) -> i32 { let mut state1 = State::new( FileNum::File1, - &file1, + file1, &stdin, settings.key1, settings.print_unpaired, @@ -619,7 +622,7 @@ fn exec(file1: &str, file2: &str, settings: &Settings) -> i32 { let mut state2 = State::new( FileNum::File2, - &file2, + file2, &stdin, settings.key2, settings.print_unpaired, diff --git a/src/uu/kill/Cargo.toml b/src/uu/kill/Cargo.toml index e33411c70..c3a5368d9 100644 --- a/src/uu/kill/Cargo.toml +++ b/src/uu/kill/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/kill.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } libc = "0.2.42" uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["signals"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/kill/src/kill.rs b/src/uu/kill/src/kill.rs index 6c2464c92..92868efdb 100644 --- a/src/uu/kill/src/kill.rs +++ b/src/uu/kill/src/kill.rs @@ -43,38 +43,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let (args, obs_signal) = handle_obsolete(args); let usage = format!("{} [OPTIONS]... PID...", executable!()); - let matches = App::new(executable!()) - .version(crate_version!()) - .about(ABOUT) - .usage(&usage[..]) - .arg( - Arg::with_name(options::LIST) - .short("l") - .long(options::LIST) - .help("Lists signals") - .conflicts_with(options::TABLE) - .conflicts_with(options::TABLE_OLD), - ) - .arg( - Arg::with_name(options::TABLE) - .short("t") - .long(options::TABLE) - .help("Lists table of signals"), - ) - .arg(Arg::with_name(options::TABLE_OLD).short("L").hidden(true)) - .arg( - Arg::with_name(options::SIGNAL) - .short("s") - .long(options::SIGNAL) - .help("Sends given signal") - .takes_value(true), - ) - .arg( - Arg::with_name(options::PIDS_OR_SIGNALS) - .hidden(true) - .multiple(true), - ) - .get_matches_from(args); + let matches = uu_app().usage(&usage[..]).get_matches_from(args); let mode = if matches.is_present(options::TABLE) || matches.is_present(options::TABLE_OLD) { Mode::Table @@ -106,12 +75,45 @@ pub fn uumain(args: impl uucore::Args) -> i32 { EXIT_OK } +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about(ABOUT) + .arg( + Arg::with_name(options::LIST) + .short("l") + .long(options::LIST) + .help("Lists signals") + .conflicts_with(options::TABLE) + .conflicts_with(options::TABLE_OLD), + ) + .arg( + Arg::with_name(options::TABLE) + .short("t") + .long(options::TABLE) + .help("Lists table of signals"), + ) + .arg(Arg::with_name(options::TABLE_OLD).short("L").hidden(true)) + .arg( + Arg::with_name(options::SIGNAL) + .short("s") + .long(options::SIGNAL) + .help("Sends given signal") + .takes_value(true), + ) + .arg( + Arg::with_name(options::PIDS_OR_SIGNALS) + .hidden(true) + .multiple(true), + ) +} + fn handle_obsolete(mut args: Vec) -> (Vec, Option) { let mut i = 0; while i < args.len() { // this is safe because slice is valid when it is referenced let slice = &args[i].clone(); - if slice.starts_with('-') && slice.len() > 1 && slice.chars().nth(1).unwrap().is_digit(10) { + if slice.starts_with('-') && slice.chars().nth(1).map_or(false, |c| c.is_digit(10)) { let val = &slice[1..]; match val.parse() { Ok(num) => { @@ -129,33 +131,24 @@ fn handle_obsolete(mut args: Vec) -> (Vec, Option) { } fn table() { - let mut name_width = 0; - /* Compute the maximum width of a signal name. */ - for s in &ALL_SIGNALS { - if s.name.len() > name_width { - name_width = s.name.len() - } - } + let name_width = ALL_SIGNALS.iter().map(|n| n.len()).max().unwrap(); for (idx, signal) in ALL_SIGNALS.iter().enumerate() { - print!("{0: >#2} {1: <#8}", idx + 1, signal.name); - //TODO: obtain max signal width here - + print!("{0: >#2} {1: <#2$}", idx, signal, name_width + 2); if (idx + 1) % 7 == 0 { println!(); } } + println!() } fn print_signal(signal_name_or_value: &str) { - for signal in &ALL_SIGNALS { - if signal.name == signal_name_or_value - || (format!("SIG{}", signal.name)) == signal_name_or_value - { - println!("{}", signal.value); + for (value, &signal) in ALL_SIGNALS.iter().enumerate() { + if signal == signal_name_or_value || (format!("SIG{}", signal)) == signal_name_or_value { + println!("{}", value); exit!(EXIT_OK as i32) - } else if signal_name_or_value == signal.value.to_string() { - println!("{}", signal.name); + } else if signal_name_or_value == value.to_string() { + println!("{}", signal); exit!(EXIT_OK as i32) } } @@ -165,8 +158,8 @@ fn print_signal(signal_name_or_value: &str) { fn print_signals() { let mut pos = 0; for (idx, signal) in ALL_SIGNALS.iter().enumerate() { - pos += signal.name.len(); - print!("{}", signal.name); + pos += signal.len(); + print!("{}", signal); if idx > 0 && pos > 73 { println!(); pos = 0; diff --git a/src/uu/link/Cargo.toml b/src/uu/link/Cargo.toml index 14a6ac7c9..0457ec479 100644 --- a/src/uu/link/Cargo.toml +++ b/src/uu/link/Cargo.toml @@ -18,7 +18,7 @@ path = "src/link.rs" libc = "0.2.42" uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } [[bin]] name = "link" diff --git a/src/uu/link/src/link.rs b/src/uu/link/src/link.rs index 08401ebaf..ad7702044 100644 --- a/src/uu/link/src/link.rs +++ b/src/uu/link/src/link.rs @@ -32,19 +32,7 @@ pub fn normalize_error_message(e: Error) -> String { pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); - let matches = App::new(executable!()) - .version(crate_version!()) - .about(ABOUT) - .usage(&usage[..]) - .arg( - Arg::with_name(options::FILES) - .hidden(true) - .required(true) - .min_values(2) - .max_values(2) - .takes_value(true), - ) - .get_matches_from(args); + let matches = uu_app().usage(&usage[..]).get_matches_from(args); let files: Vec<_> = matches .values_of_os(options::FILES) @@ -61,3 +49,17 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } } } + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about(ABOUT) + .arg( + Arg::with_name(options::FILES) + .hidden(true) + .required(true) + .min_values(2) + .max_values(2) + .takes_value(true), + ) +} diff --git a/src/uu/ln/Cargo.toml b/src/uu/ln/Cargo.toml index c19d8fb52..4386d7522 100644 --- a/src/uu/ln/Cargo.toml +++ b/src/uu/ln/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/ln.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } libc = "0.2.42" uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["fs"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/ln/src/ln.rs b/src/uu/ln/src/ln.rs index cd5eef842..b08eba97a 100644 --- a/src/uu/ln/src/ln.rs +++ b/src/uu/ln/src/ln.rs @@ -27,7 +27,6 @@ use uucore::fs::{canonicalize, CanonicalizeMode}; pub struct Settings { overwrite: OverwriteMode, backup: BackupMode, - force: bool, suffix: String, symbolic: bool, relative: bool, @@ -54,7 +53,7 @@ pub enum BackupMode { fn get_usage() -> String { format!( - "{0} [OPTION]... [-T] TARGET LINK_executable!() (1st form) + "{0} [OPTION]... [-T] TARGET LINK_NAME (1st form) {0} [OPTION]... TARGET (2nd form) {0} [OPTION]... TARGET... DIRECTORY (3rd form) {0} [OPTION]... -t DIRECTORY TARGET... (4th form)", @@ -64,7 +63,7 @@ fn get_usage() -> String { fn get_long_usage() -> String { String::from( - " In the 1st form, create a link to TARGET with the name LINK_executable!(). + " In the 1st form, create a link to TARGET with the name LINK_NAME. In the 2nd form, create a link to TARGET in the current directory. In the 3rd and 4th forms, create links to each TARGET in DIRECTORY. Create hard links by default, symbolic links with --symbolic. @@ -78,17 +77,19 @@ fn get_long_usage() -> String { static ABOUT: &str = "change file owner and group"; -static OPT_B: &str = "b"; -static OPT_BACKUP: &str = "backup"; -static OPT_FORCE: &str = "force"; -static OPT_INTERACTIVE: &str = "interactive"; -static OPT_NO_DEREFERENCE: &str = "no-dereference"; -static OPT_SYMBOLIC: &str = "symbolic"; -static OPT_SUFFIX: &str = "suffix"; -static OPT_TARGET_DIRECTORY: &str = "target-directory"; -static OPT_NO_TARGET_DIRECTORY: &str = "no-target-directory"; -static OPT_RELATIVE: &str = "relative"; -static OPT_VERBOSE: &str = "verbose"; +mod options { + pub const B: &str = "b"; + pub const BACKUP: &str = "backup"; + pub const FORCE: &str = "force"; + pub const INTERACTIVE: &str = "interactive"; + pub const NO_DEREFERENCE: &str = "no-dereference"; + pub const SYMBOLIC: &str = "symbolic"; + pub const SUFFIX: &str = "suffix"; + pub const TARGET_DIRECTORY: &str = "target-directory"; + pub const NO_TARGET_DIRECTORY: &str = "no-target-directory"; + pub const RELATIVE: &str = "relative"; + pub const VERBOSE: &str = "verbose"; +} static ARG_FILES: &str = "files"; @@ -96,109 +97,9 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); let long_usage = get_long_usage(); - let matches = App::new(executable!()) - .version(crate_version!()) - .about(ABOUT) + let matches = uu_app() .usage(&usage[..]) .after_help(&long_usage[..]) - .arg(Arg::with_name(OPT_B).short(OPT_B).help( - "make a backup of each file that would otherwise be overwritten or \ - removed", - )) - .arg( - Arg::with_name(OPT_BACKUP) - .long(OPT_BACKUP) - .help( - "make a backup of each file that would otherwise be overwritten \ - or removed", - ) - .takes_value(true) - .possible_value("simple") - .possible_value("never") - .possible_value("numbered") - .possible_value("t") - .possible_value("existing") - .possible_value("nil") - .possible_value("none") - .possible_value("off") - .value_name("METHOD"), - ) - // TODO: opts.arg( - // Arg::with_name(("d", "directory", "allow users with appropriate privileges to attempt \ - // to make hard links to directories"); - .arg( - Arg::with_name(OPT_FORCE) - .short("f") - .long(OPT_FORCE) - .help("remove existing destination files"), - ) - .arg( - Arg::with_name(OPT_INTERACTIVE) - .short("i") - .long(OPT_INTERACTIVE) - .help("prompt whether to remove existing destination files"), - ) - .arg( - Arg::with_name(OPT_NO_DEREFERENCE) - .short("n") - .long(OPT_NO_DEREFERENCE) - .help( - "treat LINK_executable!() as a normal file if it is a \ - symbolic link to a directory", - ), - ) - // TODO: opts.arg( - // Arg::with_name(("L", "logical", "dereference TARGETs that are symbolic links"); - // - // TODO: opts.arg( - // Arg::with_name(("P", "physical", "make hard links directly to symbolic links"); - .arg( - Arg::with_name(OPT_SYMBOLIC) - .short("s") - .long("symbolic") - .help("make symbolic links instead of hard links"), - ) - .arg( - Arg::with_name(OPT_SUFFIX) - .short("S") - .long(OPT_SUFFIX) - .help("override the usual backup suffix") - .value_name("SUFFIX") - .takes_value(true), - ) - .arg( - Arg::with_name(OPT_TARGET_DIRECTORY) - .short("t") - .long(OPT_TARGET_DIRECTORY) - .help("specify the DIRECTORY in which to create the links") - .value_name("DIRECTORY") - .conflicts_with(OPT_NO_TARGET_DIRECTORY), - ) - .arg( - Arg::with_name(OPT_NO_TARGET_DIRECTORY) - .short("T") - .long(OPT_NO_TARGET_DIRECTORY) - .help("treat LINK_executable!() as a normal file always"), - ) - .arg( - Arg::with_name(OPT_RELATIVE) - .short("r") - .long(OPT_RELATIVE) - .help("create symbolic links relative to link location"), - ) - .arg( - Arg::with_name(OPT_VERBOSE) - .short("v") - .long(OPT_VERBOSE) - .help("print name of each linked file"), - ) - .arg( - Arg::with_name(ARG_FILES) - .multiple(true) - .takes_value(true) - .required(true) - .min_values(1), - ) .get_matches_from(args); /* the list of files */ @@ -209,18 +110,18 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .map(PathBuf::from) .collect(); - let overwrite_mode = if matches.is_present(OPT_FORCE) { + let overwrite_mode = if matches.is_present(options::FORCE) { OverwriteMode::Force - } else if matches.is_present(OPT_INTERACTIVE) { + } else if matches.is_present(options::INTERACTIVE) { OverwriteMode::Interactive } else { OverwriteMode::NoClobber }; - let backup_mode = if matches.is_present(OPT_B) { + let backup_mode = if matches.is_present(options::B) { BackupMode::ExistingBackup - } else if matches.is_present(OPT_BACKUP) { - match matches.value_of(OPT_BACKUP) { + } else if matches.is_present(options::BACKUP) { + match matches.value_of(options::BACKUP) { None => BackupMode::ExistingBackup, Some(mode) => match mode { "simple" | "never" => BackupMode::SimpleBackup, @@ -234,8 +135,8 @@ pub fn uumain(args: impl uucore::Args) -> i32 { BackupMode::NoBackup }; - let backup_suffix = if matches.is_present(OPT_SUFFIX) { - matches.value_of(OPT_SUFFIX).unwrap() + let backup_suffix = if matches.is_present(options::SUFFIX) { + matches.value_of(options::SUFFIX).unwrap() } else { "~" }; @@ -243,34 +144,137 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let settings = Settings { overwrite: overwrite_mode, backup: backup_mode, - force: matches.is_present(OPT_FORCE), suffix: backup_suffix.to_string(), - symbolic: matches.is_present(OPT_SYMBOLIC), - relative: matches.is_present(OPT_RELATIVE), - target_dir: matches.value_of(OPT_TARGET_DIRECTORY).map(String::from), - no_target_dir: matches.is_present(OPT_NO_TARGET_DIRECTORY), - no_dereference: matches.is_present(OPT_NO_DEREFERENCE), - verbose: matches.is_present(OPT_VERBOSE), + symbolic: matches.is_present(options::SYMBOLIC), + relative: matches.is_present(options::RELATIVE), + target_dir: matches + .value_of(options::TARGET_DIRECTORY) + .map(String::from), + no_target_dir: matches.is_present(options::NO_TARGET_DIRECTORY), + no_dereference: matches.is_present(options::NO_DEREFERENCE), + verbose: matches.is_present(options::VERBOSE), }; exec(&paths[..], &settings) } +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about(ABOUT) + .arg(Arg::with_name(options::B).short(options::B).help( + "make a backup of each file that would otherwise be overwritten or \ + removed", + )) + .arg( + Arg::with_name(options::BACKUP) + .long(options::BACKUP) + .help( + "make a backup of each file that would otherwise be overwritten \ + or removed", + ) + .takes_value(true) + .possible_values(&[ + "simple", "never", "numbered", "t", "existing", "nil", "none", "off", + ]) + .value_name("METHOD"), + ) + // TODO: opts.arg( + // Arg::with_name(("d", "directory", "allow users with appropriate privileges to attempt \ + // to make hard links to directories"); + .arg( + Arg::with_name(options::FORCE) + .short("f") + .long(options::FORCE) + .help("remove existing destination files"), + ) + .arg( + Arg::with_name(options::INTERACTIVE) + .short("i") + .long(options::INTERACTIVE) + .help("prompt whether to remove existing destination files"), + ) + .arg( + Arg::with_name(options::NO_DEREFERENCE) + .short("n") + .long(options::NO_DEREFERENCE) + .help( + "treat LINK_NAME as a normal file if it is a \ + symbolic link to a directory", + ), + ) + // TODO: opts.arg( + // Arg::with_name(("L", "logical", "dereference TARGETs that are symbolic links"); + // + // TODO: opts.arg( + // Arg::with_name(("P", "physical", "make hard links directly to symbolic links"); + .arg( + Arg::with_name(options::SYMBOLIC) + .short("s") + .long("symbolic") + .help("make symbolic links instead of hard links") + // override added for https://github.com/uutils/coreutils/issues/2359 + .overrides_with(options::SYMBOLIC), + ) + .arg( + Arg::with_name(options::SUFFIX) + .short("S") + .long(options::SUFFIX) + .help("override the usual backup suffix") + .value_name("SUFFIX") + .takes_value(true), + ) + .arg( + Arg::with_name(options::TARGET_DIRECTORY) + .short("t") + .long(options::TARGET_DIRECTORY) + .help("specify the DIRECTORY in which to create the links") + .value_name("DIRECTORY") + .conflicts_with(options::NO_TARGET_DIRECTORY), + ) + .arg( + Arg::with_name(options::NO_TARGET_DIRECTORY) + .short("T") + .long(options::NO_TARGET_DIRECTORY) + .help("treat LINK_NAME as a normal file always"), + ) + .arg( + Arg::with_name(options::RELATIVE) + .short("r") + .long(options::RELATIVE) + .help("create symbolic links relative to link location") + .requires(options::SYMBOLIC), + ) + .arg( + Arg::with_name(options::VERBOSE) + .short("v") + .long(options::VERBOSE) + .help("print name of each linked file"), + ) + .arg( + Arg::with_name(ARG_FILES) + .multiple(true) + .takes_value(true) + .required(true) + .min_values(1), + ) +} + fn exec(files: &[PathBuf], settings: &Settings) -> i32 { // Handle cases where we create links in a directory first. if let Some(ref name) = settings.target_dir { // 4th form: a directory is specified by -t. - return link_files_in_dir(files, &PathBuf::from(name), &settings); + return link_files_in_dir(files, &PathBuf::from(name), settings); } if !settings.no_target_dir { if files.len() == 1 { // 2nd form: the target directory is the current directory. - return link_files_in_dir(files, &PathBuf::from("."), &settings); + return link_files_in_dir(files, &PathBuf::from("."), settings); } let last_file = &PathBuf::from(files.last().unwrap()); if files.len() > 2 || last_file.is_dir() { // 3rd form: create links in the last argument. - return link_files_in_dir(&files[0..files.len() - 1], last_file, &settings); + return link_files_in_dir(&files[0..files.len() - 1], last_file, settings); } } @@ -310,47 +314,48 @@ fn link_files_in_dir(files: &[PathBuf], target_dir: &Path, settings: &Settings) let mut all_successful = true; for srcpath in files.iter() { - let targetpath = if settings.no_dereference && settings.force { - // In that case, we don't want to do link resolution - // We need to clean the target - if is_symlink(target_dir) { - if target_dir.is_file() { - if let Err(e) = fs::remove_file(target_dir) { - show_error!("Could not update {}: {}", target_dir.display(), e) - }; - } - if target_dir.is_dir() { - // Not sure why but on Windows, the symlink can be - // considered as a dir - // See test_ln::test_symlink_no_deref_dir - if let Err(e) = fs::remove_dir(target_dir) { - show_error!("Could not update {}: {}", target_dir.display(), e) - }; - } - } - target_dir.to_path_buf() - } else { - match srcpath.as_os_str().to_str() { - Some(name) => { - match Path::new(name).file_name() { - Some(basename) => target_dir.join(basename), - // This can be None only for "." or "..". Trying - // to create a link with such name will fail with - // EEXIST, which agrees with the behavior of GNU - // coreutils. - None => target_dir.join(name), + let targetpath = + if settings.no_dereference && matches!(settings.overwrite, OverwriteMode::Force) { + // In that case, we don't want to do link resolution + // We need to clean the target + if is_symlink(target_dir) { + if target_dir.is_file() { + if let Err(e) = fs::remove_file(target_dir) { + show_error!("Could not update {}: {}", target_dir.display(), e) + }; + } + if target_dir.is_dir() { + // Not sure why but on Windows, the symlink can be + // considered as a dir + // See test_ln::test_symlink_no_deref_dir + if let Err(e) = fs::remove_dir(target_dir) { + show_error!("Could not update {}: {}", target_dir.display(), e) + }; } } - None => { - show_error!( - "cannot stat '{}': No such file or directory", - srcpath.display() - ); - all_successful = false; - continue; + target_dir.to_path_buf() + } else { + match srcpath.as_os_str().to_str() { + Some(name) => { + match Path::new(name).file_name() { + Some(basename) => target_dir.join(basename), + // This can be None only for "." or "..". Trying + // to create a link with such name will fail with + // EEXIST, which agrees with the behavior of GNU + // coreutils. + None => target_dir.join(name), + } + } + None => { + show_error!( + "cannot stat '{}': No such file or directory", + srcpath.display() + ); + all_successful = false; + continue; + } } - } - }; + }; if let Err(e) = link(srcpath, &targetpath, settings) { show_error!( @@ -371,7 +376,8 @@ fn link_files_in_dir(files: &[PathBuf], target_dir: &Path, settings: &Settings) fn relative_path<'a>(src: &Path, dst: &Path) -> Result> { let src_abs = canonicalize(src, CanonicalizeMode::Normal)?; - let dst_abs = canonicalize(dst, CanonicalizeMode::Normal)?; + let mut dst_abs = canonicalize(dst.parent().unwrap(), CanonicalizeMode::Normal)?; + dst_abs.push(dst.components().last().unwrap()); let suffix_pos = src_abs .components() .zip(dst_abs.components()) @@ -380,19 +386,22 @@ fn relative_path<'a>(src: &Path, dst: &Path) -> Result> { let src_iter = src_abs.components().skip(suffix_pos).map(|x| x.as_os_str()); - let result: PathBuf = dst_abs + let mut result: PathBuf = dst_abs .components() .skip(suffix_pos + 1) .map(|_| OsStr::new("..")) .chain(src_iter) .collect(); + if result.as_os_str().is_empty() { + result.push("."); + } Ok(result.into()) } fn link(src: &Path, dst: &Path, settings: &Settings) -> Result<()> { let mut backup_path = None; let source: Cow<'_, Path> = if settings.relative { - relative_path(&src, dst)? + relative_path(src, dst)? } else { src.into() }; @@ -421,10 +430,6 @@ fn link(src: &Path, dst: &Path, settings: &Settings) -> Result<()> { } } - if settings.no_dereference && settings.force && dst.exists() { - fs::remove_file(dst)?; - } - if settings.symbolic { symlink(&source, dst)?; } else { diff --git a/src/uu/logname/Cargo.toml b/src/uu/logname/Cargo.toml index 4aa4d68f4..2a541073f 100644 --- a/src/uu/logname/Cargo.toml +++ b/src/uu/logname/Cargo.toml @@ -16,7 +16,7 @@ path = "src/logname.rs" [dependencies] libc = "0.2.42" -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/logname/src/logname.rs b/src/uu/logname/src/logname.rs index ba5880403..4a6f43418 100644 --- a/src/uu/logname/src/logname.rs +++ b/src/uu/logname/src/logname.rs @@ -45,11 +45,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .accept_any(); let usage = get_usage(); - let _ = App::new(executable!()) - .version(crate_version!()) - .about(SUMMARY) - .usage(&usage[..]) - .get_matches_from(args); + let _ = uu_app().usage(&usage[..]).get_matches_from(args); match get_userlogin() { Some(userlogin) => println!("{}", userlogin), @@ -58,3 +54,9 @@ pub fn uumain(args: impl uucore::Args) -> i32 { 0 } + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about(SUMMARY) +} diff --git a/src/uu/ls/Cargo.toml b/src/uu/ls/Cargo.toml index ab58a7300..ecd4f1b8d 100644 --- a/src/uu/ls/Cargo.toml +++ b/src/uu/ls/Cargo.toml @@ -17,7 +17,7 @@ path = "src/ls.rs" [dependencies] locale = "0.2.2" chrono = "0.4.19" -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } unicode-width = "0.1.8" number_prefix = "0.4" term_grid = "0.1.5" diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 3c7b22360..2748cbe24 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -14,7 +14,6 @@ extern crate uucore; extern crate lazy_static; mod quoting_style; -mod version_cmp; use clap::{crate_version, App, Arg}; use globset::{self, Glob, GlobSet, GlobSetBuilder}; @@ -26,10 +25,11 @@ use quoting_style::{escape_name, QuotingStyle}; use std::os::windows::fs::MetadataExt; use std::{ cmp::Reverse, + error::Error, + fmt::Display, fs::{self, DirEntry, FileType, Metadata}, io::{stdout, BufWriter, Stdout, Write}, path::{Path, PathBuf}, - process::exit, time::{SystemTime, UNIX_EPOCH}, }; #[cfg(unix)] @@ -38,12 +38,13 @@ use std::{ os::unix::fs::{FileTypeExt, MetadataExt}, time::Duration, }; - use term_grid::{Cell, Direction, Filling, Grid, GridOptions}; +use uucore::error::{set_exit_code, FromIo, UCustomError, UResult}; use unicode_width::UnicodeWidthStr; #[cfg(unix)] use uucore::libc::{S_IXGRP, S_IXOTH, S_IXUSR}; +use uucore::{fs::display_permissions, version_cmp::version_cmp}; static ABOUT: &str = " By default, ls will list the files and contents of any directories on @@ -125,6 +126,32 @@ pub mod options { pub static IGNORE: &str = "ignore"; } +#[derive(Debug)] +enum LsError { + InvalidLineWidth(String), + NoMetadata(PathBuf), +} + +impl UCustomError for LsError { + fn code(&self) -> i32 { + match self { + LsError::InvalidLineWidth(_) => 2, + LsError::NoMetadata(_) => 1, + } + } +} + +impl Error for LsError {} + +impl Display for LsError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + LsError::InvalidLineWidth(s) => write!(f, "invalid line width: '{}'", s), + LsError::NoMetadata(p) => write!(f, "could not open file: '{}'", p.display()), + } + } +} + #[derive(PartialEq, Eq)] enum Format { Columns, @@ -218,7 +245,7 @@ struct LongFormat { impl Config { #[allow(clippy::cognitive_complexity)] - fn from(options: clap::ArgMatches) -> Config { + fn from(options: clap::ArgMatches) -> UResult { let (mut format, opt) = if let Some(format_) = options.value_of(options::FORMAT) { ( match format_ { @@ -369,23 +396,22 @@ impl Config { } }; - let width = options - .value_of(options::WIDTH) - .map(|x| { - x.parse::().unwrap_or_else(|_e| { - show_error!("invalid line width: ‘{}’", x); - exit(2); - }) - }) - .or_else(|| termsize::get().map(|s| s.cols)); + let width = match options.value_of(options::WIDTH) { + Some(x) => match x.parse::() { + Ok(u) => Some(u), + Err(_) => return Err(LsError::InvalidLineWidth(x.into()).into()), + }, + None => termsize::get().map(|s| s.cols), + }; #[allow(clippy::needless_bool)] let show_control = if options.is_present(options::HIDE_CONTROL_CHARS) { false - } else if options.is_present(options::SHOW_CONTROL_CHARS) { + } else if options.is_present(options::SHOW_CONTROL_CHARS) || atty::is(atty::Stream::Stdout) + { true } else { - false // TODO: only if output is a terminal and the program is `ls` + false }; let quoting_style = if let Some(style) = options.value_of(options::QUOTING_STYLE) { @@ -527,7 +553,7 @@ impl Config { Dereference::DirArgs }; - Config { + Ok(Config { format, files, sort, @@ -546,21 +572,34 @@ impl Config { quoting_style, indicator_style, time_style, - } + }) } } -pub fn uumain(args: impl uucore::Args) -> i32 { +#[uucore_procs::gen_uumain] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { let args = args .collect_str(InvalidEncodingHandling::Ignore) .accept_any(); let usage = get_usage(); - let app = App::new(executable!()) + let app = uu_app().usage(&usage[..]); + + let matches = app.get_matches_from(args); + + let locs = matches + .values_of(options::PATHS) + .map(|v| v.map(ToString::to_string).collect()) + .unwrap_or_else(|| vec![String::from(".")]); + + list(locs, Config::from(matches)?) +} + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) .version(crate_version!()) .about(ABOUT) - .usage(&usage[..]) // Format arguments .arg( @@ -755,7 +794,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { Arg::with_name(options::time::CHANGE) .short(options::time::CHANGE) .help("If the long listing format (e.g., -l, -o) is being used, print the status \ - change time (the ‘ctime’ in the inode) instead of the modification time. When \ + change time (the 'ctime' in the inode) instead of the modification time. When \ explicitly sorting by time (--sort=time or -t) or when not using a long listing \ format, sort according to the status change time.") .overrides_with_all(&[ @@ -1094,16 +1133,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { // Positional arguments .arg(Arg::with_name(options::PATHS).multiple(true).takes_value(true)) - .after_help(AFTER_HELP); - - let matches = app.get_matches_from(args); - - let locs = matches - .values_of(options::PATHS) - .map(|v| v.map(ToString::to_string).collect()) - .unwrap_or_else(|| vec![String::from(".")]); - - list(locs, Config::from(matches)) + .after_help(AFTER_HELP) } /// Represents a Path along with it's associated data @@ -1186,31 +1216,27 @@ impl PathData { } } -fn list(locs: Vec, config: Config) -> i32 { +fn list(locs: Vec, config: Config) -> UResult<()> { let mut files = Vec::::new(); let mut dirs = Vec::::new(); - let mut has_failed = false; let mut out = BufWriter::new(stdout()); for loc in &locs { let p = PathBuf::from(&loc); - if !p.exists() { - show_error!("'{}': {}", &loc, "No such file or directory"); - /* - We found an error, the return code of ls should not be 0 - And no need to continue the execution - */ - has_failed = true; + let path_data = PathData::new(p, None, None, &config, true); + + if path_data.md().is_none() { + show!(std::io::ErrorKind::NotFound + .map_err_context(|| format!("cannot access '{}'", path_data.p_buf.display()))); + // We found an error, no need to continue the execution continue; } - let path_data = PathData::new(p, None, None, &config, true); - let show_dir_contents = match path_data.file_type() { Some(ft) => !config.directory && ft.is_dir(), None => { - has_failed = true; + set_exit_code(1); false } }; @@ -1231,11 +1257,8 @@ fn list(locs: Vec, config: Config) -> i32 { } enter_directory(&dir, &config, &mut out); } - if has_failed { - 1 - } else { - 0 - } + + Ok(()) } fn sort_entries(entries: &mut Vec, config: &Config) { @@ -1243,7 +1266,7 @@ fn sort_entries(entries: &mut Vec, config: &Config) { Sort::Time => entries.sort_by_key(|k| { Reverse( k.md() - .and_then(|md| get_system_time(&md, config)) + .and_then(|md| get_system_time(md, config)) .unwrap_or(UNIX_EPOCH), ) }), @@ -1252,7 +1275,8 @@ fn sort_entries(entries: &mut Vec, config: &Config) { } // The default sort in GNU ls is case insensitive Sort::Name => entries.sort_by(|a, b| a.display_name.cmp(&b.display_name)), - Sort::Version => entries.sort_by(|a, b| version_cmp::version_cmp(&a.p_buf, &b.p_buf)), + Sort::Version => entries + .sort_by(|a, b| version_cmp(&a.p_buf.to_string_lossy(), &b.p_buf.to_string_lossy())), Sort::Extension => entries.sort_by(|a, b| { a.p_buf .extension() @@ -1269,7 +1293,8 @@ fn sort_entries(entries: &mut Vec, config: &Config) { #[cfg(windows)] fn is_hidden(file_path: &DirEntry) -> bool { - let metadata = fs::metadata(file_path.path()).unwrap(); + let path = file_path.path(); + let metadata = fs::metadata(&path).unwrap_or_else(|_| fs::symlink_metadata(&path).unwrap()); let attr = metadata.file_attributes(); (attr & 0x2) > 0 } @@ -1323,14 +1348,14 @@ fn enter_directory(dir: &PathData, config: &Config, out: &mut BufWriter) .filter(|p| p.file_type().map(|ft| ft.is_dir()).unwrap_or(false)) { let _ = writeln!(out, "\n{}:", e.p_buf.display()); - enter_directory(&e, config, out); + enter_directory(e, config, out); } } } fn get_metadata(entry: &Path, dereference: bool) -> std::io::Result { if dereference { - entry.metadata().or_else(|_| entry.symlink_metadata()) + entry.metadata() } else { entry.symlink_metadata() } @@ -1339,8 +1364,8 @@ fn get_metadata(entry: &Path, dereference: bool) -> std::io::Result { fn display_dir_entry_size(entry: &PathData, config: &Config) -> (usize, usize) { if let Some(md) = entry.md() { ( - display_symlink_count(&md).len(), - display_size_or_rdev(&md, config).len(), + display_symlink_count(md).len(), + display_size_or_rdev(md, config).len(), ) } else { (0, 0) @@ -1371,7 +1396,7 @@ fn display_items(items: &[PathData], config: &Config, out: &mut BufWriter { @@ -1462,8 +1487,6 @@ fn display_grid( } } -use uucore::fs::display_permissions; - fn display_item_long( item: &PathData, max_links: usize, @@ -1473,7 +1496,7 @@ fn display_item_long( ) { let md = match item.md() { None => { - show_error!("could not show file: {}", &item.p_buf.display()); + show!(LsError::NoMetadata(item.p_buf.clone())); return; } Some(md) => md, @@ -1482,40 +1505,40 @@ fn display_item_long( #[cfg(unix)] { if config.inode { - let _ = write!(out, "{} ", get_inode(&md)); + let _ = write!(out, "{} ", get_inode(md)); } } let _ = write!( out, "{} {}", - display_permissions(&md, true), - pad_left(display_symlink_count(&md), max_links), + display_permissions(md, true), + pad_left(display_symlink_count(md), max_links), ); if config.long.owner { - let _ = write!(out, " {}", display_uname(&md, config)); + let _ = write!(out, " {}", display_uname(md, config)); } if config.long.group { - let _ = write!(out, " {}", display_group(&md, config)); + let _ = write!(out, " {}", display_group(md, config)); } // Author is only different from owner on GNU/Hurd, so we reuse // the owner, since GNU/Hurd is not currently supported by Rust. if config.long.author { - let _ = write!(out, " {}", display_uname(&md, config)); + let _ = write!(out, " {}", display_uname(md, config)); } let _ = writeln!( out, " {} {} {}", pad_left(display_size_or_rdev(md, config), max_size), - display_date(&md, config), + display_date(md, config), // unwrap is fine because it fails when metadata is not available // but we already know that it is because it's checked at the // start of the function. - display_file_name(&item, config).unwrap().contents, + display_file_name(item, config).unwrap().contents, ); } @@ -1732,7 +1755,11 @@ fn display_file_name(path: &PathData, config: &Config) -> Option { #[cfg(unix)] { if config.format != Format::Long && config.inode { - name = get_inode(path.md()?) + " " + &name; + name = path + .md() + .map_or_else(|| "?".to_string(), |md| get_inode(md)) + + " " + + &name; } } @@ -1741,7 +1768,7 @@ fn display_file_name(path: &PathData, config: &Config) -> Option { let mut width = name.width(); if let Some(ls_colors) = &config.color { - name = color_name(&ls_colors, &path.p_buf, name, path.md()?); + name = color_name(ls_colors, &path.p_buf, name, path.md()?); } if config.indicator_style != IndicatorStyle::None { @@ -1786,7 +1813,7 @@ fn display_file_name(path: &PathData, config: &Config) -> Option { } fn color_name(ls_colors: &LsColors, path: &Path, name: String, md: &Metadata) -> String { - match ls_colors.style_for_path_with_metadata(path, Some(&md)) { + match ls_colors.style_for_path_with_metadata(path, Some(md)) { Some(style) => style.to_ansi_term_style().paint(name).to_string(), None => name, } diff --git a/src/uu/ls/src/version_cmp.rs b/src/uu/ls/src/version_cmp.rs deleted file mode 100644 index e3f7e29e3..000000000 --- a/src/uu/ls/src/version_cmp.rs +++ /dev/null @@ -1,306 +0,0 @@ -use std::cmp::Ordering; -use std::path::Path; - -/// Compare paths in a way that matches the GNU version sort, meaning that -/// numbers get sorted in a natural way. -pub(crate) fn version_cmp(a: &Path, b: &Path) -> Ordering { - let a_string = a.to_string_lossy(); - let b_string = b.to_string_lossy(); - let mut a = a_string.chars().peekable(); - let mut b = b_string.chars().peekable(); - - // The order determined from the number of leading zeroes. - // This is used if the filenames are equivalent up to leading zeroes. - let mut leading_zeroes = Ordering::Equal; - - loop { - match (a.next(), b.next()) { - // If the characters are both numerical. We collect the rest of the number - // and parse them to u64's and compare them. - (Some(a_char @ '0'..='9'), Some(b_char @ '0'..='9')) => { - let mut a_leading_zeroes = 0; - if a_char == '0' { - a_leading_zeroes = 1; - while let Some('0') = a.peek() { - a_leading_zeroes += 1; - a.next(); - } - } - - let mut b_leading_zeroes = 0; - if b_char == '0' { - b_leading_zeroes = 1; - while let Some('0') = b.peek() { - b_leading_zeroes += 1; - b.next(); - } - } - // The first different number of leading zeros determines the order - // so if it's already been determined by a previous number, we leave - // it as that ordering. - // It's b.cmp(&a), because the *largest* number of leading zeros - // should go first - if leading_zeroes == Ordering::Equal { - leading_zeroes = b_leading_zeroes.cmp(&a_leading_zeroes); - } - - let mut a_str = String::new(); - let mut b_str = String::new(); - if a_char != '0' { - a_str.push(a_char); - } - if b_char != '0' { - b_str.push(b_char); - } - - // Unwrapping here is fine because we only call next if peek returns - // Some(_), so next should also return Some(_). - while let Some('0'..='9') = a.peek() { - a_str.push(a.next().unwrap()); - } - - while let Some('0'..='9') = b.peek() { - b_str.push(b.next().unwrap()); - } - - // Since the leading zeroes are stripped, the length can be - // used to compare the numbers. - match a_str.len().cmp(&b_str.len()) { - Ordering::Equal => {} - x => return x, - } - - // At this point, leading zeroes are stripped and the lengths - // are equal, meaning that the strings can be compared using - // the standard compare function. - match a_str.cmp(&b_str) { - Ordering::Equal => {} - x => return x, - } - } - // If there are two characters we just compare the characters - (Some(a_char), Some(b_char)) => match a_char.cmp(&b_char) { - Ordering::Equal => {} - x => return x, - }, - // Otherwise, we compare the options (because None < Some(_)) - (a_opt, b_opt) => match a_opt.cmp(&b_opt) { - // If they are completely equal except for leading zeroes, we use the leading zeroes. - Ordering::Equal => return leading_zeroes, - x => return x, - }, - } - } -} - -#[cfg(test)] -mod tests { - use crate::version_cmp::version_cmp; - use std::cmp::Ordering; - use std::path::PathBuf; - #[test] - fn test_version_cmp() { - // Identical strings - assert_eq!( - version_cmp(&PathBuf::from("hello"), &PathBuf::from("hello")), - Ordering::Equal - ); - - assert_eq!( - version_cmp(&PathBuf::from("file12"), &PathBuf::from("file12")), - Ordering::Equal - ); - - assert_eq!( - version_cmp( - &PathBuf::from("file12-suffix"), - &PathBuf::from("file12-suffix") - ), - Ordering::Equal - ); - - assert_eq!( - version_cmp( - &PathBuf::from("file12-suffix24"), - &PathBuf::from("file12-suffix24") - ), - Ordering::Equal - ); - - // Shortened names - assert_eq!( - version_cmp(&PathBuf::from("world"), &PathBuf::from("wo")), - Ordering::Greater, - ); - - assert_eq!( - version_cmp(&PathBuf::from("hello10wo"), &PathBuf::from("hello10world")), - Ordering::Less, - ); - - // Simple names - assert_eq!( - version_cmp(&PathBuf::from("world"), &PathBuf::from("hello")), - Ordering::Greater, - ); - - assert_eq!( - version_cmp(&PathBuf::from("hello"), &PathBuf::from("world")), - Ordering::Less - ); - - assert_eq!( - version_cmp(&PathBuf::from("apple"), &PathBuf::from("ant")), - Ordering::Greater - ); - - assert_eq!( - version_cmp(&PathBuf::from("ant"), &PathBuf::from("apple")), - Ordering::Less - ); - - // Uppercase letters - assert_eq!( - version_cmp(&PathBuf::from("Beef"), &PathBuf::from("apple")), - Ordering::Less, - "Uppercase letters are sorted before all lowercase letters" - ); - - assert_eq!( - version_cmp(&PathBuf::from("Apple"), &PathBuf::from("apple")), - Ordering::Less - ); - - assert_eq!( - version_cmp(&PathBuf::from("apple"), &PathBuf::from("aPple")), - Ordering::Greater - ); - - // Numbers - assert_eq!( - version_cmp(&PathBuf::from("100"), &PathBuf::from("20")), - Ordering::Greater, - "Greater numbers are greater even if they start with a smaller digit", - ); - - assert_eq!( - version_cmp(&PathBuf::from("20"), &PathBuf::from("20")), - Ordering::Equal, - "Equal numbers are equal" - ); - - assert_eq!( - version_cmp(&PathBuf::from("15"), &PathBuf::from("200")), - Ordering::Less, - "Small numbers are smaller" - ); - - // Comparing numbers with other characters - assert_eq!( - version_cmp(&PathBuf::from("1000"), &PathBuf::from("apple")), - Ordering::Less, - "Numbers are sorted before other characters" - ); - - assert_eq!( - // spell-checker:disable-next-line - version_cmp(&PathBuf::from("file1000"), &PathBuf::from("fileapple")), - Ordering::Less, - "Numbers in the middle of the name are sorted before other characters" - ); - - // Leading zeroes - assert_eq!( - version_cmp(&PathBuf::from("012"), &PathBuf::from("12")), - Ordering::Less, - "A single leading zero can make a difference" - ); - - assert_eq!( - version_cmp(&PathBuf::from("000800"), &PathBuf::from("0000800")), - Ordering::Greater, - "Leading number of zeroes is used even if both non-zero number of zeros" - ); - - // Numbers and other characters combined - assert_eq!( - version_cmp(&PathBuf::from("ab10"), &PathBuf::from("aa11")), - Ordering::Greater - ); - - assert_eq!( - version_cmp(&PathBuf::from("aa10"), &PathBuf::from("aa11")), - Ordering::Less, - "Numbers after other characters are handled correctly." - ); - - assert_eq!( - version_cmp(&PathBuf::from("aa2"), &PathBuf::from("aa100")), - Ordering::Less, - "Numbers after alphabetical characters are handled correctly." - ); - - assert_eq!( - version_cmp(&PathBuf::from("aa10bb"), &PathBuf::from("aa11aa")), - Ordering::Less, - "Number is used even if alphabetical characters after it differ." - ); - - assert_eq!( - version_cmp(&PathBuf::from("aa10aa0010"), &PathBuf::from("aa11aa1")), - Ordering::Less, - "Second number is ignored if the first number differs." - ); - - assert_eq!( - version_cmp(&PathBuf::from("aa10aa0010"), &PathBuf::from("aa10aa1")), - Ordering::Greater, - "Second number is used if the rest is equal." - ); - - assert_eq!( - version_cmp(&PathBuf::from("aa10aa0010"), &PathBuf::from("aa00010aa1")), - Ordering::Greater, - "Second number is used if the rest is equal up to leading zeroes of the first number." - ); - - assert_eq!( - version_cmp(&PathBuf::from("aa10aa0022"), &PathBuf::from("aa010aa022")), - Ordering::Greater, - "The leading zeroes of the first number has priority." - ); - - assert_eq!( - version_cmp(&PathBuf::from("aa10aa0022"), &PathBuf::from("aa10aa022")), - Ordering::Less, - "The leading zeroes of other numbers than the first are used." - ); - - assert_eq!( - version_cmp(&PathBuf::from("file-1.4"), &PathBuf::from("file-1.13")), - Ordering::Less, - "Periods are handled as normal text, not as a decimal point." - ); - - // Greater than u64::Max - // u64 == 18446744073709551615 so this should be plenty: - // 20000000000000000000000 - assert_eq!( - version_cmp( - &PathBuf::from("aa2000000000000000000000bb"), - &PathBuf::from("aa002000000000000000000001bb") - ), - Ordering::Less, - "Numbers larger than u64::MAX are handled correctly without crashing" - ); - - assert_eq!( - version_cmp( - &PathBuf::from("aa2000000000000000000000bb"), - &PathBuf::from("aa002000000000000000000000bb") - ), - Ordering::Greater, - "Leading zeroes for numbers larger than u64::MAX are handled correctly without crashing" - ); - } -} diff --git a/src/uu/mkdir/Cargo.toml b/src/uu/mkdir/Cargo.toml index a8d374bf9..ad7972f2d 100644 --- a/src/uu/mkdir/Cargo.toml +++ b/src/uu/mkdir/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/mkdir.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } libc = "0.2.42" uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["fs", "mode"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/mkdir/src/mkdir.rs b/src/uu/mkdir/src/mkdir.rs index d1461c0c9..a99867570 100644 --- a/src/uu/mkdir/src/mkdir.rs +++ b/src/uu/mkdir/src/mkdir.rs @@ -8,148 +8,120 @@ #[macro_use] extern crate uucore; +use clap::OsValues; use clap::{crate_version, App, Arg}; use std::fs; use std::path::Path; +use uucore::error::{FromIo, UResult, USimpleError}; static ABOUT: &str = "Create the given DIRECTORY(ies) if they do not exist"; -static OPT_MODE: &str = "mode"; -static OPT_PARENTS: &str = "parents"; -static OPT_VERBOSE: &str = "verbose"; - -static ARG_DIRS: &str = "dirs"; +mod options { + pub const MODE: &str = "mode"; + pub const PARENTS: &str = "parents"; + pub const VERBOSE: &str = "verbose"; + pub const DIRS: &str = "dirs"; +} fn get_usage() -> String { format!("{0} [OPTION]... [USER]", executable!()) } -/** - * Handles option parsing - */ -pub fn uumain(args: impl uucore::Args) -> i32 { +#[uucore_procs::gen_uumain] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { let usage = get_usage(); // Linux-specific options, not implemented // opts.optflag("Z", "context", "set SELinux security context" + // " of each created directory to CTX"), - let matches = App::new(executable!()) - .version(crate_version!()) - .about(ABOUT) - .usage(&usage[..]) - .arg( - Arg::with_name(OPT_MODE) - .short("m") - .long(OPT_MODE) - .help("set file mode") - .default_value("755"), - ) - .arg( - Arg::with_name(OPT_PARENTS) - .short("p") - .long(OPT_PARENTS) - .alias("parent") - .help("make parent directories as needed"), - ) - .arg( - Arg::with_name(OPT_VERBOSE) - .short("v") - .long(OPT_VERBOSE) - .help("print a message for each printed directory"), - ) - .arg( - Arg::with_name(ARG_DIRS) - .multiple(true) - .takes_value(true) - .min_values(1), - ) - .get_matches_from(args); + let matches = uu_app().usage(&usage[..]).get_matches_from(args); - let dirs: Vec = matches - .values_of(ARG_DIRS) - .map(|v| v.map(ToString::to_string).collect()) - .unwrap_or_default(); - - let verbose = matches.is_present(OPT_VERBOSE); - let recursive = matches.is_present(OPT_PARENTS); + let dirs = matches.values_of_os(options::DIRS).unwrap_or_default(); + let verbose = matches.is_present(options::VERBOSE); + let recursive = matches.is_present(options::PARENTS); // Translate a ~str in octal form to u16, default to 755 // Not tested on Windows - let mode_match = matches.value_of(OPT_MODE); - let mode: u16 = match mode_match { - Some(m) => { - let res: Option = u16::from_str_radix(&m, 8).ok(); - match res { - Some(r) => r, - _ => crash!(1, "no mode given"), - } - } - _ => 0o755_u16, + let mode: u16 = match matches.value_of(options::MODE) { + Some(m) => u16::from_str_radix(m, 8) + .map_err(|_| USimpleError::new(1, format!("invalid mode '{}'", m)))?, + None => 0o755_u16, }; exec(dirs, recursive, mode, verbose) } -/** - * Create the list of new directories - */ -fn exec(dirs: Vec, recursive: bool, mode: u16, verbose: bool) -> i32 { - let mut status = 0; - let empty = Path::new(""); - for dir in &dirs { - let path = Path::new(dir); - if !recursive { - if let Some(parent) = path.parent() { - if parent != empty && !parent.exists() { - show_error!( - "cannot create directory '{}': No such file or directory", - path.display() - ); - status = 1; - continue; - } - } - } - status |= mkdir(path, recursive, mode, verbose); - } - status +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about(ABOUT) + .arg( + Arg::with_name(options::MODE) + .short("m") + .long(options::MODE) + .help("set file mode (not implemented on windows)") + .default_value("755"), + ) + .arg( + Arg::with_name(options::PARENTS) + .short("p") + .long(options::PARENTS) + .alias("parent") + .help("make parent directories as needed"), + ) + .arg( + Arg::with_name(options::VERBOSE) + .short("v") + .long(options::VERBOSE) + .help("print a message for each printed directory"), + ) + .arg( + Arg::with_name(options::DIRS) + .multiple(true) + .takes_value(true) + .min_values(1), + ) } /** - * Wrapper to catch errors, return 1 if failed + * Create the list of new directories */ -fn mkdir(path: &Path, recursive: bool, mode: u16, verbose: bool) -> i32 { +fn exec(dirs: OsValues, recursive: bool, mode: u16, verbose: bool) -> UResult<()> { + for dir in dirs { + let path = Path::new(dir); + show_if_err!(mkdir(path, recursive, mode, verbose)); + } + Ok(()) +} + +fn mkdir(path: &Path, recursive: bool, mode: u16, verbose: bool) -> UResult<()> { let create_dir = if recursive { fs::create_dir_all } else { fs::create_dir }; - if let Err(e) = create_dir(path) { - show_error!("{}: {}", path.display(), e.to_string()); - return 1; - } + + create_dir(path).map_err_context(|| format!("cannot create directory '{}'", path.display()))?; if verbose { println!("{}: created directory '{}'", executable!(), path.display()); } - #[cfg(any(unix, target_os = "redox"))] - fn chmod(path: &Path, mode: u16) -> i32 { - use std::fs::{set_permissions, Permissions}; - use std::os::unix::fs::PermissionsExt; - - let mode = Permissions::from_mode(u32::from(mode)); - - if let Err(err) = set_permissions(path, mode) { - show_error!("{}: {}", path.display(), err); - return 1; - } - 0 - } - #[cfg(windows)] - #[allow(unused_variables)] - fn chmod(path: &Path, mode: u16) -> i32 { - // chmod on Windows only sets the readonly flag, which isn't even honored on directories - 0 - } chmod(path, mode) } + +#[cfg(any(unix, target_os = "redox"))] +fn chmod(path: &Path, mode: u16) -> UResult<()> { + use std::fs::{set_permissions, Permissions}; + use std::os::unix::fs::PermissionsExt; + + let mode = Permissions::from_mode(u32::from(mode)); + + set_permissions(path, mode) + .map_err_context(|| format!("cannot set permissions '{}'", path.display())) +} + +#[cfg(windows)] +fn chmod(_path: &Path, _mode: u16) -> UResult<()> { + // chmod on Windows only sets the readonly flag, which isn't even honored on directories + Ok(()) +} diff --git a/src/uu/mkfifo/Cargo.toml b/src/uu/mkfifo/Cargo.toml index d66003b10..5a78183ea 100644 --- a/src/uu/mkfifo/Cargo.toml +++ b/src/uu/mkfifo/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/mkfifo.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } libc = "0.2.42" uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/mkfifo/src/mkfifo.rs b/src/uu/mkfifo/src/mkfifo.rs index cf2fefa50..ea0906567 100644 --- a/src/uu/mkfifo/src/mkfifo.rs +++ b/src/uu/mkfifo/src/mkfifo.rs @@ -29,27 +29,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .collect_str(InvalidEncodingHandling::Ignore) .accept_any(); - let matches = App::new(executable!()) - .name(NAME) - .version(crate_version!()) - .usage(USAGE) - .about(SUMMARY) - .arg( - Arg::with_name(options::MODE) - .short("m") - .long(options::MODE) - .help("file permissions for the fifo") - .default_value("0666") - .value_name("0666"), - ) - .arg( - Arg::with_name(options::SE_LINUX_SECURITY_CONTEXT) - .short(options::SE_LINUX_SECURITY_CONTEXT) - .help("set the SELinux security context to default type") - ) - .arg(Arg::with_name(options::CONTEXT).long(options::CONTEXT).value_name("CTX").help("like -Z, or if CTX is specified then set the SELinux\nor SMACK security context to CTX")) - .arg(Arg::with_name(options::FIFO).hidden(true).multiple(true)) - .get_matches_from(args); + let matches = uu_app().get_matches_from(args); if matches.is_present(options::CONTEXT) { crash!(1, "--context is not implemented"); @@ -59,7 +39,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } let mode = match matches.value_of(options::MODE) { - Some(m) => match usize::from_str_radix(&m, 8) { + Some(m) => match usize::from_str_radix(m, 8) { Ok(m) => m, Err(e) => { show_error!("invalid mode: {}", e); @@ -88,3 +68,34 @@ pub fn uumain(args: impl uucore::Args) -> i32 { exit_code } + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .name(NAME) + .version(crate_version!()) + .usage(USAGE) + .about(SUMMARY) + .arg( + Arg::with_name(options::MODE) + .short("m") + .long(options::MODE) + .help("file permissions for the fifo") + .default_value("0666") + .value_name("0666"), + ) + .arg( + Arg::with_name(options::SE_LINUX_SECURITY_CONTEXT) + .short(options::SE_LINUX_SECURITY_CONTEXT) + .help("set the SELinux security context to default type"), + ) + .arg( + Arg::with_name(options::CONTEXT) + .long(options::CONTEXT) + .value_name("CTX") + .help( + "like -Z, or if CTX is specified then set the SELinux \ + or SMACK security context to CTX", + ), + ) + .arg(Arg::with_name(options::FIFO).hidden(true).multiple(true)) +} diff --git a/src/uu/mknod/Cargo.toml b/src/uu/mknod/Cargo.toml index 1320e3546..c7ba535fd 100644 --- a/src/uu/mknod/Cargo.toml +++ b/src/uu/mknod/Cargo.toml @@ -16,7 +16,7 @@ name = "uu_mknod" path = "src/mknod.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } libc = "^0.2.42" uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["mode"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/mknod/src/mknod.rs b/src/uu/mknod/src/mknod.rs index e5e6ef1fa..8cc7db908 100644 --- a/src/uu/mknod/src/mknod.rs +++ b/src/uu/mknod/src/mknod.rs @@ -89,48 +89,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { // opts.optflag("Z", "", "set the SELinux security context to default type"); // opts.optopt("", "context", "like -Z, or if CTX is specified then set the SELinux or SMACK security context to CTX"); - let matches = App::new(executable!()) - .version(crate_version!()) - .usage(USAGE) - .after_help(LONG_HELP) - .about(ABOUT) - .arg( - Arg::with_name("mode") - .short("m") - .long("mode") - .value_name("MODE") - .help("set file permission bits to MODE, not a=rw - umask"), - ) - .arg( - Arg::with_name("name") - .value_name("NAME") - .help("name of the new file") - .required(true) - .index(1), - ) - .arg( - Arg::with_name("type") - .value_name("TYPE") - .help("type of the new file (b, c, u or p)") - .required(true) - .validator(valid_type) - .index(2), - ) - .arg( - Arg::with_name("major") - .value_name("MAJOR") - .help("major file type") - .validator(valid_u64) - .index(3), - ) - .arg( - Arg::with_name("minor") - .value_name("MINOR") - .help("minor file type") - .validator(valid_u64) - .index(4), - ) - .get_matches_from(args); + let matches = uu_app().get_matches_from(args); let mode = match get_mode(&matches) { Ok(mode) => mode, @@ -185,6 +144,50 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } } +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .usage(USAGE) + .after_help(LONG_HELP) + .about(ABOUT) + .arg( + Arg::with_name("mode") + .short("m") + .long("mode") + .value_name("MODE") + .help("set file permission bits to MODE, not a=rw - umask"), + ) + .arg( + Arg::with_name("name") + .value_name("NAME") + .help("name of the new file") + .required(true) + .index(1), + ) + .arg( + Arg::with_name("type") + .value_name("TYPE") + .help("type of the new file (b, c, u or p)") + .required(true) + .validator(valid_type) + .index(2), + ) + .arg( + Arg::with_name("major") + .value_name("MAJOR") + .help("major file type") + .validator(valid_u64) + .index(3), + ) + .arg( + Arg::with_name("minor") + .value_name("MINOR") + .help("minor file type") + .validator(valid_u64) + .index(4), + ) +} + fn get_mode(matches: &ArgMatches) -> Result { match matches.value_of("mode") { None => Ok(MODE_RW_UGO), @@ -210,7 +213,7 @@ fn valid_type(tpe: String) -> Result<(), String> { if vec!['b', 'c', 'u', 'p'].contains(&first_char) { Ok(()) } else { - Err(format!("invalid device type ‘{}’", tpe)) + Err(format!("invalid device type '{}'", tpe)) } }) } diff --git a/src/uu/mktemp/Cargo.toml b/src/uu/mktemp/Cargo.toml index c669f0acc..93fb88857 100644 --- a/src/uu/mktemp/Cargo.toml +++ b/src/uu/mktemp/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/mktemp.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } rand = "0.5" tempfile = "3.1" uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } diff --git a/src/uu/mktemp/src/mktemp.rs b/src/uu/mktemp/src/mktemp.rs index 67a88273d..8a4b472aa 100644 --- a/src/uu/mktemp/src/mktemp.rs +++ b/src/uu/mktemp/src/mktemp.rs @@ -12,8 +12,11 @@ extern crate uucore; use clap::{crate_version, App, Arg}; +use uucore::error::{FromIo, UCustomError, UResult}; use std::env; +use std::error::Error; +use std::fmt::Display; use std::iter; use std::path::{is_separator, PathBuf}; @@ -37,13 +40,103 @@ fn get_usage() -> String { format!("{0} [OPTION]... [TEMPLATE]", executable!()) } -pub fn uumain(args: impl uucore::Args) -> i32 { +#[derive(Debug)] +enum MkTempError { + PersistError(PathBuf), + MustEndInX(String), + TooFewXs(String), + ContainsDirSeparator(String), + InvalidTemplate(String), +} + +impl UCustomError for MkTempError {} + +impl Error for MkTempError {} + +impl Display for MkTempError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use MkTempError::*; + match self { + PersistError(p) => write!(f, "could not persist file '{}'", p.display()), + MustEndInX(s) => write!(f, "with --suffix, template '{}' must end in X", s), + TooFewXs(s) => write!(f, "too few X's in template '{}'", s), + ContainsDirSeparator(s) => { + write!(f, "invalid suffix '{}', contains directory separator", s) + } + InvalidTemplate(s) => write!( + f, + "invalid template, '{}'; with --tmpdir, it may not be absolute", + s + ), + } + } +} + +#[uucore_procs::gen_uumain] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { let usage = get_usage(); - let matches = App::new(executable!()) + let matches = uu_app().usage(&usage[..]).get_matches_from(args); + + let template = matches.value_of(ARG_TEMPLATE).unwrap(); + let tmpdir = matches.value_of(OPT_TMPDIR).unwrap_or_default(); + + let (template, mut tmpdir) = if matches.is_present(OPT_TMPDIR) + && !PathBuf::from(tmpdir).is_dir() // if a temp dir is provided, it must be an actual path + && tmpdir.contains("XXX") + // If this is a template, it has to contain at least 3 X + && template == DEFAULT_TEMPLATE + // That means that clap does not think we provided a template + { + // Special case to workaround a limitation of clap when doing + // mktemp --tmpdir apt-key-gpghome.XXX + // The behavior should be + // mktemp --tmpdir $TMPDIR apt-key-gpghome.XX + // As --tmpdir is empty + // + // Fixed in clap 3 + // See https://github.com/clap-rs/clap/pull/1587 + let tmp = env::temp_dir(); + (tmpdir, tmp) + } else if !matches.is_present(OPT_TMPDIR) { + let tmp = env::temp_dir(); + (template, tmp) + } else { + (template, PathBuf::from(tmpdir)) + }; + + let make_dir = matches.is_present(OPT_DIRECTORY); + let dry_run = matches.is_present(OPT_DRY_RUN); + let suppress_file_err = matches.is_present(OPT_QUIET); + + let (prefix, rand, suffix) = parse_template(template, matches.value_of(OPT_SUFFIX))?; + + if matches.is_present(OPT_TMPDIR) && PathBuf::from(prefix).is_absolute() { + return Err(MkTempError::InvalidTemplate(template.into()).into()); + } + + if matches.is_present(OPT_T) { + tmpdir = env::temp_dir() + } + + let res = if dry_run { + dry_exec(tmpdir, prefix, rand, suffix) + } else { + exec(tmpdir, prefix, rand, suffix, make_dir) + }; + + if suppress_file_err { + // Mapping all UErrors to ExitCodes prevents the errors from being printed + res.map_err(|e| e.code().into()) + } else { + res + } +} + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) .version(crate_version!()) .about(ABOUT) - .usage(&usage[..]) .arg( Arg::with_name(OPT_DIRECTORY) .short("d") @@ -77,14 +170,14 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .long(OPT_TMPDIR) .help( "interpret TEMPLATE relative to DIR; if DIR is not specified, use \ - $TMPDIR if set, else /tmp. With this option, TEMPLATE must not \ + $TMPDIR ($TMP on windows) if set, else /tmp. With this option, TEMPLATE must not \ be an absolute name; unlike with -t, TEMPLATE may contain \ slashes, but mktemp creates only the final component", ) .value_name("DIR"), ) .arg(Arg::with_name(OPT_T).short(OPT_T).help( - "Generate a template (using the supplied prefix and TMPDIR if set) \ + "Generate a template (using the supplied prefix and TMPDIR (TMP on windows) if set) \ to create a filename template [deprecated]", )) .arg( @@ -94,96 +187,42 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .max_values(1) .default_value(DEFAULT_TEMPLATE), ) - .get_matches_from(args); - - let template = matches.value_of(ARG_TEMPLATE).unwrap(); - let tmpdir = matches.value_of(OPT_TMPDIR).unwrap_or_default(); - - let (template, mut tmpdir) = if matches.is_present(OPT_TMPDIR) - && !PathBuf::from(tmpdir).is_dir() // if a temp dir is provided, it must be an actual path - && tmpdir.contains("XXX") - // If this is a template, it has to contain at least 3 X - && template == DEFAULT_TEMPLATE - // That means that clap does not think we provided a template - { - // Special case to workaround a limitation of clap when doing - // mktemp --tmpdir apt-key-gpghome.XXX - // The behavior should be - // mktemp --tmpdir $TMPDIR apt-key-gpghome.XX - // As --tmpdir is empty - // - // Fixed in clap 3 - // See https://github.com/clap-rs/clap/pull/1587 - let tmp = env::temp_dir(); - (tmpdir, tmp) - } else if !matches.is_present(OPT_TMPDIR) { - let tmp = env::temp_dir(); - (template, tmp) - } else { - (template, PathBuf::from(tmpdir)) - }; - - let make_dir = matches.is_present(OPT_DIRECTORY); - let dry_run = matches.is_present(OPT_DRY_RUN); - let suppress_file_err = matches.is_present(OPT_QUIET); - - let (prefix, rand, suffix) = match parse_template(template) { - Some((p, r, s)) => match matches.value_of(OPT_SUFFIX) { - Some(suf) => { - if s.is_empty() { - (p, r, suf) - } else { - crash!( - 1, - "Template should end with 'X' when you specify suffix option." - ) - } - } - None => (p, r, s), - }, - None => ("", 0, ""), - }; - - if rand < 3 { - crash!(1, "Too few 'X's in template") - } - - if suffix.chars().any(is_separator) { - crash!(1, "suffix cannot contain any path separators"); - } - - if matches.is_present(OPT_TMPDIR) && PathBuf::from(prefix).is_absolute() { - show_error!( - "invalid template, ‘{}’; with --tmpdir, it may not be absolute", - template - ); - return 1; - }; - - if matches.is_present(OPT_T) { - tmpdir = env::temp_dir() - }; - - if dry_run { - dry_exec(tmpdir, prefix, rand, &suffix) - } else { - exec(tmpdir, prefix, rand, &suffix, make_dir, suppress_file_err) - } } -fn parse_template(temp: &str) -> Option<(&str, usize, &str)> { +fn parse_template<'a>( + temp: &'a str, + suffix: Option<&'a str>, +) -> UResult<(&'a str, usize, &'a str)> { let right = match temp.rfind('X') { Some(r) => r + 1, - None => return None, + None => return Err(MkTempError::TooFewXs(temp.into()).into()), }; let left = temp[..right].rfind(|c| c != 'X').map_or(0, |i| i + 1); let prefix = &temp[..left]; let rand = right - left; - let suffix = &temp[right..]; - Some((prefix, rand, suffix)) + + if rand < 3 { + return Err(MkTempError::TooFewXs(temp.into()).into()); + } + + let mut suf = &temp[right..]; + + if let Some(s) = suffix { + if suf.is_empty() { + suf = s; + } else { + return Err(MkTempError::MustEndInX(temp.into()).into()); + } + }; + + if suf.chars().any(is_separator) { + return Err(MkTempError::ContainsDirSeparator(suf.into()).into()); + } + + Ok((prefix, rand, suf)) } -pub fn dry_exec(mut tmpdir: PathBuf, prefix: &str, rand: usize, suffix: &str) -> i32 { +pub fn dry_exec(mut tmpdir: PathBuf, prefix: &str, rand: usize, suffix: &str) -> UResult<()> { let len = prefix.len() + suffix.len() + rand; let mut buf = String::with_capacity(len); buf.push_str(prefix); @@ -206,51 +245,35 @@ pub fn dry_exec(mut tmpdir: PathBuf, prefix: &str, rand: usize, suffix: &str) -> } tmpdir.push(buf); println!("{}", tmpdir.display()); - 0 + Ok(()) } -fn exec(dir: PathBuf, prefix: &str, rand: usize, suffix: &str, make_dir: bool, quiet: bool) -> i32 { - let res = if make_dir { - let tmpdir = Builder::new() - .prefix(prefix) - .rand_bytes(rand) - .suffix(suffix) - .tempdir_in(&dir); - - // `into_path` consumes the TempDir without removing it - tmpdir.map(|d| d.into_path().to_string_lossy().to_string()) - } else { - let tmpfile = Builder::new() - .prefix(prefix) - .rand_bytes(rand) - .suffix(suffix) - .tempfile_in(&dir); - - match tmpfile { - Ok(f) => { - // `keep` ensures that the file is not deleted - match f.keep() { - Ok((_, p)) => Ok(p.to_string_lossy().to_string()), - Err(e) => { - show_error!("'{}': {}", dir.display(), e); - return 1; - } - } - } - Err(x) => Err(x), - } +fn exec(dir: PathBuf, prefix: &str, rand: usize, suffix: &str, make_dir: bool) -> UResult<()> { + let context = || { + format!( + "failed to create file via template '{}{}{}'", + prefix, + "X".repeat(rand), + suffix + ) }; - match res { - Ok(ref f) => { - println!("{}", f); - 0 - } - Err(e) => { - if !quiet { - show_error!("{}: {}", e, dir.display()); - } - 1 - } - } + let mut builder = Builder::new(); + builder.prefix(prefix).rand_bytes(rand).suffix(suffix); + + let path = if make_dir { + builder + .tempdir_in(&dir) + .map_err_context(context)? + .into_path() // `into_path` consumes the TempDir without removing it + } else { + builder + .tempfile_in(&dir) + .map_err_context(context)? + .keep() // `keep` ensures that the file is not deleted + .map_err(|e| MkTempError::PersistError(e.file.path().to_path_buf()))? + .1 + }; + println!("{}", path.display()); + Ok(()) } diff --git a/src/uu/more/Cargo.toml b/src/uu/more/Cargo.toml index 9b1a3d7b6..497f91f4e 100644 --- a/src/uu/more/Cargo.toml +++ b/src/uu/more/Cargo.toml @@ -15,11 +15,13 @@ edition = "2018" path = "src/more.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } uucore = { version = ">=0.0.7", package = "uucore", path = "../../uucore" } uucore_procs = { version = ">=0.0.5", package = "uucore_procs", path = "../../uucore_procs" } crossterm = ">=0.19" -atty = "0.2.14" +atty = "0.2" +unicode-width = "0.1.7" +unicode-segmentation = "1.7.1" [target.'cfg(target_os = "redox")'.dependencies] redox_termios = "0.1" diff --git a/src/uu/more/src/more.rs b/src/uu/more/src/more.rs index deadba4e4..8f25cd7e4 100644 --- a/src/uu/more/src/more.rs +++ b/src/uu/more/src/more.rs @@ -11,7 +11,6 @@ extern crate uucore; use std::{ - convert::TryInto, fs::File, io::{stdin, stdout, BufReader, Read, Stdout, Write}, path::Path, @@ -29,6 +28,11 @@ use crossterm::{ terminal, }; +use unicode_segmentation::UnicodeSegmentation; +use unicode_width::UnicodeWidthStr; + +const BELL: &str = "\x07"; + pub mod options { pub const SILENT: &str = "silent"; pub const LOGICAL: &str = "logical"; @@ -47,17 +51,59 @@ pub mod options { const MULTI_FILE_TOP_PROMPT: &str = "::::::::::::::\n{}\n::::::::::::::\n"; pub fn uumain(args: impl uucore::Args) -> i32 { - let matches = App::new(executable!()) + let matches = uu_app().get_matches_from(args); + + let mut buff = String::new(); + let silent = matches.is_present(options::SILENT); + if let Some(files) = matches.values_of(options::FILES) { + let mut stdout = setup_term(); + let length = files.len(); + + let mut files_iter = files.peekable(); + while let (Some(file), next_file) = (files_iter.next(), files_iter.peek()) { + let file = Path::new(file); + if file.is_dir() { + terminal::disable_raw_mode().unwrap(); + show_usage_error!("'{}' is a directory.", file.display()); + return 1; + } + if !file.exists() { + terminal::disable_raw_mode().unwrap(); + show_error!("cannot open {}: No such file or directory", file.display()); + return 1; + } + if length > 1 { + buff.push_str(&MULTI_FILE_TOP_PROMPT.replace("{}", file.to_str().unwrap())); + } + let mut reader = BufReader::new(File::open(file).unwrap()); + reader.read_to_string(&mut buff).unwrap(); + more(&buff, &mut stdout, next_file.copied(), silent); + buff.clear(); + } + reset_term(&mut stdout); + } else if atty::isnt(atty::Stream::Stdin) { + stdin().read_to_string(&mut buff).unwrap(); + let mut stdout = setup_term(); + more(&buff, &mut stdout, None, silent); + reset_term(&mut stdout); + } else { + show_usage_error!("bad usage"); + } + 0 +} + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) .about("A file perusal filter for CRT viewing.") .version(crate_version!()) - // The commented arguments below are unimplemented: - /* .arg( Arg::with_name(options::SILENT) .short("d") .long(options::SILENT) .help("Display help instead of ringing bell"), ) + // The commented arguments below are unimplemented: + /* .arg( Arg::with_name(options::LOGICAL) .short("f") @@ -134,43 +180,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .multiple(true) .help("Path to the files to be read"), ) - .get_matches_from(args); - - let mut buff = String::new(); - if let Some(files) = matches.values_of(options::FILES) { - let mut stdout = setup_term(); - let length = files.len(); - for (idx, file) in files.enumerate() { - let file = Path::new(file); - if file.is_dir() { - terminal::disable_raw_mode().unwrap(); - show_usage_error!("'{}' is a directory.", file.display()); - return 1; - } - if !file.exists() { - terminal::disable_raw_mode().unwrap(); - show_error!("cannot open {}: No such file or directory", file.display()); - return 1; - } - if length > 1 { - buff.push_str(&MULTI_FILE_TOP_PROMPT.replace("{}", file.to_str().unwrap())); - } - let mut reader = BufReader::new(File::open(file).unwrap()); - reader.read_to_string(&mut buff).unwrap(); - let is_last = idx + 1 == length; - more(&buff, &mut stdout, is_last); - buff.clear(); - } - reset_term(&mut stdout); - } else if atty::isnt(atty::Stream::Stdin) { - stdin().read_to_string(&mut buff).unwrap(); - let mut stdout = setup_term(); - more(&buff, &mut stdout, true); - reset_term(&mut stdout); - } else { - show_usage_error!("bad usage"); - } - 0 } #[cfg(not(target_os = "fuchsia"))] @@ -200,35 +209,18 @@ fn reset_term(stdout: &mut std::io::Stdout) { #[inline(always)] fn reset_term(_: &mut usize) {} -fn more(buff: &str, mut stdout: &mut Stdout, is_last: bool) { +fn more(buff: &str, mut stdout: &mut Stdout, next_file: Option<&str>, silent: bool) { let (cols, rows) = terminal::size().unwrap(); let lines = break_buff(buff, usize::from(cols)); - let line_count: u16 = lines.len().try_into().unwrap(); - let mut upper_mark = 0; - let mut lines_left = line_count.saturating_sub(upper_mark + rows); - - draw( - &mut upper_mark, - rows, - &mut stdout, - lines.clone(), - line_count, - ); - - // Specifies whether we have reached the end of the file and should - // return on the next key press. However, we immediately return when - // this is the last file. - let mut to_be_done = false; - if lines_left == 0 && is_last { - if is_last { - return; - } else { - to_be_done = true; - } + let mut pager = Pager::new(rows, lines, next_file, silent); + pager.draw(stdout, None); + if pager.should_close() { + return; } loop { + let mut wrong_key = None; if event::poll(Duration::from_millis(10)).unwrap() { match event::read().unwrap() { Event::Key(KeyEvent { @@ -250,62 +242,138 @@ fn more(buff: &str, mut stdout: &mut Stdout, is_last: bool) { code: KeyCode::Char(' '), modifiers: KeyModifiers::NONE, }) => { - upper_mark = upper_mark.saturating_add(rows.saturating_sub(1)); + if pager.should_close() { + return; + } else { + pager.page_down(); + } } Event::Key(KeyEvent { code: KeyCode::Up, modifiers: KeyModifiers::NONE, }) => { - upper_mark = upper_mark.saturating_sub(rows.saturating_sub(1)); + pager.page_up(); } + Event::Resize(col, row) => { + pager.page_resize(col, row); + } + Event::Key(KeyEvent { + code: KeyCode::Char(k), + .. + }) => wrong_key = Some(k), _ => continue, } - lines_left = line_count.saturating_sub(upper_mark + rows); - draw( - &mut upper_mark, - rows, - &mut stdout, - lines.clone(), - line_count, - ); - if lines_left == 0 { - if to_be_done || is_last { - return; - } - to_be_done = true; - } + pager.draw(stdout, wrong_key); } } } -fn draw( - upper_mark: &mut u16, - rows: u16, - mut stdout: &mut std::io::Stdout, +struct Pager<'a> { + // The current line at the top of the screen + upper_mark: usize, + // The number of rows that fit on the screen + content_rows: u16, lines: Vec, - lc: u16, -) { - execute!(stdout, terminal::Clear(terminal::ClearType::CurrentLine)).unwrap(); - let (up_mark, lower_mark) = calc_range(*upper_mark, rows, lc); - // Reduce the row by 1 for the prompt - let displayed_lines = lines - .iter() - .skip(up_mark.into()) - .take(usize::from(rows.saturating_sub(1))); + next_file: Option<&'a str>, + line_count: usize, + silent: bool, +} - for line in displayed_lines { - stdout - .write_all(format!("\r{}\n", line).as_bytes()) - .unwrap(); +impl<'a> Pager<'a> { + fn new(rows: u16, lines: Vec, next_file: Option<&'a str>, silent: bool) -> Self { + let line_count = lines.len(); + Self { + upper_mark: 0, + content_rows: rows.saturating_sub(1), + lines, + next_file, + line_count, + silent, + } + } + + fn should_close(&mut self) -> bool { + self.upper_mark + .saturating_add(self.content_rows.into()) + .ge(&self.line_count) + } + + fn page_down(&mut self) { + self.upper_mark = self.upper_mark.saturating_add(self.content_rows.into()); + } + + fn page_up(&mut self) { + self.upper_mark = self.upper_mark.saturating_sub(self.content_rows.into()); + } + + // TODO: Deal with column size changes. + fn page_resize(&mut self, _: u16, row: u16) { + self.content_rows = row.saturating_sub(1); + } + + fn draw(&self, stdout: &mut std::io::Stdout, wrong_key: Option) { + let lower_mark = self + .line_count + .min(self.upper_mark.saturating_add(self.content_rows.into())); + self.draw_lines(stdout); + self.draw_prompt(stdout, lower_mark, wrong_key); + stdout.flush().unwrap(); + } + + fn draw_lines(&self, stdout: &mut std::io::Stdout) { + execute!(stdout, terminal::Clear(terminal::ClearType::CurrentLine)).unwrap(); + let displayed_lines = self + .lines + .iter() + .skip(self.upper_mark) + .take(self.content_rows.into()); + + for line in displayed_lines { + stdout + .write_all(format!("\r{}\n", line).as_bytes()) + .unwrap(); + } + } + + fn draw_prompt(&self, stdout: &mut Stdout, lower_mark: usize, wrong_key: Option) { + let status_inner = if lower_mark == self.line_count { + format!("Next file: {}", self.next_file.unwrap_or_default()) + } else { + format!( + "{}%", + (lower_mark as f64 / self.line_count as f64 * 100.0).round() as u16 + ) + }; + + let status = format!("--More--({})", status_inner); + + let banner = match (self.silent, wrong_key) { + (true, Some(key)) => { + format!( + "{} [Unknown key: '{}'. Press 'h' for instructions. (unimplemented)]", + status, key + ) + } + (true, None) => format!("{}[Press space to continue, 'q' to quit.]", status), + (false, Some(_)) => format!("{}{}", status, BELL), + (false, None) => status, + }; + + write!( + stdout, + "\r{}{}{}", + Attribute::Reverse, + banner, + Attribute::Reset + ) + .unwrap(); } - make_prompt_and_flush(&mut stdout, lower_mark, lc); - *upper_mark = up_mark; } // Break the lines on the cols of the terminal fn break_buff(buff: &str, cols: usize) -> Vec { - let mut lines = Vec::new(); + let mut lines = Vec::with_capacity(buff.lines().count()); for l in buff.lines() { lines.append(&mut break_line(l, cols)); @@ -313,64 +381,39 @@ fn break_buff(buff: &str, cols: usize) -> Vec { lines } -fn break_line(mut line: &str, cols: usize) -> Vec { - let breaks = (line.len() / cols).saturating_add(1); - let mut lines = Vec::with_capacity(breaks); - // TODO: Use unicode width instead of the length in bytes. - if line.len() < cols { +fn break_line(line: &str, cols: usize) -> Vec { + let width = UnicodeWidthStr::width(line); + let mut lines = Vec::new(); + if width < cols { lines.push(line.to_string()); return lines; } - for _ in 1..=breaks { - let (line1, line2) = line.split_at(cols); - lines.push(line1.to_string()); - if line2.len() < cols { - lines.push(line2.to_string()); - break; + let gr_idx = UnicodeSegmentation::grapheme_indices(line, true); + let mut last_index = 0; + let mut total_width = 0; + for (index, grapheme) in gr_idx { + let width = UnicodeWidthStr::width(grapheme); + total_width += width; + + if total_width > cols { + lines.push(line[last_index..index].to_string()); + last_index = index; + total_width = width; } - line = line2; + } + + if last_index != line.len() { + lines.push(line[last_index..].to_string()); } lines } -// Calculate upper_mark based on certain parameters -fn calc_range(mut upper_mark: u16, rows: u16, line_count: u16) -> (u16, u16) { - let mut lower_mark = upper_mark.saturating_add(rows); - - if lower_mark >= line_count { - upper_mark = line_count.saturating_sub(rows); - lower_mark = line_count; - } else { - lower_mark = lower_mark.saturating_sub(1) - } - (upper_mark, lower_mark) -} - -// Make a prompt similar to original more -fn make_prompt_and_flush(stdout: &mut Stdout, lower_mark: u16, lc: u16) { - write!( - stdout, - "\r{}--More--({}%){}", - Attribute::Reverse, - ((lower_mark as f64 / lc as f64) * 100.0).round() as u16, - Attribute::Reset - ) - .unwrap(); - stdout.flush().unwrap(); -} - #[cfg(test)] mod tests { - use super::{break_line, calc_range}; + use super::break_line; + use unicode_width::UnicodeWidthStr; - // It is good to test the above functions - #[test] - fn test_calc_range() { - assert_eq!((0, 24), calc_range(0, 25, 100)); - assert_eq!((50, 74), calc_range(50, 25, 100)); - assert_eq!((75, 100), calc_range(85, 25, 100)); - } #[test] fn test_break_lines_long() { let mut test_string = String::with_capacity(100); @@ -379,11 +422,12 @@ mod tests { } let lines = break_line(&test_string, 80); + let widths: Vec = lines + .iter() + .map(|s| UnicodeWidthStr::width(&s[..])) + .collect(); - assert_eq!( - (80, 80, 40), - (lines[0].len(), lines[1].len(), lines[2].len()) - ); + assert_eq!((80, 80, 40), (widths[0], widths[1], widths[2])); } #[test] @@ -397,4 +441,22 @@ mod tests { assert_eq!(20, lines[0].len()); } + + #[test] + fn test_break_line_zwj() { + let mut test_string = String::with_capacity(1100); + for _ in 0..20 { + test_string.push_str("👩🏻‍🔬"); + } + + let lines = break_line(&test_string, 80); + + let widths: Vec = lines + .iter() + .map(|s| UnicodeWidthStr::width(&s[..])) + .collect(); + + // 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])); + } } diff --git a/src/uu/mv/Cargo.toml b/src/uu/mv/Cargo.toml index 8f1e7b9ee..94d3de15e 100644 --- a/src/uu/mv/Cargo.toml +++ b/src/uu/mv/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/mv.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } fs_extra = "1.1.0" uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/mv/src/mv.rs b/src/uu/mv/src/mv.rs index 6b6482702..4a761861f 100644 --- a/src/uu/mv/src/mv.rs +++ b/src/uu/mv/src/mv.rs @@ -70,11 +70,64 @@ fn get_usage() -> String { pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); - let matches = App::new(executable!()) + let matches = uu_app() + .after_help(&*format!( + "{}\n{}", + LONG_HELP, + backup_control::BACKUP_CONTROL_LONG_HELP + )) + .usage(&usage[..]) + .get_matches_from(args); + + let files: Vec = matches + .values_of(ARG_FILES) + .map(|v| v.map(ToString::to_string).collect()) + .unwrap_or_default(); + + let overwrite_mode = determine_overwrite_mode(&matches); + let backup_mode = backup_control::determine_backup_mode( + matches.is_present(OPT_BACKUP_NO_ARG) || matches.is_present(OPT_BACKUP), + matches.value_of(OPT_BACKUP), + ); + + if overwrite_mode == OverwriteMode::NoClobber && backup_mode != BackupMode::NoBackup { + show_usage_error!("options --backup and --no-clobber are mutually exclusive"); + return 1; + } + + let backup_suffix = backup_control::determine_backup_suffix(matches.value_of(OPT_SUFFIX)); + + let behavior = Behavior { + overwrite: overwrite_mode, + backup: backup_mode, + suffix: backup_suffix, + update: matches.is_present(OPT_UPDATE), + target_dir: matches.value_of(OPT_TARGET_DIRECTORY).map(String::from), + no_target_dir: matches.is_present(OPT_NO_TARGET_DIRECTORY), + verbose: matches.is_present(OPT_VERBOSE), + }; + + let paths: Vec = { + fn strip_slashes(p: &Path) -> &Path { + p.components().as_path() + } + let to_owned = |p: &Path| p.to_owned(); + let paths = files.iter().map(Path::new); + + if matches.is_present(OPT_STRIP_TRAILING_SLASHES) { + paths.map(strip_slashes).map(to_owned).collect() + } else { + paths.map(to_owned).collect() + } + }; + + exec(&paths[..], behavior) +} + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) .version(crate_version!()) .about(ABOUT) - .after_help(&*format!("{}\n{}", LONG_HELP, backup_control::BACKUP_CONTROL_LONG_HELP)) - .usage(&usage[..]) .arg( Arg::with_name(OPT_BACKUP) .long(OPT_BACKUP) @@ -153,51 +206,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .min_values(2) .required(true) ) - .get_matches_from(args); - - let files: Vec = matches - .values_of(ARG_FILES) - .map(|v| v.map(ToString::to_string).collect()) - .unwrap_or_default(); - - let overwrite_mode = determine_overwrite_mode(&matches); - let backup_mode = backup_control::determine_backup_mode( - matches.is_present(OPT_BACKUP_NO_ARG) || matches.is_present(OPT_BACKUP), - matches.value_of(OPT_BACKUP), - ); - - if overwrite_mode == OverwriteMode::NoClobber && backup_mode != BackupMode::NoBackup { - show_usage_error!("options --backup and --no-clobber are mutually exclusive"); - return 1; - } - - let backup_suffix = backup_control::determine_backup_suffix(matches.value_of(OPT_SUFFIX)); - - let behavior = Behavior { - overwrite: overwrite_mode, - backup: backup_mode, - suffix: backup_suffix, - update: matches.is_present(OPT_UPDATE), - target_dir: matches.value_of(OPT_TARGET_DIRECTORY).map(String::from), - no_target_dir: matches.is_present(OPT_NO_TARGET_DIRECTORY), - verbose: matches.is_present(OPT_VERBOSE), - }; - - let paths: Vec = { - fn strip_slashes(p: &Path) -> &Path { - p.components().as_path() - } - let to_owned = |p: &Path| p.to_owned(); - let paths = files.iter().map(Path::new); - - if matches.is_present(OPT_STRIP_TRAILING_SLASHES) { - paths.map(strip_slashes).map(to_owned).collect() - } else { - paths.map(to_owned).collect() - } - }; - - exec(&paths[..], behavior) } fn determine_overwrite_mode(matches: &ArgMatches) -> OverwriteMode { @@ -230,7 +238,7 @@ fn exec(files: &[PathBuf], b: Behavior) -> i32 { // lacks permission to access metadata. if source.symlink_metadata().is_err() { show_error!( - "cannot stat ‘{}’: No such file or directory", + "cannot stat '{}': No such file or directory", source.display() ); return 1; @@ -240,7 +248,7 @@ fn exec(files: &[PathBuf], b: Behavior) -> i32 { if b.no_target_dir { if !source.is_dir() { show_error!( - "cannot overwrite directory ‘{}’ with non-directory", + "cannot overwrite directory '{}' with non-directory", target.display() ); return 1; @@ -249,7 +257,7 @@ fn exec(files: &[PathBuf], b: Behavior) -> i32 { return match rename(source, target, &b) { Err(e) => { show_error!( - "cannot move ‘{}’ to ‘{}’: {}", + "cannot move '{}' to '{}': {}", source.display(), target.display(), e.to_string() @@ -263,7 +271,7 @@ fn exec(files: &[PathBuf], b: Behavior) -> i32 { return move_files_into_dir(&[source.clone()], target, &b); } else if target.exists() && source.is_dir() { show_error!( - "cannot overwrite non-directory ‘{}’ with directory ‘{}’", + "cannot overwrite non-directory '{}' with directory '{}'", target.display(), source.display() ); @@ -278,7 +286,7 @@ fn exec(files: &[PathBuf], b: Behavior) -> i32 { _ => { if b.no_target_dir { show_error!( - "mv: extra operand ‘{}’\n\ + "mv: extra operand '{}'\n\ Try '{} --help' for more information.", files[2].display(), executable!() @@ -294,7 +302,7 @@ fn exec(files: &[PathBuf], b: Behavior) -> i32 { fn move_files_into_dir(files: &[PathBuf], target_dir: &Path, b: &Behavior) -> i32 { if !target_dir.is_dir() { - show_error!("target ‘{}’ is not a directory", target_dir.display()); + show_error!("target '{}' is not a directory", target_dir.display()); return 1; } @@ -304,7 +312,7 @@ fn move_files_into_dir(files: &[PathBuf], target_dir: &Path, b: &Behavior) -> i3 Some(name) => target_dir.join(name), None => { show_error!( - "cannot stat ‘{}’: No such file or directory", + "cannot stat '{}': No such file or directory", sourcepath.display() ); @@ -315,7 +323,7 @@ fn move_files_into_dir(files: &[PathBuf], target_dir: &Path, b: &Behavior) -> i3 if let Err(e) = rename(sourcepath, &targetpath, b) { show_error!( - "cannot move ‘{}’ to ‘{}’: {}", + "cannot move '{}' to '{}': {}", sourcepath.display(), targetpath.display(), e.to_string() @@ -338,7 +346,7 @@ fn rename(from: &Path, to: &Path, b: &Behavior) -> io::Result<()> { match b.overwrite { OverwriteMode::NoClobber => return Ok(()), OverwriteMode::Interactive => { - println!("{}: overwrite ‘{}’? ", executable!(), to.display()); + println!("{}: overwrite '{}'? ", executable!(), to.display()); if !read_yes() { return Ok(()); } @@ -371,9 +379,9 @@ fn rename(from: &Path, to: &Path, b: &Behavior) -> io::Result<()> { rename_with_fallback(from, to)?; if b.verbose { - print!("‘{}’ -> ‘{}’", from.display(), to.display()); + print!("'{}' -> '{}'", from.display(), to.display()); match backup_path { - Some(path) => println!(" (backup: ‘{}’)", path.display()), + Some(path) => println!(" (backup: '{}')", path.display()), None => println!(), } } @@ -389,7 +397,7 @@ fn rename_with_fallback(from: &Path, to: &Path) -> io::Result<()> { let file_type = metadata.file_type(); if file_type.is_symlink() { - rename_symlink_fallback(&from, &to)?; + rename_symlink_fallback(from, to)?; } else if file_type.is_dir() { // We remove the destination directory if it exists to match the // behavior of `fs::rename`. As far as I can tell, `fs_extra`'s diff --git a/src/uu/nice/Cargo.toml b/src/uu/nice/Cargo.toml index 279e79ae3..eed524b8a 100644 --- a/src/uu/nice/Cargo.toml +++ b/src/uu/nice/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/nice.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } libc = "0.2.42" nix = { version="<=0.13" } uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } diff --git a/src/uu/nice/src/nice.rs b/src/uu/nice/src/nice.rs index 77baad0ca..d5a4094d1 100644 --- a/src/uu/nice/src/nice.rs +++ b/src/uu/nice/src/nice.rs @@ -46,20 +46,7 @@ process).", pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); - let matches = App::new(executable!()) - .setting(AppSettings::TrailingVarArg) - .version(crate_version!()) - .usage(&usage[..]) - .arg( - Arg::with_name(options::ADJUSTMENT) - .short("n") - .long(options::ADJUSTMENT) - .help("add N to the niceness (default is 10)") - .takes_value(true) - .allow_hyphen_values(true), - ) - .arg(Arg::with_name(options::COMMAND).multiple(true)) - .get_matches_from(args); + let matches = uu_app().usage(&usage[..]).get_matches_from(args); let mut niceness = unsafe { nix::errno::Errno::clear(); @@ -120,3 +107,18 @@ pub fn uumain(args: impl uucore::Args) -> i32 { 126 } } + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .setting(AppSettings::TrailingVarArg) + .version(crate_version!()) + .arg( + Arg::with_name(options::ADJUSTMENT) + .short("n") + .long(options::ADJUSTMENT) + .help("add N to the niceness (default is 10)") + .takes_value(true) + .allow_hyphen_values(true), + ) + .arg(Arg::with_name(options::COMMAND).multiple(true)) +} diff --git a/src/uu/nl/Cargo.toml b/src/uu/nl/Cargo.toml index a51a2555e..4197bfd8e 100644 --- a/src/uu/nl/Cargo.toml +++ b/src/uu/nl/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/nl.rs" [dependencies] -clap = "2.33.3" +clap = { version = "2.33", features = ["wrap_help"] } aho-corasick = "0.7.3" libc = "0.2.42" memchr = "2.2.0" diff --git a/src/uu/nl/src/nl.rs b/src/uu/nl/src/nl.rs index c062eedd9..81e76aa26 100644 --- a/src/uu/nl/src/nl.rs +++ b/src/uu/nl/src/nl.rs @@ -88,7 +88,62 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .collect_str(InvalidEncodingHandling::ConvertLossy) .accept_any(); - let matches = App::new(executable!()) + let matches = uu_app().get_matches_from(args); + + // A mutable settings object, initialized with the defaults. + let mut settings = Settings { + header_numbering: NumberingStyle::NumberForNone, + body_numbering: NumberingStyle::NumberForAll, + footer_numbering: NumberingStyle::NumberForNone, + section_delimiter: ['\\', ':'], + starting_line_number: 1, + line_increment: 1, + join_blank_lines: 1, + number_width: 6, + number_format: NumberFormat::Right, + renumber: true, + number_separator: String::from("\t"), + }; + + // Update the settings from the command line options, and terminate the + // program if some options could not successfully be parsed. + let parse_errors = helper::parse_options(&mut settings, &matches); + if !parse_errors.is_empty() { + show_error!("Invalid arguments supplied."); + for message in &parse_errors { + println!("{}", message); + } + return 1; + } + + let mut read_stdin = false; + let files: Vec = match matches.values_of(options::FILE) { + Some(v) => v.clone().map(|v| v.to_owned()).collect(), + None => vec!["-".to_owned()], + }; + + for file in &files { + if file == "-" { + // If both file names and '-' are specified, we choose to treat first all + // regular files, and then read from stdin last. + read_stdin = true; + continue; + } + let path = Path::new(file); + let reader = File::open(path).unwrap(); + let mut buffer = BufReader::new(reader); + nl(&mut buffer, &settings); + } + + if read_stdin { + let mut buffer = BufReader::new(stdin()); + nl(&mut buffer, &settings); + } + 0 +} + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) .name(NAME) .version(crate_version!()) .usage(USAGE) @@ -169,58 +224,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .help("use NUMBER columns for line numbers") .value_name("NUMBER"), ) - .get_matches_from(args); - - // A mutable settings object, initialized with the defaults. - let mut settings = Settings { - header_numbering: NumberingStyle::NumberForNone, - body_numbering: NumberingStyle::NumberForAll, - footer_numbering: NumberingStyle::NumberForNone, - section_delimiter: ['\\', ':'], - starting_line_number: 1, - line_increment: 1, - join_blank_lines: 1, - number_width: 6, - number_format: NumberFormat::Right, - renumber: true, - number_separator: String::from("\t"), - }; - - // Update the settings from the command line options, and terminate the - // program if some options could not successfully be parsed. - let parse_errors = helper::parse_options(&mut settings, &matches); - if !parse_errors.is_empty() { - show_error!("Invalid arguments supplied."); - for message in &parse_errors { - println!("{}", message); - } - return 1; - } - - let mut read_stdin = false; - let files: Vec = match matches.values_of(options::FILE) { - Some(v) => v.clone().map(|v| v.to_owned()).collect(), - None => vec!["-".to_owned()], - }; - - for file in &files { - if file == "-" { - // If both file names and '-' are specified, we choose to treat first all - // regular files, and then read from stdin last. - read_stdin = true; - continue; - } - let path = Path::new(file); - let reader = File::open(path).unwrap(); - let mut buffer = BufReader::new(reader); - nl(&mut buffer, &settings); - } - - if read_stdin { - let mut buffer = BufReader::new(stdin()); - nl(&mut buffer, &settings); - } - 0 } // nl implements the main functionality for an individual buffer. @@ -247,7 +250,7 @@ fn nl(reader: &mut BufReader, settings: &Settings) { let mut line_filter: fn(&str, ®ex::Regex) -> bool = pass_regex; for mut l in reader.lines().map(|r| r.unwrap()) { // Sanitize the string. We want to print the newline ourselves. - if !l.is_empty() && l.chars().rev().next().unwrap() == '\n' { + if l.ends_with('\n') { l.pop(); } // Next we iterate through the individual chars to see if this diff --git a/src/uu/nohup/Cargo.toml b/src/uu/nohup/Cargo.toml index 5bbbd9dff..f7166a4b6 100644 --- a/src/uu/nohup/Cargo.toml +++ b/src/uu/nohup/Cargo.toml @@ -15,8 +15,9 @@ edition = "2018" path = "src/nohup.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } libc = "0.2.42" +atty = "0.2" uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["fs"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/nohup/src/nohup.rs b/src/uu/nohup/src/nohup.rs index ea379ff49..acc101e4e 100644 --- a/src/uu/nohup/src/nohup.rs +++ b/src/uu/nohup/src/nohup.rs @@ -19,7 +19,6 @@ use std::fs::{File, OpenOptions}; use std::io::Error; use std::os::unix::prelude::*; use std::path::{Path, PathBuf}; -use uucore::fs::{is_stderr_interactive, is_stdin_interactive, is_stdout_interactive}; use uucore::InvalidEncodingHandling; static ABOUT: &str = "Run COMMAND ignoring hangup signals."; @@ -46,19 +45,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .collect_str(InvalidEncodingHandling::ConvertLossy) .accept_any(); - let matches = App::new(executable!()) - .version(crate_version!()) - .about(ABOUT) - .usage(&usage[..]) - .after_help(LONG_HELP) - .arg( - Arg::with_name(options::CMD) - .hidden(true) - .required(true) - .multiple(true), - ) - .setting(AppSettings::TrailingVarArg) - .get_matches_from(args); + let matches = uu_app().usage(&usage[..]).get_matches_from(args); replace_fds(); @@ -83,8 +70,22 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } } +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about(ABOUT) + .after_help(LONG_HELP) + .arg( + Arg::with_name(options::CMD) + .hidden(true) + .required(true) + .multiple(true), + ) + .setting(AppSettings::TrailingVarArg) +} + fn replace_fds() { - if is_stdin_interactive() { + if atty::is(atty::Stream::Stdin) { let new_stdin = match File::open(Path::new("/dev/null")) { Ok(t) => t, Err(e) => crash!(2, "Cannot replace STDIN: {}", e), @@ -94,7 +95,7 @@ fn replace_fds() { } } - if is_stdout_interactive() { + if atty::is(atty::Stream::Stdout) { let new_stdout = find_stdout(); let fd = new_stdout.as_raw_fd(); @@ -103,7 +104,7 @@ fn replace_fds() { } } - if is_stderr_interactive() && unsafe { dup2(1, 2) } != 2 { + if atty::is(atty::Stream::Stderr) && unsafe { dup2(1, 2) } != 2 { crash!(2, "Cannot replace STDERR: {}", Error::last_os_error()) } } diff --git a/src/uu/nproc/Cargo.toml b/src/uu/nproc/Cargo.toml index be9d8f2e3..a4eec07eb 100644 --- a/src/uu/nproc/Cargo.toml +++ b/src/uu/nproc/Cargo.toml @@ -17,7 +17,7 @@ path = "src/nproc.rs" [dependencies] libc = "0.2.42" num_cpus = "1.10" -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["fs"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/nproc/src/nproc.rs b/src/uu/nproc/src/nproc.rs index 13f1862d2..1f284685b 100644 --- a/src/uu/nproc/src/nproc.rs +++ b/src/uu/nproc/src/nproc.rs @@ -33,24 +33,7 @@ fn get_usage() -> String { pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); - let matches = App::new(executable!()) - .version(crate_version!()) - .about(ABOUT) - .usage(&usage[..]) - .arg( - Arg::with_name(OPT_ALL) - .short("") - .long(OPT_ALL) - .help("print the number of cores available to the system"), - ) - .arg( - Arg::with_name(OPT_IGNORE) - .short("") - .long(OPT_IGNORE) - .takes_value(true) - .help("ignore up to N cores"), - ) - .get_matches_from(args); + let matches = uu_app().usage(&usage[..]).get_matches_from(args); let mut ignore = match matches.value_of(OPT_IGNORE) { Some(numstr) => match numstr.parse() { @@ -86,6 +69,25 @@ pub fn uumain(args: impl uucore::Args) -> i32 { 0 } +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about(ABOUT) + .arg( + Arg::with_name(OPT_ALL) + .short("") + .long(OPT_ALL) + .help("print the number of cores available to the system"), + ) + .arg( + Arg::with_name(OPT_IGNORE) + .short("") + .long(OPT_IGNORE) + .takes_value(true) + .help("ignore up to N cores"), + ) +} + #[cfg(any( target_os = "linux", target_vendor = "apple", diff --git a/src/uu/numfmt/Cargo.toml b/src/uu/numfmt/Cargo.toml index ac5266d68..7a81e36d6 100644 --- a/src/uu/numfmt/Cargo.toml +++ b/src/uu/numfmt/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/numfmt.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/numfmt/src/format.rs b/src/uu/numfmt/src/format.rs index ebe380569..e44446818 100644 --- a/src/uu/numfmt/src/format.rs +++ b/src/uu/numfmt/src/format.rs @@ -1,7 +1,5 @@ -use crate::options::NumfmtOptions; -use crate::units::{ - DisplayableSuffix, RawSuffix, Result, Suffix, Transform, Unit, IEC_BASES, SI_BASES, -}; +use crate::options::{NumfmtOptions, RoundMethod}; +use crate::units::{DisplayableSuffix, RawSuffix, Result, Suffix, Unit, IEC_BASES, SI_BASES}; /// Iterate over a line's fields, where each field is a contiguous sequence of /// non-whitespace, optionally prefixed with one or more characters of leading @@ -62,7 +60,7 @@ impl<'a> Iterator for WhitespaceSplitter<'a> { fn parse_suffix(s: &str) -> Result<(f64, Option)> { if s.is_empty() { - return Err("invalid number: ‘’".to_string()); + return Err("invalid number: ''".to_string()); } let with_i = s.ends_with('i'); @@ -70,18 +68,18 @@ fn parse_suffix(s: &str) -> Result<(f64, Option)> { if with_i { iter.next_back(); } - let suffix: Option = match iter.next_back() { - Some('K') => Ok(Some((RawSuffix::K, with_i))), - Some('M') => Ok(Some((RawSuffix::M, with_i))), - Some('G') => Ok(Some((RawSuffix::G, with_i))), - Some('T') => Ok(Some((RawSuffix::T, with_i))), - Some('P') => Ok(Some((RawSuffix::P, with_i))), - Some('E') => Ok(Some((RawSuffix::E, with_i))), - Some('Z') => Ok(Some((RawSuffix::Z, with_i))), - Some('Y') => Ok(Some((RawSuffix::Y, with_i))), - Some('0'..='9') => Ok(None), - _ => Err(format!("invalid suffix in input: ‘{}’", s)), - }?; + let suffix = match iter.next_back() { + Some('K') => Some((RawSuffix::K, with_i)), + Some('M') => Some((RawSuffix::M, with_i)), + Some('G') => Some((RawSuffix::G, with_i)), + Some('T') => Some((RawSuffix::T, with_i)), + Some('P') => Some((RawSuffix::P, with_i)), + Some('E') => Some((RawSuffix::E, with_i)), + Some('Z') => Some((RawSuffix::Z, with_i)), + Some('Y') => Some((RawSuffix::Y, with_i)), + Some('0'..='9') => None, + _ => return Err(format!("invalid suffix in input: '{}'", s)), + }; let suffix_len = match suffix { None => 0, @@ -91,7 +89,7 @@ fn parse_suffix(s: &str) -> Result<(f64, Option)> { let number = s[..s.len() - suffix_len] .parse::() - .map_err(|_| format!("invalid number: ‘{}’", s))?; + .map_err(|_| format!("invalid number: '{}'", s))?; Ok((number, suffix)) } @@ -127,44 +125,50 @@ fn remove_suffix(i: f64, s: Option, u: &Unit) -> Result { } } -fn transform_from(s: &str, opts: &Transform) -> Result { +fn transform_from(s: &str, opts: &Unit) -> Result { let (i, suffix) = parse_suffix(s)?; - remove_suffix(i, suffix, &opts.unit).map(|n| if n < 0.0 { -n.abs().ceil() } else { n.ceil() }) + remove_suffix(i, suffix, opts).map(|n| if n < 0.0 { -n.abs().ceil() } else { n.ceil() }) } -/// Divide numerator by denominator, with ceiling. +/// Divide numerator by denominator, with rounding. /// -/// If the result of the division is less than 10.0, truncate the result -/// to the next highest tenth. +/// If the result of the division is less than 10.0, round to one decimal point. /// -/// Otherwise, truncate the result to the next highest whole number. +/// Otherwise, round to an integer. /// /// # Examples: /// /// ``` -/// use uu_numfmt::format::div_ceil; +/// use uu_numfmt::format::div_round; +/// use uu_numfmt::options::RoundMethod; /// -/// assert_eq!(div_ceil(1.01, 1.0), 1.1); -/// assert_eq!(div_ceil(999.1, 1000.), 1.0); -/// assert_eq!(div_ceil(1001., 10.), 101.); -/// assert_eq!(div_ceil(9991., 10.), 1000.); -/// assert_eq!(div_ceil(-12.34, 1.0), -13.0); -/// assert_eq!(div_ceil(1000.0, -3.14), -319.0); -/// assert_eq!(div_ceil(-271828.0, -271.0), 1004.0); +/// // Rounding methods: +/// assert_eq!(div_round(1.01, 1.0, RoundMethod::FromZero), 1.1); +/// assert_eq!(div_round(1.01, 1.0, RoundMethod::TowardsZero), 1.0); +/// assert_eq!(div_round(1.01, 1.0, RoundMethod::Up), 1.1); +/// assert_eq!(div_round(1.01, 1.0, RoundMethod::Down), 1.0); +/// assert_eq!(div_round(1.01, 1.0, RoundMethod::Nearest), 1.0); +/// +/// // Division: +/// assert_eq!(div_round(999.1, 1000.0, RoundMethod::FromZero), 1.0); +/// assert_eq!(div_round(1001., 10., RoundMethod::FromZero), 101.); +/// assert_eq!(div_round(9991., 10., RoundMethod::FromZero), 1000.); +/// assert_eq!(div_round(-12.34, 1.0, RoundMethod::FromZero), -13.0); +/// assert_eq!(div_round(1000.0, -3.14, RoundMethod::FromZero), -319.0); +/// assert_eq!(div_round(-271828.0, -271.0, RoundMethod::FromZero), 1004.0); /// ``` -pub fn div_ceil(n: f64, d: f64) -> f64 { - let v = n / (d / 10.0); - let (v, sign) = if v < 0.0 { (v.abs(), -1.0) } else { (v, 1.0) }; +pub fn div_round(n: f64, d: f64, method: RoundMethod) -> f64 { + let v = n / d; - if v < 100.0 { - v.ceil() / 10.0 * sign + if v.abs() < 10.0 { + method.round(10.0 * v) / 10.0 } else { - (v / 10.0).ceil() * sign + method.round(v) } } -fn consider_suffix(n: f64, u: &Unit) -> Result<(f64, Option)> { +fn consider_suffix(n: f64, u: &Unit, round_method: RoundMethod) -> Result<(f64, Option)> { use crate::units::RawSuffix::*; let abs_n = n.abs(); @@ -190,7 +194,7 @@ fn consider_suffix(n: f64, u: &Unit) -> Result<(f64, Option)> { _ => return Err("Number is too big and unsupported".to_string()), }; - let v = div_ceil(n, bases[i]); + let v = div_round(n, bases[i], round_method); // check if rounding pushed us into the next base if v.abs() >= bases[1] { @@ -200,8 +204,8 @@ fn consider_suffix(n: f64, u: &Unit) -> Result<(f64, Option)> { } } -fn transform_to(s: f64, opts: &Transform) -> Result { - let (i2, s) = consider_suffix(s, &opts.unit)?; +fn transform_to(s: f64, opts: &Unit, round_method: RoundMethod) -> Result { + let (i2, s) = consider_suffix(s, opts, round_method)?; Ok(match s { None => format!("{}", i2), Some(s) if i2.abs() < 10.0 => format!("{:.1}{}", i2, DisplayableSuffix(s)), @@ -217,10 +221,11 @@ fn format_string( let number = transform_to( transform_from(source, &options.transform.from)?, &options.transform.to, + options.round, )?; Ok(match implicit_padding.unwrap_or(options.padding) { - p if p == 0 => number, + 0 => number, p if p > 0 => format!("{:>padding$}", number, padding = p as usize), p => format!("{: Result<()> { } if field_selected { - print!("{}", format_string(&field.trim_start(), options, None)?); + print!("{}", format_string(field.trim_start(), options, None)?); } else { // print unselected field without conversion print!("{}", field); @@ -271,7 +276,7 @@ fn format_and_print_whitespace(s: &str, options: &NumfmtOptions) -> Result<()> { None }; - print!("{}", format_string(&field, options, implicit_padding)?); + print!("{}", format_string(field, options, implicit_padding)?); } else { // print unselected field without conversion print!("{}{}", prefix, field); diff --git a/src/uu/numfmt/src/numfmt.rs b/src/uu/numfmt/src/numfmt.rs index 086336437..01f12c51b 100644 --- a/src/uu/numfmt/src/numfmt.rs +++ b/src/uu/numfmt/src/numfmt.rs @@ -5,18 +5,20 @@ // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. +// spell-checker:ignore N'th M'th + #[macro_use] extern crate uucore; use crate::format::format_and_print; use crate::options::*; -use crate::units::{Result, Transform, Unit}; +use crate::units::{Result, Unit}; use clap::{crate_version, App, AppSettings, Arg, ArgMatches}; use std::io::{BufRead, Write}; use uucore::ranges::Range; pub mod format; -mod options; +pub mod options; mod units; static ABOUT: &str = "Convert numbers from/to human-readable strings"; @@ -92,10 +94,7 @@ fn parse_options(args: &ArgMatches) -> Result { let from = parse_unit(args.value_of(options::FROM).unwrap())?; let to = parse_unit(args.value_of(options::TO).unwrap())?; - let transform = TransformOptions { - from: Transform { unit: from }, - to: Transform { unit: to }, - }; + let transform = TransformOptions { from, to }; let padding = match args.value_of(options::PADDING) { Some(s) => s.parse::().map_err(|err| err.to_string()), @@ -114,17 +113,16 @@ fn parse_options(args: &ArgMatches) -> Result { 0 => Err(value), _ => Ok(n), }) - .map_err(|value| format!("invalid header value ‘{}’", value)) + .map_err(|value| format!("invalid header value '{}'", value)) } }?; - let fields = match args.value_of(options::FIELD) { - Some("-") => vec![Range { + let fields = match args.value_of(options::FIELD).unwrap() { + "-" => vec![Range { low: 1, high: std::usize::MAX, }], - Some(v) => Range::from_list(v)?, - None => unreachable!(), + v => Range::from_list(v)?, }; let delimiter = args.value_of(options::DELIMITER).map_or(Ok(None), |arg| { @@ -135,22 +133,51 @@ fn parse_options(args: &ArgMatches) -> Result { } })?; + // unwrap is fine because the argument has a default value + let round = match args.value_of(options::ROUND).unwrap() { + "up" => RoundMethod::Up, + "down" => RoundMethod::Down, + "from-zero" => RoundMethod::FromZero, + "towards-zero" => RoundMethod::TowardsZero, + "nearest" => RoundMethod::Nearest, + _ => unreachable!("Should be restricted by clap"), + }; + Ok(NumfmtOptions { transform, padding, header, fields, delimiter, + round, }) } pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); - let matches = App::new(executable!()) + let matches = uu_app().usage(&usage[..]).get_matches_from(args); + + let result = + parse_options(&matches).and_then(|options| match matches.values_of(options::NUMBER) { + Some(values) => handle_args(values, options), + None => handle_stdin(options), + }); + + match result { + Err(e) => { + std::io::stdout().flush().expect("error flushing stdout"); + show_error!("{}", e); + 1 + } + _ => 0, + } +} + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) .version(crate_version!()) .about(ABOUT) - .usage(&usage[..]) .after_help(LONG_HELP) .setting(AppSettings::AllowNegativeNumbers) .arg( @@ -203,21 +230,16 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .default_value(options::HEADER_DEFAULT) .hide_default_value(true), ) + .arg( + Arg::with_name(options::ROUND) + .long(options::ROUND) + .help( + "use METHOD for rounding when scaling; METHOD can be: up,\ + down, from-zero (default), towards-zero, nearest", + ) + .value_name("METHOD") + .default_value("from-zero") + .possible_values(&["up", "down", "from-zero", "towards-zero", "nearest"]), + ) .arg(Arg::with_name(options::NUMBER).hidden(true).multiple(true)) - .get_matches_from(args); - - let result = - parse_options(&matches).and_then(|options| match matches.values_of(options::NUMBER) { - Some(values) => handle_args(values, options), - None => handle_stdin(options), - }); - - match result { - Err(e) => { - std::io::stdout().flush().expect("error flushing stdout"); - show_error!("{}", e); - 1 - } - _ => 0, - } } diff --git a/src/uu/numfmt/src/options.rs b/src/uu/numfmt/src/options.rs index 17f0a6fbe..59bf9d8d3 100644 --- a/src/uu/numfmt/src/options.rs +++ b/src/uu/numfmt/src/options.rs @@ -1,4 +1,4 @@ -use crate::units::Transform; +use crate::units::Unit; use uucore::ranges::Range; pub const DELIMITER: &str = "delimiter"; @@ -10,12 +10,13 @@ pub const HEADER: &str = "header"; pub const HEADER_DEFAULT: &str = "1"; pub const NUMBER: &str = "NUMBER"; pub const PADDING: &str = "padding"; +pub const ROUND: &str = "round"; pub const TO: &str = "to"; pub const TO_DEFAULT: &str = "none"; pub struct TransformOptions { - pub from: Transform, - pub to: Transform, + pub from: Unit, + pub to: Unit, } pub struct NumfmtOptions { @@ -24,4 +25,38 @@ pub struct NumfmtOptions { pub header: usize, pub fields: Vec, pub delimiter: Option, + pub round: RoundMethod, +} + +#[derive(Clone, Copy)] +pub enum RoundMethod { + Up, + Down, + FromZero, + TowardsZero, + Nearest, +} + +impl RoundMethod { + pub fn round(&self, f: f64) -> f64 { + match self { + RoundMethod::Up => f.ceil(), + RoundMethod::Down => f.floor(), + RoundMethod::FromZero => { + if f < 0.0 { + f.floor() + } else { + f.ceil() + } + } + RoundMethod::TowardsZero => { + if f < 0.0 { + f.ceil() + } else { + f.floor() + } + } + RoundMethod::Nearest => f.round(), + } + } } diff --git a/src/uu/numfmt/src/units.rs b/src/uu/numfmt/src/units.rs index 5f9907bdf..8a2895ab7 100644 --- a/src/uu/numfmt/src/units.rs +++ b/src/uu/numfmt/src/units.rs @@ -24,10 +24,6 @@ pub enum Unit { None, } -pub struct Transform { - pub unit: Unit, -} - pub type Result = std::result::Result; #[derive(Clone, Copy, Debug)] diff --git a/src/uu/od/Cargo.toml b/src/uu/od/Cargo.toml index 6f9a75318..24da14b31 100644 --- a/src/uu/od/Cargo.toml +++ b/src/uu/od/Cargo.toml @@ -16,7 +16,7 @@ path = "src/od.rs" [dependencies] byteorder = "1.3.2" -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } half = "1.6" libc = "0.2.42" uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } diff --git a/src/uu/od/src/formatteriteminfo.rs b/src/uu/od/src/formatteriteminfo.rs index d44d97a92..13cf62246 100644 --- a/src/uu/od/src/formatteriteminfo.rs +++ b/src/uu/od/src/formatteriteminfo.rs @@ -2,6 +2,7 @@ use std::fmt; +#[allow(clippy::enum_variant_names)] #[derive(Copy)] pub enum FormatWriter { IntWriter(fn(u64) -> String), diff --git a/src/uu/od/src/inputdecoder.rs b/src/uu/od/src/inputdecoder.rs index f6ba59885..606495461 100644 --- a/src/uu/od/src/inputdecoder.rs +++ b/src/uu/od/src/inputdecoder.rs @@ -115,7 +115,7 @@ impl<'a> MemoryDecoder<'a> { /// Creates a clone of the internal buffer. The clone only contain the valid data. pub fn clone_buffer(&self, other: &mut Vec) { - other.clone_from(&self.data); + other.clone_from(self.data); other.resize(self.used_normal_length, 0); } diff --git a/src/uu/od/src/od.rs b/src/uu/od/src/od.rs index 33303f0fc..ec5bb595a 100644 --- a/src/uu/od/src/od.rs +++ b/src/uu/od/src/od.rs @@ -43,6 +43,7 @@ use crate::partialreader::*; use crate::peekreader::*; use crate::prn_char::format_ascii_dump; use clap::{self, crate_version, AppSettings, Arg, ArgMatches}; +use uucore::parse_size::ParseSizeError; use uucore::InvalidEncodingHandling; const PEEK_BUFFER_SIZE: usize = 4; // utf-8 can be 4 bytes @@ -128,42 +129,34 @@ impl OdOptions { } }; - let mut skip_bytes = match matches.value_of(options::SKIP_BYTES) { - None => 0, - Some(s) => match parse_number_of_bytes(&s) { - Ok(i) => i, - Err(_) => { - return Err(format!("Invalid argument --skip-bytes={}", s)); - } - }, - }; + let mut skip_bytes = matches.value_of(options::SKIP_BYTES).map_or(0, |s| { + parse_number_of_bytes(s).unwrap_or_else(|e| { + crash!(1, "{}", format_error_message(e, s, options::SKIP_BYTES)) + }) + }); let mut label: Option = None; - let input_strings = match parse_inputs(&matches) { - Ok(CommandLineInputs::FileNames(v)) => v, - Ok(CommandLineInputs::FileAndOffset((f, s, l))) => { + let parsed_input = parse_inputs(&matches).map_err(|e| format!("Invalid inputs: {}", e))?; + let input_strings = match parsed_input { + CommandLineInputs::FileNames(v) => v, + CommandLineInputs::FileAndOffset((f, s, l)) => { skip_bytes = s; label = l; vec![f] } - Err(e) => { - return Err(format!("Invalid inputs: {}", e)); - } }; - let formats = match parse_format_flags(&args) { - Ok(f) => f, - Err(e) => { - return Err(e); - } - }; + let formats = parse_format_flags(&args)?; + + let mut line_bytes = matches.value_of(options::WIDTH).map_or(16, |s| { + if matches.occurrences_of(options::WIDTH) == 0 { + return 16; + }; + parse_number_of_bytes(s) + .unwrap_or_else(|e| crash!(1, "{}", format_error_message(e, s, options::WIDTH))) + }); - let mut line_bytes = match matches.value_of(options::WIDTH) { - None => 16, - Some(_) if matches.occurrences_of(options::WIDTH) == 0 => 16, - Some(s) => s.parse::().unwrap_or(0), - }; let min_bytes = formats.iter().fold(1, |max, next| { cmp::max(max, next.formatter_item_info.byte_size) }); @@ -174,15 +167,11 @@ impl OdOptions { let output_duplicates = matches.is_present(options::OUTPUT_DUPLICATES); - let read_bytes = match matches.value_of(options::READ_BYTES) { - None => None, - Some(s) => match parse_number_of_bytes(&s) { - Ok(i) => Some(i), - Err(_) => { - return Err(format!("Invalid argument --read-bytes={}", s)); - } - }, - }; + let read_bytes = matches.value_of(options::READ_BYTES).map(|s| { + parse_number_of_bytes(s).unwrap_or_else(|e| { + crash!(1, "{}", format_error_message(e, s, options::READ_BYTES)) + }) + }); let radix = match matches.value_of(options::ADDRESS_RADIX) { None => Radix::Octal, @@ -225,7 +214,45 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .collect_str(InvalidEncodingHandling::Ignore) .accept_any(); - let clap_opts = clap::App::new(executable!()) + let clap_opts = uu_app(); + + let clap_matches = clap_opts + .clone() // Clone to reuse clap_opts to print help + .get_matches_from(args.clone()); + + let od_options = match OdOptions::new(clap_matches, args) { + Err(s) => { + crash!(1, "{}", s); + } + Ok(o) => o, + }; + + let mut input_offset = + InputOffset::new(od_options.radix, od_options.skip_bytes, od_options.label); + + let mut input = open_input_peek_reader( + &od_options.input_strings, + od_options.skip_bytes, + od_options.read_bytes, + ); + let mut input_decoder = InputDecoder::new( + &mut input, + od_options.line_bytes, + PEEK_BUFFER_SIZE, + od_options.byte_order, + ); + + let output_info = OutputInfo::new( + od_options.line_bytes, + &od_options.formats[..], + od_options.output_duplicates, + ); + + odfunc(&mut input_offset, &mut input_decoder, &output_info) +} + +pub fn uu_app() -> clap::App<'static, 'static> { + clap::App::new(executable!()) .version(crate_version!()) .about(ABOUT) .usage(USAGE) @@ -263,7 +290,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .short("S") .long(options::STRINGS) .help( - "output strings of at least BYTES graphic chars. 3 is assumed when \ + "NotImplemented: output strings of at least BYTES graphic chars. 3 is assumed when \ BYTES is not specified.", ) .default_value("3") @@ -445,42 +472,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { AppSettings::DontDelimitTrailingValues, AppSettings::DisableVersion, AppSettings::DeriveDisplayOrder, - ]); - - let clap_matches = clap_opts - .clone() // Clone to reuse clap_opts to print help - .get_matches_from(args.clone()); - - let od_options = match OdOptions::new(clap_matches, args) { - Err(s) => { - show_usage_error!("{}", s); - return 1; - } - Ok(o) => o, - }; - - let mut input_offset = - InputOffset::new(od_options.radix, od_options.skip_bytes, od_options.label); - - let mut input = open_input_peek_reader( - &od_options.input_strings, - od_options.skip_bytes, - od_options.read_bytes, - ); - let mut input_decoder = InputDecoder::new( - &mut input, - od_options.line_bytes, - PEEK_BUFFER_SIZE, - od_options.byte_order, - ); - - let output_info = OutputInfo::new( - od_options.line_bytes, - &od_options.formats[..], - od_options.output_duplicates, - ); - - odfunc(&mut input_offset, &mut input_decoder, &output_info) + ]) } /// Loops through the input line by line, calling print_bytes to take care of the output. @@ -537,7 +529,7 @@ where print_bytes( &input_offset.format_byte_offset(), &memory_decoder, - &output_info, + output_info, ); } @@ -636,3 +628,13 @@ fn open_input_peek_reader( let pr = PartialReader::new(mf, skip_bytes, read_bytes); PeekReader::new(pr) } + +fn format_error_message(error: ParseSizeError, s: &str, option: &str) -> String { + // NOTE: + // GNU's od echos affected flag, -N or --read-bytes (-j or --skip-bytes, etc.), depending user's selection + // GNU's od does distinguish between "invalid (suffix in) argument" + match error { + ParseSizeError::ParseFailure(_) => format!("invalid --{} argument '{}'", option, s), + ParseSizeError::SizeTooBig(_) => format!("--{} argument '{}' too large", option, s), + } +} diff --git a/src/uu/od/src/output_info.rs b/src/uu/od/src/output_info.rs index a204fa36e..49c2a09a2 100644 --- a/src/uu/od/src/output_info.rs +++ b/src/uu/od/src/output_info.rs @@ -68,7 +68,7 @@ impl OutputInfo { let print_width_line = print_width_block * (line_bytes / byte_size_block); let spaced_formatters = - OutputInfo::create_spaced_formatter_info(&formats, byte_size_block, print_width_block); + OutputInfo::create_spaced_formatter_info(formats, byte_size_block, print_width_block); OutputInfo { byte_size_line: line_bytes, diff --git a/src/uu/od/src/parse_formats.rs b/src/uu/od/src/parse_formats.rs index abf05ea18..f5b150d61 100644 --- a/src/uu/od/src/parse_formats.rs +++ b/src/uu/od/src/parse_formats.rs @@ -108,10 +108,8 @@ pub fn parse_format_flags(args: &[String]) -> Result formats.extend(v.into_iter()), - Err(e) => return Err(e), - } + let v = parse_type_string(arg)?; + formats.extend(v.into_iter()); expect_type_string = false; } else if arg.starts_with("--") { if arg.len() == 2 { @@ -119,10 +117,8 @@ pub fn parse_format_flags(args: &[String]) -> Result Result formats.extend(v.into_iter()), - Err(e) => return Err(e), - } + let v = parse_type_string(&format_spec)?; + formats.extend(v.into_iter()); expect_type_string = false; } } @@ -275,17 +269,13 @@ fn parse_type_string(params: &str) -> Result, Strin let mut chars = params.chars(); let mut ch = chars.next(); - while ch.is_some() { - let type_char = ch.unwrap(); - let type_char = match format_type(type_char) { - Some(t) => t, - None => { - return Err(format!( - "unexpected char '{}' in format specification '{}'", - type_char, params - )); - } - }; + while let Some(type_char) = ch { + let type_char = format_type(type_char).ok_or_else(|| { + format!( + "unexpected char '{}' in format specification '{}'", + type_char, params + ) + })?; let type_cat = format_type_category(type_char); @@ -301,30 +291,25 @@ fn parse_type_string(params: &str) -> Result, Strin ch = chars.next(); } if !decimal_size.is_empty() { - byte_size = match decimal_size.parse() { - Err(_) => { - return Err(format!( - "invalid number '{}' in format specification '{}'", - decimal_size, params - )) - } - Ok(n) => n, - } + byte_size = decimal_size.parse().map_err(|_| { + format!( + "invalid number '{}' in format specification '{}'", + decimal_size, params + ) + })?; } } if is_format_dump_char(ch, &mut show_ascii_dump) { ch = chars.next(); } - match od_format_type(type_char, byte_size) { - Some(ft) => formats.push(ParsedFormatterItemInfo::new(ft, show_ascii_dump)), - None => { - return Err(format!( - "invalid size '{}' in format specification '{}'", - byte_size, params - )) - } - } + let ft = od_format_type(type_char, byte_size).ok_or_else(|| { + format!( + "invalid size '{}' in format specification '{}'", + byte_size, params + ) + })?; + formats.push(ParsedFormatterItemInfo::new(ft, show_ascii_dump)); } Ok(formats) @@ -335,16 +320,13 @@ pub fn parse_format_flags_str( args_str: &Vec<&'static str>, ) -> Result, String> { let args: Vec = args_str.iter().map(|s| s.to_string()).collect(); - match parse_format_flags(&args) { - Err(e) => Err(e), - Ok(v) => { - // tests using this function assume add_ascii_dump is not set - Ok(v.into_iter() - .inspect(|f| assert!(!f.add_ascii_dump)) - .map(|f| f.formatter_item_info) - .collect()) - } - } + parse_format_flags(&args).map(|v| { + // tests using this function assume add_ascii_dump is not set + v.into_iter() + .inspect(|f| assert!(!f.add_ascii_dump)) + .map(|f| f.formatter_item_info) + .collect() + }) } #[test] diff --git a/src/uu/od/src/parse_inputs.rs b/src/uu/od/src/parse_inputs.rs index 288c0870f..419b7173d 100644 --- a/src/uu/od/src/parse_inputs.rs +++ b/src/uu/od/src/parse_inputs.rs @@ -55,7 +55,7 @@ pub fn parse_inputs(matches: &dyn CommandLineOpts) -> Result) -> Result Ok(CommandLineInputs::FileNames(vec!["-".to_string()])), 1 => { - let offset0 = parse_offset_operand(&input_strings[0]); + let offset0 = parse_offset_operand(input_strings[0]); Ok(match offset0 { Ok(n) => CommandLineInputs::FileAndOffset(("-".to_string(), n, None)), _ => CommandLineInputs::FileNames( @@ -97,8 +97,8 @@ pub fn parse_inputs_traditional(input_strings: Vec<&str>) -> Result { - let offset0 = parse_offset_operand(&input_strings[0]); - let offset1 = parse_offset_operand(&input_strings[1]); + let offset0 = parse_offset_operand(input_strings[0]); + let offset1 = parse_offset_operand(input_strings[1]); match (offset0, offset1) { (Ok(n), Ok(m)) => Ok(CommandLineInputs::FileAndOffset(( "-".to_string(), @@ -114,8 +114,8 @@ pub fn parse_inputs_traditional(input_strings: Vec<&str>) -> Result { - let offset = parse_offset_operand(&input_strings[1]); - let label = parse_offset_operand(&input_strings[2]); + let offset = parse_offset_operand(input_strings[1]); + let label = parse_offset_operand(input_strings[2]); match (offset, label) { (Ok(n), Ok(m)) => Ok(CommandLineInputs::FileAndOffset(( input_strings[0].to_string(), diff --git a/src/uu/od/src/parse_nrofbytes.rs b/src/uu/od/src/parse_nrofbytes.rs index d2ba1527b..d6329c60a 100644 --- a/src/uu/od/src/parse_nrofbytes.rs +++ b/src/uu/od/src/parse_nrofbytes.rs @@ -1,14 +1,17 @@ -pub fn parse_number_of_bytes(s: &str) -> Result { +use uucore::parse_size::{parse_size, ParseSizeError}; + +pub fn parse_number_of_bytes(s: &str) -> Result { let mut start = 0; let mut len = s.len(); - let mut radix = 10; + let mut radix = 16; let mut multiply = 1; if s.starts_with("0x") || s.starts_with("0X") { start = 2; - radix = 16; } else if s.starts_with('0') { radix = 8; + } else { + return parse_size(&s[start..]); } let mut ends_with = s.chars().rev(); @@ -56,78 +59,33 @@ pub fn parse_number_of_bytes(s: &str) -> Result { Some('P') => 1000 * 1000 * 1000 * 1000 * 1000, #[cfg(target_pointer_width = "64")] Some('E') => 1000 * 1000 * 1000 * 1000 * 1000 * 1000, - _ => return Err("parse failed"), + _ => return Err(ParseSizeError::ParseFailure(s.to_string())), } } _ => {} } - match usize::from_str_radix(&s[start..len], radix) { - Ok(i) => Ok(i * multiply), - Err(_) => Err("parse failed"), - } -} - -#[allow(dead_code)] -fn parse_number_of_bytes_str(s: &str) -> Result { - parse_number_of_bytes(&String::from(s)) + let factor = match usize::from_str_radix(&s[start..len], radix) { + Ok(f) => f, + Err(e) => return Err(ParseSizeError::ParseFailure(e.to_string())), + }; + factor + .checked_mul(multiply) + .ok_or_else(|| ParseSizeError::SizeTooBig(s.to_string())) } #[test] fn test_parse_number_of_bytes() { - // normal decimal numbers - assert_eq!(0, parse_number_of_bytes_str("0").unwrap()); - assert_eq!(5, parse_number_of_bytes_str("5").unwrap()); - assert_eq!(999, parse_number_of_bytes_str("999").unwrap()); - assert_eq!(2 * 512, parse_number_of_bytes_str("2b").unwrap()); - assert_eq!(2 * 1024, parse_number_of_bytes_str("2k").unwrap()); - assert_eq!(4 * 1024, parse_number_of_bytes_str("4K").unwrap()); - assert_eq!(2 * 1048576, parse_number_of_bytes_str("2m").unwrap()); - assert_eq!(4 * 1048576, parse_number_of_bytes_str("4M").unwrap()); - assert_eq!(1073741824, parse_number_of_bytes_str("1G").unwrap()); - assert_eq!(2000, parse_number_of_bytes_str("2kB").unwrap()); - assert_eq!(4000, parse_number_of_bytes_str("4KB").unwrap()); - assert_eq!(2000000, parse_number_of_bytes_str("2mB").unwrap()); - assert_eq!(4000000, parse_number_of_bytes_str("4MB").unwrap()); - assert_eq!(2000000000, parse_number_of_bytes_str("2GB").unwrap()); - // octal input - assert_eq!(8, parse_number_of_bytes_str("010").unwrap()); - assert_eq!(8 * 512, parse_number_of_bytes_str("010b").unwrap()); - assert_eq!(8 * 1024, parse_number_of_bytes_str("010k").unwrap()); - assert_eq!(8 * 1048576, parse_number_of_bytes_str("010m").unwrap()); + assert_eq!(8, parse_number_of_bytes("010").unwrap()); + assert_eq!(8 * 512, parse_number_of_bytes("010b").unwrap()); + assert_eq!(8 * 1024, parse_number_of_bytes("010k").unwrap()); + assert_eq!(8 * 1_048_576, parse_number_of_bytes("010m").unwrap()); // hex input - assert_eq!(15, parse_number_of_bytes_str("0xf").unwrap()); - assert_eq!(15, parse_number_of_bytes_str("0XF").unwrap()); - assert_eq!(27, parse_number_of_bytes_str("0x1b").unwrap()); - assert_eq!(16 * 1024, parse_number_of_bytes_str("0x10k").unwrap()); - assert_eq!(16 * 1048576, parse_number_of_bytes_str("0x10m").unwrap()); - - // invalid input - parse_number_of_bytes_str("").unwrap_err(); - parse_number_of_bytes_str("-1").unwrap_err(); - parse_number_of_bytes_str("1e2").unwrap_err(); - parse_number_of_bytes_str("xyz").unwrap_err(); - parse_number_of_bytes_str("b").unwrap_err(); - parse_number_of_bytes_str("1Y").unwrap_err(); - parse_number_of_bytes_str("∞").unwrap_err(); -} - -#[test] -#[cfg(target_pointer_width = "64")] -fn test_parse_number_of_bytes_64bits() { - assert_eq!(1099511627776, parse_number_of_bytes_str("1T").unwrap()); - assert_eq!(1125899906842624, parse_number_of_bytes_str("1P").unwrap()); - assert_eq!( - 1152921504606846976, - parse_number_of_bytes_str("1E").unwrap() - ); - - assert_eq!(2000000000000, parse_number_of_bytes_str("2TB").unwrap()); - assert_eq!(2000000000000000, parse_number_of_bytes_str("2PB").unwrap()); - assert_eq!( - 2000000000000000000, - parse_number_of_bytes_str("2EB").unwrap() - ); + assert_eq!(15, parse_number_of_bytes("0xf").unwrap()); + assert_eq!(15, parse_number_of_bytes("0XF").unwrap()); + assert_eq!(27, parse_number_of_bytes("0x1b").unwrap()); + assert_eq!(16 * 1024, parse_number_of_bytes("0x10k").unwrap()); + assert_eq!(16 * 1_048_576, parse_number_of_bytes("0x10m").unwrap()); } diff --git a/src/uu/od/src/partialreader.rs b/src/uu/od/src/partialreader.rs index ee3588830..f155a7bd2 100644 --- a/src/uu/od/src/partialreader.rs +++ b/src/uu/od/src/partialreader.rs @@ -36,16 +36,15 @@ impl Read for PartialReader { while self.skip > 0 { let skip_count = cmp::min(self.skip, MAX_SKIP_BUFFER); - match self.inner.read(&mut bytes[..skip_count]) { - Ok(0) => { + match self.inner.read(&mut bytes[..skip_count])? { + 0 => { // this is an error as we still have more to skip return Err(io::Error::new( io::ErrorKind::UnexpectedEof, "tried to skip past end of input", )); } - Ok(n) => self.skip -= n, - Err(e) => return Err(e), + n => self.skip -= n, } } } diff --git a/src/uu/paste/Cargo.toml b/src/uu/paste/Cargo.toml index 4e9971368..cfc70a3bd 100644 --- a/src/uu/paste/Cargo.toml +++ b/src/uu/paste/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/paste.rs" [dependencies] -clap = "2.33.3" +clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/paste/src/paste.rs b/src/uu/paste/src/paste.rs index f2fa3c81c..7f7969687 100644 --- a/src/uu/paste/src/paste.rs +++ b/src/uu/paste/src/paste.rs @@ -37,7 +37,22 @@ fn read_line( } pub fn uumain(args: impl uucore::Args) -> i32 { - let matches = App::new(executable!()) + let matches = uu_app().get_matches_from(args); + + let serial = matches.is_present(options::SERIAL); + let delimiters = matches.value_of(options::DELIMITER).unwrap().to_owned(); + let files = matches + .values_of(options::FILE) + .unwrap() + .map(|s| s.to_owned()) + .collect(); + paste(files, serial, delimiters); + + 0 +} + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) .version(crate_version!()) .about(ABOUT) .arg( @@ -61,18 +76,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .multiple(true) .default_value("-"), ) - .get_matches_from(args); - - let serial = matches.is_present(options::SERIAL); - let delimiters = matches.value_of(options::DELIMITER).unwrap().to_owned(); - let files = matches - .values_of(options::FILE) - .unwrap() - .map(|s| s.to_owned()) - .collect(); - paste(files, serial, delimiters); - - 0 } fn paste(filenames: Vec, serial: bool, delimiters: String) { diff --git a/src/uu/pathchk/Cargo.toml b/src/uu/pathchk/Cargo.toml index 8c4e61d2b..c39eb6e16 100644 --- a/src/uu/pathchk/Cargo.toml +++ b/src/uu/pathchk/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/pathchk.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } libc = "0.2.42" uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/pathchk/src/pathchk.rs b/src/uu/pathchk/src/pathchk.rs index 9667e0ba1..335266456 100644 --- a/src/uu/pathchk/src/pathchk.rs +++ b/src/uu/pathchk/src/pathchk.rs @@ -49,27 +49,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .collect_str(InvalidEncodingHandling::ConvertLossy) .accept_any(); - let matches = App::new(executable!()) - .version(crate_version!()) - .about(ABOUT) - .usage(&usage[..]) - .arg( - Arg::with_name(options::POSIX) - .short("p") - .help("check for most POSIX systems"), - ) - .arg( - Arg::with_name(options::POSIX_SPECIAL) - .short("P") - .help(r#"check for empty names and leading "-""#), - ) - .arg( - Arg::with_name(options::PORTABILITY) - .long(options::PORTABILITY) - .help("check for all POSIX systems (equivalent to -p -P)"), - ) - .arg(Arg::with_name(options::PATH).hidden(true).multiple(true)) - .get_matches_from(args); + let matches = uu_app().usage(&usage[..]).get_matches_from(args); // set working mode let is_posix = matches.values_of(options::POSIX).is_some(); @@ -115,13 +95,35 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } } +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about(ABOUT) + .arg( + Arg::with_name(options::POSIX) + .short("p") + .help("check for most POSIX systems"), + ) + .arg( + Arg::with_name(options::POSIX_SPECIAL) + .short("P") + .help(r#"check for empty names and leading "-""#), + ) + .arg( + Arg::with_name(options::PORTABILITY) + .long(options::PORTABILITY) + .help("check for all POSIX systems (equivalent to -p -P)"), + ) + .arg(Arg::with_name(options::PATH).hidden(true).multiple(true)) +} + // check a path, given as a slice of it's components and an operating mode fn check_path(mode: &Mode, path: &[String]) -> bool { match *mode { - Mode::Basic => check_basic(&path), - Mode::Extra => check_default(&path) && check_extra(&path), - Mode::Both => check_basic(&path) && check_extra(&path), - _ => check_default(&path), + Mode::Basic => check_basic(path), + Mode::Extra => check_default(path) && check_extra(path), + Mode::Both => check_basic(path) && check_extra(path), + _ => check_default(path), } } @@ -156,7 +158,7 @@ fn check_basic(path: &[String]) -> bool { ); return false; } - if !check_portable_chars(&p) { + if !check_portable_chars(p) { return false; } } @@ -168,7 +170,7 @@ fn check_basic(path: &[String]) -> bool { fn check_extra(path: &[String]) -> bool { // components: leading hyphens for p in path { - if !no_leading_hyphen(&p) { + if !no_leading_hyphen(p) { writeln!( &mut std::io::stderr(), "leading hyphen in file name component '{}'", @@ -241,13 +243,14 @@ fn no_leading_hyphen(path_segment: &str) -> bool { // check whether a path segment contains only valid (read: portable) characters fn check_portable_chars(path_segment: &str) -> bool { - let valid_str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-".to_string(); - for ch in path_segment.chars() { - if !valid_str.contains(ch) { + const VALID_CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-"; + for (i, ch) in path_segment.as_bytes().iter().enumerate() { + if !VALID_CHARS.contains(ch) { + let invalid = path_segment[i..].chars().next().unwrap(); writeln!( &mut std::io::stderr(), "nonportable character '{}' in file name component '{}'", - ch, + invalid, path_segment ); return false; diff --git a/src/uu/pinky/Cargo.toml b/src/uu/pinky/Cargo.toml index a3c36259a..2cdb28d92 100644 --- a/src/uu/pinky/Cargo.toml +++ b/src/uu/pinky/Cargo.toml @@ -17,7 +17,7 @@ path = "src/pinky.rs" [dependencies] uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["utmpx", "entries"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } -clap = "2.33.3" +clap = { version = "2.33", features = ["wrap_help"] } [[bin]] name = "pinky" diff --git a/src/uu/pinky/src/pinky.rs b/src/uu/pinky/src/pinky.rs index 27dcc2421..16bcfd3c9 100644 --- a/src/uu/pinky/src/pinky.rs +++ b/src/uu/pinky/src/pinky.rs @@ -60,62 +60,9 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); let after_help = get_long_usage(); - let matches = App::new(executable!()) - .version(crate_version!()) - .about(ABOUT) + let matches = uu_app() .usage(&usage[..]) .after_help(&after_help[..]) - .arg( - Arg::with_name(options::LONG_FORMAT) - .short("l") - .requires(options::USER) - .help("produce long format output for the specified USERs"), - ) - .arg( - Arg::with_name(options::OMIT_HOME_DIR) - .short("b") - .help("omit the user's home directory and shell in long format"), - ) - .arg( - Arg::with_name(options::OMIT_PROJECT_FILE) - .short("h") - .help("omit the user's project file in long format"), - ) - .arg( - Arg::with_name(options::OMIT_PLAN_FILE) - .short("p") - .help("omit the user's plan file in long format"), - ) - .arg( - Arg::with_name(options::SHORT_FORMAT) - .short("s") - .help("do short format output, this is the default"), - ) - .arg( - Arg::with_name(options::OMIT_HEADINGS) - .short("f") - .help("omit the line of column headings in short format"), - ) - .arg( - Arg::with_name(options::OMIT_NAME) - .short("w") - .help("omit the user's full name in short format"), - ) - .arg( - Arg::with_name(options::OMIT_NAME_HOST) - .short("i") - .help("omit the user's full name and remote host in short format"), - ) - .arg( - Arg::with_name(options::OMIT_NAME_HOST_TIME) - .short("q") - .help("omit the user's full name, remote host and idle time in short format"), - ) - .arg( - Arg::with_name(options::USER) - .takes_value(true) - .multiple(true), - ) .get_matches_from(args); let users: Vec = matches @@ -182,6 +129,63 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } } +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about(ABOUT) + .arg( + Arg::with_name(options::LONG_FORMAT) + .short("l") + .requires(options::USER) + .help("produce long format output for the specified USERs"), + ) + .arg( + Arg::with_name(options::OMIT_HOME_DIR) + .short("b") + .help("omit the user's home directory and shell in long format"), + ) + .arg( + Arg::with_name(options::OMIT_PROJECT_FILE) + .short("h") + .help("omit the user's project file in long format"), + ) + .arg( + Arg::with_name(options::OMIT_PLAN_FILE) + .short("p") + .help("omit the user's plan file in long format"), + ) + .arg( + Arg::with_name(options::SHORT_FORMAT) + .short("s") + .help("do short format output, this is the default"), + ) + .arg( + Arg::with_name(options::OMIT_HEADINGS) + .short("f") + .help("omit the line of column headings in short format"), + ) + .arg( + Arg::with_name(options::OMIT_NAME) + .short("w") + .help("omit the user's full name in short format"), + ) + .arg( + Arg::with_name(options::OMIT_NAME_HOST) + .short("i") + .help("omit the user's full name and remote host in short format"), + ) + .arg( + Arg::with_name(options::OMIT_NAME_HOST_TIME) + .short("q") + .help("omit the user's full name, remote host and idle time in short format"), + ) + .arg( + Arg::with_name(options::USER) + .takes_value(true) + .multiple(true), + ) +} + struct Pinky { include_idle: bool, include_heading: bool, @@ -234,7 +238,7 @@ fn idle_string(when: i64) -> String { } fn time_string(ut: &Utmpx) -> String { - time::strftime("%Y-%m-%d %H:%M", &ut.login_time()).unwrap() + time::strftime("%b %e %H:%M", &ut.login_time()).unwrap() // LC_ALL=C } impl Pinky { @@ -283,7 +287,7 @@ impl Pinky { } } - print!(" {}", time_string(&ut)); + print!(" {}", time_string(ut)); let mut s = ut.host(); if self.include_where && !s.is_empty() { diff --git a/src/uu/pr/Cargo.toml b/src/uu/pr/Cargo.toml index 6d9ec2304..122bed694 100644 --- a/src/uu/pr/Cargo.toml +++ b/src/uu/pr/Cargo.toml @@ -15,6 +15,7 @@ edition = "2018" path = "src/pr.rs" [dependencies] +clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.7", package="uucore", path="../../uucore", features=["utmpx", "entries"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } getopts = "0.2.21" diff --git a/src/uu/pr/src/pr.rs b/src/uu/pr/src/pr.rs index 486cedc00..d6b9e8ca3 100644 --- a/src/uu/pr/src/pr.rs +++ b/src/uu/pr/src/pr.rs @@ -23,6 +23,7 @@ use std::fs::{metadata, File}; use std::io::{stdin, stdout, BufRead, BufReader, Lines, Read, Stdout, Write}; #[cfg(unix)] use std::os::unix::fs::FileTypeExt; +use uucore::executable; type IOError = std::io::Error; @@ -167,6 +168,11 @@ quick_error! { } } +pub fn uu_app() -> clap::App<'static, 'static> { + // TODO: migrate to clap to get more shell completions + clap::App::new(executable!()) +} + pub fn uumain(args: impl uucore::Args) -> i32 { let args = args .collect_str(uucore::InvalidEncodingHandling::Ignore) @@ -401,18 +407,18 @@ pub fn uumain(args: impl uucore::Args) -> i32 { for file_group in file_groups { let result_options = build_options(&matches, &file_group, args.join(" ")); + let options = match result_options { + Ok(options) => options, + Err(err) => { + print_error(&matches, err); + return 1; + } + }; - if result_options.is_err() { - print_error(&matches, result_options.err().unwrap()); - return 1; - } - - let options = &result_options.unwrap(); - - let cmd_result = if file_group.len() == 1 { - pr(&file_group.get(0).unwrap(), options) + let cmd_result = if let Ok(group) = file_group.iter().exactly_one() { + pr(group, &options) } else { - mpr(&file_group, options) + mpr(&file_group, &options) }; let status = match cmd_result { @@ -442,11 +448,12 @@ fn recreate_arguments(args: &[String]) -> Vec { let mut arguments = args.to_owned(); let num_option = args.iter().find_position(|x| n_regex.is_match(x.trim())); if let Some((pos, _value)) = num_option { - let num_val_opt = args.get(pos + 1); - if num_val_opt.is_some() && !num_regex.is_match(num_val_opt.unwrap()) { - let could_be_file = arguments.remove(pos + 1); - arguments.insert(pos + 1, format!("{}", NumberingMode::default().width)); - arguments.insert(pos + 2, could_be_file); + if let Some(num_val_opt) = args.get(pos + 1) { + if !num_regex.is_match(num_val_opt) { + let could_be_file = arguments.remove(pos + 1); + arguments.insert(pos + 1, format!("{}", NumberingMode::default().width)); + arguments.insert(pos + 2, could_be_file); + } } } @@ -666,12 +673,13 @@ fn build_options( None => end_page_in_plus_option, }; - if end_page.is_some() && start_page > end_page.unwrap() { - return Err(PrError::EncounteredErrors(format!( - "invalid --pages argument '{}:{}'", - start_page, - end_page.unwrap() - ))); + if let Some(end_page) = end_page { + if start_page > end_page { + return Err(PrError::EncounteredErrors(format!( + "invalid --pages argument '{}:{}'", + start_page, end_page + ))); + } } let default_lines_per_page = if form_feed_used { @@ -947,7 +955,7 @@ fn read_stream_and_create_pages( let current_page = x + 1; current_page >= start_page - && (last_page.is_none() || current_page <= last_page.unwrap()) + && last_page.map_or(true, |last_page| current_page <= last_page) }), ) } @@ -996,8 +1004,8 @@ fn mpr(paths: &[String], options: &OutputOptions) -> Result { for (_key, file_line_group) in file_line_groups.into_iter() { for file_line in file_line_group { - if file_line.line_content.is_err() { - return Err(file_line.line_content.unwrap_err().into()); + if let Err(e) = file_line.line_content { + return Err(e.into()); } let new_page_number = file_line.page_number; if page_counter != new_page_number { @@ -1030,8 +1038,7 @@ fn print_page(lines: &[FileLine], options: &OutputOptions, page: usize) -> Resul let lines_written = write_columns(lines, options, out)?; - for index in 0..trailer_content.len() { - let x = trailer_content.get(index).unwrap(); + for (index, x) in trailer_content.iter().enumerate() { out.write_all(x.as_bytes())?; if index + 1 != trailer_content.len() { out.write_all(line_separator)?; @@ -1074,8 +1081,7 @@ fn write_columns( let mut offset = 0; for col in 0..columns { let mut inserted = 0; - for i in offset..lines.len() { - let line = lines.get(i).unwrap(); + for line in &lines[offset..] { if line.file_id != col { break; } @@ -1114,7 +1120,7 @@ fn write_columns( for (i, cell) in row.iter().enumerate() { if cell.is_none() && options.merge_files_print.is_some() { out.write_all( - get_line_for_printing(&options, &blank_line, columns, i, &line_width, indexes) + get_line_for_printing(options, &blank_line, columns, i, &line_width, indexes) .as_bytes(), )?; } else if cell.is_none() { @@ -1124,7 +1130,7 @@ fn write_columns( let file_line = cell.unwrap(); out.write_all( - get_line_for_printing(&options, file_line, columns, i, &line_width, indexes) + get_line_for_printing(options, file_line, columns, i, &line_width, indexes) .as_bytes(), )?; lines_printed += 1; @@ -1149,7 +1155,7 @@ fn get_line_for_printing( indexes: usize, ) -> String { let blank_line = String::new(); - let formatted_line_number = get_formatted_line_number(&options, file_line.line_number, index); + let formatted_line_number = get_formatted_line_number(options, file_line.line_number, index); let mut complete_line = format!( "{}{}", diff --git a/src/uu/printenv/Cargo.toml b/src/uu/printenv/Cargo.toml index be95b8157..faa24a33b 100644 --- a/src/uu/printenv/Cargo.toml +++ b/src/uu/printenv/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/printenv.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/printenv/src/printenv.rs b/src/uu/printenv/src/printenv.rs index 5c2594835..6e0ca7157 100644 --- a/src/uu/printenv/src/printenv.rs +++ b/src/uu/printenv/src/printenv.rs @@ -26,23 +26,7 @@ fn get_usage() -> String { pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); - let matches = App::new(executable!()) - .version(crate_version!()) - .about(ABOUT) - .usage(&usage[..]) - .arg( - Arg::with_name(OPT_NULL) - .short("0") - .long(OPT_NULL) - .help("end each output line with 0 byte rather than newline"), - ) - .arg( - Arg::with_name(ARG_VARIABLES) - .multiple(true) - .takes_value(true) - .min_values(1), - ) - .get_matches_from(args); + let matches = uu_app().usage(&usage[..]).get_matches_from(args); let variables: Vec = matches .values_of(ARG_VARIABLES) @@ -69,3 +53,21 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } 0 } + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about(ABOUT) + .arg( + Arg::with_name(OPT_NULL) + .short("0") + .long(OPT_NULL) + .help("end each output line with 0 byte rather than newline"), + ) + .arg( + Arg::with_name(ARG_VARIABLES) + .multiple(true) + .takes_value(true) + .min_values(1), + ) +} diff --git a/src/uu/printf/Cargo.toml b/src/uu/printf/Cargo.toml index bc77d31be..f980837e7 100644 --- a/src/uu/printf/Cargo.toml +++ b/src/uu/printf/Cargo.toml @@ -18,6 +18,7 @@ edition = "2018" path = "src/printf.rs" [dependencies] +clap = { version = "2.33", features = ["wrap_help"] } itertools = "0.8.0" uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/printf/src/printf.rs b/src/uu/printf/src/printf.rs index 88d18838d..efa9aea57 100644 --- a/src/uu/printf/src/printf.rs +++ b/src/uu/printf/src/printf.rs @@ -2,14 +2,18 @@ // spell-checker:ignore (change!) each's // spell-checker:ignore (ToDO) LONGHELP FORMATSTRING templating parameterizing formatstr +#[macro_use] +extern crate uucore; + +use clap::{crate_version, App, Arg}; use uucore::InvalidEncodingHandling; mod cli; mod memo; mod tokenize; -static NAME: &str = "printf"; -static VERSION: &str = env!("CARGO_PKG_VERSION"); +const VERSION: &str = "version"; +const HELP: &str = "help"; static LONGHELP_LEAD: &str = "printf USAGE: printf FORMATSTRING [ARGUMENT]... @@ -290,10 +294,16 @@ pub fn uumain(args: impl uucore::Args) -> i32 { if formatstr == "--help" { print!("{} {}", LONGHELP_LEAD, LONGHELP_BODY); } else if formatstr == "--version" { - println!("{} {}", NAME, VERSION); + println!("{} {}", executable!(), crate_version!()); } else { let printf_args = &args[2..]; memo::Memo::run_all(formatstr, printf_args); } 0 } + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .arg(Arg::with_name(VERSION).long(VERSION)) + .arg(Arg::with_name(HELP).long(HELP)) +} diff --git a/src/uu/printf/src/tokenize/num_format/formatters/cninetyninehexfloatf.rs b/src/uu/printf/src/tokenize/num_format/formatters/cninetyninehexfloatf.rs index f96a991b5..0ca993680 100644 --- a/src/uu/printf/src/tokenize/num_format/formatters/cninetyninehexfloatf.rs +++ b/src/uu/printf/src/tokenize/num_format/formatters/cninetyninehexfloatf.rs @@ -26,7 +26,7 @@ impl Formatter for CninetyNineHexFloatf { ) -> Option { let second_field = field.second_field.unwrap_or(6) + 1; let analysis = FloatAnalysis::analyze( - &str_in, + str_in, initial_prefix, Some(second_field as usize), None, diff --git a/src/uu/printf/src/tokenize/num_format/formatters/decf.rs b/src/uu/printf/src/tokenize/num_format/formatters/decf.rs index 5798eadcb..3376345e0 100644 --- a/src/uu/printf/src/tokenize/num_format/formatters/decf.rs +++ b/src/uu/printf/src/tokenize/num_format/formatters/decf.rs @@ -55,18 +55,9 @@ impl Formatter for Decf { ); // strip trailing zeroes if let Some(ref post_dec) = f_sci.post_decimal { - let mut i = post_dec.len(); - { - let mut it = post_dec.chars(); - while let Some(c) = it.next_back() { - if c != '0' { - break; - } - i -= 1; - } - } - if i != post_dec.len() { - f_sci.post_decimal = Some(String::from(&post_dec[0..i])); + let trimmed = post_dec.trim_end_matches('0'); + if trimmed.len() != post_dec.len() { + f_sci.post_decimal = Some(trimmed.to_owned()); } } let f_fl = get_primitive_dec( diff --git a/src/uu/printf/src/tokenize/num_format/formatters/float_common.rs b/src/uu/printf/src/tokenize/num_format/formatters/float_common.rs index dd8259233..97009b586 100644 --- a/src/uu/printf/src/tokenize/num_format/formatters/float_common.rs +++ b/src/uu/printf/src/tokenize/num_format/formatters/float_common.rs @@ -247,8 +247,12 @@ pub fn get_primitive_dec( first_segment.len() as isize - 1, ) } else { - match first_segment.chars().next() { - Some('0') => { + match first_segment + .chars() + .next() + .expect("float_common: no chars in first segment.") + { + '0' => { let it = second_segment.chars().enumerate(); let mut m: isize = 0; let mut pre = String::from("0"); @@ -266,10 +270,7 @@ pub fn get_primitive_dec( } (pre, post, m) } - Some(_) => (first_segment, second_segment, 0), - None => { - panic!("float_common: no chars in first segment."); - } + _ => (first_segment, second_segment, 0), } } } else { @@ -298,11 +299,11 @@ pub fn get_primitive_dec( pub fn primitive_to_str_common(prim: &FormatPrimitive, field: &FormatField) -> String { let mut final_str = String::new(); if let Some(ref prefix) = prim.prefix { - final_str.push_str(&prefix); + final_str.push_str(prefix); } match prim.pre_decimal { Some(ref pre_decimal) => { - final_str.push_str(&pre_decimal); + final_str.push_str(pre_decimal); } None => { panic!( diff --git a/src/uu/printf/src/tokenize/num_format/formatters/floatf.rs b/src/uu/printf/src/tokenize/num_format/formatters/floatf.rs index aed50f18e..afb2bcf08 100644 --- a/src/uu/printf/src/tokenize/num_format/formatters/floatf.rs +++ b/src/uu/printf/src/tokenize/num_format/formatters/floatf.rs @@ -21,7 +21,7 @@ impl Formatter for Floatf { ) -> Option { let second_field = field.second_field.unwrap_or(6) + 1; let analysis = FloatAnalysis::analyze( - &str_in, + str_in, initial_prefix, None, Some(second_field as usize), diff --git a/src/uu/printf/src/tokenize/num_format/formatters/intf.rs b/src/uu/printf/src/tokenize/num_format/formatters/intf.rs index 02c59211b..b6c18d436 100644 --- a/src/uu/printf/src/tokenize/num_format/formatters/intf.rs +++ b/src/uu/printf/src/tokenize/num_format/formatters/intf.rs @@ -252,7 +252,7 @@ impl Formatter for Intf { fn primitive_to_str(&self, prim: &FormatPrimitive, field: FormatField) -> String { let mut final_str: String = String::new(); if let Some(ref prefix) = prim.prefix { - final_str.push_str(&prefix); + final_str.push_str(prefix); } // integral second fields is zero-padded minimum-width // which gets handled before general minimum-width @@ -266,7 +266,7 @@ impl Formatter for Intf { i -= 1; } } - final_str.push_str(&pre_decimal); + final_str.push_str(pre_decimal); } None => { panic!( diff --git a/src/uu/printf/src/tokenize/num_format/num_format.rs b/src/uu/printf/src/tokenize/num_format/num_format.rs index a8a60cc57..b32731f2d 100644 --- a/src/uu/printf/src/tokenize/num_format/num_format.rs +++ b/src/uu/printf/src/tokenize/num_format/num_format.rs @@ -235,7 +235,7 @@ pub fn num_format(field: &FormatField, in_str_opt: Option<&String>) -> Option) -> Option { text_so_far.push('%'); - err_conv(&text_so_far); + err_conv(text_so_far); false } } diff --git a/src/uu/ptx/Cargo.toml b/src/uu/ptx/Cargo.toml index eb4413cbd..1ccdd9ad4 100644 --- a/src/uu/ptx/Cargo.toml +++ b/src/uu/ptx/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/ptx.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } aho-corasick = "0.7.3" libc = "0.2.42" memchr = "2.2.0" diff --git a/src/uu/ptx/src/ptx.rs b/src/uu/ptx/src/ptx.rs index 69960ac49..01b14bc4d 100644 --- a/src/uu/ptx/src/ptx.rs +++ b/src/uu/ptx/src/ptx.rs @@ -213,7 +213,7 @@ fn read_input(input_files: &[String], config: &Config) -> FileMap { files.push("-"); } else if config.gnu_ext { for file in input_files { - files.push(&file); + files.push(file); } } else { files.push(&input_files[0]); @@ -503,7 +503,7 @@ fn format_tex_line( let keyword = &line[word_ref.position..word_ref.position_end]; let after_chars_trim_idx = (word_ref.position_end, chars_line.len()); let all_after = &chars_line[after_chars_trim_idx.0..after_chars_trim_idx.1]; - let (tail, before, after, head) = get_output_chunks(&all_before, &keyword, &all_after, &config); + let (tail, before, after, head) = get_output_chunks(all_before, keyword, all_after, config); output.push_str(&format!( "{5}{0}{6}{5}{1}{6}{5}{2}{6}{5}{3}{6}{5}{4}{6}", format_tex_field(&tail), @@ -515,7 +515,7 @@ fn format_tex_line( "}" )); if config.auto_ref || config.input_ref { - output.push_str(&format!("{}{}{}", "{", format_tex_field(&reference), "}")); + output.push_str(&format!("{}{}{}", "{", format_tex_field(reference), "}")); } output } @@ -546,7 +546,7 @@ fn format_roff_line( let keyword = &line[word_ref.position..word_ref.position_end]; let after_chars_trim_idx = (word_ref.position_end, chars_line.len()); let all_after = &chars_line[after_chars_trim_idx.0..after_chars_trim_idx.1]; - let (tail, before, after, head) = get_output_chunks(&all_before, &keyword, &all_after, &config); + let (tail, before, after, head) = get_output_chunks(all_before, keyword, all_after, config); output.push_str(&format!( " \"{}\" \"{}\" \"{}{}\" \"{}\"", format_roff_field(&tail), @@ -556,7 +556,7 @@ fn format_roff_line( format_roff_field(&head) )); if config.auto_ref || config.input_ref { - output.push_str(&format!(" \"{}\"", format_roff_field(&reference))); + output.push_str(&format!(" \"{}\"", format_roff_field(reference))); } output } @@ -638,7 +638,28 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .accept_any(); // let mut opts = Options::new(); - let matches = App::new(executable!()) + let matches = uu_app().get_matches_from(args); + + let input_files: Vec = match &matches.values_of(options::FILE) { + Some(v) => v.clone().map(|v| v.to_owned()).collect(), + None => vec!["-".to_string()], + }; + + let config = get_config(&matches); + let word_filter = WordFilter::new(&matches, &config); + let file_map = read_input(&input_files, &config); + let word_set = create_word_set(&config, &word_filter, &file_map); + let output_file = if !config.gnu_ext && matches.args.len() == 2 { + matches.value_of(options::FILE).unwrap_or("-").to_string() + } else { + "-".to_owned() + }; + write_traditional_output(&config, &file_map, &word_set, &output_file); + 0 +} + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) .name(NAME) .version(crate_version!()) .usage(BRIEF) @@ -762,22 +783,4 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .value_name("NUMBER") .takes_value(true), ) - .get_matches_from(args); - - let input_files: Vec = match &matches.values_of(options::FILE) { - Some(v) => v.clone().map(|v| v.to_owned()).collect(), - None => vec!["-".to_string()], - }; - - let config = get_config(&matches); - let word_filter = WordFilter::new(&matches, &config); - let file_map = read_input(&input_files, &config); - let word_set = create_word_set(&config, &word_filter, &file_map); - let output_file = if !config.gnu_ext && matches.args.len() == 2 { - matches.value_of(options::FILE).unwrap_or("-").to_string() - } else { - "-".to_owned() - }; - write_traditional_output(&config, &file_map, &word_set, &output_file); - 0 } diff --git a/src/uu/pwd/Cargo.toml b/src/uu/pwd/Cargo.toml index f4350d54c..3393a63b3 100644 --- a/src/uu/pwd/Cargo.toml +++ b/src/uu/pwd/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/pwd.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/pwd/src/pwd.rs b/src/uu/pwd/src/pwd.rs index 9b4e5c600..764a63a88 100644 --- a/src/uu/pwd/src/pwd.rs +++ b/src/uu/pwd/src/pwd.rs @@ -39,23 +39,7 @@ fn get_usage() -> String { pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); - let matches = App::new(executable!()) - .version(crate_version!()) - .about(ABOUT) - .usage(&usage[..]) - .arg( - Arg::with_name(OPT_LOGICAL) - .short("L") - .long(OPT_LOGICAL) - .help("use PWD from environment, even if it contains symlinks"), - ) - .arg( - Arg::with_name(OPT_PHYSICAL) - .short("P") - .long(OPT_PHYSICAL) - .help("avoid all symlinks"), - ) - .get_matches_from(args); + let matches = uu_app().usage(&usage[..]).get_matches_from(args); match env::current_dir() { Ok(logical_path) => { @@ -73,3 +57,21 @@ pub fn uumain(args: impl uucore::Args) -> i32 { 0 } + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about(ABOUT) + .arg( + Arg::with_name(OPT_LOGICAL) + .short("L") + .long(OPT_LOGICAL) + .help("use PWD from environment, even if it contains symlinks"), + ) + .arg( + Arg::with_name(OPT_PHYSICAL) + .short("P") + .long(OPT_PHYSICAL) + .help("avoid all symlinks"), + ) +} diff --git a/src/uu/readlink/Cargo.toml b/src/uu/readlink/Cargo.toml index 6e4be4dd8..65b5c149b 100644 --- a/src/uu/readlink/Cargo.toml +++ b/src/uu/readlink/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/readlink.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } libc = "0.2.42" uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["fs"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/readlink/src/readlink.rs b/src/uu/readlink/src/readlink.rs index 02e286315..826fa0254 100644 --- a/src/uu/readlink/src/readlink.rs +++ b/src/uu/readlink/src/readlink.rs @@ -35,69 +35,7 @@ fn get_usage() -> String { pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); - let matches = App::new(executable!()) - .version(crate_version!()) - .about(ABOUT) - .usage(&usage[..]) - .arg( - Arg::with_name(OPT_CANONICALIZE) - .short("f") - .long(OPT_CANONICALIZE) - .help( - "canonicalize by following every symlink in every component of the \ - given name recursively; all but the last component must exist", - ), - ) - .arg( - Arg::with_name(OPT_CANONICALIZE_EXISTING) - .short("e") - .long("canonicalize-existing") - .help( - "canonicalize by following every symlink in every component of the \ - given name recursively, all components must exist", - ), - ) - .arg( - Arg::with_name(OPT_CANONICALIZE_MISSING) - .short("m") - .long(OPT_CANONICALIZE_MISSING) - .help( - "canonicalize by following every symlink in every component of the \ - given name recursively, without requirements on components existence", - ), - ) - .arg( - Arg::with_name(OPT_NO_NEWLINE) - .short("n") - .long(OPT_NO_NEWLINE) - .help("do not output the trailing delimiter"), - ) - .arg( - Arg::with_name(OPT_QUIET) - .short("q") - .long(OPT_QUIET) - .help("suppress most error messages"), - ) - .arg( - Arg::with_name(OPT_SILENT) - .short("s") - .long(OPT_SILENT) - .help("suppress most error messages"), - ) - .arg( - Arg::with_name(OPT_VERBOSE) - .short("v") - .long(OPT_VERBOSE) - .help("report error message"), - ) - .arg( - Arg::with_name(OPT_ZERO) - .short("z") - .long(OPT_ZERO) - .help("separate output with NUL rather than newline"), - ) - .arg(Arg::with_name(ARG_FILES).multiple(true).takes_value(true)) - .get_matches_from(args); + let matches = uu_app().usage(&usage[..]).get_matches_from(args); let mut no_newline = matches.is_present(OPT_NO_NEWLINE); let use_zero = matches.is_present(OPT_ZERO); @@ -159,6 +97,70 @@ pub fn uumain(args: impl uucore::Args) -> i32 { 0 } +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about(ABOUT) + .arg( + Arg::with_name(OPT_CANONICALIZE) + .short("f") + .long(OPT_CANONICALIZE) + .help( + "canonicalize by following every symlink in every component of the \ + given name recursively; all but the last component must exist", + ), + ) + .arg( + Arg::with_name(OPT_CANONICALIZE_EXISTING) + .short("e") + .long("canonicalize-existing") + .help( + "canonicalize by following every symlink in every component of the \ + given name recursively, all components must exist", + ), + ) + .arg( + Arg::with_name(OPT_CANONICALIZE_MISSING) + .short("m") + .long(OPT_CANONICALIZE_MISSING) + .help( + "canonicalize by following every symlink in every component of the \ + given name recursively, without requirements on components existence", + ), + ) + .arg( + Arg::with_name(OPT_NO_NEWLINE) + .short("n") + .long(OPT_NO_NEWLINE) + .help("do not output the trailing delimiter"), + ) + .arg( + Arg::with_name(OPT_QUIET) + .short("q") + .long(OPT_QUIET) + .help("suppress most error messages"), + ) + .arg( + Arg::with_name(OPT_SILENT) + .short("s") + .long(OPT_SILENT) + .help("suppress most error messages"), + ) + .arg( + Arg::with_name(OPT_VERBOSE) + .short("v") + .long(OPT_VERBOSE) + .help("report error message"), + ) + .arg( + Arg::with_name(OPT_ZERO) + .short("z") + .long(OPT_ZERO) + .help("separate output with NUL rather than newline"), + ) + .arg(Arg::with_name(ARG_FILES).multiple(true).takes_value(true)) +} + fn show(path: &Path, no_newline: bool, use_zero: bool) { let path = path.to_str().unwrap(); if use_zero { diff --git a/src/uu/realpath/Cargo.toml b/src/uu/realpath/Cargo.toml index 327a875f8..dc21bdaca 100644 --- a/src/uu/realpath/Cargo.toml +++ b/src/uu/realpath/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/realpath.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["fs"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/realpath/src/realpath.rs b/src/uu/realpath/src/realpath.rs index 1a96b7f80..fe2ad4ccc 100644 --- a/src/uu/realpath/src/realpath.rs +++ b/src/uu/realpath/src/realpath.rs @@ -29,10 +29,35 @@ fn get_usage() -> String { pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); - let matches = App::new(executable!()) + let matches = uu_app().usage(&usage[..]).get_matches_from(args); + + /* the list of files */ + + let paths: Vec = matches + .values_of(ARG_FILES) + .unwrap() + .map(PathBuf::from) + .collect(); + + let strip = matches.is_present(OPT_STRIP); + let zero = matches.is_present(OPT_ZERO); + let quiet = matches.is_present(OPT_QUIET); + let mut retcode = 0; + for path in &paths { + if let Err(e) = resolve_path(path, strip, zero) { + if !quiet { + show_error!("{}: {}", e, path.display()); + } + retcode = 1 + }; + } + retcode +} + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) .version(crate_version!()) .about(ABOUT) - .usage(&usage[..]) .arg( Arg::with_name(OPT_QUIET) .short("q") @@ -58,29 +83,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .required(true) .min_values(1), ) - .get_matches_from(args); - - /* the list of files */ - - let paths: Vec = matches - .values_of(ARG_FILES) - .unwrap() - .map(PathBuf::from) - .collect(); - - let strip = matches.is_present(OPT_STRIP); - let zero = matches.is_present(OPT_ZERO); - let quiet = matches.is_present(OPT_QUIET); - let mut retcode = 0; - for path in &paths { - if let Err(e) = resolve_path(path, strip, zero) { - if !quiet { - show_error!("{}: {}", e, path.display()); - } - retcode = 1 - }; - } - retcode } /// Resolve a path to an absolute form and print it. diff --git a/src/uu/relpath/Cargo.toml b/src/uu/relpath/Cargo.toml index 7a316c29c..1240d9b1b 100644 --- a/src/uu/relpath/Cargo.toml +++ b/src/uu/relpath/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/relpath.rs" [dependencies] -clap = "2.33.3" +clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["fs"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/relpath/src/relpath.rs b/src/uu/relpath/src/relpath.rs index a997e1c5f..cb0fba7cc 100644 --- a/src/uu/relpath/src/relpath.rs +++ b/src/uu/relpath/src/relpath.rs @@ -35,26 +35,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .accept_any(); let usage = get_usage(); - let matches = App::new(executable!()) - .version(crate_version!()) - .about(ABOUT) - .usage(&usage[..]) - .arg( - Arg::with_name(options::DIR) - .short("d") - .takes_value(true) - .help("If any of FROM and TO is not subpath of DIR, output absolute path instead of relative"), - ) - .arg( - Arg::with_name(options::TO) - .required(true) - .takes_value(true), - ) - .arg( - Arg::with_name(options::FROM) - .takes_value(true), - ) - .get_matches_from(args); + let matches = uu_app().usage(&usage[..]).get_matches_from(args); let to = Path::new(matches.value_of(options::TO).unwrap()).to_path_buf(); // required let from = match matches.value_of(options::FROM) { @@ -99,3 +80,24 @@ pub fn uumain(args: impl uucore::Args) -> i32 { println!("{}", result.display()); 0 } + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about(ABOUT) + .arg( + Arg::with_name(options::DIR) + .short("d") + .takes_value(true) + .help("If any of FROM and TO is not subpath of DIR, output absolute path instead of relative"), + ) + .arg( + Arg::with_name(options::TO) + .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name(options::FROM) + .takes_value(true), + ) +} diff --git a/src/uu/rm/Cargo.toml b/src/uu/rm/Cargo.toml index 9974111aa..20fd60745 100644 --- a/src/uu/rm/Cargo.toml +++ b/src/uu/rm/Cargo.toml @@ -15,13 +15,15 @@ edition = "2018" path = "src/rm.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } walkdir = "2.2" remove_dir_all = "0.5.1" +winapi = { version="0.3", features=[] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["fs"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } + [[bin]] name = "rm" path = "src/main.rs" diff --git a/src/uu/rm/src/rm.rs b/src/uu/rm/src/rm.rs index 43a4f4780..259d1ab39 100644 --- a/src/uu/rm/src/rm.rs +++ b/src/uu/rm/src/rm.rs @@ -5,7 +5,7 @@ // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -// spell-checker:ignore (ToDO) bitor ulong +// spell-checker:ignore (path) eacces #[macro_use] extern crate uucore; @@ -77,11 +77,72 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); let long_usage = get_long_usage(); - let matches = App::new(executable!()) - .version(crate_version!()) - .about(ABOUT) + let matches = uu_app() .usage(&usage[..]) .after_help(&long_usage[..]) + .get_matches_from(args); + + let files: Vec = matches + .values_of(ARG_FILES) + .map(|v| v.map(ToString::to_string).collect()) + .unwrap_or_default(); + + let force = matches.is_present(OPT_FORCE); + + if files.is_empty() && !force { + // Still check by hand and not use clap + // Because "rm -f" is a thing + show_error!("missing an argument"); + show_error!("for help, try '{0} --help'", executable!()); + return 1; + } else { + let options = Options { + force, + interactive: { + if matches.is_present(OPT_PROMPT) { + InteractiveMode::Always + } else if matches.is_present(OPT_PROMPT_MORE) { + InteractiveMode::Once + } else if matches.is_present(OPT_INTERACTIVE) { + match matches.value_of(OPT_INTERACTIVE).unwrap() { + "none" => InteractiveMode::None, + "once" => InteractiveMode::Once, + "always" => InteractiveMode::Always, + val => crash!(1, "Invalid argument to interactive ({})", val), + } + } else { + InteractiveMode::None + } + }, + one_fs: matches.is_present(OPT_ONE_FILE_SYSTEM), + preserve_root: !matches.is_present(OPT_NO_PRESERVE_ROOT), + recursive: matches.is_present(OPT_RECURSIVE) || matches.is_present(OPT_RECURSIVE_R), + dir: matches.is_present(OPT_DIR), + verbose: matches.is_present(OPT_VERBOSE), + }; + if options.interactive == InteractiveMode::Once && (options.recursive || files.len() > 3) { + let msg = if options.recursive { + "Remove all arguments recursively? " + } else { + "Remove all arguments? " + }; + if !prompt(msg) { + return 0; + } + } + + if remove(files, options) { + return 1; + } + } + + 0 +} + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about(ABOUT) .arg( Arg::with_name(OPT_FORCE) @@ -151,63 +212,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .takes_value(true) .min_values(1) ) - .get_matches_from(args); - - let files: Vec = matches - .values_of(ARG_FILES) - .map(|v| v.map(ToString::to_string).collect()) - .unwrap_or_default(); - - let force = matches.is_present(OPT_FORCE); - - if files.is_empty() && !force { - // Still check by hand and not use clap - // Because "rm -f" is a thing - show_error!("missing an argument"); - show_error!("for help, try '{0} --help'", executable!()); - return 1; - } else { - let options = Options { - force, - interactive: { - if matches.is_present(OPT_PROMPT) { - InteractiveMode::Always - } else if matches.is_present(OPT_PROMPT_MORE) { - InteractiveMode::Once - } else if matches.is_present(OPT_INTERACTIVE) { - match matches.value_of(OPT_INTERACTIVE).unwrap() { - "none" => InteractiveMode::None, - "once" => InteractiveMode::Once, - "always" => InteractiveMode::Always, - val => crash!(1, "Invalid argument to interactive ({})", val), - } - } else { - InteractiveMode::None - } - }, - one_fs: matches.is_present(OPT_ONE_FILE_SYSTEM), - preserve_root: !matches.is_present(OPT_NO_PRESERVE_ROOT), - recursive: matches.is_present(OPT_RECURSIVE) || matches.is_present(OPT_RECURSIVE_R), - dir: matches.is_present(OPT_DIR), - verbose: matches.is_present(OPT_VERBOSE), - }; - if options.interactive == InteractiveMode::Once && (options.recursive || files.len() > 3) { - let msg = if options.recursive { - "Remove all arguments recursively? " - } else { - "Remove all arguments? " - }; - if !prompt(msg) { - return 0; - } - } - - if remove(files, options) { - return 1; - } - } - - 0 } // TODO: implement one-file-system (this may get partially implemented in walkdir) @@ -255,7 +259,18 @@ fn handle_dir(path: &Path, options: &Options) -> bool { // correctly on Windows if let Err(e) = remove_dir_all(path) { had_err = true; - show_error!("could not remove '{}': {}", path.display(), e); + if e.kind() == std::io::ErrorKind::PermissionDenied { + // GNU compatibility (rm/fail-eacces.sh) + // here, GNU doesn't use some kind of remove_dir_all + // It will show directory+file + show_error!( + "cannot remove '{}': {}", + path.display(), + "Permission denied" + ); + } else { + show_error!("cannot remove '{}': {}", path.display(), e); + } } } else { let mut dirs: VecDeque = VecDeque::new(); @@ -314,7 +329,16 @@ fn remove_dir(path: &Path, options: &Options) -> bool { } } Err(e) => { - show_error!("cannot remove '{}': {}", path.display(), e); + if e.kind() == std::io::ErrorKind::PermissionDenied { + // GNU compatibility (rm/fail-eacces.sh) + show_error!( + "cannot remove '{}': {}", + path.display(), + "Permission denied" + ); + } else { + show_error!("cannot remove '{}': {}", path.display(), e); + } return true; } } @@ -352,7 +376,16 @@ fn remove_file(path: &Path, options: &Options) -> bool { } } Err(e) => { - show_error!("removing '{}': {}", path.display(), e); + if e.kind() == std::io::ErrorKind::PermissionDenied { + // GNU compatibility (rm/fail-eacces.sh) + show_error!( + "cannot remove '{}': {}", + path.display(), + "Permission denied" + ); + } else { + show_error!("cannot remove '{}': {}", path.display(), e); + } return true; } } @@ -401,9 +434,7 @@ use std::os::windows::prelude::MetadataExt; #[cfg(windows)] fn is_symlink_dir(metadata: &fs::Metadata) -> bool { - use std::os::raw::c_ulong; - pub type DWORD = c_ulong; - pub const FILE_ATTRIBUTE_DIRECTORY: DWORD = 0x10; + use winapi::um::winnt::FILE_ATTRIBUTE_DIRECTORY; metadata.file_type().is_symlink() && ((metadata.file_attributes() & FILE_ATTRIBUTE_DIRECTORY) != 0) diff --git a/src/uu/rmdir/Cargo.toml b/src/uu/rmdir/Cargo.toml index b6e04f71c..40c2efbb1 100644 --- a/src/uu/rmdir/Cargo.toml +++ b/src/uu/rmdir/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/rmdir.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/rmdir/src/rmdir.rs b/src/uu/rmdir/src/rmdir.rs index d13a21f60..8dbaf79a8 100644 --- a/src/uu/rmdir/src/rmdir.rs +++ b/src/uu/rmdir/src/rmdir.rs @@ -33,10 +33,29 @@ fn get_usage() -> String { pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); - let matches = App::new(executable!()) + let matches = uu_app().usage(&usage[..]).get_matches_from(args); + + let dirs: Vec = matches + .values_of(ARG_DIRS) + .map(|v| v.map(ToString::to_string).collect()) + .unwrap_or_default(); + + let ignore = matches.is_present(OPT_IGNORE_FAIL_NON_EMPTY); + let parents = matches.is_present(OPT_PARENTS); + let verbose = matches.is_present(OPT_VERBOSE); + + match remove(dirs, ignore, parents, verbose) { + Ok(()) => ( /* pass */ ), + Err(e) => return e, + } + + 0 +} + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) .version(crate_version!()) .about(ABOUT) - .usage(&usage[..]) .arg( Arg::with_name(OPT_IGNORE_FAIL_NON_EMPTY) .long(OPT_IGNORE_FAIL_NON_EMPTY) @@ -64,23 +83,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .min_values(1) .required(true), ) - .get_matches_from(args); - - let dirs: Vec = matches - .values_of(ARG_DIRS) - .map(|v| v.map(ToString::to_string).collect()) - .unwrap_or_default(); - - let ignore = matches.is_present(OPT_IGNORE_FAIL_NON_EMPTY); - let parents = matches.is_present(OPT_PARENTS); - let verbose = matches.is_present(OPT_VERBOSE); - - match remove(dirs, ignore, parents, verbose) { - Ok(()) => ( /* pass */ ), - Err(e) => return e, - } - - 0 } fn remove(dirs: Vec, ignore: bool, parents: bool, verbose: bool) -> Result<(), i32> { @@ -88,7 +90,7 @@ fn remove(dirs: Vec, ignore: bool, parents: bool, verbose: bool) -> Resu for dir in &dirs { let path = Path::new(&dir[..]); - r = remove_dir(&path, ignore, verbose).and(r); + r = remove_dir(path, ignore, verbose).and(r); if parents { let mut p = path; while let Some(new_p) = p.parent() { @@ -109,17 +111,14 @@ fn remove(dirs: Vec, ignore: bool, parents: bool, verbose: bool) -> Resu } fn remove_dir(path: &Path, ignore: bool, verbose: bool) -> Result<(), i32> { - let mut read_dir = match fs::read_dir(path) { - Ok(m) => m, - Err(e) if e.raw_os_error() == Some(ENOTDIR) => { + let mut read_dir = fs::read_dir(path).map_err(|e| { + if e.raw_os_error() == Some(ENOTDIR) { show_error!("failed to remove '{}': Not a directory", path.display()); - return Err(1); - } - Err(e) => { + } else { show_error!("reading directory '{}': {}", path.display(), e); - return Err(1); } - }; + 1 + })?; let mut r = Ok(()); diff --git a/src/uu/seq/Cargo.toml b/src/uu/seq/Cargo.toml index 32f2bbac8..726c7242b 100644 --- a/src/uu/seq/Cargo.toml +++ b/src/uu/seq/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/seq.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } num-bigint = "0.4.0" num-traits = "0.2.14" uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } diff --git a/src/uu/seq/src/seq.rs b/src/uu/seq/src/seq.rs index 954d15f2f..50a93d3af 100644 --- a/src/uu/seq/src/seq.rs +++ b/src/uu/seq/src/seq.rs @@ -87,42 +87,7 @@ impl FromStr for Number { pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); - let matches = App::new(executable!()) - .setting(AppSettings::AllowLeadingHyphen) - .version(crate_version!()) - .about(ABOUT) - .usage(&usage[..]) - .arg( - Arg::with_name(OPT_SEPARATOR) - .short("s") - .long("separator") - .help("Separator character (defaults to \\n)") - .takes_value(true) - .number_of_values(1), - ) - .arg( - Arg::with_name(OPT_TERMINATOR) - .short("t") - .long("terminator") - .help("Terminator character (defaults to \\n)") - .takes_value(true) - .number_of_values(1), - ) - .arg( - Arg::with_name(OPT_WIDTHS) - .short("w") - .long("widths") - .help("Equalize widths of all numbers by padding with zeros"), - ) - .arg( - Arg::with_name(ARG_NUMBERS) - .multiple(true) - .takes_value(true) - .allow_hyphen_values(true) - .max_values(3) - .required(true), - ) - .get_matches_from(args); + let matches = uu_app().usage(&usage[..]).get_matches_from(args); let numbers = matches.values_of(ARG_NUMBERS).unwrap().collect::>(); @@ -197,6 +162,43 @@ pub fn uumain(args: impl uucore::Args) -> i32 { 0 } +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .setting(AppSettings::AllowLeadingHyphen) + .version(crate_version!()) + .about(ABOUT) + .arg( + Arg::with_name(OPT_SEPARATOR) + .short("s") + .long("separator") + .help("Separator character (defaults to \\n)") + .takes_value(true) + .number_of_values(1), + ) + .arg( + Arg::with_name(OPT_TERMINATOR) + .short("t") + .long("terminator") + .help("Terminator character (defaults to \\n)") + .takes_value(true) + .number_of_values(1), + ) + .arg( + Arg::with_name(OPT_WIDTHS) + .short("w") + .long("widths") + .help("Equalize widths of all numbers by padding with zeros"), + ) + .arg( + Arg::with_name(ARG_NUMBERS) + .multiple(true) + .takes_value(true) + .allow_hyphen_values(true) + .max_values(3) + .required(true), + ) +} + fn done_printing(next: &T, increment: &T, last: &T) -> bool { if increment >= &T::zero() { next > last diff --git a/src/uu/shred/Cargo.toml b/src/uu/shred/Cargo.toml index dda68b45b..dafff162b 100644 --- a/src/uu/shred/Cargo.toml +++ b/src/uu/shred/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/shred.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } filetime = "0.2.1" libc = "0.2.42" rand = "0.5" diff --git a/src/uu/shred/src/shred.rs b/src/uu/shred/src/shred.rs index 6a43ed478..90336ea95 100644 --- a/src/uu/shred/src/shred.rs +++ b/src/uu/shred/src/shred.rs @@ -24,7 +24,7 @@ extern crate uucore; static NAME: &str = "shred"; const BLOCK_SIZE: usize = 512; -const NAME_CHARSET: &str = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_."; +const NAME_CHARSET: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_."; // Patterns as shown in the GNU coreutils shred implementation const PATTERNS: [&[u8]; 22] = [ @@ -89,7 +89,7 @@ impl Iterator for FilenameGenerator { // Make the return value, then increment let mut ret = String::new(); for i in name_charset_indices.iter() { - let c: char = NAME_CHARSET.chars().nth(*i).unwrap(); + let c = char::from(NAME_CHARSET[*i]); ret.push(c); } @@ -163,16 +163,14 @@ impl<'a> BytesGenerator<'a> { return None; } - let this_block_size = { - if !self.exact { + let this_block_size = if !self.exact { + self.block_size + } else { + let bytes_left = self.total_bytes - self.bytes_generated.get(); + if bytes_left >= self.block_size as u64 { self.block_size } else { - let bytes_left = self.total_bytes - self.bytes_generated.get(); - if bytes_left >= self.block_size as u64 { - self.block_size - } else { - (bytes_left % self.block_size as u64) as usize - } + (bytes_left % self.block_size as u64) as usize } }; @@ -184,12 +182,10 @@ impl<'a> BytesGenerator<'a> { rng.fill(bytes); } PassType::Pattern(pattern) => { - let skip = { - if self.bytes_generated.get() == 0 { - 0 - } else { - (pattern.len() as u64 % self.bytes_generated.get()) as usize - } + let skip = if self.bytes_generated.get() == 0 { + 0 + } else { + (pattern.len() as u64 % self.bytes_generated.get()) as usize }; // Copy the pattern in chunks rather than simply one byte at a time @@ -276,11 +272,68 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); - let app = App::new(executable!()) + let app = uu_app().usage(&usage[..]); + + let matches = app.get_matches_from(args); + + let mut errs: Vec = vec![]; + + if !matches.is_present(options::FILE) { + show_error!("Missing an argument"); + show_error!("For help, try '{} --help'", NAME); + return 0; + } + + let iterations = match matches.value_of(options::ITERATIONS) { + Some(s) => match s.parse::() { + Ok(u) => u, + Err(_) => { + errs.push(format!("invalid number of passes: '{}'", s)); + 0 + } + }, + None => unreachable!(), + }; + + // TODO: implement --remove HOW + // The optional HOW parameter indicates how to remove a directory entry: + // - 'unlink' => use a standard unlink call. + // - 'wipe' => also first obfuscate bytes in the name. + // - 'wipesync' => also sync each obfuscated byte to disk. + // The default mode is 'wipesync', but note it can be expensive. + + // TODO: implement --random-source + + let force = matches.is_present(options::FORCE); + let remove = matches.is_present(options::REMOVE); + let size_arg = matches.value_of(options::SIZE).map(|s| s.to_string()); + let size = get_size(size_arg); + let exact = matches.is_present(options::EXACT) && size.is_none(); // if -s is given, ignore -x + let zero = matches.is_present(options::ZERO); + let verbose = matches.is_present(options::VERBOSE); + + if !errs.is_empty() { + show_error!("Invalid arguments supplied."); + for message in errs { + show_error!("{}", message); + } + return 1; + } + + for path_str in matches.values_of(options::FILE).unwrap() { + wipe_file( + path_str, iterations, remove, size, exact, zero, verbose, force, + ); + } + + 0 +} + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) .version(crate_version!()) .about(ABOUT) .after_help(AFTER_HELP) - .usage(&usage[..]) .arg( Arg::with_name(options::FORCE) .long(options::FORCE) @@ -331,61 +384,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .help("add a final overwrite with zeros to hide shredding"), ) // Positional arguments - .arg(Arg::with_name(options::FILE).hidden(true).multiple(true)); - - let matches = app.get_matches_from(args); - - let mut errs: Vec = vec![]; - - if !matches.is_present(options::FILE) { - show_error!("Missing an argument"); - show_error!("For help, try '{} --help'", NAME); - return 0; - } - - let iterations = match matches.value_of(options::ITERATIONS) { - Some(s) => match s.parse::() { - Ok(u) => u, - Err(_) => { - errs.push(format!("invalid number of passes: '{}'", s)); - 0 - } - }, - None => unreachable!(), - }; - - // TODO: implement --remove HOW - // The optional HOW parameter indicates how to remove a directory entry: - // - 'unlink' => use a standard unlink call. - // - 'wipe' => also first obfuscate bytes in the name. - // - 'wipesync' => also sync each obfuscated byte to disk. - // The default mode is 'wipesync', but note it can be expensive. - - // TODO: implement --random-source - - let force = matches.is_present(options::FORCE); - let remove = matches.is_present(options::REMOVE); - let size_arg = matches.value_of(options::SIZE).map(|s| s.to_string()); - let size = get_size(size_arg); - let exact = matches.is_present(options::EXACT) && size.is_none(); // if -s is given, ignore -x - let zero = matches.is_present(options::ZERO); - let verbose = matches.is_present(options::VERBOSE); - - if !errs.is_empty() { - show_error!("Invalid arguments supplied."); - for message in errs { - show_error!("{}", message); - } - return 1; - } - - for path_str in matches.values_of(options::FILE).unwrap() { - wipe_file( - &path_str, iterations, remove, size, exact, zero, verbose, force, - ); - } - - 0 + .arg(Arg::with_name(options::FILE).hidden(true).multiple(true)) } // TODO: Add support for all postfixes here up to and including EiB @@ -659,7 +658,7 @@ fn do_remove(path: &Path, orig_filename: &str, verbose: bool) -> Result<(), io:: println!("{}: {}: removing", NAME, orig_filename); } - let renamed_path: Option = wipe_name(&path, verbose); + let renamed_path: Option = wipe_name(path, verbose); if let Some(rp) = renamed_path { fs::remove_file(rp)?; } diff --git a/src/uu/shuf/Cargo.toml b/src/uu/shuf/Cargo.toml index dbf559454..6c0353681 100644 --- a/src/uu/shuf/Cargo.toml +++ b/src/uu/shuf/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/shuf.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } rand = "0.5" uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/shuf/src/shuf.rs b/src/uu/shuf/src/shuf.rs index 88a47585f..4690d1c6e 100644 --- a/src/uu/shuf/src/shuf.rs +++ b/src/uu/shuf/src/shuf.rs @@ -56,7 +56,66 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .collect_str(InvalidEncodingHandling::ConvertLossy) .accept_any(); - let matches = App::new(executable!()) + let matches = uu_app().get_matches_from(args); + + let mode = if let Some(args) = matches.values_of(options::ECHO) { + Mode::Echo(args.map(String::from).collect()) + } else if let Some(range) = matches.value_of(options::INPUT_RANGE) { + match parse_range(range) { + Ok(m) => Mode::InputRange(m), + Err(msg) => { + crash!(1, "{}", msg); + } + } + } else { + Mode::Default(matches.value_of(options::FILE).unwrap_or("-").to_string()) + }; + + let options = Options { + head_count: match matches.value_of(options::HEAD_COUNT) { + Some(count) => match count.parse::() { + Ok(val) => val, + Err(_) => { + show_error!("invalid line count: '{}'", count); + return 1; + } + }, + None => std::usize::MAX, + }, + output: matches.value_of(options::OUTPUT).map(String::from), + random_source: matches.value_of(options::RANDOM_SOURCE).map(String::from), + repeat: matches.is_present(options::REPEAT), + sep: if matches.is_present(options::ZERO_TERMINATED) { + 0x00_u8 + } else { + 0x0a_u8 + }, + }; + + 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); + } + 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::Default(filename) => { + let fdata = read_input_file(&filename); + let mut fdata = vec![&fdata[..]]; + find_seps(&mut fdata, options.sep); + shuf_bytes(&mut fdata, options); + } + } + + 0 +} + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) .name(NAME) .version(crate_version!()) .template(TEMPLATE) @@ -118,62 +177,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .help("line delimiter is NUL, not newline"), ) .arg(Arg::with_name(options::FILE).takes_value(true)) - .get_matches_from(args); - - let mode = if let Some(args) = matches.values_of(options::ECHO) { - Mode::Echo(args.map(String::from).collect()) - } else if let Some(range) = matches.value_of(options::INPUT_RANGE) { - match parse_range(range) { - Ok(m) => Mode::InputRange(m), - Err(msg) => { - crash!(1, "{}", msg); - } - } - } else { - Mode::Default(matches.value_of(options::FILE).unwrap_or("-").to_string()) - }; - - let options = Options { - head_count: match matches.value_of(options::HEAD_COUNT) { - Some(count) => match count.parse::() { - Ok(val) => val, - Err(_) => { - show_error!("invalid line count: '{}'", count); - return 1; - } - }, - None => std::usize::MAX, - }, - output: matches.value_of(options::OUTPUT).map(String::from), - random_source: matches.value_of(options::RANDOM_SOURCE).map(String::from), - repeat: matches.is_present(options::REPEAT), - sep: if matches.is_present(options::ZERO_TERMINATED) { - 0x00_u8 - } else { - 0x0a_u8 - }, - }; - - 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); - } - 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::Default(filename) => { - let fdata = read_input_file(&filename); - let mut fdata = vec![&fdata[..]]; - find_seps(&mut fdata, options.sep); - shuf_bytes(&mut fdata, options); - } - } - - 0 } fn read_input_file(filename: &str) -> Vec { @@ -285,14 +288,12 @@ fn parse_range(input_range: &str) -> Result<(usize, usize), String> { if split.len() != 2 { Err(format!("invalid input range: '{}'", input_range)) } else { - let begin = match split[0].parse::() { - Ok(m) => m, - Err(_) => return Err(format!("invalid input range: '{}'", split[0])), - }; - let end = match split[1].parse::() { - Ok(m) => m, - Err(_) => return Err(format!("invalid input range: '{}'", split[1])), - }; + let begin = split[0] + .parse::() + .map_err(|_| format!("invalid input range: '{}'", split[0]))?; + let end = split[1] + .parse::() + .map_err(|_| format!("invalid input range: '{}'", split[1]))?; Ok((begin, end + 1)) } } diff --git a/src/uu/sleep/Cargo.toml b/src/uu/sleep/Cargo.toml index fe7ee2941..14c4c5300 100644 --- a/src/uu/sleep/Cargo.toml +++ b/src/uu/sleep/Cargo.toml @@ -15,8 +15,8 @@ edition = "2018" path = "src/sleep.rs" [dependencies] -clap = "2.33" -uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["parse_time"] } +clap = { version = "2.33", features = ["wrap_help"] } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] diff --git a/src/uu/sleep/src/sleep.rs b/src/uu/sleep/src/sleep.rs index c78c1cfc9..ada3336df 100644 --- a/src/uu/sleep/src/sleep.rs +++ b/src/uu/sleep/src/sleep.rs @@ -35,10 +35,20 @@ fn get_usage() -> String { pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); - let matches = App::new(executable!()) + let matches = uu_app().usage(&usage[..]).get_matches_from(args); + + if let Some(values) = matches.values_of(options::NUMBER) { + let numbers = values.collect(); + sleep(numbers); + } + + 0 +} + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) .version(crate_version!()) .about(ABOUT) - .usage(&usage[..]) .after_help(LONG_HELP) .arg( Arg::with_name(options::NUMBER) @@ -49,14 +59,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .multiple(true) .required(true), ) - .get_matches_from(args); - - if let Some(values) = matches.values_of(options::NUMBER) { - let numbers = values.collect(); - sleep(numbers); - } - - 0 } fn sleep(args: Vec<&str>) { diff --git a/src/uu/sort/Cargo.toml b/src/uu/sort/Cargo.toml index f06610248..a2e135bb6 100644 --- a/src/uu/sort/Cargo.toml +++ b/src/uu/sort/Cargo.toml @@ -16,7 +16,7 @@ path = "src/sort.rs" [dependencies] binary-heap-plus = "0.4.1" -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } compare = "0.1.0" fnv = "1.0.7" itertools = "0.10.0" @@ -24,7 +24,6 @@ memchr = "2.4.0" ouroboros = "0.9.3" rand = "0.7" rayon = "1.5" -semver = "0.9.0" tempfile = "3" unicode-width = "0.1.8" uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["fs"] } diff --git a/src/uu/sort/src/check.rs b/src/uu/sort/src/check.rs index d3b9d6669..f1cd22686 100644 --- a/src/uu/sort/src/check.rs +++ b/src/uu/sort/src/check.rs @@ -8,7 +8,7 @@ //! Check if a file is ordered use crate::{ - chunks::{self, Chunk}, + chunks::{self, Chunk, RecycledChunk}, compare_by, open, GlobalSettings, }; use itertools::Itertools; @@ -34,9 +34,7 @@ pub fn check(path: &str, settings: &GlobalSettings) -> i32 { move || reader(file, recycled_receiver, loaded_sender, &settings) }); for _ in 0..2 { - recycled_sender - .send(Chunk::new(vec![0; 100 * 1024], |_| Vec::new())) - .unwrap(); + let _ = recycled_sender.send(RecycledChunk::new(100 * 1024)); } let mut prev_chunk: Option = None; @@ -46,21 +44,29 @@ pub fn check(path: &str, settings: &GlobalSettings) -> i32 { if let Some(prev_chunk) = prev_chunk.take() { // Check if the first element of the new chunk is greater than the last // element from the previous chunk - let prev_last = prev_chunk.borrow_lines().last().unwrap(); - let new_first = chunk.borrow_lines().first().unwrap(); + let prev_last = prev_chunk.lines().last().unwrap(); + let new_first = chunk.lines().first().unwrap(); - if compare_by(prev_last, new_first, &settings) == Ordering::Greater { + if compare_by( + prev_last, + new_first, + settings, + prev_chunk.line_data(), + chunk.line_data(), + ) == Ordering::Greater + { if !settings.check_silent { println!("sort: {}:{}: disorder: {}", path, line_idx, new_first.line); } return 1; } - recycled_sender.send(prev_chunk).ok(); + let _ = recycled_sender.send(prev_chunk.recycle()); } - for (a, b) in chunk.borrow_lines().iter().tuple_windows() { + for (a, b) in chunk.lines().iter().tuple_windows() { line_idx += 1; - if compare_by(a, b, &settings) == Ordering::Greater { + if compare_by(a, b, settings, chunk.line_data(), chunk.line_data()) == Ordering::Greater + { if !settings.check_silent { println!("sort: {}:{}: disorder: {}", path, line_idx, b.line); } @@ -76,17 +82,15 @@ pub fn check(path: &str, settings: &GlobalSettings) -> i32 { /// The function running on the reader thread. fn reader( mut file: Box, - receiver: Receiver, + receiver: Receiver, sender: SyncSender, settings: &GlobalSettings, ) { - let mut sender = Some(sender); let mut carry_over = vec![]; - for chunk in receiver.iter() { - let (recycled_lines, recycled_buffer) = chunk.recycle(); - chunks::read( - &mut sender, - recycled_buffer, + for recycled_chunk in receiver.iter() { + let should_continue = chunks::read( + &sender, + recycled_chunk, None, &mut carry_over, &mut file, @@ -96,8 +100,10 @@ fn reader( } else { b'\n' }, - recycled_lines, settings, - ) + ); + if !should_continue { + break; + } } } diff --git a/src/uu/sort/src/chunks.rs b/src/uu/sort/src/chunks.rs index 23567833b..9e9d212c2 100644 --- a/src/uu/sort/src/chunks.rs +++ b/src/uu/sort/src/chunks.rs @@ -15,7 +15,7 @@ use std::{ use memchr::memchr_iter; use ouroboros::self_referencing; -use crate::{GlobalSettings, Line}; +use crate::{numeric_str_cmp::NumInfo, GeneralF64ParseResult, GlobalSettings, Line}; /// The chunk that is passed around between threads. /// `lines` consist of slices into `buffer`. @@ -25,106 +25,175 @@ pub struct Chunk { pub buffer: Vec, #[borrows(buffer)] #[covariant] - pub lines: Vec>, + pub contents: ChunkContents<'this>, +} + +#[derive(Debug)] +pub struct ChunkContents<'a> { + pub lines: Vec>, + pub line_data: LineData<'a>, +} + +#[derive(Debug)] +pub struct LineData<'a> { + pub selections: Vec<&'a str>, + pub num_infos: Vec, + pub parsed_floats: Vec, } impl Chunk { /// Destroy this chunk and return its components to be reused. - /// - /// # Returns - /// - /// * The `lines` vector, emptied - /// * The `buffer` vector, **not** emptied - pub fn recycle(mut self) -> (Vec>, Vec) { - let recycled_lines = self.with_lines_mut(|lines| { - lines.clear(); - unsafe { + pub fn recycle(mut self) -> RecycledChunk { + let recycled_contents = self.with_contents_mut(|contents| { + contents.lines.clear(); + contents.line_data.selections.clear(); + contents.line_data.num_infos.clear(); + contents.line_data.parsed_floats.clear(); + let lines = unsafe { // SAFETY: It is safe to (temporarily) transmute to a vector of lines with a longer lifetime, // because the vector is empty. // Transmuting is necessary to make recycling possible. See https://github.com/rust-lang/rfcs/pull/2802 // for a rfc to make this unnecessary. Its example is similar to the code here. - std::mem::transmute::>, Vec>>(std::mem::take(lines)) - } + std::mem::transmute::>, Vec>>(std::mem::take( + &mut contents.lines, + )) + }; + let selections = unsafe { + // SAFETY: (same as above) It is safe to (temporarily) transmute to a vector of &str with a longer lifetime, + // because the vector is empty. + std::mem::transmute::, Vec<&'static str>>(std::mem::take( + &mut contents.line_data.selections, + )) + }; + ( + lines, + selections, + std::mem::take(&mut contents.line_data.num_infos), + std::mem::take(&mut contents.line_data.parsed_floats), + ) }); - (recycled_lines, self.into_heads().buffer) + RecycledChunk { + lines: recycled_contents.0, + selections: recycled_contents.1, + num_infos: recycled_contents.2, + parsed_floats: recycled_contents.3, + buffer: self.into_heads().buffer, + } + } + + pub fn lines(&self) -> &Vec { + &self.borrow_contents().lines + } + pub fn line_data(&self) -> &LineData { + &self.borrow_contents().line_data + } +} + +pub struct RecycledChunk { + lines: Vec>, + selections: Vec<&'static str>, + num_infos: Vec, + parsed_floats: Vec, + buffer: Vec, +} + +impl RecycledChunk { + pub fn new(capacity: usize) -> Self { + RecycledChunk { + lines: Vec::new(), + selections: Vec::new(), + num_infos: Vec::new(), + parsed_floats: Vec::new(), + buffer: vec![0; capacity], + } } } /// Read a chunk, parse lines and send them. /// -/// No empty chunk will be sent. If we reach the end of the input, sender_option -/// is set to None. If this function however does not set sender_option to None, -/// it is not guaranteed that there is still input left: If the input fits _exactly_ -/// into a buffer, we will only notice that there's nothing more to read at the next -/// invocation. +/// No empty chunk will be sent. If we reach the end of the input, `false` is returned. +/// However, if this function returns `true`, it is not guaranteed that there is still +/// input left: If the input fits _exactly_ into a buffer, we will only notice that there's +/// nothing more to read at the next invocation. In case there is no input left, nothing will +/// be sent. /// /// # Arguments /// /// (see also `read_to_chunk` for a more detailed documentation) /// -/// * `sender_option`: The sender to send the lines to the sorter. If `None`, this function does nothing. -/// * `buffer`: The recycled buffer. All contents will be overwritten, but it must already be filled. +/// * `sender`: The sender to send the lines to the sorter. +/// * `recycled_chunk`: The recycled chunk, as returned by `Chunk::recycle`. /// (i.e. `buffer.len()` should be equal to `buffer.capacity()`) /// * `max_buffer_size`: How big `buffer` can be. /// * `carry_over`: The bytes that must be carried over in between invocations. /// * `file`: The current file. /// * `next_files`: What `file` should be updated to next. /// * `separator`: The line separator. -/// * `lines`: The recycled vector to fill with lines. Must be empty. /// * `settings`: The global settings. #[allow(clippy::too_many_arguments)] -#[allow(clippy::borrowed_box)] -pub fn read( - sender_option: &mut Option>, - mut buffer: Vec, +pub fn read( + sender: &SyncSender, + recycled_chunk: RecycledChunk, max_buffer_size: Option, carry_over: &mut Vec, - file: &mut Box, - next_files: &mut impl Iterator>, + file: &mut T, + next_files: &mut impl Iterator, separator: u8, - lines: Vec>, settings: &GlobalSettings, -) { - assert!(lines.is_empty()); - if let Some(sender) = sender_option { - if buffer.len() < carry_over.len() { - buffer.resize(carry_over.len() + 10 * 1024, 0); - } - buffer[..carry_over.len()].copy_from_slice(&carry_over); - let (read, should_continue) = read_to_buffer( - file, - next_files, - &mut buffer, - max_buffer_size, - carry_over.len(), - separator, - ); - carry_over.clear(); - carry_over.extend_from_slice(&buffer[read..]); +) -> bool { + let RecycledChunk { + lines, + selections, + num_infos, + parsed_floats, + mut buffer, + } = recycled_chunk; + if buffer.len() < carry_over.len() { + buffer.resize(carry_over.len() + 10 * 1024, 0); + } + buffer[..carry_over.len()].copy_from_slice(carry_over); + let (read, should_continue) = read_to_buffer( + file, + next_files, + &mut buffer, + max_buffer_size, + carry_over.len(), + separator, + ); + carry_over.clear(); + carry_over.extend_from_slice(&buffer[read..]); - let payload = Chunk::new(buffer, |buf| { + if read != 0 { + let payload = Chunk::new(buffer, |buffer| { + let selections = unsafe { + // SAFETY: It is safe to transmute to an empty vector of selections with shorter lifetime. + // It was only temporarily transmuted to a Vec> to make recycling possible. + std::mem::transmute::, Vec<&'_ str>>(selections) + }; let mut lines = unsafe { - // SAFETY: It is safe to transmute to a vector of lines with shorter lifetime, + // SAFETY: (same as above) It is safe to transmute to a vector of lines with shorter lifetime, // because it was only temporarily transmuted to a Vec> to make recycling possible. std::mem::transmute::>, Vec>>(lines) }; - let read = crash_if_err!(1, std::str::from_utf8(&buf[..read])); - parse_lines(read, &mut lines, separator, &settings); - lines + let read = crash_if_err!(1, std::str::from_utf8(&buffer[..read])); + let mut line_data = LineData { + selections, + num_infos, + parsed_floats, + }; + parse_lines(read, &mut lines, &mut line_data, separator, settings); + ChunkContents { lines, line_data } }); - if !payload.borrow_lines().is_empty() { - sender.send(payload).unwrap(); - } - if !should_continue { - *sender_option = None; - } + sender.send(payload).unwrap(); } + should_continue } /// Split `read` into `Line`s, and add them to `lines`. fn parse_lines<'a>( mut read: &'a str, lines: &mut Vec>, + line_data: &mut LineData<'a>, separator: u8, settings: &GlobalSettings, ) { @@ -133,9 +202,15 @@ fn parse_lines<'a>( read = &read[..read.len() - 1]; } + assert!(lines.is_empty()); + assert!(line_data.selections.is_empty()); + assert!(line_data.num_infos.is_empty()); + assert!(line_data.parsed_floats.is_empty()); + let mut token_buffer = vec![]; lines.extend( read.split(separator as char) - .map(|line| Line::create(line, settings)), + .enumerate() + .map(|(index, line)| Line::create(line, index, line_data, &mut token_buffer, settings)), ); } @@ -165,16 +240,16 @@ fn parse_lines<'a>( /// The remaining bytes must be copied to the start of the buffer for the next invocation, /// if another invocation is necessary, which is determined by the other return value. /// * Whether this function should be called again. -#[allow(clippy::borrowed_box)] -fn read_to_buffer( - file: &mut Box, - next_files: &mut impl Iterator>, +fn read_to_buffer( + file: &mut T, + next_files: &mut impl Iterator, buffer: &mut Vec, max_buffer_size: Option, start_offset: usize, separator: u8, ) -> (usize, bool) { let mut read_target = &mut buffer[start_offset..]; + let mut last_file_target_size = read_target.len(); loop { match file.read(read_target) { Ok(0) => { @@ -193,7 +268,7 @@ fn read_to_buffer( continue; } } - let mut sep_iter = memchr_iter(separator, &buffer).rev(); + let mut sep_iter = memchr_iter(separator, buffer).rev(); let last_line_end = sep_iter.next(); if sep_iter.next().is_some() { // We read enough lines. @@ -208,14 +283,27 @@ fn read_to_buffer( read_target = &mut buffer[len..]; } } else { - // This file is empty. + // This file has been fully read. + let mut leftover_len = read_target.len(); + if last_file_target_size != leftover_len { + // The file was not empty. + let read_len = buffer.len() - leftover_len; + if buffer[read_len - 1] != separator { + // The file did not end with a separator. We have to insert one. + buffer[read_len] = separator; + leftover_len -= 1; + } + let read_len = buffer.len() - leftover_len; + read_target = &mut buffer[read_len..]; + } if let Some(next_file) = next_files.next() { // There is another file. + last_file_target_size = leftover_len; *file = next_file; } else { // This was the last file. - let leftover_len = read_target.len(); - return (buffer.len() - leftover_len, false); + let read_len = buffer.len() - leftover_len; + return (read_len, false); } } } diff --git a/src/uu/sort/src/custom_str_cmp.rs b/src/uu/sort/src/custom_str_cmp.rs index a087a9fc2..089d33bc4 100644 --- a/src/uu/sort/src/custom_str_cmp.rs +++ b/src/uu/sort/src/custom_str_cmp.rs @@ -38,7 +38,7 @@ pub fn custom_str_cmp( ) -> Ordering { if !(ignore_case || ignore_non_dictionary || ignore_non_printing) { // There are no custom settings. Fall back to the default strcmp, which is faster. - return a.cmp(&b); + return a.cmp(b); } let mut a_chars = a .chars() diff --git a/src/uu/sort/src/ext_sort.rs b/src/uu/sort/src/ext_sort.rs index 9b1845efa..e0814b7a2 100644 --- a/src/uu/sort/src/ext_sort.rs +++ b/src/uu/sort/src/ext_sort.rs @@ -12,10 +12,10 @@ //! The buffers for the individual chunks are recycled. There are two buffers. use std::cmp::Ordering; -use std::io::{BufWriter, Write}; +use std::io::Write; use std::path::Path; +use std::path::PathBuf; use std::{ - fs::OpenOptions, io::Read, sync::mpsc::{Receiver, SyncSender}, thread, @@ -23,63 +23,115 @@ use std::{ use itertools::Itertools; -use tempfile::TempDir; - +use crate::chunks::RecycledChunk; +use crate::merge::ClosedTmpFile; +use crate::merge::WriteableCompressedTmpFile; +use crate::merge::WriteablePlainTmpFile; +use crate::merge::WriteableTmpFile; use crate::{ chunks::{self, Chunk}, - compare_by, merge, output_sorted_lines, sort_by, GlobalSettings, + compare_by, merge, sort_by, GlobalSettings, }; +use crate::{print_sorted, Line}; +use tempfile::TempDir; -const MIN_BUFFER_SIZE: usize = 8_000; +const START_BUFFER_SIZE: usize = 8_000; /// Sort files by using auxiliary files for storing intermediate chunks (if needed), and output the result. pub fn ext_sort(files: &mut impl Iterator>, settings: &GlobalSettings) { - let tmp_dir = crash_if_err!( - 1, - tempfile::Builder::new() - .prefix("uutils_sort") - .tempdir_in(&settings.tmp_dir) - ); let (sorted_sender, sorted_receiver) = std::sync::mpsc::sync_channel(1); let (recycled_sender, recycled_receiver) = std::sync::mpsc::sync_channel(1); thread::spawn({ let settings = settings.clone(); move || sorter(recycled_receiver, sorted_sender, settings) }); - let read_result = reader_writer( + if settings.compress_prog.is_some() { + reader_writer::<_, WriteableCompressedTmpFile>( + files, + settings, + sorted_receiver, + recycled_sender, + ); + } else { + reader_writer::<_, WriteablePlainTmpFile>( + files, + settings, + sorted_receiver, + recycled_sender, + ); + } +} + +fn reader_writer>, Tmp: WriteableTmpFile + 'static>( + files: F, + settings: &GlobalSettings, + receiver: Receiver, + sender: SyncSender, +) { + let separator = if settings.zero_terminated { + b'\0' + } else { + b'\n' + }; + + // Heuristically chosen: Dividing by 10 seems to keep our memory usage roughly + // around settings.buffer_size as a whole. + let buffer_size = settings.buffer_size / 10; + let read_result: ReadResult = read_write_loop( files, - &tmp_dir, - if settings.zero_terminated { - b'\0' - } else { - b'\n' - }, + &settings.tmp_dir, + separator, // Heuristically chosen: Dividing by 10 seems to keep our memory usage roughly // around settings.buffer_size as a whole. - settings.buffer_size / 10, - settings.clone(), - sorted_receiver, - recycled_sender, + buffer_size, + settings, + receiver, + sender, ); match read_result { - ReadResult::WroteChunksToFile { chunks_written } => { - let files = (0..chunks_written) - .map(|chunk_num| tmp_dir.path().join(chunk_num.to_string())) - .collect::>(); - let mut merger = merge::merge(&files, settings); + ReadResult::WroteChunksToFile { tmp_files, tmp_dir } => { + let tmp_dir_size = tmp_files.len(); + let mut merger = merge::merge_with_file_limit::<_, _, Tmp>( + tmp_files.into_iter().map(|c| c.reopen()), + settings, + Some((tmp_dir, tmp_dir_size)), + ); merger.write_all(settings); } ReadResult::SortedSingleChunk(chunk) => { - output_sorted_lines(chunk.borrow_lines().iter(), settings); + if settings.unique { + print_sorted( + chunk.lines().iter().dedup_by(|a, b| { + compare_by(a, b, settings, chunk.line_data(), chunk.line_data()) + == Ordering::Equal + }), + settings, + ); + } else { + print_sorted(chunk.lines().iter(), settings); + } } ReadResult::SortedTwoChunks([a, b]) => { - let merged_iter = a - .borrow_lines() - .iter() - .merge_by(b.borrow_lines().iter(), |line_a, line_b| { - compare_by(line_a, line_b, settings) != Ordering::Greater - }); - output_sorted_lines(merged_iter, settings); + let merged_iter = a.lines().iter().map(|line| (line, &a)).merge_by( + b.lines().iter().map(|line| (line, &b)), + |(line_a, a), (line_b, b)| { + compare_by(line_a, line_b, settings, a.line_data(), b.line_data()) + != Ordering::Greater + }, + ); + if settings.unique { + print_sorted( + merged_iter + .dedup_by(|(line_a, a), (line_b, b)| { + compare_by(line_a, line_b, settings, a.line_data(), b.line_data()) + == Ordering::Equal + }) + .map(|(line, _)| line), + settings, + ); + } else { + print_sorted(merged_iter.map(|(line, _)| line), settings); + } } ReadResult::EmptyInput => { // don't output anything @@ -90,13 +142,15 @@ pub fn ext_sort(files: &mut impl Iterator>, settings /// The function that is executed on the sorter thread. fn sorter(receiver: Receiver, sender: SyncSender, settings: GlobalSettings) { while let Ok(mut payload) = receiver.recv() { - payload.with_lines_mut(|lines| sort_by(lines, &settings)); + payload.with_contents_mut(|contents| { + sort_by(&mut contents.lines, &settings, &contents.line_data) + }); sender.send(payload).unwrap(); } } /// Describes how we read the chunks from the input. -enum ReadResult { +enum ReadResult { /// The input was empty. Nothing was read. EmptyInput, /// The input fits into a single Chunk, which was kept in memory. @@ -105,43 +159,42 @@ enum ReadResult { SortedTwoChunks([Chunk; 2]), /// The input was read into multiple chunks, which were written to auxiliary files. WroteChunksToFile { - /// The number of chunks written to auxiliary files. - chunks_written: usize, + tmp_files: Vec, + tmp_dir: TempDir, }, } - /// The function that is executed on the reader/writer thread. -/// -/// # Returns -/// * The number of chunks read. -fn reader_writer( +fn read_write_loop( mut files: impl Iterator>, - tmp_dir: &TempDir, + tmp_dir_parent: &Path, separator: u8, buffer_size: usize, - settings: GlobalSettings, + settings: &GlobalSettings, receiver: Receiver, sender: SyncSender, -) -> ReadResult { - let mut sender_option = Some(sender); - +) -> ReadResult { let mut file = files.next().unwrap(); let mut carry_over = vec![]; // kick things off with two reads for _ in 0..2 { - chunks::read( - &mut sender_option, - vec![0; MIN_BUFFER_SIZE], + let should_continue = chunks::read( + &sender, + RecycledChunk::new(if START_BUFFER_SIZE < buffer_size { + START_BUFFER_SIZE + } else { + buffer_size + }), Some(buffer_size), &mut carry_over, &mut file, &mut files, separator, - Vec::new(), - &settings, + settings, ); - if sender_option.is_none() { + + if !should_continue { + drop(sender); // We have already read the whole input. Since we are in our first two reads, // this means that we can fit the whole input into memory. Bypass writing below and // handle this case in a more straightforward way. @@ -157,50 +210,70 @@ fn reader_writer( } } + let tmp_dir = crash_if_err!( + 1, + tempfile::Builder::new() + .prefix("uutils_sort") + .tempdir_in(tmp_dir_parent) + ); + + let mut sender_option = Some(sender); let mut file_number = 0; + let mut tmp_files = vec![]; loop { let mut chunk = match receiver.recv() { Ok(it) => it, _ => { - return ReadResult::WroteChunksToFile { - chunks_written: file_number, - } + return ReadResult::WroteChunksToFile { tmp_files, tmp_dir }; } }; - write( + let tmp_file = write::( &mut chunk, - &tmp_dir.path().join(file_number.to_string()), + tmp_dir.path().join(file_number.to_string()), + settings.compress_prog.as_deref(), separator, ); + tmp_files.push(tmp_file); file_number += 1; - let (recycled_lines, recycled_buffer) = chunk.recycle(); + let recycled_chunk = chunk.recycle(); - chunks::read( - &mut sender_option, - recycled_buffer, - None, - &mut carry_over, - &mut file, - &mut files, - separator, - recycled_lines, - &settings, - ); + if let Some(sender) = &sender_option { + let should_continue = chunks::read( + sender, + recycled_chunk, + None, + &mut carry_over, + &mut file, + &mut files, + separator, + settings, + ); + if !should_continue { + sender_option = None; + } + } } } /// Write the lines in `chunk` to `file`, separated by `separator`. -fn write(chunk: &mut Chunk, file: &Path, separator: u8) { - chunk.with_lines_mut(|lines| { - // Write the lines to the file - let file = crash_if_err!(1, OpenOptions::new().create(true).write(true).open(file)); - let mut writer = BufWriter::new(file); - for s in lines.iter() { - crash_if_err!(1, writer.write_all(s.line.as_bytes())); - crash_if_err!(1, writer.write_all(&[separator])); - } - }); +/// `compress_prog` is used to optionally compress file contents. +fn write( + chunk: &mut Chunk, + file: PathBuf, + compress_prog: Option<&str>, + separator: u8, +) -> I::Closed { + let mut tmp_file = I::create(file, compress_prog); + write_lines(chunk.lines(), tmp_file.as_write(), separator); + tmp_file.finished_writing() +} + +fn write_lines<'a, T: Write>(lines: &[Line<'a>], writer: &mut T, separator: u8) { + for s in lines { + writer.write_all(s.line.as_bytes()).unwrap(); + writer.write_all(&[separator]).unwrap(); + } } diff --git a/src/uu/sort/src/merge.rs b/src/uu/sort/src/merge.rs index 696353829..12d7a9b9b 100644 --- a/src/uu/sort/src/merge.rs +++ b/src/uu/sort/src/merge.rs @@ -9,42 +9,130 @@ use std::{ cmp::Ordering, - ffi::OsStr, - io::{Read, Write}, + fs::{self, File}, + io::{BufWriter, Read, Write}, iter, + path::PathBuf, + process::{Child, ChildStdin, ChildStdout, Command, Stdio}, rc::Rc, sync::mpsc::{channel, sync_channel, Receiver, Sender, SyncSender}, thread, }; use compare::Compare; +use itertools::Itertools; +use tempfile::TempDir; use crate::{ - chunks::{self, Chunk}, - compare_by, open, GlobalSettings, + chunks::{self, Chunk, RecycledChunk}, + compare_by, GlobalSettings, }; -// Merge already sorted files. -pub fn merge<'a>(files: &[impl AsRef], settings: &'a GlobalSettings) -> FileMerger<'a> { +/// Merge pre-sorted `Box`s. +/// +/// If `settings.merge_batch_size` is greater than the length of `files`, intermediate files will be used. +/// If `settings.compress_prog` is `Some`, intermediate files will be compressed with it. +pub fn merge>>( + files: Files, + settings: &GlobalSettings, +) -> FileMerger { + if settings.compress_prog.is_none() { + merge_with_file_limit::<_, _, WriteablePlainTmpFile>( + files.map(|file| PlainMergeInput { inner: file }), + settings, + None, + ) + } else { + merge_with_file_limit::<_, _, WriteableCompressedTmpFile>( + files.map(|file| PlainMergeInput { inner: file }), + settings, + None, + ) + } +} + +// Merge already sorted `MergeInput`s. +pub fn merge_with_file_limit< + M: MergeInput + 'static, + F: ExactSizeIterator, + Tmp: WriteableTmpFile + 'static, +>( + files: F, + settings: &GlobalSettings, + tmp_dir: Option<(TempDir, usize)>, +) -> FileMerger { + if files.len() > settings.merge_batch_size { + // If we did not get a tmp_dir, create one. + let (tmp_dir, mut tmp_dir_size) = tmp_dir.unwrap_or_else(|| { + ( + tempfile::Builder::new() + .prefix("uutils_sort") + .tempdir_in(&settings.tmp_dir) + .unwrap(), + 0, + ) + }); + let mut remaining_files = files.len(); + let batches = files.chunks(settings.merge_batch_size); + let mut batches = batches.into_iter(); + let mut temporary_files = vec![]; + while remaining_files != 0 { + // Work around the fact that `Chunks` is not an `ExactSizeIterator`. + remaining_files = remaining_files.saturating_sub(settings.merge_batch_size); + let mut merger = merge_without_limit(batches.next().unwrap(), settings); + let mut tmp_file = Tmp::create( + tmp_dir.path().join(tmp_dir_size.to_string()), + settings.compress_prog.as_deref(), + ); + tmp_dir_size += 1; + merger.write_all_to(settings, tmp_file.as_write()); + temporary_files.push(tmp_file.finished_writing()); + } + assert!(batches.next().is_none()); + merge_with_file_limit::<_, _, Tmp>( + temporary_files + .into_iter() + .map(Box::new(|c: Tmp::Closed| c.reopen()) + as Box< + dyn FnMut(Tmp::Closed) -> ::Reopened, + >), + settings, + Some((tmp_dir, tmp_dir_size)), + ) + } else { + merge_without_limit(files, settings) + } +} + +/// Merge files without limiting how many files are concurrently open. +/// +/// It is the responsibility of the caller to ensure that `files` yields only +/// as many files as we are allowed to open concurrently. +fn merge_without_limit>( + files: F, + settings: &GlobalSettings, +) -> FileMerger { let (request_sender, request_receiver) = channel(); - let mut reader_files = Vec::with_capacity(files.len()); - let mut loaded_receivers = Vec::with_capacity(files.len()); - for (file_number, file) in files.iter().map(open).enumerate() { + let mut reader_files = Vec::with_capacity(files.size_hint().0); + let mut loaded_receivers = Vec::with_capacity(files.size_hint().0); + for (file_number, file) in files.enumerate() { let (sender, receiver) = sync_channel(2); loaded_receivers.push(receiver); - reader_files.push(ReaderFile { + reader_files.push(Some(ReaderFile { file, - sender: Some(sender), + sender, carry_over: vec![], - }); + })); + // Send the initial chunk to trigger a read for each file request_sender - .send((file_number, Chunk::new(vec![0; 8 * 1024], |_| Vec::new()))) + .send((file_number, RecycledChunk::new(8 * 1024))) .unwrap(); } + // Send the second chunk for each file for file_number in 0..reader_files.len() { request_sender - .send((file_number, Chunk::new(vec![0; 8 * 1024], |_| Vec::new()))) + .send((file_number, RecycledChunk::new(8 * 1024))) .unwrap(); } @@ -85,37 +173,43 @@ pub fn merge<'a>(files: &[impl AsRef], settings: &'a GlobalSettings) -> F } } /// The struct on the reader thread representing an input file -struct ReaderFile { - file: Box, - sender: Option>, +struct ReaderFile { + file: M, + sender: SyncSender, carry_over: Vec, } /// The function running on the reader thread. fn reader( - recycled_receiver: Receiver<(usize, Chunk)>, - files: &mut [ReaderFile], + recycled_receiver: Receiver<(usize, RecycledChunk)>, + files: &mut [Option>], settings: &GlobalSettings, separator: u8, ) { - for (file_idx, chunk) in recycled_receiver.iter() { - let (recycled_lines, recycled_buffer) = chunk.recycle(); - let ReaderFile { + for (file_idx, recycled_chunk) in recycled_receiver.iter() { + if let Some(ReaderFile { file, sender, carry_over, - } = &mut files[file_idx]; - chunks::read( - sender, - recycled_buffer, - None, - carry_over, - file, - &mut iter::empty(), - separator, - recycled_lines, - settings, - ); + }) = &mut files[file_idx] + { + let should_continue = chunks::read( + sender, + recycled_chunk, + None, + carry_over, + file.as_read(), + &mut iter::empty(), + separator, + settings, + ); + if !should_continue { + // Remove the file from the list by replacing it with `None`. + let ReaderFile { file, .. } = files[file_idx].take().unwrap(); + // Depending on the kind of the `MergeInput`, this may delete the file: + file.finished_reading(); + } + } } } /// The struct on the main thread representing an input file @@ -138,7 +232,7 @@ struct PreviousLine { /// Merges files together. This is **not** an iterator because of lifetime problems. pub struct FileMerger<'a> { heap: binary_heap_plus::BinaryHeap>, - request_sender: Sender<(usize, Chunk)>, + request_sender: Sender<(usize, RecycledChunk)>, prev: Option, } @@ -146,7 +240,11 @@ impl<'a> FileMerger<'a> { /// Write the merged contents to the output file. pub fn write_all(&mut self, settings: &GlobalSettings) { let mut out = settings.out_writer(); - while self.write_next(settings, &mut out) {} + self.write_all_to(settings, &mut out); + } + + pub fn write_all_to(&mut self, settings: &GlobalSettings, out: &mut impl Write) { + while self.write_next(settings, out) {} } fn write_next(&mut self, settings: &GlobalSettings, out: &mut impl Write) -> bool { @@ -157,14 +255,16 @@ impl<'a> FileMerger<'a> { file_number: file.file_number, }); - file.current_chunk.with_lines(|lines| { - let current_line = &lines[file.line_idx]; + file.current_chunk.with_contents(|contents| { + let current_line = &contents.lines[file.line_idx]; if settings.unique { if let Some(prev) = &prev { let cmp = compare_by( - &prev.chunk.borrow_lines()[prev.line_idx], + &prev.chunk.lines()[prev.line_idx], current_line, settings, + prev.chunk.line_data(), + file.current_chunk.line_data(), ); if cmp == Ordering::Equal { return; @@ -174,8 +274,7 @@ impl<'a> FileMerger<'a> { current_line.print(out, settings); }); - let was_last_line_for_file = - file.current_chunk.borrow_lines().len() == file.line_idx + 1; + let was_last_line_for_file = file.current_chunk.lines().len() == file.line_idx + 1; if was_last_line_for_file { if let Ok(next_chunk) = file.receiver.recv() { @@ -186,13 +285,16 @@ impl<'a> FileMerger<'a> { self.heap.pop(); } } else { + // This will cause the comparison to use a different line and the heap to readjust. self.heap.peek_mut().unwrap().line_idx += 1; } if let Some(prev) = prev { if let Ok(prev_chunk) = Rc::try_unwrap(prev.chunk) { + // If nothing is referencing the previous chunk anymore, this means that the previous line + // was the last line of the chunk. We can recycle the chunk. self.request_sender - .send((prev.file_number, prev_chunk)) + .send((prev.file_number, prev_chunk.recycle())) .ok(); } } @@ -209,16 +311,206 @@ struct FileComparator<'a> { impl<'a> Compare for FileComparator<'a> { fn compare(&self, a: &MergeableFile, b: &MergeableFile) -> Ordering { let mut cmp = compare_by( - &a.current_chunk.borrow_lines()[a.line_idx], - &b.current_chunk.borrow_lines()[b.line_idx], + &a.current_chunk.lines()[a.line_idx], + &b.current_chunk.lines()[b.line_idx], self.settings, + a.current_chunk.line_data(), + b.current_chunk.line_data(), ); if cmp == Ordering::Equal { // To make sorting stable, we need to consider the file number as well, // as lines from a file with a lower number are to be considered "earlier". cmp = a.file_number.cmp(&b.file_number); } - // Our BinaryHeap is a max heap. We use it as a min heap, so we need to reverse the ordering. + // BinaryHeap is a max heap. We use it as a min heap, so we need to reverse the ordering. cmp.reverse() } } + +// Wait for the child to exit and check its exit code. +fn assert_child_success(mut child: Child, program: &str) { + if !matches!( + child.wait().map(|e| e.code()), + Ok(Some(0)) | Ok(None) | Err(_) + ) { + crash!(2, "'{}' terminated abnormally", program) + } +} + +/// A temporary file that can be written to. +pub trait WriteableTmpFile { + type Closed: ClosedTmpFile; + type InnerWrite: Write; + fn create(path: PathBuf, compress_prog: Option<&str>) -> Self; + /// Closes the temporary file. + fn finished_writing(self) -> Self::Closed; + fn as_write(&mut self) -> &mut Self::InnerWrite; +} +/// A temporary file that is (temporarily) closed, but can be reopened. +pub trait ClosedTmpFile { + type Reopened: MergeInput; + /// Reopens the temporary file. + fn reopen(self) -> Self::Reopened; +} +/// A pre-sorted input for merging. +pub trait MergeInput: Send { + type InnerRead: Read; + /// Cleans this `MergeInput` up. + /// Implementations may delete the backing file. + fn finished_reading(self); + fn as_read(&mut self) -> &mut Self::InnerRead; +} + +pub struct WriteablePlainTmpFile { + path: PathBuf, + file: BufWriter, +} +pub struct ClosedPlainTmpFile { + path: PathBuf, +} +pub struct PlainTmpMergeInput { + path: PathBuf, + file: File, +} +impl WriteableTmpFile for WriteablePlainTmpFile { + type Closed = ClosedPlainTmpFile; + type InnerWrite = BufWriter; + + fn create(path: PathBuf, _: Option<&str>) -> Self { + WriteablePlainTmpFile { + file: BufWriter::new(File::create(&path).unwrap()), + path, + } + } + + fn finished_writing(self) -> Self::Closed { + ClosedPlainTmpFile { path: self.path } + } + + fn as_write(&mut self) -> &mut Self::InnerWrite { + &mut self.file + } +} +impl ClosedTmpFile for ClosedPlainTmpFile { + type Reopened = PlainTmpMergeInput; + fn reopen(self) -> Self::Reopened { + PlainTmpMergeInput { + file: File::open(&self.path).unwrap(), + path: self.path, + } + } +} +impl MergeInput for PlainTmpMergeInput { + type InnerRead = File; + + fn finished_reading(self) { + fs::remove_file(self.path).ok(); + } + + fn as_read(&mut self) -> &mut Self::InnerRead { + &mut self.file + } +} + +pub struct WriteableCompressedTmpFile { + path: PathBuf, + compress_prog: String, + child: Child, + child_stdin: BufWriter, +} +pub struct ClosedCompressedTmpFile { + path: PathBuf, + compress_prog: String, +} +pub struct CompressedTmpMergeInput { + path: PathBuf, + compress_prog: String, + child: Child, + child_stdout: ChildStdout, +} +impl WriteableTmpFile for WriteableCompressedTmpFile { + type Closed = ClosedCompressedTmpFile; + type InnerWrite = BufWriter; + + fn create(path: PathBuf, compress_prog: Option<&str>) -> Self { + let compress_prog = compress_prog.unwrap(); + let mut command = Command::new(compress_prog); + command + .stdin(Stdio::piped()) + .stdout(File::create(&path).unwrap()); + let mut child = crash_if_err!( + 2, + command.spawn().map_err(|err| format!( + "couldn't execute compress program: errno {}", + err.raw_os_error().unwrap() + )) + ); + let child_stdin = child.stdin.take().unwrap(); + WriteableCompressedTmpFile { + path, + compress_prog: compress_prog.to_owned(), + child, + child_stdin: BufWriter::new(child_stdin), + } + } + + fn finished_writing(self) -> Self::Closed { + drop(self.child_stdin); + assert_child_success(self.child, &self.compress_prog); + ClosedCompressedTmpFile { + path: self.path, + compress_prog: self.compress_prog, + } + } + + fn as_write(&mut self) -> &mut Self::InnerWrite { + &mut self.child_stdin + } +} +impl ClosedTmpFile for ClosedCompressedTmpFile { + type Reopened = CompressedTmpMergeInput; + + fn reopen(self) -> Self::Reopened { + let mut command = Command::new(&self.compress_prog); + let file = File::open(&self.path).unwrap(); + command.stdin(file).stdout(Stdio::piped()).arg("-d"); + let mut child = crash_if_err!( + 2, + command.spawn().map_err(|err| format!( + "couldn't execute compress program: errno {}", + err.raw_os_error().unwrap() + )) + ); + let child_stdout = child.stdout.take().unwrap(); + CompressedTmpMergeInput { + path: self.path, + compress_prog: self.compress_prog, + child, + child_stdout, + } + } +} +impl MergeInput for CompressedTmpMergeInput { + type InnerRead = ChildStdout; + + fn finished_reading(self) { + drop(self.child_stdout); + assert_child_success(self.child, &self.compress_prog); + fs::remove_file(self.path).ok(); + } + + fn as_read(&mut self) -> &mut Self::InnerRead { + &mut self.child_stdout + } +} + +pub struct PlainMergeInput { + inner: R, +} +impl MergeInput for PlainMergeInput { + type InnerRead = R; + fn finished_reading(self) {} + fn as_read(&mut self) -> &mut Self::InnerRead { + &mut self.inner + } +} diff --git a/src/uu/sort/src/numeric_str_cmp.rs b/src/uu/sort/src/numeric_str_cmp.rs index 8cd3faab2..d753c2d9d 100644 --- a/src/uu/sort/src/numeric_str_cmp.rs +++ b/src/uu/sort/src/numeric_str_cmp.rs @@ -81,28 +81,12 @@ impl NumInfo { } if Self::is_invalid_char(char, &mut had_decimal_pt, &parse_settings) { - let si_unit = if parse_settings.accept_si_units { - match char { - 'K' | 'k' => 3, - 'M' => 6, - 'G' => 9, - 'T' => 12, - 'P' => 15, - 'E' => 18, - 'Z' => 21, - 'Y' => 24, - _ => 0, - } - } else { - 0 - }; return if let Some(start) = start { + let has_si_unit = parse_settings.accept_si_units + && matches!(char, 'K' | 'k' | 'M' | 'G' | 'T' | 'P' | 'E' | 'Z' | 'Y'); ( - NumInfo { - exponent: exponent + si_unit, - sign, - }, - start..idx, + NumInfo { exponent, sign }, + start..if has_si_unit { idx + 1 } else { idx }, ) } else { ( @@ -182,8 +166,53 @@ impl NumInfo { } } -/// compare two numbers as strings without parsing them as a number first. This should be more performant and can handle numbers more precisely. +fn get_unit(unit: Option) -> u8 { + if let Some(unit) = unit { + match unit { + 'K' | 'k' => 1, + 'M' => 2, + 'G' => 3, + 'T' => 4, + 'P' => 5, + 'E' => 6, + 'Z' => 7, + 'Y' => 8, + _ => 0, + } + } else { + 0 + } +} + +/// Compare two numbers according to the rules of human numeric comparison. +/// The SI-Unit takes precedence over the actual value (i.e. 2000M < 1G). +pub fn human_numeric_str_cmp( + (a, a_info): (&str, &NumInfo), + (b, b_info): (&str, &NumInfo), +) -> Ordering { + // 1. Sign + if a_info.sign != b_info.sign { + return a_info.sign.cmp(&b_info.sign); + } + // 2. Unit + let a_unit = get_unit(a.chars().next_back()); + let b_unit = get_unit(b.chars().next_back()); + let ordering = a_unit.cmp(&b_unit); + if ordering != Ordering::Equal { + if a_info.sign == Sign::Negative { + ordering.reverse() + } else { + ordering + } + } else { + // 3. Number + numeric_str_cmp((a, a_info), (b, b_info)) + } +} + +/// Compare two numbers as strings without parsing them as a number first. This should be more performant and can handle numbers more precisely. /// NumInfo is needed to provide a fast path for most numbers. +#[inline(always)] pub fn numeric_str_cmp((a, a_info): (&str, &NumInfo), (b, b_info): (&str, &NumInfo)) -> Ordering { // check for a difference in the sign if a_info.sign != b_info.sign { diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index 5825e73bd..1ba5ee0b5 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -23,16 +23,15 @@ mod ext_sort; mod merge; mod numeric_str_cmp; +use chunks::LineData; use clap::{crate_version, App, Arg}; use custom_str_cmp::custom_str_cmp; use ext_sort::ext_sort; use fnv::FnvHasher; -use itertools::Itertools; -use numeric_str_cmp::{numeric_str_cmp, NumInfo, NumInfoParseSettings}; +use numeric_str_cmp::{human_numeric_str_cmp, numeric_str_cmp, NumInfo, NumInfoParseSettings}; use rand::distributions::Alphanumeric; use rand::{thread_rng, Rng}; use rayon::prelude::*; -use semver::Version; use std::cmp::Ordering; use std::env; use std::ffi::OsStr; @@ -43,10 +42,12 @@ use std::ops::Range; use std::path::Path; use std::path::PathBuf; use unicode_width::UnicodeWidthStr; +use uucore::parse_size::{parse_size, ParseSizeError}; +use uucore::version_cmp::version_cmp; use uucore::InvalidEncodingHandling; -static NAME: &str = "sort"; -static ABOUT: &str = "Display sorted concatenation of all FILE(s)."; +const NAME: &str = "sort"; +const ABOUT: &str = "Display sorted concatenation of all FILE(s)."; const LONG_HELP_KEYS: &str = "The key format is FIELD[.CHAR][OPTIONS][,FIELD[.CHAR]][OPTIONS]. @@ -58,47 +59,59 @@ If CHAR is set 0, it means the end of the field. CHAR defaults to 1 for the star Valid options are: MbdfhnRrV. They override the global options for this key."; -static OPT_HUMAN_NUMERIC_SORT: &str = "human-numeric-sort"; -static OPT_MONTH_SORT: &str = "month-sort"; -static OPT_NUMERIC_SORT: &str = "numeric-sort"; -static OPT_GENERAL_NUMERIC_SORT: &str = "general-numeric-sort"; -static OPT_VERSION_SORT: &str = "version-sort"; +mod options { + pub mod modes { + pub const SORT: &str = "sort"; -static OPT_SORT: &str = "sort"; + pub const HUMAN_NUMERIC: &str = "human-numeric-sort"; + pub const MONTH: &str = "month-sort"; + pub const NUMERIC: &str = "numeric-sort"; + pub const GENERAL_NUMERIC: &str = "general-numeric-sort"; + pub const VERSION: &str = "version-sort"; + pub const RANDOM: &str = "random-sort"; -static ALL_SORT_MODES: &[&str] = &[ - OPT_GENERAL_NUMERIC_SORT, - OPT_HUMAN_NUMERIC_SORT, - OPT_MONTH_SORT, - OPT_NUMERIC_SORT, - OPT_VERSION_SORT, - OPT_RANDOM, -]; + pub const ALL_SORT_MODES: [&str; 6] = [ + GENERAL_NUMERIC, + HUMAN_NUMERIC, + MONTH, + NUMERIC, + VERSION, + RANDOM, + ]; + } -static OPT_DICTIONARY_ORDER: &str = "dictionary-order"; -static OPT_MERGE: &str = "merge"; -static OPT_CHECK: &str = "check"; -static OPT_CHECK_SILENT: &str = "check-silent"; -static OPT_DEBUG: &str = "debug"; -static OPT_IGNORE_CASE: &str = "ignore-case"; -static OPT_IGNORE_BLANKS: &str = "ignore-blanks"; -static OPT_IGNORE_NONPRINTING: &str = "ignore-nonprinting"; -static OPT_OUTPUT: &str = "output"; -static OPT_REVERSE: &str = "reverse"; -static OPT_STABLE: &str = "stable"; -static OPT_UNIQUE: &str = "unique"; -static OPT_KEY: &str = "key"; -static OPT_SEPARATOR: &str = "field-separator"; -static OPT_RANDOM: &str = "random-sort"; -static OPT_ZERO_TERMINATED: &str = "zero-terminated"; -static OPT_PARALLEL: &str = "parallel"; -static OPT_FILES0_FROM: &str = "files0-from"; -static OPT_BUF_SIZE: &str = "buffer-size"; -static OPT_TMP_DIR: &str = "temporary-directory"; + pub mod check { + pub const CHECK: &str = "check"; + pub const CHECK_SILENT: &str = "check-silent"; + pub const SILENT: &str = "silent"; + pub const QUIET: &str = "quiet"; + pub const DIAGNOSE_FIRST: &str = "diagnose-first"; + } -static ARG_FILES: &str = "files"; + pub const DICTIONARY_ORDER: &str = "dictionary-order"; + pub const MERGE: &str = "merge"; + pub const DEBUG: &str = "debug"; + pub const IGNORE_CASE: &str = "ignore-case"; + pub const IGNORE_LEADING_BLANKS: &str = "ignore-leading-blanks"; + pub const IGNORE_NONPRINTING: &str = "ignore-nonprinting"; + pub const OUTPUT: &str = "output"; + pub const REVERSE: &str = "reverse"; + pub const STABLE: &str = "stable"; + pub const UNIQUE: &str = "unique"; + pub const KEY: &str = "key"; + pub const SEPARATOR: &str = "field-separator"; + pub const ZERO_TERMINATED: &str = "zero-terminated"; + pub const PARALLEL: &str = "parallel"; + pub const FILES0_FROM: &str = "files0-from"; + pub const BUF_SIZE: &str = "buffer-size"; + pub const TMP_DIR: &str = "temporary-directory"; + pub const COMPRESS_PROG: &str = "compress-program"; + pub const BATCH_SIZE: &str = "batch-size"; -static DECIMAL_PT: char = '.'; + pub const FILES: &str = "files"; +} + +const DECIMAL_PT: char = '.'; const NEGATIVE: char = '-'; const POSITIVE: char = '+'; @@ -106,7 +119,7 @@ const POSITIVE: char = '+'; // Choosing a higher buffer size does not result in performance improvements // (at least not on my machine). TODO: In the future, we should also take the amount of // available memory into consideration, instead of relying on this constant only. -static DEFAULT_BUF_SIZE: usize = 1_000_000_000; // 1 GB +const DEFAULT_BUF_SIZE: usize = 1_000_000_000; // 1 GB #[derive(Eq, Ord, PartialEq, PartialOrd, Clone, Copy, Debug)] enum SortMode { @@ -137,7 +150,7 @@ impl SortMode { pub struct GlobalSettings { mode: SortMode, debug: bool, - ignore_blanks: bool, + ignore_leading_blanks: bool, ignore_case: bool, dictionary_order: bool, ignore_non_printing: bool, @@ -155,35 +168,45 @@ pub struct GlobalSettings { zero_terminated: bool, buffer_size: usize, tmp_dir: PathBuf, + compress_prog: Option, + merge_batch_size: usize, + precomputed: Precomputed, +} + +/// Data needed for sorting. Should be computed once before starting to sort +/// by calling `GlobalSettings::init_precomputed`. +#[derive(Clone, Debug)] +struct Precomputed { + needs_tokens: bool, + num_infos_per_line: usize, + floats_per_line: usize, + selections_per_line: usize, } impl GlobalSettings { - /// Interpret this `&str` as a number with an optional trailing si unit. - /// - /// If there is no trailing si unit, the implicit unit is K. - /// The suffix B causes the number to be interpreted as a byte count. - fn parse_byte_count(input: &str) -> usize { - const SI_UNITS: &[char] = &['B', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']; + /// Parse a SIZE string into a number of bytes. + /// A size string comprises an integer and an optional unit. + /// The unit may be k, K, m, M, g, G, t, T, P, E, Z, Y (powers of 1024), or b which is 1. + /// Default is K. + fn parse_byte_count(input: &str) -> Result { + // GNU sort (8.32) valid: 1b, k, K, m, M, g, G, t, T, P, E, Z, Y + // GNU sort (8.32) invalid: b, B, 1B, p, e, z, y + const ALLOW_LIST: &[char] = &[ + 'b', 'k', 'K', 'm', 'M', 'g', 'G', 't', 'T', 'P', 'E', 'Z', 'Y', + ]; + let mut size_string = input.trim().to_string(); - let input = input.trim(); - - let (num_str, si_unit) = - if input.ends_with(|c: char| SI_UNITS.contains(&c.to_ascii_uppercase())) { - let mut chars = input.chars(); - let si_suffix = chars.next_back().unwrap().to_ascii_uppercase(); - let si_unit = SI_UNITS.iter().position(|&c| c == si_suffix).unwrap(); - let num_str = chars.as_str(); - (num_str, si_unit) - } else { - (input, 1) - }; - - let num_usize: usize = num_str - .trim() - .parse() - .unwrap_or_else(|e| crash!(1, "failed to parse buffer size `{}`: {}", num_str, e)); - - num_usize.saturating_mul(1000usize.saturating_pow(si_unit as u32)) + if size_string.ends_with(|c: char| ALLOW_LIST.contains(&c) || c.is_digit(10)) { + // b 1, K 1024 (default) + if size_string.ends_with(|c: char| c.is_digit(10)) { + size_string.push('K'); + } else if size_string.ends_with('b') { + size_string.pop(); + } + parse_size(&size_string) + } else { + Err(ParseSizeError::ParseFailure("invalid suffix".to_string())) + } } fn out_writer(&self) -> BufWriter> { @@ -198,6 +221,25 @@ impl GlobalSettings { None => BufWriter::new(Box::new(stdout()) as Box), } } + + /// Precompute some data needed for sorting. + /// This function **must** be called before starting to sort, and `GlobalSettings` may not be altered + /// afterwards. + fn init_precomputed(&mut self) { + self.precomputed.needs_tokens = self.selectors.iter().any(|s| s.needs_tokens); + self.precomputed.selections_per_line = + self.selectors.iter().filter(|s| s.needs_selection).count(); + self.precomputed.num_infos_per_line = self + .selectors + .iter() + .filter(|s| matches!(s.settings.mode, SortMode::Numeric | SortMode::HumanNumeric)) + .count(); + self.precomputed.floats_per_line = self + .selectors + .iter() + .filter(|s| matches!(s.settings.mode, SortMode::GeneralNumeric)) + .count(); + } } impl Default for GlobalSettings { @@ -205,7 +247,7 @@ impl Default for GlobalSettings { GlobalSettings { mode: SortMode::Default, debug: false, - ignore_blanks: false, + ignore_leading_blanks: false, ignore_case: false, dictionary_order: false, ignore_non_printing: false, @@ -223,9 +265,18 @@ impl Default for GlobalSettings { zero_terminated: false, buffer_size: DEFAULT_BUF_SIZE, tmp_dir: PathBuf::new(), + compress_prog: None, + merge_batch_size: 32, + precomputed: Precomputed { + num_infos_per_line: 0, + floats_per_line: 0, + selections_per_line: 0, + needs_tokens: false, + }, } } } + #[derive(Clone, PartialEq, Debug)] struct KeySettings { mode: SortMode, @@ -294,7 +345,7 @@ impl From<&GlobalSettings> for KeySettings { fn from(settings: &GlobalSettings) -> Self { Self { mode: settings.mode, - ignore_blanks: settings.ignore_blanks, + ignore_blanks: settings.ignore_leading_blanks, ignore_case: settings.ignore_case, ignore_non_printing: settings.ignore_non_printing, reverse: settings.reverse, @@ -308,32 +359,10 @@ impl Default for KeySettings { Self::from(&GlobalSettings::default()) } } - -#[derive(Clone, Debug)] -enum NumCache { +enum Selection<'a> { AsF64(GeneralF64ParseResult), - WithInfo(NumInfo), -} - -impl NumCache { - fn as_f64(&self) -> GeneralF64ParseResult { - match self { - NumCache::AsF64(n) => *n, - _ => unreachable!(), - } - } - fn as_num_info(&self) -> &NumInfo { - match self { - NumCache::WithInfo(n) => n, - _ => unreachable!(), - } - } -} - -#[derive(Clone, Debug)] -struct Selection<'a> { - slice: &'a str, - num_cache: Option>, + WithNumInfo(&'a str, NumInfo), + Str(&'a str), } type Field = Range; @@ -341,42 +370,55 @@ type Field = Range; #[derive(Clone, Debug)] pub struct Line<'a> { line: &'a str, - selections: Box<[Selection<'a>]>, + index: usize, } impl<'a> Line<'a> { - fn create(string: &'a str, settings: &GlobalSettings) -> Self { - let fields = if settings + /// Creates a new `Line`. + /// + /// If additional data is needed for sorting it is added to `line_data`. + /// `token_buffer` allows to reuse the allocation for tokens. + fn create( + line: &'a str, + index: usize, + line_data: &mut LineData<'a>, + token_buffer: &mut Vec, + settings: &GlobalSettings, + ) -> Self { + token_buffer.clear(); + if settings.precomputed.needs_tokens { + tokenize(line, settings.separator, token_buffer); + } + for (selector, selection) in settings .selectors .iter() - .any(|selector| selector.needs_tokens) + .map(|selector| (selector, selector.get_selection(line, token_buffer))) { - // Only tokenize if we will need tokens. - Some(tokenize(string, settings.separator)) - } else { - None - }; - - Line { - line: string, - selections: settings - .selectors - .iter() - .filter(|selector| !selector.is_default_selection) - .map(|selector| selector.get_selection(string, fields.as_deref())) - .collect(), + match selection { + Selection::AsF64(parsed_float) => line_data.parsed_floats.push(parsed_float), + Selection::WithNumInfo(str, num_info) => { + line_data.num_infos.push(num_info); + line_data.selections.push(str); + } + Selection::Str(str) => { + if selector.needs_selection { + line_data.selections.push(str) + } + } + } } + Self { line, index } } fn print(&self, writer: &mut impl Write, settings: &GlobalSettings) { if settings.zero_terminated && !settings.debug { - crash_if_err!(1, writer.write_all(self.line.as_bytes())); - crash_if_err!(1, writer.write_all(b"\0")); + writer.write_all(self.line.as_bytes()).unwrap(); + writer.write_all(b"\0").unwrap(); } else if !settings.debug { - crash_if_err!(1, writer.write_all(self.line.as_bytes())); - crash_if_err!(1, writer.write_all(b"\n")); + writer.write_all(self.line.as_bytes()).unwrap(); + writer.write_all(b"\n").unwrap(); } else { - crash_if_err!(1, self.print_debug(settings, writer)); + self.print_debug(settings, writer).unwrap(); } } @@ -394,9 +436,10 @@ impl<'a> Line<'a> { let line = self.line.replace('\t', ">"); writeln!(writer, "{}", line)?; - let fields = tokenize(&self.line, settings.separator); + let mut fields = vec![]; + tokenize(self.line, settings.separator, &mut fields); for selector in settings.selectors.iter() { - let mut selection = selector.get_range(&self.line, Some(&fields)); + let mut selection = selector.get_range(self.line, Some(&fields)); match selector.settings.mode { SortMode::Numeric | SortMode::HumanNumeric => { // find out which range is used for numeric comparisons @@ -501,7 +544,7 @@ impl<'a> Line<'a> { && !settings.stable && !settings.unique && (settings.dictionary_order - || settings.ignore_blanks + || settings.ignore_leading_blanks || settings.ignore_case || settings.ignore_non_printing || settings.mode != SortMode::Default @@ -525,51 +568,51 @@ impl<'a> Line<'a> { } } -/// Tokenize a line into fields. -fn tokenize(line: &str, separator: Option) -> Vec { +/// Tokenize a line into fields. The result is stored into `token_buffer`. +fn tokenize(line: &str, separator: Option, token_buffer: &mut Vec) { + assert!(token_buffer.is_empty()); if let Some(separator) = separator { - tokenize_with_separator(line, separator) + tokenize_with_separator(line, separator, token_buffer) } else { - tokenize_default(line) + tokenize_default(line, token_buffer) } } /// By default fields are separated by the first whitespace after non-whitespace. /// Whitespace is included in fields at the start. -fn tokenize_default(line: &str) -> Vec { - let mut tokens = vec![0..0]; +/// The result is stored into `token_buffer`. +fn tokenize_default(line: &str, token_buffer: &mut Vec) { + token_buffer.push(0..0); // pretend that there was whitespace in front of the line let mut previous_was_whitespace = true; for (idx, char) in line.char_indices() { if char.is_whitespace() { if !previous_was_whitespace { - tokens.last_mut().unwrap().end = idx; - tokens.push(idx..0); + token_buffer.last_mut().unwrap().end = idx; + token_buffer.push(idx..0); } previous_was_whitespace = true; } else { previous_was_whitespace = false; } } - tokens.last_mut().unwrap().end = line.len(); - tokens + token_buffer.last_mut().unwrap().end = line.len(); } /// Split between separators. These separators are not included in fields. -fn tokenize_with_separator(line: &str, separator: char) -> Vec { - let mut tokens = vec![]; +/// The result is stored into `token_buffer`. +fn tokenize_with_separator(line: &str, separator: char, token_buffer: &mut Vec) { let separator_indices = line.char_indices() .filter_map(|(i, c)| if c == separator { Some(i) } else { None }); let mut start = 0; for sep_idx in separator_indices { - tokens.push(start..sep_idx); + token_buffer.push(start..sep_idx); start = sep_idx + 1; } if start < line.len() { - tokens.push(start..line.len()); + token_buffer.push(start..line.len()); } - tokens } #[derive(Clone, PartialEq, Debug)] @@ -626,8 +669,10 @@ struct FieldSelector { to: Option, settings: KeySettings, needs_tokens: bool, - // Whether the selection for each line is going to be the whole line with no NumCache - is_default_selection: bool, + // Whether this selector operates on a sub-slice of a line. + // Selections are therefore not needed when this selector matches the whole line + // or the sort mode is general-numeric. + needs_selection: bool, } impl Default for FieldSelector { @@ -637,7 +682,7 @@ impl Default for FieldSelector { to: None, settings: Default::default(), needs_tokens: false, - is_default_selection: true, + needs_selection: false, } } } @@ -665,9 +710,11 @@ impl FieldSelector { // This would be ideal for a try block, I think. In the meantime this closure allows // to use the `?` operator here. Self::new( - KeyPosition::new(from, 1, global_settings.ignore_blanks)?, - to.map(|(to, _)| KeyPosition::new(to, 0, global_settings.ignore_blanks)) - .transpose()?, + KeyPosition::new(from, 1, global_settings.ignore_leading_blanks)?, + to.map(|(to, _)| { + KeyPosition::new(to, 0, global_settings.ignore_leading_blanks) + }) + .transpose()?, KeySettings::from(global_settings), ) })() @@ -731,14 +778,12 @@ impl FieldSelector { Err("invalid character index 0 for the start position of a field".to_string()) } else { Ok(Self { - is_default_selection: from.field == 1 - && from.char == 1 - && to.is_none() - && !matches!( - settings.mode, - SortMode::Numeric | SortMode::GeneralNumeric | SortMode::HumanNumeric - ) - && !from.ignore_blanks, + needs_selection: (from.field != 1 + || from.char != 1 + || to.is_some() + || matches!(settings.mode, SortMode::Numeric | SortMode::HumanNumeric) + || from.ignore_blanks) + && !matches!(settings.mode, SortMode::GeneralNumeric), needs_tokens: from.field != 1 || from.char == 0 || to.is_some(), from, to, @@ -748,12 +793,16 @@ impl FieldSelector { } /// Get the selection that corresponds to this selector for the line. - /// If needs_fields returned false, tokens may be None. - fn get_selection<'a>(&self, line: &'a str, tokens: Option<&[Field]>) -> Selection<'a> { - let mut range = &line[self.get_range(&line, tokens)]; - let num_cache = if self.settings.mode == SortMode::Numeric - || self.settings.mode == SortMode::HumanNumeric - { + /// If needs_fields returned false, tokens may be empty. + fn get_selection<'a>(&self, line: &'a str, tokens: &[Field]) -> Selection<'a> { + // `get_range` expects `None` when we don't need tokens and would get confused by an empty vector. + let tokens = if self.needs_tokens { + Some(tokens) + } else { + None + }; + let mut range = &line[self.get_range(line, tokens)]; + if self.settings.mode == SortMode::Numeric || self.settings.mode == SortMode::HumanNumeric { // Parse NumInfo for this number. let (info, num_range) = NumInfo::parse( range, @@ -764,24 +813,18 @@ impl FieldSelector { ); // Shorten the range to what we need to pass to numeric_str_cmp later. range = &range[num_range]; - Some(Box::new(NumCache::WithInfo(info))) + Selection::WithNumInfo(range, info) } else if self.settings.mode == SortMode::GeneralNumeric { // Parse this number as f64, as this is the requirement for general numeric sorting. - Some(Box::new(NumCache::AsF64(general_f64_parse( - &range[get_leading_gen(range)], - )))) + Selection::AsF64(general_f64_parse(&range[get_leading_gen(range)])) } else { // This is not a numeric sort, so we don't need a NumCache. - None - }; - Selection { - slice: range, - num_cache, + Selection::Str(range) } } /// Look up the range in the line that corresponds to this selector. - /// If needs_fields returned false, tokens may be None. + /// If needs_fields returned false, tokens must be None. fn get_range<'a>(&self, line: &'a str, tokens: Option<&[Field]>) -> Range { enum Resolution { // The start index of the resolved character, inclusive @@ -840,7 +883,7 @@ impl FieldSelector { match resolve_index(line, tokens, &self.from) { Resolution::StartOfChar(from) => { - let to = self.to.as_ref().map(|to| resolve_index(line, tokens, &to)); + let to = self.to.as_ref().map(|to| resolve_index(line, tokens, to)); let mut range = match to { Some(Resolution::StartOfChar(mut to)) => { @@ -883,9 +926,10 @@ With no FILE, or when FILE is -, read standard input.", ) } +/// Creates an `Arg` that conflicts with all other sort modes. fn make_sort_mode_arg<'a, 'b>(mode: &'a str, short: &'b str, help: &'b str) -> Arg<'a, 'b> { let mut arg = Arg::with_name(mode).short(short).long(mode).help(help); - for possible_mode in ALL_SORT_MODES { + for possible_mode in &options::modes::ALL_SORT_MODES { if *possible_mode != mode { arg = arg.conflicts_with(possible_mode); } @@ -900,204 +944,14 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); let mut settings: GlobalSettings = Default::default(); - let matches = App::new(executable!()) - .version(crate_version!()) - .about(ABOUT) - .usage(&usage[..]) - .arg( - Arg::with_name(OPT_SORT) - .long(OPT_SORT) - .takes_value(true) - .possible_values( - &[ - "general-numeric", - "human-numeric", - "month", - "numeric", - "version", - "random", - ] - ) - .conflicts_with_all(ALL_SORT_MODES) - ) - .arg( - make_sort_mode_arg( - OPT_HUMAN_NUMERIC_SORT, - "h", - "compare according to human readable sizes, eg 1M > 100k" - ), - ) - .arg( - make_sort_mode_arg( - OPT_MONTH_SORT, - "M", - "compare according to month name abbreviation" - ), - ) - .arg( - make_sort_mode_arg( - OPT_NUMERIC_SORT, - "n", - "compare according to string numerical value" - ), - ) - .arg( - make_sort_mode_arg( - OPT_GENERAL_NUMERIC_SORT, - "g", - "compare according to string general numerical value" - ), - ) - .arg( - make_sort_mode_arg( - OPT_VERSION_SORT, - "V", - "Sort by SemVer version number, eg 1.12.2 > 1.1.2", - ), - ) - .arg( - make_sort_mode_arg( - OPT_RANDOM, - "R", - "shuffle in random order", - ), - ) - .arg( - Arg::with_name(OPT_DICTIONARY_ORDER) - .short("d") - .long(OPT_DICTIONARY_ORDER) - .help("consider only blanks and alphanumeric characters") - .conflicts_with_all(&[OPT_NUMERIC_SORT, OPT_GENERAL_NUMERIC_SORT, OPT_HUMAN_NUMERIC_SORT, OPT_MONTH_SORT]), - ) - .arg( - Arg::with_name(OPT_MERGE) - .short("m") - .long(OPT_MERGE) - .help("merge already sorted files; do not sort"), - ) - .arg( - Arg::with_name(OPT_CHECK) - .short("c") - .long(OPT_CHECK) - .help("check for sorted input; do not sort"), - ) - .arg( - Arg::with_name(OPT_CHECK_SILENT) - .short("C") - .long(OPT_CHECK_SILENT) - .help("exit successfully if the given file is already sorted, and exit with status 1 otherwise."), - ) - .arg( - Arg::with_name(OPT_IGNORE_CASE) - .short("f") - .long(OPT_IGNORE_CASE) - .help("fold lower case to upper case characters"), - ) - .arg( - Arg::with_name(OPT_IGNORE_NONPRINTING) - .short("i") - .long(OPT_IGNORE_NONPRINTING) - .help("ignore nonprinting characters") - .conflicts_with_all(&[OPT_NUMERIC_SORT, OPT_GENERAL_NUMERIC_SORT, OPT_HUMAN_NUMERIC_SORT, OPT_MONTH_SORT]), - ) - .arg( - Arg::with_name(OPT_IGNORE_BLANKS) - .short("b") - .long(OPT_IGNORE_BLANKS) - .help("ignore leading blanks when finding sort keys in each line"), - ) - .arg( - Arg::with_name(OPT_OUTPUT) - .short("o") - .long(OPT_OUTPUT) - .help("write output to FILENAME instead of stdout") - .takes_value(true) - .value_name("FILENAME"), - ) - .arg( - Arg::with_name(OPT_REVERSE) - .short("r") - .long(OPT_REVERSE) - .help("reverse the output"), - ) - .arg( - Arg::with_name(OPT_STABLE) - .short("s") - .long(OPT_STABLE) - .help("stabilize sort by disabling last-resort comparison"), - ) - .arg( - Arg::with_name(OPT_UNIQUE) - .short("u") - .long(OPT_UNIQUE) - .help("output only the first of an equal run"), - ) - .arg( - Arg::with_name(OPT_KEY) - .short("k") - .long(OPT_KEY) - .help("sort by a key") - .long_help(LONG_HELP_KEYS) - .multiple(true) - .takes_value(true), - ) - .arg( - Arg::with_name(OPT_SEPARATOR) - .short("t") - .long(OPT_SEPARATOR) - .help("custom separator for -k") - .takes_value(true)) - .arg( - Arg::with_name(OPT_ZERO_TERMINATED) - .short("z") - .long(OPT_ZERO_TERMINATED) - .help("line delimiter is NUL, not newline"), - ) - .arg( - Arg::with_name(OPT_PARALLEL) - .long(OPT_PARALLEL) - .help("change the number of threads running concurrently to NUM_THREADS") - .takes_value(true) - .value_name("NUM_THREADS"), - ) - .arg( - Arg::with_name(OPT_BUF_SIZE) - .short("S") - .long(OPT_BUF_SIZE) - .help("sets the maximum SIZE of each segment in number of sorted items") - .takes_value(true) - .value_name("SIZE"), - ) - .arg( - Arg::with_name(OPT_TMP_DIR) - .short("T") - .long(OPT_TMP_DIR) - .help("use DIR for temporaries, not $TMPDIR or /tmp") - .takes_value(true) - .value_name("DIR"), - ) - .arg( - Arg::with_name(OPT_FILES0_FROM) - .long(OPT_FILES0_FROM) - .help("read input from the files specified by NUL-terminated NUL_FILES") - .takes_value(true) - .value_name("NUL_FILES") - .multiple(true), - ) - .arg( - Arg::with_name(OPT_DEBUG) - .long(OPT_DEBUG) - .help("underline the parts of the line that are actually used for sorting"), - ) - .arg(Arg::with_name(ARG_FILES).multiple(true).takes_value(true)) - .get_matches_from(args); + let matches = uu_app().usage(&usage[..]).get_matches_from(args); - settings.debug = matches.is_present(OPT_DEBUG); + settings.debug = matches.is_present(options::DEBUG); // check whether user specified a zero terminated list of files for input, otherwise read files from args - let mut files: Vec = if matches.is_present(OPT_FILES0_FROM) { + let mut files: Vec = if matches.is_present(options::FILES0_FROM) { let files0_from: Vec = matches - .values_of(OPT_FILES0_FROM) + .values_of(options::FILES0_FROM) .map(|v| v.map(ToString::to_string).collect()) .unwrap_or_default(); @@ -1116,72 +970,93 @@ pub fn uumain(args: impl uucore::Args) -> i32 { files } else { matches - .values_of(ARG_FILES) + .values_of(options::FILES) .map(|v| v.map(ToString::to_string).collect()) .unwrap_or_default() }; - settings.mode = if matches.is_present(OPT_HUMAN_NUMERIC_SORT) - || matches.value_of(OPT_SORT) == Some("human-numeric") + settings.mode = if matches.is_present(options::modes::HUMAN_NUMERIC) + || matches.value_of(options::modes::SORT) == Some("human-numeric") { SortMode::HumanNumeric - } else if matches.is_present(OPT_MONTH_SORT) || matches.value_of(OPT_SORT) == Some("month") { + } else if matches.is_present(options::modes::MONTH) + || matches.value_of(options::modes::SORT) == Some("month") + { SortMode::Month - } else if matches.is_present(OPT_GENERAL_NUMERIC_SORT) - || matches.value_of(OPT_SORT) == Some("general-numeric") + } else if matches.is_present(options::modes::GENERAL_NUMERIC) + || matches.value_of(options::modes::SORT) == Some("general-numeric") { SortMode::GeneralNumeric - } else if matches.is_present(OPT_NUMERIC_SORT) || matches.value_of(OPT_SORT) == Some("numeric") + } else if matches.is_present(options::modes::NUMERIC) + || matches.value_of(options::modes::SORT) == Some("numeric") { SortMode::Numeric - } else if matches.is_present(OPT_VERSION_SORT) || matches.value_of(OPT_SORT) == Some("version") + } else if matches.is_present(options::modes::VERSION) + || matches.value_of(options::modes::SORT) == Some("version") { SortMode::Version - } else if matches.is_present(OPT_RANDOM) || matches.value_of(OPT_SORT) == Some("random") { + } else if matches.is_present(options::modes::RANDOM) + || matches.value_of(options::modes::SORT) == Some("random") + { settings.salt = get_rand_string(); SortMode::Random } else { SortMode::Default }; - settings.dictionary_order = matches.is_present(OPT_DICTIONARY_ORDER); - settings.ignore_non_printing = matches.is_present(OPT_IGNORE_NONPRINTING); - if matches.is_present(OPT_PARALLEL) { + settings.dictionary_order = matches.is_present(options::DICTIONARY_ORDER); + settings.ignore_non_printing = matches.is_present(options::IGNORE_NONPRINTING); + if matches.is_present(options::PARALLEL) { // "0" is default - threads = num of cores settings.threads = matches - .value_of(OPT_PARALLEL) + .value_of(options::PARALLEL) .map(String::from) .unwrap_or_else(|| "0".to_string()); env::set_var("RAYON_NUM_THREADS", &settings.threads); } settings.buffer_size = matches - .value_of(OPT_BUF_SIZE) - .map(GlobalSettings::parse_byte_count) - .unwrap_or(DEFAULT_BUF_SIZE); + .value_of(options::BUF_SIZE) + .map_or(DEFAULT_BUF_SIZE, |s| { + GlobalSettings::parse_byte_count(s) + .unwrap_or_else(|e| crash!(2, "{}", format_error_message(e, s, options::BUF_SIZE))) + }); settings.tmp_dir = matches - .value_of(OPT_TMP_DIR) + .value_of(options::TMP_DIR) .map(PathBuf::from) .unwrap_or_else(env::temp_dir); - settings.zero_terminated = matches.is_present(OPT_ZERO_TERMINATED); - settings.merge = matches.is_present(OPT_MERGE); + settings.compress_prog = matches.value_of(options::COMPRESS_PROG).map(String::from); - settings.check = matches.is_present(OPT_CHECK); - if matches.is_present(OPT_CHECK_SILENT) { - settings.check_silent = matches.is_present(OPT_CHECK_SILENT); + if let Some(n_merge) = matches.value_of(options::BATCH_SIZE) { + settings.merge_batch_size = n_merge + .parse() + .unwrap_or_else(|_| crash!(2, "invalid --batch-size argument '{}'", n_merge)); + } + + settings.zero_terminated = matches.is_present(options::ZERO_TERMINATED); + settings.merge = matches.is_present(options::MERGE); + + settings.check = matches.is_present(options::check::CHECK); + if matches.is_present(options::check::CHECK_SILENT) + || matches!( + matches.value_of(options::check::CHECK), + Some(options::check::SILENT) | Some(options::check::QUIET) + ) + { + settings.check_silent = true; settings.check = true; }; - settings.ignore_case = matches.is_present(OPT_IGNORE_CASE); + settings.ignore_case = matches.is_present(options::IGNORE_CASE); - settings.ignore_blanks = matches.is_present(OPT_IGNORE_BLANKS); + settings.ignore_leading_blanks = matches.is_present(options::IGNORE_LEADING_BLANKS); - settings.output_file = matches.value_of(OPT_OUTPUT).map(String::from); - settings.reverse = matches.is_present(OPT_REVERSE); - settings.stable = matches.is_present(OPT_STABLE); - settings.unique = matches.is_present(OPT_UNIQUE); + settings.output_file = matches.value_of(options::OUTPUT).map(String::from); + settings.reverse = matches.is_present(options::REVERSE); + settings.stable = matches.is_present(options::STABLE); + settings.unique = matches.is_present(options::UNIQUE); if files.is_empty() { /* if no file, default to stdin */ @@ -1190,7 +1065,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { crash!(1, "extra operand `{}' not allowed with -c", files[1]) } - if let Some(arg) = matches.args.get(OPT_SEPARATOR) { + if let Some(arg) = matches.args.get(options::SEPARATOR) { let separator = arg.vals[0].to_string_lossy(); let separator = separator; if separator.len() != 1 { @@ -1199,15 +1074,15 @@ pub fn uumain(args: impl uucore::Args) -> i32 { settings.separator = Some(separator.chars().next().unwrap()) } - if matches.is_present(OPT_KEY) { - for key in &matches.args[OPT_KEY].vals { + if let Some(values) = matches.values_of(options::KEY) { + for value in values { settings .selectors - .push(FieldSelector::parse(&key.to_string_lossy(), &settings)); + .push(FieldSelector::parse(value, &settings)); } } - if !matches.is_present(OPT_KEY) { + if !matches.is_present(options::KEY) { // add a default selector matching the whole line let key_settings = KeySettings::from(&settings); settings.selectors.push( @@ -1224,23 +1099,243 @@ pub fn uumain(args: impl uucore::Args) -> i32 { ); } + settings.init_precomputed(); + exec(&files, &settings) } -fn output_sorted_lines<'a>(iter: impl Iterator>, settings: &GlobalSettings) { - if settings.unique { - print_sorted( - iter.dedup_by(|a, b| compare_by(a, b, &settings) == Ordering::Equal), - &settings, - ); - } else { - print_sorted(iter, &settings); - } +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about(ABOUT) + .arg( + Arg::with_name(options::modes::SORT) + .long(options::modes::SORT) + .takes_value(true) + .possible_values( + &[ + "general-numeric", + "human-numeric", + "month", + "numeric", + "version", + "random", + ] + ) + .conflicts_with_all(&options::modes::ALL_SORT_MODES) + ) + .arg( + make_sort_mode_arg( + options::modes::HUMAN_NUMERIC, + "h", + "compare according to human readable sizes, eg 1M > 100k" + ), + ) + .arg( + make_sort_mode_arg( + options::modes::MONTH, + "M", + "compare according to month name abbreviation" + ), + ) + .arg( + make_sort_mode_arg( + options::modes::NUMERIC, + "n", + "compare according to string numerical value" + ), + ) + .arg( + make_sort_mode_arg( + options::modes::GENERAL_NUMERIC, + "g", + "compare according to string general numerical value" + ), + ) + .arg( + make_sort_mode_arg( + options::modes::VERSION, + "V", + "Sort by SemVer version number, eg 1.12.2 > 1.1.2", + ), + ) + .arg( + make_sort_mode_arg( + options::modes::RANDOM, + "R", + "shuffle in random order", + ), + ) + .arg( + Arg::with_name(options::DICTIONARY_ORDER) + .short("d") + .long(options::DICTIONARY_ORDER) + .help("consider only blanks and alphanumeric characters") + .conflicts_with_all( + &[ + options::modes::NUMERIC, + options::modes::GENERAL_NUMERIC, + options::modes::HUMAN_NUMERIC, + options::modes::MONTH, + ] + ), + ) + .arg( + Arg::with_name(options::MERGE) + .short("m") + .long(options::MERGE) + .help("merge already sorted files; do not sort"), + ) + .arg( + Arg::with_name(options::check::CHECK) + .short("c") + .long(options::check::CHECK) + .takes_value(true) + .require_equals(true) + .min_values(0) + .possible_values(&[ + options::check::SILENT, + options::check::QUIET, + options::check::DIAGNOSE_FIRST, + ]) + .conflicts_with(options::OUTPUT) + .help("check for sorted input; do not sort"), + ) + .arg( + Arg::with_name(options::check::CHECK_SILENT) + .short("C") + .long(options::check::CHECK_SILENT) + .conflicts_with(options::OUTPUT) + .help("exit successfully if the given file is already sorted, and exit with status 1 otherwise."), + ) + .arg( + Arg::with_name(options::IGNORE_CASE) + .short("f") + .long(options::IGNORE_CASE) + .help("fold lower case to upper case characters"), + ) + .arg( + Arg::with_name(options::IGNORE_NONPRINTING) + .short("i") + .long(options::IGNORE_NONPRINTING) + .help("ignore nonprinting characters") + .conflicts_with_all( + &[ + options::modes::NUMERIC, + options::modes::GENERAL_NUMERIC, + options::modes::HUMAN_NUMERIC, + options::modes::MONTH + ] + ), + ) + .arg( + Arg::with_name(options::IGNORE_LEADING_BLANKS) + .short("b") + .long(options::IGNORE_LEADING_BLANKS) + .help("ignore leading blanks when finding sort keys in each line"), + ) + .arg( + Arg::with_name(options::OUTPUT) + .short("o") + .long(options::OUTPUT) + .help("write output to FILENAME instead of stdout") + .takes_value(true) + .value_name("FILENAME"), + ) + .arg( + Arg::with_name(options::REVERSE) + .short("r") + .long(options::REVERSE) + .help("reverse the output"), + ) + .arg( + Arg::with_name(options::STABLE) + .short("s") + .long(options::STABLE) + .help("stabilize sort by disabling last-resort comparison"), + ) + .arg( + Arg::with_name(options::UNIQUE) + .short("u") + .long(options::UNIQUE) + .help("output only the first of an equal run"), + ) + .arg( + Arg::with_name(options::KEY) + .short("k") + .long(options::KEY) + .help("sort by a key") + .long_help(LONG_HELP_KEYS) + .multiple(true) + .takes_value(true), + ) + .arg( + Arg::with_name(options::SEPARATOR) + .short("t") + .long(options::SEPARATOR) + .help("custom separator for -k") + .takes_value(true)) + .arg( + Arg::with_name(options::ZERO_TERMINATED) + .short("z") + .long(options::ZERO_TERMINATED) + .help("line delimiter is NUL, not newline"), + ) + .arg( + Arg::with_name(options::PARALLEL) + .long(options::PARALLEL) + .help("change the number of threads running concurrently to NUM_THREADS") + .takes_value(true) + .value_name("NUM_THREADS"), + ) + .arg( + Arg::with_name(options::BUF_SIZE) + .short("S") + .long(options::BUF_SIZE) + .help("sets the maximum SIZE of each segment in number of sorted items") + .takes_value(true) + .value_name("SIZE"), + ) + .arg( + Arg::with_name(options::TMP_DIR) + .short("T") + .long(options::TMP_DIR) + .help("use DIR for temporaries, not $TMPDIR or /tmp") + .takes_value(true) + .value_name("DIR"), + ) + .arg( + Arg::with_name(options::COMPRESS_PROG) + .long(options::COMPRESS_PROG) + .help("compress temporary files with PROG, decompress with PROG -d") + .long_help("PROG has to take input from stdin and output to stdout") + .value_name("PROG") + ) + .arg( + Arg::with_name(options::BATCH_SIZE) + .long(options::BATCH_SIZE) + .help("Merge at most N_MERGE inputs at once.") + .value_name("N_MERGE") + ) + .arg( + Arg::with_name(options::FILES0_FROM) + .long(options::FILES0_FROM) + .help("read input from the files specified by NUL-terminated NUL_FILES") + .takes_value(true) + .value_name("NUL_FILES") + .multiple(true), + ) + .arg( + Arg::with_name(options::DEBUG) + .long(options::DEBUG) + .help("underline the parts of the line that are actually used for sorting"), + ) + .arg(Arg::with_name(options::FILES).multiple(true).takes_value(true)) } fn exec(files: &[String], settings: &GlobalSettings) -> i32 { if settings.merge { - let mut file_merger = merge::merge(files, settings); + let mut file_merger = merge::merge(files.iter().map(open), settings); file_merger.write_all(settings); } else if settings.check { if files.len() > 1 { @@ -1250,62 +1345,74 @@ fn exec(files: &[String], settings: &GlobalSettings) -> i32 { } else { let mut lines = files.iter().map(open); - ext_sort(&mut lines, &settings); + ext_sort(&mut lines, settings); } 0 } -fn sort_by<'a>(unsorted: &mut Vec>, settings: &GlobalSettings) { +fn sort_by<'a>(unsorted: &mut Vec>, settings: &GlobalSettings, line_data: &LineData<'a>) { if settings.stable || settings.unique { - unsorted.par_sort_by(|a, b| compare_by(a, b, &settings)) + unsorted.par_sort_by(|a, b| compare_by(a, b, settings, line_data, line_data)) } else { - unsorted.par_sort_unstable_by(|a, b| compare_by(a, b, &settings)) + unsorted.par_sort_unstable_by(|a, b| compare_by(a, b, settings, line_data, line_data)) } } -fn compare_by<'a>(a: &Line<'a>, b: &Line<'a>, global_settings: &GlobalSettings) -> Ordering { - let mut idx = 0; +fn compare_by<'a>( + a: &Line<'a>, + b: &Line<'a>, + global_settings: &GlobalSettings, + a_line_data: &LineData<'a>, + b_line_data: &LineData<'a>, +) -> Ordering { + let mut selection_index = 0; + let mut num_info_index = 0; + let mut parsed_float_index = 0; for selector in &global_settings.selectors { - let mut _selections = None; - let (a_selection, b_selection) = if selector.is_default_selection { + let (a_str, b_str) = if !selector.needs_selection { // We can select the whole line. - // We have to store the selections outside of the if-block so that they live long enough. - _selections = Some(( - Selection { - slice: a.line, - num_cache: None, - }, - Selection { - slice: b.line, - num_cache: None, - }, - )); - // Unwrap the selections again, and return references to them. - ( - &_selections.as_ref().unwrap().0, - &_selections.as_ref().unwrap().1, - ) + (a.line, b.line) } else { - let selections = (&a.selections[idx], &b.selections[idx]); - idx += 1; + let selections = ( + a_line_data.selections + [a.index * global_settings.precomputed.selections_per_line + selection_index], + b_line_data.selections + [b.index * global_settings.precomputed.selections_per_line + selection_index], + ); + selection_index += 1; selections }; - let a_str = a_selection.slice; - let b_str = b_selection.slice; + let settings = &selector.settings; let cmp: Ordering = match settings.mode { SortMode::Random => random_shuffle(a_str, b_str, &global_settings.salt), - SortMode::Numeric | SortMode::HumanNumeric => numeric_str_cmp( - (a_str, a_selection.num_cache.as_ref().unwrap().as_num_info()), - (b_str, b_selection.num_cache.as_ref().unwrap().as_num_info()), - ), - SortMode::GeneralNumeric => general_numeric_compare( - a_selection.num_cache.as_ref().unwrap().as_f64(), - b_selection.num_cache.as_ref().unwrap().as_f64(), - ), + SortMode::Numeric => { + let a_num_info = &a_line_data.num_infos + [a.index * global_settings.precomputed.num_infos_per_line + num_info_index]; + let b_num_info = &b_line_data.num_infos + [b.index * global_settings.precomputed.num_infos_per_line + num_info_index]; + num_info_index += 1; + numeric_str_cmp((a_str, a_num_info), (b_str, b_num_info)) + } + SortMode::HumanNumeric => { + let a_num_info = &a_line_data.num_infos + [a.index * global_settings.precomputed.num_infos_per_line + num_info_index]; + let b_num_info = &b_line_data.num_infos + [b.index * global_settings.precomputed.num_infos_per_line + num_info_index]; + num_info_index += 1; + human_numeric_str_cmp((a_str, a_num_info), (b_str, b_num_info)) + } + SortMode::GeneralNumeric => { + let a_float = &a_line_data.parsed_floats + [a.index * global_settings.precomputed.floats_per_line + parsed_float_index]; + let b_float = &b_line_data.parsed_floats + [b.index * global_settings.precomputed.floats_per_line + parsed_float_index]; + parsed_float_index += 1; + general_numeric_compare(a_float, b_float) + } SortMode::Month => month_compare(a_str, b_str), - SortMode::Version => version_compare(a_str, b_str), + SortMode::Version => version_cmp(a_str, b_str), SortMode::Default => custom_str_cmp( a_str, b_str, @@ -1397,7 +1504,7 @@ fn get_leading_gen(input: &str) -> Range { } #[derive(Copy, Clone, PartialEq, PartialOrd, Debug)] -enum GeneralF64ParseResult { +pub enum GeneralF64ParseResult { Invalid, NaN, NegInfinity, @@ -1424,8 +1531,8 @@ fn general_f64_parse(a: &str) -> GeneralF64ParseResult { /// Compares two floats, with errors and non-numerics assumed to be -inf. /// Stops coercing at the first non-numeric char. /// We explicitly need to convert to f64 in this case. -fn general_numeric_compare(a: GeneralF64ParseResult, b: GeneralF64ParseResult) -> Ordering { - a.partial_cmp(&b).unwrap() +fn general_numeric_compare(a: &GeneralF64ParseResult, b: &GeneralF64ParseResult) -> Ordering { + a.partial_cmp(b).unwrap() } fn get_rand_string() -> String { @@ -1443,8 +1550,6 @@ fn get_hash(t: &T) -> u64 { } fn random_shuffle(a: &str, b: &str, salt: &str) -> Ordering { - #![allow(clippy::comparison_chain)] - let da = get_hash(&[a, salt].concat()); let db = get_hash(&[b, salt].concat()); @@ -1512,31 +1617,6 @@ fn month_compare(a: &str, b: &str) -> Ordering { } } -fn version_parse(a: &str) -> Version { - let result = Version::parse(a); - - match result { - Ok(vers_a) => vers_a, - // Non-version lines parse to 0.0.0 - Err(_e) => Version::parse("0.0.0").unwrap(), - } -} - -fn version_compare(a: &str, b: &str) -> Ordering { - #![allow(clippy::comparison_chain)] - let ver_a = version_parse(a); - let ver_b = version_parse(b); - - // Version::cmp is not implemented; implement comparison directly - if ver_a > ver_b { - Ordering::Greater - } else if ver_a < ver_b { - Ordering::Less - } else { - Ordering::Equal - } -} - fn print_sorted<'a, T: Iterator>>(iter: T, settings: &GlobalSettings) { let mut writer = settings.out_writer(); for line in iter { @@ -1560,11 +1640,27 @@ fn open(path: impl AsRef) -> Box { } } +fn format_error_message(error: ParseSizeError, s: &str, option: &str) -> String { + // NOTE: + // GNU's sort echos affected flag, -S or --buffer-size, depending user's selection + // GNU's sort does distinguish between "invalid (suffix in) argument" + match error { + ParseSizeError::ParseFailure(_) => format!("invalid --{} argument '{}'", option, s), + ParseSizeError::SizeTooBig(_) => format!("--{} argument '{}' too large", option, s), + } +} + #[cfg(test)] mod tests { use super::*; + fn tokenize_helper(line: &str, separator: Option) -> Vec { + let mut buffer = vec![]; + tokenize(line, separator, &mut buffer); + buffer + } + #[test] fn test_get_hash() { let a = "Ted".to_string(); @@ -1593,7 +1689,7 @@ mod tests { let a = "1.2.3-alpha2"; let b = "1.4.0"; - assert_eq!(Ordering::Less, version_compare(a, b)); + assert_eq!(Ordering::Less, version_cmp(a, b)); } #[test] @@ -1608,20 +1704,23 @@ mod tests { #[test] fn test_tokenize_fields() { let line = "foo bar b x"; - assert_eq!(tokenize(line, None), vec![0..3, 3..7, 7..9, 9..14,],); + assert_eq!(tokenize_helper(line, None), vec![0..3, 3..7, 7..9, 9..14,],); } #[test] fn test_tokenize_fields_leading_whitespace() { let line = " foo bar b x"; - assert_eq!(tokenize(line, None), vec![0..7, 7..11, 11..13, 13..18,]); + assert_eq!( + tokenize_helper(line, None), + vec![0..7, 7..11, 11..13, 13..18,] + ); } #[test] fn test_tokenize_fields_custom_separator() { let line = "aaa foo bar b x"; assert_eq!( - tokenize(line, Some('a')), + tokenize_helper(line, Some('a')), vec![0..0, 1..1, 2..2, 3..9, 10..18,] ); } @@ -1629,11 +1728,11 @@ mod tests { #[test] fn test_tokenize_fields_trailing_custom_separator() { let line = "a"; - assert_eq!(tokenize(line, Some('a')), vec![0..0]); + assert_eq!(tokenize_helper(line, Some('a')), vec![0..0]); let line = "aa"; - assert_eq!(tokenize(line, Some('a')), vec![0..0, 1..1]); + assert_eq!(tokenize_helper(line, Some('a')), vec![0..0, 1..1]); let line = "..a..a"; - assert_eq!(tokenize(line, Some('a')), vec![0..2, 3..5]); + assert_eq!(tokenize_helper(line, Some('a')), vec![0..2, 3..5]); } #[test] @@ -1641,12 +1740,50 @@ mod tests { fn test_line_size() { // We should make sure to not regress the size of the Line struct because // it is unconditional overhead for every line we sort. - assert_eq!(std::mem::size_of::(), 32); - // These are the fields of Line: - assert_eq!(std::mem::size_of::<&str>(), 16); - assert_eq!(std::mem::size_of::>(), 16); + assert_eq!(std::mem::size_of::(), 24); + } - // How big is a selection? Constant cost all lines pay when we need selections. - assert_eq!(std::mem::size_of::(), 24); + #[test] + fn test_parse_byte_count() { + let valid_input = [ + ("0", 0), + ("50K", 50 * 1024), + ("50k", 50 * 1024), + ("1M", 1024 * 1024), + ("100M", 100 * 1024 * 1024), + #[cfg(not(target_pointer_width = "32"))] + ("1000G", 1000 * 1024 * 1024 * 1024), + #[cfg(not(target_pointer_width = "32"))] + ("10T", 10 * 1024 * 1024 * 1024 * 1024), + ("1b", 1), + ("1024b", 1024), + ("1024Mb", 1024 * 1024 * 1024), // NOTE: This might not be how GNU `sort` behaves for 'Mb' + ("1", 1024), // K is default + ("50", 50 * 1024), + ("K", 1024), + ("k", 1024), + ("m", 1024 * 1024), + #[cfg(not(target_pointer_width = "32"))] + ("E", 1024 * 1024 * 1024 * 1024 * 1024 * 1024), + ]; + for (input, expected_output) in &valid_input { + assert_eq!( + GlobalSettings::parse_byte_count(input), + Ok(*expected_output) + ); + } + + // SizeTooBig + let invalid_input = ["500E", "1Y"]; + for input in &invalid_input { + #[cfg(not(target_pointer_width = "128"))] + assert!(GlobalSettings::parse_byte_count(input).is_err()); + } + + // ParseFailure + let invalid_input = ["nonsense", "1B", "B", "b", "p", "e", "z", "y"]; + for input in &invalid_input { + assert!(GlobalSettings::parse_byte_count(input).is_err()); + } } } diff --git a/src/uu/split/Cargo.toml b/src/uu/split/Cargo.toml index 056fbe034..e19695a39 100644 --- a/src/uu/split/Cargo.toml +++ b/src/uu/split/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/split.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/split/src/platform/unix.rs b/src/uu/split/src/platform/unix.rs index 20d9d637b..a115d1959 100644 --- a/src/uu/split/src/platform/unix.rs +++ b/src/uu/split/src/platform/unix.rs @@ -66,7 +66,7 @@ impl FilterWriter { /// * `filepath` - Path of the output file (forwarded to command as $FILE) fn new(command: &str, filepath: &str) -> FilterWriter { // set $FILE, save previous value (if there was one) - let _with_env_var_set = WithEnvVarSet::new("FILE", &filepath); + let _with_env_var_set = WithEnvVarSet::new("FILE", filepath); let shell_process = Command::new(env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_owned())) @@ -117,7 +117,7 @@ pub fn instantiate_current_writer( ) as Box), Some(ref filter_command) => BufWriter::new(Box::new( // spawn a shell command and write to it - FilterWriter::new(&filter_command, &filename), + FilterWriter::new(filter_command, filename), ) as Box), } } diff --git a/src/uu/split/src/split.rs b/src/uu/split/src/split.rs index 6550c35ac..ccc98ee5e 100644 --- a/src/uu/split/src/split.rs +++ b/src/uu/split/src/split.rs @@ -13,11 +13,13 @@ extern crate uucore; mod platform; use clap::{crate_version, App, Arg}; +use std::convert::TryFrom; use std::env; use std::fs::File; use std::io::{stdin, BufRead, BufReader, BufWriter, Read, Write}; use std::path::Path; use std::{char, fs::remove_file}; +use uucore::parse_size::parse_size; static NAME: &str = "split"; @@ -28,7 +30,7 @@ static OPT_ADDITIONAL_SUFFIX: &str = "additional-suffix"; static OPT_FILTER: &str = "filter"; static OPT_NUMERIC_SUFFIXES: &str = "numeric-suffixes"; static OPT_SUFFIX_LENGTH: &str = "suffix-length"; -static OPT_DEFAULT_SUFFIX_LENGTH: usize = 2; +static OPT_DEFAULT_SUFFIX_LENGTH: &str = "2"; static OPT_VERBOSE: &str = "verbose"; static ARG_INPUT: &str = "input"; @@ -52,85 +54,10 @@ size is 1000, and default PREFIX is 'x'. With no INPUT, or when INPUT is pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); let long_usage = get_long_usage(); - let default_suffix_length_str = OPT_DEFAULT_SUFFIX_LENGTH.to_string(); - let matches = App::new(executable!()) - .version(crate_version!()) - .about("Create output files containing consecutive or interleaved sections of input") + let matches = uu_app() .usage(&usage[..]) .after_help(&long_usage[..]) - // strategy (mutually exclusive) - .arg( - Arg::with_name(OPT_BYTES) - .short("b") - .long(OPT_BYTES) - .takes_value(true) - .default_value("2") - .help("use suffixes of length N (default 2)"), - ) - .arg( - Arg::with_name(OPT_LINE_BYTES) - .short("C") - .long(OPT_LINE_BYTES) - .takes_value(true) - .default_value("2") - .help("put at most SIZE bytes of lines per output file"), - ) - .arg( - Arg::with_name(OPT_LINES) - .short("l") - .long(OPT_LINES) - .takes_value(true) - .default_value("1000") - .help("write to shell COMMAND file name is $FILE (Currently not implemented for Windows)"), - ) - // rest of the arguments - .arg( - Arg::with_name(OPT_ADDITIONAL_SUFFIX) - .long(OPT_ADDITIONAL_SUFFIX) - .takes_value(true) - .default_value("") - .help("additional suffix to append to output file names"), - ) - .arg( - Arg::with_name(OPT_FILTER) - .long(OPT_FILTER) - .takes_value(true) - .help("write to shell COMMAND file name is $FILE (Currently not implemented for Windows)"), - ) - .arg( - Arg::with_name(OPT_NUMERIC_SUFFIXES) - .short("d") - .long(OPT_NUMERIC_SUFFIXES) - .takes_value(true) - .default_value("0") - .help("use numeric suffixes instead of alphabetic"), - ) - .arg( - Arg::with_name(OPT_SUFFIX_LENGTH) - .short("a") - .long(OPT_SUFFIX_LENGTH) - .takes_value(true) - .default_value(default_suffix_length_str.as_str()) - .help("use suffixes of length N (default 2)"), - ) - .arg( - Arg::with_name(OPT_VERBOSE) - .long(OPT_VERBOSE) - .help("print a diagnostic just before each output file is opened"), - ) - .arg( - Arg::with_name(ARG_INPUT) - .takes_value(true) - .default_value("-") - .index(1) - ) - .arg( - Arg::with_name(ARG_PREFIX) - .takes_value(true) - .default_value("x") - .index(2) - ) .get_matches_from(args); let mut settings = Settings { @@ -199,6 +126,84 @@ pub fn uumain(args: impl uucore::Args) -> i32 { split(&settings) } +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about("Create output files containing consecutive or interleaved sections of input") + // strategy (mutually exclusive) + .arg( + Arg::with_name(OPT_BYTES) + .short("b") + .long(OPT_BYTES) + .takes_value(true) + .default_value("2") + .help("use suffixes of length N (default 2)"), + ) + .arg( + Arg::with_name(OPT_LINE_BYTES) + .short("C") + .long(OPT_LINE_BYTES) + .takes_value(true) + .default_value("2") + .help("put at most SIZE bytes of lines per output file"), + ) + .arg( + Arg::with_name(OPT_LINES) + .short("l") + .long(OPT_LINES) + .takes_value(true) + .default_value("1000") + .help("write to shell COMMAND file name is $FILE (Currently not implemented for Windows)"), + ) + // rest of the arguments + .arg( + Arg::with_name(OPT_ADDITIONAL_SUFFIX) + .long(OPT_ADDITIONAL_SUFFIX) + .takes_value(true) + .default_value("") + .help("additional suffix to append to output file names"), + ) + .arg( + Arg::with_name(OPT_FILTER) + .long(OPT_FILTER) + .takes_value(true) + .help("write to shell COMMAND file name is $FILE (Currently not implemented for Windows)"), + ) + .arg( + Arg::with_name(OPT_NUMERIC_SUFFIXES) + .short("d") + .long(OPT_NUMERIC_SUFFIXES) + .takes_value(true) + .default_value("0") + .help("use numeric suffixes instead of alphabetic"), + ) + .arg( + Arg::with_name(OPT_SUFFIX_LENGTH) + .short("a") + .long(OPT_SUFFIX_LENGTH) + .takes_value(true) + .default_value(OPT_DEFAULT_SUFFIX_LENGTH) + .help("use suffixes of length N (default 2)"), + ) + .arg( + Arg::with_name(OPT_VERBOSE) + .long(OPT_VERBOSE) + .help("print a diagnostic just before each output file is opened"), + ) + .arg( + Arg::with_name(ARG_INPUT) + .takes_value(true) + .default_value("-") + .index(1) + ) + .arg( + Arg::with_name(ARG_PREFIX) + .takes_value(true) + .default_value("x") + .index(2) + ) +} + #[allow(dead_code)] struct Settings { prefix: String, @@ -231,10 +236,9 @@ struct LineSplitter { impl LineSplitter { fn new(settings: &Settings) -> LineSplitter { LineSplitter { - lines_per_split: settings - .strategy_param - .parse() - .unwrap_or_else(|e| crash!(1, "invalid number of lines: {}", e)), + lines_per_split: settings.strategy_param.parse().unwrap_or_else(|_| { + crash!(1, "invalid number of lines: '{}'", settings.strategy_param) + }), } } } @@ -276,40 +280,14 @@ struct ByteSplitter { impl ByteSplitter { fn new(settings: &Settings) -> ByteSplitter { - // These multipliers are the same as supported by GNU coreutils. - let modifiers: Vec<(&str, u128)> = vec![ - ("K", 1024u128), - ("M", 1024 * 1024), - ("G", 1024 * 1024 * 1024), - ("T", 1024 * 1024 * 1024 * 1024), - ("P", 1024 * 1024 * 1024 * 1024 * 1024), - ("E", 1024 * 1024 * 1024 * 1024 * 1024 * 1024), - ("Z", 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024), - ("Y", 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024), - ("KB", 1000), - ("MB", 1000 * 1000), - ("GB", 1000 * 1000 * 1000), - ("TB", 1000 * 1000 * 1000 * 1000), - ("PB", 1000 * 1000 * 1000 * 1000 * 1000), - ("EB", 1000 * 1000 * 1000 * 1000 * 1000 * 1000), - ("ZB", 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000), - ("YB", 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000), - ]; - - // This sequential find is acceptable since none of the modifiers are - // suffixes of any other modifiers, a la Huffman codes. - let (suffix, multiplier) = modifiers - .iter() - .find(|(suffix, _)| settings.strategy_param.ends_with(suffix)) - .unwrap_or(&("", 1)); - - // Try to parse the actual numeral. - let n = &settings.strategy_param[0..(settings.strategy_param.len() - suffix.len())] - .parse::() - .unwrap_or_else(|e| crash!(1, "invalid number of bytes: {}", e)); + let size_string = &settings.strategy_param; + let size_num = match parse_size(size_string) { + Ok(n) => n, + Err(e) => crash!(1, "invalid number of bytes: {}", e.to_string()), + }; ByteSplitter { - bytes_per_split: n * multiplier, + bytes_per_split: u128::try_from(size_num).unwrap(), } } } diff --git a/src/uu/stat/Cargo.toml b/src/uu/stat/Cargo.toml index 86b7da139..81af993a5 100644 --- a/src/uu/stat/Cargo.toml +++ b/src/uu/stat/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/stat.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["entries", "libc", "fs", "fsext"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/stat/src/stat.rs b/src/uu/stat/src/stat.rs index fa070d9b7..70c06bdf6 100644 --- a/src/uu/stat/src/stat.rs +++ b/src/uu/stat/src/stat.rs @@ -24,7 +24,7 @@ use std::{cmp, fs, iter}; macro_rules! check_bound { ($str: ident, $bound:expr, $beg: expr, $end: expr) => { if $end >= $bound { - return Err(format!("‘{}’: invalid directive", &$str[$beg..$end])); + return Err(format!("'{}': invalid directive", &$str[$beg..$end])); } }; } @@ -477,7 +477,7 @@ impl Stater { Stater::generate_tokens(&Stater::default_format(show_fs, terse, false), use_printf) .unwrap() } else { - Stater::generate_tokens(&format_str, use_printf)? + Stater::generate_tokens(format_str, use_printf)? }; let default_dev_tokens = Stater::generate_tokens(&Stater::default_format(show_fs, terse, true), use_printf) @@ -947,11 +947,24 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); let long_usage = get_long_usage(); - let matches = App::new(executable!()) - .version(crate_version!()) - .about(ABOUT) + let matches = uu_app() .usage(&usage[..]) .after_help(&long_usage[..]) + .get_matches_from(args); + + match Stater::new(matches) { + Ok(stater) => stater.exec(), + Err(e) => { + show_error!("{}", e); + 1 + } + } +} + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about(ABOUT) .arg( Arg::with_name(options::DEREFERENCE) .short("L") @@ -996,13 +1009,4 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .takes_value(true) .min_values(1), ) - .get_matches_from(args); - - match Stater::new(matches) { - Ok(stater) => stater.exec(), - Err(e) => { - show_error!("{}", e); - 1 - } - } } diff --git a/src/uu/stdbuf/Cargo.toml b/src/uu/stdbuf/Cargo.toml index 884a98785..a3eb059eb 100644 --- a/src/uu/stdbuf/Cargo.toml +++ b/src/uu/stdbuf/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/stdbuf.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } tempfile = "3.1" uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/stdbuf/src/stdbuf.rs b/src/uu/stdbuf/src/stdbuf.rs index 852fe3ef9..7460a2cb2 100644 --- a/src/uu/stdbuf/src/stdbuf.rs +++ b/src/uu/stdbuf/src/stdbuf.rs @@ -19,6 +19,7 @@ use std::path::PathBuf; use std::process::Command; use tempfile::tempdir; use tempfile::TempDir; +use uucore::parse_size::parse_size; use uucore::InvalidEncodingHandling; static ABOUT: &str = @@ -55,7 +56,7 @@ const STDBUF_INJECT: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/libstdbuf enum BufferType { Default, Line, - Size(u64), + Size(usize), } struct ProgramOptions { @@ -69,9 +70,9 @@ impl<'a> TryFrom<&ArgMatches<'a>> for ProgramOptions { fn try_from(matches: &ArgMatches) -> Result { Ok(ProgramOptions { - stdin: check_option(&matches, options::INPUT)?, - stdout: check_option(&matches, options::OUTPUT)?, - stderr: check_option(&matches, options::ERROR)?, + stdin: check_option(matches, options::INPUT)?, + stdout: check_option(matches, options::OUTPUT)?, + stderr: check_option(matches, options::ERROR)?, }) } } @@ -104,41 +105,6 @@ fn preload_strings() -> (&'static str, &'static str) { crash!(1, "Command not supported for this operating system!") } -fn parse_size(size: &str) -> Option { - let ext = size.trim_start_matches(|c: char| c.is_digit(10)); - let num = size.trim_end_matches(char::is_alphabetic); - let mut recovered = num.to_owned(); - recovered.push_str(ext); - if recovered != size { - return None; - } - let buf_size: u64 = match num.parse().ok() { - Some(m) => m, - None => return None, - }; - let (power, base): (u32, u64) = match ext { - "" => (0, 0), - "KB" => (1, 1024), - "K" => (1, 1000), - "MB" => (2, 1024), - "M" => (2, 1000), - "GB" => (3, 1024), - "G" => (3, 1000), - "TB" => (4, 1024), - "T" => (4, 1000), - "PB" => (5, 1024), - "P" => (5, 1000), - "EB" => (6, 1024), - "E" => (6, 1000), - "ZB" => (7, 1024), - "Z" => (7, 1000), - "YB" => (8, 1024), - "Y" => (8, 1000), - _ => return None, - }; - Some(buf_size * base.pow(power)) -} - fn check_option(matches: &ArgMatches, name: &str) -> Result { match matches.value_of(name) { Some(value) => match value { @@ -151,13 +117,10 @@ fn check_option(matches: &ArgMatches, name: &str) -> Result { - let size = match parse_size(x) { - Some(m) => m, - None => return Err(ProgramOptionsError(format!("invalid mode {}", x))), - }; - Ok(BufferType::Size(size)) - } + x => parse_size(x).map_or_else( + |e| crash!(125, "invalid mode {}", e), + |m| Ok(BufferType::Size(m)), + ), }, None => Ok(BufferType::Default), } @@ -191,10 +154,40 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .accept_any(); let usage = get_usage(); - let matches = App::new(executable!()) + let matches = uu_app().usage(&usage[..]).get_matches_from(args); + + let options = ProgramOptions::try_from(&matches) + .unwrap_or_else(|e| crash!(125, "{}\nTry 'stdbuf --help' for more information.", e.0)); + + let mut command_values = matches.values_of::<&str>(options::COMMAND).unwrap(); + let mut command = Command::new(command_values.next().unwrap()); + let command_params: Vec<&str> = command_values.collect(); + + let mut tmp_dir = tempdir().unwrap(); + let (preload_env, libstdbuf) = return_if_err!(1, get_preload_env(&mut tmp_dir)); + command.env(preload_env, libstdbuf); + set_command_env(&mut command, "_STDBUF_I", options.stdin); + set_command_env(&mut command, "_STDBUF_O", options.stdout); + set_command_env(&mut command, "_STDBUF_E", options.stderr); + command.args(command_params); + + let mut process = match command.spawn() { + Ok(p) => p, + Err(e) => crash!(1, "failed to execute process: {}", e), + }; + match process.wait() { + Ok(status) => match status.code() { + Some(i) => i, + None => crash!(1, "process killed by signal {}", status.signal().unwrap()), + }, + Err(e) => crash!(1, "{}", e), + } +} + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) .version(crate_version!()) .about(ABOUT) - .usage(&usage[..]) .after_help(LONG_HELP) .setting(AppSettings::TrailingVarArg) .arg( @@ -228,32 +221,4 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .hidden(true) .required(true), ) - .get_matches_from(args); - - let options = ProgramOptions::try_from(&matches) - .unwrap_or_else(|e| crash!(125, "{}\nTry 'stdbuf --help' for more information.", e.0)); - - let mut command_values = matches.values_of::<&str>(options::COMMAND).unwrap(); - let mut command = Command::new(command_values.next().unwrap()); - let command_params: Vec<&str> = command_values.collect(); - - let mut tmp_dir = tempdir().unwrap(); - let (preload_env, libstdbuf) = return_if_err!(1, get_preload_env(&mut tmp_dir)); - command.env(preload_env, libstdbuf); - set_command_env(&mut command, "_STDBUF_I", options.stdin); - set_command_env(&mut command, "_STDBUF_O", options.stdout); - set_command_env(&mut command, "_STDBUF_E", options.stderr); - command.args(command_params); - - let mut process = match command.spawn() { - Ok(p) => p, - Err(e) => crash!(1, "failed to execute process: {}", e), - }; - match process.wait() { - Ok(status) => match status.code() { - Some(i) => i, - None => crash!(1, "process killed by signal {}", status.signal().unwrap()), - }, - Err(e) => crash!(1, "{}", e), - } } diff --git a/src/uu/sum/Cargo.toml b/src/uu/sum/Cargo.toml index 64b6d3de9..e16c865a3 100644 --- a/src/uu/sum/Cargo.toml +++ b/src/uu/sum/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/sum.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/sum/src/sum.rs b/src/uu/sum/src/sum.rs index 4d42d7a97..0ce612859 100644 --- a/src/uu/sum/src/sum.rs +++ b/src/uu/sum/src/sum.rs @@ -98,24 +98,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .collect_str(InvalidEncodingHandling::ConvertLossy) .accept_any(); - let matches = App::new(executable!()) - .name(NAME) - .version(crate_version!()) - .usage(USAGE) - .about(SUMMARY) - .arg(Arg::with_name(options::FILE).multiple(true).hidden(true)) - .arg( - Arg::with_name(options::BSD_COMPATIBLE) - .short(options::BSD_COMPATIBLE) - .help("use the BSD sum algorithm, use 1K blocks (default)"), - ) - .arg( - Arg::with_name(options::SYSTEM_V_COMPATIBLE) - .short("s") - .long(options::SYSTEM_V_COMPATIBLE) - .help("use System V sum algorithm, use 512 bytes blocks"), - ) - .get_matches_from(args); + let matches = uu_app().get_matches_from(args); let files: Vec = match matches.values_of(options::FILE) { Some(v) => v.clone().map(|v| v.to_owned()).collect(), @@ -155,3 +138,23 @@ pub fn uumain(args: impl uucore::Args) -> i32 { exit_code } + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .name(NAME) + .version(crate_version!()) + .usage(USAGE) + .about(SUMMARY) + .arg(Arg::with_name(options::FILE).multiple(true).hidden(true)) + .arg( + Arg::with_name(options::BSD_COMPATIBLE) + .short(options::BSD_COMPATIBLE) + .help("use the BSD sum algorithm, use 1K blocks (default)"), + ) + .arg( + Arg::with_name(options::SYSTEM_V_COMPATIBLE) + .short("s") + .long(options::SYSTEM_V_COMPATIBLE) + .help("use System V sum algorithm, use 512 bytes blocks"), + ) +} diff --git a/src/uu/sync/Cargo.toml b/src/uu/sync/Cargo.toml index fcff6002e..83efb815d 100644 --- a/src/uu/sync/Cargo.toml +++ b/src/uu/sync/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/sync.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } libc = "0.2.42" uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["wide"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/sync/src/sync.rs b/src/uu/sync/src/sync.rs index 53d1a5701..4fcdf49f9 100644 --- a/src/uu/sync/src/sync.rs +++ b/src/uu/sync/src/sync.rs @@ -166,26 +166,7 @@ fn get_usage() -> String { pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); - let matches = App::new(executable!()) - .version(crate_version!()) - .about(ABOUT) - .usage(&usage[..]) - .arg( - Arg::with_name(options::FILE_SYSTEM) - .short("f") - .long(options::FILE_SYSTEM) - .conflicts_with(options::DATA) - .help("sync the file systems that contain the files (Linux and Windows only)"), - ) - .arg( - Arg::with_name(options::DATA) - .short("d") - .long(options::DATA) - .conflicts_with(options::FILE_SYSTEM) - .help("sync only file data, no unneeded metadata (Linux only)"), - ) - .arg(Arg::with_name(ARG_FILES).multiple(true).takes_value(true)) - .get_matches_from(args); + let matches = uu_app().usage(&usage[..]).get_matches_from(args); let files: Vec = matches .values_of(ARG_FILES) @@ -211,6 +192,27 @@ pub fn uumain(args: impl uucore::Args) -> i32 { 0 } +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about(ABOUT) + .arg( + Arg::with_name(options::FILE_SYSTEM) + .short("f") + .long(options::FILE_SYSTEM) + .conflicts_with(options::DATA) + .help("sync the file systems that contain the files (Linux and Windows only)"), + ) + .arg( + Arg::with_name(options::DATA) + .short("d") + .long(options::DATA) + .conflicts_with(options::FILE_SYSTEM) + .help("sync only file data, no unneeded metadata (Linux only)"), + ) + .arg(Arg::with_name(ARG_FILES).multiple(true).takes_value(true)) +} + fn sync() -> isize { unsafe { platform::do_sync() } } diff --git a/src/uu/tac/Cargo.toml b/src/uu/tac/Cargo.toml index 3a530d0ce..2d0623cd9 100644 --- a/src/uu/tac/Cargo.toml +++ b/src/uu/tac/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/tac.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/tac/src/tac.rs b/src/uu/tac/src/tac.rs index be1852ec5..ae1fd9bc5 100644 --- a/src/uu/tac/src/tac.rs +++ b/src/uu/tac/src/tac.rs @@ -31,7 +31,31 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .collect_str(InvalidEncodingHandling::ConvertLossy) .accept_any(); - let matches = App::new(executable!()) + let matches = uu_app().get_matches_from(args); + + let before = matches.is_present(options::BEFORE); + let regex = matches.is_present(options::REGEX); + let separator = match matches.value_of(options::SEPARATOR) { + Some(m) => { + if m.is_empty() { + crash!(1, "separator cannot be empty") + } else { + m.to_owned() + } + } + None => "\n".to_owned(), + }; + + let files: Vec = match matches.values_of(options::FILE) { + Some(v) => v.map(|v| v.to_owned()).collect(), + None => vec!["-".to_owned()], + }; + + tac(files, before, regex, &separator[..]) +} + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) .name(NAME) .version(crate_version!()) .usage(USAGE) @@ -58,27 +82,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .takes_value(true), ) .arg(Arg::with_name(options::FILE).hidden(true).multiple(true)) - .get_matches_from(args); - - let before = matches.is_present(options::BEFORE); - let regex = matches.is_present(options::REGEX); - let separator = match matches.value_of(options::SEPARATOR) { - Some(m) => { - if m.is_empty() { - crash!(1, "separator cannot be empty") - } else { - m.to_owned() - } - } - None => "\n".to_owned(), - }; - - let files: Vec = match matches.values_of(options::FILE) { - Some(v) => v.map(|v| v.to_owned()).collect(), - None => vec!["-".to_owned()], - }; - - tac(files, before, regex, &separator[..]) } fn tac(filenames: Vec, before: bool, _: bool, separator: &str) -> i32 { diff --git a/src/uu/tail/Cargo.toml b/src/uu/tail/Cargo.toml index 273c67bb3..a895819cd 100644 --- a/src/uu/tail/Cargo.toml +++ b/src/uu/tail/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/tail.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } libc = "0.2.42" uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["ringbuffer"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/tail/src/tail.rs b/src/uu/tail/src/tail.rs index 15a819d35..4970cdcc2 100644 --- a/src/uu/tail/src/tail.rs +++ b/src/uu/tail/src/tail.rs @@ -5,7 +5,6 @@ // * // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -// * // spell-checker:ignore (ToDO) seekable seek'd tail'ing ringbuffer ringbuf @@ -21,19 +20,18 @@ use chunks::ReverseChunks; use clap::{App, Arg}; use std::collections::VecDeque; -use std::error::Error; use std::fmt; use std::fs::File; use std::io::{stdin, stdout, BufRead, BufReader, Read, Seek, SeekFrom, Write}; use std::path::Path; use std::thread::sleep; use std::time::Duration; +use uucore::parse_size::{parse_size, ParseSizeError}; use uucore::ringbuffer::RingBuffer; pub mod options { pub mod verbosity { pub static QUIET: &str = "quiet"; - pub static SILENT: &str = "silent"; pub static VERBOSE: &str = "verbose"; } pub static BYTES: &str = "bytes"; @@ -42,13 +40,12 @@ pub mod options { pub static PID: &str = "pid"; pub static SLEEP_INT: &str = "sleep-interval"; pub static ZERO_TERM: &str = "zero-terminated"; + pub static ARG_FILES: &str = "files"; } -static ARG_FILES: &str = "files"; - enum FilterMode { - Bytes(u64), - Lines(u64, u8), // (number of lines, delimiter) + Bytes(usize), + Lines(usize, u8), // (number of lines, delimiter) } struct Settings { @@ -75,73 +72,7 @@ impl Default for Settings { pub fn uumain(args: impl uucore::Args) -> i32 { let mut settings: Settings = Default::default(); - let app = App::new(executable!()) - .version(crate_version!()) - .about("output the last part of files") - .arg( - Arg::with_name(options::BYTES) - .short("c") - .long(options::BYTES) - .takes_value(true) - .allow_hyphen_values(true) - .help("Number of bytes to print"), - ) - .arg( - Arg::with_name(options::FOLLOW) - .short("f") - .long(options::FOLLOW) - .help("Print the file as it grows"), - ) - .arg( - Arg::with_name(options::LINES) - .short("n") - .long(options::LINES) - .takes_value(true) - .allow_hyphen_values(true) - .help("Number of lines to print"), - ) - .arg( - Arg::with_name(options::PID) - .long(options::PID) - .takes_value(true) - .help("with -f, terminate after process ID, PID dies"), - ) - .arg( - Arg::with_name(options::verbosity::QUIET) - .short("q") - .long(options::verbosity::QUIET) - .help("never output headers giving file names"), - ) - .arg( - Arg::with_name(options::verbosity::SILENT) - .long(options::verbosity::SILENT) - .help("synonym of --quiet"), - ) - .arg( - Arg::with_name(options::SLEEP_INT) - .short("s") - .takes_value(true) - .long(options::SLEEP_INT) - .help("Number or seconds to sleep between polling the file when running with -f"), - ) - .arg( - Arg::with_name(options::verbosity::VERBOSE) - .short("v") - .long(options::verbosity::VERBOSE) - .help("always output headers giving file names"), - ) - .arg( - Arg::with_name(options::ZERO_TERM) - .short("z") - .long(options::ZERO_TERM) - .help("Line delimiter is NUL, not newline"), - ) - .arg( - Arg::with_name(ARG_FILES) - .multiple(true) - .takes_value(true) - .min_values(1), - ); + let app = uu_app(); let matches = app.get_matches_from(args); @@ -171,38 +102,21 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } } - match matches.value_of(options::LINES) { - Some(n) => { - let mut slice: &str = n; - if slice.chars().next().unwrap_or('_') == '+' { - settings.beginning = true; - slice = &slice[1..]; - } - match parse_size(slice) { - Ok(m) => settings.mode = FilterMode::Lines(m, b'\n'), - Err(e) => { - show_error!("{}", e.to_string()); - return 1; - } - } + let mode_and_beginning = if let Some(arg) = matches.value_of(options::BYTES) { + match parse_num(arg) { + Ok((n, beginning)) => (FilterMode::Bytes(n), beginning), + Err(e) => crash!(1, "invalid number of bytes: {}", e.to_string()), } - None => { - if let Some(n) = matches.value_of(options::BYTES) { - let mut slice: &str = n; - if slice.chars().next().unwrap_or('_') == '+' { - settings.beginning = true; - slice = &slice[1..]; - } - match parse_size(slice) { - Ok(m) => settings.mode = FilterMode::Bytes(m), - Err(e) => { - show_error!("{}", e.to_string()); - return 1; - } - } - } + } else if let Some(arg) = matches.value_of(options::LINES) { + match parse_num(arg) { + Ok((n, beginning)) => (FilterMode::Lines(n, b'\n'), beginning), + Err(e) => crash!(1, "invalid number of lines: {}", e.to_string()), } + } else { + (FilterMode::Lines(10, b'\n'), false) }; + settings.mode = mode_and_beginning.0; + settings.beginning = mode_and_beginning.1; if matches.is_present(options::ZERO_TERM) { if let FilterMode::Lines(count, _) = settings.mode { @@ -211,11 +125,10 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } let verbose = matches.is_present(options::verbosity::VERBOSE); - let quiet = matches.is_present(options::verbosity::QUIET) - || matches.is_present(options::verbosity::SILENT); + let quiet = matches.is_present(options::verbosity::QUIET); let files: Vec = matches - .values_of(ARG_FILES) + .values_of(options::ARG_FILES) .map(|v| v.map(ToString::to_string).collect()) .unwrap_or_default(); @@ -264,96 +177,75 @@ pub fn uumain(args: impl uucore::Args) -> i32 { 0 } -#[derive(Debug, PartialEq, Eq)] -pub enum ParseSizeErr { - ParseFailure(String), - SizeTooBig(String), -} - -impl Error for ParseSizeErr { - fn description(&self) -> &str { - match *self { - ParseSizeErr::ParseFailure(ref s) => &*s, - ParseSizeErr::SizeTooBig(ref s) => &*s, - } - } -} - -impl fmt::Display for ParseSizeErr { - fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { - let s = match self { - ParseSizeErr::ParseFailure(s) => s, - ParseSizeErr::SizeTooBig(s) => s, - }; - write!(f, "{}", s) - } -} - -impl ParseSizeErr { - fn parse_failure(s: &str) -> ParseSizeErr { - ParseSizeErr::ParseFailure(format!("invalid size: '{}'", s)) - } - - fn size_too_big(s: &str) -> ParseSizeErr { - ParseSizeErr::SizeTooBig(format!( - "invalid size: '{}': Value too large to be stored in data type", - s - )) - } -} - -pub type ParseSizeResult = Result; - -pub fn parse_size(mut size_slice: &str) -> Result { - let mut base = if size_slice.chars().last().unwrap_or('_') == 'B' { - size_slice = &size_slice[..size_slice.len() - 1]; - 1000u64 - } else { - 1024u64 - }; - - let exponent = if !size_slice.is_empty() { - let mut has_suffix = true; - let exp = match size_slice.chars().last().unwrap_or('_') { - 'K' | 'k' => 1u64, - 'M' => 2u64, - 'G' => 3u64, - 'T' => 4u64, - 'P' => 5u64, - 'E' => 6u64, - 'Z' | 'Y' => { - return Err(ParseSizeErr::size_too_big(size_slice)); - } - 'b' => { - base = 512u64; - 1u64 - } - _ => { - has_suffix = false; - 0u64 - } - }; - if has_suffix { - size_slice = &size_slice[..size_slice.len() - 1]; - } - exp - } else { - 0u64 - }; - - let mut multiplier = 1u64; - for _ in 0u64..exponent { - multiplier *= base; - } - if base == 1000u64 && exponent == 0u64 { - // sole B is not a valid suffix - Err(ParseSizeErr::parse_failure(size_slice)) - } else { - let value: Option = size_slice.parse().ok(); - value - .map(|v| Ok((multiplier as i64 * v.abs()) as u64)) - .unwrap_or_else(|| Err(ParseSizeErr::parse_failure(size_slice))) - } +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about("output the last part of files") + // TODO: add usage + .arg( + Arg::with_name(options::BYTES) + .short("c") + .long(options::BYTES) + .takes_value(true) + .allow_hyphen_values(true) + .overrides_with_all(&[options::BYTES, options::LINES]) + .help("Number of bytes to print"), + ) + .arg( + Arg::with_name(options::FOLLOW) + .short("f") + .long(options::FOLLOW) + .help("Print the file as it grows"), + ) + .arg( + Arg::with_name(options::LINES) + .short("n") + .long(options::LINES) + .takes_value(true) + .allow_hyphen_values(true) + .overrides_with_all(&[options::BYTES, options::LINES]) + .help("Number of lines to print"), + ) + .arg( + Arg::with_name(options::PID) + .long(options::PID) + .takes_value(true) + .help("with -f, terminate after process ID, PID dies"), + ) + .arg( + Arg::with_name(options::verbosity::QUIET) + .short("q") + .long(options::verbosity::QUIET) + .visible_alias("silent") + .overrides_with_all(&[options::verbosity::QUIET, options::verbosity::VERBOSE]) + .help("never output headers giving file names"), + ) + .arg( + Arg::with_name(options::SLEEP_INT) + .short("s") + .takes_value(true) + .long(options::SLEEP_INT) + .help("Number or seconds to sleep between polling the file when running with -f"), + ) + .arg( + Arg::with_name(options::verbosity::VERBOSE) + .short("v") + .long(options::verbosity::VERBOSE) + .overrides_with_all(&[options::verbosity::QUIET, options::verbosity::VERBOSE]) + .help("always output headers giving file names"), + ) + .arg( + Arg::with_name(options::ZERO_TERM) + .short("z") + .long(options::ZERO_TERM) + .help("Line delimiter is NUL, not newline"), + ) + .arg( + Arg::with_name(options::ARG_FILES) + .multiple(true) + .takes_value(true) + .min_values(1), + ) } fn follow(readers: &mut [BufReader], filenames: &[String], settings: &Settings) { @@ -469,7 +361,7 @@ fn bounded_tail(file: &mut File, settings: &Settings) { /// If any element of `iter` is an [`Err`], then this function panics. fn unbounded_tail_collect( iter: impl Iterator>, - count: u64, + count: usize, beginning: bool, ) -> VecDeque where @@ -514,3 +406,22 @@ fn print_byte(stdout: &mut T, ch: u8) { crash!(1, "{}", err); } } + +fn parse_num(src: &str) -> Result<(usize, bool), ParseSizeError> { + let mut size_string = src.trim(); + let mut starting_with = false; + + if let Some(c) = size_string.chars().next() { + if c == '+' || c == '-' { + // tail: '-' is not documented (8.32 man pages) + size_string = &size_string[1..]; + if c == '+' { + starting_with = true; + } + } + } else { + return Err(ParseSizeError::ParseFailure(src.to_string())); + } + + parse_size(size_string).map(|n| (n, starting_with)) +} diff --git a/src/uu/tee/Cargo.toml b/src/uu/tee/Cargo.toml index 7ac81adc4..a88d76508 100644 --- a/src/uu/tee/Cargo.toml +++ b/src/uu/tee/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/tee.rs" [dependencies] -clap = "2.33.3" +clap = { version = "2.33", features = ["wrap_help"] } libc = "0.2.42" retain_mut = "0.1.2" uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["libc"] } diff --git a/src/uu/tee/src/tee.rs b/src/uu/tee/src/tee.rs index f5f24d944..a207dee63 100644 --- a/src/uu/tee/src/tee.rs +++ b/src/uu/tee/src/tee.rs @@ -39,25 +39,7 @@ fn get_usage() -> String { pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); - let matches = App::new(executable!()) - .version(crate_version!()) - .about(ABOUT) - .usage(&usage[..]) - .after_help("If a FILE is -, it refers to a file named - .") - .arg( - Arg::with_name(options::APPEND) - .long(options::APPEND) - .short("a") - .help("append to the given FILEs, do not overwrite"), - ) - .arg( - Arg::with_name(options::IGNORE_INTERRUPTS) - .long(options::IGNORE_INTERRUPTS) - .short("i") - .help("ignore interrupt signals (ignored on non-Unix platforms)"), - ) - .arg(Arg::with_name(options::FILE).multiple(true)) - .get_matches_from(args); + let matches = uu_app().usage(&usage[..]).get_matches_from(args); let options = Options { append: matches.is_present(options::APPEND), @@ -74,6 +56,26 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } } +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about(ABOUT) + .after_help("If a FILE is -, it refers to a file named - .") + .arg( + Arg::with_name(options::APPEND) + .long(options::APPEND) + .short("a") + .help("append to the given FILEs, do not overwrite"), + ) + .arg( + Arg::with_name(options::IGNORE_INTERRUPTS) + .long(options::IGNORE_INTERRUPTS) + .short("i") + .help("ignore interrupt signals (ignored on non-Unix platforms)"), + ) + .arg(Arg::with_name(options::FILE).multiple(true)) +} + #[cfg(unix)] fn ignore_interrupts() -> Result<()> { let ret = unsafe { libc::signal(libc::SIGINT, libc::SIG_IGN) }; diff --git a/src/uu/test/Cargo.toml b/src/uu/test/Cargo.toml index e1f6e62e7..6f6dd340e 100644 --- a/src/uu/test/Cargo.toml +++ b/src/uu/test/Cargo.toml @@ -15,6 +15,7 @@ edition = "2018" path = "src/test.rs" [dependencies] +clap = { version = "2.33", features = ["wrap_help"] } libc = "0.2.42" uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/test/src/parser.rs b/src/uu/test/src/parser.rs index d4302bd67..5eec781ba 100644 --- a/src/uu/test/src/parser.rs +++ b/src/uu/test/src/parser.rs @@ -167,7 +167,7 @@ impl Parser { self.expr(); match self.next_token() { Symbol::Literal(s) if s == ")" => (), - _ => panic!("expected ‘)’"), + _ => panic!("expected ')'"), } } } @@ -314,7 +314,7 @@ impl Parser { self.expr(); match self.tokens.next() { - Some(token) => Err(format!("extra argument ‘{}’", token.to_string_lossy())), + Some(token) => Err(format!("extra argument '{}'", token.to_string_lossy())), None => Ok(()), } } diff --git a/src/uu/test/src/test.rs b/src/uu/test/src/test.rs index acf0f7eca..dba840d3c 100644 --- a/src/uu/test/src/test.rs +++ b/src/uu/test/src/test.rs @@ -6,16 +6,38 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (vars) FiletestOp StrlenOp +// spell-checker:ignore (vars) egid euid FiletestOp StrlenOp mod parser; +use clap::{App, AppSettings}; use parser::{parse, Symbol}; use std::ffi::{OsStr, OsString}; +use std::path::Path; +use uucore::executable; -pub fn uumain(args: impl uucore::Args) -> i32 { - // TODO: handle being called as `[` - let args: Vec<_> = args.skip(1).collect(); +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .setting(AppSettings::DisableHelpFlags) + .setting(AppSettings::DisableVersion) +} + +pub fn uumain(mut args: impl uucore::Args) -> i32 { + let program = args.next().unwrap_or_else(|| OsString::from("test")); + let binary_name = Path::new(&program) + .file_name() + .unwrap_or_else(|| OsStr::new("test")) + .to_string_lossy(); + let mut args: Vec<_> = args.collect(); + + // If invoked via name '[', matching ']' must be in the last arg + if binary_name == "[" { + let last = args.pop(); + if last != Some(OsString::from("]")) { + eprintln!("[: missing ']'"); + return 2; + } + } let result = parse(args).and_then(|mut stack| eval(&mut stack)); @@ -74,7 +96,7 @@ fn eval(stack: &mut Vec) -> Result { return Ok(true); } _ => { - return Err(format!("missing argument after ‘{:?}’", op)); + return Err(format!("missing argument after '{:?}'", op)); } }; @@ -96,8 +118,11 @@ fn eval(stack: &mut Vec) -> Result { "-e" => path(&f, PathCondition::Exists), "-f" => path(&f, PathCondition::Regular), "-g" => path(&f, PathCondition::GroupIdFlag), + "-G" => path(&f, PathCondition::GroupOwns), "-h" => path(&f, PathCondition::SymLink), + "-k" => path(&f, PathCondition::Sticky), "-L" => path(&f, PathCondition::SymLink), + "-O" => path(&f, PathCondition::UserOwns), "-p" => path(&f, PathCondition::Fifo), "-r" => path(&f, PathCondition::Readable), "-S" => path(&f, PathCondition::Socket), @@ -123,7 +148,7 @@ fn eval(stack: &mut Vec) -> Result { } fn integers(a: &OsStr, b: &OsStr, op: &OsStr) -> Result { - let format_err = |value| format!("invalid integer ‘{}’", value); + let format_err = |value| format!("invalid integer '{}'", value); let a = a.to_string_lossy(); let a: i64 = a.parse().map_err(|_| format_err(a))?; @@ -139,7 +164,7 @@ fn integers(a: &OsStr, b: &OsStr, op: &OsStr) -> Result { "-ge" => a >= b, "-lt" => a < b, "-le" => a <= b, - _ => return Err(format!("unknown operator ‘{}’", operator)), + _ => return Err(format!("unknown operator '{}'", operator)), }) } @@ -147,7 +172,7 @@ fn isatty(fd: &OsStr) -> Result { let fd = fd.to_string_lossy(); fd.parse() - .map_err(|_| format!("invalid integer ‘{}’", fd)) + .map_err(|_| format!("invalid integer '{}'", fd)) .map(|i| { #[cfg(not(target_os = "redox"))] unsafe { @@ -166,7 +191,10 @@ enum PathCondition { Exists, Regular, GroupIdFlag, + GroupOwns, SymLink, + Sticky, + UserOwns, Fifo, Readable, Socket, @@ -183,6 +211,7 @@ fn path(path: &OsStr, condition: PathCondition) -> bool { const S_ISUID: u32 = 0o4000; const S_ISGID: u32 = 0o2000; + const S_ISVTX: u32 = 0o1000; enum Permission { Read = 0o4, @@ -190,18 +219,28 @@ fn path(path: &OsStr, condition: PathCondition) -> bool { Execute = 0o1, } - let perm = |metadata: Metadata, p: Permission| { + let geteuid = || { #[cfg(not(target_os = "redox"))] - let (uid, gid) = unsafe { (libc::getuid(), libc::getgid()) }; + let euid = unsafe { libc::geteuid() }; #[cfg(target_os = "redox")] - let (uid, gid) = ( - syscall::getuid().unwrap() as u32, - syscall::getgid().unwrap() as u32, - ); + let euid = syscall::geteuid().unwrap() as u32; - if uid == metadata.uid() { + euid + }; + + let getegid = || { + #[cfg(not(target_os = "redox"))] + let egid = unsafe { libc::getegid() }; + #[cfg(target_os = "redox")] + let egid = syscall::getegid().unwrap() as u32; + + egid + }; + + let perm = |metadata: Metadata, p: Permission| { + if geteuid() == metadata.uid() { metadata.mode() & ((p as u32) << 6) != 0 - } else if gid == metadata.gid() { + } else if getegid() == metadata.gid() { metadata.mode() & ((p as u32) << 3) != 0 } else { metadata.mode() & (p as u32) != 0 @@ -230,7 +269,10 @@ fn path(path: &OsStr, condition: PathCondition) -> bool { PathCondition::Exists => true, PathCondition::Regular => file_type.is_file(), PathCondition::GroupIdFlag => metadata.mode() & S_ISGID != 0, + PathCondition::GroupOwns => metadata.gid() == getegid(), PathCondition::SymLink => metadata.file_type().is_symlink(), + PathCondition::Sticky => metadata.mode() & S_ISVTX != 0, + PathCondition::UserOwns => metadata.uid() == geteuid(), PathCondition::Fifo => file_type.is_fifo(), PathCondition::Readable => perm(metadata, Permission::Read), PathCondition::Socket => file_type.is_socket(), @@ -257,7 +299,10 @@ fn path(path: &OsStr, condition: PathCondition) -> bool { PathCondition::Exists => true, PathCondition::Regular => stat.is_file(), PathCondition::GroupIdFlag => false, + PathCondition::GroupOwns => unimplemented!(), PathCondition::SymLink => false, + PathCondition::Sticky => false, + PathCondition::UserOwns => unimplemented!(), PathCondition::Fifo => false, PathCondition::Readable => false, // TODO PathCondition::Socket => false, diff --git a/src/uu/timeout/Cargo.toml b/src/uu/timeout/Cargo.toml index 206a98c08..63a16c086 100644 --- a/src/uu/timeout/Cargo.toml +++ b/src/uu/timeout/Cargo.toml @@ -15,10 +15,10 @@ edition = "2018" path = "src/timeout.rs" [dependencies] -clap = "2.33" -getopts = "0.2.18" +clap = { version = "2.33", features = ["wrap_help"] } libc = "0.2.42" -uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["parse_time", "process", "signals"] } +nix = "0.20.0" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["process", "signals"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/timeout/src/timeout.rs b/src/uu/timeout/src/timeout.rs index 4ef9b2331..464414c5e 100644 --- a/src/uu/timeout/src/timeout.rs +++ b/src/uu/timeout/src/timeout.rs @@ -5,7 +5,7 @@ // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -// spell-checker:ignore (ToDO) tstr sigstr cmdname setpgid +// spell-checker:ignore (ToDO) tstr sigstr cmdname setpgid sigchld #[macro_use] extern crate uucore; @@ -17,13 +17,13 @@ use std::io::ErrorKind; use std::process::{Command, Stdio}; use std::time::Duration; use uucore::process::ChildExt; -use uucore::signals::signal_by_name_or_value; +use uucore::signals::{signal_by_name_or_value, signal_name_by_value}; use uucore::InvalidEncodingHandling; static ABOUT: &str = "Start COMMAND, and kill it if still running after DURATION."; fn get_usage() -> String { - format!("{0} [OPTION]... [FILE]...", executable!()) + format!("{0} [OPTION] DURATION COMMAND...", executable!()) } const ERR_EXIT_STATUS: i32 = 125; @@ -33,29 +33,29 @@ pub mod options { pub static KILL_AFTER: &str = "kill-after"; pub static SIGNAL: &str = "signal"; pub static PRESERVE_STATUS: &str = "preserve-status"; + pub static VERBOSE: &str = "verbose"; // Positional args. pub static DURATION: &str = "duration"; pub static COMMAND: &str = "command"; - pub static ARGS: &str = "args"; } struct Config { foreground: bool, - kill_after: Duration, + kill_after: Option, signal: usize, duration: Duration, preserve_status: bool, + verbose: bool, - command: String, - command_args: Vec, + command: Vec, } impl Config { fn from(options: clap::ArgMatches) -> Config { let signal = match options.value_of(options::SIGNAL) { Some(signal_) => { - let signal_result = signal_by_name_or_value(&signal_); + let signal_result = signal_by_name_or_value(signal_); match signal_result { None => { unreachable!("invalid signal '{}'", signal_); @@ -66,23 +66,22 @@ impl Config { _ => uucore::signals::signal_by_name_or_value("TERM").unwrap(), }; - let kill_after: Duration = match options.value_of(options::KILL_AFTER) { - Some(time) => uucore::parse_time::from_str(&time).unwrap(), - None => Duration::new(0, 0), - }; + let kill_after = options + .value_of(options::KILL_AFTER) + .map(|time| uucore::parse_time::from_str(time).unwrap()); let duration: Duration = uucore::parse_time::from_str(options.value_of(options::DURATION).unwrap()).unwrap(); let preserve_status: bool = options.is_present(options::PRESERVE_STATUS); let foreground = options.is_present(options::FOREGROUND); + let verbose = options.is_present(options::VERBOSE); - let command: String = options.value_of(options::COMMAND).unwrap().to_string(); - - let command_args: Vec = match options.values_of(options::ARGS) { - Some(values) => values.map(|x| x.to_owned()).collect(), - None => vec![], - }; + let command = options + .values_of(options::COMMAND) + .unwrap() + .map(String::from) + .collect::>(); Config { foreground, @@ -90,8 +89,8 @@ impl Config { signal, duration, preserve_status, + verbose, command, - command_args, } } } @@ -103,9 +102,25 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); - let app = App::new("timeout") + let app = uu_app().usage(&usage[..]); + + let matches = app.get_matches_from(args); + + let config = Config::from(matches); + timeout( + &config.command, + config.duration, + config.signal, + config.kill_after, + config.foreground, + config.preserve_status, + config.verbose, + ) +} + +pub fn uu_app() -> App<'static, 'static> { + App::new("timeout") .version(crate_version!()) - .usage(&usage[..]) .about(ABOUT) .arg( Arg::with_name(options::FOREGROUND) @@ -128,6 +143,12 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .help("specify the signal to be sent on timeout; SIGNAL may be a name like 'HUP' or a number; see 'kill -l' for a list of signals") .takes_value(true) ) + .arg( + Arg::with_name(options::VERBOSE) + .short("v") + .long(options::VERBOSE) + .help("diagnose to stderr any signal sent upon timeout") + ) .arg( Arg::with_name(options::DURATION) .index(1) @@ -137,42 +158,38 @@ pub fn uumain(args: impl uucore::Args) -> i32 { Arg::with_name(options::COMMAND) .index(2) .required(true) + .multiple(true) ) - .arg( - Arg::with_name(options::ARGS).multiple(true) + .setting(AppSettings::TrailingVarArg) +} + +/// Remove pre-existing SIGCHLD handlers that would make waiting for the child's exit code fail. +fn unblock_sigchld() { + unsafe { + nix::sys::signal::signal( + nix::sys::signal::Signal::SIGCHLD, + nix::sys::signal::SigHandler::SigDfl, ) - .setting(AppSettings::TrailingVarArg); - - let matches = app.get_matches_from(args); - - let config = Config::from(matches); - timeout( - &config.command, - &config.command_args, - config.duration, - config.signal, - config.kill_after, - config.foreground, - config.preserve_status, - ) + .unwrap(); + } } /// TODO: Improve exit codes, and make them consistent with the GNU Coreutils exit codes. fn timeout( - cmdname: &str, - args: &[String], + cmd: &[String], duration: Duration, signal: usize, - kill_after: Duration, + kill_after: Option, foreground: bool, preserve_status: bool, + verbose: bool, ) -> i32 { if !foreground { unsafe { libc::setpgid(0, 0) }; } - let mut process = match Command::new(cmdname) - .args(args) + let mut process = match Command::new(&cmd[0]) + .args(&cmd[1..]) .stdin(Stdio::inherit()) .stdout(Stdio::inherit()) .stderr(Stdio::inherit()) @@ -190,32 +207,44 @@ fn timeout( } } }; + unblock_sigchld(); match process.wait_or_timeout(duration) { Ok(Some(status)) => status.code().unwrap_or_else(|| status.signal().unwrap()), Ok(None) => { + if verbose { + show_error!( + "sending signal {} to command '{}'", + signal_name_by_value(signal).unwrap(), + cmd[0] + ); + } return_if_err!(ERR_EXIT_STATUS, process.send_signal(signal)); - match process.wait_or_timeout(kill_after) { - Ok(Some(status)) => { - if preserve_status { - status.code().unwrap_or_else(|| status.signal().unwrap()) - } else { - 124 + if let Some(kill_after) = kill_after { + match process.wait_or_timeout(kill_after) { + Ok(Some(status)) => { + if preserve_status { + status.code().unwrap_or_else(|| status.signal().unwrap()) + } else { + 124 + } } - } - Ok(None) => { - if kill_after == Duration::new(0, 0) { - // XXX: this may not be right - return 124; + Ok(None) => { + if verbose { + show_error!("sending signal KILL to command '{}'", cmd[0]); + } + return_if_err!( + ERR_EXIT_STATUS, + process.send_signal( + uucore::signals::signal_by_name_or_value("KILL").unwrap() + ) + ); + return_if_err!(ERR_EXIT_STATUS, process.wait()); + 137 } - return_if_err!( - ERR_EXIT_STATUS, - process - .send_signal(uucore::signals::signal_by_name_or_value("KILL").unwrap()) - ); - return_if_err!(ERR_EXIT_STATUS, process.wait()); - 137 + Err(_) => 124, } - Err(_) => 124, + } else { + 124 } } Err(_) => { diff --git a/src/uu/touch/Cargo.toml b/src/uu/touch/Cargo.toml index 0608a7b7c..e2f948a5a 100644 --- a/src/uu/touch/Cargo.toml +++ b/src/uu/touch/Cargo.toml @@ -16,7 +16,7 @@ path = "src/touch.rs" [dependencies] filetime = "0.2.1" -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } time = "0.1.40" uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["libc"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index be4e51041..dd2b05d0e 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -16,9 +16,8 @@ extern crate uucore; use clap::{crate_version, App, Arg, ArgGroup}; use filetime::*; use std::fs::{self, File}; -use std::io::Error; use std::path::Path; -use std::process; +use uucore::error::{FromIo, UResult, USimpleError}; static ABOUT: &str = "Update the access and modification times of each FILE to the current time."; pub mod options { @@ -52,13 +51,87 @@ fn get_usage() -> String { format!("{0} [OPTION]... [USER]", executable!()) } -pub fn uumain(args: impl uucore::Args) -> i32 { +#[uucore_procs::gen_uumain] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { let usage = get_usage(); - let matches = App::new(executable!()) + let matches = uu_app().usage(&usage[..]).get_matches_from(args); + + let files = matches.values_of_os(ARG_FILES).unwrap(); + + let (mut atime, mut mtime) = + if let Some(reference) = matches.value_of_os(options::sources::REFERENCE) { + stat(Path::new(reference), !matches.is_present(options::NO_DEREF))? + } else { + let timestamp = if let Some(date) = matches.value_of(options::sources::DATE) { + parse_date(date)? + } else if let Some(current) = matches.value_of(options::sources::CURRENT) { + parse_timestamp(current)? + } else { + local_tm_to_filetime(time::now()) + }; + (timestamp, timestamp) + }; + + for filename in files { + let path = Path::new(filename); + if !path.exists() { + // no-dereference included here for compatibility + if matches.is_present(options::NO_CREATE) || matches.is_present(options::NO_DEREF) { + continue; + } + + if let Err(e) = File::create(path) { + show!(e.map_err_context(|| format!("cannot touch '{}'", path.display()))); + continue; + }; + + // Minor optimization: if no reference time was specified, we're done. + if !matches.is_present(options::SOURCES) { + continue; + } + } + + // If changing "only" atime or mtime, grab the existing value of the other. + // Note that "-a" and "-m" may be passed together; this is not an xor. + if matches.is_present(options::ACCESS) + || matches.is_present(options::MODIFICATION) + || matches.is_present(options::TIME) + { + let st = stat(path, !matches.is_present(options::NO_DEREF))?; + let time = matches.value_of(options::TIME).unwrap_or(""); + + if !(matches.is_present(options::ACCESS) + || time.contains(&"access".to_owned()) + || time.contains(&"atime".to_owned()) + || time.contains(&"use".to_owned())) + { + atime = st.0; + } + + if !(matches.is_present(options::MODIFICATION) + || time.contains(&"modify".to_owned()) + || time.contains(&"mtime".to_owned())) + { + mtime = st.1; + } + } + + if matches.is_present(options::NO_DEREF) { + set_symlink_file_times(path, atime, mtime) + } else { + filetime::set_file_times(path, atime, mtime) + } + .map_err_context(|| format!("setting times of '{}'", path.display()))?; + } + + Ok(()) +} + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) .version(crate_version!()) .about(ABOUT) - .usage(&usage[..]) .arg( Arg::with_name(options::ACCESS) .short("a") @@ -128,128 +201,23 @@ pub fn uumain(args: impl uucore::Args) -> i32 { options::sources::DATE, options::sources::REFERENCE, ])) - .get_matches_from(args); - - let files: Vec = matches - .values_of(ARG_FILES) - .map(|v| v.map(ToString::to_string).collect()) - .unwrap_or_default(); - - let (mut atime, mut mtime) = if matches.is_present(options::sources::REFERENCE) { - stat( - matches.value_of(options::sources::REFERENCE).unwrap(), - !matches.is_present(options::NO_DEREF), - ) - } else if matches.is_present(options::sources::DATE) - || matches.is_present(options::sources::CURRENT) - { - let timestamp = if matches.is_present(options::sources::DATE) { - parse_date(matches.value_of(options::sources::DATE).unwrap()) - } else { - parse_timestamp(matches.value_of(options::sources::CURRENT).unwrap()) - }; - (timestamp, timestamp) - } else { - let now = local_tm_to_filetime(time::now()); - (now, now) - }; - - let mut error_code = 0; - - for filename in &files { - let path = &filename[..]; - - if !Path::new(path).exists() { - // no-dereference included here for compatibility - if matches.is_present(options::NO_CREATE) || matches.is_present(options::NO_DEREF) { - continue; - } - - if let Err(e) = File::create(path) { - show_warning!("cannot touch '{}': {}", path, e); - error_code = 1; - continue; - }; - - // Minor optimization: if no reference time was specified, we're done. - if !matches.is_present(options::SOURCES) { - continue; - } - } - - // If changing "only" atime or mtime, grab the existing value of the other. - // Note that "-a" and "-m" may be passed together; this is not an xor. - if matches.is_present(options::ACCESS) - || matches.is_present(options::MODIFICATION) - || matches.is_present(options::TIME) - { - let st = stat(path, !matches.is_present(options::NO_DEREF)); - let time = matches.value_of(options::TIME).unwrap_or(""); - - if !(matches.is_present(options::ACCESS) - || time.contains(&"access".to_owned()) - || time.contains(&"atime".to_owned()) - || time.contains(&"use".to_owned())) - { - atime = st.0; - } - - if !(matches.is_present(options::MODIFICATION) - || time.contains(&"modify".to_owned()) - || time.contains(&"mtime".to_owned())) - { - mtime = st.1; - } - } - - if matches.is_present(options::NO_DEREF) { - if let Err(e) = set_symlink_file_times(path, atime, mtime) { - // we found an error, it should fail in any case - error_code = 1; - if e.kind() == std::io::ErrorKind::PermissionDenied { - // GNU compatibility (not-owner.sh) - show_error!("setting times of '{}': {}", path, "Permission denied"); - } else { - show_error!("setting times of '{}': {}", path, e); - } - } - } else if let Err(e) = filetime::set_file_times(path, atime, mtime) { - // we found an error, it should fail in any case - error_code = 1; - - if e.kind() == std::io::ErrorKind::PermissionDenied { - // GNU compatibility (not-owner.sh) - show_error!("setting times of '{}': {}", path, "Permission denied"); - } else { - show_error!("setting times of '{}': {}", path, e); - } - } - } - error_code } -fn stat(path: &str, follow: bool) -> (FileTime, FileTime) { +fn stat(path: &Path, follow: bool) -> UResult<(FileTime, FileTime)> { let metadata = if follow { fs::symlink_metadata(path) } else { fs::metadata(path) - }; - - match metadata { - Ok(m) => ( - FileTime::from_last_access_time(&m), - FileTime::from_last_modification_time(&m), - ), - Err(_) => crash!( - 1, - "failed to get attributes of '{}': {}", - path, - Error::last_os_error() - ), } + .map_err_context(|| format!("failed to get attributes of '{}'", path.display()))?; + + Ok(( + FileTime::from_last_access_time(&metadata), + FileTime::from_last_modification_time(&metadata), + )) } -fn parse_date(str: &str) -> FileTime { +fn parse_date(str: &str) -> UResult { // This isn't actually compatible with GNU touch, but there doesn't seem to // be any simple specification for what format this parameter allows and I'm // not about to implement GNU parse_datetime. @@ -257,14 +225,22 @@ fn parse_date(str: &str) -> FileTime { let formats = vec!["%c", "%F"]; for f in formats { if let Ok(tm) = time::strptime(str, f) { - return local_tm_to_filetime(to_local(tm)); + return Ok(local_tm_to_filetime(to_local(tm))); } } - show_error!("Unable to parse date: {}\n", str); - process::exit(1); + + if let Ok(tm) = time::strptime(str, "@%s") { + // Don't convert to local time in this case - seconds since epoch are not time-zone dependent + return Ok(local_tm_to_filetime(tm)); + } + + Err(USimpleError::new( + 1, + format!("Unable to parse date: {}", str), + )) } -fn parse_timestamp(s: &str) -> FileTime { +fn parse_timestamp(s: &str) -> UResult { let now = time::now(); let (format, ts) = match s.chars().count() { 15 => ("%Y%m%d%H%M.%S", s.to_owned()), @@ -273,31 +249,28 @@ fn parse_timestamp(s: &str) -> FileTime { 10 => ("%y%m%d%H%M", s.to_owned()), 11 => ("%Y%m%d%H%M.%S", format!("{}{}", now.tm_year + 1900, s)), 8 => ("%Y%m%d%H%M", format!("{}{}", now.tm_year + 1900, s)), - _ => panic!("Unknown timestamp format"), + _ => return Err(USimpleError::new(1, format!("invalid date format '{}'", s))), }; - match time::strptime(&ts, format) { - Ok(tm) => { - let mut local = to_local(tm); - local.tm_isdst = -1; - let ft = local_tm_to_filetime(local); + let tm = time::strptime(&ts, format) + .map_err(|_| USimpleError::new(1, format!("invalid date format '{}'", s)))?; - // We have to check that ft is valid time. Due to daylight saving - // time switch, local time can jump from 1:59 AM to 3:00 AM, - // in which case any time between 2:00 AM and 2:59 AM is not valid. - // Convert back to local time and see if we got the same value back. - let ts = time::Timespec { - sec: ft.unix_seconds(), - nsec: 0, - }; - let tm2 = time::at(ts); - if tm.tm_hour != tm2.tm_hour { - show_error!("invalid date format {}", s); - process::exit(1); - } + let mut local = to_local(tm); + local.tm_isdst = -1; + let ft = local_tm_to_filetime(local); - ft - } - Err(e) => panic!("Unable to parse timestamp\n{}", e), + // We have to check that ft is valid time. Due to daylight saving + // time switch, local time can jump from 1:59 AM to 3:00 AM, + // in which case any time between 2:00 AM and 2:59 AM is not valid. + // Convert back to local time and see if we got the same value back. + let ts = time::Timespec { + sec: ft.unix_seconds(), + nsec: 0, + }; + let tm2 = time::at(ts); + if tm.tm_hour != tm2.tm_hour { + return Err(USimpleError::new(1, format!("invalid date format '{}'", s))); } + + Ok(ft) } diff --git a/src/uu/tr/Cargo.toml b/src/uu/tr/Cargo.toml index a3d066bfb..7783db144 100644 --- a/src/uu/tr/Cargo.toml +++ b/src/uu/tr/Cargo.toml @@ -17,7 +17,7 @@ path = "src/tr.rs" [dependencies] bit-set = "0.5.0" fnv = "1.0.5" -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/tr/src/expand.rs b/src/uu/tr/src/expand.rs index 7d0c61c30..5d960921e 100644 --- a/src/uu/tr/src/expand.rs +++ b/src/uu/tr/src/expand.rs @@ -22,14 +22,15 @@ use std::ops::RangeInclusive; /// character; octal escape sequences consume 1 to 3 octal digits. #[inline] fn parse_sequence(s: &str) -> (char, usize) { - let c = s.chars().next().expect("invalid escape: empty string"); + let mut s = s.chars(); + let c = s.next().expect("invalid escape: empty string"); if ('0'..='7').contains(&c) { let mut v = c.to_digit(8).unwrap(); let mut consumed = 1; let bits_per_digit = 3; - for c in s.chars().skip(1).take(2) { + for c in s.take(2) { match c.to_digit(8) { Some(c) => { v = (v << bits_per_digit) | c; diff --git a/src/uu/tr/src/tr.rs b/src/uu/tr/src/tr.rs index 3c362dcec..28ce70c22 100644 --- a/src/uu/tr/src/tr.rs +++ b/src/uu/tr/src/tr.rs @@ -249,46 +249,9 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); let after_help = get_long_usage(); - let matches = App::new(executable!()) - .version(crate_version!()) - .about(ABOUT) + let matches = uu_app() .usage(&usage[..]) .after_help(&after_help[..]) - .arg( - Arg::with_name(options::COMPLEMENT) - // .visible_short_alias('C') // TODO: requires clap "3.0.0-beta.2" - .short("c") - .long(options::COMPLEMENT) - .help("use the complement of SET1"), - ) - .arg( - Arg::with_name("C") // work around for `Arg::visible_short_alias` - .short("C") - .help("same as -c"), - ) - .arg( - Arg::with_name(options::DELETE) - .short("d") - .long(options::DELETE) - .help("delete characters in SET1, do not translate"), - ) - .arg( - Arg::with_name(options::SQUEEZE) - .long(options::SQUEEZE) - .short("s") - .help( - "replace each sequence of a repeated character that is - listed in the last specified SET, with a single occurrence - of that character", - ), - ) - .arg( - Arg::with_name(options::TRUNCATE) - .long(options::TRUNCATE) - .short("t") - .help("first truncate SET1 to length of SET2"), - ) - .arg(Arg::with_name(options::SETS).multiple(true)) .get_matches_from(args); let delete_flag = matches.is_present(options::DELETE); @@ -311,7 +274,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { if !(delete_flag || squeeze_flag) && sets.len() < 2 { show_error!( - "missing operand after ‘{}’\nTry `{} --help` for more information.", + "missing operand after '{}'\nTry `{} --help` for more information.", sets[0], executable!() ); @@ -358,3 +321,44 @@ pub fn uumain(args: impl uucore::Args) -> i32 { 0 } + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about(ABOUT) + .arg( + Arg::with_name(options::COMPLEMENT) + // .visible_short_alias('C') // TODO: requires clap "3.0.0-beta.2" + .short("c") + .long(options::COMPLEMENT) + .help("use the complement of SET1"), + ) + .arg( + Arg::with_name("C") // work around for `Arg::visible_short_alias` + .short("C") + .help("same as -c"), + ) + .arg( + Arg::with_name(options::DELETE) + .short("d") + .long(options::DELETE) + .help("delete characters in SET1, do not translate"), + ) + .arg( + Arg::with_name(options::SQUEEZE) + .long(options::SQUEEZE) + .short("s") + .help( + "replace each sequence of a repeated character that is + listed in the last specified SET, with a single occurrence + of that character", + ), + ) + .arg( + Arg::with_name(options::TRUNCATE) + .long(options::TRUNCATE) + .short("t") + .help("first truncate SET1 to length of SET2"), + ) + .arg(Arg::with_name(options::SETS).multiple(true)) +} diff --git a/src/uu/true/Cargo.toml b/src/uu/true/Cargo.toml index 9f13318fd..06e7c35ff 100644 --- a/src/uu/true/Cargo.toml +++ b/src/uu/true/Cargo.toml @@ -15,6 +15,7 @@ edition = "2018" path = "src/true.rs" [dependencies] +clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/true/src/true.rs b/src/uu/true/src/true.rs index 7cb23f621..ea53b0075 100644 --- a/src/uu/true/src/true.rs +++ b/src/uu/true/src/true.rs @@ -5,6 +5,14 @@ // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -pub fn uumain(_: impl uucore::Args) -> i32 { +use clap::App; +use uucore::executable; + +pub fn uumain(args: impl uucore::Args) -> i32 { + uu_app().get_matches_from(args); 0 } + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) +} diff --git a/src/uu/truncate/Cargo.toml b/src/uu/truncate/Cargo.toml index e2c0afadc..50d3dc4f3 100644 --- a/src/uu/truncate/Cargo.toml +++ b/src/uu/truncate/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/truncate.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/truncate/src/truncate.rs b/src/uu/truncate/src/truncate.rs index 8e785ad21..bb7aa61d4 100644 --- a/src/uu/truncate/src/truncate.rs +++ b/src/uu/truncate/src/truncate.rs @@ -11,19 +11,21 @@ extern crate uucore; use clap::{crate_version, App, Arg}; +use std::convert::TryFrom; use std::fs::{metadata, OpenOptions}; use std::io::ErrorKind; use std::path::Path; +use uucore::parse_size::{parse_size, ParseSizeError}; #[derive(Debug, Eq, PartialEq)] enum TruncateMode { - Absolute(u64), - Extend(u64), - Reduce(u64), - AtMost(u64), - AtLeast(u64), - RoundDown(u64), - RoundUp(u64), + Absolute(usize), + Extend(usize), + Reduce(usize), + AtMost(usize), + AtLeast(usize), + RoundDown(usize), + RoundUp(usize), } impl TruncateMode { @@ -38,7 +40,7 @@ impl TruncateMode { /// let fsize = 10; /// assert_eq!(mode.to_size(fsize), 15); /// ``` - fn to_size(&self, fsize: u64) -> u64 { + fn to_size(&self, fsize: usize) -> usize { match self { TruncateMode::Absolute(size) => *size, TruncateMode::Extend(size) => fsize + size, @@ -58,10 +60,9 @@ pub mod options { pub static NO_CREATE: &str = "no-create"; pub static REFERENCE: &str = "reference"; pub static SIZE: &str = "size"; + pub static ARG_FILES: &str = "files"; } -static ARG_FILES: &str = "files"; - fn get_usage() -> String { format!("{0} [OPTION]... [FILE]...", executable!()) } @@ -92,42 +93,13 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); let long_usage = get_long_usage(); - let matches = App::new(executable!()) - .version(crate_version!()) - .about(ABOUT) + let matches = uu_app() .usage(&usage[..]) .after_help(&long_usage[..]) - .arg( - Arg::with_name(options::IO_BLOCKS) - .short("o") - .long(options::IO_BLOCKS) - .help("treat SIZE as the number of I/O blocks of the file rather than bytes (NOT IMPLEMENTED)") - ) - .arg( - Arg::with_name(options::NO_CREATE) - .short("c") - .long(options::NO_CREATE) - .help("do not create files that do not exist") - ) - .arg( - Arg::with_name(options::REFERENCE) - .short("r") - .long(options::REFERENCE) - .help("base the size of each file on the size of RFILE") - .value_name("RFILE") - ) - .arg( - Arg::with_name(options::SIZE) - .short("s") - .long("size") - .help("set or adjust the size of each file according to SIZE, which is in bytes unless --io-blocks is specified") - .value_name("SIZE") - ) - .arg(Arg::with_name(ARG_FILES).multiple(true).takes_value(true).min_values(1)) .get_matches_from(args); let files: Vec = matches - .values_of(ARG_FILES) + .values_of(options::ARG_FILES) .map(|v| v.map(ToString::to_string).collect()) .unwrap_or_default(); @@ -149,8 +121,8 @@ pub fn uumain(args: impl uucore::Args) -> i32 { crash!( 1, "cannot stat '{}': No such file or directory", - reference.unwrap() - ); + reference.unwrap_or_else(|| "".to_string()) + ); // TODO: fix '--no-create' see test_reference and test_truncate_bytes_size } _ => crash!(1, "{}", e.to_string()), } @@ -160,6 +132,46 @@ pub fn uumain(args: impl uucore::Args) -> i32 { 0 } +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about(ABOUT) + .arg( + Arg::with_name(options::IO_BLOCKS) + .short("o") + .long(options::IO_BLOCKS) + .help("treat SIZE as the number of I/O blocks of the file rather than bytes (NOT IMPLEMENTED)") + ) + .arg( + Arg::with_name(options::NO_CREATE) + .short("c") + .long(options::NO_CREATE) + .help("do not create files that do not exist") + ) + .arg( + Arg::with_name(options::REFERENCE) + .short("r") + .long(options::REFERENCE) + .required_unless(options::SIZE) + .help("base the size of each file on the size of RFILE") + .value_name("RFILE") + ) + .arg( + Arg::with_name(options::SIZE) + .short("s") + .long(options::SIZE) + .required_unless(options::REFERENCE) + .help("set or adjust the size of each file according to SIZE, which is in bytes unless --io-blocks is specified") + .value_name("SIZE") + ) + .arg(Arg::with_name(options::ARG_FILES) + .value_name("FILE") + .multiple(true) + .takes_value(true) + .required(true) + .min_values(1)) +} + /// Truncate the named file to the specified size. /// /// If `create` is true, then the file will be created if it does not @@ -172,10 +184,10 @@ pub fn uumain(args: impl uucore::Args) -> i32 { /// /// 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: usize) -> std::io::Result<()> { let path = Path::new(filename); let f = OpenOptions::new().write(true).create(create).open(path)?; - f.set_len(size) + f.set_len(u64::try_from(size).unwrap()) } /// Truncate files to a size relative to a given file. @@ -202,13 +214,13 @@ fn truncate_reference_and_size( let mode = match parse_mode_and_size(size_string) { Ok(m) => match m { TruncateMode::Absolute(_) => { - crash!(1, "you must specify a relative ‘--size’ with ‘--reference’") + crash!(1, "you must specify a relative '--size' with '--reference'") } _ => m, }, - Err(_) => crash!(1, "Invalid number: ‘{}’", size_string), + Err(e) => crash!(1, "Invalid number: {}", e.to_string()), }; - let fsize = metadata(rfilename)?.len(); + let fsize = usize::try_from(metadata(rfilename)?.len()).unwrap(); let tsize = mode.to_size(fsize); for filename in &filenames { file_truncate(filename, create, tsize)?; @@ -232,7 +244,7 @@ fn truncate_reference_file_only( filenames: Vec, create: bool, ) -> std::io::Result<()> { - let tsize = metadata(rfilename)?.len(); + let tsize = usize::try_from(metadata(rfilename)?.len()).unwrap(); for filename in &filenames { file_truncate(filename, create, tsize)?; } @@ -261,10 +273,10 @@ fn truncate_size_only( ) -> std::io::Result<()> { let mode = match parse_mode_and_size(size_string) { Ok(m) => m, - Err(_) => crash!(1, "Invalid number: ‘{}’", size_string), + Err(e) => crash!(1, "Invalid number: {}", e.to_string()), }; for filename in &filenames { - let fsize = metadata(filename).map(|m| m.len()).unwrap_or(0); + let fsize = usize::try_from(metadata(filename)?.len()).unwrap(); let tsize = mode.to_size(fsize); file_truncate(filename, create, tsize)?; } @@ -290,7 +302,7 @@ fn truncate( } (Some(rfilename), None) => truncate_reference_file_only(&rfilename, filenames, create), (None, Some(size_string)) => truncate_size_only(&size_string, filenames, create), - (None, None) => crash!(1, "you must specify either --reference or --size"), + (None, None) => crash!(1, "you must specify either --reference or --size"), // this case cannot happen anymore because it's handled by clap } } @@ -317,117 +329,35 @@ fn is_modifier(c: char) -> bool { /// ```rust,ignore /// assert_eq!(parse_mode_and_size("+123"), (TruncateMode::Extend, 123)); /// ``` -fn parse_mode_and_size(size_string: &str) -> Result { +fn parse_mode_and_size(size_string: &str) -> Result { // Trim any whitespace. - let size_string = size_string.trim(); + let mut size_string = size_string.trim(); // Get the modifier character from the size string, if any. For // example, if the argument is "+123", then the modifier is '+'. - let c = size_string.chars().next().unwrap(); - let size_string = if is_modifier(c) { - &size_string[1..] + if let Some(c) = size_string.chars().next() { + if is_modifier(c) { + size_string = &size_string[1..]; + } + parse_size(size_string).map(match c { + '+' => TruncateMode::Extend, + '-' => TruncateMode::Reduce, + '<' => TruncateMode::AtMost, + '>' => TruncateMode::AtLeast, + '/' => TruncateMode::RoundDown, + '%' => TruncateMode::RoundUp, + _ => TruncateMode::Absolute, + }) } else { - size_string - }; - parse_size(size_string).map(match c { - '+' => TruncateMode::Extend, - '-' => TruncateMode::Reduce, - '<' => TruncateMode::AtMost, - '>' => TruncateMode::AtLeast, - '/' => TruncateMode::RoundDown, - '%' => TruncateMode::RoundUp, - _ => TruncateMode::Absolute, - }) -} - -/// Parse a size string into a number of bytes. -/// -/// A size string comprises an integer and an optional unit. The unit -/// may be K, M, G, T, P, E, Z, or Y (powers of 1024) or KB, MB, -/// etc. (powers of 1000). -/// -/// # Errors -/// -/// This function returns an error if the string does not begin with a -/// numeral, or if the unit is not one of the supported units described -/// in the preceding section. -/// -/// # Examples -/// -/// ```rust,ignore -/// assert_eq!(parse_size("123").unwrap(), 123); -/// assert_eq!(parse_size("123K").unwrap(), 123 * 1024); -/// assert_eq!(parse_size("123KB").unwrap(), 123 * 1000); -/// ``` -fn parse_size(size: &str) -> Result { - // Get the numeric part of the size argument. For example, if the - // argument is "123K", then the numeric part is "123". - let numeric_string: String = size.chars().take_while(|c| c.is_digit(10)).collect(); - let number: u64 = match numeric_string.parse() { - Ok(n) => n, - Err(_) => return Err(()), - }; - - // Get the alphabetic units part of the size argument and compute - // the factor it represents. For example, if the argument is "123K", - // then the unit part is "K" and the factor is 1024. This may be the - // empty string, in which case, the factor is 1. - let n = numeric_string.len(); - let (base, exponent): (u64, u32) = match &size[n..] { - "" => (1, 0), - "K" | "k" => (1024, 1), - "M" | "m" => (1024, 2), - "G" | "g" => (1024, 3), - "T" | "t" => (1024, 4), - "P" | "p" => (1024, 5), - "E" | "e" => (1024, 6), - "Z" | "z" => (1024, 7), - "Y" | "y" => (1024, 8), - "KB" | "kB" => (1000, 1), - "MB" | "mB" => (1000, 2), - "GB" | "gB" => (1000, 3), - "TB" | "tB" => (1000, 4), - "PB" | "pB" => (1000, 5), - "EB" | "eB" => (1000, 6), - "ZB" | "zB" => (1000, 7), - "YB" | "yB" => (1000, 8), - _ => return Err(()), - }; - let factor = base.pow(exponent); - Ok(number * factor) + Err(ParseSizeError::ParseFailure(size_string.to_string())) + } } #[cfg(test)] mod tests { use crate::parse_mode_and_size; - use crate::parse_size; use crate::TruncateMode; - #[test] - fn test_parse_size_zero() { - assert_eq!(parse_size("0").unwrap(), 0); - assert_eq!(parse_size("0K").unwrap(), 0); - assert_eq!(parse_size("0KB").unwrap(), 0); - } - - #[test] - fn test_parse_size_without_factor() { - assert_eq!(parse_size("123").unwrap(), 123); - } - - #[test] - fn test_parse_size_kilobytes() { - assert_eq!(parse_size("123K").unwrap(), 123 * 1024); - assert_eq!(parse_size("123KB").unwrap(), 123 * 1000); - } - - #[test] - fn test_parse_size_megabytes() { - assert_eq!(parse_size("123").unwrap(), 123); - assert_eq!(parse_size("123M").unwrap(), 123 * 1024 * 1024); - assert_eq!(parse_size("123MB").unwrap(), 123 * 1000 * 1000); - } - #[test] fn test_parse_mode_and_size() { assert_eq!(parse_mode_and_size("10"), Ok(TruncateMode::Absolute(10))); diff --git a/src/uu/tsort/src/tsort.rs b/src/uu/tsort/src/tsort.rs index 8bd6dabef..0a323f837 100644 --- a/src/uu/tsort/src/tsort.rs +++ b/src/uu/tsort/src/tsort.rs @@ -30,16 +30,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .collect_str(InvalidEncodingHandling::ConvertLossy) .accept_any(); - let matches = App::new(executable!()) - .version(crate_version!()) - .usage(USAGE) - .about(SUMMARY) - .arg( - Arg::with_name(options::FILE) - .default_value("-") - .hidden(true), - ) - .get_matches_from(args); + let matches = uu_app().get_matches_from(args); let input = matches .value_of(options::FILE) @@ -98,6 +89,18 @@ pub fn uumain(args: impl uucore::Args) -> i32 { 0 } +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .usage(USAGE) + .about(SUMMARY) + .arg( + Arg::with_name(options::FILE) + .default_value("-") + .hidden(true), + ) +} + // We use String as a representation of node here // but using integer may improve performance. struct Graph { diff --git a/src/uu/tty/Cargo.toml b/src/uu/tty/Cargo.toml index 7be27a900..90396ff40 100644 --- a/src/uu/tty/Cargo.toml +++ b/src/uu/tty/Cargo.toml @@ -15,8 +15,9 @@ edition = "2018" path = "src/tty.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } libc = "0.2.42" +atty = "0.2" uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["fs"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/tty/src/tty.rs b/src/uu/tty/src/tty.rs index 074bcf182..7412cdf45 100644 --- a/src/uu/tty/src/tty.rs +++ b/src/uu/tty/src/tty.rs @@ -14,7 +14,7 @@ extern crate uucore; use clap::{crate_version, App, Arg}; use std::ffi::CStr; -use uucore::fs::is_stdin_interactive; +use std::io::Write; use uucore::InvalidEncodingHandling; static ABOUT: &str = "Print the file name of the terminal connected to standard input."; @@ -33,19 +33,15 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .collect_str(InvalidEncodingHandling::ConvertLossy) .accept_any(); - let matches = App::new(executable!()) - .version(crate_version!()) - .about(ABOUT) - .usage(&usage[..]) - .arg( - Arg::with_name(options::SILENT) - .long(options::SILENT) - .visible_alias("quiet") - .short("s") - .help("print nothing, only return an exit status") - .required(false), - ) - .get_matches_from(args); + let matches = uu_app().usage(&usage[..]).get_matches_from_safe(args); + + let matches = match matches { + Ok(m) => m, + Err(e) => { + eprint!("{}", e); + return 2; + } + }; let silent = matches.is_present(options::SILENT); @@ -59,17 +55,38 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } }; + let mut stdout = std::io::stdout(); + if !silent { - if !tty.chars().all(|c| c.is_whitespace()) { - println!("{}", tty); + let write_result = if !tty.chars().all(|c| c.is_whitespace()) { + writeln!(stdout, "{}", tty) } else { - println!("not a tty"); + writeln!(stdout, "not a tty") + }; + if write_result.is_err() || stdout.flush().is_err() { + // Don't return to prevent a panic later when another flush is attempted + // because the `uucore_procs::main` macro inserts a flush after execution for every utility. + std::process::exit(3); } } - if is_stdin_interactive() { + if atty::is(atty::Stream::Stdin) { libc::EXIT_SUCCESS } else { libc::EXIT_FAILURE } } + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about(ABOUT) + .arg( + Arg::with_name(options::SILENT) + .long(options::SILENT) + .visible_alias("quiet") + .short("s") + .help("print nothing, only return an exit status") + .required(false), + ) +} diff --git a/src/uu/uname/Cargo.toml b/src/uu/uname/Cargo.toml index 9707d8444..54a1591a2 100644 --- a/src/uu/uname/Cargo.toml +++ b/src/uu/uname/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/uname.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } platform-info = "0.1" uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/uname/src/uname.rs b/src/uu/uname/src/uname.rs index aa591ee18..dda859722 100644 --- a/src/uu/uname/src/uname.rs +++ b/src/uu/uname/src/uname.rs @@ -47,49 +47,7 @@ const HOST_OS: &str = "Redox"; pub fn uumain(args: impl uucore::Args) -> i32 { let usage = format!("{} [OPTION]...", executable!()); - let matches = App::new(executable!()) - .version(crate_version!()) - .about(ABOUT) - .usage(&usage[..]) - .arg(Arg::with_name(options::ALL) - .short("a") - .long(options::ALL) - .help("Behave as though all of the options -mnrsv were specified.")) - .arg(Arg::with_name(options::KERNELNAME) - .short("s") - .long(options::KERNELNAME) - .alias("sysname") // Obsolescent option in GNU uname - .help("print the kernel name.")) - .arg(Arg::with_name(options::NODENAME) - .short("n") - .long(options::NODENAME) - .help("print the nodename (the nodename may be a name that the system is known by to a communications network).")) - .arg(Arg::with_name(options::KERNELRELEASE) - .short("r") - .long(options::KERNELRELEASE) - .alias("release") // Obsolescent option in GNU uname - .help("print the operating system release.")) - .arg(Arg::with_name(options::KERNELVERSION) - .short("v") - .long(options::KERNELVERSION) - .help("print the operating system version.")) - .arg(Arg::with_name(options::HWPLATFORM) - .short("i") - .long(options::HWPLATFORM) - .help("print the hardware platform (non-portable)")) - .arg(Arg::with_name(options::MACHINE) - .short("m") - .long(options::MACHINE) - .help("print the machine hardware name.")) - .arg(Arg::with_name(options::PROCESSOR) - .short("p") - .long(options::PROCESSOR) - .help("print the processor type (non-portable)")) - .arg(Arg::with_name(options::OS) - .short("o") - .long(options::OS) - .help("print the operating system name.")) - .get_matches_from(args); + let matches = uu_app().usage(&usage[..]).get_matches_from(args); let uname = return_if_err!(1, PlatformInfo::new()); let mut output = String::new(); @@ -155,3 +113,47 @@ pub fn uumain(args: impl uucore::Args) -> i32 { 0 } + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about(ABOUT) + .arg(Arg::with_name(options::ALL) + .short("a") + .long(options::ALL) + .help("Behave as though all of the options -mnrsv were specified.")) + .arg(Arg::with_name(options::KERNELNAME) + .short("s") + .long(options::KERNELNAME) + .alias("sysname") // Obsolescent option in GNU uname + .help("print the kernel name.")) + .arg(Arg::with_name(options::NODENAME) + .short("n") + .long(options::NODENAME) + .help("print the nodename (the nodename may be a name that the system is known by to a communications network).")) + .arg(Arg::with_name(options::KERNELRELEASE) + .short("r") + .long(options::KERNELRELEASE) + .alias("release") // Obsolescent option in GNU uname + .help("print the operating system release.")) + .arg(Arg::with_name(options::KERNELVERSION) + .short("v") + .long(options::KERNELVERSION) + .help("print the operating system version.")) + .arg(Arg::with_name(options::HWPLATFORM) + .short("i") + .long(options::HWPLATFORM) + .help("print the hardware platform (non-portable)")) + .arg(Arg::with_name(options::MACHINE) + .short("m") + .long(options::MACHINE) + .help("print the machine hardware name.")) + .arg(Arg::with_name(options::PROCESSOR) + .short("p") + .long(options::PROCESSOR) + .help("print the processor type (non-portable)")) + .arg(Arg::with_name(options::OS) + .short("o") + .long(options::OS) + .help("print the operating system name.")) +} diff --git a/src/uu/unexpand/Cargo.toml b/src/uu/unexpand/Cargo.toml index e39dd87ca..5e47d8b58 100644 --- a/src/uu/unexpand/Cargo.toml +++ b/src/uu/unexpand/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/unexpand.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } unicode-width = "0.1.5" uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/unexpand/src/unexpand.rs b/src/uu/unexpand/src/unexpand.rs index 92b3c7520..50e3f186d 100644 --- a/src/uu/unexpand/src/unexpand.rs +++ b/src/uu/unexpand/src/unexpand.rs @@ -94,7 +94,15 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .collect_str(InvalidEncodingHandling::Ignore) .accept_any(); - let matches = App::new(executable!()) + let matches = uu_app().get_matches_from(args); + + unexpand(Options::new(matches)); + + 0 +} + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) .name(NAME) .version(crate_version!()) .usage(USAGE) @@ -126,11 +134,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .long(options::NO_UTF8) .takes_value(false) .help("interpret input file as 8-bit ASCII rather than UTF-8")) - .get_matches_from(args); - - unexpand(Options::new(matches)); - - 0 } fn open(path: String) -> BufReader> { diff --git a/src/uu/uniq/Cargo.toml b/src/uu/uniq/Cargo.toml index 3fe89b450..be082fe88 100644 --- a/src/uu/uniq/Cargo.toml +++ b/src/uu/uniq/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/uniq.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } strum = "0.20" strum_macros = "0.20" uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } diff --git a/src/uu/uniq/src/uniq.rs b/src/uu/uniq/src/uniq.rs index aee024dd4..20639c850 100644 --- a/src/uu/uniq/src/uniq.rs +++ b/src/uu/uniq/src/uniq.rs @@ -238,11 +238,52 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); let long_usage = get_long_usage(); - let matches = App::new(executable!()) - .version(crate_version!()) - .about(ABOUT) + let matches = uu_app() .usage(&usage[..]) .after_help(&long_usage[..]) + .get_matches_from(args); + + let files: Vec = matches + .values_of(ARG_FILES) + .map(|v| v.map(ToString::to_string).collect()) + .unwrap_or_default(); + + let (in_file_name, out_file_name) = match files.len() { + 0 => ("-".to_owned(), "-".to_owned()), + 1 => (files[0].clone(), "-".to_owned()), + 2 => (files[0].clone(), files[1].clone()), + _ => { + // Cannot happen as clap will fail earlier + crash!(1, "Extra operand: {}", files[2]); + } + }; + + let uniq = Uniq { + repeats_only: matches.is_present(options::REPEATED) + || matches.is_present(options::ALL_REPEATED), + uniques_only: matches.is_present(options::UNIQUE), + all_repeated: matches.is_present(options::ALL_REPEATED) + || matches.is_present(options::GROUP), + delimiters: get_delimiter(&matches), + show_counts: matches.is_present(options::COUNT), + skip_fields: opt_parsed(options::SKIP_FIELDS, &matches), + slice_start: opt_parsed(options::SKIP_CHARS, &matches), + slice_stop: opt_parsed(options::CHECK_CHARS, &matches), + ignore_case: matches.is_present(options::IGNORE_CASE), + zero_terminated: matches.is_present(options::ZERO_TERMINATED), + }; + uniq.print_uniq( + &mut open_input_file(in_file_name), + &mut open_output_file(out_file_name), + ); + + 0 +} + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about(ABOUT) .arg( Arg::with_name(options::ALL_REPEATED) .short("D") @@ -329,43 +370,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .takes_value(true) .max_values(2), ) - .get_matches_from(args); - - let files: Vec = matches - .values_of(ARG_FILES) - .map(|v| v.map(ToString::to_string).collect()) - .unwrap_or_default(); - - let (in_file_name, out_file_name) = match files.len() { - 0 => ("-".to_owned(), "-".to_owned()), - 1 => (files[0].clone(), "-".to_owned()), - 2 => (files[0].clone(), files[1].clone()), - _ => { - // Cannot happen as clap will fail earlier - crash!(1, "Extra operand: {}", files[2]); - } - }; - - let uniq = Uniq { - repeats_only: matches.is_present(options::REPEATED) - || matches.is_present(options::ALL_REPEATED), - uniques_only: matches.is_present(options::UNIQUE), - all_repeated: matches.is_present(options::ALL_REPEATED) - || matches.is_present(options::GROUP), - delimiters: get_delimiter(&matches), - show_counts: matches.is_present(options::COUNT), - skip_fields: opt_parsed(options::SKIP_FIELDS, &matches), - slice_start: opt_parsed(options::SKIP_CHARS, &matches), - slice_stop: opt_parsed(options::CHECK_CHARS, &matches), - ignore_case: matches.is_present(options::IGNORE_CASE), - zero_terminated: matches.is_present(options::ZERO_TERMINATED), - }; - uniq.print_uniq( - &mut open_input_file(in_file_name), - &mut open_output_file(out_file_name), - ); - - 0 } fn get_delimiter(matches: &ArgMatches) -> Delimiters { diff --git a/src/uu/unlink/Cargo.toml b/src/uu/unlink/Cargo.toml index 08da2624e..ef0f291f8 100644 --- a/src/uu/unlink/Cargo.toml +++ b/src/uu/unlink/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/unlink.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } libc = "0.2.42" uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/unlink/src/unlink.rs b/src/uu/unlink/src/unlink.rs index 343f2653f..49f17cb12 100644 --- a/src/uu/unlink/src/unlink.rs +++ b/src/uu/unlink/src/unlink.rs @@ -33,12 +33,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); - let matches = App::new(executable!()) - .version(crate_version!()) - .about(ABOUT) - .usage(&usage[..]) - .arg(Arg::with_name(OPT_PATH).hidden(true).multiple(true)) - .get_matches_from(args); + let matches = uu_app().usage(&usage[..]).get_matches_from(args); let paths: Vec = matches .values_of(OPT_PATH) @@ -98,3 +93,10 @@ pub fn uumain(args: impl uucore::Args) -> i32 { 0 } + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about(ABOUT) + .arg(Arg::with_name(OPT_PATH).hidden(true).multiple(true)) +} diff --git a/src/uu/uptime/Cargo.toml b/src/uu/uptime/Cargo.toml index 1136e6420..eec745ab1 100644 --- a/src/uu/uptime/Cargo.toml +++ b/src/uu/uptime/Cargo.toml @@ -16,7 +16,7 @@ path = "src/uptime.rs" [dependencies] chrono = "0.4" -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["libc", "utmpx"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/uptime/src/uptime.rs b/src/uu/uptime/src/uptime.rs index 3683a4de0..35270093c 100644 --- a/src/uu/uptime/src/uptime.rs +++ b/src/uu/uptime/src/uptime.rs @@ -38,17 +38,7 @@ fn get_usage() -> String { pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); - let matches = App::new(executable!()) - .version(crate_version!()) - .about(ABOUT) - .usage(&usage[..]) - .arg( - Arg::with_name(options::SINCE) - .short("s") - .long(options::SINCE) - .help("system up since"), - ) - .get_matches_from(args); + let matches = uu_app().usage(&usage[..]).get_matches_from(args); let (boot_time, user_count) = process_utmpx(); let uptime = get_uptime(boot_time); @@ -73,6 +63,18 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } } +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about(ABOUT) + .arg( + Arg::with_name(options::SINCE) + .short("s") + .long(options::SINCE) + .help("system up since"), + ) +} + #[cfg(unix)] fn print_loadavg() { use uucore::libc::c_double; diff --git a/src/uu/users/Cargo.toml b/src/uu/users/Cargo.toml index 84da13020..6cafd7c32 100644 --- a/src/uu/users/Cargo.toml +++ b/src/uu/users/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/users.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["utmpx"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/users/src/users.rs b/src/uu/users/src/users.rs index 5b1f1c037..ef878497c 100644 --- a/src/uu/users/src/users.rs +++ b/src/uu/users/src/users.rs @@ -34,12 +34,9 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); let after_help = get_long_usage(); - let matches = App::new(executable!()) - .version(crate_version!()) - .about(ABOUT) + let matches = uu_app() .usage(&usage[..]) .after_help(&after_help[..]) - .arg(Arg::with_name(ARG_FILES).takes_value(true).max_values(1)) .get_matches_from(args); let files: Vec = matches @@ -66,3 +63,10 @@ pub fn uumain(args: impl uucore::Args) -> i32 { 0 } + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about(ABOUT) + .arg(Arg::with_name(ARG_FILES).takes_value(true).max_values(1)) +} diff --git a/src/uu/wc/Cargo.toml b/src/uu/wc/Cargo.toml index 8ae79dc08..ad4301e7a 100644 --- a/src/uu/wc/Cargo.toml +++ b/src/uu/wc/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/wc.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } thiserror = "1.0" diff --git a/src/uu/wc/src/wc.rs b/src/uu/wc/src/wc.rs index 031c25739..0bcc66664 100644 --- a/src/uu/wc/src/wc.rs +++ b/src/uu/wc/src/wc.rs @@ -134,10 +134,39 @@ impl Input { pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); - let matches = App::new(executable!()) + let matches = uu_app().usage(&usage[..]).get_matches_from(args); + + let mut inputs: Vec = matches + .values_of(ARG_FILES) + .map(|v| { + v.map(|i| { + if i == "-" { + Input::Stdin(StdinKind::Explicit) + } else { + Input::Path(ToString::to_string(i)) + } + }) + .collect() + }) + .unwrap_or_default(); + + if inputs.is_empty() { + inputs.push(Input::Stdin(StdinKind::Implicit)); + } + + let settings = Settings::new(&matches); + + if wc(inputs, &settings).is_ok() { + 0 + } else { + 1 + } +} + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) .version(crate_version!()) .about(ABOUT) - .usage(&usage[..]) .arg( Arg::with_name(options::BYTES) .short("c") @@ -169,33 +198,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .help("print the word counts"), ) .arg(Arg::with_name(ARG_FILES).multiple(true).takes_value(true)) - .get_matches_from(args); - - let mut inputs: Vec = matches - .values_of(ARG_FILES) - .map(|v| { - v.map(|i| { - if i == "-" { - Input::Stdin(StdinKind::Explicit) - } else { - Input::Path(ToString::to_string(i)) - } - }) - .collect() - }) - .unwrap_or_default(); - - if inputs.is_empty() { - inputs.push(Input::Stdin(StdinKind::Implicit)); - } - - let settings = Settings::new(&matches); - - if wc(inputs, &settings).is_ok() { - 0 - } else { - 1 - } } fn word_count_from_reader( @@ -374,8 +376,8 @@ fn wc(inputs: Vec, settings: &Settings) -> Result<(), u32> { let num_inputs = inputs.len(); for input in &inputs { - let word_count = word_count_from_input(&input, settings).unwrap_or_else(|err| { - show_error(&input, err); + let word_count = word_count_from_input(input, settings).unwrap_or_else(|err| { + show_error(input, err); error_count += 1; WordCount::default() }); diff --git a/src/uu/who/Cargo.toml b/src/uu/who/Cargo.toml index 4d8eccb45..06388c7bf 100644 --- a/src/uu/who/Cargo.toml +++ b/src/uu/who/Cargo.toml @@ -17,7 +17,7 @@ path = "src/who.rs" [dependencies] uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["utmpx"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } -clap = "2.33.3" +clap = { version = "2.33", features = ["wrap_help"] } [[bin]] name = "who" diff --git a/src/uu/who/src/who.rs b/src/uu/who/src/who.rs index d2f64aa94..6a9c88710 100644 --- a/src/uu/who/src/who.rs +++ b/src/uu/who/src/who.rs @@ -64,11 +64,105 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); let after_help = get_long_usage(); - let matches = App::new(executable!()) - .version(crate_version!()) - .about(ABOUT) + let matches = uu_app() .usage(&usage[..]) .after_help(&after_help[..]) + .get_matches_from(args); + + let files: Vec = matches + .values_of(options::FILE) + .map(|v| v.map(ToString::to_string).collect()) + .unwrap_or_default(); + + // If true, attempt to canonicalize hostnames via a DNS lookup. + let do_lookup = matches.is_present(options::LOOKUP); + + // If true, display only a list of usernames and count of + // the users logged on. + // Ignored for 'who am i'. + let short_list = matches.is_present(options::COUNT); + + let all = matches.is_present(options::ALL); + + // If true, display a line at the top describing each field. + let include_heading = matches.is_present(options::HEADING); + + // If true, display a '+' for each user if mesg y, a '-' if mesg n, + // or a '?' if their tty cannot be statted. + let include_mesg = all || matches.is_present(options::MESG) || matches.is_present("w"); + + // If true, display the last boot time. + let need_boottime = all || matches.is_present(options::BOOT); + + // If true, display dead processes. + let need_deadprocs = all || matches.is_present(options::DEAD); + + // If true, display processes waiting for user login. + let need_login = all || matches.is_present(options::LOGIN); + + // If true, display processes started by init. + let need_initspawn = all || matches.is_present(options::PROCESS); + + // If true, display the last clock change. + let need_clockchange = all || matches.is_present(options::TIME); + + // If true, display the current runlevel. + let need_runlevel = all || matches.is_present(options::RUNLEVEL); + + let use_defaults = !(all + || need_boottime + || need_deadprocs + || need_login + || need_initspawn + || need_runlevel + || need_clockchange + || matches.is_present(options::USERS)); + + // If true, display user processes. + let need_users = all || matches.is_present(options::USERS) || use_defaults; + + // If true, display the hours:minutes since each user has touched + // the keyboard, or "." if within the last minute, or "old" if + // not within the last day. + let include_idle = need_deadprocs || need_login || need_runlevel || need_users; + + // If true, display process termination & exit status. + let include_exit = need_deadprocs; + + // If true, display only name, line, and time fields. + let short_output = !include_exit && use_defaults; + + // If true, display info only for the controlling tty. + let my_line_only = matches.is_present(options::ONLY_HOSTNAME_USER) || files.len() == 2; + + let mut who = Who { + do_lookup, + short_list, + short_output, + include_idle, + include_heading, + include_mesg, + include_exit, + need_boottime, + need_deadprocs, + need_login, + need_initspawn, + need_clockchange, + need_runlevel, + need_users, + my_line_only, + args: files, + }; + + who.exec(); + + 0 +} + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about(ABOUT) .arg( Arg::with_name(options::ALL) .long(options::ALL) @@ -164,162 +258,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .min_values(1) .max_values(2), ) - .get_matches_from(args); - - let files: Vec = matches - .values_of(options::FILE) - .map(|v| v.map(ToString::to_string).collect()) - .unwrap_or_default(); - - // If true, attempt to canonicalize hostnames via a DNS lookup. - let do_lookup = matches.is_present(options::LOOKUP); - - // If true, display only a list of usernames and count of - // the users logged on. - // Ignored for 'who am i'. - let short_list = matches.is_present(options::COUNT); - - // If true, display only name, line, and time fields. - let mut short_output = false; - - // If true, display the hours:minutes since each user has touched - // the keyboard, or "." if within the last minute, or "old" if - // not within the last day. - let mut include_idle = false; - - // If true, display a line at the top describing each field. - let include_heading = matches.is_present(options::HEADING); - - // If true, display a '+' for each user if mesg y, a '-' if mesg n, - // or a '?' if their tty cannot be statted. - let include_mesg = matches.is_present(options::ALL) - || matches.is_present(options::MESG) - || matches.is_present("w"); - - // If true, display process termination & exit status. - let mut include_exit = false; - - // If true, display the last boot time. - let mut need_boottime = false; - - // If true, display dead processes. - let mut need_deadprocs = false; - - // If true, display processes waiting for user login. - let mut need_login = false; - - // If true, display processes started by init. - let mut need_initspawn = false; - - // If true, display the last clock change. - let mut need_clockchange = false; - - // If true, display the current runlevel. - let mut need_runlevel = false; - - // If true, display user processes. - let mut need_users = false; - - // If true, display info only for the controlling tty. - let mut my_line_only = false; - - let mut assumptions = true; - - #[allow(clippy::useless_let_if_seq)] - { - if matches.is_present(options::ALL) { - need_boottime = true; - need_deadprocs = true; - need_login = true; - need_initspawn = true; - need_runlevel = true; - need_clockchange = true; - need_users = true; - include_idle = true; - include_exit = true; - assumptions = false; - } - - if matches.is_present(options::BOOT) { - need_boottime = true; - assumptions = false; - } - - if matches.is_present(options::DEAD) { - need_deadprocs = true; - include_idle = true; - include_exit = true; - assumptions = false; - } - - if matches.is_present(options::LOGIN) { - need_login = true; - include_idle = true; - assumptions = false; - } - - if matches.is_present(options::ONLY_HOSTNAME_USER) || files.len() == 2 { - my_line_only = true; - } - - if matches.is_present(options::PROCESS) { - need_initspawn = true; - assumptions = false; - } - - if matches.is_present(options::RUNLEVEL) { - need_runlevel = true; - include_idle = true; - assumptions = false; - } - - if matches.is_present(options::SHORT) { - short_output = true; - } - - if matches.is_present(options::TIME) { - need_clockchange = true; - assumptions = false; - } - - if matches.is_present(options::USERS) { - need_users = true; - include_idle = true; - assumptions = false; - } - - if assumptions { - need_users = true; - short_output = true; - } - - if include_exit { - short_output = false; - } - } - - let mut who = Who { - do_lookup, - short_list, - short_output, - include_idle, - include_heading, - include_mesg, - include_exit, - need_boottime, - need_deadprocs, - need_login, - need_initspawn, - need_clockchange, - need_runlevel, - need_users, - my_line_only, - args: files, - }; - - who.exec(); - - 0 } struct Who { @@ -366,7 +304,7 @@ fn idle_string<'a>(when: i64, boottime: i64) -> Cow<'a, str> { } fn time_string(ut: &Utmpx) -> String { - time::strftime("%Y-%m-%d %H:%M", &ut.login_time()).unwrap() + time::strftime("%b %e %H:%M", &ut.login_time()).unwrap() // LC_ALL=C } #[inline] @@ -589,8 +527,8 @@ impl Who { buf.push_str(&msg); } buf.push_str(&format!(" {:<12}", line)); - // "%Y-%m-%d %H:%M" - let time_size = 4 + 1 + 2 + 1 + 2 + 1 + 2 + 1 + 2; + // "%b %e %H:%M" (LC_ALL=C) + let time_size = 3 + 2 + 2 + 1 + 2; buf.push_str(&format!(" {:<1$}", time, time_size)); if !self.short_output { diff --git a/src/uu/whoami/Cargo.toml b/src/uu/whoami/Cargo.toml index f8dc01440..a7fc19848 100644 --- a/src/uu/whoami/Cargo.toml +++ b/src/uu/whoami/Cargo.toml @@ -15,12 +15,11 @@ edition = "2018" path = "src/whoami.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["entries", "wide"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [target.'cfg(target_os = "windows")'.dependencies] -advapi32-sys = "0.2.0" winapi = { version = "0.3", features = ["lmcons"] } [[bin]] diff --git a/src/uu/whoami/src/platform/windows.rs b/src/uu/whoami/src/platform/windows.rs index 5d648877b..3fe8eb1e7 100644 --- a/src/uu/whoami/src/platform/windows.rs +++ b/src/uu/whoami/src/platform/windows.rs @@ -11,7 +11,7 @@ extern crate winapi; use self::winapi::shared::lmcons; use self::winapi::shared::minwindef; -use self::winapi::um::winnt; +use self::winapi::um::{winbase, winnt}; use std::io::{Error, Result}; use std::mem; use uucore::wide::FromWide; @@ -20,7 +20,7 @@ pub unsafe fn get_username() -> Result { #[allow(deprecated)] let mut buffer: [winnt::WCHAR; lmcons::UNLEN as usize + 1] = mem::uninitialized(); let mut len = buffer.len() as minwindef::DWORD; - if advapi32::GetUserNameW(buffer.as_mut_ptr(), &mut len) == 0 { + if winbase::GetUserNameW(buffer.as_mut_ptr(), &mut len) == 0 { return Err(Error::last_os_error()); } let username = String::from_wide(&buffer[..len as usize - 1]); diff --git a/src/uu/whoami/src/whoami.rs b/src/uu/whoami/src/whoami.rs index 383fb40b5..bd2eea1e3 100644 --- a/src/uu/whoami/src/whoami.rs +++ b/src/uu/whoami/src/whoami.rs @@ -1,3 +1,5 @@ +use clap::App; + // * This file is part of the uutils coreutils package. // * // * (c) Jordi Boggiano @@ -15,7 +17,7 @@ extern crate uucore; mod platform; pub fn uumain(args: impl uucore::Args) -> i32 { - let app = app_from_crate!(); + let app = uu_app(); if let Err(err) = app.get_matches_from_safe(args) { if err.kind == clap::ErrorKind::HelpDisplayed @@ -34,6 +36,10 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } } +pub fn uu_app() -> App<'static, 'static> { + app_from_crate!() +} + pub fn exec() { unsafe { match platform::get_username() { diff --git a/src/uu/yes/Cargo.toml b/src/uu/yes/Cargo.toml index 4a843ddd8..0338a4037 100644 --- a/src/uu/yes/Cargo.toml +++ b/src/uu/yes/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/yes.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["zero-copy"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/yes/src/yes.rs b/src/uu/yes/src/yes.rs index 1fc2d92bc..2c0d43000 100644 --- a/src/uu/yes/src/yes.rs +++ b/src/uu/yes/src/yes.rs @@ -12,7 +12,7 @@ extern crate clap; #[macro_use] extern crate uucore; -use clap::Arg; +use clap::{App, Arg}; use std::borrow::Cow; use std::io::{self, Write}; use uucore::zero_copy::ZeroCopyWriter; @@ -22,7 +22,7 @@ use uucore::zero_copy::ZeroCopyWriter; const BUF_SIZE: usize = 16 * 1024; pub fn uumain(args: impl uucore::Args) -> i32 { - let app = app_from_crate!().arg(Arg::with_name("STRING").index(1).multiple(true)); + let app = uu_app(); let matches = match app.get_matches_from_safe(args) { Ok(m) => m, @@ -56,6 +56,10 @@ pub fn uumain(args: impl uucore::Args) -> i32 { 0 } +pub fn uu_app() -> App<'static, 'static> { + app_from_crate!().arg(Arg::with_name("STRING").index(1).multiple(true)) +} + #[cfg(not(feature = "latency"))] fn prepare_buffer<'a>(input: &'a str, buffer: &'a mut [u8; BUF_SIZE]) -> &'a [u8] { if input.len() < BUF_SIZE / 2 { diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index 482252680..0c11d2c15 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -44,7 +44,6 @@ entries = ["libc"] fs = ["libc"] fsext = ["libc", "time"] mode = ["libc"] -parse_time = [] perms = ["libc"] process = ["libc"] ringbuffer = [] diff --git a/src/uucore/src/lib/features.rs b/src/uucore/src/lib/features.rs index 310a41fe1..c1e1ec31e 100644 --- a/src/uucore/src/lib/features.rs +++ b/src/uucore/src/lib/features.rs @@ -6,8 +6,6 @@ pub mod encoding; pub mod fs; #[cfg(feature = "fsext")] pub mod fsext; -#[cfg(feature = "parse_time")] -pub mod parse_time; #[cfg(feature = "ringbuffer")] pub mod ringbuffer; #[cfg(feature = "zero-copy")] diff --git a/src/uucore/src/lib/features/entries.rs b/src/uucore/src/lib/features/entries.rs index d2dce2461..6b986e616 100644 --- a/src/uucore/src/lib/features/entries.rs +++ b/src/uucore/src/lib/features/entries.rs @@ -5,7 +5,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (vars) Passwd cstr fnam gecos ngroups +// spell-checker:ignore (vars) Passwd cstr fnam gecos ngroups egid //! Get password/group file entry //! @@ -47,6 +47,9 @@ use std::io::Result as IOResult; use std::ptr; extern "C" { + /// From: https://man7.org/linux/man-pages/man3/getgrouplist.3.html + /// > The getgrouplist() function scans the group database to obtain + /// > the list of groups that user belongs to. fn getgrouplist( name: *const c_char, gid: gid_t, @@ -55,6 +58,13 @@ extern "C" { ) -> c_int; } +/// From: https://man7.org/linux/man-pages/man2/getgroups.2.html +/// > getgroups() returns the supplementary group IDs of the calling +/// > process in list. +/// > If size is zero, list is not modified, but the total number of +/// > supplementary group IDs for the process is returned. This allows +/// > the caller to determine the size of a dynamically allocated list +/// > to be used in a further call to getgroups(). pub fn get_groups() -> IOResult> { let ngroups = unsafe { getgroups(0, ptr::null_mut()) }; if ngroups == -1 { @@ -72,6 +82,46 @@ pub fn get_groups() -> IOResult> { } } +/// The list of group IDs returned from GNU's `groups` and GNU's `id --groups` +/// starts with the effective group ID (egid). +/// This is a wrapper for `get_groups()` to mimic this behavior. +/// +/// If `arg_id` is `None` (default), `get_groups_gnu` moves the effective +/// group id (egid) to the first entry in the returned Vector. +/// If `arg_id` is `Some(x)`, `get_groups_gnu` moves the id with value `x` +/// to the first entry in the returned Vector. This might be necessary +/// for `id --groups --real` if `gid` and `egid` are not equal. +/// +/// From: https://www.man7.org/linux/man-pages/man3/getgroups.3p.html +/// > As implied by the definition of supplementary groups, the +/// > effective group ID may appear in the array returned by +/// > getgroups() or it may be returned only by getegid(). Duplication +/// > may exist, but the application needs to call getegid() to be sure +/// > of getting all of the information. Various implementation +/// > variations and administrative sequences cause the set of groups +/// > appearing in the result of getgroups() to vary in order and as to +/// > whether the effective group ID is included, even when the set of +/// > groups is the same (in the mathematical sense of ``set''). (The +/// > history of a process and its parents could affect the details of +/// > the result.) +#[cfg(all(unix, feature = "process"))] +pub fn get_groups_gnu(arg_id: Option) -> IOResult> { + let groups = get_groups()?; + let egid = arg_id.unwrap_or_else(crate::features::process::getegid); + Ok(sort_groups(groups, egid)) +} + +#[cfg(all(unix, 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); + } else { + groups.insert(0, egid); + } + groups +} + +#[derive(Copy, Clone)] pub struct Passwd { inner: passwd, } @@ -144,16 +194,38 @@ impl Passwd { self.inner } + /// This is a wrapper function for `libc::getgrouplist`. + /// + /// From: https://man7.org/linux/man-pages/man3/getgrouplist.3.html + /// > If the number of groups of which user is a member is less than or + /// > equal to *ngroups, then the value *ngroups is returned. + /// > If the user is a member of more than *ngroups groups, then + /// > getgrouplist() returns -1. In this case, the value returned in + /// > *ngroups can be used to resize the buffer passed to a further + /// > call getgrouplist(). + /// + /// However, on macOS/darwin (and maybe others?) `getgrouplist` does + /// not update `ngroups` if `ngroups` is too small. Therefore, if not + /// updated by `getgrouplist`, `ngroups` needs to be increased in a + /// loop until `getgrouplist` stops returning -1. pub fn belongs_to(&self) -> Vec { let mut ngroups: c_int = 8; + let mut ngroups_old: c_int; let mut groups = Vec::with_capacity(ngroups as usize); let gid = self.inner.pw_gid; let name = self.inner.pw_name; - unsafe { - if getgrouplist(name, gid, groups.as_mut_ptr(), &mut ngroups) == -1 { + loop { + ngroups_old = ngroups; + if unsafe { getgrouplist(name, gid, groups.as_mut_ptr(), &mut ngroups) } == -1 { + if ngroups == ngroups_old { + ngroups *= 2; + } groups.resize(ngroups as usize, 0); - getgrouplist(name, gid, groups.as_mut_ptr(), &mut ngroups); + } else { + break; } + } + unsafe { groups.set_len(ngroups as usize); } groups.truncate(ngroups as usize); @@ -268,3 +340,27 @@ pub fn usr2uid(name: &str) -> IOResult { pub fn grp2gid(name: &str) -> IOResult { Group::locate(name).map(|p| p.gid()) } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_sort_groups() { + assert_eq!(sort_groups(vec![1, 2, 3], 4), vec![4, 1, 2, 3]); + assert_eq!(sort_groups(vec![1, 2, 3], 3), vec![3, 1, 2]); + assert_eq!(sort_groups(vec![1, 2, 3], 2), vec![2, 1, 3]); + assert_eq!(sort_groups(vec![1, 2, 3], 1), vec![1, 2, 3]); + assert_eq!(sort_groups(vec![1, 2, 3], 0), vec![0, 1, 2, 3]); + } + + #[test] + fn test_entries_get_groups_gnu() { + if let Ok(mut groups) = get_groups() { + if let Some(last) = groups.pop() { + groups.insert(0, last); + assert_eq!(get_groups_gnu(Some(last)).unwrap(), groups); + } + } + } +} diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index 38cdbef94..36bdbfed0 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -113,22 +113,14 @@ fn resolve>(original: P) -> IOResult { )); } - match fs::symlink_metadata(&result) { - Err(e) => return Err(e), - Ok(ref m) if !m.file_type().is_symlink() => break, - Ok(..) => { - followed += 1; - match fs::read_link(&result) { - Ok(path) => { - result.pop(); - result.push(path); - } - Err(e) => { - return Err(e); - } - } - } + if !fs::symlink_metadata(&result)?.file_type().is_symlink() { + break; } + + followed += 1; + let path = fs::read_link(&result)?; + result.pop(); + result.push(path); } Ok(result) } @@ -193,10 +185,8 @@ pub fn canonicalize>(original: P, can_mode: CanonicalizeMode) -> } match resolve(&result) { - Err(e) => match can_mode { - CanonicalizeMode::Missing => continue, - _ => return Err(e), - }, + Err(_) if can_mode == CanonicalizeMode::Missing => continue, + Err(e) => return Err(e), Ok(path) => { result.pop(); result.push(path); @@ -211,65 +201,19 @@ pub fn canonicalize>(original: P, can_mode: CanonicalizeMode) -> } match resolve(&result) { - Err(e) => { - if can_mode == CanonicalizeMode::Existing { - return Err(e); - } + Err(e) if can_mode == CanonicalizeMode::Existing => { + return Err(e); } Ok(path) => { result.pop(); result.push(path); } + Err(_) => (), } } Ok(result) } -#[cfg(unix)] -pub fn is_stdin_interactive() -> bool { - unsafe { libc::isatty(libc::STDIN_FILENO) == 1 } -} - -#[cfg(windows)] -pub fn is_stdin_interactive() -> bool { - false -} - -#[cfg(target_os = "redox")] -pub fn is_stdin_interactive() -> bool { - termion::is_tty(&io::stdin()) -} - -#[cfg(unix)] -pub fn is_stdout_interactive() -> bool { - unsafe { libc::isatty(libc::STDOUT_FILENO) == 1 } -} - -#[cfg(windows)] -pub fn is_stdout_interactive() -> bool { - false -} - -#[cfg(target_os = "redox")] -pub fn is_stdout_interactive() -> bool { - termion::is_tty(&io::stdout()) -} - -#[cfg(unix)] -pub fn is_stderr_interactive() -> bool { - unsafe { libc::isatty(libc::STDERR_FILENO) == 1 } -} - -#[cfg(windows)] -pub fn is_stderr_interactive() -> bool { - false -} - -#[cfg(target_os = "redox")] -pub fn is_stderr_interactive() -> bool { - termion::is_tty(&io::stderr()) -} - #[cfg(not(unix))] #[allow(unused_variables)] pub fn display_permissions(metadata: &fs::Metadata, display_file_type: bool) -> String { diff --git a/src/uucore/src/lib/features/mode.rs b/src/uucore/src/lib/features/mode.rs index 4fb5a6509..fe109d73d 100644 --- a/src/uucore/src/lib/features/mode.rs +++ b/src/uucore/src/lib/features/mode.rs @@ -89,19 +89,19 @@ fn parse_levels(mode: &str) -> (u32, usize) { } fn parse_op(mode: &str, default: Option) -> Result<(char, usize), String> { - match mode.chars().next() { - Some(ch) => match ch { - '+' | '-' | '=' => Ok((ch, 1)), - _ => match default { - Some(ch) => Ok((ch, 0)), - None => Err(format!( - "invalid operator (expected +, -, or =, but found {})", - ch - )), - }, - }, - None => Err("unexpected end of mode".to_owned()), - } + let ch = mode + .chars() + .next() + .ok_or_else(|| "unexpected end of mode".to_owned())?; + Ok(match ch { + '+' | '-' | '=' => (ch, 1), + _ => { + let ch = default.ok_or_else(|| { + format!("invalid operator (expected +, -, or =, but found {})", ch) + })?; + (ch, 0) + } + }) } fn parse_change(mode: &str, fperm: u32, considering_dir: bool) -> (u32, usize) { diff --git a/src/uucore/src/lib/features/perms.rs b/src/uucore/src/lib/features/perms.rs index eb6cca102..89c30b53b 100644 --- a/src/uucore/src/lib/features/perms.rs +++ b/src/uucore/src/lib/features/perms.rs @@ -92,7 +92,7 @@ pub fn wrap_chgrp>( out = format!( "group of '{}' retained as {}", path.display(), - entries::gid2grp(dest_gid).unwrap() + entries::gid2grp(dest_gid).unwrap_or_default() ); } } diff --git a/src/uucore/src/lib/features/process.rs b/src/uucore/src/lib/features/process.rs index 975123cf7..21bfa992c 100644 --- a/src/uucore/src/lib/features/process.rs +++ b/src/uucore/src/lib/features/process.rs @@ -17,18 +17,22 @@ use std::process::ExitStatus as StdExitStatus; use std::thread; use std::time::{Duration, Instant}; +/// `geteuid()` returns the effective user ID of the calling process. pub fn geteuid() -> uid_t { unsafe { libc::geteuid() } } +/// `getegid()` returns the effective group ID of the calling process. pub fn getegid() -> gid_t { unsafe { libc::getegid() } } +/// `getgid()` returns the real group ID of the calling process. pub fn getgid() -> gid_t { unsafe { libc::getgid() } } +/// `getuid()` returns the real user ID of the calling process. pub fn getuid() -> uid_t { unsafe { libc::getuid() } } @@ -93,6 +97,7 @@ pub trait ChildExt { fn send_signal(&mut self, signal: usize) -> io::Result<()>; /// Wait for a process to finish or return after the specified duration. + /// A `timeout` of zero disables the timeout. fn wait_or_timeout(&mut self, timeout: Duration) -> io::Result>; } @@ -106,6 +111,11 @@ impl ChildExt for Child { } fn wait_or_timeout(&mut self, timeout: Duration) -> io::Result> { + if timeout == Duration::from_micros(0) { + return self + .wait() + .map(|status| Some(ExitStatus::from_std_status(status))); + } // .try_wait() doesn't drop stdin, so we do it manually drop(self.stdin.take()); diff --git a/src/uucore/src/lib/features/signals.rs b/src/uucore/src/lib/features/signals.rs index d22fa1791..e6d2e7763 100644 --- a/src/uucore/src/lib/features/signals.rs +++ b/src/uucore/src/lib/features/signals.rs @@ -10,11 +10,6 @@ pub static DEFAULT_SIGNAL: usize = 15; -pub struct Signal<'a> { - pub name: &'a str, - pub value: usize, -} - /* Linux Programmer's Manual @@ -29,131 +24,10 @@ Linux Programmer's Manual */ #[cfg(target_os = "linux")] -pub static ALL_SIGNALS: [Signal<'static>; 31] = [ - Signal { - name: "HUP", - value: 1, - }, - Signal { - name: "INT", - value: 2, - }, - Signal { - name: "QUIT", - value: 3, - }, - Signal { - name: "ILL", - value: 4, - }, - Signal { - name: "TRAP", - value: 5, - }, - Signal { - name: "ABRT", - value: 6, - }, - Signal { - name: "BUS", - value: 7, - }, - Signal { - name: "FPE", - value: 8, - }, - Signal { - name: "KILL", - value: 9, - }, - Signal { - name: "USR1", - value: 10, - }, - Signal { - name: "SEGV", - value: 11, - }, - Signal { - name: "USR2", - value: 12, - }, - Signal { - name: "PIPE", - value: 13, - }, - Signal { - name: "ALRM", - value: 14, - }, - Signal { - name: "TERM", - value: 15, - }, - Signal { - name: "STKFLT", - value: 16, - }, - Signal { - name: "CHLD", - value: 17, - }, - Signal { - name: "CONT", - value: 18, - }, - Signal { - name: "STOP", - value: 19, - }, - Signal { - name: "TSTP", - value: 20, - }, - Signal { - name: "TTIN", - value: 21, - }, - Signal { - name: "TTOU", - value: 22, - }, - Signal { - name: "URG", - value: 23, - }, - Signal { - name: "XCPU", - value: 24, - }, - Signal { - name: "XFSZ", - value: 25, - }, - Signal { - name: "VTALRM", - value: 26, - }, - Signal { - name: "PROF", - value: 27, - }, - Signal { - name: "WINCH", - value: 28, - }, - Signal { - name: "POLL", - value: 29, - }, - Signal { - name: "PWR", - value: 30, - }, - Signal { - name: "SYS", - value: 31, - }, +pub static ALL_SIGNALS: [&str; 32] = [ + "EXIT", "HUP", "INT", "QUIT", "ILL", "TRAP", "ABRT", "BUS", "FPE", "KILL", "USR1", "SEGV", + "USR2", "PIPE", "ALRM", "TERM", "STKFLT", "CHLD", "CONT", "STOP", "TSTP", "TTIN", "TTOU", + "URG", "XCPU", "XFSZ", "VTALRM", "PROF", "WINCH", "POLL", "PWR", "SYS", ]; /* @@ -198,131 +72,10 @@ No Name Default Action Description */ #[cfg(any(target_vendor = "apple", target_os = "freebsd"))] -pub static ALL_SIGNALS: [Signal<'static>; 31] = [ - Signal { - name: "HUP", - value: 1, - }, - Signal { - name: "INT", - value: 2, - }, - Signal { - name: "QUIT", - value: 3, - }, - Signal { - name: "ILL", - value: 4, - }, - Signal { - name: "TRAP", - value: 5, - }, - Signal { - name: "ABRT", - value: 6, - }, - Signal { - name: "EMT", - value: 7, - }, - Signal { - name: "FPE", - value: 8, - }, - Signal { - name: "KILL", - value: 9, - }, - Signal { - name: "BUS", - value: 10, - }, - Signal { - name: "SEGV", - value: 11, - }, - Signal { - name: "SYS", - value: 12, - }, - Signal { - name: "PIPE", - value: 13, - }, - Signal { - name: "ALRM", - value: 14, - }, - Signal { - name: "TERM", - value: 15, - }, - Signal { - name: "URG", - value: 16, - }, - Signal { - name: "STOP", - value: 17, - }, - Signal { - name: "TSTP", - value: 18, - }, - Signal { - name: "CONT", - value: 19, - }, - Signal { - name: "CHLD", - value: 20, - }, - Signal { - name: "TTIN", - value: 21, - }, - Signal { - name: "TTOU", - value: 22, - }, - Signal { - name: "IO", - value: 23, - }, - Signal { - name: "XCPU", - value: 24, - }, - Signal { - name: "XFSZ", - value: 25, - }, - Signal { - name: "VTALRM", - value: 26, - }, - Signal { - name: "PROF", - value: 27, - }, - Signal { - name: "WINCH", - value: 28, - }, - Signal { - name: "INFO", - value: 29, - }, - Signal { - name: "USR1", - value: 30, - }, - Signal { - name: "USR2", - value: 31, - }, +pub static ALL_SIGNALS: [&str; 32] = [ + "EXIT", "HUP", "INT", "QUIT", "ILL", "TRAP", "ABRT", "EMT", "FPE", "KILL", "BUS", "SEGV", + "SYS", "PIPE", "ALRM", "TERM", "URG", "STOP", "TSTP", "CONT", "CHLD", "TTIN", "TTOU", "IO", + "XCPU", "XFSZ", "VTALRM", "PROF", "WINCH", "INFO", "USR1", "USR2", ]; pub fn signal_by_name_or_value(signal_name_or_value: &str) -> Option { @@ -335,56 +88,45 @@ pub fn signal_by_name_or_value(signal_name_or_value: &str) -> Option { } let signal_name = signal_name_or_value.trim_start_matches("SIG"); - ALL_SIGNALS - .iter() - .find(|s| s.name == signal_name) - .map(|s| s.value) + ALL_SIGNALS.iter().position(|&s| s == signal_name) } -#[inline(always)] pub fn is_signal(num: usize) -> bool { - // Named signals start at 1 - num <= ALL_SIGNALS.len() + num < ALL_SIGNALS.len() } -#[test] -fn signals_all_contiguous() { - for (i, signal) in ALL_SIGNALS.iter().enumerate() { - assert_eq!(signal.value, i + 1); - } -} - -#[test] -fn signals_all_are_signal() { - for signal in &ALL_SIGNALS { - assert!(is_signal(signal.value)); - } +pub fn signal_name_by_value(signal_value: usize) -> Option<&'static str> { + ALL_SIGNALS.get(signal_value).copied() } #[test] fn signal_by_value() { assert_eq!(signal_by_name_or_value("0"), Some(0)); - for signal in &ALL_SIGNALS { - assert_eq!( - signal_by_name_or_value(&signal.value.to_string()), - Some(signal.value) - ); + for (value, _signal) in ALL_SIGNALS.iter().enumerate() { + assert_eq!(signal_by_name_or_value(&value.to_string()), Some(value)); } } #[test] fn signal_by_short_name() { - for signal in &ALL_SIGNALS { - assert_eq!(signal_by_name_or_value(signal.name), Some(signal.value)); + for (value, signal) in ALL_SIGNALS.iter().enumerate() { + assert_eq!(signal_by_name_or_value(signal), Some(value)); } } #[test] fn signal_by_long_name() { - for signal in &ALL_SIGNALS { + for (value, signal) in ALL_SIGNALS.iter().enumerate() { assert_eq!( - signal_by_name_or_value(&format!("SIG{}", signal.name)), - Some(signal.value) + signal_by_name_or_value(&format!("SIG{}", signal)), + Some(value) ); } } + +#[test] +fn name() { + for (value, signal) in ALL_SIGNALS.iter().enumerate() { + assert_eq!(signal_name_by_value(value), Some(*signal)); + } +} diff --git a/src/uucore/src/lib/features/utmpx.rs b/src/uucore/src/lib/features/utmpx.rs index a794b01da..5077d9e59 100644 --- a/src/uucore/src/lib/features/utmpx.rs +++ b/src/uucore/src/lib/features/utmpx.rs @@ -207,7 +207,7 @@ impl Utmpx { flags: AI_CANONNAME, ..AddrInfoHints::default() }; - let sockets = getaddrinfo(Some(&hostname), None, Some(hints)) + let sockets = getaddrinfo(Some(hostname), None, Some(hints)) .unwrap() .collect::>>()?; for socket in sockets { diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index 0b0d0fddf..1ac26b04e 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -19,17 +19,23 @@ pub extern crate winapi; //## internal modules -mod macros; // crate macros (macro_rules-type; exported to `crate::...`) - mod features; // feature-gated code modules +mod macros; // crate macros (macro_rules-type; exported to `crate::...`) mod mods; // core cross-platform modules +mod parser; // string parsing modules // * cross-platform modules pub use crate::mods::backup_control; pub use crate::mods::coreopts; +pub use crate::mods::error; pub use crate::mods::os; pub use crate::mods::panic; pub use crate::mods::ranges; +pub use crate::mods::version_cmp; + +// * string parsing modules +pub use crate::parser::parse_size; +pub use crate::parser::parse_time; // * feature-gated modules #[cfg(feature = "encoding")] @@ -38,8 +44,6 @@ pub use crate::features::encoding; pub use crate::features::fs; #[cfg(feature = "fsext")] pub use crate::features::fsext; -#[cfg(feature = "parse_time")] -pub use crate::features::parse_time; #[cfg(feature = "ringbuffer")] pub use crate::features::ringbuffer; #[cfg(feature = "zero-copy")] @@ -184,7 +188,7 @@ mod tests { fn make_os_vec(os_str: &OsStr) -> Vec { vec![ OsString::from("test"), - OsString::from("สวัสดี"), + OsString::from("สวัสดี"), // spell-checker:disable-line os_str.to_os_string(), ] } diff --git a/src/uucore/src/lib/macros.rs b/src/uucore/src/lib/macros.rs index 07d47eed8..6e3a2166f 100644 --- a/src/uucore/src/lib/macros.rs +++ b/src/uucore/src/lib/macros.rs @@ -21,6 +21,27 @@ macro_rules! executable( }) ); +#[macro_export] +macro_rules! show( + ($err:expr) => ({ + let e = $err; + uucore::error::set_exit_code(e.code()); + eprintln!("{}: {}", executable!(), e); + if e.usage() { + eprintln!("Try '{} --help' for more information.", executable!()); + } + }) +); + +#[macro_export] +macro_rules! show_if_err( + ($res:expr) => ({ + if let Err(e) = $res { + show!(e); + } + }) +); + /// Show an error to stderr in a similar style to GNU coreutils. #[macro_export] macro_rules! show_error( diff --git a/src/uucore/src/lib/mods.rs b/src/uucore/src/lib/mods.rs index 2689361a0..b0235832b 100644 --- a/src/uucore/src/lib/mods.rs +++ b/src/uucore/src/lib/mods.rs @@ -2,6 +2,8 @@ pub mod backup_control; pub mod coreopts; +pub mod error; pub mod os; pub mod panic; pub mod ranges; +pub mod version_cmp; diff --git a/src/uucore/src/lib/mods/backup_control.rs b/src/uucore/src/lib/mods/backup_control.rs index 83268d351..b8f389c83 100644 --- a/src/uucore/src/lib/mods/backup_control.rs +++ b/src/uucore/src/lib/mods/backup_control.rs @@ -37,6 +37,19 @@ pub fn determine_backup_suffix(supplied_suffix: Option<&str>) -> String { } } +/// # TODO +/// +/// This function currently deviates slightly from how the [manual][1] describes +/// that it should work. In particular, the current implementation: +/// +/// 1. Doesn't strictly respect the order in which to determine the backup type, +/// which is (in order of precedence) +/// 1. Take a valid value to the '--backup' option +/// 2. Take the value of the `VERSION_CONTROL` env var +/// 3. default to 'existing' +/// 2. Doesn't accept abbreviations to the 'backup_option' parameter +/// +/// [1]: https://www.gnu.org/software/coreutils/manual/html_node/Backup-options.html pub fn determine_backup_mode(backup_opt_exists: bool, backup_opt: Option<&str>) -> BackupMode { if backup_opt_exists { match backup_opt.map(String::from) { diff --git a/src/uucore/src/lib/mods/error.rs b/src/uucore/src/lib/mods/error.rs new file mode 100644 index 000000000..ae509ff00 --- /dev/null +++ b/src/uucore/src/lib/mods/error.rs @@ -0,0 +1,529 @@ +//! All utils return exit with an exit code. Usually, the following scheme is used: +//! * `0`: succeeded +//! * `1`: minor problems +//! * `2`: major problems +//! +//! This module provides types to reconcile these exit codes with idiomatic Rust error +//! handling. This has a couple advantages over manually using [`std::process::exit`]: +//! 1. It enables the use of `?`, `map_err`, `unwrap_or`, etc. in `uumain`. +//! 1. It encourages the use of `UResult`/`Result` in functions in the utils. +//! 1. The error messages are largely standardized across utils. +//! 1. Standardized error messages can be created from external result types +//! (i.e. [`std::io::Result`] & `clap::ClapResult`). +//! 1. `set_exit_code` takes away the burden of manually tracking exit codes for non-fatal errors. +//! +//! # Usage +//! The signature of a typical util should be: +//! ```ignore +//! fn uumain(args: impl uucore::Args) -> UResult<()> { +//! ... +//! } +//! ``` +//! [`UResult`] is a simple wrapper around [`Result`] with a custom error type: [`UError`]. The +//! most important difference with types implementing [`std::error::Error`] is that [`UError`]s +//! can specify the exit code of the program when they are returned from `uumain`: +//! * When `Ok` is returned, the code set with [`set_exit_code`] is used as exit code. If +//! [`set_exit_code`] was not used, then `0` is used. +//! * When `Err` is returned, the code corresponding with the error is used as exit code and the +//! error message is displayed. +//! +//! Additionally, the errors can be displayed manually with the [`show`] and [`show_if_err`] macros: +//! ```ignore +//! let res = Err(USimpleError::new(1, "Error!!")); +//! show_if_err!(res); +//! // or +//! if let Err(e) = res { +//! show!(e); +//! } +//! ``` +//! +//! **Note**: The [`show`] and [`show_if_err`] macros set the exit code of the program using +//! [`set_exit_code`]. See the documentation on that function for more information. +//! +//! # Guidelines +//! * Use common errors where possible. +//! * Add variants to [`UCommonError`] if an error appears in multiple utils. +//! * Prefer proper custom error types over [`ExitCode`] and [`USimpleError`]. +//! * [`USimpleError`] may be used in small utils with simple error handling. +//! * Using [`ExitCode`] is not recommended but can be useful for converting utils to use +//! [`UResult`]. + +use std::{ + error::Error, + fmt::{Display, Formatter}, + sync::atomic::{AtomicI32, Ordering}, +}; + +static EXIT_CODE: AtomicI32 = AtomicI32::new(0); + +/// Get the last exit code set with [`set_exit_code`]. +/// The default value is `0`. +pub fn get_exit_code() -> i32 { + EXIT_CODE.load(Ordering::SeqCst) +} + +/// Set the exit code for the program if `uumain` returns `Ok(())`. +/// +/// This function is most useful for non-fatal errors, for example when applying an operation to +/// multiple files: +/// ```ignore +/// use uucore::error::{UResult, set_exit_code}; +/// +/// fn uumain(args: impl uucore::Args) -> UResult<()> { +/// ... +/// for file in files { +/// let res = some_operation_that_might_fail(file); +/// match res { +/// Ok() => {}, +/// Err(_) => set_exit_code(1), +/// } +/// } +/// Ok(()) // If any of the operations failed, 1 is returned. +/// } +/// ``` +pub fn set_exit_code(code: i32) { + EXIT_CODE.store(code, Ordering::SeqCst); +} + +/// Should be returned by all utils. +/// +/// Two additional methods are implemented on [`UResult`] on top of the normal [`Result`] methods: +/// `map_err_code` & `map_err_code_message`. +/// +/// These methods are used to convert [`UCommonError`]s into errors with a custom error code and +/// message. +pub type UResult = Result; + +trait UResultTrait { + fn map_err_code(self, mapper: fn(&UCommonError) -> Option) -> Self; + fn map_err_code_and_message(self, mapper: fn(&UCommonError) -> Option<(i32, String)>) -> Self; +} + +impl UResultTrait for UResult { + fn map_err_code(self, mapper: fn(&UCommonError) -> Option) -> Self { + if let Err(UError::Common(error)) = self { + if let Some(code) = mapper(&error) { + Err(UCommonErrorWithCode { code, error }.into()) + } else { + Err(error.into()) + } + } else { + self + } + } + + fn map_err_code_and_message(self, mapper: fn(&UCommonError) -> Option<(i32, String)>) -> Self { + if let Err(UError::Common(ref error)) = self { + if let Some((code, message)) = mapper(error) { + return Err(USimpleError { code, message }.into()); + } + } + self + } +} + +/// The error type of [`UResult`]. +/// +/// `UError::Common` errors are defined in [`uucore`](crate) while `UError::Custom` errors are +/// defined by the utils. +/// ``` +/// use uucore::error::USimpleError; +/// let err = USimpleError::new(1, "Error!!".into()); +/// assert_eq!(1, err.code()); +/// assert_eq!(String::from("Error!!"), format!("{}", err)); +/// ``` +pub enum UError { + Common(UCommonError), + Custom(Box), +} + +impl UError { + pub fn code(&self) -> i32 { + match self { + UError::Common(e) => e.code(), + UError::Custom(e) => e.code(), + } + } + + pub fn usage(&self) -> bool { + match self { + UError::Common(e) => e.usage(), + UError::Custom(e) => e.usage(), + } + } +} + +impl From for UError { + fn from(v: UCommonError) -> Self { + UError::Common(v) + } +} + +impl From for UError { + fn from(v: i32) -> Self { + UError::Custom(Box::new(ExitCode(v))) + } +} + +impl From for UError { + fn from(v: E) -> Self { + UError::Custom(Box::new(v) as Box) + } +} + +impl Display for UError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + UError::Common(e) => e.fmt(f), + UError::Custom(e) => e.fmt(f), + } + } +} + +/// Custom errors defined by the utils. +/// +/// All errors should implement [`std::error::Error`], [`std::fmt::Display`] and +/// [`std::fmt::Debug`] and have an additional `code` method that specifies the exit code of the +/// program if the error is returned from `uumain`. +/// +/// An example of a custom error from `ls`: +/// ``` +/// use uucore::error::{UCustomError}; +/// use std::{ +/// error::Error, +/// fmt::{Display, Debug}, +/// path::PathBuf +/// }; +/// +/// #[derive(Debug)] +/// enum LsError { +/// InvalidLineWidth(String), +/// NoMetadata(PathBuf), +/// } +/// +/// impl UCustomError for LsError { +/// fn code(&self) -> i32 { +/// match self { +/// LsError::InvalidLineWidth(_) => 2, +/// LsError::NoMetadata(_) => 1, +/// } +/// } +/// } +/// +/// impl Error for LsError {} +/// +/// impl Display for LsError { +/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +/// match self { +/// LsError::InvalidLineWidth(s) => write!(f, "invalid line width: '{}'", s), +/// LsError::NoMetadata(p) => write!(f, "could not open file: '{}'", p.display()), +/// } +/// } +/// } +/// ``` +/// A crate like [`quick_error`](https://crates.io/crates/quick-error) might also be used, but will +/// still require an `impl` for the `code` method. +pub trait UCustomError: Error { + fn code(&self) -> i32 { + 1 + } + + fn usage(&self) -> bool { + false + } +} + +impl From> for i32 { + fn from(e: Box) -> i32 { + e.code() + } +} + +/// A [`UCommonError`] with an overridden exit code. +/// +/// This exit code is returned instead of the default exit code for the [`UCommonError`]. This is +/// typically created with the either the `UResult::map_err_code` or `UCommonError::with_code` +/// method. +#[derive(Debug)] +pub struct UCommonErrorWithCode { + code: i32, + error: UCommonError, +} + +impl Error for UCommonErrorWithCode {} + +impl Display for UCommonErrorWithCode { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + self.error.fmt(f) + } +} + +impl UCustomError for UCommonErrorWithCode { + fn code(&self) -> i32 { + self.code + } +} + +/// A simple error type with an exit code and a message that implements [`UCustomError`]. +/// +/// It is typically created with the `UResult::map_err_code_and_message` method. Alternatively, it +/// can be constructed by manually: +/// ``` +/// use uucore::error::{UResult, USimpleError}; +/// let err = USimpleError { code: 1, message: "error!".into()}; +/// let res: UResult<()> = Err(err.into()); +/// // or using the `new` method: +/// let res: UResult<()> = Err(USimpleError::new(1, "error!".into())); +/// ``` +#[derive(Debug)] +pub struct USimpleError { + pub code: i32, + pub message: String, +} + +impl USimpleError { + #[allow(clippy::new_ret_no_self)] + pub fn new(code: i32, message: String) -> UError { + UError::Custom(Box::new(Self { code, message })) + } +} + +impl Error for USimpleError {} + +impl Display for USimpleError { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + self.message.fmt(f) + } +} + +impl UCustomError for USimpleError { + fn code(&self) -> i32 { + self.code + } +} + +#[derive(Debug)] +pub struct UUsageError { + pub code: i32, + pub message: String, +} + +impl UUsageError { + #[allow(clippy::new_ret_no_self)] + pub fn new(code: i32, message: String) -> UError { + UError::Custom(Box::new(Self { code, message })) + } +} + +impl Error for UUsageError {} + +impl Display for UUsageError { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + self.message.fmt(f) + } +} + +impl UCustomError for UUsageError { + fn code(&self) -> i32 { + self.code + } + + fn usage(&self) -> bool { + true + } +} + +/// Wrapper type around [`std::io::Error`]. +/// +/// The messages displayed by [`UIoError`] should match the error messages displayed by GNU +/// coreutils. +/// +/// There are two ways to construct this type: with [`UIoError::new`] or by calling the +/// [`FromIo::map_err_context`] method on a [`std::io::Result`] or [`std::io::Error`]. +/// ``` +/// use uucore::error::{FromIo, UResult, UIoError, UCommonError}; +/// use std::fs::File; +/// use std::path::Path; +/// let path = Path::new("test.txt"); +/// +/// // Manual construction +/// let e: UIoError = UIoError::new( +/// std::io::ErrorKind::NotFound, +/// format!("cannot access '{}'", path.display()) +/// ); +/// let res: UResult<()> = Err(e.into()); +/// +/// // Converting from an `std::io::Error`. +/// let res: UResult = File::open(path).map_err_context(|| format!("cannot access '{}'", path.display())); +/// ``` +#[derive(Debug)] +pub struct UIoError { + context: String, + inner: std::io::Error, +} + +impl UIoError { + pub fn new(kind: std::io::ErrorKind, context: String) -> Self { + Self { + context, + inner: std::io::Error::new(kind, ""), + } + } + + pub fn code(&self) -> i32 { + 1 + } + + pub fn usage(&self) -> bool { + false + } +} + +impl Error for UIoError {} + +impl Display for UIoError { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + use std::io::ErrorKind::*; + write!( + f, + "{}: {}", + self.context, + match self.inner.kind() { + NotFound => "No such file or directory", + PermissionDenied => "Permission denied", + ConnectionRefused => "Connection refused", + ConnectionReset => "Connection reset", + ConnectionAborted => "Connection aborted", + NotConnected => "Not connected", + AddrInUse => "Address in use", + AddrNotAvailable => "Address not available", + BrokenPipe => "Broken pipe", + AlreadyExists => "Already exists", + WouldBlock => "Would block", + InvalidInput => "Invalid input", + InvalidData => "Invalid data", + TimedOut => "Timed out", + WriteZero => "Write zero", + Interrupted => "Interrupted", + Other => "Other", + UnexpectedEof => "Unexpected end of file", + _ => panic!("Unexpected io error: {}", self.inner), + }, + ) + } +} + +/// Enables the conversion from `std::io::Error` to `UError` and from `std::io::Result` to +/// `UResult`. +pub trait FromIo { + fn map_err_context(self, context: impl FnOnce() -> String) -> T; +} + +impl FromIo for std::io::Error { + fn map_err_context(self, context: impl FnOnce() -> String) -> UIoError { + UIoError { + context: (context)(), + inner: self, + } + } +} + +impl FromIo> for std::io::Result { + fn map_err_context(self, context: impl FnOnce() -> String) -> UResult { + self.map_err(|e| UError::Common(UCommonError::Io(e.map_err_context(context)))) + } +} + +impl FromIo for std::io::ErrorKind { + fn map_err_context(self, context: impl FnOnce() -> String) -> UIoError { + UIoError { + context: (context)(), + inner: std::io::Error::new(self, ""), + } + } +} + +impl From for UCommonError { + fn from(e: UIoError) -> UCommonError { + UCommonError::Io(e) + } +} + +impl From for UError { + fn from(e: UIoError) -> UError { + let common: UCommonError = e.into(); + common.into() + } +} + +/// Common errors for utilities. +/// +/// If identical errors appear across multiple utilities, they should be added here. +#[derive(Debug)] +pub enum UCommonError { + Io(UIoError), + // Clap(UClapError), +} + +impl UCommonError { + pub fn with_code(self, code: i32) -> UCommonErrorWithCode { + UCommonErrorWithCode { code, error: self } + } + + pub fn code(&self) -> i32 { + 1 + } + + pub fn usage(&self) -> bool { + false + } +} + +impl From for i32 { + fn from(common: UCommonError) -> i32 { + match common { + UCommonError::Io(e) => e.code(), + } + } +} + +impl Error for UCommonError {} + +impl Display for UCommonError { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + match self { + UCommonError::Io(e) => e.fmt(f), + } + } +} + +/// A special error type that does not print any message when returned from +/// `uumain`. Especially useful for porting utilities to using [`UResult`]. +/// +/// There are two ways to construct an [`ExitCode`]: +/// ``` +/// use uucore::error::{ExitCode, UResult}; +/// // Explicit +/// let res: UResult<()> = Err(ExitCode(1).into()); +/// +/// // Using into on `i32`: +/// let res: UResult<()> = Err(1.into()); +/// ``` +/// This type is especially useful for a trivial conversion from utils returning [`i32`] to +/// returning [`UResult`]. +#[derive(Debug)] +pub struct ExitCode(pub i32); + +impl Error for ExitCode {} + +impl Display for ExitCode { + fn fmt(&self, _: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + Ok(()) + } +} + +impl UCustomError for ExitCode { + fn code(&self) -> i32 { + self.0 + } +} diff --git a/src/uucore/src/lib/mods/panic.rs b/src/uucore/src/lib/mods/panic.rs index 6947df2ac..ba0ecdf12 100644 --- a/src/uucore/src/lib/mods/panic.rs +++ b/src/uucore/src/lib/mods/panic.rs @@ -8,7 +8,7 @@ pub fn mute_sigpipe_panic() { let hook = panic::take_hook(); panic::set_hook(Box::new(move |info| { if let Some(res) = info.payload().downcast_ref::() { - if res.contains("Broken pipe") { + if res.contains("BrokenPipe") { return; } } diff --git a/src/uucore/src/lib/mods/ranges.rs b/src/uucore/src/lib/mods/ranges.rs index d4a6bf601..9e1e67d5a 100644 --- a/src/uucore/src/lib/mods/ranges.rs +++ b/src/uucore/src/lib/mods/ranges.rs @@ -85,10 +85,9 @@ impl Range { let mut ranges: Vec = vec![]; for item in list.split(',') { - match FromStr::from_str(item) { - Ok(range_item) => ranges.push(range_item), - Err(e) => return Err(format!("range '{}' was invalid: {}", item, e)), - } + let range_item = FromStr::from_str(item) + .map_err(|e| format!("range '{}' was invalid: {}", item, e))?; + ranges.push(range_item); } ranges.sort(); diff --git a/src/uucore/src/lib/mods/version_cmp.rs b/src/uucore/src/lib/mods/version_cmp.rs new file mode 100644 index 000000000..99b8c8b40 --- /dev/null +++ b/src/uucore/src/lib/mods/version_cmp.rs @@ -0,0 +1,361 @@ +use std::cmp::Ordering; + +/// Compares the non-digit parts of a version. +/// Special cases: ~ are before everything else, even ends ("a~" < "a") +/// Letters are before non-letters +fn version_non_digit_cmp(a: &str, b: &str) -> Ordering { + let mut a_chars = a.chars(); + let mut b_chars = b.chars(); + loop { + match (a_chars.next(), b_chars.next()) { + (Some(c1), Some(c2)) if c1 == c2 => {} + (None, None) => return Ordering::Equal, + (_, Some('~')) => return Ordering::Greater, + (Some('~'), _) => return Ordering::Less, + (None, Some(_)) => return Ordering::Less, + (Some(_), None) => return Ordering::Greater, + (Some(c1), Some(c2)) if c1.is_ascii_alphabetic() && !c2.is_ascii_alphabetic() => { + return Ordering::Less + } + (Some(c1), Some(c2)) if !c1.is_ascii_alphabetic() && c2.is_ascii_alphabetic() => { + return Ordering::Greater + } + (Some(c1), Some(c2)) => return c1.cmp(&c2), + } + } +} + +/// Remove file endings matching the regex (\.[A-Za-z~][A-Za-z0-9~]*)*$ +fn remove_file_ending(a: &str) -> &str { + let mut ending_start = None; + let mut prev_was_dot = false; + for (idx, char) in a.char_indices() { + if char == '.' { + if ending_start.is_none() || prev_was_dot { + ending_start = Some(idx); + } + prev_was_dot = true; + } else if prev_was_dot { + prev_was_dot = false; + if !char.is_ascii_alphabetic() && char != '~' { + ending_start = None; + } + } else if !char.is_ascii_alphanumeric() && char != '~' { + ending_start = None; + } + } + if prev_was_dot { + ending_start = None; + } + if let Some(ending_start) = ending_start { + &a[..ending_start] + } else { + a + } +} + +pub fn version_cmp(mut a: &str, mut b: &str) -> Ordering { + let str_cmp = a.cmp(b); + if str_cmp == Ordering::Equal { + return str_cmp; + } + + // Special cases: + // 1. Empty strings + match (a.is_empty(), b.is_empty()) { + (true, false) => return Ordering::Less, + (false, true) => return Ordering::Greater, + (true, true) => unreachable!(), + (false, false) => {} + } + // 2. Dots + match (a == ".", b == ".") { + (true, false) => return Ordering::Less, + (false, true) => return Ordering::Greater, + (true, true) => unreachable!(), + (false, false) => {} + } + // 3. Two Dots + match (a == "..", b == "..") { + (true, false) => return Ordering::Less, + (false, true) => return Ordering::Greater, + (true, true) => unreachable!(), + (false, false) => {} + } + // 4. Strings starting with a dot + match (a.starts_with('.'), b.starts_with('.')) { + (true, false) => return Ordering::Less, + (false, true) => return Ordering::Greater, + (true, true) => { + // Strip the leading dot for later comparisons + a = &a[1..]; + b = &b[1..]; + } + _ => {} + } + + // Try to strip file extensions + let (mut a, mut b) = match (remove_file_ending(a), remove_file_ending(b)) { + (a_stripped, b_stripped) if a_stripped == b_stripped => { + // If both would be the same after stripping file extensions, don't strip them. + (a, b) + } + stripped => stripped, + }; + + // 1. Compare leading non-numerical part + // 2. Compare leading numerical part + // 3. Repeat + loop { + let a_numerical_start = a.find(|c: char| c.is_ascii_digit()).unwrap_or(a.len()); + let b_numerical_start = b.find(|c: char| c.is_ascii_digit()).unwrap_or(b.len()); + + let a_str = &a[..a_numerical_start]; + let b_str = &b[..b_numerical_start]; + + match version_non_digit_cmp(a_str, b_str) { + Ordering::Equal => {} + ord => return ord, + } + + a = &a[a_numerical_start..]; + b = &b[a_numerical_start..]; + + let a_numerical_end = a.find(|c: char| !c.is_ascii_digit()).unwrap_or(a.len()); + let b_numerical_end = b.find(|c: char| !c.is_ascii_digit()).unwrap_or(b.len()); + + let a_str = a[..a_numerical_end].trim_start_matches('0'); + let b_str = b[..b_numerical_end].trim_start_matches('0'); + + match a_str.len().cmp(&b_str.len()) { + Ordering::Equal => {} + ord => return ord, + } + + match a_str.cmp(b_str) { + Ordering::Equal => {} + ord => return ord, + } + + a = &a[a_numerical_end..]; + b = &b[b_numerical_end..]; + + if a.is_empty() && b.is_empty() { + // Default to the lexical comparison. + return str_cmp; + } + } +} + +#[cfg(test)] +mod tests { + use crate::version_cmp::version_cmp; + use std::cmp::Ordering; + #[test] + fn test_version_cmp() { + // Identical strings + assert_eq!(version_cmp("hello", "hello"), Ordering::Equal); + + assert_eq!(version_cmp("file12", "file12"), Ordering::Equal); + + assert_eq!( + version_cmp("file12-suffix", "file12-suffix"), + Ordering::Equal + ); + + assert_eq!( + version_cmp("file12-suffix24", "file12-suffix24"), + Ordering::Equal + ); + + // Shortened names + assert_eq!(version_cmp("world", "wo"), Ordering::Greater,); + + assert_eq!(version_cmp("hello10wo", "hello10world"), Ordering::Less,); + + // Simple names + assert_eq!(version_cmp("world", "hello"), Ordering::Greater,); + + assert_eq!(version_cmp("hello", "world"), Ordering::Less); + + assert_eq!(version_cmp("apple", "ant"), Ordering::Greater); + + assert_eq!(version_cmp("ant", "apple"), Ordering::Less); + + // Uppercase letters + assert_eq!( + version_cmp("Beef", "apple"), + Ordering::Less, + "Uppercase letters are sorted before all lowercase letters" + ); + + assert_eq!(version_cmp("Apple", "apple"), Ordering::Less); + + assert_eq!(version_cmp("apple", "aPple"), Ordering::Greater); + + // Numbers + assert_eq!( + version_cmp("100", "20"), + Ordering::Greater, + "Greater numbers are greater even if they start with a smaller digit", + ); + + assert_eq!( + version_cmp("20", "20"), + Ordering::Equal, + "Equal numbers are equal" + ); + + assert_eq!( + version_cmp("15", "200"), + Ordering::Less, + "Small numbers are smaller" + ); + + // Comparing numbers with other characters + assert_eq!( + version_cmp("1000", "apple"), + Ordering::Less, + "Numbers are sorted before other characters" + ); + + assert_eq!( + // spell-checker:disable-next-line + version_cmp("file1000", "fileapple"), + Ordering::Less, + "Numbers in the middle of the name are sorted before other characters" + ); + + // Leading zeroes + assert_eq!( + version_cmp("012", "12"), + Ordering::Less, + "A single leading zero can make a difference" + ); + + assert_eq!( + version_cmp("000800", "0000800"), + Ordering::Greater, + "Leading number of zeroes is used even if both non-zero number of zeros" + ); + + // Numbers and other characters combined + assert_eq!(version_cmp("ab10", "aa11"), Ordering::Greater); + + assert_eq!( + version_cmp("aa10", "aa11"), + Ordering::Less, + "Numbers after other characters are handled correctly." + ); + + assert_eq!( + version_cmp("aa2", "aa100"), + Ordering::Less, + "Numbers after alphabetical characters are handled correctly." + ); + + assert_eq!( + version_cmp("aa10bb", "aa11aa"), + Ordering::Less, + "Number is used even if alphabetical characters after it differ." + ); + + assert_eq!( + version_cmp("aa10aa0010", "aa11aa1"), + Ordering::Less, + "Second number is ignored if the first number differs." + ); + + assert_eq!( + version_cmp("aa10aa0010", "aa10aa1"), + Ordering::Greater, + "Second number is used if the rest is equal." + ); + + assert_eq!( + version_cmp("aa10aa0010", "aa00010aa1"), + Ordering::Greater, + "Second number is used if the rest is equal up to leading zeroes of the first number." + ); + + assert_eq!( + version_cmp("aa10aa0022", "aa010aa022"), + Ordering::Greater, + "The leading zeroes of the first number has priority." + ); + + assert_eq!( + version_cmp("aa10aa0022", "aa10aa022"), + Ordering::Less, + "The leading zeroes of other numbers than the first are used." + ); + + assert_eq!( + version_cmp("file-1.4", "file-1.13"), + Ordering::Less, + "Periods are handled as normal text, not as a decimal point." + ); + + // Greater than u64::Max + // u64 == 18446744073709551615 so this should be plenty: + // 20000000000000000000000 + assert_eq!( + version_cmp("aa2000000000000000000000bb", "aa002000000000000000000001bb"), + Ordering::Less, + "Numbers larger than u64::MAX are handled correctly without crashing" + ); + + assert_eq!( + version_cmp("aa2000000000000000000000bb", "aa002000000000000000000000bb"), + Ordering::Greater, + "Leading zeroes for numbers larger than u64::MAX are \ + handled correctly without crashing" + ); + + assert_eq!( + version_cmp(" a", "a"), + Ordering::Greater, + "Whitespace is after letters because letters are before non-letters" + ); + + assert_eq!( + version_cmp("a~", "ab"), + Ordering::Less, + "A tilde is before other letters" + ); + + assert_eq!( + version_cmp("a~", "a"), + Ordering::Less, + "A tilde is before the line end" + ); + assert_eq!( + version_cmp("~", ""), + Ordering::Greater, + "A tilde is after the empty string" + ); + assert_eq!( + version_cmp(".f", ".1"), + Ordering::Greater, + "if both start with a dot it is ignored for the comparison" + ); + + // The following tests are incompatible with GNU as of 2021/06. + // I think that's because of a bug in GNU, reported as https://lists.gnu.org/archive/html/bug-coreutils/2021-06/msg00045.html + assert_eq!( + version_cmp("a..a", "a.+"), + Ordering::Less, + ".a is stripped before the comparison" + ); + assert_eq!( + version_cmp("a.", "a+"), + Ordering::Greater, + ". is not stripped before the comparison" + ); + assert_eq!( + version_cmp("a\0a", "a"), + Ordering::Greater, + "NULL bytes are handled comparison" + ); + } +} diff --git a/src/uucore/src/lib/parser.rs b/src/uucore/src/lib/parser.rs new file mode 100644 index 000000000..d09777e10 --- /dev/null +++ b/src/uucore/src/lib/parser.rs @@ -0,0 +1,2 @@ +pub mod parse_size; +pub mod parse_time; diff --git a/src/uucore/src/lib/parser/parse_size.rs b/src/uucore/src/lib/parser/parse_size.rs new file mode 100644 index 000000000..ec0b08c9e --- /dev/null +++ b/src/uucore/src/lib/parser/parse_size.rs @@ -0,0 +1,324 @@ +// * 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 (ToDO) hdsf ghead gtail + +use std::convert::TryFrom; +use std::error::Error; +use std::fmt; + +/// Parse a size string into a number of bytes. +/// +/// A size string comprises an integer and an optional unit. The unit +/// may be K, M, G, T, P, E, Z or Y (powers of 1024), or KB, MB, +/// etc. (powers of 1000), or b which is 512. +/// Binary prefixes can be used, too: KiB=K, MiB=M, and so on. +/// +/// # Errors +/// +/// Will return `ParseSizeError` if it's not possible to parse this +/// string into a number, e.g. if the string does not begin with a +/// numeral, or if the unit is not one of the supported units described +/// in the preceding section. +/// +/// # Examples +/// +/// ```rust +/// use uucore::parse_size::parse_size; +/// assert_eq!(Ok(123), parse_size("123")); +/// assert_eq!(Ok(9 * 1000), parse_size("9kB")); // kB is 1000 +/// assert_eq!(Ok(2 * 1024), parse_size("2K")); // K is 1024 +/// ``` +pub fn parse_size(size: &str) -> Result { + if size.is_empty() { + return Err(ParseSizeError::parse_failure(size)); + } + // Get the numeric part of the size argument. For example, if the + // argument is "123K", then the numeric part is "123". + let numeric_string: String = size.chars().take_while(|c| c.is_digit(10)).collect(); + let number: usize = if !numeric_string.is_empty() { + match numeric_string.parse() { + Ok(n) => n, + Err(_) => return Err(ParseSizeError::parse_failure(size)), + } + } else { + 1 + }; + + // Get the alphabetic units part of the size argument and compute + // the factor it represents. For example, if the argument is "123K", + // then the unit part is "K" and the factor is 1024. This may be the + // empty string, in which case, the factor is 1. + let unit = &size[numeric_string.len()..]; + let (base, exponent): (u128, u32) = match unit { + "" => (1, 0), + "b" => (512, 1), // (`od`, `head` and `tail` use "b") + "KiB" | "kiB" | "K" | "k" => (1024, 1), + "MiB" | "miB" | "M" | "m" => (1024, 2), + "GiB" | "giB" | "G" | "g" => (1024, 3), + "TiB" | "tiB" | "T" | "t" => (1024, 4), + "PiB" | "piB" | "P" | "p" => (1024, 5), + "EiB" | "eiB" | "E" | "e" => (1024, 6), + "ZiB" | "ziB" | "Z" | "z" => (1024, 7), + "YiB" | "yiB" | "Y" | "y" => (1024, 8), + "KB" | "kB" => (1000, 1), + "MB" | "mB" => (1000, 2), + "GB" | "gB" => (1000, 3), + "TB" | "tB" => (1000, 4), + "PB" | "pB" => (1000, 5), + "EB" | "eB" => (1000, 6), + "ZB" | "zB" => (1000, 7), + "YB" | "yB" => (1000, 8), + _ => return Err(ParseSizeError::parse_failure(size)), + }; + let factor = match usize::try_from(base.pow(exponent)) { + Ok(n) => n, + Err(_) => return Err(ParseSizeError::size_too_big(size)), + }; + number + .checked_mul(factor) + .ok_or_else(|| ParseSizeError::size_too_big(size)) +} + +#[derive(Debug, PartialEq, Eq)] +pub enum ParseSizeError { + ParseFailure(String), // Syntax + SizeTooBig(String), // Overflow +} + +impl Error for ParseSizeError { + fn description(&self) -> &str { + match *self { + ParseSizeError::ParseFailure(ref s) => &*s, + ParseSizeError::SizeTooBig(ref s) => &*s, + } + } +} + +impl fmt::Display for ParseSizeError { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + let s = match self { + ParseSizeError::ParseFailure(s) => s, + ParseSizeError::SizeTooBig(s) => s, + }; + write!(f, "{}", s) + } +} + +impl ParseSizeError { + fn parse_failure(s: &str) -> ParseSizeError { + // stderr on linux (GNU coreutils 8.32) (LC_ALL=C) + // has to be handled in the respective uutils because strings differ, e.g.: + // + // `NUM` + // head: invalid number of bytes: '1fb' + // tail: invalid number of bytes: '1fb' + // + // `SIZE` + // split: invalid number of bytes: '1fb' + // truncate: Invalid number: '1fb' + // + // `MODE` + // stdbuf: invalid mode '1fb' + // + // `SIZE` + // sort: invalid suffix in --buffer-size argument '1fb' + // sort: invalid --buffer-size argument 'fb' + // + // `SIZE` + // du: invalid suffix in --buffer-size argument '1fb' + // du: invalid suffix in --threshold argument '1fb' + // du: invalid --buffer-size argument 'fb' + // du: invalid --threshold argument 'fb' + // + // `BYTES` + // od: invalid suffix in --read-bytes argument '1fb' + // od: invalid --read-bytes argument argument 'fb' + // --skip-bytes + // --width + // --strings + // etc. + ParseSizeError::ParseFailure(format!("'{}'", s)) + } + + fn size_too_big(s: &str) -> ParseSizeError { + // stderr on linux (GNU coreutils 8.32) (LC_ALL=C) + // has to be handled in the respective uutils because strings differ, e.g.: + // + // head: invalid number of bytes: '1Y': Value too large for defined data type + // tail: invalid number of bytes: '1Y': Value too large for defined data type + // split: invalid number of bytes: '1Y': Value too large for defined data type + // truncate: Invalid number: '1Y': Value too large for defined data type + // stdbuf: invalid mode '1Y': Value too large for defined data type + // sort: -S argument '1Y' too large + // du: -B argument '1Y' too large + // od: -N argument '1Y' too large + // etc. + // + // stderr on macos (brew - GNU coreutils 8.32) also differs for the same version, e.g.: + // ghead: invalid number of bytes: '1Y': Value too large to be stored in data type + // gtail: invalid number of bytes: '1Y': Value too large to be stored in data type + ParseSizeError::SizeTooBig(format!("'{}': Value too large for defined data type", s)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn variant_eq(a: &ParseSizeError, b: &ParseSizeError) -> bool { + std::mem::discriminant(a) == std::mem::discriminant(b) + } + + #[test] + fn all_suffixes() { + // Units are K,M,G,T,P,E,Z,Y (powers of 1024) or KB,MB,... (powers of 1000). + // Binary prefixes can be used, too: KiB=K, MiB=M, and so on. + let suffixes = [ + ('K', 1u32), + ('M', 2u32), + ('G', 3u32), + ('T', 4u32), + ('P', 5u32), + ('E', 6u32), + #[cfg(target_pointer_width = "128")] + ('Z', 7u32), // ParseSizeError::SizeTooBig on x64 + #[cfg(target_pointer_width = "128")] + ('Y', 8u32), // ParseSizeError::SizeTooBig on x64 + ]; + + for &(c, exp) in &suffixes { + let s = format!("2{}B", c); // KB + assert_eq!(Ok((2 * (1000_u128).pow(exp)) as usize), parse_size(&s)); + let s = format!("2{}", c); // K + assert_eq!(Ok((2 * (1024_u128).pow(exp)) as usize), parse_size(&s)); + let s = format!("2{}iB", c); // KiB + assert_eq!(Ok((2 * (1024_u128).pow(exp)) as usize), parse_size(&s)); + let s = format!("2{}iB", c.to_lowercase()); // kiB + assert_eq!(Ok((2 * (1024_u128).pow(exp)) as usize), parse_size(&s)); + + // suffix only + let s = format!("{}B", c); // KB + assert_eq!(Ok(((1000_u128).pow(exp)) as usize), parse_size(&s)); + let s = format!("{}", c); // K + assert_eq!(Ok(((1024_u128).pow(exp)) as usize), parse_size(&s)); + let s = format!("{}iB", c); // KiB + assert_eq!(Ok(((1024_u128).pow(exp)) as usize), parse_size(&s)); + let s = format!("{}iB", c.to_lowercase()); // kiB + assert_eq!(Ok(((1024_u128).pow(exp)) as usize), parse_size(&s)); + } + } + + #[test] + #[cfg(not(target_pointer_width = "128"))] + fn overflow_x64() { + assert!(parse_size("10000000000000000000000").is_err()); + assert!(parse_size("1000000000T").is_err()); + assert!(parse_size("100000P").is_err()); + assert!(parse_size("100E").is_err()); + assert!(parse_size("1Z").is_err()); + assert!(parse_size("1Y").is_err()); + + assert!(variant_eq( + &parse_size("1Z").unwrap_err(), + &ParseSizeError::SizeTooBig(String::new()) + )); + + assert_eq!( + ParseSizeError::SizeTooBig("'1Y': Value too large for defined data type".to_string()), + parse_size("1Y").unwrap_err() + ); + } + + #[test] + #[cfg(target_pointer_width = "32")] + fn overflow_x32() { + assert!(variant_eq( + &parse_size("1T").unwrap_err(), + &ParseSizeError::SizeTooBig(String::new()) + )); + assert!(variant_eq( + &parse_size("1000G").unwrap_err(), + &ParseSizeError::SizeTooBig(String::new()) + )); + } + + #[test] + fn invalid_syntax() { + let test_strings = [ + "328hdsf3290", + "5MiB nonsense", + "5mib", + "biB", + "-", + "+", + "", + "-1", + "1e2", + "∞", + ]; + for &test_string in &test_strings { + assert_eq!( + parse_size(test_string).unwrap_err(), + ParseSizeError::ParseFailure(format!("'{}'", test_string)) + ); + } + } + + #[test] + fn b_suffix() { + assert_eq!(Ok(3 * 512), parse_size("3b")); // b is 512 + } + + #[test] + fn no_suffix() { + assert_eq!(Ok(1234), parse_size("1234")); + assert_eq!(Ok(0), parse_size("0")); + assert_eq!(Ok(5), parse_size("5")); + assert_eq!(Ok(999), parse_size("999")); + } + + #[test] + fn kilobytes_suffix() { + assert_eq!(Ok(123 * 1000), parse_size("123KB")); // KB is 1000 + assert_eq!(Ok(9 * 1000), parse_size("9kB")); // kB is 1000 + assert_eq!(Ok(2 * 1024), parse_size("2K")); // K is 1024 + assert_eq!(Ok(0), parse_size("0K")); + assert_eq!(Ok(0), parse_size("0KB")); + assert_eq!(Ok(1000), parse_size("KB")); + assert_eq!(Ok(1024), parse_size("K")); + assert_eq!(Ok(2000), parse_size("2kB")); + assert_eq!(Ok(4000), parse_size("4KB")); + } + + #[test] + fn megabytes_suffix() { + assert_eq!(Ok(123 * 1024 * 1024), parse_size("123M")); + assert_eq!(Ok(123 * 1000 * 1000), parse_size("123MB")); + assert_eq!(Ok(1024 * 1024), parse_size("M")); + assert_eq!(Ok(1000 * 1000), parse_size("MB")); + assert_eq!(Ok(2 * 1_048_576), parse_size("2m")); + assert_eq!(Ok(4 * 1_048_576), parse_size("4M")); + assert_eq!(Ok(2_000_000), parse_size("2mB")); + assert_eq!(Ok(4_000_000), parse_size("4MB")); + } + + #[test] + fn gigabytes_suffix() { + assert_eq!(Ok(1_073_741_824), parse_size("1G")); + assert_eq!(Ok(2_000_000_000), parse_size("2GB")); + } + + #[test] + #[cfg(target_pointer_width = "64")] + fn x64() { + assert_eq!(Ok(1_099_511_627_776), parse_size("1T")); + assert_eq!(Ok(1_125_899_906_842_624), parse_size("1P")); + assert_eq!(Ok(1_152_921_504_606_846_976), parse_size("1E")); + assert_eq!(Ok(2_000_000_000_000), parse_size("2TB")); + assert_eq!(Ok(2_000_000_000_000_000), parse_size("2PB")); + assert_eq!(Ok(2_000_000_000_000_000_000), parse_size("2EB")); + } +} diff --git a/src/uucore/src/lib/features/parse_time.rs b/src/uucore/src/lib/parser/parse_time.rs similarity index 78% rename from src/uucore/src/lib/features/parse_time.rs rename to src/uucore/src/lib/parser/parse_time.rs index 8e822685b..fdf43b727 100644 --- a/src/uucore/src/lib/features/parse_time.rs +++ b/src/uucore/src/lib/parser/parse_time.rs @@ -20,20 +20,18 @@ pub fn from_str(string: &str) -> Result { 'm' | 'M' => (slice, 60), 'h' | 'H' => (slice, 60 * 60), 'd' | 'D' => (slice, 60 * 60 * 24), - val => { - if !val.is_alphabetic() { - (string, 1) - } else if string == "inf" || string == "infinity" { + val if !val.is_alphabetic() => (string, 1), + _ => { + if string == "inf" || string == "infinity" { ("inf", 1) } else { return Err(format!("invalid time interval '{}'", string)); } } }; - let num = match numstr.parse::() { - Ok(m) => m, - Err(e) => return Err(format!("invalid time interval '{}': {}", string, e)), - }; + let num = numstr + .parse::() + .map_err(|e| format!("invalid time interval '{}': {}", string, e))?; const NANOS_PER_SEC: u32 = 1_000_000_000; let whole_secs = num.trunc(); diff --git a/src/uucore_procs/src/lib.rs b/src/uucore_procs/src/lib.rs index e0d247c3f..f62e4178e 100644 --- a/src/uucore_procs/src/lib.rs +++ b/src/uucore_procs/src/lib.rs @@ -1,6 +1,10 @@ // Copyright (C) ~ Roy Ivy III ; MIT license extern crate proc_macro; +use proc_macro::TokenStream; +use proc_macro2::{Ident, Span}; +use quote::quote; +use syn::{self, parse_macro_input, ItemFn}; //## rust proc-macro background info //* ref: @@ @@ -41,7 +45,7 @@ impl syn::parse::Parse for Tokens { } #[proc_macro] -pub fn main(stream: proc_macro::TokenStream) -> proc_macro::TokenStream { +pub fn main(stream: TokenStream) -> TokenStream { let Tokens { expr } = syn::parse_macro_input!(stream as Tokens); proc_dbg!(&expr); @@ -78,5 +82,35 @@ pub fn main(stream: proc_macro::TokenStream) -> proc_macro::TokenStream { std::process::exit(code); } }; - proc_macro::TokenStream::from(result) + TokenStream::from(result) +} + +#[proc_macro_attribute] +pub fn gen_uumain(_args: TokenStream, stream: TokenStream) -> TokenStream { + let mut ast = parse_macro_input!(stream as ItemFn); + + // Change the name of the function to "uumain_result" to prevent name-conflicts + ast.sig.ident = Ident::new("uumain_result", Span::call_site()); + + let new = quote!( + pub fn uumain(args: impl uucore::Args) -> i32 { + #ast + let result = uumain_result(args); + match result { + Ok(()) => uucore::error::get_exit_code(), + Err(e) => { + let s = format!("{}", e); + if s != "" { + show_error!("{}", s); + } + if e.usage() { + eprintln!("Try '{} --help' for more information.", executable!()); + } + e.code() + } + } + } + ); + + TokenStream::from(new) } diff --git a/tests/by-util/test_base32.rs b/tests/by-util/test_base32.rs index 8e3e780c5..38ead28f1 100644 --- a/tests/by-util/test_base32.rs +++ b/tests/by-util/test_base32.rs @@ -103,7 +103,7 @@ fn test_wrap_bad_arg() { .arg(wrap_param) .arg("b") .fails() - .stderr_only("base32: Invalid wrap size: ‘b’: invalid digit found in string\n"); + .stderr_only("base32: Invalid wrap size: 'b': invalid digit found in string\n"); } } @@ -114,7 +114,7 @@ fn test_base32_extra_operand() { .arg("a.txt") .arg("a.txt") .fails() - .stderr_only("base32: extra operand ‘a.txt’"); + .stderr_only("base32: extra operand 'a.txt'"); } #[test] diff --git a/tests/by-util/test_base64.rs b/tests/by-util/test_base64.rs index 236f53fb1..7c7f19205 100644 --- a/tests/by-util/test_base64.rs +++ b/tests/by-util/test_base64.rs @@ -89,7 +89,7 @@ fn test_wrap_bad_arg() { .arg(wrap_param) .arg("b") .fails() - .stderr_only("base64: Invalid wrap size: ‘b’: invalid digit found in string\n"); + .stderr_only("base64: Invalid wrap size: 'b': invalid digit found in string\n"); } } @@ -100,7 +100,7 @@ fn test_base64_extra_operand() { .arg("a.txt") .arg("a.txt") .fails() - .stderr_only("base64: extra operand ‘a.txt’"); + .stderr_only("base64: extra operand 'a.txt'"); } #[test] diff --git a/tests/by-util/test_cat.rs b/tests/by-util/test_cat.rs index fadf378ab..d83b5515b 100644 --- a/tests/by-util/test_cat.rs +++ b/tests/by-util/test_cat.rs @@ -438,7 +438,7 @@ fn test_domain_socket() { let child = new_ucmd!().args(&[socket_path]).run_no_wait(); barrier.wait(); let stdout = &child.wait_with_output().unwrap().stdout; - let output = String::from_utf8_lossy(&stdout); + let output = String::from_utf8_lossy(stdout); assert_eq!("a\tb", output); thread.join().unwrap(); diff --git a/tests/by-util/test_chgrp.rs b/tests/by-util/test_chgrp.rs index 45380b80b..762e922c4 100644 --- a/tests/by-util/test_chgrp.rs +++ b/tests/by-util/test_chgrp.rs @@ -1,4 +1,4 @@ -// spell-checker:ignore (words) nosuchgroup +// spell-checker:ignore (words) nosuchgroup groupname use crate::common::util::*; use rust_users::*; @@ -10,6 +10,33 @@ fn test_invalid_option() { static DIR: &str = "/tmp"; +// we should always get both arguments, regardless of whether --reference was used +#[test] +fn test_help() { + new_ucmd!() + .arg("--help") + .succeeds() + .stdout_contains("ARGS:\n \n ... "); +} + +#[test] +fn test_help_ref() { + new_ucmd!() + .arg("--help") + .arg("--reference=ref_file") + .succeeds() + .stdout_contains("ARGS:\n \n ... "); +} + +#[test] +fn test_ref_help() { + new_ucmd!() + .arg("--reference=ref_file") + .arg("--help") + .succeeds() + .stdout_contains("ARGS:\n \n ... "); +} + #[test] fn test_invalid_group() { new_ucmd!() @@ -121,9 +148,52 @@ fn test_reference() { fn test_reference() { new_ucmd!() .arg("-v") - .arg("--reference=/etc/passwd") + .arg("--reference=ref_file") .arg("/etc") - .succeeds(); + .fails() + // group name can differ, so just check the first part of the message + .stderr_contains("chgrp: changing group of '/etc': Operation not permitted (os error 1)\nfailed to change group of '/etc' from "); +} + +#[test] +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +fn test_reference_multi_no_equal() { + new_ucmd!() + .arg("-v") + .arg("--reference") + .arg("ref_file") + .arg("file1") + .arg("file2") + .succeeds() + .stderr_contains("chgrp: group of 'file1' retained as ") + .stderr_contains("\nchgrp: group of 'file2' retained as "); +} + +#[test] +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +fn test_reference_last() { + new_ucmd!() + .arg("-v") + .arg("file1") + .arg("file2") + .arg("file3") + .arg("--reference") + .arg("ref_file") + .succeeds() + .stderr_contains("chgrp: group of 'file1' retained as ") + .stderr_contains("\nchgrp: group of 'file2' retained as ") + .stderr_contains("\nchgrp: group of 'file3' retained as "); +} + +#[test] +fn test_missing_files() { + new_ucmd!() + .arg("-v") + .arg("groupname") + .fails() + .stderr_contains( + "error: The following required arguments were not provided:\n ...\n", + ); } #[test] @@ -135,7 +205,7 @@ fn test_big_p() { .arg("bin") .arg("/proc/self/cwd") .fails() - .stderr_is( + .stderr_contains( "chgrp: changing group of '/proc/self/cwd': Operation not permitted (os error 1)\n", ); } diff --git a/tests/by-util/test_chown.rs b/tests/by-util/test_chown.rs index c8a8ea538..86365f51b 100644 --- a/tests/by-util/test_chown.rs +++ b/tests/by-util/test_chown.rs @@ -172,14 +172,14 @@ fn test_chown_only_colon() { // expected: // $ chown -v :: file.txt 2>out_err ; echo $? ; cat out_err // 1 - // chown: invalid group: ‘::’ + // chown: invalid group: '::' scene .ucmd() .arg("::") .arg("--verbose") .arg(file1) .fails() - .stderr_contains(&"invalid group: ‘::’"); + .stderr_contains(&"invalid group: '::'"); } #[test] diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index ff607f984..19f93e499 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -618,7 +618,7 @@ fn test_cp_deref() { // Check the content of the destination file that was copied. assert_eq!(at.read(TEST_COPY_TO_FOLDER_FILE), "Hello, World!\n"); let path_to_check = path_to_new_symlink.to_str().unwrap(); - assert_eq!(at.read(&path_to_check), "Hello, World!\n"); + assert_eq!(at.read(path_to_check), "Hello, World!\n"); } #[test] fn test_cp_no_deref() { @@ -655,7 +655,7 @@ fn test_cp_no_deref() { // Check the content of the destination file that was copied. assert_eq!(at.read(TEST_COPY_TO_FOLDER_FILE), "Hello, World!\n"); let path_to_check = path_to_new_symlink.to_str().unwrap(); - assert_eq!(at.read(&path_to_check), "Hello, World!\n"); + assert_eq!(at.read(path_to_check), "Hello, World!\n"); } #[test] @@ -726,6 +726,15 @@ fn test_cp_parents_dest_not_directory() { .stderr_contains("with --parents, the destination must be a directory"); } +#[test] +fn test_cp_preserve_no_args() { + new_ucmd!() + .arg(TEST_COPY_FROM_FOLDER_FILE) + .arg(TEST_HELLO_WORLD_DEST) + .arg("--preserve") + .succeeds(); +} + #[test] // For now, disable the test on Windows. Symlinks aren't well support on Windows. // It works on Unix for now and it works locally when run from a powershell @@ -823,7 +832,7 @@ fn test_cp_deref_folder_to_folder() { // Check the content of the symlink let path_to_check = path_to_new_symlink.to_str().unwrap(); - assert_eq!(at.read(&path_to_check), "Hello, World!\n"); + assert_eq!(at.read(path_to_check), "Hello, World!\n"); } #[test] @@ -923,7 +932,7 @@ fn test_cp_no_deref_folder_to_folder() { // Check the content of the symlink let path_to_check = path_to_new_symlink.to_str().unwrap(); - assert_eq!(at.read(&path_to_check), "Hello, World!\n"); + assert_eq!(at.read(path_to_check), "Hello, World!\n"); } #[test] @@ -1290,3 +1299,42 @@ fn test_closes_file_descriptors() { .with_limit(Resource::NOFILE, 9, 9) .succeeds(); } + +#[test] +fn test_copy_dir_symlink() { + let (at, mut ucmd) = at_and_ucmd!(); + at.mkdir("dir"); + at.symlink_dir("dir", "dir-link"); + ucmd.args(&["-r", "dir-link", "copy"]).succeeds(); + assert_eq!(at.resolve_link("copy"), "dir"); +} + +#[test] +fn test_copy_dir_with_symlinks() { + let (at, mut ucmd) = at_and_ucmd!(); + at.mkdir("dir"); + at.make_file("dir/file"); + + TestScenario::new("ln") + .ucmd() + .arg("-sr") + .arg(at.subdir.join("dir/file")) + .arg(at.subdir.join("dir/file-link")) + .succeeds(); + + ucmd.args(&["-r", "dir", "copy"]).succeeds(); + assert_eq!(at.resolve_link("copy/file-link"), "file"); +} + +#[test] +#[cfg(not(windows))] +fn test_copy_symlink_force() { + let (at, mut ucmd) = at_and_ucmd!(); + at.touch("file"); + at.symlink_file("file", "file-link"); + at.touch("copy"); + + ucmd.args(&["file-link", "copy", "-f", "--no-dereference"]) + .succeeds(); + assert_eq!(at.resolve_link("copy"), "file"); +} diff --git a/tests/by-util/test_cut.rs b/tests/by-util/test_cut.rs index 8f81b94c1..92bab4d75 100644 --- a/tests/by-util/test_cut.rs +++ b/tests/by-util/test_cut.rs @@ -157,3 +157,12 @@ fn test_directory_and_no_such_file() { .run() .stderr_is("cut: some: No such file or directory\n"); } + +#[test] +fn test_equal_as_delimiter() { + new_ucmd!() + .args(&["-f", "2", "-d="]) + .pipe_in("--dir=./out/lib") + .succeeds() + .stdout_only("./out/lib\n"); +} diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 72747fa66..a7a5fa583 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -117,7 +117,7 @@ fn test_date_format_without_plus() { new_ucmd!() .arg("%s") .fails() - .stderr_contains("date: invalid date ‘%s’") + .stderr_contains("date: invalid date '%s'") .code_is(1); } diff --git a/tests/by-util/test_du.rs b/tests/by-util/test_du.rs index 3c177c6bf..029f5e516 100644 --- a/tests/by-util/test_du.rs +++ b/tests/by-util/test_du.rs @@ -1,9 +1,16 @@ +// * 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 (paths) sublink subwords use crate::common::util::*; const SUB_DIR: &str = "subdir/deeper"; +const SUB_DEEPER_DIR: &str = "subdir/deeper/deeper_dir"; const SUB_DIR_LINKS: &str = "subdir/links"; +const SUB_DIR_LINKS_DEEPER_SYM_DIR: &str = "subdir/links/deeper_dir"; const SUB_FILE: &str = "subdir/links/subwords.txt"; const SUB_LINK: &str = "subdir/links/sublink.txt"; @@ -16,7 +23,7 @@ fn _du_basics(s: &str) { let answer = "32\t./subdir 8\t./subdir/deeper 24\t./subdir/links -40\t./ +40\t. "; assert_eq!(s, answer); } @@ -25,7 +32,7 @@ fn _du_basics(s: &str) { let answer = "28\t./subdir 8\t./subdir/deeper 16\t./subdir/links -36\t./ +36\t. "; assert_eq!(s, answer); } @@ -49,15 +56,15 @@ fn test_du_basics_subdir() { #[cfg(target_vendor = "apple")] fn _du_basics_subdir(s: &str) { - assert_eq!(s, "4\tsubdir/deeper\n"); + assert_eq!(s, "4\tsubdir/deeper/deeper_dir\n8\tsubdir/deeper\n"); } #[cfg(target_os = "windows")] fn _du_basics_subdir(s: &str) { - assert_eq!(s, "0\tsubdir/deeper\n"); + assert_eq!(s, "0\tsubdir/deeper\\deeper_dir\n0\tsubdir/deeper\n"); } #[cfg(target_os = "freebsd")] fn _du_basics_subdir(s: &str) { - assert_eq!(s, "8\tsubdir/deeper\n"); + assert_eq!(s, "8\tsubdir/deeper/deeper_dir\n16\tsubdir/deeper\n"); } #[cfg(all( not(target_vendor = "apple"), @@ -73,6 +80,26 @@ fn _du_basics_subdir(s: &str) { } } +#[test] +fn test_du_invalid_size() { + let args = &["block-size", "threshold"]; + for s in args { + new_ucmd!() + .arg(format!("--{}=1fb4t", s)) + .arg("/tmp") + .fails() + .code_is(1) + .stderr_only(format!("du: invalid --{} argument '1fb4t'", s)); + #[cfg(not(target_pointer_width = "128"))] + new_ucmd!() + .arg(format!("--{}=1Y", s)) + .arg("/tmp") + .fails() + .code_is(1) + .stderr_only(format!("du: --{} argument '1Y' too large", s)); + } +} + #[test] fn test_du_basics_bad_name() { new_ucmd!() @@ -185,12 +212,7 @@ fn test_du_d_flag() { { let result_reference = scene.cmd("du").arg("-d1").run(); if result_reference.succeeded() { - assert_eq!( - // TODO: gnu `du` doesn't use trailing "/" here - // result.stdout_str(), result_reference.stdout_str() - result.stdout_str().trim_end_matches("/\n"), - result_reference.stdout_str().trim_end_matches('\n') - ); + assert_eq!(result.stdout_str(), result_reference.stdout_str()); return; } } @@ -199,15 +221,15 @@ fn test_du_d_flag() { #[cfg(target_vendor = "apple")] fn _du_d_flag(s: &str) { - assert_eq!(s, "16\t./subdir\n20\t./\n"); + assert_eq!(s, "20\t./subdir\n24\t.\n"); } #[cfg(target_os = "windows")] fn _du_d_flag(s: &str) { - assert_eq!(s, "8\t./subdir\n8\t./\n"); + assert_eq!(s, "8\t.\\subdir\n8\t.\n"); } #[cfg(target_os = "freebsd")] fn _du_d_flag(s: &str) { - assert_eq!(s, "28\t./subdir\n36\t./\n"); + assert_eq!(s, "36\t./subdir\n44\t.\n"); } #[cfg(all( not(target_vendor = "apple"), @@ -217,9 +239,127 @@ fn _du_d_flag(s: &str) { fn _du_d_flag(s: &str) { // MS-WSL linux has altered expected output if !uucore::os::is_wsl_1() { - assert_eq!(s, "28\t./subdir\n36\t./\n"); + assert_eq!(s, "28\t./subdir\n36\t.\n"); } else { - assert_eq!(s, "8\t./subdir\n8\t./\n"); + assert_eq!(s, "8\t./subdir\n8\t.\n"); + } +} + +#[test] +fn test_du_dereference() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.symlink_dir(SUB_DEEPER_DIR, SUB_DIR_LINKS_DEEPER_SYM_DIR); + + let result = scene.ucmd().arg("-L").arg(SUB_DIR_LINKS).succeeds(); + + #[cfg(target_os = "linux")] + { + let result_reference = scene.cmd("du").arg("-L").arg(SUB_DIR_LINKS).run(); + if result_reference.succeeded() { + assert_eq!(result.stdout_str(), result_reference.stdout_str()); + return; + } + } + + _du_dereference(result.stdout_str()); +} + +#[cfg(target_vendor = "apple")] +fn _du_dereference(s: &str) { + assert_eq!(s, "4\tsubdir/links/deeper_dir\n16\tsubdir/links\n"); +} +#[cfg(target_os = "windows")] +fn _du_dereference(s: &str) { + assert_eq!(s, "0\tsubdir/links\\deeper_dir\n8\tsubdir/links\n"); +} +#[cfg(target_os = "freebsd")] +fn _du_dereference(s: &str) { + assert_eq!(s, "8\tsubdir/links/deeper_dir\n24\tsubdir/links\n"); +} +#[cfg(all( + not(target_vendor = "apple"), + not(target_os = "windows"), + not(target_os = "freebsd") +))] +fn _du_dereference(s: &str) { + // MS-WSL linux has altered expected output + if !uucore::os::is_wsl_1() { + assert_eq!(s, "8\tsubdir/links/deeper_dir\n24\tsubdir/links\n"); + } else { + assert_eq!(s, "0\tsubdir/links/deeper_dir\n8\tsubdir/links\n"); + } +} + +#[test] +fn test_du_inodes_basic() { + let scene = TestScenario::new(util_name!()); + let result = scene.ucmd().arg("--inodes").succeeds(); + + #[cfg(target_os = "linux")] + { + let result_reference = scene.cmd("du").arg("--inodes").run(); + assert_eq!(result.stdout_str(), result_reference.stdout_str()); + } + + #[cfg(not(target_os = "linux"))] + _du_inodes_basic(result.stdout_str()); +} + +#[cfg(target_os = "windows")] +fn _du_inodes_basic(s: &str) { + assert_eq!( + s, + "2\t.\\subdir\\deeper\\deeper_dir +4\t.\\subdir\\deeper +3\t.\\subdir\\links +8\t.\\subdir +11\t. +" + ); +} + +#[cfg(not(target_os = "windows"))] +fn _du_inodes_basic(s: &str) { + assert_eq!( + s, + "2\t./subdir/deeper/deeper_dir +4\t./subdir/deeper +3\t./subdir/links +8\t./subdir +11\t. +" + ); +} + +#[test] +fn test_du_inodes() { + let scene = TestScenario::new(util_name!()); + + scene + .ucmd() + .arg("--summarize") + .arg("--inodes") + .succeeds() + .stdout_only("11\t.\n"); + + let result = scene + .ucmd() + .arg("--separate-dirs") + .arg("--inodes") + .succeeds(); + + #[cfg(target_os = "windows")] + result.stdout_contains("3\t.\\subdir\\links\n"); + #[cfg(not(target_os = "windows"))] + result.stdout_contains("3\t./subdir/links\n"); + result.stdout_contains("3\t.\n"); + + #[cfg(target_os = "linux")] + { + let result_reference = scene.cmd("du").arg("--separate-dirs").arg("--inodes").run(); + assert_eq!(result.stdout_str(), result_reference.stdout_str()); } } @@ -286,7 +426,7 @@ fn test_du_no_permission() { let result = scene.ucmd().arg(SUB_DIR_LINKS).run(); // TODO: replace with ".fails()" once `du` is fixed result.stderr_contains( - "du: cannot read directory ‘subdir/links‘: Permission denied (os error 13)", + "du: cannot read directory 'subdir/links': Permission denied (os error 13)", ); #[cfg(target_os = "linux")] @@ -312,3 +452,134 @@ fn _du_no_permission(s: &str) { fn _du_no_permission(s: &str) { assert_eq!(s, "4\tsubdir/links\n"); } + +#[test] +fn test_du_one_file_system() { + let scene = TestScenario::new(util_name!()); + + let result = scene.ucmd().arg("-x").arg(SUB_DIR).succeeds(); + + #[cfg(target_os = "linux")] + { + let result_reference = scene.cmd("du").arg("-x").arg(SUB_DIR).run(); + if result_reference.succeeded() { + assert_eq!(result.stdout_str(), result_reference.stdout_str()); + return; + } + } + _du_basics_subdir(result.stdout_str()); +} + +#[test] +fn test_du_threshold() { + let scene = TestScenario::new(util_name!()); + + let threshold = if cfg!(windows) { "7K" } else { "10K" }; + + scene + .ucmd() + .arg(format!("--threshold={}", threshold)) + .succeeds() + .stdout_contains("links") + .stdout_does_not_contain("deeper_dir"); + + scene + .ucmd() + .arg(format!("--threshold=-{}", threshold)) + .succeeds() + .stdout_does_not_contain("links") + .stdout_contains("deeper_dir"); +} + +#[test] +fn test_du_apparent_size() { + let scene = TestScenario::new(util_name!()); + let result = scene.ucmd().arg("--apparent-size").succeeds(); + + #[cfg(target_os = "linux")] + { + let result_reference = scene.cmd("du").arg("--apparent-size").run(); + assert_eq!(result.stdout_str(), result_reference.stdout_str()); + } + + #[cfg(not(target_os = "linux"))] + _du_apparent_size(result.stdout_str()); +} + +#[cfg(target_os = "windows")] +fn _du_apparent_size(s: &str) { + assert_eq!( + s, + "1\t.\\subdir\\deeper\\deeper_dir +1\t.\\subdir\\deeper +6\t.\\subdir\\links +6\t.\\subdir +6\t. +" + ); +} +#[cfg(target_vendor = "apple")] +fn _du_apparent_size(s: &str) { + assert_eq!( + s, + "1\t./subdir/deeper/deeper_dir +1\t./subdir/deeper +6\t./subdir/links +6\t./subdir +6\t. +" + ); +} +#[cfg(target_os = "freebsd")] +fn _du_apparent_size(s: &str) { + assert_eq!( + s, + "1\t./subdir/deeper/deeper_dir +2\t./subdir/deeper +6\t./subdir/links +8\t./subdir +8\t. +" + ); +} +#[cfg(all( + not(target_vendor = "apple"), + not(target_os = "windows"), + not(target_os = "freebsd") +))] +fn _du_apparent_size(s: &str) { + assert_eq!( + s, + "5\t./subdir/deeper/deeper_dir +9\t./subdir/deeper +10\t./subdir/links +22\t./subdir +26\t. +" + ); +} + +#[test] +fn test_du_bytes() { + let scene = TestScenario::new(util_name!()); + let result = scene.ucmd().arg("--bytes").succeeds(); + + #[cfg(target_os = "linux")] + { + let result_reference = scene.cmd("du").arg("--bytes").run(); + assert_eq!(result.stdout_str(), result_reference.stdout_str()); + } + + #[cfg(target_os = "windows")] + result.stdout_contains("5145\t.\\subdir\n"); + #[cfg(target_vendor = "apple")] + result.stdout_contains("5625\t./subdir\n"); + #[cfg(target_os = "freebsd")] + result.stdout_contains("7193\t./subdir\n"); + #[cfg(all( + not(target_vendor = "apple"), + not(target_os = "windows"), + not(target_os = "freebsd") + ))] + result.stdout_contains("21529\t./subdir\n"); +} diff --git a/tests/by-util/test_env.rs b/tests/by-util/test_env.rs index 4db3b59bd..1d76c433d 100644 --- a/tests/by-util/test_env.rs +++ b/tests/by-util/test_env.rs @@ -187,11 +187,10 @@ fn test_change_directory() { .arg(&temporary_path) .succeeds() .stdout_move_str(); - assert_eq!( - out.lines() - .any(|line| line.ends_with(temporary_path.file_name().unwrap().to_str().unwrap())), - false - ); + + assert!(!out + .lines() + .any(|line| line.ends_with(temporary_path.file_name().unwrap().to_str().unwrap()))); } #[test] diff --git a/tests/by-util/test_groups.rs b/tests/by-util/test_groups.rs index cee13bdc3..9bd0cd12a 100644 --- a/tests/by-util/test_groups.rs +++ b/tests/by-util/test_groups.rs @@ -1,41 +1,176 @@ use crate::common::util::*; +// spell-checker:ignore (ToDO) coreutil + +// These tests run the GNU coreutils `(g)groups` binary in `$PATH` in order to gather reference values. +// If the `(g)groups` in `$PATH` doesn't include a coreutils version string, +// or the version is too low, the test is skipped. + +// The reference version is 8.32. Here 8.30 was chosen because right now there's no +// ubuntu image for github action available with a higher version than 8.30. +const VERSION_MIN: &str = "8.30"; // minimum Version for the reference `groups` in $PATH +const VERSION_MIN_MULTIPLE_USERS: &str = "8.31"; // this feature was introduced in GNU's coreutils 8.31 +const UUTILS_WARNING: &str = "uutils-tests-warning"; +const UUTILS_INFO: &str = "uutils-tests-info"; + +macro_rules! unwrap_or_return { + ( $e:expr ) => { + match $e { + Ok(x) => x, + Err(e) => { + println!("{}: test skipped: {}", UUTILS_INFO, e); + return; + } + } + }; +} + +fn whoami() -> String { + // Apparently some CI environments have configuration issues, e.g. with 'whoami' and 'id'. + // + // From the Logs: "Build (ubuntu-18.04, x86_64-unknown-linux-gnu, feat_os_unix, use-cross)" + // whoami: cannot find name for user ID 1001 + // id --name: cannot find name for user ID 1001 + // id --name: cannot find name for group ID 116 + // + // However, when running "id" from within "/bin/bash" it looks fine: + // id: "uid=1001(runner) gid=118(docker) groups=118(docker),4(adm),101(systemd-journal)" + // whoami: "runner" + + // Use environment variable to get current user instead of + // invoking `whoami` and fall back to user "nobody" on error. + std::env::var("USER").unwrap_or_else(|e| { + println!("{}: {}, using \"nobody\" instead", UUTILS_WARNING, e); + "nobody".to_string() + }) +} + #[test] +#[cfg(unix)] fn test_groups() { let result = new_ucmd!().run(); - println!("result.stdout = {}", result.stdout_str()); - println!("result.stderr = {}", result.stderr_str()); - if is_ci() && result.stdout_str().trim().is_empty() { - // In the CI, some server are failing to return the group. - // As seems to be a configuration issue, ignoring it - return; - } - result.success(); - assert!(!result.stdout_str().trim().is_empty()); + let exp_result = unwrap_or_return!(expected_result(&[])); + + result + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str()) + .code_is(exp_result.code()); } #[test] -fn test_groups_arg() { - // get the username with the "id -un" command - let result = TestScenario::new("id").ucmd_keepenv().arg("-un").run(); - println!("result.stdout = {}", result.stdout_str()); - println!("result.stderr = {}", result.stderr_str()); - let s1 = String::from(result.stdout_str().trim()); - if is_ci() && s1.parse::().is_ok() { - // In the CI, some server are failing to return id -un. - // So, if we are getting a uid, just skip this test - // As seems to be a configuration issue, ignoring it +#[cfg(unix)] +fn test_groups_username() { + let test_users = [&whoami()[..]]; + + let result = new_ucmd!().args(&test_users).run(); + let exp_result = unwrap_or_return!(expected_result(&test_users)); + + result + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str()) + .code_is(exp_result.code()); +} + +#[test] +#[cfg(unix)] +fn test_groups_username_multiple() { + // TODO: [2021-06; jhscheer] refactor this as `let util_name = host_name_for(util_name!())` when that function is added to 'tests/common' + #[cfg(target_os = "linux")] + let util_name = util_name!(); + #[cfg(all(unix, not(target_os = "linux")))] + let util_name = &format!("g{}", util_name!()); + let version_check_string = check_coreutil_version(util_name, VERSION_MIN_MULTIPLE_USERS); + if version_check_string.starts_with(UUTILS_WARNING) { + println!("{}\ntest skipped", version_check_string); return; } + let test_users = ["root", "man", "postfix", "sshd", &whoami()]; - println!("result.stdout = {}", result.stdout_str()); - println!("result.stderr = {}", result.stderr_str()); - result.success(); - assert!(!result.stdout_str().is_empty()); - let username = result.stdout_str().trim(); + let result = new_ucmd!().args(&test_users).run(); + let exp_result = unwrap_or_return!(expected_result(&test_users)); - // call groups with the user name to check that we - // are getting something - new_ucmd!().arg(username).succeeds(); - assert!(!result.stdout_str().is_empty()); + result + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str()) + .code_is(exp_result.code()); +} + +fn check_coreutil_version(util_name: &str, version_expected: &str) -> String { + // example: + // $ id --version | head -n 1 + // id (GNU coreutils) 8.32.162-4eda + let scene = TestScenario::new(util_name); + let version_check = scene + .cmd_keepenv(&util_name) + .env("LC_ALL", "C") + .arg("--version") + .run(); + version_check + .stdout_str() + .split('\n') + .collect::>() + .get(0) + .map_or_else( + || format!("{}: unexpected output format for reference coreutil: '{} --version'", UUTILS_WARNING, util_name), + |s| { + if s.contains(&format!("(GNU coreutils) {}", version_expected)) { + s.to_string() + } else if s.contains("(GNU coreutils)") { + let version_found = s.split_whitespace().last().unwrap()[..4].parse::().unwrap_or_default(); + let version_expected = version_expected.parse::().unwrap_or_default(); + if version_found > version_expected { + format!("{}: version for the reference coreutil '{}' is higher than expected; expected: {}, found: {}", UUTILS_INFO, util_name, version_expected, version_found) + } else { + format!("{}: version for the reference coreutil '{}' does not match; expected: {}, found: {}", UUTILS_WARNING, util_name, version_expected, version_found) } + } else { + format!("{}: no coreutils version string found for reference coreutils '{} --version'", UUTILS_WARNING, util_name) + } + }, + ) +} + +#[allow(clippy::needless_borrow)] +#[cfg(unix)] +fn expected_result(args: &[&str]) -> Result { + // TODO: [2021-06; jhscheer] refactor this as `let util_name = host_name_for(util_name!())` when that function is added to 'tests/common' + #[cfg(target_os = "linux")] + let util_name = util_name!(); + #[cfg(all(unix, not(target_os = "linux")))] + let util_name = &format!("g{}", util_name!()); + + let version_check_string = check_coreutil_version(util_name, VERSION_MIN); + if version_check_string.starts_with(UUTILS_WARNING) { + return Err(version_check_string); + } + println!("{}", version_check_string); + + let scene = TestScenario::new(util_name); + let result = scene + .cmd_keepenv(util_name) + .env("LC_ALL", "C") + .args(args) + .run(); + + let (stdout, stderr): (String, String) = if cfg!(target_os = "linux") { + ( + result.stdout_str().to_string(), + result.stderr_str().to_string(), + ) + } else { + // strip 'g' prefix from results: + let from = util_name.to_string() + ":"; + let to = &from[1..]; + ( + result.stdout_str().replace(&from, to), + result.stderr_str().replace(&from, to), + ) + }; + + Ok(CmdResult::new( + Some(result.tmpd()), + Some(result.code()), + result.succeeded(), + stdout.as_bytes(), + stderr.as_bytes(), + )) } diff --git a/tests/by-util/test_head.rs b/tests/by-util/test_head.rs index 2d6d60d15..a3e3a79d7 100755 --- a/tests/by-util/test_head.rs +++ b/tests/by-util/test_head.rs @@ -1,4 +1,9 @@ -// spell-checker:ignore (words) bogusfile emptyfile +// * 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) bogusfile emptyfile abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstu use crate::common::util::*; @@ -265,3 +270,61 @@ fn test_bad_utf8_lines() { .succeeds() .stdout_is_bytes(output); } + +#[test] +fn test_head_invalid_num() { + new_ucmd!() + .args(&["-c", "1024R", "emptyfile.txt"]) + .fails() + .stderr_is("head: invalid number of bytes: '1024R'"); + new_ucmd!() + .args(&["-n", "1024R", "emptyfile.txt"]) + .fails() + .stderr_is("head: invalid number of lines: '1024R'"); + #[cfg(not(target_pointer_width = "128"))] + new_ucmd!() + .args(&["-c", "1Y", "emptyfile.txt"]) + .fails() + .stderr_is("head: invalid number of bytes: '1Y': Value too large for defined data type"); + #[cfg(not(target_pointer_width = "128"))] + new_ucmd!() + .args(&["-n", "1Y", "emptyfile.txt"]) + .fails() + .stderr_is("head: invalid number of lines: '1Y': Value too large for defined data type"); + #[cfg(target_pointer_width = "32")] + { + let sizes = ["1000G", "10T"]; + for size in &sizes { + new_ucmd!() + .args(&["-c", size]) + .fails() + .code_is(1) + .stderr_only(format!( + "head: invalid number of bytes: '{}': Value too large for defined data type", + size + )); + } + } +} + +#[test] +fn test_head_num_with_undocumented_sign_bytes() { + // tail: '-' is not documented (8.32 man pages) + // head: '+' is not documented (8.32 man pages) + const ALPHABET: &str = "abcdefghijklmnopqrstuvwxyz"; + new_ucmd!() + .args(&["-c", "5"]) + .pipe_in(ALPHABET) + .succeeds() + .stdout_is("abcde"); + new_ucmd!() + .args(&["-c", "-5"]) + .pipe_in(ALPHABET) + .succeeds() + .stdout_is("abcdefghijklmnopqrstu"); + new_ucmd!() + .args(&["-c", "+5"]) + .pipe_in(ALPHABET) + .succeeds() + .stdout_is("abcde"); +} diff --git a/tests/by-util/test_id.rs b/tests/by-util/test_id.rs index 1f8249aab..bacf57037 100644 --- a/tests/by-util/test_id.rs +++ b/tests/by-util/test_id.rs @@ -1,147 +1,186 @@ use crate::common::util::*; -// Apparently some CI environments have configuration issues, e.g. with 'whoami' and 'id'. -// If we are running inside the CI and "needle" is in "stderr" skipping this test is -// considered okay. If we are not inside the CI this calls assert!(result.success). -// -// From the Logs: "Build (ubuntu-18.04, x86_64-unknown-linux-gnu, feat_os_unix, use-cross)" -// stderr: "whoami: cannot find name for user ID 1001" -// Maybe: "adduser --uid 1001 username" can put things right? -// stderr = id: Could not find uid 1001: No such id: 1001 -fn skipping_test_is_okay(result: &CmdResult, needle: &str) -> bool { - if !result.succeeded() { - println!("result.stdout = {}", result.stdout_str()); - println!("result.stderr = {}", result.stderr_str()); - if is_ci() && result.stderr_str().contains(needle) { - println!("test skipped:"); - return true; - } else { - result.success(); +// spell-checker:ignore (ToDO) coreutil + +// These tests run the GNU coreutils `(g)id` binary in `$PATH` in order to gather reference values. +// If the `(g)id` in `$PATH` doesn't include a coreutils version string, +// or the version is too low, the test is skipped. + +// The reference version is 8.32. Here 8.30 was chosen because right now there's no +// ubuntu image for github action available with a higher version than 8.30. +const VERSION_MIN: &str = "8.30"; // minimum Version for the reference `id` in $PATH +const VERSION_MIN_MULTIPLE_USERS: &str = "8.31"; // this feature was introduced in GNU's coreutils 8.31 +const UUTILS_WARNING: &str = "uutils-tests-warning"; +const UUTILS_INFO: &str = "uutils-tests-info"; + +macro_rules! unwrap_or_return { + ( $e:expr ) => { + match $e { + Ok(x) => x, + Err(e) => { + println!("{}: test skipped: {}", UUTILS_INFO, e); + return; + } } - } - false + }; } -fn return_whoami_username() -> String { - let scene = TestScenario::new("whoami"); - let result = scene.cmd("whoami").run(); - if skipping_test_is_okay(&result, "whoami: cannot find name for user ID") { - println!("test skipped:"); - return String::from(""); - } +fn whoami() -> String { + // Apparently some CI environments have configuration issues, e.g. with 'whoami' and 'id'. + // + // From the Logs: "Build (ubuntu-18.04, x86_64-unknown-linux-gnu, feat_os_unix, use-cross)" + // whoami: cannot find name for user ID 1001 + // id --name: cannot find name for user ID 1001 + // id --name: cannot find name for group ID 116 + // + // However, when running "id" from within "/bin/bash" it looks fine: + // id: "uid=1001(runner) gid=118(docker) groups=118(docker),4(adm),101(systemd-journal)" + // whoami: "runner" - result.stdout_str().trim().to_string() + // Use environment variable to get current user instead of + // invoking `whoami` and fall back to user "nobody" on error. + std::env::var("USER").unwrap_or_else(|e| { + println!("{}: {}, using \"nobody\" instead", UUTILS_WARNING, e); + "nobody".to_string() + }) } #[test] -fn test_id() { - let scene = TestScenario::new(util_name!()); +#[cfg(unix)] +fn test_id_no_specified_user() { + let result = new_ucmd!().run(); + let exp_result = unwrap_or_return!(expected_result(&[])); + let mut _exp_stdout = exp_result.stdout_str().to_string(); - let result = scene.ucmd().arg("-u").succeeds(); - let uid = result.stdout_str().trim(); - - let result = scene.ucmd().run(); - if skipping_test_is_okay(&result, "Could not find uid") { - return; - } - - // Verify that the id found by --user/-u exists in the list - result.stdout_contains(uid); -} - -#[test] -fn test_id_from_name() { - let username = return_whoami_username(); - if username.is_empty() { - return; - } - - let scene = TestScenario::new(util_name!()); - let result = scene.ucmd().arg(&username).run(); - if skipping_test_is_okay(&result, "Could not find uid") { - return; - } - - let uid = result.stdout_str().trim(); - - let result = scene.ucmd().run(); - if skipping_test_is_okay(&result, "Could not find uid") { - return; + #[cfg(target_os = "linux")] + { + // NOTE: (SELinux NotImplemented) strip 'context' part from exp_stdout: + if let Some(context_offset) = exp_result.stdout_str().find(" context=") { + _exp_stdout.replace_range(context_offset.._exp_stdout.len() - 1, ""); + } } result - // Verify that the id found by --user/-u exists in the list - .stdout_contains(uid) - // Verify that the username found by whoami exists in the list - .stdout_contains(username); + .stdout_is(_exp_stdout) + .stderr_is(exp_result.stderr_str()) + .code_is(exp_result.code()); } #[test] -fn test_id_name_from_id() { - let result = new_ucmd!().arg("-nu").run(); +#[cfg(unix)] +fn test_id_single_user() { + let test_users = [&whoami()[..]]; - let username_id = result.stdout_str().trim(); - - let username_whoami = return_whoami_username(); - if username_whoami.is_empty() { - return; - } - - assert_eq!(username_id, username_whoami); -} - -#[test] -fn test_id_group() { let scene = TestScenario::new(util_name!()); + let mut exp_result = unwrap_or_return!(expected_result(&test_users)); + scene + .ucmd() + .args(&test_users) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) + .code_is(exp_result.code()); - let mut result = scene.ucmd().arg("-g").succeeds(); - let s1 = result.stdout_str().trim(); - assert!(s1.parse::().is_ok()); - - result = scene.ucmd().arg("--group").succeeds(); - let s1 = result.stdout_str().trim(); - assert!(s1.parse::().is_ok()); -} - -#[test] -fn test_id_groups() { - let scene = TestScenario::new(util_name!()); - - let result = scene.ucmd().arg("-G").succeeds(); - let groups = result.stdout_str().trim().split_whitespace(); - for s in groups { - assert!(s.parse::().is_ok()); - } - - let result = scene.ucmd().arg("--groups").succeeds(); - let groups = result.stdout_str().trim().split_whitespace(); - for s in groups { - assert!(s.parse::().is_ok()); + // u/g/G z/n + for &opt in &["--user", "--group", "--groups"] { + let mut args = vec![opt]; + args.extend_from_slice(&test_users); + exp_result = unwrap_or_return!(expected_result(&args)); + scene + .ucmd() + .args(&args) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) + .code_is(exp_result.code()); + args.push("--zero"); + exp_result = unwrap_or_return!(expected_result(&args)); + scene + .ucmd() + .args(&args) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) + .code_is(exp_result.code()); + args.push("--name"); + exp_result = unwrap_or_return!(expected_result(&args)); + scene + .ucmd() + .args(&args) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) + .code_is(exp_result.code()); + args.pop(); + exp_result = unwrap_or_return!(expected_result(&args)); + scene + .ucmd() + .args(&args) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) + .code_is(exp_result.code()); } } #[test] -fn test_id_user() { - let scene = TestScenario::new(util_name!()); +#[cfg(unix)] +fn test_id_single_user_non_existing() { + let args = &["hopefully_non_existing_username"]; + let result = new_ucmd!().args(args).run(); + let exp_result = unwrap_or_return!(expected_result(args)); - let result = scene.ucmd().arg("-u").succeeds(); - let s1 = result.stdout_str().trim(); - assert!(s1.parse::().is_ok()); - - let result = scene.ucmd().arg("--user").succeeds(); - let s1 = result.stdout_str().trim(); - assert!(s1.parse::().is_ok()); + // It is unknown why on macOS (and possibly others?) `id` adds "Invalid argument". + // coreutils 8.32: $ LC_ALL=C id foobar + // macOS: stderr: "id: 'foobar': no such user: Invalid argument" + // linux: stderr: "id: 'foobar': no such user" + result + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) + .code_is(exp_result.code()); } #[test] +#[cfg(unix)] +fn test_id_name() { + let scene = TestScenario::new(util_name!()); + for &opt in &["--user", "--group", "--groups"] { + let args = [opt, "--name"]; + let result = scene.ucmd().args(&args).run(); + let exp_result = unwrap_or_return!(expected_result(&args)); + result + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str()) + .code_is(exp_result.code()); + + if opt == "--user" { + assert_eq!(result.stdout_str().trim_end(), whoami()); + } + } +} + +#[test] +#[cfg(unix)] +fn test_id_real() { + let scene = TestScenario::new(util_name!()); + for &opt in &["--user", "--group", "--groups"] { + let args = [opt, "--real"]; + let result = scene.ucmd().args(&args).run(); + let exp_result = unwrap_or_return!(expected_result(&args)); + result + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str()) + .code_is(exp_result.code()); + } +} + +#[test] +#[cfg(all(unix, not(target_os = "linux")))] fn test_id_pretty_print() { - let username = return_whoami_username(); - if username.is_empty() { - return; - } + // `-p` is BSD only and not supported on GNU's `id` + let username = whoami(); - let scene = TestScenario::new(util_name!()); - let result = scene.ucmd().arg("-p").run(); + let result = new_ucmd!().arg("-p").run(); if result.stdout_str().trim().is_empty() { // this fails only on: "MinRustV (ubuntu-latest, feat_os_unix)" // `rustc 1.40.0 (73528e339 2019-12-16)` @@ -150,20 +189,317 @@ fn test_id_pretty_print() { // stdout = // stderr = ', tests/common/util.rs:157:13 println!("test skipped:"); - return; + } else { + result.success().stdout_contains(username); } - - result.success().stdout_contains(username); } #[test] +#[cfg(all(unix, not(target_os = "linux")))] fn test_id_password_style() { - let username = return_whoami_username(); - if username.is_empty() { + // `-P` is BSD only and not supported on GNU's `id` + let username = whoami(); + let result = new_ucmd!().arg("-P").arg(&username).succeeds(); + assert!(result.stdout_str().starts_with(&username)); +} + +#[test] +#[cfg(unix)] +fn test_id_multiple_users() { + #[cfg(target_os = "linux")] + let util_name = util_name!(); + #[cfg(all(unix, not(target_os = "linux")))] + let util_name = &format!("g{}", util_name!()); + let version_check_string = check_coreutil_version(util_name, VERSION_MIN_MULTIPLE_USERS); + if version_check_string.starts_with(UUTILS_WARNING) { + println!("{}\ntest skipped", version_check_string); return; } - let result = new_ucmd!().arg("-P").succeeds(); + // Same typical users that GNU test suite is using. + let test_users = ["root", "man", "postfix", "sshd", &whoami()]; - assert!(result.stdout_str().starts_with(&username)); + let scene = TestScenario::new(util_name!()); + let mut exp_result = unwrap_or_return!(expected_result(&test_users)); + scene + .ucmd() + .args(&test_users) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) + .code_is(exp_result.code()); + + // u/g/G z/n + for &opt in &["--user", "--group", "--groups"] { + let mut args = vec![opt]; + args.extend_from_slice(&test_users); + exp_result = unwrap_or_return!(expected_result(&args)); + scene + .ucmd() + .args(&args) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) + .code_is(exp_result.code()); + args.push("--zero"); + exp_result = unwrap_or_return!(expected_result(&args)); + scene + .ucmd() + .args(&args) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) + .code_is(exp_result.code()); + args.push("--name"); + exp_result = unwrap_or_return!(expected_result(&args)); + scene + .ucmd() + .args(&args) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) + .code_is(exp_result.code()); + args.pop(); + exp_result = unwrap_or_return!(expected_result(&args)); + scene + .ucmd() + .args(&args) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) + .code_is(exp_result.code()); + } +} + +#[test] +#[cfg(unix)] +fn test_id_multiple_users_non_existing() { + #[cfg(target_os = "linux")] + let util_name = util_name!(); + #[cfg(all(unix, not(target_os = "linux")))] + let util_name = &format!("g{}", util_name!()); + let version_check_string = check_coreutil_version(util_name, VERSION_MIN_MULTIPLE_USERS); + if version_check_string.starts_with(UUTILS_WARNING) { + println!("{}\ntest skipped", version_check_string); + return; + } + + let test_users = [ + "root", + "hopefully_non_existing_username1", + &whoami(), + "man", + "hopefully_non_existing_username2", + "hopefully_non_existing_username3", + "postfix", + "sshd", + "hopefully_non_existing_username4", + &whoami(), + ]; + + let scene = TestScenario::new(util_name!()); + let mut exp_result = unwrap_or_return!(expected_result(&test_users)); + scene + .ucmd() + .args(&test_users) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) + .code_is(exp_result.code()); + + // u/g/G z/n + for &opt in &["--user", "--group", "--groups"] { + let mut args = vec![opt]; + args.extend_from_slice(&test_users); + exp_result = unwrap_or_return!(expected_result(&args)); + scene + .ucmd() + .args(&args) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) + .code_is(exp_result.code()); + args.push("--zero"); + exp_result = unwrap_or_return!(expected_result(&args)); + scene + .ucmd() + .args(&args) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) + .code_is(exp_result.code()); + args.push("--name"); + exp_result = unwrap_or_return!(expected_result(&args)); + scene + .ucmd() + .args(&args) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) + .code_is(exp_result.code()); + args.pop(); + exp_result = unwrap_or_return!(expected_result(&args)); + scene + .ucmd() + .args(&args) + .run() + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str().replace(": Invalid argument", "")) + .code_is(exp_result.code()); + } +} + +#[test] +#[cfg(unix)] +fn test_id_default_format() { + let scene = TestScenario::new(util_name!()); + for &opt1 in &["--name", "--real"] { + // id: cannot print only names or real IDs in default format + let args = [opt1]; + scene + .ucmd() + .args(&args) + .fails() + .stderr_only(unwrap_or_return!(expected_result(&args)).stderr_str()); + for &opt2 in &["--user", "--group", "--groups"] { + // u/g/G n/r + let args = [opt2, opt1]; + let result = scene.ucmd().args(&args).run(); + let exp_result = unwrap_or_return!(expected_result(&args)); + result + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str()) + .code_is(exp_result.code()); + } + } + for &opt2 in &["--user", "--group", "--groups"] { + // u/g/G + let args = [opt2]; + scene + .ucmd() + .args(&args) + .succeeds() + .stdout_only(unwrap_or_return!(expected_result(&args)).stdout_str()); + } +} + +#[test] +#[cfg(unix)] +fn test_id_zero() { + let scene = TestScenario::new(util_name!()); + for z_flag in &["-z", "--zero"] { + // id: option --zero not permitted in default format + scene + .ucmd() + .args(&[z_flag]) + .fails() + .stderr_only(unwrap_or_return!(expected_result(&[z_flag])).stderr_str()); + for &opt1 in &["--name", "--real"] { + // id: cannot print only names or real IDs in default format + let args = [opt1, z_flag]; + scene + .ucmd() + .args(&args) + .fails() + .stderr_only(unwrap_or_return!(expected_result(&args)).stderr_str()); + for &opt2 in &["--user", "--group", "--groups"] { + // u/g/G n/r z + let args = [opt2, z_flag, opt1]; + let result = scene.ucmd().args(&args).run(); + let exp_result = unwrap_or_return!(expected_result(&args)); + result + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str()) + .code_is(exp_result.code()); + } + } + for &opt2 in &["--user", "--group", "--groups"] { + // u/g/G z + let args = [opt2, z_flag]; + scene + .ucmd() + .args(&args) + .succeeds() + .stdout_only(unwrap_or_return!(expected_result(&args)).stdout_str()); + } + } +} + +fn check_coreutil_version(util_name: &str, version_expected: &str) -> String { + // example: + // $ id --version | head -n 1 + // id (GNU coreutils) 8.32.162-4eda + let scene = TestScenario::new(util_name); + let version_check = scene + .cmd_keepenv(&util_name) + .env("LC_ALL", "C") + .arg("--version") + .run(); + version_check + .stdout_str() + .split('\n') + .collect::>() + .get(0) + .map_or_else( + || format!("{}: unexpected output format for reference coreutil: '{} --version'", UUTILS_WARNING, util_name), + |s| { + if s.contains(&format!("(GNU coreutils) {}", version_expected)) { + s.to_string() + } else if s.contains("(GNU coreutils)") { + let version_found = s.split_whitespace().last().unwrap()[..4].parse::().unwrap_or_default(); + let version_expected = version_expected.parse::().unwrap_or_default(); + if version_found > version_expected { + format!("{}: version for the reference coreutil '{}' is higher than expected; expected: {}, found: {}", UUTILS_INFO, util_name, version_expected, version_found) + } else { + format!("{}: version for the reference coreutil '{}' does not match; expected: {}, found: {}", UUTILS_WARNING, util_name, version_expected, version_found) } + } else { + format!("{}: no coreutils version string found for reference coreutils '{} --version'", UUTILS_WARNING, util_name) + } + }, + ) +} + +#[allow(clippy::needless_borrow)] +#[cfg(unix)] +fn expected_result(args: &[&str]) -> Result { + #[cfg(target_os = "linux")] + let util_name = util_name!(); + #[cfg(all(unix, not(target_os = "linux")))] + let util_name = &format!("g{}", util_name!()); + + let version_check_string = check_coreutil_version(util_name, VERSION_MIN); + if version_check_string.starts_with(UUTILS_WARNING) { + return Err(version_check_string); + } + println!("{}", version_check_string); + + let scene = TestScenario::new(util_name); + let result = scene + .cmd_keepenv(util_name) + .env("LC_ALL", "C") + .args(args) + .run(); + + let (stdout, stderr): (String, String) = if cfg!(target_os = "linux") { + ( + result.stdout_str().to_string(), + result.stderr_str().to_string(), + ) + } else { + // strip 'g' prefix from results: + let from = util_name.to_string() + ":"; + let to = &from[1..]; + ( + result.stdout_str().replace(&from, to), + result.stderr_str().replace(&from, to), + ) + }; + + Ok(CmdResult::new( + Some(result.tmpd()), + Some(result.code()), + result.succeeded(), + stdout.as_bytes(), + stderr.as_bytes(), + )) } diff --git a/tests/by-util/test_install.rs b/tests/by-util/test_install.rs index 3ab5cbdfb..06808db6b 100644 --- a/tests/by-util/test_install.rs +++ b/tests/by-util/test_install.rs @@ -674,3 +674,410 @@ fn test_install_creating_leading_dir_fails_on_long_name() { .fails() .stderr_contains("failed to create"); } + +#[test] +fn test_install_dir() { + let (at, mut ucmd) = at_and_ucmd!(); + let dir = "target_dir"; + let file1 = "source_file1"; + let file2 = "source_file2"; + + at.touch(file1); + at.touch(file2); + at.mkdir(dir); + ucmd.arg(file1) + .arg(file2) + .arg(&format!("--target-directory={}", dir)) + .succeeds() + .no_stderr(); + + assert!(at.file_exists(file1)); + assert!(at.file_exists(file2)); + assert!(at.file_exists(&format!("{}/{}", dir, file1))); + assert!(at.file_exists(&format!("{}/{}", dir, file2))); +} +// +// test backup functionality +#[test] +fn test_install_backup_short_no_args_files() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let file_a = "test_install_simple_backup_file_a"; + let file_b = "test_install_simple_backup_file_b"; + + at.touch(file_a); + at.touch(file_b); + scene + .ucmd() + .arg("-b") + .arg(file_a) + .arg(file_b) + .succeeds() + .no_stderr(); + + assert!(at.file_exists(file_a)); + assert!(at.file_exists(file_b)); + assert!(at.file_exists(&format!("{}~", file_b))); +} + +#[test] +fn test_install_backup_short_no_args_file_to_dir() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let file = "test_install_simple_backup_file_a"; + let dest_dir = "test_install_dest/"; + let expect = format!("{}{}", dest_dir, file); + + at.touch(file); + at.mkdir(dest_dir); + at.touch(&expect); + scene + .ucmd() + .arg("-b") + .arg(file) + .arg(dest_dir) + .succeeds() + .no_stderr(); + + assert!(at.file_exists(file)); + assert!(at.file_exists(&expect)); + assert!(at.file_exists(&format!("{}~", expect))); +} + +// Long --backup option is tested separately as it requires a slightly different +// handling than '-b' does. +#[test] +fn test_install_backup_long_no_args_files() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let file_a = "test_install_simple_backup_file_a"; + let file_b = "test_install_simple_backup_file_b"; + + at.touch(file_a); + at.touch(file_b); + scene + .ucmd() + .arg("--backup") + .arg(file_a) + .arg(file_b) + .succeeds() + .no_stderr(); + + assert!(at.file_exists(file_a)); + assert!(at.file_exists(file_b)); + assert!(at.file_exists(&format!("{}~", file_b))); +} + +#[test] +fn test_install_backup_long_no_args_file_to_dir() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let file = "test_install_simple_backup_file_a"; + let dest_dir = "test_install_dest/"; + let expect = format!("{}{}", dest_dir, file); + + at.touch(file); + at.mkdir(dest_dir); + at.touch(&expect); + scene + .ucmd() + .arg("--backup") + .arg(file) + .arg(dest_dir) + .succeeds() + .no_stderr(); + + assert!(at.file_exists(file)); + assert!(at.file_exists(&expect)); + assert!(at.file_exists(&format!("{}~", expect))); +} + +#[test] +fn test_install_backup_short_custom_suffix() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let file_a = "test_install_backup_custom_suffix_file_a"; + let file_b = "test_install_backup_custom_suffix_file_b"; + let suffix = "super-suffix-of-the-century"; + + at.touch(file_a); + at.touch(file_b); + scene + .ucmd() + .arg("-b") + .arg(format!("--suffix={}", suffix)) + .arg(file_a) + .arg(file_b) + .succeeds() + .no_stderr(); + + assert!(at.file_exists(file_a)); + assert!(at.file_exists(file_b)); + assert!(at.file_exists(&format!("{}{}", file_b, suffix))); +} + +#[test] +fn test_install_backup_custom_suffix_via_env() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let file_a = "test_install_backup_custom_suffix_file_a"; + let file_b = "test_install_backup_custom_suffix_file_b"; + let suffix = "super-suffix-of-the-century"; + + at.touch(file_a); + at.touch(file_b); + scene + .ucmd() + .arg("-b") + .env("SIMPLE_BACKUP_SUFFIX", suffix) + .arg(file_a) + .arg(file_b) + .succeeds() + .no_stderr(); + + assert!(at.file_exists(file_a)); + assert!(at.file_exists(file_b)); + assert!(at.file_exists(&format!("{}{}", file_b, suffix))); +} + +#[test] +fn test_install_backup_numbered_with_t() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let file_a = "test_install_backup_numbering_file_a"; + let file_b = "test_install_backup_numbering_file_b"; + + at.touch(file_a); + at.touch(file_b); + scene + .ucmd() + .arg("--backup=t") + .arg(file_a) + .arg(file_b) + .succeeds() + .no_stderr(); + + assert!(at.file_exists(file_a)); + assert!(at.file_exists(file_b)); + assert!(at.file_exists(&format!("{}.~1~", file_b))); +} + +#[test] +fn test_install_backup_numbered_with_numbered() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let file_a = "test_install_backup_numbering_file_a"; + let file_b = "test_install_backup_numbering_file_b"; + + at.touch(file_a); + at.touch(file_b); + scene + .ucmd() + .arg("--backup=numbered") + .arg(file_a) + .arg(file_b) + .succeeds() + .no_stderr(); + + assert!(at.file_exists(file_a)); + assert!(at.file_exists(file_b)); + assert!(at.file_exists(&format!("{}.~1~", file_b))); +} + +#[test] +fn test_install_backup_existing() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let file_a = "test_install_backup_numbering_file_a"; + let file_b = "test_install_backup_numbering_file_b"; + + at.touch(file_a); + at.touch(file_b); + scene + .ucmd() + .arg("--backup=existing") + .arg(file_a) + .arg(file_b) + .succeeds() + .no_stderr(); + + assert!(at.file_exists(file_a)); + assert!(at.file_exists(file_b)); + assert!(at.file_exists(&format!("{}~", file_b))); +} + +#[test] +fn test_install_backup_nil() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let file_a = "test_install_backup_numbering_file_a"; + let file_b = "test_install_backup_numbering_file_b"; + + at.touch(file_a); + at.touch(file_b); + scene + .ucmd() + .arg("--backup=nil") + .arg(file_a) + .arg(file_b) + .succeeds() + .no_stderr(); + + assert!(at.file_exists(file_a)); + assert!(at.file_exists(file_b)); + assert!(at.file_exists(&format!("{}~", file_b))); +} + +#[test] +fn test_install_backup_numbered_if_existing_backup_existing() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let file_a = "test_install_backup_numbering_file_a"; + let file_b = "test_install_backup_numbering_file_b"; + let file_b_backup = "test_install_backup_numbering_file_b.~1~"; + + at.touch(file_a); + at.touch(file_b); + at.touch(file_b_backup); + scene + .ucmd() + .arg("--backup=existing") + .arg(file_a) + .arg(file_b) + .succeeds() + .no_stderr(); + + assert!(at.file_exists(file_a)); + assert!(at.file_exists(file_b)); + assert!(at.file_exists(file_b_backup)); + assert!(at.file_exists(&*format!("{}.~2~", file_b))); +} + +#[test] +fn test_install_backup_numbered_if_existing_backup_nil() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let file_a = "test_install_backup_numbering_file_a"; + let file_b = "test_install_backup_numbering_file_b"; + let file_b_backup = "test_install_backup_numbering_file_b.~1~"; + + at.touch(file_a); + at.touch(file_b); + at.touch(file_b_backup); + scene + .ucmd() + .arg("--backup=nil") + .arg(file_a) + .arg(file_b) + .succeeds() + .no_stderr(); + + assert!(at.file_exists(file_a)); + assert!(at.file_exists(file_b)); + assert!(at.file_exists(file_b_backup)); + assert!(at.file_exists(&*format!("{}.~2~", file_b))); +} + +#[test] +fn test_install_backup_simple() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let file_a = "test_install_backup_numbering_file_a"; + let file_b = "test_install_backup_numbering_file_b"; + + at.touch(file_a); + at.touch(file_b); + scene + .ucmd() + .arg("--backup=simple") + .arg(file_a) + .arg(file_b) + .succeeds() + .no_stderr(); + + assert!(at.file_exists(file_a)); + assert!(at.file_exists(file_b)); + assert!(at.file_exists(&format!("{}~", file_b))); +} + +#[test] +fn test_install_backup_never() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let file_a = "test_install_backup_numbering_file_a"; + let file_b = "test_install_backup_numbering_file_b"; + + at.touch(file_a); + at.touch(file_b); + scene + .ucmd() + .arg("--backup=never") + .arg(file_a) + .arg(file_b) + .succeeds() + .no_stderr(); + + assert!(at.file_exists(file_a)); + assert!(at.file_exists(file_b)); + assert!(at.file_exists(&format!("{}~", file_b))); +} + +#[test] +fn test_install_backup_none() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let file_a = "test_install_backup_numbering_file_a"; + let file_b = "test_install_backup_numbering_file_b"; + + at.touch(file_a); + at.touch(file_b); + scene + .ucmd() + .arg("--backup=none") + .arg(file_a) + .arg(file_b) + .succeeds() + .no_stderr(); + + assert!(at.file_exists(file_a)); + assert!(at.file_exists(file_b)); + assert!(!at.file_exists(&format!("{}~", file_b))); +} + +#[test] +fn test_install_backup_off() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let file_a = "test_install_backup_numbering_file_a"; + let file_b = "test_install_backup_numbering_file_b"; + + at.touch(file_a); + at.touch(file_b); + scene + .ucmd() + .arg("--backup=off") + .arg(file_a) + .arg(file_b) + .succeeds() + .no_stderr(); + + assert!(at.file_exists(file_a)); + assert!(at.file_exists(file_b)); + assert!(!at.file_exists(&format!("{}~", file_b))); +} diff --git a/tests/by-util/test_ln.rs b/tests/by-util/test_ln.rs index e475e3608..9fa73c0bc 100644 --- a/tests/by-util/test_ln.rs +++ b/tests/by-util/test_ln.rs @@ -428,20 +428,6 @@ fn test_symlink_relative() { assert_eq!(at.resolve_link(link), file_a); } -#[test] -fn test_hardlink_relative() { - let (at, mut ucmd) = at_and_ucmd!(); - let file_a = "test_hardlink_relative_a"; - let link = "test_hardlink_relative_link"; - - at.touch(file_a); - - // relative hardlink - ucmd.args(&["-r", "-v", file_a, link]) - .succeeds() - .stdout_only(format!("'{}' -> '{}'\n", link, file_a)); -} - #[test] fn test_symlink_relative_path() { let (at, mut ucmd) = at_and_ucmd!(); @@ -571,3 +557,34 @@ fn test_symlink_no_deref_file() { assert!(at.is_symlink(link)); assert_eq!(at.resolve_link(link), file1); } + +#[test] +fn test_relative_requires_symbolic() { + new_ucmd!().args(&["-r", "foo", "bar"]).fails(); +} + +#[test] +fn test_relative_dst_already_symlink() { + let (at, mut ucmd) = at_and_ucmd!(); + at.touch("file1"); + at.symlink_file("file1", "file2"); + ucmd.arg("-srf").arg("file1").arg("file2").succeeds(); + at.is_symlink("file2"); +} + +#[test] +fn test_relative_src_already_symlink() { + let (at, mut ucmd) = at_and_ucmd!(); + at.touch("file1"); + at.symlink_file("file1", "file2"); + ucmd.arg("-sr").arg("file2").arg("file3").succeeds(); + assert!(at.resolve_link("file3").ends_with("file1")); +} + +#[test] +fn test_relative_recursive() { + let (at, mut ucmd) = at_and_ucmd!(); + at.mkdir("dir"); + ucmd.args(&["-sr", "dir", "dir/recursive"]).succeeds(); + assert_eq!(at.resolve_link("dir/recursive"), "."); +} diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index 20c6b913d..44d14c304 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -168,7 +168,7 @@ fn test_ls_width() { .ucmd() .args(&option.split(' ').collect::>()) .fails() - .stderr_only("ls: invalid line width: ‘1a’"); + .stderr_only("ls: invalid line width: '1a'"); } } @@ -398,7 +398,7 @@ fn test_ls_long_formats() { .arg("--author") .arg("test-long-formats") .succeeds(); - assert!(re_three.is_match(&result.stdout_str())); + assert!(re_three.is_match(result.stdout_str())); #[cfg(unix)] { @@ -701,20 +701,20 @@ fn test_ls_styles() { .arg("-l") .arg("--time-style=full-iso") .succeeds(); - assert!(re_full.is_match(&result.stdout_str())); + assert!(re_full.is_match(result.stdout_str())); //long-iso let result = scene .ucmd() .arg("-l") .arg("--time-style=long-iso") .succeeds(); - assert!(re_long.is_match(&result.stdout_str())); + assert!(re_long.is_match(result.stdout_str())); //iso let result = scene.ucmd().arg("-l").arg("--time-style=iso").succeeds(); - assert!(re_iso.is_match(&result.stdout_str())); + assert!(re_iso.is_match(result.stdout_str())); //locale let result = scene.ucmd().arg("-l").arg("--time-style=locale").succeeds(); - assert!(re_locale.is_match(&result.stdout_str())); + assert!(re_locale.is_match(result.stdout_str())); //Overwrite options tests let result = scene @@ -723,19 +723,19 @@ fn test_ls_styles() { .arg("--time-style=long-iso") .arg("--time-style=iso") .succeeds(); - assert!(re_iso.is_match(&result.stdout_str())); + assert!(re_iso.is_match(result.stdout_str())); let result = scene .ucmd() .arg("--time-style=iso") .arg("--full-time") .succeeds(); - assert!(re_full.is_match(&result.stdout_str())); + assert!(re_full.is_match(result.stdout_str())); let result = scene .ucmd() .arg("--full-time") .arg("--time-style=iso") .succeeds(); - assert!(re_iso.is_match(&result.stdout_str())); + assert!(re_iso.is_match(result.stdout_str())); let result = scene .ucmd() @@ -743,7 +743,7 @@ fn test_ls_styles() { .arg("--time-style=iso") .arg("--full-time") .succeeds(); - assert!(re_full.is_match(&result.stdout_str())); + assert!(re_full.is_match(result.stdout_str())); let result = scene .ucmd() @@ -751,7 +751,7 @@ fn test_ls_styles() { .arg("-x") .arg("-l") .succeeds(); - assert!(re_full.is_match(&result.stdout_str())); + assert!(re_full.is_match(result.stdout_str())); at.touch("test2"); let result = scene.ucmd().arg("--full-time").arg("-x").succeeds(); @@ -1143,7 +1143,7 @@ fn test_ls_indicator_style() { for opt in options { scene .ucmd() - .arg(format!("{}", opt)) + .arg(opt.to_string()) .succeeds() .stdout_contains(&"/"); } @@ -2021,3 +2021,28 @@ fn test_ls_path() { .run() .stdout_is(expected_stdout); } + +#[test] +fn test_ls_dangling_symlinks() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.mkdir("temp_dir"); + at.symlink_file("does_not_exist", "temp_dir/dangle"); + + scene.ucmd().arg("-L").arg("temp_dir/dangle").fails(); + scene.ucmd().arg("-H").arg("temp_dir/dangle").fails(); + + scene + .ucmd() + .arg("temp_dir/dangle") + .succeeds() + .stdout_contains("dangle"); + + scene + .ucmd() + .arg("-Li") + .arg("temp_dir") + .succeeds() // this should fail, though at the moment, ls lacks a way to propagate errors encountered during display + .stdout_contains(if cfg!(windows) { "dangle" } else { "? dangle" }); +} diff --git a/tests/by-util/test_mktemp.rs b/tests/by-util/test_mktemp.rs index d601bad5b..e824df061 100644 --- a/tests/by-util/test_mktemp.rs +++ b/tests/by-util/test_mktemp.rs @@ -17,7 +17,10 @@ static TEST_TEMPLATE8: &str = "tempXXXl/ate"; #[cfg(windows)] static TEST_TEMPLATE8: &str = "tempXXXl\\ate"; +#[cfg(not(windows))] const TMPDIR: &str = "TMPDIR"; +#[cfg(windows)] +const TMPDIR: &str = "TMP"; #[test] fn test_mktemp_mktemp() { @@ -122,7 +125,8 @@ fn test_mktemp_mktemp_t() { .arg(TEST_TEMPLATE8) .fails() .no_stdout() - .stderr_contains("suffix cannot contain any path separators"); + .stderr_contains("invalid suffix") + .stderr_contains("contains directory separator"); } #[test] @@ -386,7 +390,7 @@ fn test_mktemp_tmpdir_one_arg() { let scene = TestScenario::new(util_name!()); let result = scene - .ucmd() + .ucmd_keepenv() .arg("--tmpdir") .arg("apt-key-gpghome.XXXXXXXXXX") .succeeds(); @@ -399,7 +403,7 @@ fn test_mktemp_directory_tmpdir() { let scene = TestScenario::new(util_name!()); let result = scene - .ucmd() + .ucmd_keepenv() .arg("--directory") .arg("--tmpdir") .arg("apt-key-gpghome.XXXXXXXXXX") diff --git a/tests/by-util/test_mv.rs b/tests/by-util/test_mv.rs index 2f35bf5eb..02c65f68d 100644 --- a/tests/by-util/test_mv.rs +++ b/tests/by-util/test_mv.rs @@ -614,7 +614,7 @@ fn test_mv_overwrite_nonempty_dir() { // Not same error as GNU; the error message is a rust builtin // TODO: test (and implement) correct error message (or at least decide whether to do so) // Current: "mv: couldn't rename path (Directory not empty; from=a; to=b)" - // GNU: "mv: cannot move ‘a’ to ‘b’: Directory not empty" + // GNU: "mv: cannot move 'a' to 'b': Directory not empty" // Verbose output for the move should not be shown on failure let result = ucmd.arg("-vT").arg(dir_a).arg(dir_b).fails(); @@ -638,7 +638,7 @@ fn test_mv_backup_dir() { .arg(dir_b) .succeeds() .stdout_only(format!( - "‘{}’ -> ‘{}’ (backup: ‘{}~’)\n", + "'{}' -> '{}' (backup: '{}~')\n", dir_a, dir_b, dir_b )); @@ -672,7 +672,7 @@ fn test_mv_errors() { // $ at.touch file && at.mkdir dir // $ mv -T file dir - // err == mv: cannot overwrite directory ‘dir’ with non-directory + // err == mv: cannot overwrite directory 'dir' with non-directory scene .ucmd() .arg("-T") @@ -680,13 +680,13 @@ fn test_mv_errors() { .arg(dir) .fails() .stderr_is(format!( - "mv: cannot overwrite directory ‘{}’ with non-directory\n", + "mv: cannot overwrite directory '{}' with non-directory\n", dir )); // $ at.mkdir dir && at.touch file // $ mv dir file - // err == mv: cannot overwrite non-directory ‘file’ with directory ‘dir’ + // err == mv: cannot overwrite non-directory 'file' with directory 'dir' assert!(!scene .ucmd() .arg(dir) @@ -713,7 +713,7 @@ fn test_mv_verbose() { .arg(file_a) .arg(file_b) .succeeds() - .stdout_only(format!("‘{}’ -> ‘{}’\n", file_a, file_b)); + .stdout_only(format!("'{}' -> '{}'\n", file_a, file_b)); at.touch(file_a); scene @@ -723,12 +723,13 @@ fn test_mv_verbose() { .arg(file_b) .succeeds() .stdout_only(format!( - "‘{}’ -> ‘{}’ (backup: ‘{}~’)\n", + "'{}' -> '{}' (backup: '{}~')\n", file_a, file_b, file_b )); } #[test] +#[cfg(target_os = "linux")] // mkdir does not support -m on windows. Freebsd doesn't return a permission error either. fn test_mv_permission_error() { let scene = TestScenario::new("mkdir"); let folder1 = "bar"; @@ -738,12 +739,11 @@ fn test_mv_permission_error() { scene.ucmd().arg("-m777").arg(folder2).succeeds(); scene - .cmd_keepenv(util_name!()) + .ccmd("mv") .arg(folder2) .arg(folder_to_move) - .run() - .stderr_str() - .ends_with("Permission denied"); + .fails() + .stderr_contains("Permission denied"); } // Todo: @@ -756,5 +756,5 @@ fn test_mv_permission_error() { // -r--r--r-- 1 user user 0 okt 25 11:21 b // $ // $ mv -v a b -// mv: try to overwrite ‘b’, overriding mode 0444 (r--r--r--)? y -// ‘a’ -> ‘b’ +// mv: try to overwrite 'b', overriding mode 0444 (r--r--r--)? y +// 'a' -> 'b' diff --git a/tests/by-util/test_numfmt.rs b/tests/by-util/test_numfmt.rs index bb29d431e..336b0f7cd 100644 --- a/tests/by-util/test_numfmt.rs +++ b/tests/by-util/test_numfmt.rs @@ -35,7 +35,7 @@ fn test_from_iec_i_requires_suffix() { new_ucmd!() .args(&["--from=iec-i", "1024"]) .fails() - .stderr_is("numfmt: missing 'i' suffix in input: ‘1024’ (e.g Ki/Mi/Gi)"); + .stderr_is("numfmt: missing 'i' suffix in input: '1024' (e.g Ki/Mi/Gi)"); } #[test] @@ -123,7 +123,7 @@ fn test_header_error_if_non_numeric() { new_ucmd!() .args(&["--header=two"]) .run() - .stderr_is("numfmt: invalid header value ‘two’"); + .stderr_is("numfmt: invalid header value 'two'"); } #[test] @@ -131,7 +131,7 @@ fn test_header_error_if_0() { new_ucmd!() .args(&["--header=0"]) .run() - .stderr_is("numfmt: invalid header value ‘0’"); + .stderr_is("numfmt: invalid header value '0'"); } #[test] @@ -139,7 +139,7 @@ fn test_header_error_if_negative() { new_ucmd!() .args(&["--header=-3"]) .run() - .stderr_is("numfmt: invalid header value ‘-3’"); + .stderr_is("numfmt: invalid header value '-3'"); } #[test] @@ -187,7 +187,7 @@ fn test_should_report_invalid_empty_number_on_empty_stdin() { .args(&["--from=auto"]) .pipe_in("\n") .run() - .stderr_is("numfmt: invalid number: ‘’\n"); + .stderr_is("numfmt: invalid number: ''\n"); } #[test] @@ -196,7 +196,7 @@ fn test_should_report_invalid_empty_number_on_blank_stdin() { .args(&["--from=auto"]) .pipe_in(" \t \n") .run() - .stderr_is("numfmt: invalid number: ‘’\n"); + .stderr_is("numfmt: invalid number: ''\n"); } #[test] @@ -205,14 +205,14 @@ fn test_should_report_invalid_suffix_on_stdin() { .args(&["--from=auto"]) .pipe_in("1k") .run() - .stderr_is("numfmt: invalid suffix in input: ‘1k’\n"); + .stderr_is("numfmt: invalid suffix in input: '1k'\n"); // GNU numfmt reports this one as “invalid number” new_ucmd!() .args(&["--from=auto"]) .pipe_in("NaN") .run() - .stderr_is("numfmt: invalid suffix in input: ‘NaN’\n"); + .stderr_is("numfmt: invalid suffix in input: 'NaN'\n"); } #[test] @@ -222,7 +222,7 @@ fn test_should_report_invalid_number_with_interior_junk() { .args(&["--from=auto"]) .pipe_in("1x0K") .run() - .stderr_is("numfmt: invalid number: ‘1x0K’\n"); + .stderr_is("numfmt: invalid number: '1x0K'\n"); } #[test] @@ -461,7 +461,7 @@ fn test_delimiter_overrides_whitespace_separator() { .args(&["-d,"]) .pipe_in("1 234,56") .fails() - .stderr_is("numfmt: invalid number: ‘1 234’\n"); + .stderr_is("numfmt: invalid number: '1 234'\n"); } #[test] @@ -481,3 +481,27 @@ fn test_delimiter_with_padding_and_fields() { .succeeds() .stdout_only(" 1.0K| 2.0K\n"); } + +#[test] +fn test_round() { + for (method, exp) in &[ + ("from-zero", ["9.1K", "-9.1K", "9.1K", "-9.1K"]), + ("towards-zero", ["9.0K", "-9.0K", "9.0K", "-9.0K"]), + ("up", ["9.1K", "-9.0K", "9.1K", "-9.0K"]), + ("down", ["9.0K", "-9.1K", "9.0K", "-9.1K"]), + ("nearest", ["9.0K", "-9.0K", "9.1K", "-9.1K"]), + ] { + new_ucmd!() + .args(&[ + "--to=si", + &format!("--round={}", method), + "--", + "9001", + "-9001", + "9099", + "-9099", + ]) + .succeeds() + .stdout_only(exp.join("\n") + "\n"); + } +} diff --git a/tests/by-util/test_od.rs b/tests/by-util/test_od.rs index c21c683dc..33d7d4dc4 100644 --- a/tests/by-util/test_od.rs +++ b/tests/by-util/test_od.rs @@ -1,3 +1,8 @@ +// * 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. + extern crate unindent; use self::unindent::*; @@ -804,3 +809,40 @@ fn test_traditional_only_label() { ", )); } + +#[test] +fn test_od_invalid_bytes() { + const INVALID_SIZE: &str = "1fb4t"; + const BIG_SIZE: &str = "1Y"; + + // NOTE: + // GNU's od (8.32) with option '--width' does not accept 'Y' as valid suffix. + // According to the man page it should be valid in the same way it is valid for + // '--read-bytes' and '--skip-bytes'. + + let options = [ + "--read-bytes", + "--skip-bytes", + "--width", + // "--strings", // TODO: consider testing here once '--strings' is implemented + ]; + for option in &options { + new_ucmd!() + .arg(format!("{}={}", option, INVALID_SIZE)) + .arg("file") + .fails() + .code_is(1) + .stderr_only(format!( + "od: invalid {} argument '{}'", + option, INVALID_SIZE + )); + + #[cfg(not(target_pointer_width = "128"))] + new_ucmd!() + .arg(format!("{}={}", option, BIG_SIZE)) + .arg("file") + .fails() + .code_is(1) + .stderr_only(format!("od: {} argument '{}' too large", option, BIG_SIZE)); + } +} diff --git a/tests/by-util/test_pathchk.rs b/tests/by-util/test_pathchk.rs index 3bc12f0b6..8ba3b9033 100644 --- a/tests/by-util/test_pathchk.rs +++ b/tests/by-util/test_pathchk.rs @@ -38,7 +38,7 @@ fn test_posix_mode() { // fail on long path new_ucmd!() - .args(&["-p", &"dir".repeat(libc::PATH_MAX as usize + 1).as_str()]) + .args(&["-p", "dir".repeat(libc::PATH_MAX as usize + 1).as_str()]) .fails() .no_stdout(); @@ -46,7 +46,7 @@ fn test_posix_mode() { new_ucmd!() .args(&[ "-p", - &format!("dir/{}", "file".repeat(libc::FILENAME_MAX as usize + 1)).as_str(), + format!("dir/{}", "file".repeat(libc::FILENAME_MAX as usize + 1)).as_str(), ]) .fails() .no_stdout(); @@ -76,7 +76,7 @@ fn test_posix_special() { // fail on long path new_ucmd!() - .args(&["-P", &"dir".repeat(libc::PATH_MAX as usize + 1).as_str()]) + .args(&["-P", "dir".repeat(libc::PATH_MAX as usize + 1).as_str()]) .fails() .no_stdout(); @@ -84,7 +84,7 @@ fn test_posix_special() { new_ucmd!() .args(&[ "-P", - &format!("dir/{}", "file".repeat(libc::FILENAME_MAX as usize + 1)).as_str(), + format!("dir/{}", "file".repeat(libc::FILENAME_MAX as usize + 1)).as_str(), ]) .fails() .no_stdout(); @@ -117,7 +117,7 @@ fn test_posix_all() { .args(&[ "-p", "-P", - &"dir".repeat(libc::PATH_MAX as usize + 1).as_str(), + "dir".repeat(libc::PATH_MAX as usize + 1).as_str(), ]) .fails() .no_stdout(); @@ -127,7 +127,7 @@ fn test_posix_all() { .args(&[ "-p", "-P", - &format!("dir/{}", "file".repeat(libc::FILENAME_MAX as usize + 1)).as_str(), + format!("dir/{}", "file".repeat(libc::FILENAME_MAX as usize + 1)).as_str(), ]) .fails() .no_stdout(); diff --git a/tests/by-util/test_pinky.rs b/tests/by-util/test_pinky.rs index 0813e5e1b..bc2833a42 100644 --- a/tests/by-util/test_pinky.rs +++ b/tests/by-util/test_pinky.rs @@ -102,9 +102,11 @@ fn expected_result(args: &[&str]) -> String { #[cfg(target_vendor = "apple")] let util_name = format!("g{}", util_name!()); + // note: clippy::needless_borrow *false positive* + #[allow(clippy::needless_borrow)] TestScenario::new(&util_name) .cmd_keepenv(util_name) - .env("LANGUAGE", "C") + .env("LC_ALL", "C") .args(args) .succeeds() .stdout_move_str() diff --git a/tests/by-util/test_pr.rs b/tests/by-util/test_pr.rs index def361fab..4a79a3eda 100644 --- a/tests/by-util/test_pr.rs +++ b/tests/by-util/test_pr.rs @@ -3,6 +3,7 @@ use crate::common::util::*; use chrono::offset::Local; use chrono::DateTime; +use chrono::Duration; use std::fs::metadata; fn file_last_modified_time(ucmd: &UCommand, path: &str) -> String { @@ -20,8 +21,23 @@ fn file_last_modified_time(ucmd: &UCommand, path: &str) -> String { .unwrap_or_default() } -fn now_time() -> String { - Local::now().format("%b %d %H:%M %Y").to_string() +fn all_minutes(from: DateTime, to: DateTime) -> Vec { + let to = to + Duration::minutes(1); + const FORMAT: &str = "%b %d %H:%M %Y"; + let mut vec = vec![]; + let mut current = from; + while current < to { + vec.push(current.format(FORMAT).to_string()); + current = current + Duration::minutes(1); + } + vec +} + +fn valid_last_modified_template_vars(from: DateTime) -> Vec> { + all_minutes(from, Local::now()) + .into_iter() + .map(|time| vec![("{last_modified_time}".to_string(), time)]) + .collect() } #[test] @@ -33,10 +49,7 @@ fn test_without_any_options() { scenario .args(&[test_file_path]) .succeeds() - .stdout_is_templated_fixture( - expected_test_file_path, - vec![(&"{last_modified_time}".to_string(), &value)], - ); + .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]); } #[test] @@ -48,10 +61,7 @@ fn test_with_numbering_option_with_number_width() { scenario .args(&["-n", "2", test_file_path]) .succeeds() - .stdout_is_templated_fixture( - expected_test_file_path, - vec![(&"{last_modified_time}".to_string(), &value)], - ); + .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]); } #[test] @@ -66,10 +76,7 @@ fn test_with_long_header_option() { .succeeds() .stdout_is_templated_fixture( expected_test_file_path, - vec![ - (&"{last_modified_time}".to_string(), &value), - (&"{header}".to_string(), &header.to_string()), - ], + &[("{last_modified_time}", &value), ("{header}", header)], ); new_ucmd!() @@ -77,10 +84,7 @@ fn test_with_long_header_option() { .succeeds() .stdout_is_templated_fixture( expected_test_file_path, - vec![ - (&"{last_modified_time}".to_string(), &value), - (&"{header}".to_string(), &header.to_string()), - ], + &[("{last_modified_time}", &value), ("{header}", header)], ); } @@ -93,18 +97,12 @@ fn test_with_double_space_option() { scenario .args(&["-d", test_file_path]) .succeeds() - .stdout_is_templated_fixture( - expected_test_file_path, - vec![(&"{last_modified_time}".to_string(), &value)], - ); + .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]); new_ucmd!() .args(&["--double-space", test_file_path]) .succeeds() - .stdout_is_templated_fixture( - expected_test_file_path, - vec![(&"{last_modified_time}".to_string(), &value)], - ); + .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]); } #[test] @@ -116,10 +114,7 @@ fn test_with_first_line_number_option() { scenario .args(&["-N", "5", "-n", test_file_path]) .succeeds() - .stdout_is_templated_fixture( - expected_test_file_path, - vec![(&"{last_modified_time}".to_string(), &value)], - ); + .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]); } #[test] @@ -131,10 +126,7 @@ fn test_with_first_line_number_long_option() { scenario .args(&["--first-line-number=5", "-n", test_file_path]) .succeeds() - .stdout_is_templated_fixture( - expected_test_file_path, - vec![(&"{last_modified_time}".to_string(), &value)], - ); + .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]); } #[test] @@ -146,10 +138,7 @@ fn test_with_number_option_with_custom_separator_char() { scenario .args(&["-nc", test_file_path]) .succeeds() - .stdout_is_templated_fixture( - expected_test_file_path, - vec![(&"{last_modified_time}".to_string(), &value)], - ); + .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]); } #[test] @@ -161,10 +150,7 @@ fn test_with_number_option_with_custom_separator_char_and_width() { scenario .args(&["-nc1", test_file_path]) .succeeds() - .stdout_is_templated_fixture( - expected_test_file_path, - vec![(&"{last_modified_time}".to_string(), &value)], - ); + .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]); } #[test] @@ -207,25 +193,19 @@ fn test_with_page_range() { scenario .args(&["--pages=15", test_file_path]) .succeeds() - .stdout_is_templated_fixture( - expected_test_file_path, - vec![(&"{last_modified_time}".to_string(), &value)], - ); + .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]); new_ucmd!() .args(&["+15", test_file_path]) .succeeds() - .stdout_is_templated_fixture( - expected_test_file_path, - vec![(&"{last_modified_time}".to_string(), &value)], - ); + .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]); new_ucmd!() .args(&["--pages=15:17", test_file_path]) .succeeds() .stdout_is_templated_fixture( expected_test_file_path1, - vec![(&"{last_modified_time}".to_string(), &value)], + &[("{last_modified_time}", &value)], ); new_ucmd!() @@ -233,7 +213,7 @@ fn test_with_page_range() { .succeeds() .stdout_is_templated_fixture( expected_test_file_path1, - vec![(&"{last_modified_time}".to_string(), &value)], + &[("{last_modified_time}", &value)], ); } @@ -246,10 +226,7 @@ fn test_with_no_header_trailer_option() { scenario .args(&["-t", test_file_path]) .succeeds() - .stdout_is_templated_fixture( - expected_test_file_path, - vec![(&"{last_modified_time}".to_string(), &value)], - ); + .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]); } #[test] @@ -262,10 +239,7 @@ fn test_with_page_length_option() { scenario .args(&["--pages=2:3", "-l", "100", "-n", test_file_path]) .succeeds() - .stdout_is_templated_fixture( - expected_test_file_path, - vec![(&"{last_modified_time}".to_string(), &value)], - ); + .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]); new_ucmd!() .args(&["--pages=2:3", "-l", "5", "-n", test_file_path]) @@ -288,14 +262,14 @@ fn test_with_suppress_error_option() { fn test_with_stdin() { let expected_file_path = "stdin.log.expected"; let mut scenario = new_ucmd!(); - let now = now_time(); + let start = Local::now(); scenario .pipe_in_fixture("stdin.log") .args(&["--pages=1:2", "-n", "-"]) .run() - .stdout_is_templated_fixture( + .stdout_is_templated_fixture_any( expected_file_path, - vec![(&"{last_modified_time}".to_string(), &now)], + &valid_last_modified_template_vars(start), ); } @@ -308,18 +282,12 @@ fn test_with_column() { scenario .args(&["--pages=3:5", "--column=3", "-n", test_file_path]) .succeeds() - .stdout_is_templated_fixture( - expected_test_file_path, - vec![(&"{last_modified_time}".to_string(), &value)], - ); + .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]); new_ucmd!() .args(&["--pages=3:5", "-3", "-n", test_file_path]) .succeeds() - .stdout_is_templated_fixture( - expected_test_file_path, - vec![(&"{last_modified_time}".to_string(), &value)], - ); + .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]); } #[test] @@ -331,10 +299,7 @@ fn test_with_column_across_option() { scenario .args(&["--pages=3:5", "--column=3", "-a", "-n", test_file_path]) .succeeds() - .stdout_is_templated_fixture( - expected_test_file_path, - vec![(&"{last_modified_time}".to_string(), &value)], - ); + .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]); } #[test] @@ -354,10 +319,7 @@ fn test_with_column_across_option_and_column_separator() { test_file_path, ]) .succeeds() - .stdout_is_templated_fixture( - expected_test_file_path, - vec![(&"{last_modified_time}".to_string(), &value)], - ); + .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]); new_ucmd!() .args(&[ @@ -371,7 +333,7 @@ fn test_with_column_across_option_and_column_separator() { .succeeds() .stdout_is_templated_fixture( expected_test_file_path1, - vec![(&"{last_modified_time}".to_string(), &value)], + &[("{last_modified_time}", &value)], ); } @@ -382,25 +344,25 @@ 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 now = now_time(); + let start = Local::now(); new_ucmd!() .args(&["--pages=1:2", "-m", "-n", test_file_path, test_file_path1]) .succeeds() - .stdout_is_templated_fixture( + .stdout_is_templated_fixture_any( expected_test_file_path, - vec![(&"{last_modified_time}".to_string(), &now)], + &valid_last_modified_template_vars(start), ); - let now = now_time(); + let start = Local::now(); new_ucmd!() .args(&["--pages=2:4", "-m", "-n", test_file_path, test_file_path1]) .succeeds() - .stdout_is_templated_fixture( + .stdout_is_templated_fixture_any( expected_test_file_path1, - vec![(&"{last_modified_time}".to_string(), &now)], + &valid_last_modified_template_vars(start), ); - let now = now_time(); + let start = Local::now(); new_ucmd!() .args(&[ "--pages=1:2", @@ -413,9 +375,9 @@ fn test_with_mpr() { test_file_path, ]) .succeeds() - .stdout_is_templated_fixture( + .stdout_is_templated_fixture_any( expected_test_file_path2, - vec![(&"{last_modified_time}".to_string(), &now)], + &valid_last_modified_template_vars(start), ); } @@ -452,10 +414,7 @@ fn test_with_offset_space_option() { test_file_path, ]) .succeeds() - .stdout_is_templated_fixture( - expected_test_file_path, - vec![(&"{last_modified_time}".to_string(), &value)], - ); + .stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]); } #[test] @@ -497,9 +456,9 @@ fn test_with_pr_core_utils_tests() { scenario_with_expected_status.stdout_is_templated_fixture( test_file_path, - vec![ - (&"{last_modified_time}".to_string(), &value), - (&"{file_name}".to_string(), &input_file_path.to_string()), + &[ + ("{last_modified_time}", &value), + ("{file_name}", input_file_path), ], ); } @@ -511,12 +470,12 @@ 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 now = now_time(); + let start = Local::now(); scenario .args(&["+1:2", "-J", "-m", test_file_1, test_file_2]) .run() - .stdout_is_templated_fixture( + .stdout_is_templated_fixture_any( expected_file_path, - vec![(&"{last_modified_time}".to_string(), &now)], + &valid_last_modified_template_vars(start), ); } diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index 02636b027..1d41ddac5 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -1,3 +1,8 @@ +// * 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) ints use crate::common::util::*; @@ -21,17 +26,31 @@ fn test_helper(file_name: &str, possible_args: &[&str]) { #[test] fn test_buffer_sizes() { - let buffer_sizes = [ - "0", "50K", "50k", "1M", "100M", "1000G", "10T", "500E", "1Y", - ]; + let buffer_sizes = ["0", "50K", "50k", "1M", "100M"]; for buffer_size in &buffer_sizes { - new_ucmd!() + TestScenario::new(util_name!()) + .ucmd_keepenv() .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_keepenv() + .arg("-n") + .arg("-S") + .arg(buffer_size) + .arg("ext_sort.txt") + .succeeds() + .stdout_is_fixture("ext_sort.expected"); + } + } } } @@ -43,11 +62,39 @@ fn test_invalid_buffer_size() { .arg("-S") .arg(invalid_buffer_size) .fails() + .code_is(2) .stderr_only(format!( - "sort: failed to parse buffer size `{}`: invalid digit found in string", + "sort: invalid --buffer-size argument '{}'", invalid_buffer_size )); } + #[cfg(not(target_pointer_width = "128"))] + new_ucmd!() + .arg("-n") + .arg("-S") + .arg("1Y") + .arg("ext_sort.txt") + .fails() + .code_is(2) + .stderr_only("sort: --buffer-size argument '1Y' too large"); + + #[cfg(target_pointer_width = "32")] + { + let buffer_sizes = ["1000G", "10T"]; + for buffer_size in &buffer_sizes { + new_ucmd!() + .arg("-n") + .arg("-S") + .arg(buffer_size) + .arg("ext_sort.txt") + .fails() + .code_is(2) + .stderr_only(format!( + "sort: --buffer-size argument '{}' too large", + buffer_size + )); + } + } } #[test] @@ -80,11 +127,7 @@ fn test_months_whitespace() { #[test] fn test_version_empty_lines() { - new_ucmd!() - .arg("-V") - .arg("version-empty-lines.txt") - .succeeds() - .stdout_is("\n\n\n\n\n\n\n1.2.3-alpha\n1.2.3-alpha2\n\t\t\t1.12.4\n11.2.3\n"); + test_helper("version-empty-lines", &["-V", "--version-sort"]); } #[test] @@ -409,10 +452,20 @@ fn test_human_block_sizes2() { .arg(human_numeric_sort_param) .pipe_in(input) .succeeds() - .stdout_only("-8T\n0.8M\n8981K\n21G\n909991M\n"); + .stdout_only("-8T\n8981K\n0.8M\n909991M\n21G\n"); } } +#[test] +fn test_human_numeric_zero_stable() { + let input = "0M\n0K\n-0K\n-P\n-0M\n"; + new_ucmd!() + .arg("-hs") + .pipe_in(input) + .succeeds() + .stdout_only(input); +} + #[test] fn test_month_default2() { for month_sort_param in &["-M", "--month-sort", "--sort=month"] { @@ -717,26 +770,30 @@ fn test_pipe() { #[test] fn test_check() { - new_ucmd!() - .arg("-c") - .arg("check_fail.txt") - .fails() - .stdout_is("sort: check_fail.txt:6: disorder: 5\n"); + for diagnose_arg in &["-c", "--check", "--check=diagnose-first"] { + new_ucmd!() + .arg(diagnose_arg) + .arg("check_fail.txt") + .fails() + .stdout_is("sort: check_fail.txt:6: disorder: 5\n"); - new_ucmd!() - .arg("-c") - .arg("multiple_files.expected") - .succeeds() - .stdout_is(""); + new_ucmd!() + .arg(diagnose_arg) + .arg("multiple_files.expected") + .succeeds() + .stdout_is(""); + } } #[test] fn test_check_silent() { - new_ucmd!() - .arg("-C") - .arg("check_fail.txt") - .fails() - .stdout_is(""); + for silent_arg in &["-C", "--check=silent", "--check=quiet"] { + new_ucmd!() + .arg(silent_arg) + .arg("check_fail.txt") + .fails() + .stdout_is(""); + } } #[test] @@ -790,5 +847,107 @@ fn test_nonexistent_file() { #[test] fn test_blanks() { - test_helper("blanks", &["-b", "--ignore-blanks"]); + test_helper("blanks", &["-b", "--ignore-leading-blanks"]); +} + +#[test] +fn sort_multiple() { + new_ucmd!() + .args(&["no_trailing_newline1.txt", "no_trailing_newline2.txt"]) + .succeeds() + .stdout_is("a\nb\nb\n"); +} + +#[test] +fn sort_empty_chunk() { + new_ucmd!() + .args(&["-S", "40b"]) + .pipe_in("a\na\n") + .succeeds() + .stdout_is("a\na\n"); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_compress() { + new_ucmd!() + .args(&[ + "ext_sort.txt", + "-n", + "--compress-program", + "gzip", + "-S", + "10", + ]) + .succeeds() + .stdout_only_fixture("ext_sort.expected"); +} + +#[test] +fn test_compress_fail() { + TestScenario::new(util_name!()) + .ucmd_keepenv() + .args(&[ + "ext_sort.txt", + "-n", + "--compress-program", + "nonexistent-program", + "-S", + "10", + ]) + .fails() + .stderr_only("sort: couldn't execute compress program: errno 2"); +} + +#[test] +fn test_merge_batches() { + TestScenario::new(util_name!()) + .ucmd_keepenv() + .args(&["ext_sort.txt", "-n", "-S", "150b"]) + .succeeds() + .stdout_only_fixture("ext_sort.expected"); +} + +#[test] +fn test_merge_batch_size() { + TestScenario::new(util_name!()) + .ucmd_keepenv() + .arg("--batch-size=2") + .arg("-m") + .arg("--unique") + .arg("merge_ints_interleaved_1.txt") + .arg("merge_ints_interleaved_2.txt") + .arg("merge_ints_interleaved_3.txt") + .arg("merge_ints_interleaved_3.txt") + .arg("merge_ints_interleaved_2.txt") + .arg("merge_ints_interleaved_1.txt") + .succeeds() + .stdout_only_fixture("merge_ints_interleaved.expected"); +} + +#[test] +fn test_sigpipe_panic() { + let mut cmd = new_ucmd!(); + let mut child = cmd.args(&["ext_sort.txt"]).run_no_wait(); + // Dropping the stdout should not lead to an error. + // The "Broken pipe" error should be silently ignored. + drop(child.stdout.take()); + assert_eq!( + String::from_utf8(child.wait_with_output().unwrap().stderr), + Ok(String::new()) + ); +} + +#[test] +fn test_conflict_check_out() { + let check_flags = ["-c=silent", "-c=quiet", "-c=diagnose-first", "-c", "-C"]; + for check_flag in &check_flags { + new_ucmd!() + .arg(check_flag) + .arg("-o=/dev/null") + .fails() + .stderr_contains( + "error: The argument '--output ' cannot be used with '--check", + ); + } } diff --git a/tests/by-util/test_split.rs b/tests/by-util/test_split.rs index 85b28326b..229925a1c 100644 --- a/tests/by-util/test_split.rs +++ b/tests/by-util/test_split.rs @@ -1,3 +1,8 @@ +// * 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. + extern crate rand; extern crate regex; @@ -285,3 +290,53 @@ fn test_filter_command_fails() { ucmd.args(&["--filter=/a/path/that/totally/does/not/exist", name]) .fails(); } + +#[test] +fn test_split_lines_number() { + // Test if stdout/stderr for '--lines' option is correct + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch("file"); + + scene + .ucmd() + .args(&["--lines", "2", "file"]) + .succeeds() + .no_stderr() + .no_stdout(); + scene + .ucmd() + .args(&["--lines", "2fb", "file"]) + .fails() + .code_is(1) + .stderr_only("split: invalid number of lines: '2fb'"); +} + +#[test] +fn test_split_invalid_bytes_size() { + new_ucmd!() + .args(&["-b", "1024R"]) + .fails() + .code_is(1) + .stderr_only("split: invalid number of bytes: '1024R'"); + #[cfg(not(target_pointer_width = "128"))] + new_ucmd!() + .args(&["-b", "1Y"]) + .fails() + .code_is(1) + .stderr_only("split: invalid number of bytes: '1Y': Value too large for defined data type"); + #[cfg(target_pointer_width = "32")] + { + let sizes = ["1000G", "10T"]; + for size in &sizes { + new_ucmd!() + .args(&["-b", size]) + .fails() + .code_is(1) + .stderr_only(format!( + "split: invalid number of bytes: '{}': Value too large for defined data type", + size + )); + } + } +} diff --git a/tests/by-util/test_stat.rs b/tests/by-util/test_stat.rs index 89dd96752..ddf78815f 100644 --- a/tests/by-util/test_stat.rs +++ b/tests/by-util/test_stat.rs @@ -313,9 +313,11 @@ fn expected_result(args: &[&str]) -> String { #[cfg(target_vendor = "apple")] let util_name = format!("g{}", util_name!()); + // note: clippy::needless_borrow *false positive* + #[allow(clippy::needless_borrow)] TestScenario::new(&util_name) .cmd_keepenv(util_name) - .env("LANGUAGE", "C") + .env("LC_ALL", "C") .args(args) .succeeds() .stdout_move_str() diff --git a/tests/by-util/test_stdbuf.rs b/tests/by-util/test_stdbuf.rs index 2e09601ce..66892ea0f 100644 --- a/tests/by-util/test_stdbuf.rs +++ b/tests/by-util/test_stdbuf.rs @@ -57,8 +57,18 @@ fn test_stdbuf_line_buffering_stdin_fails() { #[cfg(not(target_os = "windows"))] #[test] fn test_stdbuf_invalid_mode_fails() { - new_ucmd!() - .args(&["-i", "1024R", "head"]) - .fails() - .stderr_is("stdbuf: invalid mode 1024R\nTry 'stdbuf --help' for more information."); + let options = ["--input", "--output", "--error"]; + for option in &options { + new_ucmd!() + .args(&[*option, "1024R", "head"]) + .fails() + .code_is(125) + .stderr_only("stdbuf: invalid mode '1024R'"); + #[cfg(not(target_pointer_width = "128"))] + new_ucmd!() + .args(&[*option, "1Y", "head"]) + .fails() + .code_is(125) + .stderr_contains("stdbuf: invalid mode '1Y': Value too large for defined data type"); + } } diff --git a/tests/by-util/test_tail.rs b/tests/by-util/test_tail.rs index b31344c34..e8dd63317 100644 --- a/tests/by-util/test_tail.rs +++ b/tests/by-util/test_tail.rs @@ -1,6 +1,12 @@ +// * 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 (ToDO) abcdefghijklmnopqrstuvwxyz efghijklmnopqrstuvwxyz vwxyz emptyfile + extern crate tail; -use self::tail::parse_size; use crate::common::util::*; use std::char::from_digit; use std::io::Write; @@ -236,41 +242,6 @@ fn test_bytes_big() { } } -#[test] -fn test_parse_size() { - // No suffix. - assert_eq!(Ok(1234), parse_size("1234")); - - // kB is 1000 - assert_eq!(Ok(9 * 1000), parse_size("9kB")); - - // K is 1024 - assert_eq!(Ok(2 * 1024), parse_size("2K")); - - let suffixes = [ - ('M', 2u32), - ('G', 3u32), - ('T', 4u32), - ('P', 5u32), - ('E', 6u32), - ]; - - for &(c, exp) in &suffixes { - let s = format!("2{}B", c); - assert_eq!(Ok(2 * (1000_u64).pow(exp)), parse_size(&s)); - - let s = format!("2{}", c); - assert_eq!(Ok(2 * (1024_u64).pow(exp)), parse_size(&s)); - } - - // Sizes that are too big. - assert!(parse_size("1Z").is_err()); - assert!(parse_size("1Y").is_err()); - - // Bad number - assert!(parse_size("328hdsf3290").is_err()); // spell-checker:disable-line -} - #[test] fn test_lines_with_size_suffix() { const FILE: &str = "test_lines_with_size_suffix.txt"; @@ -320,12 +291,11 @@ fn test_multiple_input_files_with_suppressed_headers() { #[test] fn test_multiple_input_quiet_flag_overrides_verbose_flag_for_suppressing_headers() { - // TODO: actually the later one should win, i.e. -qv should lead to headers being printed, -vq to them being suppressed new_ucmd!() .arg(FOOBAR_TXT) .arg(FOOBAR_2_TXT) - .arg("-q") .arg("-v") + .arg("-q") .run() .stdout_is_fixture("foobar_multiple_quiet.expected"); } @@ -388,3 +358,61 @@ fn test_positive_zero_lines() { .succeeds() .stdout_is("a\nb\nc\nd\ne\n"); } + +#[test] +fn test_tail_invalid_num() { + new_ucmd!() + .args(&["-c", "1024R", "emptyfile.txt"]) + .fails() + .stderr_is("tail: invalid number of bytes: '1024R'"); + new_ucmd!() + .args(&["-n", "1024R", "emptyfile.txt"]) + .fails() + .stderr_is("tail: invalid number of lines: '1024R'"); + #[cfg(not(target_pointer_width = "128"))] + new_ucmd!() + .args(&["-c", "1Y", "emptyfile.txt"]) + .fails() + .stderr_is("tail: invalid number of bytes: '1Y': Value too large for defined data type"); + #[cfg(not(target_pointer_width = "128"))] + new_ucmd!() + .args(&["-n", "1Y", "emptyfile.txt"]) + .fails() + .stderr_is("tail: invalid number of lines: '1Y': Value too large for defined data type"); + #[cfg(target_pointer_width = "32")] + { + let sizes = ["1000G", "10T"]; + for size in &sizes { + new_ucmd!() + .args(&["-c", size]) + .fails() + .code_is(1) + .stderr_only(format!( + "tail: invalid number of bytes: '{}': Value too large for defined data type", + size + )); + } + } +} + +#[test] +fn test_tail_num_with_undocumented_sign_bytes() { + // tail: '-' is not documented (8.32 man pages) + // head: '+' is not documented (8.32 man pages) + const ALPHABET: &str = "abcdefghijklmnopqrstuvwxyz"; + new_ucmd!() + .args(&["-c", "5"]) + .pipe_in(ALPHABET) + .succeeds() + .stdout_is("vwxyz"); + new_ucmd!() + .args(&["-c", "-5"]) + .pipe_in(ALPHABET) + .succeeds() + .stdout_is("vwxyz"); + new_ucmd!() + .args(&["-c", "+5"]) + .pipe_in(ALPHABET) + .succeeds() + .stdout_is("efghijklmnopqrstuvwxyz"); +} diff --git a/tests/by-util/test_test.rs b/tests/by-util/test_test.rs index aaf09d657..1867927da 100644 --- a/tests/by-util/test_test.rs +++ b/tests/by-util/test_test.rs @@ -8,7 +8,7 @@ // file that was distributed with this source code. // -// spell-checker:ignore (words) pseudofloat +// spell-checker:ignore (words) egid euid pseudofloat use crate::common::util::*; @@ -165,7 +165,7 @@ fn test_dangling_string_comparison_is_error() { .args(&["missing_something", "="]) .run() .status_code(2) - .stderr_is("test: missing argument after ‘=’"); + .stderr_is("test: missing argument after '='"); } #[test] @@ -265,7 +265,7 @@ fn test_float_inequality_is_error() { .args(&["123.45", "-ge", "6"]) .run() .status_code(2) - .stderr_is("test: invalid integer ‘123.45’"); + .stderr_is("test: invalid integer '123.45'"); } #[test] @@ -283,7 +283,7 @@ fn test_invalid_utf8_integer_compare() { cmd.run() .status_code(2) - .stderr_is("test: invalid integer ‘fo�o’"); + .stderr_is("test: invalid integer 'fo�o'"); let mut cmd = new_ucmd!(); cmd.raw.arg(arg); @@ -291,7 +291,7 @@ fn test_invalid_utf8_integer_compare() { cmd.run() .status_code(2) - .stderr_is("test: invalid integer ‘fo�o’"); + .stderr_is("test: invalid integer 'fo�o'"); } #[test] @@ -476,6 +476,73 @@ fn test_nonexistent_file_is_not_symlink() { .succeeds(); } +#[test] +#[cfg(not(windows))] // Windows has no concept of sticky bit +fn test_file_is_sticky() { + let scenario = TestScenario::new(util_name!()); + let mut ucmd = scenario.ucmd(); + let mut chmod = scenario.cmd("chmod"); + + scenario.fixtures.touch("sticky_file"); + chmod.args(&["+t", "sticky_file"]).succeeds(); + + ucmd.args(&["-k", "sticky_file"]).succeeds(); +} + +#[test] +fn test_file_is_not_sticky() { + new_ucmd!() + .args(&["-k", "regular_file"]) + .run() + .status_code(1); +} + +#[test] +#[cfg(not(windows))] +fn test_file_owned_by_euid() { + new_ucmd!().args(&["-O", "regular_file"]).succeeds(); +} + +#[test] +#[cfg(not(windows))] +fn test_nonexistent_file_not_owned_by_euid() { + new_ucmd!() + .args(&["-O", "nonexistent_file"]) + .run() + .status_code(1); +} + +#[test] +#[cfg(all(not(windows), not(target_os = "freebsd")))] +fn test_file_not_owned_by_euid() { + new_ucmd!() + .args(&["-f", "/bin/sh", "-a", "!", "-O", "/bin/sh"]) + .succeeds(); +} + +#[test] +#[cfg(not(windows))] +fn test_file_owned_by_egid() { + new_ucmd!().args(&["-G", "regular_file"]).succeeds(); +} + +#[test] +#[cfg(not(windows))] +fn test_nonexistent_file_not_owned_by_egid() { + new_ucmd!() + .args(&["-G", "nonexistent_file"]) + .run() + .status_code(1); +} + +#[test] +#[cfg(all(not(windows), not(target_os = "freebsd")))] +fn test_file_not_owned_by_egid() { + new_ucmd!() + .args(&["-f", "/bin/sh", "-a", "!", "-G", "/bin/sh"]) + .succeeds(); +} + #[test] fn test_op_precedence_and_or_1() { new_ucmd!().args(&[" ", "-o", "", "-a", ""]).succeeds(); @@ -607,7 +674,7 @@ fn test_erroneous_parenthesized_expression() { .args(&["a", "!=", "(", "b", "-a", "b", ")", "!=", "c"]) .run() .status_code(2) - .stderr_is("test: extra argument ‘b’"); + .stderr_is("test: extra argument 'b'"); } #[test] @@ -623,3 +690,31 @@ fn test_or_as_filename() { fn test_string_length_and_nothing() { new_ucmd!().args(&["-n", "a", "-a"]).run().status_code(2); } + +#[test] +fn test_bracket_syntax_success() { + let scenario = TestScenario::new("["); + let mut ucmd = scenario.ucmd(); + + ucmd.args(&["1", "-eq", "1", "]"]).succeeds(); +} + +#[test] +fn test_bracket_syntax_failure() { + let scenario = TestScenario::new("["); + let mut ucmd = scenario.ucmd(); + + ucmd.args(&["1", "-eq", "2", "]"]).run().status_code(1); +} + +#[test] +fn test_bracket_syntax_missing_right_bracket() { + let scenario = TestScenario::new("["); + let mut ucmd = scenario.ucmd(); + + // Missing closing bracket takes precedence over other possible errors. + ucmd.args(&["1", "-eq"]) + .run() + .status_code(2) + .stderr_is("[: missing ']'"); +} diff --git a/tests/by-util/test_timeout.rs b/tests/by-util/test_timeout.rs index 28273e00f..9be29065a 100644 --- a/tests/by-util/test_timeout.rs +++ b/tests/by-util/test_timeout.rs @@ -9,3 +9,39 @@ fn test_subcommand_return_code() { new_ucmd!().arg("1").arg("false").run().status_code(1); } + +#[test] +fn test_command_with_args() { + new_ucmd!() + .args(&["1700", "echo", "-n", "abcd"]) + .succeeds() + .stdout_only("abcd"); +} + +#[test] +fn test_verbose() { + for &verbose_flag in &["-v", "--verbose"] { + new_ucmd!() + .args(&[verbose_flag, ".1", "sleep", "10"]) + .fails() + .stderr_only("timeout: sending signal TERM to command 'sleep'"); + new_ucmd!() + .args(&[verbose_flag, "-s0", "-k.1", ".1", "sleep", "10"]) + .fails() + .stderr_only("timeout: sending signal EXIT to command 'sleep'\ntimeout: sending signal KILL to command 'sleep'"); + } +} + +#[test] +fn test_zero_timeout() { + new_ucmd!() + .args(&["-v", "0", "sleep", ".1"]) + .succeeds() + .no_stderr() + .no_stdout(); + new_ucmd!() + .args(&["-v", "0", "-s0", "-k0", "sleep", ".1"]) + .succeeds() + .no_stderr() + .no_stdout(); +} diff --git a/tests/by-util/test_touch.rs b/tests/by-util/test_touch.rs index c861a50dd..3ed7f3bb2 100644 --- a/tests/by-util/test_touch.rs +++ b/tests/by-util/test_touch.rs @@ -6,6 +6,7 @@ use self::touch::filetime::{self, FileTime}; extern crate time; use crate::common::util::*; +use std::path::PathBuf; fn get_file_times(at: &AtPath, path: &str) -> (FileTime, FileTime) { let m = at.metadata(path); @@ -374,6 +375,24 @@ fn test_touch_set_date2() { assert_eq!(mtime, start_of_year); } +#[test] +fn test_touch_set_date3() { + let (at, mut ucmd) = at_and_ucmd!(); + let file = "test_touch_set_date"; + + ucmd.args(&["-d", "@1623786360", file]) + .succeeds() + .no_stderr(); + + assert!(at.file_exists(file)); + + let expected = FileTime::from_unix_time(1623786360, 0); + let (atime, mtime) = get_file_times(&at, file); + assert_eq!(atime, mtime); + assert_eq!(atime, expected); + assert_eq!(mtime, expected); +} + #[test] fn test_touch_set_date_wrong_format() { let (_at, mut ucmd) = at_and_ucmd!(); @@ -466,3 +485,37 @@ fn test_touch_trailing_slash() { let file = "no-file/"; ucmd.args(&[file]).fails(); } + +#[test] +fn test_touch_no_such_file_error_msg() { + let dirname = "nonexistent"; + let filename = "file"; + let path = PathBuf::from(dirname).join(filename); + let path_str = path.to_str().unwrap(); + + new_ucmd!().arg(&path).fails().stderr_only(format!( + "touch: cannot touch '{}': No such file or directory", + path_str + )); +} + +#[test] +#[cfg(unix)] +fn test_touch_permission_denied_error_msg() { + let (at, mut ucmd) = at_and_ucmd!(); + + let dirname = "dir_with_read_only_access"; + let filename = "file"; + let path = PathBuf::from(dirname).join(filename); + let path_str = path.to_str().unwrap(); + + // create dest without write permissions + at.mkdir(dirname); + at.set_readonly(dirname); + + let full_path = at.plus_as_string(path_str); + ucmd.arg(&full_path).fails().stderr_only(format!( + "touch: cannot touch '{}': Permission denied", + &full_path + )); +} diff --git a/tests/by-util/test_truncate.rs b/tests/by-util/test_truncate.rs index bb0f4a596..4b2e9e502 100644 --- a/tests/by-util/test_truncate.rs +++ b/tests/by-util/test_truncate.rs @@ -1,3 +1,10 @@ +// * 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) RFILE + use crate::common::util::*; use std::io::{Seek, SeekFrom, Write}; @@ -45,9 +52,18 @@ fn test_reference() { let at = &scene.fixtures; let mut file = at.make_file(FILE2); - scene.ucmd().arg("-s").arg("+5KB").arg(FILE1).run(); + // 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.ucmd().arg("--reference").arg(FILE1).arg(FILE2).run(); + scene + .ucmd() + .arg("--reference") + .arg(FILE1) + .arg(FILE2) + .succeeds(); file.seek(SeekFrom::End(0)).unwrap(); let actual = file.seek(SeekFrom::Current(0)).unwrap(); @@ -231,23 +247,30 @@ fn test_size_and_reference() { ); } +#[test] +fn test_error_filename_only() { + // truncate: you must specify either '--size' or '--reference' + new_ucmd!().args(&["file"]).fails().stderr_contains( + "error: The following required arguments were not provided: + --reference + --size ", + ); +} + #[test] fn test_invalid_numbers() { - // TODO For compatibility with GNU, `truncate -s 0X` should cause - // the same error as `truncate -s 0X file`, but currently it returns - // a different error. new_ucmd!() .args(&["-s", "0X", "file"]) .fails() - .stderr_contains("Invalid number: ‘0X’"); + .stderr_contains("Invalid number: '0X'"); new_ucmd!() .args(&["-s", "0XB", "file"]) .fails() - .stderr_contains("Invalid number: ‘0XB’"); + .stderr_contains("Invalid number: '0XB'"); new_ucmd!() .args(&["-s", "0B", "file"]) .fails() - .stderr_contains("Invalid number: ‘0B’"); + .stderr_contains("Invalid number: '0B'"); } #[test] @@ -265,3 +288,36 @@ fn test_reference_with_size_file_not_found() { .fails() .stderr_contains("cannot stat 'a': No such file or directory"); } + +#[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(&["--size", "1024R", "file"]) + .fails() + .code_is(1) + .stderr_only("truncate: Invalid number: '1024R'"); + #[cfg(not(target_pointer_width = "128"))] + new_ucmd!() + .args(&["--size", "1Y", "file"]) + .fails() + .code_is(1) + .stderr_only("truncate: Invalid number: '1Y': Value too large for defined data type"); + #[cfg(target_pointer_width = "32")] + { + let sizes = ["1000G", "10T"]; + for size in &sizes { + new_ucmd!() + .args(&["--size", size, "file"]) + .fails() + .code_is(1) + .stderr_only(format!( + "truncate: Invalid number: '{}': Value too large for defined data type", + size + )); + } + } +} diff --git a/tests/by-util/test_tty.rs b/tests/by-util/test_tty.rs index 6bca54e03..6ba8cd029 100644 --- a/tests/by-util/test_tty.rs +++ b/tests/by-util/test_tty.rs @@ -1,11 +1,14 @@ +use std::fs::File; + use crate::common::util::*; #[test] #[cfg(not(windows))] fn test_dev_null() { new_ucmd!() - .pipe_in(" = actual.split_whitespace().collect(); let mut v_expect: Vec<&str> = expect.split_whitespace().collect(); - // TODO: `--users` differs from GNU's output on macOS - // Diff < left / right > : - // <"runner console 2021-05-20 22:03 00:08 196\n" - // >"runner console 2021-05-20 22:03 old 196\n" + // TODO: `--users` sometimes differs from GNU's output on macOS (race condition?) + // actual: "runner console Jun 23 06:37 00:34 196\n" + // expect: "runner console Jun 23 06:37 old 196\n" if cfg!(target_os = "macos") { - v_actual.remove(4); - v_expect.remove(4); + v_actual.remove(5); + v_expect.remove(5); } assert_eq!(v_actual, v_expect); @@ -238,9 +237,11 @@ fn expected_result(args: &[&str]) -> String { #[cfg(target_vendor = "apple")] let util_name = format!("g{}", util_name!()); + // note: clippy::needless_borrow *false positive* + #[allow(clippy::needless_borrow)] TestScenario::new(&util_name) .cmd_keepenv(util_name) - .env("LANGUAGE", "C") + .env("LC_ALL", "C") .args(args) .succeeds() .stdout_move_str() diff --git a/tests/common/util.rs b/tests/common/util.rs index 2f7d7dcc4..f881cff21 100644 --- a/tests/common/util.rs +++ b/tests/common/util.rs @@ -69,6 +69,22 @@ pub struct CmdResult { } impl CmdResult { + pub fn new( + tmpd: Option>, + code: Option, + success: bool, + stdout: &[u8], + stderr: &[u8], + ) -> CmdResult { + CmdResult { + tmpd, + code, + success, + stdout: stdout.to_vec(), + stderr: stderr.to_vec(), + } + } + /// Returns a reference to the program's standard output as a slice of bytes pub fn stdout(&self) -> &[u8] { &self.stdout @@ -207,6 +223,18 @@ impl CmdResult { self } + /// like `stdout_is`, but succeeds if any elements of `expected` matches stdout. + pub fn stdout_is_any + std::fmt::Debug>(&self, expected: Vec) -> &CmdResult { + if !expected.iter().any(|msg| self.stdout_str() == msg.as_ref()) { + panic!( + "stdout was {}\nExpected any of {:#?}", + self.stdout_str(), + expected + ) + } + self + } + /// Like `stdout_is` but newlines are normalized to `\n`. pub fn normalized_newlines_stdout_is>(&self, msg: T) -> &CmdResult { let msg = msg.as_ref().replace("\r\n", "\n"); @@ -231,7 +259,7 @@ impl CmdResult { pub fn stdout_is_templated_fixture>( &self, file_rel_path: T, - template_vars: Vec<(&String, &String)>, + template_vars: &[(&str, &str)], ) -> &CmdResult { let mut contents = String::from_utf8(read_scenario_fixture(&self.tmpd, file_rel_path)).unwrap(); @@ -241,6 +269,23 @@ impl CmdResult { self.stdout_is(contents) } + /// like `stdout_is_templated_fixture`, but succeeds if any replacement by `template_vars` results in the actual stdout. + pub fn stdout_is_templated_fixture_any>( + &self, + file_rel_path: T, + template_vars: &[Vec<(String, String)>], + ) { + let contents = String::from_utf8(read_scenario_fixture(&self.tmpd, file_rel_path)).unwrap(); + let possible_values = template_vars.iter().map(|vars| { + let mut contents = contents.clone(); + for kv in vars.iter() { + contents = contents.replace(&kv.0, &kv.1); + } + contents + }); + self.stdout_is_any(possible_values.collect()); + } + /// asserts that the command resulted in stderr stream output that equals the /// passed in value, when both are trimmed of trailing whitespace /// stderr_only is a better choice unless stdout may or will be non-empty @@ -625,11 +670,20 @@ impl AtPath { // Source: // http://stackoverflow.com/questions/31439011/getfinalpathnamebyhandle-without-prepended let prefix = "\\\\?\\"; + // FixME: replace ... + #[allow(clippy::manual_strip)] if s.starts_with(prefix) { String::from(&s[prefix.len()..]) } else { s } + // ... with ... + // if let Some(stripped) = s.strip_prefix(prefix) { + // String::from(stripped) + // } else { + // s + // } + // ... when using MSRV with stabilized `strip_prefix()` } } diff --git a/tests/fixtures/chgrp/file1 b/tests/fixtures/chgrp/file1 new file mode 100644 index 000000000..73b6f48ab --- /dev/null +++ b/tests/fixtures/chgrp/file1 @@ -0,0 +1 @@ +target file 1 diff --git a/tests/fixtures/chgrp/file2 b/tests/fixtures/chgrp/file2 new file mode 100644 index 000000000..7ecd32965 --- /dev/null +++ b/tests/fixtures/chgrp/file2 @@ -0,0 +1 @@ +target file 2 diff --git a/tests/fixtures/chgrp/file3 b/tests/fixtures/chgrp/file3 new file mode 100644 index 000000000..73d293aba --- /dev/null +++ b/tests/fixtures/chgrp/file3 @@ -0,0 +1 @@ +target file 3 diff --git a/tests/fixtures/chgrp/ref_file b/tests/fixtures/chgrp/ref_file new file mode 100644 index 000000000..aba32d56e --- /dev/null +++ b/tests/fixtures/chgrp/ref_file @@ -0,0 +1 @@ +Reference file diff --git a/tests/fixtures/du/subdir/deeper/deeper_dir/deeper_words.txt b/tests/fixtures/du/subdir/deeper/deeper_dir/deeper_words.txt new file mode 100644 index 000000000..a04238969 --- /dev/null +++ b/tests/fixtures/du/subdir/deeper/deeper_dir/deeper_words.txt @@ -0,0 +1 @@ +hello world! diff --git a/tests/fixtures/sort/human_block_sizes.expected b/tests/fixtures/sort/human_block_sizes.expected index 0e4fdfbb6..5b4f8bb83 100644 --- a/tests/fixtures/sort/human_block_sizes.expected +++ b/tests/fixtures/sort/human_block_sizes.expected @@ -1,3 +1,4 @@ +0K K 844K 981K diff --git a/tests/fixtures/sort/human_block_sizes.expected.debug b/tests/fixtures/sort/human_block_sizes.expected.debug index cde98628e..398ff9db4 100644 --- a/tests/fixtures/sort/human_block_sizes.expected.debug +++ b/tests/fixtures/sort/human_block_sizes.expected.debug @@ -1,3 +1,6 @@ +0K +__ +__ K ^ no match for key _ diff --git a/tests/fixtures/sort/human_block_sizes.txt b/tests/fixtures/sort/human_block_sizes.txt index 9cc2b3c6c..a5adb9b5e 100644 --- a/tests/fixtures/sort/human_block_sizes.txt +++ b/tests/fixtures/sort/human_block_sizes.txt @@ -9,4 +9,5 @@ 844K 981K 13M -K \ No newline at end of file +K +0K \ No newline at end of file diff --git a/tests/fixtures/sort/no_trailing_newline1.txt b/tests/fixtures/sort/no_trailing_newline1.txt new file mode 100644 index 000000000..0a207c060 --- /dev/null +++ b/tests/fixtures/sort/no_trailing_newline1.txt @@ -0,0 +1,2 @@ +a +b \ No newline at end of file diff --git a/tests/fixtures/sort/no_trailing_newline2.txt b/tests/fixtures/sort/no_trailing_newline2.txt new file mode 100644 index 000000000..63d8dbd40 --- /dev/null +++ b/tests/fixtures/sort/no_trailing_newline2.txt @@ -0,0 +1 @@ +b \ No newline at end of file diff --git a/tests/fixtures/sort/version-empty-lines.expected b/tests/fixtures/sort/version-empty-lines.expected index c496c0ff5..69a648966 100644 --- a/tests/fixtures/sort/version-empty-lines.expected +++ b/tests/fixtures/sort/version-empty-lines.expected @@ -8,4 +8,8 @@ 1.2.3-alpha 1.2.3-alpha2 11.2.3 +bar2 +bar2.0.0 +foo0.1 +foo1.0 1.12.4 diff --git a/tests/fixtures/sort/version-empty-lines.expected.debug b/tests/fixtures/sort/version-empty-lines.expected.debug new file mode 100644 index 000000000..d3f2aaceb --- /dev/null +++ b/tests/fixtures/sort/version-empty-lines.expected.debug @@ -0,0 +1,45 @@ + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key + +^ no match for key +^ no match for key +1.2.3-alpha +___________ +___________ +1.2.3-alpha2 +____________ +____________ +11.2.3 +______ +______ +bar2 +____ +____ +bar2.0.0 +________ +________ +foo0.1 +______ +______ +foo1.0 +______ +______ +>>>1.12.4 +_________ +_________ diff --git a/tests/fixtures/sort/version-empty-lines.txt b/tests/fixtures/sort/version-empty-lines.txt index 9b6b89788..fef474259 100644 --- a/tests/fixtures/sort/version-empty-lines.txt +++ b/tests/fixtures/sort/version-empty-lines.txt @@ -9,3 +9,7 @@ 1.12.4 +foo1.0 +foo0.1 +bar2.0.0 +bar2 \ No newline at end of file diff --git a/util/GHA-delete-GNU-workflow-logs.sh b/util/GHA-delete-GNU-workflow-logs.sh new file mode 100644 index 000000000..19e3311d4 --- /dev/null +++ b/util/GHA-delete-GNU-workflow-logs.sh @@ -0,0 +1,44 @@ +#!/bin/sh + +# spell-checker:ignore (utils) gitsome jq ; (gh) repos + +ME="${0}" +ME_dir="$(dirname -- "${ME}")" +ME_parent_dir="$(dirname -- "${ME_dir}")" +ME_parent_dir_abs="$(realpath -mP -- "${ME_parent_dir}")" + +# ref: + +# note: requires `gh` and `jq` + +## tools available? + +# * `gh` available? +unset GH +gh --version 1>/dev/null 2>&1 +if [ $? -eq 0 ]; then export GH="gh"; fi + +# * `jq` available? +unset JQ +jq --version 1>/dev/null 2>&1 +if [ $? -eq 0 ]; then export JQ="jq"; fi + +if [ -z "${GH}" ] || [ -z "${JQ}" ]; then + if [ -z "${GH}" ]; then + echo 'ERR!: missing `gh` (see install instructions at )' 1>&2 + fi + if [ -z "${JQ}" ]; then + echo 'ERR!: missing `jq` (install with `sudo apt install jq`)' 1>&2 + fi + exit 1 +fi + +dry_run=true + +USER_NAME=uutils +REPO_NAME=coreutils +WORK_NAME=GNU + +# * `--paginate` retrieves all pages +# gh api --paginate "repos/${USER_NAME}/${REPO_NAME}/actions/runs" | jq -r ".workflow_runs[] | select(.name == \"${WORK_NAME}\") | (.id)" | xargs -n1 sh -c "for arg do { echo gh api repos/${USER_NAME}/${REPO_NAME}/actions/runs/\${arg} -X DELETE ; if [ -z "$dry_run" ]; then gh api repos/${USER_NAME}/${REPO_NAME}/actions/runs/\${arg} -X DELETE ; fi ; } ; done ;" _ +gh api "repos/${USER_NAME}/${REPO_NAME}/actions/runs" | jq -r ".workflow_runs[] | select(.name == \"${WORK_NAME}\") | (.id)" | xargs -n1 sh -c "for arg do { echo gh api repos/${USER_NAME}/${REPO_NAME}/actions/runs/\${arg} -X DELETE ; if [ -z "$dry_run" ]; then gh api repos/${USER_NAME}/${REPO_NAME}/actions/runs/\${arg} -X DELETE ; fi ; } ; done ;" _ diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 64329bd0c..798a33456 100644 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -1,16 +1,16 @@ #!/bin/bash -# spell-checker:ignore (paths) abmon deref discrim getlimits getopt ginstall gnulib inacc infloop inotify reflink ; (misc) INT_OFLOW OFLOW ; (vars/env) BUILDDIR SRCDIR +# spell-checker:ignore (paths) abmon deref discrim eacces getlimits getopt ginstall gnulib inacc infloop inotify reflink ; (misc) INT_OFLOW OFLOW ; (vars/env) BUILDDIR SRCDIR set -e if test ! -d ../gnu; then echo "Could not find ../gnu" - echo "git clone git@github.com:coreutils/coreutils.git ../gnu" + echo "git clone git@github.com:coreutils/coreutils.git gnu" exit 1 fi if test ! -d ../gnulib; then echo "Could not find ../gnulib" - echo "git clone git@github.com:coreutils/gnulib.git ../gnulib" + echo "git clone git@github.com:coreutils/gnulib.git gnulib" exit 1 fi @@ -44,7 +44,7 @@ sed -i 's|"\$@|/usr/bin/timeout 600 "\$@|' build-aux/test-driver # Change the PATH in the Makefile to test the uutils coreutils instead of the GNU coreutils sed -i "s/^[[:blank:]]*PATH=.*/ PATH='${BUILDDIR//\//\\/}\$(PATH_SEPARATOR)'\"\$\$PATH\" \\\/" Makefile sed -i 's| tr | /usr/bin/tr |' tests/init.sh -make +make -j "$(nproc)" # Generate the factor tests, so they can be fixed # Used to be 36. Reduced to 20 to decrease the log size for i in {00..20} @@ -59,7 +59,7 @@ do done -grep -rl 'path_prepend_' tests/* | xargs sed -i 's|path_prepend_ ./src||' +grep -rl 'path_prepend_' tests/* | xargs sed -i 's| path_prepend_ ./src||' sed -i -e 's|^seq |/usr/bin/seq |' -e 's|sha1sum |/usr/bin/sha1sum |' tests/factor/t*sh # Remove tests checking for --version & --help @@ -94,8 +94,28 @@ sed -i 's|cp |/usr/bin/cp |' tests/mv/hard-2.sh sed -i 's|paste |/usr/bin/paste |' tests/misc/od-endian.sh sed -i 's|seq |/usr/bin/seq |' tests/misc/sort-discrim.sh -#Add specific timeout to tests that currently hang to limit time spent waiting +# Add specific timeout to tests that currently hang to limit time spent waiting sed -i 's|seq \$|/usr/bin/timeout 0.1 seq \$|' tests/misc/seq-precision.sh tests/misc/seq-long-double.sh sed -i 's|cat |/usr/bin/timeout 0.1 cat |' tests/misc/cat-self.sh + +# Remove dup of /usr/bin/ when executed several times +grep -rl '/usr/bin//usr/bin/' tests/* | xargs --no-run-if-empty sed -i 's|/usr/bin//usr/bin/|/usr/bin/|g' + + +#### Adjust tests to make them work with Rust/coreutils +# in some cases, what we are doing in rust/coreutils is good (or better) +# we should not regress our project just to match what GNU is going. +# So, do some changes on the fly + +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/file'|rm: cannot remove 'a'|g" tests/rm/cycle.sh + +sed -i -e "s|rm: cannot remove directory 'b/a/p'|rm: cannot remove 'b'|g" tests/rm/rm1.sh + +sed -i -e "s|rm: cannot remove 'a/1'|rm: cannot remove 'a'|g" tests/rm/rm2.sh + +sed -i -e "s|removed directory 'a/'|removed directory 'a'|g" tests/rm/v-slash.sh + test -f "${BUILDDIR}/getlimits" || cp src/getlimits "${BUILDDIR}"