diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 0b245da74..80d03e257 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -7,6 +7,8 @@ name: CICD # 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 runtest tempfile testsuite uutils +# ToDO: [2021-06; rivy] change from `cargo-tree` to `cargo tree` once MSRV is >= 1.45 + env: PROJECT_NAME: coreutils PROJECT_DESC: "Core universal (cross-platform) utilities" @@ -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,13 +62,13 @@ 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() { for var in "$@" ; do echo steps.vars.outputs.${var}="${!var}"; echo ::set-output name=${var}::${!var}; done; } + 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='' ; @@ -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,13 +106,13 @@ 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() { for var in "$@" ; do echo steps.vars.outputs.${var}="${!var}"; echo ::set-output name=${var}::${!var}; done; } + 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='' ; @@ -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,49 +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" ; } - 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 - - 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 libselinux1 libselinux1-dev ; make build + - name: "`make test`" + shell: bash + run: | + make test build: name: Build @@ -235,8 +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-latest , target: x86_64-unknown-linux-gnu , features: feat_os_unix , 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 } @@ -249,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 ;; @@ -267,7 +279,7 @@ jobs: shell: bash run: | ## VARs setup - outputs() { for var in "$@" ; do echo steps.vars.outputs.${var}="${!var}"; echo ::set-output name=${var}::${!var}; done; } + 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) @@ -353,7 +365,7 @@ jobs: - 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' @@ -373,7 +385,7 @@ jobs: shell: bash run: | ## Dependent VARs setup - outputs() { for var in "$@" ; do echo steps.vars.outputs.${var}="${!var}"; echo ::set-output name=${var}::${!var}; done; } + 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} @@ -390,26 +402,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: @@ -436,7 +448,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) @@ -477,6 +489,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 }} @@ -489,11 +532,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 @@ -504,7 +547,7 @@ jobs: shell: bash run: | ## VARs setup - outputs() { for var in "$@" ; do echo steps.vars.outputs.${var}="${!var}"; echo ::set-output name=${var}::${!var}; done; } + 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 @@ -539,7 +582,7 @@ jobs: shell: bash run: | ## Dependent VARs setup - outputs() { for var in "$@" ; do echo steps.vars.outputs.${var}="${!var}"; echo ::set-output name=${var}::${!var}; done; } + 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;)" @@ -587,7 +630,7 @@ jobs: 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 80% rename from .github/workflows/GNU.yml rename to .github/workflows/GnuTests.yml index 570217865..fb6ffc0ac 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,16 +33,18 @@ 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: | @@ -53,9 +56,10 @@ jobs: bash uutils/util/run-gnu-test.sh tests/id/setgid.sh bash uutils/util/run-gnu-test.sh tests/id/zero.sh bash uutils/util/run-gnu-test.sh tests/id/gnu-zero-uids.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 @@ -65,7 +69,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" \ @@ -79,12 +89,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 0a091633f..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 diff --git a/.vscode/cspell.dictionaries/workspace.wordlist.txt b/.vscode/cspell.dictionaries/workspace.wordlist.txt index ed634dffb..7242199a5 100644 --- a/.vscode/cspell.dictionaries/workspace.wordlist.txt +++ b/.vscode/cspell.dictionaries/workspace.wordlist.txt @@ -48,17 +48,19 @@ xattr # * rust/rustc RUSTDOCFLAGS RUSTFLAGS +clippy +rustc +rustfmt +rustup +# bitor # BitOr trait function bitxor # BitXor trait function -clippy concat fract powi println repr rfind -rustc -rustfmt struct structs substr diff --git a/Cargo.lock b/Cargo.lock index d70b94de6..00e7b2b51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -220,6 +220,7 @@ dependencies = [ "atty", "bitflags", "strsim", + "term_size", "textwrap", "unicode-width", "vec_map", @@ -261,6 +262,7 @@ version = "0.0.6" dependencies = [ "atty", "chrono", + "clap", "conv", "filetime", "glob 0.3.0", @@ -1566,21 +1568,6 @@ dependencies = [ "walkdir", ] -[[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" @@ -2107,6 +2094,7 @@ dependencies = [ name = "uu_expr" version = "0.0.6" dependencies = [ + "clap", "libc", "num-bigint", "num-traits", @@ -2134,6 +2122,7 @@ dependencies = [ name = "uu_false" version = "0.0.6" dependencies = [ + "clap", "uucore", "uucore_procs", ] @@ -2199,6 +2188,7 @@ dependencies = [ name = "uu_hostid" version = "0.0.6" dependencies = [ + "clap", "libc", "uucore", "uucore_procs", @@ -2476,6 +2466,7 @@ name = "uu_pr" version = "0.0.6" dependencies = [ "chrono", + "clap", "getopts", "itertools 0.10.0", "quick-error 2.0.1", @@ -2498,6 +2489,7 @@ dependencies = [ name = "uu_printf" version = "0.0.6" dependencies = [ + "clap", "itertools 0.8.2", "uucore", "uucore_procs", @@ -2631,7 +2623,6 @@ dependencies = [ "ouroboros", "rand 0.7.3", "rayon", - "semver", "tempfile", "unicode-width", "uucore", @@ -2734,6 +2725,7 @@ dependencies = [ name = "uu_test" version = "0.0.6" dependencies = [ + "clap", "libc", "redox_syscall 0.1.57", "uucore", @@ -2777,6 +2769,7 @@ dependencies = [ name = "uu_true" version = "0.0.6" dependencies = [ + "clap", "uucore", "uucore_procs", ] diff --git a/Cargo.toml b/Cargo.toml index 06e6ed505..bc6e664ef 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" } diff --git a/GNUmakefile b/GNUmakefile index e5ad01340..89a4dca80 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -314,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) @@ -321,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 fd8709b64..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 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..ef12eb82a 100644 --- a/src/uu/arch/src/arch.rs +++ b/src/uu/arch/src/arch.rs @@ -6,24 +6,32 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +// clippy bug https://github.com/rust-lang/rust-clippy/issues/7422 +#![allow(clippy::nonstandard_macro_braces)] + #[macro_use] 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 e6a01cb34..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; @@ -56,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 256b674e2..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 == "-" { @@ -58,7 +58,7 @@ impl Config { .value_of(options::WRAP) .map(|num| { num.parse::() - .map_err(|e| format!("Invalid wrap size: ‘{}’: {}", num, e)) + .map_err(|e| format!("Invalid wrap size: '{}': {}", num, e)) }) .transpose()?; @@ -78,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) @@ -106,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 0dd831027..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; 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 098a3e2b2..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) { @@ -116,6 +92,32 @@ 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::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 path = fullname.trim_end_matches(is_separator); diff --git a/src/uu/cat/Cargo.toml b/src/uu/cat/Cargo.toml index 9218e84fe..f20cddcf9 100644 --- a/src/uu/cat/Cargo.toml +++ b/src/uu/cat/Cargo.toml @@ -15,7 +15,7 @@ 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"] } diff --git a/src/uu/cat/src/cat.rs b/src/uu/cat/src/cat.rs index 889ba424a..8ad563c5d 100644 --- a/src/uu/cat/src/cat.rs +++ b/src/uu/cat/src/cat.rs @@ -10,6 +10,9 @@ // spell-checker:ignore (ToDO) nonprint nonblank nonprinting +// clippy bug https://github.com/rust-lang/rust-clippy/issues/7422 +#![allow(clippy::nonstandard_macro_braces)] + #[cfg(unix)] extern crate unix_socket; #[macro_use] @@ -169,7 +172,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) @@ -229,61 +290,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( diff --git a/src/uu/chgrp/Cargo.toml b/src/uu/chgrp/Cargo.toml index 0e43f7c02..619bdaaad 100644 --- a/src/uu/chgrp/Cargo.toml +++ b/src/uu/chgrp/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/chgrp.rs" [dependencies] -clap = "2.33" +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 454a0386c..489be59eb 100644 --- a/src/uu/chgrp/src/chgrp.rs +++ b/src/uu/chgrp/src/chgrp.rs @@ -73,84 +73,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); - let mut app = App::new(executable!()) - .version(VERSION) - .about(ABOUT) - .usage(&usage[..]) - .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"), - ); + let mut app = uu_app().usage(&usage[..]); // we change the positional args based on whether // --reference was used. @@ -274,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 2d5787099..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,55 +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(|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 } // Iterate 'args' and delete the first occurrence 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 ab9f10dba..e1d3ff22b 100644 --- a/src/uu/chown/src/chown.rs +++ b/src/uu/chown/src/chown.rs @@ -73,101 +73,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(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::dereference::DEREFERENCE).long(options::dereference::DEREFERENCE).help( - "affect the referent of each symbolic link (this is the default), rather than the symbolic link itself", - )) - .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::FROM) - .long(options::FROM) - .help( - "change the owner and/or group of each file only if its current owner and/or group match those specified here. Either may be omitted, in which case a match is not required for the omitted attribute", - ) - .value_name("CURRENT_OWNER:CURRENT_GROUP"), - ) - .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::verbosity::QUIET) - .long(options::verbosity::QUIET) - .help("suppress most error messages"), - ) - .arg( - Arg::with_name(options::RECURSIVE) - .short("R") - .long(options::RECURSIVE) - .help("operate on files and directories recursively"), - ) - .arg( - Arg::with_name(options::REFERENCE) - .long(options::REFERENCE) - .help("use RFILE's owner and group rather than specifying OWNER:GROUP values") - .value_name("RFILE") - .min_values(1), - ) - .arg(Arg::with_name(options::verbosity::SILENT).short("f").long(options::verbosity::SILENT)) - .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") - .overrides_with_all(&[options::traverse::EVERY, options::traverse::NO_TRAVERSE]), - ) - .arg( - Arg::with_name(options::traverse::EVERY) - .short(options::traverse::EVERY) - .help("traverse every symbolic link to a directory encountered") - .overrides_with_all(&[options::traverse::TRAVERSE, options::traverse::NO_TRAVERSE]), - ) - .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::verbosity::VERBOSE) - .long(options::verbosity::VERBOSE) - .help("output a diagnostic for every file processed"), - ) - .arg( - Arg::with_name(ARG_OWNER) - .multiple(false) - .takes_value(true) - .required(true), - ) - .arg( - Arg::with_name(ARG_FILES) - .multiple(true) - .takes_value(true) - .required(true) - .min_values(1), - ) - .get_matches_from(args); + let matches = uu_app().usage(&usage[..]).get_matches_from(args); /* First arg is the owner/group */ let owner = matches.value_of(ARG_OWNER).unwrap(); @@ -273,6 +179,102 @@ pub fn uumain(args: impl uucore::Args) -> i32 { executor.exec() } +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_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::dereference::DEREFERENCE).long(options::dereference::DEREFERENCE).help( + "affect the referent of each symbolic link (this is the default), rather than the symbolic link itself", + )) + .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::FROM) + .long(options::FROM) + .help( + "change the owner and/or group of each file only if its current owner and/or group match those specified here. Either may be omitted, in which case a match is not required for the omitted attribute", + ) + .value_name("CURRENT_OWNER:CURRENT_GROUP"), + ) + .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::verbosity::QUIET) + .long(options::verbosity::QUIET) + .help("suppress most error messages"), + ) + .arg( + Arg::with_name(options::RECURSIVE) + .short("R") + .long(options::RECURSIVE) + .help("operate on files and directories recursively"), + ) + .arg( + Arg::with_name(options::REFERENCE) + .long(options::REFERENCE) + .help("use RFILE's owner and group rather than specifying OWNER:GROUP values") + .value_name("RFILE") + .min_values(1), + ) + .arg(Arg::with_name(options::verbosity::SILENT).short("f").long(options::verbosity::SILENT)) + .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") + .overrides_with_all(&[options::traverse::EVERY, options::traverse::NO_TRAVERSE]), + ) + .arg( + Arg::with_name(options::traverse::EVERY) + .short(options::traverse::EVERY) + .help("traverse every symbolic link to a directory encountered") + .overrides_with_all(&[options::traverse::TRAVERSE, options::traverse::NO_TRAVERSE]), + ) + .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::verbosity::VERBOSE) + .long(options::verbosity::VERBOSE) + .help("output a diagnostic for every file processed"), + ) + .arg( + Arg::with_name(ARG_OWNER) + .multiple(false) + .takes_value(true) + .required(true), + ) + .arg( + Arg::with_name(ARG_FILES) + .multiple(true) + .takes_value(true) + .required(true) + .min_values(1), + ) +} + fn parse_spec(spec: &str) -> Result<(Option, Option), String> { let args = spec.split_terminator(':').collect::>(); let usr_only = args.len() == 1 && !args[0].is_empty(); @@ -281,7 +283,7 @@ fn parse_spec(spec: &str) -> Result<(Option, Option), String> { let uid = if usr_only || usr_grp { Some( Passwd::locate(args[0]) - .map_err(|_| format!("invalid user: ‘{}’", spec))? + .map_err(|_| format!("invalid user: '{}'", spec))? .uid(), ) } else { @@ -290,7 +292,7 @@ fn parse_spec(spec: &str) -> Result<(Option, Option), String> { let gid = if grp_only || usr_grp { Some( Group::locate(args[1]) - .map_err(|_| format!("invalid group: ‘{}’", spec))? + .map_err(|_| format!("invalid group: '{}'", spec))? .gid(), ) } else { diff --git a/src/uu/chroot/src/chroot.rs b/src/uu/chroot/src/chroot.rs index 86d4a4900..2c0f8522c 100644 --- a/src/uu/chroot/src/chroot.rs +++ b/src/uu/chroot/src/chroot.rs @@ -36,54 +36,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(SYNTAX) - .arg( - Arg::with_name(options::NEWROOT) - .hidden(true) - .required(true) - .index(1), - ) - .arg( - Arg::with_name(options::USER) - .short("u") - .long(options::USER) - .help("User (ID or name) to switch before running the program") - .value_name("USER"), - ) - .arg( - Arg::with_name(options::GROUP) - .short("g") - .long(options::GROUP) - .help("Group (ID or name) to switch to") - .value_name("GROUP"), - ) - .arg( - Arg::with_name(options::GROUPS) - .short("G") - .long(options::GROUPS) - .help("Comma-separated list of groups to switch to") - .value_name("GROUP1,GROUP2..."), - ) - .arg( - Arg::with_name(options::USERSPEC) - .long(options::USERSPEC) - .help( - "Colon-separated user and group to switch to. \ - Same as -u USER -g GROUP. \ - Userspec has higher preference than -u and/or -g", - ) - .value_name("USER:GROUP"), - ) - .arg( - Arg::with_name(options::COMMAND) - .hidden(true) - .multiple(true) - .index(2), - ) - .get_matches_from(args); + let matches = uu_app().get_matches_from(args); let default_shell: &'static str = "/bin/sh"; let default_option: &'static str = "-i"; @@ -138,6 +91,56 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } } +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) + .index(1), + ) + .arg( + Arg::with_name(options::USER) + .short("u") + .long(options::USER) + .help("User (ID or name) to switch before running the program") + .value_name("USER"), + ) + .arg( + Arg::with_name(options::GROUP) + .short("g") + .long(options::GROUP) + .help("Group (ID or name) to switch to") + .value_name("GROUP"), + ) + .arg( + Arg::with_name(options::GROUPS) + .short("G") + .long(options::GROUPS) + .help("Comma-separated list of groups to switch to") + .value_name("GROUP1,GROUP2..."), + ) + .arg( + Arg::with_name(options::USERSPEC) + .long(options::USERSPEC) + .help( + "Colon-separated user and group to switch to. \ + Same as -u USER -g GROUP. \ + Userspec has higher preference than -u and/or -g", + ) + .value_name("USER:GROUP"), + ) + .arg( + Arg::with_name(options::COMMAND) + .hidden(true) + .multiple(true) + .index(2), + ) +} + fn set_context(root: &Path, options: &clap::ArgMatches) { let userspec_str = options.value_of(options::USERSPEC); let user_str = options.value_of(options::USER).unwrap_or_default(); 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 6a812c186..e88cc78b3 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -180,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(), @@ -217,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 7a6086bb5..aa10432a2 100644 --- a/src/uu/comm/src/comm.rs +++ b/src/uu/comm/src/comm.rs @@ -137,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) @@ -167,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/src/cp.rs b/src/uu/cp/src/cp.rs index 851117bde..4deaefa98 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -290,13 +290,10 @@ 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(options::TARGET_DIRECTORY) .short("t") .conflicts_with(options::NO_TARGET_DIRECTORY) @@ -378,7 +375,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .arg(Arg::with_name(options::UPDATE) .short("u") .long(options::UPDATE) - .help("copy only when the SOURCE file is newer than the destination file\ + .help("copy only when the SOURCE file is newer than the destination file \ or when the destination file is missing")) .arg(Arg::with_name(options::REFLINK) .long(options::REFLINK) @@ -401,7 +398,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .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(options::PRESERVE_DEFAULT_ATTRIBUTES) .short("-p") @@ -464,6 +461,17 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .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)); @@ -667,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, @@ -1218,28 +1233,39 @@ 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.dereference && fs::symlink_metadata(&source)?.file_type().is_symlink() { - copy_link(source, 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))?; } @@ -1254,11 +1280,16 @@ fn copy_link(source: &Path, dest: &Path) -> CopyResult<()> { Some(name) => dest.join(name).into(), None => crash!( EXIT_ERR, - "cannot stat ‘{}’: No such file or directory", + "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)) 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 d69254a3a..048ec80d8 100644 --- a/src/uu/csplit/src/csplit.rs +++ b/src/uu/csplit/src/csplit.rs @@ -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/csplit_error.rs b/src/uu/csplit/src/csplit_error.rs index 637cf8890..e2f514ea9 100644 --- a/src/uu/csplit/src/csplit_error.rs +++ b/src/uu/csplit/src/csplit_error.rs @@ -1,3 +1,6 @@ +// clippy bug https://github.com/rust-lang/rust-clippy/issues/7422 +#![allow(clippy::nonstandard_macro_braces)] + use std::io; use thiserror::Error; diff --git a/src/uu/cut/Cargo.toml b/src/uu/cut/Cargo.toml index 47b8223c5..9a83ff554 100644 --- a/src/uu/cut/Cargo.toml +++ b/src/uu/cut/Cargo.toml @@ -15,7 +15,7 @@ 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" diff --git a/src/uu/cut/src/cut.rs b/src/uu/cut/src/cut.rs index 6602b1eb1..e33b8a2fe 100644 --- a/src/uu/cut/src/cut.rs +++ b/src/uu/cut/src/cut.rs @@ -396,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); @@ -627,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 7d47fa5c4..a97c78c78 100644 --- a/src/uu/dircolors/Cargo.toml +++ b/src/uu/dircolors/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/dircolors.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" } 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 2fa2e8b91..70b609e31 100644 --- a/src/uu/dircolors/src/dircolors.rs +++ b/src/uu/dircolors/src/dircolors.rs @@ -73,36 +73,7 @@ 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::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)) - .get_matches_from(&args); + let matches = uu_app().usage(&usage[..]).get_matches_from(&args); let files = matches .values_of(options::FILE) @@ -123,7 +94,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { 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)", files[0] ); @@ -155,7 +126,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { result = parse(INTERNAL_DB.lines(), out_format, "") } else { if files.len() > 1 { - show_usage_error!("extra operand ‘{}’", files[1]); + show_usage_error!("extra operand '{}'", files[1]); return 1; } match File::open(files[0]) { @@ -181,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; @@ -192,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 dcd1f720e..60f37db06 100644 --- a/src/uu/du/Cargo.toml +++ b/src/uu/du/Cargo.toml @@ -15,7 +15,7 @@ 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" } diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index e466b8afe..05167853c 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -10,6 +10,7 @@ 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; @@ -62,6 +63,8 @@ mod options { 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"; } @@ -87,6 +90,8 @@ struct Options { total: bool, separate_dirs: bool, one_file_system: bool, + dereference: bool, + inodes: bool, } #[derive(PartialEq, Eq, Hash, Clone, Copy)] @@ -100,6 +105,7 @@ struct Stat { is_dir: bool, size: u64, blocks: u64, + inodes: u64, inode: Option, created: Option, accessed: u64, @@ -107,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 { @@ -121,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, @@ -138,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()), @@ -251,6 +263,18 @@ fn read_block_size(s: Option<&str>) -> usize { } } +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 + } +} + // this takes `my_stat` to avoid having to stat files multiple times. // XXX: this should use the impl Trait return type when it is stabilized fn du( @@ -268,7 +292,7 @@ fn du( Err(e) => { safe_writeln!( stderr(), - "{}: cannot read directory ‘{}‘: {}", + "{}: cannot read directory '{}': {}", options.program_name, my_stat.path.display(), e @@ -279,8 +303,14 @@ 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)) = @@ -293,14 +323,9 @@ fn du( } futures.push(du(this_stat, options, depth + 1, inodes)); } else { - if let Some(inode) = this_stat.inode { - 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); } @@ -308,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), @@ -327,10 +345,11 @@ 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 @@ -388,10 +407,196 @@ 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 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(s), _) if summarize => { + show_error!("summarizing conflicts with --max-depth={}", s); + return 1; + } + (Some(s), None) => { + show_error!("invalid maximum depth '{}'", s); + return 1; + } + (Some(_), Some(_)) | (None, _) => { /* valid */ } + } + + let options = Options { + all: matches.is_present(options::ALL), + program_name: NAME.to_owned(), + 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!["."], + }; + + 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 + } else { + 1024 + }; + let convert_size_fn = { + if matches.is_present(options::HUMAN_READABLE) || matches.is_present(options::SI) { + convert_size_human + } else if matches.is_present(options::BYTES) { + convert_size_b + } else if matches.is_present(options::BLOCK_SIZE_1K) { + convert_size_k + } else if matches.is_present(options::BLOCK_SIZE_1M) { + convert_size_m + } else { + convert_size_other + } + }; + 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 { + "full-iso" => "%Y-%m-%d %H:%M:%S.%f %z", + "long-iso" => "%Y-%m-%d %H:%M", + "iso" => "%Y-%m-%d", + _ => { + show_error!( + "invalid argument '{}' for 'time style' +Valid arguments are: +- 'full-iso' +- 'long-iso' +- 'iso' +Try '{} --help' for more information.", + s, + NAME + ); + return 1; + } + }, + None => "%Y-%m-%d %H:%M", + }; + + let line_separator = if matches.is_present(options::NULL) { + "\0" + } else { + "\n" + }; + + let mut grand_total = 0; + for path_string in files { + let path = PathBuf::from(&path_string); + 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 = 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 = { + match matches.value_of(options::TIME) { + Some(s) => match s { + "ctime" | "status" => stat.modified, + "access" | "atime" | "use" => stat.accessed, + "birth" | "creation" => { + if let Some(time) = stat.created { + time + } else { + show_error!( + "Invalid argument '{}' for --time. +'birth' and 'creation' arguments are not supported on this platform.", + s + ); + return 1; + } + } + // below should never happen as clap already restricts the values. + _ => unreachable!("Invalid field for --time"), + }, + None => stat.modified, + } + }; + DateTime::::from(UNIX_EPOCH + Duration::from_secs(secs)) + }; + if !summarize || index == len - 1 { + let time_str = tm.format(time_format_str).to_string(); + print!( + "{}\t{}\t{}{}", + convert_size(size), + time_str, + stat.path.display(), + line_separator + ); + } + } else if !summarize || index == len - 1 { + print!( + "{}\t{}{}", + convert_size(size), + stat.path.display(), + line_separator + ); + } + if options.total && index == (len - 1) { + // The last element will be the total size of the the path under + // path_string. We add it to the grand total. + grand_total += size; + } + } + } + Err(_) => { + show_error!("{}: {}", path_string, "No such file or directory"); + } + } + } + + if options.total { + print!("{}\ttotal", convert_size(grand_total)); + print!("{}", line_separator); + } + + 0 +} + +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) .version(crate_version!()) .about(SUMMARY) - .usage(&usage[..]) .after_help(LONG_HELP) .arg( Arg::with_name(options::ALL) @@ -449,8 +654,8 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .help("print sizes in human readable format (e.g., 1K 234M 2G)") ) .arg( - Arg::with_name("inodes") - .long("inodes") + Arg::with_name(options::INODES) + .long(options::INODES) .help( "list inode usage information instead of block usage like --block-size=1K" ) @@ -466,12 +671,12 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .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(options::DEREFERENCE) + .short("L") + .long(options::DEREFERENCE) + .help("dereference all symbolic links") + ) // .arg( // Arg::with_name("no-dereference") // .short("P") @@ -563,184 +768,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .hidden(true) .multiple(true) ) - .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(s), _) if summarize => { - show_error!("summarizing conflicts with --max-depth={}", s); - return 1; - } - (Some(s), None) => { - show_error!("invalid maximum depth '{}'", s); - return 1; - } - (Some(_), Some(_)) | (None, _) => { /* valid */ } - } - - let options = Options { - all: matches.is_present(options::ALL), - program_name: NAME.to_owned(), - 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), - }; - - 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 - } - }; - - 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 - } else { - 1024 - }; - let convert_size_fn = { - if matches.is_present(options::HUMAN_READABLE) || matches.is_present(options::SI) { - convert_size_human - } else if matches.is_present(options::BYTES) { - convert_size_b - } else if matches.is_present(options::BLOCK_SIZE_1K) { - convert_size_k - } else if matches.is_present(options::BLOCK_SIZE_1M) { - convert_size_m - } else { - convert_size_other - } - }; - let convert_size = |size| convert_size_fn(size, multiplier, block_size); - - let time_format_str = match matches.value_of("time-style") { - Some(s) => match s { - "full-iso" => "%Y-%m-%d %H:%M:%S.%f %z", - "long-iso" => "%Y-%m-%d %H:%M", - "iso" => "%Y-%m-%d", - _ => { - show_error!( - "invalid argument '{}' for 'time style' -Valid arguments are: -- 'full-iso' -- 'long-iso' -- 'iso' -Try '{} --help' for more information.", - s, - NAME - ); - return 1; - } - }, - None => "%Y-%m-%d %H:%M", - }; - - let line_separator = if matches.is_present(options::NULL) { - "\0" - } else { - "\n" - }; - - let mut grand_total = 0; - for path_string in files { - let path = PathBuf::from(&path_string); - match Stat::new(path) { - Ok(stat) => { - let mut inodes: HashSet = HashSet::new(); - - 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 - }; - - if threshold.map_or(false, |threshold| threshold.should_exclude(size)) { - continue; - } - - if matches.is_present(options::TIME) { - let tm = { - let secs = { - match matches.value_of(options::TIME) { - Some(s) => match s { - "ctime" | "status" => stat.modified, - "access" | "atime" | "use" => stat.accessed, - "birth" | "creation" => { - if let Some(time) = stat.created { - time - } else { - show_error!( - "Invalid argument ‘{}‘ for --time. -‘birth‘ and ‘creation‘ arguments are not supported on this platform.", - s - ); - return 1; - } - } - // below should never happen as clap already restricts the values. - _ => unreachable!("Invalid field for --time"), - }, - None => stat.modified, - } - }; - DateTime::::from(UNIX_EPOCH + Duration::from_secs(secs)) - }; - if !summarize || index == len - 1 { - let time_str = tm.format(time_format_str).to_string(); - print!( - "{}\t{}\t{}{}", - convert_size(size), - time_str, - stat.path.display(), - line_separator - ); - } - } else if !summarize || index == len - 1 { - print!( - "{}\t{}{}", - convert_size(size), - stat.path.display(), - line_separator - ); - } - if options.total && index == (len - 1) { - // The last element will be the total size of the the path under - // path_string. We add it to the grand total. - grand_total += size; - } - } - } - Err(_) => { - show_error!("{}: {}", path_string, "No such file or directory"); - } - } - } - - if options.total { - print!("{}\ttotal", convert_size(grand_total)); - print!("{}", line_separator); - } - - 0 } #[derive(Clone, Copy)] 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 d83a4fe06..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<()> { 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 0ea66d7e9..51ff92801 100644 --- a/src/uu/env/src/env.rs +++ b/src/uu/env/src/env.rs @@ -114,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!()) @@ -158,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"); 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 d9d669e7c..66c3eb259 100644 --- a/src/uu/expand/src/expand.rs +++ b/src/uu/expand/src/expand.rs @@ -108,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) @@ -138,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> { 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 8238917f7..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 @@ -133,5 +140,5 @@ Environment variables: } fn print_version() { - println!("{} {}", NAME, VERSION); + println!("{} {}", executable!(), crate_version!()); } 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 118f7f5f9..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,31 +96,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 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) { diff --git a/src/uu/groups/Cargo.toml b/src/uu/groups/Cargo.toml index 4a5a537e5..14ee44d18 100644 --- a/src/uu/groups/Cargo.toml +++ b/src/uu/groups/Cargo.toml @@ -17,7 +17,7 @@ path = "src/groups.rs" [dependencies] 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 07c25cebb..a40d1a490 100644 --- a/src/uu/groups/src/groups.rs +++ b/src/uu/groups/src/groups.rs @@ -5,6 +5,13 @@ // // 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 @@ -14,50 +21,77 @@ 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_gnu(None) - .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/Cargo.toml b/src/uu/hashsum/Cargo.toml index 11388ebf8..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" diff --git a/src/uu/hashsum/src/hashsum.rs b/src/uu/hashsum/src/hashsum.rs index a007473ab..d9feb6648 100644 --- a/src/uu/hashsum/src/hashsum.rs +++ b/src/uu/hashsum/src/hashsum.rs @@ -285,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 @@ -445,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 aceecd941..e17e17034 100644 --- a/src/uu/head/src/head.rs +++ b/src/uu/head/src/head.rs @@ -40,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) @@ -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(); 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 a14422924..fd7c5164b 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" } selinux = "0.1.1" diff --git a/src/uu/id/src/id.rs b/src/uu/id/src/id.rs index 0e29536e3..b220bfdba 100644 --- a/src/uu/id/src/id.rs +++ b/src/uu/id/src/id.rs @@ -13,11 +13,11 @@ // 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) testsuite for `id`. +// 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 Testsuite (8.32) +// * 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 @@ -26,7 +26,7 @@ // * 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 gsflag zflag cflag testsuite +// spell-checker:ignore (ToDO) asid auditid auditinfo auid cstr egid emod euid getaudit getlogin gflag nflag pline rflag termid uflag gsflag zflag cflag #![allow(non_camel_case_types)] #![allow(dead_code)] @@ -119,109 +119,9 @@ 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[..]) .after_help(&after_help[..]) - .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) - .conflicts_with(options::OPT_EFFECTIVE_USER) - .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_CONTEXT, - 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) - .conflicts_with_all(&[options::OPT_GROUP, options::OPT_EFFECTIVE_USER]) - .help("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), - ) .get_matches_from(args); let users: Vec = matches @@ -255,7 +155,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { crash!(1, "cannot print only names or real IDs in default format"); } if state.zflag && default_format && !state.cflag { - // NOTE: GNU testsuite "id/zero.sh" needs this stderr output: + // NOTE: GNU test suite "id/zero.sh" needs this stderr output: crash!(1, "option --zero not permitted in default format"); } if state.user_specified && state.cflag { @@ -302,7 +202,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { match Passwd::locate(users[i].as_str()) { Ok(p) => Some(p), Err(_) => { - show_error!("‘{}’: no such user", users[i]); + show_error!("'{}': no such user", users[i]); exit_code = 1; if i + 1 >= users.len() { break; @@ -418,6 +318,110 @@ pub fn uumain(args: impl uucore::Args) -> i32 { exit_code } +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) + .conflicts_with(options::OPT_EFFECTIVE_USER) + .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_CONTEXT, + 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) + .conflicts_with_all(&[options::OPT_GROUP, options::OPT_EFFECTIVE_USER]) + .help("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) { if let Some(p) = possible_pw { print!("uid\t{}\ngroups\t", p.name()); 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 ad5ea694c..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") @@ -308,16 +308,16 @@ fn behavior(matches: &ArgMatches) -> Result { None }; - let backup_suffix = if matches.is_present(OPT_SUFFIX) { - matches.value_of(OPT_SUFFIX).ok_or(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), @@ -330,6 +330,7 @@ fn behavior(matches: &ArgMatches) -> Result { .unwrap_or(DEFAULT_STRIP_PROGRAM), ), create_leading: matches.is_present(OPT_CREATE_LEADING), + target_dir, }) } @@ -392,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 { @@ -417,8 +419,8 @@ fn standard(paths: Vec, b: Behavior) -> i32 { } } - 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", @@ -512,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 @@ -619,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 4cdfe2141..60721f212 100644 --- a/src/uu/join/src/join.rs +++ b/src/uu/join/src/join.rs @@ -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 { 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 c48864564..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,6 +75,39 @@ 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() { 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 ce1dd15b0..b08eba97a 100644 --- a/src/uu/ln/src/ln.rs +++ b/src/uu/ln/src/ln.rs @@ -97,11 +97,71 @@ 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); + + /* the list of files */ + + let paths: Vec = matches + .values_of(ARG_FILES) + .unwrap() + .map(PathBuf::from) + .collect(); + + let overwrite_mode = if matches.is_present(options::FORCE) { + OverwriteMode::Force + } else if matches.is_present(options::INTERACTIVE) { + OverwriteMode::Interactive + } else { + OverwriteMode::NoClobber + }; + + let backup_mode = if matches.is_present(options::B) { + BackupMode::ExistingBackup + } else if matches.is_present(options::BACKUP) { + match matches.value_of(options::BACKUP) { + None => BackupMode::ExistingBackup, + Some(mode) => match mode { + "simple" | "never" => BackupMode::SimpleBackup, + "numbered" | "t" => BackupMode::NumberedBackup, + "existing" | "nil" => BackupMode::ExistingBackup, + "none" | "off" => BackupMode::NoBackup, + _ => panic!(), // cannot happen as it is managed by clap + }, + } + } else { + BackupMode::NoBackup + }; + + let backup_suffix = if matches.is_present(options::SUFFIX) { + matches.value_of(options::SUFFIX).unwrap() + } else { + "~" + }; + + let settings = Settings { + overwrite: overwrite_mode, + backup: backup_mode, + suffix: backup_suffix.to_string(), + 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", @@ -198,62 +258,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 overwrite_mode = if matches.is_present(options::FORCE) { - OverwriteMode::Force - } else if matches.is_present(options::INTERACTIVE) { - OverwriteMode::Interactive - } else { - OverwriteMode::NoClobber - }; - - let backup_mode = if matches.is_present(options::B) { - BackupMode::ExistingBackup - } else if matches.is_present(options::BACKUP) { - match matches.value_of(options::BACKUP) { - None => BackupMode::ExistingBackup, - Some(mode) => match mode { - "simple" | "never" => BackupMode::SimpleBackup, - "numbered" | "t" => BackupMode::NumberedBackup, - "existing" | "nil" => BackupMode::ExistingBackup, - "none" | "off" => BackupMode::NoBackup, - _ => panic!(), // cannot happen as it is managed by clap - }, - } - } else { - BackupMode::NoBackup - }; - - let backup_suffix = if matches.is_present(options::SUFFIX) { - matches.value_of(options::SUFFIX).unwrap() - } else { - "~" - }; - - let settings = Settings { - overwrite: overwrite_mode, - backup: backup_mode, - suffix: backup_suffix.to_string(), - 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) } fn exec(files: &[PathBuf], settings: &Settings) -> i32 { @@ -382,12 +386,15 @@ 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()) } 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 0bffa2e52..a2c6f3481 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -7,6 +7,9 @@ // spell-checker:ignore (ToDO) cpio svgz webm somegroup nlink rmvb xspf +// clippy bug https://github.com/rust-lang/rust-clippy/issues/7422 +#![allow(clippy::nonstandard_macro_braces)] + #[macro_use] extern crate uucore; #[cfg(unix)] @@ -14,7 +17,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 +28,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,26 +41,20 @@ 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}; - -static ABOUT: &str = " - By default, ls will list the files and contents of any directories on - the command line, expect that it will ignore files and directories - whose names start with '.' -"; -static AFTER_HELP: &str = "The TIME_STYLE argument can be full-iso, long-iso, iso. -Also the TIME_STYLE environment variable sets the default style to use."; +use uucore::{fs::display_permissions, version_cmp::version_cmp}; fn get_usage() -> String { format!("{0} [OPTION]... [FILE]...", executable!()) } pub mod options { + pub mod format { pub static ONE_LINE: &str = "1"; pub static LONG: &str = "long"; @@ -68,10 +65,12 @@ pub mod options { pub static LONG_NO_GROUP: &str = "o"; pub static LONG_NUMERIC_UID_GID: &str = "numeric-uid-gid"; } + pub mod files { pub static ALL: &str = "all"; pub static ALMOST_ALL: &str = "almost-all"; } + pub mod sort { pub static SIZE: &str = "S"; pub static TIME: &str = "t"; @@ -79,30 +78,36 @@ pub mod options { pub static VERSION: &str = "v"; pub static EXTENSION: &str = "X"; } + pub mod time { pub static ACCESS: &str = "u"; pub static CHANGE: &str = "c"; } + pub mod size { pub static HUMAN_READABLE: &str = "human-readable"; pub static SI: &str = "si"; } + pub mod quoting { pub static ESCAPE: &str = "escape"; pub static LITERAL: &str = "literal"; pub static C: &str = "quote-name"; } - pub static QUOTING_STYLE: &str = "quoting-style"; + pub mod indicator_style { pub static SLASH: &str = "p"; pub static FILE_TYPE: &str = "file-type"; pub static CLASSIFY: &str = "classify"; } + pub mod dereference { pub static ALL: &str = "dereference"; pub static ARGS: &str = "dereference-command-line"; pub static DIR_ARGS: &str = "dereference-command-line-symlink-to-dir"; } + + pub static QUOTING_STYLE: &str = "quoting-style"; pub static HIDE_CONTROL_CHARS: &str = "hide-control-chars"; pub static SHOW_CONTROL_CHARS: &str = "show-control-chars"; pub static WIDTH: &str = "width"; @@ -125,6 +130,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 +249,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,15 +400,13 @@ 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) { @@ -528,7 +557,7 @@ impl Config { Dereference::DirArgs }; - Config { + Ok(Config { format, files, sort, @@ -547,29 +576,54 @@ 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!()) - .version(crate_version!()) - .about(ABOUT) - .usage(&usage[..]) + 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( + "By default, ls will list the files and contents of any directories on \ + the command line, expect that it will ignore files and directories \ + whose names start with '.'.", + ) // Format arguments .arg( Arg::with_name(options::FORMAT) .long(options::FORMAT) .help("Set the display format.") .takes_value(true) - .possible_values(&["long", "verbose", "single-column", "columns", "vertical", "across", "horizontal", "commas"]) + .possible_values(&[ + "long", + "verbose", + "single-column", + "columns", + "vertical", + "across", + "horizontal", + "commas", + ]) .hide_possible_values(true) .require_equals(true) .overrides_with_all(&[ @@ -639,41 +693,51 @@ pub fn uumain(args: impl uucore::Args) -> i32 { Arg::with_name(options::format::ONE_LINE) .short(options::format::ONE_LINE) .help("List one file per line.") - .multiple(true) + .multiple(true), ) .arg( Arg::with_name(options::format::LONG_NO_GROUP) .short(options::format::LONG_NO_GROUP) - .help("Long format without group information. Identical to --format=long with --no-group.") - .multiple(true) + .help( + "Long format without group information. \ + Identical to --format=long with --no-group.", + ) + .multiple(true), ) .arg( Arg::with_name(options::format::LONG_NO_OWNER) .short(options::format::LONG_NO_OWNER) .help("Long format without owner information.") - .multiple(true) + .multiple(true), ) .arg( Arg::with_name(options::format::LONG_NUMERIC_UID_GID) .short("n") .long(options::format::LONG_NUMERIC_UID_GID) .help("-l with numeric UIDs and GIDs.") - .multiple(true) + .multiple(true), ) - // Quoting style .arg( Arg::with_name(options::QUOTING_STYLE) .long(options::QUOTING_STYLE) .takes_value(true) .help("Set quoting style.") - .possible_values(&["literal", "shell", "shell-always", "shell-escape", "shell-escape-always", "c", "escape"]) + .possible_values(&[ + "literal", + "shell", + "shell-always", + "shell-escape", + "shell-escape-always", + "c", + "escape", + ]) .overrides_with_all(&[ options::QUOTING_STYLE, options::quoting::LITERAL, options::quoting::ESCAPE, options::quoting::C, - ]) + ]), ) .arg( Arg::with_name(options::quoting::LITERAL) @@ -685,7 +749,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { options::quoting::LITERAL, options::quoting::ESCAPE, options::quoting::C, - ]) + ]), ) .arg( Arg::with_name(options::quoting::ESCAPE) @@ -697,7 +761,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { options::quoting::LITERAL, options::quoting::ESCAPE, options::quoting::C, - ]) + ]), ) .arg( Arg::with_name(options::quoting::C) @@ -709,76 +773,63 @@ pub fn uumain(args: impl uucore::Args) -> i32 { options::quoting::LITERAL, options::quoting::ESCAPE, options::quoting::C, - ]) + ]), ) - // Control characters .arg( Arg::with_name(options::HIDE_CONTROL_CHARS) .short("q") .long(options::HIDE_CONTROL_CHARS) .help("Replace control characters with '?' if they are not escaped.") - .overrides_with_all(&[ - options::HIDE_CONTROL_CHARS, - options::SHOW_CONTROL_CHARS, - ]) + .overrides_with_all(&[options::HIDE_CONTROL_CHARS, options::SHOW_CONTROL_CHARS]), ) .arg( Arg::with_name(options::SHOW_CONTROL_CHARS) .long(options::SHOW_CONTROL_CHARS) .help("Show control characters 'as is' if they are not escaped.") - .overrides_with_all(&[ - options::HIDE_CONTROL_CHARS, - options::SHOW_CONTROL_CHARS, - ]) + .overrides_with_all(&[options::HIDE_CONTROL_CHARS, options::SHOW_CONTROL_CHARS]), ) - // Time arguments .arg( Arg::with_name(options::TIME) .long(options::TIME) - .help("Show time in :\n\ + .help( + "Show time in :\n\ \taccess time (-u): atime, access, use;\n\ \tchange time (-t): ctime, status.\n\ - \tbirth time: birth, creation;") + \tbirth time: birth, creation;", + ) .value_name("field") .takes_value(true) - .possible_values(&["atime", "access", "use", "ctime", "status", "birth", "creation"]) + .possible_values(&[ + "atime", "access", "use", "ctime", "status", "birth", "creation", + ]) .hide_possible_values(true) .require_equals(true) - .overrides_with_all(&[ - options::TIME, - options::time::ACCESS, - options::time::CHANGE, - ]) + .overrides_with_all(&[options::TIME, options::time::ACCESS, options::time::CHANGE]), ) .arg( 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 \ + .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 \ 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(&[ - options::TIME, - options::time::ACCESS, - options::time::CHANGE, - ]) + format, sort according to the status change time.", + ) + .overrides_with_all(&[options::TIME, options::time::ACCESS, options::time::CHANGE]), ) .arg( Arg::with_name(options::time::ACCESS) .short(options::time::ACCESS) - .help("If the long listing format (e.g., -l, -o) is being used, print the status \ + .help( + "If the long listing format (e.g., -l, -o) is being used, print the status \ access time 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 \ - access time.") - .overrides_with_all(&[ - options::TIME, - options::time::ACCESS, - options::time::CHANGE, - ]) + access time.", + ) + .overrides_with_all(&[options::TIME, options::time::ACCESS, options::time::CHANGE]), ) - // Hide and ignore .arg( Arg::with_name(options::HIDE) @@ -786,7 +837,9 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .takes_value(true) .multiple(true) .value_name("PATTERN") - .help("do not list implied entries matching shell PATTERN (overridden by -a or -A)") + .help( + "do not list implied entries matching shell PATTERN (overridden by -a or -A)", + ), ) .arg( Arg::with_name(options::IGNORE) @@ -795,7 +848,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .takes_value(true) .multiple(true) .value_name("PATTERN") - .help("do not list implied entries matching shell PATTERN") + .help("do not list implied entries matching shell PATTERN"), ) .arg( Arg::with_name(options::IGNORE_BACKUPS) @@ -803,7 +856,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .long(options::IGNORE_BACKUPS) .help("Ignore entries which end with ~."), ) - // Sort arguments .arg( Arg::with_name(options::SORT) @@ -820,7 +872,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { options::sort::NONE, options::sort::VERSION, options::sort::EXTENSION, - ]) + ]), ) .arg( Arg::with_name(options::sort::SIZE) @@ -833,7 +885,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { options::sort::NONE, options::sort::VERSION, options::sort::EXTENSION, - ]) + ]), ) .arg( Arg::with_name(options::sort::TIME) @@ -846,7 +898,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { options::sort::NONE, options::sort::VERSION, options::sort::EXTENSION, - ]) + ]), ) .arg( Arg::with_name(options::sort::VERSION) @@ -859,7 +911,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { options::sort::NONE, options::sort::VERSION, options::sort::EXTENSION, - ]) + ]), ) .arg( Arg::with_name(options::sort::EXTENSION) @@ -872,14 +924,16 @@ pub fn uumain(args: impl uucore::Args) -> i32 { options::sort::NONE, options::sort::VERSION, options::sort::EXTENSION, - ]) + ]), ) .arg( Arg::with_name(options::sort::NONE) .short(options::sort::NONE) - .help("Do not sort; list the files in whatever order they are stored in the \ - directory. This is especially useful when listing very large directories, \ - since not doing any sorting can be noticeably faster.") + .help( + "Do not sort; list the files in whatever order they are stored in the \ + directory. This is especially useful when listing very large directories, \ + since not doing any sorting can be noticeably faster.", + ) .overrides_with_all(&[ options::SORT, options::sort::SIZE, @@ -887,9 +941,8 @@ pub fn uumain(args: impl uucore::Args) -> i32 { options::sort::NONE, options::sort::VERSION, options::sort::EXTENSION, - ]) + ]), ) - // Dereferencing .arg( Arg::with_name(options::dereference::ALL) @@ -903,48 +956,43 @@ pub fn uumain(args: impl uucore::Args) -> i32 { options::dereference::ALL, options::dereference::DIR_ARGS, options::dereference::ARGS, - ]) + ]), ) .arg( Arg::with_name(options::dereference::DIR_ARGS) .long(options::dereference::DIR_ARGS) .help( "Do not dereference symlinks except when they link to directories and are \ - given as command line arguments.", + given as command line arguments.", ) .overrides_with_all(&[ options::dereference::ALL, options::dereference::DIR_ARGS, options::dereference::ARGS, - ]) + ]), ) .arg( Arg::with_name(options::dereference::ARGS) .short("H") .long(options::dereference::ARGS) - .help( - "Do not dereference symlinks except when given as command line arguments.", - ) + .help("Do not dereference symlinks except when given as command line arguments.") .overrides_with_all(&[ options::dereference::ALL, options::dereference::DIR_ARGS, options::dereference::ARGS, - ]) + ]), ) - // Long format options .arg( Arg::with_name(options::NO_GROUP) .long(options::NO_GROUP) .short("-G") - .help("Do not show group in long format.") - ) - .arg( - Arg::with_name(options::AUTHOR) - .long(options::AUTHOR) - .help("Show author in long format. On the supported platforms, the author \ - always matches the file owner.") + .help("Do not show group in long format."), ) + .arg(Arg::with_name(options::AUTHOR).long(options::AUTHOR).help( + "Show author in long format. \ + On the supported platforms, the author always matches the file owner.", + )) // Other Flags .arg( Arg::with_name(options::files::ALL) @@ -957,9 +1005,9 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .short("A") .long(options::files::ALMOST_ALL) .help( - "In a directory, do not ignore all file names that start with '.', only ignore \ - '.' and '..'.", - ), + "In a directory, do not ignore all file names that start with '.', \ +only ignore '.' and '..'.", + ), ) .arg( Arg::with_name(options::DIRECTORY) @@ -982,7 +1030,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .arg( Arg::with_name(options::size::SI) .long(options::size::SI) - .help("Print human readable file sizes using powers of 1000 instead of 1024.") + .help("Print human readable file sizes using powers of 1000 instead of 1024."), ) .arg( Arg::with_name(options::INODE) @@ -994,9 +1042,11 @@ pub fn uumain(args: impl uucore::Args) -> i32 { Arg::with_name(options::REVERSE) .short("r") .long(options::REVERSE) - .help("Reverse whatever the sorting method is--e.g., list files in reverse \ + .help( + "Reverse whatever the sorting method is e.g., list files in reverse \ alphabetical order, youngest first, smallest first, or whatever.", - )) + ), + ) .arg( Arg::with_name(options::RECURSIVE) .short("R") @@ -1009,7 +1059,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .short("w") .help("Assume that the terminal is COLS columns wide.") .value_name("COLS") - .takes_value(true) + .takes_value(true), ) .arg( Arg::with_name(options::COLOR) @@ -1022,8 +1072,10 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .arg( Arg::with_name(options::INDICATOR_STYLE) .long(options::INDICATOR_STYLE) - .help(" append indicator with style WORD to entry names: none (default), slash\ - (-p), file-type (--file-type), classify (-F)") + .help( + "Append indicator with style WORD to entry names: \ + none (default), slash (-p), file-type (--file-type), classify (-F)", + ) .takes_value(true) .possible_values(&["none", "slash", "file-type", "classify"]) .overrides_with_all(&[ @@ -1031,21 +1083,24 @@ pub fn uumain(args: impl uucore::Args) -> i32 { options::indicator_style::SLASH, options::indicator_style::CLASSIFY, options::INDICATOR_STYLE, - ])) - .arg( + ]), + ) + .arg( Arg::with_name(options::indicator_style::CLASSIFY) .short("F") .long(options::indicator_style::CLASSIFY) - .help("Append a character to each file name indicating the file type. Also, for \ - regular files that are executable, append '*'. The file type indicators are \ - '/' for directories, '@' for symbolic links, '|' for FIFOs, '=' for sockets, \ - '>' for doors, and nothing for regular files.") + .help( + "Append a character to each file name indicating the file type. Also, for \ + regular files that are executable, append '*'. The file type indicators are \ + '/' for directories, '@' for symbolic links, '|' for FIFOs, '=' for sockets, \ + '>' for doors, and nothing for regular files.", + ) .overrides_with_all(&[ options::indicator_style::FILE_TYPE, options::indicator_style::SLASH, options::indicator_style::CLASSIFY, options::INDICATOR_STYLE, - ]) + ]), ) .arg( Arg::with_name(options::indicator_style::FILE_TYPE) @@ -1056,18 +1111,19 @@ pub fn uumain(args: impl uucore::Args) -> i32 { options::indicator_style::SLASH, options::indicator_style::CLASSIFY, options::INDICATOR_STYLE, - ])) + ]), + ) .arg( Arg::with_name(options::indicator_style::SLASH) .short(options::indicator_style::SLASH) - .help("Append / indicator to directories." - ) + .help("Append / indicator to directories.") .overrides_with_all(&[ options::indicator_style::FILE_TYPE, options::indicator_style::SLASH, options::indicator_style::CLASSIFY, options::INDICATOR_STYLE, - ])) + ]), + ) .arg( //This still needs support for posix-*, +FORMAT Arg::with_name(options::TIME_STYLE) @@ -1075,36 +1131,25 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .help("time/date format with -l; see TIME_STYLE below") .value_name("TIME_STYLE") .env("TIME_STYLE") - .possible_values(&[ - "full-iso", - "long-iso", - "iso", - "locale", - ]) - .overrides_with_all(&[ - options::TIME_STYLE - ]) + .possible_values(&["full-iso", "long-iso", "iso", "locale"]) + .overrides_with_all(&[options::TIME_STYLE]), ) .arg( Arg::with_name(options::FULL_TIME) - .long(options::FULL_TIME) - .overrides_with(options::FULL_TIME) - .help("like -l --time-style=full-iso") + .long(options::FULL_TIME) + .overrides_with(options::FULL_TIME) + .help("like -l --time-style=full-iso"), + ) + // Positional arguments + .arg( + Arg::with_name(options::PATHS) + .multiple(true) + .takes_value(true), + ) + .after_help( + "The TIME_STYLE argument can be full-iso, long-iso, iso. \ + Also the TIME_STYLE environment variable sets the default style to use.", ) - - // 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)) } /// Represents a Path along with it's associated data @@ -1187,31 +1232,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 } }; @@ -1232,11 +1273,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) { @@ -1253,7 +1291,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() @@ -1270,7 +1309,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 } @@ -1331,7 +1371,7 @@ fn enter_directory(dir: &PathData, config: &Config, out: &mut BufWriter) 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() } @@ -1463,8 +1503,6 @@ fn display_grid( } } -use uucore::fs::display_permissions; - fn display_item_long( item: &PathData, max_links: usize, @@ -1474,7 +1512,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, @@ -1733,7 +1771,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; } } 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 e8a8ef2db..7362601ba 100644 --- a/src/uu/mkdir/src/mkdir.rs +++ b/src/uu/mkdir/src/mkdir.rs @@ -5,151 +5,126 @@ // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. +// clippy bug https://github.com/rust-lang/rust-clippy/issues/7422 +#![allow(clippy::nonstandard_macro_braces)] + #[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 b8a6bbe38..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"); @@ -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 e04de8702..ef5c41abf 100644 --- a/src/uu/mktemp/src/mktemp.rs +++ b/src/uu/mktemp/src/mktemp.rs @@ -8,12 +8,18 @@ // spell-checker:ignore (paths) GPGHome +// clippy bug https://github.com/rust-lang/rust-clippy/issues/7422 +#![allow(clippy::nonstandard_macro_braces)] + #[macro_use] 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 +43,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 +173,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 +190,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 +248,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 af6781876..497f91f4e 100644 --- a/src/uu/more/Cargo.toml +++ b/src/uu/more/Cargo.toml @@ -15,7 +15,7 @@ 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" diff --git a/src/uu/more/src/more.rs b/src/uu/more/src/more.rs index 206cebbc2..ecc779ba6 100644 --- a/src/uu/more/src/more.rs +++ b/src/uu/more/src/more.rs @@ -51,7 +51,49 @@ 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!()) .arg( @@ -138,45 +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(); - 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 } #[cfg(not(target_os = "fuchsia"))] @@ -210,14 +213,14 @@ fn more(buff: &str, mut stdout: &mut Stdout, next_file: Option<&str>, silent: bo let (cols, rows) = terminal::size().unwrap(); let lines = break_buff(buff, usize::from(cols)); - let mut pager = Pager::new(rows as usize, lines, next_file, silent); - pager.draw(stdout, false); + let mut pager = Pager::new(rows, lines, next_file, silent); + pager.draw(stdout, None); if pager.should_close() { return; } loop { - let mut wrong_key = false; + let mut wrong_key = None; if event::poll(Duration::from_millis(10)).unwrap() { match event::read().unwrap() { Event::Key(KeyEvent { @@ -239,7 +242,11 @@ fn more(buff: &str, mut stdout: &mut Stdout, next_file: Option<&str>, silent: bo code: KeyCode::Char(' '), modifiers: KeyModifiers::NONE, }) => { - pager.page_down(); + if pager.should_close() { + return; + } else { + pager.page_down(); + } } Event::Key(KeyEvent { code: KeyCode::Up, @@ -247,15 +254,17 @@ fn more(buff: &str, mut stdout: &mut Stdout, next_file: Option<&str>, silent: bo }) => { pager.page_up(); } - _ => { - wrong_key = true; + Event::Resize(col, row) => { + pager.page_resize(col, row); } + Event::Key(KeyEvent { + code: KeyCode::Char(k), + .. + }) => wrong_key = Some(k), + _ => continue, } pager.draw(stdout, wrong_key); - if pager.should_close() { - return; - } } } } @@ -264,54 +273,49 @@ 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: usize, + content_rows: u16, lines: Vec, next_file: Option<&'a str>, line_count: usize, - close_on_down: bool, silent: bool, } impl<'a> Pager<'a> { - fn new(rows: usize, lines: Vec, next_file: Option<&'a str>, silent: bool) -> Self { + 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 - 1, + content_rows: rows.saturating_sub(1), lines, next_file, line_count, - close_on_down: false, silent, } } fn should_close(&mut self) -> bool { - if self.upper_mark + self.content_rows >= self.line_count { - if self.close_on_down { - return true; - } - if self.next_file.is_none() { - return true; - } else { - self.close_on_down = true; - } - } else { - self.close_on_down = false; - } - false + self.upper_mark + .saturating_add(self.content_rows.into()) + .ge(&self.line_count) } fn page_down(&mut self) { - self.upper_mark += self.content_rows; + 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); + self.upper_mark = self.upper_mark.saturating_sub(self.content_rows.into()); } - fn draw(&self, stdout: &mut std::io::Stdout, wrong_key: bool) { - let lower_mark = self.line_count.min(self.upper_mark + self.content_rows); + // 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(); @@ -323,7 +327,7 @@ impl<'a> Pager<'a> { .lines .iter() .skip(self.upper_mark) - .take(self.content_rows); + .take(self.content_rows.into()); for line in displayed_lines { stdout @@ -332,7 +336,7 @@ impl<'a> Pager<'a> { } } - fn draw_prompt(&self, stdout: &mut Stdout, lower_mark: usize, wrong_key: bool) { + 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 { @@ -345,10 +349,13 @@ impl<'a> Pager<'a> { let status = format!("--More--({})", status_inner); let banner = match (self.silent, wrong_key) { - (true, true) => "[Press 'h' for instructions. (unimplemented)]".to_string(), - (true, false) => format!("{}[Press space to continue, 'q' to quit.]", status), - (false, true) => format!("{}{}", status, BELL), - (false, false) => status, + (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!( @@ -364,7 +371,7 @@ impl<'a> Pager<'a> { // 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)); 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 bb402737e..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!(), } } 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 a3181e11f..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. diff --git a/src/uu/nohup/Cargo.toml b/src/uu/nohup/Cargo.toml index 839219a84..f7166a4b6 100644 --- a/src/uu/nohup/Cargo.toml +++ b/src/uu/nohup/Cargo.toml @@ -15,7 +15,7 @@ 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"] } diff --git a/src/uu/nohup/src/nohup.rs b/src/uu/nohup/src/nohup.rs index 4e6fd7a7e..acc101e4e 100644 --- a/src/uu/nohup/src/nohup.rs +++ b/src/uu/nohup/src/nohup.rs @@ -45,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(); @@ -82,6 +70,20 @@ 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 atty::is(atty::Stream::Stdin) { let new_stdin = match File::open(Path::new("/dev/null")) { 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 ee692d8f0..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 { 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/od.rs b/src/uu/od/src/od.rs index bf6c39011..ec5bb595a 100644 --- a/src/uu/od/src/od.rs +++ b/src/uu/od/src/od.rs @@ -214,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) @@ -434,41 +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) => { - 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) + ]) } /// Loops through the input line by line, calling print_bytes to take care of the output. 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 358881509..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,6 +95,28 @@ 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 { 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 d15730b32..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 { 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 239a0970f..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) 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/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 31da8f05d..01b14bc4d 100644 --- a/src/uu/ptx/src/ptx.rs +++ b/src/uu/ptx/src/ptx.rs @@ -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 d84756fd3..20fd60745 100644 --- a/src/uu/rm/Cargo.toml +++ b/src/uu/rm/Cargo.toml @@ -15,7 +15,7 @@ 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=[] } diff --git a/src/uu/rm/src/rm.rs b/src/uu/rm/src/rm.rs index 40a24cea7..259d1ab39 100644 --- a/src/uu/rm/src/rm.rs +++ b/src/uu/rm/src/rm.rs @@ -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) 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 fc22cca09..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> { 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 177143811..90336ea95 100644 --- a/src/uu/shred/src/shred.rs +++ b/src/uu/shred/src/shred.rs @@ -272,62 +272,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let usage = get_usage(); - let app = App::new(executable!()) - .version(crate_version!()) - .about(ABOUT) - .after_help(AFTER_HELP) - .usage(&usage[..]) - .arg( - Arg::with_name(options::FORCE) - .long(options::FORCE) - .short("f") - .help("change permissions to allow writing if necessary"), - ) - .arg( - Arg::with_name(options::ITERATIONS) - .long(options::ITERATIONS) - .short("n") - .help("overwrite N times instead of the default (3)") - .value_name("NUMBER") - .default_value("3"), - ) - .arg( - Arg::with_name(options::SIZE) - .long(options::SIZE) - .short("s") - .takes_value(true) - .value_name("N") - .help("shred this many bytes (suffixes like K, M, G accepted)"), - ) - .arg( - Arg::with_name(options::REMOVE) - .short("u") - .long(options::REMOVE) - .help("truncate and remove file after overwriting; See below"), - ) - .arg( - Arg::with_name(options::VERBOSE) - .long(options::VERBOSE) - .short("v") - .help("show progress"), - ) - .arg( - Arg::with_name(options::EXACT) - .long(options::EXACT) - .short("x") - .help( - "do not round file sizes up to the next full block;\n\ - this is the default for non-regular files", - ), - ) - .arg( - Arg::with_name(options::ZERO) - .long(options::ZERO) - .short("z") - .help("add a final overwrite with zeros to hide shredding"), - ) - // Positional arguments - .arg(Arg::with_name(options::FILE).hidden(true).multiple(true)); + let app = uu_app().usage(&usage[..]); let matches = app.get_matches_from(args); @@ -384,6 +329,64 @@ pub fn uumain(args: impl uucore::Args) -> i32 { 0 } +pub fn uu_app() -> App<'static, 'static> { + App::new(executable!()) + .version(crate_version!()) + .about(ABOUT) + .after_help(AFTER_HELP) + .arg( + Arg::with_name(options::FORCE) + .long(options::FORCE) + .short("f") + .help("change permissions to allow writing if necessary"), + ) + .arg( + Arg::with_name(options::ITERATIONS) + .long(options::ITERATIONS) + .short("n") + .help("overwrite N times instead of the default (3)") + .value_name("NUMBER") + .default_value("3"), + ) + .arg( + Arg::with_name(options::SIZE) + .long(options::SIZE) + .short("s") + .takes_value(true) + .value_name("N") + .help("shred this many bytes (suffixes like K, M, G accepted)"), + ) + .arg( + Arg::with_name(options::REMOVE) + .short("u") + .long(options::REMOVE) + .help("truncate and remove file after overwriting; See below"), + ) + .arg( + Arg::with_name(options::VERBOSE) + .long(options::VERBOSE) + .short("v") + .help("show progress"), + ) + .arg( + Arg::with_name(options::EXACT) + .long(options::EXACT) + .short("x") + .help( + "do not round file sizes up to the next full block;\n\ + this is the default for non-regular files", + ), + ) + .arg( + Arg::with_name(options::ZERO) + .long(options::ZERO) + .short("z") + .help("add a final overwrite with zeros to hide shredding"), + ) + // Positional arguments + .arg(Arg::with_name(options::FILE).hidden(true).multiple(true)) +} + // TODO: Add support for all postfixes here up to and including EiB // http://www.gnu.org/software/coreutils/manual/coreutils.html#Block-size fn get_size(size_str_opt: Option) -> Option { 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 2d1f558de..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 { diff --git a/src/uu/sleep/Cargo.toml b/src/uu/sleep/Cargo.toml index 618ea7e28..14c4c5300 100644 --- a/src/uu/sleep/Cargo.toml +++ b/src/uu/sleep/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/sleep.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/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 f53e4edb4..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,7 +34,7 @@ pub fn check(path: &str, settings: &GlobalSettings) -> i32 { move || reader(file, recycled_receiver, loaded_sender, &settings) }); for _ in 0..2 { - let _ = recycled_sender.send(Chunk::new(vec![0; 100 * 1024], |_| Vec::new())); + let _ = recycled_sender.send(RecycledChunk::new(100 * 1024)); } let mut prev_chunk: Option = None; @@ -44,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; } - let _ = recycled_sender.send(prev_chunk); + 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); } @@ -74,16 +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 carry_over = vec![]; - for chunk in receiver.iter() { - let (recycled_lines, recycled_buffer) = chunk.recycle(); + for recycled_chunk in receiver.iter() { let should_continue = chunks::read( &sender, - recycled_buffer, + recycled_chunk, None, &mut carry_over, &mut file, @@ -93,7 +100,6 @@ fn reader( } else { b'\n' }, - recycled_lines, settings, ); if !should_continue { diff --git a/src/uu/sort/src/chunks.rs b/src/uu/sort/src/chunks.rs index d452401df..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,28 +25,87 @@ 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], + } } } @@ -63,28 +122,32 @@ impl Chunk { /// (see also `read_to_chunk` for a more detailed documentation) /// /// * `sender`: The sender to send the lines to the sorter. -/// * `buffer`: The recycled buffer. All contents will be overwritten, but it must already be filled. +/// * `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)] pub fn read( sender: &SyncSender, - mut buffer: Vec, + recycled_chunk: RecycledChunk, max_buffer_size: Option, carry_over: &mut Vec, file: &mut T, next_files: &mut impl Iterator, separator: u8, - lines: Vec>, settings: &GlobalSettings, ) -> bool { - assert!(lines.is_empty()); + 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); } @@ -101,15 +164,25 @@ pub fn read( carry_over.extend_from_slice(&buffer[read..]); if read != 0 { - let payload = Chunk::new(buffer, |buf| { + 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 } }); sender.send(payload).unwrap(); } @@ -120,6 +193,7 @@ pub fn read( fn parse_lines<'a>( mut read: &'a str, lines: &mut Vec>, + line_data: &mut LineData<'a>, separator: u8, settings: &GlobalSettings, ) { @@ -128,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)), ); } diff --git a/src/uu/sort/src/ext_sort.rs b/src/uu/sort/src/ext_sort.rs index 44ff6014a..e0814b7a2 100644 --- a/src/uu/sort/src/ext_sort.rs +++ b/src/uu/sort/src/ext_sort.rs @@ -23,15 +23,16 @@ use std::{ use itertools::Itertools; +use crate::chunks::RecycledChunk; use crate::merge::ClosedTmpFile; use crate::merge::WriteableCompressedTmpFile; use crate::merge::WriteablePlainTmpFile; use crate::merge::WriteableTmpFile; -use crate::Line; 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 START_BUFFER_SIZE: usize = 8_000; @@ -98,16 +99,39 @@ fn reader_writer>, Tmp: WriteableTmpFile 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 @@ -118,7 +142,9 @@ fn reader_writer>, Tmp: WriteableTmpFile /// 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(); } } @@ -154,20 +180,16 @@ fn read_write_loop( for _ in 0..2 { let should_continue = chunks::read( &sender, - vec![ - 0; - if START_BUFFER_SIZE < buffer_size { - START_BUFFER_SIZE - } else { - buffer_size - } - ], + 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, ); @@ -216,18 +238,17 @@ fn read_write_loop( file_number += 1; - let (recycled_lines, recycled_buffer) = chunk.recycle(); + let recycled_chunk = chunk.recycle(); if let Some(sender) = &sender_option { let should_continue = chunks::read( sender, - recycled_buffer, + recycled_chunk, None, &mut carry_over, &mut file, &mut files, separator, - recycled_lines, settings, ); if !should_continue { @@ -245,12 +266,9 @@ fn write( compress_prog: Option<&str>, separator: u8, ) -> I::Closed { - chunk.with_lines_mut(|lines| { - // Write the lines to the file - let mut tmp_file = I::create(file, compress_prog); - write_lines(lines, tmp_file.as_write(), separator); - tmp_file.finished_writing() - }) + 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) { diff --git a/src/uu/sort/src/merge.rs b/src/uu/sort/src/merge.rs index 173faaffc..12d7a9b9b 100644 --- a/src/uu/sort/src/merge.rs +++ b/src/uu/sort/src/merge.rs @@ -24,7 +24,7 @@ use itertools::Itertools; use tempfile::TempDir; use crate::{ - chunks::{self, Chunk}, + chunks::{self, Chunk, RecycledChunk}, compare_by, GlobalSettings, }; @@ -125,14 +125,14 @@ fn merge_without_limit>( })); // 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(); } @@ -181,13 +181,12 @@ struct ReaderFile { /// The function running on the reader thread. fn reader( - recycled_receiver: Receiver<(usize, Chunk)>, + 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(); + for (file_idx, recycled_chunk) in recycled_receiver.iter() { if let Some(ReaderFile { file, sender, @@ -196,13 +195,12 @@ fn reader( { let should_continue = chunks::read( sender, - recycled_buffer, + recycled_chunk, None, carry_over, file.as_read(), &mut iter::empty(), separator, - recycled_lines, settings, ); if !should_continue { @@ -234,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, } @@ -257,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; @@ -274,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() { @@ -295,7 +294,7 @@ impl<'a> FileMerger<'a> { // 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(); } } @@ -312,9 +311,11 @@ 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, 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 7f3d2872e..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; @@ -44,6 +43,7 @@ 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; const NAME: &str = "sort"; @@ -170,6 +170,17 @@ pub struct GlobalSettings { 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 { @@ -210,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 { @@ -237,9 +267,16 @@ impl Default for GlobalSettings { 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, @@ -322,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; @@ -355,31 +370,44 @@ 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) { @@ -408,7 +436,8 @@ 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)); match selector.settings.mode { @@ -539,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)] @@ -640,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 { @@ -651,7 +682,7 @@ impl Default for FieldSelector { to: None, settings: Default::default(), needs_tokens: false, - is_default_selection: true, + needs_selection: false, } } } @@ -747,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, @@ -764,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> { + /// 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)]; - let num_cache = if self.settings.mode == SortMode::Numeric - || self.settings.mode == SortMode::HumanNumeric - { + if self.settings.mode == SortMode::Numeric || self.settings.mode == SortMode::HumanNumeric { // Parse NumInfo for this number. let (info, num_range) = NumInfo::parse( range, @@ -780,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 @@ -917,232 +944,7 @@ 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(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, - ]) - .help("check for sorted input; do not sort"), - ) - .arg( - Arg::with_name(options::check::CHECK_SILENT) - .short("C") - .long(options::check::CHECK_SILENT) - .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)) - .get_matches_from(args); + let matches = uu_app().usage(&usage[..]).get_matches_from(args); settings.debug = matches.is_present(options::DEBUG); @@ -1297,18 +1099,238 @@ 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 { @@ -1328,57 +1350,69 @@ fn exec(files: &[String], settings: &GlobalSettings) -> i32 { 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, @@ -1470,7 +1504,7 @@ fn get_leading_gen(input: &str) -> Range { } #[derive(Copy, Clone, PartialEq, PartialOrd, Debug)] -enum GeneralF64ParseResult { +pub enum GeneralF64ParseResult { Invalid, NaN, NegInfinity, @@ -1497,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 { @@ -1583,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 { @@ -1646,6 +1655,12 @@ 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(); @@ -1674,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] @@ -1689,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,] ); } @@ -1710,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] @@ -1722,13 +1740,7 @@ 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); - - // How big is a selection? Constant cost all lines pay when we need selections. - assert_eq!(std::mem::size_of::(), 24); + assert_eq!(std::mem::size_of::(), 24); } #[test] 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/split.rs b/src/uu/split/src/split.rs index 0d5543d8b..ccc98ee5e 100644 --- a/src/uu/split/src/split.rs +++ b/src/uu/split/src/split.rs @@ -30,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"; @@ -54,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 { @@ -201,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, @@ -234,7 +237,7 @@ impl LineSplitter { fn new(settings: &Settings) -> LineSplitter { LineSplitter { lines_per_split: settings.strategy_param.parse().unwrap_or_else(|_| { - crash!(1, "invalid number of lines: ‘{}’", settings.strategy_param) + crash!(1, "invalid number of lines: '{}'", settings.strategy_param) }), } } 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 4e1d9d2c9..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])); } }; } @@ -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 fc0c83ec8..7460a2cb2 100644 --- a/src/uu/stdbuf/src/stdbuf.rs +++ b/src/uu/stdbuf/src/stdbuf.rs @@ -154,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( @@ -191,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 8950886a2..4970cdcc2 100644 --- a/src/uu/tail/src/tail.rs +++ b/src/uu/tail/src/tail.rs @@ -72,74 +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") - // 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), - ); + let app = uu_app(); let matches = app.get_matches_from(args); @@ -244,6 +177,77 @@ pub fn uumain(args: impl uucore::Args) -> i32 { 0 } +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) { assert!(settings.follow); let mut last = readers.len() - 1; 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 5f20b95f0..dba840d3c 100644 --- a/src/uu/test/src/test.rs +++ b/src/uu/test/src/test.rs @@ -10,12 +10,34 @@ 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)); } }; @@ -126,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))?; @@ -142,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)), }) } @@ -150,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 { diff --git a/src/uu/timeout/Cargo.toml b/src/uu/timeout/Cargo.toml index 70ce64630..63a16c086 100644 --- a/src/uu/timeout/Cargo.toml +++ b/src/uu/timeout/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/timeout.rs" [dependencies] -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } libc = "0.2.42" nix = "0.20.0" uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["process", "signals"] } diff --git a/src/uu/timeout/src/timeout.rs b/src/uu/timeout/src/timeout.rs index bc92157ca..464414c5e 100644 --- a/src/uu/timeout/src/timeout.rs +++ b/src/uu/timeout/src/timeout.rs @@ -89,8 +89,8 @@ impl Config { signal, duration, preserve_status, - command, verbose, + command, } } } @@ -102,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) @@ -144,20 +160,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .required(true) .multiple(true) ) - .setting(AppSettings::TrailingVarArg); - - 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, - ) + .setting(AppSettings::TrailingVarArg) } /// Remove pre-existing SIGCHLD handlers that would make waiting for the child's exit code fail. 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 2e1c3c8e8..bfc7a4197 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -8,6 +8,9 @@ // spell-checker:ignore (ToDO) filetime strptime utcoff strs datetime MMDDhhmm +// clippy bug https://github.com/rust-lang/rust-clippy/issues/7422 +#![allow(clippy::nonstandard_macro_braces)] + pub extern crate filetime; #[macro_use] @@ -16,9 +19,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 +54,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,136 +204,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) { - match e.kind() { - std::io::ErrorKind::NotFound => { - show_error!("cannot touch '{}': {}", path, "No such file or directory") - } - std::io::ErrorKind::PermissionDenied => { - show_error!("cannot touch '{}': {}", path, "Permission denied") - } - _ => show_error!("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. @@ -265,18 +228,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))); } } + 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 local_tm_to_filetime(tm); + return Ok(local_tm_to_filetime(tm)); } - show_error!("Unable to parse date: {}\n", str); - process::exit(1); + + 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()), @@ -285,31 +252,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/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 f81a95ab2..bb7aa61d4 100644 --- a/src/uu/truncate/src/truncate.rs +++ b/src/uu/truncate/src/truncate.rs @@ -93,45 +93,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(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)) .get_matches_from(args); let files: Vec = matches @@ -168,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 @@ -210,7 +214,7 @@ 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, }, 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 49b7669df..90396ff40 100644 --- a/src/uu/tty/Cargo.toml +++ b/src/uu/tty/Cargo.toml @@ -15,7 +15,7 @@ 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"] } diff --git a/src/uu/tty/src/tty.rs b/src/uu/tty/src/tty.rs index cc5052dea..7412cdf45 100644 --- a/src/uu/tty/src/tty.rs +++ b/src/uu/tty/src/tty.rs @@ -33,19 +33,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::SILENT) - .long(options::SILENT) - .visible_alias("quiet") - .short("s") - .help("print nothing, only return an exit status") - .required(false), - ) - .get_matches_from_safe(args); + let matches = uu_app().usage(&usage[..]).get_matches_from_safe(args); let matches = match matches { Ok(m) => m, @@ -88,3 +76,17 @@ pub fn uumain(args: impl uucore::Args) -> i32 { 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 d1e1f75ca..95d71e77a 100644 --- a/src/uu/wc/src/wc.rs +++ b/src/uu/wc/src/wc.rs @@ -4,6 +4,8 @@ // * // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. +// clippy bug https://github.com/rust-lang/rust-clippy/issues/7422 +#![allow(clippy::nonstandard_macro_braces)] #[macro_use] extern crate uucore; @@ -134,10 +136,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 +200,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( 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 44f565438..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,96 +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); - - 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 } struct Who { @@ -300,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] @@ -523,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 28670c8b5..a7fc19848 100644 --- a/src/uu/whoami/Cargo.toml +++ b/src/uu/whoami/Cargo.toml @@ -15,7 +15,7 @@ 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" } 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/src/lib/features/encoding.rs b/src/uucore/src/lib/features/encoding.rs index 03fa0ed8b..08c0d27e9 100644 --- a/src/uucore/src/lib/features/encoding.rs +++ b/src/uucore/src/lib/features/encoding.rs @@ -7,6 +7,9 @@ // spell-checker:ignore (strings) ABCDEFGHIJKLMNOPQRSTUVWXYZ +// clippy bug https://github.com/rust-lang/rust-clippy/issues/7422 +#![allow(clippy::nonstandard_macro_braces)] + extern crate data_encoding; use self::data_encoding::{DecodeError, BASE32, BASE64}; diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index f765b7b3e..1ac26b04e 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -27,9 +27,11 @@ 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; @@ -186,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/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/parse_size.rs b/src/uucore/src/lib/parser/parse_size.rs index 58213adef..ec0b08c9e 100644 --- a/src/uucore/src/lib/parser/parse_size.rs +++ b/src/uucore/src/lib/parser/parse_size.rs @@ -18,7 +18,7 @@ use std::fmt; /// /// # Errors /// -/// Will return `ParseSizeError` if it’s not possible to parse this +/// 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. @@ -109,19 +109,19 @@ impl fmt::Display for ParseSizeError { impl ParseSizeError { fn parse_failure(s: &str) -> ParseSizeError { - // stderr on linux (GNU coreutils 8.32) + // 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’ + // head: invalid number of bytes: '1fb' + // tail: invalid number of bytes: '1fb' // // `SIZE` - // split: invalid number of bytes: ‘1fb’ - // truncate: Invalid number: ‘1fb’ + // split: invalid number of bytes: '1fb' + // truncate: Invalid number: '1fb' // // `MODE` - // stdbuf: invalid mode ‘1fb’ + // stdbuf: invalid mode '1fb' // // `SIZE` // sort: invalid suffix in --buffer-size argument '1fb' @@ -140,27 +140,27 @@ impl ParseSizeError { // --width // --strings // etc. - ParseSizeError::ParseFailure(format!("‘{}’", s)) + ParseSizeError::ParseFailure(format!("'{}'", s)) } fn size_too_big(s: &str) -> ParseSizeError { - // stderr on linux (GNU coreutils 8.32) + // 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 + // 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)) + // 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)) } } @@ -227,7 +227,7 @@ mod tests { )); assert_eq!( - ParseSizeError::SizeTooBig("‘1Y’: Value too large for defined data type".to_string()), + ParseSizeError::SizeTooBig("'1Y': Value too large for defined data type".to_string()), parse_size("1Y").unwrap_err() ); } @@ -262,7 +262,7 @@ mod tests { for &test_string in &test_strings { assert_eq!( parse_size(test_string).unwrap_err(), - ParseSizeError::ParseFailure(format!("‘{}’", test_string)) + ParseSizeError::ParseFailure(format!("'{}'", test_string)) ); } } 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_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 4ce587e02..19f93e499 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -1325,3 +1325,16 @@ fn test_copy_dir_with_symlinks() { 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 e21010ec8..92bab4d75 100644 --- a/tests/by-util/test_cut.rs +++ b/tests/by-util/test_cut.rs @@ -162,7 +162,7 @@ fn test_directory_and_no_such_file() { fn test_equal_as_delimiter() { new_ucmd!() .args(&["-f", "2", "-d="]) - .pipe_in("--libdir=./out/lib") + .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 93875ae51..029f5e516 100644 --- a/tests/by-util/test_du.rs +++ b/tests/by-util/test_du.rs @@ -8,7 +8,9 @@ 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"; @@ -21,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); } @@ -30,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); } @@ -54,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"), @@ -210,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; } } @@ -224,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"), @@ -242,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()); } } @@ -311,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")] @@ -366,12 +481,105 @@ fn test_du_threshold() { .arg(format!("--threshold={}", threshold)) .succeeds() .stdout_contains("links") - .stdout_does_not_contain("deeper"); + .stdout_does_not_contain("deeper_dir"); scene .ucmd() .arg(format!("--threshold=-{}", threshold)) .succeeds() .stdout_does_not_contain("links") - .stdout_contains("deeper"); + .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_groups.rs b/tests/by-util/test_groups.rs index c1b98aea1..9bd0cd12a 100644 --- a/tests/by-util/test_groups.rs +++ b/tests/by-util/test_groups.rs @@ -1,56 +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() { - if !is_ci() { - new_ucmd!().succeeds().stdout_is(expected_result(&[])); - } else { - // TODO: investigate how this could be tested in CI - // stderr = groups: cannot find name for group ID 116 - println!("test skipped:"); - } + let result = new_ucmd!().run(); + 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] #[cfg(unix)] -#[ignore = "fixme: 'groups USERNAME' needs more debugging"] fn test_groups_username() { - let scene = TestScenario::new(util_name!()); - let whoami_result = scene.cmd("whoami").run(); + let test_users = [&whoami()[..]]; - let username = if whoami_result.succeeded() { - whoami_result.stdout_move_str() - } else if is_ci() { - String::from("docker") - } else { - println!("test skipped:"); + 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()]; + + 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()); +} + +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), + ) }; - // TODO: stdout should be in the form: "username : group1 group2 group3" - - scene - .ucmd() - .arg(&username) - .succeeds() - .stdout_is(expected_result(&[&username])); -} - -#[cfg(unix)] -fn expected_result(args: &[&str]) -> String { - // We want to use GNU id. On most linux systems, this is "id", but on - // bsd-like systems (e.g. FreeBSD, MacOS), it is commonly "gid". - #[cfg(any(target_os = "linux"))] - let util_name = "id"; - #[cfg(not(target_os = "linux"))] - let util_name = "gid"; - - TestScenario::new(util_name) - .cmd_keepenv(util_name) - .env("LANGUAGE", "C") - .args(args) - .args(&["-Gn"]) - .succeeds() - .stdout_move_str() + 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 2c4b66696..246f5b62a 100755 --- a/tests/by-util/test_head.rs +++ b/tests/by-util/test_head.rs @@ -250,26 +250,48 @@ hello ); } +#[test] +fn test_bad_utf8() { + let bytes: &[u8] = b"\xfc\x80\x80\x80\x80\xaf"; + new_ucmd!() + .args(&["-c", "6"]) + .pipe_in(bytes) + .succeeds() + .stdout_is_bytes(bytes); +} + +#[test] +fn test_bad_utf8_lines() { + let input: &[u8] = + b"\xfc\x80\x80\x80\x80\xaf\nb\xfc\x80\x80\x80\x80\xaf\nb\xfc\x80\x80\x80\x80\xaf"; + let output = b"\xfc\x80\x80\x80\x80\xaf\nb\xfc\x80\x80\x80\x80\xaf\n"; + new_ucmd!() + .args(&["-n", "2"]) + .pipe_in(input) + .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’"); + .stderr_is("head: invalid number of bytes: '1024R'"); new_ucmd!() .args(&["-n", "1024R", "emptyfile.txt"]) .fails() - .stderr_is("head: invalid number of lines: ‘1024R’"); + .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"); + .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"); + .stderr_is("head: invalid number of lines: '1Y': Value too large for defined data type"); #[cfg(target_pointer_width = "32")] { let sizes = ["1000G", "10T"]; @@ -279,7 +301,7 @@ fn test_head_invalid_num() { .fails() .code_is(1) .stderr_only(format!( - "head: invalid number of bytes: ‘{}’: Value too large for defined data type", + "head: invalid number of bytes: '{}': Value too large for defined data type", size )); } diff --git a/tests/by-util/test_id.rs b/tests/by-util/test_id.rs index bbb9533af..8442af538 100644 --- a/tests/by-util/test_id.rs +++ b/tests/by-util/test_id.rs @@ -1,6 +1,6 @@ use crate::common::util::*; -// spell-checker:ignore (ToDO) testsuite coreutil +// 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, @@ -8,11 +8,12 @@ use crate::common::util::*; // 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_EXPECTED: &str = "8.30"; // Version expected for the reference `id` in $PATH -const VERSION_MULTIPLE_USERS: &str = "8.31"; +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"; +#[allow(clippy::needless_return)] macro_rules! unwrap_or_return { ( $e:expr ) => { match $e { @@ -202,13 +203,13 @@ fn test_id_multiple_users() { 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_MULTIPLE_USERS); + 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; } - // Same typical users that GNU testsuite is using. + // Same typical users that GNU test suite is using. let test_users = ["root", "man", "postfix", "sshd", &whoami()]; let scene = TestScenario::new(util_name!()); @@ -270,7 +271,7 @@ fn test_id_multiple_users_non_existing() { 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_MULTIPLE_USERS); + 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; @@ -502,7 +503,6 @@ fn test_id_no_specified_user_posixly() { "{}: test skipped: Kernel has no support for SElinux context", UUTILS_INFO ); - return; } else { let result = scene.ucmd().succeeds(); assert!(result.stdout_str().contains("context=")); @@ -517,7 +517,7 @@ fn check_coreutil_version(util_name: &str, version_expected: &str) -> String { let scene = TestScenario::new(util_name); let version_check = scene .cmd_keepenv(&util_name) - .env("LANGUAGE", "C") + .env("LC_ALL", "C") .arg("--version") .run(); version_check @@ -552,7 +552,7 @@ fn expected_result(args: &[&str]) -> Result { #[cfg(all(unix, not(target_os = "linux")))] let util_name = &format!("g{}", util_name!()); - let version_check_string = check_coreutil_version(util_name, VERSION_EXPECTED); + let version_check_string = check_coreutil_version(util_name, VERSION_MIN); if version_check_string.starts_with(UUTILS_WARNING) { return Err(version_check_string); } @@ -561,7 +561,7 @@ fn expected_result(args: &[&str]) -> Result { let scene = TestScenario::new(util_name); let result = scene .cmd_keepenv(util_name) - .env("LANGUAGE", "C") + .env("LC_ALL", "C") .args(args) .run(); 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 fc97ff779..9fa73c0bc 100644 --- a/tests/by-util/test_ln.rs +++ b/tests/by-util/test_ln.rs @@ -580,3 +580,11 @@ fn test_relative_src_already_symlink() { 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 f8aa4453b..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'"); } } @@ -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_pinky.rs b/tests/by-util/test_pinky.rs index 8b50ec2bd..bc2833a42 100644 --- a/tests/by-util/test_pinky.rs +++ b/tests/by-util/test_pinky.rs @@ -106,7 +106,7 @@ fn expected_result(args: &[&str]) -> String { #[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 fb6703f28..4a79a3eda 100644 --- a/tests/by-util/test_pr.rs +++ b/tests/by-util/test_pr.rs @@ -22,6 +22,7 @@ fn file_last_modified_time(ucmd: &UCommand, path: &str) -> 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; diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index 0f9a9d3f1..1d41ddac5 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -28,7 +28,8 @@ fn test_helper(file_name: &str, possible_args: &[&str]) { fn test_buffer_sizes() { 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) @@ -40,7 +41,8 @@ fn test_buffer_sizes() { { let buffer_sizes = ["1000G", "10T"]; for buffer_size in &buffer_sizes { - new_ucmd!() + TestScenario::new(util_name!()) + .ucmd_keepenv() .arg("-n") .arg("-S") .arg(buffer_size) @@ -125,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] @@ -454,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"] { @@ -877,7 +885,8 @@ fn test_compress() { #[test] fn test_compress_fail() { - new_ucmd!() + TestScenario::new(util_name!()) + .ucmd_keepenv() .args(&[ "ext_sort.txt", "-n", @@ -892,7 +901,8 @@ fn test_compress_fail() { #[test] fn test_merge_batches() { - new_ucmd!() + TestScenario::new(util_name!()) + .ucmd_keepenv() .args(&["ext_sort.txt", "-n", "-S", "150b"]) .succeeds() .stdout_only_fixture("ext_sort.expected"); @@ -900,7 +910,8 @@ fn test_merge_batches() { #[test] fn test_merge_batch_size() { - new_ucmd!() + TestScenario::new(util_name!()) + .ucmd_keepenv() .arg("--batch-size=2") .arg("-m") .arg("--unique") @@ -926,3 +937,17 @@ fn test_sigpipe_panic() { 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 a1350534f..229925a1c 100644 --- a/tests/by-util/test_split.rs +++ b/tests/by-util/test_split.rs @@ -309,7 +309,7 @@ fn test_split_lines_number() { .args(&["--lines", "2fb", "file"]) .fails() .code_is(1) - .stderr_only("split: invalid number of lines: ‘2fb’"); + .stderr_only("split: invalid number of lines: '2fb'"); } #[test] @@ -318,13 +318,13 @@ fn test_split_invalid_bytes_size() { .args(&["-b", "1024R"]) .fails() .code_is(1) - .stderr_only("split: invalid number of bytes: ‘1024R’"); + .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"); + .stderr_only("split: invalid number of bytes: '1Y': Value too large for defined data type"); #[cfg(target_pointer_width = "32")] { let sizes = ["1000G", "10T"]; @@ -334,7 +334,7 @@ fn test_split_invalid_bytes_size() { .fails() .code_is(1) .stderr_only(format!( - "split: invalid number of bytes: ‘{}’: Value too large for defined data type", + "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 37328d5ae..ddf78815f 100644 --- a/tests/by-util/test_stat.rs +++ b/tests/by-util/test_stat.rs @@ -317,7 +317,7 @@ fn expected_result(args: &[&str]) -> String { #[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 fc1b9324a..66892ea0f 100644 --- a/tests/by-util/test_stdbuf.rs +++ b/tests/by-util/test_stdbuf.rs @@ -63,12 +63,12 @@ fn test_stdbuf_invalid_mode_fails() { .args(&[*option, "1024R", "head"]) .fails() .code_is(125) - .stderr_only("stdbuf: invalid mode ‘1024R’"); + .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"); + .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 8478944e2..e8dd63317 100644 --- a/tests/by-util/test_tail.rs +++ b/tests/by-util/test_tail.rs @@ -364,21 +364,21 @@ fn test_tail_invalid_num() { new_ucmd!() .args(&["-c", "1024R", "emptyfile.txt"]) .fails() - .stderr_is("tail: invalid number of bytes: ‘1024R’"); + .stderr_is("tail: invalid number of bytes: '1024R'"); new_ucmd!() .args(&["-n", "1024R", "emptyfile.txt"]) .fails() - .stderr_is("tail: invalid number of lines: ‘1024R’"); + .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"); + .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"); + .stderr_is("tail: invalid number of lines: '1Y': Value too large for defined data type"); #[cfg(target_pointer_width = "32")] { let sizes = ["1000G", "10T"]; @@ -388,7 +388,7 @@ fn test_tail_invalid_num() { .fails() .code_is(1) .stderr_only(format!( - "tail: invalid number of bytes: ‘{}’: Value too large for defined data type", + "tail: invalid number of bytes: '{}': Value too large for defined data type", size )); } diff --git a/tests/by-util/test_test.rs b/tests/by-util/test_test.rs index c4964d6bf..1867927da 100644 --- a/tests/by-util/test_test.rs +++ b/tests/by-util/test_test.rs @@ -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] @@ -674,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] @@ -690,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_truncate.rs b/tests/by-util/test_truncate.rs index 2da59035e..4b2e9e502 100644 --- a/tests/by-util/test_truncate.rs +++ b/tests/by-util/test_truncate.rs @@ -249,7 +249,7 @@ fn test_size_and_reference() { #[test] fn test_error_filename_only() { - // truncate: you must specify either ‘--size’ or ‘--reference’ + // truncate: you must specify either '--size' or '--reference' new_ucmd!().args(&["file"]).fails().stderr_contains( "error: The following required arguments were not provided: --reference @@ -262,15 +262,15 @@ fn test_invalid_numbers() { 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] @@ -299,13 +299,13 @@ fn test_truncate_bytes_size() { .args(&["--size", "1024R", "file"]) .fails() .code_is(1) - .stderr_only("truncate: Invalid number: ‘1024R’"); + .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"); + .stderr_only("truncate: Invalid number: '1Y': Value too large for defined data type"); #[cfg(target_pointer_width = "32")] { let sizes = ["1000G", "10T"]; @@ -315,7 +315,7 @@ fn test_truncate_bytes_size() { .fails() .code_is(1) .stderr_only(format!( - "truncate: Invalid number: ‘{}’: Value too large for defined data type", + "truncate: Invalid number: '{}': Value too large for defined data type", size )); } diff --git a/tests/by-util/test_users.rs b/tests/by-util/test_users.rs index 68bdf9a5e..1bcbdbdc1 100644 --- a/tests/by-util/test_users.rs +++ b/tests/by-util/test_users.rs @@ -17,7 +17,7 @@ fn test_users_check_name() { #[allow(clippy::needless_borrow)] let expected = TestScenario::new(&util_name) .cmd_keepenv(util_name) - .env("LANGUAGE", "C") + .env("LC_ALL", "C") .succeeds() .stdout_move_str(); diff --git a/tests/by-util/test_who.rs b/tests/by-util/test_who.rs index 4907d2306..9315a5956 100644 --- a/tests/by-util/test_who.rs +++ b/tests/by-util/test_who.rs @@ -158,13 +158,12 @@ fn test_users() { let mut v_actual: Vec<&str> = 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); @@ -242,7 +241,7 @@ fn expected_result(args: &[&str]) -> String { #[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/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/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 ;" _