diff --git a/.cirrus.yml b/.cirrus.yml deleted file mode 100644 index 50f8a25b1..000000000 --- a/.cirrus.yml +++ /dev/null @@ -1,21 +0,0 @@ -env: - # Temporary workaround for error `error: sysinfo not supported on - # this platform` seen on FreeBSD platforms, affecting Rustup - # - # References: https://github.com/rust-lang/rustup/issues/2774 - RUSTUP_IO_THREADS: 1 - -task: - name: stable x86_64-unknown-freebsd-12 - freebsd_instance: - image: freebsd-12-2-release-amd64 - setup_script: - - pkg install -y curl gmake - - curl https://sh.rustup.rs -sSf --output rustup.sh - - sh rustup.sh -y --profile=minimal - build_script: - - . $HOME/.cargo/env - - cargo build - test_script: - - . $HOME/.cargo/env - - cargo test -p uucore -p coreutils diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 8a1e142df..d096ff43c 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -14,7 +14,6 @@ env: PROJECT_DESC: "Core universal (cross-platform) utilities" PROJECT_AUTH: "uutils" RUST_MIN_SRV: "1.47.0" ## MSRV v1.47.0 - RUST_COV_SRV: "2021-05-06" ## (~v1.52.0) supported rust version for code coverage; (date required/used by 'coverage') ## !maint: refactor when code coverage support is included in the stable channel on: [push, pull_request] @@ -102,11 +101,17 @@ jobs: fail-fast: false matrix: job: - - { os: ubuntu-latest , features: feat_os_unix } + - { os: ubuntu-latest } - { os: macos-latest , features: feat_os_macos } - { os: windows-latest , features: feat_os_windows } steps: - uses: actions/checkout@v2 + - name: Install/setup prerequisites + shell: bash + run: | + case '${{ matrix.job.os }}' in + macos-latest) brew install coreutils ;; # needed for show-utils.sh + esac - name: Initialize workflow variables id: vars shell: bash @@ -115,9 +120,14 @@ jobs: 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 + CARGO_FEATURES_OPTION='--all-features' ; + if [ -n "${{ matrix.job.features }}" ]; then CARGO_FEATURES_OPTION='--features ${{ matrix.job.features }}' ; fi outputs CARGO_FEATURES_OPTION + # * determine sub-crate utility list + UTILITY_LIST="$(./util/show-utils.sh ${CARGO_FEATURES_OPTION})" + echo UTILITY_LIST=${UTILITY_LIST} + CARGO_UTILITY_LIST_OPTIONS="$(for u in ${UTILITY_LIST}; do echo "-puu_${u}"; done;)" + outputs CARGO_UTILITY_LIST_OPTIONS - name: Install `rust` toolchain uses: actions-rs/toolchain@v1 with: @@ -130,7 +140,7 @@ jobs: run: | ## `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:]]+${PWD//\//\\/}\/(.*):([0-9]+):([0-9]+).*$/::error file=\2,line=\3,col=\4::ERROR: \`cargo clippy\`: \1 (file:'\2', line:\3)/p;" -e '}' ; exit 1 ; } + S=$(cargo +nightly clippy --all-targets ${{ steps.vars.outputs.CARGO_UTILITY_LIST_OPTIONS }} ${{ 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 @@ -521,6 +531,52 @@ jobs: n_fails=$(echo "$output" | grep "^FAIL:\s" | wc --lines) if [ $n_fails -gt 0 ] ; then echo "::warning ::${n_fails}+ test failures" ; fi + test_freebsd: + runs-on: macos-latest + name: Tests/FreeBSD test suite + env: + mem: 2048 + steps: + - uses: actions/checkout@v2 + - name: Prepare, build and test + id: test + uses: vmactions/freebsd-vm@v0.1.5 + with: + usesh: true + prepare: pkg install -y curl gmake sudo + run: | + # Need to be run in the same block. Otherwise, we are back on the mac host. + set -e + pw adduser -n cuuser -d /root/ -g wheel -c "Coreutils user to build" -w random + chown -R cuuser:wheel /root/ /Users/runner/work/coreutils/ + whoami + + # Needs to be done in a sudo as we are changing users + sudo -i -u cuuser sh << EOF + set -e + whoami + curl https://sh.rustup.rs -sSf --output rustup.sh + sh rustup.sh -y --profile=minimal + ## Info + # environment + echo "## environment" + echo "CI='${CI}'" + # tooling info display + echo "## tooling" + . $HOME/.cargo/env + cargo -V + rustc -V + env + + # where the files are resynced + cd /Users/runner/work/coreutils/coreutils/ + cargo build + cargo test --features feat_os_unix -p uucore -p coreutils + # Clean to avoid to rsync back the files + cargo clean + EOF + + coverage: name: Code Coverage runs-on: ${{ matrix.job.os }} @@ -550,7 +606,7 @@ jobs: ## VARs setup outputs() { step_id="vars"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo ::set-output name=${var}::${!var}; done; } # toolchain - TOOLCHAIN="nightly-${{ env.RUST_COV_SRV }}" ## default to "nightly" toolchain (required for certain required unstable compiler flags) ## !maint: refactor when stable channel has needed support + TOOLCHAIN="nightly" ## default to "nightly" toolchain (required for certain required unstable compiler flags) ## !maint: refactor when stable channel has needed support # * specify gnu-type TOOLCHAIN for windows; `grcov` requires gnu-style code coverage data files case ${{ matrix.job.os }} in windows-*) TOOLCHAIN="$TOOLCHAIN-x86_64-pc-windows-gnu" ;; esac; # * use requested TOOLCHAIN if specified @@ -652,3 +708,35 @@ jobs: flags: ${{ steps.vars.outputs.CODECOV_FLAGS }} name: codecov-umbrella fail_ci_if_error: false + + unused_deps: + name: Unused deps + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + - { os: ubuntu-latest , features: feat_os_unix } + - { os: macos-latest , features: feat_os_macos } + - { os: windows-latest , features: feat_os_windows } + steps: + - uses: actions/checkout@v2 + - name: Install `rust` toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + default: true + profile: minimal + - name: Install `cargo-udeps` + uses: actions-rs/install@v0.1 + with: + crate: cargo-udeps + version: latest + use-tool-cache: true + env: + RUSTUP_TOOLCHAIN: stable + - name: Confirms there isn't any unused deps + shell: bash + run: | + cargo +nightly udeps --all-targets &> udeps.log || cat udeps.log + grep "seem to have been used" udeps.log diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e3ad98ee3..f90466bed 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: - id: rust-clippy name: Rust clippy description: Run cargo clippy on files included in the commit. - entry: cargo +nightly clippy --all-targets --all-features -- + entry: cargo +nightly clippy --workspace --all-targets --all-features -- pass_filenames: false types: [file, rust] language: system diff --git a/.vscode/cspell.dictionaries/jargon.wordlist.txt b/.vscode/cspell.dictionaries/jargon.wordlist.txt index 232266427..9b1d0a8da 100644 --- a/.vscode/cspell.dictionaries/jargon.wordlist.txt +++ b/.vscode/cspell.dictionaries/jargon.wordlist.txt @@ -1,3 +1,4 @@ +AFAICT arity autogenerate autogenerated @@ -8,6 +9,8 @@ bytewise canonicalization canonicalize canonicalizing +codepoint +codepoints colorizable colorize coprime @@ -35,10 +38,14 @@ falsey fileio flamegraph fullblock +getfacl gibi gibibytes glob globbing +hardcode +hardcoded +hardcoding hardfloat hardlink hardlinks @@ -49,7 +56,9 @@ iflag iflags kibi kibibytes +libacl lcase +lossily mebi mebibytes mergeable @@ -90,6 +99,7 @@ seedable semver semiprime semiprimes +setfacl shortcode shortcodes siginfo @@ -107,6 +117,7 @@ toolchain truthy ucase unbuffered +udeps unescape unintuitive unprefixed diff --git a/.vscode/cspell.dictionaries/shell.wordlist.txt b/.vscode/cspell.dictionaries/shell.wordlist.txt index 88ecee35b..4ed281efb 100644 --- a/.vscode/cspell.dictionaries/shell.wordlist.txt +++ b/.vscode/cspell.dictionaries/shell.wordlist.txt @@ -8,6 +8,7 @@ csh globstar inotify localtime +mksh mountinfo mountpoint mtab @@ -91,6 +92,7 @@ rerast rollup sed selinuxenabled +sestatus wslpath xargs diff --git a/.vscode/cspell.dictionaries/workspace.wordlist.txt b/.vscode/cspell.dictionaries/workspace.wordlist.txt index b506f6847..d37a59465 100644 --- a/.vscode/cspell.dictionaries/workspace.wordlist.txt +++ b/.vscode/cspell.dictionaries/workspace.wordlist.txt @@ -9,12 +9,14 @@ aho-corasick backtrace blake2b_simd bstr +bytecount byteorder chacha chrono conv corasick crossterm +exacl filetime formatteriteminfo fsext @@ -66,6 +68,7 @@ structs substr splitn trunc +uninit # * uutils basenc @@ -106,12 +109,18 @@ whoami # * vars/errno errno +EACCES +EBADF +EBUSY EEXIST +EINVAL ENODATA ENOENT ENOSYS -EPERM +ENOTEMPTY EOPNOTSUPP +EPERM +EROFS # * vars/fcntl F_GETFL @@ -162,6 +171,7 @@ blocksize canonname chroot dlsym +execvp fdatasync freeaddrinfo getaddrinfo @@ -268,6 +278,7 @@ ULONG ULONGLONG UNLEN WCHAR +WSADATA errhandlingapi fileapi handleapi @@ -313,3 +324,6 @@ uucore_procs uumain uutil uutils + +# * function names +getcwd diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 7a73a41bf..000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,2 +0,0 @@ -{ -} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index cf959ee21..2b74d7923 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,12 @@ version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +[[package]] +name = "ahash" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e" + [[package]] name = "aho-corasick" version = "0.7.18" @@ -104,9 +110,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "1.3.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da1976d75adbe5fbc88130ecd119529cf1cc6a93ae1546d8696ee66f0d21af1" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitvec" @@ -167,6 +173,12 @@ dependencies = [ "utf8-width", ] +[[package]] +name = "bytecount" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72feb31ffc86498dacdbd0fcebb56138e7177a8cc5cea4516031d15ae85a742e" + [[package]] name = "byteorder" version = "1.4.3" @@ -219,7 +231,7 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "853eda514c284c2287f4bf20ae614f8781f40a81d32ecda6e91449304dfe077c" dependencies = [ - "glob 0.3.0", + "glob", "libc", "libloading", ] @@ -235,7 +247,7 @@ dependencies = [ "bitflags", "strsim", "term_size", - "textwrap", + "textwrap 0.11.0", "unicode-width", "vec_map", ] @@ -279,7 +291,7 @@ dependencies = [ "clap", "conv", "filetime", - "glob 0.3.0", + "glob", "lazy_static", "libc", "nix 0.20.0", @@ -290,7 +302,7 @@ dependencies = [ "selinux", "sha1", "tempfile", - "textwrap", + "textwrap 0.14.2", "time", "unindent", "unix_socket", @@ -363,6 +375,7 @@ dependencies = [ "uu_relpath", "uu_rm", "uu_rmdir", + "uu_runcon", "uu_seq", "uu_shred", "uu_shuf", @@ -622,6 +635,17 @@ dependencies = [ "syn", ] +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote 1.0.9", + "syn", +] + [[package]] name = "diff" version = "0.1.12" @@ -637,6 +661,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "dlv-list" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68df3f2b690c1b86e65ef7830956aededf3cb0a16f898f79b9a6f421a7b6211b" +dependencies = [ + "rand 0.8.4", +] + [[package]] name = "dns-lookup" version = "1.0.5" @@ -684,6 +717,21 @@ dependencies = [ "termcolor", ] +[[package]] +name = "exacl" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "769bbd173781e84865b957cf83449f0d2869f4c9d2f191cbbffffb3d9751ba2b" +dependencies = [ + "bitflags", + "log", + "nix 0.21.0", + "num_enum", + "scopeguard", + "serde", + "uuid", +] + [[package]] name = "fake-simd" version = "0.1.2" @@ -789,12 +837,6 @@ dependencies = [ "wasi 0.10.2+wasi-snapshot-preview1", ] -[[package]] -name = "glob" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb" - [[package]] name = "glob" version = "0.3.0" @@ -820,6 +862,15 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62aca2aba2d62b4a7f5b33f3712cb1b0692779a56fb510499d5c0aa594daeaf3" +[[package]] +name = "hashbrown" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" +dependencies = [ + "ahash", +] + [[package]] name = "heck" version = "0.3.3" @@ -878,9 +929,9 @@ dependencies = [ [[package]] name = "ioctl-sys" -version = "0.5.2" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e2c4b26352496eaaa8ca7cfa9bd99e93419d3f7983dc6e99c2a35fe9e33504a" +checksum = "1c429fffa658f288669529fc26565f728489a2e39bc7b24a428aaaf51355182e" [[package]] name = "itertools" @@ -937,9 +988,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.85" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ccac4b00700875e6a07c6cde370d44d32fa01c5a65cdd2fca6858c479d28bb3" +checksum = "3cb00336871be5ed2c8ed44b60ae9959dc5b9f08539422ed43f09e34ecaeba21" [[package]] name = "libloading" @@ -951,15 +1002,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "locale" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fdbe492a9c0238da900a1165c42fc5067161ce292678a6fe80921f30fe307fd" -dependencies = [ - "libc", -] - [[package]] name = "lock_api" version = "0.4.4" @@ -1053,15 +1095,14 @@ dependencies = [ [[package]] name = "nix" -version = "0.13.1" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dbdc256eaac2e3bd236d93ad999d3479ef775c863dbda3068c4006a92eec51b" +checksum = "b2ccba0cfe4fdf15982d1674c69b1fd80bad427d293849982668dfe454bd61f2" dependencies = [ "bitflags", "cc", - "cfg-if 0.1.10", + "cfg-if 1.0.0", "libc", - "void", ] [[package]] @@ -1076,6 +1117,19 @@ dependencies = [ "libc", ] +[[package]] +name = "nix" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3728fec49d363a50a8828a190b379a446cc5cf085c06259bbbeb34447e4ec7" +dependencies = [ + "bitflags", + "cc", + "cfg-if 1.0.0", + "libc", + "memoffset", +] + [[package]] name = "nodrop" version = "0.1.14" @@ -1116,9 +1170,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e0d047c1062aa51e256408c560894e5251f08925980e53cf1aa5bd00eec6512" +checksum = "74e768dff5fb39a41b3bcd30bb25cf989706c90d028d1ad71971987aa309d535" dependencies = [ "autocfg", "num-integer", @@ -1154,6 +1208,28 @@ dependencies = [ "libc", ] +[[package]] +name = "num_enum" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9bd055fb730c4f8f4f57d45d35cd6b3f0980535b056dc7ff119cee6a66ed6f" +dependencies = [ + "derivative", + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "486ea01961c4a818096de679a8b740b26d9033146ac5291b1c98557658f8cdd9" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote 1.0.9", + "syn", +] + [[package]] name = "number_prefix" version = "0.4.0" @@ -1194,6 +1270,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "ordered-multimap" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c672c7ad9ec066e428c00eb917124a06f08db19e2584de982cc34b1f4c12485" +dependencies = [ + "dlv-list", + "hashbrown", +] + [[package]] name = "ouroboros" version = "0.10.1" @@ -1311,6 +1397,16 @@ dependencies = [ "output_vt100", ] +[[package]] +name = "proc-macro-crate" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fdbd1df62156fbc5945f4762632564d7d038153091c3fcf1067f6aef7cff92" +dependencies = [ + "thiserror", + "toml", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -1613,9 +1709,13 @@ dependencies = [ [[package]] name = "rust-ini" -version = "0.13.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e52c148ef37f8c375d49d5a73aa70713125b7f19095948a923f80afdeb22ec2" +checksum = "63471c4aa97a1cf8332a5f97709a79a4234698de6a1f5087faf66f2dae810e22" +dependencies = [ + "cfg-if 1.0.0", + "ordered-multimap", +] [[package]] name = "rustc-hash" @@ -1646,9 +1746,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "selinux" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aa2f705dd871c2eb90888bb2d44b13218b34f5c7318c3971df62f799d0143eb" +checksum = "1cf704a543fe60d898f3253f1cc37655d0f0e9cdb68ef6230557e0e031b80608" dependencies = [ "bitflags", "libc", @@ -1670,6 +1770,26 @@ dependencies = [ "walkdir", ] +[[package]] +name = "serde" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" +dependencies = [ + "proc-macro2", + "quote 1.0.9", + "syn", +] + [[package]] name = "sha1" version = "0.6.0" @@ -1752,6 +1872,12 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" +[[package]] +name = "smawk" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" + [[package]] name = "socket2" version = "0.3.19" @@ -1783,15 +1909,15 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] name = "strum" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7318c509b5ba57f18533982607f24070a55d353e90d4cae30c467cdb2ad5ac5c" +checksum = "aaf86bbcfd1fa9670b7a129f64fc0c9fcbbfe4f1bc4210e9e98fe71ffc12cde2" [[package]] name = "strum_macros" -version = "0.20.1" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee8bc6b87a5112aeeab1f4a9f7ab634fe6cbefc4850006df31267f4cfb9e3149" +checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec" dependencies = [ "heck", "proc-macro2", @@ -1858,6 +1984,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi 0.3.9", +] + [[package]] name = "termion" version = "1.5.6" @@ -1893,6 +2029,18 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "textwrap" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" +dependencies = [ + "smawk", + "terminal_size", + "unicode-linebreak", + "unicode-width", +] + [[package]] name = "thiserror" version = "1.0.26" @@ -1923,12 +2071,30 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + [[package]] name = "typenum" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" +[[package]] +name = "unicode-linebreak" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a52dcaab0c48d931f7cc8ef826fa51690a08e1ea55117ef26f89864f532383f" +dependencies = [ + "regex", +] + [[package]] name = "unicode-segmentation" version = "1.8.0" @@ -1979,6 +2145,12 @@ dependencies = [ "log", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8-width" version = "0.1.5" @@ -2067,7 +2239,6 @@ dependencies = [ "clap", "uucore", "uucore_procs", - "walkdir", ] [[package]] @@ -2086,10 +2257,8 @@ name = "uu_chown" version = "0.0.7" dependencies = [ "clap", - "glob 0.3.0", "uucore", "uucore_procs", - "walkdir", ] [[package]] @@ -2126,10 +2295,12 @@ name = "uu_cp" version = "0.0.7" dependencies = [ "clap", + "exacl", "filetime", "ioctl-sys", "libc", "quick-error 1.2.3", + "selinux", "uucore", "uucore_procs", "walkdir", @@ -2142,7 +2313,6 @@ name = "uu_csplit" version = "0.0.7" dependencies = [ "clap", - "glob 0.2.11", "regex", "thiserror", "uucore", @@ -2202,7 +2372,7 @@ name = "uu_dircolors" version = "0.0.7" dependencies = [ "clap", - "glob 0.3.0", + "glob", "uucore", "uucore_procs", ] @@ -2334,6 +2504,7 @@ dependencies = [ "hex", "libc", "md5", + "memchr 2.4.0", "regex", "regex-syntax", "sha1", @@ -2455,7 +2626,6 @@ dependencies = [ "clap", "globset", "lazy_static", - "locale", "lscolors", "number_prefix", "once_cell", @@ -2514,7 +2684,7 @@ dependencies = [ "atty", "clap", "crossterm", - "nix 0.13.1", + "nix 0.19.1", "redox_syscall", "redox_termios", "unicode-segmentation", @@ -2539,7 +2709,7 @@ version = "0.0.7" dependencies = [ "clap", "libc", - "nix 0.13.1", + "nix 0.20.0", "uucore", "uucore_procs", ] @@ -2639,7 +2809,6 @@ dependencies = [ "itertools 0.10.1", "quick-error 2.0.1", "regex", - "time", "uucore", "uucore_procs", ] @@ -2731,6 +2900,20 @@ name = "uu_rmdir" version = "0.0.7" dependencies = [ "clap", + "libc", + "uucore", + "uucore_procs", +] + +[[package]] +name = "uu_runcon" +version = "0.0.7" +dependencies = [ + "clap", + "fts-sys", + "libc", + "selinux", + "thiserror", "uucore", "uucore_procs", ] @@ -2751,10 +2934,8 @@ name = "uu_shred" version = "0.0.7" dependencies = [ "clap", - "filetime", "libc", - "rand 0.5.6", - "time", + "rand 0.7.3", "uucore", "uucore_procs", ] @@ -2863,6 +3044,8 @@ name = "uu_tac" version = "0.0.7" dependencies = [ "clap", + "memchr 2.4.0", + "regex", "uucore", "uucore_procs", ] @@ -3008,7 +3191,6 @@ name = "uu_unlink" version = "0.0.7" dependencies = [ "clap", - "libc", "uucore", "uucore_procs", ] @@ -3036,10 +3218,12 @@ dependencies = [ name = "uu_wc" version = "0.0.7" dependencies = [ + "bytecount", "clap", "libc", "nix 0.20.0", - "thiserror", + "unicode-width", + "utf-8", "uucore", "uucore_procs", ] @@ -3058,6 +3242,7 @@ name = "uu_whoami" version = "0.0.7" dependencies = [ "clap", + "libc", "uucore", "uucore_procs", "winapi 0.3.9", @@ -3068,6 +3253,7 @@ name = "uu_yes" version = "0.0.7" dependencies = [ "clap", + "nix 0.20.0", "uucore", "uucore_procs", ] @@ -3084,11 +3270,12 @@ dependencies = [ "getopts", "lazy_static", "libc", - "nix 0.13.1", - "platform-info", + "nix 0.20.0", + "once_cell", "termion", "thiserror", "time", + "walkdir", "wild", "winapi 0.3.9", "z85", @@ -3103,6 +3290,12 @@ dependencies = [ "syn", ] +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" + [[package]] name = "vec_map" version = "0.8.2" @@ -3115,12 +3308,6 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" -[[package]] -name = "void" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" - [[package]] name = "walkdir" version = "2.3.2" @@ -3159,7 +3346,7 @@ version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "035793abb854745033f01a07647a79831eba29ec0be377205f2a25b0aa830020" dependencies = [ - "glob 0.3.0", + "glob", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index abf6a42de..7b1399abf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -95,8 +95,10 @@ feat_common_core = [ "true", "truncate", "tsort", + "touch", "unexpand", "uniq", + "unlink", "wc", "yes", ] @@ -146,7 +148,12 @@ feat_os_unix_musl = [ # NOTE: # The selinux(-sys) crate requires `libselinux` headers and shared library to be accessible in the C toolchain at compile time. # Running a uutils compiled with `feat_selinux` requires an SELinux enabled Kernel at run time. -feat_selinux = ["id/selinux", "selinux", "feat_require_selinux"] +feat_selinux = ["cp/selinux", "id/selinux", "selinux", "feat_require_selinux"] +# "feat_acl" == set of utilities providing support for acl (access control lists) if enabled with `--features feat_acl`. +# NOTE: +# On linux, the posix-acl/acl-sys crate requires `libacl` headers and shared library to be accessible in the C toolchain at compile time. +# On FreeBSD and macOS this is not required. +feat_acl = ["cp/feat_acl"] ## feature sets with requirements (restricting cross-platform availability) # # ** NOTE: these `feat_require_...` sets should be minimized as much as possible to encourage cross-platform availability of utilities @@ -176,7 +183,6 @@ feat_require_unix = [ "timeout", "tty", "uname", - "unlink", ] # "feat_require_unix_utmpx" == set of utilities requiring unix utmp/utmpx support # * ref: @@ -189,6 +195,7 @@ feat_require_unix_utmpx = [ # "feat_require_selinux" == set of utilities depending on SELinux. feat_require_selinux = [ "chcon", + "runcon", ] ## (alternate/newer/smaller platforms) feature sets # "feat_os_unix_fuchsia" == set of utilities which can be built/run on the "Fuchsia" OS (refs: ; ) @@ -239,9 +246,9 @@ test = [ "uu_test" ] [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 +textwrap = { version="0.14", features=["terminal_size"] } uucore = { version=">=0.0.9", package="uucore", path="src/uucore" } -selinux = { version="0.2.1", optional = true } +selinux = { version="0.2.3", optional = true } # * uutils uu_test = { optional=true, version="0.0.7", package="uu_test", path="src/uu/test" } # @@ -313,6 +320,7 @@ realpath = { optional=true, version="0.0.7", package="uu_realpath", path="src/uu relpath = { optional=true, version="0.0.7", package="uu_relpath", path="src/uu/relpath" } rm = { optional=true, version="0.0.7", package="uu_rm", path="src/uu/rm" } rmdir = { optional=true, version="0.0.7", package="uu_rmdir", path="src/uu/rmdir" } +runcon = { optional=true, version="0.0.7", package="uu_runcon", path="src/uu/runcon" } seq = { optional=true, version="0.0.7", package="uu_seq", path="src/uu/seq" } shred = { optional=true, version="0.0.7", package="uu_shred", path="src/uu/shred" } shuf = { optional=true, version="0.0.7", package="uu_shuf", path="src/uu/shuf" } @@ -358,7 +366,6 @@ conv = "0.3" filetime = "0.2" glob = "0.3.0" libc = "0.2" -nix = "0.20.0" pretty_assertions = "0.7.2" rand = "0.7" regex = "1.0" @@ -370,8 +377,12 @@ uucore = { version=">=0.0.9", package="uucore", path="src/uucore", features=["en walkdir = "2.2" atty = "0.2" -[target.'cfg(unix)'.dev-dependencies] +[target.'cfg(target_os = "linux")'.dev-dependencies] rlimit = "0.4.0" + +[target.'cfg(unix)'.dev-dependencies] +nix = "0.20.0" + rust-users = { version="0.10", package="users" } unix_socket = "0.5.0" diff --git a/DEVELOPER_INSTRUCTIONS.md b/DEVELOPER_INSTRUCTIONS.md index e0a5cf001..027d4dca1 100644 --- a/DEVELOPER_INSTRUCTIONS.md +++ b/DEVELOPER_INSTRUCTIONS.md @@ -1,3 +1,29 @@ +Documentation +------------- + +The source of the documentation is available on: + +https://uutils.github.io/coreutils-docs/coreutils/ + +The documentation is updated everyday on this repository: + +https://github.com/uutils/coreutils-docs + +Running GNU tests +----------------- + + + +- Check out https://github.com/coreutils/coreutils next to your fork as gnu +- Check out https://github.com/coreutils/gnulib next to your fork as gnulib +- Rename the checkout of your fork to uutils + +At the end you should have uutils, gnu and gnulib checked out next to each other. + +- Run `cd uutils && ./util/build-gnu.sh && cd ..` to get everything ready (this may take a while) +- Finally, you can run `tests with bash uutils/util/run-gnu-test.sh `. Instead of `` insert the test you want to run, e.g. `tests/misc/wc-proc`. + + Code Coverage Report Generation --------------------------------- diff --git a/GNUmakefile b/GNUmakefile index 4c550dadc..367568ca8 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -157,7 +157,8 @@ UNIX_PROGS := \ who SELINUX_PROGS := \ - chcon + chcon \ + runcon ifneq ($(OS),Windows_NT) PROGS := $(PROGS) $(UNIX_PROGS) @@ -216,6 +217,7 @@ TEST_PROGS := \ realpath \ rm \ rmdir \ + runcon \ seq \ sort \ split \ diff --git a/README.md b/README.md index bc3cc0a98..7e420bb33 100644 --- a/README.md +++ b/README.md @@ -365,8 +365,8 @@ To contribute to uutils, please see [CONTRIBUTING](CONTRIBUTING.md). | Done | Semi-Done | To Do | |-----------|-----------|--------| -| arch | cp | runcon | -| base32 | date | stty | +| arch | cp | stty | +| base32 | date | | | base64 | dd | | | basename | df | | | basenc | expr | | @@ -426,6 +426,7 @@ To contribute to uutils, please see [CONTRIBUTING](CONTRIBUTING.md). | relpath | | | | rm | | | | rmdir | | | +| runcon | | | | seq | | | | shred | | | | shuf | | | diff --git a/src/bin/coreutils.rs b/src/bin/coreutils.rs index 3e8df57f7..1de1b6354 100644 --- a/src/bin/coreutils.rs +++ b/src/bin/coreutils.rs @@ -10,10 +10,12 @@ use clap::Arg; use clap::Shell; use std::cmp; use std::collections::hash_map::HashMap; +use std::ffi::OsStr; use std::ffi::OsString; use std::io::{self, Write}; use std::path::{Path, PathBuf}; use std::process; +use uucore::display::Quotable; const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -70,18 +72,27 @@ fn main() { Some(OsString::from(*util)) } else { // unmatched binary name => regard as multi-binary container and advance argument list + uucore::set_utility_is_second_arg(); args.next() }; // 0th argument equals util name? if let Some(util_os) = util_name { - let util = util_os.as_os_str().to_string_lossy(); + fn not_found(util: &OsStr) -> ! { + println!("{}: function/utility not found", util.maybe_quote()); + process::exit(1); + } + + let util = match util_os.to_str() { + Some(util) => util, + None => not_found(&util_os), + }; if util == "completion" { gen_completions(args, utils); } - match utils.get(&util[..]) { + match utils.get(util) { Some(&(uumain, _)) => { process::exit(uumain((vec![util_os].into_iter()).chain(args))); } @@ -89,9 +100,12 @@ fn main() { if util == "--help" || util == "-h" { // see if they want help on a specific util if let Some(util_os) = args.next() { - let util = util_os.as_os_str().to_string_lossy(); + let util = match util_os.to_str() { + Some(util) => util, + None => not_found(&util_os), + }; - match utils.get(&util[..]) { + match utils.get(util) { Some(&(uumain, _)) => { let code = uumain( (vec![util_os, OsString::from("--help")].into_iter()) @@ -100,17 +114,13 @@ fn main() { io::stdout().flush().expect("could not flush stdout"); process::exit(code); } - None => { - println!("{}: function/utility not found", util); - process::exit(1); - } + None => not_found(&util_os), } } usage(&utils, binary_as_util); process::exit(0); } else { - println!("{}: function/utility not found", util); - process::exit(1); + not_found(&util_os); } } } diff --git a/src/uu/arch/src/arch.rs b/src/uu/arch/src/arch.rs index 94ec97e98..478fef6f1 100644 --- a/src/uu/arch/src/arch.rs +++ b/src/uu/arch/src/arch.rs @@ -27,7 +27,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .after_help(SUMMARY) diff --git a/src/uu/base32/Cargo.toml b/src/uu/base32/Cargo.toml index 3f6f79a0b..bc896bdb2 100644 --- a/src/uu/base32/Cargo.toml +++ b/src/uu/base32/Cargo.toml @@ -22,3 +22,7 @@ uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_p [[bin]] name = "base32" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/base32/src/base32.rs b/src/uu/base32/src/base32.rs index 9a29717ac..667fd927e 100644 --- a/src/uu/base32/src/base32.rs +++ b/src/uu/base32/src/base32.rs @@ -28,14 +28,14 @@ static VERSION: &str = env!("CARGO_PKG_VERSION"); static BASE_CMD_PARSE_ERROR: i32 = 1; -fn get_usage() -> String { - format!("{0} [OPTION]... [FILE]", executable!()) +fn usage() -> String { + format!("{0} [OPTION]... [FILE]", uucore::execution_phrase()) } pub fn uumain(args: impl uucore::Args) -> i32 { let format = Format::Base32; - let usage = get_usage(); - let name = executable!(); + let usage = usage(); + let name = uucore::util_name(); let config_result: Result = base_common::parse_base_cmd_args(args, name, VERSION, ABOUT, &usage); @@ -59,5 +59,5 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - base_common::base_app(executable!(), VERSION, ABOUT) + base_common::base_app(uucore::util_name(), VERSION, ABOUT) } diff --git a/src/uu/base32/src/base_common.rs b/src/uu/base32/src/base_common.rs index 615093311..0fd0fa5c4 100644 --- a/src/uu/base32/src/base_common.rs +++ b/src/uu/base32/src/base_common.rs @@ -9,6 +9,7 @@ use std::io::{stdout, Read, Write}; +use uucore::display::Quotable; use uucore::encoding::{wrap_print, Data, Format}; use uucore::InvalidEncodingHandling; @@ -40,8 +41,9 @@ impl Config { let name = values.next().unwrap(); if let Some(extra_op) = values.next() { return Err(format!( - "extra operand '{}'\nTry '{} --help' for more information.", - extra_op, app_name + "extra operand {}\nTry '{} --help' for more information.", + extra_op.quote(), + app_name )); } @@ -49,7 +51,7 @@ impl Config { None } else { if !Path::exists(Path::new(name)) { - return Err(format!("{}: No such file or directory", name)); + return Err(format!("{}: No such file or directory", name.maybe_quote())); } Some(name.to_owned()) } @@ -61,7 +63,7 @@ impl Config { .value_of(options::WRAP) .map(|num| { num.parse::() - .map_err(|_| format!("invalid wrap size: '{}'", num)) + .map_err(|_| format!("invalid wrap size: {}", num.quote())) }) .transpose()?; @@ -122,7 +124,7 @@ pub fn base_app<'a>(name: &str, version: &'a str, about: &'a str) -> App<'static pub fn get_input<'a>(config: &Config, stdin_ref: &'a Stdin) -> Box { match &config.to_read { Some(name) => { - let file_buf = safe_unwrap!(File::open(Path::new(name))); + let file_buf = crash_if_err!(1, File::open(Path::new(name))); Box::new(BufReader::new(file_buf)) // as Box } None => { diff --git a/src/uu/base64/Cargo.toml b/src/uu/base64/Cargo.toml index ff5a9aa48..011964dc1 100644 --- a/src/uu/base64/Cargo.toml +++ b/src/uu/base64/Cargo.toml @@ -23,3 +23,7 @@ uu_base32 = { version=">=0.0.6", package="uu_base32", path="../base32"} [[bin]] name = "base64" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/base64/src/base64.rs b/src/uu/base64/src/base64.rs index 71ed44e6e..ded157362 100644 --- a/src/uu/base64/src/base64.rs +++ b/src/uu/base64/src/base64.rs @@ -29,14 +29,14 @@ static VERSION: &str = env!("CARGO_PKG_VERSION"); static BASE_CMD_PARSE_ERROR: i32 = 1; -fn get_usage() -> String { - format!("{0} [OPTION]... [FILE]", executable!()) +fn usage() -> String { + format!("{0} [OPTION]... [FILE]", uucore::execution_phrase()) } pub fn uumain(args: impl uucore::Args) -> i32 { let format = Format::Base64; - let usage = get_usage(); - let name = executable!(); + let usage = usage(); + let name = uucore::util_name(); let config_result: Result = base_common::parse_base_cmd_args(args, name, VERSION, ABOUT, &usage); let config = config_result.unwrap_or_else(|s| crash!(BASE_CMD_PARSE_ERROR, "{}", s)); diff --git a/src/uu/basename/Cargo.toml b/src/uu/basename/Cargo.toml index b5270eba9..b5b0a462c 100644 --- a/src/uu/basename/Cargo.toml +++ b/src/uu/basename/Cargo.toml @@ -22,3 +22,7 @@ uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_p [[bin]] name = "basename" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/basename/src/basename.rs b/src/uu/basename/src/basename.rs index 5450ee3f2..464c0e4f1 100644 --- a/src/uu/basename/src/basename.rs +++ b/src/uu/basename/src/basename.rs @@ -17,11 +17,11 @@ use uucore::InvalidEncodingHandling; static SUMMARY: &str = "Print NAME with any leading directory components removed If specified, also remove a trailing SUFFIX"; -fn get_usage() -> String { +fn usage() -> String { format!( "{0} NAME [SUFFIX] {0} OPTION... NAME...", - executable!() + uucore::execution_phrase() ) } @@ -36,7 +36,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let args = args .collect_str(InvalidEncodingHandling::ConvertLossy) .accept_any(); - let usage = get_usage(); + let usage = usage(); // // Argument parsing // @@ -47,7 +47,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { crash!( 1, "{1}\nTry '{0} --help' for more information.", - executable!(), + uucore::execution_phrase(), "missing operand" ); } @@ -61,7 +61,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { crash!( 1, "extra operand '{1}'\nTry '{0} --help' for more information.", - executable!(), + uucore::execution_phrase(), matches.values_of(options::NAME).unwrap().nth(2).unwrap() ); } @@ -93,7 +93,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(SUMMARY) .arg( @@ -119,9 +119,15 @@ pub fn uu_app() -> App<'static, 'static> { } fn basename(fullname: &str, suffix: &str) -> String { - // Remove all platform-specific path separators from the end + // Remove all platform-specific path separators from the end. let path = fullname.trim_end_matches(is_separator); + // If the path contained *only* suffix characters (for example, if + // `fullname` were "///" and `suffix` were "/"), then `path` would + // be left with the empty string. In that case, we set `path` to be + // the original `fullname` to avoid returning the empty path. + let path = if path.is_empty() { fullname } else { path }; + // Convert to path buffer and get last path component let pb = PathBuf::from(path); match pb.components().last() { diff --git a/src/uu/basenc/Cargo.toml b/src/uu/basenc/Cargo.toml index 17cf0ec18..e8350d83d 100644 --- a/src/uu/basenc/Cargo.toml +++ b/src/uu/basenc/Cargo.toml @@ -23,3 +23,7 @@ uu_base32 = { version=">=0.0.6", package="uu_base32", path="../base32"} [[bin]] name = "basenc" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/basenc/src/basenc.rs b/src/uu/basenc/src/basenc.rs index e1daea4e6..86c251ad1 100644 --- a/src/uu/basenc/src/basenc.rs +++ b/src/uu/basenc/src/basenc.rs @@ -42,12 +42,12 @@ const ENCODINGS: &[(&str, Format)] = &[ ("base2m", Format::Base2Msbf), ]; -fn get_usage() -> String { - format!("{0} [OPTION]... [FILE]", executable!()) +fn usage() -> String { + format!("{0} [OPTION]... [FILE]", uucore::execution_phrase()) } pub fn uu_app() -> App<'static, 'static> { - let mut app = base_common::base_app(executable!(), crate_version!(), ABOUT); + let mut app = base_common::base_app(uucore::util_name(), crate_version!(), ABOUT); for encoding in ENCODINGS { app = app.arg(Arg::with_name(encoding.0).long(encoding.0)); } @@ -55,7 +55,7 @@ pub fn uu_app() -> App<'static, 'static> { } fn parse_cmd_args(args: impl uucore::Args) -> (Config, Format) { - let usage = get_usage(); + let usage = usage(); let matches = uu_app().usage(&usage[..]).get_matches_from( args.collect_str(InvalidEncodingHandling::ConvertLossy) .accept_any(), @@ -75,7 +75,7 @@ fn parse_cmd_args(args: impl uucore::Args) -> (Config, Format) { } pub fn uumain(args: impl uucore::Args) -> i32 { - let name = executable!(); + let name = uucore::util_name(); let (config, format) = parse_cmd_args(args); // Create a reference to stdin so we can return a locked stdin from // parse_base_cmd_args diff --git a/src/uu/cat/Cargo.toml b/src/uu/cat/Cargo.toml index d80514385..d4f137d7e 100644 --- a/src/uu/cat/Cargo.toml +++ b/src/uu/cat/Cargo.toml @@ -18,7 +18,7 @@ path = "src/cat.rs" clap = { version = "2.33", features = ["wrap_help"] } thiserror = "1.0" atty = "0.2" -uucore = { version=">=0.0.9", package="uucore", path="../../uucore", features=["fs"] } +uucore = { version=">=0.0.9", package="uucore", path="../../uucore", features=["fs", "pipes"] } uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_procs" } [target.'cfg(unix)'.dependencies] diff --git a/src/uu/cat/src/cat.rs b/src/uu/cat/src/cat.rs index b9d07bcda..baf8af6d5 100644 --- a/src/uu/cat/src/cat.rs +++ b/src/uu/cat/src/cat.rs @@ -20,6 +20,7 @@ use clap::{crate_version, App, Arg}; use std::fs::{metadata, File}; use std::io::{self, Read, Write}; use thiserror::Error; +use uucore::display::Quotable; use uucore::error::UResult; #[cfg(unix)] @@ -28,8 +29,6 @@ use std::os::unix::io::AsRawFd; /// Linux splice support #[cfg(any(target_os = "linux", target_os = "android"))] mod splice; -#[cfg(any(target_os = "linux", target_os = "android"))] -use std::os::unix::io::RawFd; /// Unix domain socket support #[cfg(unix)] @@ -136,10 +135,18 @@ struct OutputState { one_blank_kept: bool, } +#[cfg(unix)] +trait FdReadable: Read + AsRawFd {} +#[cfg(not(unix))] +trait FdReadable: Read {} + +#[cfg(unix)] +impl FdReadable for T where T: Read + AsRawFd {} +#[cfg(not(unix))] +impl FdReadable for T where T: Read {} + /// Represents an open file handle, stream, or other device -struct InputHandle { - #[cfg(any(target_os = "linux", target_os = "android"))] - file_descriptor: RawFd, +struct InputHandle { reader: R, is_interactive: bool, } @@ -234,7 +241,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .name(NAME) .version(crate_version!()) .usage(SYNTAX) @@ -296,7 +303,7 @@ pub fn uu_app() -> App<'static, 'static> { ) } -fn cat_handle( +fn cat_handle( handle: &mut InputHandle, options: &OutputOptions, state: &mut OutputState, @@ -318,8 +325,6 @@ fn cat_path( if path == "-" { let stdin = io::stdin(); let mut handle = InputHandle { - #[cfg(any(target_os = "linux", target_os = "android"))] - file_descriptor: stdin.as_raw_fd(), reader: stdin, is_interactive: atty::is(atty::Stream::Stdin), }; @@ -332,8 +337,6 @@ fn cat_path( let socket = UnixStream::connect(path)?; socket.shutdown(Shutdown::Write)?; let mut handle = InputHandle { - #[cfg(any(target_os = "linux", target_os = "android"))] - file_descriptor: socket.as_raw_fd(), reader: socket, is_interactive: false, }; @@ -346,8 +349,6 @@ fn cat_path( return Err(CatError::OutputIsInput); } let mut handle = InputHandle { - #[cfg(any(target_os = "linux", target_os = "android"))] - file_descriptor: file.as_raw_fd(), reader: file, is_interactive: false, }; @@ -386,7 +387,7 @@ fn cat_files(files: Vec, options: &OutputOptions) -> UResult<()> { for path in &files { if let Err(err) = cat_path(path, options, &mut state, &out_info) { - error_messages.push(format!("{}: {}", path, err)); + error_messages.push(format!("{}: {}", path.maybe_quote(), err)); } } if state.skipped_carriage_return { @@ -396,7 +397,7 @@ fn cat_files(files: Vec, options: &OutputOptions) -> UResult<()> { Ok(()) } else { // each next line is expected to display "cat: …" - let line_joiner = format!("\n{}: ", executable!()); + let line_joiner = format!("\n{}: ", uucore::util_name()); Err(uucore::error::USimpleError::new( error_messages.len() as i32, @@ -436,14 +437,14 @@ fn get_input_type(path: &str) -> CatResult { /// Writes handle to stdout with no configuration. This allows a /// simple memory copy. -fn write_fast(handle: &mut InputHandle) -> CatResult<()> { +fn write_fast(handle: &mut InputHandle) -> CatResult<()> { let stdout = io::stdout(); let mut stdout_lock = stdout.lock(); #[cfg(any(target_os = "linux", target_os = "android"))] { // If we're on Linux or Android, try to use the splice() system call // for faster writing. If it works, we're done. - if !splice::write_fast_using_splice(handle, stdout_lock.as_raw_fd())? { + if !splice::write_fast_using_splice(handle, &stdout_lock)? { return Ok(()); } } @@ -461,7 +462,7 @@ fn write_fast(handle: &mut InputHandle) -> CatResult<()> { /// Outputs file contents to stdout in a line-by-line fashion, /// propagating any errors that might occur. -fn write_lines( +fn write_lines( handle: &mut InputHandle, options: &OutputOptions, state: &mut OutputState, diff --git a/src/uu/cat/src/splice.rs b/src/uu/cat/src/splice.rs index 0b46fc662..26802c7e6 100644 --- a/src/uu/cat/src/splice.rs +++ b/src/uu/cat/src/splice.rs @@ -1,10 +1,11 @@ -use super::{CatResult, InputHandle}; +use super::{CatResult, FdReadable, InputHandle}; -use nix::fcntl::{splice, SpliceFFlags}; -use nix::unistd::{self, pipe}; -use std::io::Read; -use std::os::unix::io::RawFd; +use nix::unistd; +use std::os::unix::io::{AsRawFd, RawFd}; +use uucore::pipes::{pipe, splice, splice_exact}; + +const SPLICE_SIZE: usize = 1024 * 128; const BUF_SIZE: usize = 1024 * 16; /// This function is called from `write_fast()` on Linux and Android. The @@ -15,38 +16,25 @@ const BUF_SIZE: usize = 1024 * 16; /// The `bool` in the result value indicates if we need to fall back to normal /// copying or not. False means we don't have to. #[inline] -pub(super) fn write_fast_using_splice( +pub(super) fn write_fast_using_splice( handle: &mut InputHandle, - write_fd: RawFd, + write_fd: &impl AsRawFd, ) -> CatResult { - let (pipe_rd, pipe_wr) = match pipe() { - Ok(r) => r, - Err(_) => { - // It is very rare that creating a pipe fails, but it can happen. - return Ok(true); - } - }; + let (pipe_rd, pipe_wr) = pipe()?; loop { - match splice( - handle.file_descriptor, - None, - pipe_wr, - None, - BUF_SIZE, - SpliceFFlags::empty(), - ) { + match splice(&handle.reader, &pipe_wr, SPLICE_SIZE) { Ok(n) => { if n == 0 { return Ok(false); } - if splice_exact(pipe_rd, write_fd, n).is_err() { + if splice_exact(&pipe_rd, write_fd, n).is_err() { // If the first splice manages to copy to the intermediate // pipe, but the second splice to stdout fails for some reason // we can recover by copying the data that we have from the // intermediate pipe to stdout using normal read/write. Then // we tell the caller to fall back. - copy_exact(pipe_rd, write_fd, n)?; + copy_exact(pipe_rd.as_raw_fd(), write_fd.as_raw_fd(), n)?; return Ok(true); } } @@ -57,35 +45,23 @@ pub(super) fn write_fast_using_splice( } } -/// Splice wrapper which handles short writes. -#[inline] -fn splice_exact(read_fd: RawFd, write_fd: RawFd, num_bytes: usize) -> nix::Result<()> { - let mut left = num_bytes; - loop { - let written = splice(read_fd, None, write_fd, None, left, SpliceFFlags::empty())?; - left -= written; - if left == 0 { - break; - } - } - Ok(()) -} - -/// Caller must ensure that `num_bytes <= BUF_SIZE`, otherwise this function -/// will panic. The way we use this function in `write_fast_using_splice` -/// above is safe because `splice` is set to write at most `BUF_SIZE` to the -/// pipe. -#[inline] +/// Move exactly `num_bytes` bytes from `read_fd` to `write_fd`. +/// +/// Panics if not enough bytes can be read. fn copy_exact(read_fd: RawFd, write_fd: RawFd, num_bytes: usize) -> nix::Result<()> { let mut left = num_bytes; let mut buf = [0; BUF_SIZE]; - loop { - let read = unistd::read(read_fd, &mut buf[..left])?; - let written = unistd::write(write_fd, &buf[..read])?; - left -= written; - if left == 0 { - break; + while left > 0 { + let read = unistd::read(read_fd, &mut buf)?; + assert_ne!(read, 0, "unexpected end of pipe"); + let mut written = 0; + while written < read { + match unistd::write(write_fd, &buf[written..read])? { + 0 => panic!(), + n => written += n, + } } + left -= read; } Ok(()) } diff --git a/src/uu/chcon/src/chcon.rs b/src/uu/chcon/src/chcon.rs index 2a5efba46..9664f69f5 100644 --- a/src/uu/chcon/src/chcon.rs +++ b/src/uu/chcon/src/chcon.rs @@ -2,7 +2,7 @@ #![allow(clippy::upper_case_acronyms)] -use uucore::{executable, show_error, show_usage_error, show_warning}; +use uucore::{display::Quotable, show_error, show_usage_error, show_warning}; use clap::{App, Arg}; use selinux::{OpaqueSecurityContext, SecurityContext}; @@ -56,7 +56,7 @@ fn get_usage() -> String { "{0} [OPTION]... CONTEXT FILE... \n \ {0} [OPTION]... [-u USER] [-r ROLE] [-l RANGE] [-t TYPE] FILE... \n \ {0} [OPTION]... --reference=RFILE FILE...", - executable!() + uucore::execution_phrase() ) } @@ -111,13 +111,13 @@ pub fn uumain(args: impl uucore::Args) -> i32 { Ok(context) => context, Err(_r) => { - show_error!("Invalid security context '{}'.", context.to_string_lossy()); + show_error!("Invalid security context {}.", context.quote()); return libc::EXIT_FAILURE; } }; if SecurityContext::from_c_str(&c_context, false).check() == Some(false) { - show_error!("Invalid security context '{}'.", context.to_string_lossy()); + show_error!("Invalid security context {}.", context.quote()); return libc::EXIT_FAILURE; } @@ -152,7 +152,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(VERSION) .about(ABOUT) .arg( @@ -281,7 +281,6 @@ pub fn uu_app() -> App<'static, 'static> { #[derive(Debug)] struct Options { verbose: bool, - dereference: bool, preserve_root: bool, recursive_mode: RecursiveMode, affect_symlink_referent: bool, @@ -331,9 +330,6 @@ fn parse_command_line(config: clap::App, args: impl uucore::Args) -> Result Result = std::result::Result; #[derive(thiserror::Error, Debug)] @@ -30,7 +32,7 @@ pub(crate) enum Error { source: io::Error, }, - #[error("{operation} failed on '{}'", .operand1.to_string_lossy())] + #[error("{operation} failed on {}", .operand1.quote())] Io1 { operation: &'static str, operand1: OsString, diff --git a/src/uu/chgrp/Cargo.toml b/src/uu/chgrp/Cargo.toml index 5a2591f56..0d1b7e5aa 100644 --- a/src/uu/chgrp/Cargo.toml +++ b/src/uu/chgrp/Cargo.toml @@ -18,7 +18,6 @@ path = "src/chgrp.rs" clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.9", package="uucore", path="../../uucore", features=["entries", "fs", "perms"] } uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_procs" } -walkdir = "2.2" [[bin]] name = "chgrp" diff --git a/src/uu/chgrp/src/chgrp.rs b/src/uu/chgrp/src/chgrp.rs index dd851c504..1795ad0d5 100644 --- a/src/uu/chgrp/src/chgrp.rs +++ b/src/uu/chgrp/src/chgrp.rs @@ -5,170 +5,35 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (ToDO) COMFOLLOW Chgrper RFILE RFILE's derefer dgid nonblank nonprint nonprinting +// spell-checker:ignore (ToDO) COMFOLLOW Chowner RFILE RFILE's derefer dgid nonblank nonprint nonprinting #[macro_use] extern crate uucore; +use uucore::display::Quotable; pub use uucore::entries; -use uucore::fs::resolve_relative_path; -use uucore::libc::gid_t; -use uucore::perms::{wrap_chgrp, Verbosity}; +use uucore::error::{FromIo, UResult, USimpleError}; +use uucore::perms::{chown_base, options, IfFrom}; -use clap::{App, Arg}; - -extern crate walkdir; -use walkdir::WalkDir; +use clap::{App, Arg, ArgMatches}; use std::fs; -use std::fs::Metadata; use std::os::unix::fs::MetadataExt; -use std::path::Path; -use uucore::InvalidEncodingHandling; - static ABOUT: &str = "Change the group of each FILE to GROUP."; static VERSION: &str = env!("CARGO_PKG_VERSION"); -pub mod options { - pub mod verbosity { - pub static CHANGES: &str = "changes"; - pub static QUIET: &str = "quiet"; - pub static SILENT: &str = "silent"; - pub static VERBOSE: &str = "verbose"; - } - pub mod preserve_root { - pub static PRESERVE: &str = "preserve-root"; - pub static NO_PRESERVE: &str = "no-preserve-root"; - } - pub mod dereference { - pub static DEREFERENCE: &str = "dereference"; - pub static NO_DEREFERENCE: &str = "no-dereference"; - } - pub static RECURSIVE: &str = "recursive"; - pub mod traverse { - pub static TRAVERSE: &str = "H"; - pub static NO_TRAVERSE: &str = "P"; - pub static EVERY: &str = "L"; - } - pub static REFERENCE: &str = "reference"; - pub static ARG_GROUP: &str = "GROUP"; - pub static ARG_FILES: &str = "FILE"; -} - -const FTS_COMFOLLOW: u8 = 1; -const FTS_PHYSICAL: u8 = 1 << 1; -const FTS_LOGICAL: u8 = 1 << 2; - fn get_usage() -> String { format!( "{0} [OPTION]... GROUP FILE...\n {0} [OPTION]... --reference=RFILE FILE...", - executable!() + uucore::execution_phrase() ) } -pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args - .collect_str(InvalidEncodingHandling::ConvertLossy) - .accept_any(); - - let usage = get_usage(); - - let mut app = uu_app().usage(&usage[..]); - - // we change the positional args based on whether - // --reference was used. - let mut reference = false; - let mut help = false; - // stop processing options on -- - for arg in args.iter().take_while(|s| *s != "--") { - if arg.starts_with("--reference=") || arg == "--reference" { - reference = true; - } else if arg == "--help" { - // we stop processing once we see --help, - // as it doesn't matter if we've seen reference or not - help = true; - break; - } - } - - if help || !reference { - // add both positional arguments - app = app.arg( - Arg::with_name(options::ARG_GROUP) - .value_name(options::ARG_GROUP) - .required(true) - .takes_value(true) - .multiple(false), - ) - } - app = app.arg( - Arg::with_name(options::ARG_FILES) - .value_name(options::ARG_FILES) - .multiple(true) - .takes_value(true) - .required(true) - .min_values(1), - ); - - let matches = app.get_matches_from(args); - - /* Get the list of files */ - let files: Vec = matches - .values_of(options::ARG_FILES) - .map(|v| v.map(ToString::to_string).collect()) - .unwrap_or_default(); - - let preserve_root = matches.is_present(options::preserve_root::PRESERVE); - - let mut derefer = if matches.is_present(options::dereference::DEREFERENCE) { - 1 - } else if matches.is_present(options::dereference::NO_DEREFERENCE) { - 0 - } else { - -1 - }; - - let mut bit_flag = if matches.is_present(options::traverse::TRAVERSE) { - FTS_COMFOLLOW | FTS_PHYSICAL - } else if matches.is_present(options::traverse::EVERY) { - FTS_LOGICAL - } else { - FTS_PHYSICAL - }; - - let recursive = matches.is_present(options::RECURSIVE); - if recursive { - if bit_flag == FTS_PHYSICAL { - if derefer == 1 { - show_error!("-R --dereference requires -H or -L"); - return 1; - } - derefer = 0; - } - } else { - bit_flag = FTS_PHYSICAL; - } - - let verbosity = if matches.is_present(options::verbosity::CHANGES) { - Verbosity::Changes - } else if matches.is_present(options::verbosity::SILENT) - || matches.is_present(options::verbosity::QUIET) - { - Verbosity::Silent - } else if matches.is_present(options::verbosity::VERBOSE) { - Verbosity::Verbose - } else { - Verbosity::Normal - }; - +fn parse_gid_and_uid(matches: &ArgMatches) -> UResult<(Option, Option, IfFrom)> { let dest_gid = if let Some(file) = matches.value_of(options::REFERENCE) { - match fs::metadata(&file) { - Ok(meta) => Some(meta.gid()), - Err(e) => { - show_error!("failed to get attributes of '{}': {}", file, e); - return 1; - } - } + fs::metadata(&file) + .map(|meta| Some(meta.gid())) + .map_err_context(|| format!("failed to get attributes of {}", file.quote()))? } else { let group = matches.value_of(options::ARG_GROUP).unwrap_or_default(); if group.is_empty() { @@ -177,27 +42,32 @@ pub fn uumain(args: impl uucore::Args) -> i32 { match entries::grp2gid(group) { Ok(g) => Some(g), _ => { - show_error!("invalid group: {}", group); - return 1; + return Err(USimpleError::new( + 1, + format!("invalid group: {}", group.quote()), + )) } } } }; + Ok((dest_gid, None, IfFrom::All)) +} - let executor = Chgrper { - bit_flag, - dest_gid, - verbosity, - recursive, - dereference: derefer != 0, - preserve_root, - files, - }; - executor.exec() +#[uucore_procs::gen_uumain] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let usage = get_usage(); + + chown_base( + uu_app().usage(&usage[..]), + args, + options::ARG_GROUP, + parse_gid_and_uid, + true, + ) } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(VERSION) .about(ABOUT) .arg( @@ -275,172 +145,3 @@ pub fn uu_app() -> App<'static, 'static> { .help("traverse every symbolic link to a directory encountered"), ) } - -struct Chgrper { - dest_gid: Option, - bit_flag: u8, - verbosity: Verbosity, - files: Vec, - recursive: bool, - preserve_root: bool, - dereference: bool, -} - -macro_rules! unwrap { - ($m:expr, $e:ident, $err:block) => { - match $m { - Ok(meta) => meta, - Err($e) => $err, - } - }; -} - -impl Chgrper { - fn exec(&self) -> i32 { - let mut ret = 0; - for f in &self.files { - ret |= self.traverse(f); - } - ret - } - - #[cfg(windows)] - fn is_bind_root>(&self, root: P) -> bool { - // TODO: is there an equivalent on Windows? - false - } - - #[cfg(unix)] - fn is_bind_root>(&self, path: P) -> bool { - if let (Ok(given), Ok(root)) = (fs::metadata(path), fs::metadata("/")) { - given.dev() == root.dev() && given.ino() == root.ino() - } else { - // FIXME: not totally sure if it's okay to just ignore an error here - false - } - } - - fn traverse>(&self, root: P) -> i32 { - let follow_arg = self.dereference || self.bit_flag != FTS_PHYSICAL; - let path = root.as_ref(); - let meta = match self.obtain_meta(path, follow_arg) { - Some(m) => m, - _ => return 1, - }; - - // Prohibit only if: - // (--preserve-root and -R present) && - // ( - // (argument is not symlink && resolved to be '/') || - // (argument is symlink && should follow argument && resolved to be '/') - // ) - if self.recursive && self.preserve_root { - let may_exist = if follow_arg { - path.canonicalize().ok() - } else { - let real = resolve_relative_path(path); - if real.is_dir() { - Some(real.canonicalize().expect("failed to get real path")) - } else { - Some(real.into_owned()) - } - }; - - if let Some(p) = may_exist { - if p.parent().is_none() || self.is_bind_root(p) { - show_error!("it is dangerous to operate recursively on '/'"); - show_error!("use --no-preserve-root to override this failsafe"); - return 1; - } - } - } - - let ret = match wrap_chgrp( - path, - &meta, - self.dest_gid, - follow_arg, - self.verbosity.clone(), - ) { - Ok(n) => { - if !n.is_empty() { - show_error!("{}", n); - } - 0 - } - Err(e) => { - if self.verbosity != Verbosity::Silent { - show_error!("{}", e); - } - 1 - } - }; - - if !self.recursive { - ret - } else { - ret | self.dive_into(&root) - } - } - - fn dive_into>(&self, root: P) -> i32 { - let mut ret = 0; - let root = root.as_ref(); - let follow = self.dereference || self.bit_flag & FTS_LOGICAL != 0; - for entry in WalkDir::new(root).follow_links(follow).min_depth(1) { - let entry = unwrap!(entry, e, { - ret = 1; - show_error!("{}", e); - continue; - }); - let path = entry.path(); - let meta = match self.obtain_meta(path, follow) { - Some(m) => m, - _ => { - ret = 1; - continue; - } - }; - - ret = match wrap_chgrp(path, &meta, self.dest_gid, follow, self.verbosity.clone()) { - Ok(n) => { - if !n.is_empty() { - show_error!("{}", n); - } - 0 - } - Err(e) => { - if self.verbosity != Verbosity::Silent { - show_error!("{}", e); - } - 1 - } - } - } - - ret - } - - fn obtain_meta>(&self, path: P, follow: bool) -> Option { - use self::Verbosity::*; - let path = path.as_ref(); - let meta = if follow { - unwrap!(path.metadata(), e, { - match self.verbosity { - Silent => (), - _ => show_error!("cannot access '{}': {}", path.display(), e), - } - return None; - }) - } else { - unwrap!(path.symlink_metadata(), e, { - match self.verbosity { - Silent => (), - _ => show_error!("cannot dereference '{}': {}", path.display(), e), - } - return None; - }) - }; - Some(meta) - } -} diff --git a/src/uu/chmod/src/chmod.rs b/src/uu/chmod/src/chmod.rs index d89827c97..e25202fbe 100644 --- a/src/uu/chmod/src/chmod.rs +++ b/src/uu/chmod/src/chmod.rs @@ -14,6 +14,7 @@ use clap::{crate_version, App, Arg}; use std::fs; use std::os::unix::fs::{MetadataExt, PermissionsExt}; use std::path::Path; +use uucore::display::Quotable; use uucore::fs::display_permissions_unix; use uucore::libc::mode_t; #[cfg(not(windows))] @@ -36,12 +37,12 @@ mod options { pub const FILE: &str = "FILE"; } -fn get_usage() -> String { +fn usage() -> String { format!( "{0} [OPTION]... MODE[,MODE]... FILE... or: {0} [OPTION]... OCTAL-MODE FILE... or: {0} [OPTION]... --reference=RFILE FILE...", - executable!() + uucore::execution_phrase() ) } @@ -58,7 +59,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { // a possible MODE prefix '-' needs to be removed (e.g. "chmod -x FILE"). let mode_had_minus_prefix = strip_minus_from_mode(&mut args); - let usage = get_usage(); + let usage = usage(); let after_help = get_long_usage(); let matches = uu_app() @@ -75,7 +76,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .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), + Err(err) => crash!(1, "cannot stat attributes of {}: {}", fref.quote(), err), }); let modes = matches.value_of(options::MODE).unwrap(); // should always be Some because required let cmode = if mode_had_minus_prefix { @@ -98,6 +99,10 @@ pub fn uumain(args: impl uucore::Args) -> i32 { Some(cmode) }; + if files.is_empty() { + crash!(1, "missing operand"); + } + let chmoder = Chmoder { changes, quiet, @@ -116,7 +121,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .arg( @@ -180,6 +185,9 @@ pub fn uu_app() -> App<'static, 'static> { // e.g. "chmod -v -xw -R FILE" -> "chmod -v xw -R FILE" pub fn strip_minus_from_mode(args: &mut Vec) -> bool { for arg in args { + if arg == "--" { + break; + } if arg.starts_with('-') { if let Some(second) = arg.chars().nth(1) { match second { @@ -216,21 +224,24 @@ impl Chmoder { if !file.exists() { if is_symlink(file) { println!( - "failed to change mode of '{}' from 0000 (---------) to 0000 (---------)", - filename + "failed to change mode of {} from 0000 (---------) to 0000 (---------)", + filename.quote() ); if !self.quiet { - show_error!("cannot operate on dangling symlink '{}'", filename); + show_error!("cannot operate on dangling symlink {}", filename.quote()); } - } else { - show_error!("cannot access '{}': No such file or directory", filename); + } else if !self.quiet { + show_error!( + "cannot access {}: No such file or directory", + filename.quote() + ); } return Err(1); } if self.recursive && self.preserve_root && filename == "/" { show_error!( - "it is dangerous to operate recursively on '{}'\nuse --no-preserve-root to override this failsafe", - filename + "it is dangerous to operate recursively on {}\nuse --no-preserve-root to override this failsafe", + filename.quote() ); return Err(1); } @@ -253,23 +264,27 @@ impl Chmoder { // instead it just sets the readonly attribute on the file Err(0) } - #[cfg(any(unix, target_os = "redox"))] + #[cfg(unix)] fn chmod_file(&self, file: &Path) -> Result<(), i32> { - let mut fperm = match fs::metadata(file) { + use uucore::mode::get_umask; + + let fperm = match fs::metadata(file) { Ok(meta) => meta.mode() & 0o7777, Err(err) => { if is_symlink(file) { if self.verbose { println!( - "neither symbolic link '{}' nor referent has been changed", - file.display() + "neither symbolic link {} nor referent has been changed", + file.quote() ); } return Ok(()); } else if err.kind() == std::io::ErrorKind::PermissionDenied { - show_error!("'{}': Permission denied", file.display()); + // These two filenames would normally be conditionally + // quoted, but GNU's tests expect them to always be quoted + show_error!("{}: Permission denied", file.quote()); } else { - show_error!("'{}': {}", file.display(), err); + show_error!("{}: {}", file.quote(), err); } return Err(1); } @@ -278,18 +293,30 @@ impl Chmoder { Some(mode) => self.change_file(fperm, mode, file)?, None => { let cmode_unwrapped = self.cmode.clone().unwrap(); + let mut new_mode = fperm; + let mut naively_expected_new_mode = new_mode; for mode in cmode_unwrapped.split(',') { // cmode is guaranteed to be Some in this case let arr: &[char] = &['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; let result = if mode.contains(arr) { - mode::parse_numeric(fperm, mode) + mode::parse_numeric(new_mode, mode, file.is_dir()).map(|v| (v, v)) } else { - mode::parse_symbolic(fperm, mode, file.is_dir()) + mode::parse_symbolic(new_mode, mode, get_umask(), file.is_dir()).map(|m| { + // calculate the new mode as if umask was 0 + let naive_mode = mode::parse_symbolic( + naively_expected_new_mode, + mode, + 0, + file.is_dir(), + ) + .unwrap(); // we know that mode must be valid, so this cannot fail + (m, naive_mode) + }) }; match result { - Ok(mode) => { - self.change_file(fperm, mode, file)?; - fperm = mode; + Ok((mode, naive_mode)) => { + new_mode = mode; + naively_expected_new_mode = naive_mode; } Err(f) => { if !self.quiet { @@ -299,6 +326,17 @@ impl Chmoder { } } } + self.change_file(fperm, new_mode, file)?; + // if a permission would have been removed if umask was 0, but it wasn't because umask was not 0, print an error and fail + if (new_mode & !naively_expected_new_mode) != 0 { + show_error!( + "{}: new permissions are {}, not {}", + file.maybe_quote(), + display_permissions_unix(new_mode as mode_t, false), + display_permissions_unix(naively_expected_new_mode as mode_t, false) + ); + return Err(1); + } } } @@ -310,8 +348,8 @@ impl Chmoder { if fperm == mode { if self.verbose && !self.changes { println!( - "mode of '{}' retained as {:04o} ({})", - file.display(), + "mode of {} retained as {:04o} ({})", + file.quote(), fperm, display_permissions_unix(fperm as mode_t, false), ); @@ -322,9 +360,9 @@ impl Chmoder { show_error!("{}", err); } if self.verbose { - show_error!( - "failed to change mode of file '{}' from {:o} ({}) to {:o} ({})", - file.display(), + println!( + "failed to change mode of file {} from {:04o} ({}) to {:04o} ({})", + file.quote(), fperm, display_permissions_unix(fperm as mode_t, false), mode, @@ -334,9 +372,9 @@ impl Chmoder { Err(1) } else { if self.verbose || self.changes { - show_error!( - "mode of '{}' changed from {:o} ({}) to {:o} ({})", - file.display(), + println!( + "mode of {} changed from {:04o} ({}) to {:04o} ({})", + file.quote(), fperm, display_permissions_unix(fperm as mode_t, false), mode, diff --git a/src/uu/chown/Cargo.toml b/src/uu/chown/Cargo.toml index 828c214be..e6dc7d4fe 100644 --- a/src/uu/chown/Cargo.toml +++ b/src/uu/chown/Cargo.toml @@ -16,10 +16,8 @@ path = "src/chown.rs" [dependencies] clap = { version = "2.33", features = ["wrap_help"] } -glob = "0.3.0" uucore = { version=">=0.0.9", package="uucore", path="../../uucore", features=["entries", "fs", "perms"] } uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_procs" } -walkdir = "2.2" [[bin]] name = "chown" diff --git a/src/uu/chown/src/chown.rs b/src/uu/chown/src/chown.rs index 7df263c5d..1cd71d3f5 100644 --- a/src/uu/chown/src/chown.rs +++ b/src/uu/chown/src/chown.rs @@ -5,130 +5,33 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (ToDO) COMFOLLOW Chowner Passwd RFILE RFILE's derefer dgid duid +// spell-checker:ignore (ToDO) COMFOLLOW Passwd RFILE RFILE's derefer dgid duid groupname #[macro_use] extern crate uucore; +use uucore::display::Quotable; pub use uucore::entries::{self, Group, Locate, Passwd}; -use uucore::fs::resolve_relative_path; -use uucore::libc::{gid_t, uid_t}; -use uucore::perms::{wrap_chown, Verbosity}; +use uucore::perms::{chown_base, options, IfFrom}; use uucore::error::{FromIo, UResult, USimpleError}; -use clap::{crate_version, App, Arg}; +use clap::{crate_version, App, Arg, ArgMatches}; -use walkdir::WalkDir; - -use std::fs::{self, Metadata}; +use std::fs; use std::os::unix::fs::MetadataExt; -use std::convert::AsRef; -use std::path::Path; -use uucore::InvalidEncodingHandling; - static ABOUT: &str = "change file owner and group"; -pub mod options { - pub mod verbosity { - pub static CHANGES: &str = "changes"; - pub static QUIET: &str = "quiet"; - pub static SILENT: &str = "silent"; - pub static VERBOSE: &str = "verbose"; - } - pub mod preserve_root { - pub static PRESERVE: &str = "preserve-root"; - pub static NO_PRESERVE: &str = "no-preserve-root"; - } - pub mod dereference { - pub static DEREFERENCE: &str = "dereference"; - pub static NO_DEREFERENCE: &str = "no-dereference"; - } - pub static FROM: &str = "from"; - pub static RECURSIVE: &str = "recursive"; - pub mod traverse { - pub static TRAVERSE: &str = "H"; - pub static NO_TRAVERSE: &str = "P"; - pub static EVERY: &str = "L"; - } - pub static REFERENCE: &str = "reference"; -} - -static ARG_OWNER: &str = "owner"; -static ARG_FILES: &str = "files"; - -const FTS_COMFOLLOW: u8 = 1; -const FTS_PHYSICAL: u8 = 1 << 1; -const FTS_LOGICAL: u8 = 1 << 2; - fn get_usage() -> String { format!( "{0} [OPTION]... [OWNER][:[GROUP]] FILE...\n{0} [OPTION]... --reference=RFILE FILE...", - executable!() + uucore::execution_phrase() ) } -#[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 matches = uu_app().usage(&usage[..]).get_matches_from(args); - - /* First arg is the owner/group */ - let owner = matches.value_of(ARG_OWNER).unwrap(); - - /* Then the list of files */ - let files: Vec = matches - .values_of(ARG_FILES) - .map(|v| v.map(ToString::to_string).collect()) - .unwrap_or_default(); - - let preserve_root = matches.is_present(options::preserve_root::PRESERVE); - - let mut derefer = if matches.is_present(options::dereference::NO_DEREFERENCE) { - 1 - } else { - 0 - }; - - let mut bit_flag = if matches.is_present(options::traverse::TRAVERSE) { - FTS_COMFOLLOW | FTS_PHYSICAL - } else if matches.is_present(options::traverse::EVERY) { - FTS_LOGICAL - } else { - FTS_PHYSICAL - }; - - let recursive = matches.is_present(options::RECURSIVE); - if recursive { - if bit_flag == FTS_PHYSICAL { - if derefer == 1 { - return Err(USimpleError::new(1, "-R --dereference requires -H or -L")); - } - derefer = 0; - } - } else { - bit_flag = FTS_PHYSICAL; - } - - let verbosity = if matches.is_present(options::verbosity::CHANGES) { - Verbosity::Changes - } else if matches.is_present(options::verbosity::SILENT) - || matches.is_present(options::verbosity::QUIET) - { - Verbosity::Silent - } else if matches.is_present(options::verbosity::VERBOSE) { - Verbosity::Verbose - } else { - Verbosity::Normal - }; - +fn parse_gid_uid_and_filter(matches: &ArgMatches) -> UResult<(Option, Option, IfFrom)> { let filter = if let Some(spec) = matches.value_of(options::FROM) { - match parse_spec(spec)? { + match parse_spec(spec, ':')? { (Some(uid), None) => IfFrom::User(uid), (None, Some(gid)) => IfFrom::Group(gid), (Some(uid), Some(gid)) => IfFrom::UserGroup(uid, gid), @@ -142,30 +45,32 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let dest_gid: Option; if let Some(file) = matches.value_of(options::REFERENCE) { let meta = fs::metadata(&file) - .map_err_context(|| format!("failed to get attributes of '{}'", file))?; + .map_err_context(|| format!("failed to get attributes of {}", file.quote()))?; dest_gid = Some(meta.gid()); dest_uid = Some(meta.uid()); } else { - let (u, g) = parse_spec(owner)?; + let (u, g) = parse_spec(matches.value_of(options::ARG_OWNER).unwrap(), ':')?; dest_uid = u; dest_gid = g; } - let executor = Chowner { - bit_flag, - dest_uid, - dest_gid, - verbosity, - recursive, - dereference: derefer != 0, - filter, - preserve_root, - files, - }; - executor.exec() + Ok((dest_gid, dest_uid, filter)) +} + +#[uucore_procs::gen_uumain] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let usage = get_usage(); + + chown_base( + uu_app().usage(&usage[..]), + args, + options::ARG_OWNER, + parse_gid_uid_and_filter, + false, + ) } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .arg( @@ -174,22 +79,31 @@ pub fn uu_app() -> App<'static, 'static> { .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::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)", + "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", + "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"), ) @@ -221,7 +135,11 @@ pub fn uu_app() -> App<'static, 'static> { .value_name("RFILE") .min_values(1), ) - .arg(Arg::with_name(options::verbosity::SILENT).short("f").long(options::verbosity::SILENT)) + .arg( + Arg::with_name(options::verbosity::SILENT) + .short("f") + .long(options::verbosity::SILENT), + ) .arg( Arg::with_name(options::traverse::TRAVERSE) .short(options::traverse::TRAVERSE) @@ -243,41 +161,55 @@ pub fn uu_app() -> App<'static, 'static> { .arg( Arg::with_name(options::verbosity::VERBOSE) .long(options::verbosity::VERBOSE) + .short("v") .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) -> UResult<(Option, Option)> { - let args = spec.split_terminator(':').collect::>(); - let usr_only = args.len() == 1 && !args[0].is_empty(); - let grp_only = args.len() == 2 && args[0].is_empty(); - let usr_grp = args.len() == 2 && !args[0].is_empty() && !args[1].is_empty(); - let uid = if usr_only || usr_grp { - Some( - Passwd::locate(args[0]) - .map_err(|_| USimpleError::new(1, format!("invalid user: '{}'", spec)))? - .uid(), - ) +/// Parse the username and groupname +/// +/// In theory, it should be username:groupname +/// but ... +/// it can user.name:groupname +/// or username.groupname +/// +/// # Arguments +/// +/// * `spec` - The input from the user +/// * `sep` - Should be ':' or '.' +fn parse_spec(spec: &str, sep: char) -> UResult<(Option, Option)> { + assert!(['.', ':'].contains(&sep)); + let mut args = spec.splitn(2, sep); + let user = args.next().unwrap_or(""); + let group = args.next().unwrap_or(""); + + let uid = if !user.is_empty() { + Some(match Passwd::locate(user) { + Ok(u) => u.uid(), // We have been able to get the uid + Err(_) => + // we have NOT been able to find the uid + // but we could be in the case where we have user.group + { + if spec.contains('.') && !spec.contains(':') && sep == ':' { + // but the input contains a '.' but not a ':' + // we might have something like username.groupname + // So, try to parse it this way + return parse_spec(spec, '.'); + } else { + return Err(USimpleError::new( + 1, + format!("invalid user: {}", spec.quote()), + )); + } + } + }) } else { None }; - let gid = if grp_only || usr_grp { + let gid = if !group.is_empty() { Some( - Group::locate(args[1]) - .map_err(|_| USimpleError::new(1, format!("invalid group: '{}'", spec)))? + Group::locate(group) + .map_err(|_| USimpleError::new(1, format!("invalid group: {}", spec.quote())))? .gid(), ) } else { @@ -286,203 +218,16 @@ fn parse_spec(spec: &str) -> UResult<(Option, Option)> { Ok((uid, gid)) } -enum IfFrom { - All, - User(u32), - Group(u32), - UserGroup(u32, u32), -} - -struct Chowner { - dest_uid: Option, - dest_gid: Option, - bit_flag: u8, - verbosity: Verbosity, - filter: IfFrom, - files: Vec, - recursive: bool, - preserve_root: bool, - dereference: bool, -} - -macro_rules! unwrap { - ($m:expr, $e:ident, $err:block) => { - match $m { - Ok(meta) => meta, - Err($e) => $err, - } - }; -} - -impl Chowner { - fn exec(&self) -> UResult<()> { - let mut ret = 0; - for f in &self.files { - ret |= self.traverse(f); - } - if ret != 0 { - return Err(ret.into()); - } - Ok(()) - } - - fn traverse>(&self, root: P) -> i32 { - let follow_arg = self.dereference || self.bit_flag != FTS_PHYSICAL; - let path = root.as_ref(); - let meta = match self.obtain_meta(path, follow_arg) { - Some(m) => m, - _ => return 1, - }; - - // Prohibit only if: - // (--preserve-root and -R present) && - // ( - // (argument is not symlink && resolved to be '/') || - // (argument is symlink && should follow argument && resolved to be '/') - // ) - if self.recursive && self.preserve_root { - let may_exist = if follow_arg { - path.canonicalize().ok() - } else { - let real = resolve_relative_path(path); - if real.is_dir() { - Some(real.canonicalize().expect("failed to get real path")) - } else { - Some(real.into_owned()) - } - }; - - if let Some(p) = may_exist { - if p.parent().is_none() { - show_error!("it is dangerous to operate recursively on '/'"); - show_error!("use --no-preserve-root to override this failsafe"); - return 1; - } - } - } - - let ret = if self.matched(meta.uid(), meta.gid()) { - match wrap_chown( - path, - &meta, - self.dest_uid, - self.dest_gid, - follow_arg, - self.verbosity.clone(), - ) { - Ok(n) => { - if !n.is_empty() { - show_error!("{}", n); - } - 0 - } - Err(e) => { - if self.verbosity != Verbosity::Silent { - show_error!("{}", e); - } - 1 - } - } - } else { - 0 - }; - - if !self.recursive { - ret - } else { - ret | self.dive_into(&root) - } - } - - fn dive_into>(&self, root: P) -> i32 { - let mut ret = 0; - let root = root.as_ref(); - let follow = self.dereference || self.bit_flag & FTS_LOGICAL != 0; - for entry in WalkDir::new(root).follow_links(follow).min_depth(1) { - let entry = unwrap!(entry, e, { - ret = 1; - show_error!("{}", e); - continue; - }); - let path = entry.path(); - let meta = match self.obtain_meta(path, follow) { - Some(m) => m, - _ => { - ret = 1; - continue; - } - }; - - if !self.matched(meta.uid(), meta.gid()) { - continue; - } - - ret = match wrap_chown( - path, - &meta, - self.dest_uid, - self.dest_gid, - follow, - self.verbosity.clone(), - ) { - Ok(n) => { - if !n.is_empty() { - show_error!("{}", n); - } - 0 - } - Err(e) => { - if self.verbosity != Verbosity::Silent { - show_error!("{}", e); - } - 1 - } - } - } - ret - } - - fn obtain_meta>(&self, path: P, follow: bool) -> Option { - use self::Verbosity::*; - let path = path.as_ref(); - let meta = if follow { - unwrap!(path.metadata(), e, { - match self.verbosity { - Silent => (), - _ => show_error!("cannot access '{}': {}", path.display(), e), - } - return None; - }) - } else { - unwrap!(path.symlink_metadata(), e, { - match self.verbosity { - Silent => (), - _ => show_error!("cannot dereference '{}': {}", path.display(), e), - } - return None; - }) - }; - Some(meta) - } - - #[inline] - fn matched(&self, uid: uid_t, gid: gid_t) -> bool { - match self.filter { - IfFrom::All => true, - IfFrom::User(u) => u == uid, - IfFrom::Group(g) => g == gid, - IfFrom::UserGroup(u, g) => u == uid && g == gid, - } - } -} - #[cfg(test)] mod test { use super::*; #[test] fn test_parse_spec() { - assert!(matches!(parse_spec(":"), Ok((None, None)))); - assert!(format!("{}", parse_spec("::").err().unwrap()).starts_with("invalid group: ")); + assert!(matches!(parse_spec(":", ':'), Ok((None, None)))); + assert!(matches!(parse_spec(".", ':'), Ok((None, None)))); + assert!(matches!(parse_spec(".", '.'), Ok((None, None)))); + assert!(format!("{}", parse_spec("::", ':').err().unwrap()).starts_with("invalid group: ")); + assert!(format!("{}", parse_spec("..", ':').err().unwrap()).starts_with("invalid group: ")); } } diff --git a/src/uu/chroot/src/chroot.rs b/src/uu/chroot/src/chroot.rs index 2c0f8522c..40799d009 100644 --- a/src/uu/chroot/src/chroot.rs +++ b/src/uu/chroot/src/chroot.rs @@ -15,10 +15,10 @@ use std::ffi::CString; use std::io::Error; use std::path::Path; use std::process::Command; +use uucore::display::Quotable; use uucore::libc::{self, chroot, setgid, setgroups, setuid}; use uucore::{entries, InvalidEncodingHandling}; -static NAME: &str = "chroot"; static ABOUT: &str = "Run COMMAND with root directory set to NEWROOT."; static SYNTAX: &str = "[OPTION]... NEWROOT [COMMAND [ARG]...]"; @@ -47,15 +47,15 @@ pub fn uumain(args: impl uucore::Args) -> i32 { None => crash!( 1, "Missing operand: NEWROOT\nTry '{} --help' for more information.", - NAME + uucore::execution_phrase() ), }; if !newroot.is_dir() { crash!( 1, - "cannot change root directory to `{}`: no such directory", - newroot.display() + "cannot change root directory to {}: no such directory", + newroot.quote() ); } @@ -92,7 +92,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .usage(SYNTAX) @@ -150,7 +150,7 @@ fn set_context(root: &Path, options: &clap::ArgMatches) { Some(u) => { let s: Vec<&str> = u.split(':').collect(); if s.len() != 2 || s.iter().any(|&spec| spec.is_empty()) { - crash!(1, "invalid userspec: `{}`", u) + crash!(1, "invalid userspec: {}", u.quote()) }; s } @@ -171,7 +171,6 @@ fn set_context(root: &Path, options: &clap::ArgMatches) { } fn enter_chroot(root: &Path) { - let root_str = root.display(); std::env::set_current_dir(root).unwrap(); let err = unsafe { chroot(CString::new(".").unwrap().as_bytes_with_nul().as_ptr() as *const libc::c_char) @@ -180,7 +179,7 @@ fn enter_chroot(root: &Path) { crash!( 1, "cannot chroot to {}: {}", - root_str, + root.quote(), Error::last_os_error() ) }; @@ -190,7 +189,7 @@ fn set_main_group(group: &str) { if !group.is_empty() { let group_id = match entries::grp2gid(group) { Ok(g) => g, - _ => crash!(1, "no such group: {}", group), + _ => crash!(1, "no such group: {}", group.maybe_quote()), }; let err = unsafe { setgid(group_id) }; if err != 0 { @@ -235,7 +234,12 @@ fn set_user(user: &str) { let user_id = entries::usr2uid(user).unwrap(); let err = unsafe { setuid(user_id as libc::uid_t) }; if err != 0 { - crash!(1, "cannot set user to {}: {}", user, Error::last_os_error()) + crash!( + 1, + "cannot set user to {}: {}", + user.maybe_quote(), + Error::last_os_error() + ) } } } diff --git a/src/uu/cksum/Cargo.toml b/src/uu/cksum/Cargo.toml index b92b680c8..287a2285f 100644 --- a/src/uu/cksum/Cargo.toml +++ b/src/uu/cksum/Cargo.toml @@ -23,3 +23,7 @@ uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_p [[bin]] name = "cksum" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index e88cc78b3..e682aa70c 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -14,6 +14,7 @@ use clap::{crate_version, App, Arg}; use std::fs::File; use std::io::{self, stdin, BufReader, Read}; use std::path::Path; +use uucore::display::Quotable; use uucore::InvalidEncodingHandling; // NOTE: CRC_TABLE_LEN *must* be <= 256 as we cast 0..CRC_TABLE_LEN to u8 @@ -191,7 +192,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { match cksum("-") { Ok((crc, size)) => println!("{} {}", crc, size), Err(err) => { - show_error!("{}", err); + show_error!("-: {}", err); return 2; } } @@ -203,7 +204,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { match cksum(fname.as_ref()) { Ok((crc, size)) => println!("{} {} {}", crc, size, fname), Err(err) => { - show_error!("'{}' {}", fname, err); + show_error!("{}: {}", fname.maybe_quote(), err); exit_code = 2; } } @@ -213,7 +214,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .name(NAME) .version(crate_version!()) .about(SUMMARY) diff --git a/src/uu/comm/Cargo.toml b/src/uu/comm/Cargo.toml index 1deb094e2..e44c3511c 100644 --- a/src/uu/comm/Cargo.toml +++ b/src/uu/comm/Cargo.toml @@ -23,3 +23,7 @@ uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_p [[bin]] name = "comm" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/comm/src/comm.rs b/src/uu/comm/src/comm.rs index aa10432a2..56af42fd9 100644 --- a/src/uu/comm/src/comm.rs +++ b/src/uu/comm/src/comm.rs @@ -7,9 +7,6 @@ // spell-checker:ignore (ToDO) delim mkdelim -#[macro_use] -extern crate uucore; - use std::cmp::Ordering; use std::fs::File; use std::io::{self, stdin, BufRead, BufReader, Stdin}; @@ -31,8 +28,8 @@ mod options { pub const FILE_2: &str = "FILE2"; } -fn get_usage() -> String { - format!("{} [OPTION]... FILE1 FILE2", executable!()) +fn usage() -> String { + format!("{} [OPTION]... FILE1 FILE2", uucore::execution_phrase()) } fn mkdelim(col: usize, opts: &ArgMatches) -> String { @@ -132,7 +129,7 @@ fn open_file(name: &str) -> io::Result { } pub fn uumain(args: impl uucore::Args) -> i32 { - let usage = get_usage(); + let usage = usage(); let args = args .collect_str(InvalidEncodingHandling::ConvertLossy) .accept_any(); @@ -148,7 +145,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .after_help(LONG_HELP) diff --git a/src/uu/cp/Cargo.toml b/src/uu/cp/Cargo.toml index b7b3809a7..62aef932b 100644 --- a/src/uu/cp/Cargo.toml +++ b/src/uu/cp/Cargo.toml @@ -23,19 +23,29 @@ clap = { version = "2.33", features = ["wrap_help"] } filetime = "0.2" libc = "0.2.85" quick-error = "1.2.3" -uucore = { version=">=0.0.9", package="uucore", path="../../uucore", features=["fs"] } +selinux = { version="0.2.3", optional=true } +uucore = { version=">=0.0.9", package="uucore", path="../../uucore", features=["fs", "perms", "mode"] } uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_procs" } walkdir = "2.2" [target.'cfg(target_os = "linux")'.dependencies] -ioctl-sys = "0.5.2" +ioctl-sys = "0.6" [target.'cfg(target_os = "windows")'.dependencies] winapi = { version="0.3", features=["fileapi"] } [target.'cfg(unix)'.dependencies] xattr="0.2.1" +exacl= { version = "0.6.0", optional=true } [[bin]] name = "cp" path = "src/main.rs" + +[features] +feat_selinux = ["selinux"] +feat_acl = ["exacl"] + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 7c67649c2..cd33f9fa6 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -18,6 +18,7 @@ extern crate quick_error; #[macro_use] extern crate uucore; +use uucore::display::Quotable; #[cfg(windows)] use winapi::um::fileapi::CreateFileW; #[cfg(windows)] @@ -48,7 +49,7 @@ use std::path::{Path, PathBuf, StripPrefixError}; use std::str::FromStr; use std::string::ToString; use uucore::backup_control::{self, BackupMode}; -use uucore::fs::{canonicalize, CanonicalizeMode}; +use uucore::fs::{canonicalize, MissingHandling, ResolveMode}; use walkdir::WalkDir; #[cfg(unix)] @@ -67,6 +68,7 @@ quick_error! { IoErrContext(err: io::Error, path: String) { display("{}: {}", path, err) context(path: &'a str, err: io::Error) -> (err, path.to_owned()) + context(context: String, err: io::Error) -> (err, context) cause(err) } @@ -99,7 +101,7 @@ quick_error! { NotImplemented(opt: String) { display("Option '{}' not yet implemented.", opt) } /// Invalid arguments to backup - Backup(description: String) { display("{}\nTry 'cp --help' for more information.", description) } + Backup(description: String) { display("{}\nTry '{} --help' for more information.", description, uucore::execution_phrase()) } } } @@ -180,12 +182,15 @@ pub enum CopyMode { AttrOnly, } -#[derive(Clone, Eq, PartialEq)] +// The ordering here determines the order in which attributes are (re-)applied. +// In particular, Ownership must be changed first to avoid interfering with mode change. +#[derive(Clone, Eq, PartialEq, Debug, PartialOrd, Ord)] pub enum Attribute { #[cfg(unix)] - Mode, Ownership, + Mode, Timestamps, + #[cfg(feature = "feat_selinux")] Context, Links, Xattr, @@ -218,12 +223,12 @@ static LONG_HELP: &str = ""; static EXIT_OK: i32 = 0; static EXIT_ERR: i32 = 1; -fn get_usage() -> String { +fn usage() -> String { format!( "{0} [OPTION]... [-T] SOURCE DEST {0} [OPTION]... SOURCE... DIRECTORY {0} [OPTION]... -t DIRECTORY SOURCE...", - executable!() + uucore::execution_phrase() ) } @@ -231,8 +236,6 @@ fn get_usage() -> String { mod options { pub const ARCHIVE: &str = "archive"; pub const ATTRIBUTES_ONLY: &str = "attributes-only"; - pub const BACKUP: &str = "backup"; - pub const BACKUP_NO_ARG: &str = "b"; pub const CLI_SYMBOLIC_LINKS: &str = "cli-symbolic-links"; pub const CONTEXT: &str = "context"; pub const COPY_CONTENTS: &str = "copy-contents"; @@ -242,7 +245,7 @@ mod options { pub const LINK: &str = "link"; pub const NO_CLOBBER: &str = "no-clobber"; pub const NO_DEREFERENCE: &str = "no-dereference"; - pub const NO_DEREFERENCE_PRESERVE_LINKS: &str = "no-dereference-preserve-linkgs"; + pub const NO_DEREFERENCE_PRESERVE_LINKS: &str = "no-dereference-preserve-links"; pub const NO_PRESERVE: &str = "no-preserve"; pub const NO_TARGET_DIRECTORY: &str = "no-target-directory"; pub const ONE_FILE_SYSTEM: &str = "one-file-system"; @@ -257,7 +260,6 @@ mod options { pub const REMOVE_DESTINATION: &str = "remove-destination"; pub const SPARSE: &str = "sparse"; pub const STRIP_TRAILING_SLASHES: &str = "strip-trailing-slashes"; - pub const SUFFIX: &str = "suffix"; pub const SYMBOLIC_LINK: &str = "symbolic-link"; pub const TARGET_DIRECTORY: &str = "target-directory"; pub const UPDATE: &str = "update"; @@ -269,6 +271,7 @@ static PRESERVABLE_ATTRIBUTES: &[&str] = &[ "mode", "ownership", "timestamps", + #[cfg(feature = "feat_selinux")] "context", "links", "xattr", @@ -276,24 +279,18 @@ static PRESERVABLE_ATTRIBUTES: &[&str] = &[ ]; #[cfg(not(unix))] -static PRESERVABLE_ATTRIBUTES: &[&str] = &[ - "ownership", - "timestamps", - "context", - "links", - "xattr", - "all", -]; +static PRESERVABLE_ATTRIBUTES: &[&str] = + &["mode", "timestamps", "context", "links", "xattr", "all"]; static DEFAULT_ATTRIBUTES: &[Attribute] = &[ - #[cfg(unix)] Attribute::Mode, + #[cfg(unix)] Attribute::Ownership, Attribute::Timestamps, ]; pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .arg(Arg::with_name(options::TARGET_DIRECTORY) @@ -355,24 +352,9 @@ pub fn uu_app() -> App<'static, 'static> { .conflicts_with(options::FORCE) .help("remove each existing destination file before attempting to open it \ (contrast with --force). On Windows, current only works for writeable files.")) - .arg(Arg::with_name(options::BACKUP) - .long(options::BACKUP) - .help("make a backup of each existing destination file") - .takes_value(true) - .require_equals(true) - .min_values(0) - .value_name("CONTROL") - ) - .arg(Arg::with_name(options::BACKUP_NO_ARG) - .short(options::BACKUP_NO_ARG) - .help("like --backup but does not accept an argument") - ) - .arg(Arg::with_name(options::SUFFIX) - .short("S") - .long(options::SUFFIX) - .takes_value(true) - .value_name("SUFFIX") - .help("override the usual backup suffix")) + .arg(backup_control::arguments::backup()) + .arg(backup_control::arguments::backup_no_args()) + .arg(backup_control::arguments::suffix()) .arg(Arg::with_name(options::UPDATE) .short("u") .long(options::UPDATE) @@ -399,13 +381,13 @@ pub fn uu_app() -> App<'static, 'static> { .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, ownership (unix only), timestamps), \ if possible additional attributes: context, links, xattr, all")) .arg(Arg::with_name(options::PRESERVE_DEFAULT_ATTRIBUTES) .short("-p") .long(options::PRESERVE_DEFAULT_ATTRIBUTES) .conflicts_with_all(&[options::PRESERVE, options::NO_PRESERVE, options::ARCHIVE]) - .help("same as --preserve=mode(unix only),ownership,timestamps")) + .help("same as --preserve=mode,ownership(unix only),timestamps")) .arg(Arg::with_name(options::NO_PRESERVE) .long(options::NO_PRESERVE) .takes_value(true) @@ -465,7 +447,7 @@ pub fn uu_app() -> App<'static, 'static> { } pub fn uumain(args: impl uucore::Args) -> i32 { - let usage = get_usage(); + let usage = usage(); let matches = uu_app() .after_help(&*format!( "{}\n{}", @@ -550,17 +532,18 @@ impl FromStr for Attribute { fn from_str(value: &str) -> CopyResult { Ok(match &*value.to_lowercase() { - #[cfg(unix)] "mode" => Attribute::Mode, + #[cfg(unix)] "ownership" => Attribute::Ownership, "timestamps" => Attribute::Timestamps, + #[cfg(feature = "feat_selinux")] "context" => Attribute::Context, "links" => Attribute::Links, "xattr" => Attribute::Xattr, _ => { return Err(Error::InvalidArgument(format!( - "invalid attribute '{}'", - value + "invalid attribute {}", + value.quote() ))); } }) @@ -570,14 +553,16 @@ impl FromStr for Attribute { fn add_all_attributes() -> Vec { use Attribute::*; - #[cfg(target_os = "windows")] - let attr = vec![Ownership, Timestamps, Context, Xattr, Links]; - - #[cfg(not(target_os = "windows"))] - let mut attr = vec![Ownership, Timestamps, Context, Xattr, Links]; - - #[cfg(unix)] - attr.insert(0, Mode); + let attr = vec![ + #[cfg(unix)] + Ownership, + Mode, + Timestamps, + #[cfg(feature = "feat_selinux")] + Context, + Links, + Xattr, + ]; attr } @@ -604,20 +589,12 @@ impl Options { || matches.is_present(options::RECURSIVE_ALIAS) || matches.is_present(options::ARCHIVE); - let backup_mode = backup_control::determine_backup_mode( - matches.is_present(options::BACKUP_NO_ARG), - matches.is_present(options::BACKUP), - matches.value_of(options::BACKUP), - ); - let backup_mode = match backup_mode { - Err(err) => { - return Err(Error::Backup(err)); - } + let backup_mode = match backup_control::determine_backup_mode(matches) { + Err(e) => return Err(Error::Backup(format!("{}", e))), Ok(mode) => mode, }; - let backup_suffix = - backup_control::determine_backup_suffix(matches.value_of(options::SUFFIX)); + let backup_suffix = backup_control::determine_backup_suffix(matches); let overwrite = OverwriteMode::from_matches(matches); @@ -628,7 +605,7 @@ impl Options { .map(ToString::to_string); // Parse attributes to preserve - let preserve_attributes: Vec = if matches.is_present(options::PRESERVE) { + let mut preserve_attributes: Vec = if matches.is_present(options::PRESERVE) { match matches.values_of(options::PRESERVE) { None => DEFAULT_ATTRIBUTES.to_vec(), Some(attribute_strs) => { @@ -655,6 +632,11 @@ impl Options { vec![] }; + // Make sure ownership is changed before other attributes, + // as chown clears some of the permission and therefore could undo previous changes + // if not executed first. + preserve_attributes.sort_unstable(); + let options = Options { attributes_only: matches.is_present(options::ATTRIBUTES_ONLY), copy_contents: matches.is_present(options::COPY_CONTENTS), @@ -678,8 +660,8 @@ impl Options { "never" => ReflinkMode::Never, value => { return Err(Error::InvalidArgument(format!( - "invalid argument '{}' for \'reflink\'", - value + "invalid argument {} for \'reflink\'", + value.quote() ))); } } @@ -851,7 +833,7 @@ fn copy(sources: &[Source], target: &TargetSlice, options: &Options) -> CopyResu let mut seen_sources = HashSet::with_capacity(sources.len()); for source in sources { if seen_sources.contains(source) { - show_warning!("source '{}' specified more than once", source.display()); + show_warning!("source {} specified more than once", source.quote()); } else { let mut found_hard_link = false; if preserve_hard_links { @@ -892,8 +874,8 @@ fn construct_dest_path( ) -> CopyResult { if options.no_target_dir && target.is_dir() { return Err(format!( - "cannot overwrite directory '{}' with non-directory", - target.display() + "cannot overwrite directory {} with non-directory", + target.quote() ) .into()); } @@ -960,7 +942,7 @@ fn adjust_canonicalization(p: &Path) -> Cow { /// will not cause a short-circuit. fn copy_directory(root: &Path, target: &TargetSlice, options: &Options) -> CopyResult<()> { if !options.recursive { - return Err(format!("omitting directory '{}'", root.display()).into()); + return Err(format!("omitting directory {}", root.quote()).into()); } // if no-dereference is enabled and this is a symlink, copy it as a file @@ -1060,12 +1042,12 @@ impl OverwriteMode { match *self { OverwriteMode::NoClobber => Err(Error::NotAllFilesCopied), OverwriteMode::Interactive(_) => { - if prompt_yes!("{}: overwrite {}? ", executable!(), path.display()) { + if prompt_yes!("{}: overwrite {}? ", uucore::util_name(), path.quote()) { Ok(()) } else { Err(Error::Skipped(format!( "Not overwriting {} at user request", - path.display() + path.quote() ))) } } @@ -1075,27 +1057,66 @@ impl OverwriteMode { } fn copy_attribute(source: &Path, dest: &Path, attribute: &Attribute) -> CopyResult<()> { - let context = &*format!("'{}' -> '{}'", source.display().to_string(), dest.display()); + let context = &*format!("{} -> {}", source.quote(), dest.quote()); + let source_metadata = fs::symlink_metadata(source).context(context)?; match *attribute { - #[cfg(unix)] Attribute::Mode => { - let mode = fs::metadata(source).context(context)?.permissions().mode(); - let mut dest_metadata = fs::metadata(source).context(context)?.permissions(); - dest_metadata.set_mode(mode); + fs::set_permissions(dest, source_metadata.permissions()).context(context)?; + // FIXME: Implement this for windows as well + #[cfg(feature = "feat_acl")] + exacl::getfacl(source, None) + .and_then(|acl| exacl::setfacl(&[dest], &acl, None)) + .map_err(|err| Error::Error(err.to_string()))?; } + #[cfg(unix)] Attribute::Ownership => { - let metadata = fs::metadata(source).context(context)?; - fs::set_permissions(dest, metadata.permissions()).context(context)?; + use std::os::unix::prelude::MetadataExt; + use uucore::perms::wrap_chown; + use uucore::perms::Verbosity; + use uucore::perms::VerbosityLevel; + + let dest_uid = source_metadata.uid(); + let dest_gid = source_metadata.gid(); + + wrap_chown( + dest, + &dest.symlink_metadata().context(context)?, + Some(dest_uid), + Some(dest_gid), + false, + Verbosity { + groups_only: false, + level: VerbosityLevel::Normal, + }, + ) + .map_err(Error::Error)?; } Attribute::Timestamps => { - let metadata = fs::metadata(source)?; filetime::set_file_times( Path::new(dest), - FileTime::from_last_access_time(&metadata), - FileTime::from_last_modification_time(&metadata), + FileTime::from_last_access_time(&source_metadata), + FileTime::from_last_modification_time(&source_metadata), )?; } - Attribute::Context => {} + #[cfg(feature = "feat_selinux")] + Attribute::Context => { + let context = selinux::SecurityContext::of_path(source, false, false).map_err(|e| { + format!( + "failed to get security context of {}: {}", + source.display(), + e + ) + })?; + if let Some(context) = context { + context.set_for_path(dest, false, false).map_err(|e| { + format!( + "failed to set security context for {}: {}", + dest.display(), + e + ) + })?; + } + } Attribute::Links => {} Attribute::Xattr => { #[cfg(unix)] @@ -1132,7 +1153,7 @@ fn symlink_file(source: &Path, dest: &Path, context: &str) -> CopyResult<()> { } fn context_for(src: &Path, dest: &Path) -> String { - format!("'{}' -> '{}'", src.display(), dest.display()) + format!("{} -> {}", src.quote(), dest.quote()) } /// Implements a simple backup copy for the destination file. @@ -1169,8 +1190,8 @@ fn handle_existing_dest(source: &Path, dest: &Path, options: &Options) -> CopyRe Ok(()) } -/// Copy the a file from `source` to `dest`. No path manipulation is -/// done on either `source` or `dest`, the are used as provided. +/// Copy the a file from `source` to `dest`. `source` will be dereferenced if +/// `options.dereference` is set to true. `dest` will always be dereferenced. /// /// Behavior when copying to existing files is contingent on the /// `options.overwrite` mode. If a file is skipped, the return type @@ -1187,41 +1208,66 @@ fn copy_file(source: &Path, dest: &Path, options: &Options) -> CopyResult<()> { println!("{}", context_for(source, dest)); } - #[allow(unused)] - { - // TODO: implement --preserve flag - let mut preserve_context = false; - for attribute in &options.preserve_attributes { - if *attribute == Attribute::Context { - preserve_context = true; - } + // Calculate the context upfront before canonicalizing the path + let context = context_for(source, dest); + let context = context.as_str(); + + // canonicalize dest and source so that later steps can work with the paths directly + let dest = canonicalize(dest, MissingHandling::Missing, ResolveMode::Physical).unwrap(); + let source = if options.dereference { + canonicalize(source, MissingHandling::Missing, ResolveMode::Physical).unwrap() + } else { + source.to_owned() + }; + + let dest_permissions = if dest.exists() { + dest.symlink_metadata().context(context)?.permissions() + } else { + #[allow(unused_mut)] + let mut permissions = source.symlink_metadata().context(context)?.permissions(); + #[cfg(unix)] + { + use uucore::mode::get_umask; + + let mut mode = permissions.mode(); + + // remove sticky bit, suid and gid bit + const SPECIAL_PERMS_MASK: u32 = 0o7000; + mode &= !SPECIAL_PERMS_MASK; + + // apply umask + mode &= !get_umask(); + + permissions.set_mode(mode); } - } + permissions + }; + match options.copy_mode { CopyMode::Link => { - fs::hard_link(source, dest).context(&*context_for(source, dest))?; + fs::hard_link(&source, &dest).context(context)?; } CopyMode::Copy => { - copy_helper(source, dest, options)?; + copy_helper(&source, &dest, options, context)?; } CopyMode::SymLink => { - symlink_file(source, dest, &*context_for(source, dest))?; + symlink_file(&source, &dest, context)?; } CopyMode::Sparse => return Err(Error::NotImplemented(options::SPARSE.to_string())), CopyMode::Update => { if dest.exists() { - let src_metadata = fs::metadata(source)?; - let dest_metadata = fs::metadata(dest)?; + let src_metadata = fs::symlink_metadata(&source)?; + let dest_metadata = fs::symlink_metadata(&dest)?; let src_time = src_metadata.modified()?; let dest_time = dest_metadata.modified()?; if src_time <= dest_time { return Ok(()); } else { - copy_helper(source, dest, options)?; + copy_helper(&source, &dest, options, context)?; } } else { - copy_helper(source, dest, options)?; + copy_helper(&source, &dest, options, context)?; } } CopyMode::AttrOnly => { @@ -1229,53 +1275,51 @@ fn copy_file(source: &Path, dest: &Path, options: &Options) -> CopyResult<()> { .write(true) .truncate(false) .create(true) - .open(dest) + .open(&dest) .unwrap(); } }; + + // TODO: implement something similar to gnu's lchown + if fs::symlink_metadata(&dest) + .map(|meta| !meta.file_type().is_symlink()) + .unwrap_or(false) + { + fs::set_permissions(&dest, dest_permissions).unwrap(); + } for attribute in &options.preserve_attributes { - copy_attribute(source, dest, attribute)?; + copy_attribute(&source, &dest, attribute)?; } Ok(()) } /// 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<()> { +fn copy_helper(source: &Path, dest: &Path, options: &Options, context: &str) -> CopyResult<()> { 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" { + if source.as_os_str() == "/dev/null" { /* workaround a limitation of fs::copy * https://github.com/rust-lang/rust/issues/79390 */ File::create(dest)?; - } else if !options.dereference && is_symlink { + } else if 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)?; - } + #[cfg(target_os = "macos")] + copy_on_write_macos(source, dest, options.reflink_mode, context)?; + #[cfg(target_os = "linux")] + copy_on_write_linux(source, dest, options.reflink_mode, context)?; } else { - fs::copy(source, dest).context(&*context_for(source, dest))?; + fs::copy(source, dest).context(context)?; } Ok(()) @@ -1289,8 +1333,8 @@ 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", - source.display() + "cannot stat {}: No such file or directory", + source.quote() ), } } else { @@ -1306,16 +1350,21 @@ fn copy_link(source: &Path, dest: &Path) -> CopyResult<()> { /// Copies `source` to `dest` using copy-on-write if possible. #[cfg(target_os = "linux")] -fn copy_on_write_linux(source: &Path, dest: &Path, mode: ReflinkMode) -> CopyResult<()> { +fn copy_on_write_linux( + source: &Path, + dest: &Path, + mode: ReflinkMode, + context: &str, +) -> CopyResult<()> { debug_assert!(mode != ReflinkMode::Never); - let src_file = File::open(source).context(&*context_for(source, dest))?; + let src_file = File::open(source).context(context)?; let dst_file = OpenOptions::new() .write(true) .truncate(false) .create(true) .open(dest) - .context(&*context_for(source, dest))?; + .context(context)?; match mode { ReflinkMode::Always => unsafe { let result = ficlone(dst_file.as_raw_fd(), src_file.as_raw_fd() as *const i32); @@ -1334,7 +1383,7 @@ fn copy_on_write_linux(source: &Path, dest: &Path, mode: ReflinkMode) -> CopyRes ReflinkMode::Auto => unsafe { let result = ficlone(dst_file.as_raw_fd(), src_file.as_raw_fd() as *const i32); if result != 0 { - fs::copy(source, dest).context(&*context_for(source, dest))?; + fs::copy(source, dest).context(context)?; } Ok(()) }, @@ -1344,7 +1393,12 @@ fn copy_on_write_linux(source: &Path, dest: &Path, mode: ReflinkMode) -> CopyRes /// Copies `source` to `dest` using copy-on-write if possible. #[cfg(target_os = "macos")] -fn copy_on_write_macos(source: &Path, dest: &Path, mode: ReflinkMode) -> CopyResult<()> { +fn copy_on_write_macos( + source: &Path, + dest: &Path, + mode: ReflinkMode, + context: &str, +) -> CopyResult<()> { debug_assert!(mode != ReflinkMode::Never); // Extract paths in a form suitable to be passed to a syscall. @@ -1389,7 +1443,7 @@ fn copy_on_write_macos(source: &Path, dest: &Path, mode: ReflinkMode) -> CopyRes format!("failed to clone {:?} from {:?}: {}", source, dest, error).into(), ) } - ReflinkMode::Auto => fs::copy(source, dest).context(&*context_for(source, dest))?, + ReflinkMode::Auto => fs::copy(source, dest).context(context)?, ReflinkMode::Never => unreachable!(), }; } @@ -1401,11 +1455,11 @@ fn copy_on_write_macos(source: &Path, dest: &Path, mode: ReflinkMode) -> CopyRes pub fn verify_target_type(target: &Path, target_type: &TargetType) -> CopyResult<()> { match (target_type, target.is_dir()) { (&TargetType::Directory, false) => { - Err(format!("target: '{}' is not a directory", target.display()).into()) + Err(format!("target: {} is not a directory", target.quote()).into()) } (&TargetType::File, true) => Err(format!( - "cannot overwrite directory '{}' with non-directory", - target.display() + "cannot overwrite directory {} with non-directory", + target.quote() ) .into()), _ => Ok(()), @@ -1430,8 +1484,8 @@ pub fn localize_to_target(root: &Path, source: &Path, target: &Path) -> CopyResu pub fn paths_refer_to_same_file(p1: &Path, p2: &Path) -> io::Result { // We have to take symlinks and relative paths into account. - let pathbuf1 = canonicalize(p1, CanonicalizeMode::Normal)?; - let pathbuf2 = canonicalize(p2, CanonicalizeMode::Normal)?; + let pathbuf1 = canonicalize(p1, MissingHandling::Normal, ResolveMode::Logical)?; + let pathbuf2 = canonicalize(p2, MissingHandling::Normal, ResolveMode::Logical)?; Ok(pathbuf1 == pathbuf2) } diff --git a/src/uu/csplit/Cargo.toml b/src/uu/csplit/Cargo.toml index 2ddc3b89c..40d4eebfa 100644 --- a/src/uu/csplit/Cargo.toml +++ b/src/uu/csplit/Cargo.toml @@ -18,10 +18,13 @@ path = "src/csplit.rs" clap = { version = "2.33", features = ["wrap_help"] } thiserror = "1.0" regex = "1.0.0" -glob = "0.2.11" uucore = { version=">=0.0.9", package="uucore", path="../../uucore", features=["entries", "fs"] } uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "csplit" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/csplit/src/csplit.rs b/src/uu/csplit/src/csplit.rs index 048ec80d8..dbf65b71d 100644 --- a/src/uu/csplit/src/csplit.rs +++ b/src/uu/csplit/src/csplit.rs @@ -10,6 +10,7 @@ use std::{ fs::{remove_file, File}, io::{BufRead, BufWriter, Write}, }; +use uucore::display::Quotable; mod csplit_error; mod patterns; @@ -34,8 +35,11 @@ mod options { pub const PATTERN: &str = "pattern"; } -fn get_usage() -> String { - format!("{0} [OPTION]... FILE PATTERN...", executable!()) +fn usage() -> String { + format!( + "{0} [OPTION]... FILE PATTERN...", + uucore::execution_phrase() + ) } /// Command line options for csplit. @@ -565,7 +569,7 @@ mod tests { assert_eq!(input_splitter.add_line_to_buffer(0, line), None); assert_eq!(input_splitter.buffer_len(), 1); } - item @ _ => panic!("wrong item: {:?}", item), + item => panic!("wrong item: {:?}", item), }; match input_splitter.next() { @@ -574,7 +578,7 @@ mod tests { assert_eq!(input_splitter.add_line_to_buffer(1, line), None); assert_eq!(input_splitter.buffer_len(), 2); } - item @ _ => panic!("wrong item: {:?}", item), + item => panic!("wrong item: {:?}", item), }; match input_splitter.next() { @@ -586,7 +590,7 @@ mod tests { ); assert_eq!(input_splitter.buffer_len(), 2); } - item @ _ => panic!("wrong item: {:?}", item), + item => panic!("wrong item: {:?}", item), }; input_splitter.rewind_buffer(); @@ -596,7 +600,7 @@ mod tests { assert_eq!(line, String::from("bbb")); assert_eq!(input_splitter.buffer_len(), 1); } - item @ _ => panic!("wrong item: {:?}", item), + item => panic!("wrong item: {:?}", item), }; match input_splitter.next() { @@ -604,7 +608,7 @@ mod tests { assert_eq!(line, String::from("ccc")); assert_eq!(input_splitter.buffer_len(), 0); } - item @ _ => panic!("wrong item: {:?}", item), + item => panic!("wrong item: {:?}", item), }; match input_splitter.next() { @@ -612,7 +616,7 @@ mod tests { assert_eq!(line, String::from("ddd")); assert_eq!(input_splitter.buffer_len(), 0); } - item @ _ => panic!("wrong item: {:?}", item), + item => panic!("wrong item: {:?}", item), }; assert!(input_splitter.next().is_none()); @@ -637,7 +641,7 @@ mod tests { assert_eq!(input_splitter.add_line_to_buffer(0, line), None); assert_eq!(input_splitter.buffer_len(), 1); } - item @ _ => panic!("wrong item: {:?}", item), + item => panic!("wrong item: {:?}", item), }; match input_splitter.next() { @@ -646,7 +650,7 @@ mod tests { assert_eq!(input_splitter.add_line_to_buffer(1, line), None); assert_eq!(input_splitter.buffer_len(), 2); } - item @ _ => panic!("wrong item: {:?}", item), + item => panic!("wrong item: {:?}", item), }; match input_splitter.next() { @@ -655,7 +659,7 @@ mod tests { assert_eq!(input_splitter.add_line_to_buffer(2, line), None); assert_eq!(input_splitter.buffer_len(), 3); } - item @ _ => panic!("wrong item: {:?}", item), + item => panic!("wrong item: {:?}", item), }; input_splitter.rewind_buffer(); @@ -666,7 +670,7 @@ mod tests { assert_eq!(input_splitter.add_line_to_buffer(0, line), None); assert_eq!(input_splitter.buffer_len(), 3); } - item @ _ => panic!("wrong item: {:?}", item), + item => panic!("wrong item: {:?}", item), }; match input_splitter.next() { @@ -674,7 +678,7 @@ mod tests { assert_eq!(line, String::from("aaa")); assert_eq!(input_splitter.buffer_len(), 2); } - item @ _ => panic!("wrong item: {:?}", item), + item => panic!("wrong item: {:?}", item), }; match input_splitter.next() { @@ -682,7 +686,7 @@ mod tests { assert_eq!(line, String::from("bbb")); assert_eq!(input_splitter.buffer_len(), 1); } - item @ _ => panic!("wrong item: {:?}", item), + item => panic!("wrong item: {:?}", item), }; match input_splitter.next() { @@ -690,7 +694,7 @@ mod tests { assert_eq!(line, String::from("ccc")); assert_eq!(input_splitter.buffer_len(), 0); } - item @ _ => panic!("wrong item: {:?}", item), + item => panic!("wrong item: {:?}", item), }; match input_splitter.next() { @@ -698,7 +702,7 @@ mod tests { assert_eq!(line, String::from("ddd")); assert_eq!(input_splitter.buffer_len(), 0); } - item @ _ => panic!("wrong item: {:?}", item), + item => panic!("wrong item: {:?}", item), }; assert!(input_splitter.next().is_none()); @@ -706,7 +710,7 @@ mod tests { } pub fn uumain(args: impl uucore::Args) -> i32 { - let usage = get_usage(); + let usage = usage(); let args = args .collect_str(InvalidEncodingHandling::Ignore) .accept_any(); @@ -722,16 +726,16 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .unwrap() .map(str::to_string) .collect(); - let patterns = return_if_err!(1, patterns::get_patterns(&patterns[..])); + let patterns = crash_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()); + let file = crash_if_err!(1, File::open(file_name)); + let file_metadata = crash_if_err!(1, file.metadata()); if !file_metadata.is_file() { - crash!(1, "'{}' is not a regular file", file_name); + crash!(1, "{} is not a regular file", file_name.quote()); } crash_if_err!(1, csplit(&options, patterns, BufReader::new(file))); }; @@ -739,7 +743,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(SUMMARY) .arg( diff --git a/src/uu/csplit/src/csplit_error.rs b/src/uu/csplit/src/csplit_error.rs index 637cf8890..1d4823ee2 100644 --- a/src/uu/csplit/src/csplit_error.rs +++ b/src/uu/csplit/src/csplit_error.rs @@ -1,26 +1,28 @@ use std::io; use thiserror::Error; +use uucore::display::Quotable; + /// Errors thrown by the csplit command #[derive(Debug, Error)] pub enum CsplitError { #[error("IO error: {}", _0)] IoError(io::Error), - #[error("'{}': line number out of range", _0)] + #[error("{}: line number out of range", ._0.quote())] LineOutOfRange(String), - #[error("'{}': line number out of range on repetition {}", _0, _1)] + #[error("{}: line number out of range on repetition {}", ._0.quote(), _1)] LineOutOfRangeOnRepetition(String, usize), - #[error("'{}': match not found", _0)] + #[error("{}: match not found", ._0.quote())] MatchNotFound(String), - #[error("'{}': match not found on repetition {}", _0, _1)] + #[error("{}: match not found on repetition {}", ._0.quote(), _1)] MatchNotFoundOnRepetition(String, usize), #[error("line number must be greater than zero")] LineNumberIsZero, #[error("line number '{}' is smaller than preceding line number, {}", _0, _1)] LineNumberSmallerThanPrevious(usize, usize), - #[error("invalid pattern: {}", _0)] + #[error("{}: invalid pattern", ._0.quote())] InvalidPattern(String), - #[error("invalid number: '{}'", _0)] + #[error("invalid number: {}", ._0.quote())] InvalidNumber(String), #[error("incorrect conversion specification in suffix")] SuffixFormatIncorrect, diff --git a/src/uu/cut/Cargo.toml b/src/uu/cut/Cargo.toml index 6f92b39d1..c49450251 100644 --- a/src/uu/cut/Cargo.toml +++ b/src/uu/cut/Cargo.toml @@ -25,3 +25,7 @@ atty = "0.2" [[bin]] name = "cut" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/cut/src/cut.rs b/src/uu/cut/src/cut.rs index e33b8a2fe..35d92b83f 100644 --- a/src/uu/cut/src/cut.rs +++ b/src/uu/cut/src/cut.rs @@ -15,6 +15,7 @@ use clap::{crate_version, App, Arg}; use std::fs::File; use std::io::{stdin, stdout, BufReader, BufWriter, Read, Write}; use std::path::Path; +use uucore::display::Quotable; use self::searcher::Searcher; use uucore::ranges::Range; @@ -351,19 +352,19 @@ fn cut_files(mut filenames: Vec, mode: Mode) -> i32 { let path = Path::new(&filename[..]); if path.is_dir() { - show_error!("{}: Is a directory", filename); + show_error!("{}: Is a directory", filename.maybe_quote()); continue; } if path.metadata().is_err() { - show_error!("{}: No such file or directory", filename); + show_error!("{}: No such file or directory", filename.maybe_quote()); continue; } let file = match File::open(&path) { Ok(f) => f, Err(e) => { - show_error!("opening '{}': {}", &filename[..], e); + show_error!("opening {}: {}", filename.quote(), e); continue; } }; @@ -548,7 +549,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .name(NAME) .version(crate_version!()) .usage(SYNTAX) diff --git a/src/uu/date/Cargo.toml b/src/uu/date/Cargo.toml index c144d0d81..d2af8c4f1 100644 --- a/src/uu/date/Cargo.toml +++ b/src/uu/date/Cargo.toml @@ -29,3 +29,7 @@ winapi = { version = "0.3", features = ["minwinbase", "sysinfoapi", "minwindef"] [[bin]] name = "date" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index 042daa616..adcf77024 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -8,9 +8,6 @@ // spell-checker:ignore (chrono) Datelike Timelike ; (format) DATEFILE MMDDhhmm ; (vars) datetime datetimes -#[macro_use] -extern crate uucore; - use chrono::{DateTime, FixedOffset, Local, Offset, Utc}; #[cfg(windows)] use chrono::{Datelike, Timelike}; @@ -20,6 +17,8 @@ use libc::{clock_settime, timespec, CLOCK_REALTIME}; use std::fs::File; use std::io::{BufRead, BufReader}; use std::path::PathBuf; +use uucore::display::Quotable; +use uucore::show_error; #[cfg(windows)] use winapi::{ shared::minwindef::WORD, @@ -148,7 +147,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let format = if let Some(form) = matches.value_of(OPT_FORMAT) { if !form.starts_with('+') { - eprintln!("date: invalid date '{}'", form); + show_error!("invalid date {}", form.quote()); return 1; } let form = form[1..].to_string(); @@ -177,7 +176,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); + show_error!("invalid date {}", input.quote()); return 1; } Some(Ok(date)) => Some(date), @@ -243,7 +242,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { println!("{}", formatted); } Err((input, _err)) => { - println!("date: invalid date '{}'", input); + show_error!("invalid date {}", input.quote()); } } } @@ -253,7 +252,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .arg( @@ -355,13 +354,13 @@ fn set_system_datetime(_date: DateTime) -> i32 { #[cfg(target_os = "macos")] fn set_system_datetime(_date: DateTime) -> i32 { - eprintln!("date: setting the date is not supported by macOS"); + show_error!("setting the date is not supported by macOS"); 1 } #[cfg(target_os = "redox")] fn set_system_datetime(_date: DateTime) -> i32 { - eprintln!("date: setting the date is not supported by Redox"); + show_error!("setting the date is not supported by Redox"); 1 } @@ -381,7 +380,7 @@ fn set_system_datetime(date: DateTime) -> i32 { if result != 0 { let error = std::io::Error::last_os_error(); - eprintln!("date: cannot set date: {}", error); + show_error!("cannot set date: {}", error); error.raw_os_error().unwrap() } else { 0 @@ -411,7 +410,7 @@ fn set_system_datetime(date: DateTime) -> i32 { if result == 0 { let error = std::io::Error::last_os_error(); - eprintln!("date: cannot set date: {}", error); + show_error!("cannot set date: {}", error); error.raw_os_error().unwrap() } else { 0 diff --git a/src/uu/dd/Cargo.toml b/src/uu/dd/Cargo.toml index a0ed1ab91..007ebb8ff 100644 --- a/src/uu/dd/Cargo.toml +++ b/src/uu/dd/Cargo.toml @@ -31,3 +31,7 @@ signal-hook = "0.3.9" [[bin]] name = "dd" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index b300dff2d..9f1d28714 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -7,8 +7,6 @@ // spell-checker:ignore fname, tname, fpath, specfile, testfile, unspec, ifile, ofile, outfile, fullblock, urand, fileio, atoe, atoibm, behaviour, bmax, bremain, btotal, cflags, creat, ctable, ctty, datastructures, doesnt, etoa, fileout, fname, gnudd, iconvflags, nocache, noctty, noerror, nofollow, nolinks, nonblock, oconvflags, outfile, parseargs, rlen, rmax, rposition, rremain, rsofar, rstat, sigusr, sigval, wlen, wstat -#[macro_use] -extern crate uucore; use uucore::InvalidEncodingHandling; #[cfg(test)] @@ -275,13 +273,19 @@ impl Input { } } +trait OutputTrait: Sized + Write { + fn new(matches: &Matches) -> Result>; + fn fsync(&mut self) -> io::Result<()>; + fn fdatasync(&mut self) -> io::Result<()>; +} + struct Output { dst: W, obs: usize, cflags: OConvFlags, } -impl Output { +impl OutputTrait for Output { fn new(matches: &Matches) -> Result> { let obs = parseargs::parse_obs(matches)?; let cflags = parseargs::parse_conv_flag_output(matches)?; @@ -300,6 +304,100 @@ impl Output { } } +impl Output +where + Self: OutputTrait, +{ + fn write_blocks(&mut self, buf: Vec) -> io::Result { + let mut writes_complete = 0; + let mut writes_partial = 0; + let mut bytes_total = 0; + + for chunk in buf.chunks(self.obs) { + match self.write(chunk)? { + wlen if wlen < chunk.len() => { + writes_partial += 1; + bytes_total += wlen; + } + wlen => { + writes_complete += 1; + bytes_total += wlen; + } + } + } + + Ok(WriteStat { + writes_complete, + writes_partial, + bytes_total: bytes_total.try_into().unwrap_or(0u128), + }) + } + + fn dd_out(mut self, mut i: Input) -> Result<(), Box> { + let mut rstat = ReadStat { + reads_complete: 0, + reads_partial: 0, + records_truncated: 0, + }; + let mut wstat = WriteStat { + writes_complete: 0, + writes_partial: 0, + bytes_total: 0, + }; + let start = time::Instant::now(); + let bsize = calc_bsize(i.ibs, self.obs); + + let prog_tx = { + let (tx, rx) = mpsc::channel(); + thread::spawn(gen_prog_updater(rx, i.print_level)); + tx + }; + + while below_count_limit(&i.count, &rstat, &wstat) { + // Read/Write + let loop_bsize = calc_loop_bsize(&i.count, &rstat, &wstat, i.ibs, bsize); + match read_helper(&mut i, loop_bsize)? { + ( + ReadStat { + reads_complete: 0, + reads_partial: 0, + .. + }, + _, + ) => break, + (rstat_update, buf) => { + let wstat_update = self.write_blocks(buf)?; + + rstat += rstat_update; + wstat += wstat_update; + } + }; + // Update Prog + prog_tx.send(ProgUpdate { + read_stat: rstat, + write_stat: wstat, + duration: start.elapsed(), + })?; + } + + if self.cflags.fsync { + self.fsync()?; + } else if self.cflags.fdatasync { + self.fdatasync()?; + } + + match i.print_level { + Some(StatusLevel::Noxfer) | Some(StatusLevel::None) => {} + _ => print_transfer_stats(&ProgUpdate { + read_stat: rstat, + write_stat: wstat, + duration: start.elapsed(), + }), + } + Ok(()) + } +} + #[cfg(target_os = "linux")] fn make_linux_oflags(oflags: &OFlags) -> Option { let mut flag = 0; @@ -340,7 +438,7 @@ fn make_linux_oflags(oflags: &OFlags) -> Option { } } -impl Output { +impl OutputTrait for Output { fn new(matches: &Matches) -> Result> { fn open_dst(path: &Path, cflags: &OConvFlags, oflags: &OFlags) -> Result { let mut opts = OpenOptions::new(); @@ -430,62 +528,6 @@ impl Write for Output { } } -impl Output { - /// Write all data in the given buffer in writes of size obs. - fn write_blocks(&mut self, buf: Vec) -> io::Result { - let mut writes_complete = 0; - let mut writes_partial = 0; - let mut bytes_total = 0; - - for chunk in buf.chunks(self.obs) { - match self.write(chunk)? { - wlen if wlen < chunk.len() => { - writes_partial += 1; - bytes_total += wlen; - } - wlen => { - writes_complete += 1; - bytes_total += wlen; - } - } - } - - Ok(WriteStat { - writes_complete, - writes_partial, - bytes_total: bytes_total.try_into().unwrap_or(0u128), - }) - } -} - -impl Output { - /// Write all data in the given buffer in writes of size obs. - fn write_blocks(&mut self, buf: Vec) -> io::Result { - let mut writes_complete = 0; - let mut writes_partial = 0; - let mut bytes_total = 0; - - for chunk in buf.chunks(self.obs) { - match self.write(chunk)? { - wlen if wlen < chunk.len() => { - writes_partial += 1; - bytes_total += wlen; - } - wlen => { - writes_complete += 1; - bytes_total += wlen; - } - } - } - - Ok(WriteStat { - writes_complete, - writes_partial, - bytes_total: bytes_total.try_into().unwrap_or(0u128), - }) - } -} - /// Splits the content of buf into cbs-length blocks /// Appends padding as specified by conv=block and cbs=N /// Expects ascii encoded data @@ -827,140 +869,6 @@ fn below_count_limit(count: &Option, rstat: &ReadStat, wstat: &WriteS } } -/// Perform the copy/convert operations. Stdout version -/// Note: The body of this function should be kept identical to dd_fileout. This is definitely a problem from a maintenance perspective -/// and should be addressed (TODO). The problem exists because some of dd's functionality depends on whether the output is a file or stdout. -fn dd_stdout(mut i: Input, mut o: Output) -> Result<(), Box> { - let mut rstat = ReadStat { - reads_complete: 0, - reads_partial: 0, - records_truncated: 0, - }; - let mut wstat = WriteStat { - writes_complete: 0, - writes_partial: 0, - bytes_total: 0, - }; - let start = time::Instant::now(); - let bsize = calc_bsize(i.ibs, o.obs); - - let prog_tx = { - let (tx, rx) = mpsc::channel(); - thread::spawn(gen_prog_updater(rx, i.print_level)); - tx - }; - - while below_count_limit(&i.count, &rstat, &wstat) { - // Read/Write - let loop_bsize = calc_loop_bsize(&i.count, &rstat, &wstat, i.ibs, bsize); - match read_helper(&mut i, loop_bsize)? { - ( - ReadStat { - reads_complete: 0, - reads_partial: 0, - .. - }, - _, - ) => break, - (rstat_update, buf) => { - let wstat_update = o.write_blocks(buf)?; - - rstat += rstat_update; - wstat += wstat_update; - } - }; - // Update Prog - prog_tx.send(ProgUpdate { - read_stat: rstat, - write_stat: wstat, - duration: start.elapsed(), - })?; - } - - if o.cflags.fsync { - o.fsync()?; - } else if o.cflags.fdatasync { - o.fdatasync()?; - } - - match i.print_level { - Some(StatusLevel::Noxfer) | Some(StatusLevel::None) => {} - _ => print_transfer_stats(&ProgUpdate { - read_stat: rstat, - write_stat: wstat, - duration: start.elapsed(), - }), - } - Ok(()) -} - -/// Perform the copy/convert operations. File backed output version -/// Note: The body of this function should be kept identical to dd_stdout. This is definitely a problem from a maintenance perspective -/// and should be addressed (TODO). The problem exists because some of dd's functionality depends on whether the output is a file or stdout. -fn dd_fileout(mut i: Input, mut o: Output) -> Result<(), Box> { - let mut rstat = ReadStat { - reads_complete: 0, - reads_partial: 0, - records_truncated: 0, - }; - let mut wstat = WriteStat { - writes_complete: 0, - writes_partial: 0, - bytes_total: 0, - }; - let start = time::Instant::now(); - let bsize = calc_bsize(i.ibs, o.obs); - - let prog_tx = { - let (tx, rx) = mpsc::channel(); - thread::spawn(gen_prog_updater(rx, i.print_level)); - tx - }; - - while below_count_limit(&i.count, &rstat, &wstat) { - // Read/Write - let loop_bsize = calc_loop_bsize(&i.count, &rstat, &wstat, i.ibs, bsize); - match read_helper(&mut i, loop_bsize)? { - ( - ReadStat { - reads_complete: 0, - reads_partial: 0, - .. - }, - _, - ) => break, - (rstat_update, buf) => { - let wstat_update = o.write_blocks(buf)?; - - rstat += rstat_update; - wstat += wstat_update; - } - }; - // Update Prog - prog_tx.send(ProgUpdate { - read_stat: rstat, - write_stat: wstat, - duration: start.elapsed(), - })?; - } - - if o.cflags.fsync { - o.fsync()?; - } else if o.cflags.fdatasync { - o.fdatasync()?; - } - - match i.print_level { - Some(StatusLevel::Noxfer) | Some(StatusLevel::None) => {} - _ => print_transfer_stats(&ProgUpdate { - read_stat: rstat, - write_stat: wstat, - duration: start.elapsed(), - }), - } - Ok(()) -} - fn append_dashes_if_not_present(mut acc: Vec, mut s: String) -> Vec { if !s.starts_with("--") && !s.starts_with('-') { s.insert_str(0, "--"); @@ -1009,7 +917,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let (i, o) = unpack_or_rtn!(Input::::new(&matches), Output::::new(&matches)); - dd_fileout(i, o) + o.dd_out(i) } (false, true) => { let (i, o) = unpack_or_rtn!( @@ -1017,7 +925,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { Output::::new(&matches) ); - dd_fileout(i, o) + o.dd_out(i) } (true, false) => { let (i, o) = unpack_or_rtn!( @@ -1025,7 +933,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { Output::::new(&matches) ); - dd_stdout(i, o) + o.dd_out(i) } (false, false) => { let (i, o) = unpack_or_rtn!( @@ -1033,7 +941,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { Output::::new(&matches) ); - dd_stdout(i, o) + o.dd_out(i) } }; match result { @@ -1046,7 +954,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> clap::App<'static, 'static> { - clap::App::new(executable!()) + clap::App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .arg( diff --git a/src/uu/dd/src/dd_unit_tests/conversion_tests.rs b/src/uu/dd/src/dd_unit_tests/conversion_tests.rs index c4515d3a2..9255a1a89 100644 --- a/src/uu/dd/src/dd_unit_tests/conversion_tests.rs +++ b/src/uu/dd/src/dd_unit_tests/conversion_tests.rs @@ -153,7 +153,7 @@ fn all_valid_ascii_ebcdic_ascii_roundtrip_conv_test() { cflags: OConvFlags::default(), }; - dd_fileout(i, o).unwrap(); + o.dd_out(i).unwrap(); // EBCDIC->ASCII let test_name = "all-valid-ebcdic-to-ascii"; @@ -175,7 +175,7 @@ fn all_valid_ascii_ebcdic_ascii_roundtrip_conv_test() { cflags: OConvFlags::default(), }; - dd_fileout(i, o).unwrap(); + o.dd_out(i).unwrap(); // Final Comparison let res = File::open(&tmp_fname_ea).unwrap(); diff --git a/src/uu/dd/src/dd_unit_tests/mod.rs b/src/uu/dd/src/dd_unit_tests/mod.rs index 27b5a18ad..9641c9bba 100644 --- a/src/uu/dd/src/dd_unit_tests/mod.rs +++ b/src/uu/dd/src/dd_unit_tests/mod.rs @@ -67,7 +67,7 @@ macro_rules! make_spec_test ( #[test] fn $test_id() { - dd_fileout($i,$o).unwrap(); + $o.dd_out($i).unwrap(); let res = File::open($tmp_fname).unwrap(); // Check test file isn't empty (unless spec file is too) diff --git a/src/uu/dd/src/parseargs/unit_tests.rs b/src/uu/dd/src/parseargs/unit_tests.rs index d4d74f58f..b898f1e5d 100644 --- a/src/uu/dd/src/parseargs/unit_tests.rs +++ b/src/uu/dd/src/parseargs/unit_tests.rs @@ -10,7 +10,7 @@ fn unimplemented_flags_should_error_non_linux() { let mut succeeded = Vec::new(); // The following flags are only implemented in linux - for flag in vec![ + for &flag in &[ "direct", "directory", "dsync", @@ -27,13 +27,11 @@ fn unimplemented_flags_should_error_non_linux() { ]; let matches = uu_app().get_matches_from_safe(args).unwrap(); - match parse_iflags(&matches) { - Ok(_) => succeeded.push(format!("iflag={}", flag)), - Err(_) => { /* expected behaviour :-) */ } + if parse_iflags(&matches).is_ok() { + succeeded.push(format!("iflag={}", flag)); } - match parse_oflags(&matches) { - Ok(_) => succeeded.push(format!("oflag={}", flag)), - Err(_) => { /* expected behaviour :-) */ } + if parse_oflags(&matches).is_ok() { + succeeded.push(format!("oflag={}", flag)); } } @@ -50,7 +48,7 @@ fn unimplemented_flags_should_error() { let mut succeeded = Vec::new(); // The following flags are not implemented - for flag in vec!["cio", "nocache", "nolinks", "text", "binary"] { + for &flag in &["cio", "nocache", "nolinks", "text", "binary"] { let args = vec![ String::from("dd"), format!("--iflag={}", flag), @@ -58,13 +56,11 @@ fn unimplemented_flags_should_error() { ]; let matches = uu_app().get_matches_from_safe(args).unwrap(); - match parse_iflags(&matches) { - Ok(_) => succeeded.push(format!("iflag={}", flag)), - Err(_) => { /* expected behaviour :-) */ } + if parse_iflags(&matches).is_ok() { + succeeded.push(format!("iflag={}", flag)) } - match parse_oflags(&matches) { - Ok(_) => succeeded.push(format!("oflag={}", flag)), - Err(_) => { /* expected behaviour :-) */ } + if parse_oflags(&matches).is_ok() { + succeeded.push(format!("oflag={}", flag)) } } @@ -356,7 +352,7 @@ fn parse_icf_token_ibm() { assert_eq!(exp.len(), act.len()); for cf in &exp { - assert!(exp.contains(&cf)); + assert!(exp.contains(cf)); } } @@ -373,7 +369,7 @@ fn parse_icf_tokens_elu() { assert_eq!(exp.len(), act.len()); for cf in &exp { - assert!(exp.contains(&cf)); + assert!(exp.contains(cf)); } } @@ -405,7 +401,7 @@ fn parse_icf_tokens_remaining() { assert_eq!(exp.len(), act.len()); for cf in &exp { - assert!(exp.contains(&cf)); + assert!(exp.contains(cf)); } } @@ -429,7 +425,7 @@ fn parse_iflag_tokens() { assert_eq!(exp.len(), act.len()); for cf in &exp { - assert!(exp.contains(&cf)); + assert!(exp.contains(cf)); } } @@ -453,7 +449,7 @@ fn parse_oflag_tokens() { assert_eq!(exp.len(), act.len()); for cf in &exp { - assert!(exp.contains(&cf)); + assert!(exp.contains(cf)); } } @@ -481,7 +477,7 @@ fn parse_iflag_tokens_linux() { assert_eq!(exp.len(), act.len()); for cf in &exp { - assert!(exp.contains(&cf)); + assert!(exp.contains(cf)); } } @@ -509,7 +505,7 @@ fn parse_oflag_tokens_linux() { assert_eq!(exp.len(), act.len()); for cf in &exp { - assert!(exp.contains(&cf)); + assert!(exp.contains(cf)); } } diff --git a/src/uu/df/src/df.rs b/src/uu/df/src/df.rs index cdfdf0b2d..310d3c664 100644 --- a/src/uu/df/src/df.rs +++ b/src/uu/df/src/df.rs @@ -28,9 +28,6 @@ use std::fmt::Display; #[cfg(unix)] use std::mem; -#[cfg(target_os = "freebsd")] -use uucore::libc::{c_char, fsid_t, uid_t}; - #[cfg(windows)] use std::path::Path; @@ -79,8 +76,8 @@ struct Filesystem { usage: FsUsage, } -fn get_usage() -> String { - format!("{0} [OPTION]... [FILE]...", executable!()) +fn usage() -> String { + format!("{0} [OPTION]... [FILE]...", uucore::execution_phrase()) } impl FsSelector { @@ -284,7 +281,7 @@ impl UError for DfError { #[uucore_procs::gen_uumain] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let usage = get_usage(); + let usage = usage(); let matches = uu_app().usage(&usage[..]).get_matches_from(args); let paths: Vec = matches @@ -295,7 +292,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { #[cfg(windows)] { if matches.is_present(OPT_INODES) { - println!("{}: doesn't support -i option", executable!()); + println!("{}: doesn't support -i option", uucore::util_name()); return Ok(()); } } @@ -427,7 +424,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .arg( diff --git a/src/uu/dircolors/Cargo.toml b/src/uu/dircolors/Cargo.toml index e9e333ec6..ad95564f3 100644 --- a/src/uu/dircolors/Cargo.toml +++ b/src/uu/dircolors/Cargo.toml @@ -23,3 +23,7 @@ uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_p [[bin]] name = "dircolors" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/dircolors/src/dircolors.rs b/src/uu/dircolors/src/dircolors.rs index 70b609e31..5d0b3ac3e 100644 --- a/src/uu/dircolors/src/dircolors.rs +++ b/src/uu/dircolors/src/dircolors.rs @@ -17,6 +17,7 @@ use std::fs::File; use std::io::{BufRead, BufReader}; use clap::{crate_version, App, Arg}; +use uucore::display::Quotable; mod options { pub const BOURNE_SHELL: &str = "bourne-shell"; @@ -62,8 +63,8 @@ pub fn guess_syntax() -> OutputFmt { } } -fn get_usage() -> String { - format!("{0} {1}", executable!(), SYNTAX) +fn usage() -> String { + format!("{0} {1}", uucore::execution_phrase(), SYNTAX) } pub fn uumain(args: impl uucore::Args) -> i32 { @@ -71,7 +72,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .collect_str(InvalidEncodingHandling::Ignore) .accept_any(); - let usage = get_usage(); + let usage = usage(); let matches = uu_app().usage(&usage[..]).get_matches_from(&args); @@ -94,9 +95,9 @@ 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] + files[0].quote() ); return 1; } @@ -126,7 +127,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].quote()); return 1; } match File::open(files[0]) { @@ -135,7 +136,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { result = parse(fin.lines().filter_map(Result::ok), out_format, files[0]) } Err(e) => { - show_error!("{}: {}", files[0], e); + show_error!("{}: {}", files[0].maybe_quote(), e); return 1; } } @@ -153,7 +154,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(SUMMARY) .after_help(LONG_HELP) @@ -314,7 +315,8 @@ where if val.is_empty() { return Err(format!( "{}:{}: invalid line; missing second token", - fp, num + fp.maybe_quote(), + num )); } let lower = key.to_lowercase(); @@ -341,7 +343,12 @@ where } else if let Some(s) = table.get(lower.as_str()) { result.push_str(format!("{}={}:", s, val).as_str()); } else { - return Err(format!("{}:{}: unrecognized keyword {}", fp, num, key)); + return Err(format!( + "{}:{}: unrecognized keyword {}", + fp.maybe_quote(), + num, + key + )); } } } diff --git a/src/uu/dirname/src/dirname.rs b/src/uu/dirname/src/dirname.rs index e7dcc2195..601d93ac0 100644 --- a/src/uu/dirname/src/dirname.rs +++ b/src/uu/dirname/src/dirname.rs @@ -10,6 +10,7 @@ extern crate uucore; use clap::{crate_version, App, Arg}; use std::path::Path; +use uucore::display::print_verbatim; use uucore::error::{UResult, UUsageError}; use uucore::InvalidEncodingHandling; @@ -20,8 +21,8 @@ mod options { pub const DIR: &str = "dir"; } -fn get_usage() -> String { - format!("{0} [OPTION] NAME...", executable!()) +fn usage() -> String { + format!("{0} [OPTION] NAME...", uucore::execution_phrase()) } fn get_long_usage() -> String { @@ -37,7 +38,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .collect_str(InvalidEncodingHandling::ConvertLossy) .accept_any(); - let usage = get_usage(); + let usage = usage(); let after_help = get_long_usage(); let matches = uu_app() @@ -65,7 +66,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { if d.components().next() == None { print!(".") } else { - print!("{}", d.to_string_lossy()); + print_verbatim(d).unwrap(); } } None => { @@ -86,7 +87,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .about(ABOUT) .version(crate_version!()) .arg( diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index d85cc941c..9fd44b001 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -32,6 +32,7 @@ use std::path::PathBuf; use std::str::FromStr; use std::time::{Duration, UNIX_EPOCH}; use std::{error::Error, fmt::Display}; +use uucore::display::{print_verbatim, Quotable}; use uucore::error::{UError, UResult}; use uucore::parse_size::{parse_size, ParseSizeError}; use uucore::InvalidEncodingHandling; @@ -70,7 +71,6 @@ mod options { pub const FILE: &str = "FILE"; } -const NAME: &str = "du"; const SUMMARY: &str = "estimate file space usage"; const LONG_HELP: &str = " Display values are in units of the first available SIZE from --block-size, @@ -87,7 +87,7 @@ const UNITS: [(char, u32); 6] = [('E', 6), ('P', 5), ('T', 4), ('G', 3), ('M', 2 struct Options { all: bool, - program_name: String, + util_name: String, max_depth: Option, total: bool, separate_dirs: bool, @@ -294,9 +294,9 @@ fn du( Err(e) => { safe_writeln!( stderr(), - "{}: cannot read directory '{}': {}", - options.program_name, - my_stat.path.display(), + "{}: cannot read directory {}: {}", + options.util_name, + my_stat.path.quote(), e ); return Box::new(iter::once(my_stat)); @@ -335,11 +335,11 @@ fn du( } Err(error) => match error.kind() { ErrorKind::PermissionDenied => { - let description = format!("cannot access '{}'", entry.path().display()); + let description = format!("cannot access {}", entry.path().quote()); let error_message = "Permission denied"; show_error_custom_description!(description, "{}", error_message) } - _ => show_error!("cannot access '{}': {}", entry.path().display(), error), + _ => show_error!("cannot access {}: {}", entry.path().quote(), error), }, }, Err(error) => show_error!("{}", error), @@ -393,11 +393,11 @@ fn convert_size_other(size: u64, _multiplier: u64, block_size: u64) -> String { format!("{}", ((size as f64) / (block_size as f64)).ceil()) } -fn get_usage() -> String { +fn usage() -> String { format!( "{0} [OPTION]... [FILE]... {0} [OPTION]... --files0-from=F", - executable!() + uucore::execution_phrase() ) } @@ -412,25 +412,30 @@ enum DuError { impl Display for DuError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - DuError::InvalidMaxDepthArg(s) => write!(f, "invalid maximum depth '{}'", s), + DuError::InvalidMaxDepthArg(s) => write!(f, "invalid maximum depth {}", s.quote()), DuError::SummarizeDepthConflict(s) => { - write!(f, "summarizing conflicts with --max-depth={}", s) + write!( + f, + "summarizing conflicts with --max-depth={}", + s.maybe_quote() + ) } DuError::InvalidTimeStyleArg(s) => write!( f, - "invalid argument '{}' for 'time style' + "invalid argument {} for 'time style' Valid arguments are: - 'full-iso' - 'long-iso' - 'iso' Try '{} --help' for more information.", - s, NAME + s.quote(), + uucore::execution_phrase() ), DuError::InvalidTimeArg(s) => write!( f, - "Invalid argument '{}' for --time. + "Invalid argument {} for --time. 'birth' and 'creation' arguments are not supported on this platform.", - s + s.quote() ), } } @@ -456,7 +461,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .collect_str(InvalidEncodingHandling::Ignore) .accept_any(); - let usage = get_usage(); + let usage = usage(); let matches = uu_app().usage(&usage[..]).get_matches_from(args); @@ -466,7 +471,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let options = Options { all: matches.is_present(options::ALL), - program_name: NAME.to_owned(), + util_name: uucore::util_name().to_owned(), max_depth, total: matches.is_present(options::TOTAL), separate_dirs: matches.is_present(options::SEPARATE_DIRS), @@ -566,21 +571,14 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { }; 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 - ); + print!("{}\t{}\t", convert_size(size), time_str); + print_verbatim(stat.path).unwrap(); + print!("{}", line_separator); } } else if !summarize || index == len - 1 { - print!( - "{}\t{}{}", - convert_size(size), - stat.path.display(), - line_separator - ); + print!("{}\t", convert_size(size)); + print_verbatim(stat.path).unwrap(); + print!("{}", line_separator); } if options.total && index == (len - 1) { // The last element will be the total size of the the path under @@ -590,7 +588,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } } Err(_) => { - show_error!("{}: {}", path_string, "No such file or directory"); + show_error!( + "{}: {}", + path_string.maybe_quote(), + "No such file or directory" + ); } } } @@ -625,7 +627,7 @@ fn parse_depth(max_depth_str: Option<&str>, summarize: bool) -> UResult App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(SUMMARY) .after_help(LONG_HELP) @@ -837,8 +839,8 @@ fn format_error_message(error: ParseSizeError, s: &str, option: &str) -> String // GNU's du echos affected flag, -B or --block-size (-t or --threshold), depending user's selection // GNU's du does distinguish between "invalid (suffix in) argument" match error { - ParseSizeError::ParseFailure(_) => format!("invalid --{} argument '{}'", option, s), - ParseSizeError::SizeTooBig(_) => format!("--{} argument '{}' too large", option, s), + ParseSizeError::ParseFailure(_) => format!("invalid --{} argument {}", option, s.quote()), + ParseSizeError::SizeTooBig(_) => format!("--{} argument {} too large", option, s.quote()), } } diff --git a/src/uu/echo/src/echo.rs b/src/uu/echo/src/echo.rs index aae1ad10d..601fd8d48 100644 --- a/src/uu/echo/src/echo.rs +++ b/src/uu/echo/src/echo.rs @@ -132,7 +132,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .name(NAME) // TrailingVarArg specifies the final positional argument is a VarArg // and it doesn't attempts the parse any further args. diff --git a/src/uu/env/Cargo.toml b/src/uu/env/Cargo.toml index 9f778aa3f..c368cfbac 100644 --- a/src/uu/env/Cargo.toml +++ b/src/uu/env/Cargo.toml @@ -17,7 +17,7 @@ path = "src/env.rs" [dependencies] clap = { version = "2.33", features = ["wrap_help"] } libc = "0.2.42" -rust-ini = "0.13.0" +rust-ini = "0.17.0" uucore = { version=">=0.0.9", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/env/src/env.rs b/src/uu/env/src/env.rs index 51ff92801..aec97fc24 100644 --- a/src/uu/env/src/env.rs +++ b/src/uu/env/src/env.rs @@ -12,6 +12,9 @@ #[macro_use] extern crate clap; +#[macro_use] +extern crate uucore; + use clap::{App, AppSettings, Arg}; use ini::Ini; use std::borrow::Cow; @@ -19,6 +22,8 @@ use std::env; use std::io::{self, Write}; use std::iter::Iterator; use std::process::Command; +use uucore::display::Quotable; +use uucore::error::{UResult, USimpleError}; const USAGE: &str = "env [OPTION]... [-] [NAME=VALUE]... [COMMAND [ARG]...]"; const AFTER_HELP: &str = "\ @@ -61,8 +66,14 @@ fn parse_name_value_opt<'a>(opts: &mut Options<'a>, opt: &'a str) -> Result(opts: &mut Options<'a>, opt: &'a str) -> Result<(), i32> { if opts.null { - eprintln!("{}: cannot specify --null (-0) with command", crate_name!()); - eprintln!("Type \"{} --help\" for detailed information", crate_name!()); + eprintln!( + "{}: cannot specify --null (-0) with command", + uucore::util_name() + ); + eprintln!( + "Type \"{} --help\" for detailed information", + uucore::execution_phrase() + ); Err(1) } else { opts.program.push(opt); @@ -70,7 +81,7 @@ fn parse_program_opt<'a>(opts: &mut Options<'a>, opt: &'a str) -> Result<(), i32 } } -fn load_config_file(opts: &mut Options) -> Result<(), i32> { +fn load_config_file(opts: &mut Options) -> UResult<()> { // NOTE: config files are parsed using an INI parser b/c it's available and compatible with ".env"-style files // ... * but support for actual INI files, although working, is not intended, nor claimed for &file in &opts.files { @@ -83,13 +94,13 @@ fn load_config_file(opts: &mut Options) -> Result<(), i32> { }; let conf = conf.map_err(|error| { - eprintln!("env: error: \"{}\": {}", file, error); + show_error!("{}: {}", file.maybe_quote(), error); 1 })?; for (_, prop) in &conf { // ignore all INI section lines (treat them as comments) - for (key, value) in prop { + for (key, value) in prop.iter() { env::set_var(key, value); } } @@ -157,7 +168,7 @@ pub fn uu_app() -> App<'static, 'static> { .help("remove variable from the environment")) } -fn run_env(args: impl uucore::Args) -> Result<(), i32> { +fn run_env(args: impl uucore::Args) -> UResult<()> { let app = uu_app(); let matches = app.get_matches_from(args); @@ -188,8 +199,10 @@ fn run_env(args: impl uucore::Args) -> Result<(), i32> { match env::set_current_dir(d) { Ok(()) => d, Err(error) => { - eprintln!("env: cannot change directory to \"{}\": {}", d, error); - return Err(125); + return Err(USimpleError::new( + 125, + format!("cannot change directory to \"{}\": {}", d, error), + )); } }; } @@ -253,9 +266,9 @@ fn run_env(args: impl uucore::Args) -> Result<(), i32> { // FIXME: this should just use execvp() (no fork()) on Unix-like systems match Command::new(&*prog).args(args).status() { - Ok(exit) if !exit.success() => return Err(exit.code().unwrap()), - Err(ref err) if err.kind() == io::ErrorKind::NotFound => return Err(127), - Err(_) => return Err(126), + Ok(exit) if !exit.success() => return Err(exit.code().unwrap().into()), + Err(ref err) if err.kind() == io::ErrorKind::NotFound => return Err(127.into()), + Err(_) => return Err(126.into()), Ok(_) => (), } } else { @@ -266,9 +279,7 @@ fn run_env(args: impl uucore::Args) -> Result<(), i32> { Ok(()) } -pub fn uumain(args: impl uucore::Args) -> i32 { - match run_env(args) { - Ok(()) => 0, - Err(code) => code, - } +#[uucore_procs::gen_uumain] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + run_env(args) } diff --git a/src/uu/expand/Cargo.toml b/src/uu/expand/Cargo.toml index 5921ef679..e9b2cc747 100644 --- a/src/uu/expand/Cargo.toml +++ b/src/uu/expand/Cargo.toml @@ -23,3 +23,7 @@ uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_p [[bin]] name = "expand" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/expand/src/expand.rs b/src/uu/expand/src/expand.rs index d5c37ce21..b09b8aaab 100644 --- a/src/uu/expand/src/expand.rs +++ b/src/uu/expand/src/expand.rs @@ -17,6 +17,7 @@ use std::fs::File; use std::io::{stdin, stdout, BufRead, BufReader, BufWriter, Read, Write}; use std::str::from_utf8; use unicode_width::UnicodeWidthChar; +use uucore::display::Quotable; static ABOUT: &str = "Convert tabs in each FILE to spaces, writing to standard output. With no FILE, or when FILE is -, read standard input."; @@ -32,8 +33,8 @@ static LONG_HELP: &str = ""; static DEFAULT_TABSTOP: usize = 8; -fn get_usage() -> String { - format!("{0} [OPTION]... [FILE]...", executable!()) +fn usage() -> String { + format!("{0} [OPTION]... [FILE]...", uucore::execution_phrase()) } /// The mode to use when replacing tabs beyond the last one specified in @@ -170,7 +171,7 @@ impl Options { } pub fn uumain(args: impl uucore::Args) -> i32 { - let usage = get_usage(); + let usage = usage(); let matches = uu_app().usage(&usage[..]).get_matches_from(args); expand(Options::new(&matches)); @@ -178,7 +179,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .after_help(LONG_HELP) @@ -216,7 +217,7 @@ fn open(path: String) -> BufReader> { } else { file_buf = match File::open(&path[..]) { Ok(a) => a, - Err(e) => crash!(1, "{}: {}\n", &path[..], e), + Err(e) => crash!(1, "{}: {}\n", path.maybe_quote(), e), }; BufReader::new(Box::new(file_buf) as Box) } @@ -329,12 +330,15 @@ fn expand(options: Options) { // now dump out either spaces if we're expanding, or a literal tab if we're not if init || !options.iflag { if nts <= options.tspaces.len() { - safe_unwrap!(output.write_all(options.tspaces[..nts].as_bytes())); + crash_if_err!( + 1, + output.write_all(options.tspaces[..nts].as_bytes()) + ); } else { - safe_unwrap!(output.write_all(" ".repeat(nts).as_bytes())); + crash_if_err!(1, output.write_all(" ".repeat(nts).as_bytes())); }; } else { - safe_unwrap!(output.write_all(&buf[byte..byte + nbytes])); + crash_if_err!(1, output.write_all(&buf[byte..byte + nbytes])); } } _ => { @@ -352,14 +356,14 @@ fn expand(options: Options) { init = false; } - safe_unwrap!(output.write_all(&buf[byte..byte + nbytes])); + crash_if_err!(1, output.write_all(&buf[byte..byte + nbytes])); } } byte += nbytes; // advance the pointer } - safe_unwrap!(output.flush()); + crash_if_err!(1, output.flush()); buf.truncate(0); // clear the buffer } } diff --git a/src/uu/expr/Cargo.toml b/src/uu/expr/Cargo.toml index 65d4fa636..035f00721 100644 --- a/src/uu/expr/Cargo.toml +++ b/src/uu/expr/Cargo.toml @@ -26,3 +26,7 @@ uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_p [[bin]] name = "expr" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/expr/src/expr.rs b/src/uu/expr/src/expr.rs index 92c15565d..2d82300ff 100644 --- a/src/uu/expr/src/expr.rs +++ b/src/uu/expr/src/expr.rs @@ -18,7 +18,7 @@ const VERSION: &str = "version"; const HELP: &str = "help"; pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .arg(Arg::with_name(VERSION).long(VERSION)) .arg(Arg::with_name(HELP).long(HELP)) } @@ -140,5 +140,5 @@ Environment variables: } fn print_version() { - println!("{} {}", executable!(), crate_version!()); + println!("{} {}", uucore::util_name(), crate_version!()); } diff --git a/src/uu/factor/Cargo.toml b/src/uu/factor/Cargo.toml index 76c06a34c..9d62e5f2b 100644 --- a/src/uu/factor/Cargo.toml +++ b/src/uu/factor/Cargo.toml @@ -34,3 +34,7 @@ path = "src/main.rs" [lib] path = "src/cli.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/factor/src/cli.rs b/src/uu/factor/src/cli.rs index 7963f162f..30541c244 100644 --- a/src/uu/factor/src/cli.rs +++ b/src/uu/factor/src/cli.rs @@ -16,6 +16,7 @@ use std::io::{self, stdin, stdout, BufRead, Write}; mod factor; use clap::{crate_version, App, Arg}; pub use factor::*; +use uucore::display::Quotable; mod miller_rabin; pub mod numeric; @@ -52,7 +53,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { if let Some(values) = matches.values_of(options::NUMBER) { for number in values { if let Err(e) = print_factors_str(number, &mut w, &mut factors_buffer) { - show_warning!("{}: {}", number, e); + show_warning!("{}: {}", number.maybe_quote(), e); } } } else { @@ -61,7 +62,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { for line in stdin.lock().lines() { for number in line.unwrap().split_whitespace() { if let Err(e) = print_factors_str(number, &mut w, &mut factors_buffer) { - show_warning!("{}: {}", number, e); + show_warning!("{}: {}", number.maybe_quote(), e); } } } @@ -75,7 +76,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(SUMMARY) .arg(Arg::with_name(options::NUMBER).multiple(true)) diff --git a/src/uu/factor/src/table.rs b/src/uu/factor/src/table.rs index 6d4047e49..0fb338d9d 100644 --- a/src/uu/factor/src/table.rs +++ b/src/uu/factor/src/table.rs @@ -86,7 +86,7 @@ mod tests { let mut n_c: [u64; CHUNK_SIZE] = rng.gen(); let mut f_c: [Factors; CHUNK_SIZE] = rng.gen(); - let mut n_i = n_c.clone(); + let mut n_i = n_c; let mut f_i = f_c.clone(); for (n, f) in n_i.iter_mut().zip(f_i.iter_mut()) { factor(n, f); diff --git a/src/uu/false/src/false.rs b/src/uu/false/src/false.rs index 170788898..88ec1af06 100644 --- a/src/uu/false/src/false.rs +++ b/src/uu/false/src/false.rs @@ -9,7 +9,7 @@ extern crate uucore; use clap::App; -use uucore::{error::UResult, executable}; +use uucore::error::UResult; #[uucore_procs::gen_uumain] pub fn uumain(args: impl uucore::Args) -> UResult<()> { @@ -18,5 +18,5 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) } diff --git a/src/uu/fmt/Cargo.toml b/src/uu/fmt/Cargo.toml index dea0726a6..75b81c354 100644 --- a/src/uu/fmt/Cargo.toml +++ b/src/uu/fmt/Cargo.toml @@ -24,3 +24,7 @@ uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_p [[bin]] name = "fmt" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/fmt/src/fmt.rs b/src/uu/fmt/src/fmt.rs index 8c2c8d9d9..669c98b14 100644 --- a/src/uu/fmt/src/fmt.rs +++ b/src/uu/fmt/src/fmt.rs @@ -15,6 +15,7 @@ use std::cmp; use std::fs::File; use std::io::{stdin, stdout, Write}; use std::io::{BufReader, BufWriter, Read}; +use uucore::display::Quotable; use self::linebreak::break_lines; use self::parasplit::ParagraphStream; @@ -50,8 +51,8 @@ static OPT_TAB_WIDTH: &str = "tab-width"; static ARG_FILES: &str = "files"; -fn get_usage() -> String { - format!("{} [OPTION]... [FILE]...", executable!()) +fn usage() -> String { + format!("{} [OPTION]... [FILE]...", uucore::execution_phrase()) } pub type FileOrStdReader = BufReader>; @@ -75,7 +76,7 @@ pub struct FmtOptions { #[allow(clippy::cognitive_complexity)] pub fn uumain(args: impl uucore::Args) -> i32 { - let usage = get_usage(); + let usage = usage(); let matches = uu_app().usage(&usage[..]).get_matches_from(args); @@ -132,7 +133,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { fmt_opts.width = match s.parse::() { Ok(t) => t, Err(e) => { - crash!(1, "Invalid WIDTH specification: `{}': {}", s, e); + crash!(1, "Invalid WIDTH specification: {}: {}", s.quote(), e); } }; if fmt_opts.width > MAX_WIDTH { @@ -149,7 +150,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { fmt_opts.goal = match s.parse::() { Ok(t) => t, Err(e) => { - crash!(1, "Invalid GOAL specification: `{}': {}", s, e); + crash!(1, "Invalid GOAL specification: {}: {}", s.quote(), e); } }; if !matches.is_present(OPT_WIDTH) { @@ -163,7 +164,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { fmt_opts.tabwidth = match s.parse::() { Ok(t) => t, Err(e) => { - crash!(1, "Invalid TABWIDTH specification: `{}': {}", s, e); + crash!(1, "Invalid TABWIDTH specification: {}: {}", s.quote(), e); } }; }; @@ -187,7 +188,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { _ => match File::open(i) { Ok(f) => BufReader::new(Box::new(f) as Box), Err(e) => { - show_warning!("{}: {}", i, e); + show_warning!("{}: {}", i.maybe_quote(), e); continue; } }, @@ -211,7 +212,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .arg( diff --git a/src/uu/fold/Cargo.toml b/src/uu/fold/Cargo.toml index 446be290d..7ec886264 100644 --- a/src/uu/fold/Cargo.toml +++ b/src/uu/fold/Cargo.toml @@ -22,3 +22,7 @@ uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_p [[bin]] name = "fold" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/fold/src/fold.rs b/src/uu/fold/src/fold.rs index 1dbc8cdc7..c4cc16469 100644 --- a/src/uu/fold/src/fold.rs +++ b/src/uu/fold/src/fold.rs @@ -64,7 +64,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .name(NAME) .version(crate_version!()) .usage(SYNTAX) @@ -119,7 +119,7 @@ fn fold(filenames: Vec, bytes: bool, spaces: bool, width: usize) { stdin_buf = stdin(); &mut stdin_buf as &mut dyn Read } else { - file_buf = safe_unwrap!(File::open(Path::new(filename))); + file_buf = crash_if_err!(1, File::open(Path::new(filename))); &mut file_buf as &mut dyn Read }); diff --git a/src/uu/groups/src/groups.rs b/src/uu/groups/src/groups.rs index a40d1a490..43e2a2239 100644 --- a/src/uu/groups/src/groups.rs +++ b/src/uu/groups/src/groups.rs @@ -17,7 +17,10 @@ #[macro_use] extern crate uucore; -use uucore::entries::{get_groups_gnu, gid2grp, Locate, Passwd}; +use uucore::{ + display::Quotable, + entries::{get_groups_gnu, gid2grp, Locate, Passwd}, +}; use clap::{crate_version, App, Arg}; @@ -28,12 +31,12 @@ 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} [OPTION]... [USERNAME]...", executable!()) +fn usage() -> String { + format!("{0} [OPTION]... [USERNAME]...", uucore::execution_phrase()) } pub fn uumain(args: impl uucore::Args) -> i32 { - let usage = get_usage(); + let usage = usage(); let matches = uu_app().usage(&usage[..]).get_matches_from(args); @@ -77,7 +80,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .join(" ") ); } else { - show_error!("'{}': no such user", user); + show_error!("{}: no such user", user.quote()); exit_code = 1; } } @@ -85,7 +88,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .arg( diff --git a/src/uu/hashsum/Cargo.toml b/src/uu/hashsum/Cargo.toml index 4b541ec81..7cb88dede 100644 --- a/src/uu/hashsum/Cargo.toml +++ b/src/uu/hashsum/Cargo.toml @@ -19,6 +19,7 @@ digest = "0.6.1" clap = { version = "2.33", features = ["wrap_help"] } hex = "0.2.0" libc = "0.2.42" +memchr = "2" md5 = "0.3.5" regex = "1.0.1" regex-syntax = "0.6.7" @@ -32,3 +33,7 @@ uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_p [[bin]] name = "hashsum" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/hashsum/src/digest.rs b/src/uu/hashsum/src/digest.rs index 9093d94a7..531dc7e4f 100644 --- a/src/uu/hashsum/src/digest.rs +++ b/src/uu/hashsum/src/digest.rs @@ -1,10 +1,22 @@ +// spell-checker:ignore memmem +//! Implementations of digest functions, like md5 and sha1. +//! +//! The [`Digest`] trait represents the interface for providing inputs +//! to these digest functions and accessing the resulting hash. The +//! [`DigestWriter`] struct provides a wrapper around [`Digest`] that +//! implements the [`Write`] trait, for use in situations where calling +//! [`write`] would be useful. extern crate digest; extern crate md5; extern crate sha1; extern crate sha2; extern crate sha3; +use std::io::Write; + use hex::ToHex; +#[cfg(windows)] +use memchr::memmem; use crate::digest::digest::{ExtendableOutput, Input, XofReader}; @@ -158,3 +170,76 @@ impl_digest_sha!(sha3::Sha3_384, 384); impl_digest_sha!(sha3::Sha3_512, 512); impl_digest_shake!(sha3::Shake128); impl_digest_shake!(sha3::Shake256); + +/// A struct that writes to a digest. +/// +/// This struct wraps a [`Digest`] and provides a [`Write`] +/// implementation that passes input bytes directly to the +/// [`Digest::input`]. +/// +/// On Windows, if `binary` is `false`, then the [`write`] +/// implementation replaces instances of "\r\n" with "\n" before passing +/// the input bytes to the [`digest`]. +pub struct DigestWriter<'a> { + digest: &'a mut Box, + + /// Whether to write to the digest in binary mode or text mode on Windows. + /// + /// If this is `false`, then instances of "\r\n" are replaced with + /// "\n" before passing input bytes to the [`digest`]. + #[allow(dead_code)] + binary: bool, + // TODO This is dead code only on non-Windows operating systems. It + // might be better to use a `#[cfg(windows)]` guard here. +} + +impl<'a> DigestWriter<'a> { + pub fn new(digest: &'a mut Box, binary: bool) -> DigestWriter { + DigestWriter { digest, binary } + } +} + +impl<'a> Write for DigestWriter<'a> { + #[cfg(not(windows))] + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.digest.input(buf); + Ok(buf.len()) + } + + #[cfg(windows)] + fn write(&mut self, buf: &[u8]) -> std::io::Result { + if self.binary { + self.digest.input(buf); + return Ok(buf.len()); + } + + // In Windows text mode, replace each occurrence of "\r\n" + // with "\n". + // + // Find all occurrences of "\r\n", inputting the slice just + // before the "\n" in the previous instance of "\r\n" and + // the beginning of this "\r\n". + // + // FIXME This fails if one call to `write()` ends with the + // "\r" and the next call to `write()` begins with the "\n". + let n = buf.len(); + let mut i_prev = 0; + for i in memmem::find_iter(buf, b"\r\n") { + self.digest.input(&buf[i_prev..i]); + i_prev = i + 1; + } + self.digest.input(&buf[i_prev..n]); + + // Even though we dropped a "\r" for each "\r\n" we found, we + // still report the number of bytes written as `n`. This is + // because the meaning of the returned number is supposed to be + // the number of bytes consumed by the writer, so that if the + // calling code were calling `write()` in a loop, it would know + // where the next contiguous slice of the buffer starts. + Ok(n) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} diff --git a/src/uu/hashsum/src/hashsum.rs b/src/uu/hashsum/src/hashsum.rs index d9feb6648..4186043f5 100644 --- a/src/uu/hashsum/src/hashsum.rs +++ b/src/uu/hashsum/src/hashsum.rs @@ -18,6 +18,7 @@ extern crate uucore; mod digest; use self::digest::Digest; +use self::digest::DigestWriter; use clap::{App, Arg, ArgMatches}; use hex::ToHex; @@ -33,6 +34,7 @@ use std::io::{self, stdin, BufRead, BufReader, Read}; use std::iter; use std::num::ParseIntError; use std::path::Path; +use uucore::display::Quotable; const NAME: &str = "hashsum"; @@ -342,7 +344,7 @@ pub fn uu_app_common() -> App<'static, 'static> { const TEXT_HELP: &str = "read in text mode"; #[cfg(not(windows))] const TEXT_HELP: &str = "read in text mode (default)"; - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about("Compute and check message digests.") .arg( @@ -468,25 +470,42 @@ where stdin_buf = stdin(); Box::new(stdin_buf) as Box } else { - file_buf = safe_unwrap!(File::open(filename)); + file_buf = crash_if_err!(1, File::open(filename)); Box::new(file_buf) as Box }); if options.check { // Set up Regexes for line validation and parsing + // + // First, we compute the number of bytes we expect to be in + // the digest string. If the algorithm has a variable number + // of output bits, then we use the `+` modifier in the + // regular expression, otherwise we use the `{n}` modifier, + // where `n` is the number of bytes. let bytes = options.digest.output_bits() / 4; - let gnu_re = safe_unwrap!(Regex::new(&format!( - r"^(?P[a-fA-F0-9]{{{}}}) (?P[ \*])(?P.*)", - bytes - ))); - let bsd_re = safe_unwrap!(Regex::new(&format!( - r"^{algorithm} \((?P.*)\) = (?P[a-fA-F0-9]{{{digest_size}}})", - algorithm = options.algoname, - digest_size = bytes - ))); + let modifier = if bytes > 0 { + format!("{{{}}}", bytes) + } else { + "+".to_string() + }; + let gnu_re = crash_if_err!( + 1, + Regex::new(&format!( + r"^(?P[a-fA-F0-9]{}) (?P[ \*])(?P.*)", + modifier, + )) + ); + let bsd_re = crash_if_err!( + 1, + Regex::new(&format!( + r"^{algorithm} \((?P.*)\) = (?P[a-fA-F0-9]{digest_size})", + algorithm = options.algoname, + digest_size = modifier, + )) + ); let buffer = file; for (i, line) in buffer.lines().enumerate() { - let line = safe_unwrap!(line); + let line = crash_if_err!(1, line); let (ck_filename, sum, binary_check) = match gnu_re.captures(&line) { Some(caps) => ( caps.name("fileName").unwrap().as_str(), @@ -507,7 +526,7 @@ where if options.warn { show_warning!( "{}: {}: improperly formatted {} checksum line", - filename.display(), + filename.maybe_quote(), i + 1, options.algoname ); @@ -516,15 +535,27 @@ where } }, }; - let f = safe_unwrap!(File::open(ck_filename)); + let f = crash_if_err!(1, File::open(ck_filename)); let mut ckf = BufReader::new(Box::new(f) as Box); - let real_sum = safe_unwrap!(digest_reader( - &mut *options.digest, - &mut ckf, - binary_check, - options.output_bits - )) + let real_sum = crash_if_err!( + 1, + digest_reader( + &mut options.digest, + &mut ckf, + binary_check, + options.output_bits + ) + ) .to_ascii_lowercase(); + // FIXME: Filenames with newlines should be treated specially. + // GNU appears to replace newlines by \n and backslashes by + // \\ and prepend a backslash (to the hash or filename) if it did + // this escaping. + // Different sorts of output (checking vs outputting hashes) may + // handle this differently. Compare carefully to GNU. + // If you can, try to preserve invalid unicode using OsStr(ing)Ext + // and display it using uucore::display::print_verbatim(). This is + // easier (and more important) on Unix than on Windows. if sum == real_sum { if !options.quiet { println!("{}: OK", ck_filename); @@ -537,12 +568,15 @@ where } } } else { - let sum = safe_unwrap!(digest_reader( - &mut *options.digest, - &mut file, - options.binary, - options.output_bits - )); + let sum = crash_if_err!( + 1, + digest_reader( + &mut options.digest, + &mut file, + options.binary, + options.output_bits + ) + ); if options.tag { println!("{} ({}) = {}", options.algoname, filename.display(), sum); } else { @@ -564,55 +598,21 @@ where Ok(()) } -fn digest_reader<'a, T: Read>( - digest: &mut (dyn Digest + 'a), +fn digest_reader( + digest: &mut Box, reader: &mut BufReader, binary: bool, output_bits: usize, ) -> io::Result { digest.reset(); - // Digest file, do not hold too much in memory at any given moment - let windows = cfg!(windows); - let mut buffer = Vec::with_capacity(524_288); - let mut vec = Vec::with_capacity(524_288); - let mut looking_for_newline = false; - loop { - match reader.read_to_end(&mut buffer) { - Ok(0) => { - break; - } - Ok(nread) => { - if windows && !binary { - // Windows text mode returns '\n' when reading '\r\n' - for &b in buffer.iter().take(nread) { - if looking_for_newline { - if b != b'\n' { - vec.push(b'\r'); - } - if b != b'\r' { - vec.push(b); - looking_for_newline = false; - } - } else if b != b'\r' { - vec.push(b); - } else { - looking_for_newline = true; - } - } - digest.input(&vec); - vec.clear(); - } else { - digest.input(&buffer[..nread]); - } - } - Err(e) => return Err(e), - } - } - if windows && looking_for_newline { - vec.push(b'\r'); - digest.input(&vec); - } + // Read bytes from `reader` and write those bytes to `digest`. + // + // If `binary` is `false` and the operating system is Windows, then + // `DigestWriter` replaces "\r\n" with "\n" before it writes the + // bytes into `digest`. Otherwise, it just inserts the bytes as-is. + let mut digest_writer = DigestWriter::new(digest, binary); + std::io::copy(reader, &mut digest_writer)?; if digest.output_bits() > 0 { Ok(digest.result_str()) diff --git a/src/uu/head/Cargo.toml b/src/uu/head/Cargo.toml index 1019ac74f..4fa4c0c81 100644 --- a/src/uu/head/Cargo.toml +++ b/src/uu/head/Cargo.toml @@ -22,3 +22,7 @@ uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_p [[bin]] name = "head" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/head/src/head.rs b/src/uu/head/src/head.rs index e17e17034..ead734088 100644 --- a/src/uu/head/src/head.rs +++ b/src/uu/head/src/head.rs @@ -9,7 +9,8 @@ use clap::{crate_version, App, Arg}; use std::convert::TryFrom; use std::ffi::OsString; use std::io::{self, ErrorKind, Read, Seek, SeekFrom, Write}; -use uucore::{crash, executable, show_error, show_error_custom_description}; +use uucore::display::Quotable; +use uucore::{crash, show_error_custom_description}; const EXIT_FAILURE: i32 = 1; const EXIT_SUCCESS: i32 = 0; @@ -41,7 +42,7 @@ use lines::zlines; use take::take_all_but; pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .usage(USAGE) @@ -127,10 +128,10 @@ fn arg_iterate<'a>( match parse::parse_obsolete(s) { Some(Ok(iter)) => Ok(Box::new(vec![first].into_iter().chain(iter).chain(args))), Some(Err(e)) => match e { - parse::ParseError::Syntax => Err(format!("bad argument format: '{}'", s)), + parse::ParseError::Syntax => Err(format!("bad argument format: {}", s.quote())), parse::ParseError::Overflow => Err(format!( - "invalid argument: '{}' Value too large for defined datatype", - s + "invalid argument: {} Value too large for defined datatype", + s.quote() )), }, None => Ok(Box::new(vec![first, second].into_iter().chain(args))), @@ -418,7 +419,7 @@ fn uu_head(options: &HeadOptions) -> Result<(), u32> { let mut file = match std::fs::File::open(name) { Ok(f) => f, Err(err) => { - let prefix = format!("cannot open '{}' for reading", name); + let prefix = format!("cannot open {} for reading", name.quote()); match err.kind() { ErrorKind::NotFound => { show_error_custom_description!(prefix, "No such file or directory"); @@ -483,7 +484,7 @@ mod tests { fn options(args: &str) -> Result { let combined = "head ".to_owned() + args; let args = combined.split_whitespace(); - HeadOptions::get_from(args.map(|s| OsString::from(s))) + HeadOptions::get_from(args.map(OsString::from)) } #[test] fn test_args_modes() { @@ -522,6 +523,7 @@ mod tests { assert!(options("-c IsThisJustFantasy").is_err()); } #[test] + #[allow(clippy::bool_comparison)] fn test_options_correct_defaults() { let opts = HeadOptions::new(); let opts2: HeadOptions = Default::default(); @@ -552,7 +554,7 @@ mod tests { assert!(parse_mode("1T", Modes::Bytes).is_err()); } fn arg_outputs(src: &str) -> Result { - let split = src.split_whitespace().map(|x| OsString::from(x)); + let split = src.split_whitespace().map(OsString::from); match arg_iterate(split) { Ok(args) => { let vec = args diff --git a/src/uu/hostid/src/hostid.rs b/src/uu/hostid/src/hostid.rs index b0f68968d..4c9cafa35 100644 --- a/src/uu/hostid/src/hostid.rs +++ b/src/uu/hostid/src/hostid.rs @@ -29,7 +29,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .usage(SYNTAX) } diff --git a/src/uu/hostname/src/hostname.rs b/src/uu/hostname/src/hostname.rs index 045e43045..2de6627e8 100644 --- a/src/uu/hostname/src/hostname.rs +++ b/src/uu/hostname/src/hostname.rs @@ -10,18 +10,13 @@ #[macro_use] extern crate uucore; -use clap::{crate_version, App, Arg, ArgMatches}; use std::collections::hash_set::HashSet; use std::net::ToSocketAddrs; use std::str; -#[cfg(windows)] -use uucore::error::UUsageError; -use uucore::error::{UResult, USimpleError}; -#[cfg(windows)] -use winapi::shared::minwindef::MAKEWORD; -#[cfg(windows)] -use winapi::um::winsock2::{WSACleanup, WSAStartup}; +use clap::{crate_version, App, Arg, ArgMatches}; + +use uucore::error::{FromIo, UResult}; static ABOUT: &str = "Display or set the system's host name."; @@ -31,113 +26,125 @@ static OPT_FQDN: &str = "fqdn"; static OPT_SHORT: &str = "short"; static OPT_HOST: &str = "host"; -#[uucore_procs::gen_uumain] -pub fn uumain(args: impl uucore::Args) -> UResult<()> { - #![allow(clippy::let_and_return)] - #[cfg(windows)] - unsafe { - #[allow(deprecated)] - let mut data = std::mem::uninitialized(); - if WSAStartup(MAKEWORD(2, 2), &mut data as *mut _) != 0 { - return Err(UUsageError::new( - 1, - "Failed to start Winsock 2.2".to_string(), - )); +#[cfg(windows)] +mod wsa { + use std::io; + + use winapi::shared::minwindef::MAKEWORD; + use winapi::um::winsock2::{WSACleanup, WSAStartup, WSADATA}; + + pub(super) struct WsaHandle(()); + + pub(super) fn start() -> io::Result { + let err = unsafe { + let mut data = std::mem::MaybeUninit::::uninit(); + WSAStartup(MAKEWORD(2, 2), data.as_mut_ptr()) + }; + if err != 0 { + Err(io::Error::from_raw_os_error(err)) + } else { + Ok(WsaHandle(())) } } - let result = execute(args); - #[cfg(windows)] - unsafe { - WSACleanup(); - } - result -} -fn get_usage() -> String { - format!("{0} [OPTION]... [HOSTNAME]", executable!()) -} -fn execute(args: impl uucore::Args) -> UResult<()> { - let usage = get_usage(); - 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) { - return Err(USimpleError::new(1, format!("{}", err))); - } else { - Ok(()) + impl Drop for WsaHandle { + fn drop(&mut self) { + unsafe { + // This possibly returns an error but we can't handle it + let _err = WSACleanup(); } } } } +fn usage() -> String { + format!("{0} [OPTION]... [HOSTNAME]", uucore::execution_phrase()) +} + +#[uucore_procs::gen_uumain] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let usage = usage(); + let matches = uu_app().usage(&usage[..]).get_matches_from(args); + + #[cfg(windows)] + let _handle = wsa::start().map_err_context(|| "failed to start Winsock".to_owned())?; + + match matches.value_of_os(OPT_HOST) { + None => display_hostname(&matches), + Some(host) => hostname::set(host).map_err_context(|| "failed to set hostname".to_owned()), + } +} + pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .arg( Arg::with_name(OPT_DOMAIN) .short("d") .long("domain") + .overrides_with_all(&[OPT_DOMAIN, OPT_IP_ADDRESS, OPT_FQDN, OPT_SHORT]) .help("Display the name of the DNS domain if possible"), ) .arg( Arg::with_name(OPT_IP_ADDRESS) .short("i") .long("ip-address") + .overrides_with_all(&[OPT_DOMAIN, OPT_IP_ADDRESS, OPT_FQDN, OPT_SHORT]) .help("Display the network address(es) of the host"), ) - // TODO: support --long .arg( Arg::with_name(OPT_FQDN) .short("f") .long("fqdn") + .overrides_with_all(&[OPT_DOMAIN, OPT_IP_ADDRESS, OPT_FQDN, OPT_SHORT]) .help("Display the FQDN (Fully Qualified Domain Name) (default)"), ) - .arg(Arg::with_name(OPT_SHORT).short("s").long("short").help( - "Display the short hostname (the portion before the first dot) if \ - possible", - )) + .arg( + Arg::with_name(OPT_SHORT) + .short("s") + .long("short") + .overrides_with_all(&[OPT_DOMAIN, OPT_IP_ADDRESS, OPT_FQDN, OPT_SHORT]) + .help("Display the short hostname (the portion before the first dot) if possible"), + ) .arg(Arg::with_name(OPT_HOST)) } fn display_hostname(matches: &ArgMatches) -> UResult<()> { - let hostname = hostname::get().unwrap().into_string().unwrap(); + let hostname = hostname::get() + .map_err_context(|| "failed to get hostname".to_owned())? + .to_string_lossy() + .into_owned(); if matches.is_present(OPT_IP_ADDRESS) { // XXX: to_socket_addrs needs hostname:port so append a dummy port and remove it later. // This was originally supposed to use std::net::lookup_host, but that seems to be // deprecated. Perhaps we should use the dns-lookup crate? let hostname = hostname + ":1"; - match hostname.to_socket_addrs() { - Ok(addresses) => { - let mut hashset = HashSet::new(); - let mut output = String::new(); - for addr in addresses { - // XXX: not sure why this is necessary... - if !hashset.contains(&addr) { - let mut ip = format!("{}", addr); - if ip.ends_with(":1") { - let len = ip.len(); - ip.truncate(len - 2); - } - output.push_str(&ip); - output.push(' '); - hashset.insert(addr); - } + let addresses = hostname + .to_socket_addrs() + .map_err_context(|| "failed to resolve socket addresses".to_owned())?; + let mut hashset = HashSet::new(); + let mut output = String::new(); + for addr in addresses { + // XXX: not sure why this is necessary... + if !hashset.contains(&addr) { + let mut ip = addr.to_string(); + if ip.ends_with(":1") { + let len = ip.len(); + ip.truncate(len - 2); } - let len = output.len(); - if len > 0 { - println!("{}", &output[0..len - 1]); - } - - Ok(()) - } - Err(f) => { - return Err(USimpleError::new(1, format!("{}", f))); + output.push_str(&ip); + output.push(' '); + hashset.insert(addr); } } + let len = output.len(); + if len > 0 { + println!("{}", &output[0..len - 1]); + } + + Ok(()) } else { if matches.is_present(OPT_SHORT) || matches.is_present(OPT_DOMAIN) { let mut it = hostname.char_indices().filter(|&ci| ci.1 == '.'); diff --git a/src/uu/id/src/id.rs b/src/uu/id/src/id.rs index 16c665273..1229b577e 100644 --- a/src/uu/id/src/id.rs +++ b/src/uu/id/src/id.rs @@ -41,6 +41,7 @@ extern crate uucore; use clap::{crate_version, App, Arg}; use std::ffi::CStr; +use uucore::display::Quotable; use uucore::entries::{self, Group, Locate, Passwd}; use uucore::error::UResult; use uucore::error::{set_exit_code, USimpleError}; @@ -76,8 +77,8 @@ mod options { pub const ARG_USERS: &str = "USER"; } -fn get_usage() -> String { - format!("{0} [OPTION]... [USER]...", executable!()) +fn usage() -> String { + format!("{0} [OPTION]... [USER]...", uucore::execution_phrase()) } fn get_description() -> String { @@ -127,7 +128,7 @@ struct State { #[uucore_procs::gen_uumain] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let usage = get_usage(); + let usage = usage(); let after_help = get_description(); let matches = uu_app() @@ -230,7 +231,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { 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].quote()); set_exit_code(1); if i + 1 >= users.len() { break; @@ -347,7 +348,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .arg( diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index cbbe9c18b..1c09f7f34 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -16,9 +16,11 @@ 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::display::Quotable; use uucore::entries::{grp2gid, usr2uid}; -use uucore::error::{FromIo, UError, UIoError, UResult, USimpleError}; -use uucore::perms::{wrap_chgrp, wrap_chown, Verbosity}; +use uucore::error::{FromIo, UError, UIoError, UResult}; +use uucore::mode::get_umask; +use uucore::perms::{wrap_chown, Verbosity, VerbosityLevel}; use libc::{getegid, geteuid}; use std::error::Error; @@ -86,46 +88,38 @@ impl Display for InstallError { use InstallError as IE; match self { IE::Unimplemented(opt) => write!(f, "Unimplemented feature: {}", opt), - IE::DirNeedsArg() => write!( - f, - "{} with -d requires at least one argument.", - executable!() - ), - IE::CreateDirFailed(dir, e) => { - Display::fmt(&uio_error!(e, "failed to create {}", dir.display()), f) + IE::DirNeedsArg() => { + write!( + f, + "{} with -d requires at least one argument.", + uucore::util_name() + ) } - IE::ChmodFailed(file) => write!(f, "failed to chmod {}", file.display()), + IE::CreateDirFailed(dir, e) => { + Display::fmt(&uio_error!(e, "failed to create {}", dir.quote()), f) + } + IE::ChmodFailed(file) => write!(f, "failed to chmod {}", file.quote()), IE::InvalidTarget(target) => write!( f, "invalid target {}: No such file or directory", - target.display() + target.quote() ), IE::TargetDirIsntDir(target) => { - write!(f, "target '{}' is not a directory", target.display()) + write!(f, "target {} is not a directory", target.quote()) } IE::BackupFailed(from, to, e) => Display::fmt( - &uio_error!( - e, - "cannot backup '{}' to '{}'", - from.display(), - to.display() - ), + &uio_error!(e, "cannot backup {} to {}", from.quote(), to.quote()), f, ), IE::InstallFailed(from, to, e) => Display::fmt( - &uio_error!( - e, - "cannot install '{}' to '{}'", - from.display(), - to.display() - ), + &uio_error!(e, "cannot install {} to {}", from.quote(), to.quote()), f, ), IE::StripProgramFailed(msg) => write!(f, "strip program failed: {}", msg), IE::MetadataFailed(e) => Display::fmt(&uio_error!(e, ""), f), - IE::NoSuchUser(user) => write!(f, "no such user: {}", user), - IE::NoSuchGroup(group) => write!(f, "no such group: {}", group), - IE::OmittingDirectory(dir) => write!(f, "omitting directory '{}'", dir.display()), + IE::NoSuchUser(user) => write!(f, "no such user: {}", user.maybe_quote()), + IE::NoSuchGroup(group) => write!(f, "no such group: {}", group.maybe_quote()), + IE::OmittingDirectory(dir) => write!(f, "omitting directory {}", dir.quote()), } } } @@ -152,8 +146,6 @@ static ABOUT: &str = "Copy SOURCE to DEST or multiple SOURCE(s) to the existing DIRECTORY, while setting permission modes and owner/group"; static OPT_COMPARE: &str = "compare"; -static OPT_BACKUP: &str = "backup"; -static OPT_BACKUP_NO_ARG: &str = "backup2"; static OPT_DIRECTORY: &str = "directory"; static OPT_IGNORED: &str = "ignored"; static OPT_CREATE_LEADING: &str = "create-leading"; @@ -163,7 +155,6 @@ static OPT_OWNER: &str = "owner"; static OPT_PRESERVE_TIMESTAMPS: &str = "preserve-timestamps"; static OPT_STRIP: &str = "strip"; static OPT_STRIP_PROGRAM: &str = "strip-program"; -static OPT_SUFFIX: &str = "suffix"; static OPT_TARGET_DIRECTORY: &str = "target-directory"; static OPT_NO_TARGET_DIRECTORY: &str = "no-target-directory"; static OPT_VERBOSE: &str = "verbose"; @@ -172,8 +163,8 @@ static OPT_CONTEXT: &str = "context"; static ARG_FILES: &str = "files"; -fn get_usage() -> String { - format!("{0} [OPTION]... [FILE]...", executable!()) +fn usage() -> String { + format!("{0} [OPTION]... [FILE]...", uucore::execution_phrase()) } /// Main install utility function, called from main.rs. @@ -182,7 +173,7 @@ fn get_usage() -> String { /// #[uucore_procs::gen_uumain] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let usage = get_usage(); + let usage = usage(); let matches = uu_app().usage(&usage[..]).get_matches_from(args); @@ -202,23 +193,14 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .arg( - Arg::with_name(OPT_BACKUP) - .long(OPT_BACKUP) - .help("make a backup of each existing destination file") - .takes_value(true) - .require_equals(true) - .min_values(0) - .value_name("CONTROL") + backup_control::arguments::backup() ) .arg( - // TODO implement flag - Arg::with_name(OPT_BACKUP_NO_ARG) - .short("b") - .help("like --backup but does not accept an argument") + backup_control::arguments::backup_no_args() ) .arg( Arg::with_name(OPT_IGNORED) @@ -276,9 +258,9 @@ pub fn uu_app() -> App<'static, 'static> { ) .arg( Arg::with_name(OPT_STRIP) - .short("s") - .long(OPT_STRIP) - .help("strip symbol tables (no action Windows)") + .short("s") + .long(OPT_STRIP) + .help("strip symbol tables (no action Windows)") ) .arg( Arg::with_name(OPT_STRIP_PROGRAM) @@ -287,14 +269,7 @@ pub fn uu_app() -> App<'static, 'static> { .value_name("PROGRAM") ) .arg( - // TODO implement flag - Arg::with_name(OPT_SUFFIX) - .short("S") - .long(OPT_SUFFIX) - .help("override the usual backup suffix") - .value_name("SUFFIX") - .takes_value(true) - .min_values(1) + backup_control::arguments::suffix() ) .arg( // TODO implement flag @@ -376,7 +351,7 @@ fn behavior(matches: &ArgMatches) -> UResult { let specified_mode: Option = if matches.is_present(OPT_MODE) { let x = matches.value_of(OPT_MODE).ok_or(1)?; - Some(mode::parse(x, considering_dir).map_err(|err| { + Some(mode::parse(x, considering_dir, get_umask()).map_err(|err| { show_error!("Invalid mode string: {}", err); 1 })?) @@ -384,23 +359,14 @@ fn behavior(matches: &ArgMatches) -> UResult { None }; - 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), - ); - let backup_mode = match backup_mode { - Err(err) => return Err(USimpleError::new(1, err)), - Ok(mode) => mode, - }; - + let backup_mode = backup_control::determine_backup_mode(matches)?; let target_dir = matches.value_of(OPT_TARGET_DIRECTORY).map(|d| d.to_owned()); Ok(Behavior { main_function, specified_mode, backup_mode, - suffix: backup_control::determine_backup_suffix(matches.value_of(OPT_SUFFIX)), + suffix: backup_control::determine_backup_suffix(matches), 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), @@ -441,14 +407,14 @@ fn directory(paths: Vec, b: Behavior) -> UResult<()> { // the default mode. Hence it is safe to use fs::create_dir_all // and then only modify the target's dir mode. if let Err(e) = - fs::create_dir_all(path).map_err_context(|| format!("{}", path.display())) + fs::create_dir_all(path).map_err_context(|| path.maybe_quote().to_string()) { show!(e); continue; } if b.verbose { - println!("creating directory '{}'", path.display()); + println!("creating directory {}", path.quote()); } } @@ -470,7 +436,7 @@ fn directory(paths: Vec, b: Behavior) -> UResult<()> { fn is_new_file_path(path: &Path) -> bool { !path.exists() && (path.parent().map(Path::is_dir).unwrap_or(true) - || path.parent().unwrap().to_string_lossy().is_empty()) // In case of a simple file + || path.parent().unwrap().as_os_str().is_empty()) // In case of a simple file } /// Perform an install, given a list of paths and behavior. @@ -524,11 +490,10 @@ fn copy_files_into_dir(files: &[PathBuf], target_dir: &Path, b: &Behavior) -> UR return Err(InstallError::TargetDirIsntDir(target_dir.to_path_buf()).into()); } for sourcepath in files.iter() { - if !sourcepath.exists() { - let err = UIoError::new( - std::io::ErrorKind::NotFound, - format!("cannot stat '{}'", sourcepath.display()), - ); + if let Err(err) = sourcepath + .metadata() + .map_err_context(|| format!("cannot stat {}", sourcepath.quote())) + { show!(err); continue; } @@ -591,7 +556,7 @@ fn copy(from: &Path, to: &Path, b: &Behavior) -> UResult<()> { } } - if from.to_string_lossy() == "/dev/null" { + if from.as_os_str() == "/dev/null" { /* workaround a limitation of fs::copy * https://github.com/rust-lang/rust/issues/79390 */ @@ -639,7 +604,10 @@ fn copy(from: &Path, to: &Path, b: &Behavior) -> UResult<()> { Some(owner_id), Some(gid), false, - Verbosity::Normal, + Verbosity { + groups_only: false, + level: VerbosityLevel::Normal, + }, ) { Ok(n) => { if !n.is_empty() { @@ -660,7 +628,17 @@ fn copy(from: &Path, to: &Path, b: &Behavior) -> UResult<()> { Ok(g) => g, _ => return Err(InstallError::NoSuchGroup(b.group.clone()).into()), }; - match wrap_chgrp(to, &meta, Some(group_id), false, Verbosity::Normal) { + match wrap_chown( + to, + &meta, + Some(group_id), + None, + false, + Verbosity { + groups_only: true, + level: VerbosityLevel::Normal, + }, + ) { Ok(n) => { if !n.is_empty() { show_error!("{}", n); @@ -686,9 +664,9 @@ fn copy(from: &Path, to: &Path, b: &Behavior) -> UResult<()> { } if b.verbose { - print!("'{}' -> '{}'", from.display(), to.display()); + print!("{} -> {}", from.quote(), to.quote()); match backup_path { - Some(path) => println!(" (backup: '{}')", path.display()), + Some(path) => println!(" (backup: {})", path.quote()), None => println!(), } } diff --git a/src/uu/install/src/mode.rs b/src/uu/install/src/mode.rs index b8d5cd839..310e1fb43 100644 --- a/src/uu/install/src/mode.rs +++ b/src/uu/install/src/mode.rs @@ -4,14 +4,14 @@ use std::path::Path; use uucore::mode; /// Takes a user-supplied string and tries to parse to u16 mode bitmask. -pub fn parse(mode_string: &str, considering_dir: bool) -> Result { +pub fn parse(mode_string: &str, considering_dir: bool, umask: u32) -> Result { let numbers: &[char] = &['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; // Passing 000 as the existing permissions seems to mirror GNU behavior. if mode_string.contains(numbers) { - mode::parse_numeric(0, mode_string) + mode::parse_numeric(0, mode_string, considering_dir) } else { - mode::parse_symbolic(0, mode_string, considering_dir) + mode::parse_symbolic(0, mode_string, umask, considering_dir) } } @@ -22,8 +22,9 @@ pub fn parse(mode_string: &str, considering_dir: bool) -> Result { #[cfg(any(unix, target_os = "redox"))] pub fn chmod(path: &Path, mode: u32) -> Result<(), ()> { use std::os::unix::fs::PermissionsExt; + use uucore::display::Quotable; fs::set_permissions(path, fs::Permissions::from_mode(mode)).map_err(|err| { - show_error!("{}: chmod failed with error {}", path.display(), err); + show_error!("{}: chmod failed with error {}", path.maybe_quote(), err); }) } diff --git a/src/uu/join/Cargo.toml b/src/uu/join/Cargo.toml index f108d5a4e..7e5ced498 100644 --- a/src/uu/join/Cargo.toml +++ b/src/uu/join/Cargo.toml @@ -22,3 +22,7 @@ uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_p [[bin]] name = "join" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/join/src/join.rs b/src/uu/join/src/join.rs index d108a08ef..51dfeec6f 100644 --- a/src/uu/join/src/join.rs +++ b/src/uu/join/src/join.rs @@ -11,9 +11,10 @@ extern crate uucore; use clap::{crate_version, App, Arg}; -use std::cmp::{min, Ordering}; +use std::cmp::Ordering; use std::fs::File; use std::io::{stdin, BufRead, BufReader, Lines, Stdin}; +use uucore::display::Quotable; static NAME: &str = "join"; @@ -102,17 +103,12 @@ impl<'a> Repr<'a> { } /// Print each field except the one at the index. - fn print_fields(&self, line: &Line, index: usize, max_fields: Option) { - for i in 0..min(max_fields.unwrap_or(usize::max_value()), line.fields.len()) { + fn print_fields(&self, line: &Line, index: usize) { + for i in 0..line.fields.len() { if i != index { print!("{}{}", self.separator, line.fields[i]); } } - if let Some(n) = max_fields { - for _ in line.fields.len()..n { - print!("{}", self.separator) - } - } } /// Print each field or the empty filler if the field is not set. @@ -186,18 +182,18 @@ impl Spec { return Spec::Key; } - crash!(1, "invalid field specifier: '{}'", format); + crash!(1, "invalid field specifier: {}", format.quote()); } Some('1') => FileNum::File1, Some('2') => FileNum::File2, - _ => crash!(1, "invalid file number in field spec: '{}'", format), + _ => crash!(1, "invalid file number in field spec: {}", format.quote()), }; if let Some('.') = chars.next() { return Spec::Field(file_num, parse_field_number(chars.as_str())); } - crash!(1, "invalid field specifier: '{}'", format); + crash!(1, "invalid field specifier: {}", format.quote()); } } @@ -233,7 +229,6 @@ struct State<'a> { print_unpaired: bool, lines: Lines>, seq: Vec, - max_fields: Option, line_num: usize, has_failed: bool, } @@ -251,7 +246,7 @@ impl<'a> State<'a> { } else { match File::open(name) { Ok(file) => Box::new(BufReader::new(file)) as Box, - Err(err) => crash!(1, "{}: {}", name, err), + Err(err) => crash!(1, "{}: {}", name.maybe_quote(), err), } }; @@ -262,7 +257,6 @@ impl<'a> State<'a> { print_unpaired, lines: f.lines(), seq: Vec::new(), - max_fields: None, line_num: 0, has_failed: false, } @@ -329,8 +323,8 @@ impl<'a> State<'a> { }); } else { repr.print_field(key); - repr.print_fields(line1, self.key, self.max_fields); - repr.print_fields(line2, other.key, other.max_fields); + repr.print_fields(line1, self.key); + repr.print_fields(line2, other.key); } println!(); @@ -361,14 +355,15 @@ impl<'a> State<'a> { !self.seq.is_empty() } - fn initialize(&mut self, read_sep: Sep, autoformat: bool) { + fn initialize(&mut self, read_sep: Sep, autoformat: bool) -> usize { if let Some(line) = self.read_line(read_sep) { - if autoformat { - self.max_fields = Some(line.fields.len()); - } - self.seq.push(line); + + if autoformat { + return self.seq[0].fields.len(); + } } + 0 } fn finalize(&mut self, input: &Input, repr: &Repr) { @@ -399,7 +394,11 @@ impl<'a> State<'a> { let diff = input.compare(self.get_current_key(), line.get_field(self.key)); if diff == Ordering::Greater { - eprintln!("{}:{}: is not sorted", self.file_name, self.line_num); + eprintln!( + "{}:{}: is not sorted", + self.file_name.maybe_quote(), + self.line_num + ); // This is fatal if the check is enabled. if input.check_order == CheckOrder::Enabled { @@ -431,7 +430,7 @@ impl<'a> State<'a> { }); } else { repr.print_field(line.get_field(self.key)); - repr.print_fields(line, self.key, self.max_fields); + repr.print_fields(line, self.key); } println!(); @@ -512,7 +511,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { crash!(1, "both files cannot be standard input"); } - exec(file1, file2, &settings) + exec(file1, file2, settings) } pub fn uu_app() -> App<'static, 'static> { @@ -622,7 +621,7 @@ FILENUM is 1 or 2, corresponding to FILE1 or FILE2", ) } -fn exec(file1: &str, file2: &str, settings: &Settings) -> i32 { +fn exec(file1: &str, file2: &str, settings: Settings) -> i32 { let stdin = stdin(); let mut state1 = State::new( @@ -647,18 +646,34 @@ fn exec(file1: &str, file2: &str, settings: &Settings) -> i32 { settings.check_order, ); + let format = if settings.autoformat { + let mut format = vec![Spec::Key]; + let mut initialize = |state: &mut State| { + let max_fields = state.initialize(settings.separator, settings.autoformat); + for i in 0..max_fields { + if i != state.key { + format.push(Spec::Field(state.file_num, i)); + } + } + }; + initialize(&mut state1); + initialize(&mut state2); + format + } else { + state1.initialize(settings.separator, settings.autoformat); + state2.initialize(settings.separator, settings.autoformat); + settings.format + }; + let repr = Repr::new( match settings.separator { Sep::Char(sep) => sep, _ => ' ', }, - &settings.format, + &format, &settings.empty, ); - state1.initialize(settings.separator, settings.autoformat); - state2.initialize(settings.separator, settings.autoformat); - if settings.headers { state1.print_headers(&state2, &repr); state1.reset_read_line(&input); @@ -717,7 +732,7 @@ fn get_field_number(keys: Option, key: Option) -> usize { fn parse_field_number(value: &str) -> usize { match value.parse::() { Ok(result) if result > 0 => result - 1, - _ => crash!(1, "invalid field number: '{}'", value), + _ => crash!(1, "invalid field number: {}", value.quote()), } } @@ -725,7 +740,7 @@ fn parse_file_number(value: &str) -> FileNum { match value { "1" => FileNum::File1, "2" => FileNum::File2, - value => crash!(1, "invalid file number: '{}'", value), + value => crash!(1, "invalid file number: {}", value.quote()), } } diff --git a/src/uu/kill/src/kill.rs b/src/uu/kill/src/kill.rs index b3f5010ca..f269f7283 100644 --- a/src/uu/kill/src/kill.rs +++ b/src/uu/kill/src/kill.rs @@ -13,8 +13,9 @@ extern crate uucore; use clap::{crate_version, App, Arg}; use libc::{c_int, pid_t}; use std::io::Error; +use uucore::display::Quotable; use uucore::error::{UResult, USimpleError}; -use uucore::signals::ALL_SIGNALS; +use uucore::signals::{signal_by_name_or_value, ALL_SIGNALS}; use uucore::InvalidEncodingHandling; static ABOUT: &str = "Send signal to processes or list information about signals."; @@ -36,12 +37,12 @@ pub enum Mode { #[uucore_procs::gen_uumain] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let args = args + let mut args = args .collect_str(InvalidEncodingHandling::Ignore) .accept_any(); - let (args, obs_signal) = handle_obsolete(args); + let obs_signal = handle_obsolete(&mut args); - let usage = format!("{} [OPTIONS]... PID...", executable!()); + let usage = format!("{} [OPTIONS]... PID...", uucore::execution_phrase()); let matches = uu_app().usage(&usage[..]).get_matches_from(args); let mode = if matches.is_present(options::TABLE) || matches.is_present(options::TABLE_OLD) { @@ -59,13 +60,15 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { match mode { Mode::Kill => { - let sig = match (obs_signal, matches.value_of(options::SIGNAL)) { - (Some(s), Some(_)) => s, // -s takes precedence - (Some(s), None) => s, - (None, Some(s)) => s.to_owned(), - (None, None) => "TERM".to_owned(), + let sig = if let Some(signal) = obs_signal { + signal + } else if let Some(signal) = matches.value_of(options::SIGNAL) { + parse_signal_value(signal)? + } else { + 15_usize //SIGTERM }; - kill(&sig, &pids_or_signals) + let pids = parse_pids(&pids_or_signals)?; + kill(sig, &pids) } Mode::Table => { table(); @@ -76,7 +79,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .arg( @@ -108,26 +111,22 @@ pub fn uu_app() -> App<'static, 'static> { ) } -fn handle_obsolete(mut args: Vec) -> (Vec, Option) { - let mut i = 0; - while i < args.len() { - // this is safe because slice is valid when it is referenced - let slice = &args[i].clone(); - if slice.starts_with('-') && slice.chars().nth(1).map_or(false, |c| c.is_digit(10)) { - let val = &slice[1..]; - match val.parse() { - Ok(num) => { - if uucore::signals::is_signal(num) { - args.remove(i); - return (args, Some(val.to_owned())); - } - } - Err(_) => break, /* getopts will error out for us */ +fn handle_obsolete(args: &mut Vec) -> Option { + // Sanity check + if args.len() > 2 { + // Old signal can only be in the first argument position + let slice = args[1].as_str(); + if let Some(signal) = slice.strip_prefix('-') { + // Check if it is a valid signal + let opt_signal = signal_by_name_or_value(signal); + if opt_signal.is_some() { + // remove the signal before return + args.remove(1); + return opt_signal; } } - i += 1; } - (args, None) + None } fn table() { @@ -154,7 +153,7 @@ fn print_signal(signal_name_or_value: &str) -> UResult<()> { } Err(USimpleError::new( 1, - format!("unknown signal name {}", signal_name_or_value), + format!("unknown signal name {}", signal_name_or_value.quote()), )) } @@ -183,31 +182,32 @@ fn list(arg: Option) -> UResult<()> { } } -fn kill(signalname: &str, pids: &[String]) -> UResult<()> { - let optional_signal_value = uucore::signals::signal_by_name_or_value(signalname); - let signal_value = match optional_signal_value { - Some(x) => x, - None => { - return Err(USimpleError::new( - 1, - format!("unknown signal name {}", signalname), - )); +fn parse_signal_value(signal_name: &str) -> UResult { + let optional_signal_value = signal_by_name_or_value(signal_name); + match optional_signal_value { + Some(x) => Ok(x), + None => Err(USimpleError::new( + 1, + format!("unknown signal name {}", signal_name.quote()), + )), + } +} + +fn parse_pids(pids: &[String]) -> UResult> { + pids.iter() + .map(|x| { + x.parse::().map_err(|e| { + USimpleError::new(1, format!("failed to parse argument {}: {}", x.quote(), e)) + }) + }) + .collect() +} + +fn kill(signal_value: usize, pids: &[usize]) -> UResult<()> { + for &pid in pids { + if unsafe { libc::kill(pid as pid_t, signal_value as c_int) } != 0 { + show!(USimpleError::new(1, format!("{}", Error::last_os_error()))); } - }; - for pid in pids { - match pid.parse::() { - Ok(x) => { - if unsafe { libc::kill(x as pid_t, signal_value as c_int) } != 0 { - show!(USimpleError::new(1, format!("{}", Error::last_os_error()))); - } - } - Err(e) => { - return Err(USimpleError::new( - 1, - format!("failed to parse argument {}: {}", pid, e), - )); - } - }; } Ok(()) } diff --git a/src/uu/link/Cargo.toml b/src/uu/link/Cargo.toml index 025ac7554..d37ac6761 100644 --- a/src/uu/link/Cargo.toml +++ b/src/uu/link/Cargo.toml @@ -23,3 +23,7 @@ clap = { version = "2.33", features = ["wrap_help"] } [[bin]] name = "link" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/link/src/link.rs b/src/uu/link/src/link.rs index ad7702044..73e81b107 100644 --- a/src/uu/link/src/link.rs +++ b/src/uu/link/src/link.rs @@ -19,8 +19,8 @@ pub mod options { pub static FILES: &str = "FILES"; } -fn get_usage() -> String { - format!("{0} FILE1 FILE2", executable!()) +fn usage() -> String { + format!("{0} FILE1 FILE2", uucore::execution_phrase()) } pub fn normalize_error_message(e: Error) -> String { @@ -31,7 +31,7 @@ pub fn normalize_error_message(e: Error) -> String { } pub fn uumain(args: impl uucore::Args) -> i32 { - let usage = get_usage(); + let usage = usage(); let matches = uu_app().usage(&usage[..]).get_matches_from(args); let files: Vec<_> = matches @@ -51,7 +51,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .arg( diff --git a/src/uu/ln/src/ln.rs b/src/uu/ln/src/ln.rs index 7010ff5e4..e480d8f13 100644 --- a/src/uu/ln/src/ln.rs +++ b/src/uu/ln/src/ln.rs @@ -11,11 +11,12 @@ extern crate uucore; use clap::{crate_version, App, Arg}; +use uucore::display::Quotable; use uucore::error::{UError, UResult}; use std::borrow::Cow; use std::error::Error; -use std::ffi::OsStr; +use std::ffi::{OsStr, OsString}; use std::fmt::Display; use std::fs; @@ -26,7 +27,7 @@ use std::os::unix::fs::symlink; use std::os::windows::fs::{symlink_dir, symlink_file}; use std::path::{Path, PathBuf}; use uucore::backup_control::{self, BackupMode}; -use uucore::fs::{canonicalize, CanonicalizeMode}; +use uucore::fs::{canonicalize, MissingHandling, ResolveMode}; pub struct Settings { overwrite: OverwriteMode, @@ -49,30 +50,28 @@ pub enum OverwriteMode { #[derive(Debug)] enum LnError { - TargetIsDirectory(String), + TargetIsDirectory(PathBuf), SomeLinksFailed, FailedToLink(String), - MissingDestination(String), - ExtraOperand(String), - InvalidBackupMode(String), + MissingDestination(PathBuf), + ExtraOperand(OsString), } impl Display for LnError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::TargetIsDirectory(s) => write!(f, "target '{}' is not a directory", s), - Self::FailedToLink(s) => write!(f, "failed to link '{}'", s), + Self::TargetIsDirectory(s) => write!(f, "target {} is not a directory", s.quote()), + Self::FailedToLink(e) => write!(f, "failed to link: {}", e), Self::SomeLinksFailed => write!(f, "some links failed to create"), Self::MissingDestination(s) => { - write!(f, "missing destination file operand after '{}'", s) + write!(f, "missing destination file operand after {}", s.quote()) } Self::ExtraOperand(s) => write!( f, - "extra operand '{}'\nTry '{} --help' for more information.", - s, - executable!() + "extra operand {}\nTry '{} --help' for more information.", + s.quote(), + uucore::execution_phrase() ), - Self::InvalidBackupMode(s) => write!(f, "{}", s), } } } @@ -87,22 +86,21 @@ impl UError for LnError { Self::FailedToLink(_) => 1, Self::MissingDestination(_) => 1, Self::ExtraOperand(_) => 1, - Self::InvalidBackupMode(_) => 1, } } } -fn get_usage() -> String { +fn usage() -> String { format!( "{0} [OPTION]... [-T] TARGET LINK_NAME (1st form) {0} [OPTION]... TARGET (2nd form) {0} [OPTION]... TARGET... DIRECTORY (3rd form) {0} [OPTION]... -t DIRECTORY TARGET... (4th form)", - executable!() + uucore::execution_phrase() ) } -fn get_long_usage() -> String { +fn long_usage() -> String { String::from( " In the 1st form, create a link to TARGET with the name LINK_NAME. In the 2nd form, create a link to TARGET in the current directory. @@ -119,13 +117,10 @@ fn get_long_usage() -> String { static ABOUT: &str = "change file owner and group"; mod options { - pub const BACKUP_NO_ARG: &str = "b"; - pub const BACKUP: &str = "backup"; pub const FORCE: &str = "force"; pub const INTERACTIVE: &str = "interactive"; pub const NO_DEREFERENCE: &str = "no-dereference"; pub const SYMBOLIC: &str = "symbolic"; - pub const SUFFIX: &str = "suffix"; pub const TARGET_DIRECTORY: &str = "target-directory"; pub const NO_TARGET_DIRECTORY: &str = "no-target-directory"; pub const RELATIVE: &str = "relative"; @@ -136,8 +131,8 @@ static ARG_FILES: &str = "files"; #[uucore_procs::gen_uumain] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let usage = get_usage(); - let long_usage = get_long_usage(); + let usage = usage(); + let long_usage = long_usage(); let matches = uu_app() .usage(&usage[..]) @@ -164,19 +159,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { OverwriteMode::NoClobber }; - let backup_mode = backup_control::determine_backup_mode( - matches.is_present(options::BACKUP_NO_ARG), - matches.is_present(options::BACKUP), - matches.value_of(options::BACKUP), - ); - let backup_mode = match backup_mode { - Err(err) => { - return Err(LnError::InvalidBackupMode(err).into()); - } - Ok(mode) => mode, - }; - - let backup_suffix = backup_control::determine_backup_suffix(matches.value_of(options::SUFFIX)); + let backup_mode = backup_control::determine_backup_mode(&matches)?; + let backup_suffix = backup_control::determine_backup_suffix(&matches); let settings = Settings { overwrite: overwrite_mode, @@ -196,23 +180,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) - .arg( - Arg::with_name(options::BACKUP) - .long(options::BACKUP) - .help("make a backup of each existing destination file") - .takes_value(true) - .require_equals(true) - .min_values(0) - .value_name("CONTROL"), - ) - .arg( - Arg::with_name(options::BACKUP_NO_ARG) - .short(options::BACKUP_NO_ARG) - .help("like --backup but does not accept an argument"), - ) + .arg(backup_control::arguments::backup()) + .arg(backup_control::arguments::backup_no_args()) // TODO: opts.arg( // Arg::with_name(("d", "directory", "allow users with appropriate privileges to attempt \ // to make hard links to directories"); @@ -250,14 +222,7 @@ pub fn uu_app() -> App<'static, 'static> { // override added for https://github.com/uutils/coreutils/issues/2359 .overrides_with(options::SYMBOLIC), ) - .arg( - Arg::with_name(options::SUFFIX) - .short("S") - .long(options::SUFFIX) - .help("override the usual backup suffix") - .value_name("SUFFIX") - .takes_value(true), - ) + .arg(backup_control::arguments::suffix()) .arg( Arg::with_name(options::TARGET_DIRECTORY) .short("t") @@ -315,10 +280,10 @@ fn exec(files: &[PathBuf], settings: &Settings) -> UResult<()> { // 1st form. Now there should be only two operands, but if -T is // specified we may have a wrong number of operands. if files.len() == 1 { - return Err(LnError::MissingDestination(files[0].to_string_lossy().into()).into()); + return Err(LnError::MissingDestination(files[0].clone()).into()); } if files.len() > 2 { - return Err(LnError::ExtraOperand(files[2].display().to_string()).into()); + return Err(LnError::ExtraOperand(files[2].clone().into()).into()); } assert!(!files.is_empty()); @@ -330,7 +295,7 @@ fn exec(files: &[PathBuf], settings: &Settings) -> UResult<()> { fn link_files_in_dir(files: &[PathBuf], target_dir: &Path, settings: &Settings) -> UResult<()> { if !target_dir.is_dir() { - return Err(LnError::TargetIsDirectory(target_dir.display().to_string()).into()); + return Err(LnError::TargetIsDirectory(target_dir.to_owned()).into()); } let mut all_successful = true; @@ -342,7 +307,7 @@ fn link_files_in_dir(files: &[PathBuf], target_dir: &Path, settings: &Settings) if is_symlink(target_dir) { if target_dir.is_file() { if let Err(e) = fs::remove_file(target_dir) { - show_error!("Could not update {}: {}", target_dir.display(), e) + show_error!("Could not update {}: {}", target_dir.quote(), e) }; } if target_dir.is_dir() { @@ -350,7 +315,7 @@ fn link_files_in_dir(files: &[PathBuf], target_dir: &Path, settings: &Settings) // considered as a dir // See test_ln::test_symlink_no_deref_dir if let Err(e) = fs::remove_dir(target_dir) { - show_error!("Could not update {}: {}", target_dir.display(), e) + show_error!("Could not update {}: {}", target_dir.quote(), e) }; } } @@ -368,10 +333,7 @@ fn link_files_in_dir(files: &[PathBuf], target_dir: &Path, settings: &Settings) } } None => { - show_error!( - "cannot stat '{}': No such file or directory", - srcpath.display() - ); + show_error!("cannot stat {}: No such file or directory", srcpath.quote()); all_successful = false; continue; } @@ -380,9 +342,9 @@ fn link_files_in_dir(files: &[PathBuf], target_dir: &Path, settings: &Settings) if let Err(e) = link(srcpath, &targetpath, settings) { show_error!( - "cannot link '{}' to '{}': {}", - targetpath.display(), - srcpath.display(), + "cannot link {} to {}: {}", + targetpath.quote(), + srcpath.quote(), e ); all_successful = false; @@ -396,8 +358,12 @@ fn link_files_in_dir(files: &[PathBuf], target_dir: &Path, settings: &Settings) } fn relative_path<'a>(src: &Path, dst: &Path) -> Result> { - let src_abs = canonicalize(src, CanonicalizeMode::Normal)?; - let mut dst_abs = canonicalize(dst.parent().unwrap(), CanonicalizeMode::Normal)?; + let src_abs = canonicalize(src, MissingHandling::Normal, ResolveMode::Logical)?; + let mut dst_abs = canonicalize( + dst.parent().unwrap(), + MissingHandling::Normal, + ResolveMode::Logical, + )?; dst_abs.push(dst.components().last().unwrap()); let suffix_pos = src_abs .components() @@ -431,7 +397,7 @@ fn link(src: &Path, dst: &Path, settings: &Settings) -> Result<()> { match settings.overwrite { OverwriteMode::NoClobber => {} OverwriteMode::Interactive => { - print!("{}: overwrite '{}'? ", executable!(), dst.display()); + print!("{}: overwrite {}? ", uucore::util_name(), dst.quote()); if !read_yes() { return Ok(()); } @@ -458,9 +424,9 @@ fn link(src: &Path, dst: &Path, settings: &Settings) -> Result<()> { } if settings.verbose { - print!("'{}' -> '{}'", dst.display(), &source.display()); + print!("{} -> {}", dst.quote(), source.quote()); match backup_path { - Some(path) => println!(" (backup: '{}')", path.display()), + Some(path) => println!(" (backup: {})", path.quote()), None => println!(), } } diff --git a/src/uu/logname/src/logname.rs b/src/uu/logname/src/logname.rs index 4a6f43418..56866ff62 100644 --- a/src/uu/logname/src/logname.rs +++ b/src/uu/logname/src/logname.rs @@ -35,8 +35,8 @@ fn get_userlogin() -> Option { static SUMMARY: &str = "Print user's login name"; -fn get_usage() -> String { - String::from(executable!()) +fn usage() -> &'static str { + uucore::execution_phrase() } pub fn uumain(args: impl uucore::Args) -> i32 { @@ -44,8 +44,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .collect_str(InvalidEncodingHandling::Ignore) .accept_any(); - let usage = get_usage(); - let _ = uu_app().usage(&usage[..]).get_matches_from(args); + let _ = uu_app().usage(usage()).get_matches_from(args); match get_userlogin() { Some(userlogin) => println!("{}", userlogin), @@ -56,7 +55,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(SUMMARY) } diff --git a/src/uu/ls/Cargo.toml b/src/uu/ls/Cargo.toml index dbe6bacaa..e907e8e02 100644 --- a/src/uu/ls/Cargo.toml +++ b/src/uu/ls/Cargo.toml @@ -15,7 +15,6 @@ edition = "2018" path = "src/ls.rs" [dependencies] -locale = "0.2.2" chrono = "0.4.19" clap = { version = "2.33", features = ["wrap_help"] } unicode-width = "0.1.8" @@ -35,3 +34,7 @@ lazy_static = "1.4.0" [[bin]] name = "ls" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 450acf8cd..c5c65334e 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -21,6 +21,7 @@ use lscolors::LsColors; use number_prefix::NumberPrefix; use once_cell::unsync::OnceCell; use quoting_style::{escape_name, QuotingStyle}; +use std::ffi::OsString; #[cfg(windows)] use std::os::windows::fs::MetadataExt; use std::{ @@ -39,15 +40,18 @@ use std::{ time::Duration, }; use term_grid::{Cell, Direction, Filling, Grid, GridOptions}; -use uucore::error::{set_exit_code, FromIo, UError, UResult}; +use uucore::{ + display::Quotable, + error::{set_exit_code, UError, UResult}, +}; use unicode_width::UnicodeWidthStr; #[cfg(unix)] use uucore::libc::{S_IXGRP, S_IXOTH, S_IXUSR}; use uucore::{fs::display_permissions, version_cmp::version_cmp}; -fn get_usage() -> String { - format!("{0} [OPTION]... [FILE]...", executable!()) +fn usage() -> String { + format!("{0} [OPTION]... [FILE]...", uucore::execution_phrase()) } pub mod options { @@ -149,8 +153,8 @@ 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()), + LsError::InvalidLineWidth(s) => write!(f, "invalid line width: {}", s.quote()), + LsError::NoMetadata(p) => write!(f, "could not open file: {}", p.quote()), } } } @@ -248,7 +252,7 @@ struct LongFormat { impl Config { #[allow(clippy::cognitive_complexity)] - fn from(options: clap::ArgMatches) -> UResult { + fn from(options: &clap::ArgMatches) -> UResult { let (mut format, opt) = if let Some(format_) = options.value_of(options::FORMAT) { ( match format_ { @@ -409,18 +413,18 @@ impl Config { }, None => match termsize::get() { Some(size) => size.cols, - None => match std::env::var("COLUMNS") { - Ok(columns) => match columns.parse() { - Ok(columns) => columns, - Err(_) => { + None => match std::env::var_os("COLUMNS") { + Some(columns) => match columns.to_str().and_then(|s| s.parse().ok()) { + Some(columns) => columns, + None => { show_error!( - "ignoring invalid width in environment variable COLUMNS: '{}'", - columns + "ignoring invalid width in environment variable COLUMNS: {}", + columns.quote() ); DEFAULT_TERM_WIDTH } }, - Err(_) => DEFAULT_TERM_WIDTH, + None => DEFAULT_TERM_WIDTH, }, }, }; @@ -428,11 +432,10 @@ impl Config { #[allow(clippy::needless_bool)] let show_control = if options.is_present(options::HIDE_CONTROL_CHARS) { false - } else if options.is_present(options::SHOW_CONTROL_CHARS) || atty::is(atty::Stream::Stdout) - { + } else if options.is_present(options::SHOW_CONTROL_CHARS) { true } else { - false + !atty::is(atty::Stream::Stdout) }; let quoting_style = if let Some(style) = options.value_of(options::QUOTING_STYLE) { @@ -538,7 +541,7 @@ impl Config { Ok(p) => { ignore_patterns.add(p); } - Err(_) => show_warning!("Invalid pattern for ignore: '{}'", pattern), + Err(_) => show_warning!("Invalid pattern for ignore: {}", pattern.quote()), } } @@ -548,7 +551,7 @@ impl Config { Ok(p) => { ignore_patterns.add(p); } - Err(_) => show_warning!("Invalid pattern for hide: '{}'", pattern), + Err(_) => show_warning!("Invalid pattern for hide: {}", pattern.quote()), } } } @@ -599,26 +602,23 @@ impl Config { #[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 usage = usage(); let app = uu_app().usage(&usage[..]); let matches = app.get_matches_from(args); + let config = Config::from(&matches)?; let locs = matches - .values_of(options::PATHS) - .map(|v| v.map(ToString::to_string).collect()) - .unwrap_or_else(|| vec![String::from(".")]); + .values_of_os(options::PATHS) + .map(|v| v.map(Path::new).collect()) + .unwrap_or_else(|| vec![Path::new(".")]); - list(locs, Config::from(matches)?) + list(locs, config) } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about( "By default, ls will list the files and contents of any directories on \ @@ -1177,7 +1177,7 @@ struct PathData { md: OnceCell>, ft: OnceCell>, // Name of the file - will be empty for . or .. - display_name: String, + display_name: OsString, // PathBuf that all above data corresponds to p_buf: PathBuf, must_dereference: bool, @@ -1187,7 +1187,7 @@ impl PathData { fn new( p_buf: PathBuf, file_type: Option>, - file_name: Option, + file_name: Option, config: &Config, command_line: bool, ) -> Self { @@ -1195,16 +1195,13 @@ impl PathData { // For '..', the filename is None let display_name = if let Some(name) = file_name { name + } else if command_line { + p_buf.clone().into() } else { - let display_os_str = if command_line { - p_buf.as_os_str() - } else { - p_buf - .file_name() - .unwrap_or_else(|| p_buf.iter().next_back().unwrap()) - }; - - display_os_str.to_string_lossy().into_owned() + p_buf + .file_name() + .unwrap_or_else(|| p_buf.iter().next_back().unwrap()) + .to_owned() }; let must_dereference = match &config.dereference { Dereference::All => true, @@ -1249,19 +1246,24 @@ impl PathData { } } -fn list(locs: Vec, config: Config) -> UResult<()> { +fn list(locs: Vec<&Path>, config: Config) -> UResult<()> { let mut files = Vec::::new(); let mut dirs = Vec::::new(); let mut out = BufWriter::new(stdout()); for loc in &locs { - let p = PathBuf::from(&loc); + let p = PathBuf::from(loc); 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()))); + // FIXME: Would be nice to use the actual error instead of hardcoding it + // Presumably other errors can happen too? + show_error!( + "cannot access {}: No such file or directory", + path_data.p_buf.quote() + ); + set_exit_code(1); // We found an error, no need to continue the execution continue; } @@ -1286,6 +1288,7 @@ fn list(locs: Vec, config: Config) -> UResult<()> { sort_entries(&mut dirs, &config); for dir in dirs { if locs.len() > 1 || config.recursive { + // FIXME: This should use the quoting style and propagate errors let _ = writeln!(out, "\n{}:", dir.p_buf.display()); } enter_directory(&dir, &config, &mut out); @@ -1362,8 +1365,8 @@ fn enter_directory(dir: &PathData, config: &Config, out: &mut BufWriter) vec![] }; - let mut temp: Vec<_> = safe_unwrap!(fs::read_dir(&dir.p_buf)) - .map(|res| safe_unwrap!(res)) + let mut temp: Vec<_> = crash_if_err!(1, fs::read_dir(&dir.p_buf)) + .map(|res| crash_if_err!(1, res)) .filter(|e| should_display(e, config)) .map(|e| PathData::new(DirEntry::path(&e), Some(e.file_type()), None, config, false)) .collect(); @@ -1394,14 +1397,17 @@ fn get_metadata(entry: &Path, dereference: bool) -> std::io::Result { } } -fn display_dir_entry_size(entry: &PathData, config: &Config) -> (usize, usize) { +fn display_dir_entry_size(entry: &PathData, config: &Config) -> (usize, usize, usize, usize) { + // TODO: Cache/memoize the display_* results so we don't have to recalculate them. if let Some(md) = entry.md() { ( display_symlink_count(md).len(), + display_uname(md, config).len(), + display_group(md, config).len(), display_size_or_rdev(md, config).len(), ) } else { - (0, 0) + (0, 0, 0, 0) } } @@ -1409,15 +1415,28 @@ fn pad_left(string: String, count: usize) -> String { format!("{:>width$}", string, width = count) } +fn pad_right(string: String, count: usize) -> String { + format!("{:) { if config.format == Format::Long { - let (mut max_links, mut max_width) = (1, 1); + let ( + mut longest_link_count_len, + mut longest_uname_len, + mut longest_group_len, + mut longest_size_len, + ) = (1, 1, 1, 1); let mut total_size = 0; for item in items { - let (links, width) = display_dir_entry_size(item, config); - max_links = links.max(max_links); - max_width = width.max(max_width); + let (link_count_len, uname_len, group_len, size_len) = + display_dir_entry_size(item, config); + longest_link_count_len = link_count_len.max(longest_link_count_len); + longest_size_len = size_len.max(longest_size_len); + longest_uname_len = uname_len.max(longest_uname_len); + longest_group_len = group_len.max(longest_group_len); + longest_size_len = size_len.max(longest_size_len); total_size += item.md().map_or(0, |md| get_block_size(md, config)); } @@ -1426,7 +1445,15 @@ fn display_items(items: &[PathData], config: &Config, out: &mut BufWriter, ) { @@ -1557,27 +1612,39 @@ fn display_item_long( out, "{} {}", display_permissions(md, true), - pad_left(display_symlink_count(md), max_links), + pad_left(display_symlink_count(md), longest_link_count_len), ); if config.long.owner { - let _ = write!(out, " {}", display_uname(md, config)); + let _ = write!( + out, + " {}", + pad_right(display_uname(md, config), longest_uname_len) + ); } if config.long.group { - let _ = write!(out, " {}", display_group(md, config)); + let _ = write!( + out, + " {}", + pad_right(display_group(md, config), longest_group_len) + ); } // Author is only different from owner on GNU/Hurd, so we reuse // the owner, since GNU/Hurd is not currently supported by Rust. if config.long.author { - let _ = write!(out, " {}", display_uname(md, config)); + let _ = write!( + out, + " {}", + pad_right(display_uname(md, config), longest_uname_len) + ); } let _ = writeln!( out, " {} {} {}", - pad_left(display_size_or_rdev(md, config), max_size), + pad_left(display_size_or_rdev(md, config), longest_size_len), display_date(md, config), // unwrap is fine because it fails when metadata is not available // but we already know that it is because it's checked at the @@ -1597,7 +1664,6 @@ fn get_inode(metadata: &Metadata) -> String { use std::sync::Mutex; #[cfg(unix)] use uucore::entries; -use uucore::InvalidEncodingHandling; #[cfg(unix)] fn cached_uid2usr(uid: u32) -> String { @@ -1798,7 +1864,20 @@ fn classify_file(path: &PathData) -> Option { } } +/// Takes a [`PathData`] struct and returns a cell with a name ready for displaying. +/// +/// This function relies on the following parameters in the provided `&Config`: +/// * `config.quoting_style` to decide how we will escape `name` using [`escape_name`]. +/// * `config.inode` decides whether to display inode numbers beside names using [`get_inode`]. +/// * `config.color` decides whether it's going to color `name` using [`color_name`]. +/// * `config.indicator_style` to append specific characters to `name` using [`classify_file`]. +/// * `config.format` to display symlink targets if `Format::Long`. This function is also +/// responsible for coloring symlink target names if `config.color` is specified. +/// +/// Note that non-unicode sequences in symlink targets are dealt with using +/// [`std::path::Path::to_string_lossy`]. fn display_file_name(path: &PathData, config: &Config) -> Option { + // This is our return value. We start by `&path.display_name` and modify it along the way. let mut name = escape_name(&path.display_name, &config.quoting_style); #[cfg(unix)] @@ -1851,7 +1930,41 @@ fn display_file_name(path: &PathData, config: &Config) -> Option { if config.format == Format::Long && path.file_type()?.is_symlink() { if let Ok(target) = path.p_buf.read_link() { name.push_str(" -> "); - name.push_str(&target.to_string_lossy()); + + // We might as well color the symlink output after the arrow. + // This makes extra system calls, but provides important information that + // people run `ls -l --color` are very interested in. + if let Some(ls_colors) = &config.color { + // We get the absolute path to be able to construct PathData with valid Metadata. + // This is because relative symlinks will fail to get_metadata. + let mut absolute_target = target.clone(); + if target.is_relative() { + if let Some(parent) = path.p_buf.parent() { + absolute_target = parent.join(absolute_target); + } + } + + let target_data = PathData::new(absolute_target, None, None, config, false); + + // If we have a symlink to a valid file, we use the metadata of said file. + // Because we use an absolute path, we can assume this is guaranteed to exist. + // Otherwise, we use path.md(), which will guarantee we color to the same + // color of non-existent symlinks according to style_for_path_with_metadata. + let target_metadata = match target_data.md() { + Some(md) => md, + None => path.md()?, + }; + + name.push_str(&color_name( + ls_colors, + &target_data.p_buf, + target.to_string_lossy().into_owned(), + target_metadata, + )); + } else { + // If no coloring is required, we just use target as is. + name.push_str(&target.to_string_lossy()); + } } } diff --git a/src/uu/ls/src/quoting_style.rs b/src/uu/ls/src/quoting_style.rs index f9ba55f7b..149733cf9 100644 --- a/src/uu/ls/src/quoting_style.rs +++ b/src/uu/ls/src/quoting_style.rs @@ -1,6 +1,10 @@ use std::char::from_digit; +use std::ffi::OsStr; -const SPECIAL_SHELL_CHARS: &str = "~`#$&*()|[]{};\\'\"<>?! "; +// These are characters with special meaning in the shell (e.g. bash). +// The first const contains characters that only have a special meaning when they appear at the beginning of a name. +const SPECIAL_SHELL_CHARS_START: &[char] = &['~', '#']; +const SPECIAL_SHELL_CHARS: &str = "`$&*()|[]{};\\'\"<>?! "; pub(super) enum QuotingStyle { Shell { @@ -198,6 +202,8 @@ fn shell_without_escape(name: &str, quotes: Quotes, show_control_chars: bool) -> } } } + + must_quote = must_quote || name.starts_with(SPECIAL_SHELL_CHARS_START); (escaped_str, must_quote) } @@ -246,22 +252,25 @@ fn shell_with_escape(name: &str, quotes: Quotes) -> (String, bool) { } } } + must_quote = must_quote || name.starts_with(SPECIAL_SHELL_CHARS_START); (escaped_str, must_quote) } -pub(super) fn escape_name(name: &str, style: &QuotingStyle) -> String { +pub(super) fn escape_name(name: &OsStr, style: &QuotingStyle) -> String { match style { QuotingStyle::Literal { show_control } => { if !show_control { - name.chars() + name.to_string_lossy() + .chars() .flat_map(|c| EscapedChar::new_literal(c).hide_control()) .collect() } else { - name.into() + name.to_string_lossy().into_owned() } } QuotingStyle::C { quotes } => { let escaped_str: String = name + .to_string_lossy() .chars() .flat_map(|c| EscapedChar::new_c(c, *quotes)) .collect(); @@ -277,7 +286,8 @@ pub(super) fn escape_name(name: &str, style: &QuotingStyle) -> String { always_quote, show_control, } => { - let (quotes, must_quote) = if name.contains('"') { + let name = name.to_string_lossy(); + let (quotes, must_quote) = if name.contains(&['"', '`', '$', '\\'][..]) { (Quotes::Single, true) } else if name.contains('\'') { (Quotes::Double, true) @@ -288,9 +298,9 @@ pub(super) fn escape_name(name: &str, style: &QuotingStyle) -> String { }; let (escaped_str, contains_quote_chars) = if *escape { - shell_with_escape(name, quotes) + shell_with_escape(&name, quotes) } else { - shell_without_escape(name, quotes, *show_control) + shell_without_escape(&name, quotes, *show_control) }; match (must_quote | contains_quote_chars, quotes) { @@ -356,7 +366,7 @@ mod tests { fn check_names(name: &str, map: Vec<(&str, &str)>) { assert_eq!( map.iter() - .map(|(_, style)| escape_name(name, &get_style(style))) + .map(|(_, style)| escape_name(name.as_ref(), &get_style(style))) .collect::>(), map.iter() .map(|(correct, _)| correct.to_string()) @@ -659,4 +669,62 @@ mod tests { ], ); } + + #[test] + fn test_tilde_and_hash() { + check_names("~", vec![("'~'", "shell"), ("'~'", "shell-escape")]); + check_names( + "~name", + vec![("'~name'", "shell"), ("'~name'", "shell-escape")], + ); + check_names( + "some~name", + vec![("some~name", "shell"), ("some~name", "shell-escape")], + ); + check_names("name~", vec![("name~", "shell"), ("name~", "shell-escape")]); + + check_names("#", vec![("'#'", "shell"), ("'#'", "shell-escape")]); + check_names( + "#name", + vec![("'#name'", "shell"), ("'#name'", "shell-escape")], + ); + check_names( + "some#name", + vec![("some#name", "shell"), ("some#name", "shell-escape")], + ); + check_names("name#", vec![("name#", "shell"), ("name#", "shell-escape")]); + } + + #[test] + fn test_special_chars_in_double_quotes() { + check_names( + "can'$t", + vec![ + ("'can'\\''$t'", "shell"), + ("'can'\\''$t'", "shell-always"), + ("'can'\\''$t'", "shell-escape"), + ("'can'\\''$t'", "shell-escape-always"), + ], + ); + + check_names( + "can'`t", + vec![ + ("'can'\\''`t'", "shell"), + ("'can'\\''`t'", "shell-always"), + ("'can'\\''`t'", "shell-escape"), + ("'can'\\''`t'", "shell-escape-always"), + ], + ); + + check_names( + "can'\\t", + vec![ + ("'can'\\''\\t'", "shell"), + ("'can'\\''\\t'", "shell-always"), + ("'can'\\''\\t'", "shell-escape"), + ("'can'\\''\\t'", "shell-escape-always"), + ], + ); + } } diff --git a/src/uu/mkdir/src/mkdir.rs b/src/uu/mkdir/src/mkdir.rs index a99867570..92c068408 100644 --- a/src/uu/mkdir/src/mkdir.rs +++ b/src/uu/mkdir/src/mkdir.rs @@ -12,6 +12,7 @@ use clap::OsValues; use clap::{crate_version, App, Arg}; use std::fs; use std::path::Path; +use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError}; static ABOUT: &str = "Create the given DIRECTORY(ies) if they do not exist"; @@ -22,13 +23,13 @@ mod options { pub const DIRS: &str = "dirs"; } -fn get_usage() -> String { - format!("{0} [OPTION]... [USER]", executable!()) +fn usage() -> String { + format!("{0} [OPTION]... [USER]", uucore::execution_phrase()) } #[uucore_procs::gen_uumain] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let usage = get_usage(); + let usage = usage(); // Linux-specific options, not implemented // opts.optflag("Z", "context", "set SELinux security context" + @@ -43,7 +44,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // Not tested on Windows 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)))?, + .map_err(|_| USimpleError::new(1, format!("invalid mode {}", m.quote())))?, None => 0o755_u16, }; @@ -51,7 +52,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .arg( @@ -100,10 +101,14 @@ fn mkdir(path: &Path, recursive: bool, mode: u16, verbose: bool) -> UResult<()> fs::create_dir }; - create_dir(path).map_err_context(|| format!("cannot create directory '{}'", path.display()))?; + create_dir(path).map_err_context(|| format!("cannot create directory {}", path.quote()))?; if verbose { - println!("{}: created directory '{}'", executable!(), path.display()); + println!( + "{}: created directory {}", + uucore::util_name(), + path.quote() + ); } chmod(path, mode) @@ -117,7 +122,7 @@ fn chmod(path: &Path, mode: u16) -> UResult<()> { let mode = Permissions::from_mode(u32::from(mode)); set_permissions(path, mode) - .map_err_context(|| format!("cannot set permissions '{}'", path.display())) + .map_err_context(|| format!("cannot set permissions {}", path.quote())) } #[cfg(windows)] diff --git a/src/uu/mkfifo/src/mkfifo.rs b/src/uu/mkfifo/src/mkfifo.rs index ea0906567..66c3f7bb6 100644 --- a/src/uu/mkfifo/src/mkfifo.rs +++ b/src/uu/mkfifo/src/mkfifo.rs @@ -11,7 +11,7 @@ extern crate uucore; use clap::{crate_version, App, Arg}; use libc::mkfifo; use std::ffi::CString; -use uucore::InvalidEncodingHandling; +use uucore::{display::Quotable, InvalidEncodingHandling}; static NAME: &str = "mkfifo"; static USAGE: &str = "mkfifo [OPTION]... NAME..."; @@ -61,7 +61,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { mkfifo(name.as_ptr(), mode as libc::mode_t) }; if err == -1 { - show_error!("cannot create fifo '{}': File exists", f); + show_error!("cannot create fifo {}: File exists", f.quote()); exit_code = 1; } } @@ -70,7 +70,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .name(NAME) .version(crate_version!()) .usage(USAGE) diff --git a/src/uu/mknod/src/mknod.rs b/src/uu/mknod/src/mknod.rs index 8cc7db908..dd529c3ba 100644 --- a/src/uu/mknod/src/mknod.rs +++ b/src/uu/mknod/src/mknod.rs @@ -16,9 +16,9 @@ use clap::{crate_version, App, Arg, ArgMatches}; use libc::{dev_t, mode_t}; use libc::{S_IFBLK, S_IFCHR, S_IFIFO, S_IRGRP, S_IROTH, S_IRUSR, S_IWGRP, S_IWOTH, S_IWUSR}; +use uucore::display::Quotable; use uucore::InvalidEncodingHandling; -static NAME: &str = "mknod"; static ABOUT: &str = "Create the special file NAME of the given TYPE."; static USAGE: &str = "mknod [OPTION]... NAME TYPE [MAJOR MINOR]"; static LONG_HELP: &str = "Mandatory arguments to long options are mandatory for short options too. @@ -72,7 +72,8 @@ fn _mknod(file_name: &str, mode: mode_t, dev: dev_t) -> i32 { } if errno == -1 { - let c_str = CString::new(NAME).expect("Failed to convert to CString"); + let c_str = CString::new(uucore::execution_phrase().as_bytes()) + .expect("Failed to convert to CString"); // shows the error from the mknod syscall libc::perror(c_str.as_ptr()); } @@ -113,7 +114,10 @@ pub fn uumain(args: impl uucore::Args) -> i32 { if ch == 'p' { if matches.is_present("major") || matches.is_present("minor") { eprintln!("Fifos do not have major and minor device numbers."); - eprintln!("Try '{} --help' for more information.", NAME); + eprintln!( + "Try '{} --help' for more information.", + uucore::execution_phrase() + ); 1 } else { _mknod(file_name, S_IFIFO | mode, 0) @@ -122,7 +126,10 @@ pub fn uumain(args: impl uucore::Args) -> i32 { match (matches.value_of("major"), matches.value_of("minor")) { (None, None) | (_, None) | (None, _) => { eprintln!("Special files require major and minor device numbers."); - eprintln!("Try '{} --help' for more information.", NAME); + eprintln!( + "Try '{} --help' for more information.", + uucore::execution_phrase() + ); 1 } (Some(major), Some(minor)) => { @@ -145,7 +152,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .usage(USAGE) .after_help(LONG_HELP) @@ -213,7 +220,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.quote())) } }) } diff --git a/src/uu/mktemp/src/mktemp.rs b/src/uu/mktemp/src/mktemp.rs index e1dd604a0..81a3521e9 100644 --- a/src/uu/mktemp/src/mktemp.rs +++ b/src/uu/mktemp/src/mktemp.rs @@ -12,6 +12,7 @@ extern crate uucore; use clap::{crate_version, App, Arg}; +use uucore::display::{println_verbatim, Quotable}; use uucore::error::{FromIo, UError, UResult}; use std::env; @@ -36,8 +37,8 @@ static OPT_T: &str = "t"; static ARG_TEMPLATE: &str = "template"; -fn get_usage() -> String { - format!("{0} [OPTION]... [TEMPLATE]", executable!()) +fn usage() -> String { + format!("{0} [OPTION]... [TEMPLATE]", uucore::execution_phrase()) } #[derive(Debug)] @@ -57,16 +58,20 @@ 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), + PersistError(p) => write!(f, "could not persist file {}", p.quote()), + MustEndInX(s) => write!(f, "with --suffix, template {} must end in X", s.quote()), + TooFewXs(s) => write!(f, "too few X's in template {}", s.quote()), ContainsDirSeparator(s) => { - write!(f, "invalid suffix '{}', contains directory separator", s) + write!( + f, + "invalid suffix {}, contains directory separator", + s.quote() + ) } InvalidTemplate(s) => write!( f, - "invalid template, '{}'; with --tmpdir, it may not be absolute", - s + "invalid template, {}; with --tmpdir, it may not be absolute", + s.quote() ), } } @@ -74,7 +79,7 @@ impl Display for MkTempError { #[uucore_procs::gen_uumain] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let usage = get_usage(); + let usage = usage(); let matches = uu_app().usage(&usage[..]).get_matches_from(args); @@ -134,7 +139,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .arg( @@ -224,28 +229,26 @@ fn parse_template<'a>( 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); - buf.extend(iter::repeat('X').take(rand)); - buf.push_str(suffix); + let mut buf = Vec::with_capacity(len); + buf.extend(prefix.as_bytes()); + buf.extend(iter::repeat(b'X').take(rand)); + buf.extend(suffix.as_bytes()); // Randomize. - unsafe { - // We guarantee utf8. - let bytes = &mut buf.as_mut_vec()[prefix.len()..prefix.len() + rand]; - rand::thread_rng().fill(bytes); - for byte in bytes.iter_mut() { - *byte = match *byte % 62 { - v @ 0..=9 => (v + b'0'), - v @ 10..=35 => (v - 10 + b'a'), - v @ 36..=61 => (v - 36 + b'A'), - _ => unreachable!(), - } + let bytes = &mut buf[prefix.len()..prefix.len() + rand]; + rand::thread_rng().fill(bytes); + for byte in bytes.iter_mut() { + *byte = match *byte % 62 { + v @ 0..=9 => (v + b'0'), + v @ 10..=35 => (v - 10 + b'a'), + v @ 36..=61 => (v - 36 + b'A'), + _ => unreachable!(), } } + // We guarantee utf8. + let buf = String::from_utf8(buf).unwrap(); tmpdir.push(buf); - println!("{}", tmpdir.display()); - Ok(()) + println_verbatim(tmpdir).map_err_context(|| "failed to print directory name".to_owned()) } fn exec(dir: PathBuf, prefix: &str, rand: usize, suffix: &str, make_dir: bool) -> UResult<()> { @@ -274,6 +277,5 @@ fn exec(dir: PathBuf, prefix: &str, rand: usize, suffix: &str, make_dir: bool) - .map_err(|e| MkTempError::PersistError(e.file.path().to_path_buf()))? .1 }; - println!("{}", path.display()); - Ok(()) + println_verbatim(path).map_err_context(|| "failed to print directory name".to_owned()) } diff --git a/src/uu/more/Cargo.toml b/src/uu/more/Cargo.toml index 2d66dc950..cd292eea9 100644 --- a/src/uu/more/Cargo.toml +++ b/src/uu/more/Cargo.toml @@ -28,8 +28,12 @@ redox_termios = "0.1" redox_syscall = "0.2" [target.'cfg(all(unix, not(target_os = "fuchsia")))'.dependencies] -nix = "<=0.13" +nix = "0.19" [[bin]] name = "more" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/more/src/more.rs b/src/uu/more/src/more.rs index ecc779ba6..3a601c1e8 100644 --- a/src/uu/more/src/more.rs +++ b/src/uu/more/src/more.rs @@ -30,6 +30,7 @@ use crossterm::{ use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; +use uucore::display::Quotable; const BELL: &str = "\x07"; @@ -64,12 +65,12 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let file = Path::new(file); if file.is_dir() { terminal::disable_raw_mode().unwrap(); - show_usage_error!("'{}' is a directory.", file.display()); + show_usage_error!("{} is a directory.", file.quote()); return 1; } if !file.exists() { terminal::disable_raw_mode().unwrap(); - show_error!("cannot open {}: No such file or directory", file.display()); + show_error!("cannot open {}: No such file or directory", file.quote()); return 1; } if length > 1 { @@ -93,7 +94,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .about("A file perusal filter for CRT viewing.") .version(crate_version!()) .arg( diff --git a/src/uu/mv/Cargo.toml b/src/uu/mv/Cargo.toml index 9af0cb2a3..82b1da6e1 100644 --- a/src/uu/mv/Cargo.toml +++ b/src/uu/mv/Cargo.toml @@ -23,3 +23,7 @@ uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_p [[bin]] name = "mv" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/mv/src/mv.rs b/src/uu/mv/src/mv.rs index 166e8cb1a..9d23f86de 100644 --- a/src/uu/mv/src/mv.rs +++ b/src/uu/mv/src/mv.rs @@ -21,6 +21,7 @@ use std::os::unix; use std::os::windows; use std::path::{Path, PathBuf}; use uucore::backup_control::{self, BackupMode}; +use uucore::display::Quotable; use fs_extra::dir::{move_dir, CopyOptions as DirCopyOptions}; @@ -44,13 +45,10 @@ pub enum OverwriteMode { static ABOUT: &str = "Move SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY."; static LONG_HELP: &str = ""; -static OPT_BACKUP: &str = "backup"; -static OPT_BACKUP_NO_ARG: &str = "b"; static OPT_FORCE: &str = "force"; static OPT_INTERACTIVE: &str = "interactive"; static OPT_NO_CLOBBER: &str = "no-clobber"; static OPT_STRIP_TRAILING_SLASHES: &str = "strip-trailing-slashes"; -static OPT_SUFFIX: &str = "suffix"; static OPT_TARGET_DIRECTORY: &str = "target-directory"; static OPT_NO_TARGET_DIRECTORY: &str = "no-target-directory"; static OPT_UPDATE: &str = "update"; @@ -58,17 +56,17 @@ static OPT_VERBOSE: &str = "verbose"; static ARG_FILES: &str = "files"; -fn get_usage() -> String { +fn usage() -> String { format!( "{0} [OPTION]... [-T] SOURCE DEST {0} [OPTION]... SOURCE... DIRECTORY {0} [OPTION]... -t DIRECTORY SOURCE...", - executable!() + uucore::execution_phrase() ) } pub fn uumain(args: impl uucore::Args) -> i32 { - let usage = get_usage(); + let usage = usage(); let matches = uu_app() .after_help(&*format!( @@ -85,14 +83,9 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .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), - ); - let backup_mode = match backup_mode { - Err(err) => { - show_usage_error!("{}", err); + let backup_mode = match backup_control::determine_backup_mode(&matches) { + Err(e) => { + show!(e); return 1; } Ok(mode) => mode, @@ -103,7 +96,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { return 1; } - let backup_suffix = backup_control::determine_backup_suffix(matches.value_of(OPT_SUFFIX)); + let backup_suffix = backup_control::determine_backup_suffix(&matches); let behavior = Behavior { overwrite: overwrite_mode, @@ -133,22 +126,14 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .arg( - Arg::with_name(OPT_BACKUP) - .long(OPT_BACKUP) - .help("make a backup of each existing destination file") - .takes_value(true) - .require_equals(true) - .min_values(0) - .value_name("CONTROL") + backup_control::arguments::backup() ) .arg( - Arg::with_name(OPT_BACKUP_NO_ARG) - .short(OPT_BACKUP_NO_ARG) - .help("like --backup but does not accept an argument") + backup_control::arguments::backup_no_args() ) .arg( Arg::with_name(OPT_FORCE) @@ -173,12 +158,7 @@ pub fn uu_app() -> App<'static, 'static> { .help("remove any trailing slashes from each SOURCE argument") ) .arg( - Arg::with_name(OPT_SUFFIX) - .short("S") - .long(OPT_SUFFIX) - .help("override the usual backup suffix") - .takes_value(true) - .value_name("SUFFIX") + backup_control::arguments::suffix() ) .arg( Arg::with_name(OPT_TARGET_DIRECTORY) @@ -244,10 +224,7 @@ fn exec(files: &[PathBuf], b: Behavior) -> i32 { // `Ok()` results unless the source does not exist, or the user // lacks permission to access metadata. if source.symlink_metadata().is_err() { - show_error!( - "cannot stat '{}': No such file or directory", - source.display() - ); + show_error!("cannot stat {}: No such file or directory", source.quote()); return 1; } @@ -255,8 +232,8 @@ fn exec(files: &[PathBuf], b: Behavior) -> i32 { if b.no_target_dir { if !source.is_dir() { show_error!( - "cannot overwrite directory '{}' with non-directory", - target.display() + "cannot overwrite directory {} with non-directory", + target.quote() ); return 1; } @@ -264,9 +241,9 @@ fn exec(files: &[PathBuf], b: Behavior) -> i32 { return match rename(source, target, &b) { Err(e) => { show_error!( - "cannot move '{}' to '{}': {}", - source.display(), - target.display(), + "cannot move {} to {}: {}", + source.quote(), + target.quote(), e.to_string() ); 1 @@ -278,9 +255,9 @@ 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 '{}'", - target.display(), - source.display() + "cannot overwrite non-directory {} with directory {}", + target.quote(), + source.quote() ); return 1; } @@ -293,10 +270,10 @@ 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!() + files[2].quote(), + uucore::execution_phrase() ); return 1; } @@ -309,7 +286,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.quote()); return 1; } @@ -319,8 +296,8 @@ 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", - sourcepath.display() + "cannot stat {}: No such file or directory", + sourcepath.quote() ); all_successful = false; @@ -330,9 +307,9 @@ 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 '{}': {}", - sourcepath.display(), - targetpath.display(), + "cannot move {} to {}: {}", + sourcepath.quote(), + targetpath.quote(), e.to_string() ); all_successful = false; @@ -353,7 +330,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 {}? ", uucore::util_name(), to.quote()); if !read_yes() { return Ok(()); } @@ -386,9 +363,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.quote(), to.quote()); match backup_path { - Some(path) => println!(" (backup: '{}')", path.display()), + Some(path) => println!(" (backup: {})", path.quote()), None => println!(), } } diff --git a/src/uu/nice/Cargo.toml b/src/uu/nice/Cargo.toml index 31c310790..5f0c2c07a 100644 --- a/src/uu/nice/Cargo.toml +++ b/src/uu/nice/Cargo.toml @@ -17,7 +17,7 @@ path = "src/nice.rs" [dependencies] clap = { version = "2.33", features = ["wrap_help"] } libc = "0.2.42" -nix = { version="<=0.13" } +nix = "0.20" uucore = { version=">=0.0.9", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/nice/src/nice.rs b/src/uu/nice/src/nice.rs index 49efe32e0..fbc2be0e5 100644 --- a/src/uu/nice/src/nice.rs +++ b/src/uu/nice/src/nice.rs @@ -22,7 +22,7 @@ pub mod options { pub static COMMAND: &str = "COMMAND"; } -fn get_usage() -> String { +fn usage() -> String { format!( " {0} [OPTIONS] [COMMAND [ARGS]] @@ -31,12 +31,12 @@ Run COMMAND with an adjusted niceness, which affects process scheduling. With no COMMAND, print the current niceness. Niceness values range from at least -20 (most favorable to the process) to 19 (least favorable to the process).", - executable!() + uucore::execution_phrase() ) } pub fn uumain(args: impl uucore::Args) -> i32 { - let usage = get_usage(); + let usage = usage(); let matches = uu_app().usage(&usage[..]).get_matches_from(args); @@ -53,8 +53,8 @@ pub fn uumain(args: impl uucore::Args) -> i32 { Some(nstr) => { if !matches.is_present(options::COMMAND) { show_error!( - "A command must be given with an adjustment.\nTry \"{} --help\" for more information.", - executable!() + "A command must be given with an adjustment.\nTry '{} --help' for more information.", + uucore::execution_phrase() ); return 125; } @@ -101,7 +101,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .setting(AppSettings::TrailingVarArg) .version(crate_version!()) .arg( diff --git a/src/uu/nl/Cargo.toml b/src/uu/nl/Cargo.toml index 57676768f..ca0d7827d 100644 --- a/src/uu/nl/Cargo.toml +++ b/src/uu/nl/Cargo.toml @@ -27,3 +27,7 @@ uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_p [[bin]] name = "nl" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/nl/src/nl.rs b/src/uu/nl/src/nl.rs index 81e76aa26..600ebace0 100644 --- a/src/uu/nl/src/nl.rs +++ b/src/uu/nl/src/nl.rs @@ -143,7 +143,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .name(NAME) .version(crate_version!()) .usage(USAGE) diff --git a/src/uu/nohup/src/nohup.rs b/src/uu/nohup/src/nohup.rs index acc101e4e..d83170ae8 100644 --- a/src/uu/nohup/src/nohup.rs +++ b/src/uu/nohup/src/nohup.rs @@ -19,6 +19,7 @@ use std::fs::{File, OpenOptions}; use std::io::Error; use std::os::unix::prelude::*; use std::path::{Path, PathBuf}; +use uucore::display::Quotable; use uucore::InvalidEncodingHandling; static ABOUT: &str = "Run COMMAND ignoring hangup signals."; @@ -40,7 +41,7 @@ mod options { } pub fn uumain(args: impl uucore::Args) -> i32 { - let usage = get_usage(); + let usage = usage(); let args = args .collect_str(InvalidEncodingHandling::ConvertLossy) .accept_any(); @@ -71,7 +72,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .after_help(LONG_HELP) @@ -122,13 +123,16 @@ fn find_stdout() -> File { .open(Path::new(NOHUP_OUT)) { Ok(t) => { - show_error!("ignoring input and appending output to '{}'", NOHUP_OUT); + show_error!( + "ignoring input and appending output to {}", + NOHUP_OUT.quote() + ); t } Err(e1) => { let home = match env::var("HOME") { Err(_) => { - show_error!("failed to open '{}': {}", NOHUP_OUT, e1); + show_error!("failed to open {}: {}", NOHUP_OUT.quote(), e1); exit!(internal_failure_code) } Ok(h) => h, @@ -143,12 +147,15 @@ fn find_stdout() -> File { .open(&homeout) { Ok(t) => { - show_error!("ignoring input and appending output to '{}'", homeout_str); + show_error!( + "ignoring input and appending output to {}", + homeout_str.quote() + ); t } Err(e2) => { - show_error!("failed to open '{}': {}", NOHUP_OUT, e1); - show_error!("failed to open '{}': {}", homeout_str, e2); + show_error!("failed to open {}: {}", NOHUP_OUT.quote(), e1); + show_error!("failed to open {}: {}", homeout_str.quote(), e2); exit!(internal_failure_code) } } @@ -156,8 +163,11 @@ fn find_stdout() -> File { } } -fn get_usage() -> String { - format!("{0} COMMAND [ARG]...\n {0} FLAG", executable!()) +fn usage() -> String { + format!( + "{0} COMMAND [ARG]...\n {0} FLAG", + uucore::execution_phrase() + ) } #[cfg(target_vendor = "apple")] diff --git a/src/uu/nproc/src/nproc.rs b/src/uu/nproc/src/nproc.rs index 1f284685b..16b8d8c3a 100644 --- a/src/uu/nproc/src/nproc.rs +++ b/src/uu/nproc/src/nproc.rs @@ -27,12 +27,12 @@ static OPT_IGNORE: &str = "ignore"; static ABOUT: &str = "Print the number of cores available to the current process."; -fn get_usage() -> String { - format!("{0} [OPTIONS]...", executable!()) +fn usage() -> String { + format!("{0} [OPTIONS]...", uucore::execution_phrase()) } pub fn uumain(args: impl uucore::Args) -> i32 { - let usage = get_usage(); + let usage = usage(); let matches = uu_app().usage(&usage[..]).get_matches_from(args); let mut ignore = match matches.value_of(OPT_IGNORE) { @@ -70,7 +70,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .arg( diff --git a/src/uu/numfmt/Cargo.toml b/src/uu/numfmt/Cargo.toml index a3bdcf261..6239da7f9 100644 --- a/src/uu/numfmt/Cargo.toml +++ b/src/uu/numfmt/Cargo.toml @@ -22,3 +22,7 @@ uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_p [[bin]] name = "numfmt" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/numfmt/src/format.rs b/src/uu/numfmt/src/format.rs index e44446818..bdee83e12 100644 --- a/src/uu/numfmt/src/format.rs +++ b/src/uu/numfmt/src/format.rs @@ -1,3 +1,5 @@ +use uucore::display::Quotable; + use crate::options::{NumfmtOptions, RoundMethod}; use crate::units::{DisplayableSuffix, RawSuffix, Result, Suffix, Unit, IEC_BASES, SI_BASES}; @@ -78,7 +80,7 @@ fn parse_suffix(s: &str) -> Result<(f64, Option)> { 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)), + _ => return Err(format!("invalid suffix in input: {}", s.quote())), }; let suffix_len = match suffix { @@ -89,7 +91,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.quote()))?; Ok((number, suffix)) } diff --git a/src/uu/numfmt/src/numfmt.rs b/src/uu/numfmt/src/numfmt.rs index 01f12c51b..da2fa8130 100644 --- a/src/uu/numfmt/src/numfmt.rs +++ b/src/uu/numfmt/src/numfmt.rs @@ -15,6 +15,7 @@ use crate::options::*; use crate::units::{Result, Unit}; use clap::{crate_version, App, AppSettings, Arg, ArgMatches}; use std::io::{BufRead, Write}; +use uucore::display::Quotable; use uucore::ranges::Range; pub mod format; @@ -50,8 +51,8 @@ FIELDS supports cut(1) style field ranges: Multiple fields/ranges can be separated with commas "; -fn get_usage() -> String { - format!("{0} [OPTION]... [NUMBER]...", executable!()) +fn usage() -> String { + format!("{0} [OPTION]... [NUMBER]...", uucore::execution_phrase()) } fn handle_args<'a>(args: impl Iterator, options: NumfmtOptions) -> Result<()> { @@ -113,7 +114,7 @@ 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.quote())) } }?; @@ -154,7 +155,7 @@ fn parse_options(args: &ArgMatches) -> Result { } pub fn uumain(args: impl uucore::Args) -> i32 { - let usage = get_usage(); + let usage = usage(); let matches = uu_app().usage(&usage[..]).get_matches_from(args); @@ -175,7 +176,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .after_help(LONG_HELP) diff --git a/src/uu/od/Cargo.toml b/src/uu/od/Cargo.toml index 804183025..ee785e773 100644 --- a/src/uu/od/Cargo.toml +++ b/src/uu/od/Cargo.toml @@ -25,3 +25,7 @@ uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_p [[bin]] name = "od" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/od/src/inputdecoder.rs b/src/uu/od/src/inputdecoder.rs index 606495461..c347ccc10 100644 --- a/src/uu/od/src/inputdecoder.rs +++ b/src/uu/od/src/inputdecoder.rs @@ -161,6 +161,7 @@ mod tests { use std::io::Cursor; #[test] + #[allow(clippy::float_cmp)] fn smoke_test() { let data = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xff, 0xff]; let mut input = PeekReader::new(Cursor::new(&data)); diff --git a/src/uu/od/src/mockstream.rs b/src/uu/od/src/mockstream.rs index 810e1d35b..5beecbf9c 100644 --- a/src/uu/od/src/mockstream.rs +++ b/src/uu/od/src/mockstream.rs @@ -56,15 +56,15 @@ impl FailingMockStream { /// `kind` and `message` can be specified to define the exact error. pub fn new(kind: ErrorKind, message: &'static str, repeat_count: i32) -> FailingMockStream { FailingMockStream { - kind: kind, - message: message, - repeat_count: repeat_count, + kind, + message, + repeat_count, } } fn error(&mut self) -> Result { if self.repeat_count == 0 { - return Ok(0); + Ok(0) } else { if self.repeat_count > 0 { self.repeat_count -= 1; diff --git a/src/uu/od/src/multifilereader.rs b/src/uu/od/src/multifilereader.rs index 1255da66d..1b3aba03d 100644 --- a/src/uu/od/src/multifilereader.rs +++ b/src/uu/od/src/multifilereader.rs @@ -5,6 +5,8 @@ use std::io; use std::io::BufReader; use std::vec::Vec; +use uucore::display::Quotable; + pub enum InputSource<'a> { FileName(&'a str), Stdin, @@ -57,12 +59,7 @@ impl<'b> MultifileReader<'b> { // print an error at the time that the file is needed, // then move on the the next file. // This matches the behavior of the original `od` - eprintln!( - "{}: '{}': {}", - executable!().split("::").next().unwrap(), // remove module - fname, - e - ); + show_error!("{}: {}", fname.maybe_quote(), e); self.any_err = true } } @@ -95,11 +92,7 @@ impl<'b> io::Read for MultifileReader<'b> { Ok(0) => break, Ok(n) => n, Err(e) => { - eprintln!( - "{}: I/O: {}", - executable!().split("::").next().unwrap(), // remove module - e - ); + show_error!("I/O: {}", e); self.any_err = true; break; } @@ -131,9 +124,10 @@ mod tests { #[test] fn test_multi_file_reader_one_read() { - let mut inputs = Vec::new(); - inputs.push(InputSource::Stream(Box::new(Cursor::new(&b"abcd"[..])))); - inputs.push(InputSource::Stream(Box::new(Cursor::new(&b"ABCD"[..])))); + let inputs = vec![ + InputSource::Stream(Box::new(Cursor::new(&b"abcd"[..]))), + InputSource::Stream(Box::new(Cursor::new(&b"ABCD"[..]))), + ]; let mut v = [0; 10]; let mut sut = MultifileReader::new(inputs); @@ -145,9 +139,10 @@ mod tests { #[test] fn test_multi_file_reader_two_reads() { - let mut inputs = Vec::new(); - inputs.push(InputSource::Stream(Box::new(Cursor::new(&b"abcd"[..])))); - inputs.push(InputSource::Stream(Box::new(Cursor::new(&b"ABCD"[..])))); + let inputs = vec![ + InputSource::Stream(Box::new(Cursor::new(&b"abcd"[..]))), + InputSource::Stream(Box::new(Cursor::new(&b"ABCD"[..]))), + ]; let mut v = [0; 5]; let mut sut = MultifileReader::new(inputs); @@ -163,9 +158,10 @@ mod tests { let c = Cursor::new(&b"1234"[..]) .chain(FailingMockStream::new(ErrorKind::Other, "Failing", 1)) .chain(Cursor::new(&b"5678"[..])); - let mut inputs = Vec::new(); - inputs.push(InputSource::Stream(Box::new(c))); - inputs.push(InputSource::Stream(Box::new(Cursor::new(&b"ABCD"[..])))); + let inputs = vec![ + InputSource::Stream(Box::new(c)), + InputSource::Stream(Box::new(Cursor::new(&b"ABCD"[..]))), + ]; let mut v = [0; 5]; let mut sut = MultifileReader::new(inputs); @@ -180,24 +176,25 @@ mod tests { #[test] fn test_multi_file_reader_read_error_at_start() { - let mut inputs = Vec::new(); - inputs.push(InputSource::Stream(Box::new(FailingMockStream::new( - ErrorKind::Other, - "Failing", - 1, - )))); - inputs.push(InputSource::Stream(Box::new(Cursor::new(&b"abcd"[..])))); - inputs.push(InputSource::Stream(Box::new(FailingMockStream::new( - ErrorKind::Other, - "Failing", - 1, - )))); - inputs.push(InputSource::Stream(Box::new(Cursor::new(&b"ABCD"[..])))); - inputs.push(InputSource::Stream(Box::new(FailingMockStream::new( - ErrorKind::Other, - "Failing", - 1, - )))); + let inputs = vec![ + InputSource::Stream(Box::new(FailingMockStream::new( + ErrorKind::Other, + "Failing", + 1, + ))), + InputSource::Stream(Box::new(Cursor::new(&b"abcd"[..]))), + InputSource::Stream(Box::new(FailingMockStream::new( + ErrorKind::Other, + "Failing", + 1, + ))), + InputSource::Stream(Box::new(Cursor::new(&b"ABCD"[..]))), + InputSource::Stream(Box::new(FailingMockStream::new( + ErrorKind::Other, + "Failing", + 1, + ))), + ]; let mut v = [0; 5]; let mut sut = MultifileReader::new(inputs); diff --git a/src/uu/od/src/od.rs b/src/uu/od/src/od.rs index 359531d4e..e9983f991 100644 --- a/src/uu/od/src/od.rs +++ b/src/uu/od/src/od.rs @@ -43,6 +43,7 @@ use crate::partialreader::*; use crate::peekreader::*; use crate::prn_char::format_ascii_dump; use clap::{self, crate_version, AppSettings, Arg, ArgMatches}; +use uucore::display::Quotable; use uucore::parse_size::ParseSizeError; use uucore::InvalidEncodingHandling; @@ -252,7 +253,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> clap::App<'static, 'static> { - clap::App::new(executable!()) + clap::App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .usage(USAGE) @@ -635,7 +636,7 @@ fn format_error_message(error: ParseSizeError, s: &str, option: &str) -> String // GNU's od echos affected flag, -N or --read-bytes (-j or --skip-bytes, etc.), depending user's selection // GNU's od does distinguish between "invalid (suffix in) argument" match error { - ParseSizeError::ParseFailure(_) => format!("invalid --{} argument '{}'", option, s), - ParseSizeError::SizeTooBig(_) => format!("--{} argument '{}' too large", option, s), + ParseSizeError::ParseFailure(_) => format!("invalid --{} argument {}", option, s.quote()), + ParseSizeError::SizeTooBig(_) => format!("--{} argument {} too large", option, s.quote()), } } diff --git a/src/uu/od/src/parse_formats.rs b/src/uu/od/src/parse_formats.rs index f5b150d61..301bb5154 100644 --- a/src/uu/od/src/parse_formats.rs +++ b/src/uu/od/src/parse_formats.rs @@ -1,5 +1,7 @@ // spell-checker:ignore formatteriteminfo docopt fvox fvoxw vals acdx +use uucore::display::Quotable; + use crate::formatteriteminfo::FormatterItemInfo; use crate::prn_char::*; use crate::prn_float::*; @@ -272,8 +274,9 @@ fn parse_type_string(params: &str) -> Result, Strin while let Some(type_char) = ch { let type_char = format_type(type_char).ok_or_else(|| { format!( - "unexpected char '{}' in format specification '{}'", - type_char, params + "unexpected char '{}' in format specification {}", + type_char, + params.quote() ) })?; @@ -293,8 +296,9 @@ fn parse_type_string(params: &str) -> Result, Strin if !decimal_size.is_empty() { byte_size = decimal_size.parse().map_err(|_| { format!( - "invalid number '{}' in format specification '{}'", - decimal_size, params + "invalid number {} in format specification {}", + decimal_size.quote(), + params.quote() ) })?; } @@ -305,8 +309,9 @@ fn parse_type_string(params: &str) -> Result, Strin let ft = od_format_type(type_char, byte_size).ok_or_else(|| { format!( - "invalid size '{}' in format specification '{}'", - byte_size, params + "invalid size '{}' in format specification {}", + byte_size, + params.quote() ) })?; formats.push(ParsedFormatterItemInfo::new(ft, show_ascii_dump)); @@ -316,9 +321,7 @@ fn parse_type_string(params: &str) -> Result, Strin } #[cfg(test)] -pub fn parse_format_flags_str( - args_str: &Vec<&'static str>, -) -> Result, String> { +pub fn parse_format_flags_str(args_str: &[&'static str]) -> Result, String> { let args: Vec = args_str.iter().map(|s| s.to_string()).collect(); parse_format_flags(&args).map(|v| { // tests using this function assume add_ascii_dump is not set @@ -332,7 +335,7 @@ pub fn parse_format_flags_str( #[test] fn test_no_options() { assert_eq!( - parse_format_flags_str(&vec!["od"]).unwrap(), + parse_format_flags_str(&["od"]).unwrap(), vec![FORMAT_ITEM_OCT16] ); } @@ -340,7 +343,7 @@ fn test_no_options() { #[test] fn test_one_option() { assert_eq!( - parse_format_flags_str(&vec!["od", "-F"]).unwrap(), + parse_format_flags_str(&["od", "-F"]).unwrap(), vec![FORMAT_ITEM_F64] ); } @@ -348,7 +351,7 @@ fn test_one_option() { #[test] fn test_two_separate_options() { assert_eq!( - parse_format_flags_str(&vec!["od", "-F", "-x"]).unwrap(), + parse_format_flags_str(&["od", "-F", "-x"]).unwrap(), vec![FORMAT_ITEM_F64, FORMAT_ITEM_HEX16] ); } @@ -356,7 +359,7 @@ fn test_two_separate_options() { #[test] fn test_two_combined_options() { assert_eq!( - parse_format_flags_str(&vec!["od", "-Fx"]).unwrap(), + parse_format_flags_str(&["od", "-Fx"]).unwrap(), vec![FORMAT_ITEM_F64, FORMAT_ITEM_HEX16] ); } @@ -364,7 +367,7 @@ fn test_two_combined_options() { #[test] fn test_ignore_non_format_parameters() { assert_eq!( - parse_format_flags_str(&vec!["od", "-d", "-Ax"]).unwrap(), + parse_format_flags_str(&["od", "-d", "-Ax"]).unwrap(), vec![FORMAT_ITEM_DEC16U] ); } @@ -372,7 +375,7 @@ fn test_ignore_non_format_parameters() { #[test] fn test_ignore_separate_parameters() { assert_eq!( - parse_format_flags_str(&vec!["od", "-I", "-A", "x"]).unwrap(), + parse_format_flags_str(&["od", "-I", "-A", "x"]).unwrap(), vec![FORMAT_ITEM_DEC64S] ); } @@ -380,36 +383,36 @@ fn test_ignore_separate_parameters() { #[test] fn test_ignore_trailing_vals() { assert_eq!( - parse_format_flags_str(&vec!["od", "-D", "--", "-x"]).unwrap(), + parse_format_flags_str(&["od", "-D", "--", "-x"]).unwrap(), vec![FORMAT_ITEM_DEC32U] ); } #[test] fn test_invalid_long_format() { - parse_format_flags_str(&vec!["od", "--format=X"]).unwrap_err(); - parse_format_flags_str(&vec!["od", "--format=xX"]).unwrap_err(); - parse_format_flags_str(&vec!["od", "--format=aC"]).unwrap_err(); - parse_format_flags_str(&vec!["od", "--format=fI"]).unwrap_err(); - parse_format_flags_str(&vec!["od", "--format=xD"]).unwrap_err(); + parse_format_flags_str(&["od", "--format=X"]).unwrap_err(); + parse_format_flags_str(&["od", "--format=xX"]).unwrap_err(); + parse_format_flags_str(&["od", "--format=aC"]).unwrap_err(); + parse_format_flags_str(&["od", "--format=fI"]).unwrap_err(); + parse_format_flags_str(&["od", "--format=xD"]).unwrap_err(); - parse_format_flags_str(&vec!["od", "--format=xC1"]).unwrap_err(); - parse_format_flags_str(&vec!["od", "--format=x1C"]).unwrap_err(); - parse_format_flags_str(&vec!["od", "--format=xz1"]).unwrap_err(); - parse_format_flags_str(&vec!["od", "--format=xzC"]).unwrap_err(); - parse_format_flags_str(&vec!["od", "--format=xzz"]).unwrap_err(); - parse_format_flags_str(&vec!["od", "--format=xCC"]).unwrap_err(); + parse_format_flags_str(&["od", "--format=xC1"]).unwrap_err(); + parse_format_flags_str(&["od", "--format=x1C"]).unwrap_err(); + parse_format_flags_str(&["od", "--format=xz1"]).unwrap_err(); + parse_format_flags_str(&["od", "--format=xzC"]).unwrap_err(); + parse_format_flags_str(&["od", "--format=xzz"]).unwrap_err(); + parse_format_flags_str(&["od", "--format=xCC"]).unwrap_err(); - parse_format_flags_str(&vec!["od", "--format=c1"]).unwrap_err(); - parse_format_flags_str(&vec!["od", "--format=x256"]).unwrap_err(); - parse_format_flags_str(&vec!["od", "--format=d5"]).unwrap_err(); - parse_format_flags_str(&vec!["od", "--format=f1"]).unwrap_err(); + parse_format_flags_str(&["od", "--format=c1"]).unwrap_err(); + parse_format_flags_str(&["od", "--format=x256"]).unwrap_err(); + parse_format_flags_str(&["od", "--format=d5"]).unwrap_err(); + parse_format_flags_str(&["od", "--format=f1"]).unwrap_err(); } #[test] fn test_long_format_a() { assert_eq!( - parse_format_flags_str(&vec!["od", "--format=a"]).unwrap(), + parse_format_flags_str(&["od", "--format=a"]).unwrap(), vec![FORMAT_ITEM_A] ); } @@ -417,7 +420,7 @@ fn test_long_format_a() { #[test] fn test_long_format_cz() { assert_eq!( - parse_format_flags(&vec!["od".to_string(), "--format=cz".to_string()]).unwrap(), + parse_format_flags(&["od".to_string(), "--format=cz".to_string()]).unwrap(), vec![ParsedFormatterItemInfo::new(FORMAT_ITEM_C, true)] ); } @@ -425,7 +428,7 @@ fn test_long_format_cz() { #[test] fn test_long_format_d() { assert_eq!( - parse_format_flags_str(&vec!["od", "--format=d8"]).unwrap(), + parse_format_flags_str(&["od", "--format=d8"]).unwrap(), vec![FORMAT_ITEM_DEC64S] ); } @@ -433,7 +436,7 @@ fn test_long_format_d() { #[test] fn test_long_format_d_default() { assert_eq!( - parse_format_flags_str(&vec!["od", "--format=d"]).unwrap(), + parse_format_flags_str(&["od", "--format=d"]).unwrap(), vec![FORMAT_ITEM_DEC32S] ); } @@ -441,7 +444,7 @@ fn test_long_format_d_default() { #[test] fn test_long_format_o_default() { assert_eq!( - parse_format_flags_str(&vec!["od", "--format=o"]).unwrap(), + parse_format_flags_str(&["od", "--format=o"]).unwrap(), vec![FORMAT_ITEM_OCT32] ); } @@ -449,7 +452,7 @@ fn test_long_format_o_default() { #[test] fn test_long_format_u_default() { assert_eq!( - parse_format_flags_str(&vec!["od", "--format=u"]).unwrap(), + parse_format_flags_str(&["od", "--format=u"]).unwrap(), vec![FORMAT_ITEM_DEC32U] ); } @@ -457,7 +460,7 @@ fn test_long_format_u_default() { #[test] fn test_long_format_x_default() { assert_eq!( - parse_format_flags_str(&vec!["od", "--format=x"]).unwrap(), + parse_format_flags_str(&["od", "--format=x"]).unwrap(), vec![FORMAT_ITEM_HEX32] ); } @@ -465,7 +468,7 @@ fn test_long_format_x_default() { #[test] fn test_long_format_f_default() { assert_eq!( - parse_format_flags_str(&vec!["od", "--format=f"]).unwrap(), + parse_format_flags_str(&["od", "--format=f"]).unwrap(), vec![FORMAT_ITEM_F32] ); } @@ -473,7 +476,7 @@ fn test_long_format_f_default() { #[test] fn test_long_format_next_arg() { assert_eq!( - parse_format_flags_str(&vec!["od", "--format", "f8"]).unwrap(), + parse_format_flags_str(&["od", "--format", "f8"]).unwrap(), vec![FORMAT_ITEM_F64] ); } @@ -481,7 +484,7 @@ fn test_long_format_next_arg() { #[test] fn test_short_format_next_arg() { assert_eq!( - parse_format_flags_str(&vec!["od", "-t", "x8"]).unwrap(), + parse_format_flags_str(&["od", "-t", "x8"]).unwrap(), vec![FORMAT_ITEM_HEX64] ); } @@ -489,23 +492,23 @@ fn test_short_format_next_arg() { #[test] fn test_short_format_combined_arg() { assert_eq!( - parse_format_flags_str(&vec!["od", "-tu8"]).unwrap(), + parse_format_flags_str(&["od", "-tu8"]).unwrap(), vec![FORMAT_ITEM_DEC64U] ); } #[test] fn test_format_next_arg_invalid() { - parse_format_flags_str(&vec!["od", "--format", "-v"]).unwrap_err(); - parse_format_flags_str(&vec!["od", "--format"]).unwrap_err(); - parse_format_flags_str(&vec!["od", "-t", "-v"]).unwrap_err(); - parse_format_flags_str(&vec!["od", "-t"]).unwrap_err(); + parse_format_flags_str(&["od", "--format", "-v"]).unwrap_err(); + parse_format_flags_str(&["od", "--format"]).unwrap_err(); + parse_format_flags_str(&["od", "-t", "-v"]).unwrap_err(); + parse_format_flags_str(&["od", "-t"]).unwrap_err(); } #[test] fn test_mixed_formats() { assert_eq!( - parse_format_flags(&vec![ + parse_format_flags(&[ "od".to_string(), "--skip-bytes=2".to_string(), "-vItu1z".to_string(), diff --git a/src/uu/od/src/prn_float.rs b/src/uu/od/src/prn_float.rs index 411bc9c10..a9bf1279e 100644 --- a/src/uu/od/src/prn_float.rs +++ b/src/uu/od/src/prn_float.rs @@ -91,6 +91,7 @@ fn format_float(f: f64, width: usize, precision: usize) -> String { } #[test] +#[allow(clippy::excessive_precision)] fn test_format_flo32() { assert_eq!(format_flo32(1.0), " 1.0000000"); assert_eq!(format_flo32(9.9999990), " 9.9999990"); diff --git a/src/uu/paste/Cargo.toml b/src/uu/paste/Cargo.toml index 19a674c3e..c4873b1d0 100644 --- a/src/uu/paste/Cargo.toml +++ b/src/uu/paste/Cargo.toml @@ -22,3 +22,7 @@ uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_p [[bin]] name = "paste" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/paste/src/paste.rs b/src/uu/paste/src/paste.rs index 7f7969687..9ac5507df 100644 --- a/src/uu/paste/src/paste.rs +++ b/src/uu/paste/src/paste.rs @@ -52,7 +52,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .arg( diff --git a/src/uu/pathchk/src/pathchk.rs b/src/uu/pathchk/src/pathchk.rs index 7f728667f..8afeaff18 100644 --- a/src/uu/pathchk/src/pathchk.rs +++ b/src/uu/pathchk/src/pathchk.rs @@ -15,6 +15,7 @@ extern crate uucore; use clap::{crate_version, App, Arg}; use std::fs; use std::io::{ErrorKind, Write}; +use uucore::display::Quotable; use uucore::InvalidEncodingHandling; // operating mode @@ -25,7 +26,6 @@ enum Mode { Both, // a combination of `Basic` and `Extra` } -static NAME: &str = "pathchk"; static ABOUT: &str = "Check whether file names are valid or portable"; mod options { @@ -39,12 +39,12 @@ mod options { const POSIX_PATH_MAX: usize = 256; const POSIX_NAME_MAX: usize = 14; -fn get_usage() -> String { - format!("{0} [OPTION]... NAME...", executable!()) +fn usage() -> String { + format!("{0} [OPTION]... NAME...", uucore::execution_phrase()) } pub fn uumain(args: impl uucore::Args) -> i32 { - let usage = get_usage(); + let usage = usage(); let args = args .collect_str(InvalidEncodingHandling::ConvertLossy) .accept_any(); @@ -69,7 +69,10 @@ pub fn uumain(args: impl uucore::Args) -> i32 { // take necessary actions let paths = matches.values_of(options::PATH); let mut res = if paths.is_none() { - show_error!("missing operand\nTry {} --help for more information", NAME); + show_error!( + "missing operand\nTry '{} --help' for more information", + uucore::execution_phrase() + ); false } else { true @@ -96,7 +99,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .arg( @@ -151,10 +154,10 @@ fn check_basic(path: &[String]) -> bool { if component_len > POSIX_NAME_MAX { writeln!( &mut std::io::stderr(), - "limit {} exceeded by length {} of file name component '{}'", + "limit {} exceeded by length {} of file name component {}", POSIX_NAME_MAX, component_len, - p + p.quote() ); return false; } @@ -173,8 +176,8 @@ fn check_extra(path: &[String]) -> bool { if p.starts_with('-') { writeln!( &mut std::io::stderr(), - "leading hyphen in file name component '{}'", - p + "leading hyphen in file name component {}", + p.quote() ); return false; } @@ -195,10 +198,10 @@ fn check_default(path: &[String]) -> bool { if total_len > libc::PATH_MAX as usize { writeln!( &mut std::io::stderr(), - "limit {} exceeded by length {} of file name '{}'", + "limit {} exceeded by length {} of file name {}", libc::PATH_MAX, total_len, - joined_path + joined_path.quote() ); return false; } @@ -208,10 +211,10 @@ fn check_default(path: &[String]) -> bool { if component_len > libc::FILENAME_MAX as usize { writeln!( &mut std::io::stderr(), - "limit {} exceeded by length {} of file name component '{}'", + "limit {} exceeded by length {} of file name component {}", libc::FILENAME_MAX, component_len, - p + p.quote() ); return false; } @@ -244,9 +247,9 @@ fn check_portable_chars(path_segment: &str) -> bool { let invalid = path_segment[i..].chars().next().unwrap(); writeln!( &mut std::io::stderr(), - "nonportable character '{}' in file name component '{}'", + "nonportable character '{}' in file name component {}", invalid, - path_segment + path_segment.quote() ); return false; } diff --git a/src/uu/pinky/src/pinky.rs b/src/uu/pinky/src/pinky.rs index 16bcfd3c9..4aa27affa 100644 --- a/src/uu/pinky/src/pinky.rs +++ b/src/uu/pinky/src/pinky.rs @@ -40,8 +40,8 @@ mod options { pub const USER: &str = "user"; } -fn get_usage() -> String { - format!("{0} [OPTION]... [USER]...", executable!()) +fn usage() -> String { + format!("{0} [OPTION]... [USER]...", uucore::execution_phrase()) } fn get_long_usage() -> String { @@ -57,7 +57,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .collect_str(InvalidEncodingHandling::Ignore) .accept_any(); - let usage = get_usage(); + let usage = usage(); let after_help = get_long_usage(); let matches = uu_app() @@ -130,7 +130,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .arg( @@ -291,7 +291,7 @@ impl Pinky { let mut s = ut.host(); if self.include_where && !s.is_empty() { - s = safe_unwrap!(ut.canon_host()); + s = crash_if_err!(1, ut.canon_host()); print!(" {}", s); } diff --git a/src/uu/pr/Cargo.toml b/src/uu/pr/Cargo.toml index d446e66a0..09993c3b9 100644 --- a/src/uu/pr/Cargo.toml +++ b/src/uu/pr/Cargo.toml @@ -19,8 +19,6 @@ clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.7", package="uucore", path="../../uucore", features=["entries"] } uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_procs" } getopts = "0.2.21" -time = "0.1.41" -# A higher version would cause a conflict with time chrono = "0.4.19" quick-error = "2.0.1" itertools = "0.10" @@ -29,3 +27,7 @@ regex = "1.0" [[bin]] name = "pr" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/pr/src/pr.rs b/src/uu/pr/src/pr.rs index d6b9e8ca3..45d9480a7 100644 --- a/src/uu/pr/src/pr.rs +++ b/src/uu/pr/src/pr.rs @@ -23,7 +23,8 @@ 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; + +use uucore::display::Quotable; type IOError = std::io::Error; @@ -170,7 +171,7 @@ quick_error! { pub fn uu_app() -> clap::App<'static, 'static> { // TODO: migrate to clap to get more shell completions - clap::App::new(executable!()) + clap::App::new(uucore::util_name()) } pub fn uumain(args: impl uucore::Args) -> i32 { @@ -518,7 +519,7 @@ fn parse_usize(matches: &Matches, opt: &str) -> Option> { let i = value_to_parse.0; let option = value_to_parse.1; i.parse().map_err(|_e| { - PrError::EncounteredErrors(format!("invalid {} argument '{}'", option, i)) + PrError::EncounteredErrors(format!("invalid {} argument {}", option, i.quote())) }) }; matches @@ -620,7 +621,7 @@ fn build_options( let unparsed_num = i.get(1).unwrap().as_str().trim(); let x: Vec<_> = unparsed_num.split(':').collect(); x[0].to_string().parse::().map_err(|_e| { - PrError::EncounteredErrors(format!("invalid {} argument '{}'", "+", unparsed_num)) + PrError::EncounteredErrors(format!("invalid {} argument {}", "+", unparsed_num.quote())) }) }) { Some(res) => res?, @@ -634,7 +635,11 @@ fn build_options( .map(|unparsed_num| { let x: Vec<_> = unparsed_num.split(':').collect(); x[1].to_string().parse::().map_err(|_e| { - PrError::EncounteredErrors(format!("invalid {} argument '{}'", "+", unparsed_num)) + PrError::EncounteredErrors(format!( + "invalid {} argument {}", + "+", + unparsed_num.quote() + )) }) }) { Some(res) => Some(res?), @@ -644,7 +649,10 @@ fn build_options( let invalid_pages_map = |i: String| { let unparsed_value = matches.opt_str(options::PAGE_RANGE_OPTION).unwrap(); i.parse::().map_err(|_e| { - PrError::EncounteredErrors(format!("invalid --pages argument '{}'", unparsed_value)) + PrError::EncounteredErrors(format!( + "invalid --pages argument {}", + unparsed_value.quote() + )) }) }; @@ -742,7 +750,7 @@ fn build_options( let start_column_option = match re_col.captures(&free_args).map(|i| { let unparsed_num = i.get(1).unwrap().as_str().trim(); unparsed_num.parse::().map_err(|_e| { - PrError::EncounteredErrors(format!("invalid {} argument '{}'", "-", unparsed_num)) + PrError::EncounteredErrors(format!("invalid {} argument {}", "-", unparsed_num.quote())) }) }) { Some(res) => Some(res?), diff --git a/src/uu/printenv/Cargo.toml b/src/uu/printenv/Cargo.toml index 040997393..466f69af0 100644 --- a/src/uu/printenv/Cargo.toml +++ b/src/uu/printenv/Cargo.toml @@ -22,3 +22,7 @@ uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_p [[bin]] name = "printenv" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/printenv/src/printenv.rs b/src/uu/printenv/src/printenv.rs index 6e0ca7157..5d32cfbcc 100644 --- a/src/uu/printenv/src/printenv.rs +++ b/src/uu/printenv/src/printenv.rs @@ -7,9 +7,6 @@ /* last synced with: printenv (GNU coreutils) 8.13 */ -#[macro_use] -extern crate uucore; - use clap::{crate_version, App, Arg}; use std::env; @@ -19,12 +16,12 @@ static OPT_NULL: &str = "null"; static ARG_VARIABLES: &str = "variables"; -fn get_usage() -> String { - format!("{0} [VARIABLE]... [OPTION]...", executable!()) +fn usage() -> String { + format!("{0} [VARIABLE]... [OPTION]...", uucore::execution_phrase()) } pub fn uumain(args: impl uucore::Args) -> i32 { - let usage = get_usage(); + let usage = usage(); let matches = uu_app().usage(&usage[..]).get_matches_from(args); @@ -55,7 +52,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .arg( diff --git a/src/uu/printf/Cargo.toml b/src/uu/printf/Cargo.toml index a0bd27d8e..f4034083a 100644 --- a/src/uu/printf/Cargo.toml +++ b/src/uu/printf/Cargo.toml @@ -26,3 +26,7 @@ uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_p [[bin]] name = "printf" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/printf/src/cli.rs b/src/uu/printf/src/cli.rs index a5e9c9775..fa2fda1d2 100644 --- a/src/uu/printf/src/cli.rs +++ b/src/uu/printf/src/cli.rs @@ -2,20 +2,11 @@ // spell-checker:ignore (ToDO) bslice -use std::env; -use std::io::{stderr, stdout, Write}; +use std::io::{stdout, Write}; pub const EXIT_OK: i32 = 0; pub const EXIT_ERR: i32 = 1; -pub fn err_msg(msg: &str) { - let exe_path = match env::current_exe() { - Ok(p) => p.to_string_lossy().into_owned(), - _ => String::from(""), - }; - writeln!(&mut stderr(), "{}: {}", exe_path, msg).unwrap(); -} - // by default stdout only flushes // to console when a newline is passed. pub fn flush_char(c: char) { diff --git a/src/uu/printf/src/memo.rs b/src/uu/printf/src/memo.rs index 2f12f9192..a87d4fa89 100644 --- a/src/uu/printf/src/memo.rs +++ b/src/uu/printf/src/memo.rs @@ -8,8 +8,9 @@ use itertools::put_back_n; use std::iter::Peekable; use std::slice::Iter; +use uucore::display::Quotable; +use uucore::show_warning; -use crate::cli; use crate::tokenize::sub::Sub; use crate::tokenize::token::{Token, Tokenizer}; use crate::tokenize::unescaped_text::UnescapedText; @@ -19,10 +20,10 @@ pub struct Memo { } fn warn_excess_args(first_arg: &str) { - cli::err_msg(&format!( - "warning: ignoring excess arguments, starting with '{}'", - first_arg - )); + show_warning!( + "ignoring excess arguments, starting with {}", + first_arg.quote() + ); } impl Memo { diff --git a/src/uu/printf/src/printf.rs b/src/uu/printf/src/printf.rs index efa9aea57..d3c8dca90 100644 --- a/src/uu/printf/src/printf.rs +++ b/src/uu/printf/src/printf.rs @@ -2,9 +2,6 @@ // 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; @@ -281,11 +278,11 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .collect_str(InvalidEncodingHandling::Ignore) .accept_any(); - let location = &args[0]; if args.len() <= 1 { println!( - "{0}: missing operand\nTry '{0} --help' for more information.", - location + "{0}: missing operand\nTry '{1} --help' for more information.", + uucore::util_name(), + uucore::execution_phrase() ); return 1; } @@ -294,7 +291,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { if formatstr == "--help" { print!("{} {}", LONGHELP_LEAD, LONGHELP_BODY); } else if formatstr == "--version" { - println!("{} {}", executable!(), crate_version!()); + println!("{} {}", uucore::util_name(), crate_version!()); } else { let printf_args = &args[2..]; memo::Memo::run_all(formatstr, printf_args); @@ -303,7 +300,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .arg(Arg::with_name(VERSION).long(VERSION)) .arg(Arg::with_name(HELP).long(HELP)) } diff --git a/src/uu/printf/src/tokenize/num_format/formatter.rs b/src/uu/printf/src/tokenize/num_format/formatter.rs index f5f5d71b1..790c338ae 100644 --- a/src/uu/printf/src/tokenize/num_format/formatter.rs +++ b/src/uu/printf/src/tokenize/num_format/formatter.rs @@ -3,15 +3,15 @@ use itertools::{put_back_n, PutBackN}; use std::str::Chars; +use uucore::{display::Quotable, show_error}; use super::format_field::FormatField; -use crate::cli; - // contains the rough ingredients to final // output for a number, organized together // to allow for easy generalization of output manipulation // (e.g. max number of digits after decimal) +#[derive(Default)] pub struct FormatPrimitive { pub prefix: Option, pub pre_decimal: Option, @@ -19,17 +19,6 @@ pub struct FormatPrimitive { pub suffix: Option, } -impl Default for FormatPrimitive { - fn default() -> FormatPrimitive { - FormatPrimitive { - prefix: None, - pre_decimal: None, - post_decimal: None, - suffix: None, - } - } -} - #[derive(Clone, PartialEq)] pub enum Base { Ten = 10, @@ -66,5 +55,5 @@ pub fn get_it_at(offset: usize, str_in: &str) -> PutBackN { // TODO: put this somewhere better pub fn warn_incomplete_conv(pf_arg: &str) { // important: keep println here not print - cli::err_msg(&format!("{}: value not completely converted", pf_arg)) + show_error!("{}: value not completely converted", pf_arg.maybe_quote()); } diff --git a/src/uu/printf/src/tokenize/num_format/num_format.rs b/src/uu/printf/src/tokenize/num_format/num_format.rs index b32731f2d..a9fee58e1 100644 --- a/src/uu/printf/src/tokenize/num_format/num_format.rs +++ b/src/uu/printf/src/tokenize/num_format/num_format.rs @@ -7,6 +7,9 @@ use std::env; use std::vec::Vec; +use uucore::display::Quotable; +use uucore::{show_error, show_warning}; + use super::format_field::{FieldType, FormatField}; use super::formatter::{Base, FormatPrimitive, Formatter, InitialPrefix}; use super::formatters::cninetyninehexfloatf::CninetyNineHexFloatf; @@ -15,11 +18,9 @@ use super::formatters::floatf::Floatf; use super::formatters::intf::Intf; use super::formatters::scif::Scif; -use crate::cli; - pub fn warn_expected_numeric(pf_arg: &str) { // important: keep println here not print - cli::err_msg(&format!("{}: expected a numeric value", pf_arg)); + show_error!("{}: expected a numeric value", pf_arg.maybe_quote()); } // when character constant arguments have excess characters @@ -29,11 +30,11 @@ fn warn_char_constant_ign(remaining_bytes: Vec) { Ok(_) => {} Err(e) => { if let env::VarError::NotPresent = e { - cli::err_msg(&format!( - "warning: {:?}: character(s) following character \ + show_warning!( + "{:?}: character(s) following character \ constant have been ignored", &*remaining_bytes - )); + ); } } } diff --git a/src/uu/printf/src/tokenize/sub.rs b/src/uu/printf/src/tokenize/sub.rs index d01e00699..48d854fab 100644 --- a/src/uu/printf/src/tokenize/sub.rs +++ b/src/uu/printf/src/tokenize/sub.rs @@ -10,6 +10,7 @@ use std::iter::Peekable; use std::process::exit; use std::slice::Iter; use std::str::Chars; +use uucore::show_error; // use std::collections::HashSet; use super::num_format::format_field::{FieldType, FormatField}; @@ -19,7 +20,7 @@ use super::unescaped_text::UnescapedText; use crate::cli; fn err_conv(sofar: &str) { - cli::err_msg(&format!("%{}: invalid conversion specification", sofar)); + show_error!("%{}: invalid conversion specification", sofar); exit(cli::EXIT_ERR); } diff --git a/src/uu/ptx/Cargo.toml b/src/uu/ptx/Cargo.toml index 852379e15..fea4e5c1f 100644 --- a/src/uu/ptx/Cargo.toml +++ b/src/uu/ptx/Cargo.toml @@ -27,3 +27,7 @@ uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_p [[bin]] name = "ptx" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/ptx/src/ptx.rs b/src/uu/ptx/src/ptx.rs index 01b14bc4d..3619b8d4d 100644 --- a/src/uu/ptx/src/ptx.rs +++ b/src/uu/ptx/src/ptx.rs @@ -17,6 +17,7 @@ use std::collections::{BTreeSet, HashMap, HashSet}; use std::default::Default; use std::fs::File; use std::io::{stdin, stdout, BufRead, BufReader, BufWriter, Read, Write}; +use uucore::display::Quotable; use uucore::InvalidEncodingHandling; static NAME: &str = "ptx"; @@ -292,7 +293,11 @@ fn create_word_set(config: &Config, filter: &WordFilter, file_map: &FileMap) -> fn get_reference(config: &Config, word_ref: &WordRef, line: &str, context_reg: &Regex) -> String { if config.auto_ref { - format!("{}:{}", word_ref.filename, word_ref.local_line_nr + 1) + format!( + "{}:{}", + word_ref.filename.maybe_quote(), + word_ref.local_line_nr + 1 + ) } else if config.input_ref { let (beg, end) = match context_reg.find(line) { Some(x) => (x.start(), x.end()), @@ -659,7 +664,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .name(NAME) .version(crate_version!()) .usage(BRIEF) diff --git a/src/uu/pwd/src/pwd.rs b/src/uu/pwd/src/pwd.rs index 37effe618..1138dba8e 100644 --- a/src/uu/pwd/src/pwd.rs +++ b/src/uu/pwd/src/pwd.rs @@ -11,62 +11,152 @@ extern crate uucore; use clap::{crate_version, App, Arg}; use std::env; use std::io; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; -use uucore::error::{FromIo, UResult, USimpleError}; +use uucore::display::println_verbatim; +use uucore::error::{FromIo, UResult}; static ABOUT: &str = "Display the full filename of the current working directory."; static OPT_LOGICAL: &str = "logical"; static OPT_PHYSICAL: &str = "physical"; -pub fn absolute_path(path: &Path) -> io::Result { - let path_buf = path.canonicalize()?; +fn physical_path() -> io::Result { + // std::env::current_dir() is a thin wrapper around libc's getcwd(). - #[cfg(windows)] - let path_buf = Path::new( - path_buf - .as_path() - .to_string_lossy() - .trim_start_matches(r"\\?\"), - ) - .to_path_buf(); + // On Unix, getcwd() must return the physical path: + // https://pubs.opengroup.org/onlinepubs/9699919799/functions/getcwd.html + #[cfg(unix)] + { + env::current_dir() + } - Ok(path_buf) + // On Windows we have to resolve it. + // On other systems we also resolve it, just in case. + #[cfg(not(unix))] + { + env::current_dir().and_then(|path| path.canonicalize()) + } } -fn get_usage() -> String { - format!("{0} [OPTION]... FILE...", executable!()) +fn logical_path() -> io::Result { + // getcwd() on Windows seems to include symlinks, so this is easy. + #[cfg(windows)] + { + env::current_dir() + } + + // If we're not on Windows we do things Unix-style. + // + // Typical Unix-like kernels don't actually keep track of the logical working + // directory. They know the precise directory a process is in, and the getcwd() + // syscall reconstructs a path from that. + // + // The logical working directory is maintained by the shell, in the $PWD + // environment variable. So we check carefully if that variable looks + // reasonable, and if not then we fall back to the physical path. + // + // POSIX: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/pwd.html + #[cfg(not(windows))] + { + use std::path::Path; + fn looks_reasonable(path: &Path) -> bool { + // First, check if it's an absolute path. + if !path.has_root() { + return false; + } + + // Then, make sure there are no . or .. components. + // Path::components() isn't useful here, it normalizes those out. + + // to_string_lossy() may allocate, but that's fine, we call this + // only once per run. It may also lose information, but not any + // information that we need for this check. + if path + .to_string_lossy() + .split(std::path::is_separator) + .any(|piece| piece == "." || piece == "..") + { + return false; + } + + // Finally, check if it matches the directory we're in. + #[cfg(unix)] + { + use std::fs::metadata; + use std::os::unix::fs::MetadataExt; + let path_info = match metadata(path) { + Ok(info) => info, + Err(_) => return false, + }; + let real_info = match metadata(".") { + Ok(info) => info, + Err(_) => return false, + }; + if path_info.dev() != real_info.dev() || path_info.ino() != real_info.ino() { + return false; + } + } + + #[cfg(not(unix))] + { + use std::fs::canonicalize; + let canon_path = match canonicalize(path) { + Ok(path) => path, + Err(_) => return false, + }; + let real_path = match canonicalize(".") { + Ok(path) => path, + Err(_) => return false, + }; + if canon_path != real_path { + return false; + } + } + + true + } + + match env::var_os("PWD").map(PathBuf::from) { + Some(value) if looks_reasonable(&value) => Ok(value), + _ => env::current_dir(), + } + } +} + +fn usage() -> String { + format!("{0} [OPTION]... FILE...", uucore::execution_phrase()) } #[uucore_procs::gen_uumain] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let usage = get_usage(); + let usage = usage(); let matches = uu_app().usage(&usage[..]).get_matches_from(args); + let cwd = if matches.is_present(OPT_LOGICAL) { + logical_path() + } else { + physical_path() + } + .map_err_context(|| "failed to get current directory".to_owned())?; - match env::current_dir() { - Ok(logical_path) => { - if matches.is_present(OPT_LOGICAL) { - println!("{}", logical_path.display()); - } else { - let physical_path = absolute_path(&logical_path) - .map_err_context(|| "failed to get absolute path".to_string())?; - println!("{}", physical_path.display()); - } - } - Err(e) => { - return Err(USimpleError::new( - 1, - format!("failed to get current directory {}", e), - )) - } - }; + // \\?\ is a prefix Windows gives to paths under certain circumstances, + // including when canonicalizing them. + // With the right extension trait we can remove it non-lossily, but + // we print it lossily anyway, so no reason to bother. + #[cfg(windows)] + let cwd = cwd + .to_string_lossy() + .strip_prefix(r"\\?\") + .map(Into::into) + .unwrap_or(cwd); + + println_verbatim(&cwd).map_err_context(|| "failed to print current directory".to_owned())?; Ok(()) } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .arg( @@ -79,6 +169,7 @@ pub fn uu_app() -> App<'static, 'static> { Arg::with_name(OPT_PHYSICAL) .short("P") .long(OPT_PHYSICAL) + .overrides_with(OPT_LOGICAL) .help("avoid all symlinks"), ) } diff --git a/src/uu/readlink/Cargo.toml b/src/uu/readlink/Cargo.toml index 9e0f939d1..8552f611d 100644 --- a/src/uu/readlink/Cargo.toml +++ b/src/uu/readlink/Cargo.toml @@ -23,3 +23,7 @@ uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_p [[bin]] name = "readlink" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/readlink/src/readlink.rs b/src/uu/readlink/src/readlink.rs index 826fa0254..d6dd1634a 100644 --- a/src/uu/readlink/src/readlink.rs +++ b/src/uu/readlink/src/readlink.rs @@ -14,9 +14,9 @@ use clap::{crate_version, App, Arg}; use std::fs; use std::io::{stdout, Write}; use std::path::{Path, PathBuf}; -use uucore::fs::{canonicalize, CanonicalizeMode}; +use uucore::display::Quotable; +use uucore::fs::{canonicalize, MissingHandling, ResolveMode}; -const NAME: &str = "readlink"; const ABOUT: &str = "Print value of a symbolic link or canonical file name."; const OPT_CANONICALIZE: &str = "canonicalize"; const OPT_CANONICALIZE_MISSING: &str = "canonicalize-missing"; @@ -29,12 +29,12 @@ const OPT_ZERO: &str = "zero"; const ARG_FILES: &str = "files"; -fn get_usage() -> String { - format!("{0} [OPTION]... [FILE]...", executable!()) +fn usage() -> String { + format!("{0} [OPTION]... [FILE]...", uucore::execution_phrase()) } pub fn uumain(args: impl uucore::Args) -> i32 { - let usage = get_usage(); + let usage = usage(); let matches = uu_app().usage(&usage[..]).get_matches_from(args); let mut no_newline = matches.is_present(OPT_NO_NEWLINE); @@ -42,14 +42,21 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let silent = matches.is_present(OPT_SILENT) || matches.is_present(OPT_QUIET); let verbose = matches.is_present(OPT_VERBOSE); - let can_mode = if matches.is_present(OPT_CANONICALIZE) { - CanonicalizeMode::Normal - } else if matches.is_present(OPT_CANONICALIZE_EXISTING) { - CanonicalizeMode::Existing - } else if matches.is_present(OPT_CANONICALIZE_MISSING) { - CanonicalizeMode::Missing + let res_mode = if matches.is_present(OPT_CANONICALIZE) + || matches.is_present(OPT_CANONICALIZE_EXISTING) + || matches.is_present(OPT_CANONICALIZE_MISSING) + { + ResolveMode::Logical } else { - CanonicalizeMode::None + ResolveMode::None + }; + + let can_mode = if matches.is_present(OPT_CANONICALIZE_EXISTING) { + MissingHandling::Existing + } else if matches.is_present(OPT_CANONICALIZE_MISSING) { + MissingHandling::Missing + } else { + MissingHandling::Normal }; let files: Vec = matches @@ -59,34 +66,34 @@ pub fn uumain(args: impl uucore::Args) -> i32 { if files.is_empty() { crash!( 1, - "missing operand\nTry {} --help for more information", - NAME + "missing operand\nTry '{} --help' for more information", + uucore::execution_phrase() ); } if no_newline && files.len() > 1 && !silent { - eprintln!("{}: ignoring --no-newline with multiple arguments", NAME); + show_error!("ignoring --no-newline with multiple arguments"); no_newline = false; } for f in &files { let p = PathBuf::from(f); - if can_mode == CanonicalizeMode::None { + if res_mode == ResolveMode::None { match fs::read_link(&p) { Ok(path) => show(&path, no_newline, use_zero), Err(err) => { if verbose { - eprintln!("{}: {}: errno {}", NAME, f, err.raw_os_error().unwrap()); + show_error!("{}: errno {}", f.maybe_quote(), err.raw_os_error().unwrap()); } return 1; } } } else { - match canonicalize(&p, can_mode) { + match canonicalize(&p, can_mode, res_mode) { Ok(path) => show(&path, no_newline, use_zero), Err(err) => { if verbose { - eprintln!("{}: {}: errno {:?}", NAME, f, err.raw_os_error().unwrap()); + show_error!("{}: errno {}", f.maybe_quote(), err.raw_os_error().unwrap()); } return 1; } @@ -98,7 +105,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .arg( diff --git a/src/uu/realpath/Cargo.toml b/src/uu/realpath/Cargo.toml index f5b9af2e7..3916c4ce6 100644 --- a/src/uu/realpath/Cargo.toml +++ b/src/uu/realpath/Cargo.toml @@ -22,3 +22,7 @@ uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_p [[bin]] name = "realpath" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/realpath/src/realpath.rs b/src/uu/realpath/src/realpath.rs index fe2ad4ccc..d13aed6c7 100644 --- a/src/uu/realpath/src/realpath.rs +++ b/src/uu/realpath/src/realpath.rs @@ -11,23 +11,33 @@ extern crate uucore; use clap::{crate_version, App, Arg}; -use std::path::{Path, PathBuf}; -use uucore::fs::{canonicalize, CanonicalizeMode}; +use std::{ + io::{stdout, Write}, + path::{Path, PathBuf}, +}; +use uucore::{ + display::{print_verbatim, Quotable}, + fs::{canonicalize, MissingHandling, ResolveMode}, +}; static ABOUT: &str = "print the resolved path"; static OPT_QUIET: &str = "quiet"; static OPT_STRIP: &str = "strip"; static OPT_ZERO: &str = "zero"; +static OPT_PHYSICAL: &str = "physical"; +static OPT_LOGICAL: &str = "logical"; +const OPT_CANONICALIZE_MISSING: &str = "canonicalize-missing"; +const OPT_CANONICALIZE_EXISTING: &str = "canonicalize-existing"; static ARG_FILES: &str = "files"; -fn get_usage() -> String { - format!("{0} [OPTION]... FILE...", executable!()) +fn usage() -> String { + format!("{0} [OPTION]... FILE...", uucore::execution_phrase()) } pub fn uumain(args: impl uucore::Args) -> i32 { - let usage = get_usage(); + let usage = usage(); let matches = uu_app().usage(&usage[..]).get_matches_from(args); @@ -42,11 +52,19 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let strip = matches.is_present(OPT_STRIP); let zero = matches.is_present(OPT_ZERO); let quiet = matches.is_present(OPT_QUIET); + let logical = matches.is_present(OPT_LOGICAL); + let can_mode = if matches.is_present(OPT_CANONICALIZE_EXISTING) { + MissingHandling::Existing + } else if matches.is_present(OPT_CANONICALIZE_MISSING) { + MissingHandling::Missing + } else { + MissingHandling::Normal + }; let mut retcode = 0; for path in &paths { - if let Err(e) = resolve_path(path, strip, zero) { + if let Err(e) = resolve_path(path, strip, zero, logical, can_mode) { if !quiet { - show_error!("{}: {}", e, path.display()); + show_error!("{}: {}", path.maybe_quote(), e); } retcode = 1 }; @@ -55,7 +73,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .arg( @@ -76,6 +94,37 @@ pub fn uu_app() -> App<'static, 'static> { .long(OPT_ZERO) .help("Separate output filenames with \\0 rather than newline"), ) + .arg( + Arg::with_name(OPT_LOGICAL) + .short("L") + .long(OPT_LOGICAL) + .help("resolve '..' components before symlinks"), + ) + .arg( + Arg::with_name(OPT_PHYSICAL) + .short("P") + .long(OPT_PHYSICAL) + .overrides_with_all(&[OPT_STRIP, OPT_LOGICAL]) + .help("resolve symlinks as encountered (default)"), + ) + .arg( + Arg::with_name(OPT_CANONICALIZE_EXISTING) + .short("e") + .long(OPT_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(ARG_FILES) .multiple(true) @@ -96,14 +145,24 @@ pub fn uu_app() -> App<'static, 'static> { /// /// This function returns an error if there is a problem resolving /// symbolic links. -fn resolve_path(p: &Path, strip: bool, zero: bool) -> std::io::Result<()> { - let mode = if strip { - CanonicalizeMode::None +fn resolve_path( + p: &Path, + strip: bool, + zero: bool, + logical: bool, + can_mode: MissingHandling, +) -> std::io::Result<()> { + let resolve = if strip { + ResolveMode::None + } else if logical { + ResolveMode::Logical } else { - CanonicalizeMode::Normal + ResolveMode::Physical }; - let abs = canonicalize(p, mode)?; - let line_ending = if zero { '\0' } else { '\n' }; - print!("{}{}", abs.display(), line_ending); + let abs = canonicalize(p, can_mode, resolve)?; + let line_ending = if zero { b'\0' } else { b'\n' }; + + print_verbatim(&abs)?; + stdout().write_all(&[line_ending])?; Ok(()) } diff --git a/src/uu/relpath/Cargo.toml b/src/uu/relpath/Cargo.toml index 89376c12d..bcb048af9 100644 --- a/src/uu/relpath/Cargo.toml +++ b/src/uu/relpath/Cargo.toml @@ -22,3 +22,7 @@ uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_p [[bin]] name = "relpath" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/relpath/src/relpath.rs b/src/uu/relpath/src/relpath.rs index cb0fba7cc..16b920861 100644 --- a/src/uu/relpath/src/relpath.rs +++ b/src/uu/relpath/src/relpath.rs @@ -7,13 +7,11 @@ // spell-checker:ignore (ToDO) subpath absto absfrom absbase -#[macro_use] -extern crate uucore; - use clap::{crate_version, App, Arg}; use std::env; use std::path::{Path, PathBuf}; -use uucore::fs::{canonicalize, CanonicalizeMode}; +use uucore::display::println_verbatim; +use uucore::fs::{canonicalize, MissingHandling, ResolveMode}; use uucore::InvalidEncodingHandling; static ABOUT: &str = "Convert TO destination to the relative path from the FROM dir. @@ -25,15 +23,15 @@ mod options { pub const FROM: &str = "FROM"; } -fn get_usage() -> String { - format!("{} [-d DIR] TO [FROM]", executable!()) +fn usage() -> String { + format!("{} [-d DIR] TO [FROM]", uucore::execution_phrase()) } pub fn uumain(args: impl uucore::Args) -> i32 { let args = args .collect_str(InvalidEncodingHandling::ConvertLossy) .accept_any(); - let usage = get_usage(); + let usage = usage(); let matches = uu_app().usage(&usage[..]).get_matches_from(args); @@ -42,16 +40,16 @@ pub fn uumain(args: impl uucore::Args) -> i32 { Some(p) => Path::new(p).to_path_buf(), None => env::current_dir().unwrap(), }; - let absto = canonicalize(to, CanonicalizeMode::Normal).unwrap(); - let absfrom = canonicalize(from, CanonicalizeMode::Normal).unwrap(); + let absto = canonicalize(to, MissingHandling::Normal, ResolveMode::Logical).unwrap(); + let absfrom = canonicalize(from, MissingHandling::Normal, ResolveMode::Logical).unwrap(); if matches.is_present(options::DIR) { let base = Path::new(&matches.value_of(options::DIR).unwrap()).to_path_buf(); - let absbase = canonicalize(base, CanonicalizeMode::Normal).unwrap(); + let absbase = canonicalize(base, MissingHandling::Normal, ResolveMode::Logical).unwrap(); if !absto.as_path().starts_with(absbase.as_path()) || !absfrom.as_path().starts_with(absbase.as_path()) { - println!("{}", absto.display()); + println_verbatim(absto).unwrap(); return 0; } } @@ -77,12 +75,12 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .map(|x| result.push(x.as_os_str())) .last(); - println!("{}", result.display()); + println_verbatim(result).unwrap(); 0 } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .arg( diff --git a/src/uu/rm/Cargo.toml b/src/uu/rm/Cargo.toml index 2c30446e8..6099d137a 100644 --- a/src/uu/rm/Cargo.toml +++ b/src/uu/rm/Cargo.toml @@ -18,12 +18,16 @@ path = "src/rm.rs" clap = { version = "2.33", features = ["wrap_help"] } walkdir = "2.2" remove_dir_all = "0.5.1" -winapi = { version="0.3", features=[] } - uucore = { version=">=0.0.9", package="uucore", path="../../uucore", features=["fs"] } uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_procs" } +[target.'cfg(windows)'.dependencies] +winapi = { version="0.3", features=[] } [[bin]] name = "rm" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/rm/src/rm.rs b/src/uu/rm/src/rm.rs index 259d1ab39..54fce52ff 100644 --- a/src/uu/rm/src/rm.rs +++ b/src/uu/rm/src/rm.rs @@ -17,6 +17,7 @@ use std::fs; use std::io::{stderr, stdin, BufRead, Write}; use std::ops::BitOr; use std::path::{Path, PathBuf}; +use uucore::display::Quotable; use walkdir::{DirEntry, WalkDir}; #[derive(Eq, PartialEq, Clone, Copy)] @@ -52,8 +53,8 @@ static OPT_VERBOSE: &str = "verbose"; static ARG_FILES: &str = "files"; -fn get_usage() -> String { - format!("{0} [OPTION]... FILE...", executable!()) +fn usage() -> String { + format!("{0} [OPTION]... FILE...", uucore::execution_phrase()) } fn get_long_usage() -> String { @@ -74,7 +75,7 @@ fn get_long_usage() -> String { } pub fn uumain(args: impl uucore::Args) -> i32 { - let usage = get_usage(); + let usage = usage(); let long_usage = get_long_usage(); let matches = uu_app() @@ -93,7 +94,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { // 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!()); + show_error!("for help, try '{0} --help'", uucore::execution_phrase()); return 1; } else { let options = Options { @@ -140,7 +141,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) @@ -236,7 +237,10 @@ fn remove(files: Vec, options: Options) -> bool { // (e.g., permission), even rm -f should fail with // outputting the error, but there's no easy eay. if !options.force { - show_error!("cannot remove '{}': No such file or directory", filename); + show_error!( + "cannot remove {}: No such file or directory", + filename.quote() + ); true } else { false @@ -263,13 +267,9 @@ fn handle_dir(path: &Path, options: &Options) -> bool { // GNU compatibility (rm/fail-eacces.sh) // here, GNU doesn't use some kind of remove_dir_all // It will show directory+file - show_error!( - "cannot remove '{}': {}", - path.display(), - "Permission denied" - ); + show_error!("cannot remove {}: {}", path.quote(), "Permission denied"); } else { - show_error!("cannot remove '{}': {}", path.display(), e); + show_error!("cannot remove {}: {}", path.quote(), e); } } } else { @@ -287,7 +287,7 @@ fn handle_dir(path: &Path, options: &Options) -> bool { } Err(e) => { had_err = true; - show_error!("recursing in '{}': {}", path.display(), e); + show_error!("recursing in {}: {}", path.quote(), e); } } } @@ -299,12 +299,12 @@ fn handle_dir(path: &Path, options: &Options) -> bool { } else if options.dir && (!is_root || !options.preserve_root) { had_err = remove_dir(path, options).bitor(had_err); } else if options.recursive { - show_error!("could not remove directory '{}'", path.display()); + show_error!("could not remove directory {}", path.quote()); had_err = true; } else { show_error!( - "cannot remove '{}': Is a directory", // GNU's rm error message does not include help - path.display() + "cannot remove {}: Is a directory", // GNU's rm error message does not include help + path.quote() ); had_err = true; } @@ -325,36 +325,36 @@ fn remove_dir(path: &Path, options: &Options) -> bool { match fs::remove_dir(path) { Ok(_) => { if options.verbose { - println!("removed directory '{}'", normalize(path).display()); + println!("removed directory {}", normalize(path).quote()); } } Err(e) => { if e.kind() == std::io::ErrorKind::PermissionDenied { // GNU compatibility (rm/fail-eacces.sh) show_error!( - "cannot remove '{}': {}", - path.display(), + "cannot remove {}: {}", + path.quote(), "Permission denied" ); } else { - show_error!("cannot remove '{}': {}", path.display(), e); + show_error!("cannot remove {}: {}", path.quote(), e); } return true; } } } else { // directory can be read but is not empty - show_error!("cannot remove '{}': Directory not empty", path.display()); + show_error!("cannot remove {}: Directory not empty", path.quote()); return true; } } else { // called to remove a symlink_dir (windows) without "-r"/"-R" or "-d" - show_error!("cannot remove '{}': Is a directory", path.display()); + show_error!("cannot remove {}: Is a directory", path.quote()); return true; } } else { // GNU's rm shows this message if directory is empty but not readable - show_error!("cannot remove '{}': Directory not empty", path.display()); + show_error!("cannot remove {}: Directory not empty", path.quote()); return true; } } @@ -372,19 +372,15 @@ fn remove_file(path: &Path, options: &Options) -> bool { match fs::remove_file(path) { Ok(_) => { if options.verbose { - println!("removed '{}'", normalize(path).display()); + println!("removed {}", normalize(path).quote()); } } Err(e) => { if e.kind() == std::io::ErrorKind::PermissionDenied { // GNU compatibility (rm/fail-eacces.sh) - show_error!( - "cannot remove '{}': {}", - path.display(), - "Permission denied" - ); + show_error!("cannot remove {}: {}", path.quote(), "Permission denied"); } else { - show_error!("cannot remove '{}': {}", path.display(), e); + show_error!("cannot remove {}: {}", path.quote(), e); } return true; } @@ -396,9 +392,9 @@ fn remove_file(path: &Path, options: &Options) -> bool { fn prompt_file(path: &Path, is_dir: bool) -> bool { if is_dir { - prompt(&(format!("rm: remove directory '{}'? ", path.display()))) + prompt(&(format!("rm: remove directory {}? ", path.quote()))) } else { - prompt(&(format!("rm: remove file '{}'? ", path.display()))) + prompt(&(format!("rm: remove file {}? ", path.quote()))) } } diff --git a/src/uu/rmdir/Cargo.toml b/src/uu/rmdir/Cargo.toml index 27d94ec1d..cdb08f908 100644 --- a/src/uu/rmdir/Cargo.toml +++ b/src/uu/rmdir/Cargo.toml @@ -18,6 +18,7 @@ path = "src/rmdir.rs" clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.9", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_procs" } +libc = "0.2.42" [[bin]] name = "rmdir" diff --git a/src/uu/rmdir/src/rmdir.rs b/src/uu/rmdir/src/rmdir.rs index 8dbaf79a8..f982cf4c3 100644 --- a/src/uu/rmdir/src/rmdir.rs +++ b/src/uu/rmdir/src/rmdir.rs @@ -11,8 +11,12 @@ extern crate uucore; use clap::{crate_version, App, Arg}; -use std::fs; +use std::fs::{read_dir, remove_dir}; +use std::io; use std::path::Path; +use uucore::display::Quotable; +use uucore::error::{set_exit_code, strip_errno, UResult}; +use uucore::util_name; static ABOUT: &str = "Remove the DIRECTORY(ies), if they are empty."; static OPT_IGNORE_FAIL_NON_EMPTY: &str = "ignore-fail-on-non-empty"; @@ -21,39 +25,158 @@ static OPT_VERBOSE: &str = "verbose"; static ARG_DIRS: &str = "dirs"; -#[cfg(unix)] -static ENOTDIR: i32 = 20; -#[cfg(windows)] -static ENOTDIR: i32 = 267; - -fn get_usage() -> String { - format!("{0} [OPTION]... DIRECTORY...", executable!()) +fn usage() -> String { + format!("{0} [OPTION]... DIRECTORY...", uucore::execution_phrase()) } -pub fn uumain(args: impl uucore::Args) -> i32 { - let usage = get_usage(); +#[uucore_procs::gen_uumain] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let usage = usage(); 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 opts = Opts { + ignore: matches.is_present(OPT_IGNORE_FAIL_NON_EMPTY), + parents: matches.is_present(OPT_PARENTS), + verbose: matches.is_present(OPT_VERBOSE), + }; - let ignore = matches.is_present(OPT_IGNORE_FAIL_NON_EMPTY); - let parents = matches.is_present(OPT_PARENTS); - let verbose = matches.is_present(OPT_VERBOSE); + for path in matches + .values_of_os(ARG_DIRS) + .unwrap_or_default() + .map(Path::new) + { + if let Err(error) = remove(path, opts) { + let Error { error, path } = error; - match remove(dirs, ignore, parents, verbose) { - Ok(()) => ( /* pass */ ), - Err(e) => return e, + if opts.ignore && dir_not_empty(&error, path) { + continue; + } + + set_exit_code(1); + + // If `foo` is a symlink to a directory then `rmdir foo/` may give + // a "not a directory" error. This is confusing as `rm foo/` says + // "is a directory". + // This differs from system to system. Some don't give an error. + // Windows simply allows calling RemoveDirectory on symlinks so we + // don't need to worry about it here. + // GNU rmdir seems to print "Symbolic link not followed" if: + // - It has a trailing slash + // - It's a symlink + // - It either points to a directory or dangles + #[cfg(unix)] + { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + + fn is_symlink(path: &Path) -> io::Result { + Ok(path.symlink_metadata()?.file_type().is_symlink()) + } + + fn points_to_directory(path: &Path) -> io::Result { + Ok(path.metadata()?.file_type().is_dir()) + } + + let bytes = path.as_os_str().as_bytes(); + if error.raw_os_error() == Some(libc::ENOTDIR) && bytes.ends_with(b"/") { + // Strip the trailing slash or .symlink_metadata() will follow the symlink + let no_slash: &Path = OsStr::from_bytes(&bytes[..bytes.len() - 1]).as_ref(); + if is_symlink(no_slash).unwrap_or(false) + && points_to_directory(no_slash).unwrap_or(true) + { + show_error!( + "failed to remove {}: Symbolic link not followed", + path.quote() + ); + continue; + } + } + } + + show_error!("failed to remove {}: {}", path.quote(), strip_errno(&error)); + } } - 0 + Ok(()) +} + +struct Error<'a> { + error: io::Error, + path: &'a Path, +} + +fn remove(mut path: &Path, opts: Opts) -> Result<(), Error<'_>> { + remove_single(path, opts)?; + if opts.parents { + while let Some(new) = path.parent() { + path = new; + if path.as_os_str() == "" { + break; + } + remove_single(path, opts)?; + } + } + Ok(()) +} + +fn remove_single(path: &Path, opts: Opts) -> Result<(), Error<'_>> { + if opts.verbose { + println!("{}: removing directory, {}", util_name(), path.quote()); + } + remove_dir(path).map_err(|error| Error { error, path }) +} + +// POSIX: https://pubs.opengroup.org/onlinepubs/009696799/functions/rmdir.html +#[cfg(not(windows))] +const NOT_EMPTY_CODES: &[i32] = &[libc::ENOTEMPTY, libc::EEXIST]; + +// 145 is ERROR_DIR_NOT_EMPTY, determined experimentally. +#[cfg(windows)] +const NOT_EMPTY_CODES: &[i32] = &[145]; + +// Other error codes you might get for directories that could be found and are +// not empty. +// This is a subset of the error codes listed in rmdir(2) from the Linux man-pages +// project. Maybe other systems have additional codes that apply? +#[cfg(not(windows))] +const PERHAPS_EMPTY_CODES: &[i32] = &[libc::EACCES, libc::EBUSY, libc::EPERM, libc::EROFS]; + +// Probably incomplete, I can't find a list of possible errors for +// RemoveDirectory anywhere. +#[cfg(windows)] +const PERHAPS_EMPTY_CODES: &[i32] = &[ + 5, // ERROR_ACCESS_DENIED, found experimentally. +]; + +fn dir_not_empty(error: &io::Error, path: &Path) -> bool { + if let Some(code) = error.raw_os_error() { + if NOT_EMPTY_CODES.contains(&code) { + return true; + } + // If --ignore-fail-on-non-empty is used then we want to ignore all errors + // for non-empty directories, even if the error was e.g. because there's + // no permission. So we do an additional check. + if PERHAPS_EMPTY_CODES.contains(&code) { + if let Ok(mut iterator) = read_dir(path) { + if iterator.next().is_some() { + return true; + } + } + } + } + false +} + +#[derive(Clone, Copy, Debug)] +struct Opts { + ignore: bool, + parents: bool, + verbose: bool, } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .arg( @@ -84,57 +207,3 @@ pub fn uu_app() -> App<'static, 'static> { .required(true), ) } - -fn remove(dirs: Vec, ignore: bool, parents: bool, verbose: bool) -> Result<(), i32> { - let mut r = Ok(()); - - for dir in &dirs { - let path = Path::new(&dir[..]); - r = remove_dir(path, ignore, verbose).and(r); - if parents { - let mut p = path; - while let Some(new_p) = p.parent() { - p = new_p; - match p.as_os_str().to_str() { - None => break, - Some(s) => match s { - "" | "." | "/" => break, - _ => (), - }, - }; - r = remove_dir(p, ignore, verbose).and(r); - } - } - } - - r -} - -fn remove_dir(path: &Path, ignore: bool, verbose: bool) -> Result<(), i32> { - let mut read_dir = fs::read_dir(path).map_err(|e| { - if e.raw_os_error() == Some(ENOTDIR) { - show_error!("failed to remove '{}': Not a directory", path.display()); - } else { - show_error!("reading directory '{}': {}", path.display(), e); - } - 1 - })?; - - let mut r = Ok(()); - - if read_dir.next().is_none() { - match fs::remove_dir(path) { - Err(e) => { - show_error!("removing directory '{}': {}", path.display(), e); - r = Err(1); - } - Ok(_) if verbose => println!("removing directory, '{}'", path.display()), - _ => (), - } - } else if !ignore { - show_error!("failed to remove '{}': Directory not empty", path.display()); - r = Err(1); - } - - r -} diff --git a/src/uu/runcon/Cargo.toml b/src/uu/runcon/Cargo.toml new file mode 100644 index 000000000..4e4c0bed6 --- /dev/null +++ b/src/uu/runcon/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "uu_runcon" +version = "0.0.7" +authors = ["uutils developers"] +license = "MIT" +description = "runcon ~ (uutils) run command with specified security context" +homepage = "https://github.com/uutils/coreutils" +repository = "https://github.com/uutils/coreutils/tree/master/src/uu/runcon" +keywords = ["coreutils", "uutils", "cli", "utility"] +categories = ["command-line-utilities"] +edition = "2018" + +[lib] +path = "src/runcon.rs" + +[dependencies] +clap = { version = "2.33", features = ["wrap_help"] } +uucore = { version = ">=0.0.9", package="uucore", path="../../uucore", features=["entries", "fs", "perms"] } +uucore_procs = { version = ">=0.0.6", package="uucore_procs", path="../../uucore_procs" } +selinux = { version = "0.2" } +fts-sys = { version = "0.2" } +thiserror = { version = "1.0" } +libc = { version = "0.2" } + +[[bin]] +name = "runcon" +path = "src/main.rs" diff --git a/src/uu/runcon/src/errors.rs b/src/uu/runcon/src/errors.rs new file mode 100644 index 000000000..5f8258de0 --- /dev/null +++ b/src/uu/runcon/src/errors.rs @@ -0,0 +1,75 @@ +use std::ffi::OsString; +use std::fmt::Write; +use std::io; +use std::str::Utf8Error; + +use uucore::display::Quotable; + +pub(crate) type Result = std::result::Result; + +#[derive(thiserror::Error, Debug)] +pub(crate) enum Error { + #[error("No command is specified")] + MissingCommand, + + #[error("SELinux is not enabled")] + SELinuxNotEnabled, + + #[error(transparent)] + NotUTF8(#[from] Utf8Error), + + #[error(transparent)] + CommandLine(#[from] clap::Error), + + #[error("{operation} failed")] + SELinux { + operation: &'static str, + source: selinux::errors::Error, + }, + + #[error("{operation} failed")] + Io { + operation: &'static str, + source: io::Error, + }, + + #[error("{operation} failed on {}", .operand1.quote())] + Io1 { + operation: &'static str, + operand1: OsString, + source: io::Error, + }, +} + +impl Error { + pub(crate) fn from_io(operation: &'static str, source: io::Error) -> Self { + Self::Io { operation, source } + } + + pub(crate) fn from_io1( + operation: &'static str, + operand1: impl Into, + source: io::Error, + ) -> Self { + Self::Io1 { + operation, + operand1: operand1.into(), + source, + } + } + + pub(crate) fn from_selinux(operation: &'static str, source: selinux::errors::Error) -> Self { + Self::SELinux { operation, source } + } +} + +pub(crate) fn report_full_error(mut err: &dyn std::error::Error) -> String { + let mut desc = String::with_capacity(256); + write!(&mut desc, "{}", err).unwrap(); + while let Some(source) = err.source() { + err = source; + write!(&mut desc, ": {}", err).unwrap(); + } + desc.push('.'); + desc +} diff --git a/src/uu/runcon/src/main.rs b/src/uu/runcon/src/main.rs new file mode 100644 index 000000000..86aae54e5 --- /dev/null +++ b/src/uu/runcon/src/main.rs @@ -0,0 +1 @@ +uucore_procs::main!(uu_runcon); diff --git a/src/uu/runcon/src/runcon.rs b/src/uu/runcon/src/runcon.rs new file mode 100644 index 000000000..b2f1468bd --- /dev/null +++ b/src/uu/runcon/src/runcon.rs @@ -0,0 +1,450 @@ +// spell-checker:ignore (vars) RFILE + +use uucore::{show_error, show_usage_error}; + +use clap::{App, Arg}; +use selinux::{OpaqueSecurityContext, SecurityClass, SecurityContext}; + +use std::borrow::Cow; +use std::ffi::{CStr, CString, OsStr, OsString}; +use std::os::raw::c_char; +use std::os::unix::ffi::OsStrExt; +use std::{io, ptr}; + +mod errors; + +use errors::{report_full_error, Error, Result}; + +const VERSION: &str = env!("CARGO_PKG_VERSION"); +const ABOUT: &str = "Run command with specified security context."; +const DESCRIPTION: &str = "Run COMMAND with completely-specified CONTEXT, or with current or \ + transitioned security context modified by one or more of \ + LEVEL, ROLE, TYPE, and USER.\n\n\ + If none of --compute, --type, --user, --role or --range is specified, \ + then the first argument is used as the complete context.\n\n\ + Note that only carefully-chosen contexts are likely to successfully run.\n\n\ + With neither CONTEXT nor COMMAND are specified, \ + then this prints the current security context."; + +pub mod options { + pub const COMPUTE: &str = "compute"; + + pub const USER: &str = "user"; + pub const ROLE: &str = "role"; + pub const TYPE: &str = "type"; + pub const RANGE: &str = "range"; +} + +// This list is NOT exhaustive. This command might perform an `execvp()` to run +// a different program. When that happens successfully, the exit status of this +// process will be the exit status of that program. +mod error_exit_status { + pub const SUCCESS: i32 = libc::EXIT_SUCCESS; + pub const NOT_FOUND: i32 = 127; + pub const COULD_NOT_EXECUTE: i32 = 126; + pub const ANOTHER_ERROR: i32 = libc::EXIT_FAILURE; +} + +fn get_usage() -> String { + format!( + "{0} [CONTEXT COMMAND [ARG...]]\n \ + {0} [-c] [-u USER] [-r ROLE] [-t TYPE] [-l RANGE] COMMAND [ARG...]", + uucore::execution_phrase() + ) +} + +pub fn uumain(args: impl uucore::Args) -> i32 { + let usage = get_usage(); + + let config = uu_app().usage(usage.as_ref()); + + let options = match parse_command_line(config, args) { + Ok(r) => r, + Err(r) => { + if let Error::CommandLine(ref r) = r { + match r.kind { + clap::ErrorKind::HelpDisplayed | clap::ErrorKind::VersionDisplayed => { + println!("{}", r); + return error_exit_status::SUCCESS; + } + _ => {} + } + } + + show_usage_error!("{}.\n", r); + return error_exit_status::ANOTHER_ERROR; + } + }; + + match &options.mode { + CommandLineMode::Print => { + if let Err(r) = print_current_context() { + show_error!("{}", report_full_error(&r)); + return error_exit_status::ANOTHER_ERROR; + } + } + + CommandLineMode::PlainContext { context, command } => { + let (exit_status, err) = + if let Err(err) = get_plain_context(context).and_then(set_next_exec_context) { + (error_exit_status::ANOTHER_ERROR, err) + } else { + // On successful execution, the following call never returns, + // and this process image is replaced. + execute_command(command, &options.arguments) + }; + + show_error!("{}", report_full_error(&err)); + return exit_status; + } + + CommandLineMode::CustomContext { + compute_transition_context, + user, + role, + the_type, + range, + command, + } => { + if let Some(command) = command { + let (exit_status, err) = if let Err(err) = get_custom_context( + *compute_transition_context, + user.as_deref(), + role.as_deref(), + the_type.as_deref(), + range.as_deref(), + command, + ) + .and_then(set_next_exec_context) + { + (error_exit_status::ANOTHER_ERROR, err) + } else { + // On successful execution, the following call never returns, + // and this process image is replaced. + execute_command(command, &options.arguments) + }; + + show_error!("{}", report_full_error(&err)); + return exit_status; + } else if let Err(r) = print_current_context() { + show_error!("{}", report_full_error(&r)); + return error_exit_status::ANOTHER_ERROR; + } + } + } + + error_exit_status::SUCCESS +} + +pub fn uu_app() -> App<'static, 'static> { + App::new(uucore::util_name()) + .version(VERSION) + .about(ABOUT) + .after_help(DESCRIPTION) + .arg( + Arg::with_name(options::COMPUTE) + .short("c") + .long(options::COMPUTE) + .takes_value(false) + .help("Compute process transition context before modifying."), + ) + .arg( + Arg::with_name(options::USER) + .short("u") + .long(options::USER) + .takes_value(true) + .value_name("USER") + .help("Set user USER in the target security context."), + ) + .arg( + Arg::with_name(options::ROLE) + .short("r") + .long(options::ROLE) + .takes_value(true) + .value_name("ROLE") + .help("Set role ROLE in the target security context."), + ) + .arg( + Arg::with_name(options::TYPE) + .short("t") + .long(options::TYPE) + .takes_value(true) + .value_name("TYPE") + .help("Set type TYPE in the target security context."), + ) + .arg( + Arg::with_name(options::RANGE) + .short("l") + .long(options::RANGE) + .takes_value(true) + .value_name("RANGE") + .help("Set range RANGE in the target security context."), + ) + .arg(Arg::with_name("ARG").multiple(true)) + // Once "ARG" is parsed, everything after that belongs to it. + // + // This is not how POSIX does things, but this is how the GNU implementation + // parses its command line. + .setting(clap::AppSettings::TrailingVarArg) +} + +#[derive(Debug)] +enum CommandLineMode { + Print, + + PlainContext { + context: OsString, + command: OsString, + }, + + CustomContext { + /// Compute process transition context before modifying. + compute_transition_context: bool, + + /// Use the current context with the specified user. + user: Option, + + /// Use the current context with the specified role. + role: Option, + + /// Use the current context with the specified type. + the_type: Option, + + /// Use the current context with the specified range. + range: Option, + + // `command` can be `None`, in which case we're dealing with this syntax: + // runcon [-c] [-u USER] [-r ROLE] [-t TYPE] [-l RANGE] + // + // This syntax is undocumented, but it is accepted by the GNU implementation, + // so we do the same for compatibility. + command: Option, + }, +} + +#[derive(Debug)] +struct Options { + mode: CommandLineMode, + arguments: Vec, +} + +fn parse_command_line(config: App, args: impl uucore::Args) -> Result { + let matches = config.get_matches_from_safe(args)?; + + let compute_transition_context = matches.is_present(options::COMPUTE); + + let mut args = matches + .values_of_os("ARG") + .unwrap_or_default() + .map(OsString::from); + + if compute_transition_context + || matches.is_present(options::USER) + || matches.is_present(options::ROLE) + || matches.is_present(options::TYPE) + || matches.is_present(options::RANGE) + { + // runcon [-c] [-u USER] [-r ROLE] [-t TYPE] [-l RANGE] [COMMAND [args]] + + let mode = CommandLineMode::CustomContext { + compute_transition_context, + user: matches.value_of_os(options::USER).map(Into::into), + role: matches.value_of_os(options::ROLE).map(Into::into), + the_type: matches.value_of_os(options::TYPE).map(Into::into), + range: matches.value_of_os(options::RANGE).map(Into::into), + command: args.next(), + }; + + Ok(Options { + mode, + arguments: args.collect(), + }) + } else if let Some(context) = args.next() { + // runcon CONTEXT COMMAND [args] + + args.next() + .ok_or(Error::MissingCommand) + .map(move |command| Options { + mode: CommandLineMode::PlainContext { context, command }, + arguments: args.collect(), + }) + } else { + // runcon + + Ok(Options { + mode: CommandLineMode::Print, + arguments: Vec::default(), + }) + } +} + +fn print_current_context() -> Result<()> { + let op = "Getting security context of the current process"; + let context = SecurityContext::current(false).map_err(|r| Error::from_selinux(op, r))?; + + let context = context + .to_c_string() + .map_err(|r| Error::from_selinux(op, r))?; + + if let Some(context) = context { + let context = context.as_ref().to_str()?; + println!("{}", context); + } else { + println!(); + } + Ok(()) +} + +fn set_next_exec_context(context: OpaqueSecurityContext) -> Result<()> { + let c_context = context + .to_c_string() + .map_err(|r| Error::from_selinux("Creating new context", r))?; + + let sc = SecurityContext::from_c_str(&c_context, false); + + if sc.check() != Some(true) { + let ctx = OsStr::from_bytes(c_context.as_bytes()); + let err = io::ErrorKind::InvalidInput.into(); + return Err(Error::from_io1("Checking security context", ctx, err)); + } + + sc.set_for_next_exec() + .map_err(|r| Error::from_selinux("Setting new security context", r)) +} + +fn get_plain_context(context: &OsStr) -> Result { + if selinux::kernel_support() == selinux::KernelSupport::Unsupported { + return Err(Error::SELinuxNotEnabled); + } + + let c_context = os_str_to_c_string(context)?; + + OpaqueSecurityContext::from_c_str(&c_context) + .map_err(|r| Error::from_selinux("Creating new context", r)) +} + +fn get_transition_context(command: &OsStr) -> Result { + // Generate context based on process transition. + let sec_class = SecurityClass::from_name("process") + .map_err(|r| Error::from_selinux("Getting process security class", r))?; + + // Get context of file to be executed. + let file_context = match SecurityContext::of_path(command, true, false) { + Ok(Some(context)) => context, + + Ok(None) => { + let err = io::Error::from_raw_os_error(libc::ENODATA); + return Err(Error::from_io1("getfilecon", command, err)); + } + + Err(r) => { + let op = "Getting security context of command file"; + return Err(Error::from_selinux(op, r)); + } + }; + + let process_context = SecurityContext::current(false) + .map_err(|r| Error::from_selinux("Getting security context of the current process", r))?; + + // Compute result of process transition. + process_context + .of_labeling_decision(&file_context, sec_class, "") + .map_err(|r| Error::from_selinux("Computing result of process transition", r)) +} + +fn get_initial_custom_opaque_context( + compute_transition_context: bool, + command: &OsStr, +) -> Result { + let context = if compute_transition_context { + get_transition_context(command)? + } else { + SecurityContext::current(false).map_err(|r| { + Error::from_selinux("Getting security context of the current process", r) + })? + }; + + let c_context = context + .to_c_string() + .map_err(|r| Error::from_selinux("Getting security context", r))? + .unwrap_or_else(|| Cow::Owned(CString::default())); + + OpaqueSecurityContext::from_c_str(c_context.as_ref()) + .map_err(|r| Error::from_selinux("Creating new context", r)) +} + +fn get_custom_context( + compute_transition_context: bool, + user: Option<&OsStr>, + role: Option<&OsStr>, + the_type: Option<&OsStr>, + range: Option<&OsStr>, + command: &OsStr, +) -> Result { + use OpaqueSecurityContext as OSC; + type SetNewValueProc = fn(&OSC, &CStr) -> selinux::errors::Result<()>; + + if selinux::kernel_support() == selinux::KernelSupport::Unsupported { + return Err(Error::SELinuxNotEnabled); + } + + let osc = get_initial_custom_opaque_context(compute_transition_context, command)?; + + let list: &[(Option<&OsStr>, SetNewValueProc, &'static str)] = &[ + (user, OSC::set_user, "Setting security context user"), + (role, OSC::set_role, "Setting security context role"), + (the_type, OSC::set_type, "Setting security context type"), + (range, OSC::set_range, "Setting security context range"), + ]; + + for &(new_value, method, op) in list { + if let Some(new_value) = new_value { + let c_new_value = os_str_to_c_string(new_value)?; + method(&osc, &c_new_value).map_err(|r| Error::from_selinux(op, r))?; + } + } + Ok(osc) +} + +/// The actual return type of this function should be `Result` +/// However, until the *never* type is stabilized, one way to indicate to the +/// compiler the only valid return type is to say "if this returns, it will +/// always return an error". +fn execute_command(command: &OsStr, arguments: &[OsString]) -> (i32, Error) { + let c_command = match os_str_to_c_string(command) { + Ok(v) => v, + Err(r) => return (error_exit_status::ANOTHER_ERROR, r), + }; + + let argv_storage: Vec = match arguments + .iter() + .map(AsRef::as_ref) + .map(os_str_to_c_string) + .collect::>() + { + Ok(v) => v, + Err(r) => return (error_exit_status::ANOTHER_ERROR, r), + }; + + let mut argv: Vec<*const c_char> = Vec::with_capacity(arguments.len().saturating_add(2)); + argv.push(c_command.as_ptr()); + argv.extend(argv_storage.iter().map(AsRef::as_ref).map(CStr::as_ptr)); + argv.push(ptr::null()); + + unsafe { libc::execvp(c_command.as_ptr(), argv.as_ptr()) }; + + let err = io::Error::last_os_error(); + let exit_status = if err.kind() == io::ErrorKind::NotFound { + error_exit_status::NOT_FOUND + } else { + error_exit_status::COULD_NOT_EXECUTE + }; + + let err = Error::from_io1("Executing command", command, err); + (exit_status, err) +} + +fn os_str_to_c_string(s: &OsStr) -> Result { + CString::new(s.as_bytes()) + .map_err(|_r| Error::from_io("CString::new()", io::ErrorKind::InvalidInput.into())) +} diff --git a/src/uu/seq/Cargo.toml b/src/uu/seq/Cargo.toml index 68aa87bad..4618115cb 100644 --- a/src/uu/seq/Cargo.toml +++ b/src/uu/seq/Cargo.toml @@ -24,3 +24,7 @@ uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_p [[bin]] name = "seq" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/seq/src/digits.rs b/src/uu/seq/src/digits.rs new file mode 100644 index 000000000..bde933978 --- /dev/null +++ b/src/uu/seq/src/digits.rs @@ -0,0 +1,190 @@ +//! Counting number of digits needed to represent a number. +//! +//! The [`num_integral_digits`] and [`num_fractional_digits`] functions +//! count the number of digits needed to represent a number in decimal +//! notation (like "123.456"). +use std::convert::TryInto; +use std::num::ParseIntError; + +use uucore::display::Quotable; + +/// The number of digits after the decimal point in a given number. +/// +/// The input `s` is a string representing a number, either an integer +/// or a floating point number in either decimal notation or scientific +/// notation. This function returns the number of digits after the +/// decimal point needed to print the number in decimal notation. +/// +/// # Examples +/// +/// ```rust,ignore +/// assert_eq!(num_fractional_digits("123.45e-1").unwrap(), 3); +/// ``` +pub fn num_fractional_digits(s: &str) -> Result { + match (s.find('.'), s.find('e')) { + // For example, "123456". + (None, None) => Ok(0), + + // For example, "123e456". + (None, Some(j)) => { + let exponent: i64 = s[j + 1..].parse()?; + if exponent < 0 { + Ok(-exponent as usize) + } else { + Ok(0) + } + } + + // For example, "123.456". + (Some(i), None) => Ok(s.len() - (i + 1)), + + // For example, "123.456e789". + (Some(i), Some(j)) if i < j => { + // Because of the match guard, this subtraction will not underflow. + let num_digits_between_decimal_point_and_e = (j - (i + 1)) as i64; + let exponent: i64 = s[j + 1..].parse()?; + if num_digits_between_decimal_point_and_e < exponent { + Ok(0) + } else { + Ok((num_digits_between_decimal_point_and_e - exponent) + .try_into() + .unwrap()) + } + } + _ => crash!( + 1, + "invalid floating point argument: {}\n Try '{} --help' for more information.", + s.quote(), + uucore::execution_phrase() + ), + } +} + +/// The number of digits before the decimal point in a given number. +/// +/// The input `s` is a string representing a number, either an integer +/// or a floating point number in either decimal notation or scientific +/// notation. This function returns the number of digits before the +/// decimal point needed to print the number in decimal notation. +/// +/// # Examples +/// +/// ```rust,ignore +/// assert_eq!(num_fractional_digits("123.45e-1").unwrap(), 2); +/// ``` +pub fn num_integral_digits(s: &str) -> Result { + match (s.find('.'), s.find('e')) { + // For example, "123456". + (None, None) => Ok(s.len()), + + // For example, "123e456". + (None, Some(j)) => { + let exponent: i64 = s[j + 1..].parse()?; + let total = j as i64 + exponent; + if total < 1 { + Ok(1) + } else { + Ok(total.try_into().unwrap()) + } + } + + // For example, "123.456". + (Some(i), None) => Ok(i), + + // For example, "123.456e789". + (Some(i), Some(j)) => { + let exponent: i64 = s[j + 1..].parse()?; + let minimum: usize = { + let integral_part: f64 = crash_if_err!(1, s[..j].parse()); + if integral_part == -0.0 && integral_part.is_sign_negative() { + 2 + } else { + 1 + } + }; + + let total = i as i64 + exponent; + if total < minimum as i64 { + Ok(minimum) + } else { + Ok(total.try_into().unwrap()) + } + } + } +} + +#[cfg(test)] +mod tests { + + mod test_num_integral_digits { + use crate::num_integral_digits; + + #[test] + fn test_integer() { + assert_eq!(num_integral_digits("123").unwrap(), 3); + } + + #[test] + fn test_decimal() { + assert_eq!(num_integral_digits("123.45").unwrap(), 3); + } + + #[test] + fn test_scientific_no_decimal_positive_exponent() { + assert_eq!(num_integral_digits("123e4").unwrap(), 3 + 4); + } + + #[test] + fn test_scientific_with_decimal_positive_exponent() { + assert_eq!(num_integral_digits("123.45e6").unwrap(), 3 + 6); + } + + #[test] + fn test_scientific_no_decimal_negative_exponent() { + assert_eq!(num_integral_digits("123e-4").unwrap(), 1); + } + + #[test] + fn test_scientific_with_decimal_negative_exponent() { + assert_eq!(num_integral_digits("123.45e-6").unwrap(), 1); + assert_eq!(num_integral_digits("123.45e-1").unwrap(), 2); + } + } + + mod test_num_fractional_digits { + use crate::num_fractional_digits; + + #[test] + fn test_integer() { + assert_eq!(num_fractional_digits("123").unwrap(), 0); + } + + #[test] + fn test_decimal() { + assert_eq!(num_fractional_digits("123.45").unwrap(), 2); + } + + #[test] + fn test_scientific_no_decimal_positive_exponent() { + assert_eq!(num_fractional_digits("123e4").unwrap(), 0); + } + + #[test] + fn test_scientific_with_decimal_positive_exponent() { + assert_eq!(num_fractional_digits("123.45e6").unwrap(), 0); + assert_eq!(num_fractional_digits("123.45e1").unwrap(), 1); + } + + #[test] + fn test_scientific_no_decimal_negative_exponent() { + assert_eq!(num_fractional_digits("123e-4").unwrap(), 4); + assert_eq!(num_fractional_digits("123e-1").unwrap(), 1); + } + + #[test] + fn test_scientific_with_decimal_negative_exponent() { + assert_eq!(num_fractional_digits("123.45e-6").unwrap(), 8); + assert_eq!(num_fractional_digits("123.45e-1").unwrap(), 3); + } + } +} diff --git a/src/uu/seq/src/seq.rs b/src/uu/seq/src/seq.rs index 50a93d3af..aac8f2280 100644 --- a/src/uu/seq/src/seq.rs +++ b/src/uu/seq/src/seq.rs @@ -12,9 +12,15 @@ use num_traits::One; use num_traits::Zero; use num_traits::{Num, ToPrimitive}; use std::cmp; -use std::io::{stdout, Write}; +use std::io::{stdout, ErrorKind, Write}; use std::str::FromStr; +mod digits; +use crate::digits::num_fractional_digits; +use crate::digits::num_integral_digits; + +use uucore::display::Quotable; + static ABOUT: &str = "Display numbers from FIRST to LAST, in steps of INCREMENT."; static OPT_SEPARATOR: &str = "separator"; static OPT_TERMINATOR: &str = "terminator"; @@ -22,12 +28,12 @@ static OPT_WIDTHS: &str = "widths"; static ARG_NUMBERS: &str = "numbers"; -fn get_usage() -> String { +fn usage() -> String { format!( "{0} [OPTION]... LAST {0} [OPTION]... FIRST LAST {0} [OPTION]... FIRST INCREMENT LAST", - executable!() + uucore::execution_phrase() ) } #[derive(Clone)] @@ -38,6 +44,8 @@ struct SeqOptions { } enum Number { + /// Negative zero, as if it were an integer. + MinusZero, BigInt(BigInt), F64(f64), } @@ -45,6 +53,7 @@ enum Number { impl Number { fn is_zero(&self) -> bool { match self { + Number::MinusZero => true, Number::BigInt(n) => n.is_zero(), Number::F64(n) => n.is_zero(), } @@ -52,41 +61,74 @@ impl Number { fn into_f64(self) -> f64 { match self { + Number::MinusZero => -0., // BigInt::to_f64() can not return None. Number::BigInt(n) => n.to_f64().unwrap(), Number::F64(n) => n, } } + + /// Convert this number into a bigint, consuming it. + /// + /// For floats, this returns the [`BigInt`] corresponding to the + /// floor of the number. + fn into_bigint(self) -> BigInt { + match self { + Number::MinusZero => BigInt::zero(), + Number::F64(x) => BigInt::from(x.floor() as i64), + Number::BigInt(n) => n, + } + } } impl FromStr for Number { type Err = String; fn from_str(mut s: &str) -> Result { + s = s.trim_start(); if s.starts_with('+') { s = &s[1..]; } match s.parse::() { - Ok(n) => Ok(Number::BigInt(n)), + Ok(n) => { + // If `s` is '-0', then `parse()` returns + // `BigInt::zero()`, but we need to return + // `Number::MinusZero` instead. + if n == BigInt::zero() && s.starts_with('-') { + Ok(Number::MinusZero) + } else { + Ok(Number::BigInt(n)) + } + } Err(_) => match s.parse::() { Ok(value) if value.is_nan() => Err(format!( - "invalid 'not-a-number' argument: '{}'\nTry '{} --help' for more information.", - s, - executable!(), + "invalid 'not-a-number' argument: {}\nTry '{} --help' for more information.", + s.quote(), + uucore::execution_phrase(), )), Ok(value) => Ok(Number::F64(value)), Err(_) => Err(format!( - "invalid floating point argument: '{}'\nTry '{} --help' for more information.", - s, - executable!(), + "invalid floating point argument: {}\nTry '{} --help' for more information.", + s.quote(), + uucore::execution_phrase(), )), }, } } } +/// A range of integers. +/// +/// The elements are (first, increment, last). +type RangeInt = (BigInt, BigInt, BigInt); + +/// A range of f64. +/// +/// The elements are (first, increment, last). +type RangeF64 = (f64, f64, f64); + pub fn uumain(args: impl uucore::Args) -> i32 { - let usage = get_usage(); + let usage = usage(); let matches = uu_app().usage(&usage[..]).get_matches_from(args); let numbers = matches.values_of(ARG_NUMBERS).unwrap().collect::>(); @@ -101,21 +143,47 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let mut padding = 0; let first = if numbers.len() > 1 { let slice = numbers[0]; - let len = slice.len(); - let dec = slice.find('.').unwrap_or(len); - largest_dec = len - dec; - padding = dec; - return_if_err!(1, slice.parse()) + largest_dec = num_fractional_digits(slice).unwrap_or_else(|_| { + crash!( + 1, + "invalid floating point argument: {}\n Try '{} --help' for more information.", + slice.quote(), + uucore::execution_phrase() + ) + }); + padding = num_integral_digits(slice).unwrap_or_else(|_| { + crash!( + 1, + "invalid floating point argument: {}\n Try '{} --help' for more information.", + slice.quote(), + uucore::execution_phrase() + ) + }); + crash_if_err!(1, slice.parse()) } else { Number::BigInt(BigInt::one()) }; let increment = if numbers.len() > 2 { let slice = numbers[1]; - let len = slice.len(); - let dec = slice.find('.').unwrap_or(len); - largest_dec = cmp::max(largest_dec, len - dec); - padding = cmp::max(padding, dec); - return_if_err!(1, slice.parse()) + let dec = num_fractional_digits(slice).unwrap_or_else(|_| { + crash!( + 1, + "invalid floating point argument: {}\n Try '{} --help' for more information.", + slice.quote(), + uucore::execution_phrase() + ) + }); + let int_digits = num_integral_digits(slice).unwrap_or_else(|_| { + crash!( + 1, + "invalid floating point argument: {}\n Try '{} --help' for more information.", + slice.quote(), + uucore::execution_phrase() + ) + }); + largest_dec = cmp::max(largest_dec, dec); + padding = cmp::max(padding, int_digits); + crash_if_err!(1, slice.parse()) } else { Number::BigInt(BigInt::one()) }; @@ -123,47 +191,74 @@ pub fn uumain(args: impl uucore::Args) -> i32 { show_error!( "invalid Zero increment value: '{}'\nTry '{} --help' for more information.", numbers[1], - executable!() + uucore::execution_phrase() ); return 1; } - let last = { + let last: Number = { let slice = numbers[numbers.len() - 1]; - padding = cmp::max(padding, slice.find('.').unwrap_or_else(|| slice.len())); - return_if_err!(1, slice.parse()) + let int_digits = num_integral_digits(slice).unwrap_or_else(|_| { + crash!( + 1, + "invalid floating point argument: {}\n Try '{} --help' for more information.", + slice.quote(), + uucore::execution_phrase() + ) + }); + padding = cmp::max(padding, int_digits); + crash_if_err!(1, slice.parse()) }; - if largest_dec > 0 { - largest_dec -= 1; - } - match (first, last, increment) { - (Number::BigInt(first), Number::BigInt(last), Number::BigInt(increment)) => { + let is_negative_zero_f64 = |x: f64| x == -0.0 && x.is_sign_negative() && largest_dec == 0; + let result = match (first, last, increment) { + // For example, `seq -0 1 2` or `seq -0 1 2.0`. + (Number::MinusZero, last, Number::BigInt(increment)) => print_seq_integers( + (BigInt::zero(), increment, last.into_bigint()), + options.separator, + options.terminator, + options.widths, + padding, + true, + ), + // For example, `seq -0e0 1 2` or `seq -0e0 1 2.0`. + (Number::F64(x), last, Number::BigInt(increment)) if is_negative_zero_f64(x) => { print_seq_integers( - first, - increment, - last, + (BigInt::zero(), increment, last.into_bigint()), options.separator, options.terminator, options.widths, padding, + true, ) } + // For example, `seq 0 1 2` or `seq 0 1 2.0`. + (Number::BigInt(first), last, Number::BigInt(increment)) => print_seq_integers( + (first, increment, last.into_bigint()), + options.separator, + options.terminator, + options.widths, + padding, + false, + ), + // For example, `seq 0 0.5 1` or `seq 0.0 0.5 1` or `seq 0.0 0.5 1.0`. (first, last, increment) => print_seq( - first.into_f64(), - increment.into_f64(), - last.into_f64(), + (first.into_f64(), increment.into_f64(), last.into_f64()), largest_dec, options.separator, options.terminator, options.widths, padding, ), + }; + match result { + Ok(_) => 0, + Err(err) if err.kind() == ErrorKind::BrokenPipe => 0, + Err(_) => 1, } - 0 } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .setting(AppSettings::AllowLeadingHyphen) .version(crate_version!()) .about(ABOUT) @@ -208,67 +303,96 @@ fn done_printing(next: &T, increment: &T, last: &T) -> bool } /// Floating point based code path -#[allow(clippy::too_many_arguments)] fn print_seq( - first: f64, - increment: f64, - last: f64, + range: RangeF64, largest_dec: usize, separator: String, terminator: String, pad: bool, padding: usize, -) { +) -> std::io::Result<()> { + let stdout = stdout(); + let mut stdout = stdout.lock(); + let (first, increment, last) = range; let mut i = 0isize; + let is_first_minus_zero = first == -0.0 && first.is_sign_negative(); let mut value = first + i as f64 * increment; + let padding = if pad { padding + 1 + largest_dec } else { 0 }; + let mut is_first_iteration = true; while !done_printing(&value, &increment, &last) { - let istr = format!("{:.*}", largest_dec, value); - let ilen = istr.len(); - let before_dec = istr.find('.').unwrap_or(ilen); - if pad && before_dec < padding { - for _ in 0..(padding - before_dec) { - print!("0"); - } + if !is_first_iteration { + write!(stdout, "{}", separator)?; } - print!("{}", istr); + let mut width = padding; + if is_first_iteration && is_first_minus_zero { + write!(stdout, "-")?; + width -= 1; + } + is_first_iteration = false; + write!( + stdout, + "{value:>0width$.precision$}", + value = value, + width = width, + precision = largest_dec, + )?; i += 1; value = first + i as f64 * increment; - if !done_printing(&value, &increment, &last) { - print!("{}", separator); - } } - if (first >= last && increment < 0f64) || (first <= last && increment > 0f64) { - print!("{}", terminator); + if !is_first_iteration { + write!(stdout, "{}", terminator)?; } - crash_if_err!(1, stdout().flush()); + stdout.flush()?; + Ok(()) } -/// BigInt based code path +/// Print an integer sequence. +/// +/// This function prints a sequence of integers defined by `range`, +/// which defines the first integer, last integer, and increment of the +/// range. The `separator` is inserted between each integer and +/// `terminator` is inserted at the end. +/// +/// The `pad` parameter indicates whether to pad numbers to the width +/// given in `padding`. +/// +/// If `is_first_minus_zero` is `true`, then the `first` parameter is +/// printed as if it were negative zero, even though no such number +/// exists as an integer (negative zero only exists for floating point +/// numbers). Only set this to `true` if `first` is actually zero. fn print_seq_integers( - first: BigInt, - increment: BigInt, - last: BigInt, + range: RangeInt, separator: String, terminator: String, pad: bool, padding: usize, -) { + is_first_minus_zero: bool, +) -> std::io::Result<()> { + let stdout = stdout(); + let mut stdout = stdout.lock(); + let (first, increment, last) = range; let mut value = first; let mut is_first_iteration = true; while !done_printing(&value, &increment, &last) { if !is_first_iteration { - print!("{}", separator); + write!(stdout, "{}", separator)?; + } + let mut width = padding; + if is_first_iteration && is_first_minus_zero { + write!(stdout, "-")?; + width -= 1; } is_first_iteration = false; if pad { - print!("{number:>0width$}", number = value, width = padding); + write!(stdout, "{number:>0width$}", number = value, width = width)?; } else { - print!("{}", value); + write!(stdout, "{}", value)?; } value += &increment; } if !is_first_iteration { - print!("{}", terminator); + write!(stdout, "{}", terminator)?; } + Ok(()) } diff --git a/src/uu/shred/Cargo.toml b/src/uu/shred/Cargo.toml index 89ed980c1..d87732d84 100644 --- a/src/uu/shred/Cargo.toml +++ b/src/uu/shred/Cargo.toml @@ -16,13 +16,15 @@ path = "src/shred.rs" [dependencies] clap = { version = "2.33", features = ["wrap_help"] } -filetime = "0.2.1" libc = "0.2.42" -rand = "0.5" -time = "0.1.40" +rand = "0.7" uucore = { version=">=0.0.9", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "shred" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/shred/src/shred.rs b/src/uu/shred/src/shred.rs index 90336ea95..fa455f027 100644 --- a/src/uu/shred/src/shred.rs +++ b/src/uu/shred/src/shred.rs @@ -9,7 +9,8 @@ // spell-checker:ignore (words) writeback wipesync use clap::{crate_version, App, Arg}; -use rand::{Rng, ThreadRng}; +use rand::prelude::SliceRandom; +use rand::Rng; use std::cell::{Cell, RefCell}; use std::fs; use std::fs::{File, OpenOptions}; @@ -17,12 +18,12 @@ use std::io; use std::io::prelude::*; use std::io::SeekFrom; use std::path::{Path, PathBuf}; -use uucore::InvalidEncodingHandling; +use uucore::display::Quotable; +use uucore::{util_name, InvalidEncodingHandling}; #[macro_use] extern crate uucore; -static NAME: &str = "shred"; const BLOCK_SIZE: usize = 512; const NAME_CHARSET: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_."; @@ -119,7 +120,7 @@ struct BytesGenerator<'a> { block_size: usize, exact: bool, // if false, every block's size is block_size gen_type: PassType<'a>, - rng: Option>, + rng: Option>, bytes: [u8; BLOCK_SIZE], } @@ -213,8 +214,8 @@ static ABOUT: &str = "Overwrite the specified FILE(s) repeatedly, in order to ma for even very expensive hardware probing to recover the data. "; -fn get_usage() -> String { - format!("{} [OPTION]... FILE...", executable!()) +fn usage() -> String { + format!("{} [OPTION]... FILE...", uucore::execution_phrase()) } static AFTER_HELP: &str = @@ -270,7 +271,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .collect_str(InvalidEncodingHandling::Ignore) .accept_any(); - let usage = get_usage(); + let usage = usage(); let app = uu_app().usage(&usage[..]); @@ -280,7 +281,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { if !matches.is_present(options::FILE) { show_error!("Missing an argument"); - show_error!("For help, try '{} --help'", NAME); + show_error!("For help, try '{} --help'", uucore::execution_phrase()); return 0; } @@ -288,7 +289,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { Some(s) => match s.parse::() { Ok(u) => u, Err(_) => { - errs.push(format!("invalid number of passes: '{}'", s)); + errs.push(format!("invalid number of passes: {}", s.quote())); 0 } }, @@ -330,7 +331,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .after_help(AFTER_HELP) @@ -413,7 +414,11 @@ fn get_size(size_str_opt: Option) -> Option { let coefficient = match size_str.parse::() { Ok(u) => u, Err(_) => { - println!("{}: {}: Invalid file size", NAME, size_str_opt.unwrap()); + println!( + "{}: {}: Invalid file size", + util_name(), + size_str_opt.unwrap().maybe_quote() + ); exit!(1); } }; @@ -451,11 +456,11 @@ fn wipe_file( // Get these potential errors out of the way first let path: &Path = Path::new(path_str); if !path.exists() { - show_error!("{}: No such file or directory", path.display()); + show_error!("{}: No such file or directory", path.maybe_quote()); return; } if !path.is_file() { - show_error!("{}: Not a file", path.display()); + show_error!("{}: Not a file", path.maybe_quote()); return; } @@ -499,7 +504,8 @@ fn wipe_file( for pattern in PATTERNS.iter().take(remainder) { pass_sequence.push(PassType::Pattern(pattern)); } - rand::thread_rng().shuffle(&mut pass_sequence[..]); // randomize the order of application + let mut rng = rand::thread_rng(); + pass_sequence.shuffle(&mut rng); // randomize the order of application let n_random = 3 + n_passes / 10; // Minimum 3 random passes; ratio of 10 after // Evenly space random passes; ensures one at the beginning and end @@ -518,7 +524,7 @@ fn wipe_file( let mut file: File = match OpenOptions::new().write(true).truncate(false).open(path) { Ok(f) => f, Err(e) => { - show_error!("{}: failed to open for writing: {}", path.display(), e); + show_error!("{}: failed to open for writing: {}", path.maybe_quote(), e); return; } }; @@ -533,8 +539,8 @@ fn wipe_file( if total_passes.to_string().len() == 1 { println!( "{}: {}: pass {}/{} ({})... ", - NAME, - path.display(), + util_name(), + path.maybe_quote(), i + 1, total_passes, pass_name @@ -542,8 +548,8 @@ fn wipe_file( } else { println!( "{}: {}: pass {:2.0}/{:2.0} ({})... ", - NAME, - path.display(), + util_name(), + path.maybe_quote(), i + 1, total_passes, pass_name @@ -554,7 +560,7 @@ fn wipe_file( match do_pass(&mut file, path, &mut generator, *pass_type, size) { Ok(_) => {} Err(e) => { - show_error!("{}: File write pass failed: {}", path.display(), e); + show_error!("{}: File write pass failed: {}", path.maybe_quote(), e); } } // Ignore failed writes; just keep trying @@ -565,7 +571,7 @@ fn wipe_file( match do_remove(path, path_str, verbose) { Ok(_) => {} Err(e) => { - show_error!("{}: failed to remove file: {}", path.display(), e); + show_error!("{}: failed to remove file: {}", path.maybe_quote(), e); } } } @@ -620,9 +626,9 @@ fn wipe_name(orig_path: &Path, verbose: bool) -> Option { if verbose { println!( "{}: {}: renamed to {}", - NAME, - last_path.display(), - new_path.display() + util_name(), + last_path.maybe_quote(), + new_path.quote() ); } @@ -639,9 +645,9 @@ fn wipe_name(orig_path: &Path, verbose: bool) -> Option { Err(e) => { println!( "{}: {}: Couldn't rename to {}: {}", - NAME, - last_path.display(), - new_path.display(), + util_name(), + last_path.maybe_quote(), + new_path.quote(), e ); return None; @@ -655,7 +661,7 @@ fn wipe_name(orig_path: &Path, verbose: bool) -> Option { fn do_remove(path: &Path, orig_filename: &str, verbose: bool) -> Result<(), io::Error> { if verbose { - println!("{}: {}: removing", NAME, orig_filename); + println!("{}: {}: removing", util_name(), orig_filename.maybe_quote()); } let renamed_path: Option = wipe_name(path, verbose); @@ -664,7 +670,7 @@ fn do_remove(path: &Path, orig_filename: &str, verbose: bool) -> Result<(), io:: } if verbose { - println!("{}: {}: removed", NAME, orig_filename); + println!("{}: {}: removed", util_name(), orig_filename.maybe_quote()); } Ok(()) diff --git a/src/uu/shuf/Cargo.toml b/src/uu/shuf/Cargo.toml index 5c99c6d26..bb3ccc710 100644 --- a/src/uu/shuf/Cargo.toml +++ b/src/uu/shuf/Cargo.toml @@ -23,3 +23,7 @@ uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_p [[bin]] name = "shuf" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/shuf/src/shuf.rs b/src/uu/shuf/src/shuf.rs index 4690d1c6e..9a899d746 100644 --- a/src/uu/shuf/src/shuf.rs +++ b/src/uu/shuf/src/shuf.rs @@ -14,6 +14,7 @@ use clap::{crate_version, App, Arg}; use rand::Rng; use std::fs::File; use std::io::{stdin, stdout, BufReader, BufWriter, Read, Write}; +use uucore::display::Quotable; use uucore::InvalidEncodingHandling; enum Mode { @@ -76,7 +77,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { Some(count) => match count.parse::() { Ok(val) => val, Err(_) => { - show_error!("invalid line count: '{}'", count); + show_error!("invalid line count: {}", count.quote()); return 1; } }, @@ -115,7 +116,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .name(NAME) .version(crate_version!()) .template(TEMPLATE) @@ -185,13 +186,13 @@ fn read_input_file(filename: &str) -> Vec { } else { match File::open(filename) { Ok(f) => Box::new(f) as Box, - Err(e) => crash!(1, "failed to open '{}': {}", filename, e), + Err(e) => crash!(1, "failed to open {}: {}", filename.quote(), e), } }); let mut data = Vec::new(); if let Err(e) = file.read_to_end(&mut data) { - crash!(1, "failed reading '{}': {}", filename, e) + crash!(1, "failed reading {}: {}", filename.quote(), e) }; data @@ -235,15 +236,17 @@ fn shuf_bytes(input: &mut Vec<&[u8]>, opts: Options) { None => Box::new(stdout()) as Box, Some(s) => match File::create(&s[..]) { Ok(f) => Box::new(f) as Box, - Err(e) => crash!(1, "failed to open '{}' for writing: {}", &s[..], e), + Err(e) => crash!(1, "failed to open {} for writing: {}", s.quote(), e), }, }); let mut rng = match opts.random_source { - Some(r) => WrappedRng::RngFile(rand::read::ReadRng::new(match File::open(&r[..]) { - Ok(f) => f, - Err(e) => crash!(1, "failed to open random source '{}': {}", &r[..], e), - })), + Some(r) => WrappedRng::RngFile(rand::rngs::adapter::ReadRng::new( + match File::open(&r[..]) { + Ok(f) => f, + Err(e) => crash!(1, "failed to open random source {}: {}", r.quote(), e), + }, + )), None => WrappedRng::RngDefault(rand::thread_rng()), }; @@ -286,21 +289,21 @@ fn shuf_bytes(input: &mut Vec<&[u8]>, opts: Options) { fn parse_range(input_range: &str) -> Result<(usize, usize), String> { let split: Vec<&str> = input_range.split('-').collect(); if split.len() != 2 { - Err(format!("invalid input range: '{}'", input_range)) + Err(format!("invalid input range: {}", input_range.quote())) } else { let begin = split[0] .parse::() - .map_err(|_| format!("invalid input range: '{}'", split[0]))?; + .map_err(|_| format!("invalid input range: {}", split[0].quote()))?; let end = split[1] .parse::() - .map_err(|_| format!("invalid input range: '{}'", split[1]))?; + .map_err(|_| format!("invalid input range: {}", split[1].quote()))?; Ok((begin, end + 1)) } } enum WrappedRng { - RngFile(rand::read::ReadRng), - RngDefault(rand::ThreadRng), + RngFile(rand::rngs::adapter::ReadRng), + RngDefault(rand::rngs::ThreadRng), } impl WrappedRng { diff --git a/src/uu/sleep/src/sleep.rs b/src/uu/sleep/src/sleep.rs index 127804a9f..a70a524c4 100644 --- a/src/uu/sleep/src/sleep.rs +++ b/src/uu/sleep/src/sleep.rs @@ -26,17 +26,17 @@ mod options { pub const NUMBER: &str = "NUMBER"; } -fn get_usage() -> String { +fn usage() -> String { format!( "{0} {1}[SUFFIX]... \n {0} OPTION", - executable!(), + uucore::execution_phrase(), options::NUMBER ) } #[uucore_procs::gen_uumain] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let usage = get_usage(); + let usage = usage(); let matches = uu_app().usage(&usage[..]).get_matches_from(args); @@ -49,7 +49,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .after_help(LONG_HELP) diff --git a/src/uu/sort/src/ext_sort.rs b/src/uu/sort/src/ext_sort.rs index b4827f962..bf332e4e8 100644 --- a/src/uu/sort/src/ext_sort.rs +++ b/src/uu/sort/src/ext_sort.rs @@ -12,6 +12,7 @@ //! The buffers for the individual chunks are recycled. There are two buffers. use std::cmp::Ordering; +use std::fs::File; use std::io::Write; use std::path::PathBuf; use std::{ @@ -238,7 +239,7 @@ fn read_write_loop( let tmp_file = write::( &mut chunk, - tmp_dir.next_file_path()?, + tmp_dir.next_file()?, settings.compress_prog.as_deref(), separator, )?; @@ -268,7 +269,7 @@ fn read_write_loop( /// `compress_prog` is used to optionally compress file contents. fn write( chunk: &mut Chunk, - file: PathBuf, + file: (File, PathBuf), compress_prog: Option<&str>, separator: u8, ) -> UResult { diff --git a/src/uu/sort/src/merge.rs b/src/uu/sort/src/merge.rs index 64a7632bf..934d1c208 100644 --- a/src/uu/sort/src/merge.rs +++ b/src/uu/sort/src/merge.rs @@ -46,7 +46,7 @@ fn replace_output_file_in_input_files( if let Some(copy) = © { *file = copy.clone().into_os_string(); } else { - let copy_path = tmp_dir.next_file_path()?; + let (_file, copy_path) = tmp_dir.next_file()?; std::fs::copy(file_path, ©_path) .map_err(|error| SortError::OpenTmpFileFailed { error })?; *file = copy_path.clone().into_os_string(); @@ -110,7 +110,7 @@ pub fn merge_with_file_limit< remaining_files = remaining_files.saturating_sub(settings.merge_batch_size); let merger = merge_without_limit(batches.next().unwrap(), settings)?; let mut tmp_file = - Tmp::create(tmp_dir.next_file_path()?, settings.compress_prog.as_deref())?; + Tmp::create(tmp_dir.next_file()?, settings.compress_prog.as_deref())?; merger.write_all_to(settings, tmp_file.as_write())?; temporary_files.push(tmp_file.finished_writing()?); } @@ -379,7 +379,7 @@ fn check_child_success(mut child: Child, program: &str) -> UResult<()> { pub trait WriteableTmpFile: Sized { type Closed: ClosedTmpFile; type InnerWrite: Write; - fn create(path: PathBuf, compress_prog: Option<&str>) -> UResult; + fn create(file: (File, PathBuf), compress_prog: Option<&str>) -> UResult; /// Closes the temporary file. fn finished_writing(self) -> UResult; fn as_write(&mut self) -> &mut Self::InnerWrite; @@ -414,11 +414,9 @@ impl WriteableTmpFile for WriteablePlainTmpFile { type Closed = ClosedPlainTmpFile; type InnerWrite = BufWriter; - fn create(path: PathBuf, _: Option<&str>) -> UResult { + fn create((file, path): (File, PathBuf), _: Option<&str>) -> UResult { Ok(WriteablePlainTmpFile { - file: BufWriter::new( - File::create(&path).map_err(|error| SortError::OpenTmpFileFailed { error })?, - ), + file: BufWriter::new(file), path, }) } @@ -476,12 +474,10 @@ impl WriteableTmpFile for WriteableCompressedTmpFile { type Closed = ClosedCompressedTmpFile; type InnerWrite = BufWriter; - fn create(path: PathBuf, compress_prog: Option<&str>) -> UResult { + fn create((file, path): (File, PathBuf), compress_prog: Option<&str>) -> UResult { let compress_prog = compress_prog.unwrap(); let mut command = Command::new(compress_prog); - let tmp_file = - File::create(&path).map_err(|error| SortError::OpenTmpFileFailed { error })?; - command.stdin(Stdio::piped()).stdout(tmp_file); + command.stdin(Stdio::piped()).stdout(file); let mut child = command .spawn() .map_err(|err| SortError::CompressProgExecutionFailed { diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index e0b445782..fe286aa6d 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -45,14 +45,14 @@ use std::path::Path; use std::path::PathBuf; use std::str::Utf8Error; use unicode_width::UnicodeWidthStr; -use uucore::error::{set_exit_code, UError, UResult, USimpleError, UUsageError}; +use uucore::display::Quotable; +use uucore::error::{set_exit_code, strip_errno, UError, UResult, USimpleError, UUsageError}; use uucore::parse_size::{parse_size, ParseSizeError}; use uucore::version_cmp::version_cmp; use uucore::InvalidEncodingHandling; use crate::tmp_dir::TmpDirWrapper; -const NAME: &str = "sort"; const ABOUT: &str = "Display sorted concatenation of all FILE(s)."; const LONG_HELP_KEYS: &str = "The key format is FIELD[.CHAR][OPTIONS][,FIELD[.CHAR]][OPTIONS]. @@ -140,7 +140,7 @@ enum SortError { error: std::io::Error, }, ReadFailed { - path: String, + path: PathBuf, error: std::io::Error, }, ParseKeyError { @@ -190,7 +190,7 @@ impl Display for SortError { write!( f, "{}:{}: disorder: {}", - file.to_string_lossy(), + file.maybe_quote(), line_number, line ) @@ -198,33 +198,33 @@ impl Display for SortError { Ok(()) } } - SortError::OpenFailed { path, error } => write!( - f, - "open failed: {}: {}", - path, - strip_errno(&error.to_string()) - ), - SortError::ParseKeyError { key, msg } => { - write!(f, "failed to parse key `{}`: {}", key, msg) - } - SortError::ReadFailed { path, error } => write!( - f, - "cannot read: {}: {}", - path, - strip_errno(&error.to_string()) - ), - SortError::OpenTmpFileFailed { error } => { + SortError::OpenFailed { path, error } => { write!( f, - "failed to open temporary file: {}", - strip_errno(&error.to_string()) + "open failed: {}: {}", + path.maybe_quote(), + strip_errno(error) ) } + SortError::ParseKeyError { key, msg } => { + write!(f, "failed to parse key {}: {}", key.quote(), msg) + } + SortError::ReadFailed { path, error } => { + write!( + f, + "cannot read: {}: {}", + path.maybe_quote(), + strip_errno(error) + ) + } + SortError::OpenTmpFileFailed { error } => { + write!(f, "failed to open temporary file: {}", strip_errno(error)) + } SortError::CompressProgExecutionFailed { code } => { write!(f, "couldn't execute compress program: errno {}", code) } SortError::CompressProgTerminatedAbnormally { prog } => { - write!(f, "'{}' terminated abnormally", prog) + write!(f, "{} terminated abnormally", prog.quote()) } SortError::TmpDirCreationFailed => write!(f, "could not create temporary directory"), SortError::Uft8Error { error } => write!(f, "{}", error), @@ -767,19 +767,19 @@ impl KeyPosition { let field = field_and_char .next() - .ok_or_else(|| format!("invalid key `{}`", key))?; + .ok_or_else(|| format!("invalid key {}", key.quote()))?; let char = field_and_char.next(); let field = field .parse() - .map_err(|e| format!("failed to parse field index `{}`: {}", field, e))?; + .map_err(|e| format!("failed to parse field index {}: {}", field.quote(), e))?; if field == 0 { return Err("field index can not be 0".to_string()); } let char = char.map_or(Ok(default_char_index), |char| { char.parse() - .map_err(|e| format!("failed to parse character index `{}`: {}", char, e)) + .map_err(|e| format!("failed to parse character index {}: {}", char.quote(), e)) })?; Ok(Self { @@ -800,7 +800,7 @@ impl Default for KeyPosition { } } -#[derive(Clone, PartialEq, Debug)] +#[derive(Clone, PartialEq, Debug, Default)] struct FieldSelector { from: KeyPosition, to: Option, @@ -812,18 +812,6 @@ struct FieldSelector { needs_selection: bool, } -impl Default for FieldSelector { - fn default() -> Self { - Self { - from: Default::default(), - to: None, - settings: Default::default(), - needs_tokens: false, - needs_selection: false, - } - } -} - impl FieldSelector { /// Splits this position into the actual position and the attached options. fn split_key_options(position: &str) -> (&str, &str) { @@ -890,7 +878,7 @@ impl FieldSelector { 'R' => key_settings.set_sort_mode(SortMode::Random)?, 'r' => key_settings.reverse = true, 'V' => key_settings.set_sort_mode(SortMode::Version)?, - c => return Err(format!("invalid option: `{}`", c)), + c => return Err(format!("invalid option: '{}'", c)), } } Ok(ignore_blanks) @@ -1055,13 +1043,13 @@ impl FieldSelector { } } -fn get_usage() -> String { +fn usage() -> String { format!( "{0} [OPTION]... [FILE]... Write the sorted concatenation of all FILE(s) to standard output. Mandatory arguments for long options are mandatory for short options too. With no FILE, or when FILE is -, read standard input.", - NAME + uucore::execution_phrase() ) } @@ -1081,7 +1069,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let args = args .collect_str(InvalidEncodingHandling::Ignore) .accept_any(); - let usage = get_usage(); + let usage = usage(); let mut settings: GlobalSettings = Default::default(); let matches = match uu_app().usage(&usage[..]).get_matches_from_safe(args) { @@ -1190,7 +1178,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { if let Some(n_merge) = matches.value_of(options::BATCH_SIZE) { settings.merge_batch_size = n_merge.parse().map_err(|_| { - UUsageError::new(2, format!("invalid --batch-size argument '{}'", n_merge)) + UUsageError::new( + 2, + format!("invalid --batch-size argument {}", n_merge.quote()), + ) })?; } @@ -1222,23 +1213,30 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } else if settings.check && files.len() != 1 { return Err(UUsageError::new( 2, - format!( - "extra operand `{}' not allowed with -c", - files[1].to_string_lossy() - ), + format!("extra operand {} not allowed with -c", files[1].quote()), )); } if let Some(arg) = matches.args.get(options::SEPARATOR) { - let separator = arg.vals[0].to_string_lossy(); - let mut separator = separator.as_ref(); + let mut separator = arg.vals[0].to_str().ok_or_else(|| { + UUsageError::new( + 2, + format!("separator is not valid unicode: {}", arg.vals[0].quote()), + ) + })?; if separator == "\\0" { separator = "\0"; } + // This rejects non-ASCII codepoints, but perhaps we don't have to. + // On the other hand GNU accepts any single byte, valid unicode or not. + // (Supporting multi-byte chars would require changes in tokenize_with_separator().) if separator.len() != 1 { return Err(UUsageError::new( 2, - "separator must be exactly one character long", + format!( + "separator must be exactly one character long: {}", + separator.quote() + ), )); } settings.separator = Some(separator.chars().next().unwrap()) @@ -1287,7 +1285,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .arg( @@ -1815,11 +1813,6 @@ fn print_sorted<'a, T: Iterator>>( } } -/// Strips the trailing " (os error XX)" from io error strings. -fn strip_errno(err: &str) -> &str { - &err[..err.find(" (os error ").unwrap_or(err.len())] -} - fn open(path: impl AsRef) -> UResult> { let path = path.as_ref(); if path == "-" { @@ -1832,7 +1825,7 @@ fn open(path: impl AsRef) -> UResult> { match File::open(path) { Ok(f) => Ok(Box::new(f) as Box), Err(error) => Err(SortError::ReadFailed { - path: path.to_string_lossy().to_string(), + path: path.to_owned(), error, } .into()), @@ -1844,8 +1837,8 @@ fn format_error_message(error: ParseSizeError, s: &str, option: &str) -> String // GNU's sort echos affected flag, -S or --buffer-size, depending user's selection // GNU's sort does distinguish between "invalid (suffix in) argument" match error { - ParseSizeError::ParseFailure(_) => format!("invalid --{} argument '{}'", option, s), - ParseSizeError::SizeTooBig(_) => format!("--{} argument '{}' too large", option, s), + ParseSizeError::ParseFailure(_) => format!("invalid --{} argument {}", option, s.quote()), + ParseSizeError::SizeTooBig(_) => format!("--{} argument {} too large", option, s.quote()), } } diff --git a/src/uu/sort/src/tmp_dir.rs b/src/uu/sort/src/tmp_dir.rs index 884f2cd00..32ffbbf0d 100644 --- a/src/uu/sort/src/tmp_dir.rs +++ b/src/uu/sort/src/tmp_dir.rs @@ -1,4 +1,5 @@ use std::{ + fs::File, path::{Path, PathBuf}, sync::{Arc, Mutex}, }; @@ -54,7 +55,7 @@ impl TmpDirWrapper { .map_err(|e| USimpleError::new(2, format!("failed to set up signal handler: {}", e))) } - pub fn next_file_path(&mut self) -> UResult { + pub fn next_file(&mut self) -> UResult<(File, PathBuf)> { if self.temp_dir.is_none() { self.init_tmp_dir()?; } @@ -62,7 +63,11 @@ impl TmpDirWrapper { let _lock = self.lock.lock().unwrap(); let file_name = self.size.to_string(); self.size += 1; - Ok(self.temp_dir.as_ref().unwrap().path().join(file_name)) + let path = self.temp_dir.as_ref().unwrap().path().join(file_name); + Ok(( + File::create(&path).map_err(|error| SortError::OpenTmpFileFailed { error })?, + path, + )) } } diff --git a/src/uu/split/Cargo.toml b/src/uu/split/Cargo.toml index 6583d705e..d2168bf49 100644 --- a/src/uu/split/Cargo.toml +++ b/src/uu/split/Cargo.toml @@ -22,3 +22,7 @@ uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_p [[bin]] name = "split" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/split/src/split.rs b/src/uu/split/src/split.rs index ccc98ee5e..581b632d2 100644 --- a/src/uu/split/src/split.rs +++ b/src/uu/split/src/split.rs @@ -19,10 +19,9 @@ use std::fs::File; use std::io::{stdin, BufRead, BufReader, BufWriter, Read, Write}; use std::path::Path; use std::{char, fs::remove_file}; +use uucore::display::Quotable; use uucore::parse_size::parse_size; -static NAME: &str = "split"; - static OPT_BYTES: &str = "bytes"; static OPT_LINE_BYTES: &str = "line-bytes"; static OPT_LINES: &str = "lines"; @@ -36,8 +35,11 @@ static OPT_VERBOSE: &str = "verbose"; static ARG_INPUT: &str = "input"; static ARG_PREFIX: &str = "prefix"; -fn get_usage() -> String { - format!("{0} [OPTION]... [INPUT [PREFIX]]", NAME) +fn usage() -> String { + format!( + "{0} [OPTION]... [INPUT [PREFIX]]", + uucore::execution_phrase() + ) } fn get_long_usage() -> String { format!( @@ -47,12 +49,12 @@ fn get_long_usage() -> String { Output fixed-size pieces of INPUT to PREFIXaa, PREFIX ab, ...; default size is 1000, and default PREFIX is 'x'. With no INPUT, or when INPUT is -, read standard input.", - get_usage() + usage() ) } pub fn uumain(args: impl uucore::Args) -> i32 { - let usage = get_usage(); + let usage = usage(); let long_usage = get_long_usage(); let matches = uu_app() @@ -103,7 +105,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { settings.strategy = String::from(OPT_LINES); settings.strategy_param = matches.value_of(OPT_LINES).unwrap().to_owned(); // take any (other) defined strategy - for strategy in vec![OPT_LINE_BYTES, OPT_BYTES].into_iter() { + for &strategy in &[OPT_LINE_BYTES, OPT_BYTES] { if matches.occurrences_of(strategy) > 0 { settings.strategy = String::from(strategy); settings.strategy_param = matches.value_of(strategy).unwrap().to_owned(); @@ -127,7 +129,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about("Create output files containing consecutive or interleaved sections of input") // strategy (mutually exclusive) @@ -237,7 +239,11 @@ 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.quote() + ) }), } } @@ -372,8 +378,8 @@ fn split(settings: &Settings) -> i32 { let r = File::open(Path::new(&settings.input)).unwrap_or_else(|_| { crash!( 1, - "cannot open '{}' for reading: No such file or directory", - settings.input + "cannot open {} for reading: No such file or directory", + settings.input.quote() ) }); Box::new(r) as Box @@ -382,7 +388,7 @@ fn split(settings: &Settings) -> i32 { let mut splitter: Box = match settings.strategy.as_str() { s if s == OPT_LINES => Box::new(LineSplitter::new(settings)), s if (s == OPT_BYTES || s == OPT_LINE_BYTES) => Box::new(ByteSplitter::new(settings)), - a => crash!(1, "strategy {} not supported", a), + a => crash!(1, "strategy {} not supported", a.quote()), }; let mut fileno = 0; diff --git a/src/uu/stat/src/stat.rs b/src/uu/stat/src/stat.rs index c56971f6b..fd4a6443d 100644 --- a/src/uu/stat/src/stat.rs +++ b/src/uu/stat/src/stat.rs @@ -7,6 +7,7 @@ #[macro_use] extern crate uucore; +use uucore::display::Quotable; use uucore::entries; use uucore::fs::display_permissions; use uucore::fsext::{ @@ -24,7 +25,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].quote())); } }; } @@ -652,11 +653,7 @@ impl Stater { return 1; } }; - arg = format!( - "`{}' -> `{}'", - file, - dst.to_string_lossy() - ); + arg = format!("{} -> {}", file.quote(), dst.quote()); } else { arg = file.to_string(); } @@ -750,7 +747,7 @@ impl Stater { } } Err(e) => { - show_error!("cannot stat '{}': {}", file, e); + show_error!("cannot stat {}: {}", file.quote(), e); return 1; } } @@ -843,7 +840,11 @@ impl Stater { } } Err(e) => { - show_error!("cannot read file system information for '{}': {}", file, e); + show_error!( + "cannot read file system information for {}: {}", + file.quote(), + e + ); return 1; } } @@ -882,8 +883,8 @@ impl Stater { } } -fn get_usage() -> String { - format!("{0} [OPTION]... FILE...", executable!()) +fn usage() -> String { + format!("{0} [OPTION]... FILE...", uucore::execution_phrase()) } fn get_long_usage() -> String { @@ -945,7 +946,7 @@ for details about the options it supports. } pub fn uumain(args: impl uucore::Args) -> i32 { - let usage = get_usage(); + let usage = usage(); let long_usage = get_long_usage(); let matches = uu_app() @@ -963,7 +964,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .arg( diff --git a/src/uu/stdbuf/src/stdbuf.rs b/src/uu/stdbuf/src/stdbuf.rs index 7460a2cb2..77d80f777 100644 --- a/src/uu/stdbuf/src/stdbuf.rs +++ b/src/uu/stdbuf/src/stdbuf.rs @@ -47,8 +47,8 @@ mod options { pub const COMMAND: &str = "command"; } -fn get_usage() -> String { - format!("{0} OPTION... COMMAND", executable!()) +fn usage() -> String { + format!("{0} OPTION... COMMAND", uucore::execution_phrase()) } const STDBUF_INJECT: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/libstdbuf.so")); @@ -152,19 +152,25 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let args = args .collect_str(InvalidEncodingHandling::Ignore) .accept_any(); - let usage = get_usage(); + let usage = usage(); 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 options = ProgramOptions::try_from(&matches).unwrap_or_else(|e| { + crash!( + 125, + "{}\nTry '{} --help' for more information.", + e.0, + uucore::execution_phrase() + ) + }); 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)); + let (preload_env, libstdbuf) = crash_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); @@ -185,7 +191,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .after_help(LONG_HELP) diff --git a/src/uu/sum/Cargo.toml b/src/uu/sum/Cargo.toml index 5a212d0d3..41f2d0a38 100644 --- a/src/uu/sum/Cargo.toml +++ b/src/uu/sum/Cargo.toml @@ -22,3 +22,7 @@ uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_p [[bin]] name = "sum" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/sum/src/sum.rs b/src/uu/sum/src/sum.rs index 0ce612859..15c20f09d 100644 --- a/src/uu/sum/src/sum.rs +++ b/src/uu/sum/src/sum.rs @@ -14,6 +14,7 @@ use clap::{crate_version, App, Arg}; use std::fs::File; use std::io::{stdin, Read, Result}; use std::path::Path; +use uucore::display::Quotable; use uucore::InvalidEncodingHandling; static NAME: &str = "sum"; @@ -118,7 +119,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let reader = match open(file) { Ok(f) => f, Err(error) => { - show_error!("'{}' {}", file, error); + show_error!("{}: {}", file.maybe_quote(), error); exit_code = 2; continue; } @@ -140,7 +141,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .name(NAME) .version(crate_version!()) .usage(USAGE) diff --git a/src/uu/sync/src/sync.rs b/src/uu/sync/src/sync.rs index 4fcdf49f9..d6a21f280 100644 --- a/src/uu/sync/src/sync.rs +++ b/src/uu/sync/src/sync.rs @@ -14,6 +14,7 @@ extern crate uucore; use clap::{crate_version, App, Arg}; use std::path::Path; +use uucore::display::Quotable; static EXIT_ERR: i32 = 1; @@ -159,12 +160,12 @@ mod platform { } } -fn get_usage() -> String { - format!("{0} [OPTION]... FILE...", executable!()) +fn usage() -> String { + format!("{0} [OPTION]... FILE...", uucore::execution_phrase()) } pub fn uumain(args: impl uucore::Args) -> i32 { - let usage = get_usage(); + let usage = usage(); let matches = uu_app().usage(&usage[..]).get_matches_from(args); @@ -175,7 +176,11 @@ pub fn uumain(args: impl uucore::Args) -> i32 { for f in &files { if !Path::new(&f).exists() { - crash!(EXIT_ERR, "cannot stat '{}': No such file or directory", f); + crash!( + EXIT_ERR, + "cannot stat {}: No such file or directory", + f.quote() + ); } } @@ -193,7 +198,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .arg( diff --git a/src/uu/tac/Cargo.toml b/src/uu/tac/Cargo.toml index 60e5d29ec..1e436e916 100644 --- a/src/uu/tac/Cargo.toml +++ b/src/uu/tac/Cargo.toml @@ -15,6 +15,8 @@ edition = "2018" path = "src/tac.rs" [dependencies] +memchr = "2" +regex = "1" clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.9", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_procs" } @@ -22,3 +24,7 @@ uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_p [[bin]] name = "tac" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/tac/src/tac.rs b/src/uu/tac/src/tac.rs index 01cf09215..4a93a7c65 100644 --- a/src/uu/tac/src/tac.rs +++ b/src/uu/tac/src/tac.rs @@ -5,14 +5,16 @@ // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -// spell-checker:ignore (ToDO) sbytes slen +// spell-checker:ignore (ToDO) sbytes slen dlen memmem #[macro_use] extern crate uucore; use clap::{crate_version, App, Arg}; -use std::io::{stdin, stdout, BufReader, Read, Stdout, Write}; +use memchr::memmem; +use std::io::{stdin, stdout, BufReader, Read, Write}; use std::{fs::File, path::Path}; +use uucore::display::Quotable; use uucore::InvalidEncodingHandling; static NAME: &str = "tac"; @@ -51,7 +53,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .name(NAME) .version(crate_version!()) .usage(USAGE) @@ -67,7 +69,7 @@ pub fn uu_app() -> App<'static, 'static> { Arg::with_name(options::REGEX) .short("r") .long(options::REGEX) - .help("interpret the sequence as a regular expression (NOT IMPLEMENTED)") + .help("interpret the sequence as a regular expression") .takes_value(false), ) .arg( @@ -80,11 +82,134 @@ pub fn uu_app() -> App<'static, 'static> { .arg(Arg::with_name(options::FILE).hidden(true).multiple(true)) } -fn tac(filenames: Vec, before: bool, _: bool, separator: &str) -> i32 { - let mut exit_code = 0; +/// Print lines of a buffer in reverse, with line separator given as a regex. +/// +/// `data` contains the bytes of the file. +/// +/// `pattern` is the regular expression given as a +/// [`regex::bytes::Regex`] (not a [`regex::Regex`], since the input is +/// given as a slice of bytes). If `before` is `true`, then each match +/// of this pattern in `data` is interpreted as the start of a line. If +/// `before` is `false`, then each match of this pattern is interpreted +/// as the end of a line. +/// +/// This function writes each line in `data` to [`std::io::Stdout`] in +/// reverse. +/// +/// # Errors +/// +/// If there is a problem writing to `stdout`, then this function +/// returns [`std::io::Error`]. +fn buffer_tac_regex( + data: &[u8], + pattern: regex::bytes::Regex, + before: bool, +) -> std::io::Result<()> { let mut out = stdout(); - let sbytes = separator.as_bytes(); - let slen = sbytes.len(); + + // The index of the line separator for the current line. + // + // As we scan through the `data` from right to left, we update this + // variable each time we find a new line separator. We restrict our + // regular expression search to only those bytes up to the line + // separator. + let mut this_line_end = data.len(); + + // The index of the start of the next line in the `data`. + // + // As we scan through the `data` from right to left, we update this + // variable each time we find a new line. + // + // If `before` is `true`, then each line starts immediately before + // the line separator. Otherwise, each line starts immediately after + // the line separator. + let mut following_line_start = data.len(); + + // Iterate over each byte in the buffer in reverse. When we find a + // line separator, write the line to stdout. + // + // The `before` flag controls whether the line separator appears at + // the end of the line (as in "abc\ndef\n") or at the beginning of + // the line (as in "/abc/def"). + for i in (0..data.len()).rev() { + // Determine if there is a match for `pattern` starting at index + // `i` in `data`. Only search up to the line ending that was + // found previously. + if let Some(match_) = pattern.find_at(&data[..this_line_end], i) { + // Record this index as the ending of the current line. + this_line_end = i; + + // The length of the match (that is, the line separator), in bytes. + let slen = match_.end() - match_.start(); + + if before { + out.write_all(&data[i..following_line_start])?; + following_line_start = i; + } else { + out.write_all(&data[i + slen..following_line_start])?; + following_line_start = i + slen; + } + } + } + + // After the loop terminates, write whatever bytes are remaining at + // the beginning of the buffer. + out.write_all(&data[0..following_line_start])?; + Ok(()) +} + +/// Write lines from `data` to stdout in reverse. +/// +/// This function writes to [`stdout`] each line appearing in `data`, +/// starting with the last line and ending with the first line. The +/// `separator` parameter defines what characters to use as a line +/// separator. +/// +/// If `before` is `false`, then this function assumes that the +/// `separator` appears at the end of each line, as in `"abc\ndef\n"`. +/// If `before` is `true`, then this function assumes that the +/// `separator` appears at the beginning of each line, as in +/// `"/abc/def"`. +fn buffer_tac(data: &[u8], before: bool, separator: &str) -> std::io::Result<()> { + let mut out = stdout(); + + // The number of bytes in the line separator. + let slen = separator.as_bytes().len(); + + // The index of the start of the next line in the `data`. + // + // As we scan through the `data` from right to left, we update this + // variable each time we find a new line. + // + // If `before` is `true`, then each line starts immediately before + // the line separator. Otherwise, each line starts immediately after + // the line separator. + let mut following_line_start = data.len(); + + // Iterate over each byte in the buffer in reverse. When we find a + // line separator, write the line to stdout. + // + // The `before` flag controls whether the line separator appears at + // the end of the line (as in "abc\ndef\n") or at the beginning of + // the line (as in "/abc/def"). + for i in memmem::rfind_iter(data, separator) { + if before { + out.write_all(&data[i..following_line_start])?; + following_line_start = i; + } else { + out.write_all(&data[i + slen..following_line_start])?; + following_line_start = i + slen; + } + } + + // After the loop terminates, write whatever bytes are remaining at + // the beginning of the buffer. + out.write_all(&data[0..following_line_start])?; + Ok(()) +} + +fn tac(filenames: Vec, before: bool, regex: bool, separator: &str) -> i32 { + let mut exit_code = 0; for filename in &filenames { let mut file = BufReader::new(if filename == "-" { @@ -93,11 +218,11 @@ fn tac(filenames: Vec, before: bool, _: bool, separator: &str) -> i32 { let path = Path::new(filename); if path.is_dir() || path.metadata().is_err() { if path.is_dir() { - show_error!("{}: read error: Invalid argument", filename); + show_error!("{}: read error: Invalid argument", filename.maybe_quote()); } else { show_error!( - "failed to open '{}' for reading: No such file or directory", - filename + "failed to open {} for reading: No such file or directory", + filename.quote() ); } exit_code = 1; @@ -106,7 +231,7 @@ fn tac(filenames: Vec, before: bool, _: bool, separator: &str) -> i32 { match File::open(path) { Ok(f) => Box::new(f) as Box, Err(e) => { - show_error!("failed to open '{}' for reading: {}", filename, e); + show_error!("failed to open {} for reading: {}", filename.quote(), e); exit_code = 1; continue; } @@ -115,68 +240,17 @@ fn tac(filenames: Vec, before: bool, _: bool, separator: &str) -> i32 { let mut data = Vec::new(); if let Err(e) = file.read_to_end(&mut data) { - show_error!("failed to read '{}': {}", filename, e); + show_error!("failed to read {}: {}", filename.quote(), e); exit_code = 1; continue; }; - - // find offsets in string of all separators - let mut offsets = Vec::new(); - let mut i = 0; - loop { - if i + slen > data.len() { - break; - } - - if &data[i..i + slen] == sbytes { - offsets.push(i); - i += slen; - } else { - i += 1; - } + if regex { + let pattern = crash_if_err!(1, regex::bytes::Regex::new(separator)); + buffer_tac_regex(&data, pattern, before) + } else { + buffer_tac(&data, before, separator) } - // If the file contains no line separators, then simply write - // the contents of the file directly to stdout. - if offsets.is_empty() { - out.write_all(&data) - .unwrap_or_else(|e| crash!(1, "failed to write to stdout: {}", e)); - return exit_code; - } - - // if there isn't a separator at the end of the file, fake it - if *offsets.last().unwrap() < data.len() - slen { - offsets.push(data.len()); - } - - let mut prev = *offsets.last().unwrap(); - let mut start = true; - for off in offsets.iter().rev().skip(1) { - // correctly handle case of no final separator in file - if start && prev == data.len() { - show_line(&mut out, &[], &data[*off + slen..prev], before); - start = false; - } else { - show_line(&mut out, sbytes, &data[*off + slen..prev], before); - } - prev = *off; - } - show_line(&mut out, sbytes, &data[0..prev], before); + .unwrap_or_else(|e| crash!(1, "failed to write to stdout: {}", e)); } - exit_code } - -fn show_line(out: &mut Stdout, sep: &[u8], dat: &[u8], before: bool) { - if before { - out.write_all(sep) - .unwrap_or_else(|e| crash!(1, "failed to write to stdout: {}", e)); - } - - out.write_all(dat) - .unwrap_or_else(|e| crash!(1, "failed to write to stdout: {}", e)); - - if !before { - out.write_all(sep) - .unwrap_or_else(|e| crash!(1, "failed to write to stdout: {}", e)); - } -} diff --git a/src/uu/tail/Cargo.toml b/src/uu/tail/Cargo.toml index 6928330de..6fd05b1a9 100644 --- a/src/uu/tail/Cargo.toml +++ b/src/uu/tail/Cargo.toml @@ -19,6 +19,8 @@ clap = { version = "2.33", features = ["wrap_help"] } libc = "0.2.42" uucore = { version=">=0.0.9", package="uucore", path="../../uucore", features=["ringbuffer"] } uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_procs" } + +[target.'cfg(windows)'.dependencies] winapi = { version="0.3", features=["fileapi", "handleapi", "processthreadsapi", "synchapi", "winbase"] } [target.'cfg(target_os = "redox")'.dependencies] @@ -31,3 +33,7 @@ libc = "0.2" [[bin]] name = "tail" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/tail/src/tail.rs b/src/uu/tail/src/tail.rs index 471c1a404..89fbe4d36 100644 --- a/src/uu/tail/src/tail.rs +++ b/src/uu/tail/src/tail.rs @@ -22,7 +22,7 @@ use chunks::ReverseChunks; use clap::{App, Arg}; use std::collections::VecDeque; use std::fmt; -use std::fs::File; +use std::fs::{File, Metadata}; use std::io::{stdin, stdout, BufRead, BufReader, Read, Seek, SeekFrom, Write}; use std::path::Path; use std::thread::sleep; @@ -32,6 +32,8 @@ use uucore::ringbuffer::RingBuffer; #[cfg(unix)] use crate::platform::stdin_is_pipe_or_fifo; +#[cfg(unix)] +use std::os::unix::fs::MetadataExt; pub mod options { pub mod verbosity { @@ -189,7 +191,8 @@ pub fn uumain(args: impl uucore::Args) -> i32 { continue; } let mut file = File::open(&path).unwrap(); - if is_seekable(&mut file) { + let md = file.metadata().unwrap(); + if is_seekable(&mut file) && get_block_size(&md) > 0 { bounded_tail(&mut file, &settings); if settings.follow { let reader = BufReader::new(file); @@ -213,7 +216,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about("output the last part of files") // TODO: add usage @@ -437,6 +440,8 @@ fn unbounded_tail(reader: &mut BufReader, settings: &Settings) { fn is_seekable(file: &mut T) -> bool { file.seek(SeekFrom::Current(0)).is_ok() + && file.seek(SeekFrom::End(0)).is_ok() + && file.seek(SeekFrom::Start(0)).is_ok() } #[inline] @@ -464,3 +469,14 @@ fn parse_num(src: &str) -> Result<(usize, bool), ParseSizeError> { parse_size(size_string).map(|n| (n, starting_with)) } + +fn get_block_size(md: &Metadata) -> u64 { + #[cfg(unix)] + { + md.blocks() + } + #[cfg(not(unix))] + { + md.len() + } +} diff --git a/src/uu/tee/Cargo.toml b/src/uu/tee/Cargo.toml index 01c190698..900ef3564 100644 --- a/src/uu/tee/Cargo.toml +++ b/src/uu/tee/Cargo.toml @@ -24,3 +24,7 @@ uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_p [[bin]] name = "tee" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/tee/src/tee.rs b/src/uu/tee/src/tee.rs index a207dee63..d2fb015bf 100644 --- a/src/uu/tee/src/tee.rs +++ b/src/uu/tee/src/tee.rs @@ -12,7 +12,8 @@ use clap::{crate_version, App, Arg}; use retain_mut::RetainMut; use std::fs::OpenOptions; use std::io::{copy, sink, stdin, stdout, Error, ErrorKind, Read, Result, Write}; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; +use uucore::display::Quotable; #[cfg(unix)] use uucore::libc; @@ -32,12 +33,12 @@ struct Options { files: Vec, } -fn get_usage() -> String { - format!("{0} [OPTION]... [FILE]...", executable!()) +fn usage() -> String { + format!("{0} [OPTION]... [FILE]...", uucore::execution_phrase()) } pub fn uumain(args: impl uucore::Args) -> i32 { - let usage = get_usage(); + let usage = usage(); let matches = uu_app().usage(&usage[..]).get_matches_from(args); @@ -57,7 +58,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .after_help("If a FILE is -, it refers to a file named - .") @@ -167,7 +168,7 @@ impl Write for MultiWriter { let result = writer.write_all(buf); match result { Err(f) => { - show_error!("{}: {}", writer.name, f.to_string()); + show_error!("{}: {}", writer.name.maybe_quote(), f); false } _ => true, @@ -181,7 +182,7 @@ impl Write for MultiWriter { let result = writer.flush(); match result { Err(f) => { - show_error!("{}: {}", writer.name, f.to_string()); + show_error!("{}: {}", writer.name.maybe_quote(), f); false } _ => true, @@ -214,7 +215,7 @@ impl Read for NamedReader { fn read(&mut self, buf: &mut [u8]) -> Result { match self.inner.read(buf) { Err(f) => { - show_error!("{}: {}", Path::new("stdin").display(), f.to_string()); + show_error!("stdin: {}", f); Err(f) } okay => okay, diff --git a/src/uu/test/Cargo.toml b/src/uu/test/Cargo.toml index b9931185c..3fe531d1d 100644 --- a/src/uu/test/Cargo.toml +++ b/src/uu/test/Cargo.toml @@ -26,3 +26,7 @@ redox_syscall = "0.2" [[bin]] name = "test" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/test/src/parser.rs b/src/uu/test/src/parser.rs index 5eec781ba..ce4c0dec0 100644 --- a/src/uu/test/src/parser.rs +++ b/src/uu/test/src/parser.rs @@ -10,6 +10,24 @@ use std::ffi::OsString; use std::iter::Peekable; +use uucore::display::Quotable; +use uucore::show_error; + +/// Represents one of the binary comparison operators for strings, integers, or files +#[derive(Debug, PartialEq)] +pub enum Operator { + String(OsString), + Int(OsString), + File(OsString), +} + +/// Represents one of the unary test operators for strings or files +#[derive(Debug, PartialEq)] +pub enum UnaryOperator { + StrlenOp(OsString), + FiletestOp(OsString), +} + /// Represents a parsed token from a test expression #[derive(Debug, PartialEq)] pub enum Symbol { @@ -17,11 +35,8 @@ pub enum Symbol { Bang, BoolOp(OsString), Literal(OsString), - StringOp(OsString), - IntOp(OsString), - FileOp(OsString), - StrlenOp(OsString), - FiletestOp(OsString), + Op(Operator), + UnaryOp(UnaryOperator), None, } @@ -31,17 +46,22 @@ impl Symbol { /// Returns Symbol::None in place of None fn new(token: Option) -> Symbol { match token { - Some(s) => match s.to_string_lossy().as_ref() { - "(" => Symbol::LParen, - "!" => Symbol::Bang, - "-a" | "-o" => Symbol::BoolOp(s), - "=" | "==" | "!=" => Symbol::StringOp(s), - "-eq" | "-ge" | "-gt" | "-le" | "-lt" | "-ne" => Symbol::IntOp(s), - "-ef" | "-nt" | "-ot" => Symbol::FileOp(s), - "-n" | "-z" => Symbol::StrlenOp(s), - "-b" | "-c" | "-d" | "-e" | "-f" | "-g" | "-G" | "-h" | "-k" | "-L" | "-O" - | "-p" | "-r" | "-s" | "-S" | "-t" | "-u" | "-w" | "-x" => Symbol::FiletestOp(s), - _ => Symbol::Literal(s), + Some(s) => match s.to_str() { + Some(t) => match t { + "(" => Symbol::LParen, + "!" => Symbol::Bang, + "-a" | "-o" => Symbol::BoolOp(s), + "=" | "==" | "!=" => Symbol::Op(Operator::String(s)), + "-eq" | "-ge" | "-gt" | "-le" | "-lt" | "-ne" => Symbol::Op(Operator::Int(s)), + "-ef" | "-nt" | "-ot" => Symbol::Op(Operator::File(s)), + "-n" | "-z" => Symbol::UnaryOp(UnaryOperator::StrlenOp(s)), + "-b" | "-c" | "-d" | "-e" | "-f" | "-g" | "-G" | "-h" | "-k" | "-L" | "-O" + | "-p" | "-r" | "-s" | "-S" | "-t" | "-u" | "-w" | "-x" => { + Symbol::UnaryOp(UnaryOperator::FiletestOp(s)) + } + _ => Symbol::Literal(s), + }, + None => Symbol::Literal(s), }, None => Symbol::None, } @@ -60,11 +80,11 @@ impl Symbol { Symbol::Bang => OsString::from("!"), Symbol::BoolOp(s) | Symbol::Literal(s) - | Symbol::StringOp(s) - | Symbol::IntOp(s) - | Symbol::FileOp(s) - | Symbol::StrlenOp(s) - | Symbol::FiletestOp(s) => s, + | Symbol::Op(Operator::String(s)) + | Symbol::Op(Operator::Int(s)) + | Symbol::Op(Operator::File(s)) + | Symbol::UnaryOp(UnaryOperator::StrlenOp(s)) + | Symbol::UnaryOp(UnaryOperator::FiletestOp(s)) => s, Symbol::None => panic!(), }) } @@ -78,7 +98,6 @@ impl Symbol { /// /// EXPR → TERM | EXPR BOOLOP EXPR /// TERM → ( EXPR ) -/// TERM → ( ) /// TERM → ! EXPR /// TERM → UOP str /// UOP → STRLEN | FILETEST @@ -113,6 +132,20 @@ impl Parser { Symbol::new(self.tokens.next()) } + /// Consume the next token & verify that it matches the provided value. + /// + /// # Panics + /// + /// Panics if the next token does not match the provided value. + /// + /// TODO: remove panics and convert Parser to return error messages. + fn expect(&mut self, value: &str) { + match self.next_token() { + Symbol::Literal(s) if s == value => (), + _ => panic!("expected ‘{}’", value), + } + } + /// Peek at the next token from the input stream, returning it as a Symbol. /// The stream is unchanged and will return the same Symbol on subsequent /// calls to `next()` or `peek()`. @@ -144,8 +177,7 @@ impl Parser { match symbol { Symbol::LParen => self.lparen(), Symbol::Bang => self.bang(), - Symbol::StrlenOp(_) => self.uop(symbol), - Symbol::FiletestOp(_) => self.uop(symbol), + Symbol::UnaryOp(_) => self.uop(symbol), Symbol::None => self.stack.push(symbol), literal => self.literal(literal), } @@ -154,21 +186,75 @@ impl Parser { /// Parse a (possibly) parenthesized expression. /// /// test has no reserved keywords, so "(" will be interpreted as a literal - /// if it is followed by nothing or a comparison operator OP. + /// in certain cases: + /// + /// * when found at the end of the token stream + /// * when followed by a binary operator that is not _itself_ interpreted + /// as a literal + /// fn lparen(&mut self) { - match self.peek() { - // lparen is a literal when followed by nothing or comparison - Symbol::None | Symbol::StringOp(_) | Symbol::IntOp(_) | Symbol::FileOp(_) => { + // Look ahead up to 3 tokens to determine if the lparen is being used + // as a grouping operator or should be treated as a literal string + let peek3: Vec = self + .tokens + .clone() + .take(3) + .map(|token| Symbol::new(Some(token))) + .collect(); + + match peek3.as_slice() { + // case 1: lparen is a literal when followed by nothing + [] => self.literal(Symbol::LParen.into_literal()), + + // case 2: error if end of stream is `( ` + [symbol] => { + show_error!("missing argument after ‘{:?}’", symbol); + std::process::exit(2); + } + + // case 3: `( uop )` → parenthesized unary operation; + // this case ensures we don’t get confused by `( -f ) )` + // or `( -f ( )`, for example + [Symbol::UnaryOp(_), _, Symbol::Literal(s)] if s == ")" => { + let symbol = self.next_token(); + self.uop(symbol); + self.expect(")"); + } + + // case 4: binary comparison of literal lparen, e.g. `( != )` + [Symbol::Op(_), Symbol::Literal(s)] | [Symbol::Op(_), Symbol::Literal(s), _] + if s == ")" => + { self.literal(Symbol::LParen.into_literal()); } - // empty parenthetical - Symbol::Literal(s) if s == ")" => {} + + // case 5: after handling the prior cases, any single token inside + // parentheses is a literal, e.g. `( -f )` + [_, Symbol::Literal(s)] | [_, Symbol::Literal(s), _] if s == ")" => { + let symbol = self.next_token(); + self.literal(symbol); + self.expect(")"); + } + + // case 6: two binary ops in a row, treat the first op as a literal + [Symbol::Op(_), Symbol::Op(_), _] => { + let symbol = self.next_token(); + self.literal(symbol); + self.expect(")"); + } + + // case 7: if earlier cases didn’t match, `( op …` + // indicates binary comparison of literal lparen with + // anything _except_ ")" (case 4) + [Symbol::Op(_), _] | [Symbol::Op(_), _, _] => { + self.literal(Symbol::LParen.into_literal()); + } + + // Otherwise, lparen indicates the start of a parenthesized + // expression _ => { self.expr(); - match self.next_token() { - Symbol::Literal(s) if s == ")" => (), - _ => panic!("expected ')'"), - } + self.expect(")"); } } } @@ -192,7 +278,7 @@ impl Parser { /// fn bang(&mut self) { match self.peek() { - Symbol::StringOp(_) | Symbol::IntOp(_) | Symbol::FileOp(_) | Symbol::BoolOp(_) => { + Symbol::Op(_) | Symbol::BoolOp(_) => { // we need to peek ahead one more token to disambiguate the first // three cases listed above let peek2 = Symbol::new(self.tokens.clone().nth(1)); @@ -200,7 +286,7 @@ impl Parser { match peek2 { // case 1: `! ` // case 3: `! = OP str` - Symbol::StringOp(_) | Symbol::None => { + Symbol::Op(_) | Symbol::None => { // op is literal let op = self.next_token().into_literal(); self.literal(op); @@ -293,18 +379,15 @@ impl Parser { self.stack.push(token.into_literal()); // EXPR → str OP str - match self.peek() { - Symbol::StringOp(_) | Symbol::IntOp(_) | Symbol::FileOp(_) => { - let op = self.next_token(); + if let Symbol::Op(_) = self.peek() { + let op = self.next_token(); - match self.next_token() { - Symbol::None => panic!("missing argument after {:?}", op), - token => self.stack.push(token.into_literal()), - } - - self.stack.push(op); + match self.next_token() { + Symbol::None => panic!("missing argument after {:?}", op), + token => self.stack.push(token.into_literal()), } - _ => {} + + self.stack.push(op); } } @@ -314,7 +397,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.quote())), None => Ok(()), } } diff --git a/src/uu/test/src/test.rs b/src/uu/test/src/test.rs index bed1472e2..5ce798bfa 100644 --- a/src/uu/test/src/test.rs +++ b/src/uu/test/src/test.rs @@ -11,10 +11,9 @@ mod parser; use clap::{crate_version, App, AppSettings}; -use parser::{parse, Symbol}; +use parser::{parse, Operator, Symbol, UnaryOperator}; use std::ffi::{OsStr, OsString}; -use std::path::Path; -use uucore::executable; +use uucore::{display::Quotable, show_error}; const USAGE: &str = "test EXPRESSION or: test @@ -87,17 +86,14 @@ the version described here. Please refer to your shell's documentation for details about the options it supports."; pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .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 binary_name = uucore::util_name(); let mut args: Vec<_> = args.collect(); if binary_name.ends_with('[') { @@ -117,8 +113,8 @@ pub fn uumain(mut args: impl uucore::Args) -> i32 { } // If invoked via name '[', matching ']' must be in the last arg let last = args.pop(); - if last != Some(OsString::from("]")) { - eprintln!("[: missing ']'"); + if last.as_deref() != Some(OsStr::new("]")) { + show_error!("missing ']'"); return 2; } } @@ -134,7 +130,7 @@ pub fn uumain(mut args: impl uucore::Args) -> i32 { } } Err(e) => { - eprintln!("test: {}", e); + show_error!("{}", e); 2 } } @@ -160,19 +156,19 @@ fn eval(stack: &mut Vec) -> Result { Ok(!result) } - Some(Symbol::StringOp(op)) => { + Some(Symbol::Op(Operator::String(op))) => { let b = stack.pop(); let a = stack.pop(); Ok(if op == "!=" { a != b } else { a == b }) } - Some(Symbol::IntOp(op)) => { + Some(Symbol::Op(Operator::Int(op))) => { let b = pop_literal!(); let a = pop_literal!(); Ok(integers(&a, &b, &op)?) } - Some(Symbol::FileOp(_op)) => unimplemented!(), - Some(Symbol::StrlenOp(op)) => { + Some(Symbol::Op(Operator::File(_op))) => unimplemented!(), + Some(Symbol::UnaryOp(UnaryOperator::StrlenOp(op))) => { let s = match stack.pop() { Some(Symbol::Literal(s)) => s, Some(Symbol::None) => OsString::from(""), @@ -190,12 +186,12 @@ fn eval(stack: &mut Vec) -> Result { !s.is_empty() }) } - Some(Symbol::FiletestOp(op)) => { - let op = op.to_string_lossy(); + Some(Symbol::UnaryOp(UnaryOperator::FiletestOp(op))) => { + let op = op.to_str().unwrap(); let f = pop_literal!(); - Ok(match op.as_ref() { + Ok(match op { "-b" => path(&f, PathCondition::BlockSpecial), "-c" => path(&f, PathCondition::CharacterSpecial), "-d" => path(&f, PathCondition::Directory), @@ -232,31 +228,33 @@ 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: &OsStr| format!("invalid integer {}", value.quote()); - let a = a.to_string_lossy(); - let a: i64 = a.parse().map_err(|_| format_err(a))?; + let a: i64 = a + .to_str() + .and_then(|s| s.parse().ok()) + .ok_or_else(|| format_err(a))?; - let b = b.to_string_lossy(); - let b: i64 = b.parse().map_err(|_| format_err(b))?; + let b: i64 = b + .to_str() + .and_then(|s| s.parse().ok()) + .ok_or_else(|| format_err(b))?; - let operator = op.to_string_lossy(); - Ok(match operator.as_ref() { - "-eq" => a == b, - "-ne" => a != b, - "-gt" => a > b, - "-ge" => a >= b, - "-lt" => a < b, - "-le" => a <= b, - _ => return Err(format!("unknown operator '{}'", operator)), + Ok(match op.to_str() { + Some("-eq") => a == b, + Some("-ne") => a != b, + Some("-gt") => a > b, + Some("-ge") => a >= b, + Some("-lt") => a < b, + Some("-le") => a <= b, + _ => return Err(format!("unknown operator {}", op.quote())), }) } fn isatty(fd: &OsStr) -> Result { - let fd = fd.to_string_lossy(); - - fd.parse() - .map_err(|_| format!("invalid integer '{}'", fd)) + fd.to_str() + .and_then(|s| s.parse().ok()) + .ok_or_else(|| format!("invalid integer {}", fd.quote())) .map(|i| { #[cfg(not(target_os = "redox"))] unsafe { diff --git a/src/uu/timeout/src/timeout.rs b/src/uu/timeout/src/timeout.rs index 464414c5e..f686dde3b 100644 --- a/src/uu/timeout/src/timeout.rs +++ b/src/uu/timeout/src/timeout.rs @@ -16,14 +16,18 @@ use clap::{crate_version, App, AppSettings, Arg}; use std::io::ErrorKind; use std::process::{Command, Stdio}; use std::time::Duration; +use uucore::display::Quotable; use uucore::process::ChildExt; use uucore::signals::{signal_by_name_or_value, signal_name_by_value}; use uucore::InvalidEncodingHandling; static ABOUT: &str = "Start COMMAND, and kill it if still running after DURATION."; -fn get_usage() -> String { - format!("{0} [OPTION] DURATION COMMAND...", executable!()) +fn usage() -> String { + format!( + "{0} [OPTION] DURATION COMMAND...", + uucore::execution_phrase() + ) } const ERR_EXIT_STATUS: i32 = 125; @@ -58,7 +62,7 @@ impl Config { let signal_result = signal_by_name_or_value(signal_); match signal_result { None => { - unreachable!("invalid signal '{}'", signal_); + unreachable!("invalid signal {}", signal_.quote()); } Some(signal_value) => signal_value, } @@ -100,7 +104,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .collect_str(InvalidEncodingHandling::ConvertLossy) .accept_any(); - let usage = get_usage(); + let usage = usage(); let app = uu_app().usage(&usage[..]); @@ -213,12 +217,12 @@ fn timeout( Ok(None) => { if verbose { show_error!( - "sending signal {} to command '{}'", + "sending signal {} to command {}", signal_name_by_value(signal).unwrap(), - cmd[0] + cmd[0].quote() ); } - return_if_err!(ERR_EXIT_STATUS, process.send_signal(signal)); + crash_if_err!(ERR_EXIT_STATUS, process.send_signal(signal)); if let Some(kill_after) = kill_after { match process.wait_or_timeout(kill_after) { Ok(Some(status)) => { @@ -230,15 +234,15 @@ fn timeout( } Ok(None) => { if verbose { - show_error!("sending signal KILL to command '{}'", cmd[0]); + show_error!("sending signal KILL to command {}", cmd[0].quote()); } - return_if_err!( + crash_if_err!( ERR_EXIT_STATUS, process.send_signal( uucore::signals::signal_by_name_or_value("KILL").unwrap() ) ); - return_if_err!(ERR_EXIT_STATUS, process.wait()); + crash_if_err!(ERR_EXIT_STATUS, process.wait()); 137 } Err(_) => 124, @@ -248,7 +252,7 @@ fn timeout( } } Err(_) => { - return_if_err!(ERR_EXIT_STATUS, process.send_signal(signal)); + crash_if_err!(ERR_EXIT_STATUS, process.send_signal(signal)); ERR_EXIT_STATUS } } diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index 49efa676a..6997def09 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -6,7 +6,7 @@ // For the full copyright and license information, please view the LICENSE file // that was distributed with this source code. -// spell-checker:ignore (ToDO) filetime strptime utcoff strs datetime MMDDhhmm +// spell-checker:ignore (ToDO) filetime strptime utcoff strs datetime MMDDhhmm clapv pub extern crate filetime; @@ -17,6 +17,7 @@ use clap::{crate_version, App, Arg, ArgGroup}; use filetime::*; use std::fs::{self, File}; use std::path::Path; +use uucore::display::Quotable; use uucore::error::{FromIo, UError, UResult, USimpleError}; static ABOUT: &str = "Update the access and modification times of each FILE to the current time."; @@ -47,13 +48,13 @@ fn local_tm_to_filetime(tm: time::Tm) -> FileTime { FileTime::from_unix_time(ts.sec as i64, ts.nsec as u32) } -fn get_usage() -> String { - format!("{0} [OPTION]... [USER]", executable!()) +fn usage() -> String { + format!("{0} [OPTION]... [USER]", uucore::execution_phrase()) } #[uucore_procs::gen_uumain] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let usage = get_usage(); + let usage = usage(); let matches = uu_app().usage(&usage[..]).get_matches_from(args); @@ -82,7 +83,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } if let Err(e) = File::create(path) { - show!(e.map_err_context(|| format!("cannot touch '{}'", path.display()))); + show!(e.map_err_context(|| format!("cannot touch {}", path.quote()))); continue; }; @@ -122,14 +123,14 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } else { filetime::set_file_times(path, atime, mtime) } - .map_err_context(|| format!("setting times of '{}'", path.display()))?; + .map_err_context(|| format!("setting times of {}", path.quote()))?; } Ok(()) } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .arg( @@ -175,6 +176,7 @@ pub fn uu_app() -> App<'static, 'static> { Arg::with_name(options::sources::REFERENCE) .short("r") .long(options::sources::REFERENCE) + .alias("ref") // clapv3 .help("use this file's times instead of the current time") .value_name("FILE"), ) @@ -209,7 +211,7 @@ fn stat(path: &Path, follow: bool) -> UResult<(FileTime, FileTime)> { } else { fs::metadata(path) } - .map_err_context(|| format!("failed to get attributes of '{}'", path.display()))?; + .map_err_context(|| format!("failed to get attributes of {}", path.quote()))?; Ok(( FileTime::from_last_access_time(&metadata), @@ -249,11 +251,16 @@ fn parse_timestamp(s: &str) -> UResult { 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)), - _ => return Err(USimpleError::new(1, format!("invalid date format '{}'", s))), + _ => { + return Err(USimpleError::new( + 1, + format!("invalid date format {}", s.quote()), + )) + } }; let tm = time::strptime(&ts, format) - .map_err(|_| USimpleError::new(1, format!("invalid date format '{}'", s)))?; + .map_err(|_| USimpleError::new(1, format!("invalid date format {}", s.quote())))?; let mut local = to_local(tm); local.tm_isdst = -1; @@ -269,7 +276,10 @@ fn parse_timestamp(s: &str) -> UResult { }; let tm2 = time::at(ts); if tm.tm_hour != tm2.tm_hour { - return Err(USimpleError::new(1, format!("invalid date format '{}'", s))); + return Err(USimpleError::new( + 1, + format!("invalid date format {}", s.quote()), + )); } Ok(ft) diff --git a/src/uu/tr/Cargo.toml b/src/uu/tr/Cargo.toml index db8f0fa36..bdcabcda4 100644 --- a/src/uu/tr/Cargo.toml +++ b/src/uu/tr/Cargo.toml @@ -23,3 +23,7 @@ nom = "5.1.2" [[bin]] name = "tr" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/tr/src/tr.rs b/src/uu/tr/src/tr.rs index 872f894c2..4510e9bd9 100644 --- a/src/uu/tr/src/tr.rs +++ b/src/uu/tr/src/tr.rs @@ -37,7 +37,7 @@ mod options { } fn get_usage() -> String { - format!("{} [OPTION]... SET1 [SET2]", executable!()) + format!("{} [OPTION]... SET1 [SET2]", uucore::execution_phrase()) } fn get_long_usage() -> String { @@ -77,7 +77,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { if sets.is_empty() { show_error!( "missing operand\nTry '{} --help' for more information.", - executable!() + uucore::execution_phrase() ); return 1; } @@ -86,7 +86,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { show_error!( "missing operand after '{}'\nTry '{} --help' for more information.", sets[0], - executable!() + uucore::execution_phrase() ); return 1; } @@ -95,7 +95,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { show_error!( "extra operand '{}'\nTry '{} --help' for more information.", sets[0], - executable!() + uucore::execution_phrase() ); return 1; } @@ -178,7 +178,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .arg( diff --git a/src/uu/true/src/true.rs b/src/uu/true/src/true.rs index f84a89176..6b4a87bf1 100644 --- a/src/uu/true/src/true.rs +++ b/src/uu/true/src/true.rs @@ -10,7 +10,6 @@ extern crate uucore; use clap::App; use uucore::error::UResult; -use uucore::executable; #[uucore_procs::gen_uumain] pub fn uumain(args: impl uucore::Args) -> UResult<()> { @@ -19,5 +18,5 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) } diff --git a/src/uu/truncate/Cargo.toml b/src/uu/truncate/Cargo.toml index 6441f2e14..e779e32ba 100644 --- a/src/uu/truncate/Cargo.toml +++ b/src/uu/truncate/Cargo.toml @@ -22,3 +22,7 @@ uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_p [[bin]] name = "truncate" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/truncate/src/truncate.rs b/src/uu/truncate/src/truncate.rs index bb7aa61d4..6fb1f06f6 100644 --- a/src/uu/truncate/src/truncate.rs +++ b/src/uu/truncate/src/truncate.rs @@ -15,6 +15,7 @@ use std::convert::TryFrom; use std::fs::{metadata, OpenOptions}; use std::io::ErrorKind; use std::path::Path; +use uucore::display::Quotable; use uucore::parse_size::{parse_size, ParseSizeError}; #[derive(Debug, Eq, PartialEq)] @@ -63,8 +64,8 @@ pub mod options { pub static ARG_FILES: &str = "files"; } -fn get_usage() -> String { - format!("{0} [OPTION]... [FILE]...", executable!()) +fn usage() -> String { + format!("{0} [OPTION]... [FILE]...", uucore::execution_phrase()) } fn get_long_usage() -> String { @@ -90,7 +91,7 @@ fn get_long_usage() -> String { } pub fn uumain(args: impl uucore::Args) -> i32 { - let usage = get_usage(); + let usage = usage(); let long_usage = get_long_usage(); let matches = uu_app() @@ -120,8 +121,8 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let reference = matches.value_of(options::REFERENCE).map(String::from); crash!( 1, - "cannot stat '{}': No such file or directory", - reference.unwrap_or_else(|| "".to_string()) + "cannot stat {}: No such file or directory", + reference.as_deref().unwrap_or("").quote() ); // TODO: fix '--no-create' see test_reference and test_truncate_bytes_size } _ => crash!(1, "{}", e.to_string()), @@ -133,7 +134,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .arg( diff --git a/src/uu/tsort/Cargo.toml b/src/uu/tsort/Cargo.toml index ec9dd05f9..055615003 100644 --- a/src/uu/tsort/Cargo.toml +++ b/src/uu/tsort/Cargo.toml @@ -22,3 +22,7 @@ uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_p [[bin]] name = "tsort" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/tsort/src/tsort.rs b/src/uu/tsort/src/tsort.rs index 0a323f837..11798db13 100644 --- a/src/uu/tsort/src/tsort.rs +++ b/src/uu/tsort/src/tsort.rs @@ -14,6 +14,7 @@ use std::collections::{HashMap, HashSet}; use std::fs::File; use std::io::{stdin, BufRead, BufReader, Read}; use std::path::Path; +use uucore::display::Quotable; use uucore::InvalidEncodingHandling; static SUMMARY: &str = "Topological sort the strings in FILE. @@ -45,7 +46,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { file_buf = match File::open(Path::new(&input)) { Ok(a) => a, _ => { - show_error!("{}: No such file or directory", input); + show_error!("{}: No such file or directory", input.maybe_quote()); return 1; } }; @@ -68,7 +69,11 @@ pub fn uumain(args: impl uucore::Args) -> i32 { for ab in tokens.chunks(2) { match ab.len() { 2 => g.add_edge(&ab[0], &ab[1]), - _ => crash!(1, "{}: input contains an odd number of tokens", input), + _ => crash!( + 1, + "{}: input contains an odd number of tokens", + input.maybe_quote() + ), } } } @@ -90,7 +95,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .usage(USAGE) .about(SUMMARY) diff --git a/src/uu/tty/src/tty.rs b/src/uu/tty/src/tty.rs index 7412cdf45..94e2e6b24 100644 --- a/src/uu/tty/src/tty.rs +++ b/src/uu/tty/src/tty.rs @@ -9,9 +9,6 @@ // spell-checker:ignore (ToDO) ttyname filedesc -#[macro_use] -extern crate uucore; - use clap::{crate_version, App, Arg}; use std::ffi::CStr; use std::io::Write; @@ -23,12 +20,12 @@ mod options { pub const SILENT: &str = "silent"; } -fn get_usage() -> String { - format!("{0} [OPTION]...", executable!()) +fn usage() -> String { + format!("{0} [OPTION]...", uucore::execution_phrase()) } pub fn uumain(args: impl uucore::Args) -> i32 { - let usage = get_usage(); + let usage = usage(); let args = args .collect_str(InvalidEncodingHandling::ConvertLossy) .accept_any(); @@ -78,7 +75,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .arg( diff --git a/src/uu/uname/src/uname.rs b/src/uu/uname/src/uname.rs index abd50d1b8..2c396081e 100644 --- a/src/uu/uname/src/uname.rs +++ b/src/uu/uname/src/uname.rs @@ -50,10 +50,10 @@ const HOST_OS: &str = "Fuchsia"; const HOST_OS: &str = "Redox"; pub fn uumain(args: impl uucore::Args) -> i32 { - let usage = format!("{} [OPTION]...", executable!()); + let usage = format!("{} [OPTION]...", uucore::execution_phrase()); let matches = uu_app().usage(&usage[..]).get_matches_from(args); - let uname = return_if_err!(1, PlatformInfo::new()); + let uname = crash_if_err!(1, PlatformInfo::new()); let mut output = String::new(); let all = matches.is_present(options::ALL); @@ -119,7 +119,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .arg(Arg::with_name(options::ALL) diff --git a/src/uu/unexpand/Cargo.toml b/src/uu/unexpand/Cargo.toml index a0aa3c1de..8b1169151 100644 --- a/src/uu/unexpand/Cargo.toml +++ b/src/uu/unexpand/Cargo.toml @@ -23,3 +23,7 @@ uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_p [[bin]] name = "unexpand" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/unexpand/src/unexpand.rs b/src/uu/unexpand/src/unexpand.rs index 50e3f186d..95383b89d 100644 --- a/src/uu/unexpand/src/unexpand.rs +++ b/src/uu/unexpand/src/unexpand.rs @@ -16,6 +16,7 @@ use std::fs::File; use std::io::{stdin, stdout, BufRead, BufReader, BufWriter, Read, Stdout, Write}; use std::str::from_utf8; use unicode_width::UnicodeWidthChar; +use uucore::display::Quotable; use uucore::InvalidEncodingHandling; static NAME: &str = "unexpand"; @@ -102,7 +103,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .name(NAME) .version(crate_version!()) .usage(USAGE) @@ -141,9 +142,9 @@ fn open(path: String) -> BufReader> { if path == "-" { BufReader::new(Box::new(stdin()) as Box) } else { - file_buf = match File::open(&path[..]) { + file_buf = match File::open(&path) { Ok(a) => a, - Err(e) => crash!(1, "{}: {}", &path[..], e), + Err(e) => crash!(1, "{}: {}", path.maybe_quote(), e), }; BufReader::new(Box::new(file_buf) as Box) } @@ -178,13 +179,13 @@ fn write_tabs( break; } - safe_unwrap!(output.write_all(b"\t")); + crash_if_err!(1, output.write_all(b"\t")); scol += nts; } } while col > scol { - safe_unwrap!(output.write_all(b" ")); + crash_if_err!(1, output.write_all(b" ")); scol += 1; } } @@ -272,7 +273,7 @@ fn unexpand(options: Options) { init, true, ); - safe_unwrap!(output.write_all(&buf[byte..])); + crash_if_err!(1, output.write_all(&buf[byte..])); scol = col; break; } @@ -292,7 +293,7 @@ fn unexpand(options: Options) { }; if !tabs_buffered { - safe_unwrap!(output.write_all(&buf[byte..byte + nbytes])); + crash_if_err!(1, output.write_all(&buf[byte..byte + nbytes])); scol = col; // now printed up to this column } } @@ -317,7 +318,7 @@ fn unexpand(options: Options) { } else { 0 }; - safe_unwrap!(output.write_all(&buf[byte..byte + nbytes])); + crash_if_err!(1, output.write_all(&buf[byte..byte + nbytes])); scol = col; // we've now printed up to this column } } @@ -336,7 +337,7 @@ fn unexpand(options: Options) { init, true, ); - safe_unwrap!(output.flush()); + crash_if_err!(1, output.flush()); buf.truncate(0); // clear out the buffer } } diff --git a/src/uu/uniq/Cargo.toml b/src/uu/uniq/Cargo.toml index aed487b59..06ba22688 100644 --- a/src/uu/uniq/Cargo.toml +++ b/src/uu/uniq/Cargo.toml @@ -16,11 +16,15 @@ path = "src/uniq.rs" [dependencies] clap = { version = "2.33", features = ["wrap_help"] } -strum = "0.20" -strum_macros = "0.20" +strum = "0.21" +strum_macros = "0.21" uucore = { version=">=0.0.9", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "uniq" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/uniq/src/uniq.rs b/src/uu/uniq/src/uniq.rs index 20639c850..f84bfc26d 100644 --- a/src/uu/uniq/src/uniq.rs +++ b/src/uu/uniq/src/uniq.rs @@ -14,6 +14,7 @@ use std::io::{stdin, stdout, BufRead, BufReader, BufWriter, Read, Result, Write} use std::path::Path; use std::str::FromStr; use strum_macros::{AsRefStr, EnumString}; +use uucore::display::Quotable; static ABOUT: &str = "Report or omit repeated lines."; pub mod options { @@ -217,12 +218,22 @@ fn get_line_string(io_line: Result>) -> String { fn opt_parsed(opt_name: &str, matches: &ArgMatches) -> Option { matches.value_of(opt_name).map(|arg_str| { let opt_val: Option = arg_str.parse().ok(); - opt_val.unwrap_or_else(|| crash!(1, "Invalid argument for {}: {}", opt_name, arg_str)) + opt_val.unwrap_or_else(|| { + crash!( + 1, + "Invalid argument for {}: {}", + opt_name, + arg_str.maybe_quote() + ) + }) }) } -fn get_usage() -> String { - format!("{0} [OPTION]... [INPUT [OUTPUT]]...", executable!()) +fn usage() -> String { + format!( + "{0} [OPTION]... [INPUT [OUTPUT]]...", + uucore::execution_phrase() + ) } fn get_long_usage() -> String { @@ -235,7 +246,7 @@ fn get_long_usage() -> String { } pub fn uumain(args: impl uucore::Args) -> i32 { - let usage = get_usage(); + let usage = usage(); let long_usage = get_long_usage(); let matches = uu_app() @@ -281,7 +292,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .arg( diff --git a/src/uu/unlink/Cargo.toml b/src/uu/unlink/Cargo.toml index 3f13a7231..558d18422 100644 --- a/src/uu/unlink/Cargo.toml +++ b/src/uu/unlink/Cargo.toml @@ -16,7 +16,6 @@ path = "src/unlink.rs" [dependencies] clap = { version = "2.33", features = ["wrap_help"] } -libc = "0.2.42" uucore = { version=">=0.0.9", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/unlink/src/unlink.rs b/src/uu/unlink/src/unlink.rs index 49f17cb12..1b4e4c998 100644 --- a/src/uu/unlink/src/unlink.rs +++ b/src/uu/unlink/src/unlink.rs @@ -7,96 +7,32 @@ /* last synced with: unlink (GNU coreutils) 8.21 */ -// spell-checker:ignore (ToDO) lstat IFLNK IFMT IFREG - #[macro_use] extern crate uucore; -use clap::{crate_version, App, Arg}; -use libc::{lstat, stat, unlink}; -use libc::{S_IFLNK, S_IFMT, S_IFREG}; -use std::ffi::CString; -use std::io::{Error, ErrorKind}; -use uucore::InvalidEncodingHandling; +use std::fs::remove_file; +use std::path::Path; -static ABOUT: &str = "Unlink the file at [FILE]."; +use clap::{crate_version, App, Arg}; + +use uucore::display::Quotable; +use uucore::error::{FromIo, UResult}; + +static ABOUT: &str = "Unlink the file at FILE."; static OPT_PATH: &str = "FILE"; -fn get_usage() -> String { - format!("{} [OPTION]... FILE", executable!()) -} +#[uucore_procs::gen_uumain] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let matches = uu_app().get_matches_from(args); -pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args - .collect_str(InvalidEncodingHandling::ConvertLossy) - .accept_any(); + let path: &Path = matches.value_of_os(OPT_PATH).unwrap().as_ref(); - let usage = get_usage(); - - let matches = uu_app().usage(&usage[..]).get_matches_from(args); - - let paths: Vec = matches - .values_of(OPT_PATH) - .map(|v| v.map(ToString::to_string).collect()) - .unwrap_or_default(); - - if paths.is_empty() { - crash!( - 1, - "missing operand\nTry '{0} --help' for more information.", - executable!() - ); - } else if paths.len() > 1 { - crash!( - 1, - "extra operand: '{1}'\nTry '{0} --help' for more information.", - executable!(), - paths[1] - ); - } - - let c_string = CString::new(paths[0].clone()).unwrap(); // unwrap() cannot fail, the string comes from argv so it cannot contain a \0. - - let st_mode = { - #[allow(deprecated)] - let mut buf: stat = unsafe { std::mem::uninitialized() }; - let result = unsafe { lstat(c_string.as_ptr(), &mut buf as *mut stat) }; - - if result < 0 { - crash!(1, "Cannot stat '{}': {}", paths[0], Error::last_os_error()); - } - - buf.st_mode & S_IFMT - }; - - let result = if st_mode != S_IFREG && st_mode != S_IFLNK { - Err(Error::new( - ErrorKind::Other, - "Not a regular file or symlink", - )) - } else { - let result = unsafe { unlink(c_string.as_ptr()) }; - - if result < 0 { - Err(Error::last_os_error()) - } else { - Ok(()) - } - }; - - match result { - Ok(_) => (), - Err(e) => { - crash!(1, "cannot unlink '{0}': {1}", paths[0], e); - } - } - - 0 + remove_file(path).map_err_context(|| format!("cannot unlink {}", path.quote())) } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) - .arg(Arg::with_name(OPT_PATH).hidden(true).multiple(true)) + .arg(Arg::with_name(OPT_PATH).required(true).hidden(true)) } diff --git a/src/uu/uptime/src/uptime.rs b/src/uu/uptime/src/uptime.rs index 35270093c..f649b96b6 100644 --- a/src/uu/uptime/src/uptime.rs +++ b/src/uu/uptime/src/uptime.rs @@ -17,6 +17,8 @@ extern crate uucore; pub use uucore::libc; use uucore::libc::time_t; +use uucore::error::{UResult, USimpleError}; + static ABOUT: &str = "Display the current time, the length of time the system has been up,\n\ the number of users on the system, and the average number of jobs\n\ in the run queue over the last 1, 5 and 15 minutes."; @@ -32,25 +34,24 @@ extern "C" { fn GetTickCount() -> uucore::libc::uint32_t; } -fn get_usage() -> String { - format!("{0} [OPTION]...", executable!()) +fn usage() -> String { + format!("{0} [OPTION]...", uucore::execution_phrase()) } -pub fn uumain(args: impl uucore::Args) -> i32 { - let usage = get_usage(); +#[uucore_procs::gen_uumain] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let usage = usage(); let matches = uu_app().usage(&usage[..]).get_matches_from(args); let (boot_time, user_count) = process_utmpx(); let uptime = get_uptime(boot_time); if uptime < 0 { - show_error!("could not retrieve system uptime"); - - 1 + Err(USimpleError::new(1, "could not retrieve system uptime")) } else { if matches.is_present(options::SINCE) { let initial_date = Local.timestamp(Utc::now().timestamp() - uptime, 0); println!("{}", initial_date.format("%Y-%m-%d %H:%M:%S")); - return 0; + return Ok(()); } print_time(); @@ -59,12 +60,12 @@ pub fn uumain(args: impl uucore::Args) -> i32 { print_nusers(user_count); print_loadavg(); - 0 + Ok(()) } } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .arg( diff --git a/src/uu/users/src/users.rs b/src/uu/users/src/users.rs index ef878497c..d374df181 100644 --- a/src/uu/users/src/users.rs +++ b/src/uu/users/src/users.rs @@ -8,8 +8,7 @@ // spell-checker:ignore (paths) wtmp -#[macro_use] -extern crate uucore; +use std::path::Path; use clap::{crate_version, App, Arg}; use uucore::utmpx::{self, Utmpx}; @@ -18,8 +17,8 @@ static ABOUT: &str = "Print the user names of users currently logged in to the c static ARG_FILES: &str = "files"; -fn get_usage() -> String { - format!("{0} [FILE]", executable!()) +fn usage() -> String { + format!("{0} [FILE]", uucore::execution_phrase()) } fn get_long_usage() -> String { @@ -31,7 +30,7 @@ If FILE is not specified, use {}. /var/log/wtmp as FILE is common.", } pub fn uumain(args: impl uucore::Args) -> i32 { - let usage = get_usage(); + let usage = usage(); let after_help = get_long_usage(); let matches = uu_app() @@ -39,19 +38,18 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .after_help(&after_help[..]) .get_matches_from(args); - let files: Vec = matches - .values_of(ARG_FILES) - .map(|v| v.map(ToString::to_string).collect()) + let files: Vec<&Path> = matches + .values_of_os(ARG_FILES) + .map(|v| v.map(AsRef::as_ref).collect()) .unwrap_or_default(); let filename = if !files.is_empty() { - files[0].as_ref() + files[0] } else { - utmpx::DEFAULT_FILE + utmpx::DEFAULT_FILE.as_ref() }; - let mut users = Utmpx::iter_all_records() - .read_from(filename) + let mut users = Utmpx::iter_all_records_from(filename) .filter(Utmpx::is_user_process) .map(|ut| ut.user()) .collect::>(); @@ -65,7 +63,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .arg(Arg::with_name(ARG_FILES).takes_value(true).max_values(1)) diff --git a/src/uu/wc/BENCHMARKING.md b/src/uu/wc/BENCHMARKING.md new file mode 100644 index 000000000..b9d8cc22d --- /dev/null +++ b/src/uu/wc/BENCHMARKING.md @@ -0,0 +1,124 @@ +# Benchmarking wc + + + +Much of what makes wc fast is avoiding unnecessary work. It has multiple strategies, depending on which data is requested. + +## Strategies + +### Counting bytes + +In the case of `wc -c` the content of the input doesn't have to be inspected at all, only the size has to be known. That enables a few optimizations. + +#### File size + +If it can, wc reads the file size directly. This is not interesting to benchmark, except to see if it still works. Try `wc -c largefile`. + +#### `splice()` + +On Linux `splice()` is used to get the input's length while discarding it directly. + +The best way I've found to generate a fast input to test `splice()` is to pipe the output of uutils `cat` into it. Note that GNU `cat` is slower and therefore less suitable, and that if a file is given as its input directly (as in `wc -c < largefile`) the first strategy kicks in. Try `uucat somefile | wc -c`. + +### Counting lines + +In the case of `wc -l` or `wc -cl` the input doesn't have to be decoded. It's read in chunks and the `bytecount` crate is used to count the newlines. + +It's useful to vary the line length in the input. GNU wc seems particularly bad at short lines. + +### Processing unicode + +This is the most general strategy, and it's necessary for counting words, characters, and line lengths. Individual steps are still switched on and off depending on what must be reported. + +Try varying which of the `-w`, `-m`, `-l` and `-L` flags are used. (The `-c` flag is unlikely to make a difference.) + +Passing no flags is equivalent to passing `-wcl`. That case should perhaps be given special attention as it's the default. + +## Generating files + +To generate a file with many very short lines, run `yes | head -c50000000 > 25Mshortlines`. + +To get a file with less artificial contents, download a book from Project Gutenberg and concatenate it a lot of times: + +``` +wget https://www.gutenberg.org/files/2701/2701-0.txt -O moby.txt +cat moby.txt moby.txt moby.txt moby.txt > moby4.txt +cat moby4.txt moby4.txt moby4.txt moby4.txt > moby16.txt +cat moby16.txt moby16.txt moby16.txt moby16.txt > moby64.txt +``` + +And get one with lots of unicode too: + +``` +wget https://www.gutenberg.org/files/30613/30613-0.txt -O odyssey.txt +cat odyssey.txt odyssey.txt odyssey.txt odyssey.txt > odyssey4.txt +cat odyssey4.txt odyssey4.txt odyssey4.txt odyssey4.txt > odyssey16.txt +cat odyssey16.txt odyssey16.txt odyssey16.txt odyssey16.txt > odyssey64.txt +cat odyssey64.txt odyssey64.txt odyssey64.txt odyssey64.txt > odyssey256.txt +``` + +Finally, it's interesting to try a binary file. Look for one with `du -sh /usr/bin/* | sort -h`. On my system `/usr/bin/docker` is a good candidate as it's fairly large. + +## Running benchmarks + +Use [`hyperfine`](https://github.com/sharkdp/hyperfine) to compare the performance. For example, `hyperfine 'wc somefile' 'uuwc somefile'`. + +If you want to get fancy and exhaustive, generate a table: + +| | moby64.txt | odyssey256.txt | 25Mshortlines | /usr/bin/docker | +|------------------------|--------------|------------------|-----------------|-------------------| +| `wc ` | 1.3965 | 1.6182 | 5.2967 | 2.2294 | +| `wc -c ` | 0.8134 | 1.2774 | 0.7732 | 0.9106 | +| `uucat | wc -c` | 2.7760 | 2.5565 | 2.3769 | 2.3982 | +| `wc -l ` | 1.1441 | 1.2854 | 2.9681 | 1.1493 | +| `wc -L ` | 2.1087 | 1.2551 | 5.4577 | 2.1490 | +| `wc -m ` | 2.7272 | 2.1704 | 7.3371 | 3.4347 | +| `wc -w ` | 1.9007 | 1.5206 | 4.7851 | 2.8529 | +| `wc -lwcmL ` | 1.1687 | 0.9169 | 4.4092 | 2.0663 | + +Beware that: +- Results are fuzzy and change from run to run +- You'll often want to check versions of uutils wc against each other instead of against GNU +- This takes a lot of time to generate +- This only shows the relative speedup, not the absolute time, which may be misleading if the time is very short + +Created by the following Python script: +```python +import json +import subprocess + +from tabulate import tabulate + +bins = ["wc", "uuwc"] +files = ["moby64.txt", "odyssey256.txt", "25Mshortlines", "/usr/bin/docker"] +cmds = [ + "{cmd} {file}", + "{cmd} -c {file}", + "uucat {file} | {cmd} -c", + "{cmd} -l {file}", + "{cmd} -L {file}", + "{cmd} -m {file}", + "{cmd} -w {file}", + "{cmd} -lwcmL {file}", +] + +table = [] +for cmd in cmds: + row = ["`" + cmd.format(cmd="wc", file="") + "`"] + for file in files: + subprocess.run( + [ + "hyperfine", + cmd.format(cmd=bins[0], file=file), + cmd.format(cmd=bins[1], file=file), + "--export-json=out.json", + ], + check=True, + ) + with open("out.json") as f: + res = json.load(f)["results"] + row.append(round(res[0]["mean"] / res[1]["mean"], 4)) + table.append(row) +print(tabulate(table, [""] + files, tablefmt="github")) +``` +(You may have to adjust the `bins` and `files` variables depending on your setup, and please do add other interesting cases to `cmds`.) diff --git a/src/uu/wc/Cargo.toml b/src/uu/wc/Cargo.toml index 31a7ac7af..179b17c36 100644 --- a/src/uu/wc/Cargo.toml +++ b/src/uu/wc/Cargo.toml @@ -16,9 +16,11 @@ path = "src/wc.rs" [dependencies] clap = { version = "2.33", features = ["wrap_help"] } -uucore = { version=">=0.0.9", package="uucore", path="../../uucore" } +uucore = { version=">=0.0.9", package="uucore", path="../../uucore", features=["pipes"] } uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_procs" } -thiserror = "1.0" +bytecount = "0.6.2" +utf-8 = "0.7.6" +unicode-width = "0.1.8" [target.'cfg(unix)'.dependencies] nix = "0.20" @@ -27,3 +29,7 @@ libc = "0.2" [[bin]] name = "wc" path = "src/main.rs" + +[package.metadata.cargo-udeps.ignore] +# Necessary for "make all" +normal = ["uucore_procs"] diff --git a/src/uu/wc/src/count_bytes.rs b/src/uu/wc/src/count_fast.rs similarity index 50% rename from src/uu/wc/src/count_bytes.rs rename to src/uu/wc/src/count_fast.rs index 7f06f8171..9351b6871 100644 --- a/src/uu/wc/src/count_bytes.rs +++ b/src/uu/wc/src/count_fast.rs @@ -1,60 +1,61 @@ -use super::{WcResult, WordCountable}; +use crate::word_count::WordCount; + +use super::WordCountable; #[cfg(any(target_os = "linux", target_os = "android"))] use std::fs::OpenOptions; -use std::io::ErrorKind; +use std::io::{self, ErrorKind, Read}; #[cfg(unix)] use libc::S_IFREG; #[cfg(unix)] -use nix::sys::stat::fstat; +use nix::sys::stat; #[cfg(any(target_os = "linux", target_os = "android"))] -use std::os::unix::io::{AsRawFd, RawFd}; +use std::os::unix::io::AsRawFd; #[cfg(any(target_os = "linux", target_os = "android"))] use libc::S_IFIFO; #[cfg(any(target_os = "linux", target_os = "android"))] -use nix::fcntl::{splice, SpliceFFlags}; -#[cfg(any(target_os = "linux", target_os = "android"))] -use nix::unistd::pipe; +use uucore::pipes::{pipe, splice, splice_exact}; -const BUF_SIZE: usize = 16384; - -/// Splice wrapper which handles short writes +const BUF_SIZE: usize = 16 * 1024; #[cfg(any(target_os = "linux", target_os = "android"))] -#[inline] -fn splice_exact(read_fd: RawFd, write_fd: RawFd, num_bytes: usize) -> nix::Result<()> { - let mut left = num_bytes; - loop { - let written = splice(read_fd, None, write_fd, None, left, SpliceFFlags::empty())?; - left -= written; - if left == 0 { - break; - } - } - Ok(()) -} +const SPLICE_SIZE: usize = 128 * 1024; /// This is a Linux-specific function to count the number of bytes using the /// `splice` system call, which is faster than using `read`. +/// +/// On error it returns the number of bytes it did manage to read, since the +/// caller will fall back to a simpler method. #[inline] #[cfg(any(target_os = "linux", target_os = "android"))] -fn count_bytes_using_splice(fd: RawFd) -> nix::Result { +fn count_bytes_using_splice(fd: &impl AsRawFd) -> Result { let null_file = OpenOptions::new() .write(true) .open("/dev/null") - .map_err(|_| nix::Error::last())?; - let null = null_file.as_raw_fd(); - let (pipe_rd, pipe_wr) = pipe()?; + .map_err(|_| 0_usize)?; + let null_rdev = stat::fstat(null_file.as_raw_fd()) + .map_err(|_| 0_usize)? + .st_rdev; + if (stat::major(null_rdev), stat::minor(null_rdev)) != (1, 3) { + // This is not a proper /dev/null, writing to it is probably bad + // Bit of an edge case, but it has been known to happen + return Err(0); + } + let (pipe_rd, pipe_wr) = pipe().map_err(|_| 0_usize)?; let mut byte_count = 0; loop { - let res = splice(fd, None, pipe_wr, None, BUF_SIZE, SpliceFFlags::empty())?; - if res == 0 { - break; - } - byte_count += res; - splice_exact(pipe_rd, null, res)?; + match splice(fd, &pipe_wr, SPLICE_SIZE) { + Ok(0) => break, + Ok(res) => { + byte_count += res; + if splice_exact(&pipe_rd, &null_file, res).is_err() { + return Err(byte_count); + } + } + Err(_) => return Err(byte_count), + }; } Ok(byte_count) @@ -68,23 +69,26 @@ fn count_bytes_using_splice(fd: RawFd) -> nix::Result { /// 3. Otherwise, we just read normally, but without the overhead of counting /// other things such as lines and words. #[inline] -pub(crate) fn count_bytes_fast(handle: &mut T) -> WcResult { +pub(crate) fn count_bytes_fast(handle: &mut T) -> (usize, Option) { + let mut byte_count = 0; + #[cfg(unix)] { let fd = handle.as_raw_fd(); - if let Ok(stat) = fstat(fd) { + if let Ok(stat) = stat::fstat(fd) { // If the file is regular, then the `st_size` should hold // the file's size in bytes. if (stat.st_mode & S_IFREG) != 0 { - return Ok(stat.st_size as usize); + return (stat.st_size as usize, None); } #[cfg(any(target_os = "linux", target_os = "android"))] { // Else, if we're on Linux and our file is a FIFO pipe // (or stdin), we use splice to count the number of bytes. if (stat.st_mode & S_IFIFO) != 0 { - if let Ok(n) = count_bytes_using_splice(fd) { - return Ok(n); + match count_bytes_using_splice(handle) { + Ok(n) => return (n, None), + Err(n) => byte_count = n, } } } @@ -93,15 +97,32 @@ pub(crate) fn count_bytes_fast(handle: &mut T) -> WcResult return Ok(byte_count), + Ok(0) => return (byte_count, None), Ok(n) => { byte_count += n; } Err(ref e) if e.kind() == ErrorKind::Interrupted => continue, - Err(e) => return Err(e.into()), + Err(e) => return (byte_count, Some(e)), + } + } +} + +pub(crate) fn count_bytes_and_lines_fast( + handle: &mut R, +) -> (WordCount, Option) { + let mut total = WordCount::default(); + let mut buf = [0; BUF_SIZE]; + loop { + match handle.read(&mut buf) { + Ok(0) => return (total, None), + Ok(n) => { + total.bytes += n; + total.lines += bytecount::count(&buf[..n], b'\n'); + } + Err(ref e) if e.kind() == ErrorKind::Interrupted => continue, + Err(e) => return (total, Some(e)), } } } diff --git a/src/uu/wc/src/countable.rs b/src/uu/wc/src/countable.rs index 3da910a03..a14623559 100644 --- a/src/uu/wc/src/countable.rs +++ b/src/uu/wc/src/countable.rs @@ -4,7 +4,7 @@ //! for some common file-like objects. Use the [`WordCountable::lines`] //! method to get an iterator over lines of a file-like object. use std::fs::File; -use std::io::{self, BufRead, BufReader, Read, StdinLock}; +use std::io::{BufRead, BufReader, Read, StdinLock}; #[cfg(unix)] use std::os::unix::io::AsRawFd; @@ -12,61 +12,26 @@ use std::os::unix::io::AsRawFd; #[cfg(unix)] pub trait WordCountable: AsRawFd + Read { type Buffered: BufRead; - fn lines(self) -> Lines; + fn buffered(self) -> Self::Buffered; } #[cfg(not(unix))] pub trait WordCountable: Read { type Buffered: BufRead; - fn lines(self) -> Lines; + fn buffered(self) -> Self::Buffered; } impl WordCountable for StdinLock<'_> { type Buffered = Self; - fn lines(self) -> Lines - where - Self: Sized, - { - Lines { buf: self } + fn buffered(self) -> Self::Buffered { + self } } impl WordCountable for File { type Buffered = BufReader; - fn lines(self) -> Lines - where - Self: Sized, - { - Lines { - buf: BufReader::new(self), - } - } -} - -/// An iterator over the lines of an instance of `BufRead`. -/// -/// Similar to [`io::Lines`] but yields each line as a `Vec` and -/// includes the newline character (`\n`, the `0xA` byte) that -/// terminates the line. -/// -/// [`io::Lines`]:: io::Lines -pub struct Lines { - buf: B, -} - -impl Iterator for Lines { - type Item = io::Result>; - - fn next(&mut self) -> Option { - let mut line = Vec::new(); - - // reading from a TTY seems to raise a condition on, rather than return Some(0) like a file. - // hence the option wrapped in a result here - match self.buf.read_until(b'\n', &mut line) { - Ok(0) => None, - Ok(_n) => Some(Ok(line)), - Err(e) => Some(Err(e)), - } + fn buffered(self) -> Self::Buffered { + BufReader::new(self) } } diff --git a/src/uu/wc/src/wc.rs b/src/uu/wc/src/wc.rs index 0bcc66664..16522a0a7 100644 --- a/src/uu/wc/src/wc.rs +++ b/src/uu/wc/src/wc.rs @@ -8,33 +8,27 @@ #[macro_use] extern crate uucore; -mod count_bytes; +mod count_fast; mod countable; mod word_count; -use count_bytes::count_bytes_fast; +use count_fast::{count_bytes_and_lines_fast, count_bytes_fast}; use countable::WordCountable; +use unicode_width::UnicodeWidthChar; +use utf8::{BufReadDecoder, BufReadDecoderError}; use word_count::{TitledWordCount, WordCount}; use clap::{crate_version, App, Arg, ArgMatches}; -use thiserror::Error; +use std::cmp::max; use std::fs::{self, File}; -use std::io::{self, ErrorKind, Write}; -use std::path::Path; +use std::io::{self, Write}; +use std::path::{Path, PathBuf}; + +use uucore::display::{Quotable, Quoted}; /// The minimum character width for formatting counts when reading from stdin. const MINIMUM_WIDTH: usize = 7; -#[derive(Error, Debug)] -pub enum WcError { - #[error("{0}")] - Io(#[from] io::Error), - #[error("Expected a file, found directory {0}")] - IsDirectory(String), -} - -type WcResult = Result; - struct Settings { show_bytes: bool, show_chars: bool, @@ -95,11 +89,11 @@ pub mod options { static ARG_FILES: &str = "files"; -fn get_usage() -> String { +fn usage() -> String { format!( "{0} [OPTION]... [FILE]... With no FILE, or when FILE is -, read standard input.", - executable!() + uucore::execution_phrase() ) } @@ -114,7 +108,7 @@ enum StdinKind { /// Supported inputs. enum Input { /// A regular file. - Path(String), + Path(PathBuf), /// Standard input. Stdin(StdinKind), @@ -122,28 +116,35 @@ enum Input { impl Input { /// Converts input to title that appears in stats. - fn to_title(&self) -> Option<&str> { + fn to_title(&self) -> Option<&Path> { match self { Input::Path(path) => Some(path), - Input::Stdin(StdinKind::Explicit) => Some("-"), + Input::Stdin(StdinKind::Explicit) => Some("-".as_ref()), Input::Stdin(StdinKind::Implicit) => None, } } + + fn path_display(&self) -> Quoted<'_> { + match self { + Input::Path(path) => path.maybe_quote(), + Input::Stdin(_) => "standard input".maybe_quote(), + } + } } pub fn uumain(args: impl uucore::Args) -> i32 { - let usage = get_usage(); + let usage = usage(); let matches = uu_app().usage(&usage[..]).get_matches_from(args); let mut inputs: Vec = matches - .values_of(ARG_FILES) + .values_of_os(ARG_FILES) .map(|v| { v.map(|i| { if i == "-" { Input::Stdin(StdinKind::Explicit) } else { - Input::Path(ToString::to_string(i)) + Input::Path(i.into()) } }) .collect() @@ -164,7 +165,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .arg( @@ -203,55 +204,125 @@ pub fn uu_app() -> App<'static, 'static> { fn word_count_from_reader( mut reader: T, settings: &Settings, - path: &str, -) -> WcResult { +) -> (WordCount, Option) { let only_count_bytes = settings.show_bytes && (!(settings.show_chars || settings.show_lines || settings.show_max_line_length || settings.show_words)); if only_count_bytes { - return Ok(WordCount { - bytes: count_bytes_fast(&mut reader)?, - ..WordCount::default() - }); + let (bytes, error) = count_bytes_fast(&mut reader); + return ( + WordCount { + bytes, + ..WordCount::default() + }, + error, + ); } // we do not need to decode the byte stream if we're only counting bytes/newlines let decode_chars = settings.show_chars || settings.show_words || settings.show_max_line_length; - // Sum the WordCount for each line. Show a warning for each line - // that results in an IO error when trying to read it. - let total = reader - .lines() - .filter_map(|res| match res { - Ok(line) => Some(line), - Err(e) => { - show_warning!("Error while reading {}: {}", path, e); - None + if !decode_chars { + return count_bytes_and_lines_fast(&mut reader); + } + + let mut total = WordCount::default(); + let mut reader = BufReadDecoder::new(reader.buffered()); + let mut in_word = false; + let mut current_len = 0; + + while let Some(chunk) = reader.next_strict() { + match chunk { + Ok(text) => { + for ch in text.chars() { + if settings.show_words { + if ch.is_whitespace() { + in_word = false; + } else if ch.is_ascii_control() { + // These count as characters but do not affect the word state + } else if !in_word { + in_word = true; + total.words += 1; + } + } + if settings.show_max_line_length { + match ch { + '\n' => { + total.max_line_length = max(current_len, total.max_line_length); + current_len = 0; + } + // '\x0c' = '\f' + '\r' | '\x0c' => { + total.max_line_length = max(current_len, total.max_line_length); + current_len = 0; + } + '\t' => { + current_len -= current_len % 8; + current_len += 8; + } + _ => { + current_len += ch.width().unwrap_or(0); + } + } + } + if settings.show_lines && ch == '\n' { + total.lines += 1; + } + if settings.show_chars { + total.chars += 1; + } + } + total.bytes += text.len(); } - }) - .map(|line| WordCount::from_line(&line, decode_chars)) - .sum(); - Ok(total) + Err(BufReadDecoderError::InvalidByteSequence(bytes)) => { + // GNU wc treats invalid data as neither word nor char nor whitespace, + // so no other counters are affected + total.bytes += bytes.len(); + } + Err(BufReadDecoderError::Io(e)) => { + return (total, Some(e)); + } + } + } + + total.max_line_length = max(current_len, total.max_line_length); + + (total, None) } -fn word_count_from_input(input: &Input, settings: &Settings) -> WcResult { +enum CountResult { + /// Nothing went wrong. + Success(WordCount), + /// Managed to open but failed to read. + Interrupted(WordCount, io::Error), + /// Didn't even manage to open. + Failure(io::Error), +} + +/// If we fail opening a file we only show the error. If we fail reading it +/// we show a count for what we managed to read. +/// +/// Therefore the reading implementations always return a total and sometimes +/// return an error: (WordCount, Option). +fn word_count_from_input(input: &Input, settings: &Settings) -> CountResult { match input { Input::Stdin(_) => { let stdin = io::stdin(); let stdin_lock = stdin.lock(); - word_count_from_reader(stdin_lock, settings, "-") - } - Input::Path(path) => { - let path_obj = Path::new(path); - if path_obj.is_dir() { - Err(WcError::IsDirectory(path.to_owned())) - } else { - let file = File::open(path)?; - word_count_from_reader(file, settings, path) + match word_count_from_reader(stdin_lock, settings) { + (total, Some(error)) => CountResult::Interrupted(total, error), + (total, None) => CountResult::Success(total), } } + Input::Path(path) => match File::open(path) { + Err(error) => CountResult::Failure(error), + Ok(file) => match word_count_from_reader(file, settings) { + (total, Some(error)) => CountResult::Interrupted(total, error), + (total, None) => CountResult::Success(total), + }, + }, } } @@ -264,18 +335,8 @@ fn word_count_from_input(input: &Input, settings: &Settings) -> WcResult { - show_error_custom_description!(path, "Is a directory"); - } - (Input::Path(path), WcError::Io(e)) if e.kind() == ErrorKind::NotFound => { - show_error_custom_description!(path, "No such file or directory"); - } - (_, e) => { - show_error!("{}", e); - } - }; +fn show_error(input: &Input, err: io::Error) { + show_error!("{}: {}", input.path_display(), err); } /// Compute the number of digits needed to represent any count for this input. @@ -297,9 +358,9 @@ fn show_error(input: &Input, err: WcError) { /// let input = Input::Stdin(StdinKind::Explicit); /// assert_eq!(7, digit_width(input)); /// ``` -fn digit_width(input: &Input) -> WcResult> { +fn digit_width(input: &Input) -> io::Result { match input { - Input::Stdin(_) => Ok(Some(MINIMUM_WIDTH)), + Input::Stdin(_) => Ok(MINIMUM_WIDTH), Input::Path(filename) => { let path = Path::new(filename); let metadata = fs::metadata(path)?; @@ -310,9 +371,9 @@ fn digit_width(input: &Input) -> WcResult> { // instead). See GitHub issue #2201. let num_bytes = metadata.len(); let num_digits = num_bytes.to_string().len(); - Ok(Some(num_digits)) + Ok(num_digits) } else { - Ok(None) + Ok(MINIMUM_WIDTH) } } } @@ -350,13 +411,8 @@ fn digit_width(input: &Input) -> WcResult> { fn max_width(inputs: &[Input]) -> usize { let mut result = 1; for input in inputs { - match digit_width(input) { - Ok(maybe_n) => { - if let Some(n) = maybe_n { - result = result.max(n); - } - } - Err(_) => continue, + if let Ok(n) = digit_width(input) { + result = result.max(n); } } result @@ -368,60 +424,68 @@ fn wc(inputs: Vec, settings: &Settings) -> Result<(), u32> { // The width is the number of digits needed to print the number of // bytes in the largest file. This is true regardless of whether // the `settings` indicate that the bytes will be displayed. - let mut error_count = 0; - let max_width = max_width(&inputs); + // + // If we only need to display a single number, set this to 0 to + // prevent leading spaces. + let mut failure = false; + let max_width = if settings.number_enabled() <= 1 { + 0 + } else { + max_width(&inputs) + }; let mut total_word_count = WordCount::default(); let num_inputs = inputs.len(); for input in &inputs { - let word_count = word_count_from_input(input, settings).unwrap_or_else(|err| { - show_error(input, err); - error_count += 1; - WordCount::default() - }); + let word_count = match word_count_from_input(input, settings) { + CountResult::Success(word_count) => word_count, + CountResult::Interrupted(word_count, error) => { + show_error(input, error); + failure = true; + word_count + } + CountResult::Failure(error) => { + show_error(input, error); + failure = true; + continue; + } + }; total_word_count += word_count; let result = word_count.with_title(input.to_title()); if let Err(err) = print_stats(settings, &result, max_width) { show_warning!( "failed to print result for {}: {}", - result.title.unwrap_or(""), + result + .title + .unwrap_or_else(|| "".as_ref()) + .maybe_quote(), err ); - error_count += 1; + failure = true; } } if num_inputs > 1 { - let total_result = total_word_count.with_title(Some("total")); + let total_result = total_word_count.with_title(Some("total".as_ref())); if let Err(err) = print_stats(settings, &total_result, max_width) { show_warning!("failed to print total: {}", err); - error_count += 1; + failure = true; } } - if error_count == 0 { - Ok(()) + if failure { + Err(1) } else { - Err(error_count) + Ok(()) } } -fn print_stats( - settings: &Settings, - result: &TitledWordCount, - mut min_width: usize, -) -> WcResult<()> { +fn print_stats(settings: &Settings, result: &TitledWordCount, min_width: usize) -> io::Result<()> { let stdout = io::stdout(); let mut stdout_lock = stdout.lock(); - if settings.number_enabled() <= 1 { - // Prevent a leading space in case we only need to display a single - // number. - min_width = 0; - } - let mut is_first: bool = true; if settings.show_lines { @@ -438,13 +502,6 @@ fn print_stats( write!(stdout_lock, "{:1$}", result.count.words, min_width)?; is_first = false; } - if settings.show_bytes { - if !is_first { - write!(stdout_lock, " ")?; - } - write!(stdout_lock, "{:1$}", result.count.bytes, min_width)?; - is_first = false; - } if settings.show_chars { if !is_first { write!(stdout_lock, " ")?; @@ -452,6 +509,13 @@ fn print_stats( write!(stdout_lock, "{:1$}", result.count.chars, min_width)?; is_first = false; } + if settings.show_bytes { + if !is_first { + write!(stdout_lock, " ")?; + } + write!(stdout_lock, "{:1$}", result.count.bytes, min_width)?; + is_first = false; + } if settings.show_max_line_length { if !is_first { write!(stdout_lock, " ")?; @@ -464,7 +528,7 @@ fn print_stats( } if let Some(title) = result.title { - writeln!(stdout_lock, " {}", title)?; + writeln!(stdout_lock, " {}", title.maybe_quote())?; } else { writeln!(stdout_lock)?; } diff --git a/src/uu/wc/src/word_count.rs b/src/uu/wc/src/word_count.rs index bdb510f58..617b811fc 100644 --- a/src/uu/wc/src/word_count.rs +++ b/src/uu/wc/src/word_count.rs @@ -1,19 +1,6 @@ use std::cmp::max; -use std::iter::Sum; use std::ops::{Add, AddAssign}; -use std::str::from_utf8; - -const CR: u8 = b'\r'; -const LF: u8 = b'\n'; -const SPACE: u8 = b' '; -const TAB: u8 = b'\t'; -const SYN: u8 = 0x16_u8; -const FF: u8 = 0x0C_u8; - -#[inline(always)] -fn is_word_separator(byte: u8) -> bool { - byte == SPACE || byte == TAB || byte == CR || byte == SYN || byte == FF -} +use std::path::Path; #[derive(Debug, Default, Copy, Clone)] pub struct WordCount { @@ -44,80 +31,10 @@ impl AddAssign for WordCount { } } -impl Sum for WordCount { - fn sum(iter: I) -> WordCount - where - I: Iterator, - { - iter.fold(WordCount::default(), |acc, x| acc + x) - } -} - impl WordCount { - /// Count the characters and whitespace-separated words in the given bytes. - /// - /// `line` is a slice of bytes that will be decoded as ASCII characters. - fn ascii_word_and_char_count(line: &[u8]) -> (usize, usize) { - let word_count = line.split(|&x| is_word_separator(x)).count(); - let char_count = line.iter().filter(|c| c.is_ascii()).count(); - (word_count, char_count) - } - - /// Create a [`WordCount`] from a sequence of bytes representing a line. - /// - /// If the last byte of `line` encodes a newline character (`\n`), - /// then the [`lines`] field will be set to 1. Otherwise, it will - /// be set to 0. The [`bytes`] field is simply the length of - /// `line`. - /// - /// If `decode_chars` is `false`, the [`chars`] and [`words`] - /// fields will be set to 0. If it is `true`, this function will - /// attempt to decode the bytes first as UTF-8, and failing that, - /// as ASCII. - pub fn from_line(line: &[u8], decode_chars: bool) -> WordCount { - // GNU 'wc' only counts lines that end in LF as lines - let lines = (*line.last().unwrap() == LF) as usize; - let bytes = line.len(); - let (words, chars) = if decode_chars { - WordCount::word_and_char_count(line) - } else { - (0, 0) - }; - // -L is a GNU 'wc' extension so same behavior on LF - let max_line_length = if chars > 0 { chars - lines } else { 0 }; - WordCount { - bytes, - chars, - lines, - words, - max_line_length, - } - } - - /// Count the UTF-8 characters and words in the given string slice. - /// - /// `s` is a string slice that is assumed to be a UTF-8 string. - fn utf8_word_and_char_count(s: &str) -> (usize, usize) { - let word_count = s.split_whitespace().count(); - let char_count = s.chars().count(); - (word_count, char_count) - } - - pub fn with_title(self, title: Option<&str>) -> TitledWordCount { + pub fn with_title(self, title: Option<&Path>) -> TitledWordCount { TitledWordCount { title, count: self } } - - /// Count the characters and words in the given slice of bytes. - /// - /// `line` is a slice of bytes that will be decoded as UTF-8 - /// characters, or if that fails, as ASCII characters. - fn word_and_char_count(line: &[u8]) -> (usize, usize) { - // try and convert the bytes to UTF-8 first - match from_utf8(line) { - Ok(s) => WordCount::utf8_word_and_char_count(s), - Err(..) => WordCount::ascii_word_and_char_count(line), - } - } } /// This struct supplements the actual word count with an optional title that is @@ -126,6 +43,6 @@ impl WordCount { /// it would result in unnecessary copying of `String`. #[derive(Debug, Default, Clone)] pub struct TitledWordCount<'a> { - pub title: Option<&'a str>, + pub title: Option<&'a Path>, pub count: WordCount, } diff --git a/src/uu/who/src/who.rs b/src/uu/who/src/who.rs index 6a9c88710..a975c82ba 100644 --- a/src/uu/who/src/who.rs +++ b/src/uu/who/src/who.rs @@ -44,8 +44,11 @@ static RUNLEVEL_HELP: &str = "print current runlevel"; #[cfg(not(target_os = "linux"))] static RUNLEVEL_HELP: &str = "print current runlevel (This is meaningless on non Linux)"; -fn get_usage() -> String { - format!("{0} [OPTION]... [ FILE | ARG1 ARG2 ]", executable!()) +fn usage() -> String { + format!( + "{0} [OPTION]... [ FILE | ARG1 ARG2 ]", + uucore::execution_phrase() + ) } fn get_long_usage() -> String { @@ -61,7 +64,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .collect_str(InvalidEncodingHandling::Ignore) .accept_any(); - let usage = get_usage(); + let usage = usage(); let after_help = get_long_usage(); let matches = uu_app() @@ -160,7 +163,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } pub fn uu_app() -> App<'static, 'static> { - App::new(executable!()) + App::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .arg( @@ -338,15 +341,14 @@ impl Who { utmpx::DEFAULT_FILE }; if self.short_list { - let users = Utmpx::iter_all_records() - .read_from(f) + let users = Utmpx::iter_all_records_from(f) .filter(Utmpx::is_user_process) .map(|ut| ut.user()) .collect::>(); println!("{}", users.join(" ")); println!("# users={}", users.len()); } else { - let records = Utmpx::iter_all_records().read_from(f).peekable(); + let records = Utmpx::iter_all_records_from(f).peekable(); if self.include_heading { self.print_heading() @@ -489,7 +491,7 @@ impl Who { }; let s = if self.do_lookup { - safe_unwrap!(ut.canon_host()) + crash_if_err!(1, ut.canon_host()) } else { ut.host() }; diff --git a/src/uu/whoami/Cargo.toml b/src/uu/whoami/Cargo.toml index 919aab2e5..d12ea1aea 100644 --- a/src/uu/whoami/Cargo.toml +++ b/src/uu/whoami/Cargo.toml @@ -16,12 +16,15 @@ path = "src/whoami.rs" [dependencies] clap = { version = "2.33", features = ["wrap_help"] } -uucore = { version=">=0.0.9", package="uucore", path="../../uucore", features=["entries", "wide"] } +uucore = { version=">=0.0.9", package="uucore", path="../../uucore", features=["entries"] } uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_procs" } [target.'cfg(target_os = "windows")'.dependencies] winapi = { version = "0.3", features = ["lmcons"] } +[target.'cfg(unix)'.dependencies] +libc = "0.2.42" + [[bin]] name = "whoami" path = "src/main.rs" diff --git a/src/uu/whoami/src/platform/unix.rs b/src/uu/whoami/src/platform/unix.rs index 3d5fc6f54..1c0ea15d5 100644 --- a/src/uu/whoami/src/platform/unix.rs +++ b/src/uu/whoami/src/platform/unix.rs @@ -8,14 +8,14 @@ * file that was distributed with this source code. */ -// spell-checker:ignore (ToDO) getusername +use std::ffi::OsString; +use std::io; -use std::io::Result; use uucore::entries::uid2usr; -use uucore::libc::geteuid; -pub unsafe fn get_username() -> Result { - // Get effective user id - let uid = geteuid(); - uid2usr(uid) +pub fn get_username() -> io::Result { + // SAFETY: getuid() does nothing with memory and is always successful. + let uid = unsafe { libc::geteuid() }; + // uid2usr should arguably return an OsString but currently doesn't + uid2usr(uid).map(Into::into) } diff --git a/src/uu/whoami/src/platform/windows.rs b/src/uu/whoami/src/platform/windows.rs index 3fe8eb1e7..a627bed8e 100644 --- a/src/uu/whoami/src/platform/windows.rs +++ b/src/uu/whoami/src/platform/windows.rs @@ -7,22 +7,21 @@ * file that was distributed with this source code. */ -extern crate winapi; +use std::ffi::OsString; +use std::io; +use std::os::windows::ffi::OsStringExt; -use self::winapi::shared::lmcons; -use self::winapi::shared::minwindef; -use self::winapi::um::{winbase, winnt}; -use std::io::{Error, Result}; -use std::mem; -use uucore::wide::FromWide; +use winapi::shared::lmcons; +use winapi::shared::minwindef::DWORD; +use winapi::um::winbase; -pub unsafe fn get_username() -> Result { - #[allow(deprecated)] - let mut buffer: [winnt::WCHAR; lmcons::UNLEN as usize + 1] = mem::uninitialized(); - let mut len = buffer.len() as minwindef::DWORD; - if winbase::GetUserNameW(buffer.as_mut_ptr(), &mut len) == 0 { - return Err(Error::last_os_error()); +pub fn get_username() -> io::Result { + const BUF_LEN: DWORD = lmcons::UNLEN + 1; + let mut buffer = [0_u16; BUF_LEN as usize]; + let mut len = BUF_LEN; + // SAFETY: buffer.len() == len + if unsafe { winbase::GetUserNameW(buffer.as_mut_ptr(), &mut len) } == 0 { + return Err(io::Error::last_os_error()); } - let username = String::from_wide(&buffer[..len as usize - 1]); - Ok(username) + Ok(OsString::from_wide(&buffer[..len as usize - 1])) } diff --git a/src/uu/whoami/src/whoami.rs b/src/uu/whoami/src/whoami.rs index bd2eea1e3..830b86e63 100644 --- a/src/uu/whoami/src/whoami.rs +++ b/src/uu/whoami/src/whoami.rs @@ -1,5 +1,3 @@ -use clap::App; - // * This file is part of the uutils coreutils package. // * // * (c) Jordi Boggiano @@ -14,40 +12,25 @@ extern crate clap; #[macro_use] extern crate uucore; +use clap::App; + +use uucore::display::println_verbatim; +use uucore::error::{FromIo, UResult}; + mod platform; -pub fn uumain(args: impl uucore::Args) -> i32 { - let app = uu_app(); +static ABOUT: &str = "Print the current username."; - if let Err(err) = app.get_matches_from_safe(args) { - if err.kind == clap::ErrorKind::HelpDisplayed - || err.kind == clap::ErrorKind::VersionDisplayed - { - println!("{}", err); - 0 - } else { - show_error!("{}", err); - 1 - } - } else { - exec(); - - 0 - } +#[uucore_procs::gen_uumain] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + uu_app().get_matches_from(args); + let username = platform::get_username().map_err_context(|| "failed to get username".into())?; + println_verbatim(&username).map_err_context(|| "failed to print username".into())?; + Ok(()) } pub fn uu_app() -> App<'static, 'static> { - app_from_crate!() -} - -pub fn exec() { - unsafe { - match platform::get_username() { - Ok(username) => println!("{}", username), - Err(err) => match err.raw_os_error() { - Some(0) | None => crash!(1, "failed to get username"), - Some(_) => crash!(1, "failed to get username: {}", err), - }, - } - } + App::new(uucore::util_name()) + .version(crate_version!()) + .about(ABOUT) } diff --git a/src/uu/yes/Cargo.toml b/src/uu/yes/Cargo.toml index ff8465557..ea2aae3b1 100644 --- a/src/uu/yes/Cargo.toml +++ b/src/uu/yes/Cargo.toml @@ -16,13 +16,11 @@ path = "src/yes.rs" [dependencies] clap = { version = "2.33", features = ["wrap_help"] } -uucore = { version=">=0.0.9", package="uucore", path="../../uucore", features=["zero-copy"] } +uucore = { version=">=0.0.9", package="uucore", path="../../uucore", features=["pipes"] } uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_procs" } -[features] -default = [] -# -latency = [] +[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies] +nix = "0.20.0" [[bin]] name = "yes" diff --git a/src/uu/yes/src/splice.rs b/src/uu/yes/src/splice.rs new file mode 100644 index 000000000..6f025d6a9 --- /dev/null +++ b/src/uu/yes/src/splice.rs @@ -0,0 +1,70 @@ +//! On Linux we can use vmsplice() to write data more efficiently. +//! +//! This does not always work. We're not allowed to splice to some targets, +//! and on some systems (notably WSL 1) it isn't supported at all. +//! +//! If we get an error code that suggests splicing isn't supported then we +//! tell that to the caller so it can fall back to a robust naïve method. If +//! we get another kind of error we bubble it up as normal. +//! +//! vmsplice() can only splice into a pipe, so if the output is not a pipe +//! we make our own and use splice() to bridge the gap from the pipe to the +//! output. +//! +//! We assume that an "unsupported" error will only ever happen before any +//! data was successfully written to the output. That way we don't have to +//! make any effort to rescue data from the pipe if splice() fails, we can +//! just fall back and start over from the beginning. + +use std::{io, os::unix::io::AsRawFd}; + +use nix::{errno::Errno, libc::S_IFIFO, sys::stat::fstat}; + +use uucore::pipes::{pipe, splice_exact, vmsplice}; + +pub(crate) fn splice_data(bytes: &[u8], out: &impl AsRawFd) -> Result<()> { + let is_pipe = fstat(out.as_raw_fd())?.st_mode & S_IFIFO != 0; + + if is_pipe { + loop { + let mut bytes = bytes; + while !bytes.is_empty() { + let len = vmsplice(out, bytes).map_err(maybe_unsupported)?; + bytes = &bytes[len..]; + } + } + } else { + let (read, write) = pipe()?; + loop { + let mut bytes = bytes; + while !bytes.is_empty() { + let len = vmsplice(&write, bytes).map_err(maybe_unsupported)?; + splice_exact(&read, out, len).map_err(maybe_unsupported)?; + bytes = &bytes[len..]; + } + } + } +} + +pub(crate) enum Error { + Unsupported, + Io(io::Error), +} + +type Result = std::result::Result; + +impl From for Error { + fn from(error: nix::Error) -> Self { + match error { + nix::Error::Sys(errno) => Error::Io(io::Error::from_raw_os_error(errno as i32)), + _ => Error::Io(io::Error::last_os_error()), + } + } +} + +fn maybe_unsupported(error: nix::Error) -> Error { + match error.as_errno() { + Some(Errno::EINVAL) | Some(Errno::ENOSYS) | Some(Errno::EBADF) => Error::Unsupported, + _ => error.into(), + } +} diff --git a/src/uu/yes/src/yes.rs b/src/uu/yes/src/yes.rs index 2c0d43000..03ae4e415 100644 --- a/src/uu/yes/src/yes.rs +++ b/src/uu/yes/src/yes.rs @@ -7,37 +7,27 @@ /* last synced with: yes (GNU coreutils) 8.13 */ +use std::borrow::Cow; +use std::io::{self, Write}; + #[macro_use] extern crate clap; #[macro_use] extern crate uucore; use clap::{App, Arg}; -use std::borrow::Cow; -use std::io::{self, Write}; -use uucore::zero_copy::ZeroCopyWriter; +use uucore::error::{UResult, USimpleError}; + +#[cfg(any(target_os = "linux", target_os = "android"))] +mod splice; // it's possible that using a smaller or larger buffer might provide better performance on some // systems, but honestly this is good enough const BUF_SIZE: usize = 16 * 1024; -pub fn uumain(args: impl uucore::Args) -> i32 { - let app = uu_app(); - - let matches = match app.get_matches_from_safe(args) { - Ok(m) => m, - Err(ref e) - if e.kind == clap::ErrorKind::HelpDisplayed - || e.kind == clap::ErrorKind::VersionDisplayed => - { - println!("{}", e); - return 0; - } - Err(f) => { - show_error!("{}", f); - return 1; - } - }; +#[uucore_procs::gen_uumain] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let matches = uu_app().get_matches_from(args); let string = if let Some(values) = matches.values_of("STRING") { let mut result = values.fold(String::new(), |res, s| res + s + " "); @@ -51,16 +41,17 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let mut buffer = [0; BUF_SIZE]; let bytes = prepare_buffer(&string, &mut buffer); - exec(bytes); - - 0 + match exec(bytes) { + Ok(()) => Ok(()), + Err(err) if err.kind() == io::ErrorKind::BrokenPipe => Ok(()), + Err(err) => Err(USimpleError::new(1, format!("standard output: {}", err))), + } } 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 { let mut size = 0; @@ -75,16 +66,20 @@ fn prepare_buffer<'a>(input: &'a str, buffer: &'a mut [u8; BUF_SIZE]) -> &'a [u8 } } -#[cfg(feature = "latency")] -fn prepare_buffer<'a>(input: &'a str, _buffer: &'a mut [u8; BUF_SIZE]) -> &'a [u8] { - input.as_bytes() -} +pub fn exec(bytes: &[u8]) -> io::Result<()> { + let stdout = io::stdout(); + let mut stdout = stdout.lock(); + + #[cfg(any(target_os = "linux", target_os = "android"))] + { + match splice::splice_data(bytes, &stdout) { + Ok(_) => return Ok(()), + Err(splice::Error::Io(err)) => return Err(err), + Err(splice::Error::Unsupported) => (), + } + } -pub fn exec(bytes: &[u8]) { - let mut stdout_raw = io::stdout(); - let mut writer = ZeroCopyWriter::with_default(&mut stdout_raw, |stdout| stdout.lock()); loop { - // TODO: needs to check if pipe fails - writer.write_all(bytes).unwrap(); + stdout.write_all(bytes)?; } } diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index 60399b9be..3f2276bd3 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -16,21 +16,24 @@ edition = "2018" path="src/lib/lib.rs" [dependencies] +clap = "2.33.3" dns-lookup = { version="1.0.5", optional=true } dunce = "1.0.0" getopts = "<= 0.2.21" -wild = "2.0.4" +wild = "2.0" # * optional thiserror = { version="1.0", optional=true } -lazy_static = { version="1.3", optional=true } -nix = { version="<= 0.13", optional=true } -platform-info = { version="<= 0.1", optional=true } time = { version="<= 0.1.43", optional=true } # * "problem" dependencies (pinned) data-encoding = { version="2.1", optional=true } data-encoding-macro = { version="0.1.12", optional=true } z85 = { version="3.0.3", optional=true } -libc = { version="0.2.15, <= 0.2.85", optional=true } ## libc: initial utmp support added in v0.2.15; but v0.2.68 breaks the build for MinSRV v1.31.0 +libc = { version="0.2.15", optional=true } +once_cell = "1.8.0" + +[target.'cfg(unix)'.dependencies] +walkdir = { version="2.3.2", optional=true } +nix = { version="0.20", optional=true } [dev-dependencies] clap = "2.33.3" @@ -50,11 +53,11 @@ entries = ["libc"] fs = ["libc"] fsext = ["libc", "time"] mode = ["libc"] -perms = ["libc"] +perms = ["libc", "walkdir"] process = ["libc"] ringbuffer = [] signals = [] utf8 = [] utmpx = ["time", "libc", "dns-lookup"] wide = [] -zero-copy = ["nix", "libc", "lazy_static", "platform-info"] +pipes = ["nix"] diff --git a/src/uucore/src/lib/features.rs b/src/uucore/src/lib/features.rs index f90fc7b3d..42d8ffbe7 100644 --- a/src/uucore/src/lib/features.rs +++ b/src/uucore/src/lib/features.rs @@ -8,8 +8,6 @@ pub mod fs; pub mod fsext; #[cfg(feature = "ringbuffer")] pub mod ringbuffer; -#[cfg(feature = "zero-copy")] -pub mod zero_copy; // * (platform-specific) feature-gated modules // ** non-windows @@ -21,6 +19,8 @@ pub mod mode; pub mod entries; #[cfg(all(unix, feature = "perms"))] pub mod perms; +#[cfg(all(unix, feature = "pipes"))] +pub mod pipes; #[cfg(all(unix, feature = "process"))] pub mod process; diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index 9514b6842..0b8079a5c 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -6,6 +6,8 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +//! Set of functions to manage files and symlinks + #[cfg(unix)] use libc::{ mode_t, S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFMT, S_IFREG, S_IFSOCK, S_IRGRP, @@ -15,6 +17,7 @@ use libc::{ use std::borrow::Cow; use std::env; use std::fs; +use std::io::Error as IOError; use std::io::Result as IOResult; use std::io::{Error, ErrorKind}; #[cfg(any(unix, target_os = "redox"))] @@ -54,11 +57,8 @@ pub fn resolve_relative_path(path: &Path) -> Cow { /// Controls how symbolic links should be handled when canonicalizing a path. #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum CanonicalizeMode { - /// Do not resolve any symbolic links. - None, - - /// Resolve all symbolic links. +pub enum MissingHandling { + /// Return an error if any part of the path is missing. Normal, /// Resolve symbolic links, ignoring errors on the final component. @@ -68,6 +68,19 @@ pub enum CanonicalizeMode { Missing, } +/// Controls when symbolic links are resolved +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ResolveMode { + /// Do not resolve any symbolic links. + None, + + /// Resolve symlinks as encountered when processing the path + Physical, + + /// Resolve '..' elements before symlinks + Logical, +} + // copied from https://github.com/rust-lang/cargo/blob/2e4cfc2b7d43328b207879228a2ca7d427d188bb/src/cargo/util/paths.rs#L65-L90 // both projects are MIT https://github.com/rust-lang/cargo/blob/master/LICENSE-MIT // for std impl progress see rfc https://github.com/rust-lang/rfcs/issues/2208 @@ -99,26 +112,42 @@ pub fn normalize_path(path: &Path) -> PathBuf { ret } -fn resolve>(original: P) -> IOResult { +fn resolve>(original: P) -> Result { const MAX_LINKS_FOLLOWED: u32 = 255; let mut followed = 0; let mut result = original.as_ref().to_path_buf(); + + let mut first_resolution = None; loop { if followed == MAX_LINKS_FOLLOWED { - return Err(Error::new( - ErrorKind::InvalidInput, - "maximum links followed", + return Err(( + // When we hit MAX_LINKS_FOLLOWED we should return the first resolution (that's what GNU does - for whatever reason) + first_resolution.unwrap(), + Error::new(ErrorKind::InvalidInput, "maximum links followed"), )); } - if !fs::symlink_metadata(&result)?.file_type().is_symlink() { - break; + match fs::symlink_metadata(&result) { + Ok(meta) => { + if !meta.file_type().is_symlink() { + break; + } + } + Err(e) => return Err((result, e)), } followed += 1; - let path = fs::read_link(&result)?; - result.pop(); - result.push(path); + match fs::read_link(&result) { + Ok(path) => { + result.pop(); + result.push(path); + } + Err(e) => return Err((result, e)), + } + + if first_resolution.is_none() { + first_resolution = Some(result.clone()); + } } Ok(result) } @@ -128,20 +157,32 @@ fn resolve>(original: P) -> IOResult { /// This function is a generalization of [`std::fs::canonicalize`] that /// allows controlling how symbolic links are resolved and how to deal /// with missing components. It returns the canonical, absolute form of -/// a path. The `can_mode` parameter controls how symbolic links are -/// resolved: +/// a path. +/// The `miss_mode` parameter controls how missing path elements are handled /// -/// * [`CanonicalizeMode::Normal`] makes this function behave like +/// * [`MissingHandling::Normal`] makes this function behave like /// [`std::fs::canonicalize`], resolving symbolic links and returning /// an error if the path does not exist. -/// * [`CanonicalizeMode::Missing`] makes this function ignore non-final +/// * [`MissingHandling::Missing`] makes this function ignore non-final /// components of the path that could not be resolved. -/// * [`CanonicalizeMode::Existing`] makes this function return an error +/// * [`MissingHandling::Existing`] makes this function return an error /// if the final component of the path does not exist. -/// * [`CanonicalizeMode::None`] makes this function not try to resolve -/// any symbolic links. /// -pub fn canonicalize>(original: P, can_mode: CanonicalizeMode) -> IOResult { +/// The `res_mode` parameter controls how symbolic links are +/// resolved: +/// +/// * [`ResolveMode::None`] makes this function not try to resolve +/// any symbolic links. +/// * [`ResolveMode::Physical`] makes this function resolve symlinks as they +/// are encountered +/// * [`ResolveMode::Logical`] makes this function resolve '..' components +/// before symlinks +/// +pub fn canonicalize>( + original: P, + miss_mode: MissingHandling, + res_mode: ResolveMode, +) -> IOResult { // Create an absolute path let original = original.as_ref(); let original = if original.is_absolute() { @@ -165,7 +206,11 @@ pub fn canonicalize>(original: P, can_mode: CanonicalizeMode) -> } Component::CurDir => (), Component::ParentDir => { - parts.pop(); + if res_mode == ResolveMode::Logical { + parts.pop(); + } else { + parts.push(part.as_os_str()); + } } Component::Normal(_) => { parts.push(part.as_os_str()); @@ -178,35 +223,40 @@ pub fn canonicalize>(original: P, can_mode: CanonicalizeMode) -> for part in parts[..parts.len() - 1].iter() { result.push(part); - if can_mode == CanonicalizeMode::None { + //resolve as we go to handle long relative paths on windows + if res_mode == ResolveMode::Physical { + result = normalize_path(&result); + } + + if res_mode == ResolveMode::None { continue; } match resolve(&result) { - Err(_) if can_mode == CanonicalizeMode::Missing => continue, - Err(e) => return Err(e), + Err((path, _)) if miss_mode == MissingHandling::Missing => result = path, + Err((_, e)) => return Err(e), Ok(path) => { - result.pop(); - result.push(path); + result = path; } } } result.push(parts.last().unwrap()); - if can_mode == CanonicalizeMode::None { + if res_mode == ResolveMode::None { return Ok(result); } match resolve(&result) { - Err(e) if can_mode == CanonicalizeMode::Existing => { + Err((_, e)) if miss_mode == MissingHandling::Existing => { return Err(e); } - Ok(path) => { - result.pop(); - result.push(path); + Ok(path) | Err((path, _)) => { + result = path; } - Err(_) => (), + } + if res_mode == ResolveMode::Physical { + result = normalize_path(&result); } } Ok(result) diff --git a/src/uucore/src/lib/features/fsext.rs b/src/uucore/src/lib/features/fsext.rs index 56e078c56..7525d9e41 100644 --- a/src/uucore/src/lib/features/fsext.rs +++ b/src/uucore/src/lib/features/fsext.rs @@ -7,6 +7,8 @@ // For the full copyright and license information, please view the LICENSE file // that was distributed with this source code. +//! Set of functions to manage file systems + // spell-checker:ignore (arch) bitrig ; (fs) cifs smbfs extern crate time; @@ -86,13 +88,12 @@ use std::time::UNIX_EPOCH; target_os = "linux", target_vendor = "apple", target_os = "android", - target_os = "freebsd" + target_os = "freebsd", + target_os = "openbsd" ))] pub use libc::statfs as StatFs; #[cfg(any( - target_os = "openbsd", target_os = "netbsd", - target_os = "openbsd", target_os = "bitrig", target_os = "dragonfly", target_os = "redox" @@ -103,13 +104,12 @@ pub use libc::statvfs as StatFs; target_os = "linux", target_vendor = "apple", target_os = "android", - target_os = "freebsd" + target_os = "freebsd", + target_os = "openbsd", ))] pub use libc::statfs as statfs_fn; #[cfg(any( - target_os = "openbsd", target_os = "netbsd", - target_os = "openbsd", target_os = "bitrig", target_os = "dragonfly", target_os = "redox" @@ -309,9 +309,19 @@ impl MountInfo { } } -#[cfg(any(target_vendor = "apple", target_os = "freebsd"))] +#[cfg(any( + target_vendor = "apple", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] use std::ffi::CStr; -#[cfg(any(target_os = "freebsd", target_vendor = "apple"))] +#[cfg(any( + target_os = "freebsd", + target_vendor = "apple", + target_os = "netbsd", + target_os = "openbsd" +))] impl From for MountInfo { fn from(statfs: StatFs) -> Self { let mut info = MountInfo { @@ -344,9 +354,19 @@ impl From for MountInfo { } } -#[cfg(any(target_os = "freebsd", target_vendor = "apple"))] +#[cfg(any( + target_os = "freebsd", + target_vendor = "apple", + target_os = "netbsd", + target_os = "openbsd" +))] use libc::c_int; -#[cfg(any(target_os = "freebsd", target_vendor = "apple"))] +#[cfg(any( + target_os = "freebsd", + target_vendor = "apple", + target_os = "netbsd", + target_os = "openbsd" +))] extern "C" { #[cfg(all(target_vendor = "apple", target_arch = "x86_64"))] #[link_name = "getmntinfo$INODE64"] // spell-checker:disable-line @@ -354,6 +374,8 @@ extern "C" { #[cfg(any( all(target_os = "freebsd"), + all(target_os = "netbsd"), + all(target_os = "openbsd"), all(target_vendor = "apple", target_arch = "aarch64") ))] #[link_name = "getmntinfo"] // spell-checker:disable-line @@ -364,9 +386,20 @@ extern "C" { use std::fs::File; #[cfg(target_os = "linux")] use std::io::{BufRead, BufReader}; -#[cfg(any(target_vendor = "apple", target_os = "freebsd", target_os = "windows"))] +#[cfg(any( + target_vendor = "apple", + target_os = "freebsd", + target_os = "windows", + target_os = "netbsd", + target_os = "openbsd" +))] use std::ptr; -#[cfg(any(target_vendor = "apple", target_os = "freebsd"))] +#[cfg(any( + target_vendor = "apple", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] use std::slice; /// Read file system list. pub fn read_fs_list() -> Vec { @@ -386,7 +419,12 @@ pub fn read_fs_list() -> Vec { }) .collect::>() } - #[cfg(any(target_os = "freebsd", target_vendor = "apple"))] + #[cfg(any( + target_os = "freebsd", + target_vendor = "apple", + target_os = "netbsd", + target_os = "openbsd" + ))] { let mut mount_buffer_ptr: *mut StatFs = ptr::null_mut(); let len = unsafe { get_mount_info(&mut mount_buffer_ptr, 1_i32) }; @@ -582,12 +620,17 @@ impl FsMeta for StatFs { fn io_size(&self) -> u64 { self.f_frsize as u64 } - #[cfg(any(target_vendor = "apple", target_os = "freebsd"))] + #[cfg(any(target_vendor = "apple", target_os = "freebsd", target_os = "netbsd"))] fn io_size(&self) -> u64 { self.f_iosize as u64 } // XXX: dunno if this is right - #[cfg(not(any(target_vendor = "apple", target_os = "freebsd", target_os = "linux")))] + #[cfg(not(any( + target_vendor = "apple", + target_os = "freebsd", + target_os = "linux", + target_os = "netbsd" + )))] fn io_size(&self) -> u64 { self.f_bsize as u64 } @@ -598,13 +641,23 @@ impl FsMeta for StatFs { // // Solaris, Irix and POSIX have a system call statvfs(2) that returns a // struct statvfs, containing an unsigned long f_fsid - #[cfg(any(target_vendor = "apple", target_os = "freebsd", target_os = "linux"))] + #[cfg(any( + target_vendor = "apple", + target_os = "freebsd", + target_os = "linux", + target_os = "openbsd" + ))] fn fsid(&self) -> u64 { let f_fsid: &[u32; 2] = unsafe { &*(&self.f_fsid as *const libc::fsid_t as *const [u32; 2]) }; (u64::from(f_fsid[0])) << 32 | u64::from(f_fsid[1]) } - #[cfg(not(any(target_vendor = "apple", target_os = "freebsd", target_os = "linux")))] + #[cfg(not(any( + target_vendor = "apple", + target_os = "freebsd", + target_os = "linux", + target_os = "openbsd" + )))] fn fsid(&self) -> u64 { self.f_fsid as u64 } @@ -617,12 +670,18 @@ impl FsMeta for StatFs { fn namelen(&self) -> u64 { 1024 } - #[cfg(target_os = "freebsd")] + #[cfg(any(target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))] fn namelen(&self) -> u64 { self.f_namemax as u64 // spell-checker:disable-line } // XXX: should everything just use statvfs? - #[cfg(not(any(target_vendor = "apple", target_os = "freebsd", target_os = "linux")))] + #[cfg(not(any( + target_vendor = "apple", + target_os = "freebsd", + target_os = "linux", + target_os = "netbsd", + target_os = "openbsd" + )))] fn namelen(&self) -> u64 { self.f_namemax as u64 // spell-checker:disable-line } diff --git a/src/uucore/src/lib/features/mode.rs b/src/uucore/src/lib/features/mode.rs index 8e9f063ff..02e78ba9b 100644 --- a/src/uucore/src/lib/features/mode.rs +++ b/src/uucore/src/lib/features/mode.rs @@ -5,61 +5,68 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +//! Set of functions to parse modes + // spell-checker:ignore (vars) fperm srwx -use libc::{mode_t, S_IRGRP, S_IROTH, S_IRUSR, S_IWGRP, S_IWOTH, S_IWUSR}; +use libc::{mode_t, umask, S_IRGRP, S_IROTH, S_IRUSR, S_IWGRP, S_IWOTH, S_IWUSR}; -pub fn parse_numeric(fperm: u32, mut mode: &str) -> Result { - let (op, pos) = parse_op(mode, Some('='))?; - mode = mode[pos..].trim().trim_start_matches('0'); - if mode.len() > 4 { - Err(format!("mode is too large ({} > 7777)", mode)) +pub fn parse_numeric(fperm: u32, mut mode: &str, considering_dir: bool) -> Result { + let (op, pos) = parse_op(mode).map_or_else(|_| (None, 0), |(op, pos)| (Some(op), pos)); + mode = mode[pos..].trim(); + let change = if mode.is_empty() { + 0 } else { - match u32::from_str_radix(mode, 8) { - Ok(change) => Ok(match op { - '+' => fperm | change, - '-' => fperm & !change, - '=' => change, - _ => unreachable!(), - }), - Err(err) => Err(err.to_string()), - } + u32::from_str_radix(mode, 8).map_err(|e| e.to_string())? + }; + if change > 0o7777 { + Err(format!("mode is too large ({} > 7777", change)) + } else { + Ok(match op { + Some('+') => fperm | change, + Some('-') => fperm & !change, + // If this is a directory, we keep the setgid and setuid bits, + // unless the mode contains 5 or more octal digits or the mode is "=" + None if considering_dir && mode.len() < 5 => change | (fperm & (0o4000 | 0o2000)), + None | Some('=') => change, + Some(_) => unreachable!(), + }) } } pub fn parse_symbolic( mut fperm: u32, mut mode: &str, + umask: u32, considering_dir: bool, ) -> Result { - #[cfg(unix)] - use libc::umask; - let (mask, pos) = parse_levels(mode); if pos == mode.len() { return Err(format!("invalid mode ({})", mode)); } let respect_umask = pos == 0; - let last_umask = unsafe { umask(0) }; mode = &mode[pos..]; while !mode.is_empty() { - let (op, pos) = parse_op(mode, None)?; + let (op, pos) = parse_op(mode)?; mode = &mode[pos..]; let (mut srwx, pos) = parse_change(mode, fperm, considering_dir); if respect_umask { - srwx &= !(last_umask as u32); + srwx &= !(umask as u32); } mode = &mode[pos..]; match op { '+' => fperm |= srwx & mask, '-' => fperm &= !(srwx & mask), - '=' => fperm = (fperm & !mask) | (srwx & mask), + '=' => { + if considering_dir { + // keep the setgid and setuid bits for directories + srwx |= fperm & (0o4000 | 0o2000); + } + fperm = (fperm & !mask) | (srwx & mask) + } _ => unreachable!(), } } - unsafe { - umask(last_umask); - } Ok(fperm) } @@ -68,9 +75,9 @@ fn parse_levels(mode: &str) -> (u32, usize) { let mut pos = 0; for ch in mode.chars() { mask |= match ch { - 'u' => 0o7700, - 'g' => 0o7070, - 'o' => 0o7007, + 'u' => 0o4700, + 'g' => 0o2070, + 'o' => 0o1007, 'a' => 0o7777, _ => break, }; @@ -82,24 +89,22 @@ fn parse_levels(mode: &str) -> (u32, usize) { (mask, pos) } -fn parse_op(mode: &str, default: Option) -> Result<(char, usize), String> { +fn parse_op(mode: &str) -> Result<(char, usize), String> { let ch = mode .chars() .next() .ok_or_else(|| "unexpected end of mode".to_owned())?; - Ok(match ch { - '+' | '-' | '=' => (ch, 1), - _ => { - let ch = default.ok_or_else(|| { - format!("invalid operator (expected +, -, or =, but found {})", ch) - })?; - (ch, 0) - } - }) + match ch { + '+' | '-' | '=' => Ok((ch, 1)), + _ => Err(format!( + "invalid operator (expected +, -, or =, but found {})", + ch + )), + } } fn parse_change(mode: &str, fperm: u32, considering_dir: bool) -> (u32, usize) { - let mut srwx = fperm & 0o7000; + let mut srwx = 0; let mut pos = 0; for ch in mode.chars() { match ch { @@ -130,13 +135,19 @@ pub fn parse_mode(mode: &str) -> Result { let fperm = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH; let arr: &[char] = &['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; let result = if mode.contains(arr) { - parse_numeric(fperm as u32, mode) + parse_numeric(fperm as u32, mode, true) } else { - parse_symbolic(fperm as u32, mode, true) + parse_symbolic(fperm as u32, mode, get_umask(), true) }; result.map(|mode| mode as mode_t) } +pub fn get_umask() -> u32 { + let mask = unsafe { umask(0) }; + unsafe { umask(mask) }; + mask as u32 +} + #[cfg(test)] mod test { diff --git a/src/uucore/src/lib/features/perms.rs b/src/uucore/src/lib/features/perms.rs index 69491c297..16ee01b88 100644 --- a/src/uucore/src/lib/features/perms.rs +++ b/src/uucore/src/lib/features/perms.rs @@ -3,8 +3,20 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +//! Common functions to manage permissions + +use crate::display::Quotable; +use crate::error::strip_errno; +use crate::error::UResult; +use crate::error::USimpleError; pub use crate::features::entries; -use libc::{self, gid_t, lchown, uid_t}; +use crate::fs::resolve_relative_path; +use crate::show_error; +use clap::App; +use clap::Arg; +use clap::ArgMatches; +use libc::{self, gid_t, uid_t}; +use walkdir::WalkDir; use std::io::Error as IOError; use std::io::Result as IOResult; @@ -18,86 +30,16 @@ use std::path::Path; /// The various level of verbosity #[derive(PartialEq, Clone, Debug)] -pub enum Verbosity { +pub enum VerbosityLevel { Silent, Changes, Verbose, Normal, } - -/// Actually perform the change of group on a path -fn chgrp>(path: P, gid: gid_t, follow: bool) -> IOResult<()> { - let path = path.as_ref(); - let s = CString::new(path.as_os_str().as_bytes()).unwrap(); - let ret = unsafe { - if follow { - libc::chown(s.as_ptr(), 0_u32.wrapping_sub(1), gid) - } else { - lchown(s.as_ptr(), 0_u32.wrapping_sub(1), gid) - } - }; - if ret == 0 { - Ok(()) - } else { - Err(IOError::last_os_error()) - } -} - -/// Perform the change of group on a path -/// with the various options -/// and error messages management -pub fn wrap_chgrp>( - path: P, - meta: &Metadata, - dest_gid: Option, - follow: bool, - verbosity: Verbosity, -) -> Result { - use self::Verbosity::*; - let path = path.as_ref(); - let mut out: String = String::new(); - let dest_gid = dest_gid.unwrap_or_else(|| meta.gid()); - - if let Err(e) = chgrp(path, dest_gid, follow) { - match verbosity { - Silent => (), - _ => { - out = format!("changing group of '{}': {}", path.display(), e); - if verbosity == Verbose { - out = format!( - "{}\nfailed to change group of '{}' from {} to {}", - out, - path.display(), - entries::gid2grp(meta.gid()).unwrap(), - entries::gid2grp(dest_gid).unwrap() - ); - }; - } - } - return Err(out); - } else { - let changed = dest_gid != meta.gid(); - if changed { - match verbosity { - Changes | Verbose => { - out = format!( - "changed group of '{}' from {} to {}", - path.display(), - entries::gid2grp(meta.gid()).unwrap(), - entries::gid2grp(dest_gid).unwrap() - ); - } - _ => (), - }; - } else if verbosity == Verbose { - out = format!( - "group of '{}' retained as {}", - path.display(), - entries::gid2grp(dest_gid).unwrap_or_default() - ); - } - } - Ok(out) +#[derive(PartialEq, Clone, Debug)] +pub struct Verbosity { + pub groups_only: bool, + pub level: VerbosityLevel, } /// Actually perform the change of owner on a path @@ -108,7 +50,7 @@ fn chown>(path: P, uid: uid_t, gid: gid_t, follow: bool) -> IORes if follow { libc::chown(s.as_ptr(), uid, gid) } else { - lchown(s.as_ptr(), uid, gid) + libc::lchown(s.as_ptr(), uid, gid) } }; if ret == 0 { @@ -129,27 +71,45 @@ pub fn wrap_chown>( follow: bool, verbosity: Verbosity, ) -> Result { - use self::Verbosity::*; let dest_uid = dest_uid.unwrap_or_else(|| meta.uid()); let dest_gid = dest_gid.unwrap_or_else(|| meta.gid()); let path = path.as_ref(); let mut out: String = String::new(); if let Err(e) = chown(path, dest_uid, dest_gid, follow) { - match verbosity { - Silent => (), - _ => { - out = format!("changing ownership of '{}': {}", path.display(), e); - if verbosity == Verbose { - out = format!( - "{}\nfailed to change ownership of '{}' from {}:{} to {}:{}", - out, - path.display(), - entries::uid2usr(meta.uid()).unwrap(), - entries::gid2grp(meta.gid()).unwrap(), - entries::uid2usr(dest_uid).unwrap(), - entries::gid2grp(dest_gid).unwrap() - ); + match verbosity.level { + VerbosityLevel::Silent => (), + level => { + out = format!( + "changing {} of {}: {}", + if verbosity.groups_only { + "group" + } else { + "ownership" + }, + path.quote(), + e + ); + if level == VerbosityLevel::Verbose { + out = if verbosity.groups_only { + format!( + "{}\nfailed to change group of {} from {} to {}", + out, + path.quote(), + entries::gid2grp(meta.gid()).unwrap(), + entries::gid2grp(dest_gid).unwrap() + ) + } else { + format!( + "{}\nfailed to change ownership of {} from {}:{} to {}:{}", + out, + path.quote(), + entries::uid2usr(meta.uid()).unwrap(), + entries::gid2grp(meta.gid()).unwrap(), + entries::uid2usr(dest_uid).unwrap(), + entries::gid2grp(dest_gid).unwrap() + ) + }; }; } } @@ -157,27 +117,412 @@ pub fn wrap_chown>( } else { let changed = dest_uid != meta.uid() || dest_gid != meta.gid(); if changed { - match verbosity { - Changes | Verbose => { - out = format!( - "changed ownership of '{}' from {}:{} to {}:{}", - path.display(), - entries::uid2usr(meta.uid()).unwrap(), - entries::gid2grp(meta.gid()).unwrap(), - entries::uid2usr(dest_uid).unwrap(), - entries::gid2grp(dest_gid).unwrap() - ); + match verbosity.level { + VerbosityLevel::Changes | VerbosityLevel::Verbose => { + out = if verbosity.groups_only { + format!( + "changed group of {} from {} to {}", + path.quote(), + entries::gid2grp(meta.gid()).unwrap(), + entries::gid2grp(dest_gid).unwrap() + ) + } else { + format!( + "changed ownership of {} from {}:{} to {}:{}", + path.quote(), + entries::uid2usr(meta.uid()).unwrap(), + entries::gid2grp(meta.gid()).unwrap(), + entries::uid2usr(dest_uid).unwrap(), + entries::gid2grp(dest_gid).unwrap() + ) + }; } _ => (), }; - } else if verbosity == Verbose { - out = format!( - "ownership of '{}' retained as {}:{}", - path.display(), - entries::uid2usr(dest_uid).unwrap(), - entries::gid2grp(dest_gid).unwrap() - ); + } else if verbosity.level == VerbosityLevel::Verbose { + out = if verbosity.groups_only { + format!( + "group of {} retained as {}", + path.quote(), + entries::gid2grp(dest_gid).unwrap_or_default() + ) + } else { + format!( + "ownership of {} retained as {}:{}", + path.quote(), + entries::uid2usr(dest_uid).unwrap(), + entries::gid2grp(dest_gid).unwrap() + ) + }; } } Ok(out) } + +pub enum IfFrom { + All, + User(u32), + Group(u32), + UserGroup(u32, u32), +} + +#[derive(PartialEq, Eq)] +pub enum TraverseSymlinks { + None, + First, + All, +} + +pub struct ChownExecutor { + pub dest_uid: Option, + pub dest_gid: Option, + pub traverse_symlinks: TraverseSymlinks, + pub verbosity: Verbosity, + pub filter: IfFrom, + pub files: Vec, + pub recursive: bool, + pub preserve_root: bool, + pub dereference: bool, +} + +impl ChownExecutor { + pub fn exec(&self) -> UResult<()> { + let mut ret = 0; + for f in &self.files { + ret |= self.traverse(f); + } + if ret != 0 { + return Err(ret.into()); + } + Ok(()) + } + + fn traverse>(&self, root: P) -> i32 { + let path = root.as_ref(); + let meta = match self.obtain_meta(path, self.dereference) { + Some(m) => m, + _ => return 1, + }; + + // Prohibit only if: + // (--preserve-root and -R present) && + // ( + // (argument is not symlink && resolved to be '/') || + // (argument is symlink && should follow argument && resolved to be '/') + // ) + if self.recursive && self.preserve_root { + let may_exist = if self.dereference { + path.canonicalize().ok() + } else { + let real = resolve_relative_path(path); + if real.is_dir() { + Some(real.canonicalize().expect("failed to get real path")) + } else { + Some(real.into_owned()) + } + }; + + if let Some(p) = may_exist { + if p.parent().is_none() { + show_error!("it is dangerous to operate recursively on '/'"); + show_error!("use --no-preserve-root to override this failsafe"); + return 1; + } + } + } + + let ret = if self.matched(meta.uid(), meta.gid()) { + match wrap_chown( + path, + &meta, + self.dest_uid, + self.dest_gid, + self.dereference, + self.verbosity.clone(), + ) { + Ok(n) => { + if !n.is_empty() { + show_error!("{}", n); + } + 0 + } + Err(e) => { + if self.verbosity.level != VerbosityLevel::Silent { + show_error!("{}", e); + } + 1 + } + } + } else { + 0 + }; + + if !self.recursive { + ret + } else { + ret | self.dive_into(&root) + } + } + + fn dive_into>(&self, root: P) -> i32 { + let root = root.as_ref(); + + // walkdir always dereferences the root directory, so we have to check it ourselves + // TODO: replace with `root.is_symlink()` once it is stable + if self.traverse_symlinks == TraverseSymlinks::None + && std::fs::symlink_metadata(root) + .map(|m| m.file_type().is_symlink()) + .unwrap_or(false) + { + return 0; + } + + let mut ret = 0; + let mut iterator = WalkDir::new(root) + .follow_links(self.traverse_symlinks == TraverseSymlinks::All) + .min_depth(1) + .into_iter(); + // We can't use a for loop because we need to manipulate the iterator inside the loop. + while let Some(entry) = iterator.next() { + let entry = match entry { + Err(e) => { + ret = 1; + if let Some(path) = e.path() { + show_error!( + "cannot access '{}': {}", + path.display(), + if let Some(error) = e.io_error() { + strip_errno(error) + } else { + "Too many levels of symbolic links".into() + } + ) + } else { + show_error!("{}", e) + } + continue; + } + Ok(entry) => entry, + }; + let path = entry.path(); + let meta = match self.obtain_meta(path, self.dereference) { + Some(m) => m, + _ => { + ret = 1; + if entry.file_type().is_dir() { + // Instruct walkdir to skip this directory to avoid getting another error + // when walkdir tries to query the children of this directory. + iterator.skip_current_dir(); + } + continue; + } + }; + + if !self.matched(meta.uid(), meta.gid()) { + continue; + } + + ret = match wrap_chown( + path, + &meta, + self.dest_uid, + self.dest_gid, + self.dereference, + self.verbosity.clone(), + ) { + Ok(n) => { + if !n.is_empty() { + show_error!("{}", n); + } + 0 + } + Err(e) => { + if self.verbosity.level != VerbosityLevel::Silent { + show_error!("{}", e); + } + 1 + } + } + } + ret + } + + fn obtain_meta>(&self, path: P, follow: bool) -> Option { + let path = path.as_ref(); + let meta = if follow { + path.metadata() + } else { + path.symlink_metadata() + }; + match meta { + Err(e) => { + match self.verbosity.level { + VerbosityLevel::Silent => (), + _ => show_error!( + "cannot {} {}: {}", + if follow { "dereference" } else { "access" }, + path.quote(), + strip_errno(&e) + ), + } + None + } + Ok(meta) => Some(meta), + } + } + + #[inline] + fn matched(&self, uid: uid_t, gid: gid_t) -> bool { + match self.filter { + IfFrom::All => true, + IfFrom::User(u) => u == uid, + IfFrom::Group(g) => g == gid, + IfFrom::UserGroup(u, g) => u == uid && g == gid, + } + } +} + +pub mod options { + pub mod verbosity { + pub const CHANGES: &str = "changes"; + pub const QUIET: &str = "quiet"; + pub const SILENT: &str = "silent"; + pub const VERBOSE: &str = "verbose"; + } + pub mod preserve_root { + pub const PRESERVE: &str = "preserve-root"; + pub const NO_PRESERVE: &str = "no-preserve-root"; + } + pub mod dereference { + pub const DEREFERENCE: &str = "dereference"; + pub const NO_DEREFERENCE: &str = "no-dereference"; + } + pub const FROM: &str = "from"; + pub const RECURSIVE: &str = "recursive"; + pub mod traverse { + pub const TRAVERSE: &str = "H"; + pub const NO_TRAVERSE: &str = "P"; + pub const EVERY: &str = "L"; + } + pub const REFERENCE: &str = "reference"; + pub const ARG_OWNER: &str = "OWNER"; + pub const ARG_GROUP: &str = "GROUP"; + pub const ARG_FILES: &str = "FILE"; +} + +type GidUidFilterParser<'a> = fn(&ArgMatches<'a>) -> UResult<(Option, Option, IfFrom)>; + +/// Base implementation for `chgrp` and `chown`. +/// +/// An argument called `add_arg_if_not_reference` will be added to `app` if +/// `args` does not contain the `--reference` option. +/// `parse_gid_uid_and_filter` will be called to obtain the target gid and uid, and the filter, +/// from `ArgMatches`. +/// `groups_only` determines whether verbose output will only mention the group. +pub fn chown_base<'a>( + mut app: App<'a, 'a>, + args: impl crate::Args, + add_arg_if_not_reference: &'a str, + parse_gid_uid_and_filter: GidUidFilterParser<'a>, + groups_only: bool, +) -> UResult<()> { + let args: Vec<_> = args.collect(); + let mut reference = false; + let mut help = false; + // stop processing options on -- + for arg in args.iter().take_while(|s| *s != "--") { + if arg.to_string_lossy().starts_with("--reference=") || arg == "--reference" { + reference = true; + } else if arg == "--help" { + // we stop processing once we see --help, + // as it doesn't matter if we've seen reference or not + help = true; + break; + } + } + + if help || !reference { + // add both positional arguments + // arg_group is only required if + app = app.arg( + Arg::with_name(add_arg_if_not_reference) + .value_name(add_arg_if_not_reference) + .required(true) + .takes_value(true) + .multiple(false), + ) + } + app = app.arg( + Arg::with_name(options::ARG_FILES) + .value_name(options::ARG_FILES) + .multiple(true) + .takes_value(true) + .required(true) + .min_values(1), + ); + let matches = app.get_matches_from(args); + + let files: Vec = matches + .values_of(options::ARG_FILES) + .map(|v| v.map(ToString::to_string).collect()) + .unwrap_or_default(); + + let preserve_root = matches.is_present(options::preserve_root::PRESERVE); + + let mut dereference = if matches.is_present(options::dereference::DEREFERENCE) { + Some(true) + } else if matches.is_present(options::dereference::NO_DEREFERENCE) { + Some(false) + } else { + None + }; + + let mut traverse_symlinks = if matches.is_present(options::traverse::TRAVERSE) { + TraverseSymlinks::First + } else if matches.is_present(options::traverse::EVERY) { + TraverseSymlinks::All + } else { + TraverseSymlinks::None + }; + + let recursive = matches.is_present(options::RECURSIVE); + if recursive { + if traverse_symlinks == TraverseSymlinks::None { + if dereference == Some(true) { + return Err(USimpleError::new(1, "-R --dereference requires -H or -L")); + } + dereference = Some(false); + } + } else { + traverse_symlinks = TraverseSymlinks::None; + } + + let verbosity_level = if matches.is_present(options::verbosity::CHANGES) { + VerbosityLevel::Changes + } else if matches.is_present(options::verbosity::SILENT) + || matches.is_present(options::verbosity::QUIET) + { + VerbosityLevel::Silent + } else if matches.is_present(options::verbosity::VERBOSE) { + VerbosityLevel::Verbose + } else { + VerbosityLevel::Normal + }; + let (dest_gid, dest_uid, filter) = parse_gid_uid_and_filter(&matches)?; + + let executor = ChownExecutor { + traverse_symlinks, + dest_gid, + dest_uid, + verbosity: Verbosity { + groups_only, + level: verbosity_level, + }, + recursive, + dereference: dereference.unwrap_or(true), + preserve_root, + files, + filter, + }; + executor.exec() +} diff --git a/src/uucore/src/lib/features/pipes.rs b/src/uucore/src/lib/features/pipes.rs new file mode 100644 index 000000000..b375982dd --- /dev/null +++ b/src/uucore/src/lib/features/pipes.rs @@ -0,0 +1,69 @@ +/// Thin pipe-related wrappers around functions from the `nix` crate. +use std::fs::File; +#[cfg(any(target_os = "linux", target_os = "android"))] +use std::os::unix::io::AsRawFd; +use std::os::unix::io::FromRawFd; + +#[cfg(any(target_os = "linux", target_os = "android"))] +use nix::{fcntl::SpliceFFlags, sys::uio::IoVec}; + +pub use nix::{Error, Result}; + +/// A wrapper around [`nix::unistd::Pipe`] that ensures the pipe is cleaned up. +/// +/// Returns two `File` objects: everything written to the second can be read +/// from the first. +pub fn pipe() -> Result<(File, File)> { + let (read, write) = nix::unistd::pipe()?; + // SAFETY: The file descriptors do not have other owners. + unsafe { Ok((File::from_raw_fd(read), File::from_raw_fd(write))) } +} + +/// Less noisy wrapper around [`nix::fcntl::splice`]. +/// +/// Up to `len` bytes are moved from `source` to `target`. Returns the number +/// of successfully moved bytes. +/// +/// At least one of `source` and `target` must be some sort of pipe. +/// To get around this requirement, consider splicing from your source into +/// a [`pipe`] and then from the pipe into your target (with `splice_exact`): +/// this is still very efficient. +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn splice(source: &impl AsRawFd, target: &impl AsRawFd, len: usize) -> Result { + nix::fcntl::splice( + source.as_raw_fd(), + None, + target.as_raw_fd(), + None, + len, + SpliceFFlags::empty(), + ) +} + +/// Splice wrapper which fully finishes the write. +/// +/// Exactly `len` bytes are moved from `source` into `target`. +/// +/// Panics if `source` runs out of data before `len` bytes have been moved. +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn splice_exact(source: &impl AsRawFd, target: &impl AsRawFd, len: usize) -> Result<()> { + let mut left = len; + while left != 0 { + let written = splice(source, target, left)?; + assert_ne!(written, 0, "unexpected end of data"); + left -= written; + } + Ok(()) +} + +/// Copy data from `bytes` into `target`, which must be a pipe. +/// +/// Returns the number of successfully copied bytes. +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn vmsplice(target: &impl AsRawFd, bytes: &[u8]) -> Result { + nix::fcntl::vmsplice( + target.as_raw_fd(), + &[IoVec::from_slice(bytes)], + SpliceFFlags::empty(), + ) +} diff --git a/src/uucore/src/lib/features/process.rs b/src/uucore/src/lib/features/process.rs index 21bfa992c..fc4b7ed48 100644 --- a/src/uucore/src/lib/features/process.rs +++ b/src/uucore/src/lib/features/process.rs @@ -9,6 +9,8 @@ // spell-checker:ignore (vars) cvar exitstatus // spell-checker:ignore (sys/unix) WIFSIGNALED +//! Set of functions to manage IDs + use libc::{gid_t, pid_t, uid_t}; use std::fmt; use std::io; diff --git a/src/uucore/src/lib/features/ringbuffer.rs b/src/uucore/src/lib/features/ringbuffer.rs index 1cb0d2b0d..9a6176f92 100644 --- a/src/uucore/src/lib/features/ringbuffer.rs +++ b/src/uucore/src/lib/features/ringbuffer.rs @@ -106,7 +106,6 @@ mod tests { use crate::ringbuffer::RingBuffer; use std::collections::VecDeque; - use std::iter::FromIterator; #[test] fn test_size_limit_zero() { @@ -128,7 +127,7 @@ mod tests { fn test_from_iter() { let iter = [0, 1, 2].iter(); let actual = RingBuffer::from_iter(iter, 2).data; - let expected = VecDeque::from_iter([1, 2].iter()); + let expected: VecDeque<&i32> = [1, 2].iter().collect(); assert_eq!(expected, actual); } } diff --git a/src/uucore/src/lib/features/utmpx.rs b/src/uucore/src/lib/features/utmpx.rs index 5077d9e59..a96c3f48c 100644 --- a/src/uucore/src/lib/features/utmpx.rs +++ b/src/uucore/src/lib/features/utmpx.rs @@ -24,7 +24,7 @@ //! //! ``` //! use uucore::utmpx::Utmpx; -//! for ut in Utmpx::iter_all_records().read_from("/some/where/else") { +//! for ut in Utmpx::iter_all_records_from("/some/where/else") { //! if ut.is_user_process() { //! println!("{}: {}", ut.host(), ut.user()) //! } @@ -35,9 +35,12 @@ pub extern crate time; use self::time::{Timespec, Tm}; use std::ffi::CString; -use std::io::Error as IOError; use std::io::Result as IOResult; +use std::marker::PhantomData; +use std::os::unix::ffi::OsStrExt; +use std::path::Path; use std::ptr; +use std::sync::{Mutex, MutexGuard}; pub use self::ut::*; use libc::utmpx; @@ -47,14 +50,16 @@ use libc::utmpx; pub use libc::endutxent; pub use libc::getutxent; pub use libc::setutxent; -#[cfg(any(target_vendor = "apple", target_os = "linux"))] +#[cfg(any(target_vendor = "apple", target_os = "linux", target_os = "netbsd"))] pub use libc::utmpxname; #[cfg(target_os = "freebsd")] pub unsafe extern "C" fn utmpxname(_file: *const libc::c_char) -> libc::c_int { 0 } -pub use crate::*; // import macros from `../../macros.rs` +use once_cell::sync::Lazy; + +use crate::*; // import macros from `../../macros.rs` // In case the c_char array doesn't end with NULL macro_rules! chars2string { @@ -130,6 +135,30 @@ mod ut { pub use libc::USER_PROCESS; } +#[cfg(target_os = "netbsd")] +mod ut { + pub static DEFAULT_FILE: &str = "/var/run/utmpx"; + + pub const ACCOUNTING: usize = 9; + pub const SHUTDOWN_TIME: usize = 11; + + pub use libc::_UTX_HOSTSIZE as UT_HOSTSIZE; + pub use libc::_UTX_IDSIZE as UT_IDSIZE; + pub use libc::_UTX_LINESIZE as UT_LINESIZE; + pub use libc::_UTX_USERSIZE as UT_NAMESIZE; + + pub use libc::ACCOUNTING; + pub use libc::DEAD_PROCESS; + pub use libc::EMPTY; + pub use libc::INIT_PROCESS; + pub use libc::LOGIN_PROCESS; + pub use libc::NEW_TIME; + pub use libc::OLD_TIME; + pub use libc::RUN_LVL; + pub use libc::SIGNATURE; + pub use libc::USER_PROCESS; +} + pub struct Utmpx { inner: utmpx, } @@ -224,30 +253,76 @@ impl Utmpx { Ok(host.to_string()) } + /// Iterate through all the utmp records. + /// + /// This will use the default location, or the path [`Utmpx::iter_all_records_from`] + /// was most recently called with. + /// + /// Only one instance of [`UtmpxIter`] may be active at a time. This + /// function will block as long as one is still active. Beware! pub fn iter_all_records() -> UtmpxIter { - UtmpxIter + let iter = UtmpxIter::new(); + unsafe { + // This can technically fail, and it would be nice to detect that, + // but it doesn't return anything so we'd have to do nasty things + // with errno. + setutxent(); + } + iter + } + + /// Iterate through all the utmp records from a specific file. + /// + /// No failure is reported or detected. + /// + /// This function affects subsequent calls to [`Utmpx::iter_all_records`]. + /// + /// The same caveats as for [`Utmpx::iter_all_records`] apply. + pub fn iter_all_records_from>(path: P) -> UtmpxIter { + let iter = UtmpxIter::new(); + let path = CString::new(path.as_ref().as_os_str().as_bytes()).unwrap(); + unsafe { + // In glibc, utmpxname() only fails if there's not enough memory + // to copy the string. + // Solaris returns 1 on success instead of 0. Supposedly there also + // exist systems where it returns void. + // GNU who on Debian seems to output nothing if an invalid filename + // is specified, no warning or anything. + // So this function is pretty crazy and we don't try to detect errors. + // Not much we can do besides pray. + utmpxname(path.as_ptr()); + setutxent(); + } + iter } } +// On some systems these functions are not thread-safe. On others they're +// thread-local. Therefore we use a mutex to allow only one guard to exist at +// a time, and make sure UtmpxIter cannot be sent across threads. +// +// I believe the only technical memory unsafety that could happen is a data +// race while copying the data out of the pointer returned by getutxent(), but +// ordinary race conditions are also very much possible. +static LOCK: Lazy> = Lazy::new(|| Mutex::new(())); + /// Iterator of login records -pub struct UtmpxIter; +pub struct UtmpxIter { + #[allow(dead_code)] + guard: MutexGuard<'static, ()>, + /// Ensure UtmpxIter is !Send. Technically redundant because MutexGuard + /// is also !Send. + phantom: PhantomData>, +} impl UtmpxIter { - /// Sets the name of the utmpx-format file for the other utmpx functions to access. - /// - /// If not set, default record file will be used(file path depends on the target OS) - pub fn read_from(self, f: &str) -> Self { - let res = unsafe { - let cstring = CString::new(f).unwrap(); - utmpxname(cstring.as_ptr()) - }; - if res != 0 { - show_warning!("utmpxname: {}", IOError::last_os_error()); + fn new() -> Self { + // PoisonErrors can safely be ignored + let guard = LOCK.lock().unwrap_or_else(|err| err.into_inner()); + UtmpxIter { + guard, + phantom: PhantomData, } - unsafe { - setutxent(); - } - self } } @@ -257,13 +332,24 @@ impl Iterator for UtmpxIter { unsafe { let res = getutxent(); if !res.is_null() { + // The data behind this pointer will be replaced by the next + // call to getutxent(), so we have to read it now. + // All the strings live inline in the struct as arrays, which + // makes things easier. Some(Utmpx { inner: ptr::read(res as *const _), }) } else { - endutxent(); None } } } } + +impl Drop for UtmpxIter { + fn drop(&mut self) { + unsafe { + endutxent(); + } + } +} diff --git a/src/uucore/src/lib/features/zero_copy.rs b/src/uucore/src/lib/features/zero_copy.rs deleted file mode 100644 index 1eb2c1547..000000000 --- a/src/uucore/src/lib/features/zero_copy.rs +++ /dev/null @@ -1,148 +0,0 @@ -use self::platform::*; - -use std::io::{self, Write}; - -mod platform; - -pub trait AsRawObject { - fn as_raw_object(&self) -> RawObject; -} - -pub trait FromRawObject: Sized { - /// # Safety - /// ToDO ... - unsafe fn from_raw_object(obj: RawObject) -> Option; -} - -// TODO: also make a SpliceWriter that takes an input fd and and output fd and uses splice() to -// transfer data -// TODO: make a TeeWriter or something that takes an input fd and two output fds and uses tee() to -// transfer to both output fds - -enum InnerZeroCopyWriter { - Platform(PlatformZeroCopyWriter), - Standard(T), -} - -impl Write for InnerZeroCopyWriter { - fn write(&mut self, buf: &[u8]) -> io::Result { - match self { - InnerZeroCopyWriter::Platform(ref mut writer) => writer.write(buf), - InnerZeroCopyWriter::Standard(ref mut writer) => writer.write(buf), - } - } - - fn flush(&mut self) -> io::Result<()> { - match self { - InnerZeroCopyWriter::Platform(ref mut writer) => writer.flush(), - InnerZeroCopyWriter::Standard(ref mut writer) => writer.flush(), - } - } -} - -pub struct ZeroCopyWriter { - /// This field is never used, but we need it to drop file descriptors - #[allow(dead_code)] - raw_obj_owner: Option, - - inner: InnerZeroCopyWriter, -} - -struct TransformContainer<'a, A: Write + AsRawObject + Sized, B: Write + Sized> { - /// This field is never used and probably could be converted into PhantomData, but might be - /// useful for restructuring later (at the moment it's basically left over from an earlier - /// design) - #[allow(dead_code)] - original: Option<&'a mut A>, - - transformed: Option, -} - -impl<'a, A: Write + AsRawObject + Sized, B: Write + Sized> Write for TransformContainer<'a, A, B> { - fn write(&mut self, bytes: &[u8]) -> io::Result { - self.transformed.as_mut().unwrap().write(bytes) - } - - fn flush(&mut self) -> io::Result<()> { - self.transformed.as_mut().unwrap().flush() - } -} - -impl<'a, A: Write + AsRawObject + Sized, B: Write + Sized> AsRawObject - for TransformContainer<'a, A, B> -{ - fn as_raw_object(&self) -> RawObject { - panic!("Test should never be used") - } -} - -impl ZeroCopyWriter { - pub fn new(writer: T) -> Self { - let raw_obj = writer.as_raw_object(); - match unsafe { PlatformZeroCopyWriter::new(raw_obj) } { - Ok(inner) => ZeroCopyWriter { - raw_obj_owner: Some(writer), - inner: InnerZeroCopyWriter::Platform(inner), - }, - _ => { - // creating the splice writer failed for whatever reason, so just make a default - // writer - ZeroCopyWriter { - raw_obj_owner: None, - inner: InnerZeroCopyWriter::Standard(writer), - } - } - } - } - - pub fn with_default<'a: 'b, 'b, F, W>( - writer: &'a mut T, - func: F, - ) -> ZeroCopyWriter - where - F: Fn(&'a mut T) -> W, - W: Write + Sized + 'b, - { - let raw_obj = writer.as_raw_object(); - match unsafe { PlatformZeroCopyWriter::new(raw_obj) } { - Ok(inner) => ZeroCopyWriter { - raw_obj_owner: Some(TransformContainer { - original: Some(writer), - transformed: None, - }), - inner: InnerZeroCopyWriter::Platform(inner), - }, - _ => { - // XXX: should func actually consume writer and leave it up to the user to save the value? - // maybe provide a default stdin method then? in some cases it would make more sense for the - // value to be consumed - let real_writer = func(writer); - ZeroCopyWriter { - raw_obj_owner: None, - inner: InnerZeroCopyWriter::Standard(TransformContainer { - original: None, - transformed: Some(real_writer), - }), - } - } - } - } - - // XXX: unsure how to get something like this working without allocating, so not providing it - /*pub fn stdout() -> ZeroCopyWriter { - let mut stdout = io::stdout(); - ZeroCopyWriter::with_default(&mut stdout, |stdout| { - stdout.lock() - }) - }*/ -} - -impl Write for ZeroCopyWriter { - fn write(&mut self, buf: &[u8]) -> io::Result { - self.inner.write(buf) - } - - fn flush(&mut self) -> io::Result<()> { - self.inner.flush() - } -} diff --git a/src/uucore/src/lib/features/zero_copy/platform.rs b/src/uucore/src/lib/features/zero_copy/platform.rs deleted file mode 100644 index 67e4354c5..000000000 --- a/src/uucore/src/lib/features/zero_copy/platform.rs +++ /dev/null @@ -1,21 +0,0 @@ -#[cfg(any(target_os = "linux", target_os = "android"))] -pub use self::linux::*; -#[cfg(unix)] -pub use self::unix::*; -#[cfg(windows)] -pub use self::windows::*; - -// Add any operating systems we support here -#[cfg(not(any(target_os = "linux", target_os = "android")))] -pub use self::default::*; - -#[cfg(any(target_os = "linux", target_os = "android"))] -mod linux; -#[cfg(unix)] -mod unix; -#[cfg(windows)] -mod windows; - -// Add any operating systems we support here -#[cfg(not(any(target_os = "linux", target_os = "android")))] -mod default; diff --git a/src/uucore/src/lib/features/zero_copy/platform/default.rs b/src/uucore/src/lib/features/zero_copy/platform/default.rs deleted file mode 100644 index 47239a361..000000000 --- a/src/uucore/src/lib/features/zero_copy/platform/default.rs +++ /dev/null @@ -1,21 +0,0 @@ -use crate::features::zero_copy::RawObject; - -use std::io::{self, Write}; - -pub struct PlatformZeroCopyWriter; - -impl PlatformZeroCopyWriter { - pub unsafe fn new(_obj: RawObject) -> Result { - Err(()) - } -} - -impl Write for PlatformZeroCopyWriter { - fn write(&mut self, _bytes: &[u8]) -> io::Result { - panic!("should never occur") - } - - fn flush(&mut self) -> io::Result<()> { - panic!("should never occur") - } -} diff --git a/src/uucore/src/lib/features/zero_copy/platform/linux.rs b/src/uucore/src/lib/features/zero_copy/platform/linux.rs deleted file mode 100644 index e2bed3061..000000000 --- a/src/uucore/src/lib/features/zero_copy/platform/linux.rs +++ /dev/null @@ -1,113 +0,0 @@ -use std::io::{self, Write}; -use std::os::unix::io::RawFd; - -use libc::{O_APPEND, S_IFIFO, S_IFREG}; -use nix::errno::Errno; -use nix::fcntl::{fcntl, splice, vmsplice, FcntlArg, SpliceFFlags}; -use nix::sys::stat::{fstat, FileStat}; -use nix::sys::uio::IoVec; -use nix::unistd::pipe; -use platform_info::{PlatformInfo, Uname}; - -use crate::features::zero_copy::{FromRawObject, RawObject}; - -lazy_static::lazy_static! { - static ref IN_WSL: bool = { - let info = PlatformInfo::new().unwrap(); - info.release().contains("Microsoft") - }; -} - -pub struct PlatformZeroCopyWriter { - raw_obj: RawObject, - read_pipe: RawFd, - write_pipe: RawFd, - #[allow(clippy::type_complexity)] - write_fn: fn(&mut PlatformZeroCopyWriter, &[IoVec<&[u8]>], usize) -> io::Result, -} - -impl PlatformZeroCopyWriter { - pub unsafe fn new(raw_obj: RawObject) -> nix::Result { - if *IN_WSL { - // apparently WSL hasn't implemented vmsplice(), causing writes to fail - // thus, we will just say zero-copy doesn't work there rather than working - // around it - return Err(nix::Error::from(Errno::EOPNOTSUPP)); - } - - let stat_info: FileStat = fstat(raw_obj)?; - let access_mode: libc::c_int = fcntl(raw_obj, FcntlArg::F_GETFL)?; - - let is_regular = (stat_info.st_mode & S_IFREG) != 0; - let is_append = (access_mode & O_APPEND) != 0; - let is_fifo = (stat_info.st_mode & S_IFIFO) != 0; - - if is_regular && !is_append { - let (read_pipe, write_pipe) = pipe()?; - - Ok(PlatformZeroCopyWriter { - raw_obj, - read_pipe, - write_pipe, - write_fn: write_regular, - }) - } else if is_fifo { - Ok(PlatformZeroCopyWriter { - raw_obj, - read_pipe: Default::default(), - write_pipe: Default::default(), - write_fn: write_fifo, - }) - } else { - // FIXME: how to error? - Err(nix::Error::from(Errno::UnknownErrno)) - } - } -} - -impl FromRawObject for PlatformZeroCopyWriter { - unsafe fn from_raw_object(obj: RawObject) -> Option { - PlatformZeroCopyWriter::new(obj).ok() - } -} - -impl Write for PlatformZeroCopyWriter { - fn write(&mut self, buf: &[u8]) -> io::Result { - let iovec = &[IoVec::from_slice(buf)]; - - let func = self.write_fn; - func(self, iovec, buf.len()) - } - - fn flush(&mut self) -> io::Result<()> { - // XXX: not sure if we need anything else - Ok(()) - } -} - -fn write_regular( - writer: &mut PlatformZeroCopyWriter, - iovec: &[IoVec<&[u8]>], - len: usize, -) -> io::Result { - vmsplice(writer.write_pipe, iovec, SpliceFFlags::empty()) - .and_then(|_| { - splice( - writer.read_pipe, - None, - writer.raw_obj, - None, - len, - SpliceFFlags::empty(), - ) - }) - .map_err(|_| io::Error::last_os_error()) -} - -fn write_fifo( - writer: &mut PlatformZeroCopyWriter, - iovec: &[IoVec<&[u8]>], - _len: usize, -) -> io::Result { - vmsplice(writer.raw_obj, iovec, SpliceFFlags::empty()).map_err(|_| io::Error::last_os_error()) -} diff --git a/src/uucore/src/lib/features/zero_copy/platform/unix.rs b/src/uucore/src/lib/features/zero_copy/platform/unix.rs deleted file mode 100644 index 553549c9b..000000000 --- a/src/uucore/src/lib/features/zero_copy/platform/unix.rs +++ /dev/null @@ -1,18 +0,0 @@ -use std::os::unix::io::{AsRawFd, FromRawFd, RawFd}; - -use crate::features::zero_copy::{AsRawObject, FromRawObject}; - -pub type RawObject = RawFd; - -impl AsRawObject for T { - fn as_raw_object(&self) -> RawObject { - self.as_raw_fd() - } -} - -// FIXME: check if this works right -impl FromRawObject for T { - unsafe fn from_raw_object(obj: RawObject) -> Option { - Some(T::from_raw_fd(obj)) - } -} diff --git a/src/uucore/src/lib/features/zero_copy/platform/windows.rs b/src/uucore/src/lib/features/zero_copy/platform/windows.rs deleted file mode 100644 index 8134bfda3..000000000 --- a/src/uucore/src/lib/features/zero_copy/platform/windows.rs +++ /dev/null @@ -1,19 +0,0 @@ -use std::os::windows::io::{AsRawHandle, FromRawHandle, RawHandle}; - -use crate::features::zero_copy::{AsRawObject, FromRawObject}; - -pub type RawObject = RawHandle; - -impl AsRawObject for T { - fn as_raw_object(&self) -> RawObject { - self.as_raw_handle() - } -} - -impl FromRawObject for T { - unsafe fn from_raw_object(obj: RawObject) -> Option { - Some(T::from_raw_handle(obj)) - } -} - -// TODO: see if there's some zero-copy stuff in Windows diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index aa96a1086..79cc2afc3 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -3,14 +3,6 @@ // Copyright (C) ~ Alex Lyon // Copyright (C) ~ Roy Ivy III ; MIT license -// * feature-gated external crates -#[cfg(all(feature = "lazy_static", target_os = "linux"))] -extern crate lazy_static; -#[cfg(feature = "nix")] -extern crate nix; -#[cfg(feature = "platform-info")] -extern crate platform_info; - // * feature-gated external crates (re-shared as public internal modules) #[cfg(feature = "libc")] pub extern crate libc; @@ -27,6 +19,7 @@ mod parser; // string parsing modules // * cross-platform modules pub use crate::mods::backup_control; pub use crate::mods::coreopts; +pub use crate::mods::display; pub use crate::mods::error; pub use crate::mods::os; pub use crate::mods::panic; @@ -46,8 +39,6 @@ pub use crate::features::fs; pub use crate::features::fsext; #[cfg(feature = "ringbuffer")] pub use crate::features::ringbuffer; -#[cfg(feature = "zero-copy")] -pub use crate::features::zero_copy; // * (platform-specific) feature-gated modules // ** non-windows @@ -58,6 +49,8 @@ pub use crate::features::mode; pub use crate::features::entries; #[cfg(all(unix, feature = "perms"))] pub use crate::features::perms; +#[cfg(all(unix, feature = "pipes"))] +pub use crate::features::pipes; #[cfg(all(unix, feature = "process"))] pub use crate::features::process; #[cfg(all(unix, not(target_os = "fuchsia"), feature = "signals"))] @@ -77,6 +70,55 @@ pub use crate::features::wide; //## core functions use std::ffi::OsString; +use std::sync::atomic::Ordering; + +use once_cell::sync::Lazy; + +use crate::display::Quotable; + +pub fn get_utility_is_second_arg() -> bool { + crate::macros::UTILITY_IS_SECOND_ARG.load(Ordering::SeqCst) +} + +pub fn set_utility_is_second_arg() { + crate::macros::UTILITY_IS_SECOND_ARG.store(true, Ordering::SeqCst) +} + +// args_os() can be expensive to call, it copies all of argv before iterating. +// So if we want only the first arg or so it's overkill. We cache it. +static ARGV: Lazy> = Lazy::new(|| wild::args_os().collect()); + +static UTIL_NAME: Lazy = Lazy::new(|| { + if get_utility_is_second_arg() { + &ARGV[1] + } else { + &ARGV[0] + } + .to_string_lossy() + .into_owned() +}); + +/// Derive the utility name. +pub fn util_name() -> &'static str { + &UTIL_NAME +} + +static EXECUTION_PHRASE: Lazy = Lazy::new(|| { + if get_utility_is_second_arg() { + ARGV.iter() + .take(2) + .map(|os_str| os_str.to_string_lossy().into_owned()) + .collect::>() + .join(" ") + } else { + ARGV[0].to_string_lossy().into_owned() + } +}); + +/// Derive the complete execution phrase for "usage". +pub fn execution_phrase() -> &'static str { + &EXECUTION_PHRASE +} pub enum InvalidEncodingHandling { Ignore, @@ -133,14 +175,15 @@ pub trait Args: Iterator + Sized { Ok(string) => Ok(string), Err(s_ret) => { full_conversion = false; - let lossy_conversion = s_ret.to_string_lossy(); eprintln!( - "Input with broken encoding occurred! (s = '{}') ", - &lossy_conversion + "Input with broken encoding occurred! (s = {}) ", + s_ret.quote() ); match handling { InvalidEncodingHandling::Ignore => Err(String::new()), - InvalidEncodingHandling::ConvertLossy => Err(lossy_conversion.to_string()), + InvalidEncodingHandling::ConvertLossy => { + Err(s_ret.to_string_lossy().into_owned()) + } InvalidEncodingHandling::Panic => { panic!("Broken encoding found but caller cannot handle it") } @@ -172,13 +215,8 @@ pub trait Args: Iterator + Sized { impl + Sized> Args for T {} -// args() ... -pub fn args() -> impl Iterator { - wild::args() -} - pub fn args_os() -> impl Iterator { - wild::args_os() + ARGV.iter().cloned() } #[cfg(test)] diff --git a/src/uucore/src/lib/macros.rs b/src/uucore/src/lib/macros.rs index 6e3a2166f..c7b15dba3 100644 --- a/src/uucore/src/lib/macros.rs +++ b/src/uucore/src/lib/macros.rs @@ -1,3 +1,5 @@ +use std::sync::atomic::AtomicBool; + // This file is part of the uutils coreutils package. // // (c) Alex Lyon @@ -5,31 +7,17 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -/// Deduce the name of the binary from the current source code filename. -/// -/// e.g.: `src/uu/cp/src/cp.rs` -> `cp` -#[macro_export] -macro_rules! executable( - () => ({ - let module = module_path!(); - let module = module.split("::").next().unwrap_or(module); - if &module[0..3] == "uu_" { - &module[3..] - } else { - module - } - }) -); +/// Whether we were called as a multicall binary ("coreutils ") +pub static UTILITY_IS_SECOND_ARG: AtomicBool = AtomicBool::new(false); + +//==== #[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!()); - } + $crate::error::set_exit_code(e.code()); + eprintln!("{}: {}", $crate::util_name(), e); }) ); @@ -46,7 +34,7 @@ macro_rules! show_if_err( #[macro_export] macro_rules! show_error( ($($args:tt)+) => ({ - eprint!("{}: ", executable!()); + eprint!("{}: ", $crate::util_name()); eprintln!($($args)+); }) ); @@ -55,7 +43,7 @@ macro_rules! show_error( #[macro_export] macro_rules! show_error_custom_description ( ($err:expr,$($args:tt)+) => ({ - eprint!("{}: {}: ", executable!(), $err); + eprint!("{}: {}: ", $crate::util_name(), $err); eprintln!($($args)+); }) ); @@ -63,7 +51,7 @@ macro_rules! show_error_custom_description ( #[macro_export] macro_rules! show_warning( ($($args:tt)+) => ({ - eprint!("{}: warning: ", executable!()); + eprint!("{}: warning: ", $crate::util_name()); eprintln!($($args)+); }) ); @@ -72,9 +60,19 @@ macro_rules! show_warning( #[macro_export] macro_rules! show_usage_error( ($($args:tt)+) => ({ - eprint!("{}: ", executable!()); + eprint!("{}: ", $crate::util_name()); eprintln!($($args)+); - eprintln!("Try '{} --help' for more information.", executable!()); + eprintln!("Try '{} --help' for more information.", $crate::execution_phrase()); + }) +); + +//==== + +/// Calls `exit()` with the provided exit code. +#[macro_export] +macro_rules! exit( + ($exit_code:expr) => ({ + ::std::process::exit($exit_code) }) ); @@ -82,16 +80,8 @@ macro_rules! show_usage_error( #[macro_export] macro_rules! crash( ($exit_code:expr, $($args:tt)+) => ({ - show_error!($($args)+); - ::std::process::exit($exit_code) - }) -); - -/// Calls `exit()` with the provided exit code. -#[macro_export] -macro_rules! exit( - ($exit_code:expr) => ({ - ::std::process::exit($exit_code) + $crate::show_error!($($args)+); + $crate::exit!($exit_code) }) ); @@ -102,26 +92,12 @@ macro_rules! crash_if_err( ($exit_code:expr, $exp:expr) => ( match $exp { Ok(m) => m, - Err(f) => crash!($exit_code, "{}", f), + Err(f) => $crate::crash!($exit_code, "{}", f), } ) ); -/// Unwraps the Result. Instead of panicking, it shows the error and then -/// returns from the function with the provided exit code. -/// Assumes the current function returns an i32 value. -#[macro_export] -macro_rules! return_if_err( - ($exit_code:expr, $exp:expr) => ( - match $exp { - Ok(m) => m, - Err(f) => { - show_error!("{}", f); - return $exit_code; - } - } - ) -); +//==== #[macro_export] macro_rules! safe_write( @@ -143,39 +119,27 @@ macro_rules! safe_writeln( ) ); -/// Unwraps the Result. Instead of panicking, it exists the program with exit -/// code 1. -#[macro_export] -macro_rules! safe_unwrap( - ($exp:expr) => ( - match $exp { - Ok(m) => m, - Err(f) => crash!(1, "{}", f.to_string()) - } - ) -); - //-- message templates -//-- message templates : general +//-- message templates : (join utility sub-macros) #[macro_export] -macro_rules! snippet_list_join_oxford { +macro_rules! snippet_list_join_oxford_comma { ($conjunction:expr, $valOne:expr, $valTwo:expr) => ( format!("{}, {} {}", $valOne, $conjunction, $valTwo) ); ($conjunction:expr, $valOne:expr, $valTwo:expr $(, $remaining_values:expr)*) => ( - format!("{}, {}", $valOne, snippet_list_join_inner!($conjunction, $valTwo $(, $remaining_values)*)) + format!("{}, {}", $valOne, $crate::snippet_list_join_oxford_comma!($conjunction, $valTwo $(, $remaining_values)*)) ); } #[macro_export] -macro_rules! snippet_list_join_or { - ($valOne:expr, $valTwo:expr) => ( - format!("{} or {}", $valOne, $valTwo) +macro_rules! snippet_list_join { + ($conjunction:expr, $valOne:expr, $valTwo:expr) => ( + format!("{} {} {}", $valOne, $conjunction, $valTwo) ); - ($valOne:expr, $valTwo:expr $(, $remaining_values:expr)*) => ( - format!("{}, {}", $valOne, snippet_list_join_oxford!("or", $valTwo $(, $remaining_values)*)) + ($conjunction:expr, $valOne:expr, $valTwo:expr $(, $remaining_values:expr)*) => ( + format!("{}, {}", $valOne, $crate::snippet_list_join_oxford_comma!($conjunction, $valTwo $(, $remaining_values)*)) ); } @@ -193,10 +157,10 @@ macro_rules! msg_invalid_input { #[macro_export] macro_rules! msg_invalid_opt_use { ($about:expr, $flag:expr) => { - msg_invalid_input!(format!("The '{}' option {}", $flag, $about)) + $crate::msg_invalid_input!(format!("The '{}' option {}", $flag, $about)) }; ($about:expr, $long_flag:expr, $short_flag:expr) => { - msg_invalid_input!(format!( + $crate::msg_invalid_input!(format!( "The '{}' ('{}') option {}", $long_flag, $short_flag, $about )) @@ -206,10 +170,10 @@ macro_rules! msg_invalid_opt_use { #[macro_export] macro_rules! msg_opt_only_usable_if { ($clause:expr, $flag:expr) => { - msg_invalid_opt_use!(format!("only usable if {}", $clause), $flag) + $crate::msg_invalid_opt_use!(format!("only usable if {}", $clause), $flag) }; ($clause:expr, $long_flag:expr, $short_flag:expr) => { - msg_invalid_opt_use!( + $crate::msg_invalid_opt_use!( format!("only usable if {}", $clause), $long_flag, $short_flag @@ -220,13 +184,13 @@ macro_rules! msg_opt_only_usable_if { #[macro_export] macro_rules! msg_opt_invalid_should_be { ($expects:expr, $received:expr, $flag:expr) => { - msg_invalid_opt_use!( + $crate::msg_invalid_opt_use!( format!("expects {}, but was provided {}", $expects, $received), $flag ) }; ($expects:expr, $received:expr, $long_flag:expr, $short_flag:expr) => { - msg_invalid_opt_use!( + $crate::msg_invalid_opt_use!( format!("expects {}, but was provided {}", $expects, $received), $long_flag, $short_flag @@ -239,13 +203,13 @@ macro_rules! msg_opt_invalid_should_be { #[macro_export] macro_rules! msg_expects_one_of { ($valOne:expr $(, $remaining_values:expr)*) => ( - msg_invalid_input!(format!("expects one of {}", snippet_list_join_or!($valOne $(, $remaining_values)*))) + $crate::msg_invalid_input!(format!("expects one of {}", $crate::snippet_list_join!("or", $valOne $(, $remaining_values)*))) ); } #[macro_export] macro_rules! msg_expects_no_more_than_one_of { ($valOne:expr $(, $remaining_values:expr)*) => ( - msg_invalid_input!(format!("expects no more than one of {}", snippet_list_join_or!($valOne $(, $remaining_values)*))) ; + $crate::msg_invalid_input!(format!("expects no more than one of {}", $crate::snippet_list_join!("or", $valOne $(, $remaining_values)*))) ; ); } diff --git a/src/uucore/src/lib/mods.rs b/src/uucore/src/lib/mods.rs index b0235832b..8f6d14976 100644 --- a/src/uucore/src/lib/mods.rs +++ b/src/uucore/src/lib/mods.rs @@ -2,6 +2,7 @@ pub mod backup_control; pub mod coreopts; +pub mod display; pub mod error; pub mod os; pub mod panic; diff --git a/src/uucore/src/lib/mods/backup_control.rs b/src/uucore/src/lib/mods/backup_control.rs index 6fa48d308..acb7342b7 100644 --- a/src/uucore/src/lib/mods/backup_control.rs +++ b/src/uucore/src/lib/mods/backup_control.rs @@ -1,5 +1,92 @@ +//! Implement GNU-style backup functionality. +//! +//! This module implements the backup functionality as described in the [GNU +//! manual][1]. It provides +//! +//! - pre-defined [`clap`-Arguments][2] for inclusion in utilities that +//! implement backups +//! - determination of the [backup mode][3] +//! - determination of the [backup suffix][4] +//! - [backup target path construction][5] +//! - [Error types][6] for backup-related errors +//! - GNU-compliant [help texts][7] for backup-related errors +//! +//! Backup-functionality is implemented by the following utilities: +//! +//! - `cp` +//! - `install` +//! - `ln` +//! - `mv` +//! +//! +//! [1]: https://www.gnu.org/software/coreutils/manual/html_node/Backup-options.html +//! [2]: arguments +//! [3]: `determine_backup_mode()` +//! [4]: `determine_backup_suffix()` +//! [5]: `get_backup_path()` +//! [6]: `BackupError` +//! [7]: `BACKUP_CONTROL_LONG_HELP` +//! +//! +//! # Usage example +//! +//! ``` +//! #[macro_use] +//! extern crate uucore; +//! +//! use clap::{App, Arg, ArgMatches}; +//! use std::path::{Path, PathBuf}; +//! use uucore::backup_control::{self, BackupMode}; +//! use uucore::error::{UError, UResult}; +//! +//! fn main() { +//! let usage = String::from("app [OPTION]... ARG"); +//! let long_usage = String::from("And here's a detailed explanation"); +//! +//! let matches = App::new("app") +//! .arg(backup_control::arguments::backup()) +//! .arg(backup_control::arguments::backup_no_args()) +//! .arg(backup_control::arguments::suffix()) +//! .usage(&usage[..]) +//! .after_help(&*format!( +//! "{}\n{}", +//! long_usage, +//! backup_control::BACKUP_CONTROL_LONG_HELP +//! )) +//! .get_matches_from(vec![ +//! "app", "--backup=t", "--suffix=bak~" +//! ]); +//! +//! let backup_mode = match backup_control::determine_backup_mode(&matches) { +//! Err(e) => { +//! show!(e); +//! return; +//! }, +//! Ok(mode) => mode, +//! }; +//! let backup_suffix = backup_control::determine_backup_suffix(&matches); +//! let target_path = Path::new("/tmp/example"); +//! +//! let backup_path = backup_control::get_backup_path( +//! backup_mode, target_path, &backup_suffix +//! ); +//! +//! // Perform your backups here. +//! +//! } +//! ``` + +// spell-checker:ignore backupopt + +use crate::{ + display::Quotable, + error::{UError, UResult}, +}; +use clap::ArgMatches; use std::{ env, + error::Error, + fmt::{Debug, Display}, path::{Path, PathBuf}, }; @@ -17,15 +104,148 @@ the VERSION_CONTROL environment variable. Here are the values: existing, nil numbered if numbered backups exist, simple otherwise simple, never always make simple backups"; +static VALID_ARGS_HELP: &str = "Valid arguments are: + - 'none', 'off' + - 'simple', 'never' + - 'existing', 'nil' + - 'numbered', 't'"; + +/// Available backup modes. +/// +/// The mapping of the backup modes to the CLI arguments is annotated on the +/// enum variants. #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum BackupMode { + /// Argument 'none', 'off' NoBackup, + /// Argument 'simple', 'never' SimpleBackup, + /// Argument 'numbered', 't' NumberedBackup, + /// Argument 'existing', 'nil' ExistingBackup, } -pub fn determine_backup_suffix(supplied_suffix: Option<&str>) -> String { +/// Backup error types. +/// +/// Errors are currently raised by [`determine_backup_mode`] only. All errors +/// are implemented as [`UCustomError`] for uniform handling across utilities. +#[derive(Debug, Eq, PartialEq)] +pub enum BackupError { + /// An invalid argument (e.g. 'foo') was given as backup type. First + /// parameter is the argument, second is the arguments origin (CLI or + /// ENV-var) + InvalidArgument(String, String), + /// An ambiguous argument (e.g. 'n') was given as backup type. First + /// parameter is the argument, second is the arguments origin (CLI or + /// ENV-var) + AmbiguousArgument(String, String), + /// Currently unused + BackupImpossible(), + // BackupFailed(PathBuf, PathBuf, std::io::Error), +} + +impl UError for BackupError { + fn code(&self) -> i32 { + match self { + BackupError::BackupImpossible() => 2, + _ => 1, + } + } + + fn usage(&self) -> bool { + // Suggested by clippy. + matches!( + self, + BackupError::InvalidArgument(_, _) | BackupError::AmbiguousArgument(_, _) + ) + } +} + +impl Error for BackupError {} + +impl Display for BackupError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use BackupError as BE; + match self { + BE::InvalidArgument(arg, origin) => write!( + f, + "invalid argument {} for '{}'\n{}", + arg.quote(), + origin, + VALID_ARGS_HELP + ), + BE::AmbiguousArgument(arg, origin) => write!( + f, + "ambiguous argument {} for '{}'\n{}", + arg.quote(), + origin, + VALID_ARGS_HELP + ), + BE::BackupImpossible() => write!(f, "cannot create backup"), + // Placeholder for later + // BE::BackupFailed(from, to, e) => Display::fmt( + // &uio_error!(e, "failed to backup {} to {}", from.quote(), to.quote()), + // f + // ), + } + } +} + +/// Arguments for backup-related functionality. +/// +/// Rather than implementing the `clap`-Arguments for every utility, it is +/// recommended to include the `clap` arguments via the functions provided here. +/// This way the backup-specific arguments are handled uniformly across +/// utilities and can be maintained in one central place. +pub mod arguments { + extern crate clap; + + pub static OPT_BACKUP: &str = "backupopt_backup"; + pub static OPT_BACKUP_NO_ARG: &str = "backupopt_b"; + pub static OPT_SUFFIX: &str = "backupopt_suffix"; + + /// '--backup' argument + pub fn backup() -> clap::Arg<'static, 'static> { + clap::Arg::with_name(OPT_BACKUP) + .long("backup") + .help("make a backup of each existing destination file") + .takes_value(true) + .require_equals(true) + .min_values(0) + .value_name("CONTROL") + } + + /// '-b' argument + pub fn backup_no_args() -> clap::Arg<'static, 'static> { + clap::Arg::with_name(OPT_BACKUP_NO_ARG) + .short("b") + .help("like --backup but does not accept an argument") + } + + /// '-S, --suffix' argument + pub fn suffix() -> clap::Arg<'static, 'static> { + clap::Arg::with_name(OPT_SUFFIX) + .short("S") + .long("suffix") + .help("override the usual backup suffix") + .takes_value(true) + .value_name("SUFFIX") + } +} + +/// Obtain the suffix to use for a backup. +/// +/// In order of precedence, this function obtains the backup suffix +/// +/// 1. From the '-S' or '--suffix' CLI argument, if present +/// 2. From the "SIMPLE_BACKUP_SUFFIX" environment variable, if present +/// 3. By using the default '~' if none of the others apply +/// +/// This function directly takes [`clap::ArgMatches`] as argument and looks for +/// the '-S' and '--suffix' arguments itself. +pub fn determine_backup_suffix(matches: &ArgMatches) -> String { + let supplied_suffix = matches.value_of(arguments::OPT_SUFFIX); if let Some(suffix) = supplied_suffix { String::from(suffix) } else { @@ -38,7 +258,13 @@ pub fn determine_backup_suffix(supplied_suffix: Option<&str>) -> String { /// Parses the backup options according to the [GNU manual][1], and converts /// them to an instance of `BackupMode` for further processing. /// -/// For an explanation of what the arguments mean, refer to the examples below. +/// Takes [`clap::ArgMatches`] as argument which **must** contain the options +/// from [`arguments::backup()`] and [`arguments::backup_no_args()`]. Otherwise +/// the `NoBackup` mode is returned unconditionally. +/// +/// It is recommended for anyone who would like to implement the +/// backup-functionality to use the arguments prepared in the `arguments` +/// submodule (see examples) /// /// [1]: https://www.gnu.org/software/coreutils/manual/html_node/Backup-options.html /// @@ -47,9 +273,11 @@ pub fn determine_backup_suffix(supplied_suffix: Option<&str>) -> String { /// /// If an argument supplied directly to the long `backup` option, or read in /// through the `VERSION CONTROL` env var is ambiguous (i.e. may resolve to -/// multiple backup modes) or invalid, an error is returned. The error contains -/// the formatted error string which may then be passed to the -/// [`show_usage_error`] macro. +/// multiple backup modes) or invalid, an [`InvalidArgument`][10] or +/// [`AmbiguousArgument`][11] error is returned, respectively. +/// +/// [10]: BackupError::InvalidArgument +/// [11]: BackupError::AmbiguousArgument /// /// /// # Examples @@ -61,34 +289,18 @@ pub fn determine_backup_suffix(supplied_suffix: Option<&str>) -> String { /// #[macro_use] /// extern crate uucore; /// use uucore::backup_control::{self, BackupMode}; -/// use clap::{App, Arg}; +/// use clap::{App, Arg, ArgMatches}; /// /// fn main() { -/// let OPT_BACKUP: &str = "backup"; -/// let OPT_BACKUP_NO_ARG: &str = "b"; /// let matches = App::new("app") -/// .arg(Arg::with_name(OPT_BACKUP_NO_ARG) -/// .short(OPT_BACKUP_NO_ARG)) -/// .arg(Arg::with_name(OPT_BACKUP) -/// .long(OPT_BACKUP) -/// .takes_value(true) -/// .require_equals(true) -/// .min_values(0)) +/// .arg(backup_control::arguments::backup()) +/// .arg(backup_control::arguments::backup_no_args()) /// .get_matches_from(vec![ /// "app", "-b", "--backup=t" /// ]); -/// -/// 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) -/// ); -/// let backup_mode = match backup_mode { -/// Err(err) => { -/// show_usage_error!("{}", err); -/// return; -/// }, -/// Ok(mode) => mode, -/// }; +/// +/// let backup_mode = backup_control::determine_backup_mode(&matches).unwrap(); +/// assert_eq!(backup_mode, BackupMode::NumberedBackup) /// } /// ``` /// @@ -99,57 +311,43 @@ pub fn determine_backup_suffix(supplied_suffix: Option<&str>) -> String { /// ``` /// #[macro_use] /// extern crate uucore; -/// use uucore::backup_control::{self, BackupMode}; -/// use clap::{crate_version, App, Arg, ArgMatches}; +/// use uucore::backup_control::{self, BackupMode, BackupError}; +/// use clap::{App, Arg, ArgMatches}; /// /// fn main() { -/// let OPT_BACKUP: &str = "backup"; -/// let OPT_BACKUP_NO_ARG: &str = "b"; /// let matches = App::new("app") -/// .arg(Arg::with_name(OPT_BACKUP_NO_ARG) -/// .short(OPT_BACKUP_NO_ARG)) -/// .arg(Arg::with_name(OPT_BACKUP) -/// .long(OPT_BACKUP) -/// .takes_value(true) -/// .require_equals(true) -/// .min_values(0)) +/// .arg(backup_control::arguments::backup()) +/// .arg(backup_control::arguments::backup_no_args()) /// .get_matches_from(vec![ /// "app", "-b", "--backup=n" /// ]); /// -/// 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) -/// ); -/// let backup_mode = match backup_mode { -/// Err(err) => { -/// show_usage_error!("{}", err); -/// return; -/// }, -/// Ok(mode) => mode, -/// }; +/// let backup_mode = backup_control::determine_backup_mode(&matches); +/// +/// assert!(backup_mode.is_err()); +/// let err = backup_mode.unwrap_err(); +/// // assert_eq!(err, BackupError::AmbiguousArgument); +/// // Use uucore functionality to show the error to the user +/// show!(err); /// } /// ``` -pub fn determine_backup_mode( - short_opt_present: bool, - long_opt_present: bool, - long_opt_value: Option<&str>, -) -> Result { - if long_opt_present { +pub fn determine_backup_mode(matches: &ArgMatches) -> UResult { + if matches.is_present(arguments::OPT_BACKUP) { // Use method to determine the type of backups to make. When this option // is used but method is not specified, then the value of the // VERSION_CONTROL environment variable is used. And if VERSION_CONTROL - // is not set, the default backup type is ‘existing’. - if let Some(method) = long_opt_value { + // is not set, the default backup type is 'existing'. + if let Some(method) = matches.value_of(arguments::OPT_BACKUP) { // Second argument is for the error string that is returned. match_method(method, "backup type") } else if let Ok(method) = env::var("VERSION_CONTROL") { // Second argument is for the error string that is returned. match_method(&method, "$VERSION_CONTROL") } else { + // Default if no argument is provided to '--backup' Ok(BackupMode::ExistingBackup) } - } else if short_opt_present { + } else if matches.is_present(arguments::OPT_BACKUP_NO_ARG) { // the short form of this option, -b does not accept any argument. // Using -b is equivalent to using --backup=existing. Ok(BackupMode::ExistingBackup) @@ -172,10 +370,13 @@ pub fn determine_backup_mode( /// /// # Errors /// -/// If `method` is ambiguous (i.e. may resolve to multiple backup modes) or -/// invalid, an error is returned. The error contains the formatted error string -/// which may then be passed to the [`show_usage_error`] macro. -fn match_method(method: &str, origin: &str) -> Result { +/// If `method` is invalid or ambiguous (i.e. may resolve to multiple backup +/// modes), an [`InvalidArgument`][10] or [`AmbiguousArgument`][11] error is +/// returned, respectively. +/// +/// [10]: BackupError::InvalidArgument +/// [11]: BackupError::AmbiguousArgument +fn match_method(method: &str, origin: &str) -> UResult { let matches: Vec<&&str> = BACKUP_CONTROL_VALUES .iter() .filter(|val| val.starts_with(method)) @@ -189,21 +390,10 @@ fn match_method(method: &str, origin: &str) -> Result { _ => unreachable!(), // cannot happen as we must have exactly one match // from the list above. } + } else if matches.is_empty() { + Err(BackupError::InvalidArgument(method.to_string(), origin.to_string()).into()) } else { - let error_type = if matches.is_empty() { - "invalid" - } else { - "ambiguous" - }; - Err(format!( - "{0} argument ‘{1}’ for ‘{2}’ -Valid arguments are: - - ‘none’, ‘off’ - - ‘simple’, ‘never’ - - ‘existing’, ‘nil’ - - ‘numbered’, ‘t’", - error_type, method, origin - )) + Err(BackupError::AmbiguousArgument(method.to_string(), origin.to_string()).into()) } } @@ -255,6 +445,7 @@ mod tests { use super::*; use std::env; // Required to instantiate mutex in shared context + use clap::App; use lazy_static::lazy_static; use std::sync::Mutex; @@ -271,16 +462,20 @@ mod tests { // Environment variable for "VERSION_CONTROL" static ENV_VERSION_CONTROL: &str = "VERSION_CONTROL"; + fn make_app() -> clap::App<'static, 'static> { + App::new("app") + .arg(arguments::backup()) + .arg(arguments::backup_no_args()) + .arg(arguments::suffix()) + } + // Defaults to --backup=existing #[test] fn test_backup_mode_short_only() { - let short_opt_present = true; - let long_opt_present = false; - let long_opt_value = None; let _dummy = TEST_MUTEX.lock().unwrap(); + let matches = make_app().get_matches_from(vec!["app", "-b"]); - let result = - determine_backup_mode(short_opt_present, long_opt_present, long_opt_value).unwrap(); + let result = determine_backup_mode(&matches).unwrap(); assert_eq!(result, BackupMode::ExistingBackup); } @@ -288,13 +483,10 @@ mod tests { // --backup takes precedence over -b #[test] fn test_backup_mode_long_preferred_over_short() { - let short_opt_present = true; - let long_opt_present = true; - let long_opt_value = Some("none"); let _dummy = TEST_MUTEX.lock().unwrap(); + let matches = make_app().get_matches_from(vec!["app", "-b", "--backup=none"]); - let result = - determine_backup_mode(short_opt_present, long_opt_present, long_opt_value).unwrap(); + let result = determine_backup_mode(&matches).unwrap(); assert_eq!(result, BackupMode::NoBackup); } @@ -302,13 +494,10 @@ mod tests { // --backup can be passed without an argument #[test] fn test_backup_mode_long_without_args_no_env() { - let short_opt_present = false; - let long_opt_present = true; - let long_opt_value = None; let _dummy = TEST_MUTEX.lock().unwrap(); + let matches = make_app().get_matches_from(vec!["app", "--backup"]); - let result = - determine_backup_mode(short_opt_present, long_opt_present, long_opt_value).unwrap(); + let result = determine_backup_mode(&matches).unwrap(); assert_eq!(result, BackupMode::ExistingBackup); } @@ -316,13 +505,10 @@ mod tests { // --backup can be passed with an argument only #[test] fn test_backup_mode_long_with_args() { - let short_opt_present = false; - let long_opt_present = true; - let long_opt_value = Some("simple"); let _dummy = TEST_MUTEX.lock().unwrap(); + let matches = make_app().get_matches_from(vec!["app", "--backup=simple"]); - let result = - determine_backup_mode(short_opt_present, long_opt_present, long_opt_value).unwrap(); + let result = determine_backup_mode(&matches).unwrap(); assert_eq!(result, BackupMode::SimpleBackup); } @@ -330,43 +516,36 @@ mod tests { // --backup errors on invalid argument #[test] fn test_backup_mode_long_with_args_invalid() { - let short_opt_present = false; - let long_opt_present = true; - let long_opt_value = Some("foobar"); let _dummy = TEST_MUTEX.lock().unwrap(); + let matches = make_app().get_matches_from(vec!["app", "--backup=foobar"]); - let result = determine_backup_mode(short_opt_present, long_opt_present, long_opt_value); + let result = determine_backup_mode(&matches); assert!(result.is_err()); - let text = result.unwrap_err(); - assert!(text.contains("invalid argument ‘foobar’ for ‘backup type’")); + let text = format!("{}", result.unwrap_err()); + assert!(text.contains("invalid argument 'foobar' for 'backup type'")); } // --backup errors on ambiguous argument #[test] fn test_backup_mode_long_with_args_ambiguous() { - let short_opt_present = false; - let long_opt_present = true; - let long_opt_value = Some("n"); let _dummy = TEST_MUTEX.lock().unwrap(); + let matches = make_app().get_matches_from(vec!["app", "--backup=n"]); - let result = determine_backup_mode(short_opt_present, long_opt_present, long_opt_value); + let result = determine_backup_mode(&matches); assert!(result.is_err()); - let text = result.unwrap_err(); - assert!(text.contains("ambiguous argument ‘n’ for ‘backup type’")); + let text = format!("{}", result.unwrap_err()); + assert!(text.contains("ambiguous argument 'n' for 'backup type'")); } // --backup accepts shortened arguments (si for simple) #[test] fn test_backup_mode_long_with_arg_shortened() { - let short_opt_present = false; - let long_opt_present = true; - let long_opt_value = Some("si"); let _dummy = TEST_MUTEX.lock().unwrap(); + let matches = make_app().get_matches_from(vec!["app", "--backup=si"]); - let result = - determine_backup_mode(short_opt_present, long_opt_present, long_opt_value).unwrap(); + let result = determine_backup_mode(&matches).unwrap(); assert_eq!(result, BackupMode::SimpleBackup); } @@ -374,14 +553,11 @@ mod tests { // -b ignores the "VERSION_CONTROL" environment variable #[test] fn test_backup_mode_short_only_ignore_env() { - let short_opt_present = true; - let long_opt_present = false; - let long_opt_value = None; let _dummy = TEST_MUTEX.lock().unwrap(); env::set_var(ENV_VERSION_CONTROL, "none"); + let matches = make_app().get_matches_from(vec!["app", "-b"]); - let result = - determine_backup_mode(short_opt_present, long_opt_present, long_opt_value).unwrap(); + let result = determine_backup_mode(&matches).unwrap(); assert_eq!(result, BackupMode::ExistingBackup); env::remove_var(ENV_VERSION_CONTROL); @@ -390,14 +566,11 @@ mod tests { // --backup can be passed without an argument, but reads env var if existent #[test] fn test_backup_mode_long_without_args_with_env() { - let short_opt_present = false; - let long_opt_present = true; - let long_opt_value = None; let _dummy = TEST_MUTEX.lock().unwrap(); env::set_var(ENV_VERSION_CONTROL, "none"); + let matches = make_app().get_matches_from(vec!["app", "--backup"]); - let result = - determine_backup_mode(short_opt_present, long_opt_present, long_opt_value).unwrap(); + let result = determine_backup_mode(&matches).unwrap(); assert_eq!(result, BackupMode::NoBackup); env::remove_var(ENV_VERSION_CONTROL); @@ -406,48 +579,41 @@ mod tests { // --backup errors on invalid VERSION_CONTROL env var #[test] fn test_backup_mode_long_with_env_var_invalid() { - let short_opt_present = false; - let long_opt_present = true; - let long_opt_value = None; let _dummy = TEST_MUTEX.lock().unwrap(); env::set_var(ENV_VERSION_CONTROL, "foobar"); + let matches = make_app().get_matches_from(vec!["app", "--backup"]); - let result = determine_backup_mode(short_opt_present, long_opt_present, long_opt_value); + let result = determine_backup_mode(&matches); assert!(result.is_err()); - let text = result.unwrap_err(); - assert!(text.contains("invalid argument ‘foobar’ for ‘$VERSION_CONTROL’")); + let text = format!("{}", result.unwrap_err()); + assert!(text.contains("invalid argument 'foobar' for '$VERSION_CONTROL'")); env::remove_var(ENV_VERSION_CONTROL); } // --backup errors on ambiguous VERSION_CONTROL env var #[test] fn test_backup_mode_long_with_env_var_ambiguous() { - let short_opt_present = false; - let long_opt_present = true; - let long_opt_value = None; let _dummy = TEST_MUTEX.lock().unwrap(); env::set_var(ENV_VERSION_CONTROL, "n"); + let matches = make_app().get_matches_from(vec!["app", "--backup"]); - let result = determine_backup_mode(short_opt_present, long_opt_present, long_opt_value); + let result = determine_backup_mode(&matches); assert!(result.is_err()); - let text = result.unwrap_err(); - assert!(text.contains("ambiguous argument ‘n’ for ‘$VERSION_CONTROL’")); + let text = format!("{}", result.unwrap_err()); + assert!(text.contains("ambiguous argument 'n' for '$VERSION_CONTROL'")); env::remove_var(ENV_VERSION_CONTROL); } // --backup accepts shortened env vars (si for simple) #[test] fn test_backup_mode_long_with_env_var_shortened() { - let short_opt_present = false; - let long_opt_present = true; - let long_opt_value = None; let _dummy = TEST_MUTEX.lock().unwrap(); env::set_var(ENV_VERSION_CONTROL, "si"); + let matches = make_app().get_matches_from(vec!["app", "--backup"]); - let result = - determine_backup_mode(short_opt_present, long_opt_present, long_opt_value).unwrap(); + let result = determine_backup_mode(&matches).unwrap(); assert_eq!(result, BackupMode::SimpleBackup); env::remove_var(ENV_VERSION_CONTROL); diff --git a/src/uucore/src/lib/mods/coreopts.rs b/src/uucore/src/lib/mods/coreopts.rs index f3fb77335..b534ff902 100644 --- a/src/uucore/src/lib/mods/coreopts.rs +++ b/src/uucore/src/lib/mods/coreopts.rs @@ -120,7 +120,7 @@ impl<'a> CoreOptions<'a> { macro_rules! app { ($syntax: expr, $summary: expr, $long_help: expr) => { uucore::coreopts::CoreOptions::new(uucore::coreopts::HelpText { - name: executable!(), + name: uucore::util_name(), version: env!("CARGO_PKG_VERSION"), syntax: $syntax, summary: $summary, @@ -130,7 +130,7 @@ macro_rules! app { }; ($syntax: expr, $summary: expr, $long_help: expr, $display_usage: expr) => { uucore::coreopts::CoreOptions::new(uucore::coreopts::HelpText { - name: executable!(), + name: uucore::util_name(), version: env!("CARGO_PKG_VERSION"), syntax: $syntax, summary: $summary, diff --git a/src/uucore/src/lib/mods/display.rs b/src/uucore/src/lib/mods/display.rs new file mode 100644 index 000000000..dfe64184f --- /dev/null +++ b/src/uucore/src/lib/mods/display.rs @@ -0,0 +1,557 @@ +/// Utilities for printing paths, with special attention paid to special +/// characters and invalid unicode. +/// +/// For displaying paths in informational messages use `Quotable::quote`. This +/// will wrap quotes around the filename and add the necessary escapes to make +/// it copy/paste-able into a shell. +/// +/// For writing raw paths to stdout when the output should not be quoted or escaped, +/// use `println_verbatim`. This will preserve invalid unicode. +/// +/// # Examples +/// ``` +/// use std::path::Path; +/// use uucore::display::{Quotable, println_verbatim}; +/// +/// let path = Path::new("foo/bar.baz"); +/// +/// println!("Found file {}", path.quote()); // Prints "Found file 'foo/bar.baz'" +/// println_verbatim(path)?; // Prints "foo/bar.baz" +/// # Ok::<(), std::io::Error>(()) +/// ``` +// spell-checker:ignore Fbar +use std::borrow::Cow; +use std::ffi::OsStr; +#[cfg(any(unix, target_os = "wasi", windows))] +use std::fmt::Write as FmtWrite; +use std::fmt::{self, Display, Formatter}; +use std::io::{self, Write as IoWrite}; + +#[cfg(unix)] +use std::os::unix::ffi::OsStrExt; +#[cfg(target_os = "wasi")] +use std::os::wasi::ffi::OsStrExt; +#[cfg(any(unix, target_os = "wasi"))] +use std::str::from_utf8; + +/// An extension trait for displaying filenames to users. +pub trait Quotable { + /// Returns an object that implements [`Display`] for printing filenames with + /// proper quoting and escaping for the platform. + /// + /// On Unix this corresponds to sh/bash syntax, on Windows Powershell syntax + /// is used. + /// + /// # Examples + /// + /// ``` + /// use std::path::Path; + /// use uucore::display::Quotable; + /// + /// let path = Path::new("foo/bar.baz"); + /// + /// println!("Found file {}", path.quote()); // Prints "Found file 'foo/bar.baz'" + /// ``` + fn quote(&self) -> Quoted<'_>; + + /// Like `quote()`, but don't actually add quotes unless necessary because of + /// whitespace or special characters. + /// + /// # Examples + /// + /// ``` + /// use std::path::Path; + /// use uucore::display::Quotable; + /// use uucore::show_error; + /// + /// let foo = Path::new("foo/bar.baz"); + /// let bar = Path::new("foo bar"); + /// + /// show_error!("{}: Not found", foo.maybe_quote()); // Prints "util: foo/bar.baz: Not found" + /// show_error!("{}: Not found", bar.maybe_quote()); // Prints "util: 'foo bar': Not found" + /// ``` + fn maybe_quote(&self) -> Quoted<'_> { + let mut quoted = self.quote(); + quoted.force_quote = false; + quoted + } +} + +macro_rules! impl_as_ref { + ($type: ty) => { + impl Quotable for $type { + fn quote(&self) -> Quoted<'_> { + Quoted::new(self.as_ref()) + } + } + }; +} + +impl_as_ref!(str); +impl_as_ref!(&'_ str); +impl_as_ref!(String); +impl_as_ref!(std::path::Path); +impl_as_ref!(&'_ std::path::Path); +impl_as_ref!(std::path::PathBuf); +impl_as_ref!(std::path::Component<'_>); +impl_as_ref!(std::path::Components<'_>); +impl_as_ref!(std::path::Iter<'_>); +impl_as_ref!(std::ffi::OsStr); +impl_as_ref!(&'_ std::ffi::OsStr); +impl_as_ref!(std::ffi::OsString); + +// Cow<'_, str> does not implement AsRef and this is unlikely to be fixed +// for backward compatibility reasons. Otherwise we'd use a blanket impl. +impl Quotable for Cow<'_, str> { + fn quote(&self) -> Quoted<'_> { + let text: &str = self.as_ref(); + Quoted::new(text.as_ref()) + } +} + +impl Quotable for Cow<'_, std::path::Path> { + fn quote(&self) -> Quoted<'_> { + let text: &std::path::Path = self.as_ref(); + Quoted::new(text.as_ref()) + } +} + +/// A wrapper around [`OsStr`] for printing paths with quoting and escaping applied. +#[derive(Debug, Copy, Clone)] +pub struct Quoted<'a> { + text: &'a OsStr, + force_quote: bool, +} + +impl<'a> Quoted<'a> { + fn new(text: &'a OsStr) -> Self { + Quoted { + text, + force_quote: true, + } + } +} + +impl Display for Quoted<'_> { + #[cfg(any(windows, unix, target_os = "wasi"))] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + // On Unix we emulate sh syntax. On Windows Powershell. + // They're just similar enough to share some code. + + /// Characters with special meaning outside quotes. + // https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_02 + // I don't know why % is in there, and GNU doesn't quote it either. + // {} were used in a version elsewhere but seem unnecessary, GNU doesn't + // quote them. They're used in function definitions but not in a way we + // have to worry about. + #[cfg(any(unix, target_os = "wasi"))] + const SPECIAL_SHELL_CHARS: &[u8] = b"|&;<>()$`\\\"'*?[]="; + // FIXME: I'm not a PowerShell wizard and don't know if this is correct. + // I just copied the Unix version, removed \, and added ,{} based on + // experimentation. + // I have noticed that ~?*[] only get expanded in some contexts, so watch + // out for that if doing your own tests. + // Get-ChildItem seems unwilling to quote anything so it doesn't help. + // There's the additional wrinkle that Windows has stricter requirements + // for filenames: I've been testing using a Linux build of PowerShell, but + // this code doesn't even compile on Linux. + #[cfg(windows)] + const SPECIAL_SHELL_CHARS: &[u8] = b"|&;<>()$`\"'*?[]=,{}"; + + /// Characters with a special meaning at the beginning of a name. + // ~ expands a home directory. + // # starts a comment. + // ! is a common extension for expanding the shell history. + #[cfg(any(unix, target_os = "wasi"))] + const SPECIAL_SHELL_CHARS_START: &[char] = &['~', '#', '!']; + // Same deal as before, this is possibly incomplete. + // A single stand-alone exclamation mark seems to have some special meaning. + #[cfg(windows)] + const SPECIAL_SHELL_CHARS_START: &[char] = &['~', '#', '@', '!']; + + /// Characters that are interpreted specially in a double-quoted string. + #[cfg(any(unix, target_os = "wasi"))] + const DOUBLE_UNSAFE: &[u8] = &[b'"', b'`', b'$', b'\\']; + #[cfg(windows)] + const DOUBLE_UNSAFE: &[u8] = &[b'"', b'`', b'$']; + + let text = match self.text.to_str() { + None => return write_escaped(f, self.text), + Some(text) => text, + }; + + let mut is_single_safe = true; + let mut is_double_safe = true; + let mut requires_quote = self.force_quote; + + if let Some(first) = text.chars().next() { + if SPECIAL_SHELL_CHARS_START.contains(&first) { + requires_quote = true; + } + // Unlike in Unix, quoting an argument may stop it + // from being recognized as an option. I like that very much. + // But we don't want to quote "-" because that's a common + // special argument and PowerShell doesn't mind it. + #[cfg(windows)] + if first == '-' && text.len() > 1 { + requires_quote = true; + } + } else { + // Empty string + requires_quote = true; + } + + for ch in text.chars() { + if ch.is_ascii() { + let ch = ch as u8; + if ch == b'\'' { + is_single_safe = false; + } + if DOUBLE_UNSAFE.contains(&ch) { + is_double_safe = false; + } + if !requires_quote && SPECIAL_SHELL_CHARS.contains(&ch) { + requires_quote = true; + } + if ch.is_ascii_control() { + return write_escaped(f, self.text); + } + } + if !requires_quote && ch.is_whitespace() { + // This includes unicode whitespace. + // We maybe don't have to escape it, we don't escape other lookalike + // characters either, but it's confusing if it goes unquoted. + requires_quote = true; + } + } + + if !requires_quote { + return f.write_str(text); + } else if is_single_safe { + return write_simple(f, text, '\''); + } else if is_double_safe { + return write_simple(f, text, '\"'); + } else { + return write_single_escaped(f, text); + } + + fn write_simple(f: &mut Formatter<'_>, text: &str, quote: char) -> fmt::Result { + f.write_char(quote)?; + f.write_str(text)?; + f.write_char(quote)?; + Ok(()) + } + + #[cfg(any(unix, target_os = "wasi"))] + fn write_single_escaped(f: &mut Formatter<'_>, text: &str) -> fmt::Result { + let mut iter = text.split('\''); + if let Some(chunk) = iter.next() { + if !chunk.is_empty() { + write_simple(f, chunk, '\'')?; + } + } + for chunk in iter { + f.write_str("\\'")?; + if !chunk.is_empty() { + write_simple(f, chunk, '\'')?; + } + } + Ok(()) + } + + /// Write using the syntax described here: + /// https://www.gnu.org/software/bash/manual/html_node/ANSI_002dC-Quoting.html + /// + /// Supported by these shells: + /// - bash + /// - zsh + /// - busybox sh + /// - mksh + /// + /// Not supported by these: + /// - fish + /// - dash + /// - tcsh + #[cfg(any(unix, target_os = "wasi"))] + fn write_escaped(f: &mut Formatter<'_>, text: &OsStr) -> fmt::Result { + f.write_str("$'")?; + for chunk in from_utf8_iter(text.as_bytes()) { + match chunk { + Ok(chunk) => { + for ch in chunk.chars() { + match ch { + '\n' => f.write_str("\\n")?, + '\t' => f.write_str("\\t")?, + '\r' => f.write_str("\\r")?, + // We could do \b, \f, \v, etc., but those are + // rare enough to be confusing. + // \0 doesn't work consistently because of the + // octal \nnn syntax, and null bytes can't appear + // in filenames anyway. + ch if ch.is_ascii_control() => write!(f, "\\x{:02X}", ch as u8)?, + '\\' | '\'' => { + // '?' and '"' can also be escaped this way + // but AFAICT there's no reason to do so + f.write_char('\\')?; + f.write_char(ch)?; + } + ch => { + f.write_char(ch)?; + } + } + } + } + Err(unit) => write!(f, "\\x{:02X}", unit)?, + } + } + f.write_char('\'')?; + Ok(()) + } + + #[cfg(windows)] + fn write_single_escaped(f: &mut Formatter<'_>, text: &str) -> fmt::Result { + // Quotes in Powershell can be escaped by doubling them + f.write_char('\'')?; + let mut iter = text.split('\''); + if let Some(chunk) = iter.next() { + f.write_str(chunk)?; + } + for chunk in iter { + f.write_str("''")?; + f.write_str(chunk)?; + } + f.write_char('\'')?; + Ok(()) + } + + #[cfg(windows)] + fn write_escaped(f: &mut Formatter<'_>, text: &OsStr) -> fmt::Result { + // ` takes the role of \ since \ is already used as the path separator. + // Things are UTF-16-oriented, so we escape code units as "`u{1234}". + use std::char::decode_utf16; + use std::os::windows::ffi::OsStrExt; + + f.write_char('"')?; + for ch in decode_utf16(text.encode_wide()) { + match ch { + Ok(ch) => match ch { + '\0' => f.write_str("`0")?, + '\r' => f.write_str("`r")?, + '\n' => f.write_str("`n")?, + '\t' => f.write_str("`t")?, + ch if ch.is_ascii_control() => write!(f, "`u{{{:04X}}}", ch as u8)?, + '`' => f.write_str("``")?, + '$' => f.write_str("`$")?, + '"' => f.write_str("\"\"")?, + ch => f.write_char(ch)?, + }, + Err(err) => write!(f, "`u{{{:04X}}}", err.unpaired_surrogate())?, + } + } + f.write_char('"')?; + Ok(()) + } + } + + #[cfg(not(any(unix, target_os = "wasi", windows)))] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + // As a fallback, we use Rust's own escaping rules. + // This is reasonably sane and very easy to implement. + // We use single quotes because that's hardcoded in a lot of tests. + let text = self.text.to_string_lossy(); + if self.force_quote || !text.chars().all(|ch| ch.is_alphanumeric() || ch == '.') { + write!(f, "'{}'", text.escape_debug()) + } else { + f.write_str(&text) + } + } +} + +#[cfg(any(unix, target_os = "wasi"))] +fn from_utf8_iter(mut bytes: &[u8]) -> impl Iterator> { + std::iter::from_fn(move || { + if bytes.is_empty() { + return None; + } + match from_utf8(bytes) { + Ok(text) => { + bytes = &[]; + Some(Ok(text)) + } + Err(err) if err.valid_up_to() == 0 => { + let res = bytes[0]; + bytes = &bytes[1..]; + Some(Err(res)) + } + Err(err) => { + let (valid, rest) = bytes.split_at(err.valid_up_to()); + bytes = rest; + Some(Ok(from_utf8(valid).unwrap())) + } + } + }) +} + +/// Print a path (or `OsStr`-like object) directly to stdout, with a trailing newline, +/// without losing any information if its encoding is invalid. +/// +/// This function is appropriate for commands where printing paths is the point and the +/// output is likely to be captured, like `pwd` and `basename`. For informational output +/// use `Quotable::quote`. +/// +/// FIXME: This is lossy on Windows. It could probably be implemented using some low-level +/// API that takes UTF-16, without going through io::Write. This is not a big priority +/// because broken filenames are much rarer on Windows than on Unix. +pub fn println_verbatim>(text: S) -> io::Result<()> { + let stdout = io::stdout(); + let mut stdout = stdout.lock(); + #[cfg(any(unix, target_os = "wasi"))] + { + stdout.write_all(text.as_ref().as_bytes())?; + stdout.write_all(b"\n")?; + } + #[cfg(not(any(unix, target_os = "wasi")))] + { + writeln!(stdout, "{}", std::path::Path::new(text.as_ref()).display())?; + } + Ok(()) +} + +/// Like `println_verbatim`, without the trailing newline. +pub fn print_verbatim>(text: S) -> io::Result<()> { + let mut stdout = io::stdout(); + #[cfg(any(unix, target_os = "wasi"))] + { + stdout.write_all(text.as_ref().as_bytes()) + } + #[cfg(not(any(unix, target_os = "wasi")))] + { + write!(stdout, "{}", std::path::Path::new(text.as_ref()).display()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn verify_quote(cases: &[(impl Quotable, &str)]) { + for (case, expected) in cases { + assert_eq!(case.quote().to_string(), *expected); + } + } + + fn verify_maybe(cases: &[(impl Quotable, &str)]) { + for (case, expected) in cases { + assert_eq!(case.maybe_quote().to_string(), *expected); + } + } + + /// This should hold on any platform, or else other tests fail. + #[test] + fn test_basic() { + verify_quote(&[ + ("foo", "'foo'"), + ("", "''"), + ("foo/bar.baz", "'foo/bar.baz'"), + ]); + verify_maybe(&[ + ("foo", "foo"), + ("", "''"), + ("foo bar", "'foo bar'"), + ("$foo", "'$foo'"), + ("-", "-"), + ]); + } + + #[cfg(any(unix, target_os = "wasi", windows))] + #[test] + fn test_common() { + verify_maybe(&[ + ("a#b", "a#b"), + ("#ab", "'#ab'"), + ("a~b", "a~b"), + ("!", "'!'"), + ]); + } + + #[cfg(any(unix, target_os = "wasi"))] + #[test] + fn test_unix() { + verify_quote(&[ + ("can't", r#""can't""#), + (r#"can'"t"#, r#"'can'\''"t'"#), + (r#"can'$t"#, r#"'can'\''$t'"#), + ("foo\nb\ta\r\\\0`r", r#"$'foo\nb\ta\r\\\x00`r'"#), + ("foo\x02", r#"$'foo\x02'"#), + (r#"'$''"#, r#"\''$'\'\'"#), + ]); + verify_quote(&[(OsStr::from_bytes(b"foo\xFF"), r#"$'foo\xFF'"#)]); + verify_maybe(&[ + ("-x", "-x"), + ("a,b", "a,b"), + ("a\\b", "'a\\b'"), + ("}", ("}")), + ]); + } + + #[cfg(windows)] + #[test] + fn test_windows() { + use std::ffi::OsString; + use std::os::windows::ffi::OsStringExt; + verify_quote(&[ + (r#"foo\bar"#, r#"'foo\bar'"#), + ("can't", r#""can't""#), + (r#"can'"t"#, r#"'can''"t'"#), + (r#"can'$t"#, r#"'can''$t'"#), + ("foo\nb\ta\r\\\0`r", r#""foo`nb`ta`r\`0``r""#), + ("foo\x02", r#""foo`u{0002}""#), + (r#"'$''"#, r#"'''$'''''"#), + ]); + verify_quote(&[( + OsString::from_wide(&[b'x' as u16, 0xD800]), + r#""x`u{D800}""#, + )]); + verify_maybe(&[ + ("-x", "'-x'"), + ("a,b", "'a,b'"), + ("a\\b", "a\\b"), + ("}", "'}'"), + ]); + } + + #[cfg(any(unix, target_os = "wasi"))] + #[test] + fn test_utf8_iter() { + type ByteStr = &'static [u8]; + type Chunk = Result<&'static str, u8>; + const CASES: &[(ByteStr, &[Chunk])] = &[ + (b"", &[]), + (b"hello", &[Ok("hello")]), + // Immediately invalid + (b"\xFF", &[Err(b'\xFF')]), + // Incomplete UTF-8 + (b"\xC2", &[Err(b'\xC2')]), + (b"\xF4\x8F", &[Err(b'\xF4'), Err(b'\x8F')]), + (b"\xFF\xFF", &[Err(b'\xFF'), Err(b'\xFF')]), + (b"hello\xC2", &[Ok("hello"), Err(b'\xC2')]), + (b"\xFFhello", &[Err(b'\xFF'), Ok("hello")]), + (b"\xFF\xC2hello", &[Err(b'\xFF'), Err(b'\xC2'), Ok("hello")]), + (b"foo\xFFbar", &[Ok("foo"), Err(b'\xFF'), Ok("bar")]), + ( + b"foo\xF4\x8Fbar", + &[Ok("foo"), Err(b'\xF4'), Err(b'\x8F'), Ok("bar")], + ), + ( + b"foo\xFF\xC2bar", + &[Ok("foo"), Err(b'\xFF'), Err(b'\xC2'), Ok("bar")], + ), + ]; + for &(case, expected) in CASES { + assert_eq!( + from_utf8_iter(case).collect::>().as_slice(), + expected + ); + } + } +} diff --git a/src/uucore/src/lib/mods/error.rs b/src/uucore/src/lib/mods/error.rs index 664fc9841..c04a0f2f1 100644 --- a/src/uucore/src/lib/mods/error.rs +++ b/src/uucore/src/lib/mods/error.rs @@ -99,7 +99,10 @@ pub type UResult = Result>; /// An example of a custom error from `ls`: /// /// ``` -/// use uucore::error::{UError, UResult}; +/// use uucore::{ +/// display::Quotable, +/// error::{UError, UResult} +/// }; /// use std::{ /// error::Error, /// fmt::{Display, Debug}, @@ -126,8 +129,8 @@ pub type UResult = Result>; /// 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()), +/// LsError::InvalidLineWidth(s) => write!(f, "invalid line width: {}", s.quote()), +/// LsError::NoMetadata(p) => write!(f, "could not open file: {}", p.quote()), /// } /// } /// } @@ -158,7 +161,10 @@ pub trait UError: Error + Send { /// # Example /// /// ``` - /// use uucore::error::{UError}; + /// use uucore::{ + /// display::Quotable, + /// error::UError + /// }; /// use std::{ /// error::Error, /// fmt::{Display, Debug}, @@ -189,8 +195,8 @@ pub trait UError: Error + Send { /// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { /// use MyError as ME; /// match self { - /// ME::Foo(s) => write!(f, "Unknown Foo: '{}'", s), - /// ME::Bar(p) => write!(f, "Couldn't find Bar: '{}'", p.display()), + /// ME::Foo(s) => write!(f, "Unknown Foo: {}", s.quote()), + /// ME::Bar(p) => write!(f, "Couldn't find Bar: {}", p.quote()), /// ME::Bing() => write!(f, "Exterminate!"), /// } /// } @@ -203,13 +209,16 @@ pub trait UError: Error + Send { /// Print usage help to a custom error. /// /// Return true or false to control whether a short usage help is printed - /// below the error message. The usage help is in the format: "Try '{name} - /// --help' for more information." and printed only if `true` is returned. + /// below the error message. The usage help is in the format: "Try `{name} + /// --help` for more information." and printed only if `true` is returned. /// /// # Example /// /// ``` - /// use uucore::error::{UError}; + /// use uucore::{ + /// display::Quotable, + /// error::UError + /// }; /// use std::{ /// error::Error, /// fmt::{Display, Debug}, @@ -240,8 +249,8 @@ pub trait UError: Error + Send { /// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { /// use MyError as ME; /// match self { - /// ME::Foo(s) => write!(f, "Unknown Foo: '{}'", s), - /// ME::Bar(p) => write!(f, "Couldn't find Bar: '{}'", p.display()), + /// ME::Foo(s) => write!(f, "Unknown Foo: {}", s.quote()), + /// ME::Bar(p) => write!(f, "Couldn't find Bar: {}", p.quote()), /// ME::Bing() => write!(f, "Exterminate!"), /// } /// } @@ -342,7 +351,10 @@ impl UError for UUsageError { /// 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, UError}; +/// use uucore::{ +/// display::Quotable, +/// error::{FromIo, UResult, UIoError, UError} +/// }; /// use std::fs::File; /// use std::path::Path; /// let path = Path::new("test.txt"); @@ -350,12 +362,12 @@ impl UError for UUsageError { /// // Manual construction /// let e: Box = UIoError::new( /// std::io::ErrorKind::NotFound, -/// format!("cannot access '{}'", path.display()) +/// format!("cannot access {}", path.quote()) /// ); /// 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())); +/// let res: UResult = File::open(path).map_err_context(|| format!("cannot access {}", path.quote())); /// ``` #[derive(Debug)] pub struct UIoError { @@ -368,7 +380,7 @@ impl UIoError { pub fn new>(kind: std::io::ErrorKind, context: S) -> Box { Box::new(Self { context: context.into(), - inner: std::io::Error::new(kind, ""), + inner: kind.into(), }) } } @@ -380,10 +392,12 @@ 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, + + let mut message; + let message = if self.inner.raw_os_error().is_some() { + // These are errors that come directly from the OS. + // We want to normalize their messages across systems, + // and we want to strip the "(os error X)" suffix. match self.inner.kind() { NotFound => "No such file or directory", PermissionDenied => "Permission denied", @@ -401,14 +415,46 @@ impl Display for UIoError { TimedOut => "Timed out", WriteZero => "Write zero", Interrupted => "Interrupted", - Other => "Other", UnexpectedEof => "Unexpected end of file", - _ => panic!("Unexpected io error: {}", self.inner), - }, - ) + _ => { + // TODO: When the new error variants + // (https://github.com/rust-lang/rust/issues/86442) + // are stabilized, we should add them to the match statement. + message = strip_errno(&self.inner); + capitalize(&mut message); + &message + } + } + } else { + // These messages don't need as much normalization, and the above + // messages wouldn't always be a good substitute. + // For example, ErrorKind::NotFound doesn't necessarily mean it was + // a file that was not found. + // There are also errors with entirely custom messages. + message = self.inner.to_string(); + capitalize(&mut message); + &message + }; + write!(f, "{}: {}", self.context, message) } } +/// Capitalize the first character of an ASCII string. +fn capitalize(text: &mut str) { + if let Some(first) = text.get_mut(..1) { + first.make_ascii_uppercase(); + } +} + +/// Strip the trailing " (os error XX)" from io error strings. +pub fn strip_errno(err: &std::io::Error) -> String { + let mut msg = err.to_string(); + if let Some(pos) = msg.find(" (os error ") { + msg.truncate(pos); + } + msg +} + /// Enables the conversion from [`std::io::Error`] to [`UError`] and from [`std::io::Result`] to /// [`UResult`]. pub trait FromIo { @@ -519,7 +565,7 @@ macro_rules! uio_error( /// let res: UResult<()> = Err(1.into()); /// ``` /// This type is especially useful for a trivial conversion from utils returning [`i32`] to -/// returning [`UResult`]. +/// returning [`UResult`]. #[derive(Debug)] pub struct ExitCode(pub i32); diff --git a/src/uucore/src/lib/mods/panic.rs b/src/uucore/src/lib/mods/panic.rs index ba0ecdf12..ebba10429 100644 --- a/src/uucore/src/lib/mods/panic.rs +++ b/src/uucore/src/lib/mods/panic.rs @@ -1,17 +1,42 @@ +//! Custom panic hooks that allow silencing certain types of errors. +//! +//! Use the [`mute_sigpipe_panic`] function to silence panics caused by +//! broken pipe errors. This can happen when a process is still +//! producing data when the consuming process terminates and closes the +//! pipe. For example, +//! +//! ```sh +//! $ seq inf | head -n 1 +//! ``` +//! use std::panic; +use std::panic::PanicInfo; -//## SIGPIPE handling background/discussions ... -//* `uutils` ~ , -//* rust and `rg` ~ , , +/// Decide whether a panic was caused by a broken pipe (SIGPIPE) error. +fn is_broken_pipe(info: &PanicInfo) -> bool { + if let Some(res) = info.payload().downcast_ref::() { + if res.contains("BrokenPipe") || res.contains("Broken pipe") { + return true; + } + } + false +} +/// Terminate without error on panics that occur due to broken pipe errors. +/// +/// For background discussions on `SIGPIPE` handling, see +/// +/// * https://github.com/uutils/coreutils/issues/374 +/// * https://github.com/uutils/coreutils/pull/1106 +/// * https://github.com/rust-lang/rust/issues/62569 +/// * https://github.com/BurntSushi/ripgrep/issues/200 +/// * https://github.com/crev-dev/cargo-crev/issues/287 +/// pub fn mute_sigpipe_panic() { let hook = panic::take_hook(); panic::set_hook(Box::new(move |info| { - if let Some(res) = info.payload().downcast_ref::() { - if res.contains("BrokenPipe") { - return; - } + if !is_broken_pipe(info) { + hook(info) } - hook(info) })); } diff --git a/src/uucore/src/lib/mods/ranges.rs b/src/uucore/src/lib/mods/ranges.rs index 9e1e67d5a..f142e14fb 100644 --- a/src/uucore/src/lib/mods/ranges.rs +++ b/src/uucore/src/lib/mods/ranges.rs @@ -9,6 +9,8 @@ use std::str::FromStr; +use crate::display::Quotable; + #[derive(PartialEq, Eq, PartialOrd, Ord, Debug)] pub struct Range { pub low: usize, @@ -86,7 +88,7 @@ impl Range { for item in list.split(',') { let range_item = FromStr::from_str(item) - .map_err(|e| format!("range '{}' was invalid: {}", item, e))?; + .map_err(|e| format!("range {} was invalid: {}", item.quote(), e))?; ranges.push(range_item); } diff --git a/src/uucore/src/lib/parser/parse_size.rs b/src/uucore/src/lib/parser/parse_size.rs index ec0b08c9e..c05c0d3f1 100644 --- a/src/uucore/src/lib/parser/parse_size.rs +++ b/src/uucore/src/lib/parser/parse_size.rs @@ -9,6 +9,8 @@ use std::convert::TryFrom; use std::error::Error; use std::fmt; +use crate::display::Quotable; + /// Parse a size string into a number of bytes. /// /// A size string comprises an integer and an optional unit. The unit @@ -107,6 +109,9 @@ impl fmt::Display for ParseSizeError { } } +// FIXME: It's more idiomatic to move the formatting into the Display impl, +// but there's a lot of downstream code that constructs these errors manually +// that would be affected impl ParseSizeError { fn parse_failure(s: &str) -> ParseSizeError { // stderr on linux (GNU coreutils 8.32) (LC_ALL=C) @@ -140,7 +145,7 @@ impl ParseSizeError { // --width // --strings // etc. - ParseSizeError::ParseFailure(format!("'{}'", s)) + ParseSizeError::ParseFailure(format!("{}", s.quote())) } fn size_too_big(s: &str) -> ParseSizeError { @@ -160,7 +165,10 @@ impl ParseSizeError { // 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)) + ParseSizeError::SizeTooBig(format!( + "{}: Value too large for defined data type", + s.quote() + )) } } @@ -262,7 +270,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.quote())) ); } } diff --git a/src/uucore/src/lib/parser/parse_time.rs b/src/uucore/src/lib/parser/parse_time.rs index fdf43b727..68f0ca8d0 100644 --- a/src/uucore/src/lib/parser/parse_time.rs +++ b/src/uucore/src/lib/parser/parse_time.rs @@ -9,6 +9,8 @@ use std::time::Duration; +use crate::display::Quotable; + pub fn from_str(string: &str) -> Result { let len = string.len(); if len == 0 { @@ -25,13 +27,13 @@ pub fn from_str(string: &str) -> Result { if string == "inf" || string == "infinity" { ("inf", 1) } else { - return Err(format!("invalid time interval '{}'", string)); + return Err(format!("invalid time interval {}", string.quote())); } } }; let num = numstr .parse::() - .map_err(|e| format!("invalid time interval '{}': {}", string, e))?; + .map_err(|e| format!("invalid time interval {}: {}", string.quote(), e))?; const NANOS_PER_SEC: u32 = 1_000_000_000; let whole_secs = num.trunc(); diff --git a/src/uucore_procs/src/lib.rs b/src/uucore_procs/src/lib.rs index f62e4178e..092a4a66c 100644 --- a/src/uucore_procs/src/lib.rs +++ b/src/uucore_procs/src/lib.rs @@ -104,7 +104,7 @@ pub fn gen_uumain(_args: TokenStream, stream: TokenStream) -> TokenStream { show_error!("{}", s); } if e.usage() { - eprintln!("Try '{} --help' for more information.", executable!()); + eprintln!("Try '{} --help' for more information.", uucore::execution_phrase()); } e.code() } diff --git a/tests/by-util/test_base32.rs b/tests/by-util/test_base32.rs index 178341f44..5c74c5b59 100644 --- a/tests/by-util/test_base32.rs +++ b/tests/by-util/test_base32.rs @@ -85,11 +85,15 @@ fn test_wrap() { #[test] fn test_wrap_no_arg() { for wrap_param in &["-w", "--wrap"] { - let expected_stderr = "error: The argument '--wrap \' requires a value but none was \ - supplied\n\nUSAGE:\n base32 [OPTION]... [FILE]\n\nFor more \ - information try --help" - .to_string(); - new_ucmd!() + let ts = TestScenario::new(util_name!()); + let expected_stderr = &format!( + "error: The argument '--wrap \' requires a value but none was \ + supplied\n\nUSAGE:\n {1} {0} [OPTION]... [FILE]\n\nFor more \ + information try --help", + ts.util_name, + ts.bin_path.to_string_lossy() + ); + ts.ucmd() .arg(wrap_param) .fails() .stderr_only(expected_stderr); diff --git a/tests/by-util/test_basename.rs b/tests/by-util/test_basename.rs index d9632106e..141745ac3 100644 --- a/tests/by-util/test_basename.rs +++ b/tests/by-util/test_basename.rs @@ -114,9 +114,12 @@ fn test_no_args() { #[test] fn test_no_args_output() { - new_ucmd!() - .fails() - .stderr_is("basename: missing operand\nTry 'basename --help' for more information."); + let ts = TestScenario::new(util_name!()); + ts.ucmd().fails().stderr_is(&format!( + "{0}: missing operand\nTry '{1} {0} --help' for more information.", + ts.util_name, + ts.bin_path.to_string_lossy() + )); } #[test] @@ -126,10 +129,12 @@ fn test_too_many_args() { #[test] fn test_too_many_args_output() { - new_ucmd!() - .args(&["a", "b", "c"]) - .fails() - .stderr_is("basename: extra operand 'c'\nTry 'basename --help' for more information."); + let ts = TestScenario::new(util_name!()); + ts.ucmd().args(&["a", "b", "c"]).fails().stderr_is(format!( + "{0}: extra operand 'c'\nTry '{1} {0} --help' for more information.", + ts.util_name, + ts.bin_path.to_string_lossy() + )); } #[cfg(any(unix, target_os = "redox"))] @@ -147,3 +152,32 @@ fn invalid_utf8_args_unix() { let os_str = OsStr::from_bytes(&source[..]); test_invalid_utf8_args(os_str); } + +#[test] +fn test_root() { + let expected = if cfg!(windows) { "\\\n" } else { "/\n" }; + new_ucmd!().arg("/").succeeds().stdout_is(expected); +} + +#[test] +fn test_double_slash() { + // TODO The GNU tests seem to suggest that some systems treat "//" + // as the same directory as "/" directory but not all systems. We + // should extend this test to account for that possibility. + let expected = if cfg!(windows) { "\\\n" } else { "/\n" }; + new_ucmd!().arg("//").succeeds().stdout_is(expected); + new_ucmd!() + .args(&["//", "/"]) + .succeeds() + .stdout_is(expected); + new_ucmd!() + .args(&["//", "//"]) + .succeeds() + .stdout_is(expected); +} + +#[test] +fn test_triple_slash() { + let expected = if cfg!(windows) { "\\\n" } else { "/\n" }; + new_ucmd!().arg("///").succeeds().stdout_is(expected); +} diff --git a/tests/by-util/test_cat.rs b/tests/by-util/test_cat.rs index e52be9506..b629a06e6 100644 --- a/tests/by-util/test_cat.rs +++ b/tests/by-util/test_cat.rs @@ -1,8 +1,13 @@ +// spell-checker:ignore NOFILE + use crate::common::util::*; use std::fs::OpenOptions; #[cfg(unix)] use std::io::Read; +#[cfg(target_os = "linux")] +use rlimit::Resource; + #[test] fn test_output_simple() { new_ucmd!() @@ -87,6 +92,23 @@ fn test_fifo_symlink() { thread.join().unwrap(); } +#[test] +#[cfg(target_os = "linux")] +fn test_closes_file_descriptors() { + // Each file creates a pipe, which has two file descriptors. + // If they are not closed then five is certainly too many. + new_ucmd!() + .args(&[ + "alpha.txt", + "alpha.txt", + "alpha.txt", + "alpha.txt", + "alpha.txt", + ]) + .with_limit(Resource::NOFILE, 9, 9) + .succeeds(); +} + #[test] #[cfg(unix)] fn test_piped_to_regular_file() { diff --git a/tests/by-util/test_chgrp.rs b/tests/by-util/test_chgrp.rs index 1b8057e47..1d047cfe2 100644 --- a/tests/by-util/test_chgrp.rs +++ b/tests/by-util/test_chgrp.rs @@ -43,7 +43,7 @@ fn test_invalid_group() { .arg("__nosuchgroup__") .arg("/") .fails() - .stderr_is("chgrp: invalid group: __nosuchgroup__"); + .stderr_is("chgrp: invalid group: '__nosuchgroup__'"); } #[test] @@ -230,7 +230,7 @@ fn test_big_h() { } #[test] -#[cfg(target_os = "linux")] +#[cfg(not(target_vendor = "apple"))] fn basic_succeeds() { let (at, mut ucmd) = at_and_ucmd!(); let one_group = nix::unistd::getgroups().unwrap(); @@ -251,3 +251,105 @@ fn test_no_change() { at.touch("file"); ucmd.arg("").arg(at.plus("file")).succeeds(); } + +#[test] +#[cfg(not(target_vendor = "apple"))] +fn test_permission_denied() { + use std::os::unix::prelude::PermissionsExt; + + if let Some(group) = nix::unistd::getgroups().unwrap().first() { + let (at, mut ucmd) = at_and_ucmd!(); + at.mkdir("dir"); + at.touch("dir/file"); + std::fs::set_permissions(at.plus("dir"), PermissionsExt::from_mode(0o0000)).unwrap(); + ucmd.arg("-R") + .arg(group.as_raw().to_string()) + .arg("dir") + .fails() + .stderr_only("chgrp: cannot access 'dir': Permission denied"); + } +} + +#[test] +#[cfg(not(target_vendor = "apple"))] +fn test_subdir_permission_denied() { + use std::os::unix::prelude::PermissionsExt; + + if let Some(group) = nix::unistd::getgroups().unwrap().first() { + let (at, mut ucmd) = at_and_ucmd!(); + at.mkdir("dir"); + at.mkdir("dir/subdir"); + at.touch("dir/subdir/file"); + std::fs::set_permissions(at.plus("dir/subdir"), PermissionsExt::from_mode(0o0000)).unwrap(); + ucmd.arg("-R") + .arg(group.as_raw().to_string()) + .arg("dir") + .fails() + .stderr_only("chgrp: cannot access 'dir/subdir': Permission denied"); + } +} + +#[test] +#[cfg(not(target_vendor = "apple"))] +fn test_traverse_symlinks() { + use std::os::unix::prelude::MetadataExt; + let groups = nix::unistd::getgroups().unwrap(); + if groups.len() < 2 { + return; + } + let (first_group, second_group) = (groups[0], groups[1]); + + for &(args, traverse_first, traverse_second) in &[ + (&[][..] as &[&str], false, false), + (&["-H"][..], true, false), + (&["-P"][..], false, false), + (&["-L"][..], true, true), + ] { + let scenario = TestScenario::new("chgrp"); + + let (at, mut ucmd) = (scenario.fixtures.clone(), scenario.ucmd()); + + at.mkdir("dir"); + at.mkdir("dir2"); + at.touch("dir2/file"); + at.mkdir("dir3"); + at.touch("dir3/file"); + at.symlink_dir("dir2", "dir/dir2_ln"); + at.symlink_dir("dir3", "dir3_ln"); + + scenario + .ccmd("chgrp") + .arg(first_group.to_string()) + .arg("dir2/file") + .arg("dir3/file") + .succeeds(); + + assert!(at.plus("dir2/file").metadata().unwrap().gid() == first_group.as_raw()); + assert!(at.plus("dir3/file").metadata().unwrap().gid() == first_group.as_raw()); + + ucmd.arg("-R") + .args(args) + .arg(second_group.to_string()) + .arg("dir") + .arg("dir3_ln") + .succeeds() + .no_stderr(); + + assert_eq!( + at.plus("dir2/file").metadata().unwrap().gid(), + if traverse_second { + second_group.as_raw() + } else { + first_group.as_raw() + } + ); + assert_eq!( + at.plus("dir3/file").metadata().unwrap().gid(), + if traverse_first { + second_group.as_raw() + } else { + first_group.as_raw() + } + ); + } +} diff --git a/tests/by-util/test_chmod.rs b/tests/by-util/test_chmod.rs index 186c645e5..1b8983bc3 100644 --- a/tests/by-util/test_chmod.rs +++ b/tests/by-util/test_chmod.rs @@ -1,5 +1,5 @@ use crate::common::util::*; -use std::fs::{metadata, set_permissions, OpenOptions}; +use std::fs::{metadata, set_permissions, OpenOptions, Permissions}; use std::os::unix::fs::{OpenOptionsExt, PermissionsExt}; use std::sync::Mutex; @@ -201,11 +201,6 @@ fn test_chmod_ugoa() { before: 0o100000, after: 0o100755, }, - TestCase { - args: vec!["-w", TEST_FILE], - before: 0o100777, - after: 0o100577, - }, TestCase { args: vec!["-x", TEST_FILE], before: 0o100777, @@ -213,6 +208,21 @@ fn test_chmod_ugoa() { }, ]; run_tests(tests); + + // check that we print an error if umask prevents us from removing a permission + let (at, mut ucmd) = at_and_ucmd!(); + at.touch("file"); + set_permissions(at.plus("file"), Permissions::from_mode(0o777)).unwrap(); + ucmd.args(&["-w", "file"]) + .fails() + .code_is(1) + // spell-checker:disable-next-line + .stderr_is("chmod: file: new permissions are r-xrwxrwx, not r-xr-xr-x"); + assert_eq!( + metadata(at.plus("file")).unwrap().permissions().mode(), + 0o100577 + ); + unsafe { umask(last); } @@ -330,8 +340,8 @@ fn test_chmod_recursive() { .arg("a") .arg("z") .succeeds() - .stderr_contains(&"to 333 (-wx-wx-wx)") - .stderr_contains(&"to 222 (-w--w--w-)"); + .stdout_contains(&"to 0333 (-wx-wx-wx)") + .stdout_contains(&"to 0222 (-w--w--w-)"); assert_eq!(at.metadata("z/y").permissions().mode(), 0o100222); assert_eq!(at.metadata("a/a").permissions().mode(), 0o100222); @@ -350,13 +360,24 @@ fn test_chmod_recursive() { fn test_chmod_non_existing_file() { new_ucmd!() .arg("-R") - .arg("--verbose") .arg("-r,a+w") .arg("does-not-exist") .fails() .stderr_contains(&"cannot access 'does-not-exist': No such file or directory"); } +#[test] +fn test_chmod_non_existing_file_silent() { + new_ucmd!() + .arg("-R") + .arg("--quiet") + .arg("-r,a+w") + .arg("does-not-exist") + .fails() + .no_stderr() + .code_is(1); +} + #[test] fn test_chmod_preserve_root() { new_ucmd!() @@ -490,3 +511,49 @@ fn test_chmod_strip_minus_from_mode() { assert_eq!(test.1, args.join(" ")); } } + +#[test] +fn test_chmod_keep_setgid() { + for &(from, arg, to) in &[ + (0o7777, "777", 0o46777), + (0o7777, "=777", 0o40777), + (0o7777, "0777", 0o46777), + (0o7777, "=0777", 0o40777), + (0o7777, "00777", 0o40777), + (0o2444, "a+wx", 0o42777), + (0o2444, "a=wx", 0o42333), + (0o1444, "g+s", 0o43444), + (0o4444, "u-s", 0o40444), + (0o7444, "a-s", 0o41444), + ] { + let (at, mut ucmd) = at_and_ucmd!(); + at.mkdir("dir"); + set_permissions(at.plus("dir"), Permissions::from_mode(from)).unwrap(); + let r = ucmd.arg(arg).arg("dir").succeeds(); + println!("{}", r.stderr_str()); + assert_eq!(at.metadata("dir").permissions().mode(), to); + } +} + +#[test] +fn test_no_operands() { + new_ucmd!() + .arg("777") + .fails() + .code_is(1) + .stderr_is("chmod: missing operand"); +} + +#[test] +fn test_mode_after_dash_dash() { + let (at, ucmd) = at_and_ucmd!(); + run_single_test( + &TestCase { + args: vec!["--", "-r", TEST_FILE], + before: 0o100777, + after: 0o100333, + }, + at, + ucmd, + ); +} diff --git a/tests/by-util/test_chown.rs b/tests/by-util/test_chown.rs index 86365f51b..d5ebb4600 100644 --- a/tests/by-util/test_chown.rs +++ b/tests/by-util/test_chown.rs @@ -1,4 +1,4 @@ -// spell-checker:ignore (words) agroupthatdoesntexist auserthatdoesntexist groupname notexisting passgrp +// spell-checker:ignore (words) agroupthatdoesntexist auserthatdoesntexist cuuser groupname notexisting passgrp use crate::common::util::*; #[cfg(target_os = "linux")] @@ -139,6 +139,14 @@ fn test_chown_only_owner_colon() { .succeeds() .stderr_contains(&"retained as"); + scene + .ucmd() + .arg(format!("{}.", user_name)) + .arg("--verbose") + .arg(file1) + .succeeds() + .stderr_contains(&"retained as"); + scene .ucmd() .arg("root:") @@ -180,6 +188,14 @@ fn test_chown_only_colon() { .arg(file1) .fails() .stderr_contains(&"invalid group: '::'"); + + scene + .ucmd() + .arg("..") + .arg("--verbose") + .arg(file1) + .fails() + .stderr_contains(&"invalid group: '..'"); } #[test] @@ -195,6 +211,8 @@ fn test_chown_failed_stdout() { } #[test] +// FixME: Fails on freebsd because of chown: invalid group: 'root:root' +#[cfg(not(target_os = "freebsd"))] fn test_chown_owner_group() { // test chown username:group file.txt @@ -230,6 +248,22 @@ fn test_chown_owner_group() { } result.stderr_contains(&"retained as"); + scene + .ucmd() + .arg("root:root:root") + .arg("--verbose") + .arg(file1) + .fails() + .stderr_contains(&"invalid group"); + + scene + .ucmd() + .arg("root.root.root") + .arg("--verbose") + .arg(file1) + .fails() + .stderr_contains(&"invalid group"); + // TODO: on macos group name is not recognized correctly: "chown: invalid group: 'root:root' #[cfg(any(windows, all(unix, not(target_os = "macos"))))] scene @@ -242,8 +276,72 @@ fn test_chown_owner_group() { } #[test] -// TODO: on macos group name is not recognized correctly: "chown: invalid group: ':groupname' -#[cfg(any(windows, all(unix, not(target_os = "macos"))))] +// FixME: Fails on freebsd because of chown: invalid group: 'root:root' +#[cfg(not(target_os = "freebsd"))] +fn test_chown_various_input() { + // test chown username:group file.txt + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let result = scene.cmd("whoami").run(); + if skipping_test_is_okay(&result, "whoami: cannot find name for user ID") { + return; + } + + let user_name = String::from(result.stdout_str().trim()); + assert!(!user_name.is_empty()); + + let file1 = "test_chown_file1"; + at.touch(file1); + + let result = scene.cmd("id").arg("-gn").run(); + if skipping_test_is_okay(&result, "id: cannot find name for group ID") { + return; + } + let group_name = String::from(result.stdout_str().trim()); + assert!(!group_name.is_empty()); + + let result = scene + .ucmd() + .arg(format!("{}:{}", user_name, group_name)) + .arg("--verbose") + .arg(file1) + .run(); + if skipping_test_is_okay(&result, "chown: invalid group:") { + return; + } + result.stderr_contains(&"retained as"); + + // check that username.groupname is understood + let result = scene + .ucmd() + .arg(format!("{}.{}", user_name, group_name)) + .arg("--verbose") + .arg(file1) + .run(); + if skipping_test_is_okay(&result, "chown: invalid group:") { + return; + } + result.stderr_contains(&"retained as"); + + // Fails as user.name doesn't exist in the CI + // but it is valid + scene + .ucmd() + .arg(format!("{}:{}", "user.name", "groupname")) + .arg("--verbose") + .arg(file1) + .fails() + .stderr_contains(&"chown: invalid user: 'user.name:groupname'"); +} + +#[test] +// FixME: on macos & freebsd group name is not recognized correctly: "chown: invalid group: ':groupname' +#[cfg(any( + windows, + all(unix, not(any(target_os = "macos", target_os = "freebsd"))) +))] fn test_chown_only_group() { // test chown :group file.txt @@ -321,6 +419,8 @@ fn test_chown_only_user_id() { } #[test] +// FixME: stderr = chown: ownership of 'test_chown_file1' retained as cuuser:wheel +#[cfg(not(target_os = "freebsd"))] fn test_chown_only_group_id() { // test chown :1111 file.txt @@ -398,6 +498,19 @@ fn test_chown_owner_group_id() { } result.stderr_contains(&"retained as"); + let result = scene + .ucmd() + .arg(format!("{}.{}", user_id, group_id)) + .arg("--verbose") + .arg(file1) + .run(); + if skipping_test_is_okay(&result, "invalid user") { + // From the Logs: "Build (ubuntu-18.04, x86_64-unknown-linux-gnu, feat_os_unix, use-cross)" + // stderr: "chown: invalid user: '1001.116' + return; + } + result.stderr_contains(&"retained as"); + scene .ucmd() .arg("0:0") @@ -408,6 +521,8 @@ fn test_chown_owner_group_id() { } #[test] +// FixME: Fails on freebsd because of chown: invalid group: '0:root' +#[cfg(not(target_os = "freebsd"))] fn test_chown_owner_group_mix() { // test chown 1111:group file.txt diff --git a/tests/by-util/test_chroot.rs b/tests/by-util/test_chroot.rs index 3bac07d44..65d821d01 100644 --- a/tests/by-util/test_chroot.rs +++ b/tests/by-util/test_chroot.rs @@ -23,7 +23,7 @@ fn test_enter_chroot_fails() { assert!(result .stderr_str() - .starts_with("chroot: cannot chroot to jail: Operation not permitted (os error 1)")); + .starts_with("chroot: cannot chroot to 'jail': Operation not permitted (os error 1)")); } #[test] @@ -34,7 +34,7 @@ fn test_no_such_directory() { ucmd.arg("a") .fails() - .stderr_is("chroot: cannot change root directory to `a`: no such directory"); + .stderr_is("chroot: cannot change root directory to 'a': no such directory"); } #[test] diff --git a/tests/by-util/test_cksum.rs b/tests/by-util/test_cksum.rs index 9590c1ac5..bf31ceb18 100644 --- a/tests/by-util/test_cksum.rs +++ b/tests/by-util/test_cksum.rs @@ -68,7 +68,7 @@ fn test_invalid_file() { .arg(folder_name) .fails() .no_stdout() - .stderr_contains("cksum: 'asdf' No such file or directory"); + .stderr_contains("cksum: asdf: No such file or directory"); // Then check when the file is of an invalid type at.mkdir(folder_name); @@ -76,7 +76,7 @@ fn test_invalid_file() { .arg(folder_name) .fails() .no_stdout() - .stderr_contains("cksum: 'asdf' Is a directory"); + .stderr_contains("cksum: asdf: Is a directory"); } // Make sure crc is correct for files larger than 32 bytes diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index 541e6b5d9..e86f35833 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -7,7 +7,9 @@ use std::fs::set_permissions; #[cfg(not(windows))] use std::os::unix::fs; -#[cfg(target_os = "linux")] +#[cfg(unix)] +use std::os::unix::fs::symlink as symlink_file; +#[cfg(all(unix, not(target_os = "freebsd")))] use std::os::unix::fs::PermissionsExt; #[cfg(windows)] use std::os::windows::fs::symlink_file; @@ -561,14 +563,17 @@ fn test_cp_backup_off() { #[test] fn test_cp_backup_no_clobber_conflicting_options() { - let (_, mut ucmd) = at_and_ucmd!(); - - ucmd.arg("--backup") + let ts = TestScenario::new(util_name!()); + ts.ucmd() + .arg("--backup") .arg("--no-clobber") .arg(TEST_HELLO_WORLD_SOURCE) .arg(TEST_HOW_ARE_YOU_SOURCE) - .fails() - .stderr_is("cp: options --backup and --no-clobber are mutually exclusive\nTry 'cp --help' for more information."); + .fails().stderr_is(&format!( + "{0}: options --backup and --no-clobber are mutually exclusive\nTry '{1} {0} --help' for more information.", + ts.util_name, + ts.bin_path.to_string_lossy() + )); } #[test] @@ -1302,3 +1307,64 @@ fn test_copy_symlink_force() { .succeeds(); assert_eq!(at.resolve_link("copy"), "file"); } + +#[test] +#[cfg(all(unix, not(target_os = "freebsd")))] +fn test_no_preserve_mode() { + use std::os::unix::prelude::MetadataExt; + + use uucore::mode::get_umask; + + const PERMS_ALL: u32 = 0o7777; + + let (at, mut ucmd) = at_and_ucmd!(); + at.touch("file"); + set_permissions(at.plus("file"), PermissionsExt::from_mode(PERMS_ALL)).unwrap(); + ucmd.arg("file") + .arg("dest") + .succeeds() + .no_stderr() + .no_stdout(); + let umask = get_umask(); + // remove sticky bit, setuid and setgid bit; apply umask + let expected_perms = PERMS_ALL & !0o7000 & !umask; + assert_eq!( + at.plus("dest").metadata().unwrap().mode() & 0o7777, + expected_perms + ); +} + +#[test] +#[cfg(all(unix, not(target_os = "freebsd")))] +fn test_preserve_mode() { + use std::os::unix::prelude::MetadataExt; + + const PERMS_ALL: u32 = 0o7777; + + let (at, mut ucmd) = at_and_ucmd!(); + at.touch("file"); + set_permissions(at.plus("file"), PermissionsExt::from_mode(PERMS_ALL)).unwrap(); + ucmd.arg("file") + .arg("dest") + .arg("-p") + .succeeds() + .no_stderr() + .no_stdout(); + assert_eq!( + at.plus("dest").metadata().unwrap().mode() & 0o7777, + PERMS_ALL + ); +} + +#[test] +fn test_canonicalize_symlink() { + let (at, mut ucmd) = at_and_ucmd!(); + at.mkdir("dir"); + at.touch("dir/file"); + symlink_file("../dir/file", at.plus("dir/file-ln")).unwrap(); + ucmd.arg("dir/file-ln") + .arg(".") + .succeeds() + .no_stderr() + .no_stdout(); +} diff --git a/tests/by-util/test_hashsum.rs b/tests/by-util/test_hashsum.rs index f059e53f3..545b4ee78 100644 --- a/tests/by-util/test_hashsum.rs +++ b/tests/by-util/test_hashsum.rs @@ -1,3 +1,4 @@ +// spell-checker:ignore checkfile macro_rules! get_hash( ($str:expr) => ( $str.split(' ').collect::>()[0] @@ -12,6 +13,7 @@ macro_rules! test_digest { static DIGEST_ARG: &'static str = concat!("--", stringify!($t)); static BITS_ARG: &'static str = concat!("--bits=", stringify!($size)); static EXPECTED_FILE: &'static str = concat!(stringify!($id), ".expected"); + static CHECK_FILE: &'static str = concat!(stringify!($id), ".checkfile"); #[test] fn test_single_file() { @@ -26,6 +28,40 @@ macro_rules! test_digest { assert_eq!(ts.fixtures.read(EXPECTED_FILE), get_hash!(ts.ucmd().arg(DIGEST_ARG).arg(BITS_ARG).pipe_in_fixture("input.txt").succeeds().no_stderr().stdout_str())); } + + #[test] + fn test_check() { + let ts = TestScenario::new("hashsum"); + ts.ucmd() + .args(&[DIGEST_ARG, BITS_ARG, "--check", CHECK_FILE]) + .succeeds() + .no_stderr() + .stdout_is("input.txt: OK\n"); + } + + #[cfg(windows)] + #[test] + fn test_text_mode() { + // TODO Replace this with hard-coded files that store the + // expected output of text mode on an input file that has + // "\r\n" line endings. + let result = new_ucmd!() + .args(&[DIGEST_ARG, BITS_ARG, "-b"]) + .pipe_in("a\nb\nc\n") + .succeeds(); + let expected = result.no_stderr().stdout(); + // Replace the "*-\n" at the end of the output with " -\n". + // The asterisk indicates that the digest was computed in + // binary mode. + let n = expected.len(); + let expected = [&expected[..n - 3], &[b' ', b'-', b'\n']].concat(); + new_ucmd!() + .args(&[DIGEST_ARG, BITS_ARG, "-t"]) + .pipe_in("a\r\nb\r\nc\r\n") + .succeeds() + .no_stderr() + .stdout_is(std::str::from_utf8(&expected).unwrap()); + } } )*) } diff --git a/tests/by-util/test_head.rs b/tests/by-util/test_head.rs old mode 100755 new mode 100644 diff --git a/tests/by-util/test_hostname.rs b/tests/by-util/test_hostname.rs index 3fcb1ae8b..45acff1b5 100644 --- a/tests/by-util/test_hostname.rs +++ b/tests/by-util/test_hostname.rs @@ -10,8 +10,8 @@ fn test_hostname() { assert!(ls_default_res.stdout().len() >= ls_domain_res.stdout().len()); } -// FixME: fails for "MacOS" -#[cfg(not(target_vendor = "apple"))] +// FixME: fails for "MacOS" and "freebsd" "failed to lookup address information: Name does not resolve" +#[cfg(not(any(target_os = "macos", target_os = "freebsd")))] #[test] fn test_hostname_ip() { let result = new_ucmd!().arg("-i").succeeds(); diff --git a/tests/by-util/test_install.rs b/tests/by-util/test_install.rs index 06808db6b..339a40454 100644 --- a/tests/by-util/test_install.rs +++ b/tests/by-util/test_install.rs @@ -4,7 +4,7 @@ use crate::common::util::*; use filetime::FileTime; use rust_users::*; use std::os::unix::fs::PermissionsExt; -#[cfg(not(windows))] +#[cfg(not(any(windows, target_os = "freebsd")))] use std::process::Command; #[cfg(target_os = "linux")] use std::thread::sleep; @@ -551,7 +551,9 @@ fn test_install_copy_then_compare_file_with_extra_mode() { } const STRIP_TARGET_FILE: &str = "helloworld_installed"; +#[cfg(not(any(windows, target_os = "freebsd")))] const SYMBOL_DUMP_PROGRAM: &str = "objdump"; +#[cfg(not(any(windows, target_os = "freebsd")))] const STRIP_SOURCE_FILE_SYMBOL: &str = "main"; fn strip_source_file() -> &'static str { @@ -563,7 +565,8 @@ fn strip_source_file() -> &'static str { } #[test] -#[cfg(not(windows))] +// FixME: Freebsd fails on 'No such file or directory' +#[cfg(not(any(windows, target_os = "freebsd")))] fn test_install_and_strip() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; @@ -586,7 +589,8 @@ fn test_install_and_strip() { } #[test] -#[cfg(not(windows))] +// FixME: Freebsd fails on 'No such file or directory' +#[cfg(not(any(windows, target_os = "freebsd")))] fn test_install_and_strip_with_program() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; diff --git a/tests/by-util/test_join.rs b/tests/by-util/test_join.rs index 1cab8361a..1d92bf8e7 100644 --- a/tests/by-util/test_join.rs +++ b/tests/by-util/test_join.rs @@ -227,6 +227,19 @@ fn autoformat() { .pipe_in("1 x y z\n2 p") .succeeds() .stdout_only("1 x y z a\n2 p b\n"); + + new_ucmd!() + .arg("-") + .arg("fields_2.txt") + .arg("-a") + .arg("1") + .arg("-o") + .arg("auto") + .arg("-e") + .arg(".") + .pipe_in("1 x y z\n2 p\n99 a b\n") + .succeeds() + .stdout_only("1 x y z a\n2 p . . b\n99 a b . .\n"); } #[test] diff --git a/tests/by-util/test_kill.rs b/tests/by-util/test_kill.rs index fe5d4557a..f5166c428 100644 --- a/tests/by-util/test_kill.rs +++ b/tests/by-util/test_kill.rs @@ -104,6 +104,26 @@ fn test_kill_with_signal_number_old_form() { assert_eq!(target.wait_for_signal(), Some(9)); } +#[test] +fn test_kill_with_signal_name_old_form() { + let mut target = Target::new(); + new_ucmd!() + .arg("-KILL") + .arg(format!("{}", target.pid())) + .succeeds(); + assert_eq!(target.wait_for_signal(), Some(libc::SIGKILL)); +} + +#[test] +fn test_kill_with_signal_prefixed_name_old_form() { + let mut target = Target::new(); + new_ucmd!() + .arg("-SIGKILL") + .arg(format!("{}", target.pid())) + .succeeds(); + assert_eq!(target.wait_for_signal(), Some(libc::SIGKILL)); +} + #[test] fn test_kill_with_signal_number_new_form() { let mut target = Target::new(); @@ -125,3 +145,14 @@ fn test_kill_with_signal_name_new_form() { .succeeds(); assert_eq!(target.wait_for_signal(), Some(libc::SIGKILL)); } + +#[test] +fn test_kill_with_signal_prefixed_name_new_form() { + let mut target = Target::new(); + new_ucmd!() + .arg("-s") + .arg("SIGKILL") + .arg(format!("{}", target.pid())) + .succeeds(); + assert_eq!(target.wait_for_signal(), Some(libc::SIGKILL)); +} diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index a3372050a..4082a5015 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -1,4 +1,4 @@ -// spell-checker:ignore (words) READMECAREFULLY birthtime doesntexist oneline somebackup somefile somegroup somehiddenbackup somehiddenfile +// spell-checker:ignore (words) READMECAREFULLY birthtime doesntexist oneline somebackup lrwx somefile somegroup somehiddenbackup somehiddenfile #[cfg(unix)] extern crate unix_socket; @@ -333,6 +333,259 @@ fn test_ls_long() { } } +#[cfg(not(windows))] +#[test] +fn test_ls_long_format() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.mkdir(&at.plus_as_string("test-long-dir")); + at.touch(&at.plus_as_string("test-long-dir/test-long-file")); + at.mkdir(&at.plus_as_string("test-long-dir/test-long-dir")); + + for arg in &["-l", "--long", "--format=long", "--format=verbose"] { + // Assuming sane username do not have spaces within them. + // A line of the output should be: + // One of the characters -bcCdDlMnpPsStTx? + // rwx, with - for missing permissions, thrice. + // A number, preceded by column whitespace, and followed by a single space. + // A username, currently [^ ], followed by column whitespace, twice (or thrice for Hurd). + // A number, followed by a single space. + // A month, followed by a single space. + // A day, preceded by column whitespace, and followed by a single space. + // Either a year or a time, currently [0-9:]+, preceded by column whitespace, + // and followed by a single space. + // Whatever comes after is irrelevant to this specific test. + scene.ucmd().arg(arg).arg("test-long-dir").succeeds().stdout_matches(&Regex::new( + r"\n[-bcCdDlMnpPsStTx?]([r-][w-][xt-]){3} +\d+ [^ ]+ +[^ ]+( +[^ ]+)? +\d+ [A-Z][a-z]{2} {0,2}\d{0,2} {0,2}[0-9:]+ " + ).unwrap()); + } + + // This checks for the line with the .. entry. The uname and group should be digits. + scene.ucmd().arg("-lan").arg("test-long-dir").succeeds().stdout_matches(&Regex::new( + r"\nd([r-][w-][xt-]){3} +\d+ \d+ +\d+( +\d+)? +\d+ [A-Z][a-z]{2} {0,2}\d{0,2} {0,2}[0-9:]+ \.\." + ).unwrap()); +} + +/// This test tests `ls -laR --color`. +/// This test is mainly about coloring, but, the recursion, symlink `->` processing, +/// and `.` and `..` being present in `-a` all need to work for the test to pass. +/// This test does not really test anything provided by `-l` but the file names and symlinks. +#[test] +#[cfg(all(feature = "ln", feature = "mkdir", feature = "touch"))] +fn test_ls_long_symlink_color() { + // If you break this test after breaking mkdir, touch, or ln, do not be alarmed! + // This test is made for ls, but it attempts to run those utils in the process. + + // Having Some([2, 0]) in a color basically means that "it has the same color as whatever + // is in the 2nd expected output, the 0th color", where the 0th color is the name color, and + // the 1st color is the target color, in a fixed-size array of size 2. + // Basically these are references to be used for indexing the `colors` vector defined below. + type ColorReference = Option<[usize; 2]>; + + // The string between \x1b[ and m + type Color = String; + + // The string between the color start and the color end is the file name itself. + type Name = String; + + let scene = TestScenario::new(util_name!()); + + // . + // ├── dir1 + // │ ├── file1 + // │ ├── dir2 + // │ │ └── dir3 + // │ ├── ln-dir-invalid -> dir1/dir2 + // │ ├── ln-up2 -> ../.. + // │ └── ln-root -> / + // ├── ln-file1 -> dir1/file1 + // ├── ln-file-invalid -> dir1/invalid-target + // └── ln-dir3 -> ./dir1/dir2/dir3 + prepare_folder_structure(&scene); + + // We memoize the colors so we can refer to them later. + // Each entry will be the colors of the link name and link target of a specific output. + let mut colors: Vec<[Color; 2]> = vec![]; + + // The contents of each tuple are the expected colors and names for the link and target. + // We will loop over the ls output and compare to those. + // None values mean that we do not know what color to expect yet, as LS_COLOR might + // be set differently, and as different implementations of ls may use different codes, + // for example, our ls uses `[1;36m` while the GNU ls uses `[01;36m`. + // + // These have been sorting according to default ls sort, and this affects the order of + // discovery of colors, so be very careful when changing directory/file names being created. + let expected_output: [(ColorReference, &str, ColorReference, &str); 6] = [ + // We don't know what colors are what the first time we meet a link. + (None, "ln-dir3", None, "./dir1/dir2/dir3"), + // We have acquired [0, 0], which should be the link color, + // and [0, 1], which should be the dir color, and we can compare to them from now on. + (None, "ln-file-invalid", Some([1, 1]), "dir1/invalid-target"), + // We acquired [1, 1], the non-existent color. + (Some([0, 0]), "ln-file1", None, "dir1/file1"), + (Some([1, 1]), "ln-dir-invalid", Some([1, 1]), "dir1/dir2"), + (Some([0, 0]), "ln-root", Some([0, 1]), "/"), + (Some([0, 0]), "ln-up2", Some([0, 1]), "../.."), + ]; + + // We are only interested in lines or the ls output that are symlinks. These start with "lrwx". + let result = scene.ucmd().arg("-laR").arg("--color").arg(".").succeeds(); + let mut result_lines = result + .stdout_str() + .lines() + .filter(|line| line.starts_with("lrwx")) + .enumerate(); + + // For each enumerated line, we assert that the output of ls matches the expected output. + // + // The unwraps within get_index_name_target will panic if a line starting lrwx does + // not have `colored_name -> target` within it. + while let Some((i, name, target)) = get_index_name_target(&mut result_lines) { + // The unwraps within capture_colored_string will panic if the name/target's color + // format is invalid. + let (matched_name_color, matched_name) = capture_colored_string(&name); + let (matched_target_color, matched_target) = capture_colored_string(&target); + + colors.push([matched_name_color, matched_target_color]); + + // We borrow them again after having moved them. This unwrap will never panic. + let [matched_name_color, matched_target_color] = colors.last().unwrap(); + + // We look up the Colors that are expected in `colors` using the ColorReferences + // stored in `expected_output`. + let expected_name_color = expected_output[i] + .0 + .map(|color_reference| colors[color_reference[0]][color_reference[1]].as_str()); + let expected_target_color = expected_output[i] + .2 + .map(|color_reference| colors[color_reference[0]][color_reference[1]].as_str()); + + // This is the important part. The asserts inside assert_names_and_colors_are_equal + // will panic if the colors or names do not match the expected colors or names. + // Keep in mind an expected color `Option<&str>` of None can mean either that we + // don't expect any color here, as in `expected_output[2], or don't know what specific + // color to expect yet, as in expected_output[0:1]. + assert_names_and_colors_are_equal( + matched_name_color, + expected_name_color, + &matched_name, + expected_output[i].1, + matched_target_color, + expected_target_color, + &matched_target, + expected_output[i].3, + ); + } + + // End of test, only definitions of the helper functions used above follows... + + fn get_index_name_target<'a, I>(lines: &mut I) -> Option<(usize, Name, Name)> + where + I: Iterator, + { + match lines.next() { + Some((c, s)) => { + // `name` is whatever comes between \x1b (inclusive) and the arrow. + let name = String::from("\x1b") + + s.split(" -> ") + .next() + .unwrap() + .split(" \x1b") + .last() + .unwrap(); + // `target` is whatever comes after the arrow. + let target = s.split(" -> ").last().unwrap().to_string(); + Some((c, name, target)) + } + None => None, + } + } + + #[allow(clippy::too_many_arguments)] + fn assert_names_and_colors_are_equal( + name_color: &str, + expected_name_color: Option<&str>, + name: &str, + expected_name: &str, + target_color: &str, + expected_target_color: Option<&str>, + target: &str, + expected_target: &str, + ) { + // Names are always compared. + assert_eq!(&name, &expected_name); + assert_eq!(&target, &expected_target); + + // Colors are only compared when we have inferred what color we are looking for. + if expected_name_color.is_some() { + assert_eq!(&name_color, &expected_name_color.unwrap()); + } + if expected_target_color.is_some() { + assert_eq!(&target_color, &expected_target_color.unwrap()); + } + } + + fn capture_colored_string(input: &str) -> (Color, Name) { + let colored_name = Regex::new(r"\x1b\[([0-9;]+)m(.+)\x1b\[0m").unwrap(); + match colored_name.captures(input) { + Some(captures) => ( + captures.get(1).unwrap().as_str().to_string(), + captures.get(2).unwrap().as_str().to_string(), + ), + None => ("".to_string(), input.to_string()), + } + } + + fn prepare_folder_structure(scene: &TestScenario) { + // There is no way to change directory in the CI, so this is the best we can do. + // Also, keep in mind that windows might require privilege to symlink directories. + // + // We use scene.ccmd instead of scene.fixtures because we care about relative symlinks. + // So we're going to try out the built mkdir, touch, and ln here, and we expect them to succeed. + scene.ccmd("mkdir").arg("dir1").succeeds(); + scene.ccmd("mkdir").arg("dir1/dir2").succeeds(); + scene.ccmd("mkdir").arg("dir1/dir2/dir3").succeeds(); + scene.ccmd("touch").arg("dir1/file1").succeeds(); + + scene + .ccmd("ln") + .arg("-s") + .arg("dir1/dir2") + .arg("dir1/ln-dir-invalid") + .succeeds(); + scene + .ccmd("ln") + .arg("-s") + .arg("./dir1/dir2/dir3") + .arg("ln-dir3") + .succeeds(); + scene + .ccmd("ln") + .arg("-s") + .arg("../..") + .arg("dir1/ln-up2") + .succeeds(); + scene + .ccmd("ln") + .arg("-s") + .arg("/") + .arg("dir1/ln-root") + .succeeds(); + scene + .ccmd("ln") + .arg("-s") + .arg("dir1/file1") + .arg("ln-file1") + .succeeds(); + scene + .ccmd("ln") + .arg("-s") + .arg("dir1/invalid-target") + .arg("ln-file-invalid") + .succeeds(); + } +} + #[test] fn test_ls_long_total_size() { let scene = TestScenario::new(util_name!()); @@ -1361,6 +1614,7 @@ fn test_ls_quoting_style() { // Default is shell-escape scene .ucmd() + .arg("--hide-control-chars") .arg("one\ntwo") .succeeds() .stdout_only("'one'$'\\n''two'\n"); @@ -1382,23 +1636,8 @@ fn test_ls_quoting_style() { ] { scene .ucmd() - .arg(arg) - .arg("one\ntwo") - .succeeds() - .stdout_only(format!("{}\n", correct)); - } - - for (arg, correct) in &[ - ("--quoting-style=literal", "one?two"), - ("-N", "one?two"), - ("--literal", "one?two"), - ("--quoting-style=shell", "one?two"), - ("--quoting-style=shell-always", "'one?two'"), - ] { - scene - .ucmd() - .arg(arg) .arg("--hide-control-chars") + .arg(arg) .arg("one\ntwo") .succeeds() .stdout_only(format!("{}\n", correct)); @@ -1408,7 +1647,7 @@ fn test_ls_quoting_style() { ("--quoting-style=literal", "one\ntwo"), ("-N", "one\ntwo"), ("--literal", "one\ntwo"), - ("--quoting-style=shell", "one\ntwo"), + ("--quoting-style=shell", "one\ntwo"), // FIXME: GNU ls quotes this case ("--quoting-style=shell-always", "'one\ntwo'"), ] { scene @@ -1435,6 +1674,7 @@ fn test_ls_quoting_style() { ] { scene .ucmd() + .arg("--hide-control-chars") .arg(arg) .arg("one\\two") .succeeds() @@ -1450,6 +1690,7 @@ fn test_ls_quoting_style() { ] { scene .ucmd() + .arg("--hide-control-chars") .arg(arg) .arg("one\n&two") .succeeds() @@ -1480,6 +1721,7 @@ fn test_ls_quoting_style() { ] { scene .ucmd() + .arg("--hide-control-chars") .arg(arg) .arg("one two") .succeeds() @@ -1503,6 +1745,7 @@ fn test_ls_quoting_style() { ] { scene .ucmd() + .arg("--hide-control-chars") .arg(arg) .arg("one") .succeeds() diff --git a/tests/by-util/test_more.rs b/tests/by-util/test_more.rs index 9b28ee24e..4b2719d8f 100644 --- a/tests/by-util/test_more.rs +++ b/tests/by-util/test_more.rs @@ -15,11 +15,15 @@ fn test_more_dir_arg() { // Maybe we could capture the error, i.e. "Device not found" in that case // but I am leaving this for later if atty::is(atty::Stream::Stdout) { - let result = new_ucmd!().arg(".").run(); + let ts = TestScenario::new(util_name!()); + let result = ts.ucmd().arg(".").run(); result.failure(); - const EXPECTED_ERROR_MESSAGE: &str = - "more: '.' is a directory.\nTry 'more --help' for more information."; - assert_eq!(result.stderr_str().trim(), EXPECTED_ERROR_MESSAGE); + let expected_error_message = &format!( + "{0}: '.' is a directory.\nTry '{1} {0} --help' for more information.", + ts.util_name, + ts.bin_path.to_string_lossy() + ); + assert_eq!(result.stderr_str().trim(), expected_error_message); } else { } } diff --git a/tests/by-util/test_mv.rs b/tests/by-util/test_mv.rs index 02c65f68d..8d9b00664 100644 --- a/tests/by-util/test_mv.rs +++ b/tests/by-util/test_mv.rs @@ -522,14 +522,17 @@ fn test_mv_backup_off() { #[test] fn test_mv_backup_no_clobber_conflicting_options() { - let (_, mut ucmd) = at_and_ucmd!(); + let ts = TestScenario::new(util_name!()); - ucmd.arg("--backup") + ts.ucmd().arg("--backup") .arg("--no-clobber") .arg("file1") .arg("file2") .fails() - .stderr_is("mv: options --backup and --no-clobber are mutually exclusive\nTry 'mv --help' for more information."); + .stderr_is(&format!("{0}: options --backup and --no-clobber are mutually exclusive\nTry '{1} {0} --help' for more information.", + ts.util_name, + ts.bin_path.to_string_lossy() + )); } #[test] diff --git a/tests/by-util/test_nice.rs b/tests/by-util/test_nice.rs index 25886de78..7a99a333d 100644 --- a/tests/by-util/test_nice.rs +++ b/tests/by-util/test_nice.rs @@ -22,10 +22,15 @@ fn test_negative_adjustment() { #[test] fn test_adjustment_with_no_command_should_error() { - new_ucmd!() + let ts = TestScenario::new(util_name!()); + + ts.ucmd() .args(&["-n", "19"]) .run() - .stderr_is("nice: A command must be given with an adjustment.\nTry \"nice --help\" for more information.\n"); + .stderr_is(&format!("{0}: A command must be given with an adjustment.\nTry '{1} {0} --help' for more information.\n", + ts.util_name, + ts.bin_path.to_string_lossy() + )); } #[test] diff --git a/tests/by-util/test_pwd.rs b/tests/by-util/test_pwd.rs index 2779b9e62..bc08ddbb0 100644 --- a/tests/by-util/test_pwd.rs +++ b/tests/by-util/test_pwd.rs @@ -1,9 +1,13 @@ +// spell-checker:ignore (words) symdir somefakedir + +use std::path::PathBuf; + use crate::common::util::*; #[test] fn test_default() { let (at, mut ucmd) = at_and_ucmd!(); - ucmd.run().stdout_is(at.root_dir_resolved() + "\n"); + ucmd.succeeds().stdout_is(at.root_dir_resolved() + "\n"); } #[test] @@ -11,3 +15,118 @@ fn test_failed() { let (_at, mut ucmd) = at_and_ucmd!(); ucmd.arg("will-fail").fails(); } + +#[cfg(unix)] +#[test] +fn test_deleted_dir() { + use std::process::Command; + + let ts = TestScenario::new(util_name!()); + let at = ts.fixtures.clone(); + let output = Command::new("sh") + .arg("-c") + .arg(format!( + "cd '{}'; mkdir foo; cd foo; rmdir ../foo; exec {} {}", + at.root_dir_resolved(), + ts.bin_path.to_str().unwrap(), + ts.util_name, + )) + .output() + .unwrap(); + assert!(!output.status.success()); + assert!(output.stdout.is_empty()); + assert_eq!( + output.stderr, + b"pwd: failed to get current directory: No such file or directory\n" + ); +} + +struct Env { + ucmd: UCommand, + #[cfg(not(windows))] + root: String, + subdir: String, + symdir: String, +} + +fn symlinked_env() -> Env { + let (at, mut ucmd) = at_and_ucmd!(); + at.mkdir("subdir"); + // Note: on Windows this requires admin permissions + at.symlink_dir("subdir", "symdir"); + let root = PathBuf::from(at.root_dir_resolved()); + ucmd.raw.current_dir(root.join("symdir")); + #[cfg(not(windows))] + ucmd.env("PWD", root.join("symdir")); + Env { + ucmd, + #[cfg(not(windows))] + root: root.to_string_lossy().into_owned(), + subdir: root.join("subdir").to_string_lossy().into_owned(), + symdir: root.join("symdir").to_string_lossy().into_owned(), + } +} + +#[test] +fn test_symlinked_logical() { + let mut env = symlinked_env(); + env.ucmd.arg("-L").succeeds().stdout_is(env.symdir + "\n"); +} + +#[test] +fn test_symlinked_physical() { + let mut env = symlinked_env(); + env.ucmd.arg("-P").succeeds().stdout_is(env.subdir + "\n"); +} + +#[test] +fn test_symlinked_default() { + let mut env = symlinked_env(); + env.ucmd.succeeds().stdout_is(env.subdir + "\n"); +} + +#[cfg(not(windows))] +pub mod untrustworthy_pwd_var { + use std::path::Path; + + use super::*; + + #[test] + fn test_nonexistent_logical() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.arg("-L") + .env("PWD", "/somefakedir") + .succeeds() + .stdout_is(at.root_dir_resolved() + "\n"); + } + + #[test] + fn test_wrong_logical() { + let mut env = symlinked_env(); + env.ucmd + .arg("-L") + .env("PWD", env.root) + .succeeds() + .stdout_is(env.subdir + "\n"); + } + + #[test] + fn test_redundant_logical() { + let mut env = symlinked_env(); + env.ucmd + .arg("-L") + .env("PWD", Path::new(&env.symdir).join(".")) + .succeeds() + .stdout_is(env.subdir + "\n"); + } + + #[test] + fn test_relative_logical() { + let mut env = symlinked_env(); + env.ucmd + .arg("-L") + .env("PWD", ".") + .succeeds() + .stdout_is(env.subdir + "\n"); + } +} diff --git a/tests/by-util/test_readlink.rs b/tests/by-util/test_readlink.rs index 51aebbed2..25f29004f 100644 --- a/tests/by-util/test_readlink.rs +++ b/tests/by-util/test_readlink.rs @@ -2,6 +2,17 @@ use crate::common::util::*; static GIBBERISH: &str = "supercalifragilisticexpialidocious"; +#[test] +fn test_resolve() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("foo"); + at.symlink_file("foo", "bar"); + + scene.ucmd().arg("bar").succeeds().stdout_contains("foo\n"); +} + #[test] fn test_canonicalize() { let (at, mut ucmd) = at_and_ucmd!(); diff --git a/tests/by-util/test_realpath.rs b/tests/by-util/test_realpath.rs index e1384ac74..72bf5b6ea 100644 --- a/tests/by-util/test_realpath.rs +++ b/tests/by-util/test_realpath.rs @@ -1,5 +1,9 @@ use crate::common::util::*; +use std::path::Path; + +static GIBBERISH: &str = "supercalifragilisticexpialidocious"; + #[test] fn test_realpath_current_directory() { let (at, mut ucmd) = at_and_ucmd!(); @@ -106,3 +110,95 @@ fn test_realpath_file_and_links_strip_zero() { .succeeds() .stdout_contains("bar\u{0}"); } + +#[test] +fn test_realpath_physical_mode() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.mkdir("dir1"); + at.mkdir_all("dir2/bar"); + at.symlink_dir("dir2/bar", "dir1/foo"); + + scene + .ucmd() + .arg("dir1/foo/..") + .succeeds() + .stdout_contains("dir2\n"); +} + +#[test] +fn test_realpath_logical_mode() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.mkdir("dir1"); + at.mkdir("dir2"); + at.symlink_dir("dir2", "dir1/foo"); + + scene + .ucmd() + .arg("-L") + .arg("dir1/foo/..") + .succeeds() + .stdout_contains("dir1\n"); +} + +#[test] +fn test_realpath_dangling() { + let (at, mut ucmd) = at_and_ucmd!(); + at.symlink_file("nonexistent-file", "link"); + ucmd.arg("link") + .succeeds() + .stdout_only(at.plus_as_string("nonexistent-file\n")); +} + +#[test] +fn test_realpath_loop() { + let (at, mut ucmd) = at_and_ucmd!(); + at.symlink_file("2", "1"); + at.symlink_file("3", "2"); + at.symlink_file("1", "3"); + ucmd.arg("1") + .succeeds() + .stdout_only(at.plus_as_string("2\n")); +} + +#[test] +fn test_realpath_default_allows_final_non_existent() { + let p = Path::new("").join(GIBBERISH); + let (at, mut ucmd) = at_and_ucmd!(); + let expect = path_concat!(at.root_dir_resolved(), p.to_str().unwrap()) + "\n"; + ucmd.arg(p.as_os_str()).succeeds().stdout_only(expect); +} + +#[test] +fn test_realpath_default_forbids_non_final_non_existent() { + let p = Path::new("").join(GIBBERISH).join(GIBBERISH); + new_ucmd!().arg(p.to_str().unwrap()).fails(); +} + +#[test] +fn test_realpath_existing() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.arg("-e") + .arg(".") + .succeeds() + .stdout_only(at.plus_as_string(&format!("{}\n", at.root_dir_resolved()))); +} + +#[test] +fn test_realpath_existing_error() { + new_ucmd!().arg("-e").arg(GIBBERISH).fails(); +} + +#[test] +fn test_realpath_missing() { + let p = Path::new("").join(GIBBERISH).join(GIBBERISH); + let (at, mut ucmd) = at_and_ucmd!(); + let expect = path_concat!(at.root_dir_resolved(), p.to_str().unwrap()) + "\n"; + ucmd.arg("-m") + .arg(p.as_os_str()) + .succeeds() + .stdout_only(expect); +} diff --git a/tests/by-util/test_rm.rs b/tests/by-util/test_rm.rs index 0592be244..32bc43837 100644 --- a/tests/by-util/test_rm.rs +++ b/tests/by-util/test_rm.rs @@ -255,10 +255,12 @@ fn test_rm_force_no_operand() { #[test] fn test_rm_no_operand() { - let mut ucmd = new_ucmd!(); - - ucmd.fails() - .stderr_is("rm: missing an argument\nrm: for help, try 'rm --help'\n"); + let ts = TestScenario::new(util_name!()); + ts.ucmd().fails().stderr_is(&format!( + "{0}: missing an argument\n{0}: for help, try '{1} {0} --help'\n", + ts.util_name, + ts.bin_path.to_string_lossy() + )); } #[test] diff --git a/tests/by-util/test_rmdir.rs b/tests/by-util/test_rmdir.rs index 4b74b2522..c8f22aa6c 100644 --- a/tests/by-util/test_rmdir.rs +++ b/tests/by-util/test_rmdir.rs @@ -1,126 +1,238 @@ use crate::common::util::*; +const DIR: &str = "dir"; +const DIR_FILE: &str = "dir/file"; +const NESTED_DIR: &str = "dir/ect/ory"; +const NESTED_DIR_FILE: &str = "dir/ect/ory/file"; + +#[cfg(windows)] +const NOT_FOUND: &str = "The system cannot find the file specified."; +#[cfg(not(windows))] +const NOT_FOUND: &str = "No such file or directory"; + +#[cfg(windows)] +const NOT_EMPTY: &str = "The directory is not empty."; +#[cfg(not(windows))] +const NOT_EMPTY: &str = "Directory not empty"; + +#[cfg(windows)] +const NOT_A_DIRECTORY: &str = "The directory name is invalid."; +#[cfg(not(windows))] +const NOT_A_DIRECTORY: &str = "Not a directory"; + #[test] fn test_rmdir_empty_directory_no_parents() { let (at, mut ucmd) = at_and_ucmd!(); - let dir = "test_rmdir_empty_no_parents"; - at.mkdir(dir); - assert!(at.dir_exists(dir)); + at.mkdir(DIR); - ucmd.arg(dir).succeeds().no_stderr(); + ucmd.arg(DIR).succeeds().no_stderr(); - assert!(!at.dir_exists(dir)); + assert!(!at.dir_exists(DIR)); } #[test] fn test_rmdir_empty_directory_with_parents() { let (at, mut ucmd) = at_and_ucmd!(); - let dir = "test_rmdir_empty/with/parents"; - at.mkdir_all(dir); - assert!(at.dir_exists(dir)); + at.mkdir_all(NESTED_DIR); - ucmd.arg("-p").arg(dir).succeeds().no_stderr(); + ucmd.arg("-p").arg(NESTED_DIR).succeeds().no_stderr(); - assert!(!at.dir_exists(dir)); + assert!(!at.dir_exists(NESTED_DIR)); + assert!(!at.dir_exists(DIR)); } #[test] fn test_rmdir_nonempty_directory_no_parents() { let (at, mut ucmd) = at_and_ucmd!(); - let dir = "test_rmdir_nonempty_no_parents"; - let file = "test_rmdir_nonempty_no_parents/foo"; - at.mkdir(dir); - assert!(at.dir_exists(dir)); + at.mkdir(DIR); + at.touch(DIR_FILE); - at.touch(file); - assert!(at.file_exists(file)); + ucmd.arg(DIR) + .fails() + .stderr_is(format!("rmdir: failed to remove 'dir': {}", NOT_EMPTY)); - ucmd.arg(dir).fails().stderr_is( - "rmdir: failed to remove 'test_rmdir_nonempty_no_parents': Directory not \ - empty\n", - ); - - assert!(at.dir_exists(dir)); + assert!(at.dir_exists(DIR)); } #[test] fn test_rmdir_nonempty_directory_with_parents() { let (at, mut ucmd) = at_and_ucmd!(); - let dir = "test_rmdir_nonempty/with/parents"; - let file = "test_rmdir_nonempty/with/parents/foo"; - at.mkdir_all(dir); - assert!(at.dir_exists(dir)); + at.mkdir_all(NESTED_DIR); + at.touch(NESTED_DIR_FILE); - at.touch(file); - assert!(at.file_exists(file)); + ucmd.arg("-p").arg(NESTED_DIR).fails().stderr_is(format!( + "rmdir: failed to remove 'dir/ect/ory': {}", + NOT_EMPTY + )); - ucmd.arg("-p").arg(dir).fails().stderr_is( - "rmdir: failed to remove 'test_rmdir_nonempty/with/parents': Directory not \ - empty\nrmdir: failed to remove 'test_rmdir_nonempty/with': Directory not \ - empty\nrmdir: failed to remove 'test_rmdir_nonempty': Directory not \ - empty\n", - ); - - assert!(at.dir_exists(dir)); + assert!(at.dir_exists(NESTED_DIR)); } #[test] fn test_rmdir_ignore_nonempty_directory_no_parents() { let (at, mut ucmd) = at_and_ucmd!(); - let dir = "test_rmdir_ignore_nonempty_no_parents"; - let file = "test_rmdir_ignore_nonempty_no_parents/foo"; - at.mkdir(dir); - assert!(at.dir_exists(dir)); - - at.touch(file); - assert!(at.file_exists(file)); + at.mkdir(DIR); + at.touch(DIR_FILE); ucmd.arg("--ignore-fail-on-non-empty") - .arg(dir) + .arg(DIR) .succeeds() .no_stderr(); - assert!(at.dir_exists(dir)); + assert!(at.dir_exists(DIR)); } #[test] fn test_rmdir_ignore_nonempty_directory_with_parents() { let (at, mut ucmd) = at_and_ucmd!(); - let dir = "test_rmdir_ignore_nonempty/with/parents"; - let file = "test_rmdir_ignore_nonempty/with/parents/foo"; - at.mkdir_all(dir); - assert!(at.dir_exists(dir)); - - at.touch(file); - assert!(at.file_exists(file)); + at.mkdir_all(NESTED_DIR); + at.touch(NESTED_DIR_FILE); ucmd.arg("--ignore-fail-on-non-empty") .arg("-p") - .arg(dir) + .arg(NESTED_DIR) .succeeds() .no_stderr(); - assert!(at.dir_exists(dir)); + assert!(at.dir_exists(NESTED_DIR)); } #[test] -fn test_rmdir_remove_symlink_match_gnu_error() { +fn test_rmdir_not_a_directory() { let (at, mut ucmd) = at_and_ucmd!(); - let file = "file"; - let fl = "fl"; - at.touch(file); - assert!(at.file_exists(file)); - at.symlink_file(file, fl); - assert!(at.file_exists(fl)); + at.touch("file"); - ucmd.arg("fl/") + ucmd.arg("--ignore-fail-on-non-empty") + .arg("file") .fails() - .stderr_is("rmdir: failed to remove 'fl/': Not a directory"); + .no_stdout() + .stderr_is(format!( + "rmdir: failed to remove 'file': {}", + NOT_A_DIRECTORY + )); +} + +#[test] +fn test_verbose_single() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.mkdir(DIR); + + ucmd.arg("-v") + .arg(DIR) + .succeeds() + .no_stderr() + .stdout_is("rmdir: removing directory, 'dir'\n"); +} + +#[test] +fn test_verbose_multi() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.mkdir(DIR); + + ucmd.arg("-v") + .arg("does_not_exist") + .arg(DIR) + .fails() + .stdout_is( + "rmdir: removing directory, 'does_not_exist'\n\ + rmdir: removing directory, 'dir'\n", + ) + .stderr_is(format!( + "rmdir: failed to remove 'does_not_exist': {}", + NOT_FOUND + )); +} + +#[test] +fn test_verbose_nested_failure() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.mkdir_all(NESTED_DIR); + at.touch("dir/ect/file"); + + ucmd.arg("-pv") + .arg(NESTED_DIR) + .fails() + .stdout_is( + "rmdir: removing directory, 'dir/ect/ory'\n\ + rmdir: removing directory, 'dir/ect'\n", + ) + .stderr_is(format!("rmdir: failed to remove 'dir/ect': {}", NOT_EMPTY)); +} + +#[cfg(unix)] +#[test] +fn test_rmdir_ignore_nonempty_no_permissions() { + use std::fs; + + let (at, mut ucmd) = at_and_ucmd!(); + + // We make the *parent* dir read-only to prevent deleting the dir in it. + at.mkdir_all("dir/ect/ory"); + at.touch("dir/ect/ory/file"); + let dir_ect = at.plus("dir/ect"); + let mut perms = fs::metadata(&dir_ect).unwrap().permissions(); + perms.set_readonly(true); + fs::set_permissions(&dir_ect, perms.clone()).unwrap(); + + // rmdir should now get a permissions error that it interprets as + // a non-empty error. + ucmd.arg("--ignore-fail-on-non-empty") + .arg("dir/ect/ory") + .succeeds() + .no_stderr(); + + assert!(at.dir_exists("dir/ect/ory")); + + // Politely restore permissions for cleanup + perms.set_readonly(false); + fs::set_permissions(&dir_ect, perms).unwrap(); +} + +#[test] +fn test_rmdir_remove_symlink_file() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.touch("file"); + at.symlink_file("file", "fl"); + + ucmd.arg("fl/").fails().stderr_is(format!( + "rmdir: failed to remove 'fl/': {}", + NOT_A_DIRECTORY + )); +} + +// This behavior is known to happen on Linux but not all Unixes +#[cfg(any(target_os = "linux", target_os = "android"))] +#[test] +fn test_rmdir_remove_symlink_dir() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.mkdir("dir"); + at.symlink_dir("dir", "dl"); + + ucmd.arg("dl/") + .fails() + .stderr_is("rmdir: failed to remove 'dl/': Symbolic link not followed"); +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +#[test] +fn test_rmdir_remove_symlink_dangling() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.symlink_dir("dir", "dl"); + + ucmd.arg("dl/") + .fails() + .stderr_is("rmdir: failed to remove 'dl/': Symbolic link not followed"); } diff --git a/tests/by-util/test_runcon.rs b/tests/by-util/test_runcon.rs new file mode 100644 index 000000000..047ce5769 --- /dev/null +++ b/tests/by-util/test_runcon.rs @@ -0,0 +1,151 @@ +// spell-checker:ignore (jargon) xattributes + +#![cfg(feature = "feat_selinux")] + +use crate::common::util::*; + +// TODO: Check the implementation of `--compute` somehow. + +#[test] +fn version() { + new_ucmd!().arg("--version").succeeds(); + new_ucmd!().arg("-V").succeeds(); +} + +#[test] +fn help() { + new_ucmd!().arg("--help").succeeds(); + new_ucmd!().arg("-h").succeeds(); +} + +#[test] +fn print() { + new_ucmd!().succeeds(); + + for &flag in &["-c", "--compute"] { + new_ucmd!().arg(flag).succeeds(); + } + + for &flag in &[ + "-t", "--type", "-u", "--user", "-r", "--role", "-l", "--range", + ] { + new_ucmd!().args(&[flag, "example"]).succeeds(); + new_ucmd!().args(&[flag, "example1,example2"]).succeeds(); + } +} + +#[test] +fn invalid() { + new_ucmd!().arg("invalid").fails().code_is(1); + + let args = &[ + "unconfined_u:unconfined_r:unconfined_t:s0", + "inexistent-file", + ]; + new_ucmd!().args(args).fails().code_is(127); + + let args = &["invalid", "/bin/true"]; + new_ucmd!().args(args).fails().code_is(1); + + let args = &["--compute", "inexistent-file"]; + new_ucmd!().args(args).fails().code_is(1); + + let args = &["--compute", "--compute"]; + new_ucmd!().args(args).fails().code_is(1); + + // clap has an issue that makes this test fail: https://github.com/clap-rs/clap/issues/1543 + // TODO: Enable this code once the issue is fixed in the clap version we're using. + //new_ucmd!().arg("--compute=example").fails().code_is(1); + + for &flag in &[ + "-t", "--type", "-u", "--user", "-r", "--role", "-l", "--range", + ] { + new_ucmd!().arg(flag).fails().code_is(1); + + let args = &[flag, "example", flag, "example"]; + new_ucmd!().args(args).fails().code_is(1); + } +} + +#[test] +fn plain_context() { + let ctx = "unconfined_u:unconfined_r:unconfined_t:s0-s0"; + new_ucmd!().args(&[ctx, "/bin/true"]).succeeds(); + new_ucmd!().args(&[ctx, "/bin/false"]).fails().code_is(1); + + let output = new_ucmd!().args(&[ctx, "sestatus", "-v"]).succeeds(); + let r = get_sestatus_context(output.stdout()); + assert_eq!(r, "unconfined_u:unconfined_r:unconfined_t:s0"); + + let ctx = "system_u:unconfined_r:unconfined_t:s0-s0"; + new_ucmd!().args(&[ctx, "/bin/true"]).succeeds(); + + let ctx = "system_u:system_r:unconfined_t:s0"; + let output = new_ucmd!().args(&[ctx, "sestatus", "-v"]).succeeds(); + assert_eq!(get_sestatus_context(output.stdout()), ctx); +} + +#[test] +fn custom_context() { + let t_ud = "unconfined_t"; + let u_ud = "unconfined_u"; + let r_ud = "unconfined_r"; + + new_ucmd!().args(&["--compute", "/bin/true"]).succeeds(); + + let args = &["--compute", "/bin/false"]; + new_ucmd!().args(args).fails().code_is(1); + + let args = &["--type", t_ud, "/bin/true"]; + new_ucmd!().args(args).succeeds(); + + let args = &["--compute", "--type", t_ud, "/bin/true"]; + new_ucmd!().args(args).succeeds(); + + let args = &["--user=system_u", "/bin/true"]; + new_ucmd!().args(args).succeeds(); + + let args = &["--compute", "--user=system_u", "/bin/true"]; + new_ucmd!().args(args).succeeds(); + + let args = &["--role=system_r", "/bin/true"]; + new_ucmd!().args(args).succeeds(); + + let args = &["--compute", "--role=system_r", "/bin/true"]; + new_ucmd!().args(args).succeeds(); + + new_ucmd!().args(&["--range=s0", "/bin/true"]).succeeds(); + + let args = &["--compute", "--range=s0", "/bin/true"]; + new_ucmd!().args(args).succeeds(); + + for &(ctx, u, r) in &[ + ("unconfined_u:unconfined_r:unconfined_t:s0", u_ud, r_ud), + ("system_u:unconfined_r:unconfined_t:s0", "system_u", r_ud), + ("unconfined_u:system_r:unconfined_t:s0", u_ud, "system_r"), + ("system_u:system_r:unconfined_t:s0", "system_u", "system_r"), + ] { + let args = &["-t", t_ud, "-u", u, "-r", r, "-l", "s0", "sestatus", "-v"]; + + let output = new_ucmd!().args(args).succeeds(); + assert_eq!(get_sestatus_context(output.stdout()), ctx); + } +} + +fn get_sestatus_context(output: &[u8]) -> &str { + let re = regex::bytes::Regex::new(r#"Current context:\s*(\S+)\s*"#) + .expect("Invalid regular expression"); + + output + .split(|&b| b == b'\n') + .find(|&b| b.starts_with(b"Current context:")) + .and_then(|line| { + re.captures_iter(line) + .next() + .and_then(|c| c.get(1)) + .as_ref() + .map(regex::bytes::Match::as_bytes) + }) + .and_then(|bytes| std::str::from_utf8(bytes).ok()) + .expect("Output of sestatus is unexpected") +} diff --git a/tests/by-util/test_seq.rs b/tests/by-util/test_seq.rs index be04bf1fd..27b5f99bc 100644 --- a/tests/by-util/test_seq.rs +++ b/tests/by-util/test_seq.rs @@ -1,17 +1,76 @@ use crate::common::util::*; +use std::io::Read; #[test] fn test_rejects_nan() { - new_ucmd!().args(&["NaN"]).fails().stderr_only( - "seq: invalid 'not-a-number' argument: 'NaN'\nTry 'seq --help' for more information.", - ); + let ts = TestScenario::new(util_name!()); + + ts.ucmd().args(&["NaN"]).fails().stderr_only(format!( + "{0}: invalid 'not-a-number' argument: 'NaN'\nTry '{1} {0} --help' for more information.", + ts.util_name, + ts.bin_path.to_string_lossy() + )); } #[test] fn test_rejects_non_floats() { - new_ucmd!().args(&["foo"]).fails().stderr_only( - "seq: invalid floating point argument: 'foo'\nTry 'seq --help' for more information.", - ); + let ts = TestScenario::new(util_name!()); + + ts.ucmd().args(&["foo"]).fails().stderr_only(&format!( + "{0}: invalid floating point argument: 'foo'\nTry '{1} {0} --help' for more information.", + ts.util_name, + ts.bin_path.to_string_lossy() + )); +} + +#[test] +fn test_invalid_float() { + new_ucmd!() + .args(&["1e2.3"]) + .fails() + .no_stdout() + .stderr_contains("invalid floating point argument: '1e2.3'") + .stderr_contains("for more information."); + new_ucmd!() + .args(&["1e2.3", "2"]) + .fails() + .no_stdout() + .stderr_contains("invalid floating point argument: '1e2.3'") + .stderr_contains("for more information."); + new_ucmd!() + .args(&["1", "1e2.3"]) + .fails() + .no_stdout() + .stderr_contains("invalid floating point argument: '1e2.3'") + .stderr_contains("for more information."); + new_ucmd!() + .args(&["1e2.3", "2", "3"]) + .fails() + .no_stdout() + .stderr_contains("invalid floating point argument: '1e2.3'") + .stderr_contains("for more information."); + new_ucmd!() + .args(&["1", "1e2.3", "3"]) + .fails() + .no_stdout() + .stderr_contains("invalid floating point argument: '1e2.3'") + .stderr_contains("for more information."); + new_ucmd!() + .args(&["1", "2", "1e2.3"]) + .fails() + .no_stdout() + .stderr_contains("invalid floating point argument: '1e2.3'") + .stderr_contains("for more information."); +} + +#[test] +fn test_width_invalid_float() { + new_ucmd!() + .args(&["-w", "1e2.3"]) + .fails() + .no_stdout() + .stderr_contains("invalid floating point argument: '1e2.3'") + .stderr_contains("for more information."); } // ---- Tests for the big integer based path ---- @@ -132,3 +191,293 @@ fn test_seq_wrong_arg_floats() { fn test_zero_step_floats() { new_ucmd!().args(&["10.0", "0", "32"]).fails(); } + +#[test] +fn test_preserve_negative_zero_start() { + new_ucmd!() + .args(&["-0", "1"]) + .succeeds() + .stdout_is("-0\n1\n") + .no_stderr(); + new_ucmd!() + .args(&["-0", "1", "2"]) + .succeeds() + .stdout_is("-0\n1\n2\n") + .no_stderr(); + new_ucmd!() + .args(&["-0", "1", "2.0"]) + .succeeds() + .stdout_is("-0\n1\n2\n") + .no_stderr(); +} + +#[test] +fn test_drop_negative_zero_end() { + new_ucmd!() + .args(&["1", "-1", "-0"]) + .succeeds() + .stdout_is("1\n0\n") + .no_stderr(); +} + +#[test] +fn test_width_scientific_notation() { + new_ucmd!() + .args(&["-w", "999", "1e3"]) + .succeeds() + .stdout_is("0999\n1000\n") + .no_stderr(); +} + +#[test] +fn test_width_negative_zero() { + new_ucmd!() + .args(&["-w", "-0", "1"]) + .succeeds() + .stdout_is("-0\n01\n") + .no_stderr(); + new_ucmd!() + .args(&["-w", "-0", "1", "2"]) + .succeeds() + .stdout_is("-0\n01\n02\n") + .no_stderr(); + new_ucmd!() + .args(&["-w", "-0", "1", "2.0"]) + .succeeds() + .stdout_is("-0\n01\n02\n") + .no_stderr(); +} + +#[test] +fn test_width_negative_zero_decimal_notation() { + new_ucmd!() + .args(&["-w", "-0.0", "1"]) + .succeeds() + .stdout_is("-0.0\n01.0\n") + .no_stderr(); + new_ucmd!() + .args(&["-w", "-0.0", "1.0"]) + .succeeds() + .stdout_is("-0.0\n01.0\n") + .no_stderr(); + new_ucmd!() + .args(&["-w", "-0.0", "1", "2"]) + .succeeds() + .stdout_is("-0.0\n01.0\n02.0\n") + .no_stderr(); + new_ucmd!() + .args(&["-w", "-0.0", "1", "2.0"]) + .succeeds() + .stdout_is("-0.0\n01.0\n02.0\n") + .no_stderr(); + new_ucmd!() + .args(&["-w", "-0.0", "1.0", "2"]) + .succeeds() + .stdout_is("-0.0\n01.0\n02.0\n") + .no_stderr(); + new_ucmd!() + .args(&["-w", "-0.0", "1.0", "2.0"]) + .succeeds() + .stdout_is("-0.0\n01.0\n02.0\n") + .no_stderr(); +} + +#[test] +fn test_width_negative_zero_scientific_notation() { + new_ucmd!() + .args(&["-w", "-0e0", "1"]) + .succeeds() + .stdout_is("-0\n01\n") + .no_stderr(); + new_ucmd!() + .args(&["-w", "-0e0", "1", "2"]) + .succeeds() + .stdout_is("-0\n01\n02\n") + .no_stderr(); + new_ucmd!() + .args(&["-w", "-0e0", "1", "2.0"]) + .succeeds() + .stdout_is("-0\n01\n02\n") + .no_stderr(); + + new_ucmd!() + .args(&["-w", "-0e+1", "1"]) + .succeeds() + .stdout_is("-00\n001\n") + .no_stderr(); + new_ucmd!() + .args(&["-w", "-0e+1", "1", "2"]) + .succeeds() + .stdout_is("-00\n001\n002\n") + .no_stderr(); + new_ucmd!() + .args(&["-w", "-0e+1", "1", "2.0"]) + .succeeds() + .stdout_is("-00\n001\n002\n") + .no_stderr(); + + new_ucmd!() + .args(&["-w", "-0.000e0", "1"]) + .succeeds() + .stdout_is("-0.000\n01.000\n") + .no_stderr(); + new_ucmd!() + .args(&["-w", "-0.000e0", "1", "2"]) + .succeeds() + .stdout_is("-0.000\n01.000\n02.000\n") + .no_stderr(); + new_ucmd!() + .args(&["-w", "-0.000e0", "1", "2.0"]) + .succeeds() + .stdout_is("-0.000\n01.000\n02.000\n") + .no_stderr(); + + new_ucmd!() + .args(&["-w", "-0.000e-2", "1"]) + .succeeds() + .stdout_is("-0.00000\n01.00000\n") + .no_stderr(); + new_ucmd!() + .args(&["-w", "-0.000e-2", "1", "2"]) + .succeeds() + .stdout_is("-0.00000\n01.00000\n02.00000\n") + .no_stderr(); + new_ucmd!() + .args(&["-w", "-0.000e-2", "1", "2.0"]) + .succeeds() + .stdout_is("-0.00000\n01.00000\n02.00000\n") + .no_stderr(); + + new_ucmd!() + .args(&["-w", "-0.000e5", "1"]) + .succeeds() + .stdout_is("-000000\n0000001\n") + .no_stderr(); + new_ucmd!() + .args(&["-w", "-0.000e5", "1", "2"]) + .succeeds() + .stdout_is("-000000\n0000001\n0000002\n") + .no_stderr(); + new_ucmd!() + .args(&["-w", "-0.000e5", "1", "2.0"]) + .succeeds() + .stdout_is("-000000\n0000001\n0000002\n") + .no_stderr(); + + new_ucmd!() + .args(&["-w", "-0.000e5", "1"]) + .succeeds() + .stdout_is("-000000\n0000001\n") + .no_stderr(); + new_ucmd!() + .args(&["-w", "-0.000e5", "1", "2"]) + .succeeds() + .stdout_is("-000000\n0000001\n0000002\n") + .no_stderr(); + new_ucmd!() + .args(&["-w", "-0.000e5", "1", "2.0"]) + .succeeds() + .stdout_is("-000000\n0000001\n0000002\n") + .no_stderr(); +} + +#[test] +fn test_width_decimal_scientific_notation_increment() { + new_ucmd!() + .args(&["-w", ".1", "1e-2", ".11"]) + .succeeds() + .stdout_is("0.10\n0.11\n") + .no_stderr(); + + new_ucmd!() + .args(&["-w", ".0", "1.500e-1", ".2"]) + .succeeds() + .stdout_is("0.0000\n0.1500\n") + .no_stderr(); +} + +/// Test that trailing zeros in the start argument contribute to precision. +#[test] +fn test_width_decimal_scientific_notation_trailing_zeros_start() { + new_ucmd!() + .args(&["-w", ".1000", "1e-2", ".11"]) + .succeeds() + .stdout_is("0.1000\n0.1100\n") + .no_stderr(); +} + +/// Test that trailing zeros in the increment argument contribute to precision. +#[test] +fn test_width_decimal_scientific_notation_trailing_zeros_increment() { + new_ucmd!() + .args(&["-w", "1e-1", "0.0100", ".11"]) + .succeeds() + .stdout_is("0.1000\n0.1100\n") + .no_stderr(); +} + +/// Test that trailing zeros in the end argument do not contribute to width. +#[test] +fn test_width_decimal_scientific_notation_trailing_zeros_end() { + new_ucmd!() + .args(&["-w", "1e-1", "1e-2", ".1100"]) + .succeeds() + .stdout_is("0.10\n0.11\n") + .no_stderr(); +} + +#[test] +fn test_width_floats() { + new_ucmd!() + .args(&["-w", "9.0", "10.0"]) + .succeeds() + .stdout_is("09.0\n10.0\n") + .no_stderr(); +} + +// TODO This is duplicated from `test_yes.rs`; refactor them. +/// Run `seq`, capture some of the output, close the pipe, and verify it. +fn run(args: &[&str], expected: &[u8]) { + let mut cmd = new_ucmd!(); + let mut child = cmd.args(args).run_no_wait(); + let mut stdout = child.stdout.take().unwrap(); + let mut buf = vec![0; expected.len()]; + stdout.read_exact(&mut buf).unwrap(); + drop(stdout); + assert!(child.wait().unwrap().success()); + assert_eq!(buf.as_slice(), expected); +} + +#[test] +fn test_neg_inf() { + run(&["--", "-inf", "0"], b"-inf\n-inf\n-inf\n"); +} + +#[test] +fn test_inf() { + run(&["inf"], b"1\n2\n3\n"); +} + +#[test] +fn test_ignore_leading_whitespace() { + new_ucmd!() + .arg(" 1") + .succeeds() + .stdout_is("1\n") + .no_stderr(); +} + +#[test] +fn test_trailing_whitespace_error() { + // In some locales, the GNU error message has curly quotes (‘) + // instead of straight quotes ('). We just test the straight single + // quotes. + new_ucmd!() + .arg("1 ") + .fails() + .no_stdout() + .stderr_contains("seq: invalid floating point argument: '1 '") + // FIXME The second line of the error message is "Try 'seq + // --help' for more information." + .stderr_contains("for more information."); +} diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index cfe96d3c5..2aa26ad24 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -531,7 +531,7 @@ fn test_keys_invalid_field() { new_ucmd!() .args(&["-k", "1."]) .fails() - .stderr_only("sort: failed to parse key `1.`: failed to parse character index ``: cannot parse integer from empty string"); + .stderr_only("sort: failed to parse key '1.': failed to parse character index '': cannot parse integer from empty string"); } #[test] @@ -539,7 +539,7 @@ fn test_keys_invalid_field_option() { new_ucmd!() .args(&["-k", "1.1x"]) .fails() - .stderr_only("sort: failed to parse key `1.1x`: invalid option: `x`"); + .stderr_only("sort: failed to parse key '1.1x': invalid option: 'x'"); } #[test] @@ -547,7 +547,7 @@ fn test_keys_invalid_field_zero() { new_ucmd!() .args(&["-k", "0.1"]) .fails() - .stderr_only("sort: failed to parse key `0.1`: field index can not be 0"); + .stderr_only("sort: failed to parse key '0.1': field index can not be 0"); } #[test] @@ -555,7 +555,7 @@ fn test_keys_invalid_char_zero() { new_ucmd!() .args(&["-k", "1.0"]) .fails() - .stderr_only("sort: failed to parse key `1.0`: invalid character index 0 for the start position of a field"); + .stderr_only("sort: failed to parse key '1.0': invalid character index 0 for the start position of a field"); } #[test] diff --git a/tests/by-util/test_stat.rs b/tests/by-util/test_stat.rs index af9e3de45..9bbb1c1ca 100644 --- a/tests/by-util/test_stat.rs +++ b/tests/by-util/test_stat.rs @@ -102,7 +102,7 @@ fn test_invalid_option() { new_ucmd!().arg("-w").arg("-q").arg("/").fails(); } -#[cfg(any(target_os = "linux", target_vendor = "apple"))] +#[cfg(unix)] const NORMAL_FORMAT_STR: &str = "%a %A %b %B %d %D %f %F %g %G %h %i %m %n %o %s %u %U %x %X %y %Y %z %Z"; // avoid "%w %W" (birth/creation) due to `stat` limitations and linux kernel & rust version capability variations #[cfg(any(target_os = "linux"))] diff --git a/tests/by-util/test_stdbuf.rs b/tests/by-util/test_stdbuf.rs index 66892ea0f..c05b65d70 100644 --- a/tests/by-util/test_stdbuf.rs +++ b/tests/by-util/test_stdbuf.rs @@ -25,15 +25,19 @@ fn test_stdbuf_line_buffered_stdout() { #[cfg(not(target_os = "windows"))] #[test] fn test_stdbuf_no_buffer_option_fails() { - new_ucmd!().args(&["head"]).fails().stderr_is( + let ts = TestScenario::new(util_name!()); + + ts.ucmd().args(&["head"]).fails().stderr_is(&format!( "error: The following required arguments were not provided:\n \ --error \n \ --input \n \ --output \n\n\ USAGE:\n \ - stdbuf OPTION... COMMAND\n\n\ + {1} {0} OPTION... COMMAND\n\n\ For more information try --help", - ); + ts.util_name, + ts.bin_path.to_string_lossy() + )); } #[cfg(not(target_os = "windows"))] @@ -49,9 +53,16 @@ fn test_stdbuf_trailing_var_arg() { #[cfg(not(target_os = "windows"))] #[test] fn test_stdbuf_line_buffering_stdin_fails() { - new_ucmd!().args(&["-i", "L", "head"]).fails().stderr_is( - "stdbuf: line buffering stdin is meaningless\nTry 'stdbuf --help' for more information.", - ); + let ts = TestScenario::new(util_name!()); + + ts.ucmd() + .args(&["-i", "L", "head"]) + .fails() + .stderr_is(&format!( + "{0}: line buffering stdin is meaningless\nTry '{1} {0} --help' for more information.", + ts.util_name, + ts.bin_path.to_string_lossy() + )); } #[cfg(not(target_os = "windows"))] diff --git a/tests/by-util/test_sum.rs b/tests/by-util/test_sum.rs index f09ba9d00..0248c05cf 100644 --- a/tests/by-util/test_sum.rs +++ b/tests/by-util/test_sum.rs @@ -59,7 +59,7 @@ fn test_invalid_file() { at.mkdir("a"); - ucmd.arg("a").fails().stderr_is("sum: 'a' Is a directory"); + ucmd.arg("a").fails().stderr_is("sum: a: Is a directory"); } #[test] @@ -68,5 +68,5 @@ fn test_invalid_metadata() { ucmd.arg("b") .fails() - .stderr_is("sum: 'b' No such file or directory"); + .stderr_is("sum: b: No such file or directory"); } diff --git a/tests/by-util/test_tac.rs b/tests/by-util/test_tac.rs index 599bc19c7..323aa5149 100644 --- a/tests/by-util/test_tac.rs +++ b/tests/by-util/test_tac.rs @@ -1,3 +1,4 @@ +// spell-checker:ignore axxbxx bxxaxx axxx axxxx xxaxx xxax xxxxa axyz zyax zyxa use crate::common::util::*; #[test] @@ -23,7 +24,7 @@ fn test_stdin_non_newline_separator_before() { .args(&["-b", "-s", ":"]) .pipe_in("100:200:300:400:500") .run() - .stdout_is("500:400:300:200:100"); + .stdout_is(":500:400:300:200100"); } #[test] @@ -74,6 +75,128 @@ fn test_no_line_separators() { new_ucmd!().pipe_in("a").succeeds().stdout_is("a"); } +#[test] +fn test_before_trailing_separator_no_leading_separator() { + new_ucmd!() + .arg("-b") + .pipe_in("a\nb\n") + .succeeds() + .stdout_is("\n\nba"); +} + +#[test] +fn test_before_trailing_separator_and_leading_separator() { + new_ucmd!() + .arg("-b") + .pipe_in("\na\nb\n") + .succeeds() + .stdout_is("\n\nb\na"); +} + +#[test] +fn test_before_leading_separator_no_trailing_separator() { + new_ucmd!() + .arg("-b") + .pipe_in("\na\nb") + .succeeds() + .stdout_is("\nb\na"); +} + +#[test] +fn test_before_no_separator() { + new_ucmd!() + .arg("-b") + .pipe_in("ab") + .succeeds() + .stdout_is("ab"); +} + +#[test] +fn test_before_empty_file() { + new_ucmd!().arg("-b").pipe_in("").succeeds().stdout_is(""); +} + +#[test] +fn test_multi_char_separator() { + new_ucmd!() + .args(&["-s", "xx"]) + .pipe_in("axxbxx") + .succeeds() + .stdout_is("bxxaxx"); +} + +#[test] +fn test_multi_char_separator_overlap() { + // The right-most pair of "x" characters in the input is treated as + // the only line separator. That is, "axxx" is interpreted as having + // one line comprising the string "ax" followed by the line + // separator "xx". + new_ucmd!() + .args(&["-s", "xx"]) + .pipe_in("axxx") + .succeeds() + .stdout_is("axxx"); + + // Each non-overlapping pair of "x" characters in the input is + // treated as a line separator. That is, "axxxx" is interpreted as + // having two lines: + // + // * the second line is the empty string "" followed by the line + // separator "xx", + // * the first line is the string "a" followed by the line separator + // "xx". + // + // The lines are printed in reverse, resulting in "xx" followed by + // "axx". + new_ucmd!() + .args(&["-s", "xx"]) + .pipe_in("axxxx") + .succeeds() + .stdout_is("xxaxx"); +} + +#[test] +fn test_multi_char_separator_overlap_before() { + // With the "-b" option, the line separator is assumed to be at the + // beginning of the line. In this case, That is, "axxx" is + // interpreted as having two lines: + // + // * the second line is the empty string "" preceded by the line + // separator "xx", + // * the first line is the string "ax" preceded by no line + // separator, since there are no more characters preceding it. + // + // The lines are printed in reverse, resulting in "xx" followed by + // "ax". + new_ucmd!() + .args(&["-b", "-s", "xx"]) + .pipe_in("axxx") + .succeeds() + .stdout_is("xxax"); + + // With the "-b" option, the line separator is assumed to be at the + // beginning of the line. Each non-overlapping pair of "x" + // characters in the input is treated as a line separator. That is, + // "axxxx" is interpreted as having three lines: + // + // * the third line is the empty string "" preceded by the line + // separator "xx" (the last two "x" characters in the input + // string), + // * the second line is the empty string "" preceded by the line + // separator "xx" (the first two "x" characters in the input + // string), + // * the first line is the string "a" preceded by no line separator, + // since there are no more characters preceding it. + // + // The lines are printed in reverse, resulting in "xx" followed by + // "xx" followed by "a". + new_ucmd!() + .args(&["-b", "-s", "xx"]) + .pipe_in("axxxx") + .succeeds() + .stdout_is("xxxxa"); +} + #[test] fn test_null_separator() { new_ucmd!() @@ -82,3 +205,67 @@ fn test_null_separator() { .succeeds() .stdout_is("b\0a\0"); } + +#[test] +fn test_regex() { + new_ucmd!() + .args(&["-r", "-s", "[xyz]+"]) + .pipe_in("axyz") + .succeeds() + .no_stderr() + .stdout_is("zyax"); + + new_ucmd!() + .args(&["-r", "-s", ":+"]) + .pipe_in("a:b::c:::d::::") + .succeeds() + .no_stderr() + .stdout_is(":::d:::c::b:a:"); + + new_ucmd!() + .args(&["-r", "-s", r"[\+]+[-]+[\+]+"]) + // line 0 1 2 + // |--||-----||--------| + .pipe_in("a+-+b++--++c+d-e+---+") + .succeeds() + .no_stderr() + // line 2 1 0 + // |--------||-----||--| + .stdout_is("c+d-e+---+b++--++a+-+"); +} + +#[test] +fn test_regex_before() { + new_ucmd!() + .args(&["-b", "-r", "-s", "[xyz]+"]) + .pipe_in("axyz") + .succeeds() + .no_stderr() + .stdout_is("zyxa"); + + new_ucmd!() + .args(&["-b", "-r", "-s", ":+"]) + .pipe_in(":a::b:::c::::d") + .succeeds() + .stdout_is(":d::::c:::b::a"); + + // Because `tac` searches for matches of the regular expression from + // right to left, the second to last line is + // + // +--++b + // + // not + // + // ++--++b + // + new_ucmd!() + .args(&["-b", "-r", "-s", r"[\+]+[-]+[\+]+"]) + // line 0 1 2 + // |---||----||--------| + .pipe_in("+-+a++--++b+---+c+d-e") + .succeeds() + .no_stderr() + // line 2 1 0 + // |--------||----||---| + .stdout_is("+---+c+d-e+--++b+-+a+"); +} diff --git a/tests/by-util/test_tail.rs b/tests/by-util/test_tail.rs index 28c3580bb..26d8106f0 100644 --- a/tests/by-util/test_tail.rs +++ b/tests/by-util/test_tail.rs @@ -425,3 +425,23 @@ fn test_tail_num_with_undocumented_sign_bytes() { .succeeds() .stdout_is("efghijklmnopqrstuvwxyz"); } + +#[test] +#[cfg(unix)] +fn test_tail_bytes_for_funny_files() { + // gnu/tests/tail-2/tail-c.sh + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + for &file in &["/proc/version", "/sys/kernel/profiling"] { + if !at.file_exists(file) { + continue; + } + let args = ["--bytes", "1", file]; + let result = ts.ucmd().args(&args).run(); + let exp_result = unwrap_or_return!(expected_result(&ts, &args)); + result + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str()) + .code_is(exp_result.code()); + } +} diff --git a/tests/by-util/test_test.rs b/tests/by-util/test_test.rs index 79c24651a..db74265a4 100644 --- a/tests/by-util/test_test.rs +++ b/tests/by-util/test_test.rs @@ -35,6 +35,35 @@ fn test_solo_and_or_or_is_a_literal() { new_ucmd!().arg("-o").succeeds(); } +#[test] +fn test_some_literals() { + let scenario = TestScenario::new(util_name!()); + let tests = [ + "a string", + "(", + ")", + "-", + "--", + "-0", + "-f", + "--help", + "--version", + "-eq", + "-lt", + "-ef", + "[", + ]; + + for test in &tests { + scenario.ucmd().arg(test).succeeds(); + } + + // run the inverse of all these tests + for test in &tests { + scenario.ucmd().arg("!").arg(test).run().status_code(1); + } +} + #[test] fn test_double_not_is_false() { new_ucmd!().args(&["!", "!"]).run().status_code(1); @@ -99,21 +128,6 @@ fn test_zero_len_of_empty() { new_ucmd!().args(&["-z", ""]).succeeds(); } -#[test] -fn test_solo_parenthesis_is_literal() { - let scenario = TestScenario::new(util_name!()); - let tests = [["("], [")"]]; - - for test in &tests { - scenario.ucmd().args(&test[..]).succeeds(); - } -} - -#[test] -fn test_solo_empty_parenthetical_is_error() { - new_ucmd!().args(&["(", ")"]).run().status_code(2); -} - #[test] fn test_zero_len_equals_zero_len() { new_ucmd!().args(&["", "=", ""]).succeeds(); @@ -139,6 +153,7 @@ fn test_string_comparison() { ["contained\nnewline", "=", "contained\nnewline"], ["(", "=", "("], ["(", "!=", ")"], + ["(", "!=", "="], ["!", "=", "!"], ["=", "=", "="], ]; @@ -199,11 +214,13 @@ fn test_a_bunch_of_not() { #[test] fn test_pseudofloat_equal() { + // string comparison; test(1) doesn't support comparison of actual floats new_ucmd!().args(&["123.45", "=", "123.45"]).succeeds(); } #[test] fn test_pseudofloat_not_equal() { + // string comparison; test(1) doesn't support comparison of actual floats new_ucmd!().args(&["123.45", "!=", "123.450"]).succeeds(); } @@ -230,6 +247,16 @@ fn test_some_int_compares() { for test in &tests { scenario.ucmd().args(&test[..]).succeeds(); } + + // run the inverse of all these tests + for test in &tests { + scenario + .ucmd() + .arg("!") + .args(&test[..]) + .run() + .status_code(1); + } } #[test] @@ -257,6 +284,16 @@ fn test_negative_int_compare() { for test in &tests { scenario.ucmd().args(&test[..]).succeeds(); } + + // run the inverse of all these tests + for test in &tests { + scenario + .ucmd() + .arg("!") + .args(&test[..]) + .run() + .status_code(1); + } } #[test] @@ -283,7 +320,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\\x80o'"); let mut cmd = new_ucmd!(); cmd.raw.arg(arg); @@ -291,7 +328,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\\x80o'"); } #[test] @@ -477,7 +514,9 @@ fn test_nonexistent_file_is_not_symlink() { } #[test] -#[cfg(not(windows))] // Windows has no concept of sticky bit +// FixME: freebsd fails with 'chmod: sticky_file: Inappropriate file type or format' +// Windows has no concept of sticky bit +#[cfg(not(any(windows, target_os = "freebsd")))] fn test_file_is_sticky() { let scenario = TestScenario::new(util_name!()); let mut ucmd = scenario.ucmd(); @@ -497,6 +536,93 @@ fn test_file_is_not_sticky() { .status_code(1); } +#[test] +fn test_solo_empty_parenthetical_is_error() { + new_ucmd!().args(&["(", ")"]).run().status_code(2); +} + +#[test] +fn test_parenthesized_literal() { + let scenario = TestScenario::new(util_name!()); + let tests = [ + "a string", + "(", + ")", + "-", + "--", + "-0", + "-f", + "--help", + "--version", + "-e", + "-t", + "!", + "-n", + "-z", + "[", + "-a", + "-o", + ]; + + for test in &tests { + scenario.ucmd().arg("(").arg(test).arg(")").succeeds(); + } + + // run the inverse of all these tests + for test in &tests { + scenario + .ucmd() + .arg("!") + .arg("(") + .arg(test) + .arg(")") + .run() + .status_code(1); + } +} + +#[test] +fn test_parenthesized_op_compares_literal_parenthesis() { + // ensure we aren’t treating this case as “string length of literal equal + // sign” + new_ucmd!().args(&["(", "=", ")"]).run().status_code(1); +} + +#[test] +fn test_parenthesized_string_comparison() { + let scenario = TestScenario::new(util_name!()); + let tests = [ + ["(", "foo", "!=", "bar", ")"], + ["(", "contained\nnewline", "=", "contained\nnewline", ")"], + ["(", "(", "=", "(", ")"], + ["(", "(", "!=", ")", ")"], + ["(", "!", "=", "!", ")"], + ["(", "=", "=", "=", ")"], + ]; + + for test in &tests { + scenario.ucmd().args(&test[..]).succeeds(); + } + + // run the inverse of all these tests + for test in &tests { + scenario + .ucmd() + .arg("!") + .args(&test[..]) + .run() + .status_code(1); + } +} + +#[test] +fn test_parenthesized_right_parenthesis_as_literal() { + new_ucmd!() + .args(&["(", "-f", ")", ")"]) + .run() + .status_code(1); +} + #[test] #[cfg(not(windows))] fn test_file_owned_by_euid() { diff --git a/tests/by-util/test_touch.rs b/tests/by-util/test_touch.rs index 3ed7f3bb2..983d14fe2 100644 --- a/tests/by-util/test_touch.rs +++ b/tests/by-util/test_touch.rs @@ -6,6 +6,7 @@ use self::touch::filetime::{self, FileTime}; extern crate time; use crate::common::util::*; +use std::fs::remove_file; use std::path::PathBuf; fn get_file_times(at: &AtPath, path: &str) -> (FileTime, FileTime) { @@ -16,6 +17,7 @@ fn get_file_times(at: &AtPath, path: &str) -> (FileTime, FileTime) { ) } +#[cfg(not(target_os = "freebsd"))] fn get_symlink_times(at: &AtPath, path: &str) -> (FileTime, FileTime) { let m = at.symlink_metadata(path); ( @@ -290,6 +292,8 @@ fn test_touch_set_both() { } #[test] +// FixME: Fails on freebsd because of a different nanos +#[cfg(not(target_os = "freebsd"))] fn test_touch_no_dereference() { let (at, mut ucmd) = at_and_ucmd!(); let file_a = "test_touch_no_dereference_a"; @@ -320,7 +324,8 @@ fn test_touch_no_dereference() { #[test] fn test_touch_reference() { - let (at, mut ucmd) = at_and_ucmd!(); + let scenario = TestScenario::new("touch"); + let (at, mut _ucmd) = (scenario.fixtures.clone(), scenario.ucmd()); let file_a = "test_touch_reference_a"; let file_b = "test_touch_reference_b"; let start_of_year = str_to_filetime("%Y%m%d%H%M", "201501010000"); @@ -328,15 +333,21 @@ fn test_touch_reference() { at.touch(file_a); set_file_times(&at, file_a, start_of_year, start_of_year); assert!(at.file_exists(file_a)); + for &opt in &["-r", "--ref", "--reference"] { + scenario + .ccmd("touch") + .args(&[opt, file_a, file_b]) + .succeeds() + .no_stderr(); - ucmd.args(&["-r", file_a, file_b]).succeeds().no_stderr(); + assert!(at.file_exists(file_b)); - assert!(at.file_exists(file_b)); - - let (atime, mtime) = get_file_times(&at, file_b); - assert_eq!(atime, mtime); - assert_eq!(atime, start_of_year); - assert_eq!(mtime, start_of_year); + let (atime, mtime) = get_file_times(&at, file_b); + assert_eq!(atime, mtime); + assert_eq!(atime, start_of_year); + assert_eq!(mtime, start_of_year); + let _ = remove_file(file_b); + } } #[test] diff --git a/tests/by-util/test_tr.rs b/tests/by-util/test_tr.rs index 47b097d9d..5f60f8d2a 100644 --- a/tests/by-util/test_tr.rs +++ b/tests/by-util/test_tr.rs @@ -286,6 +286,8 @@ fn test_interpret_backslash_at_eol_literally() { } #[test] +// FixME: panicked at 'failed to write to stdin of child: Broken pipe (os error 32) +#[cfg(not(target_os = "freebsd"))] fn test_more_than_2_sets() { new_ucmd!() .args(&["'abcdefgh'", "'a", "'b'"]) diff --git a/tests/by-util/test_tty.rs b/tests/by-util/test_tty.rs index 6ba8cd029..ed490e7ab 100644 --- a/tests/by-util/test_tty.rs +++ b/tests/by-util/test_tty.rs @@ -65,10 +65,24 @@ fn test_wrong_argument() { } #[test] -#[cfg(not(windows))] +// FixME: freebsd panic +#[cfg(all(unix, not(target_os = "freebsd")))] fn test_stdout_fail() { - let mut child = new_ucmd!().run_no_wait(); - drop(child.stdout.take()); - let status = child.wait().unwrap(); + use std::process::{Command, Stdio}; + let ts = TestScenario::new(util_name!()); + // Sleep inside a shell to ensure the process doesn't finish before we've + // closed its stdout + let mut proc = Command::new("sh") + .arg("-c") + .arg(format!( + "sleep 0.2; exec {} {}", + ts.bin_path.to_str().unwrap(), + ts.util_name + )) + .stdout(Stdio::piped()) + .spawn() + .unwrap(); + drop(proc.stdout.take()); + let status = proc.wait().unwrap(); assert_eq!(status.code(), Some(3)); } diff --git a/tests/by-util/test_unlink.rs b/tests/by-util/test_unlink.rs index 1999e965c..6b4fc41da 100644 --- a/tests/by-util/test_unlink.rs +++ b/tests/by-util/test_unlink.rs @@ -14,29 +14,33 @@ fn test_unlink_file() { #[test] fn test_unlink_multiple_files() { - let (at, mut ucmd) = at_and_ucmd!(); + let ts = TestScenario::new(util_name!()); + + let (at, mut ucmd) = (ts.fixtures.clone(), ts.ucmd()); let file_a = "test_unlink_multiple_file_a"; let file_b = "test_unlink_multiple_file_b"; at.touch(file_a); at.touch(file_b); - ucmd.arg(file_a).arg(file_b).fails().stderr_is( - "unlink: extra operand: 'test_unlink_multiple_file_b'\nTry 'unlink --help' \ - for more information.\n", - ); + ucmd.arg(file_a) + .arg(file_b) + .fails() + .stderr_contains("USAGE"); } #[test] fn test_unlink_directory() { let (at, mut ucmd) = at_and_ucmd!(); - let dir = "test_unlink_empty_directory"; + let dir = "dir"; at.mkdir(dir); - ucmd.arg(dir).fails().stderr_is( - "unlink: cannot unlink 'test_unlink_empty_directory': Not a regular file \ - or symlink\n", + let res = ucmd.arg(dir).fails(); + let stderr = res.stderr_str(); + assert!( + stderr == "unlink: cannot unlink 'dir': Is a directory\n" + || stderr == "unlink: cannot unlink 'dir': Permission denied\n" ); } @@ -44,8 +48,21 @@ fn test_unlink_directory() { fn test_unlink_nonexistent() { let file = "test_unlink_nonexistent"; - new_ucmd!().arg(file).fails().stderr_is( - "unlink: Cannot stat 'test_unlink_nonexistent': No such file or directory \ - (os error 2)\n", - ); + new_ucmd!() + .arg(file) + .fails() + .stderr_is("unlink: cannot unlink 'test_unlink_nonexistent': No such file or directory\n"); +} + +#[test] +fn test_unlink_symlink() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.touch("foo"); + at.symlink_file("foo", "bar"); + + ucmd.arg("bar").succeeds().no_stderr(); + + assert!(at.file_exists("foo")); + assert!(!at.file_exists("bar")); } diff --git a/tests/by-util/test_wc.rs b/tests/by-util/test_wc.rs index 88c65c997..eabaf58eb 100644 --- a/tests/by-util/test_wc.rs +++ b/tests/by-util/test_wc.rs @@ -1,6 +1,6 @@ use crate::common::util::*; -// spell-checker:ignore (flags) lwmcL clmwL ; (path) bogusfile emptyfile manyemptylines moby notrailingnewline onelongemptyline onelongword +// spell-checker:ignore (flags) lwmcL clmwL ; (path) bogusfile emptyfile manyemptylines moby notrailingnewline onelongemptyline onelongword weirdchars #[test] fn test_count_bytes_large_stdin() { @@ -53,11 +53,16 @@ fn test_utf8() { .args(&["-lwmcL"]) .pipe_in_fixture("UTF_8_test.txt") .run() - .stdout_is(" 300 4969 22781 22213 79\n"); - // GNU returns " 300 2086 22219 22781 79" - // - // TODO: we should fix the word, character, and byte count to - // match the behavior of GNU wc + .stdout_is(" 303 2119 22457 23025 79\n"); +} + +#[test] +fn test_utf8_extra() { + new_ucmd!() + .arg("-lwmcL") + .pipe_in_fixture("UTF_8_weirdchars.txt") + .run() + .stdout_is(" 25 87 442 513 48\n"); } #[test] @@ -200,22 +205,33 @@ fn test_file_bytes_dictate_width() { /// Test that getting counts from a directory is an error. #[test] fn test_read_from_directory_error() { - // TODO To match GNU `wc`, the `stdout` should be: - // - // " 0 0 0 .\n" - // + #[cfg(not(windows))] + const STDERR: &str = ".: Is a directory"; + #[cfg(windows)] + const STDERR: &str = ".: Access is denied"; + + #[cfg(not(windows))] + const STDOUT: &str = " 0 0 0 .\n"; + #[cfg(windows)] + const STDOUT: &str = ""; + new_ucmd!() .args(&["."]) .fails() - .stderr_contains(".: Is a directory\n") - .stdout_is("0 0 0 .\n"); + .stderr_contains(STDERR) + .stdout_is(STDOUT); } /// Test that getting counts from nonexistent file is an error. #[test] fn test_read_from_nonexistent_file() { + #[cfg(not(windows))] + const MSG: &str = "bogusfile: No such file or directory"; + #[cfg(windows)] + const MSG: &str = "bogusfile: The system cannot find the file specified"; new_ucmd!() .args(&["bogusfile"]) .fails() - .stderr_contains("bogusfile: No such file or directory\n"); + .stderr_contains(MSG) + .stdout_is(""); } diff --git a/tests/by-util/test_whoami.rs b/tests/by-util/test_whoami.rs index 3e8d5afa6..340c1434f 100644 --- a/tests/by-util/test_whoami.rs +++ b/tests/by-util/test_whoami.rs @@ -3,7 +3,6 @@ // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -#[cfg(unix)] use crate::common::util::*; #[test] @@ -34,7 +33,6 @@ fn test_normal_compare_id() { } #[test] -#[cfg(unix)] fn test_normal_compare_env() { let whoami = whoami(); if whoami == "nobody" { diff --git a/tests/by-util/test_yes.rs b/tests/by-util/test_yes.rs index 651491045..7e950e1ea 100644 --- a/tests/by-util/test_yes.rs +++ b/tests/by-util/test_yes.rs @@ -1 +1,72 @@ -// ToDO: add tests +use std::io::Read; + +use crate::common::util::*; + +/// Run `yes`, capture some of the output, close the pipe, and verify it. +fn run(args: &[&str], expected: &[u8]) { + let mut cmd = new_ucmd!(); + let mut child = cmd.args(args).run_no_wait(); + let mut stdout = child.stdout.take().unwrap(); + let mut buf = vec![0; expected.len()]; + stdout.read_exact(&mut buf).unwrap(); + drop(stdout); + assert!(child.wait().unwrap().success()); + assert_eq!(buf.as_slice(), expected); +} + +#[test] +fn test_simple() { + run(&[], b"y\ny\ny\ny\n"); +} + +#[test] +fn test_args() { + run(&["a", "bar", "c"], b"a bar c\na bar c\na ba"); +} + +#[test] +fn test_long_output() { + run(&[], "y\n".repeat(512 * 1024).as_bytes()); +} + +/// Test with an output that seems likely to get mangled in case of incomplete writes. +#[test] +fn test_long_odd_output() { + run(&["abcdef"], "abcdef\n".repeat(1024 * 1024).as_bytes()); +} + +/// Test with an input that doesn't fit in the standard buffer. +#[test] +fn test_long_input() { + #[cfg(not(windows))] + const TIMES: usize = 14000; + // On Windows the command line is limited to 8191 bytes. + // This is not actually enough to fill the buffer, but it's still nice to + // try something long. + #[cfg(windows)] + const TIMES: usize = 500; + let arg = "abcdefg".repeat(TIMES) + "\n"; + let expected_out = arg.repeat(30); + run(&[&arg[..arg.len() - 1]], expected_out.as_bytes()); +} + +#[test] +#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))] +fn test_piped_to_dev_full() { + use std::fs::OpenOptions; + + for &append in &[true, false] { + { + let dev_full = OpenOptions::new() + .write(true) + .append(append) + .open("/dev/full") + .unwrap(); + + new_ucmd!() + .set_stdout(dev_full) + .fails() + .stderr_contains("No space left on device"); + } + } +} diff --git a/tests/common/macros.rs b/tests/common/macros.rs index 62b8c4824..108bc0fb7 100644 --- a/tests/common/macros.rs +++ b/tests/common/macros.rs @@ -31,7 +31,11 @@ macro_rules! path_concat { #[macro_export] macro_rules! util_name { () => { - module_path!().split("_").nth(1).expect("no test name") + module_path!() + .split("_") + .nth(1) + .and_then(|s| s.split("::").next()) + .expect("no test name") }; } diff --git a/tests/common/util.rs b/tests/common/util.rs index 41389d567..f3cdec010 100644 --- a/tests/common/util.rs +++ b/tests/common/util.rs @@ -3,7 +3,7 @@ // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -//spell-checker: ignore (linux) rlimit prlimit Rlim coreutil +//spell-checker: ignore (linux) rlimit prlimit Rlim coreutil ggroups #![allow(dead_code)] @@ -713,7 +713,7 @@ impl AtPath { /// /// Fixtures can be found under `tests/fixtures/$util_name/` pub struct TestScenario { - bin_path: PathBuf, + pub bin_path: PathBuf, pub util_name: String, pub fixtures: AtPath, tmpd: Rc, @@ -1069,10 +1069,12 @@ pub fn whoami() -> String { // Use environment variable to get current user instead of // invoking `whoami` and fall back to user "nobody" on error. - std::env::var("USER").unwrap_or_else(|e| { - println!("{}: {}, using \"nobody\" instead", UUTILS_WARNING, e); - "nobody".to_string() - }) + std::env::var("USER") + .or_else(|_| std::env::var("USERNAME")) + .unwrap_or_else(|e| { + println!("{}: {}, using \"nobody\" instead", UUTILS_WARNING, e); + "nobody".to_string() + }) } /// Add prefix 'g' for `util_name` if not on linux @@ -1081,7 +1083,14 @@ pub fn host_name_for(util_name: &str) -> Cow { // In some environments, e.g. macOS/freebsd, the GNU coreutils are prefixed with "g" // to not interfere with the BSD counterparts already in `$PATH`. #[cfg(not(target_os = "linux"))] - return format!("g{}", util_name).into(); + { + // make call to `host_name_for` idempotent + if util_name.starts_with('g') && util_name != "groups" { + return util_name.into(); + } else { + return format!("g{}", util_name).into(); + } + } #[cfg(target_os = "linux")] return util_name.into(); } @@ -1195,8 +1204,8 @@ pub fn check_coreutil_version( ///``` #[cfg(unix)] pub fn expected_result(ts: &TestScenario, args: &[&str]) -> std::result::Result { + println!("{}", check_coreutil_version(&ts.util_name, VERSION_MIN)?); let util_name = &host_name_for(&ts.util_name); - println!("{}", check_coreutil_version(util_name, VERSION_MIN)?); let result = ts .cmd_keepenv(util_name.as_ref()) @@ -1493,4 +1502,25 @@ mod tests { let ts = TestScenario::new("no test name"); assert!(expected_result(&ts, &[]).is_err()); } + + #[test] + #[cfg(unix)] + fn test_host_name_for() { + #[cfg(target_os = "linux")] + { + std::assert_eq!(host_name_for("id"), "id"); + std::assert_eq!(host_name_for("groups"), "groups"); + std::assert_eq!(host_name_for("who"), "who"); + } + #[cfg(not(target_os = "linux"))] + { + // spell-checker:ignore (strings) ggroups gwho + std::assert_eq!(host_name_for("id"), "gid"); + std::assert_eq!(host_name_for("groups"), "ggroups"); + std::assert_eq!(host_name_for("who"), "gwho"); + std::assert_eq!(host_name_for("gid"), "gid"); + std::assert_eq!(host_name_for("ggroups"), "ggroups"); + std::assert_eq!(host_name_for("gwho"), "gwho"); + } + } } diff --git a/tests/fixtures/hashsum/b2sum.checkfile b/tests/fixtures/hashsum/b2sum.checkfile new file mode 100644 index 000000000..9d6781cc8 --- /dev/null +++ b/tests/fixtures/hashsum/b2sum.checkfile @@ -0,0 +1 @@ +7355dd5276c21cfe0c593b5063b96af3f96a454b33216f58314f44c3ade92e9cd6cec4210a0836246780e9baf927cc50b9a3d7073e8f9bd12780fddbcb930c6d input.txt diff --git a/tests/fixtures/hashsum/md5.checkfile b/tests/fixtures/hashsum/md5.checkfile new file mode 100644 index 000000000..328e1bd94 --- /dev/null +++ b/tests/fixtures/hashsum/md5.checkfile @@ -0,0 +1 @@ +e4d7f1b4ed2e42d15898f4b27b019da4 input.txt diff --git a/tests/fixtures/hashsum/sha1.checkfile b/tests/fixtures/hashsum/sha1.checkfile new file mode 100644 index 000000000..02c35969f --- /dev/null +++ b/tests/fixtures/hashsum/sha1.checkfile @@ -0,0 +1 @@ +b7e23ec29af22b0b4e41da31e868d57226121c84 input.txt diff --git a/tests/fixtures/hashsum/sha224.checkfile b/tests/fixtures/hashsum/sha224.checkfile new file mode 100644 index 000000000..6e3402094 --- /dev/null +++ b/tests/fixtures/hashsum/sha224.checkfile @@ -0,0 +1 @@ +6e1a93e32fb44081a401f3db3ef2e6e108b7bbeeb5705afdaf01fb27 input.txt diff --git a/tests/fixtures/hashsum/sha256.checkfile b/tests/fixtures/hashsum/sha256.checkfile new file mode 100644 index 000000000..db1d2be15 --- /dev/null +++ b/tests/fixtures/hashsum/sha256.checkfile @@ -0,0 +1 @@ +09ca7e4eaa6e8ae9c7d261167129184883644d07dfba7cbfbc4c8a2e08360d5b input.txt diff --git a/tests/fixtures/hashsum/sha384.checkfile b/tests/fixtures/hashsum/sha384.checkfile new file mode 100644 index 000000000..c53326b1d --- /dev/null +++ b/tests/fixtures/hashsum/sha384.checkfile @@ -0,0 +1 @@ +1fcdb6059ce05172a26bbe2a3ccc88ed5a8cd5fc53edfd9053304d429296a6da23b1cd9e5c9ed3bb34f00418a70cdb7e input.txt diff --git a/tests/fixtures/hashsum/sha3_224.checkfile b/tests/fixtures/hashsum/sha3_224.checkfile new file mode 100644 index 000000000..3a93cd056 --- /dev/null +++ b/tests/fixtures/hashsum/sha3_224.checkfile @@ -0,0 +1 @@ +927b362eaf84a75785bbec3370d1c9711349e93f1104eda060784221 input.txt diff --git a/tests/fixtures/hashsum/sha3_256.checkfile b/tests/fixtures/hashsum/sha3_256.checkfile new file mode 100644 index 000000000..b8bec0924 --- /dev/null +++ b/tests/fixtures/hashsum/sha3_256.checkfile @@ -0,0 +1 @@ +bfb3959527d7a3f2f09def2f6915452d55a8f122df9e164d6f31c7fcf6093e14 input.txt diff --git a/tests/fixtures/hashsum/sha3_384.checkfile b/tests/fixtures/hashsum/sha3_384.checkfile new file mode 100644 index 000000000..6b014fdd2 --- /dev/null +++ b/tests/fixtures/hashsum/sha3_384.checkfile @@ -0,0 +1 @@ +fbd0c5931195aaa9517869972b372f717bb69f7f9f72bfc0884ed0531c36a16fc2db5dd6d82131968b23ffe0e90757e5 input.txt diff --git a/tests/fixtures/hashsum/sha3_512.checkfile b/tests/fixtures/hashsum/sha3_512.checkfile new file mode 100644 index 000000000..125e2dfba --- /dev/null +++ b/tests/fixtures/hashsum/sha3_512.checkfile @@ -0,0 +1 @@ +2ed3a863a12e2f8ff140aa86232ff3603a7f24af62f0e2ca74672494ade175a9a3de42a351b5019d931a1deae0499609038d9b47268779d76198e1d410d20974 input.txt diff --git a/tests/fixtures/hashsum/sha512.checkfile b/tests/fixtures/hashsum/sha512.checkfile new file mode 100644 index 000000000..41a55cabb --- /dev/null +++ b/tests/fixtures/hashsum/sha512.checkfile @@ -0,0 +1 @@ +8710339dcb6814d0d9d2290ef422285c9322b7163951f9a0ca8f883d3305286f44139aa374848e4174f5aada663027e4548637b6d19894aec4fb6c46a139fbf9 input.txt diff --git a/tests/fixtures/hashsum/shake128_256.checkfile b/tests/fixtures/hashsum/shake128_256.checkfile new file mode 100644 index 000000000..a3769f78e --- /dev/null +++ b/tests/fixtures/hashsum/shake128_256.checkfile @@ -0,0 +1 @@ +83d41db453072caa9953f2f316480fbbcb84a5f3505460a18b3a36a814ae8e9e input.txt diff --git a/tests/fixtures/hashsum/shake256_512.checkfile b/tests/fixtures/hashsum/shake256_512.checkfile new file mode 100644 index 000000000..e16601f30 --- /dev/null +++ b/tests/fixtures/hashsum/shake256_512.checkfile @@ -0,0 +1 @@ +7c9896ea84a2a1b80b2183a3f2b4e43cd59b7d48471dc213bcedaccb699d6e6f7ad5d304928ab79329f1fc62f6db072d95b51209eb807683f5c9371872a2dd4e input.txt diff --git a/tests/fixtures/tac/delimited_primes_before.expected b/tests/fixtures/tac/delimited_primes_before.expected index 13cb1be06..1417a0150 100644 --- a/tests/fixtures/tac/delimited_primes_before.expected +++ b/tests/fixtures/tac/delimited_primes_before.expected @@ -1 +1 @@ -97:89:83:79:73:71:67:61:59:53:47:43:41:37:31:29:23:19:17:13:11:7:5:3:2 \ No newline at end of file +:97:89:83:79:73:71:67:61:59:53:47:43:41:37:31:29:23:19:17:13:11:7:5:32 \ No newline at end of file diff --git a/tests/fixtures/wc/UTF_8_test.txt b/tests/fixtures/wc/UTF_8_test.txt index a5b5d50e6..cd0474c82 100644 Binary files a/tests/fixtures/wc/UTF_8_test.txt and b/tests/fixtures/wc/UTF_8_test.txt differ diff --git a/tests/fixtures/wc/UTF_8_weirdchars.txt b/tests/fixtures/wc/UTF_8_weirdchars.txt new file mode 100644 index 000000000..0c7670f5e --- /dev/null +++ b/tests/fixtures/wc/UTF_8_weirdchars.txt @@ -0,0 +1,25 @@ +zero-width space inbetween these: x​x +and inbetween two spaces: [ ​ ] +and at the end of the line: ​ + +non-breaking space: x x [   ]   + +simple unicode: xµx [ µ ] µ + +wide: xwx [ w ] w + +simple emoji: x👩x [ 👩 ] 👩 + +complex emoji: x👩‍🔬x [ 👩‍🔬 ] 👩‍🔬 + +Hello, world! + +line feed: x x [ ] + +vertical tab: x x [ ] + +horizontal tab: x x [ ] +this should be the longest line: +1234567 12345678 123456781234567812345678 + +Control character: xx [  ]  diff --git a/tests/test_util_name.rs b/tests/test_util_name.rs new file mode 100644 index 000000000..b0a78a2e8 --- /dev/null +++ b/tests/test_util_name.rs @@ -0,0 +1,86 @@ +mod common; + +use common::util::TestScenario; + +#[cfg(unix)] +use std::os::unix::fs::symlink as symlink_file; +#[cfg(windows)] +use std::os::windows::fs::symlink_file; + +#[test] +#[cfg(feature = "ls")] +fn execution_phrase_double() { + use std::process::Command; + + let scenario = TestScenario::new("ls"); + let output = Command::new(&scenario.bin_path) + .arg("ls") + .arg("--some-invalid-arg") + .output() + .unwrap(); + assert!(String::from_utf8(output.stderr) + .unwrap() + .contains(&format!("USAGE:\n {} ls", scenario.bin_path.display(),))); +} + +#[test] +#[cfg(feature = "ls")] +fn execution_phrase_single() { + use std::process::Command; + + let scenario = TestScenario::new("ls"); + symlink_file(scenario.bin_path, scenario.fixtures.plus("uu-ls")).unwrap(); + let output = Command::new(scenario.fixtures.plus("uu-ls")) + .arg("--some-invalid-arg") + .output() + .unwrap(); + assert!(String::from_utf8(output.stderr).unwrap().contains(&format!( + "USAGE:\n {}", + scenario.fixtures.plus("uu-ls").display() + ))); +} + +#[test] +#[cfg(feature = "sort")] +fn util_name_double() { + use std::{ + io::Write, + process::{Command, Stdio}, + }; + + let scenario = TestScenario::new("sort"); + let mut child = Command::new(&scenario.bin_path) + .arg("sort") + .stdin(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + // input invalid utf8 to cause an error + child.stdin.take().unwrap().write_all(&[255]).unwrap(); + let output = child.wait_with_output().unwrap(); + assert!(String::from_utf8(output.stderr).unwrap().contains("sort: ")); +} + +#[test] +#[cfg(feature = "sort")] +fn util_name_single() { + use std::{ + io::Write, + process::{Command, Stdio}, + }; + + let scenario = TestScenario::new("sort"); + symlink_file(scenario.bin_path, scenario.fixtures.plus("uu-sort")).unwrap(); + let mut child = Command::new(scenario.fixtures.plus("uu-sort")) + .stdin(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + // input invalid utf8 to cause an error + child.stdin.take().unwrap().write_all(&[255]).unwrap(); + let output = child.wait_with_output().unwrap(); + assert!(String::from_utf8(output.stderr).unwrap().contains(&format!( + "{}: ", + scenario.fixtures.plus("uu-sort").display() + ))); +} diff --git a/util/GHA-delete-GNU-workflow-logs.sh b/util/GHA-delete-GNU-workflow-logs.sh old mode 100644 new mode 100755 diff --git a/util/build-code_coverage.sh b/util/build-code_coverage.sh old mode 100644 new mode 100755 diff --git a/util/build-gnu.sh b/util/build-gnu.sh old mode 100644 new mode 100755 diff --git a/util/compare_gnu_result.py b/util/compare_gnu_result.py old mode 100644 new mode 100755 diff --git a/util/publish.sh b/util/publish.sh old mode 100644 new mode 100755 diff --git a/util/run-gnu-test.sh b/util/run-gnu-test.sh old mode 100644 new mode 100755 index 9d51a983e..483fc1be9 --- a/util/run-gnu-test.sh +++ b/util/run-gnu-test.sh @@ -1,5 +1,6 @@ #!/bin/bash # spell-checker:ignore (env/vars) BUILDDIR GNULIB SUBDIRS +cd "$(dirname "${BASH_SOURCE[0]}")/../.." set -e BUILDDIR="${PWD}/uutils/target/release" GNULIB_DIR="${PWD}/gnulib" diff --git a/util/show-code_coverage.sh b/util/show-code_coverage.sh old mode 100644 new mode 100755 diff --git a/util/update-version.sh b/util/update-version.sh old mode 100644 new mode 100755