diff --git a/.cirrus.yml b/.cirrus.yml index 3a34a933a..5d16dce92 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -11,4 +11,4 @@ task: - cargo build test_script: - . $HOME/.cargo/env - - cargo test + - cargo test -p uucore -p coreutils diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 000000000..e0988d0bd --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,18 @@ +# Number of days of inactivity before an issue/PR becomes stale +daysUntilStale: 365 +# Number of days of inactivity before a stale issue/PR is closed +daysUntilClose: 365 +# Issues with these labels will never be considered stale +exemptLabels: + - pinned + - security + - "Good first bug" +# Label to use when marking an issue as stale +staleLabel: wontfix +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: false diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index df4c34564..cc0972bf9 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -1,7 +1,7 @@ name: CICD # spell-checker:ignore (acronyms) CICD MSVC musl -# spell-checker:ignore (env/flags) Ccodegen Coverflow Cpanic RUSTDOCFLAGS RUSTFLAGS Zpanic +# spell-checker:ignore (env/flags) Awarnings Ccodegen Coverflow Cpanic RUSTDOCFLAGS RUSTFLAGS Zpanic # spell-checker:ignore (jargon) SHAs deps softprops toolchain # spell-checker:ignore (names) CodeCOV MacOS MinGW Peltoche rivy # spell-checker:ignore (shell/tools) choco clippy dmake dpkg esac fakeroot gmake grcov halium lcov libssl mkdir popd printf pushd rustc rustfmt rustup shopt xargs @@ -11,7 +11,7 @@ env: PROJECT_NAME: coreutils PROJECT_DESC: "Core universal (cross-platform) utilities" PROJECT_AUTH: "uutils" - RUST_MIN_SRV: "1.32.0" ## v1.32.0 - minimum version for half, tempfile, etc + RUST_MIN_SRV: "1.40.0" ## v1.40.0 RUST_COV_SRV: "2020-08-01" ## (~v1.47.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] @@ -119,6 +119,12 @@ jobs: use-tool-cache: true env: RUSTUP_TOOLCHAIN: stable + - name: Confirm compatible 'Cargo.lock' + shell: bash + run: | + # Confirm compatible 'Cargo.lock' + # * 'Cargo.lock' is required to be in a format that `cargo` of MinSRV can interpret (eg, v1-format for MinSRV < v1.38) + cargo fetch --locked --quiet || { echo "::error file=Cargo.lock::Incompatible 'Cargo.lock' format; try \`cargo +${{ env.RUST_MIN_SRV }} update\`" ; exit 1 ; } - name: Info shell: bash run: | @@ -136,17 +142,67 @@ jobs: cargo-tree tree -V ## dependencies echo "## dependency list" + cargo fetch --locked --quiet ## * using the 'stable' toolchain is necessary to avoid "unexpected '--filter-platform'" errors - RUSTUP_TOOLCHAIN=stable cargo fetch --quiet - RUSTUP_TOOLCHAIN=stable cargo-tree tree --all --no-dev-dependencies --no-indent --features ${{ matrix.job.features }} | grep -vE "$PWD" | sort --unique + RUSTUP_TOOLCHAIN=stable cargo-tree tree --frozen --all --no-dev-dependencies --no-indent --features ${{ matrix.job.features }} | grep -vE "$PWD" | sort --unique + - name: Test uses: actions-rs/cargo@v1 with: command: test - args: --features "feat_os_unix" + args: --features "feat_os_unix" -p uucore -p coreutils env: RUSTFLAGS: '-Awarnings' + busybox_test: + name: Busybox test suite + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + - { os: ubuntu-latest } + steps: + - uses: actions/checkout@v1 + - name: Install `rust` toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + default: true + profile: minimal # minimal component installation (ie, no documentation) + - name: "prepare busytest" + shell: bash + run: | + make prepare-busytest + - name: "run busybox testsuite" + shell: bash + run: | + bindir=$(pwd)/target/debug + cd tmp/busybox-*/testsuite + S=$(bindir=$bindir ./runtest) && printf "%s\n" "$S" || { printf "%s\n" "$S" | grep "FAIL:" | sed -e "s/FAIL: /::warning ::Test failure:/g" ; } + + makefile_build: + name: Test the build target of the Makefile + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + - { os: ubuntu-latest } + steps: + - uses: actions/checkout@v1 + - name: Install `rust` toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + default: true + profile: minimal # minimal component installation (ie, no documentation) + - name: "Run make build" + shell: bash + run: | + sudo apt-get -y update ; sudo apt-get -y install python3-sphinx; + make build + build: name: Build runs-on: ${{ matrix.job.os }} @@ -348,7 +404,7 @@ jobs: cargo-tree tree -V ## dependencies echo "## dependency list" - cargo fetch --quiet + cargo fetch --locked --quiet cargo-tree tree --target=${{ matrix.job.target }} ${{ matrix.job.cargo-options }} ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} --all --no-dev-dependencies --no-indent | grep -vE "$PWD" | sort --unique - name: Build uses: actions-rs/cargo@v1 @@ -480,6 +536,17 @@ jobs: CARGO_UTILITY_LIST_OPTIONS="$(for u in ${UTILITY_LIST}; do echo "-puu_${u}"; done;)" echo set-output name=UTILITY_LIST::${UTILITY_LIST} echo ::set-output name=CARGO_UTILITY_LIST_OPTIONS::${CARGO_UTILITY_LIST_OPTIONS} + - name: Test uucore + uses: actions-rs/cargo@v1 + with: + command: test + args: ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} --no-fail-fast -p uucore + env: + CARGO_INCREMENTAL: '0' + RUSTC_WRAPPER: '' + RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort' + RUSTDOCFLAGS: '-Cpanic=abort' + # RUSTUP_TOOLCHAIN: ${{ steps.vars.outputs.TOOLCHAIN }} - name: Test uses: actions-rs/cargo@v1 with: diff --git a/.github/workflows/GNU.yml b/.github/workflows/GNU.yml new file mode 100644 index 000000000..35efccbe5 --- /dev/null +++ b/.github/workflows/GNU.yml @@ -0,0 +1,134 @@ +name: GNU + +on: [push, pull_request] + +jobs: + gnu: + name: Run GNU tests + runs-on: ubuntu-latest + steps: + # Checks out a copy of your repository on the ubuntu-latest machine + - name: Checkout code uutil + uses: actions/checkout@v2 + with: + path: 'uutils' + - name: Chechout GNU coreutils + uses: actions/checkout@v2 + with: + repository: 'coreutils/coreutils' + path: 'gnu' + - name: Chechout GNU corelib + uses: actions/checkout@v2 + with: + repository: 'coreutils/gnulib' + path: 'gnulib' + fetch-depth: 0 # gnu gets upset if gnulib is a shallow checkout + - name: Install `rust` toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + default: true + profile: minimal # minimal component installation (ie, no documentation) + components: rustfmt + - name: Build binaries + shell: bash + run: | + sudo apt-get update + sudo apt-get install autoconf autopoint bison texinfo gperf gcc g++ gdb python-pyinotify python3-sphinx + pushd uutils + make PROFILE=release + BUILDDIR="$PWD/target/release/" + cp "${BUILDDIR}/install" "${BUILDDIR}/ginstall" # The GNU tests rename this script before running, to avoid confusion with the make target + # Create *sum binaries + for sum in b2sum md5sum sha1sum sha224sum sha256sum sha384sum sha512sum + do + sum_path="${BUILDDIR}/${sum}" + test -f "${sum_path}" || cp "${BUILDDIR}/hashsum" "${sum_path}" + done + test -f "${BUILDDIR}/[" || cp "${BUILDDIR}/test" "${BUILDDIR}/[" + popd + GNULIB_SRCDIR="$PWD/gnulib" + pushd gnu/ + + # Any binaries that aren't built become `false` so their tests fail + for binary in $(./build-aux/gen-lists-of-programs.sh --list-progs) + do + bin_path="${BUILDDIR}/${binary}" + test -f "${bin_path}" || { echo "'${binary}' was not built with uutils, using the 'false' program"; cp "${BUILDDIR}/false" "${bin_path}"; } + done + + ./bootstrap --gnulib-srcdir="$GNULIB_SRCDIR" + ./configure --quiet --disable-gcc-warnings + #Add timeout to to protect against hangs + sed -i 's|"\$@|/usr/bin/timeout 600 "\$@|' build-aux/test-driver + # Change the PATH in the Makefile to test the uutils coreutils instead of the GNU coreutils + sed -i "s/^[[:blank:]]*PATH=.*/ PATH='${BUILDDIR//\//\\/}\$(PATH_SEPARATOR)'\"\$\$PATH\" \\\/" Makefile + sed -i 's| tr | /usr/bin/tr |' tests/init.sh + make + # Generate the factor tests, so they can be fixed + for i in {00..36} + do + make tests/factor/t${i}.sh + done + grep -rl 'path_prepend_' tests/* | xargs sed -i 's|path_prepend_ ./src||' + sed -i -e 's|^seq |/usr/bin/seq |' -e 's|sha1sum |/usr/bin/sha1sum |' tests/factor/t*sh + + # Remove tests checking for --version & --help + # Not really interesting for us and logs are too big + sed -i -e '/tests\/misc\/invalid-opt.pl/ D' \ + -e '/tests\/misc\/help-version.sh/ D' \ + -e '/tests\/misc\/help-version-getopt.sh/ D' \ + Makefile + + # Use the system coreutils where the test fails due to error in a util that is not the one being tested + sed -i 's|stat|/usr/bin/stat|' tests/chgrp/basic.sh tests/cp/existing-perm-dir.sh tests/touch/60-seconds.sh tests/misc/sort-compress-proc.sh + sed -i 's|ls -|/usr/bin/ls -|' tests/chgrp/posix-H.sh tests/chown/deref.sh tests/cp/same-file.sh tests/misc/mknod.sh tests/mv/part-symlink.sh tests/du/8gb.sh + sed -i 's|mkdir |/usr/bin/mkdir |' tests/cp/existing-perm-dir.sh tests/rm/empty-inacc.sh + sed -i 's|timeout \([[:digit:]]\)| /usr/bin/timeout \1|' tests/tail-2/inotify-rotate.sh tests/tail-2/inotify-dir-recreate.sh tests/tail-2/inotify-rotate-resources.sh tests/cp/parent-perm-race.sh tests/ls/infloop.sh tests/misc/sort-exit-early.sh tests/misc/sort-NaN-infloop.sh tests/misc/uniq-perf.sh tests/tail-2/inotify-only-regular.sh tests/tail-2/pipe-f2.sh tests/tail-2/retry.sh tests/tail-2/symlink.sh tests/tail-2/wait.sh tests/tail-2/pid.sh tests/dd/stats.sh tests/tail-2/follow-name.sh tests/misc/shuf.sh # Don't break the function called 'grep_timeout' + sed -i 's|chmod |/usr/bin/chmod |' tests/du/inacc-dir.sh tests/mkdir/p-3.sh tests/tail-2/tail-n0f.sh tests/cp/fail-perm.sh tests/du/inaccessible-cwd.sh tests/mv/i-2.sh tests/chgrp/basic.sh tests/misc/shuf.sh + sed -i 's|sort |/usr/bin/sort |' tests/ls/hyperlink.sh tests/misc/test-N.sh + sed -i 's|split |/usr/bin/split |' tests/misc/factor-parallel.sh + sed -i 's|truncate |/usr/bin/truncate |' tests/split/fail.sh + sed -i 's|dd |/usr/bin/dd |' tests/du/8gb.sh tests/tail-2/big-4gb.sh tests/cp/fiemap-2.sh init.cfg + sed -i 's|id -|/usr/bin/id -|' tests/misc/runcon-no-reorder.sh + sed -i 's|touch |/usr/bin/touch |' tests/cp/preserve-link.sh tests/cp/reflink-perm.sh tests/ls/block-size.sh tests/ls/abmon-align.sh tests/ls/rt-1.sh tests/mv/update.sh tests/misc/ls-time.sh tests/misc/stat-nanoseconds.sh tests/misc/time-style.sh tests/misc/test-N.sh + sed -i 's|ln -|/usr/bin/ln -|' tests/cp/link-deref.sh + sed -i 's|printf |/usr/bin/printf |' tests/dd/ascii.sh + sed -i 's|cp |/usr/bin/cp |' tests/mv/hard-2.sh + sed -i 's|paste |/usr/bin/paste |' tests/misc/od-endian.sh + sed -i 's|seq |/usr/bin/seq |' tests/misc/sort-discrim.sh + + #Add specific timeout to tests that currently hang to limit time spent waiting + sed -i 's|seq \$|/usr/bin/timeout 0.1 seq \$|' tests/misc/seq-precision.sh tests/misc/seq-long-double.sh + sed -i 's|cat |/usr/bin/timeout 0.1 cat |' tests/misc/cat-self.sh + + + test -f "${BUILDDIR}/getlimits" || cp src/getlimits "${BUILDDIR}" + - name: Run GNU tests + shell: bash + run: | + BUILDDIR="${PWD}/uutils/target/release" + GNULIB_DIR="${PWD}/gnulib" + pushd gnu + + timeout -sKILL 2h make -j "$(nproc)" check SUBDIRS=. RUN_EXPENSIVE_TESTS=yes RUN_VERY_EXPENSIVE_TESTS=yes VERBOSE=no || : # Kill after 4 hours in case something gets stuck in make + - name: Extract tests info + shell: bash + run: | + if test -f gnu/tests/test-suite.log + then + TOTAL=$( grep "# TOTAL:" gnu/tests/test-suite.log|cut -d' ' -f2-) + PASS=$( grep "# PASS:" gnu/tests/test-suite.log|cut -d' ' -f2-) + SKIP=$( grep "# SKIP:" gnu/tests/test-suite.log|cut -d' ' -f2-) + FAIL=$( grep "# FAIL:" gnu/tests/test-suite.log|cut -d' ' -f2-) + XPASS=$( grep "# XPASS:" gnu/tests/test-suite.log|cut -d' ' -f2-) + ERROR=$( grep "# ERROR:" gnu/tests/test-suite.log|cut -d' ' -f2-) + echo "::warning ::GNU testsuite = $TOTAL / $PASS / $FAIL / $ERROR" + else + echo "::error ::Failed to get summary of test results" + fi + + - uses: actions/upload-artifact@v2 + with: + name: test-report + path: gnu/tests/**/*.log diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..66d2a5f5a --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,8 @@ +# https://pre-commit.com +repos: + - repo: https://github.com/doublify/pre-commit-rust + rev: v1.0 + hooks: + - id: cargo-check + - id: clippy + - id: fmt diff --git a/.travis.yml b/.travis.yml index 65658179f..389ba44b0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,7 @@ matrix: - rust: nightly fast_finish: true include: - - rust: 1.32.0 + - rust: 1.40.0 env: FEATURES=unix # - rust: stable # os: linux @@ -56,7 +56,7 @@ install: script: - cargo build $CARGO_ARGS --features "$FEATURES" - - if [ ! $REDOX ]; then cargo test $CARGO_ARGS --features "$FEATURES" --no-fail-fast; fi + - if [ ! $REDOX ]; then cargo test $CARGO_ARGS -p uucore -p coreutils --features "$FEATURES" --no-fail-fast; fi - if [ -n "$TEST_INSTALL" ]; then mkdir installdir_test; DESTDIR=installdir_test make install; [ `ls installdir_test/usr/local/bin | wc -l` -gt 0 ]; fi addons: diff --git a/.vscode/cSpell.json b/.vscode/cSpell.json index 6319d3d59..8561d69ad 100644 --- a/.vscode/cSpell.json +++ b/.vscode/cSpell.json @@ -8,6 +8,8 @@ "Cygwin", "FreeBSD", "Gmail", + "GNUEABI", + "GNUEABIhf", "Irix", "MacOS", "MinGW", @@ -23,16 +25,31 @@ "Xenix", "flac", "lzma", + // cargo + "cdylib", + "rlib", // crates + "advapi", + "advapi32-sys", + "aho-corasick", + "backtrace", "byteorder", + "chacha", "chrono", + "conv", + "corasick", "filetime", "formatteriteminfo", "getopts", "itertools", + "memchr", "multifilereader", "onig", "peekreader", + "quickcheck", + "rand_chacha", + "smallvec", + "tempfile", "termion", "termios", "termsize", @@ -64,12 +81,14 @@ "colorize", "consts", "dedup", + "demangle", "deque", "dequeue", "enqueue", "executable", "executables", "gibibytes", + "hardfloat", "hardlink", "hardlinks", "hashsums", @@ -87,9 +106,12 @@ "primality", "pseudoprime", "pseudoprimes", + "procs", "readonly", "seedable", "semver", + "shortcode", + "shortcodes", "symlink", "symlinks", "syscall", @@ -144,6 +166,7 @@ "Sunrin SHIMURA", "Sunrin", "SHIMURA", "Smigle00", "Smigle", "Sylvestre Ledru", "Sylvestre", "Ledru", + "T Jameson Little", "Jameson", "Little", "Tobias Bohumir Schottdorf", "Tobias", "Bohumir", "Schottdorf", "Virgile Andreani", "Virgile", "Andreani", "Vsevolod Velichko", "Vsevolod", "Velichko", @@ -151,27 +174,37 @@ "Yury Krivopalov", "Yury", "Krivopalov", "anonymousknight", "kwantam", + "nicoo", + "rivy", // rust "clippy", "concat", + "fract", "powi", + "println", "repr", "rfind", "rustc", "rustfmt", + "struct", + "structs", "substr", "splitn", + "trunc", // shell "passwd", + "pipefail", "tcsh", // tags "Maint", // uutils + "chcon", "chgrp", "chmod", "chown", "chroot", "cksum", + "csplit", "dircolors", "hashsum", "hostid", @@ -190,16 +223,28 @@ "realpath", "relpath", "rmdir", + "runcon", "shuf", "stdbuf", + "stty", "tsort", "uname", "unexpand", "whoami", + // vars/errno + "errno", + "EOPNOTSUPP", + // vars/fcntl + "F_GETFL", + "GETFL", + "fcntl", + "vmsplice", // vars/libc "FILENO", "HOSTSIZE", "IDSIZE", + "IFIFO", + "IFREG", "IRGRP", "IROTH", "IRUSR", @@ -240,10 +285,16 @@ "socktype", "umask", "waitpid", + // vars/nix + "iovec", + "unistd", // vars/signals "SIGPIPE", // vars/sync "Condvar", + // vars/stat + "fstat", + "stat", // vars/time "Timespec", "nsec", @@ -265,9 +316,11 @@ "errhandlingapi", "fileapi", "handleapi", + "lmcons", "minwindef", "processthreadsapi", "synchapi", + "sysinfoapi", "winbase", "winerror", "winnt", @@ -285,10 +338,12 @@ "coreopts", "coreutils", "libc", + "libstdbuf", "musl", "ucmd", "utmpx", "uucore", + "uucore_procs", "uumain", "uutils" ], diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..7a73a41bf --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..6c50b811d --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +sylvestre@debian.org. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0123c298e..3c40e5dfd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,16 +5,24 @@ standard libraries are stabilized. You may *claim* an item on the to-do list by following these steps: 1. Open an issue named "Implement [the utility of your choice]", e.g. "Implement ls" -2. State that you are working on this utility. -3. Develop the utility. -4. Add integration tests. -5. Add the reference to your utility into Cargo.toml and Makefile. -6. Remove utility from the to-do list in the README. -7. Submit a pull request and close the issue. +1. State that you are working on this utility. +1. Develop the utility +1. Add integration tests. +1. Add the reference to your utility into Cargo.toml and Makefile. +1. Remove utility from the to-do list in the README. +1. Submit a pull request and close the issue. The steps above imply that, before starting to work on a utility, you should search the issues to make sure no one else is working on it. +## Best practices + +1. Follow what GNU is doing in term of options and behavior. +1. Use clap for argument management. +1. Make sure that the code coverage is covering all of the cases, including errors. +1. The code must be clippy-warning-free and rustfmt-compliant. +1. Don't hesitate to move common functions into uucore if they can be reused by other binaries. + ## Commit messages To help the project maintainers review pull requests from contributors across diff --git a/Cargo.lock b/Cargo.lock index f0b3f99d9..ce4457231 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,2720 +4,2757 @@ name = "advapi32-sys" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e06588080cb19d0acb6739808aafa5f26bfb2ca015b2b6370028b44cf7cb8a9a" dependencies = [ - "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8", + "winapi-build", ] [[package]] name = "aho-corasick" -version = "0.6.10" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" dependencies = [ - "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "aho-corasick" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 2.3.4", ] [[package]] name = "ansi_term" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" dependencies = [ - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.9", +] + +[[package]] +name = "arrayvec" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9" +dependencies = [ + "nodrop", ] [[package]] name = "atty" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "hermit-abi", + "libc", + "winapi 0.3.9", ] [[package]] name = "autocfg" -version = "0.1.7" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "autocfg" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "backtrace" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", - "backtrace-sys 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "backtrace-sys" -version = "0.1.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cc 1.0.54 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", -] +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" [[package]] name = "bit-set" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de" dependencies = [ - "bit-vec 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "bit-vec", ] [[package]] name = "bit-vec" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "bitflags" -version = "0.7.0" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" [[package]] name = "bitflags" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "blake2-rfc" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d530bdd2d52966a6d03b7a964add7ae1a288d25214066fd4b600f0f796400" +dependencies = [ + "arrayvec", + "constant_time_eq", +] [[package]] name = "block-buffer" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1339a1042f5d9f295737ad4d9a6ab6bf81c84a933dba110b9200cd6d1448b814" dependencies = [ - "byte-tools 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "generic-array 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)", + "byte-tools", + "generic-array", ] [[package]] name = "bstr" -version = "0.2.13" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a40b47ad93e1a5404e6c18dec46b628214fee441c70f4ab5d6942142cc268a3d" dependencies = [ - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "regex-automata 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static", + "memchr 2.3.4", + "regex-automata", + "serde", ] [[package]] name = "bumpalo" -version = "3.4.0" +version = "3.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe" [[package]] name = "byte-tools" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "560c32574a12a89ecd91f5e742165893f86e3ab98d21f8ea548658eb9eef5f40" [[package]] name = "byteorder" -version = "1.3.4" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "cast" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9434b9a5aa1450faa3f9cb14ea0e8c53bb5d2b3c1bfd1ab4fc03e9f33fbfb0" dependencies = [ - "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc_version", ] [[package]] name = "cc" -version = "1.0.54" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" [[package]] name = "cfg-if" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80094f509cf8b5ae86a4966a39b3ff66cd7e2a3e594accec3743ff3fabeab5b2" dependencies = [ - "num-integer 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", - "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", + "num-integer", + "num-traits", + "time", ] [[package]] name = "clap" -version = "2.33.1" +version = "2.33.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" dependencies = [ - "ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", - "atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", - "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", - "textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", - "vec_map 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", ] [[package]] name = "cloudabi" version = "0.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" dependencies = [ - "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "conv" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ff10625fd0ac447827aa30ea8b861fead473bb60aeb73af6c1c58caf0d1299" +dependencies = [ + "custom_derive", ] [[package]] name = "coreutils" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "filetime 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.3.9 (registry+https://github.com/rust-lang/crates.io-index)", - "same-file 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", - "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", - "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", - "unindent 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", - "unix_socket 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "users 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", - "uu_arch 0.0.1", - "uu_base32 0.0.1", - "uu_base64 0.0.1", - "uu_basename 0.0.1", - "uu_cat 0.0.1", - "uu_chgrp 0.0.1", - "uu_chmod 0.0.1", - "uu_chown 0.0.1", - "uu_chroot 0.0.1", - "uu_cksum 0.0.1", - "uu_comm 0.0.1", - "uu_cp 0.0.1", - "uu_cut 0.0.1", - "uu_date 0.0.1", - "uu_df 0.0.1", - "uu_dircolors 0.0.1", - "uu_dirname 0.0.1", - "uu_du 0.0.1", - "uu_echo 0.0.1", - "uu_env 0.0.1", - "uu_expand 0.0.1", - "uu_expr 0.0.1", - "uu_factor 0.0.1", - "uu_false 0.0.1", - "uu_fmt 0.0.1", - "uu_fold 0.0.1", - "uu_groups 0.0.1", - "uu_hashsum 0.0.1", - "uu_head 0.0.1", - "uu_hostid 0.0.1", - "uu_hostname 0.0.1", - "uu_id 0.0.1", - "uu_install 0.0.1", - "uu_join 0.0.1", - "uu_kill 0.0.1", - "uu_link 0.0.1", - "uu_ln 0.0.1", - "uu_logname 0.0.1", - "uu_ls 0.0.1", - "uu_mkdir 0.0.1", - "uu_mkfifo 0.0.1", - "uu_mknod 0.0.1", - "uu_mktemp 0.0.1", - "uu_more 0.0.1", - "uu_mv 0.0.1", - "uu_nice 0.0.1", - "uu_nl 0.0.1", - "uu_nohup 0.0.1", - "uu_nproc 0.0.1", - "uu_numfmt 0.0.1", - "uu_od 0.0.1", - "uu_paste 0.0.1", - "uu_pathchk 0.0.1", - "uu_pinky 0.0.1", - "uu_printenv 0.0.1", - "uu_printf 0.0.1", - "uu_ptx 0.0.1", - "uu_pwd 0.0.1", - "uu_readlink 0.0.1", - "uu_realpath 0.0.1", - "uu_relpath 0.0.1", - "uu_rm 0.0.1", - "uu_rmdir 0.0.1", - "uu_seq 0.0.1", - "uu_shred 0.0.1", - "uu_shuf 0.0.1", - "uu_sleep 0.0.1", - "uu_sort 0.0.1", - "uu_split 0.0.1", - "uu_stat 0.0.1", - "uu_stdbuf 0.0.1", - "uu_sum 0.0.1", - "uu_sync 0.0.1", - "uu_tac 0.0.1", - "uu_tail 0.0.1", - "uu_tee 0.0.1", - "uu_test 0.0.1", - "uu_timeout 0.0.1", - "uu_touch 0.0.1", - "uu_tr 0.0.1", - "uu_true 0.0.1", - "uu_truncate 0.0.1", - "uu_tsort 0.0.1", - "uu_tty 0.0.1", - "uu_uname 0.0.1", - "uu_unexpand 0.0.1", - "uu_uniq 0.0.1", - "uu_unlink 0.0.1", - "uu_uptime 0.0.1", - "uu_users 0.0.1", - "uu_wc 0.0.1", - "uu_who 0.0.1", - "uu_whoami 0.0.1", - "uu_yes 0.0.1", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "conv", + "filetime", + "glob 0.3.0", + "lazy_static", + "libc", + "nix 0.20.0", + "rand 0.7.3", + "regex", + "sha1", + "tempdir", + "tempfile", + "textwrap", + "time", + "unindent", + "unix_socket", + "users", + "uu_arch", + "uu_base32", + "uu_base64", + "uu_basename", + "uu_cat", + "uu_chgrp", + "uu_chmod", + "uu_chown", + "uu_chroot", + "uu_cksum", + "uu_comm", + "uu_cp", + "uu_csplit", + "uu_cut", + "uu_date", + "uu_df", + "uu_dircolors", + "uu_dirname", + "uu_du", + "uu_echo", + "uu_env", + "uu_expand", + "uu_expr", + "uu_factor", + "uu_false", + "uu_fmt", + "uu_fold", + "uu_groups", + "uu_hashsum", + "uu_head", + "uu_hostid", + "uu_hostname", + "uu_id", + "uu_install", + "uu_join", + "uu_kill", + "uu_link", + "uu_ln", + "uu_logname", + "uu_ls", + "uu_mkdir", + "uu_mkfifo", + "uu_mknod", + "uu_mktemp", + "uu_more", + "uu_mv", + "uu_nice", + "uu_nl", + "uu_nohup", + "uu_nproc", + "uu_numfmt", + "uu_od", + "uu_paste", + "uu_pathchk", + "uu_pinky", + "uu_printenv", + "uu_printf", + "uu_ptx", + "uu_pwd", + "uu_readlink", + "uu_realpath", + "uu_relpath", + "uu_rm", + "uu_rmdir", + "uu_seq", + "uu_shred", + "uu_shuf", + "uu_sleep", + "uu_sort", + "uu_split", + "uu_stat", + "uu_stdbuf", + "uu_sum", + "uu_sync", + "uu_tac", + "uu_tail", + "uu_tee", + "uu_test", + "uu_timeout", + "uu_touch", + "uu_tr", + "uu_true", + "uu_truncate", + "uu_tsort", + "uu_tty", + "uu_uname", + "uu_unexpand", + "uu_uniq", + "uu_unlink", + "uu_uptime", + "uu_users", + "uu_wc", + "uu_who", + "uu_whoami", + "uu_yes", + "uucore", + "walkdir", ] [[package]] name = "cpp" -version = "0.4.0" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4875a08600be48dcc9cb6ee07f104a3e0752e95184dede6a30044d6480bf50e8" dependencies = [ - "cpp_macros 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cpp_macros", ] [[package]] name = "cpp_build" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c47531e7e09532ad4827098729794f5e1a5b1c2ccbb5e295498d2e7ab451c445" dependencies = [ - "cc 1.0.54 (registry+https://github.com/rust-lang/crates.io-index)", - "cpp_common 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "cpp_syn 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", - "cpp_synmap 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "cpp_synom 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cc", + "cpp_common 0.4.0", + "cpp_syn", + "cpp_synmap", + "cpp_synom", + "lazy_static", ] [[package]] name = "cpp_common" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79e39149a7943affa02f5b6e347ca2840a129cc78d5883ee229f0f1c4027d628" dependencies = [ - "cpp_syn 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", - "cpp_synom 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", + "cpp_syn", + "cpp_synom", + "lazy_static", + "quote 0.3.15", +] + +[[package]] +name = "cpp_common" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df78ad28e5fe814285016779fb3d3b874520c799a847e6190bf2b834cc4ff283" +dependencies = [ + "lazy_static", + "proc-macro2", + "syn", ] [[package]] name = "cpp_macros" -version = "0.4.0" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f93a21e618c10abc84ebb63ffa5952e1f7a4568b8141d542d5ef860e4a8fc25" dependencies = [ - "aho-corasick 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)", - "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "cpp_common 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "cpp_syn 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", - "cpp_synom 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", + "aho-corasick", + "byteorder", + "cpp_common 0.5.6", + "if_rust_version", + "lazy_static", + "proc-macro2", + "quote 1.0.9", + "syn", ] [[package]] name = "cpp_syn" version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8cd649bf5b3804d92fe12a60c7698f5a538a6033ed8a668bf5241d4d4f1644e" dependencies = [ - "cpp_synom 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "cpp_synom", + "quote 0.3.15", + "unicode-xid 0.0.4", ] [[package]] name = "cpp_synmap" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897e4f9cdbe2874edd3ffe53718ee5d8b89e2a970057b2c93d3214104f2e90b6" dependencies = [ - "cpp_syn 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", - "cpp_synom 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", - "memchr 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "cpp_syn", + "cpp_synom", + "memchr 1.0.2", ] [[package]] name = "cpp_synom" version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc8da5694233b646150c785118f77835ad0a49680c7f312a10ef30957c67b6d" dependencies = [ - "unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.0.4", ] [[package]] name = "criterion" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab327ed7354547cc2ef43cbe20ef68b988e70b4b593cbd66a2a61733123a3d23" dependencies = [ - "atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", - "cast 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "clap 2.33.1 (registry+https://github.com/rust-lang/crates.io-index)", - "criterion-plot 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", - "csv 1.1.3 (registry+https://github.com/rust-lang/crates.io-index)", - "itertools 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", - "oorandom 11.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "plotters 0.2.15 (registry+https://github.com/rust-lang/crates.io-index)", - "rayon 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.3.9 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_cbor 0.11.1 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_derive 1.0.113 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.56 (registry+https://github.com/rust-lang/crates.io-index)", - "tinytemplate 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "walkdir 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "atty", + "cast", + "clap", + "criterion-plot", + "csv", + "itertools 0.10.0", + "lazy_static", + "num-traits", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_cbor", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", ] [[package]] name = "criterion-plot" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022feadec601fba1649cfa83586381a4ad31c6bf3a9ab7d408118b05dd9889d" dependencies = [ - "cast 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "itertools 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cast", + "itertools 0.9.0", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dca26ee1f8d361640700bde38b2c37d8c22b3ce2d360e1fc1c74ea4b0aa7d775" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.7.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94af6efb46fef72616855b036a624cf27ba656ffc9be1b9a3c931cfc7749a9a9" dependencies = [ - "crossbeam-epoch 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", - "crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", - "maybe-uninit 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 1.0.0", + "crossbeam-epoch", + "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" -version = "0.8.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2584f639eb95fea8c798496315b297cf81b9b58b6d30ab066a75455333cf4b12" dependencies = [ - "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "maybe-uninit 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "memoffset 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", - "scopeguard 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "crossbeam-queue" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", - "maybe-uninit 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 1.0.0", + "crossbeam-utils", + "lazy_static", + "memoffset", + "scopeguard", ] [[package]] name = "crossbeam-utils" -version = "0.7.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9d99fa91428effe99c5c6d4634cdeba32b8cf784fc428a2a687f61a952c49" dependencies = [ - "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "autocfg", + "cfg-if 1.0.0", + "lazy_static", ] [[package]] name = "csv" -version = "1.1.3" +version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1" dependencies = [ - "bstr 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)", - "csv-core 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "itoa 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", - "ryu 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)", + "bstr", + "csv-core", + "itoa", + "ryu", + "serde", ] [[package]] name = "csv-core" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" dependencies = [ - "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 2.3.4", ] +[[package]] +name = "custom_derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8ae57c4978a2acd8b869ce6b9ca1dfe817bff704c220209fdef2c0b75a01b9" + [[package]] name = "data-encoding" version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f47ca1860a761136924ddd2422ba77b2ea54fe8cc75b9040804a0d9d32ad97" [[package]] name = "digest" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5b29bf156f3f4b3c4f610a25ff69370616ae6e0657d416de22645483e72af0a" dependencies = [ - "generic-array 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)", + "generic-array", ] [[package]] name = "dunce" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2641c4a7c0c4101df53ea572bffdc561c146f6c2eb09e4df02bc4811e3feeb4" [[package]] name = "either" -version = "1.5.3" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" [[package]] name = "env_logger" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" dependencies = [ - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.3.9 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "failure" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "backtrace 0.3.30 (registry+https://github.com/rust-lang/crates.io-index)", - "failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "failure_derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", - "synstructure 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", + "log", + "regex", ] [[package]] name = "fake-simd" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" + +[[package]] +name = "file_diff" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31a7a908b8f32538a2143e59a6e4e2508988832d5d4d6f7c156b3cbc762643a5" [[package]] name = "filetime" -version = "0.2.10" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8" dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 1.0.0", + "libc", + "redox_syscall 0.2.5", + "winapi 0.3.9", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "fs_extra" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394" [[package]] name = "fuchsia-cprng" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" [[package]] name = "generic-array" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2297fb0e3ea512e380da24b52dca3924028f59df5e3a17a18f81d8349ca7ebe" dependencies = [ - "nodrop 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", - "typenum 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)", + "nodrop", + "typenum", ] [[package]] name = "getopts" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" dependencies = [ - "unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-width", ] [[package]] name = "getrandom" -version = "0.1.14" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 1.0.0", + "libc", + "wasi", ] +[[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" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + +[[package]] +name = "globset" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c152169ef1e421390738366d2f796655fec62621dabbd0fd476f905934061e4a" +dependencies = [ + "aho-corasick", + "bstr", + "fnv", + "log", + "regex", +] [[package]] name = "half" -version = "1.6.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62aca2aba2d62b4a7f5b33f3712cb1b0692779a56fb510499d5c0aa594daeaf3" + +[[package]] +name = "heck" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac" +dependencies = [ + "unicode-segmentation", +] [[package]] name = "hermit-abi" -version = "0.1.14" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" dependencies = [ - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", ] [[package]] name = "hex" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6a22814455d41612f41161581c2883c0c6a1c41852729b17d5ed88f01e153aa" [[package]] name = "hostname" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" dependencies = [ - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "match_cfg 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", + "match_cfg", + "winapi 0.3.9", ] +[[package]] +name = "if_rust_version" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46dbcb333e86939721589d25a3557e180b52778cb33c7fdfe9e0158ff790d5ec" + [[package]] name = "ioctl-sys" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "isatty" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", -] +checksum = "5e2c4b26352496eaaa8ca7cfa9bd99e93419d3f7983dc6e99c2a35fe9e33504a" [[package]] name = "itertools" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484" dependencies = [ - "either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "either", ] [[package]] name = "itertools" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" dependencies = [ - "either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "either", +] + +[[package]] +name = "itertools" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d572918e350e82412fe766d24b15e6682fb2ed2bbe018280caa810397cb319" +dependencies = [ + "either", ] [[package]] name = "itoa" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" [[package]] name = "js-sys" -version = "0.3.42" +version = "0.3.50" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d99f9e3e84b8f67f846ef5b4cbbc3b1c29f6c759fcbce6f01aa0e73d932a24c" dependencies = [ - "wasm-bindgen 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen", ] [[package]] name = "kernel32-sys" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" dependencies = [ - "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8", + "winapi-build", ] [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.66" +version = "0.2.85" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ccac4b00700875e6a07c6cde370d44d32fa01c5a65cdd2fca6858c479d28bb3" [[package]] name = "log" -version = "0.4.8" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 1.0.0", ] [[package]] name = "match_cfg" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" [[package]] name = "maybe-uninit" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" [[package]] name = "md5" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79c56d6a0b07f9e19282511c83fc5b086364cbae4ba8c7d5f190c3d9b0425a48" [[package]] name = "memchr" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "148fab2e51b4f1cfc66da2a7c32981d1d3c083a803978268bb11fe4b86925e7a" dependencies = [ - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", ] [[package]] name = "memchr" -version = "2.3.3" +version = "2.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" [[package]] name = "memoffset" -version = "0.5.5" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83fb6581e8ed1f85fd45c116db8405483899489e38406156c25eb743554361d" dependencies = [ - "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "nix" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "autocfg", ] [[package]] name = "nix" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dbdc256eaac2e3bd236d93ad999d3479ef775c863dbda3068c4006a92eec51b" dependencies = [ - "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "cc 1.0.54 (registry+https://github.com/rust-lang/crates.io-index)", - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags", + "cc", + "cfg-if 0.1.10", + "libc", + "void", +] + +[[package]] +name = "nix" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" +dependencies = [ + "bitflags", + "cc", + "cfg-if 1.0.0", + "libc", ] [[package]] name = "nodrop" version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" [[package]] name = "num-integer" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" dependencies = [ - "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", + "autocfg", + "num-traits", ] [[package]] name = "num-traits" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" dependencies = [ - "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "autocfg", ] [[package]] name = "num_cpus" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" dependencies = [ - "hermit-abi 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", + "hermit-abi", + "libc", ] [[package]] name = "number_prefix" -version = "0.2.8" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "num-traits 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", -] +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "numtoa" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" [[package]] name = "onig" version = "4.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8518fcb2b1b8c2f45f0ad499df4fda6087fc3475ca69a185c173b8315d2fb383" dependencies = [ - "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "onig_sys 69.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags", + "lazy_static", + "libc", + "onig_sys", ] [[package]] name = "onig_sys" version = "69.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388410bf5fa341f10e58e6db3975f4bea1ac30247dd79d37a9e5ced3cb4cc3b0" dependencies = [ - "cc 1.0.54 (registry+https://github.com/rust-lang/crates.io-index)", - "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)", + "cc", + "pkg-config", ] [[package]] name = "oorandom" -version = "11.1.2" +version = "11.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" [[package]] name = "paste" version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45ca20c77d80be666aef2b45486da86238fabe33e38306bd3118fe4af33fa880" dependencies = [ - "paste-impl 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro-hack 0.5.16 (registry+https://github.com/rust-lang/crates.io-index)", + "paste-impl", + "proc-macro-hack", ] [[package]] name = "paste-impl" version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a7db200b97ef370c8e6de0088252f7e0dfff7d047a28528e47456c0fc98b6" dependencies = [ - "proc-macro-hack 0.5.16 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro-hack", ] [[package]] name = "pkg-config" -version = "0.3.17" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" [[package]] name = "platform-info" -version = "0.0.1" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16ea9cd21d89bffb387b6c7363d23bead0807be9de676c671b474dd29e7436d3" dependencies = [ - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", + "winapi 0.3.9", ] [[package]] name = "plotters" -version = "0.2.15" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45ca0ae5f169d0917a7c7f5a9c1a3d3d9598f18f529dd2b8373ed988efea307a" dependencies = [ - "js-sys 0.3.42 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", - "web-sys 0.3.42 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b07fffcddc1cb3a1de753caa4e4df03b79922ba43cf882acc1bdd7e8df9f4590" + +[[package]] +name = "plotters-svg" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b38a02e23bd9604b842a812063aec4ef702b57989c37b655254bb61c471ad211" +dependencies = [ + "plotters-backend", ] [[package]] name = "ppv-lite86" -version = "0.2.8" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" [[package]] name = "proc-macro-hack" -version = "0.5.16" +version = "0.5.19" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" [[package]] name = "proc-macro2" -version = "1.0.18" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec" dependencies = [ - "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.2.1", ] [[package]] name = "quick-error" version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quickcheck" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44883e74aa97ad63db83c4bf8ca490f02b2fc02f92575e720c8551e843c945f" dependencies = [ - "env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "env_logger", + "log", + "rand 0.7.3", + "rand_core 0.5.1", ] [[package]] name = "quote" version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" [[package]] name = "quote" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" dependencies = [ - "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi 0.3.9", ] [[package]] name = "rand" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c618c47cd3ebd209790115ab837de41425723956ad3ce2e6a7f09890947cacb9" dependencies = [ - "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", - "fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rand" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_hc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_isaac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_jitter 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_pcg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_xorshift 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "cloudabi", + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "winapi 0.3.9", ] [[package]] name = "rand" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" dependencies = [ - "getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_pcg 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rand_chacha" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "getrandom", + "libc", + "rand_chacha", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", ] [[package]] name = "rand_chacha" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" dependencies = [ - "ppv-lite86 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "ppv-lite86", + "rand_core 0.5.1", ] [[package]] name = "rand_core" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" dependencies = [ - "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.4.2", ] [[package]] name = "rand_core" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" [[package]] name = "rand_core" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" dependencies = [ - "getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rand_hc" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "getrandom", ] [[package]] name = "rand_hc" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" dependencies = [ - "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rand_isaac" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rand_jitter" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rand_os" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", - "fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rand_pcg" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.5.1", ] [[package]] name = "rand_pcg" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" dependencies = [ - "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rand_xorshift" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.5.1", ] [[package]] name = "rayon" -version = "1.3.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b0d8e0819fadc20c74ea8373106ead0600e3a67ef1fe8da56e39b9ae7275674" dependencies = [ - "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "crossbeam-deque 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", - "either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)", - "rayon-core 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "autocfg", + "crossbeam-deque", + "either", + "rayon-core", ] [[package]] name = "rayon-core" -version = "1.7.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ab346ac5921dc62ffa9f89b7a773907511cdfa5490c572ae9be1be33e8afa4a" dependencies = [ - "crossbeam-deque 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", - "crossbeam-queue 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "num_cpus 1.13.0 (registry+https://github.com/rust-lang/crates.io-index)", + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "lazy_static", + "num_cpus", ] [[package]] name = "rdrand" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" dependencies = [ - "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.3.1", ] [[package]] name = "redox_syscall" -version = "0.1.56" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + +[[package]] +name = "redox_syscall" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9" +dependencies = [ + "bitflags", +] [[package]] name = "redox_termios" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f" dependencies = [ - "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.2.5", ] [[package]] name = "regex" -version = "1.3.9" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957056ecddbeba1b26965114e191d2e8589ce74db242b6ea25fc4062427a5c19" dependencies = [ - "aho-corasick 0.7.10 (registry+https://github.com/rust-lang/crates.io-index)", - "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "regex-syntax 0.6.18 (registry+https://github.com/rust-lang/crates.io-index)", - "thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "aho-corasick", + "memchr 2.3.4", + "regex-syntax", ] [[package]] name = "regex-automata" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1ded71d66a4a97f5e961fd0cb25a5f366a42a41570d16a763a69c092c26ae4" dependencies = [ - "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder", ] [[package]] name = "regex-syntax" -version = "0.6.18" +version = "0.6.23" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5f089152e60f62d28b835fbff2cd2e8dc0baf1ac13343bef92ab7eed84548" [[package]] name = "remove_dir_all" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" dependencies = [ - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.9", ] +[[package]] +name = "retain_mut" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53552c6c49e1e13f1a203ef0080ab3bbef0beb570a528993e83df057a9d9bba1" + [[package]] name = "rust-ini" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "rustc-demangle" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e52c148ef37f8c375d49d5a73aa70713125b7f19095948a923f80afdeb22ec2" [[package]] name = "rustc_version" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" dependencies = [ - "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "semver", ] [[package]] name = "ryu" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" [[package]] name = "same-file" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ - "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-util", ] [[package]] name = "scopeguard" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "semver" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" dependencies = [ - "semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "semver-parser", ] [[package]] name = "semver-parser" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.114" +version = "1.0.125" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171" [[package]] name = "serde_cbor" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e18acfa2f90e8b735b2836ab8d538de304cbb6729a7360729ea5a895d15a622" dependencies = [ - "half 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)", + "half", + "serde", ] [[package]] name = "serde_derive" -version = "1.0.113" +version = "1.0.125" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b093b7a2bb58203b5da3056c05b4ec1fed827dcfdb37347a8841695263b3d06d" dependencies = [ - "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote 1.0.9", + "syn", ] [[package]] name = "serde_json" -version = "1.0.56" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" dependencies = [ - "itoa 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", - "ryu 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)", + "itoa", + "ryu", + "serde", ] [[package]] name = "sha1" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" [[package]] name = "sha2" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d963c78ce367df26d7ea8b8cc655c651b42e8a1e584e869c1e17dae3ccb116a" dependencies = [ - "block-buffer 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "byte-tools 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "digest 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", - "fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "generic-array 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)", + "block-buffer", + "byte-tools", + "digest", + "fake-simd", + "generic-array", ] [[package]] name = "sha3" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26405905b6a56a94c60109cfda62610507ac14a65be531f5767dec5c5a8dd6a0" dependencies = [ - "block-buffer 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "byte-tools 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "digest 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", - "generic-array 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)", + "block-buffer", + "byte-tools", + "digest", + "generic-array", ] +[[package]] +name = "smallvec" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0" +dependencies = [ + "maybe-uninit", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] -name = "syn" -version = "0.11.11" +name = "strum" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7318c509b5ba57f18533982607f24070a55d353e90d4cae30c467cdb2ad5ac5c" + +[[package]] +name = "strum_macros" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8bc6b87a5112aeeab1f4a9f7ab634fe6cbefc4850006df31267f4cfb9e3149" dependencies = [ - "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", - "synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "heck", + "proc-macro2", + "quote 1.0.9", + "syn", ] [[package]] name = "syn" -version = "1.0.31" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ce15dd3ed8aa2f8eeac4716d6ef5ab58b6b9256db41d7e1a0224c2788e8fd87" dependencies = [ - "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote 1.0.9", + "unicode-xid 0.2.1", ] [[package]] -name = "synom" -version = "0.11.3" +name = "tempdir" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" dependencies = [ - "unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "synstructure" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.4.6", + "remove_dir_all", ] [[package]] name = "tempfile" version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", - "remove_dir_all 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.10", + "libc", + "rand 0.7.3", + "redox_syscall 0.1.57", + "remove_dir_all", + "winapi 0.3.9", ] [[package]] name = "term_grid" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230d3e804faaed5a39b08319efb797783df2fd9671b39b7596490cb486d702cf" dependencies = [ - "unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-width", ] [[package]] name = "term_size" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4129646ca0ed8f45d09b929036bafad5377103edd06e50bf574b353d2b08d9" dependencies = [ - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", + "winapi 0.3.9", ] [[package]] name = "termion" -version = "1.5.5" +version = "1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e" dependencies = [ - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", + "numtoa", + "redox_syscall 0.2.5", + "redox_termios", ] [[package]] name = "termsize" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e86d824a8e90f342ad3ef4bd51ef7119a9b681b0cc9f8ee7b2852f02ccd2517" dependencies = [ - "atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", - "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "termion 1.5.5 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "atty", + "kernel32-sys", + "libc", + "termion", + "winapi 0.2.8", ] [[package]] name = "textwrap" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" dependencies = [ - "term_size 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "term_size", + "unicode-width", ] [[package]] -name = "thread_local" -version = "1.0.1" +name = "thiserror" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e" dependencies = [ - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0" +dependencies = [ + "proc-macro2", + "quote 1.0.9", + "syn", ] [[package]] name = "time" version = "0.1.42" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f" dependencies = [ - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", + "redox_syscall 0.1.57", + "winapi 0.3.9", ] [[package]] name = "tinytemplate" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" dependencies = [ - "serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.56 (registry+https://github.com/rust-lang/crates.io-index)", + "serde", + "serde_json", +] + +[[package]] +name = "twox-hash" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f8ab788026715fa63b31960869617cba39117e520eb415b0139543e325ab59" +dependencies = [ + "cfg-if 0.1.10", + "rand 0.7.3", + "static_assertions", ] [[package]] name = "typenum" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" + +[[package]] +name = "unicode-segmentation" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" [[package]] name = "unicode-width" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" [[package]] name = "unicode-xid" version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" [[package]] name = "unicode-xid" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" [[package]] name = "unindent" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f14ee04d9415b52b3aeab06258a3f07093182b88ba0f9b8d203f211a7a7d41c7" [[package]] name = "unix_socket" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aa2700417c405c38f5e6902d699345241c28c0b7ade4abaad71e35a87eb1564" dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.10", + "libc", ] [[package]] name = "users" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4227e95324a443c9fcb06e03d4d85e91aabe9a5a02aa818688b6918b6af486" dependencies = [ - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", + "log", ] [[package]] name = "uu_arch" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "platform-info 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "platform-info", + "uucore", + "uucore_procs", ] [[package]] name = "uu_base32" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "uucore", + "uucore_procs", ] [[package]] name = "uu_base64" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "uucore", + "uucore_procs", ] [[package]] name = "uu_basename" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "uucore", + "uucore_procs", ] [[package]] name = "uu_cat" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "quick-error 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "unix_socket 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "quick-error", + "unix_socket", + "uucore", + "uucore_procs", ] [[package]] name = "uu_chgrp" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "walkdir 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "uucore", + "uucore_procs", + "walkdir", ] [[package]] name = "uu_chmod" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "walker 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "clap", + "libc", + "uucore", + "uucore_procs", + "walkdir", ] [[package]] name = "uu_chown" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "glob 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "walkdir 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "clap", + "glob 0.3.0", + "uucore", + "uucore_procs", + "walkdir", ] [[package]] name = "uu_chroot" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "uucore", + "uucore_procs", ] [[package]] name = "uu_cksum" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "libc", + "uucore", + "uucore_procs", ] [[package]] name = "uu_comm" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "libc", + "uucore", + "uucore_procs", ] [[package]] name = "uu_cp" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "clap 2.33.1 (registry+https://github.com/rust-lang/crates.io-index)", - "filetime 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", - "ioctl-sys 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "quick-error 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "walkdir 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", - "xattr 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "clap", + "filetime", + "ioctl-sys", + "libc", + "quick-error", + "uucore", + "uucore_procs", + "walkdir", + "winapi 0.3.9", + "xattr", +] + +[[package]] +name = "uu_csplit" +version = "0.0.6" +dependencies = [ + "clap", + "glob 0.2.11", + "regex", + "thiserror", + "uucore", + "uucore_procs", ] [[package]] name = "uu_cut" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "uucore", + "uucore_procs", ] [[package]] name = "uu_date" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", - "clap 2.33.1 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "chrono", + "clap", + "libc", + "uucore", + "uucore_procs", + "winapi 0.3.9", ] [[package]] name = "uu_df" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "clap 2.33.1 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "number_prefix 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "clap", + "libc", + "number_prefix", + "uucore", + "uucore_procs", + "winapi 0.3.9", ] [[package]] name = "uu_dircolors" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "glob 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "glob 0.3.0", + "uucore", + "uucore_procs", ] [[package]] name = "uu_dirname" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "libc", + "uucore", + "uucore_procs", ] [[package]] name = "uu_du" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "time", + "uucore", + "uucore_procs", + "winapi 0.3.9", ] [[package]] name = "uu_echo" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "uucore", + "uucore_procs", ] [[package]] name = "uu_env" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "clap 2.33.1 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "rust-ini 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "libc", + "rust-ini", + "uucore", + "uucore_procs", ] [[package]] name = "uu_expand" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "unicode-width", + "uucore", + "uucore_procs", ] [[package]] name = "uu_expr" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "onig 4.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "libc", + "onig", + "uucore", + "uucore_procs", ] [[package]] name = "uu_factor" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "criterion 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", - "paste 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", - "quickcheck 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "criterion", + "num-traits", + "paste", + "quickcheck", + "rand 0.7.3", + "rand_chacha", + "smallvec", + "uucore", + "uucore_procs", ] [[package]] name = "uu_false" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "uucore", + "uucore_procs", ] [[package]] name = "uu_fmt" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "libc", + "unicode-width", + "uucore", + "uucore_procs", ] [[package]] name = "uu_fold" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "uucore", + "uucore_procs", ] [[package]] name = "uu_groups" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "clap 2.33.1 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "uucore", + "uucore_procs", ] [[package]] name = "uu_hashsum" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "clap 2.33.1 (registry+https://github.com/rust-lang/crates.io-index)", - "digest 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", - "hex 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "md5 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.3.9 (registry+https://github.com/rust-lang/crates.io-index)", - "regex-syntax 0.6.18 (registry+https://github.com/rust-lang/crates.io-index)", - "sha1 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", - "sha2 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", - "sha3 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "blake2-rfc", + "clap", + "digest", + "hex", + "libc", + "md5", + "regex", + "regex-syntax", + "sha1", + "sha2", + "sha3", + "uucore", + "uucore_procs", ] [[package]] name = "uu_head" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "uucore", + "uucore_procs", ] [[package]] name = "uu_hostid" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "libc", + "uucore", + "uucore_procs", ] [[package]] name = "uu_hostname" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "clap 2.33.1 (registry+https://github.com/rust-lang/crates.io-index)", - "hostname 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "clap", + "hostname", + "libc", + "uucore", + "uucore_procs", + "winapi 0.3.9", ] [[package]] name = "uu_id" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "uucore", + "uucore_procs", ] [[package]] name = "uu_install" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "file_diff", + "filetime", + "libc", + "time", + "uucore", + "uucore_procs", ] [[package]] name = "uu_join" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "clap 2.33.1 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "uucore", + "uucore_procs", ] [[package]] name = "uu_kill" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "libc", + "uucore", + "uucore_procs", ] [[package]] name = "uu_link" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "libc", + "uucore", + "uucore_procs", ] [[package]] name = "uu_ln" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "libc", + "uucore", + "uucore_procs", ] [[package]] name = "uu_logname" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "libc", + "uucore", + "uucore_procs", ] [[package]] name = "uu_ls" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "isatty 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "number_prefix 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", - "term_grid 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", - "termsize 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", - "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "atty", + "clap", + "globset", + "lazy_static", + "number_prefix", + "term_grid", + "termsize", + "time", + "unicode-width", + "uucore", + "uucore_procs", ] [[package]] name = "uu_mkdir" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "libc", + "uucore", + "uucore_procs", ] [[package]] name = "uu_mkfifo" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "libc", + "uucore", + "uucore_procs", ] [[package]] name = "uu_mknod" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "getopts", + "libc", + "uucore", + "uucore_procs", ] [[package]] name = "uu_mktemp" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", - "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "rand 0.5.6", + "tempfile", + "uucore", + "uucore_procs", ] [[package]] name = "uu_more" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "nix 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "nix 0.13.1", + "redox_syscall 0.1.57", + "redox_termios", + "uucore", + "uucore_procs", ] [[package]] name = "uu_mv" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "fs_extra 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "fs_extra", + "uucore", + "uucore_procs", ] [[package]] name = "uu_nice" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "libc", + "nix 0.13.1", + "uucore", + "uucore_procs", ] [[package]] name = "uu_nl" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "aho-corasick 0.7.10 (registry+https://github.com/rust-lang/crates.io-index)", - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.3.9 (registry+https://github.com/rust-lang/crates.io-index)", - "regex-syntax 0.6.18 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "aho-corasick", + "clap", + "libc", + "memchr 2.3.4", + "regex", + "regex-syntax", + "uucore", + "uucore_procs", ] [[package]] name = "uu_nohup" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "libc", + "uucore", + "uucore_procs", ] [[package]] name = "uu_nproc" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "clap 2.33.1 (registry+https://github.com/rust-lang/crates.io-index)", - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "num_cpus 1.13.0 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "libc", + "num_cpus", + "uucore", + "uucore_procs", ] [[package]] name = "uu_numfmt" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "uucore", + "uucore_procs", ] [[package]] name = "uu_od" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "half 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "byteorder", + "clap", + "half", + "libc", + "uucore", + "uucore_procs", ] [[package]] name = "uu_paste" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "uucore", + "uucore_procs", ] [[package]] name = "uu_pathchk" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "libc", + "uucore", + "uucore_procs", ] [[package]] name = "uu_pinky" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "uucore", + "uucore_procs", ] [[package]] name = "uu_printenv" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "uucore", + "uucore_procs", ] [[package]] name = "uu_printf" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "itertools 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "itertools 0.8.2", + "uucore", + "uucore_procs", ] [[package]] name = "uu_ptx" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "aho-corasick 0.7.10 (registry+https://github.com/rust-lang/crates.io-index)", - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.3.9 (registry+https://github.com/rust-lang/crates.io-index)", - "regex-syntax 0.6.18 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "aho-corasick", + "clap", + "libc", + "memchr 2.3.4", + "regex", + "regex-syntax", + "uucore", + "uucore_procs", ] [[package]] name = "uu_pwd" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "uucore", + "uucore_procs", ] [[package]] name = "uu_readlink" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "libc", + "uucore", + "uucore_procs", ] [[package]] name = "uu_realpath" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "uucore", + "uucore_procs", ] [[package]] name = "uu_relpath" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "uucore", + "uucore_procs", ] [[package]] name = "uu_rm" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "remove_dir_all 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "walkdir 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "clap", + "remove_dir_all", + "uucore", + "uucore_procs", + "walkdir", ] [[package]] name = "uu_rmdir" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "uucore", + "uucore_procs", ] [[package]] name = "uu_seq" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "uucore", + "uucore_procs", ] [[package]] name = "uu_shred" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "filetime 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", - "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "filetime", + "libc", + "rand 0.5.6", + "time", + "uucore", + "uucore_procs", ] [[package]] name = "uu_shuf" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "rand 0.5.6", + "uucore", + "uucore_procs", ] [[package]] name = "uu_sleep" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "uucore", + "uucore_procs", ] [[package]] name = "uu_sort" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "itertools 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", - "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "itertools 0.8.2", + "rand 0.7.3", + "semver", + "twox-hash", + "uucore", + "uucore_procs", ] [[package]] name = "uu_split" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "uucore", + "uucore_procs", ] [[package]] name = "uu_stat" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "time", + "uucore", + "uucore_procs", ] [[package]] name = "uu_stdbuf" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "uu_stdbuf_libstdbuf 0.0.1", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "getopts", + "tempfile", + "uu_stdbuf_libstdbuf", + "uucore", + "uucore_procs", ] [[package]] name = "uu_stdbuf_libstdbuf" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "cpp 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "cpp_build 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "cpp", + "cpp_build", + "libc", + "uucore", + "uucore_procs", ] [[package]] name = "uu_sum" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "uucore", + "uucore_procs", ] [[package]] name = "uu_sync" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "clap", + "libc", + "uucore", + "uucore_procs", + "winapi 0.3.9", ] [[package]] name = "uu_tac" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "uucore", + "uucore_procs", ] [[package]] name = "uu_tail" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "clap", + "libc", + "redox_syscall 0.1.57", + "uucore", + "uucore_procs", + "winapi 0.3.9", ] [[package]] name = "uu_tee" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "libc", + "retain_mut", + "uucore", + "uucore_procs", ] [[package]] name = "uu_test" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "libc", + "redox_syscall 0.1.57", + "uucore", + "uucore_procs", ] [[package]] name = "uu_timeout" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "getopts", + "libc", + "uucore", + "uucore_procs", ] [[package]] name = "uu_touch" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "filetime 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "filetime", + "time", + "uucore", + "uucore_procs", ] [[package]] name = "uu_tr" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "bit-set 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", - "fnv 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "bit-set", + "clap", + "fnv", + "uucore", + "uucore_procs", ] [[package]] name = "uu_true" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "uucore", + "uucore_procs", ] [[package]] name = "uu_truncate" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "uucore", + "uucore_procs", ] [[package]] name = "uu_tsort" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "uucore", + "uucore_procs", ] [[package]] name = "uu_tty" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "libc", + "uucore", + "uucore_procs", ] [[package]] name = "uu_uname" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "clap 2.33.1 (registry+https://github.com/rust-lang/crates.io-index)", - "platform-info 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "platform-info", + "uucore", + "uucore_procs", ] [[package]] name = "uu_unexpand" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "unicode-width", + "uucore", + "uucore_procs", ] [[package]] name = "uu_uniq" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "strum", + "strum_macros", + "uucore", + "uucore_procs", ] [[package]] name = "uu_unlink" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "getopts", + "libc", + "uucore", + "uucore_procs", ] [[package]] name = "uu_uptime" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", - "clap 2.33.1 (registry+https://github.com/rust-lang/crates.io-index)", - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "chrono", + "clap", + "uucore", + "uucore_procs", ] [[package]] name = "uu_users" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "uucore", + "uucore_procs", ] [[package]] name = "uu_wc" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "libc", + "nix 0.20.0", + "thiserror", + "uucore", + "uucore_procs", ] [[package]] name = "uu_who" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "uucore", + "uucore_procs", ] [[package]] name = "uu_whoami" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "advapi32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "clap 2.33.1 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "advapi32-sys", + "clap", + "uucore", + "uucore_procs", + "winapi 0.3.9", ] [[package]] name = "uu_yes" -version = "0.0.1" +version = "0.0.6" dependencies = [ - "clap 2.33.1 (registry+https://github.com/rust-lang/crates.io-index)", - "uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", - "uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)", + "clap", + "uucore", + "uucore_procs", ] [[package]] name = "uucore" -version = "0.0.4" -source = "git+https://github.com/uutils/uucore.git?branch=canary#869573459f00ba0b4af9f7d828370c105f31a94e" +version = "0.0.8" dependencies = [ - "backtrace 0.3.30 (registry+https://github.com/rust-lang/crates.io-index)", - "data-encoding 2.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "dunce 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "failure 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "nix 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)", - "platform-info 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "termion 1.5.5 (registry+https://github.com/rust-lang/crates.io-index)", - "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", - "wild 2.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "data-encoding", + "dunce", + "getopts", + "lazy_static", + "libc", + "nix 0.13.1", + "platform-info", + "termion", + "thiserror", + "time", + "wild", ] [[package]] name = "uucore_procs" -version = "0.0.4" -source = "git+https://github.com/uutils/uucore.git?branch=canary#869573459f00ba0b4af9f7d828370c105f31a94e" +version = "0.0.5" dependencies = [ - "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote 1.0.9", + "syn", ] [[package]] name = "vec_map" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" [[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.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" dependencies = [ - "same-file 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "same-file", + "winapi 0.3.9", + "winapi-util", ] -[[package]] -name = "walker" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" [[package]] name = "wasm-bindgen" -version = "0.2.65" +version = "0.2.73" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83240549659d187488f91f33c0f8547cbfef0b2088bc470c116d1d260ef623d9" dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-macro 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 1.0.0", + "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.65" +version = "0.2.73" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae70622411ca953215ca6d06d3ebeb1e915f0f6613e3b495122878d7ebec7dae" dependencies = [ - "bumpalo 3.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-shared 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote 1.0.9", + "syn", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.65" +version = "0.2.73" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e734d91443f177bfdb41969de821e15c516931c3c3db3d318fa1b68975d0f6f" dependencies = [ - "quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-macro-support 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.9", + "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.65" +version = "0.2.73" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53739ff08c8a68b0fdbcd54c372b8ab800b1449ab3c9d706503bc7dd1621b2c" dependencies = [ - "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-backend 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-shared 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote 1.0.9", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.65" +version = "0.2.73" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9a543ae66aa233d14bb765ed9af4a33e81b8b58d1584cf1b47ff8cd0b9e4489" [[package]] name = "web-sys" -version = "0.3.42" +version = "0.3.50" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a905d57e488fec8861446d3393670fb50d27a262344013181c2cdf9fff5481be" dependencies = [ - "js-sys 0.3.42 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", + "js-sys", + "wasm-bindgen", ] [[package]] name = "wild" version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "035793abb854745033f01a07647a79831eba29ec0be377205f2a25b0aa830020" dependencies = [ - "glob 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "glob 0.3.0", ] [[package]] name = "winapi" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" [[package]] name = "winapi" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ - "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-build" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.2" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" dependencies = [ - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.9", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "xattr" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c" dependencies = [ - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", ] - -[metadata] -"checksum advapi32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e06588080cb19d0acb6739808aafa5f26bfb2ca015b2b6370028b44cf7cb8a9a" -"checksum aho-corasick 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)" = "81ce3d38065e618af2d7b77e10c5ad9a069859b4be3c2250f674af3840d9c8a5" -"checksum aho-corasick 0.7.10 (registry+https://github.com/rust-lang/crates.io-index)" = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada" -"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" -"checksum atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -"checksum autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" -"checksum autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" -"checksum backtrace 0.3.30 (registry+https://github.com/rust-lang/crates.io-index)" = "ada4c783bb7e7443c14e0480f429ae2cc99da95065aeab7ee1b81ada0419404f" -"checksum backtrace-sys 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "18fbebbe1c9d1f383a9cc7e8ccdb471b91c8d024ee9c2ca5b5346121fe8b4399" -"checksum bit-set 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de" -"checksum bit-vec 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5f0dc55f2d8a1a85650ac47858bb001b4c0dd73d79e3c455a842925e68d29cd3" -"checksum bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d" -"checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" -"checksum block-buffer 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1339a1042f5d9f295737ad4d9a6ab6bf81c84a933dba110b9200cd6d1448b814" -"checksum bstr 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "31accafdb70df7871592c058eca3985b71104e15ac32f64706022c58867da931" -"checksum bumpalo 3.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" -"checksum byte-tools 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "560c32574a12a89ecd91f5e742165893f86e3ab98d21f8ea548658eb9eef5f40" -"checksum byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" -"checksum cast 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "4b9434b9a5aa1450faa3f9cb14ea0e8c53bb5d2b3c1bfd1ab4fc03e9f33fbfb0" -"checksum cc 1.0.54 (registry+https://github.com/rust-lang/crates.io-index)" = "7bbb73db36c1246e9034e307d0fba23f9a2e251faa47ade70c1bd252220c8311" -"checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" -"checksum chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "80094f509cf8b5ae86a4966a39b3ff66cd7e2a3e594accec3743ff3fabeab5b2" -"checksum clap 2.33.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bdfa80d47f954d53a35a64987ca1422f495b8d6483c0fe9f7117b36c2a792129" -"checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" -"checksum cpp 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2d1cd8699ffa1b18fd388183f7762e0545eddbd5c6ec95e9e3b42a4a71a507ff" -"checksum cpp_build 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c47531e7e09532ad4827098729794f5e1a5b1c2ccbb5e295498d2e7ab451c445" -"checksum cpp_common 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "79e39149a7943affa02f5b6e347ca2840a129cc78d5883ee229f0f1c4027d628" -"checksum cpp_macros 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6bba562eb4d65561efb6cef4e5f0de5936edfee7c6af7a4dfc323f6f2c997e40" -"checksum cpp_syn 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a8cd649bf5b3804d92fe12a60c7698f5a538a6033ed8a668bf5241d4d4f1644e" -"checksum cpp_synmap 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "897e4f9cdbe2874edd3ffe53718ee5d8b89e2a970057b2c93d3214104f2e90b6" -"checksum cpp_synom 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1fc8da5694233b646150c785118f77835ad0a49680c7f312a10ef30957c67b6d" -"checksum criterion 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "70daa7ceec6cf143990669a04c7df13391d55fb27bd4079d252fca774ba244d8" -"checksum criterion-plot 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e022feadec601fba1649cfa83586381a4ad31c6bf3a9ab7d408118b05dd9889d" -"checksum crossbeam-deque 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "9f02af974daeee82218205558e51ec8768b48cf524bd01d550abe5573a608285" -"checksum crossbeam-epoch 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" -"checksum crossbeam-queue 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570" -"checksum crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" -"checksum csv 1.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "00affe7f6ab566df61b4be3ce8cf16bc2576bca0963ceb0955e45d514bf9a279" -"checksum csv-core 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" -"checksum data-encoding 2.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f4f47ca1860a761136924ddd2422ba77b2ea54fe8cc75b9040804a0d9d32ad97" -"checksum digest 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e5b29bf156f3f4b3c4f610a25ff69370616ae6e0657d416de22645483e72af0a" -"checksum dunce 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b2641c4a7c0c4101df53ea572bffdc561c146f6c2eb09e4df02bc4811e3feeb4" -"checksum either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" -"checksum env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" -"checksum failure 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "934799b6c1de475a012a02dab0ace1ace43789ee4b99bcfbf1a2e3e8ced5de82" -"checksum failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c7cdda555bb90c9bb67a3b670a0f42de8e73f5981524123ad8578aafec8ddb8b" -"checksum fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" -"checksum filetime 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "affc17579b132fc2461adf7c575cc6e8b134ebca52c51f5411388965227dc695" -"checksum fnv 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -"checksum fs_extra 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5f2a4a2034423744d2cc7ca2068453168dcdb82c438419e639a26bd87839c674" -"checksum fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" -"checksum generic-array 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)" = "fceb69994e330afed50c93524be68c42fa898c2d9fd4ee8da03bd7363acd26f2" -"checksum getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)" = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" -"checksum getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" -"checksum glob 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" -"checksum half 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d36fab90f82edc3c747f9d438e06cf0a491055896f2a279638bb5beed6c40177" -"checksum hermit-abi 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "b9586eedd4ce6b3c498bc3b4dd92fc9f11166aa908a914071953768066c67909" -"checksum hex 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d6a22814455d41612f41161581c2883c0c6a1c41852729b17d5ed88f01e153aa" -"checksum hostname 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" -"checksum ioctl-sys 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5e2c4b26352496eaaa8ca7cfa9bd99e93419d3f7983dc6e99c2a35fe9e33504a" -"checksum isatty 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e31a8281fc93ec9693494da65fbf28c0c2aa60a2eaec25dc58e2f31952e95edc" -"checksum itertools 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484" -"checksum itertools 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" -"checksum itoa 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" -"checksum js-sys 0.3.42 (registry+https://github.com/rust-lang/crates.io-index)" = "52732a3d3ad72c58ad2dc70624f9c17b46ecd0943b9a4f1ee37c4c18c5d983e2" -"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" -"checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -"checksum libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)" = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558" -"checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" -"checksum match_cfg 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" -"checksum maybe-uninit 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" -"checksum md5 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "79c56d6a0b07f9e19282511c83fc5b086364cbae4ba8c7d5f190c3d9b0425a48" -"checksum memchr 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "148fab2e51b4f1cfc66da2a7c32981d1d3c083a803978268bb11fe4b86925e7a" -"checksum memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" -"checksum memoffset 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "c198b026e1bbf08a937e94c6c60f9ec4a2267f5b0d2eec9c1b21b061ce2be55f" -"checksum nix 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4dbdc256eaac2e3bd236d93ad999d3479ef775c863dbda3068c4006a92eec51b" -"checksum nix 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "47e49f6982987135c5e9620ab317623e723bd06738fd85377e8d55f57c8b6487" -"checksum nodrop 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" -"checksum num-integer 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b" -"checksum num-traits 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)" = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" -"checksum num_cpus 1.13.0 (registry+https://github.com/rust-lang/crates.io-index)" = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" -"checksum number_prefix 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "dbf9993e59c894e3c08aa1c2712914e9e6bf1fcbfc6bef283e2183df345a4fee" -"checksum numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" -"checksum onig 4.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8518fcb2b1b8c2f45f0ad499df4fda6087fc3475ca69a185c173b8315d2fb383" -"checksum onig_sys 69.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388410bf5fa341f10e58e6db3975f4bea1ac30247dd79d37a9e5ced3cb4cc3b0" -"checksum oorandom 11.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a170cebd8021a008ea92e4db85a72f80b35df514ec664b296fdcbb654eac0b2c" -"checksum paste 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)" = "45ca20c77d80be666aef2b45486da86238fabe33e38306bd3118fe4af33fa880" -"checksum paste-impl 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)" = "d95a7db200b97ef370c8e6de0088252f7e0dfff7d047a28528e47456c0fc98b6" -"checksum pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)" = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677" -"checksum platform-info 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f2fd076acdc7a98374de6e300bf3af675997225bef21aecac2219553f04dd7e8" -"checksum plotters 0.2.15 (registry+https://github.com/rust-lang/crates.io-index)" = "0d1685fbe7beba33de0330629da9d955ac75bd54f33d7b79f9a895590124f6bb" -"checksum ppv-lite86 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "237a5ed80e274dbc66f86bd59c1e25edc039660be53194b5fe0a482e0f2612ea" -"checksum proc-macro-hack 0.5.16 (registry+https://github.com/rust-lang/crates.io-index)" = "7e0456befd48169b9f13ef0f0ad46d492cf9d2dbb918bcf38e01eed4ce3ec5e4" -"checksum proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)" = "beae6331a816b1f65d04c45b078fd8e6c93e8071771f41b8163255bbd8d7c8fa" -"checksum quick-error 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" -"checksum quickcheck 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a44883e74aa97ad63db83c4bf8ca490f02b2fc02f92575e720c8551e843c945f" -"checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" -"checksum quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" -"checksum rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c618c47cd3ebd209790115ab837de41425723956ad3ce2e6a7f09890947cacb9" -"checksum rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" -"checksum rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -"checksum rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" -"checksum rand_chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -"checksum rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" -"checksum rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" -"checksum rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -"checksum rand_hc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" -"checksum rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -"checksum rand_isaac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" -"checksum rand_jitter 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" -"checksum rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" -"checksum rand_pcg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" -"checksum rand_pcg 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" -"checksum rand_xorshift 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" -"checksum rayon 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "62f02856753d04e03e26929f820d0a0a337ebe71f849801eea335d464b349080" -"checksum rayon-core 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e92e15d89083484e11353891f1af602cc661426deb9564c298b270c726973280" -"checksum rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" -"checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" -"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" -"checksum regex 1.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "9c3780fcf44b193bc4d09f36d2a3c87b251da4a046c87795a0d35f4f927ad8e6" -"checksum regex-automata 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "ae1ded71d66a4a97f5e961fd0cb25a5f366a42a41570d16a763a69c092c26ae4" -"checksum regex-syntax 0.6.18 (registry+https://github.com/rust-lang/crates.io-index)" = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" -"checksum remove_dir_all 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" -"checksum rust-ini 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3e52c148ef37f8c375d49d5a73aa70713125b7f19095948a923f80afdeb22ec2" -"checksum rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" -"checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" -"checksum ryu 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" -"checksum same-file 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "585e8ddcedc187886a30fa705c47985c3fa88d06624095856b36ca0b82ff4421" -"checksum scopeguard 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" -"checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" -"checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" -"checksum serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)" = "5317f7588f0a5078ee60ef675ef96735a1442132dc645eb1d12c018620ed8cd3" -"checksum serde_cbor 0.11.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1e18acfa2f90e8b735b2836ab8d538de304cbb6729a7360729ea5a895d15a622" -"checksum serde_derive 1.0.113 (registry+https://github.com/rust-lang/crates.io-index)" = "93c5eaa17d0954cb481cdcfffe9d84fcfa7a1a9f2349271e678677be4c26ae31" -"checksum serde_json 1.0.56 (registry+https://github.com/rust-lang/crates.io-index)" = "3433e879a558dde8b5e8feb2a04899cf34fdde1fafb894687e52105fc1162ac3" -"checksum sha1 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" -"checksum sha2 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7d963c78ce367df26d7ea8b8cc655c651b42e8a1e584e869c1e17dae3ccb116a" -"checksum sha3 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "26405905b6a56a94c60109cfda62610507ac14a65be531f5767dec5c5a8dd6a0" -"checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" -"checksum syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" -"checksum syn 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)" = "b5304cfdf27365b7585c25d4af91b35016ed21ef88f17ced89c7093b43dba8b6" -"checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" -"checksum synstructure 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3a761d12e6d8dcb4dcf952a7a89b475e3a9d69e4a69307e01a470977642914bd" -"checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" -"checksum term_grid 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "230d3e804faaed5a39b08319efb797783df2fd9671b39b7596490cb486d702cf" -"checksum term_size 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1e4129646ca0ed8f45d09b929036bafad5377103edd06e50bf574b353d2b08d9" -"checksum termion 1.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "c22cec9d8978d906be5ac94bceb5a010d885c626c4c8855721a4dbd20e3ac905" -"checksum termsize 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "5e86d824a8e90f342ad3ef4bd51ef7119a9b681b0cc9f8ee7b2852f02ccd2517" -"checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -"checksum thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" -"checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f" -"checksum tinytemplate 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6d3dc76004a03cec1c5932bca4cdc2e39aaa798e3f82363dd94f9adf6098c12f" -"checksum typenum 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" -"checksum unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" -"checksum unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" -"checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" -"checksum unindent 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "af41d708427f8fd0e915dcebb2cae0f0e6acb2a939b2d399c265c39a38a18942" -"checksum unix_socket 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6aa2700417c405c38f5e6902d699345241c28c0b7ade4abaad71e35a87eb1564" -"checksum users 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aa4227e95324a443c9fcb06e03d4d85e91aabe9a5a02aa818688b6918b6af486" -"checksum uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)" = "" -"checksum uucore_procs 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)" = "" -"checksum vec_map 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" -"checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" -"checksum walkdir 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d" -"checksum walker 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "44971d5e5ae4f7904dffb6260ebd3910e7bcae104a94730e04a24cb6af40646b" -"checksum wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)" = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" -"checksum wasm-bindgen 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)" = "f3edbcc9536ab7eababcc6d2374a0b7bfe13a2b6d562c5e07f370456b1a8f33d" -"checksum wasm-bindgen-backend 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)" = "89ed2fb8c84bfad20ea66b26a3743f3e7ba8735a69fe7d95118c33ec8fc1244d" -"checksum wasm-bindgen-macro 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)" = "eb071268b031a64d92fc6cf691715ca5a40950694d8f683c5bb43db7c730929e" -"checksum wasm-bindgen-macro-support 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)" = "cf592c807080719d1ff2f245a687cbadb3ed28b2077ed7084b47aba8b691f2c6" -"checksum wasm-bindgen-shared 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)" = "72b6c0220ded549d63860c78c38f3bcc558d1ca3f4efa74942c536ddbbb55e87" -"checksum web-sys 0.3.42 (registry+https://github.com/rust-lang/crates.io-index)" = "8be2398f326b7ba09815d0b403095f34dd708579220d099caae89be0b32137b2" -"checksum wild 2.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "035793abb854745033f01a07647a79831eba29ec0be377205f2a25b0aa830020" -"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" -"checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" -"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" -"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -"checksum winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7168bab6e1daee33b4557efd0e95d5ca70a03706d39fa5f3fe7a236f584b03c9" -"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -"checksum xattr 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c" diff --git a/Cargo.toml b/Cargo.toml index d6f97ad21..7e3fb9139 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "coreutils" -version = "0.0.1" # "0.0.1.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "coreutils ~ GNU coreutils (updated); implemented as universal (cross-platform) utils, written in Rust" @@ -37,11 +37,13 @@ feat_common_core = [ "cksum", "comm", "cp", + "csplit", "cut", "date", "df", "dircolors", "dirname", + "du", "echo", "env", "expand", @@ -148,7 +150,6 @@ feat_require_unix = [ "chmod", "chown", "chroot", - "du", "groups", "hostid", "id", @@ -225,122 +226,132 @@ test = [ "uu_test" ] [dependencies] lazy_static = { version="1.3" } textwrap = { version="=0.11.0", features=["term_size"] } # !maint: [2020-05-10; rivy] unstable crate using undocumented features; pinned currently, will review -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="src/uucore" } # * uutils -uu_test = { optional=true, version="0.0.1", package="uu_test", path="src/uu/test" } +uu_test = { optional=true, version="0.0.6", package="uu_test", path="src/uu/test" } # -arch = { optional=true, version="0.0.1", package="uu_arch", path="src/uu/arch" } -base32 = { optional=true, version="0.0.1", package="uu_base32", path="src/uu/base32" } -base64 = { optional=true, version="0.0.1", package="uu_base64", path="src/uu/base64" } -basename = { optional=true, version="0.0.1", package="uu_basename", path="src/uu/basename" } -cat = { optional=true, version="0.0.1", package="uu_cat", path="src/uu/cat" } -chgrp = { optional=true, version="0.0.1", package="uu_chgrp", path="src/uu/chgrp" } -chmod = { optional=true, version="0.0.1", package="uu_chmod", path="src/uu/chmod" } -chown = { optional=true, version="0.0.1", package="uu_chown", path="src/uu/chown" } -chroot = { optional=true, version="0.0.1", package="uu_chroot", path="src/uu/chroot" } -cksum = { optional=true, version="0.0.1", package="uu_cksum", path="src/uu/cksum" } -comm = { optional=true, version="0.0.1", package="uu_comm", path="src/uu/comm" } -cp = { optional=true, version="0.0.1", package="uu_cp", path="src/uu/cp" } -cut = { optional=true, version="0.0.1", package="uu_cut", path="src/uu/cut" } -date = { optional=true, version="0.0.1", package="uu_date", path="src/uu/date" } -df = { optional=true, version="0.0.1", package="uu_df", path="src/uu/df" } -dircolors= { optional=true, version="0.0.1", package="uu_dircolors", path="src/uu/dircolors" } -dirname = { optional=true, version="0.0.1", package="uu_dirname", path="src/uu/dirname" } -du = { optional=true, version="0.0.1", package="uu_du", path="src/uu/du" } -echo = { optional=true, version="0.0.1", package="uu_echo", path="src/uu/echo" } -env = { optional=true, version="0.0.1", package="uu_env", path="src/uu/env" } -expand = { optional=true, version="0.0.1", package="uu_expand", path="src/uu/expand" } -expr = { optional=true, version="0.0.1", package="uu_expr", path="src/uu/expr" } -factor = { optional=true, version="0.0.1", package="uu_factor", path="src/uu/factor" } -false = { optional=true, version="0.0.1", package="uu_false", path="src/uu/false" } -fmt = { optional=true, version="0.0.1", package="uu_fmt", path="src/uu/fmt" } -fold = { optional=true, version="0.0.1", package="uu_fold", path="src/uu/fold" } -groups = { optional=true, version="0.0.1", package="uu_groups", path="src/uu/groups" } -hashsum = { optional=true, version="0.0.1", package="uu_hashsum", path="src/uu/hashsum" } -head = { optional=true, version="0.0.1", package="uu_head", path="src/uu/head" } -hostid = { optional=true, version="0.0.1", package="uu_hostid", path="src/uu/hostid" } -hostname = { optional=true, version="0.0.1", package="uu_hostname", path="src/uu/hostname" } -id = { optional=true, version="0.0.1", package="uu_id", path="src/uu/id" } -install = { optional=true, version="0.0.1", package="uu_install", path="src/uu/install" } -join = { optional=true, version="0.0.1", package="uu_join", path="src/uu/join" } -kill = { optional=true, version="0.0.1", package="uu_kill", path="src/uu/kill" } -link = { optional=true, version="0.0.1", package="uu_link", path="src/uu/link" } -ln = { optional=true, version="0.0.1", package="uu_ln", path="src/uu/ln" } -ls = { optional=true, version="0.0.1", package="uu_ls", path="src/uu/ls" } -logname = { optional=true, version="0.0.1", package="uu_logname", path="src/uu/logname" } -mkdir = { optional=true, version="0.0.1", package="uu_mkdir", path="src/uu/mkdir" } -mkfifo = { optional=true, version="0.0.1", package="uu_mkfifo", path="src/uu/mkfifo" } -mknod = { optional=true, version="0.0.1", package="uu_mknod", path="src/uu/mknod" } -mktemp = { optional=true, version="0.0.1", package="uu_mktemp", path="src/uu/mktemp" } -more = { optional=true, version="0.0.1", package="uu_more", path="src/uu/more" } -mv = { optional=true, version="0.0.1", package="uu_mv", path="src/uu/mv" } -nice = { optional=true, version="0.0.1", package="uu_nice", path="src/uu/nice" } -nl = { optional=true, version="0.0.1", package="uu_nl", path="src/uu/nl" } -nohup = { optional=true, version="0.0.1", package="uu_nohup", path="src/uu/nohup" } -nproc = { optional=true, version="0.0.1", package="uu_nproc", path="src/uu/nproc" } -numfmt = { optional=true, version="0.0.1", package="uu_numfmt", path="src/uu/numfmt" } -od = { optional=true, version="0.0.1", package="uu_od", path="src/uu/od" } -paste = { optional=true, version="0.0.1", package="uu_paste", path="src/uu/paste" } -pathchk = { optional=true, version="0.0.1", package="uu_pathchk", path="src/uu/pathchk" } -pinky = { optional=true, version="0.0.1", package="uu_pinky", path="src/uu/pinky" } -printenv = { optional=true, version="0.0.1", package="uu_printenv", path="src/uu/printenv" } -printf = { optional=true, version="0.0.1", package="uu_printf", path="src/uu/printf" } -ptx = { optional=true, version="0.0.1", package="uu_ptx", path="src/uu/ptx" } -pwd = { optional=true, version="0.0.1", package="uu_pwd", path="src/uu/pwd" } -readlink = { optional=true, version="0.0.1", package="uu_readlink", path="src/uu/readlink" } -realpath = { optional=true, version="0.0.1", package="uu_realpath", path="src/uu/realpath" } -relpath = { optional=true, version="0.0.1", package="uu_relpath", path="src/uu/relpath" } -rm = { optional=true, version="0.0.1", package="uu_rm", path="src/uu/rm" } -rmdir = { optional=true, version="0.0.1", package="uu_rmdir", path="src/uu/rmdir" } -seq = { optional=true, version="0.0.1", package="uu_seq", path="src/uu/seq" } -shred = { optional=true, version="0.0.1", package="uu_shred", path="src/uu/shred" } -shuf = { optional=true, version="0.0.1", package="uu_shuf", path="src/uu/shuf" } -sleep = { optional=true, version="0.0.1", package="uu_sleep", path="src/uu/sleep" } -sort = { optional=true, version="0.0.1", package="uu_sort", path="src/uu/sort" } -split = { optional=true, version="0.0.1", package="uu_split", path="src/uu/split" } -stat = { optional=true, version="0.0.1", package="uu_stat", path="src/uu/stat" } -stdbuf = { optional=true, version="0.0.1", package="uu_stdbuf", path="src/uu/stdbuf" } -sum = { optional=true, version="0.0.1", package="uu_sum", path="src/uu/sum" } -sync = { optional=true, version="0.0.1", package="uu_sync", path="src/uu/sync" } -tac = { optional=true, version="0.0.1", package="uu_tac", path="src/uu/tac" } -tail = { optional=true, version="0.0.1", package="uu_tail", path="src/uu/tail" } -tee = { optional=true, version="0.0.1", package="uu_tee", path="src/uu/tee" } -timeout = { optional=true, version="0.0.1", package="uu_timeout", path="src/uu/timeout" } -touch = { optional=true, version="0.0.1", package="uu_touch", path="src/uu/touch" } -tr = { optional=true, version="0.0.1", package="uu_tr", path="src/uu/tr" } -true = { optional=true, version="0.0.1", package="uu_true", path="src/uu/true" } -truncate = { optional=true, version="0.0.1", package="uu_truncate", path="src/uu/truncate" } -tsort = { optional=true, version="0.0.1", package="uu_tsort", path="src/uu/tsort" } -tty = { optional=true, version="0.0.1", package="uu_tty", path="src/uu/tty" } -uname = { optional=true, version="0.0.1", package="uu_uname", path="src/uu/uname" } -unexpand = { optional=true, version="0.0.1", package="uu_unexpand", path="src/uu/unexpand" } -uniq = { optional=true, version="0.0.1", package="uu_uniq", path="src/uu/uniq" } -unlink = { optional=true, version="0.0.1", package="uu_unlink", path="src/uu/unlink" } -uptime = { optional=true, version="0.0.1", package="uu_uptime", path="src/uu/uptime" } -users = { optional=true, version="0.0.1", package="uu_users", path="src/uu/users" } -wc = { optional=true, version="0.0.1", package="uu_wc", path="src/uu/wc" } -who = { optional=true, version="0.0.1", package="uu_who", path="src/uu/who" } -whoami = { optional=true, version="0.0.1", package="uu_whoami", path="src/uu/whoami" } -yes = { optional=true, version="0.0.1", package="uu_yes", path="src/uu/yes" } +arch = { optional=true, version="0.0.6", package="uu_arch", path="src/uu/arch" } +base32 = { optional=true, version="0.0.6", package="uu_base32", path="src/uu/base32" } +base64 = { optional=true, version="0.0.6", package="uu_base64", path="src/uu/base64" } +basename = { optional=true, version="0.0.6", package="uu_basename", path="src/uu/basename" } +cat = { optional=true, version="0.0.6", package="uu_cat", path="src/uu/cat" } +chgrp = { optional=true, version="0.0.6", package="uu_chgrp", path="src/uu/chgrp" } +chmod = { optional=true, version="0.0.6", package="uu_chmod", path="src/uu/chmod" } +chown = { optional=true, version="0.0.6", package="uu_chown", path="src/uu/chown" } +chroot = { optional=true, version="0.0.6", package="uu_chroot", path="src/uu/chroot" } +cksum = { optional=true, version="0.0.6", package="uu_cksum", path="src/uu/cksum" } +comm = { optional=true, version="0.0.6", package="uu_comm", path="src/uu/comm" } +cp = { optional=true, version="0.0.6", package="uu_cp", path="src/uu/cp" } +csplit = { optional=true, version="0.0.6", package="uu_csplit", path="src/uu/csplit" } +cut = { optional=true, version="0.0.6", package="uu_cut", path="src/uu/cut" } +date = { optional=true, version="0.0.6", package="uu_date", path="src/uu/date" } +df = { optional=true, version="0.0.6", package="uu_df", path="src/uu/df" } +dircolors= { optional=true, version="0.0.6", package="uu_dircolors", path="src/uu/dircolors" } +dirname = { optional=true, version="0.0.6", package="uu_dirname", path="src/uu/dirname" } +du = { optional=true, version="0.0.6", package="uu_du", path="src/uu/du" } +echo = { optional=true, version="0.0.6", package="uu_echo", path="src/uu/echo" } +env = { optional=true, version="0.0.6", package="uu_env", path="src/uu/env" } +expand = { optional=true, version="0.0.6", package="uu_expand", path="src/uu/expand" } +expr = { optional=true, version="0.0.6", package="uu_expr", path="src/uu/expr" } +factor = { optional=true, version="0.0.6", package="uu_factor", path="src/uu/factor" } +false = { optional=true, version="0.0.6", package="uu_false", path="src/uu/false" } +fmt = { optional=true, version="0.0.6", package="uu_fmt", path="src/uu/fmt" } +fold = { optional=true, version="0.0.6", package="uu_fold", path="src/uu/fold" } +groups = { optional=true, version="0.0.6", package="uu_groups", path="src/uu/groups" } +hashsum = { optional=true, version="0.0.6", package="uu_hashsum", path="src/uu/hashsum" } +head = { optional=true, version="0.0.6", package="uu_head", path="src/uu/head" } +hostid = { optional=true, version="0.0.6", package="uu_hostid", path="src/uu/hostid" } +hostname = { optional=true, version="0.0.6", package="uu_hostname", path="src/uu/hostname" } +id = { optional=true, version="0.0.6", package="uu_id", path="src/uu/id" } +install = { optional=true, version="0.0.6", package="uu_install", path="src/uu/install" } +join = { optional=true, version="0.0.6", package="uu_join", path="src/uu/join" } +kill = { optional=true, version="0.0.6", package="uu_kill", path="src/uu/kill" } +link = { optional=true, version="0.0.6", package="uu_link", path="src/uu/link" } +ln = { optional=true, version="0.0.6", package="uu_ln", path="src/uu/ln" } +ls = { optional=true, version="0.0.6", package="uu_ls", path="src/uu/ls" } +logname = { optional=true, version="0.0.6", package="uu_logname", path="src/uu/logname" } +mkdir = { optional=true, version="0.0.6", package="uu_mkdir", path="src/uu/mkdir" } +mkfifo = { optional=true, version="0.0.6", package="uu_mkfifo", path="src/uu/mkfifo" } +mknod = { optional=true, version="0.0.6", package="uu_mknod", path="src/uu/mknod" } +mktemp = { optional=true, version="0.0.6", package="uu_mktemp", path="src/uu/mktemp" } +more = { optional=true, version="0.0.6", package="uu_more", path="src/uu/more" } +mv = { optional=true, version="0.0.6", package="uu_mv", path="src/uu/mv" } +nice = { optional=true, version="0.0.6", package="uu_nice", path="src/uu/nice" } +nl = { optional=true, version="0.0.6", package="uu_nl", path="src/uu/nl" } +nohup = { optional=true, version="0.0.6", package="uu_nohup", path="src/uu/nohup" } +nproc = { optional=true, version="0.0.6", package="uu_nproc", path="src/uu/nproc" } +numfmt = { optional=true, version="0.0.6", package="uu_numfmt", path="src/uu/numfmt" } +od = { optional=true, version="0.0.6", package="uu_od", path="src/uu/od" } +paste = { optional=true, version="0.0.6", package="uu_paste", path="src/uu/paste" } +pathchk = { optional=true, version="0.0.6", package="uu_pathchk", path="src/uu/pathchk" } +pinky = { optional=true, version="0.0.6", package="uu_pinky", path="src/uu/pinky" } +printenv = { optional=true, version="0.0.6", package="uu_printenv", path="src/uu/printenv" } +printf = { optional=true, version="0.0.6", package="uu_printf", path="src/uu/printf" } +ptx = { optional=true, version="0.0.6", package="uu_ptx", path="src/uu/ptx" } +pwd = { optional=true, version="0.0.6", package="uu_pwd", path="src/uu/pwd" } +readlink = { optional=true, version="0.0.6", package="uu_readlink", path="src/uu/readlink" } +realpath = { optional=true, version="0.0.6", package="uu_realpath", path="src/uu/realpath" } +relpath = { optional=true, version="0.0.6", package="uu_relpath", path="src/uu/relpath" } +rm = { optional=true, version="0.0.6", package="uu_rm", path="src/uu/rm" } +rmdir = { optional=true, version="0.0.6", package="uu_rmdir", path="src/uu/rmdir" } +seq = { optional=true, version="0.0.6", package="uu_seq", path="src/uu/seq" } +shred = { optional=true, version="0.0.6", package="uu_shred", path="src/uu/shred" } +shuf = { optional=true, version="0.0.6", package="uu_shuf", path="src/uu/shuf" } +sleep = { optional=true, version="0.0.6", package="uu_sleep", path="src/uu/sleep" } +sort = { optional=true, version="0.0.6", package="uu_sort", path="src/uu/sort" } +split = { optional=true, version="0.0.6", package="uu_split", path="src/uu/split" } +stat = { optional=true, version="0.0.6", package="uu_stat", path="src/uu/stat" } +stdbuf = { optional=true, version="0.0.6", package="uu_stdbuf", path="src/uu/stdbuf" } +sum = { optional=true, version="0.0.6", package="uu_sum", path="src/uu/sum" } +sync = { optional=true, version="0.0.6", package="uu_sync", path="src/uu/sync" } +tac = { optional=true, version="0.0.6", package="uu_tac", path="src/uu/tac" } +tail = { optional=true, version="0.0.6", package="uu_tail", path="src/uu/tail" } +tee = { optional=true, version="0.0.6", package="uu_tee", path="src/uu/tee" } +timeout = { optional=true, version="0.0.6", package="uu_timeout", path="src/uu/timeout" } +touch = { optional=true, version="0.0.6", package="uu_touch", path="src/uu/touch" } +tr = { optional=true, version="0.0.6", package="uu_tr", path="src/uu/tr" } +true = { optional=true, version="0.0.6", package="uu_true", path="src/uu/true" } +truncate = { optional=true, version="0.0.6", package="uu_truncate", path="src/uu/truncate" } +tsort = { optional=true, version="0.0.6", package="uu_tsort", path="src/uu/tsort" } +tty = { optional=true, version="0.0.6", package="uu_tty", path="src/uu/tty" } +uname = { optional=true, version="0.0.6", package="uu_uname", path="src/uu/uname" } +unexpand = { optional=true, version="0.0.6", package="uu_unexpand", path="src/uu/unexpand" } +uniq = { optional=true, version="0.0.6", package="uu_uniq", path="src/uu/uniq" } +unlink = { optional=true, version="0.0.6", package="uu_unlink", path="src/uu/unlink" } +uptime = { optional=true, version="0.0.6", package="uu_uptime", path="src/uu/uptime" } +users = { optional=true, version="0.0.6", package="uu_users", path="src/uu/users" } +wc = { optional=true, version="0.0.6", package="uu_wc", path="src/uu/wc" } +who = { optional=true, version="0.0.6", package="uu_who", path="src/uu/who" } +whoami = { optional=true, version="0.0.6", package="uu_whoami", path="src/uu/whoami" } +yes = { optional=true, version="0.0.6", package="uu_yes", path="src/uu/yes" } # # * pinned transitive dependencies -pin_same-file = { version="1.0.4, < 1.0.6", package="same-file" } ## same-file v1.0.6 has compiler errors for MinRustV v1.32.0, expects 1.34 -pin_winapi-util = { version="0.1.2, < 0.1.3", package="winapi-util" } ## winapi-util v0.1.3 has compiler errors for MinRustV v1.32.0, expects 1.34 +# Not needed for now. Keep as examples: +#pin_cc = { version="1.0.61, < 1.0.62", package="cc" } ## cc v1.0.62 has compiler errors for MinRustV v1.32.0, requires 1.34 (for `std::str::split_ascii_whitespace()`) [dev-dependencies] +conv = "0.3" filetime = "0.2" +glob = "0.3.0" libc = "0.2" -rand = "0.6" +nix = "0.20.0" +rand = "0.7" regex = "1.0" -tempfile = "3.1" +sha1 = { version="0.6", features=["std"] } +## tempfile 3.2 depends on recent version of rand which depends on getrandom v0.2 which has compiler errors for MinRustV v1.32.0 +## min dep for tempfile = Rustc 1.40 +tempfile = "= 3.1.0" time = "0.1" unindent = "0.1" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary", features=["entries"] } +uucore = { version=">=0.0.8", package="uucore", path="src/uucore", features=["entries"] } +walkdir = "2.2" +tempdir = "0.3" [target.'cfg(unix)'.dev-dependencies] rust-users = { version="0.10", package="users" } unix_socket = "0.5.0" + [[bin]] name = "coreutils" path = "src/bin/coreutils.rs" diff --git a/DEVELOPER_INSTRUCTIONS.md b/DEVELOPER_INSTRUCTIONS.md new file mode 100644 index 000000000..c3b20dd46 --- /dev/null +++ b/DEVELOPER_INSTRUCTIONS.md @@ -0,0 +1,38 @@ +Code Coverage Report Generation +--------------------------------- + +Code coverage report can be generated using [grcov](https://github.com/mozilla/grcov). + +### Using Nightly Rust + +To generate [gcov-based](https://github.com/mozilla/grcov#example-how-to-generate-gcda-files-for-cc) coverage report + +```bash +$ export CARGO_INCREMENTAL=0 +$ export RUSTFLAGS="-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort" +$ export RUSTDOCFLAGS="-Cpanic=abort" +$ cargo build # e.g., --features feat_os_unix +$ cargo test # e.g., --features feat_os_unix test_pathchk +$ grcov . -s . --binary-path ./target/debug/ -t html --branch --ignore-not-existing --ignore build.rs --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?\#\[derive\()" -o ./target/debug/coverage/ +$ # open target/debug/coverage/index.html in browser +``` + +if changes are not reflected in the report then run `cargo clean` and run the above commands. + +### Using Stable Rust + +If you are using stable version of Rust that doesn't enable code coverage instrumentation by default +then add `-Z-Zinstrument-coverage` flag to `RUSTFLAGS` env variable specified above. + + +pre-commit hooks +---------------- + +A configuration for `pre-commit` is provided in the repository. It allows automatically checking every git commit you make to ensure it compiles, and passes `clippy` and `rustfmt` without warnings. + +To use the provided hook: + +1. [Install `pre-commit`](https://pre-commit.com/#install) +2. Run `pre-commit install` while in the repository directory + +Your git commits will then automatically be checked. If a check fails, an error message will explain why, and your commit will be canceled. You can then make the suggested changes, and run `git commit ...` again. diff --git a/GNUmakefile b/GNUmakefile index 01b234a47..409a527cd 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -30,7 +30,7 @@ ifneq ($(.SHELLSTATUS),0) override INSTALLDIR_MAN=$(DESTDIR)$(PREFIX)$(MANDIR) endif -#prefix to apply to uutils binary and all tool binaries +#prefix to apply to coreutils binary and all tool binaries PROG_PREFIX ?= # This won't support any directory with spaces in its name, but you can just @@ -41,7 +41,7 @@ PKG_BUILDDIR := $(BUILDDIR)/deps DOCSDIR := $(BASEDIR)/docs BUSYBOX_ROOT := $(BASEDIR)/tmp -BUSYBOX_VER := 1.24.1 +BUSYBOX_VER := 1.32.1 BUSYBOX_SRC := $(BUSYBOX_ROOT)/busybox-$(BUSYBOX_VER) # Possible programs @@ -53,7 +53,9 @@ PROGS := \ cksum \ comm \ cp \ + csplit \ cut \ + date \ df \ dircolors \ dirname \ @@ -160,7 +162,9 @@ TEST_PROGS := \ cksum \ comm \ cp \ + csplit \ cut \ + date \ dircolors \ dirname \ echo \ @@ -224,7 +228,7 @@ endif define TEST_BUSYBOX test_busybox_$(1): - (cd $(BUSYBOX_SRC)/testsuite && bindir=$(BUILDDIR) ./runtest $(RUNTEST_ARGS) $(1) ) + -(cd $(BUSYBOX_SRC)/testsuite && bindir=$(BUILDDIR) ./runtest $(RUNTEST_ARGS) $(1)) endef # Output names @@ -233,19 +237,7 @@ EXES := \ INSTALLEES := ${EXES} ifeq (${MULTICALL}, y) -INSTALLEES := ${INSTALLEES} uutils -endif - -# Shared library extension -SYSTEM := $(shell uname) -DYLIB_EXT := -ifeq ($(SYSTEM),Linux) - DYLIB_EXT := so - DYLIB_FLAGS := -shared -endif -ifeq ($(SYSTEM),Darwin) - DYLIB_EXT := dylib - DYLIB_FLAGS := -dynamiclib -undefined dynamic_lookup +INSTALLEES := ${INSTALLEES} coreutils endif all: build @@ -258,13 +250,13 @@ ifneq (${MULTICALL}, y) ${CARGO} build ${CARGOFLAGS} ${PROFILE_CMD} $(foreach pkg,$(EXES),-p uu_$(pkg)) endif -build-uutils: +build-coreutils: ${CARGO} build ${CARGOFLAGS} --features "${EXES}" ${PROFILE_CMD} --no-default-features build-manpages: cd $(DOCSDIR) && $(MAKE) man -build: build-uutils build-pkgs build-manpages +build: build-coreutils build-pkgs build-manpages $(foreach test,$(filter-out $(SKIP_UTILS),$(PROGS)),$(eval $(call TEST_BUSYBOX,$(test)))) @@ -283,10 +275,12 @@ $(BUILDDIR)/.config: $(BASEDIR)/.busybox-config cp $< $@ # Test under the busybox testsuite -$(BUILDDIR)/busybox: busybox-src build-uutils $(BUILDDIR)/.config - cp $(BUILDDIR)/uutils $(BUILDDIR)/busybox; \ +$(BUILDDIR)/busybox: busybox-src build-coreutils $(BUILDDIR)/.config + cp $(BUILDDIR)/coreutils $(BUILDDIR)/busybox; \ chmod +x $@; +prepare-busytest: $(BUILDDIR)/busybox + ifeq ($(EXES),) busytest: else @@ -304,10 +298,10 @@ install: build mkdir -p $(INSTALLDIR_BIN) mkdir -p $(INSTALLDIR_MAN) ifeq (${MULTICALL}, y) - $(INSTALL) $(BUILDDIR)/uutils $(INSTALLDIR_BIN)/$(PROG_PREFIX)uutils - cd $(INSTALLDIR_BIN) && $(foreach prog, $(filter-out uutils, $(INSTALLEES)), \ - ln -fs $(PROG_PREFIX)uutils $(PROG_PREFIX)$(prog) &&) : - cat $(DOCSDIR)/_build/man/uutils.1 | gzip > $(INSTALLDIR_MAN)/$(PROG_PREFIX)uutils.1.gz + $(INSTALL) $(BUILDDIR)/coreutils $(INSTALLDIR_BIN)/$(PROG_PREFIX)coreutils + cd $(INSTALLDIR_BIN) && $(foreach prog, $(filter-out coreutils, $(INSTALLEES)), \ + ln -fs $(PROG_PREFIX)coreutils $(PROG_PREFIX)$(prog) &&) : + cat $(DOCSDIR)/_build/man/coreutils.1 | gzip > $(INSTALLDIR_MAN)/$(PROG_PREFIX)coreutils.1.gz else $(foreach prog, $(INSTALLEES), \ $(INSTALL) $(BUILDDIR)/$(prog) $(INSTALLDIR_BIN)/$(PROG_PREFIX)$(prog);) @@ -317,10 +311,10 @@ endif uninstall: ifeq (${MULTICALL}, y) - rm -f $(addprefix $(INSTALLDIR_BIN)/,$(PROG_PREFIX)uutils) + rm -f $(addprefix $(INSTALLDIR_BIN)/,$(PROG_PREFIX)coreutils) endif - rm -f $(addprefix $(INSTALLDIR_MAN)/,$(PROG_PREFIX)uutils.1.gz) + rm -f $(addprefix $(INSTALLDIR_MAN)/,$(PROG_PREFIX)coreutils.1.gz) rm -f $(addprefix $(INSTALLDIR_BIN)/$(PROG_PREFIX),$(PROGS)) rm -f $(addprefix $(INSTALLDIR_MAN)/$(PROG_PREFIX),$(addsuffix .1.gz,$(PROGS))) -.PHONY: all build build-uutils build-pkgs build-docs test distclean clean busytest install uninstall +.PHONY: all build build-coreutils build-pkgs build-docs test distclean clean busytest install uninstall diff --git a/README.md b/README.md index 7c1c4eb34..b2257b3fd 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,13 @@ uutils coreutils ================ +[![Crates.io](https://img.shields.io/crates/v/coreutils.svg)](https://crates.io/crates/coreutils) [![Discord](https://img.shields.io/badge/discord-join-7289DA.svg?logo=discord&longCache=true&style=flat)](https://discord.gg/wQVJbvJ) [![License](http://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/uutils/coreutils/blob/master/LICENSE) -[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fuutils%2Fcoreutils.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fuutils%2Fcoreutils?ref=badge_shield) [![LOC](https://tokei.rs/b1/github/uutils/coreutils?category=code)](https://github.com/Aaronepower/tokei) [![dependency status](https://deps.rs/repo/github/uutils/coreutils/status.svg)](https://deps.rs/repo/github/uutils/coreutils) [![Build Status](https://api.travis-ci.org/uutils/coreutils.svg?branch=master)](https://travis-ci.org/uutils/coreutils) -[![Build Status (Windows)](https://ci.appveyor.com/api/projects/status/787ltcxgy86r20le?svg=true)](https://ci.appveyor.com/project/Arcterus/coreutils) [![Build Status (FreeBSD)](https://api.cirrus-ci.com/github/uutils/coreutils.svg)](https://cirrus-ci.com/github/uutils/coreutils/master) [![codecov](https://codecov.io/gh/uutils/coreutils/branch/master/graph/badge.svg)](https://codecov.io/gh/uutils/coreutils) @@ -24,9 +23,8 @@ Why? Many GNU, Linux and other utilities are useful, and obviously [some](http://gnuwin32.sourceforge.net) [effort](http://unxutils.sourceforge.net) has been spent in the past to port them to Windows. However, those projects -are either old and abandoned, are hosted on CVS (which makes it more difficult -for new contributors to contribute to them), are written in platform-specific C, or -suffer from other issues. +are written in platform-specific C, a language considered unsafe compared to Rust, and +have other issues. Rust provides a good, platform-agnostic way of writing systems utilities that are easy to compile anywhere, and this is as good a way as any to try and learn it. @@ -42,7 +40,7 @@ Requirements ### Rust Version ### uutils follows Rust's release channels and is tested against stable, beta and nightly. -The current oldest supported version of the Rust compiler is `1.32.0`. +The current oldest supported version of the Rust compiler is `1.40.0`. On both Windows and Redox, only the nightly version is tested currently. @@ -115,7 +113,7 @@ Installation Instructions Likewise, installing can simply be done using: ```bash -$ cargo install +$ cargo install --path . ``` This command will install uutils into Cargo's *bin* folder (*e.g.* `$HOME/.cargo/bin`). @@ -228,6 +226,11 @@ If you would prefer to test a select few utilities: $ cargo test --features "chmod mv tail" --no-default-features ``` +If you also want to test the core utilities: +```bash +$ cargo test -p uucore -p coreutils +``` + To debug: ```bash $ gdb --args target/debug/coreutils ls @@ -289,19 +292,20 @@ Utilities | Done | Semi-Done | To Do | |-----------|-----------|--------| | arch | cp | chcon | -| base32 | expr | csplit | -| base64 | install | dd | -| basename | ls | numfmt | -| cat | more | pr | -| chgrp | od (`--strings` and 128-bit data types missing) | runcon | -| chmod | printf | stty | +| base32 | expr | dd | +| base64 | install | numfmt | +| basename | ls | pr | +| cat | more | runcon | +| chgrp | od (`--strings` and 128-bit data types missing) | stty | +| chmod | printf | | | chown | sort | | | chroot | split | | | cksum | tail | | | comm | test | | -| cut | date | | -| dircolors | join | | -| dirname | df | | +| csplit | date | | +| cut | join | | +| dircolors | df | | +| dirname | tac | | | du | | | | echo | | | | env | | | @@ -354,7 +358,6 @@ Utilities | stdbuf | | | | sum | | | | sync | | | -| tac | | | | tee | | | | timeout | | | | touch | | | @@ -374,9 +377,37 @@ Utilities | whoami | | | | yes | | | +Targets that compile +------- + +This is an auto-generated table showing which binaries compile for each target-triple. Note that this **does not** indicate that they are fully implemented, or that the tests pass. + +|######OS######|###ARCH####|arch|base32|base64|basename|cat|chgrp|chmod|chown|chroot|cksum|comm|cp|csplit|cut|date|df|dircolors|dirname|du|echo|env|expand|expr|factor|false|fmt|fold|groups|hashsum|head|hostid|hostname|id|install|join|kill|link|ln|logname|ls|mkdir|mkfifo|mknod|mktemp|more|mv|nice|nl|nohup|nproc|numfmt|od|paste|pathchk|pinky|printenv|printf|ptx|pwd|readlink|realpath|relpath|rm|rmdir|seq|shred|shuf|sleep|sort|split|stat|stdbuf|sum|sync|tac|tail|tee|test|timeout|touch|tr|true|truncate|tsort|tty|uname|unexpand|uniq|unlink|uptime|users|wc|who|whoami|yes| +|--------------|-----------|----|------|------|--------|---|-----|-----|-----|------|-----|----|--|------|---|----|--|---------|-------|--|----|---|------|----|------|-----|---|----|------|-------|----|------|--------|--|-------|----|----|----|--|-------|--|-----|------|-----|------|----|--|----|--|-----|-----|------|--|-----|-------|-----|--------|------|---|---|--------|--------|-------|--|-----|---|-----|----|-----|----|-----|----|------|---|----|---|----|---|----|-------|-----|--|----|--------|-----|---|-----|--------|----|------|------|-----|--|---|------|---| +|linux-gnu|aarch64|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y| |y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y| +|linux-gnu|i686|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y| +|linux-gnu|powerpc64|y|y|y|y|y|y|y|y|y|y|y| |y|y|y|y|y|y|y|y|y|y| |y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y| +|linux-gnu|riscv64gc| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | +|linux-gnu|x86_64|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y| +|windows-msvc|aarch64|y|y|y|y|y| | | | |y|y|y|y|y|y|y|y|y| |y|y|y| |y|y|y|y| |y|y|y|y| | |y| |y|y|y| |y| | |y|y|y| |y| |y|y|y|y| | |y|y|y|y|y|y|y|y|y|y|y|y|y|y|y| | |y|y|y|y|y|y| |y|y|y|y|y| |y|y|y| |y| |y| | |y| +|windows-gnu|i686|y|y|y|y|y| | | | |y|y|y|y|y|y|y|y|y| |y|y|y| |y|y|y|y| |y|y|y|y| | |y| |y|y|y|y|y| | |y|y|y| |y| |y|y|y|y| | |y|y|y|y|y|y|y|y|y|y|y|y|y|y|y| | |y|y|y|y|y|y| |y|y|y|y|y|y|y|y|y| |y| |y| |y|y| +|windows-msvc|i686|y|y|y|y|y| | | | |y|y|y|y|y|y|y|y|y| |y|y|y| |y|y|y|y| |y|y|y|y| | |y| |y|y|y|y|y| | |y|y|y| |y| |y|y|y|y| | |y|y|y|y|y|y|y|y|y|y|y|y|y|y|y| | |y|y|y|y|y|y| |y|y|y|y|y| |y|y|y| |y| |y| |y|y| +|windows-gnu|x86_64|y|y|y|y|y| | | | |y|y|y|y|y|y|y|y|y| |y|y|y| |y|y|y|y| |y|y|y|y| | |y| |y|y|y|y|y| | |y|y|y| |y| |y|y|y|y| | |y|y|y|y|y|y|y|y|y|y|y|y|y|y|y| | |y|y|y|y|y|y| |y|y|y|y|y|y|y|y|y| |y| |y| |y|y| +|windows-msvc|x86_64|y|y|y|y|y| | | | |y|y|y|y|y|y|y|y|y| |y|y|y| |y|y|y|y| |y|y|y|y| | |y| |y|y|y|y|y| | |y|y|y| |y| |y|y|y|y| | |y|y|y|y|y|y|y|y|y|y|y|y|y|y|y| | |y|y|y|y|y|y| |y|y|y|y|y| |y|y|y| |y| |y| |y|y| +|apple MacOS|x86_64|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y| |y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y| +|freebsd|x86_64|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y| +|netbsd|x86_64|y|y|y|y|y|y|y|y| |y|y|y|y|y|y| |y|y|y|y|y|y| |y|y|y|y|y|y|y|y|y| |y|y| |y|y|y|y|y|y|y|y|y|y|y|y| |y|y|y|y|y| |y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y| |y|y|y|y|y|y| |y|y|y| | |y| |y|y| +|android|aarch64|y|y|y|y|y|y|y|y| |y|y|y|y|y|y| |y|y|y|y|y|y| |y|y|y|y|y|y|y|y|y| |y|y| |y|y|y|y|y|y|y|y|y|y|y|y| |y|y|y|y|y| |y|y|y|y|y|y|y|y|y|y|y|y|y|y|y| |y|y| |y|y|y|y| |y|y|y|y|y|y| |y|y|y| | |y| |y|y| +|android|x86_64|y|y|y|y|y|y|y|y| |y|y|y|y|y|y| |y|y|y|y|y|y| |y|y|y|y|y|y|y|y|y| |y|y| |y|y|y|y|y|y|y|y|y|y|y|y| |y|y|y|y|y| |y|y|y|y|y|y|y|y|y|y|y|y|y|y|y| |y|y| |y|y|y|y| |y|y|y|y|y|y| |y|y|y| | |y| |y|y| +|solaris|x86_64| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | +|wasi|wasm32| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | +|redox|x86_64| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | +|fuchsia|aarch64| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | +|fuchsia|x86_64| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | + License ------- uutils is licensed under the MIT License - see the `LICENSE` file for details -[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fuutils%2Fcoreutils.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fuutils%2Fcoreutils?ref=badge_large) +GNU Coreutils is licensed under the GPL 3.0 or later. diff --git a/build.rs b/build.rs index 625a3ccc8..ae38177b0 100644 --- a/build.rs +++ b/build.rs @@ -44,10 +44,10 @@ pub fn main() { mf.write_all( "type UtilityMap = HashMap<&'static str, fn(T) -> i32>;\n\ - \n\ - fn util_map() -> UtilityMap {\n\ - \tlet mut map = UtilityMap::new();\n\ - " + \n\ + fn util_map() -> UtilityMap {\n\ + \tlet mut map = UtilityMap::new();\n\ + " .as_bytes(), ) .unwrap(); @@ -97,21 +97,21 @@ pub fn main() { mf.write_all( format!( "\ - \tmap.insert(\"{krate}\", {krate}::uumain);\n\ - \t\tmap.insert(\"md5sum\", {krate}::uumain);\n\ - \t\tmap.insert(\"sha1sum\", {krate}::uumain);\n\ - \t\tmap.insert(\"sha224sum\", {krate}::uumain);\n\ - \t\tmap.insert(\"sha256sum\", {krate}::uumain);\n\ - \t\tmap.insert(\"sha384sum\", {krate}::uumain);\n\ - \t\tmap.insert(\"sha512sum\", {krate}::uumain);\n\ - \t\tmap.insert(\"sha3sum\", {krate}::uumain);\n\ - \t\tmap.insert(\"sha3-224sum\", {krate}::uumain);\n\ - \t\tmap.insert(\"sha3-256sum\", {krate}::uumain);\n\ - \t\tmap.insert(\"sha3-384sum\", {krate}::uumain);\n\ - \t\tmap.insert(\"sha3-512sum\", {krate}::uumain);\n\ - \t\tmap.insert(\"shake128sum\", {krate}::uumain);\n\ - \t\tmap.insert(\"shake256sum\", {krate}::uumain);\n\ - ", + \tmap.insert(\"{krate}\", {krate}::uumain);\n\ + \t\tmap.insert(\"md5sum\", {krate}::uumain);\n\ + \t\tmap.insert(\"sha1sum\", {krate}::uumain);\n\ + \t\tmap.insert(\"sha224sum\", {krate}::uumain);\n\ + \t\tmap.insert(\"sha256sum\", {krate}::uumain);\n\ + \t\tmap.insert(\"sha384sum\", {krate}::uumain);\n\ + \t\tmap.insert(\"sha512sum\", {krate}::uumain);\n\ + \t\tmap.insert(\"sha3sum\", {krate}::uumain);\n\ + \t\tmap.insert(\"sha3-224sum\", {krate}::uumain);\n\ + \t\tmap.insert(\"sha3-256sum\", {krate}::uumain);\n\ + \t\tmap.insert(\"sha3-384sum\", {krate}::uumain);\n\ + \t\tmap.insert(\"sha3-512sum\", {krate}::uumain);\n\ + \t\tmap.insert(\"shake128sum\", {krate}::uumain);\n\ + \t\tmap.insert(\"shake256sum\", {krate}::uumain);\n\ + ", krate = krate ) .as_bytes(), diff --git a/docs/compiles_table.csv b/docs/compiles_table.csv new file mode 100644 index 000000000..6da110132 --- /dev/null +++ b/docs/compiles_table.csv @@ -0,0 +1,21 @@ +target,arch,base32,base64,basename,cat,chgrp,chmod,chown,chroot,cksum,comm,cp,csplit,cut,date,df,dircolors,dirname,du,echo,env,expand,expr,factor,false,fmt,fold,groups,hashsum,head,hostid,hostname,id,install,join,kill,link,ln,logname,ls,mkdir,mkfifo,mknod,mktemp,more,mv,nice,nl,nohup,nproc,numfmt,od,paste,pathchk,pinky,printenv,printf,ptx,pwd,readlink,realpath,relpath,rm,rmdir,seq,shred,shuf,sleep,sort,split,stat,stdbuf,sum,sync,tac,tail,tee,test,timeout,touch,tr,true,truncate,tsort,tty,uname,unexpand,uniq,unlink,uptime,users,wc,who,whoami,yes +aarch64-unknown-linux-gnu,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,101,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +i686-unknown-linux-gnu,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +powerpc64-unknown-linux-gnu,0,0,0,0,0,0,0,0,0,0,0,101,0,0,0,0,0,0,0,0,0,0,101,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +riscv64gc-unknown-linux-gnu,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101 +x86_64-unknown-linux-gnu,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +aarch64-pc-windows-msvc,0,0,0,0,0,101,101,101,101,0,0,0,0,0,0,0,0,0,101,0,0,0,101,0,0,0,0,101,0,0,0,0,101,101,0,101,0,0,0,101,0,101,101,0,0,0,101,0,101,0,0,0,0,101,101,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,101,101,0,0,0,0,0,0,101,0,0,0,0,0,101,0,0,0,101,0,101,0,101,101,0 +i686-pc-windows-gnu,0,0,0,0,0,101,101,101,101,0,0,0,0,0,0,0,0,0,101,0,0,0,101,0,0,0,0,101,0,0,0,0,101,101,0,101,0,0,0,0,0,101,101,0,0,0,101,0,101,0,0,0,0,101,101,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,101,101,0,0,0,0,0,0,101,0,0,0,0,0,0,0,0,0,101,0,101,0,101,0,0 +i686-pc-windows-msvc,0,0,0,0,0,101,101,101,101,0,0,0,0,0,0,0,0,0,101,0,0,0,101,0,0,0,0,101,0,0,0,0,101,101,0,101,0,0,0,0,0,101,101,0,0,0,101,0,101,0,0,0,0,101,101,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,101,101,0,0,0,0,0,0,101,0,0,0,0,0,101,0,0,0,101,0,101,0,101,0,0 +x86_64-pc-windows-gnu,0,0,0,0,0,101,101,101,101,0,0,0,0,0,0,0,0,0,101,0,0,0,101,0,0,0,0,101,0,0,0,0,101,101,0,101,0,0,0,0,0,101,101,0,0,0,101,0,101,0,0,0,0,101,101,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,101,101,0,0,0,0,0,0,101,0,0,0,0,0,0,0,0,0,101,0,101,0,101,0,0 +x86_64-pc-windows-msvc,0,0,0,0,0,101,101,101,101,0,0,0,0,0,0,0,0,0,101,0,0,0,101,0,0,0,0,101,0,0,0,0,101,101,0,101,0,0,0,0,0,101,101,0,0,0,101,0,101,0,0,0,0,101,101,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,101,101,0,0,0,0,0,0,101,0,0,0,0,0,101,0,0,0,101,0,101,0,101,0,0 +x86_64-apple-darwin,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,101,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +x86_64-unknown-freebsd,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +x86_64-unknown-netbsd,0,0,0,0,0,0,0,0,101,0,0,0,0,0,0,101,0,0,0,0,0,0,101,0,0,0,0,0,0,0,0,0,101,0,0,101,0,0,0,0,0,0,0,0,0,0,0,0,101,0,0,0,0,0,101,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,101,0,0,0,0,0,0,101,0,0,0,101,101,0,101,0,0 +aarch64-linux-android,0,0,0,0,0,0,0,0,101,0,0,0,0,0,0,101,0,0,0,0,0,0,101,0,0,0,0,0,0,0,0,0,101,0,0,101,0,0,0,0,0,0,0,0,0,0,0,0,101,0,0,0,0,0,101,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,101,0,0,101,0,0,0,0,101,0,0,0,0,0,0,101,0,0,0,101,101,0,101,0,0 +x86_64-linux-android,0,0,0,0,0,0,0,0,101,0,0,0,0,0,0,101,0,0,0,0,0,0,101,0,0,0,0,0,0,0,0,0,101,0,0,101,0,0,0,0,0,0,0,0,0,0,0,0,101,0,0,0,0,0,101,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,101,0,0,101,0,0,0,0,101,0,0,0,0,0,0,101,0,0,0,101,101,0,101,0,0 +x86_64-sun-solaris,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101 +wasm32-wasi,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101 +x86_64-unknown-redox,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101 +aarch64-fuchsia,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101 +x86_64-fuchsia,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101 diff --git a/docs/compiles_table.py b/docs/compiles_table.py new file mode 100644 index 000000000..0cbfdf0e9 --- /dev/null +++ b/docs/compiles_table.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python3 +import multiprocessing +import subprocess +import argparse +import csv +import sys +from collections import defaultdict +from pathlib import Path + +# third party dependencies +from tqdm import tqdm + +BINS_PATH=Path("../src/uu") +CACHE_PATH=Path("compiles_table.csv") +TARGETS = [ + # Linux - GNU + "aarch64-unknown-linux-gnu", + "i686-unknown-linux-gnu", + "powerpc64-unknown-linux-gnu", + "riscv64gc-unknown-linux-gnu", + "x86_64-unknown-linux-gnu", + # Windows + "aarch64-pc-windows-msvc", + "i686-pc-windows-gnu", + "i686-pc-windows-msvc", + "x86_64-pc-windows-gnu", + "x86_64-pc-windows-msvc", + # Apple + "x86_64-apple-darwin", + "aarch64-apple-ios", + "x86_64-apple-ios", + # BSDs + "x86_64-unknown-freebsd", + "x86_64-unknown-netbsd", + # Android + "aarch64-linux-android", + "x86_64-linux-android", + # Solaris + "x86_64-sun-solaris", + # WASM + "wasm32-wasi", + # Redox + "x86_64-unknown-redox", + # Fuchsia + "aarch64-fuchsia", + "x86_64-fuchsia", +] + +class Target(str): + def __new__(cls, content): + obj = super().__new__(cls, content) + obj.arch, obj.platfrom, obj.os = Target.parse(content) + return obj + + @staticmethod + def parse(s): + elem = s.split("-") + if len(elem) == 2: + arch, platform, os = elem[0], "n/a", elem[1] + else: + arch, platform, os = elem[0], elem[1], "-".join(elem[2:]) + if os == "ios": + os = "apple IOS" + if os == "darwin": + os = "apple MacOS" + return (arch, platform, os) + + @staticmethod + def get_heading(): + return ["OS", "ARCH"] + + def get_row_heading(self): + return [self.os, self.arch] + + def requires_nightly(self): + return "redox" in self + + # Perform the 'it-compiles' check + def check(self, binary): + if self.requires_nightly(): + args = [ "cargo", "+nightly", "check", + "-p", f"uu_{binary}", "--bin", binary, + f"--target={self}"] + else: + args = ["cargo", "check", + "-p", f"uu_{binary}", "--bin", binary, + f"--target={self}"] + + res = subprocess.run(args, capture_output=True) + return res.returncode + + # Validate that the dependencies for running this target are met + def is_installed(self): + # check IOS sdk is installed, raise exception otherwise + if "ios" in self: + res = subprocess.run(["which", "xcrun"], capture_output=True) + if len(res.stdout) == 0: + raise Exception("Error: IOS sdk does not seem to be installed. Please do that manually") + if not self.requires_nightly(): + # check std toolchains are installed + toolchains = subprocess.run(["rustup", "target", "list"], capture_output=True) + toolchains = toolchains.stdout.decode('utf-8').split("\n") + if "installed" not in next(filter(lambda x: self in x, toolchains)): + raise Exception(f"Error: the {t} target is not installed. Please do that manually") + else: + # check nightly toolchains are installed + toolchains = subprocess.run(["rustup", "+nightly", "target", "list"], capture_output=True) + toolchains = toolchains.stdout.decode('utf-8').split("\n") + if "installed" not in next(filter(lambda x: self in x, toolchains)): + raise Exception(f"Error: the {t} nightly target is not installed. Please do that manually") + return True + +def install_targets(): + cmd = ["rustup", "target", "add"] + TARGETS + print(" ".join(cmd)) + ret = subprocess.run(cmd) + assert(ret.returncode == 0) + +def get_all_bins(): + bins = map(lambda x: x.name, BINS_PATH.iterdir()) + return sorted(list(bins)) + +def get_targets(selection): + if "all" in selection: + return list(map(Target, TARGETS)) + else: + # preserve the same order as in TARGETS + return list(map(Target, filter(lambda x: x in selection, TARGETS))) + +def test_helper(tup): + bin, target = tup + retcode = target.check(bin) + return (target, bin, retcode) + +def test_all_targets(targets, bins): + pool = multiprocessing.Pool() + inputs = [(b, t) for b in bins for t in targets] + + outputs = list(tqdm(pool.imap(test_helper, inputs), total=len(inputs))) + + table = defaultdict(dict) + for (t, b, r) in outputs: + table[t][b] = r + return table + +def save_csv(file, table): + targets = get_targets(table.keys()) # preserve order in CSV + bins = list(list(table.values())[0].keys()) + with open(file, "w") as csvfile: + header = ["target"] + bins + writer = csv.DictWriter(csvfile, fieldnames=header) + writer.writeheader() + for t in targets: + d = {"target" : t} + d.update(table[t]) + writer.writerow(d) + +def load_csv(file): + table = {} + cols = [] + rows = [] + with open(file, "r") as csvfile: + reader = csv.DictReader(csvfile) + cols = list(filter(lambda x: x!="target", reader.fieldnames)) + for row in reader: + t = Target(row["target"]) + rows += [t] + del row["target"] + table[t] = dict([k, int(v)] for k, v in row.items()) + return (table, rows, cols) + +def merge_tables(old, new): + from copy import deepcopy + tmp = deepcopy(old) + tmp.update(deepcopy(new)) + return tmp + +def render_md(fd, table, headings: str, row_headings: Target): + def print_row(lst, lens=[]): + lens = lens + [0] * (len(lst) - len(lens)) + for e, l in zip(lst, lens): + fmt = '|{}' if l == 0 else '|{:>%s}' % len(header[0]) + fd.write(fmt.format(e)) + fd.write("|\n") + def cell_render(target, bin): + return "y" if table[target][bin] == 0 else " " + + # add some 'hard' padding to specific columns + lens = [ + max(map(lambda x: len(x.os), row_headings)) + 2, + max(map(lambda x: len(x.arch), row_headings)) + 2 + ] + header = Target.get_heading() + header[0] = ("{:#^%d}" % lens[0]).format(header[0]) + header[1] = ("{:#^%d}" % lens[1]).format(header[1]) + + header += headings + print_row(header) + lines = list(map(lambda x: '-'*len(x), header)) + print_row(lines) + + for t in row_headings: + row = list(map(lambda b: cell_render(t, b), headings)) + row = t.get_row_heading() + row + print_row(row) + +if __name__ == "__main__": + # create the top-level parser + parser = argparse.ArgumentParser(prog='compiles_table.py') + subparsers = parser.add_subparsers(help='sub-command to execute', required=True, dest="cmd") + # create the parser for the "check" command + parser_a = subparsers.add_parser('check', help='run cargo check on specified targets and update csv cache') + parser_a.add_argument("targets", metavar="TARGET", type=str, nargs='+', choices=["all"]+TARGETS, + help="target-triple to check, as shown by 'rustup target list'") + # create the parser for the "render" command + parser_b = subparsers.add_parser('render', help='print a markdown table to stdout') + parser_b.add_argument("--equidistant", action="store_true", + help="NOT IMPLEMENTED: render each column with an equal width (in plaintext)") + args = parser.parse_args() + + if args.cmd == "render": + table, targets, bins = load_csv(CACHE_PATH) + render_md(sys.stdout, table, bins, targets) + + if args.cmd == "check": + targets = get_targets(args.targets) + bins = get_all_bins() + + assert(all(map(Target.is_installed, targets))) + table = test_all_targets(targets, bins) + + prev_table, _, _ = load_csv(CACHE_PATH) + new_table = merge_tables(prev_table, table) + save_csv(CACHE_PATH, new_table) \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index b12a8be62..74e5e1fb8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -23,6 +23,7 @@ import glob import os +import re # -- General configuration ------------------------------------------------ @@ -55,11 +56,14 @@ author = 'uutils developers' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. -# +# * take version from project "Cargo.toml" +version_file = open(os.path.join("..","Cargo.toml"), "r") +version_file_content = version_file.read() +v = re.search("^\s*version\s*=\s*\"([0-9.]+)\"", version_file_content, re.IGNORECASE | re.MULTILINE) # The short X.Y version. -version = '0.0.1' +version = v.groups()[0] # The full version, including alpha/beta/rc tags. -release = '0.0.1' +release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -179,6 +183,3 @@ texinfo_documents = [ author, 'uutils', 'A cross-platform reimplementation of GNU coreutils in Rust.', 'Miscellaneous'), ] - - - diff --git a/src/bin/coreutils.rs b/src/bin/coreutils.rs index c7a74974f..7fc494020 100644 --- a/src/bin/coreutils.rs +++ b/src/bin/coreutils.rs @@ -5,9 +5,6 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -extern crate textwrap; -extern crate uucore; - use std::cmp; use std::collections::hash_map::HashMap; use std::ffi::OsString; @@ -22,10 +19,10 @@ include!(concat!(env!("OUT_DIR"), "/uutils_map.rs")); fn usage(utils: &UtilityMap, name: &str) { println!("{} {} (multi-call binary)\n", name, VERSION); println!("Usage: {} [function [arguments...]]\n", name); - println!("Currently defined functions/utilities:\n"); + println!("Currently defined functions:\n"); #[allow(clippy::map_clone)] let mut utils: Vec<&str> = utils.keys().map(|&s| s).collect(); - utils.sort(); + utils.sort_unstable(); let display_list = utils.join(", "); let width = cmp::min(textwrap::termwidth(), 100) - 4 * 2; // (opinion/heuristic) max 100 chars wide with 4 character side indentions println!( diff --git a/src/uu/arch/Cargo.toml b/src/uu/arch/Cargo.toml index 9ab1da6dc..0b4359620 100644 --- a/src/uu/arch/Cargo.toml +++ b/src/uu/arch/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_arch" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "arch ~ (uutils) display machine architecture" @@ -15,9 +15,9 @@ edition = "2018" path = "src/arch.rs" [dependencies] -platform-info = "0.0.1" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +platform-info = "0.1" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "arch" diff --git a/src/uu/arch/src/arch.rs b/src/uu/arch/src/arch.rs index 20c278942..20392b11f 100644 --- a/src/uu/arch/src/arch.rs +++ b/src/uu/arch/src/arch.rs @@ -6,7 +6,6 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -extern crate platform_info; #[macro_use] extern crate uucore; diff --git a/src/uu/base32/Cargo.toml b/src/uu/base32/Cargo.toml index ea1482858..a1d7ba17e 100644 --- a/src/uu/base32/Cargo.toml +++ b/src/uu/base32/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_base32" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "base32 ~ (uutils) decode/encode input (base32-encoding)" @@ -15,8 +15,8 @@ edition = "2018" path = "src/base32.rs" [dependencies] -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary", features = ["encoding"] } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features = ["encoding"] } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "base32" diff --git a/src/uu/base32/src/base_common.rs b/src/uu/base32/src/base_common.rs index f6db40692..3f1436fb2 100644 --- a/src/uu/base32/src/base_common.rs +++ b/src/uu/base32/src/base_common.rs @@ -8,7 +8,7 @@ // that was distributed with this source code. use std::fs::File; -use std::io::{stdin, BufReader, Read}; +use std::io::{stdin, stdout, BufReader, Read, Write}; use std::path::Path; use uucore::encoding::{wrap_print, Data, Format}; @@ -85,7 +85,12 @@ fn handle_input( wrap_print(&data, encoded); } else { match data.decode() { - Ok(s) => print!("{}", String::from_utf8(s).unwrap()), + Ok(s) => { + if stdout().write_all(&s).is_err() { + // on windows console, writing invalid utf8 returns an error + crash!(1, "Cannot write non-utf8 data"); + } + } Err(_) => crash!(1, "invalid input"), } } diff --git a/src/uu/base64/Cargo.toml b/src/uu/base64/Cargo.toml index ea83605a0..841ab140c 100644 --- a/src/uu/base64/Cargo.toml +++ b/src/uu/base64/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_base64" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "base64 ~ (uutils) decode/encode input (base64-encoding)" @@ -15,8 +15,8 @@ edition = "2018" path = "src/base64.rs" [dependencies] -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary", features = ["encoding"] } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features = ["encoding"] } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "base64" diff --git a/src/uu/base64/src/base_common.rs b/src/uu/base64/src/base_common.rs index f6db40692..3f1436fb2 100644 --- a/src/uu/base64/src/base_common.rs +++ b/src/uu/base64/src/base_common.rs @@ -8,7 +8,7 @@ // that was distributed with this source code. use std::fs::File; -use std::io::{stdin, BufReader, Read}; +use std::io::{stdin, stdout, BufReader, Read, Write}; use std::path::Path; use uucore::encoding::{wrap_print, Data, Format}; @@ -85,7 +85,12 @@ fn handle_input( wrap_print(&data, encoded); } else { match data.decode() { - Ok(s) => print!("{}", String::from_utf8(s).unwrap()), + Ok(s) => { + if stdout().write_all(&s).is_err() { + // on windows console, writing invalid utf8 returns an error + crash!(1, "Cannot write non-utf8 data"); + } + } Err(_) => crash!(1, "invalid input"), } } diff --git a/src/uu/basename/Cargo.toml b/src/uu/basename/Cargo.toml index 8373c9fc6..92d0ca4cd 100644 --- a/src/uu/basename/Cargo.toml +++ b/src/uu/basename/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_basename" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "basename ~ (uutils) display PATHNAME with leading directory components removed" @@ -15,8 +15,8 @@ edition = "2018" path = "src/basename.rs" [dependencies] -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "basename" diff --git a/src/uu/cat/Cargo.toml b/src/uu/cat/Cargo.toml index 02f7354c3..e44a874c1 100644 --- a/src/uu/cat/Cargo.toml +++ b/src/uu/cat/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_cat" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "cat ~ (uutils) concatenate and display input" @@ -15,9 +15,10 @@ edition = "2018" path = "src/cat.rs" [dependencies] +clap = "2.33" quick-error = "1.2.3" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary", features=["fs"] } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["fs"] } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [target.'cfg(unix)'.dependencies] unix_socket = "0.5.0" diff --git a/src/uu/cat/src/cat.rs b/src/uu/cat/src/cat.rs index 0a35e243b..cf5a384a4 100644 --- a/src/uu/cat/src/cat.rs +++ b/src/uu/cat/src/cat.rs @@ -17,6 +17,7 @@ extern crate unix_socket; extern crate uucore; // last synced with: cat (GNU coreutils) 8.13 +use clap::{App, Arg}; use quick_error::ResultExt; use std::fs::{metadata, File}; use std::io::{self, stderr, stdin, stdout, BufWriter, Read, Write}; @@ -30,10 +31,11 @@ use std::os::unix::fs::FileTypeExt; #[cfg(unix)] use unix_socket::UnixStream; +static NAME: &str = "cat"; +static VERSION: &str = env!("CARGO_PKG_VERSION"); static SYNTAX: &str = "[OPTION]... [FILE]..."; static SUMMARY: &str = "Concatenate FILE(s), or standard input, to standard output With no FILE, or when FILE is -, read standard input."; -static LONG_HELP: &str = ""; #[derive(PartialEq)] enum NumberingMode { @@ -124,50 +126,122 @@ enum InputType { type CatResult = Result; +mod options { + pub static FILE: &str = "file"; + pub static SHOW_ALL: &str = "show-all"; + pub static NUMBER_NONBLANK: &str = "number-nonblank"; + pub static SHOW_NONPRINTING_ENDS: &str = "e"; + pub static SHOW_ENDS: &str = "show-ends"; + pub static NUMBER: &str = "number"; + pub static SQUEEZE_BLANK: &str = "squeeze-blank"; + pub static SHOW_NONPRINTING_TABS: &str = "t"; + pub static SHOW_TABS: &str = "show-tabs"; + pub static SHOW_NONPRINTING: &str = "show-nonprinting"; +} + pub fn uumain(args: impl uucore::Args) -> i32 { let args = args.collect_str(); - let matches = app!(SYNTAX, SUMMARY, LONG_HELP) - .optflag("A", "show-all", "equivalent to -vET") - .optflag( - "b", - "number-nonblank", - "number nonempty output lines, overrides -n", + let matches = App::new(executable!()) + .name(NAME) + .version(VERSION) + .usage(SYNTAX) + .about(SUMMARY) + .arg(Arg::with_name(options::FILE).hidden(true).multiple(true)) + .arg( + Arg::with_name(options::SHOW_ALL) + .short("A") + .long(options::SHOW_ALL) + .help("equivalent to -vET"), ) - .optflag("e", "", "equivalent to -vE") - .optflag("E", "show-ends", "display $ at end of each line") - .optflag("n", "number", "number all output lines") - .optflag("s", "squeeze-blank", "suppress repeated empty output lines") - .optflag("t", "", "equivalent to -vT") - .optflag("T", "show-tabs", "display TAB characters as ^I") - .optflag( - "v", - "show-nonprinting", - "use ^ and M- notation, except for LF (\\n) and TAB (\\t)", + .arg( + Arg::with_name(options::NUMBER_NONBLANK) + .short("b") + .long(options::NUMBER_NONBLANK) + .help("number nonempty output lines, overrides -n") + .overrides_with(options::NUMBER), ) - .parse(args); + .arg( + Arg::with_name(options::SHOW_NONPRINTING_ENDS) + .short("e") + .help("equivalent to -vE"), + ) + .arg( + Arg::with_name(options::SHOW_ENDS) + .short("E") + .long(options::SHOW_ENDS) + .help("display $ at end of each line"), + ) + .arg( + Arg::with_name(options::NUMBER) + .short("n") + .long(options::NUMBER) + .help("number all output lines"), + ) + .arg( + Arg::with_name(options::SQUEEZE_BLANK) + .short("s") + .long(options::SQUEEZE_BLANK) + .help("suppress repeated empty output lines"), + ) + .arg( + Arg::with_name(options::SHOW_NONPRINTING_TABS) + .short("t") + .long(options::SHOW_NONPRINTING_TABS) + .help("equivalent to -vT"), + ) + .arg( + Arg::with_name(options::SHOW_TABS) + .short("T") + .long(options::SHOW_TABS) + .help("display TAB characters at ^I"), + ) + .arg( + Arg::with_name(options::SHOW_NONPRINTING) + .short("v") + .long(options::SHOW_NONPRINTING) + .help("use ^ and M- notation, except for LF (\\n) and TAB (\\t)"), + ) + .get_matches_from(args); - let number_mode = if matches.opt_present("b") { + let number_mode = if matches.is_present(options::NUMBER_NONBLANK) { NumberingMode::NonEmpty - } else if matches.opt_present("n") { + } else if matches.is_present(options::NUMBER) { NumberingMode::All } else { NumberingMode::None }; - let show_nonprint = matches.opts_present(&[ - "A".to_owned(), - "e".to_owned(), - "t".to_owned(), - "v".to_owned(), - ]); - let show_ends = matches.opts_present(&["E".to_owned(), "A".to_owned(), "e".to_owned()]); - let show_tabs = matches.opts_present(&["A".to_owned(), "T".to_owned(), "t".to_owned()]); - let squeeze_blank = matches.opt_present("s"); - let mut files = matches.free; - if files.is_empty() { - files.push("-".to_owned()); - } + let show_nonprint = vec![ + options::SHOW_ALL.to_owned(), + options::SHOW_NONPRINTING_ENDS.to_owned(), + options::SHOW_NONPRINTING_TABS.to_owned(), + options::SHOW_NONPRINTING.to_owned(), + ] + .iter() + .any(|v| matches.is_present(v)); + + let show_ends = vec![ + options::SHOW_ENDS.to_owned(), + options::SHOW_ALL.to_owned(), + options::SHOW_NONPRINTING_ENDS.to_owned(), + ] + .iter() + .any(|v| matches.is_present(v)); + + let show_tabs = vec![ + options::SHOW_ALL.to_owned(), + options::SHOW_TABS.to_owned(), + options::SHOW_NONPRINTING_TABS.to_owned(), + ] + .iter() + .any(|v| matches.is_present(v)); + + let squeeze_blank = matches.is_present(options::SQUEEZE_BLANK); + let files: Vec = match matches.values_of(options::FILE) { + Some(v) => v.clone().map(|v| v.to_owned()).collect(), + None => vec!["-".to_owned()], + }; let can_write_fast = !(show_tabs || show_nonprint @@ -361,7 +435,7 @@ fn write_file_lines(file: &str, options: &OutputOptions, state: &mut OutputState } writer.write_all(options.end_of_line.as_bytes())?; if handle.is_interactive { - writer.flush().context(&file[..])?; + writer.flush().context(file)?; } } state.at_line_start = true; diff --git a/src/uu/chgrp/Cargo.toml b/src/uu/chgrp/Cargo.toml index 4e5b6b3b2..9424ad35e 100644 --- a/src/uu/chgrp/Cargo.toml +++ b/src/uu/chgrp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_chgrp" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "chgrp ~ (uutils) change the group ownership of FILE" @@ -15,9 +15,9 @@ edition = "2018" path = "src/chgrp.rs" [dependencies] -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary", features=["entries", "fs"] } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } -walkdir = "2.2.8" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["entries", "fs", "perms"] } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } +walkdir = "2.2" [[bin]] name = "chgrp" diff --git a/src/uu/chgrp/src/chgrp.rs b/src/uu/chgrp/src/chgrp.rs index e60efa0c6..b4c3360c5 100644 --- a/src/uu/chgrp/src/chgrp.rs +++ b/src/uu/chgrp/src/chgrp.rs @@ -11,23 +11,18 @@ extern crate uucore; pub use uucore::entries; use uucore::fs::resolve_relative_path; -use uucore::libc::{self, gid_t, lchown}; +use uucore::libc::gid_t; +use uucore::perms::{wrap_chgrp, Verbosity}; extern crate walkdir; use walkdir::WalkDir; -use std::io::Error as IOError; -use std::io::Result as IOResult; - use std::fs; use std::fs::Metadata; use std::os::unix::fs::MetadataExt; use std::path::Path; -use std::ffi::CString; -use std::os::unix::ffi::OsStrExt; - static SYNTAX: &str = "chgrp [OPTION]... GROUP FILE...\n or : chgrp [OPTION]... --reference=RFILE FILE..."; static SUMMARY: &str = "Change the group of each FILE to GROUP."; @@ -165,14 +160,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 { executor.exec() } -#[derive(PartialEq, Debug)] -enum Verbosity { - Silent, - Changes, - Verbose, - Normal, -} - struct Chgrper { dest_gid: gid_t, bit_flag: u8, @@ -201,23 +188,6 @@ impl Chgrper { ret } - fn chgrp>(&self, path: P, dgid: 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 as gid_t).wrapping_sub(1), dgid) - } else { - lchown(s.as_ptr(), (0 as gid_t).wrapping_sub(1), dgid) - } - }; - if ret == 0 { - Ok(()) - } else { - Err(IOError::last_os_error()) - } - } - #[cfg(windows)] fn is_bind_root>(&self, root: P) -> bool { // TODO: is there an equivalent on Windows? @@ -269,7 +239,24 @@ impl Chgrper { } } - let ret = self.wrap_chgrp(path, &meta, follow_arg); + let ret = match wrap_chgrp( + path, + &meta, + self.dest_gid, + follow_arg, + self.verbosity.clone(), + ) { + Ok(n) => { + show_info!("{}", n); + 0 + } + Err(e) => { + if self.verbosity != Verbosity::Silent { + show_info!("{}", e); + } + 1 + } + }; if !self.recursive { ret @@ -297,8 +284,22 @@ impl Chgrper { } }; - ret = self.wrap_chgrp(path, &meta, follow); + ret = match wrap_chgrp(path, &meta, self.dest_gid, follow, self.verbosity.clone()) { + Ok(n) => { + if n != "" { + show_info!("{}", n); + } + 0 + } + Err(e) => { + if self.verbosity != Verbosity::Silent { + show_info!("{}", e); + } + 1 + } + } } + ret } @@ -324,50 +325,4 @@ impl Chgrper { }; Some(meta) } - - fn wrap_chgrp>(&self, path: P, meta: &Metadata, follow: bool) -> i32 { - use self::Verbosity::*; - let mut ret = 0; - let dest_gid = self.dest_gid; - let path = path.as_ref(); - if let Err(e) = self.chgrp(path, dest_gid, follow) { - match self.verbosity { - Silent => (), - _ => { - show_info!("changing group of '{}': {}", path.display(), e); - if self.verbosity == Verbose { - println!( - "failed to change group of {} from {} to {}", - path.display(), - entries::gid2grp(meta.gid()).unwrap(), - entries::gid2grp(dest_gid).unwrap() - ); - }; - } - } - ret = 1; - } else { - let changed = dest_gid != meta.gid(); - if changed { - match self.verbosity { - Changes | Verbose => { - println!( - "changed group of {} from {} to {}", - path.display(), - entries::gid2grp(meta.gid()).unwrap(), - entries::gid2grp(dest_gid).unwrap() - ); - } - _ => (), - }; - } else if self.verbosity == Verbose { - println!( - "group of {} retained as {}", - path.display(), - entries::gid2grp(dest_gid).unwrap() - ); - } - } - ret - } } diff --git a/src/uu/chmod/Cargo.toml b/src/uu/chmod/Cargo.toml index eff0cc186..ac7030b62 100644 --- a/src/uu/chmod/Cargo.toml +++ b/src/uu/chmod/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_chmod" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "chmod ~ (uutils) change mode of FILE" @@ -15,10 +15,11 @@ edition = "2018" path = "src/chmod.rs" [dependencies] +clap = "2.33.3" libc = "0.2.42" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary", features=["fs", "mode"] } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } -walker = "1.0.0" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["fs", "mode"] } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } +walkdir = "2.2" [[bin]] name = "chmod" diff --git a/src/uu/chmod/src/chmod.rs b/src/uu/chmod/src/chmod.rs index ce3c3dd96..d9d8c8cf2 100644 --- a/src/uu/chmod/src/chmod.rs +++ b/src/uu/chmod/src/chmod.rs @@ -7,130 +7,185 @@ // spell-checker:ignore (ToDO) Chmoder cmode fmode fperm fref ugoa RFILE RFILE's -#[cfg(unix)] -extern crate libc; -extern crate walker; - #[macro_use] extern crate uucore; +use clap::{App, Arg}; use std::fs; use std::os::unix::fs::{MetadataExt, PermissionsExt}; use std::path::Path; use uucore::fs::display_permissions_unix; #[cfg(not(windows))] use uucore::mode; -use walker::Walker; +use walkdir::WalkDir; -const NAME: &str = "chmod"; -static SUMMARY: &str = "Change the mode of each FILE to MODE. +static VERSION: &str = env!("CARGO_PKG_VERSION"); +static ABOUT: &str = "Change the mode of each FILE to MODE. With --reference, change the mode of each FILE to that of RFILE."; -static LONG_HELP: &str = " - Each MODE is of the form '[ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=]?[0-7]+'. -"; + +mod options { + pub const CHANGES: &str = "changes"; + pub const QUIET: &str = "quiet"; // visible_alias("silent") + pub const VERBOSE: &str = "verbose"; + pub const NO_PRESERVE_ROOT: &str = "no-preserve-root"; + pub const PRESERVE_ROOT: &str = "preserve-root"; + pub const REFERENCE: &str = "RFILE"; + pub const RECURSIVE: &str = "recursive"; + pub const MODE: &str = "MODE"; + pub const FILE: &str = "FILE"; +} + +fn get_usage() -> String { + format!( + "{0} [OPTION]... MODE[,MODE]... FILE... +or: {0} [OPTION]... OCTAL-MODE FILE... +or: {0} [OPTION]... --reference=RFILE FILE...", + executable!() + ) +} + +fn get_long_usage() -> String { + String::from("Each MODE is of the form '[ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=]?[0-7]+'.") +} pub fn uumain(args: impl uucore::Args) -> i32 { let mut args = args.collect_str(); - let syntax = format!( - "[OPTION]... MODE[,MODE]... FILE... - {0} [OPTION]... OCTAL-MODE FILE... - {0} [OPTION]... --reference=RFILE FILE...", - NAME - ); - let mut opts = app!(&syntax, SUMMARY, LONG_HELP); - opts.optflag( - "c", - "changes", - "like verbose but report only when a change is made", - ) - // TODO: support --silent (can be done using clap) - .optflag("f", "quiet", "suppress most error messages") - .optflag( - "v", - "verbose", - "output a diagnostic for every file processed", - ) - .optflag( - "", - "no-preserve-root", - "do not treat '/' specially (the default)", - ) - .optflag("", "preserve-root", "fail to operate recursively on '/'") - .optopt( - "", - "reference", - "use RFILE's mode instead of MODE values", - "RFILE", - ) - .optflag("R", "recursive", "change files and directories recursively"); + // Before we can parse 'args' with clap (and previously getopts), + // a possible MODE prefix '-' needs to be removed (e.g. "chmod -x FILE"). + let mode_had_minus_prefix = strip_minus_from_mode(&mut args); - // sanitize input for - at beginning (e.g. chmod -x test_file). Remove - // the option and save it for later, after parsing is finished. - let negative_option = sanitize_input(&mut args); + let usage = get_usage(); + let after_help = get_long_usage(); - let mut matches = opts.parse(args); - if matches.free.is_empty() { - show_error!("missing an argument"); - show_error!("for help, try '{} --help'", NAME); - return 1; - } else { - let changes = matches.opt_present("changes"); - let quiet = matches.opt_present("quiet"); - let verbose = matches.opt_present("verbose"); - let preserve_root = matches.opt_present("preserve-root"); - let recursive = matches.opt_present("recursive"); - let fmode = matches - .opt_str("reference") + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(&usage[..]) + .after_help(&after_help[..]) + .arg( + Arg::with_name(options::CHANGES) + .long(options::CHANGES) + .short("c") + .help("like verbose but report only when a change is made"), + ) + .arg( + Arg::with_name(options::QUIET) + .long(options::QUIET) + .visible_alias("silent") + .short("f") + .help("suppress most error messages"), + ) + .arg( + Arg::with_name(options::VERBOSE) + .long(options::VERBOSE) + .short("v") + .help("output a diagnostic for every file processed"), + ) + .arg( + Arg::with_name(options::NO_PRESERVE_ROOT) + .long(options::NO_PRESERVE_ROOT) + .help("do not treat '/' specially (the default)"), + ) + .arg( + Arg::with_name(options::PRESERVE_ROOT) + .long(options::PRESERVE_ROOT) + .help("fail to operate recursively on '/'"), + ) + .arg( + Arg::with_name(options::RECURSIVE) + .long(options::RECURSIVE) + .short("R") + .help("change files and directories recursively"), + ) + .arg( + Arg::with_name(options::REFERENCE) + .long("reference") + .takes_value(true) + .help("use RFILE's mode instead of MODE values"), + ) + .arg( + Arg::with_name(options::MODE) + .required_unless(options::REFERENCE) + .takes_value(true), + // It would be nice if clap could parse with delimeter, e.g. "g-x,u+x", + // however .multiple(true) cannot be used here because FILE already needs that. + // Only one positional argument with .multiple(true) set is allowed per command + ) + .arg( + Arg::with_name(options::FILE) + .required_unless(options::MODE) + .multiple(true), + ) + .get_matches_from(args); + + let changes = matches.is_present(options::CHANGES); + let quiet = matches.is_present(options::QUIET); + let verbose = matches.is_present(options::VERBOSE); + let preserve_root = matches.is_present(options::PRESERVE_ROOT); + let recursive = matches.is_present(options::RECURSIVE); + let fmode = + matches + .value_of(options::REFERENCE) .and_then(|ref fref| match fs::metadata(fref) { Ok(meta) => Some(meta.mode()), Err(err) => crash!(1, "cannot stat attributes of '{}': {}", fref, err), }); - let cmode = if fmode.is_none() { - // If there was a negative option, now it's a good time to - // use it. - if negative_option.is_some() { - negative_option - } else { - Some(matches.free.remove(0)) - } - } else { - None - }; - let chmoder = Chmoder { - changes, - quiet, - verbose, - preserve_root, - recursive, - fmode, - cmode, - }; - match chmoder.chmod(matches.free) { - Ok(()) => {} - Err(e) => return e, - } + let modes = matches.value_of(options::MODE).unwrap(); // should always be Some because required + let mut cmode = if mode_had_minus_prefix { + // clap parsing is finished, now put prefix back + Some(format!("-{}", modes)) + } else { + Some(modes.to_string()) + }; + let mut files: Vec = matches + .values_of(options::FILE) + .map(|v| v.map(ToString::to_string).collect()) + .unwrap_or_default(); + if fmode.is_some() { + // "--reference" and MODE are mutually exclusive + // if "--reference" was used MODE needs to be interpreted as another FILE + // it wasn't possible to implement this behavior directly with clap + files.push(cmode.unwrap()); + cmode = None; + } + + let chmoder = Chmoder { + changes, + quiet, + verbose, + preserve_root, + recursive, + fmode, + cmode, + }; + match chmoder.chmod(files) { + Ok(()) => {} + Err(e) => return e, } 0 } -fn sanitize_input(args: &mut Vec) -> Option { +// Iterate 'args' and delete the first occurrence +// of a prefix '-' if it's associated with MODE +// e.g. "chmod -v -xw -R FILE" -> "chmod -v xw -R FILE" +pub fn strip_minus_from_mode(args: &mut Vec) -> bool { for i in 0..args.len() { - let first = args[i].chars().next().unwrap(); - if first != '-' { - continue; - } - if let Some(second) = args[i].chars().nth(1) { - match second { - 'r' | 'w' | 'x' | 'X' | 's' | 't' | 'u' | 'g' | 'o' | '0'..='7' => { - return Some(args.remove(i)); + if args[i].starts_with("-") { + if let Some(second) = args[i].chars().nth(1) { + match second { + 'r' | 'w' | 'x' | 'X' | 's' | 't' | 'u' | 'g' | 'o' | '0'..='7' => { + // TODO: use strip_prefix() once minimum rust version reaches 1.45.0 + args[i] = args[i][1..args[i].len()].to_string(); + return true; + } + _ => {} } - _ => {} } } } - None + false } struct Chmoder { @@ -150,72 +205,67 @@ impl Chmoder { for filename in &files { let filename = &filename[..]; let file = Path::new(filename); - if file.exists() { - if file.is_dir() { - if !self.preserve_root || filename != "/" { - if self.recursive { - let walk_dir = match Walker::new(&file) { - Ok(m) => m, - Err(f) => { - crash!(1, "{}", f.to_string()); - } - }; - // XXX: here (and elsewhere) we see that this impl will have issues - // with non-UTF-8 filenames. Using OsString won't fix this because - // on Windows OsStrings cannot be built out of non-UTF-8 chars. One - // possible fix is to use CStrings rather than Strings in the args - // to chmod() and chmod_file(). - r = self - .chmod( - walk_dir - .filter_map(|x| match x { - Ok(o) => match o.path().into_os_string().to_str() { - Some(s) => Some(s.to_owned()), - None => None, - }, - Err(_) => None, - }) - .collect(), - ) - .and(r); - r = self.chmod_file(&file, filename).and(r); - } - } else { - show_error!("could not change permissions of directory '{}'", filename); - r = Err(1); + if !file.exists() { + if is_symlink(file) { + println!( + "failed to change mode of '{}' from 0000 (---------) to 0000 (---------)", + filename + ); + if !self.quiet { + show_error!("cannot operate on dangling symlink '{}'", filename); } } else { - r = self.chmod_file(&file, filename).and(r); + show_error!("cannot access '{}': No such file or directory", filename); } + 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 + ); + return Err(1); + } + if !self.recursive { + r = self.chmod_file(&file).and(r); } else { - show_error!("no such file or directory '{}'", filename); - r = Err(1); + for entry in WalkDir::new(&filename).into_iter().filter_map(|e| e.ok()) { + let file = entry.path(); + r = self.chmod_file(&file).and(r); + } } } - r } #[cfg(windows)] - fn chmod_file(&self, file: &Path, name: &str) -> Result<(), i32> { + fn chmod_file(&self, file: &Path) -> Result<(), i32> { // chmod is useless on Windows // it doesn't set any permissions at all // instead it just sets the readonly attribute on the file Err(0) } #[cfg(any(unix, target_os = "redox"))] - fn chmod_file(&self, file: &Path, name: &str) -> Result<(), i32> { - let mut fperm = match fs::metadata(name) { + fn chmod_file(&self, file: &Path) -> Result<(), i32> { + let mut fperm = match fs::metadata(file) { Ok(meta) => meta.mode() & 0o7777, Err(err) => { - if !self.quiet { - show_error!("{}", err); + if is_symlink(file) { + if self.verbose { + println!( + "neither symbolic link '{}' nor referent has been changed", + file.display() + ); + } + return Ok(()); + } else { + show_error!("{}: '{}'", err, file.display()); } return Err(1); } }; match self.fmode { - Some(mode) => self.change_file(fperm, mode, file, name)?, + Some(mode) => self.change_file(fperm, mode, file)?, None => { let cmode_unwrapped = self.cmode.clone().unwrap(); for mode in cmode_unwrapped.split(',') { @@ -228,7 +278,7 @@ impl Chmoder { }; match result { Ok(mode) => { - self.change_file(fperm, mode, file, name)?; + self.change_file(fperm, mode, file)?; fperm = mode; } Err(f) => { @@ -246,20 +296,18 @@ impl Chmoder { } #[cfg(unix)] - fn change_file(&self, fperm: u32, mode: u32, file: &Path, path: &str) -> Result<(), i32> { + fn change_file(&self, fperm: u32, mode: u32, file: &Path) -> Result<(), i32> { if fperm == mode { if self.verbose && !self.changes { - show_info!( - "mode of '{}' retained as {:o} ({})", + println!( + "mode of '{}' retained as {:04o} ({})", file.display(), fperm, - display_permissions_unix(fperm) + display_permissions_unix(fperm), ); } Ok(()) - } else if let Err(err) = - fs::set_permissions(Path::new(path), fs::Permissions::from_mode(mode)) - { + } else if let Err(err) = fs::set_permissions(file, fs::Permissions::from_mode(mode)) { if !self.quiet { show_error!("{}", err); } @@ -289,3 +337,10 @@ impl Chmoder { } } } + +pub fn is_symlink>(path: P) -> bool { + match fs::symlink_metadata(path) { + Ok(m) => m.file_type().is_symlink(), + Err(_) => false, + } +} diff --git a/src/uu/chown/Cargo.toml b/src/uu/chown/Cargo.toml index 5b3148b28..74533af04 100644 --- a/src/uu/chown/Cargo.toml +++ b/src/uu/chown/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_chown" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "chown ~ (uutils) change the ownership of FILE" @@ -15,9 +15,10 @@ edition = "2018" path = "src/chown.rs" [dependencies] +clap = "2.33" glob = "0.3.0" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary", features=["entries", "fs"] } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["entries", "fs", "perms"] } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } walkdir = "2.2" [[bin]] diff --git a/src/uu/chown/src/chown.rs b/src/uu/chown/src/chown.rs index 09801769a..42010de03 100644 --- a/src/uu/chown/src/chown.rs +++ b/src/uu/chown/src/chown.rs @@ -11,94 +11,188 @@ extern crate uucore; pub use uucore::entries::{self, Group, Locate, Passwd}; use uucore::fs::resolve_relative_path; -use uucore::libc::{self, gid_t, lchown, uid_t}; +use uucore::libc::{gid_t, uid_t}; +use uucore::perms::{wrap_chown, Verbosity}; + +use clap::{App, Arg}; -extern crate walkdir; use walkdir::WalkDir; use std::fs::{self, Metadata}; use std::os::unix::fs::MetadataExt; -use std::io; -use std::io::Result as IOResult; - use std::convert::AsRef; use std::path::Path; -use std::ffi::CString; -use std::os::unix::ffi::OsStrExt; +static ABOUT: &str = "change file owner and group"; +static VERSION: &str = env!("CARGO_PKG_VERSION"); -static SYNTAX: &str = - "[OPTION]... [OWNER][:[GROUP]] FILE...\n chown [OPTION]... --reference=RFILE FILE..."; -static SUMMARY: &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!() + ) +} + pub fn uumain(args: impl uucore::Args) -> i32 { let args = args.collect_str(); - let mut opts = app!(SYNTAX, SUMMARY, ""); - opts.optflag("c", - "changes", - "like verbose but report only when a change is made") - .optflag("f", "silent", "") - .optflag("", "quiet", "suppress most error messages") - .optflag("v", - "verbose", - "output a diagnostic for every file processed") - .optflag("", "dereference", "affect the referent of each symbolic link (this is the default), rather than the symbolic link itself") - .optflag("h", "no-dereference", "affect symbolic links instead of any referenced file (useful only on systems that can change the ownership of a symlink)") + let usage = get_usage(); - .optopt("", "from", "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", "CURRENT_OWNER:CURRENT_GROUP") - .optopt("", - "reference", - "use RFILE's owner and group rather than specifying OWNER:GROUP values", - "RFILE") - .optflag("", - "no-preserve-root", - "do not treat '/' specially (the default)") - .optflag("", "preserve-root", "fail to operate recursively on '/'") + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(&usage[..]) + .arg( + Arg::with_name(options::verbosity::CHANGES) + .short("c") + .long(options::verbosity::CHANGES) + .help("like verbose but report only when a change is made"), + ) + .arg(Arg::with_name(options::dereference::DEREFERENCE).long(options::dereference::DEREFERENCE).help( + "affect the referent of each symbolic link (this is the default), rather than the symbolic link itself", + )) + .arg( + Arg::with_name(options::dereference::NO_DEREFERENCE) + .short("h") + .long(options::dereference::NO_DEREFERENCE) + .help( + "affect symbolic links instead of any referenced file (useful only on systems that can change the ownership of a symlink)", + ), + ) + .arg( + Arg::with_name(options::FROM) + .long(options::FROM) + .help( + "change the owner and/or group of each file only if its current owner and/or group match those specified here. Either may be omitted, in which case a match is not required for the omitted attribute", + ) + .value_name("CURRENT_OWNER:CURRENT_GROUP"), + ) + .arg( + Arg::with_name(options::preserve_root::PRESERVE) + .long(options::preserve_root::PRESERVE) + .help("fail to operate recursively on '/'"), + ) + .arg( + Arg::with_name(options::preserve_root::NO_PRESERVE) + .long(options::preserve_root::NO_PRESERVE) + .help("do not treat '/' specially (the default)"), + ) + .arg( + Arg::with_name(options::verbosity::QUIET) + .long(options::verbosity::QUIET) + .help("suppress most error messages"), + ) + .arg( + Arg::with_name(options::RECURSIVE) + .short("R") + .long(options::RECURSIVE) + .help("operate on files and directories recursively"), + ) + .arg( + Arg::with_name(options::REFERENCE) + .long(options::REFERENCE) + .help("use RFILE's owner and group rather than specifying OWNER:GROUP values") + .value_name("RFILE") + .min_values(1), + ) + .arg(Arg::with_name(options::verbosity::SILENT).short("f").long(options::verbosity::SILENT)) + .arg( + Arg::with_name(options::traverse::TRAVERSE) + .short(options::traverse::TRAVERSE) + .help("if a command line argument is a symbolic link to a directory, traverse it") + .overrides_with_all(&[options::traverse::EVERY, options::traverse::NO_TRAVERSE]), + ) + .arg( + Arg::with_name(options::traverse::EVERY) + .short(options::traverse::EVERY) + .help("traverse every symbolic link to a directory encountered") + .overrides_with_all(&[options::traverse::TRAVERSE, options::traverse::NO_TRAVERSE]), + ) + .arg( + Arg::with_name(options::traverse::NO_TRAVERSE) + .short(options::traverse::NO_TRAVERSE) + .help("do not traverse any symbolic links (default)") + .overrides_with_all(&[options::traverse::TRAVERSE, options::traverse::EVERY]), + ) + .arg( + Arg::with_name(options::verbosity::VERBOSE) + .long(options::verbosity::VERBOSE) + .help("output a diagnostic for every file processed"), + ) + .arg( + Arg::with_name(ARG_OWNER) + .multiple(false) + .takes_value(true) + .required(true), + ) + .arg( + Arg::with_name(ARG_FILES) + .multiple(true) + .takes_value(true) + .required(true) + .min_values(1), + ) + .get_matches_from(args); - .optflag("R", - "recursive", - "operate on files and directories recursively") - .optflag("H", - "", - "if a command line argument is a symbolic link to a directory, traverse it") - .optflag("L", - "", - "traverse every symbolic link to a directory encountered") - .optflag("P", "", "do not traverse any symbolic links (default)"); + /* First arg is the owner/group */ + let owner = matches.value_of(ARG_OWNER).unwrap(); - let mut bit_flag = FTS_PHYSICAL; - let mut preserve_root = false; - let mut derefer = -1; - let flags: &[char] = &['H', 'L', 'P']; - for opt in &args { - match opt.as_str() { - // If more than one is specified, only the final one takes effect. - s if s.contains(flags) => { - if let Some(idx) = s.rfind(flags) { - match s.chars().nth(idx).unwrap() { - 'H' => bit_flag = FTS_COMFOLLOW | FTS_PHYSICAL, - 'L' => bit_flag = FTS_LOGICAL, - 'P' => bit_flag = FTS_PHYSICAL, - _ => (), - } - } - } - "--no-preserve-root" => preserve_root = false, - "--preserve-root" => preserve_root = true, - "--dereference" => derefer = 1, - "--no-dereference" => derefer = 0, - _ => (), - } - } + /* 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 matches = opts.parse(args); - let recursive = matches.opt_present("recursive"); + 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 { @@ -111,17 +205,19 @@ pub fn uumain(args: impl uucore::Args) -> i32 { bit_flag = FTS_PHYSICAL; } - let verbosity = if matches.opt_present("changes") { + let verbosity = if matches.is_present(options::verbosity::CHANGES) { Verbosity::Changes - } else if matches.opt_present("silent") || matches.opt_present("quiet") { + } else if matches.is_present(options::verbosity::SILENT) + || matches.is_present(options::verbosity::QUIET) + { Verbosity::Silent - } else if matches.opt_present("verbose") { + } else if matches.is_present(options::verbosity::VERBOSE) { Verbosity::Verbose } else { Verbosity::Normal }; - let filter = if let Some(spec) = matches.opt_str("from") { + let filter = if let Some(spec) = matches.value_of(options::FROM) { match parse_spec(&spec) { Ok((Some(uid), None)) => IfFrom::User(uid), Ok((None, Some(gid))) => IfFrom::Group(gid), @@ -136,18 +232,9 @@ pub fn uumain(args: impl uucore::Args) -> i32 { IfFrom::All }; - if matches.free.is_empty() { - show_usage_error!("missing operand"); - return 1; - } else if matches.free.len() < 2 && !matches.opt_present("reference") { - show_usage_error!("missing operand after ‘{}’", matches.free[0]); - return 1; - } - - let mut files; let dest_uid: Option; let dest_gid: Option; - if let Some(file) = matches.opt_str("reference") { + if let Some(file) = matches.value_of(options::REFERENCE) { match fs::metadata(&file) { Ok(meta) => { dest_gid = Some(meta.gid()); @@ -158,9 +245,8 @@ pub fn uumain(args: impl uucore::Args) -> i32 { return 1; } } - files = matches.free; } else { - match parse_spec(&matches.free[0]) { + match parse_spec(&owner) { Ok((u, g)) => { dest_uid = u; dest_gid = g; @@ -170,8 +256,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 { return 1; } } - files = matches.free; - files.remove(0); } let executor = Chowner { bit_flag, @@ -197,7 +281,7 @@ fn parse_spec(spec: &str) -> Result<(Option, Option), String> { Ok(( Some(match Passwd::locate(args[0]) { Ok(v) => v.uid(), - _ => return Err(format!("invalid user: ‘{}’", spec)), + _ => return Err(format!("invalid user: '{}'", spec)), }), None, )) @@ -206,18 +290,18 @@ fn parse_spec(spec: &str) -> Result<(Option, Option), String> { None, Some(match Group::locate(args[1]) { Ok(v) => v.gid(), - _ => return Err(format!("invalid group: ‘{}’", spec)), + _ => return Err(format!("invalid group: '{}'", spec)), }), )) } else if usr_grp { Ok(( Some(match Passwd::locate(args[0]) { Ok(v) => v.uid(), - _ => return Err(format!("invalid user: ‘{}’", spec)), + _ => return Err(format!("invalid user: '{}'", spec)), }), Some(match Group::locate(args[1]) { Ok(v) => v.gid(), - _ => return Err(format!("invalid group: ‘{}’", spec)), + _ => return Err(format!("invalid group: '{}'", spec)), }), )) } else { @@ -225,14 +309,6 @@ fn parse_spec(spec: &str) -> Result<(Option, Option), String> { } } -#[derive(PartialEq, Debug)] -enum Verbosity { - Silent, - Changes, - Verbose, - Normal, -} - enum IfFrom { All, User(u32), @@ -270,29 +346,6 @@ impl Chowner { ret } - fn chown>( - &self, - path: P, - duid: uid_t, - dgid: 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(), duid, dgid) - } else { - lchown(s.as_ptr(), duid, dgid) - } - }; - if ret == 0 { - Ok(()) - } else { - Err(io::Error::last_os_error()) - } - } - fn traverse>(&self, root: P) -> i32 { let follow_arg = self.dereference || self.bit_flag != FTS_PHYSICAL; let path = root.as_ref(); @@ -329,7 +382,27 @@ impl Chowner { } let ret = if self.matched(meta.uid(), meta.gid()) { - self.wrap_chown(path, &meta, follow_arg) + match wrap_chown( + path, + &meta, + self.dest_uid, + self.dest_gid, + follow_arg, + self.verbosity.clone(), + ) { + Ok(n) => { + if n != "" { + show_info!("{}", n); + } + 0 + } + Err(e) => { + if self.verbosity != Verbosity::Silent { + show_info!("{}", e); + } + 1 + } + } } else { 0 }; @@ -364,7 +437,27 @@ impl Chowner { continue; } - ret = self.wrap_chown(path, &meta, follow); + ret = match wrap_chown( + path, + &meta, + self.dest_uid, + self.dest_gid, + follow, + self.verbosity.clone(), + ) { + Ok(n) => { + if n != "" { + show_info!("{}", n); + } + 0 + } + Err(e) => { + if self.verbosity != Verbosity::Silent { + show_info!("{}", e); + } + 1 + } + } } ret } @@ -392,58 +485,6 @@ impl Chowner { Some(meta) } - fn wrap_chown>(&self, path: P, meta: &Metadata, follow: bool) -> i32 { - use self::Verbosity::*; - let mut ret = 0; - let dest_uid = self.dest_uid.unwrap_or_else(|| meta.uid()); - let dest_gid = self.dest_gid.unwrap_or_else(|| meta.gid()); - let path = path.as_ref(); - if let Err(e) = self.chown(path, dest_uid, dest_gid, follow) { - match self.verbosity { - Silent => (), - _ => { - show_info!("changing ownership of '{}': {}", path.display(), e); - if self.verbosity == Verbose { - println!( - "failed to change 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() - ); - }; - } - } - ret = 1; - } else { - let changed = dest_uid != meta.uid() || dest_gid != meta.gid(); - if changed { - match self.verbosity { - Changes | Verbose => { - println!( - "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() - ); - } - _ => (), - }; - } else if self.verbosity == Verbose { - println!( - "ownership of {} retained as {}:{}", - path.display(), - entries::uid2usr(dest_uid).unwrap(), - entries::gid2grp(dest_gid).unwrap() - ); - } - } - ret - } - #[inline] fn matched(&self, uid: uid_t, gid: gid_t) -> bool { match self.filter { diff --git a/src/uu/chroot/Cargo.toml b/src/uu/chroot/Cargo.toml index b8a931c09..bf1e0ef59 100644 --- a/src/uu/chroot/Cargo.toml +++ b/src/uu/chroot/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_chroot" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "chroot ~ (uutils) run COMMAND under a new root directory" @@ -15,9 +15,9 @@ edition = "2018" path = "src/chroot.rs" [dependencies] -getopts = "0.2.18" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary", features=["entries"] } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +clap= "2.33" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["entries"] } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "chroot" diff --git a/src/uu/chroot/src/chroot.rs b/src/uu/chroot/src/chroot.rs index 89da2109e..44c5dfa02 100644 --- a/src/uu/chroot/src/chroot.rs +++ b/src/uu/chroot/src/chroot.rs @@ -8,65 +8,83 @@ // spell-checker:ignore (ToDO) NEWROOT Userspec pstatus -extern crate getopts; - #[macro_use] extern crate uucore; +use clap::{App, Arg}; +use std::ffi::CString; +use std::io::Error; +use std::path::Path; +use std::process::Command; use uucore::entries; use uucore::libc::{self, chroot, setgid, setgroups, setuid}; -use std::ffi::CString; -use std::io::Error; -use std::iter::FromIterator; -use std::path::Path; -use std::process::Command; - +static VERSION: &str = env!("CARGO_PKG_VERSION"); static NAME: &str = "chroot"; +static ABOUT: &str = "Run COMMAND with root directory set to NEWROOT."; static SYNTAX: &str = "[OPTION]... NEWROOT [COMMAND [ARG]...]"; -static SUMMARY: &str = "Run COMMAND with root directory set to NEWROOT."; -static LONG_HELP: &str = " - If COMMAND is not specified, it defaults to '$(SHELL) -i'. - If $(SHELL) is not set, /bin/sh is used. -"; + +mod options { + pub const NEWROOT: &str = "newroot"; + pub const USER: &str = "user"; + pub const GROUP: &str = "group"; + pub const GROUPS: &str = "groups"; + pub const USERSPEC: &str = "userspec"; +} pub fn uumain(args: impl uucore::Args) -> i32 { let args = args.collect_str(); - let matches = app!(SYNTAX, SUMMARY, LONG_HELP) - .optopt( - "u", - "user", - "User (ID or name) to switch before running the program", - "USER", + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(SYNTAX) + .arg(Arg::with_name(options::NEWROOT).hidden(true).required(true)) + .arg( + Arg::with_name(options::USER) + .short("u") + .long(options::USER) + .help("User (ID or name) to switch before running the program") + .value_name("USER"), ) - .optopt("g", "group", "Group (ID or name) to switch to", "GROUP") - .optopt( - "G", - "groups", - "Comma-separated list of groups to switch to", - "GROUP1,GROUP2...", + .arg( + Arg::with_name(options::GROUP) + .short("g") + .long(options::GROUP) + .help("Group (ID or name) to switch to") + .value_name("GROUP"), ) - .optopt( - "", - "userspec", - "Colon-separated user and group to switch to. \ - Same as -u USER -g GROUP. \ - Userspec has higher preference than -u and/or -g", - "USER:GROUP", + .arg( + Arg::with_name(options::GROUPS) + .short("G") + .long(options::GROUPS) + .help("Comma-separated list of groups to switch to") + .value_name("GROUP1,GROUP2..."), ) - .parse(args); - - if matches.free.is_empty() { - println!("Missing operand: NEWROOT"); - println!("Try `{} --help` for more information.", NAME); - return 1; - } + .arg( + Arg::with_name(options::USERSPEC) + .long(options::USERSPEC) + .help( + "Colon-separated user and group to switch to. \ + Same as -u USER -g GROUP. \ + Userspec has higher preference than -u and/or -g", + ) + .value_name("USER:GROUP"), + ) + .get_matches_from(args); let default_shell: &'static str = "/bin/sh"; let default_option: &'static str = "-i"; let user_shell = std::env::var("SHELL"); - let newroot = Path::new(&matches.free[0][..]); + let newroot: &Path = match matches.value_of(options::NEWROOT) { + Some(v) => Path::new(v), + None => crash!( + 1, + "Missing operand: NEWROOT\nTry '{} --help' for more information.", + NAME + ), + }; + if !newroot.is_dir() { crash!( 1, @@ -75,7 +93,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { ); } - let command: Vec<&str> = match matches.free.len() { + let command: Vec<&str> = match matches.args.len() { 1 => { let shell: &str = match user_shell { Err(_) => default_shell, @@ -83,7 +101,14 @@ pub fn uumain(args: impl uucore::Args) -> i32 { }; vec![shell, default_option] } - _ => matches.free[1..].iter().map(|x| &x[..]).collect(), + _ => { + let mut vector: Vec<&str> = Vec::new(); + for (&k, v) in matches.args.iter() { + vector.push(k.clone()); + vector.push(&v.vals[0].to_str().unwrap()); + } + vector + } }; set_context(&newroot, &matches); @@ -96,37 +121,30 @@ pub fn uumain(args: impl uucore::Args) -> i32 { if pstatus.success() { 0 } else { - match pstatus.code() { - Some(i) => i, - None => -1, - } + pstatus.code().unwrap_or(-1) } } -fn set_context(root: &Path, options: &getopts::Matches) { - let userspec_str = options.opt_str("userspec"); - let user_str = options.opt_str("user").unwrap_or_default(); - let group_str = options.opt_str("group").unwrap_or_default(); - let groups_str = options.opt_str("groups").unwrap_or_default(); +fn set_context(root: &Path, options: &clap::ArgMatches) { + let userspec_str = options.value_of(options::USERSPEC); + let user_str = options.value_of(options::USER).unwrap_or_default(); + let group_str = options.value_of(options::GROUP).unwrap_or_default(); + let groups_str = options.value_of(options::GROUPS).unwrap_or_default(); let userspec = match userspec_str { Some(ref u) => { let s: Vec<&str> = u.split(':').collect(); - if s.len() != 2 { + if s.len() != 2 || s.iter().any(|&spec| spec == "") { crash!(1, "invalid userspec: `{}`", u) }; s } None => Vec::new(), }; - let user = if userspec.is_empty() { - &user_str[..] + + let (user, group) = if userspec.is_empty() { + (&user_str[..], &group_str[..]) } else { - &userspec[0][..] - }; - let group = if userspec.is_empty() { - &group_str[..] - } else { - &userspec[1][..] + (&userspec[0][..], &userspec[1][..]) }; enter_chroot(root); @@ -170,7 +188,7 @@ fn set_main_group(group: &str) { } } -#[cfg(any(target_os = "macos", target_os = "freebsd"))] +#[cfg(any(target_vendor = "apple", target_os = "freebsd"))] fn set_groups(groups: Vec) -> libc::c_int { unsafe { setgroups(groups.len() as libc::c_int, groups.as_ptr()) } } @@ -182,11 +200,13 @@ fn set_groups(groups: Vec) -> libc::c_int { fn set_groups_from_str(groups: &str) { if !groups.is_empty() { - let groups_vec: Vec = - FromIterator::from_iter(groups.split(',').map(|x| match entries::grp2gid(x) { + let groups_vec: Vec = groups + .split(',') + .map(|x| match entries::grp2gid(x) { Ok(g) => g, _ => crash!(1, "no such group: {}", x), - })); + }) + .collect(); let err = set_groups(groups_vec); if err != 0 { crash!(1, "cannot set groups: {}", Error::last_os_error()) diff --git a/src/uu/cksum/Cargo.toml b/src/uu/cksum/Cargo.toml index ae6b29534..0332efbf8 100644 --- a/src/uu/cksum/Cargo.toml +++ b/src/uu/cksum/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_cksum" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "cksum ~ (uutils) display CRC and size of input" @@ -15,9 +15,10 @@ edition = "2018" path = "src/cksum.rs" [dependencies] +clap = "2.33" libc = "0.2.42" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "cksum" diff --git a/src/uu/cksum/build.rs b/src/uu/cksum/build.rs deleted file mode 100644 index a9edd0d59..000000000 --- a/src/uu/cksum/build.rs +++ /dev/null @@ -1,46 +0,0 @@ -// This file is part of the uutils coreutils package. -// -// (c) Alex Lyon -// (c) Michael Gehring -// -// For the full copyright and license information, please view the LICENSE -// file that was distributed with this source code. - -use std::env; -use std::fs::File; -use std::io::Write; -use std::path::Path; - -const CRC_TABLE_LEN: usize = 256; - -fn main() { - let out_dir = env::var("OUT_DIR").unwrap(); - - let mut table = Vec::with_capacity(CRC_TABLE_LEN); - for num in 0..CRC_TABLE_LEN { - table.push(crc_entry(num as u8) as u32); - } - let file = File::create(&Path::new(&out_dir).join("crc_table.rs")).unwrap(); - write!( - &file, - "#[allow(clippy::unreadable_literal)]\nconst CRC_TABLE: [u32; {}] = {:?};", - CRC_TABLE_LEN, table - ) - .unwrap(); -} - -#[inline] -fn crc_entry(input: u8) -> u32 { - let mut crc = (input as u32) << 24; - - for _ in 0..8 { - if crc & 0x8000_0000 != 0 { - crc <<= 1; - crc ^= 0x04c1_1db7; - } else { - crc <<= 1; - } - } - - crc -} diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index f589e6f81..b6436de87 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -10,15 +10,107 @@ #[macro_use] extern crate uucore; +use clap::{App, Arg}; use std::fs::File; use std::io::{self, stdin, BufReader, Read}; use std::path::Path; -include!(concat!(env!("OUT_DIR"), "/crc_table.rs")); +// NOTE: CRC_TABLE_LEN *must* be <= 256 as we cast 0..CRC_TABLE_LEN to u8 +const CRC_TABLE_LEN: usize = 256; +const CRC_TABLE: [u32; CRC_TABLE_LEN] = generate_crc_table(); -static SYNTAX: &str = "[OPTIONS] [FILE]..."; -static SUMMARY: &str = "Print CRC and size for each file"; -static LONG_HELP: &str = ""; +const VERSION: &str = env!("CARGO_PKG_VERSION"); +const NAME: &str = "cksum"; +const SYNTAX: &str = "[OPTIONS] [FILE]..."; +const SUMMARY: &str = "Print CRC and size for each file"; + +// this is basically a hack to get "loops" to work on Rust 1.33. Once we update to Rust 1.46 or +// greater, we can just use while loops +macro_rules! unroll { + (256, |$i:ident| $s:expr) => {{ + unroll!(@ 32, 0 * 32, $i, $s); + unroll!(@ 32, 1 * 32, $i, $s); + unroll!(@ 32, 2 * 32, $i, $s); + unroll!(@ 32, 3 * 32, $i, $s); + unroll!(@ 32, 4 * 32, $i, $s); + unroll!(@ 32, 5 * 32, $i, $s); + unroll!(@ 32, 6 * 32, $i, $s); + unroll!(@ 32, 7 * 32, $i, $s); + }}; + (8, |$i:ident| $s:expr) => {{ + unroll!(@ 8, 0, $i, $s); + }}; + + (@ 32, $start:expr, $i:ident, $s:expr) => {{ + unroll!(@ 8, $start + 0 * 8, $i, $s); + unroll!(@ 8, $start + 1 * 8, $i, $s); + unroll!(@ 8, $start + 2 * 8, $i, $s); + unroll!(@ 8, $start + 3 * 8, $i, $s); + }}; + (@ 8, $start:expr, $i:ident, $s:expr) => {{ + unroll!(@ 4, $start, $i, $s); + unroll!(@ 4, $start + 4, $i, $s); + }}; + (@ 4, $start:expr, $i:ident, $s:expr) => {{ + unroll!(@ 2, $start, $i, $s); + unroll!(@ 2, $start + 2, $i, $s); + }}; + (@ 2, $start:expr, $i:ident, $s:expr) => {{ + unroll!(@ 1, $start, $i, $s); + unroll!(@ 1, $start + 1, $i, $s); + }}; + (@ 1, $start:expr, $i:ident, $s:expr) => {{ + let $i = $start; + let _ = $s; + }}; +} + +const fn generate_crc_table() -> [u32; CRC_TABLE_LEN] { + let mut table = [0; CRC_TABLE_LEN]; + + // NOTE: works on Rust 1.46 + //let mut i = 0; + //while i < CRC_TABLE_LEN { + // table[i] = crc_entry(i as u8) as u32; + // + // i += 1; + //} + unroll!(256, |i| { + table[i] = crc_entry(i as u8) as u32; + }); + + table +} + +const fn crc_entry(input: u8) -> u32 { + let mut crc = (input as u32) << 24; + + // NOTE: this does not work on Rust 1.33, but *does* on 1.46 + //let mut i = 0; + //while i < 8 { + // if crc & 0x8000_0000 != 0 { + // crc <<= 1; + // crc ^= 0x04c1_1db7; + // } else { + // crc <<= 1; + // } + // + // i += 1; + //} + unroll!(8, |_i| { + let if_cond = crc & 0x8000_0000; + let if_body = (crc << 1) ^ 0x04c1_1db7; + let else_body = crc << 1; + + // NOTE: i feel like this is easier to understand than emulating an if statement in bitwise + // ops + let cond_table = [else_body, if_body]; + + crc = cond_table[(if_cond != 0) as usize]; + }); + + crc +} #[inline] fn crc_update(crc: u32, input: u8) -> u32 { @@ -48,7 +140,20 @@ fn cksum(fname: &str) -> io::Result<(u32, usize)> { let mut rd: Box = match fname { "-" => Box::new(stdin()), _ => { - file = File::open(&Path::new(fname))?; + let path = &Path::new(fname); + if path.is_dir() { + return Err(std::io::Error::new( + io::ErrorKind::InvalidInput, + "Is a directory", + )); + }; + if path.metadata().is_err() { + return Err(std::io::Error::new( + io::ErrorKind::NotFound, + "No such file or directory", + )); + }; + file = File::open(&path)?; Box::new(BufReader::new(file)) } }; @@ -68,13 +173,27 @@ fn cksum(fname: &str) -> io::Result<(u32, usize)> { Err(err) => return Err(err), } } - //Ok((0 as u32,0 as usize)) +} + +mod options { + pub static FILE: &str = "file"; } pub fn uumain(args: impl uucore::Args) -> i32 { - let matches = app!(SYNTAX, SUMMARY, LONG_HELP).parse(args.collect_str()); + let args = args.collect_str(); - let files = matches.free; + let matches = App::new(executable!()) + .name(NAME) + .version(VERSION) + .about(SUMMARY) + .usage(SYNTAX) + .arg(Arg::with_name(options::FILE).hidden(true).multiple(true)) + .get_matches_from(args); + + let files: Vec = match matches.values_of(options::FILE) { + Some(v) => v.clone().map(|v| v.to_owned()).collect(), + None => vec![], + }; if files.is_empty() { match cksum("-") { diff --git a/src/uu/comm/Cargo.toml b/src/uu/comm/Cargo.toml index 57cdbc0c4..f02217790 100644 --- a/src/uu/comm/Cargo.toml +++ b/src/uu/comm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_comm" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "comm ~ (uutils) compare sorted inputs" @@ -15,10 +15,10 @@ edition = "2018" path = "src/comm.rs" [dependencies] -getopts = "0.2.18" +clap = "2.33" libc = "0.2.42" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "comm" diff --git a/src/uu/comm/src/comm.rs b/src/uu/comm/src/comm.rs index 8c43f3a17..34b4330c9 100644 --- a/src/uu/comm/src/comm.rs +++ b/src/uu/comm/src/comm.rs @@ -7,8 +7,6 @@ // spell-checker:ignore (ToDO) delim mkdelim -extern crate getopts; - #[macro_use] extern crate uucore; @@ -17,21 +15,34 @@ use std::fs::File; use std::io::{self, stdin, BufRead, BufReader, Stdin}; use std::path::Path; -static SYNTAX: &str = "[OPTIONS] FILE1 FILE2"; -static SUMMARY: &str = "Compare sorted files line by line"; +use clap::{App, Arg, ArgMatches}; + +static VERSION: &str = env!("CARGO_PKG_VERSION"); +static ABOUT: &str = "compare two sorted files line by line"; static LONG_HELP: &str = ""; -fn mkdelim(col: usize, opts: &getopts::Matches) -> String { - let mut s = String::new(); - let delim = match opts.opt_str("output-delimiter") { - Some(d) => d, - None => "\t".to_owned(), - }; +mod options { + pub const COLUMN_1: &str = "1"; + pub const COLUMN_2: &str = "2"; + pub const COLUMN_3: &str = "3"; + pub const DELIMITER: &str = "output-delimiter"; + pub const DELIMITER_DEFAULT: &str = "\t"; + pub const FILE_1: &str = "FILE1"; + pub const FILE_2: &str = "FILE2"; +} - if col > 1 && !opts.opt_present("1") { +fn get_usage() -> String { + format!("{} [OPTION]... FILE1 FILE2", executable!()) +} + +fn mkdelim(col: usize, opts: &ArgMatches) -> String { + let mut s = String::new(); + let delim = opts.value_of(options::DELIMITER).unwrap(); + + if col > 1 && !opts.is_present(options::COLUMN_1) { s.push_str(delim.as_ref()); } - if col > 2 && !opts.opt_present("2") { + if col > 2 && !opts.is_present(options::COLUMN_2) { s.push_str(delim.as_ref()); } @@ -41,7 +52,7 @@ fn mkdelim(col: usize, opts: &getopts::Matches) -> String { fn ensure_nl(line: &mut String) { match line.chars().last() { Some('\n') => (), - _ => line.push_str("\n"), + _ => line.push('\n'), } } @@ -59,7 +70,7 @@ impl LineReader { } } -fn comm(a: &mut LineReader, b: &mut LineReader, opts: &getopts::Matches) { +fn comm(a: &mut LineReader, b: &mut LineReader, opts: &ArgMatches) { let delim: Vec = (0..4).map(|col| mkdelim(col, opts)).collect(); let ra = &mut String::new(); @@ -82,7 +93,7 @@ fn comm(a: &mut LineReader, b: &mut LineReader, opts: &getopts::Matches) { match ord { Ordering::Less => { - if !opts.opt_present("1") { + if !opts.is_present(options::COLUMN_1) { ensure_nl(ra); print!("{}{}", delim[1], ra); } @@ -90,7 +101,7 @@ fn comm(a: &mut LineReader, b: &mut LineReader, opts: &getopts::Matches) { na = a.read_line(ra); } Ordering::Greater => { - if !opts.opt_present("2") { + if !opts.is_present(options::COLUMN_2) { ensure_nl(rb); print!("{}{}", delim[2], rb); } @@ -98,7 +109,7 @@ fn comm(a: &mut LineReader, b: &mut LineReader, opts: &getopts::Matches) { nb = b.read_line(rb); } Ordering::Equal => { - if !opts.opt_present("3") { + if !opts.is_present(options::COLUMN_3) { ensure_nl(ra); print!("{}{}", delim[3], ra); } @@ -122,21 +133,42 @@ fn open_file(name: &str) -> io::Result { } pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); + let usage = get_usage(); - let matches = app!(SYNTAX, SUMMARY, LONG_HELP) - .optflag("1", "", "suppress column 1 (lines uniq to FILE1)") - .optflag("2", "", "suppress column 2 (lines uniq to FILE2)") - .optflag( - "3", - "", - "suppress column 3 (lines that appear in both files)", + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(&usage[..]) + .after_help(LONG_HELP) + .arg( + Arg::with_name(options::COLUMN_1) + .short(options::COLUMN_1) + .help("suppress column 1 (lines unique to FILE1)"), ) - .optopt("", "output-delimiter", "separate columns with STR", "STR") - .parse(args); + .arg( + Arg::with_name(options::COLUMN_2) + .short(options::COLUMN_2) + .help("suppress column 2 (lines unique to FILE2)"), + ) + .arg( + Arg::with_name(options::COLUMN_3) + .short(options::COLUMN_3) + .help("suppress column 3 (lines that appear in both files)"), + ) + .arg( + Arg::with_name(options::DELIMITER) + .long(options::DELIMITER) + .help("separate columns with STR") + .value_name("STR") + .default_value(options::DELIMITER_DEFAULT) + .hide_default_value(true), + ) + .arg(Arg::with_name(options::FILE_1).required(true)) + .arg(Arg::with_name(options::FILE_2).required(true)) + .get_matches_from(args); - let mut f1 = open_file(matches.free[0].as_ref()).unwrap(); - let mut f2 = open_file(matches.free[1].as_ref()).unwrap(); + let mut f1 = open_file(matches.value_of(options::FILE_1).unwrap()).unwrap(); + let mut f2 = open_file(matches.value_of(options::FILE_2).unwrap()).unwrap(); comm(&mut f1, &mut f2, &matches); diff --git a/src/uu/cp/Cargo.toml b/src/uu/cp/Cargo.toml index 9fa97ccb0..9d582adae 100644 --- a/src/uu/cp/Cargo.toml +++ b/src/uu/cp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_cp" -version = "0.0.1" +version = "0.0.6" authors = [ "Jordy Dickinson ", "Joshua S. Miller ", @@ -19,13 +19,13 @@ edition = "2018" path = "src/cp.rs" [dependencies] -clap = "2.32" +clap = "2.33" filetime = "0.2" -libc = "0.2.42" +libc = "0.2.85" quick-error = "1.2.3" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary", features=["fs"] } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } -walkdir = "2.2.8" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["fs"] } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } +walkdir = "2.2" [target.'cfg(target_os = "linux")'.dependencies] ioctl-sys = "0.5.2" diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 3c04a55e5..569ee78bc 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -10,22 +10,14 @@ // spell-checker:ignore (ToDO) ficlone linkgs lstat nlink nlinks pathbuf reflink strs xattrs -extern crate clap; -extern crate filetime; #[cfg(target_os = "linux")] #[macro_use] extern crate ioctl_sys; -extern crate libc; #[macro_use] extern crate quick_error; #[macro_use] extern crate uucore; -extern crate walkdir; -#[cfg(unix)] -extern crate xattr; -#[cfg(windows)] -extern crate winapi; #[cfg(windows)] use winapi::um::fileapi::CreateFileW; #[cfg(windows)] @@ -43,7 +35,6 @@ use std::ffi::CString; #[cfg(windows)] use std::ffi::OsStr; use std::fs; -#[cfg(target_os = "linux")] use std::fs::File; use std::fs::OpenOptions; use std::io; @@ -121,7 +112,7 @@ macro_rules! or_continue( }) ); -/// Prompts the user yes/no and returns `true` they if successfully +/// Prompts the user yes/no and returns `true` if they successfully /// answered yes. macro_rules! prompt_yes( ($($args:tt)+) => ({ @@ -216,6 +207,7 @@ pub struct Options { one_file_system: bool, overwrite: OverwriteMode, parents: bool, + strip_trailing_slashes: bool, reflink: bool, reflink_mode: ReflinkMode, preserve_attributes: Vec, @@ -231,11 +223,6 @@ static ABOUT: &str = "Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY."; static EXIT_OK: i32 = 0; static EXIT_ERR: i32 = 1; -/// Prints the version -fn print_version() { - println!("{} {}", executable!(), VERSION); -} - fn get_usage() -> String { format!( "{0} [OPTION]... [-T] SOURCE DEST @@ -262,6 +249,7 @@ static OPT_NO_DEREFERENCE_PRESERVE_LINKS: &str = "no-dereference-preserve-linkgs static OPT_NO_PRESERVE: &str = "no-preserve"; static OPT_NO_TARGET_DIRECTORY: &str = "no-target-directory"; static OPT_ONE_FILE_SYSTEM: &str = "one-file-system"; +static OPT_PARENT: &str = "parent"; static OPT_PARENTS: &str = "parents"; static OPT_PATHS: &str = "paths"; static OPT_PRESERVE: &str = "preserve"; @@ -277,7 +265,6 @@ static OPT_SYMBOLIC_LINK: &str = "symbolic-link"; static OPT_TARGET_DIRECTORY: &str = "target-directory"; static OPT_UPDATE: &str = "update"; static OPT_VERBOSE: &str = "verbose"; -static OPT_VERSION: &str = "version"; #[cfg(unix)] static PRESERVABLE_ATTRIBUTES: &[&str] = &[ @@ -325,10 +312,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .long(OPT_NO_TARGET_DIRECTORY) .conflicts_with(OPT_TARGET_DIRECTORY) .help("Treat DEST as a regular file and not a directory")) - .arg(Arg::with_name(OPT_VERSION) - .short("V") - .long(OPT_VERSION) - .help("output version information and exit")) .arg(Arg::with_name(OPT_INTERACTIVE) .short("i") .long(OPT_INTERACTIVE) @@ -347,10 +330,14 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .arg(Arg::with_name(OPT_RECURSIVE) .short("r") .long(OPT_RECURSIVE) - .help("copy directories recursively")) + // --archive sets this option + .help("copy directories recursively")) .arg(Arg::with_name(OPT_RECURSIVE_ALIAS) .short("R") .help("same as -r")) + .arg(Arg::with_name(OPT_STRIP_TRAILING_SLASHES) + .long(OPT_STRIP_TRAILING_SLASHES) + .help("remove any trailing slashes from each SOURCE argument")) .arg(Arg::with_name(OPT_VERBOSE) .short("v") .long(OPT_VERBOSE) @@ -405,7 +392,9 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .use_delimiter(true) .possible_values(PRESERVABLE_ATTRIBUTES) .value_name("ATTR_LIST") - .conflicts_with_all(&[OPT_PRESERVE_DEFAULT_ATTRIBUTES, OPT_NO_PRESERVE, OPT_ARCHIVE]) + .conflicts_with_all(&[OPT_PRESERVE_DEFAULT_ATTRIBUTES, OPT_NO_PRESERVE]) + // -d sets this option + // --archive sets this option .help("Preserve the specified attributes (default: mode(unix only),ownership,timestamps),\ if possible additional attributes: context, links, xattr, all")) .arg(Arg::with_name(OPT_PRESERVE_DEFAULT_ATTRIBUTES) @@ -419,45 +408,44 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .value_name("ATTR_LIST") .conflicts_with_all(&[OPT_PRESERVE_DEFAULT_ATTRIBUTES, OPT_PRESERVE, OPT_ARCHIVE]) .help("don't preserve the specified attributes")) + .arg(Arg::with_name(OPT_PARENTS) + .long(OPT_PARENTS) + .alias(OPT_PARENT) + .help("use full source file name under DIRECTORY")) .arg(Arg::with_name(OPT_NO_DEREFERENCE) .short("-P") .long(OPT_NO_DEREFERENCE) .conflicts_with(OPT_DEREFERENCE) + // -d sets this option .help("never follow symbolic links in SOURCE")) - - // TODO: implement the following args - .arg(Arg::with_name(OPT_ARCHIVE) - .short("a") - .long(OPT_ARCHIVE) - .conflicts_with_all(&[OPT_PRESERVE_DEFAULT_ATTRIBUTES, OPT_PRESERVE, OPT_NO_PRESERVE]) - .help("NotImplemented: same as -dR --preserve=all")) - .arg(Arg::with_name(OPT_COPY_CONTENTS) - .long(OPT_COPY_CONTENTS) - .conflicts_with(OPT_ATTRIBUTES_ONLY) - .help("NotImplemented: copy contents of special files when recursive")) - .arg(Arg::with_name(OPT_NO_DEREFERENCE_PRESERVE_LINKS) - .short("d") - .help("NotImplemented: same as --no-dereference --preserve=links")) .arg(Arg::with_name(OPT_DEREFERENCE) .short("L") .long(OPT_DEREFERENCE) .conflicts_with(OPT_NO_DEREFERENCE) - .help("NotImplemented: always follow symbolic links in SOURCE")) - .arg(Arg::with_name(OPT_PARENTS) - .long(OPT_PARENTS) - .help("NotImplemented: use full source file name under DIRECTORY")) + .help("always follow symbolic links in SOURCE")) + .arg(Arg::with_name(OPT_ARCHIVE) + .short("a") + .long(OPT_ARCHIVE) + .conflicts_with_all(&[OPT_PRESERVE_DEFAULT_ATTRIBUTES, OPT_PRESERVE, OPT_NO_PRESERVE]) + .help("Same as -dR --preserve=all")) + .arg(Arg::with_name(OPT_NO_DEREFERENCE_PRESERVE_LINKS) + .short("d") + .help("same as --no-dereference --preserve=links")) + .arg(Arg::with_name(OPT_ONE_FILE_SYSTEM) + .short("x") + .long(OPT_ONE_FILE_SYSTEM) + .help("stay on this file system")) + + // TODO: implement the following args + .arg(Arg::with_name(OPT_COPY_CONTENTS) + .long(OPT_COPY_CONTENTS) + .conflicts_with(OPT_ATTRIBUTES_ONLY) + .help("NotImplemented: copy contents of special files when recursive")) .arg(Arg::with_name(OPT_SPARSE) .long(OPT_SPARSE) .takes_value(true) .value_name("WHEN") .help("NotImplemented: control creation of sparse files. See below")) - .arg(Arg::with_name(OPT_STRIP_TRAILING_SLASHES) - .long(OPT_STRIP_TRAILING_SLASHES) - .help("NotImplemented: remove any trailing slashes from each SOURCE argument")) - .arg(Arg::with_name(OPT_ONE_FILE_SYSTEM) - .short("x") - .long(OPT_ONE_FILE_SYSTEM) - .help("NotImplemented: stay on this file system")) .arg(Arg::with_name(OPT_CONTEXT) .long(OPT_CONTEXT) .takes_value(true) @@ -472,14 +460,9 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .multiple(true)) .get_matches_from(args); - if matches.is_present(OPT_VERSION) { - print_version(); - return EXIT_OK; - } - let options = crash_if_err!(EXIT_ERR, Options::from_matches(&matches)); let paths: Vec = matches - .values_of("paths") + .values_of(OPT_PATHS) .map(|v| v.map(ToString::to_string).collect()) .unwrap_or_default(); @@ -557,22 +540,30 @@ impl FromStr for Attribute { return Err(Error::InvalidArgument(format!( "invalid attribute '{}'", value - ))) + ))); } }) } } +fn add_all_attributes() -> Vec { + let mut attr = Vec::new(); + #[cfg(unix)] + attr.push(Attribute::Mode); + attr.push(Attribute::Ownership); + attr.push(Attribute::Timestamps); + attr.push(Attribute::Context); + attr.push(Attribute::Xattr); + attr.push(Attribute::Links); + attr +} + impl Options { fn from_matches(matches: &ArgMatches) -> CopyResult { let not_implemented_opts = vec![ - OPT_ARCHIVE, OPT_COPY_CONTENTS, - OPT_NO_DEREFERENCE_PRESERVE_LINKS, - OPT_DEREFERENCE, - OPT_PARENTS, OPT_SPARSE, - OPT_STRIP_TRAILING_SLASHES, + #[cfg(not(any(windows, unix)))] OPT_ONE_FILE_SYSTEM, OPT_CONTEXT, #[cfg(windows)] @@ -605,13 +596,7 @@ impl Options { let mut attributes = Vec::new(); for attribute_str in attribute_strs { if attribute_str == "all" { - #[cfg(unix)] - attributes.push(Attribute::Mode); - attributes.push(Attribute::Ownership); - attributes.push(Attribute::Timestamps); - attributes.push(Attribute::Context); - attributes.push(Attribute::Xattr); - attributes.push(Attribute::Links); + attributes = add_all_attributes(); break; } else { attributes.push(Attribute::from_str(attribute_str)?); @@ -620,6 +605,11 @@ impl Options { attributes } } + } else if matches.is_present(OPT_ARCHIVE) { + // --archive is used. Same as --preserve=all + add_all_attributes() + } else if matches.is_present(OPT_NO_DEREFERENCE_PRESERVE_LINKS) { + vec![Attribute::Links] } else if matches.is_present(OPT_PRESERVE_DEFAULT_ATTRIBUTES) { DEFAULT_ATTRIBUTES.to_vec() } else { @@ -631,13 +621,17 @@ impl Options { copy_contents: matches.is_present(OPT_COPY_CONTENTS), copy_mode: CopyMode::from_matches(matches), dereference: matches.is_present(OPT_DEREFERENCE), - no_dereference: matches.is_present(OPT_NO_DEREFERENCE), + // No dereference is set with -p, -d and --archive + no_dereference: matches.is_present(OPT_NO_DEREFERENCE) + || matches.is_present(OPT_NO_DEREFERENCE_PRESERVE_LINKS) + || matches.is_present(OPT_ARCHIVE), one_file_system: matches.is_present(OPT_ONE_FILE_SYSTEM), overwrite: OverwriteMode::from_matches(matches), parents: matches.is_present(OPT_PARENTS), backup_suffix: matches.value_of(OPT_SUFFIX).unwrap().to_string(), update: matches.is_present(OPT_UPDATE), verbose: matches.is_present(OPT_VERBOSE), + strip_trailing_slashes: matches.is_present(OPT_STRIP_TRAILING_SLASHES), reflink: matches.is_present(OPT_REFLINK), reflink_mode: { if let Some(reflink) = matches.value_of(OPT_REFLINK) { @@ -648,7 +642,7 @@ impl Options { return Err(Error::InvalidArgument(format!( "invalid argument '{}' for \'reflink\'", value - ))) + ))); } } } else { @@ -695,7 +689,7 @@ fn parse_path_args(path_args: &[String], options: &Options) -> CopyResult<(Vec { // All path args are sources, and the target dir was // specified separately @@ -709,6 +703,12 @@ fn parse_path_args(path_args: &[String], options: &Options) -> CopyResult<(Vec CopyResult<() let dest = construct_dest_path(source, target, &target_type, options)?; preserve_hardlinks(&mut hard_links, source, dest, &mut found_hard_link).unwrap(); } - if !found_hard_link { if let Err(error) = copy_source(source, target, &target_type, options) { - show_error!("{}", error); match error { - Error::Skipped(_) => (), - _ => non_fatal_errors = true, + // When using --no-clobber, we don't want to show + // an error message + Error::NotAllFilesCopied => (), + Error::Skipped(_) => { + show_error!("{}", error); + } + _ => { + show_error!("{}", error); + non_fatal_errors = true + } } } } @@ -846,9 +852,17 @@ fn construct_dest_path( .into()); } + if options.parents && !target.is_dir() { + return Err("with --parents, the destination must be a directory".into()); + } + Ok(match *target_type { TargetType::Directory => { - let root = source_path.parent().unwrap_or(source_path); + let root = if options.parents { + Path::new("") + } else { + source_path.parent().unwrap_or(source_path) + }; localize_to_target(root, source_path, target)? } TargetType::File => target.to_path_buf(), @@ -924,10 +938,10 @@ fn copy_directory(root: &Path, target: &Target, options: &Options) -> CopyResult #[cfg(any(windows, target_os = "redox"))] let mut hard_links: Vec<(String, u64)> = vec![]; - for path in WalkDir::new(root) { + for path in WalkDir::new(root).same_file_system(options.one_file_system) { let p = or_continue!(path); let is_symlink = fs::symlink_metadata(p.path())?.file_type().is_symlink(); - let path = if options.no_dereference && is_symlink { + let path = if (options.no_dereference || options.dereference) && is_symlink { // we are dealing with a symlink. Don't follow it match env::current_dir() { Ok(cwd) => cwd.join(resolve_relative_path(p.path())), @@ -941,7 +955,7 @@ fn copy_directory(root: &Path, target: &Target, options: &Options) -> CopyResult Some(parent) => { #[cfg(windows)] { - // On Windows, some pathes are starting with \\? + // On Windows, some paths are starting with \\? // but not always, so, make sure that we are consistent for strip_prefix // See https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file for more info let parent_can = adjust_canonicalization(parent); @@ -968,7 +982,19 @@ fn copy_directory(root: &Path, target: &Target, options: &Options) -> CopyResult let dest = local_to_target.as_path().to_path_buf(); preserve_hardlinks(&mut hard_links, &source, dest, &mut found_hard_link).unwrap(); if !found_hard_link { - copy_file(path.as_path(), local_to_target.as_path(), options)?; + match copy_file(path.as_path(), local_to_target.as_path(), options) { + Ok(_) => Ok(()), + Err(err) => { + if fs::symlink_metadata(&source)?.file_type().is_symlink() { + // silent the error with a symlink + // In case we do --archive, we might copy the symlink + // before the file itself + Ok(()) + } else { + Err(err) + } + } + }?; } } else { copy_file(path.as_path(), local_to_target.as_path(), options)?; @@ -982,11 +1008,7 @@ fn copy_directory(root: &Path, target: &Target, options: &Options) -> CopyResult impl OverwriteMode { fn verify(&self, path: &Path) -> CopyResult<()> { match *self { - OverwriteMode::NoClobber => Err(Error::Skipped(format!( - "Not overwriting {} because of option '{}'", - path.display(), - OPT_NO_CLOBBER - ))), + OverwriteMode::NoClobber => Err(Error::NotAllFilesCopied), OverwriteMode::Interactive(_) => { if prompt_yes!("{}: overwrite {}? ", executable!(), path.display()) { Ok(()) @@ -1041,12 +1063,16 @@ fn copy_attribute(source: &Path, dest: &Path, attribute: &Attribute) -> CopyResu } } }; + Ok(()) } #[cfg(not(windows))] fn symlink_file(source: &Path, dest: &Path, context: &str) -> CopyResult<()> { - Ok(std::os::unix::fs::symlink(source, dest).context(context)?) + match std::os::unix::fs::symlink(source, dest).context(context) { + Ok(_) => Ok(()), + Err(_) => Ok(()), + } } #[cfg(windows)] @@ -1159,11 +1185,9 @@ fn copy_file(source: &Path, dest: &Path, options: &Options) -> CopyResult<()> { .unwrap(); } }; - for attribute in &options.preserve_attributes { copy_attribute(source, dest, attribute)?; } - Ok(()) } @@ -1224,7 +1248,16 @@ fn copy_helper(source: &Path, dest: &Path, options: &Options) -> CopyResult<()> dest.into() }; symlink_file(&link, &dest, &*context_for(&link, &dest))?; + } else if source.to_string_lossy() == "/dev/null" { + /* workaround a limitation of fs::copy + * https://github.com/rust-lang/rust/issues/79390 + */ + File::create(dest)?; } else { + if options.parents { + let parent = dest.parent().unwrap_or(dest); + fs::create_dir_all(parent)?; + } fs::copy(source, dest).context(&*context_for(source, dest))?; } diff --git a/src/uu/csplit/Cargo.toml b/src/uu/csplit/Cargo.toml new file mode 100644 index 000000000..7687991b0 --- /dev/null +++ b/src/uu/csplit/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "uu_csplit" +version = "0.0.6" +authors = ["uutils developers"] +license = "MIT" +description = "csplit ~ (uutils) Output pieces of FILE separated by PATTERN(s) to files 'xx00', 'xx01', ..., and output byte counts of each piece to standard output" + +homepage = "https://github.com/uutils/coreutils" +repository = "https://github.com/uutils/coreutils/tree/master/src/uu/ls" +keywords = ["coreutils", "uutils", "cross-platform", "cli", "utility"] +categories = ["command-line-utilities"] +edition = "2018" + +[lib] +path = "src/csplit.rs" + +[dependencies] +clap = "2.33" +thiserror = "1.0" +regex = "1.0.0" +glob = "0.2.11" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["entries", "fs"] } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } + +[[bin]] +name = "csplit" +path = "src/main.rs" diff --git a/src/uu/csplit/src/csplit.rs b/src/uu/csplit/src/csplit.rs new file mode 100644 index 000000000..ce11ba49a --- /dev/null +++ b/src/uu/csplit/src/csplit.rs @@ -0,0 +1,798 @@ +#![crate_name = "uu_csplit"] + +#[macro_use] +extern crate uucore; +use clap::{App, Arg, ArgMatches}; +use regex::Regex; +use std::cmp::Ordering; +use std::io::{self, BufReader}; +use std::{ + fs::{remove_file, File}, + io::{BufRead, BufWriter, Write}, +}; + +mod csplit_error; +mod patterns; +mod splitname; + +use crate::csplit_error::CsplitError; +use crate::splitname::SplitName; + +static VERSION: &str = env!("CARGO_PKG_VERSION"); +static SUMMARY: &str = "split a file into sections determined by context lines"; +static LONG_HELP: &str = "Output pieces of FILE separated by PATTERN(s) to files 'xx00', 'xx01', ..., and output byte counts of each piece to standard output."; + +mod options { + pub const SUFFIX_FORMAT: &str = "suffix-format"; + pub const SUPPRESS_MATCHED: &str = "suppress-matched"; + pub const DIGITS: &str = "digits"; + pub const PREFIX: &str = "prefix"; + pub const KEEP_FILES: &str = "keep-files"; + pub const QUIET: &str = "quiet"; + pub const ELIDE_EMPTY_FILES: &str = "elide-empty-files"; + pub const FILE: &str = "file"; + pub const PATTERN: &str = "pattern"; +} + +fn get_usage() -> String { + format!("{0} [OPTION]... FILE PATTERN...", executable!()) +} + +/// Command line options for csplit. +pub struct CsplitOptions { + split_name: crate::SplitName, + keep_files: bool, + quiet: bool, + elide_empty_files: bool, + suppress_matched: bool, +} + +impl CsplitOptions { + fn new(matches: &ArgMatches) -> CsplitOptions { + let keep_files = matches.is_present(options::KEEP_FILES); + let quiet = matches.is_present(options::QUIET); + let elide_empty_files = matches.is_present(options::ELIDE_EMPTY_FILES); + let suppress_matched = matches.is_present(options::SUPPRESS_MATCHED); + + CsplitOptions { + split_name: crash_if_err!( + 1, + SplitName::new( + matches.value_of(options::PREFIX).map(str::to_string), + matches.value_of(options::SUFFIX_FORMAT).map(str::to_string), + matches.value_of(options::DIGITS).map(str::to_string) + ) + ), + keep_files, + quiet, + elide_empty_files, + suppress_matched, + } + } +} + +/// Splits a file into severals according to the command line patterns. +/// +/// # Errors +/// +/// - [`io::Error`] if there is some problem reading/writing from/to a file. +/// - [`::CsplitError::LineOutOfRange`] if the linenum pattern is larger than the number of input +/// lines. +/// - [`::CsplitError::LineOutOfRangeOnRepetition`], like previous but after applying the pattern +/// more than once. +/// - [`::CsplitError::MatchNotFound`] if no line matched a regular expression. +/// - [`::CsplitError::MatchNotFoundOnRepetition`], like previous but after applying the pattern +/// more than once. +pub fn csplit( + options: &CsplitOptions, + patterns: Vec, + input: T, +) -> Result<(), CsplitError> +where + T: BufRead, +{ + let mut input_iter = InputSplitter::new(input.lines().enumerate()); + let mut split_writer = SplitWriter::new(&options); + let ret = do_csplit(&mut split_writer, patterns, &mut input_iter); + + // consume the rest + input_iter.rewind_buffer(); + if let Some((_, line)) = input_iter.next() { + split_writer.new_writer()?; + split_writer.writeln(line?)?; + for (_, line) in input_iter { + split_writer.writeln(line?)?; + } + split_writer.finish_split(); + } + // delete files on error by default + if ret.is_err() && !options.keep_files { + split_writer.delete_all_splits()?; + } + ret +} + +fn do_csplit( + split_writer: &mut SplitWriter, + patterns: Vec, + input_iter: &mut InputSplitter, +) -> Result<(), CsplitError> +where + I: Iterator)>, +{ + // split the file based on patterns + for pattern in patterns.into_iter() { + let pattern_as_str = pattern.to_string(); + #[allow(clippy::match_like_matches_macro)] + let is_skip = if let patterns::Pattern::SkipToMatch(_, _, _) = pattern { + true + } else { + false + }; + match pattern { + patterns::Pattern::UpToLine(n, ex) => { + let mut up_to_line = n; + for (_, ith) in ex.iter() { + split_writer.new_writer()?; + match split_writer.do_to_line(&pattern_as_str, up_to_line, input_iter) { + // the error happened when applying the pattern more than once + Err(CsplitError::LineOutOfRange(_)) if ith != 1 => { + return Err(CsplitError::LineOutOfRangeOnRepetition( + pattern_as_str.to_string(), + ith - 1, + )); + } + Err(err) => return Err(err), + // continue the splitting process + Ok(()) => (), + } + up_to_line += n; + } + } + patterns::Pattern::UpToMatch(regex, offset, ex) + | patterns::Pattern::SkipToMatch(regex, offset, ex) => { + for (max, ith) in ex.iter() { + if is_skip { + // when skipping a part of the input, no writer is created + split_writer.as_dev_null(); + } else { + split_writer.new_writer()?; + } + match ( + split_writer.do_to_match(&pattern_as_str, ®ex, offset, input_iter), + max, + ) { + // in case of ::pattern::ExecutePattern::Always, then it's fine not to find a + // matching line + (Err(CsplitError::MatchNotFound(_)), None) => { + return Ok(()); + } + // the error happened when applying the pattern more than once + (Err(CsplitError::MatchNotFound(_)), Some(m)) if m != 1 && ith != 1 => { + return Err(CsplitError::MatchNotFoundOnRepetition( + pattern_as_str.to_string(), + ith - 1, + )); + } + (Err(err), _) => return Err(err), + // continue the splitting process + (Ok(()), _) => (), + }; + } + } + }; + } + Ok(()) +} + +/// Write a portion of the input file into a split which filename is based on an incrementing +/// counter. +struct SplitWriter<'a> { + /// the options set through the command line + options: &'a CsplitOptions, + /// a split counter + counter: usize, + /// the writer to the current split + current_writer: Option>, + /// the size in bytes of the current split + size: usize, + /// flag to indicate that no content should be written to a split + dev_null: bool, +} + +impl<'a> Drop for SplitWriter<'a> { + fn drop(&mut self) { + if self.options.elide_empty_files && self.size == 0 { + let file_name = self.options.split_name.get(self.counter); + remove_file(file_name).expect("Failed to elide split"); + } + } +} + +impl<'a> SplitWriter<'a> { + fn new(options: &CsplitOptions) -> SplitWriter { + SplitWriter { + options, + counter: 0, + current_writer: None, + size: 0, + dev_null: false, + } + } + + /// Creates a new split and returns its filename. + /// + /// # Errors + /// + /// The creation of the split file may fail with some [`io::Error`]. + fn new_writer(&mut self) -> io::Result<()> { + let file_name = self.options.split_name.get(self.counter); + let file = File::create(&file_name)?; + self.current_writer = Some(BufWriter::new(file)); + self.counter += 1; + self.size = 0; + self.dev_null = false; + Ok(()) + } + + /// The current split will not keep any of the read input lines. + fn as_dev_null(&mut self) { + self.dev_null = true; + } + + /// Writes the line to the current split, appending a newline character. + /// If [`dev_null`] is true, then the line is discarded. + /// + /// # Errors + /// + /// Some [`io::Error`] may occur when attempting to write the line. + fn writeln(&mut self, line: String) -> io::Result<()> { + if !self.dev_null { + match self.current_writer { + Some(ref mut current_writer) => { + let bytes = line.as_bytes(); + current_writer.write_all(bytes)?; + current_writer.write_all(b"\n")?; + self.size += bytes.len() + 1; + } + None => panic!("trying to write to a split that was not created"), + } + } + Ok(()) + } + + /// Perform some operations after completing a split, i.e., either remove it + /// if the [`::ELIDE_EMPTY_FILES_OPT`] option is enabled, or print how much bytes were written + /// to it if [`::QUIET_OPT`] is disabled. + /// + /// # Errors + /// + /// Some [`io::Error`] if the split could not be removed in case it should be elided. + fn finish_split(&mut self) { + if !self.dev_null { + if self.options.elide_empty_files && self.size == 0 { + self.counter -= 1; + } else if !self.options.quiet { + println!("{}", self.size); + } + } + } + + /// Removes all the split files that were created. + /// + /// # Errors + /// + /// Returns an [`io::Error`] if there was a problem removing a split. + fn delete_all_splits(&self) -> io::Result<()> { + let mut ret = Ok(()); + for ith in 0..self.counter { + let file_name = self.options.split_name.get(ith); + if let Err(err) = remove_file(file_name) { + ret = Err(err); + } + } + ret + } + + /// Split the input stream up to the line number `n`. + /// + /// If the line number `n` is smaller than the current position in the input, then an empty + /// split is created. + /// + /// # Errors + /// + /// In addition to errors reading/writing from/to a file, if the line number + /// `n` is greater than the total available lines, then a + /// [`::CsplitError::LineOutOfRange`] error is returned. + fn do_to_line( + &mut self, + pattern_as_str: &str, + n: usize, + input_iter: &mut InputSplitter, + ) -> Result<(), CsplitError> + where + I: Iterator)>, + { + input_iter.rewind_buffer(); + input_iter.set_size_of_buffer(1); + + let mut ret = Err(CsplitError::LineOutOfRange(pattern_as_str.to_string())); + while let Some((ln, line)) = input_iter.next() { + let l = line?; + match n.cmp(&(&ln + 1)) { + Ordering::Less => { + if input_iter.add_line_to_buffer(ln, l).is_some() { + panic!("the buffer is big enough to contain 1 line"); + } + ret = Ok(()); + break; + } + Ordering::Equal => { + if !self.options.suppress_matched + && input_iter.add_line_to_buffer(ln, l).is_some() + { + panic!("the buffer is big enough to contain 1 line"); + } + ret = Ok(()); + break; + } + Ordering::Greater => (), + } + self.writeln(l)?; + } + self.finish_split(); + ret + } + + /// Read lines up to the line matching a [`Regex`]. With a non-zero offset, + /// the block of relevant lines can be extended (if positive), or reduced + /// (if negative). + /// + /// # Errors + /// + /// In addition to errors reading/writing from/to a file, the following errors may be returned: + /// - if no line matched, an [`::CsplitError::MatchNotFound`]. + /// - if there are not enough lines to accommodate the offset, an + /// [`::CsplitError::LineOutOfRange`]. + fn do_to_match( + &mut self, + pattern_as_str: &str, + regex: &Regex, + mut offset: i32, + input_iter: &mut InputSplitter, + ) -> Result<(), CsplitError> + where + I: Iterator)>, + { + if offset >= 0 { + // The offset is zero or positive, no need for a buffer on the lines read. + // NOTE: drain the buffer of input_iter, no match should be done within. + for line in input_iter.drain_buffer() { + self.writeln(line)?; + } + // retain the matching line + input_iter.set_size_of_buffer(1); + + while let Some((ln, line)) = input_iter.next() { + let l = line?; + if regex.is_match(&l) { + match (self.options.suppress_matched, offset) { + // no offset, add the line to the next split + (false, 0) => { + if input_iter.add_line_to_buffer(ln, l).is_some() { + panic!("the buffer is big enough to contain 1 line"); + } + } + // a positive offset, some more lines need to be added to the current split + (false, _) => self.writeln(l)?, + _ => (), + }; + offset -= 1; + + // write the extra lines required by the offset + while offset > 0 { + match input_iter.next() { + Some((_, line)) => { + self.writeln(line?)?; + } + None => { + self.finish_split(); + return Err(CsplitError::LineOutOfRange( + pattern_as_str.to_string(), + )); + } + }; + offset -= 1; + } + self.finish_split(); + return Ok(()); + } + self.writeln(l)?; + } + } else { + // With a negative offset we use a buffer to keep the lines within the offset. + // NOTE: do not drain the buffer of input_iter, in case of an LineOutOfRange error + // but do not rewind it either since no match should be done within. + // The consequence is that the buffer may already be full with lines from a previous + // split, which is taken care of when calling `shrink_buffer_to_size`. + let offset_usize = -offset as usize; + input_iter.set_size_of_buffer(offset_usize); + while let Some((ln, line)) = input_iter.next() { + let l = line?; + if regex.is_match(&l) { + for line in input_iter.shrink_buffer_to_size() { + self.writeln(line)?; + } + if !self.options.suppress_matched { + // add 1 to the buffer size to make place for the matched line + input_iter.set_size_of_buffer(offset_usize + 1); + if input_iter.add_line_to_buffer(ln, l).is_some() { + panic!("should be big enough to hold every lines"); + } + } + self.finish_split(); + if input_iter.buffer_len() < offset_usize { + return Err(CsplitError::LineOutOfRange(pattern_as_str.to_string())); + } + return Ok(()); + } + if let Some(line) = input_iter.add_line_to_buffer(ln, l) { + self.writeln(line)?; + } + } + // no match, drain the buffer into the current split + for line in input_iter.drain_buffer() { + self.writeln(line)?; + } + } + + self.finish_split(); + Err(CsplitError::MatchNotFound(pattern_as_str.to_string())) + } +} + +/// An iterator which can output items from a buffer filled externally. +/// This is used to pass matching lines to the next split and to support patterns with a negative offset. +struct InputSplitter +where + I: Iterator)>, +{ + iter: I, + buffer: Vec<::Item>, + /// the number of elements the buffer may hold + size: usize, + /// flag to indicate content off the buffer should be returned instead of off the wrapped + /// iterator + rewind: bool, +} + +impl InputSplitter +where + I: Iterator)>, +{ + fn new(iter: I) -> InputSplitter { + InputSplitter { + iter, + buffer: Vec::new(), + rewind: false, + size: 1, + } + } + + /// Rewind the iteration by outputting the buffer's content. + fn rewind_buffer(&mut self) { + self.rewind = true; + } + + /// Shrink the buffer so that its length is equal to the set size, returning an iterator for + /// the elements that were too much. + fn shrink_buffer_to_size(&mut self) -> impl Iterator + '_ { + let mut shrink_offset = 0; + if self.buffer.len() > self.size { + shrink_offset = self.buffer.len() - self.size; + } + self.buffer + .drain(..shrink_offset) + .map(|(_, line)| line.unwrap()) + } + + /// Drain the content of the buffer. + fn drain_buffer(&mut self) -> impl Iterator + '_ { + self.buffer.drain(..).map(|(_, line)| line.unwrap()) + } + + /// Set the maximum number of lines to keep. + fn set_size_of_buffer(&mut self, size: usize) { + self.size = size; + } + + /// Add a line to the buffer. If the buffer has [`size`] elements, then its head is removed and + /// the new line is pushed to the buffer. The removed head is then available in the returned + /// option. + fn add_line_to_buffer(&mut self, ln: usize, line: String) -> Option { + if self.rewind { + self.buffer.insert(0, (ln, Ok(line))); + None + } else if self.buffer.len() >= self.size { + let (_, head_line) = self.buffer.remove(0); + self.buffer.push((ln, Ok(line))); + Some(head_line.unwrap()) + } else { + self.buffer.push((ln, Ok(line))); + None + } + } + + /// Returns the number of lines stored in the buffer + fn buffer_len(&self) -> usize { + self.buffer.len() + } +} + +impl Iterator for InputSplitter +where + I: Iterator)>, +{ + type Item = ::Item; + + fn next(&mut self) -> Option { + if self.rewind { + if !self.buffer.is_empty() { + return Some(self.buffer.remove(0)); + } + self.rewind = false; + } + self.iter.next() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn input_splitter() { + let input = vec![ + Ok(String::from("aaa")), + Ok(String::from("bbb")), + Ok(String::from("ccc")), + Ok(String::from("ddd")), + ]; + let mut input_splitter = InputSplitter::new(input.into_iter().enumerate()); + + input_splitter.set_size_of_buffer(2); + assert_eq!(input_splitter.buffer_len(), 0); + + match input_splitter.next() { + Some((0, Ok(line))) => { + assert_eq!(line, String::from("aaa")); + assert_eq!(input_splitter.add_line_to_buffer(0, line), None); + assert_eq!(input_splitter.buffer_len(), 1); + } + item @ _ => panic!("wrong item: {:?}", item), + }; + + match input_splitter.next() { + Some((1, Ok(line))) => { + assert_eq!(line, String::from("bbb")); + assert_eq!(input_splitter.add_line_to_buffer(1, line), None); + assert_eq!(input_splitter.buffer_len(), 2); + } + item @ _ => panic!("wrong item: {:?}", item), + }; + + match input_splitter.next() { + Some((2, Ok(line))) => { + assert_eq!(line, String::from("ccc")); + assert_eq!( + input_splitter.add_line_to_buffer(2, line), + Some(String::from("aaa")) + ); + assert_eq!(input_splitter.buffer_len(), 2); + } + item @ _ => panic!("wrong item: {:?}", item), + }; + + input_splitter.rewind_buffer(); + + match input_splitter.next() { + Some((1, Ok(line))) => { + assert_eq!(line, String::from("bbb")); + assert_eq!(input_splitter.buffer_len(), 1); + } + item @ _ => panic!("wrong item: {:?}", item), + }; + + match input_splitter.next() { + Some((2, Ok(line))) => { + assert_eq!(line, String::from("ccc")); + assert_eq!(input_splitter.buffer_len(), 0); + } + item @ _ => panic!("wrong item: {:?}", item), + }; + + match input_splitter.next() { + Some((3, Ok(line))) => { + assert_eq!(line, String::from("ddd")); + assert_eq!(input_splitter.buffer_len(), 0); + } + item @ _ => panic!("wrong item: {:?}", item), + }; + + assert!(input_splitter.next().is_none()); + } + + #[test] + fn input_splitter_interrupt_rewind() { + let input = vec![ + Ok(String::from("aaa")), + Ok(String::from("bbb")), + Ok(String::from("ccc")), + Ok(String::from("ddd")), + ]; + let mut input_splitter = InputSplitter::new(input.into_iter().enumerate()); + + input_splitter.set_size_of_buffer(3); + assert_eq!(input_splitter.buffer_len(), 0); + + match input_splitter.next() { + Some((0, Ok(line))) => { + assert_eq!(line, String::from("aaa")); + assert_eq!(input_splitter.add_line_to_buffer(0, line), None); + assert_eq!(input_splitter.buffer_len(), 1); + } + item @ _ => panic!("wrong item: {:?}", item), + }; + + match input_splitter.next() { + Some((1, Ok(line))) => { + assert_eq!(line, String::from("bbb")); + assert_eq!(input_splitter.add_line_to_buffer(1, line), None); + assert_eq!(input_splitter.buffer_len(), 2); + } + item @ _ => panic!("wrong item: {:?}", item), + }; + + match input_splitter.next() { + Some((2, Ok(line))) => { + assert_eq!(line, String::from("ccc")); + assert_eq!(input_splitter.add_line_to_buffer(2, line), None); + assert_eq!(input_splitter.buffer_len(), 3); + } + item @ _ => panic!("wrong item: {:?}", item), + }; + + input_splitter.rewind_buffer(); + + match input_splitter.next() { + Some((0, Ok(line))) => { + assert_eq!(line, String::from("aaa")); + assert_eq!(input_splitter.add_line_to_buffer(0, line), None); + assert_eq!(input_splitter.buffer_len(), 3); + } + item @ _ => panic!("wrong item: {:?}", item), + }; + + match input_splitter.next() { + Some((0, Ok(line))) => { + assert_eq!(line, String::from("aaa")); + assert_eq!(input_splitter.buffer_len(), 2); + } + item @ _ => panic!("wrong item: {:?}", item), + }; + + match input_splitter.next() { + Some((1, Ok(line))) => { + assert_eq!(line, String::from("bbb")); + assert_eq!(input_splitter.buffer_len(), 1); + } + item @ _ => panic!("wrong item: {:?}", item), + }; + + match input_splitter.next() { + Some((2, Ok(line))) => { + assert_eq!(line, String::from("ccc")); + assert_eq!(input_splitter.buffer_len(), 0); + } + item @ _ => panic!("wrong item: {:?}", item), + }; + + match input_splitter.next() { + Some((3, Ok(line))) => { + assert_eq!(line, String::from("ddd")); + assert_eq!(input_splitter.buffer_len(), 0); + } + item @ _ => panic!("wrong item: {:?}", item), + }; + + assert!(input_splitter.next().is_none()); + } +} + +pub fn uumain(args: impl uucore::Args) -> i32 { + let usage = get_usage(); + let args = args.collect_str(); + + let matches = App::new(executable!()) + .version(VERSION) + .about(SUMMARY) + .usage(&usage[..]) + .arg( + Arg::with_name(options::SUFFIX_FORMAT) + .short("b") + .long(options::SUFFIX_FORMAT) + .value_name("FORMAT") + .help("use sprintf FORMAT instead of %02d"), + ) + .arg( + Arg::with_name(options::PREFIX) + .short("f") + .long(options::PREFIX) + .value_name("PREFIX") + .help("use PREFIX instead of 'xx'"), + ) + .arg( + Arg::with_name(options::KEEP_FILES) + .short("k") + .long(options::KEEP_FILES) + .help("do not remove output files on errors"), + ) + .arg( + Arg::with_name(options::SUPPRESS_MATCHED) + .long(options::SUPPRESS_MATCHED) + .help("suppress the lines matching PATTERN"), + ) + .arg( + Arg::with_name(options::DIGITS) + .short("n") + .long(options::DIGITS) + .value_name("DIGITS") + .help("use specified number of digits instead of 2"), + ) + .arg( + Arg::with_name(options::QUIET) + .short("s") + .long(options::QUIET) + .visible_alias("silent") + .help("do not print counts of output file sizes"), + ) + .arg( + Arg::with_name(options::ELIDE_EMPTY_FILES) + .short("z") + .long(options::ELIDE_EMPTY_FILES) + .help("remove empty output files"), + ) + .arg(Arg::with_name(options::FILE).hidden(true).required(true)) + .arg( + Arg::with_name(options::PATTERN) + .hidden(true) + .multiple(true) + .required(true), + ) + .after_help(LONG_HELP) + .get_matches_from(args); + + // get the file to split + let file_name = matches.value_of(options::FILE).unwrap(); + + // get the patterns to split on + let patterns: Vec = matches + .values_of(options::PATTERN) + .unwrap() + .map(str::to_string) + .collect(); + let patterns = return_if_err!(1, patterns::get_patterns(&patterns[..])); + let options = CsplitOptions::new(&matches); + if file_name == "-" { + let stdin = io::stdin(); + crash_if_err!(1, csplit(&options, patterns, stdin.lock())); + } else { + let file = return_if_err!(1, File::open(file_name)); + let file_metadata = return_if_err!(1, file.metadata()); + if !file_metadata.is_file() { + crash!(1, "'{}' is not a regular file", file_name); + } + crash_if_err!(1, csplit(&options, patterns, BufReader::new(file))); + }; + 0 +} diff --git a/src/uu/csplit/src/csplit_error.rs b/src/uu/csplit/src/csplit_error.rs new file mode 100644 index 000000000..637cf8890 --- /dev/null +++ b/src/uu/csplit/src/csplit_error.rs @@ -0,0 +1,35 @@ +use std::io; +use thiserror::Error; + +/// 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)] + LineOutOfRange(String), + #[error("'{}': line number out of range on repetition {}", _0, _1)] + LineOutOfRangeOnRepetition(String, usize), + #[error("'{}': match not found", _0)] + MatchNotFound(String), + #[error("'{}': match not found on repetition {}", _0, _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)] + InvalidPattern(String), + #[error("invalid number: '{}'", _0)] + InvalidNumber(String), + #[error("incorrect conversion specification in suffix")] + SuffixFormatIncorrect, + #[error("too many % conversion specifications in suffix")] + SuffixFormatTooManyPercents, +} + +impl From for CsplitError { + fn from(error: io::Error) -> Self { + CsplitError::IoError(error) + } +} diff --git a/src/uu/csplit/src/main.rs b/src/uu/csplit/src/main.rs new file mode 100644 index 000000000..97aeb3821 --- /dev/null +++ b/src/uu/csplit/src/main.rs @@ -0,0 +1 @@ +uucore_procs::main!(uu_csplit); // spell-checker:ignore procs uucore diff --git a/src/uu/csplit/src/patterns.rs b/src/uu/csplit/src/patterns.rs new file mode 100644 index 000000000..d2f14578a --- /dev/null +++ b/src/uu/csplit/src/patterns.rs @@ -0,0 +1,355 @@ +use crate::csplit_error::CsplitError; +use regex::Regex; + +/// The definition of a pattern to match on a line. +#[derive(Debug)] +pub enum Pattern { + /// Copy the file's content to a split up to, not including, the given line number. The number + /// of times the pattern is executed is detailed in [`ExecutePattern`]. + UpToLine(usize, ExecutePattern), + /// Copy the file's content to a split up to, not including, the line matching the regex. The + /// integer is an offset relative to the matched line of what to include (if positive) or + /// to exclude (if negative). The number of times the pattern is executed is detailed in + /// [`ExecutePattern`]. + UpToMatch(Regex, i32, ExecutePattern), + /// Skip the file's content up to, not including, the line matching the regex. The integer + /// is an offset relative to the matched line of what to include (if positive) or to exclude + /// (if negative). The number of times the pattern is executed is detailed in [`ExecutePattern`]. + SkipToMatch(Regex, i32, ExecutePattern), +} + +impl ToString for Pattern { + fn to_string(&self) -> String { + match self { + Pattern::UpToLine(n, _) => n.to_string(), + Pattern::UpToMatch(regex, 0, _) => format!("/{}/", regex.as_str()), + Pattern::UpToMatch(regex, offset, _) => format!("/{}/{:+}", regex.as_str(), offset), + Pattern::SkipToMatch(regex, 0, _) => format!("%{}%", regex.as_str()), + Pattern::SkipToMatch(regex, offset, _) => format!("%{}%{:+}", regex.as_str(), offset), + } + } +} + +/// The number of times a pattern can be used. +#[derive(Debug)] +pub enum ExecutePattern { + /// Execute the pattern as many times as possible + Always, + /// Execute the pattern a fixed number of times + Times(usize), +} + +impl ExecutePattern { + pub fn iter(&self) -> ExecutePatternIter { + match self { + ExecutePattern::Times(n) => ExecutePatternIter::new(Some(*n)), + ExecutePattern::Always => ExecutePatternIter::new(None), + } + } +} + +pub struct ExecutePatternIter { + max: Option, + cur: usize, +} + +impl ExecutePatternIter { + fn new(max: Option) -> ExecutePatternIter { + ExecutePatternIter { max, cur: 0 } + } +} + +impl Iterator for ExecutePatternIter { + type Item = (Option, usize); + + fn next(&mut self) -> Option<(Option, usize)> { + match self.max { + // iterate until m is reached + Some(m) => { + if self.cur == m { + None + } else { + self.cur += 1; + Some((self.max, self.cur)) + } + } + // no limit, just increment a counter + None => { + self.cur += 1; + Some((None, self.cur)) + } + } + } +} + +/// Parses the definitions of patterns given on the command line into a list of [`Pattern`]s. +/// +/// # Errors +/// +/// If a pattern is incorrect, a [`::CsplitError::InvalidPattern`] error is returned, which may be +/// due to, e.g.,: +/// - an invalid regular expression; +/// - an invalid number for, e.g., the offset. +pub fn get_patterns(args: &[String]) -> Result, CsplitError> { + let patterns = extract_patterns(args)?; + validate_line_numbers(&patterns)?; + Ok(patterns) +} + +fn extract_patterns(args: &[String]) -> Result, CsplitError> { + let mut patterns = Vec::with_capacity(args.len()); + let to_match_reg = + Regex::new(r"^(/(?P.+)/|%(?P.+)%)(?P[\+-]\d+)?$").unwrap(); + let execute_ntimes_reg = Regex::new(r"^\{(?P\d+)|\*\}$").unwrap(); + let mut iter = args.iter().peekable(); + + while let Some(arg) = iter.next() { + // get the number of times a pattern is repeated, which is at least once plus whatever is + // in the quantifier. + let execute_ntimes = match iter.peek() { + None => ExecutePattern::Times(1), + Some(&next_item) => { + match execute_ntimes_reg.captures(next_item) { + None => ExecutePattern::Times(1), + Some(r) => { + // skip the next item + iter.next(); + if let Some(times) = r.name("TIMES") { + ExecutePattern::Times(times.as_str().parse::().unwrap() + 1) + } else { + ExecutePattern::Always + } + } + } + } + }; + + // get the pattern definition + if let Some(captures) = to_match_reg.captures(arg) { + let offset = match captures.name("OFFSET") { + None => 0, + Some(m) => m.as_str().parse().unwrap(), + }; + if let Some(up_to_match) = captures.name("UPTO") { + let pattern = match Regex::new(up_to_match.as_str()) { + Err(_) => { + return Err(CsplitError::InvalidPattern(arg.to_string())); + } + Ok(reg) => reg, + }; + patterns.push(Pattern::UpToMatch(pattern, offset, execute_ntimes)); + } else if let Some(skip_to_match) = captures.name("SKIPTO") { + let pattern = match Regex::new(skip_to_match.as_str()) { + Err(_) => { + return Err(CsplitError::InvalidPattern(arg.to_string())); + } + Ok(reg) => reg, + }; + patterns.push(Pattern::SkipToMatch(pattern, offset, execute_ntimes)); + } + } else if let Ok(line_number) = arg.parse::() { + patterns.push(Pattern::UpToLine(line_number, execute_ntimes)); + } else { + return Err(CsplitError::InvalidPattern(arg.to_string())); + } + } + Ok(patterns) +} + +/// Asserts the line numbers are in increasing order, starting at 1. +fn validate_line_numbers(patterns: &[Pattern]) -> Result<(), CsplitError> { + patterns + .iter() + .filter_map(|pattern| match pattern { + Pattern::UpToLine(line_number, _) => Some(line_number), + _ => None, + }) + .try_fold(0, |prev_ln, ¤t_ln| match (prev_ln, current_ln) { + // a line number cannot be zero + (_, 0) => Err(CsplitError::LineNumberIsZero), + // two consecutifs numbers should not be equal + (n, m) if n == m => { + show_warning!("line number '{}' is the same as preceding line number", n); + Ok(n) + } + // a number cannot be greater than the one that follows + (n, m) if n > m => Err(CsplitError::LineNumberSmallerThanPrevious(m, n)), + (_, m) => Ok(m), + })?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn bad_pattern() { + let input = vec!["bad".to_string()]; + assert!(get_patterns(input.as_slice()).is_err()); + } + + #[test] + fn up_to_line_pattern() { + let input: Vec = vec!["24", "42", "{*}", "50", "{4}"] + .into_iter() + .map(|v| v.to_string()) + .collect(); + let patterns = get_patterns(input.as_slice()).unwrap(); + assert_eq!(patterns.len(), 3); + match patterns.get(0) { + Some(Pattern::UpToLine(24, ExecutePattern::Times(1))) => (), + _ => panic!("expected UpToLine pattern"), + }; + match patterns.get(1) { + Some(Pattern::UpToLine(42, ExecutePattern::Always)) => (), + _ => panic!("expected UpToLine pattern"), + }; + match patterns.get(2) { + Some(Pattern::UpToLine(50, ExecutePattern::Times(5))) => (), + _ => panic!("expected UpToLine pattern"), + }; + } + + #[test] + fn up_to_match_pattern() { + let input: Vec = vec![ + "/test1.*end$/", + "/test2.*end$/", + "{*}", + "/test3.*end$/", + "{4}", + "/test4.*end$/+3", + "/test5.*end$/-3", + ] + .into_iter() + .map(|v| v.to_string()) + .collect(); + let patterns = get_patterns(input.as_slice()).unwrap(); + assert_eq!(patterns.len(), 5); + match patterns.get(0) { + Some(Pattern::UpToMatch(reg, 0, ExecutePattern::Times(1))) => { + let parsed_reg = format!("{}", reg); + assert_eq!(parsed_reg, "test1.*end$"); + } + _ => panic!("expected UpToMatch pattern"), + }; + match patterns.get(1) { + Some(Pattern::UpToMatch(reg, 0, ExecutePattern::Always)) => { + let parsed_reg = format!("{}", reg); + assert_eq!(parsed_reg, "test2.*end$"); + } + _ => panic!("expected UpToMatch pattern"), + }; + match patterns.get(2) { + Some(Pattern::UpToMatch(reg, 0, ExecutePattern::Times(5))) => { + let parsed_reg = format!("{}", reg); + assert_eq!(parsed_reg, "test3.*end$"); + } + _ => panic!("expected UpToMatch pattern"), + }; + match patterns.get(3) { + Some(Pattern::UpToMatch(reg, 3, ExecutePattern::Times(1))) => { + let parsed_reg = format!("{}", reg); + assert_eq!(parsed_reg, "test4.*end$"); + } + _ => panic!("expected UpToMatch pattern"), + }; + match patterns.get(4) { + Some(Pattern::UpToMatch(reg, -3, ExecutePattern::Times(1))) => { + let parsed_reg = format!("{}", reg); + assert_eq!(parsed_reg, "test5.*end$"); + } + _ => panic!("expected UpToMatch pattern"), + }; + } + + #[test] + fn skip_to_match_pattern() { + let input: Vec = vec![ + "%test1.*end$%", + "%test2.*end$%", + "{*}", + "%test3.*end$%", + "{4}", + "%test4.*end$%+3", + "%test5.*end$%-3", + ] + .into_iter() + .map(|v| v.to_string()) + .collect(); + let patterns = get_patterns(input.as_slice()).unwrap(); + assert_eq!(patterns.len(), 5); + match patterns.get(0) { + Some(Pattern::SkipToMatch(reg, 0, ExecutePattern::Times(1))) => { + let parsed_reg = format!("{}", reg); + assert_eq!(parsed_reg, "test1.*end$"); + } + _ => panic!("expected SkipToMatch pattern"), + }; + match patterns.get(1) { + Some(Pattern::SkipToMatch(reg, 0, ExecutePattern::Always)) => { + let parsed_reg = format!("{}", reg); + assert_eq!(parsed_reg, "test2.*end$"); + } + _ => panic!("expected SkipToMatch pattern"), + }; + match patterns.get(2) { + Some(Pattern::SkipToMatch(reg, 0, ExecutePattern::Times(5))) => { + let parsed_reg = format!("{}", reg); + assert_eq!(parsed_reg, "test3.*end$"); + } + _ => panic!("expected SkipToMatch pattern"), + }; + match patterns.get(3) { + Some(Pattern::SkipToMatch(reg, 3, ExecutePattern::Times(1))) => { + let parsed_reg = format!("{}", reg); + assert_eq!(parsed_reg, "test4.*end$"); + } + _ => panic!("expected SkipToMatch pattern"), + }; + match patterns.get(4) { + Some(Pattern::SkipToMatch(reg, -3, ExecutePattern::Times(1))) => { + let parsed_reg = format!("{}", reg); + assert_eq!(parsed_reg, "test5.*end$"); + } + _ => panic!("expected SkipToMatch pattern"), + }; + } + + #[test] + fn line_number_zero() { + let patterns = vec![Pattern::UpToLine(0, ExecutePattern::Times(1))]; + match validate_line_numbers(&patterns) { + Err(CsplitError::LineNumberIsZero) => (), + _ => panic!("expected LineNumberIsZero error"), + } + } + + #[test] + fn line_number_smaller_than_previous() { + let input: Vec = vec!["10".to_string(), "5".to_string()]; + match get_patterns(input.as_slice()) { + Err(CsplitError::LineNumberSmallerThanPrevious(5, 10)) => (), + _ => panic!("expected LineNumberSmallerThanPrevious error"), + } + } + + #[test] + fn line_number_smaller_than_previous_separate() { + let input: Vec = vec!["10".to_string(), "/20/".to_string(), "5".to_string()]; + match get_patterns(input.as_slice()) { + Err(CsplitError::LineNumberSmallerThanPrevious(5, 10)) => (), + _ => panic!("expected LineNumberSmallerThanPrevious error"), + } + } + + #[test] + fn line_number_zero_separate() { + let input: Vec = vec!["10".to_string(), "/20/".to_string(), "0".to_string()]; + match get_patterns(input.as_slice()) { + Err(CsplitError::LineNumberIsZero) => (), + _ => panic!("expected LineNumberIsZero error"), + } + } +} diff --git a/src/uu/csplit/src/splitname.rs b/src/uu/csplit/src/splitname.rs new file mode 100644 index 000000000..66b17ba67 --- /dev/null +++ b/src/uu/csplit/src/splitname.rs @@ -0,0 +1,395 @@ +use regex::Regex; + +use crate::csplit_error::CsplitError; + +/// Computes the filename of a split, taking into consideration a possible user-defined suffix +/// format. +pub struct SplitName { + fn_split_name: Box String>, +} + +impl SplitName { + /// Creates a new SplitName with the given user-defined options: + /// - `prefix_opt` specifies a prefix for all splits. + /// - `format_opt` specifies a custom format for the suffix part of the filename, using the + /// `sprintf` format notation. + /// - `n_digits_opt` defines the width of the split number. + /// + /// # Caveats + /// + /// If `prefix_opt` and `format_opt` are defined, and the `format_opt` has some string appearing + /// before the conversion pattern (e.g., "here-%05d"), then it is appended to the passed prefix + /// via `prefix_opt`. + /// + /// If `n_digits_opt` and `format_opt` are defined, then width defined in `format_opt` is + /// taken. + pub fn new( + prefix_opt: Option, + format_opt: Option, + n_digits_opt: Option, + ) -> Result { + // get the prefix + let prefix = prefix_opt.unwrap_or_else(|| "xx".to_string()); + // the width for the split offset + let n_digits = match n_digits_opt { + None => 2, + Some(opt) => match opt.parse::() { + Ok(digits) => digits, + Err(_) => return Err(CsplitError::InvalidNumber(opt)), + }, + }; + // translate the custom format into a function + let fn_split_name: Box String> = match format_opt { + None => Box::new(move |n: usize| -> String { + format!("{}{:0width$}", prefix, n, width = n_digits) + }), + Some(custom) => { + let spec = + Regex::new(r"(?P%(?P[0#-])(?P\d+)?(?P[diuoxX]))") + .unwrap(); + let mut captures_iter = spec.captures_iter(&custom); + let custom_fn: Box String> = match captures_iter.next() { + Some(captures) => { + let all = captures.name("ALL").unwrap(); + let before = custom[0..all.start()].to_owned(); + let after = custom[all.end()..].to_owned(); + let n_digits = match captures.name("WIDTH") { + None => 0, + Some(m) => m.as_str().parse::().unwrap(), + }; + match (captures.name("FLAG"), captures.name("TYPE")) { + (Some(ref f), Some(ref t)) => { + match (f.as_str(), t.as_str()) { + /* + * zero padding + */ + // decimal + ("0", "d") | ("0", "i") | ("0", "u") => { + Box::new(move |n: usize| -> String { + format!( + "{}{}{:0width$}{}", + prefix, + before, + n, + after, + width = n_digits + ) + }) + } + // octal + ("0", "o") => Box::new(move |n: usize| -> String { + format!( + "{}{}{:0width$o}{}", + prefix, + before, + n, + after, + width = n_digits + ) + }), + // lower hexadecimal + ("0", "x") => Box::new(move |n: usize| -> String { + format!( + "{}{}{:0width$x}{}", + prefix, + before, + n, + after, + width = n_digits + ) + }), + // upper hexadecimal + ("0", "X") => Box::new(move |n: usize| -> String { + format!( + "{}{}{:0width$X}{}", + prefix, + before, + n, + after, + width = n_digits + ) + }), + + /* + * Alternate form + */ + // octal + ("#", "o") => Box::new(move |n: usize| -> String { + format!( + "{}{}{:>#width$o}{}", + prefix, + before, + n, + after, + width = n_digits + ) + }), + // lower hexadecimal + ("#", "x") => Box::new(move |n: usize| -> String { + format!( + "{}{}{:>#width$x}{}", + prefix, + before, + n, + after, + width = n_digits + ) + }), + // upper hexadecimal + ("#", "X") => Box::new(move |n: usize| -> String { + format!( + "{}{}{:>#width$X}{}", + prefix, + before, + n, + after, + width = n_digits + ) + }), + + /* + * Left adjusted + */ + // decimal + ("-", "d") | ("-", "i") | ("-", "u") => { + Box::new(move |n: usize| -> String { + format!( + "{}{}{:<#width$}{}", + prefix, + before, + n, + after, + width = n_digits + ) + }) + } + // octal + ("-", "o") => Box::new(move |n: usize| -> String { + format!( + "{}{}{:<#width$o}{}", + prefix, + before, + n, + after, + width = n_digits + ) + }), + // lower hexadecimal + ("-", "x") => Box::new(move |n: usize| -> String { + format!( + "{}{}{:<#width$x}{}", + prefix, + before, + n, + after, + width = n_digits + ) + }), + // upper hexadecimal + ("-", "X") => Box::new(move |n: usize| -> String { + format!( + "{}{}{:<#width$X}{}", + prefix, + before, + n, + after, + width = n_digits + ) + }), + + _ => return Err(CsplitError::SuffixFormatIncorrect), + } + } + _ => return Err(CsplitError::SuffixFormatIncorrect), + } + } + None => return Err(CsplitError::SuffixFormatIncorrect), + }; + + // there cannot be more than one format pattern + if captures_iter.next().is_some() { + return Err(CsplitError::SuffixFormatTooManyPercents); + } + custom_fn + } + }; + + Ok(SplitName { fn_split_name }) + } + + /// Returns the filename of the i-th split. + pub fn get(&self, n: usize) -> String { + (self.fn_split_name)(n) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn invalid_number() { + let split_name = SplitName::new(None, None, Some(String::from("bad"))); + match split_name { + Err(CsplitError::InvalidNumber(_)) => (), + _ => panic!("should fail with InvalidNumber"), + }; + } + + #[test] + fn invalid_suffix_format1() { + let split_name = SplitName::new(None, Some(String::from("no conversion string")), None); + match split_name { + Err(CsplitError::SuffixFormatIncorrect) => (), + _ => panic!("should fail with SuffixFormatIncorrect"), + }; + } + + #[test] + fn invalid_suffix_format2() { + let split_name = SplitName::new(None, Some(String::from("%042a")), None); + match split_name { + Err(CsplitError::SuffixFormatIncorrect) => (), + _ => panic!("should fail with SuffixFormatIncorrect"), + }; + } + + #[test] + fn default_formatter() { + let split_name = SplitName::new(None, None, None).unwrap(); + assert_eq!(split_name.get(2), "xx02"); + } + + #[test] + fn default_formatter_with_prefix() { + let split_name = SplitName::new(Some(String::from("aaa")), None, None).unwrap(); + assert_eq!(split_name.get(2), "aaa02"); + } + + #[test] + fn default_formatter_with_width() { + let split_name = SplitName::new(None, None, Some(String::from("5"))).unwrap(); + assert_eq!(split_name.get(2), "xx00002"); + } + + #[test] + fn zero_padding_decimal1() { + let split_name = SplitName::new(None, Some(String::from("cst-%03d-")), None).unwrap(); + assert_eq!(split_name.get(2), "xxcst-002-"); + } + + #[test] + fn zero_padding_decimal2() { + let split_name = SplitName::new( + Some(String::from("pre-")), + Some(String::from("cst-%03d-post")), + None, + ) + .unwrap(); + assert_eq!(split_name.get(2), "pre-cst-002-post"); + } + + #[test] + fn zero_padding_decimal3() { + let split_name = SplitName::new( + None, + Some(String::from("cst-%03d-")), + Some(String::from("42")), + ) + .unwrap(); + assert_eq!(split_name.get(2), "xxcst-002-"); + } + + #[test] + fn zero_padding_decimal4() { + let split_name = SplitName::new(None, Some(String::from("cst-%03i-")), None).unwrap(); + assert_eq!(split_name.get(2), "xxcst-002-"); + } + + #[test] + fn zero_padding_decimal5() { + let split_name = SplitName::new(None, Some(String::from("cst-%03u-")), None).unwrap(); + assert_eq!(split_name.get(2), "xxcst-002-"); + } + + #[test] + fn zero_padding_octal() { + let split_name = SplitName::new(None, Some(String::from("cst-%03o-")), None).unwrap(); + assert_eq!(split_name.get(42), "xxcst-052-"); + } + + #[test] + fn zero_padding_lower_hexa() { + let split_name = SplitName::new(None, Some(String::from("cst-%03x-")), None).unwrap(); + assert_eq!(split_name.get(42), "xxcst-02a-"); + } + + #[test] + fn zero_padding_upper_hexa() { + let split_name = SplitName::new(None, Some(String::from("cst-%03X-")), None).unwrap(); + assert_eq!(split_name.get(42), "xxcst-02A-"); + } + + #[test] + fn alternate_form_octal() { + let split_name = SplitName::new(None, Some(String::from("cst-%#10o-")), None).unwrap(); + assert_eq!(split_name.get(42), "xxcst- 0o52-"); + } + + #[test] + fn alternate_form_lower_hexa() { + let split_name = SplitName::new(None, Some(String::from("cst-%#10x-")), None).unwrap(); + assert_eq!(split_name.get(42), "xxcst- 0x2a-"); + } + + #[test] + fn alternate_form_upper_hexa() { + let split_name = SplitName::new(None, Some(String::from("cst-%#10X-")), None).unwrap(); + assert_eq!(split_name.get(42), "xxcst- 0x2A-"); + } + + #[test] + fn left_adjusted_decimal1() { + let split_name = SplitName::new(None, Some(String::from("cst-%-10d-")), None).unwrap(); + assert_eq!(split_name.get(42), "xxcst-42 -"); + } + + #[test] + fn left_adjusted_decimal2() { + let split_name = SplitName::new(None, Some(String::from("cst-%-10i-")), None).unwrap(); + assert_eq!(split_name.get(42), "xxcst-42 -"); + } + + #[test] + fn left_adjusted_decimal3() { + let split_name = SplitName::new(None, Some(String::from("cst-%-10u-")), None).unwrap(); + assert_eq!(split_name.get(42), "xxcst-42 -"); + } + + #[test] + fn left_adjusted_octal() { + let split_name = SplitName::new(None, Some(String::from("cst-%-10o-")), None).unwrap(); + assert_eq!(split_name.get(42), "xxcst-0o52 -"); + } + + #[test] + fn left_adjusted_lower_hexa() { + let split_name = SplitName::new(None, Some(String::from("cst-%-10x-")), None).unwrap(); + assert_eq!(split_name.get(42), "xxcst-0x2a -"); + } + + #[test] + fn left_adjusted_upper_hexa() { + let split_name = SplitName::new(None, Some(String::from("cst-%-10X-")), None).unwrap(); + assert_eq!(split_name.get(42), "xxcst-0x2A -"); + } + + #[test] + fn too_many_percent() { + let split_name = SplitName::new(None, Some(String::from("%02d-%-3x")), None); + match split_name { + Err(CsplitError::SuffixFormatTooManyPercents) => (), + _ => panic!("should fail with SuffixFormatTooManyPercents"), + }; + } +} diff --git a/src/uu/cut/Cargo.toml b/src/uu/cut/Cargo.toml index d504cebb3..d892ddeb5 100644 --- a/src/uu/cut/Cargo.toml +++ b/src/uu/cut/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_cut" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "cut ~ (uutils) display byte/field columns of input lines" @@ -15,8 +15,9 @@ edition = "2018" path = "src/cut.rs" [dependencies] -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +clap = "2.33" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "cut" diff --git a/src/uu/cut/src/buffer.rs b/src/uu/cut/src/buffer.rs index 7ee26612e..6c3238be1 100644 --- a/src/uu/cut/src/buffer.rs +++ b/src/uu/cut/src/buffer.rs @@ -107,7 +107,7 @@ impl self::Bytes::Select for ByteReader { Comp, Part, Newl, - }; + } use self::Bytes::Selected::*; diff --git a/src/uu/cut/src/cut.rs b/src/uu/cut/src/cut.rs index 33399aba0..6b09b91d9 100644 --- a/src/uu/cut/src/cut.rs +++ b/src/uu/cut/src/cut.rs @@ -10,17 +10,19 @@ #[macro_use] extern crate uucore; +use clap::{App, Arg}; use std::fs::File; use std::io::{stdin, stdout, BufRead, BufReader, Read, Stdout, Write}; use std::path::Path; -use self::ranges::Range; use self::searcher::Searcher; +use uucore::ranges::Range; mod buffer; -mod ranges; mod searcher; +static NAME: &str = "cut"; +static VERSION: &str = env!("CARGO_PKG_VERSION"); static SYNTAX: &str = "[-d] [-s] [-z] [--output-delimiter] ((-f|-b|-c) {{sequence}}) {{sourcefile}}+"; static SUMMARY: &str = @@ -125,7 +127,7 @@ enum Mode { fn list_to_ranges(list: &str, complement: bool) -> Result, String> { if complement { - Range::from_list(list).map(|r| ranges::complement(&r)) + Range::from_list(list).map(|r| uucore::ranges::complement(&r)) } else { Range::from_list(list) } @@ -399,8 +401,13 @@ fn cut_files(mut filenames: Vec, mode: Mode) -> i32 { } else { let path = Path::new(&filename[..]); - if !path.exists() { - show_error!("{}", msg_args_nonexistent_file!(filename)); + if path.is_dir() { + show_error!("{}: Is a directory", filename); + continue; + } + + if !path.metadata().is_ok() { + show_error!("{}: No such file or directory", filename); continue; } @@ -423,34 +430,123 @@ fn cut_files(mut filenames: Vec, mode: Mode) -> i32 { exit_code } +mod options { + pub const BYTES: &str = "bytes"; + pub const CHARACTERS: &str = "characters"; + pub const DELIMITER: &str = "delimiter"; + pub const FIELDS: &str = "fields"; + pub const ZERO_TERMINATED: &str = "zero-terminated"; + pub const ONLY_DELIMITED: &str = "only-delimited"; + pub const OUTPUT_DELIMITER: &str = "output-delimiter"; + pub const COMPLEMENT: &str = "complement"; + pub const FILE: &str = "file"; +} + pub fn uumain(args: impl uucore::Args) -> i32 { let args = args.collect_str(); - let matches = app!(SYNTAX, SUMMARY, LONG_HELP) - .optopt("b", "bytes", "filter byte columns from the input source", "sequence") - .optopt("c", "characters", "alias for character mode", "sequence") - .optopt("d", "delimiter", "specify the delimiter character that separates fields in the input source. Defaults to Tab.", "delimiter") - .optopt("f", "fields", "filter field columns from the input source", "sequence") - .optflag("n", "", "legacy option - has no effect.") - .optflag("", "complement", "invert the filter - instead of displaying only the filtered columns, display all but those columns") - .optflag("s", "only-delimited", "in field mode, only print lines which contain the delimiter") - .optflag("z", "zero-terminated", "instead of filtering columns based on line, filter columns based on \\0 (NULL character)") - .optopt("", "output-delimiter", "in field mode, replace the delimiter in output lines with this option's argument", "new delimiter") - .parse(args); - let complement = matches.opt_present("complement"); + let matches = App::new(executable!()) + .name(NAME) + .version(VERSION) + .usage(SYNTAX) + .about(SUMMARY) + .after_help(LONG_HELP) + .arg( + Arg::with_name(options::BYTES) + .short("b") + .long(options::BYTES) + .takes_value(true) + .help("filter byte columns from the input source") + .allow_hyphen_values(true) + .value_name("LIST") + .display_order(1), + ) + .arg( + Arg::with_name(options::CHARACTERS) + .short("c") + .long(options::CHARACTERS) + .help("alias for character mode") + .takes_value(true) + .allow_hyphen_values(true) + .value_name("LIST") + .display_order(2), + ) + .arg( + Arg::with_name(options::DELIMITER) + .short("d") + .long(options::DELIMITER) + .help("specify the delimiter character that separates fields in the input source. Defaults to Tab.") + .takes_value(true) + .value_name("DELIM") + .display_order(3), + ) + .arg( + Arg::with_name(options::FIELDS) + .short("f") + .long(options::FIELDS) + .help("filter field columns from the input source") + .takes_value(true) + .allow_hyphen_values(true) + .value_name("LIST") + .display_order(4), + ) + .arg( + Arg::with_name(options::COMPLEMENT) + .long(options::COMPLEMENT) + .help("invert the filter - instead of displaying only the filtered columns, display all but those columns") + .takes_value(false) + .display_order(5), + ) + .arg( + Arg::with_name(options::ONLY_DELIMITED) + .short("s") + .long(options::ONLY_DELIMITED) + .help("in field mode, only print lines which contain the delimiter") + .takes_value(false) + .display_order(6), + ) + .arg( + Arg::with_name(options::ZERO_TERMINATED) + .short("z") + .long(options::ZERO_TERMINATED) + .help("instead of filtering columns based on line, filter columns based on \\0 (NULL character)") + .takes_value(false) + .display_order(8), + ) + .arg( + Arg::with_name(options::OUTPUT_DELIMITER) + .long(options::OUTPUT_DELIMITER) + .help("in field mode, replace the delimiter in output lines with this option's argument") + .takes_value(true) + .value_name("NEW_DELIM") + .display_order(7), + ) + .arg( + Arg::with_name(options::FILE) + .hidden(true) + .multiple(true) + ) + .get_matches_from(args); + + let complement = matches.is_present(options::COMPLEMENT); let mode_parse = match ( - matches.opt_str("bytes"), - matches.opt_str("characters"), - matches.opt_str("fields"), + matches.value_of(options::BYTES), + matches.value_of(options::CHARACTERS), + matches.value_of(options::FIELDS), ) { (Some(byte_ranges), None, None) => { list_to_ranges(&byte_ranges[..], complement).map(|ranges| { Mode::Bytes( ranges, Options { - out_delim: matches.opt_str("output-delimiter"), - zero_terminated: matches.opt_present("zero-terminated"), + out_delim: Some( + matches + .value_of(options::OUTPUT_DELIMITER) + .unwrap_or_default() + .to_owned(), + ), + zero_terminated: matches.is_present(options::ZERO_TERMINATED), }, ) }) @@ -460,29 +556,34 @@ pub fn uumain(args: impl uucore::Args) -> i32 { Mode::Characters( ranges, Options { - out_delim: matches.opt_str("output-delimiter"), - zero_terminated: matches.opt_present("zero-terminated"), + out_delim: Some( + matches + .value_of(options::OUTPUT_DELIMITER) + .unwrap_or_default() + .to_owned(), + ), + zero_terminated: matches.is_present(options::ZERO_TERMINATED), }, ) }) } (None, None, Some(field_ranges)) => { list_to_ranges(&field_ranges[..], complement).and_then(|ranges| { - let out_delim = match matches.opt_str("output-delimiter") { + let out_delim = match matches.value_of(options::OUTPUT_DELIMITER) { Some(s) => { if s.is_empty() { Some("\0".to_owned()) } else { - Some(s) + Some(s.to_owned()) } } None => None, }; - let only_delimited = matches.opt_present("only-delimited"); - let zero_terminated = matches.opt_present("zero-terminated"); + let only_delimited = matches.is_present(options::ONLY_DELIMITED); + let zero_terminated = matches.is_present(options::ZERO_TERMINATED); - match matches.opt_str("delimiter") { + match matches.value_of(options::DELIMITER) { Some(delim) => { if delim.chars().count() > 1 { Err(msg_opt_invalid_should_be!( @@ -495,7 +596,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let delim = if delim.is_empty() { "\0".to_owned() } else { - delim + delim.to_owned() }; Ok(Mode::Fields( @@ -534,10 +635,18 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let mode_parse = match mode_parse { Err(_) => mode_parse, Ok(mode) => match mode { - Mode::Bytes(_, _) | Mode::Characters(_, _) if matches.opt_present("delimiter") => Err( - msg_opt_only_usable_if!("printing a sequence of fields", "--delimiter", "-d"), - ), - Mode::Bytes(_, _) | Mode::Characters(_, _) if matches.opt_present("only-delimited") => { + Mode::Bytes(_, _) | Mode::Characters(_, _) + if matches.is_present(options::DELIMITER) => + { + Err(msg_opt_only_usable_if!( + "printing a sequence of fields", + "--delimiter", + "-d" + )) + } + Mode::Bytes(_, _) | Mode::Characters(_, _) + if matches.is_present(options::ONLY_DELIMITED) => + { Err(msg_opt_only_usable_if!( "printing a sequence of fields", "--only-delimited", @@ -548,8 +657,14 @@ pub fn uumain(args: impl uucore::Args) -> i32 { }, }; + let files: Vec = matches + .values_of(options::FILE) + .unwrap_or_default() + .map(str::to_owned) + .collect(); + match mode_parse { - Ok(mode) => cut_files(matches.free, mode), + Ok(mode) => cut_files(files, mode), Err(err_msg) => { show_error!("{}", err_msg); 1 diff --git a/src/uu/date/Cargo.toml b/src/uu/date/Cargo.toml index 8c462fb26..db6c077bd 100644 --- a/src/uu/date/Cargo.toml +++ b/src/uu/date/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_date" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "date ~ (uutils) display or set the current time" @@ -16,9 +16,15 @@ path = "src/date.rs" [dependencies] chrono = "0.4.4" -clap = "2.32" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +clap = "2.33" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } + +[target.'cfg(unix)'.dependencies] +libc = "0.2" + +[target.'cfg(windows)'.dependencies] +winapi = { version = "0.3", features = ["minwinbase", "sysinfoapi", "minwindef"] } [[bin]] name = "date" diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index 895c68b8c..43573437d 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -9,19 +9,23 @@ // spell-checker:ignore (format) MMDDhhmm // spell-checker:ignore (ToDO) DATEFILE -extern crate chrono; - -extern crate clap; #[macro_use] extern crate uucore; +use chrono::{DateTime, FixedOffset, Local, Offset, Utc}; +#[cfg(windows)] +use chrono::{Datelike, Timelike}; use clap::{App, Arg}; - -use chrono::offset::Utc; -use chrono::{DateTime, FixedOffset, Local, Offset}; +#[cfg(all(unix, not(target_os = "macos")))] +use libc::{clock_settime, timespec, CLOCK_REALTIME}; use std::fs::File; use std::io::{BufRead, BufReader}; use std::path::PathBuf; +#[cfg(windows)] +use winapi::{ + shared::minwindef::WORD, + um::{minwinbase::SYSTEMTIME, sysinfoapi::SetSystemTime}, +}; // Options const DATE: &str = "date"; @@ -65,6 +69,11 @@ static RFC_3339_HELP_STRING: &str = "output date/time in RFC 3339 format. for date and time to the indicated precision. Example: 2006-08-14 02:34:56-06:00"; +#[cfg(not(target_os = "macos"))] +static OPT_SET_HELP_STRING: &str = "set time described by STRING"; +#[cfg(target_os = "macos")] +static OPT_SET_HELP_STRING: &str = "set time described by STRING (not available on mac yet)"; + /// Settings for this program, parsed from the command line struct Settings { utc: bool, @@ -189,7 +198,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .short("s") .long(OPT_SET) .takes_value(true) - .help("set time described by STRING"), + .help(OPT_SET_HELP_STRING), ) .arg( Arg::with_name(OPT_UNIVERSAL) @@ -225,18 +234,31 @@ pub fn uumain(args: impl uucore::Args) -> i32 { DateSource::Now }; + let set_to = match matches.value_of(OPT_SET).map(parse_date) { + None => None, + Some(Err((input, _err))) => { + eprintln!("date: invalid date '{}'", input); + return 1; + } + Some(Ok(date)) => Some(date), + }; + let settings = Settings { utc: matches.is_present(OPT_UNIVERSAL), format, date_source, - // TODO: Handle this option: - set_to: None, + set_to, }; - if let Some(_time) = settings.set_to { - unimplemented!(); - // Probably need to use this syscall: - // https://doc.rust-lang.org/libc/i686-unknown-linux-gnu/libc/fn.clock_settime.html + if let Some(date) = settings.set_to { + // All set time functions expect UTC datetimes. + let date: DateTime = if settings.utc { + date.with_timezone(&Utc) + } else { + date.into() + }; + + return set_system_datetime(date); } else { // Declare a file here because it needs to outlive the `dates` iterator. let file: File; @@ -250,15 +272,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 { now.with_timezone(now.offset()) }; - /// Parse a `String` into a `DateTime`. - /// If it fails, return a tuple of the `String` along with its `ParseError`. - fn parse_date( - s: String, - ) -> Result, (String, chrono::format::ParseError)> { - // TODO: The GNU date command can parse a wide variety of inputs. - s.parse().map_err(|e| (s, e)) - } - // Iterate over all dates - whether it's a single date or a file. let dates: Box> = match settings.date_source { DateSource::Custom(ref input) => { @@ -317,3 +330,76 @@ fn make_format_string(settings: &Settings) -> &str { Format::Default => "%c", } } + +/// Parse a `String` into a `DateTime`. +/// If it fails, return a tuple of the `String` along with its `ParseError`. +fn parse_date + Clone>( + s: S, +) -> Result, (String, chrono::format::ParseError)> { + // TODO: The GNU date command can parse a wide variety of inputs. + s.as_ref().parse().map_err(|e| (s.as_ref().into(), e)) +} + +#[cfg(not(any(unix, windows)))] +fn set_system_datetime(_date: DateTime) -> i32 { + unimplemented!("setting date not implemented (unsupported target)"); +} + +#[cfg(target_os = "macos")] +fn set_system_datetime(_date: DateTime) -> i32 { + eprintln!("date: setting the date is not supported by macOS"); + return 1; +} + +#[cfg(all(unix, not(target_os = "macos")))] +/// System call to set date (unix). +/// See here for more: +/// https://doc.rust-lang.org/libc/i686-unknown-linux-gnu/libc/fn.clock_settime.html +/// https://linux.die.net/man/3/clock_settime +/// https://www.gnu.org/software/libc/manual/html_node/Time-Types.html +fn set_system_datetime(date: DateTime) -> i32 { + let timespec = timespec { + tv_sec: date.timestamp() as _, + tv_nsec: date.timestamp_subsec_nanos() as _, + }; + + let result = unsafe { clock_settime(CLOCK_REALTIME, ×pec) }; + + if result != 0 { + let error = std::io::Error::last_os_error(); + eprintln!("date: cannot set date: {}", error); + error.raw_os_error().unwrap() + } else { + 0 + } +} + +#[cfg(windows)] +/// System call to set date (Windows). +/// See here for more: +/// https://docs.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-setsystemtime +/// https://docs.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-systemtime +fn set_system_datetime(date: DateTime) -> i32 { + let system_time = SYSTEMTIME { + wYear: date.year() as WORD, + wMonth: date.month() as WORD, + // Ignored + wDayOfWeek: 0, + wDay: date.day() as WORD, + wHour: date.hour() as WORD, + wMinute: date.minute() as WORD, + wSecond: date.second() as WORD, + // TODO: be careful of leap seconds - valid range is [0, 999] - how to handle? + wMilliseconds: ((date.nanosecond() / 1_000_000) % 1000) as WORD, + }; + + let result = unsafe { SetSystemTime(&system_time) }; + + if result == 0 { + let error = std::io::Error::last_os_error(); + eprintln!("date: cannot set date: {}", error); + error.raw_os_error().unwrap() + } else { + 0 + } +} diff --git a/src/uu/df/Cargo.toml b/src/uu/df/Cargo.toml index 4144adb41..4770cb557 100644 --- a/src/uu/df/Cargo.toml +++ b/src/uu/df/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_df" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "df ~ (uutils) display file system information" @@ -15,11 +15,11 @@ edition = "2018" path = "src/df.rs" [dependencies] -clap = "2.32" +clap = "2.33" libc = "0.2" -number_prefix = "0.2" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +number_prefix = "0.4" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [target.'cfg(target_os = "windows")'.dependencies] winapi = { version = "0.3", features = ["errhandlingapi", "fileapi", "handleapi", "winerror"] } diff --git a/src/uu/df/src/df.rs b/src/uu/df/src/df.rs index 0f9f440e2..57caf7970 100644 --- a/src/uu/df/src/df.rs +++ b/src/uu/df/src/df.rs @@ -9,17 +9,11 @@ // spell-checker:ignore (ToDO) mountinfo mtab BLOCKSIZE getmntinfo fobj mptr noatime Iused overmounted // spell-checker:ignore (libc/fs) asyncreads asyncwrites autofs bavail bfree bsize charspare cifs debugfs devfs devpts ffree frsize fsid fstypename fusectl inode inodes iosize kernfs mntbufp mntfromname mntonname mqueue namemax pipefs smbfs statfs statvfs subfs syncreads syncwrites sysfs wcslen -extern crate clap; -extern crate libc; -extern crate number_prefix; - #[macro_use] extern crate uucore; use clap::{App, Arg}; -#[cfg(windows)] -extern crate winapi; #[cfg(windows)] use winapi::um::errhandlingapi::GetLastError; #[cfg(windows)] @@ -28,7 +22,7 @@ use winapi::um::fileapi::{ GetVolumePathNamesForVolumeNameW, QueryDosDeviceW, }; -use number_prefix::{binary_prefix, decimal_prefix, PrefixNames, Prefixed, Standalone}; +use number_prefix::NumberPrefix; use std::cell::Cell; use std::collections::HashMap; use std::collections::HashSet; @@ -38,15 +32,15 @@ use std::ffi::CString; #[cfg(unix)] use std::mem; -#[cfg(any(target_os = "macos", target_os = "freebsd"))] +#[cfg(any(target_vendor = "apple", target_os = "freebsd"))] use libc::c_int; -#[cfg(target_os = "macos")] +#[cfg(target_vendor = "apple")] use libc::statfs; -#[cfg(any(target_os = "macos", target_os = "freebsd"))] +#[cfg(any(target_vendor = "apple", target_os = "freebsd"))] use std::ffi::CStr; -#[cfg(any(target_os = "macos", target_os = "freebsd", target_os = "windows"))] +#[cfg(any(target_vendor = "apple", target_os = "freebsd", target_os = "windows"))] use std::ptr; -#[cfg(any(target_os = "macos", target_os = "freebsd"))] +#[cfg(any(target_vendor = "apple", target_os = "freebsd"))] use std::slice; #[cfg(target_os = "freebsd")] @@ -106,7 +100,6 @@ static OPT_SYNC: &str = "sync"; static OPT_TYPE: &str = "type"; static OPT_PRINT_TYPE: &str = "print-type"; static OPT_EXCLUDE_TYPE: &str = "exclude-type"; -static OPT_VERSION: &str = "version"; static MOUNT_OPT_BIND: &str = "bind"; @@ -144,7 +137,7 @@ struct MountInfo { #[cfg(all( target_os = "freebsd", - not(all(target_os = "macos", target_arch = "x86_64")) + not(all(target_vendor = "apple", target_arch = "x86_64")) ))] #[repr(C)] #[derive(Copy, Clone)] @@ -216,20 +209,20 @@ fn get_usage() -> String { format!("{0} [OPTION]... [FILE]...", executable!()) } -#[cfg(any(target_os = "freebsd", target_os = "macos"))] +#[cfg(any(target_os = "freebsd", target_vendor = "apple"))] extern "C" { - #[cfg(all(target_os = "macos", target_arch = "x86_64"))] + #[cfg(all(target_vendor = "apple", target_arch = "x86_64"))] #[link_name = "getmntinfo$INODE64"] fn getmntinfo(mntbufp: *mut *mut statfs, flags: c_int) -> c_int; - #[cfg(all( - target_os = "freebsd", - not(all(target_os = "macos", target_arch = "x86_64")) + #[cfg(any( + all(target_os = "freebsd"), + all(target_vendor = "apple", target_arch = "aarch64") ))] fn getmntinfo(mntbufp: *mut *mut statfs, flags: c_int) -> c_int; } -#[cfg(any(target_os = "freebsd", target_os = "macos"))] +#[cfg(any(target_os = "freebsd", target_vendor = "apple"))] impl From for MountInfo { fn from(statfs: statfs) -> Self { let mut info = MountInfo { @@ -592,7 +585,7 @@ fn read_fs_list() -> Vec { }) .collect::>() } - #[cfg(any(target_os = "freebsd", target_os = "macos"))] + #[cfg(any(target_os = "freebsd", target_vendor = "apple"))] { let mut mptr: *mut statfs = ptr::null_mut(); let len = unsafe { getmntinfo(&mut mptr, 1 as c_int) }; @@ -676,7 +669,7 @@ fn filter_mount_list(vmi: Vec, paths: &[String], opt: &Options) -> Ve #[allow(clippy::map_entry)] { if acc.contains_key(&id) { - let seen = acc.get(&id).unwrap().replace(mi.clone()); + let seen = acc[&id].replace(mi.clone()); let target_nearer_root = seen.mount_dir.len() > mi.mount_dir.len(); // With bind mounts, prefer items nearer the root of the source let source_below_root = !seen.mount_root.is_empty() @@ -694,7 +687,7 @@ fn filter_mount_list(vmi: Vec, paths: &[String], opt: &Options) -> Ve environments for example. */ || seen.mount_dir != mi.mount_dir) { - acc.get(&id).unwrap().replace(seen); + acc[&id].replace(seen); } } else { acc.insert(id, Cell::new(mi)); @@ -717,14 +710,14 @@ fn human_readable(value: u64, base: i64) -> String { // ref: [Binary prefix](https://en.wikipedia.org/wiki/Binary_prefix) @@ // ref: [SI/metric prefix](https://en.wikipedia.org/wiki/Metric_prefix) @@ - 1000 => match decimal_prefix(value as f64) { - Standalone(bytes) => bytes.to_string(), - Prefixed(prefix, bytes) => format!("{:.1}{}", bytes, prefix.symbol()), + 1000 => match NumberPrefix::decimal(value as f64) { + NumberPrefix::Standalone(bytes) => bytes.to_string(), + NumberPrefix::Prefixed(prefix, bytes) => format!("{:.1}{}", bytes, prefix.symbol()), }, - 1024 => match binary_prefix(value as f64) { - Standalone(bytes) => bytes.to_string(), - Prefixed(prefix, bytes) => format!("{:.1}{}", bytes, prefix.symbol()), + 1024 => match NumberPrefix::binary(value as f64) { + NumberPrefix::Standalone(bytes) => bytes.to_string(), + NumberPrefix::Prefixed(prefix, bytes) => format!("{:.1}{}", bytes, prefix.symbol()), }, _ => crash!(EXIT_ERR, "Internal error: Unknown base value {}", base), @@ -760,7 +753,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .takes_value(true) .help( "scale sizes by SIZE before printing them; e.g.\ - '-BM' prints sizes in units of 1,048,576 bytes", + '-BM' prints sizes in units of 1,048,576 bytes", ), ) .arg( @@ -817,7 +810,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .use_delimiter(true) .help( "use the output format defined by FIELD_LIST,\ - or print all fields if FIELD_LIST is omitted.", + or print all fields if FIELD_LIST is omitted.", ), ) .arg( @@ -854,21 +847,10 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .use_delimiter(true) .help("limit listing to file systems not of type TYPE"), ) - .arg( - Arg::with_name(OPT_VERSION) - .short("v") - .long("version") - .help("output version information and exit"), - ) .arg(Arg::with_name(OPT_PATHS).multiple(true)) .help("Filesystem(s) to list") .get_matches_from(args); - if matches.is_present(OPT_VERSION) { - println!("{} {}", executable!(), VERSION); - return EXIT_OK; - } - let paths: Vec = matches .values_of(OPT_PATHS) .map(|v| v.map(ToString::to_string).collect()) diff --git a/src/uu/dircolors/Cargo.toml b/src/uu/dircolors/Cargo.toml index a1bb6010e..5e822820e 100644 --- a/src/uu/dircolors/Cargo.toml +++ b/src/uu/dircolors/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_dircolors" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "dircolors ~ (uutils) display commands to set LS_COLORS" @@ -16,8 +16,8 @@ path = "src/dircolors.rs" [dependencies] glob = "0.3.0" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "dircolors" diff --git a/src/uu/dircolors/src/dircolors.rs b/src/uu/dircolors/src/dircolors.rs index 0b6072999..6cb5f9e1b 100644 --- a/src/uu/dircolors/src/dircolors.rs +++ b/src/uu/dircolors/src/dircolors.rs @@ -7,8 +7,6 @@ // spell-checker:ignore (ToDO) clrtoeol dircolors eightbit endcode fnmatch leftcode multihardlink rightcode setenv sgid suid -extern crate glob; - #[macro_use] extern crate uucore; diff --git a/src/uu/dirname/Cargo.toml b/src/uu/dirname/Cargo.toml index 847666854..0975f33bb 100644 --- a/src/uu/dirname/Cargo.toml +++ b/src/uu/dirname/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_dirname" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "dirname ~ (uutils) display parent directory of PATHNAME" @@ -15,9 +15,10 @@ edition = "2018" path = "src/dirname.rs" [dependencies] +clap = "2.33" libc = "0.2.42" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "dirname" diff --git a/src/uu/dirname/src/dirname.rs b/src/uu/dirname/src/dirname.rs index 59f47ff01..1cf35d0c4 100644 --- a/src/uu/dirname/src/dirname.rs +++ b/src/uu/dirname/src/dirname.rs @@ -8,32 +8,57 @@ #[macro_use] extern crate uucore; +use clap::{App, Arg}; use std::path::Path; static NAME: &str = "dirname"; static SYNTAX: &str = "[OPTION] NAME..."; static SUMMARY: &str = "strip last component from file name"; +static VERSION: &str = env!("CARGO_PKG_VERSION"); static LONG_HELP: &str = " Output each NAME with its last non-slash component and trailing slashes removed; if NAME contains no /'s, output '.' (meaning the current directory). "; +mod options { + pub const ZERO: &str = "zero"; + pub const DIR: &str = "dir"; +} + pub fn uumain(args: impl uucore::Args) -> i32 { let args = args.collect_str(); - let matches = app!(SYNTAX, SUMMARY, LONG_HELP) - .optflag("z", "zero", "separate output with NUL rather than newline") - .parse(args); + let matches = App::new(executable!()) + .name(NAME) + .usage(SYNTAX) + .about(SUMMARY) + .after_help(LONG_HELP) + .version(VERSION) + .arg( + Arg::with_name(options::ZERO) + .short(options::ZERO) + .short("z") + .takes_value(false) + .help("separate output with NUL rather than newline"), + ) + .arg(Arg::with_name(options::DIR).hidden(true).multiple(true)) + .get_matches_from(args); - let separator = if matches.opt_present("zero") { + let separator = if matches.is_present(options::ZERO) { "\0" } else { "\n" }; - if !matches.free.is_empty() { - for path in &matches.free { + let dirnames: Vec = matches + .values_of(options::DIR) + .unwrap_or_default() + .map(str::to_owned) + .collect(); + + if !dirnames.is_empty() { + for path in dirnames.iter() { let p = Path::new(path); match p.parent() { Some(d) => { @@ -54,8 +79,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { print!("{}", separator); } } else { - println!("{0}: missing operand", NAME); - println!("Try '{0} --help' for more information.", NAME); + show_usage_error!("missing operand"); return 1; } diff --git a/src/uu/du/Cargo.toml b/src/uu/du/Cargo.toml index 6f14a6a32..eb7b23f8b 100644 --- a/src/uu/du/Cargo.toml +++ b/src/uu/du/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_du" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "du ~ (uutils) display disk usage" @@ -16,8 +16,9 @@ path = "src/du.rs" [dependencies] time = "0.1.40" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } +winapi = { version="0.3", features=[] } [[bin]] name = "du" diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index f5071e749..9c8bb9794 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -7,8 +7,6 @@ // spell-checker:ignore (ToDO) BLOCKSIZE inode inodes ment strs -extern crate time; - #[macro_use] extern crate uucore; @@ -17,9 +15,24 @@ use std::env; use std::fs; use std::io::{stderr, Result, Write}; use std::iter; +#[cfg(not(windows))] use std::os::unix::fs::MetadataExt; +#[cfg(windows)] +use std::os::windows::fs::MetadataExt; +#[cfg(windows)] +use std::os::windows::io::AsRawHandle; use std::path::PathBuf; use time::Timespec; +#[cfg(windows)] +use winapi::shared::minwindef::{DWORD, LPVOID}; +#[cfg(windows)] +use winapi::um::fileapi::{FILE_ID_INFO, FILE_STANDARD_INFO}; +#[cfg(windows)] +use winapi::um::minwinbase::{FileIdInfo, FileStandardInfo}; +#[cfg(windows)] +use winapi::um::winbase::GetFileInformationByHandleEx; +#[cfg(windows)] +use winapi::um::winnt::{FILE_ID_128, ULONGLONG}; const NAME: &str = "du"; const SUMMARY: &str = "estimate file space usage"; @@ -45,12 +58,18 @@ struct Options { separate_dirs: bool, } +#[derive(PartialEq, Eq, Hash, Clone, Copy)] +struct FileInfo { + file_id: u128, + dev_id: u64, +} + struct Stat { path: PathBuf, is_dir: bool, size: u64, blocks: u64, - inode: u64, + inode: Option, created: u64, accessed: u64, modified: u64, @@ -59,19 +78,114 @@ struct Stat { impl Stat { fn new(path: PathBuf) -> Result { let metadata = fs::symlink_metadata(&path)?; - Ok(Stat { + + #[cfg(not(windows))] + let file_info = FileInfo { + file_id: metadata.ino() as u128, + dev_id: metadata.dev(), + }; + #[cfg(not(windows))] + return Ok(Stat { path, is_dir: metadata.is_dir(), size: metadata.len(), blocks: metadata.blocks() as u64, - inode: metadata.ino() as u64, + inode: Some(file_info), created: metadata.mtime() as u64, accessed: metadata.atime() as u64, modified: metadata.mtime() as u64, + }); + + #[cfg(windows)] + let size_on_disk = get_size_on_disk(&path); + #[cfg(windows)] + let file_info = get_file_info(&path); + #[cfg(windows)] + Ok(Stat { + path, + is_dir: metadata.is_dir(), + size: metadata.len(), + blocks: size_on_disk / 1024 * 2, + inode: file_info, + created: windows_time_to_unix_time(metadata.creation_time()), + accessed: windows_time_to_unix_time(metadata.last_access_time()), + modified: windows_time_to_unix_time(metadata.last_write_time()), }) } } +#[cfg(windows)] +// https://doc.rust-lang.org/std/os/windows/fs/trait.MetadataExt.html#tymethod.creation_time +// "The returned 64-bit value [...] which represents the number of 100-nanosecond intervals since January 1, 1601 (UTC)." +fn windows_time_to_unix_time(win_time: u64) -> u64 { + win_time / 10_000 - 11_644_473_600_000 +} + +#[cfg(windows)] +fn get_size_on_disk(path: &PathBuf) -> u64 { + let mut size_on_disk = 0; + + // bind file so it stays in scope until end of function + // if it goes out of scope the handle below becomes invalid + let file = match fs::File::open(path) { + Ok(file) => file, + Err(_) => return size_on_disk, // opening directories will fail + }; + + let handle = file.as_raw_handle(); + + unsafe { + let mut file_info: FILE_STANDARD_INFO = core::mem::zeroed(); + let file_info_ptr: *mut FILE_STANDARD_INFO = &mut file_info; + + let success = GetFileInformationByHandleEx( + handle, + FileStandardInfo, + file_info_ptr as LPVOID, + std::mem::size_of::() as DWORD, + ); + + if success != 0 { + size_on_disk = *file_info.AllocationSize.QuadPart() as u64; + } + } + + size_on_disk +} + +#[cfg(windows)] +fn get_file_info(path: &PathBuf) -> Option { + let mut result = None; + + let file = match fs::File::open(path) { + Ok(file) => file, + Err(_) => return result, + }; + + let handle = file.as_raw_handle(); + + unsafe { + let mut file_info: FILE_ID_INFO = core::mem::zeroed(); + let file_info_ptr: *mut FILE_ID_INFO = &mut file_info; + + let success = GetFileInformationByHandleEx( + handle, + FileIdInfo, + file_info_ptr as LPVOID, + std::mem::size_of::() as DWORD, + ); + + if success != 0 { + result = Some(FileInfo { + file_id: std::mem::transmute::(file_info.FileId), + dev_id: std::mem::transmute::(file_info.VolumeSerialNumber), + }); + } + } + + result +} + fn unit_string_to_number(s: &str) -> Option { let mut offset = 0; let mut s_chars = s.chars().rev(); @@ -139,7 +253,7 @@ fn du( mut my_stat: Stat, options: &Options, depth: usize, - inodes: &mut HashSet, + inodes: &mut HashSet, ) -> Box> { let mut stats = vec![]; let mut futures = vec![]; @@ -166,10 +280,13 @@ fn du( if this_stat.is_dir { futures.push(du(this_stat, options, depth + 1, inodes)); } else { - if inodes.contains(&this_stat.inode) { - continue; + if this_stat.inode.is_some() { + let inode = this_stat.inode.unwrap(); + if inodes.contains(&inode) { + continue; + } + inodes.insert(inode); } - inodes.insert(this_stat.inode); my_stat.size += this_stat.size; my_stat.blocks += this_stat.blocks; if options.all { @@ -202,6 +319,9 @@ fn convert_size_human(size: u64, multiplier: u64, _block_size: u64) -> String { return format!("{:.1}{}", (size as f64) / (limit as f64), unit); } } + if size == 0 { + return format!("0"); + } format!("{}B", size) } @@ -420,7 +540,7 @@ Try '{} --help' for more information.", let path = PathBuf::from(&path_str); match Stat::new(path) { Ok(stat) => { - let mut inodes: HashSet = HashSet::new(); + let mut inodes: HashSet = HashSet::new(); let iter = du(stat, &options, 0, &mut inodes); let (_, len) = iter.size_hint(); diff --git a/src/uu/echo/Cargo.toml b/src/uu/echo/Cargo.toml index 984f872e8..15f189030 100644 --- a/src/uu/echo/Cargo.toml +++ b/src/uu/echo/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_echo" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "echo ~ (uutils) display TEXT" @@ -15,8 +15,9 @@ edition = "2018" path = "src/echo.rs" [dependencies] -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +clap = "2.33" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "echo" diff --git a/src/uu/echo/src/echo.rs b/src/uu/echo/src/echo.rs index 93c395391..4d38d7748 100644 --- a/src/uu/echo/src/echo.rs +++ b/src/uu/echo/src/echo.rs @@ -9,14 +9,17 @@ #[macro_use] extern crate uucore; +use clap::{crate_version, App, Arg}; use std::io::{self, Write}; use std::iter::Peekable; use std::str::Chars; -const SYNTAX: &str = "[OPTIONS]... [STRING]..."; +const NAME: &str = "echo"; const SUMMARY: &str = "display a line of text"; -const HELP: &str = r#" +const USAGE: &str = "[OPTIONS]... [STRING]..."; +const AFTER_HELP: &str = r#" Echo the STRING(s) to standard output. + If -e is in effect, the following sequences are recognized: \\\\ backslash @@ -33,6 +36,13 @@ const HELP: &str = r#" \\xHH byte with hexadecimal value HH (1 to 2 digits) "#; +mod options { + pub const STRING: &str = "STRING"; + pub const NO_NEWLINE: &str = "no_newline"; + pub const ENABLE_BACKSLASH_ESCAPE: &str = "enable_backslash_escape"; + pub const DISABLE_BACKSLASH_ESCAPE: &str = "disable_backslash_escape"; +} + fn parse_code( input: &mut Peekable, base: u32, @@ -103,22 +113,53 @@ fn print_escaped(input: &str, mut output: impl Write) -> io::Result { } pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); - - let matches = app!(SYNTAX, SUMMARY, HELP) - .optflag("n", "", "do not output the trailing newline") - .optflag("e", "", "enable interpretation of backslash escapes") - .optflag( - "E", - "", - "disable interpretation of backslash escapes (default)", + let matches = App::new(executable!()) + .name(NAME) + // TrailingVarArg specifies the final positional argument is a VarArg + // and it doesn't attempts the parse any further args. + // Final argument must have multiple(true) or the usage string equivalent. + .setting(clap::AppSettings::TrailingVarArg) + .setting(clap::AppSettings::AllowLeadingHyphen) + .version(crate_version!()) + .about(SUMMARY) + .after_help(AFTER_HELP) + .usage(USAGE) + .arg( + Arg::with_name(options::NO_NEWLINE) + .short("n") + .help("do not output the trailing newline") + .takes_value(false) + .display_order(1), ) - .parse(args); + .arg( + Arg::with_name(options::ENABLE_BACKSLASH_ESCAPE) + .short("e") + .help("enable interpretation of backslash escapes") + .takes_value(false) + .display_order(2), + ) + .arg( + Arg::with_name(options::DISABLE_BACKSLASH_ESCAPE) + .short("E") + .help("disable interpretation of backslash escapes (default)") + .takes_value(false) + .display_order(3), + ) + .arg( + Arg::with_name(options::STRING) + .multiple(true) + .allow_hyphen_values(true), + ) + .get_matches_from(args); - let no_newline = matches.opt_present("n"); - let escaped = matches.opt_present("e"); + let no_newline = matches.is_present(options::NO_NEWLINE); + let escaped = matches.is_present(options::ENABLE_BACKSLASH_ESCAPE); + let values: Vec = match matches.values_of(options::STRING) { + Some(s) => s.map(|s| s.to_string()).collect(), + None => vec!["".to_string()], + }; - match execute(no_newline, escaped, matches.free) { + match execute(no_newline, escaped, values) { Ok(_) => 0, Err(f) => { show_error!("{}", f); diff --git a/src/uu/env/Cargo.toml b/src/uu/env/Cargo.toml index de54617f4..ef0017e02 100644 --- a/src/uu/env/Cargo.toml +++ b/src/uu/env/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_env" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "env ~ (uutils) set each NAME to VALUE in the environment and run COMMAND" @@ -18,8 +18,8 @@ path = "src/env.rs" clap = "2.33" libc = "0.2.42" rust-ini = "0.13.0" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "env" diff --git a/src/uu/expand/Cargo.toml b/src/uu/expand/Cargo.toml index 4be65e5f4..4931cf53c 100644 --- a/src/uu/expand/Cargo.toml +++ b/src/uu/expand/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_expand" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "expand ~ (uutils) convert input tabs to spaces" @@ -15,10 +15,10 @@ edition = "2018" path = "src/expand.rs" [dependencies] -getopts = "0.2.18" +clap = "2.33" unicode-width = "0.1.5" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "expand" diff --git a/src/uu/expand/src/expand.rs b/src/uu/expand/src/expand.rs index 560ffc840..67d24086c 100644 --- a/src/uu/expand/src/expand.rs +++ b/src/uu/expand/src/expand.rs @@ -9,30 +9,39 @@ // spell-checker:ignore (ToDO) ctype cwidth iflag nbytes nspaces nums tspaces uflag -extern crate getopts; -extern crate unicode_width; - #[macro_use] extern crate uucore; +use clap::{App, Arg, ArgMatches}; use std::fs::File; use std::io::{stdin, stdout, BufRead, BufReader, BufWriter, Read, Write}; use std::iter::repeat; use std::str::from_utf8; use unicode_width::UnicodeWidthChar; -static SYNTAX: &str = "[OPTION]... [FILE]..."; -static SUMMARY: &str = "Convert tabs in each FILE to spaces, writing to standard output. +static VERSION: &str = env!("CARGO_PKG_VERSION"); +static ABOUT: &str = "Convert tabs in each FILE to spaces, writing to standard output. With no FILE, or when FILE is -, read standard input."; + +pub mod options { + pub static TABS: &str = "tabs"; + pub static INITIAL: &str = "initial"; + pub static NO_UTF8: &str = "no-utf8"; + pub static FILES: &str = "FILES"; +} + static LONG_HELP: &str = ""; static DEFAULT_TABSTOP: usize = 8; +fn get_usage() -> String { + format!("{0} [OPTION]... [FILE]...", executable!()) +} + fn tabstops_parse(s: String) -> Vec { - let words = s.split(',').collect::>(); + let words = s.split(','); let nums = words - .into_iter() .map(|sn| { sn.parse::() .unwrap_or_else(|_| crash!(1, "{}\n", "tab size contains invalid character(s)")) @@ -62,14 +71,14 @@ struct Options { } impl Options { - fn new(matches: getopts::Matches) -> Options { - let tabstops = match matches.opt_str("t") { + fn new(matches: &ArgMatches) -> Options { + let tabstops = match matches.value_of(options::TABS) { + Some(s) => tabstops_parse(s.to_string()), None => vec![DEFAULT_TABSTOP], - Some(s) => tabstops_parse(s), }; - let iflag = matches.opt_present("i"); - let uflag = !matches.opt_present("U"); + let iflag = matches.is_present(options::INITIAL); + let uflag = !matches.is_present(options::NO_UTF8); // avoid allocations when dumping out long sequences of spaces // by precomputing the longest string of spaces we will ever need @@ -84,10 +93,9 @@ impl Options { .unwrap(); // length of tabstops is guaranteed >= 1 let tspaces = repeat(' ').take(nspaces).collect(); - let files = if matches.free.is_empty() { - vec!["-".to_owned()] - } else { - matches.free + let files: Vec = match matches.values_of(options::FILES) { + Some(s) => s.map(|v| v.to_string()).collect(), + None => vec!["-".to_owned()], }; Options { @@ -101,31 +109,40 @@ impl Options { } pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); - - let matches = app!(SYNTAX, SUMMARY, LONG_HELP) - .optflag("i", "initial", "do not convert tabs after non blanks") - .optopt( - "t", - "tabs", - "have tabs NUMBER characters apart, not 8", - "NUMBER", + let usage = get_usage(); + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(&usage[..]) + .after_help(LONG_HELP) + .arg( + Arg::with_name(options::INITIAL) + .long(options::INITIAL) + .short("i") + .help("do not convert tabs after non blanks"), ) - .optopt( - "t", - "tabs", - "use comma separated list of explicit tab positions", - "LIST", + .arg( + Arg::with_name(options::TABS) + .long(options::TABS) + .short("t") + .value_name("N, LIST") + .takes_value(true) + .help("have tabs N characters apart, not 8 or use comma separated list of explicit tab positions"), ) - .optflag( - "U", - "no-utf8", - "interpret input file as 8-bit ASCII rather than UTF-8", + .arg( + Arg::with_name(options::NO_UTF8) + .long(options::NO_UTF8) + .short("U") + .help("interpret input file as 8-bit ASCII rather than UTF-8"), + ).arg( + Arg::with_name(options::FILES) + .multiple(true) + .hidden(true) + .takes_value(true) ) - .parse(args); - - expand(Options::new(matches)); + .get_matches_from(args); + expand(Options::new(&matches)); 0 } diff --git a/src/uu/expr/Cargo.toml b/src/uu/expr/Cargo.toml index 69f350cfd..c535df7ce 100644 --- a/src/uu/expr/Cargo.toml +++ b/src/uu/expr/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_expr" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "expr ~ (uutils) display the value of EXPRESSION" @@ -17,8 +17,8 @@ path = "src/expr.rs" [dependencies] libc = "0.2.42" onig = "~4.3.2" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "expr" diff --git a/src/uu/expr/src/expr.rs b/src/uu/expr/src/expr.rs index db970f8a6..fee85dfe1 100644 --- a/src/uu/expr/src/expr.rs +++ b/src/uu/expr/src/expr.rs @@ -5,7 +5,6 @@ //* For the full copyright and license information, please view the LICENSE //* file that was distributed with this source code. -extern crate onig; #[macro_use] extern crate uucore; @@ -41,7 +40,7 @@ fn process_expr(token_strings: &[String]) -> Result { fn print_expr_ok(expr_result: &str) -> i32 { println!("{}", expr_result); - if expr_result == "0" || expr_result == "" { + if expr_result == "0" || expr_result.is_empty() { 1 } else { 0 diff --git a/src/uu/expr/src/syntax_tree.rs b/src/uu/expr/src/syntax_tree.rs index d56bab4fc..3381c29bd 100644 --- a/src/uu/expr/src/syntax_tree.rs +++ b/src/uu/expr/src/syntax_tree.rs @@ -148,11 +148,11 @@ impl ASTNode { |a: &String, b: &String| Ok(bool_as_string(a >= b)), &operand_values, ), - "|" => infix_operator_or(&operand_values), - "&" => infix_operator_and(&operand_values), + "|" => Ok(infix_operator_or(&operand_values)), + "&" => Ok(infix_operator_and(&operand_values)), ":" | "match" => operator_match(&operand_values), - "length" => prefix_operator_length(&operand_values), - "index" => prefix_operator_index(&operand_values), + "length" => Ok(prefix_operator_length(&operand_values)), + "index" => Ok(prefix_operator_index(&operand_values)), "substr" => prefix_operator_substr(&operand_values), _ => Err(format!("operation not implemented: {}", op_type)), @@ -465,20 +465,20 @@ where } } -fn infix_operator_or(values: &[String]) -> Result { +fn infix_operator_or(values: &[String]) -> String { assert!(values.len() == 2); if value_as_bool(&values[0]) { - Ok(values[0].clone()) + values[0].clone() } else { - Ok(values[1].clone()) + values[1].clone() } } -fn infix_operator_and(values: &[String]) -> Result { +fn infix_operator_and(values: &[String]) -> String { if value_as_bool(&values[0]) && value_as_bool(&values[1]) { - Ok(values[0].clone()) + values[0].clone() } else { - Ok(0.to_string()) + 0.to_string() } } @@ -502,12 +502,12 @@ fn operator_match(values: &[String]) -> Result { } } -fn prefix_operator_length(values: &[String]) -> Result { +fn prefix_operator_length(values: &[String]) -> String { assert!(values.len() == 1); - Ok(values[0].len().to_string()) + values[0].len().to_string() } -fn prefix_operator_index(values: &[String]) -> Result { +fn prefix_operator_index(values: &[String]) -> String { assert!(values.len() == 2); let haystack = &values[0]; let needles = &values[1]; @@ -515,11 +515,11 @@ fn prefix_operator_index(values: &[String]) -> Result { for (current_idx, ch_h) in haystack.chars().enumerate() { for ch_n in needles.chars() { if ch_n == ch_h { - return Ok(current_idx.to_string()); + return current_idx.to_string(); } } } - Ok("0".to_string()) + "0".to_string() } fn prefix_operator_substr(values: &[String]) -> Result { diff --git a/src/uu/expr/src/tokens.rs b/src/uu/expr/src/tokens.rs index 558dae090..b65b0d482 100644 --- a/src/uu/expr/src/tokens.rs +++ b/src/uu/expr/src/tokens.rs @@ -63,6 +63,8 @@ impl Token { } } fn is_a_close_paren(&self) -> bool { + #[allow(clippy::match_like_matches_macro)] + // `matches!(...)` macro not stabilized until rust v1.42 match *self { Token::ParClose => true, _ => false, diff --git a/src/uu/factor/Cargo.toml b/src/uu/factor/Cargo.toml index c4dbc96cc..489c713be 100644 --- a/src/uu/factor/Cargo.toml +++ b/src/uu/factor/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_factor" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "factor ~ (uutils) display the prime factors of each NUMBER" @@ -12,14 +12,15 @@ categories = ["command-line-utilities"] edition = "2018" [build-dependencies] -num-traits = "0.2" # used in src/numerics.rs, which is included by build.rs +num-traits = "0.2.13" # used in src/numerics.rs, which is included by build.rs [dependencies] -num-traits = "0.2" +num-traits = "0.2.13" # Needs at least version 0.2.13 for "OverflowingAdd" rand = { version="0.7", features=["small_rng"] } -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +smallvec = { version="0.6.14, < 1.0" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [dev-dependencies] criterion = "0.3" diff --git a/src/uu/factor/build.rs b/src/uu/factor/build.rs index 4c3125472..eff21ae2f 100644 --- a/src/uu/factor/build.rs +++ b/src/uu/factor/build.rs @@ -29,6 +29,7 @@ use miller_rabin::is_prime; #[path = "src/numeric/modular_inverse.rs"] mod modular_inverse; +#[allow(unused_imports)] // imports there are used, but invisible from build.rs #[path = "src/numeric/traits.rs"] mod traits; use modular_inverse::modular_inverse; diff --git a/src/uu/factor/sieve.rs b/src/uu/factor/sieve.rs index c10321a7f..41893699b 100644 --- a/src/uu/factor/sieve.rs +++ b/src/uu/factor/sieve.rs @@ -7,7 +7,7 @@ // spell-checker:ignore (ToDO) filts, minidx, minkey paridx -use std::iter::{Chain, Cycle, Map}; +use std::iter::{Chain, Copied, Cycle}; use std::slice::Iter; /// A lazy Sieve of Eratosthenes. @@ -68,27 +68,17 @@ impl Sieve { #[allow(dead_code)] #[inline] pub fn primes() -> PrimeSieve { - #[allow(clippy::trivially_copy_pass_by_ref)] - fn deref(x: &u64) -> u64 { - *x - } - let deref = deref as fn(&u64) -> u64; - INIT_PRIMES.iter().map(deref).chain(Sieve::new()) + INIT_PRIMES.iter().copied().chain(Sieve::new()) } #[allow(dead_code)] #[inline] pub fn odd_primes() -> PrimeSieve { - #[allow(clippy::trivially_copy_pass_by_ref)] - fn deref(x: &u64) -> u64 { - *x - } - let deref = deref as fn(&u64) -> u64; - (&INIT_PRIMES[1..]).iter().map(deref).chain(Sieve::new()) + (&INIT_PRIMES[1..]).iter().copied().chain(Sieve::new()) } } -pub type PrimeSieve = Chain, fn(&u64) -> u64>, Sieve>; +pub type PrimeSieve = Chain>, Sieve>; /// An iterator that generates an infinite list of numbers that are /// not divisible by any of 2, 3, 5, or 7. diff --git a/src/uu/factor/src/factor.rs b/src/uu/factor/src/factor.rs index 4e5322084..7d2e16a11 100644 --- a/src/uu/factor/src/factor.rs +++ b/src/uu/factor/src/factor.rs @@ -5,8 +5,7 @@ // * For the full copyright and license information, please view the LICENSE file // * that was distributed with this source code. -extern crate rand; - +use smallvec::SmallVec; use std::cell::RefCell; use std::fmt; @@ -16,11 +15,16 @@ use crate::{miller_rabin, rho, table}; type Exponent = u8; #[derive(Clone, Debug)] -struct Decomposition(Vec<(u64, Exponent)>); +struct Decomposition(SmallVec<[(u64, Exponent); NUM_FACTORS_INLINE]>); + +// The number of factors to inline directly into a `Decomposition` object. +// As a consequence of the Erdős–Kac theorem, the average number of prime factors +// of integers < 10²⁵ ≃ 2⁸³ is 4, so we can use a slightly higher value. +const NUM_FACTORS_INLINE: usize = 5; impl Decomposition { fn one() -> Decomposition { - Decomposition(Vec::new()) + Decomposition(SmallVec::new()) } fn add(&mut self, factor: u64, exp: Exponent) { @@ -141,10 +145,10 @@ pub fn factor(mut n: u64) -> Factors { return factors; } - let z = n.trailing_zeros(); - if z > 0 { - factors.add(2, z as Exponent); - n >>= z; + let n_zeros = n.trailing_zeros(); + if n_zeros > 0 { + factors.add(2, n_zeros as Exponent); + n >>= n_zeros; } if n == 1 { @@ -162,8 +166,20 @@ pub fn factor(mut n: u64) -> Factors { #[cfg(test)] mod tests { - use super::{factor, Factors}; + use super::{factor, Decomposition, Exponent, Factors}; use quickcheck::quickcheck; + use smallvec::smallvec; + use std::cell::RefCell; + + #[test] + fn factor_2044854919485649() { + let f = Factors(RefCell::new(Decomposition(smallvec![ + (503, 1), + (2423, 1), + (40961, 2) + ]))); + assert_eq!(factor(f.product()), f); + } #[test] fn factor_recombines_small() { @@ -182,7 +198,7 @@ mod tests { #[test] fn factor_recombines_strong_pseudoprime() { // This is a strong pseudoprime (wrt. miller_rabin::BASIS) - // and triggered a bug in rho::factor's codepath handling + // and triggered a bug in rho::factor's code path handling // miller_rabbin::Result::Composite let pseudoprime = 17179869183; for _ in 0..20 { @@ -197,9 +213,15 @@ mod tests { i == 0 || factor(i).product() == i } - fn recombines_factors(f: Factors) -> bool { + fn recombines_factors(f: Factors) -> () { assert_eq!(factor(f.product()), f); - true + } + + fn exponentiate_factors(f: Factors, e: Exponent) -> () { + if e == 0 { return; } + if let Some(fe) = f.product().checked_pow(e.into()) { + assert_eq!(factor(fe), f ^ e); + } } } } @@ -213,7 +235,7 @@ impl quickcheck::Arbitrary for Factors { let mut n = u64::MAX; // Adam Kalai's algorithm for generating uniformly-distributed - // integers and their factorisation. + // integers and their factorization. // // See Generating Random Factored Numbers, Easily, J. Cryptology (2003) 'attempt: loop { @@ -234,3 +256,19 @@ impl quickcheck::Arbitrary for Factors { } } } + +#[cfg(test)] +impl std::ops::BitXor for Factors { + type Output = Self; + + fn bitxor(self, rhs: Exponent) -> Factors { + debug_assert_ne!(rhs, 0); + let mut r = Factors::one(); + for (p, e) in self.0.borrow().0.iter() { + r.add(*p, rhs * e); + } + + debug_assert_eq!(r.product(), self.product().pow(rhs.into())); + return r; + } +} diff --git a/src/uu/factor/src/numeric/gcd.rs b/src/uu/factor/src/numeric/gcd.rs index 01e4a23bd..36ad833a6 100644 --- a/src/uu/factor/src/numeric/gcd.rs +++ b/src/uu/factor/src/numeric/gcd.rs @@ -76,10 +76,13 @@ mod tests { gcd(0, a) == a } - fn divisor(a: u64, b: u64) -> bool { - // Test that gcd(a, b) divides a and b + fn divisor(a: u64, b: u64) -> () { + // Test that gcd(a, b) divides a and b, unless a == b == 0 + if a == 0 && b == 0 { return; } + let g = gcd(a, b); - (g != 0 && a % g == 0 && b % g == 0) || (g == 0 && a == 0 && b == 0) + assert_eq!(a % g, 0); + assert_eq!(b % g, 0); } fn commutative(a: u64, b: u64) -> bool { diff --git a/src/uu/factor/src/numeric/mod.rs b/src/uu/factor/src/numeric/mod.rs index 4a2c94cec..d086027b8 100644 --- a/src/uu/factor/src/numeric/mod.rs +++ b/src/uu/factor/src/numeric/mod.rs @@ -9,7 +9,6 @@ mod gcd; pub use gcd::gcd; pub(crate) mod traits; -use traits::{DoubleInt, Int, OverflowingAdd}; mod modular_inverse; pub(crate) use modular_inverse::modular_inverse; diff --git a/src/uu/factor/src/numeric/montgomery.rs b/src/uu/factor/src/numeric/montgomery.rs index 22fcc21a1..8c38464bc 100644 --- a/src/uu/factor/src/numeric/montgomery.rs +++ b/src/uu/factor/src/numeric/montgomery.rs @@ -7,7 +7,8 @@ // * that was distributed with this source code. use super::*; -use num_traits::identities::{One, Zero}; + +use traits::{DoubleInt, Int, One, OverflowingAdd, Zero}; pub(crate) trait Arithmetic: Copy + Sized { // The type of integers mod m, in some opaque representation @@ -72,7 +73,7 @@ impl Montgomery { let Montgomery { a, n } = self; let m = T::from_double_width(x).wrapping_mul(a); let nm = (n.as_double_width()) * (m.as_double_width()); - let (xnm, overflow) = x.overflowing_add_(nm); // x + n*m + let (xnm, overflow) = x.overflowing_add(&nm); // x + n*m debug_assert_eq!( xnm % (T::DoubleWidth::one() << T::zero().count_zeros() as usize), T::DoubleWidth::zero() @@ -128,7 +129,7 @@ impl Arithmetic for Montgomery { } fn add(&self, a: Self::ModInt, b: Self::ModInt) -> Self::ModInt { - let (r, overflow) = a.overflowing_add_(b); + let (r, overflow) = a.overflowing_add(&b); // In case of overflow, a+b = 2⁶⁴ + r = (2⁶⁴ - n) + r (working mod n) let r = if !overflow { diff --git a/src/uu/factor/src/numeric/traits.rs b/src/uu/factor/src/numeric/traits.rs index 50f5ab5c1..2e9167e0b 100644 --- a/src/uu/factor/src/numeric/traits.rs +++ b/src/uu/factor/src/numeric/traits.rs @@ -6,31 +6,16 @@ // * For the full copyright and license information, please view the LICENSE file // * that was distributed with this source code. +pub(crate) use num_traits::{ + identities::{One, Zero}, + ops::overflowing::OverflowingAdd, +}; use num_traits::{ int::PrimInt, ops::wrapping::{WrappingMul, WrappingNeg, WrappingSub}, }; use std::fmt::{Debug, Display}; -// NOTE: Trait can be removed once num-traits adds a similar one; -// see https://github.com/rust-num/num-traits/issues/168 -pub(crate) trait OverflowingAdd: Sized { - fn overflowing_add_(self, n: Self) -> (Self, bool); -} - -macro_rules! overflowing { - ($x:ty) => { - impl OverflowingAdd for $x { - fn overflowing_add_(self, n: Self) -> (Self, bool) { - self.overflowing_add(n) - } - } - }; -} -overflowing!(u32); -overflowing!(u64); -overflowing!(u128); - pub(crate) trait Int: Display + Debug + PrimInt + OverflowingAdd + WrappingNeg + WrappingSub + WrappingMul { diff --git a/src/uu/false/Cargo.toml b/src/uu/false/Cargo.toml index 784a44666..d7cbcd13a 100644 --- a/src/uu/false/Cargo.toml +++ b/src/uu/false/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_false" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "false ~ (uutils) do nothing and fail" @@ -15,8 +15,8 @@ edition = "2018" path = "src/false.rs" [dependencies] -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "false" diff --git a/src/uu/fmt/Cargo.toml b/src/uu/fmt/Cargo.toml index 59bb0e2b7..24ee13b35 100644 --- a/src/uu/fmt/Cargo.toml +++ b/src/uu/fmt/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_fmt" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "fmt ~ (uutils) reformat each paragraph of input" @@ -15,10 +15,11 @@ edition = "2018" path = "src/fmt.rs" [dependencies] +clap = "2.33" libc = "0.2.42" unicode-width = "0.1.5" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "fmt" diff --git a/src/uu/fmt/src/fmt.rs b/src/uu/fmt/src/fmt.rs index a7a3103b4..f10f4cf7f 100644 --- a/src/uu/fmt/src/fmt.rs +++ b/src/uu/fmt/src/fmt.rs @@ -7,11 +7,10 @@ // spell-checker:ignore (ToDO) PSKIP linebreak ostream parasplit tabwidth xanti xprefix -extern crate unicode_width; - #[macro_use] extern crate uucore; +use clap::{App, Arg}; use std::cmp; use std::fs::File; use std::io::{stdin, stdout, Write}; @@ -32,10 +31,29 @@ macro_rules! silent_unwrap( mod linebreak; mod parasplit; -// program's NAME and VERSION are used for -V and -h -static SYNTAX: &str = "[OPTION]... [FILE]..."; -static SUMMARY: &str = "Reformat paragraphs from input files (or stdin) to stdout."; -static LONG_HELP: &str = ""; +static ABOUT: &str = "Reformat paragraphs from input files (or stdin) to stdout."; +static VERSION: &str = env!("CARGO_PKG_VERSION"); +static MAX_WIDTH: usize = 2500; + +static OPT_CROWN_MARGIN: &str = "crown-margin"; +static OPT_TAGGED_PARAGRAPH: &str = "tagged-paragraph"; +static OPT_PRESERVE_HEADERS: &str = "preserve-headers"; +static OPT_SPLIT_ONLY: &str = "split-only"; +static OPT_UNIFORM_SPACING: &str = "uniform-spacing"; +static OPT_PREFIX: &str = "prefix"; +static OPT_SKIP_PREFIX: &str = "skip-prefix"; +static OPT_EXACT_PREFIX: &str = "exact-prefix"; +static OPT_EXACT_SKIP_PREFIX: &str = "exact-skip-prefix"; +static OPT_WIDTH: &str = "width"; +static OPT_GOAL: &str = "goal"; +static OPT_QUICK: &str = "quick"; +static OPT_TAB_WIDTH: &str = "tab-width"; + +static ARG_FILES: &str = "files"; + +fn get_usage() -> String { + format!("{} [OPTION]... [FILE]...", executable!()) +} pub type FileOrStdReader = BufReader>; pub struct FmtOptions { @@ -58,23 +76,136 @@ pub struct FmtOptions { #[allow(clippy::cognitive_complexity)] pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); + let usage = get_usage(); - let matches = app!(SYNTAX, SUMMARY, LONG_HELP) - .optflag("c", "crown-margin", "First and second line of paragraph may have different indentations, in which case the first line's indentation is preserved, and each subsequent line's indentation matches the second line.") - .optflag("t", "tagged-paragraph", "Like -c, except that the first and second line of a paragraph *must* have different indentation or they are treated as separate paragraphs.") - .optflag("m", "preserve-headers", "Attempt to detect and preserve mail headers in the input. Be careful when combining this flag with -p.") - .optflag("s", "split-only", "Split lines only, do not reflow.") - .optflag("u", "uniform-spacing", "Insert exactly one space between words, and two between sentences. Sentence breaks in the input are detected as [?!.] followed by two spaces or a newline; other punctuation is not interpreted as a sentence break.") - .optopt("p", "prefix", "Reformat only lines beginning with PREFIX, reattaching PREFIX to reformatted lines. Unless -x is specified, leading whitespace will be ignored when matching PREFIX.", "PREFIX") - .optopt("P", "skip-prefix", "Do not reformat lines beginning with PSKIP. Unless -X is specified, leading whitespace will be ignored when matching PSKIP", "PSKIP") - .optflag("x", "exact-prefix", "PREFIX must match at the beginning of the line with no preceding whitespace.") - .optflag("X", "exact-skip-prefix", "PSKIP must match at the beginning of the line with no preceding whitespace.") - .optopt("w", "width", "Fill output lines up to a maximum of WIDTH columns, default 79.", "WIDTH") - .optopt("g", "goal", "Goal width, default ~0.94*WIDTH. Must be less than WIDTH.", "GOAL") - .optflag("q", "quick", "Break lines more quickly at the expense of a potentially more ragged appearance.") - .optopt("T", "tab-width", "Treat tabs as TABWIDTH spaces for determining line length, default 8. Note that this is used only for calculating line lengths; tabs are preserved in the output.", "TABWIDTH") - .parse(args); + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(&usage[..]) + .arg( + Arg::with_name(OPT_CROWN_MARGIN) + .short("c") + .long(OPT_CROWN_MARGIN) + .help( + "First and second line of paragraph + may have different indentations, in which + case the first line's indentation is preserved, + and each subsequent line's indentation matches the second line.", + ), + ) + .arg( + Arg::with_name(OPT_TAGGED_PARAGRAPH) + .short("t") + .long("tagged-paragraph") + .help( + "Like -c, except that the first and second line of a paragraph *must* + have different indentation or they are treated as separate paragraphs.", + ), + ) + .arg( + Arg::with_name(OPT_PRESERVE_HEADERS) + .short("m") + .long("preserve-headers") + .help( + "Attempt to detect and preserve mail headers in the input. + Be careful when combining this flag with -p.", + ), + ) + .arg( + Arg::with_name(OPT_SPLIT_ONLY) + .short("s") + .long("split-only") + .help("Split lines only, do not reflow."), + ) + .arg( + Arg::with_name(OPT_UNIFORM_SPACING) + .short("u") + .long("uniform-spacing") + .help( + "Insert exactly one + space between words, and two between sentences. + Sentence breaks in the input are detected as [?!.] + followed by two spaces or a newline; other punctuation + is not interpreted as a sentence break.", + ), + ) + .arg( + Arg::with_name(OPT_PREFIX) + .short("p") + .long("prefix") + .help( + "Reformat only lines + beginning with PREFIX, reattaching PREFIX to reformatted lines. + Unless -x is specified, leading whitespace will be ignored + when matching PREFIX.", + ) + .value_name("PREFIX"), + ) + .arg( + Arg::with_name(OPT_SKIP_PREFIX) + .short("P") + .long("skip-prefix") + .help( + "Do not reformat lines + beginning with PSKIP. Unless -X is specified, leading whitespace + will be ignored when matching PSKIP", + ) + .value_name("PSKIP"), + ) + .arg( + Arg::with_name(OPT_EXACT_PREFIX) + .short("x") + .long("exact-prefix") + .help( + "PREFIX must match at the + beginning of the line with no preceding whitespace.", + ), + ) + .arg( + Arg::with_name(OPT_EXACT_SKIP_PREFIX) + .short("X") + .long("exact-skip-prefix") + .help( + "PSKIP must match at the + beginning of the line with no preceding whitespace.", + ), + ) + .arg( + Arg::with_name(OPT_WIDTH) + .short("w") + .long("width") + .help("Fill output lines up to a maximum of WIDTH columns, default 79.") + .value_name("WIDTH"), + ) + .arg( + Arg::with_name(OPT_GOAL) + .short("g") + .long("goal") + .help("Goal width, default ~0.94*WIDTH. Must be less than WIDTH.") + .value_name("GOAL"), + ) + .arg(Arg::with_name(OPT_QUICK).short("q").long("quick").help( + "Break lines more quickly at the + expense of a potentially more ragged appearance.", + )) + .arg( + Arg::with_name(OPT_TAB_WIDTH) + .short("T") + .long("tab-width") + .help( + "Treat tabs as TABWIDTH spaces for + determining line length, default 8. Note that this is used only for + calculating line lengths; tabs are preserved in the output.", + ) + .value_name("TABWIDTH"), + ) + .arg(Arg::with_name(ARG_FILES).multiple(true).takes_value(true)) + .get_matches_from(args); + + let mut files: Vec = matches + .values_of(ARG_FILES) + .map(|v| v.map(ToString::to_string).collect()) + .unwrap_or_default(); let mut fmt_opts = FmtOptions { crown: false, @@ -94,69 +225,64 @@ pub fn uumain(args: impl uucore::Args) -> i32 { tabwidth: 8, }; - if matches.opt_present("t") { - fmt_opts.tagged = true; - } - if matches.opt_present("c") { + fmt_opts.tagged = matches.is_present(OPT_TAGGED_PARAGRAPH); + if matches.is_present(OPT_CROWN_MARGIN) { fmt_opts.crown = true; fmt_opts.tagged = false; } - if matches.opt_present("m") { - fmt_opts.mail = true; - } - if matches.opt_present("u") { - fmt_opts.uniform = true; - } - if matches.opt_present("q") { - fmt_opts.quick = true; - } - if matches.opt_present("s") { + fmt_opts.mail = matches.is_present(OPT_PRESERVE_HEADERS); + fmt_opts.uniform = matches.is_present(OPT_UNIFORM_SPACING); + fmt_opts.quick = matches.is_present(OPT_QUICK); + if matches.is_present(OPT_SPLIT_ONLY) { fmt_opts.split_only = true; fmt_opts.crown = false; fmt_opts.tagged = false; } - if matches.opt_present("x") { - fmt_opts.xprefix = true; - } - if matches.opt_present("X") { - fmt_opts.xanti_prefix = true; - } + fmt_opts.xprefix = matches.is_present(OPT_EXACT_PREFIX); + fmt_opts.xanti_prefix = matches.is_present(OPT_SKIP_PREFIX); - if let Some(s) = matches.opt_str("p") { + if let Some(s) = matches.value_of(OPT_PREFIX).map(String::from) { fmt_opts.prefix = s; fmt_opts.use_prefix = true; }; - if let Some(s) = matches.opt_str("P") { + if let Some(s) = matches.value_of(OPT_SKIP_PREFIX).map(String::from) { fmt_opts.anti_prefix = s; fmt_opts.use_anti_prefix = true; }; - if let Some(s) = matches.opt_str("w") { + if let Some(s) = matches.value_of(OPT_WIDTH) { fmt_opts.width = match s.parse::() { Ok(t) => t, Err(e) => { crash!(1, "Invalid WIDTH specification: `{}': {}", s, e); } }; + if fmt_opts.width > MAX_WIDTH { + crash!( + 1, + "invalid width: '{}': Numerical result out of range", + fmt_opts.width + ); + } fmt_opts.goal = cmp::min(fmt_opts.width * 94 / 100, fmt_opts.width - 3); }; - if let Some(s) = matches.opt_str("g") { + if let Some(s) = matches.value_of(OPT_GOAL) { fmt_opts.goal = match s.parse::() { Ok(t) => t, Err(e) => { crash!(1, "Invalid GOAL specification: `{}': {}", s, e); } }; - if !matches.opt_present("w") { + if !matches.is_present(OPT_WIDTH) { fmt_opts.width = cmp::max(fmt_opts.goal * 100 / 94, fmt_opts.goal + 3); } else if fmt_opts.goal > fmt_opts.width { crash!(1, "GOAL cannot be greater than WIDTH."); } }; - if let Some(s) = matches.opt_str("T") { + if let Some(s) = matches.value_of(OPT_TAB_WIDTH) { fmt_opts.tabwidth = match s.parse::() { Ok(t) => t, Err(e) => { @@ -172,7 +298,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 { // immutable now let fmt_opts = fmt_opts; - let mut files = matches.free; if files.is_empty() { files.push("-".to_owned()); } diff --git a/src/uu/fmt/src/linebreak.rs b/src/uu/fmt/src/linebreak.rs index c41fd41bd..50cb6f77f 100644 --- a/src/uu/fmt/src/linebreak.rs +++ b/src/uu/fmt/src/linebreak.rs @@ -81,7 +81,7 @@ pub fn break_lines(para: &Paragraph, opts: &FmtOptions, ostream: &mut BufWriter< let mut break_args = BreakArgs { opts, init_len: p_init_len, - indent_str: &p_indent[..], + indent_str: p_indent, indent_len: p_indent_len, uniform, ostream, diff --git a/src/uu/fmt/src/parasplit.rs b/src/uu/fmt/src/parasplit.rs index ddf6f394b..f74a25413 100644 --- a/src/uu/fmt/src/parasplit.rs +++ b/src/uu/fmt/src/parasplit.rs @@ -264,6 +264,8 @@ impl<'a> ParagraphStream<'a> { return false; } + #[allow(clippy::match_like_matches_macro)] + // `matches!(...)` macro not stabilized until rust v1.42 l_slice[..colon_posn].chars().all(|x| match x as usize { y if y < 33 || y > 126 => false, _ => true, @@ -539,6 +541,8 @@ impl<'a> WordSplit<'a> { } fn is_punctuation(c: char) -> bool { + #[allow(clippy::match_like_matches_macro)] + // `matches!(...)` macro not stabilized until rust v1.42 match c { '!' | '.' | '?' => true, _ => false, diff --git a/src/uu/fold/Cargo.toml b/src/uu/fold/Cargo.toml index 10493a9ca..f99abc691 100644 --- a/src/uu/fold/Cargo.toml +++ b/src/uu/fold/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_fold" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "fold ~ (uutils) wrap each line of input" @@ -15,8 +15,8 @@ edition = "2018" path = "src/fold.rs" [dependencies] -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "fold" diff --git a/src/uu/fold/src/fold.rs b/src/uu/fold/src/fold.rs index 75d007dce..fac0ff28e 100644 --- a/src/uu/fold/src/fold.rs +++ b/src/uu/fold/src/fold.rs @@ -14,6 +14,8 @@ use std::fs::File; use std::io::{stdin, BufRead, BufReader, Read}; use std::path::Path; +const TAB_WIDTH: usize = 8; + static SYNTAX: &str = "[OPTION]... [FILE]..."; static SUMMARY: &str = "Writes each file (or standard input if no files are given) to standard output whilst breaking long lines"; @@ -79,7 +81,6 @@ fn handle_obsolete(args: &[String]) -> (Vec, Option) { (args.to_vec(), None) } -#[inline] fn fold(filenames: Vec, bytes: bool, spaces: bool, width: usize) { for filename in &filenames { let filename: &str = &filename; @@ -92,124 +93,176 @@ fn fold(filenames: Vec, bytes: bool, spaces: bool, width: usize) { file_buf = safe_unwrap!(File::open(Path::new(filename))); &mut file_buf as &mut dyn Read }); - fold_file(buffer, bytes, spaces, width); + + if bytes { + fold_file_bytewise(buffer, spaces, width); + } else { + fold_file(buffer, spaces, width); + } } } -#[inline] -fn fold_file(mut file: BufReader, bytes: bool, spaces: bool, width: usize) { +/// Fold `file` to fit `width` (number of columns), counting all characters as +/// one column. +/// +/// This function handles folding for the `-b`/`--bytes` option, counting +/// tab, backspace, and carriage return as occupying one column, identically +/// to all other characters in the stream. +/// +/// If `spaces` is `true`, attempt to break lines at whitespace boundaries. +fn fold_file_bytewise(mut file: BufReader, spaces: bool, width: usize) { let mut line = String::new(); - while safe_unwrap!(file.read_line(&mut line)) > 0 { - if bytes { - let len = line.len(); - let mut i = 0; - while i < len { - let width = if len - i >= width { width } else { len - i }; - let slice = { - let slice = &line[i..i + width]; - if spaces && i + width < len { - match slice.rfind(char::is_whitespace) { - Some(m) => &slice[..=m], - None => slice, - } - } else { - slice + + loop { + if let Ok(0) = file.read_line(&mut line) { + break; + } + + if line == "\n" { + println!(); + line.truncate(0); + continue; + } + + let len = line.len(); + let mut i = 0; + + while i < len { + let width = if len - i >= width { width } else { len - i }; + let slice = { + let slice = &line[i..i + width]; + if spaces && i + width < len { + match slice.rfind(char::is_whitespace) { + Some(m) => &slice[..=m], + None => slice, } - }; - print!("{}", slice); - i += slice.len(); + } else { + slice + } + }; + + // Don't duplicate trailing newlines: if the slice is "\n", the + // previous iteration folded just before the end of the line and + // has already printed this newline. + if slice == "\n" { + break; } - } else { - let mut len = line.chars().count(); - let newline = line.ends_with('\n'); - if newline { - if len == 1 { - println!(); + + i += slice.len(); + + let at_eol = i >= len; + + if at_eol { + print!("{}", slice); + } else { + println!("{}", slice); + } + } + + line.truncate(0); + } +} + +/// Fold `file` to fit `width` (number of columns). +/// +/// By default `fold` treats tab, backspace, and carriage return specially: +/// tab characters count as 8 columns, backspace decreases the +/// column count, and carriage return resets the column count to 0. +/// +/// If `spaces` is `true`, attempt to break lines at whitespace boundaries. +#[allow(unused_assignments)] +fn fold_file(mut file: BufReader, spaces: bool, width: usize) { + let mut line = String::new(); + let mut output = String::new(); + let mut col_count = 0; + let mut char_count = 0; + let mut last_space = None; + + /// Print the output line, resetting the column and character counts. + /// + /// If `spaces` is `true`, print the output line up to the last + /// encountered whitespace character (inclusive) and set the remaining + /// characters as the start of the next line. + macro_rules! emit_output { + () => { + let consume = match last_space { + Some(i) => i + 1, + None => output.len(), + }; + + println!("{}", &output[..consume]); + output.replace_range(..consume, ""); + char_count = output.len(); + + // we know there are no tabs left in output, so each char counts + // as 1 column + col_count = char_count; + + last_space = None; + }; + } + + loop { + if let Ok(0) = file.read_line(&mut line) { + break; + } + + for ch in line.chars() { + if ch == '\n' { + // make sure to _not_ split output at whitespace, since we + // know the entire output will fit + last_space = None; + emit_output!(); + break; + } + + if col_count >= width { + emit_output!(); + } + + match ch { + '\t' => { + let next_tab_stop = col_count + TAB_WIDTH - col_count % TAB_WIDTH; + + if next_tab_stop > width && !output.is_empty() { + emit_output!(); + } + + col_count = next_tab_stop; + last_space = if spaces { Some(char_count) } else { None }; + } + '\x08' => { + // FIXME: does not match GNU's handling of backspace + if col_count > 0 { + col_count -= 1; + char_count -= 1; + output.truncate(char_count); + } continue; } - len -= 1; - line.truncate(len); - } - let mut output = String::new(); - let mut count = 0; - for (i, ch) in line.chars().enumerate() { - if count >= width { - let (val, ncount) = { - let slice = &output[..]; - let (out, val, ncount) = if spaces && i + 1 < len { - match rfind_whitespace(slice) { - Some(m) => { - let routput = &slice[m + 1..slice.chars().count()]; - let ncount = routput.chars().fold(0, |out, ch: char| { - out + match ch { - '\t' => 8, - '\x08' => { - if out > 0 { - !0 - } else { - 0 - } - } - '\r' => return 0, - _ => 1, - } - }); - (&slice[0..=m], routput, ncount) - } - None => (slice, "", 0), - } - } else { - (slice, "", 0) - }; - println!("{}", out); - (val.to_owned(), ncount) - }; - output = val; - count = ncount; + '\r' => { + // FIXME: does not match GNU's handling of carriage return + output.truncate(0); + col_count = 0; + char_count = 0; + continue; } - match ch { - '\t' => { - count += 8; - if count > width { - println!("{}", output); - output.truncate(0); - count = 8; - } - } - '\x08' => { - if count > 0 { - count -= 1; - let len = output.len() - 1; - output.truncate(len); - } - continue; - } - '\r' => { - output.truncate(0); - count = 0; - continue; - } - _ => count += 1, - }; - output.push(ch); - } - if count > 0 { - if newline { - println!("{}", output); - } else { - print!("{}", output); + _ if spaces && ch.is_whitespace() => { + last_space = Some(char_count); + col_count += 1 } - } - } - } -} + _ => col_count += 1, + }; -#[inline] -fn rfind_whitespace(slice: &str) -> Option { - for (i, ch) in slice.chars().rev().enumerate() { - if ch.is_whitespace() { - return Some(slice.chars().count() - (i + 1)); + output.push(ch); + char_count += 1; } + + if col_count > 0 { + print!("{}", output); + output.truncate(0); + } + + line.truncate(0); } - None } diff --git a/src/uu/groups/Cargo.toml b/src/uu/groups/Cargo.toml index fb6d193ed..1a56bc2ab 100644 --- a/src/uu/groups/Cargo.toml +++ b/src/uu/groups/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_groups" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "groups ~ (uutils) display group memberships for USERNAME" @@ -15,9 +15,9 @@ edition = "2018" path = "src/groups.rs" [dependencies] -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary", features=["entries"] } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } -clap = "2.32" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["entries"] } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } +clap = "2.33" [[bin]] name = "groups" diff --git a/src/uu/groups/src/groups.rs b/src/uu/groups/src/groups.rs index e2ae61a91..2ce5fe70e 100644 --- a/src/uu/groups/src/groups.rs +++ b/src/uu/groups/src/groups.rs @@ -12,7 +12,6 @@ extern crate uucore; use uucore::entries::{get_groups, gid2grp, Locate, Passwd}; -extern crate clap; use clap::{App, Arg}; static VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/src/uu/hashsum/Cargo.toml b/src/uu/hashsum/Cargo.toml index aeaac7ac3..04a22cac7 100644 --- a/src/uu/hashsum/Cargo.toml +++ b/src/uu/hashsum/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_hashsum" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "hashsum ~ (uutils) display or check input digests" @@ -16,7 +16,7 @@ path = "src/hashsum.rs" [dependencies] digest = "0.6.2" -clap = "2" +clap = "2.33" hex = "0.2.0" libc = "0.2.42" md5 = "0.3.5" @@ -25,8 +25,9 @@ regex-syntax = "0.6.7" sha1 = "0.6.0" sha2 = "0.6.0" sha3 = "0.6.0" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +blake2-rfc = "0.2.18" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "hashsum" diff --git a/src/uu/hashsum/src/digest.rs b/src/uu/hashsum/src/digest.rs index ea2953dfd..218de0a36 100644 --- a/src/uu/hashsum/src/digest.rs +++ b/src/uu/hashsum/src/digest.rs @@ -1,3 +1,4 @@ +extern crate blake2_rfc; extern crate digest; extern crate md5; extern crate sha1; @@ -48,6 +49,29 @@ impl Digest for md5::Context { } } +impl Digest for blake2_rfc::blake2b::Blake2b { + fn new() -> Self { + blake2_rfc::blake2b::Blake2b::new(64) + } + + fn input(&mut self, input: &[u8]) { + self.update(input); + } + + fn result(&mut self, out: &mut [u8]) { + let hash_result = &self.clone().finalize(); + out.copy_from_slice(&hash_result.as_bytes()); + } + + fn reset(&mut self) { + *self = blake2_rfc::blake2b::Blake2b::new(64); + } + + fn output_bits(&self) -> usize { + 512 + } +} + impl Digest for sha1::Sha1 { fn new() -> Self { sha1::Sha1::new() diff --git a/src/uu/hashsum/src/hashsum.rs b/src/uu/hashsum/src/hashsum.rs index 733cd8b54..ee7d2a0f7 100644 --- a/src/uu/hashsum/src/hashsum.rs +++ b/src/uu/hashsum/src/hashsum.rs @@ -11,13 +11,6 @@ #[macro_use] extern crate clap; -extern crate hex; -extern crate md5; -extern crate regex; -extern crate regex_syntax; -extern crate sha1; -extern crate sha2; -extern crate sha3; #[macro_use] extern crate uucore; @@ -26,6 +19,7 @@ mod digest; use self::digest::Digest; +use blake2_rfc::blake2b::Blake2b; use clap::{App, Arg, ArgMatches}; use hex::ToHex; use md5::Context as Md5; @@ -57,10 +51,12 @@ struct Options { } fn is_custom_binary(program: &str) -> bool { + #[allow(clippy::match_like_matches_macro)] + // `matches!(...)` macro not stabilized until rust v1.42 match program { "md5sum" | "sha1sum" | "sha224sum" | "sha256sum" | "sha384sum" | "sha512sum" | "sha3sum" | "sha3-224sum" | "sha3-256sum" | "sha3-384sum" | "sha3-512sum" - | "shake128sum" | "shake256sum" => true, + | "shake128sum" | "shake256sum" | "b2sum" => true, _ => false, } } @@ -80,6 +76,7 @@ fn detect_algo<'a>( "sha256sum" => ("SHA256", Box::new(Sha256::new()) as Box, 256), "sha384sum" => ("SHA384", Box::new(Sha384::new()) as Box, 384), "sha512sum" => ("SHA512", Box::new(Sha512::new()) as Box, 512), + "b2sum" => ("BLAKE2", Box::new(Blake2b::new(64)) as Box, 512), "sha3sum" => match matches.value_of("bits") { Some(bits_str) => match usize::from_str_radix(&bits_str, 10) { Ok(224) => ( @@ -180,6 +177,9 @@ fn detect_algo<'a>( if matches.is_present("sha512") { set_or_crash("SHA512", Box::new(Sha512::new()), 512) } + if matches.is_present("b2sum") { + set_or_crash("BLAKE2", Box::new(Blake2b::new(64)), 512) + } if matches.is_present("sha3") { match matches.value_of("bits") { Some(bits_str) => match usize::from_str_radix(&bits_str, 10) { @@ -381,6 +381,7 @@ pub fn uumain(mut args: impl uucore::Args) -> i32 { "shake256", "work with SHAKE256 using BITS for the output size", ), + ("b2sum", "work with BLAKE2"), ]; for (name, desc) in algos { diff --git a/src/uu/head/Cargo.toml b/src/uu/head/Cargo.toml index df9f10a72..3c383cb6f 100644 --- a/src/uu/head/Cargo.toml +++ b/src/uu/head/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_head" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "head ~ (uutils) display the first lines of input" @@ -15,9 +15,9 @@ edition = "2018" path = "src/head.rs" [dependencies] -libc = "0.2.42" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +clap = "2.33" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "head" diff --git a/src/uu/head/src/head.rs b/src/uu/head/src/head.rs index 56d7e8452..3500af544 100644 --- a/src/uu/head/src/head.rs +++ b/src/uu/head/src/head.rs @@ -1,223 +1,642 @@ -// * This file is part of the uutils coreutils package. -// * -// * (c) Alan Andrade -// * -// * For the full copyright and license information, please view the LICENSE -// * file that was distributed with this source code. -// * -// * Synced with: https://raw.github.com/avsm/src/master/usr.bin/head/head.c +use clap::{App, Arg}; +use std::convert::TryFrom; +use std::ffi::OsString; +use std::io::{ErrorKind, Read, Seek, SeekFrom, Write}; +use uucore::{crash, executable, show_error}; -#[macro_use] -extern crate uucore; +const EXIT_FAILURE: i32 = 1; +const EXIT_SUCCESS: i32 = 0; +const BUF_SIZE: usize = 65536; -use std::collections::VecDeque; -use std::fs::File; -use std::io::{stdin, BufRead, BufReader, Read}; -use std::path::Path; -use std::str::from_utf8; +const VERSION: &str = env!("CARGO_PKG_VERSION"); +const ABOUT: &str = "\ + Print the first 10 lines of each FILE to standard output.\n\ + With more than one FILE, precede each with a header giving the file name.\n\ + \n\ + With no FILE, or when FILE is -, read standard input.\n\ + \n\ + Mandatory arguments to long flags are mandatory for short flags too.\ + "; +const USAGE: &str = "head [FLAG]... [FILE]..."; -static SYNTAX: &str = ""; -static SUMMARY: &str = ""; -static LONG_HELP: &str = ""; +mod options { + pub const BYTES_NAME: &str = "BYTES"; + pub const LINES_NAME: &str = "LINES"; + pub const QUIET_NAME: &str = "QUIET"; + pub const VERBOSE_NAME: &str = "VERBOSE"; + pub const ZERO_NAME: &str = "ZERO"; + pub const FILES_NAME: &str = "FILE"; +} +mod parse; +mod split; -enum FilterMode { - Bytes(usize), +fn app<'a>() -> App<'a, 'a> { + App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(USAGE) + .arg( + Arg::with_name(options::BYTES_NAME) + .short("c") + .long("bytes") + .value_name("[-]NUM") + .takes_value(true) + .help( + "\ + print the first NUM bytes of each file;\n\ + with the leading '-', print all but the last\n\ + NUM bytes of each file\ + ", + ) + .overrides_with_all(&[options::BYTES_NAME, options::LINES_NAME]) + .allow_hyphen_values(true), + ) + .arg( + Arg::with_name(options::LINES_NAME) + .short("n") + .long("lines") + .value_name("[-]NUM") + .takes_value(true) + .help( + "\ + print the first NUM lines instead of the first 10;\n\ + with the leading '-', print all but the last\n\ + NUM lines of each file\ + ", + ) + .overrides_with_all(&[options::LINES_NAME, options::BYTES_NAME]) + .allow_hyphen_values(true), + ) + .arg( + Arg::with_name(options::QUIET_NAME) + .short("q") + .long("--quiet") + .visible_alias("silent") + .help("never print headers giving file names") + .overrides_with_all(&[options::VERBOSE_NAME, options::QUIET_NAME]), + ) + .arg( + Arg::with_name(options::VERBOSE_NAME) + .short("v") + .long("verbose") + .help("always print headers giving file names") + .overrides_with_all(&[options::QUIET_NAME, options::VERBOSE_NAME]), + ) + .arg( + Arg::with_name(options::ZERO_NAME) + .short("z") + .long("zero-terminated") + .help("line delimiter is NUL, not newline") + .overrides_with(options::ZERO_NAME), + ) + .arg(Arg::with_name(options::FILES_NAME).multiple(true)) +} +#[derive(PartialEq, Debug, Clone, Copy)] +enum Modes { Lines(usize), - NLines(usize), + Bytes(usize), } -struct Settings { - mode: FilterMode, - verbose: bool, +fn parse_mode(src: &str, closure: F) -> Result<(Modes, bool), String> +where + F: FnOnce(usize) -> Modes, +{ + match parse::parse_num(src) { + Ok((n, last)) => Ok((closure(n), last)), + Err(reason) => match reason { + parse::ParseError::Syntax => Err(format!("'{}'", src)), + parse::ParseError::Overflow => { + Err(format!("'{}': Value too large for defined datatype", src)) + } + }, + } } -impl Default for Settings { - fn default() -> Settings { - Settings { - mode: FilterMode::Lines(10), - verbose: false, +fn arg_iterate<'a>( + mut args: impl uucore::Args + 'a, +) -> Result + 'a>, String> { + // argv[0] is always present + let first = args.next().unwrap(); + if let Some(second) = args.next() { + if let Some(s) = second.to_str() { + 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::Overflow => Err(format!( + "invalid argument: '{}' Value too large for defined datatype", + s + )), + }, + None => Ok(Box::new(vec![first, second].into_iter().chain(args))), + } + } else { + Err("bad argument encoding".to_owned()) } + } else { + Ok(Box::new(vec![first].into_iter())) + } +} + +#[derive(Debug, PartialEq)] +struct HeadOptions { + pub quiet: bool, + pub verbose: bool, + pub zeroed: bool, + pub all_but_last: bool, + pub mode: Modes, + pub files: Vec, +} + +impl HeadOptions { + pub fn new() -> HeadOptions { + HeadOptions { + quiet: false, + verbose: false, + zeroed: false, + all_but_last: false, + mode: Modes::Lines(10), + files: Vec::new(), + } + } + + ///Construct options from matches + pub fn get_from(args: impl uucore::Args) -> Result { + let matches = app().get_matches_from(arg_iterate(args)?); + + let mut options = HeadOptions::new(); + + options.quiet = matches.is_present(options::QUIET_NAME); + options.verbose = matches.is_present(options::VERBOSE_NAME); + options.zeroed = matches.is_present(options::ZERO_NAME); + + let mode_and_from_end = if let Some(v) = matches.value_of(options::BYTES_NAME) { + match parse_mode(v, Modes::Bytes) { + Ok(v) => v, + Err(err) => { + return Err(format!("invalid number of bytes: {}", err)); + } + } + } else if let Some(v) = matches.value_of(options::LINES_NAME) { + match parse_mode(v, Modes::Lines) { + Ok(v) => v, + Err(err) => { + return Err(format!("invalid number of lines: {}", err)); + } + } + } else { + (Modes::Lines(10), false) + }; + + options.mode = mode_and_from_end.0; + options.all_but_last = mode_and_from_end.1; + + options.files = match matches.values_of(options::FILES_NAME) { + Some(v) => v.map(|s| s.to_owned()).collect(), + None => vec!["-".to_owned()], + }; + //println!("{:#?}", options); + Ok(options) + } +} +// to make clippy shut up +impl Default for HeadOptions { + fn default() -> Self { + Self::new() + } +} + +fn rbuf_n_bytes(input: &mut impl std::io::BufRead, n: usize) -> std::io::Result<()> { + if n == 0 { + return Ok(()); + } + let mut readbuf = [0u8; BUF_SIZE]; + let mut i = 0usize; + + let stdout = std::io::stdout(); + let mut stdout = stdout.lock(); + + loop { + let read = loop { + match input.read(&mut readbuf) { + Ok(n) => break n, + Err(e) => match e.kind() { + ErrorKind::Interrupted => {} + _ => return Err(e), + }, + } + }; + if read == 0 { + // might be unexpected if + // we haven't read `n` bytes + // but this mirrors GNU's behavior + return Ok(()); + } + stdout.write_all(&readbuf[..read.min(n - i)])?; + i += read.min(n - i); + if i == n { + return Ok(()); + } + } +} + +fn rbuf_n_lines(input: &mut impl std::io::BufRead, n: usize, zero: bool) -> std::io::Result<()> { + if n == 0 { + return Ok(()); + } + let stdout = std::io::stdout(); + let mut stdout = stdout.lock(); + let mut lines = 0usize; + split::walk_lines(input, zero, |e| match e { + split::Event::Data(dat) => { + stdout.write_all(dat)?; + Ok(true) + } + split::Event::Line => { + lines += 1; + if lines == n { + Ok(false) + } else { + Ok(true) + } + } + }) +} + +fn rbuf_but_last_n_bytes(input: &mut impl std::io::BufRead, n: usize) -> std::io::Result<()> { + if n == 0 { + //prints everything + return rbuf_n_bytes(input, std::usize::MAX); + } + let stdout = std::io::stdout(); + let mut stdout = stdout.lock(); + + let mut ringbuf = vec![0u8; n]; + + // first we fill the ring buffer + if let Err(e) = input.read_exact(&mut ringbuf) { + if e.kind() == ErrorKind::UnexpectedEof { + return Ok(()); + } else { + return Err(e); + } + } + let mut buffer = [0u8; BUF_SIZE]; + loop { + let read = loop { + match input.read(&mut buffer) { + Ok(n) => break n, + Err(e) => match e.kind() { + ErrorKind::Interrupted => {} + _ => return Err(e), + }, + } + }; + if read == 0 { + return Ok(()); + } else if read >= n { + stdout.write_all(&ringbuf)?; + stdout.write_all(&buffer[..read - n])?; + for i in 0..n { + ringbuf[i] = buffer[read - n + i]; + } + } else { + stdout.write_all(&ringbuf[..read])?; + for i in 0..n - read { + ringbuf[i] = ringbuf[read + i]; + } + ringbuf[n - read..].copy_from_slice(&buffer[..read]); + } + } +} + +fn rbuf_but_last_n_lines( + input: &mut impl std::io::BufRead, + n: usize, + zero: bool, +) -> std::io::Result<()> { + if n == 0 { + //prints everything + return rbuf_n_bytes(input, std::usize::MAX); + } + let mut ringbuf = vec![Vec::new(); n]; + let stdout = std::io::stdout(); + let mut stdout = stdout.lock(); + let mut line = Vec::new(); + let mut lines = 0usize; + split::walk_lines(input, zero, |e| match e { + split::Event::Data(dat) => { + line.extend_from_slice(dat); + Ok(true) + } + split::Event::Line => { + if lines < n { + ringbuf[lines] = std::mem::replace(&mut line, Vec::new()); + lines += 1; + } else { + stdout.write_all(&ringbuf[0])?; + ringbuf.rotate_left(1); + ringbuf[n - 1] = std::mem::replace(&mut line, Vec::new()); + } + Ok(true) + } + }) +} + +fn head_backwards_file(input: &mut std::fs::File, options: &HeadOptions) -> std::io::Result<()> { + assert!(options.all_but_last); + let size = input.seek(SeekFrom::End(0))?; + let size = usize::try_from(size).unwrap(); + match options.mode { + Modes::Bytes(n) => { + if n >= size { + return Ok(()); + } else { + input.seek(SeekFrom::Start(0))?; + rbuf_n_bytes( + &mut std::io::BufReader::with_capacity(BUF_SIZE, input), + size - n, + )?; + } + } + Modes::Lines(n) => { + let mut buffer = [0u8; BUF_SIZE]; + let buffer = &mut buffer[..BUF_SIZE.min(size)]; + let mut i = 0usize; + let mut lines = 0usize; + + let found = 'o: loop { + // the casts here are ok, `buffer.len()` should never be above a few k + input.seek(SeekFrom::Current( + -((buffer.len() as i64).min((size - i) as i64)), + ))?; + input.read_exact(buffer)?; + for byte in buffer.iter().rev() { + match byte { + b'\n' if !options.zeroed => { + lines += 1; + } + 0u8 if options.zeroed => { + lines += 1; + } + _ => {} + } + // if it were just `n`, + if lines == n + 1 { + break 'o i; + } + i += 1; + } + if size - i == 0 { + return Ok(()); + } + }; + input.seek(SeekFrom::Start(0))?; + rbuf_n_bytes( + &mut std::io::BufReader::with_capacity(BUF_SIZE, input), + size - found, + )?; + } + } + Ok(()) +} + +fn head_file(input: &mut std::fs::File, options: &HeadOptions) -> std::io::Result<()> { + if options.all_but_last { + head_backwards_file(input, options) + } else { + match options.mode { + Modes::Bytes(n) => { + rbuf_n_bytes(&mut std::io::BufReader::with_capacity(BUF_SIZE, input), n) + } + Modes::Lines(n) => rbuf_n_lines( + &mut std::io::BufReader::with_capacity(BUF_SIZE, input), + n, + options.zeroed, + ), + } + } +} + +fn uu_head(options: &HeadOptions) { + let mut first = true; + for fname in &options.files { + let res = match fname.as_str() { + "-" => { + if options.verbose { + if !first { + println!(); + } + println!("==> standard input <==") + } + let stdin = std::io::stdin(); + let mut stdin = stdin.lock(); + match options.mode { + Modes::Bytes(n) => { + if options.all_but_last { + rbuf_but_last_n_bytes(&mut stdin, n) + } else { + rbuf_n_bytes(&mut stdin, n) + } + } + Modes::Lines(n) => { + if options.all_but_last { + rbuf_but_last_n_lines(&mut stdin, n, options.zeroed) + } else { + rbuf_n_lines(&mut stdin, n, options.zeroed) + } + } + } + } + name => { + let mut file = match std::fs::File::open(name) { + Ok(f) => f, + Err(err) => match err.kind() { + ErrorKind::NotFound => { + crash!( + EXIT_FAILURE, + "head: cannot open '{}' for reading: No such file or directory", + name + ); + } + ErrorKind::PermissionDenied => { + crash!( + EXIT_FAILURE, + "head: cannot open '{}' for reading: Permission denied", + name + ); + } + _ => { + crash!( + EXIT_FAILURE, + "head: cannot open '{}' for reading: {}", + name, + err + ); + } + }, + }; + if (options.files.len() > 1 && !options.quiet) || options.verbose { + println!("==> {} <==", name) + } + head_file(&mut file, options) + } + }; + if res.is_err() { + if fname.as_str() == "-" { + crash!( + EXIT_FAILURE, + "head: error reading standard input: Input/output error" + ); + } else { + crash!( + EXIT_FAILURE, + "head: error reading {}: Input/output error", + fname + ); + } + } + first = false; } } pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); - - let mut settings: Settings = Default::default(); - - // handle obsolete -number syntax - let new_args = match obsolete(&args[0..]) { - (args, Some(n)) => { - settings.mode = FilterMode::Lines(n); - args - } - (args, None) => args, - }; - - let matches = app!(SYNTAX, SUMMARY, LONG_HELP) - .optopt( - "c", - "bytes", - "Print the first K bytes. With the leading '-', print all but the last K bytes", - "[-]K", - ) - .optopt( - "n", - "lines", - "Print the first K lines. With the leading '-', print all but the last K lines", - "[-]K", - ) - .optflag("q", "quiet", "never print headers giving file names") - .optflag("v", "verbose", "always print headers giving file names") - .optflag("h", "help", "display this help and exit") - .optflag("V", "version", "output version information and exit") - .parse(new_args); - - let use_bytes = matches.opt_present("c"); - // TODO: suffixes (e.g. b, kB, etc.) - match matches.opt_str("n") { - Some(n) => { - if use_bytes { - show_error!("cannot specify both --bytes and --lines."); - return 1; - } - - match n.parse::() { - Ok(m) => { - settings.mode = if m < 0 { - let m: usize = m.abs() as usize; - FilterMode::NLines(m) - } else { - let m: usize = m.abs() as usize; - FilterMode::Lines(m) - } - } - Err(e) => { - show_error!("invalid line count '{}': {}", n, e); - return 1; - } - } - } - None => { - if let Some(count) = matches.opt_str("c") { - match count.parse::() { - Ok(m) => settings.mode = FilterMode::Bytes(m), - Err(e) => { - show_error!("invalid byte count '{}': {}", count, e); - return 1; - } - } - } + let args = match HeadOptions::get_from(args) { + Ok(o) => o, + Err(s) => { + crash!(EXIT_FAILURE, "head: {}", s); } }; + uu_head(&args); - let quiet = matches.opt_present("q"); - let verbose = matches.opt_present("v"); - let files = matches.free; - - // GNU implementation allows multiple declarations of "-q" and "-v" with the - // last flag winning. This can't be simulated with the getopts cargo unless - // we manually parse the arguments. Given the declaration of both flags, - // verbose mode always wins. This is a potential future improvement. - if files.len() > 1 && !quiet && !verbose { - settings.verbose = true; - } - if quiet { - settings.verbose = false; - } - if verbose { - settings.verbose = true; - } - - if files.is_empty() { - let mut buffer = BufReader::new(stdin()); - head(&mut buffer, &settings); - } else { - let mut first_time = true; - - for file in &files { - if settings.verbose { - if !first_time { - println!(); - } - println!("==> {} <==", file); - } - first_time = false; - - let path = Path::new(file); - let reader = File::open(&path).unwrap(); - let mut buffer = BufReader::new(reader); - if !head(&mut buffer, &settings) { - break; - } - } - } - - 0 + EXIT_SUCCESS } -// It searches for an option in the form of -123123 -// -// In case is found, the options vector will get rid of that object so that -// getopts works correctly. -fn obsolete(options: &[String]) -> (Vec, Option) { - let mut options: Vec = options.to_vec(); - let mut a = 1; - let b = options.len(); +#[cfg(test)] +mod tests { + use std::ffi::OsString; - while a < b { - let previous = options[a - 1].clone(); - let current = options[a].clone(); - let current = current.as_bytes(); - - if previous != "-n" && current.len() > 1 && current[0] == b'-' { - let len = current.len(); - for pos in 1..len { - // Ensure that the argument is only made out of digits - if !(current[pos] as char).is_numeric() { - break; - } - - // If this is the last number - if pos == len - 1 { - options.remove(a); - let number: Option = - from_utf8(¤t[1..len]).unwrap().parse::().ok(); - return (options, Some(number.unwrap())); - } - } - } - - a += 1; + use super::*; + 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))) } + #[test] + fn test_args_modes() { + let args = options("-n -10M -vz").unwrap(); + assert!(args.zeroed); + assert!(args.verbose); + assert!(args.all_but_last); + assert_eq!(args.mode, Modes::Lines(10 * 1024 * 1024)); + } + #[test] + fn test_gnu_compatibility() { + let args = options("-n 1 -c 1 -n 5 -c kiB -vqvqv").unwrap(); + assert!(args.mode == Modes::Bytes(1024)); + assert!(args.verbose); + assert_eq!(options("-5").unwrap().mode, Modes::Lines(5)); + assert_eq!(options("-2b").unwrap().mode, Modes::Bytes(1024)); + assert_eq!(options("-5 -c 1").unwrap().mode, Modes::Bytes(1)); + } + #[test] + fn all_args_test() { + assert!(options("--silent").unwrap().quiet); + assert!(options("--quiet").unwrap().quiet); + assert!(options("-q").unwrap().quiet); + assert!(options("--verbose").unwrap().verbose); + assert!(options("-v").unwrap().verbose); + assert!(options("--zero-terminated").unwrap().zeroed); + assert!(options("-z").unwrap().zeroed); + assert_eq!(options("--lines 15").unwrap().mode, Modes::Lines(15)); + assert_eq!(options("-n 15").unwrap().mode, Modes::Lines(15)); + assert_eq!(options("--bytes 15").unwrap().mode, Modes::Bytes(15)); + assert_eq!(options("-c 15").unwrap().mode, Modes::Bytes(15)); + } + #[test] + fn test_options_errors() { + assert!(options("-n IsThisTheRealLife?").is_err()); + assert!(options("-c IsThisJustFantasy").is_err()); + } + #[test] + fn test_options_correct_defaults() { + let opts = HeadOptions::new(); + let opts2: HeadOptions = Default::default(); - (options, None) -} + assert_eq!(opts, opts2); -// TODO: handle errors on read -fn head(reader: &mut BufReader, settings: &Settings) -> bool { - match settings.mode { - FilterMode::Bytes(count) => { - for byte in reader.bytes().take(count) { - print!("{}", byte.unwrap() as char); - } - } - FilterMode::Lines(count) => { - for line in reader.lines().take(count) { - println!("{}", line.unwrap()); - } - } - FilterMode::NLines(count) => { - let mut vector: VecDeque = VecDeque::new(); - - for line in reader.lines() { - vector.push_back(line.unwrap()); - if vector.len() <= count { - continue; - } - println!("{}", vector.pop_front().unwrap()); + assert!(opts.verbose == false); + assert!(opts.quiet == false); + assert!(opts.zeroed == false); + assert!(opts.all_but_last == false); + assert_eq!(opts.mode, Modes::Lines(10)); + assert!(opts.files.is_empty()); + } + #[test] + fn test_parse_mode() { + assert_eq!( + parse_mode("123", Modes::Lines), + Ok((Modes::Lines(123), false)) + ); + assert_eq!( + parse_mode("-456", Modes::Bytes), + Ok((Modes::Bytes(456), true)) + ); + assert!(parse_mode("Nonsensical Nonsense", Modes::Bytes).is_err()); + #[cfg(target_pointer_width = "64")] + assert!(parse_mode("1Y", Modes::Lines).is_err()); + #[cfg(target_pointer_width = "32")] + assert!(parse_mode("1T", Modes::Bytes).is_err()); + } + fn arg_outputs(src: &str) -> Result { + let split = src.split_whitespace().map(|x| OsString::from(x)); + match arg_iterate(split) { + Ok(args) => { + let vec = args + .map(|s| s.to_str().unwrap().to_owned()) + .collect::>(); + Ok(vec.join(" ")) } + Err(e) => Err(e), } } - true + #[test] + fn test_arg_iterate() { + // test that normal args remain unchanged + assert_eq!( + arg_outputs("head -n -5 -zv"), + Ok("head -n -5 -zv".to_owned()) + ); + // tests that nonsensical args are unchanged + assert_eq!( + arg_outputs("head -to_be_or_not_to_be,..."), + Ok("head -to_be_or_not_to_be,...".to_owned()) + ); + //test that the obsolete syntax is unrolled + assert_eq!( + arg_outputs("head -123qvqvqzc"), + Ok("head -q -z -c 123".to_owned()) + ); + //test that bad obsoletes are an error + assert!(arg_outputs("head -123FooBar").is_err()); + //test overflow + assert!(arg_outputs("head -100000000000000000000000000000000000000000").is_err()); + //test that empty args remain unchanged + assert_eq!(arg_outputs("head"), Ok("head".to_owned())); + } + #[test] + #[cfg(linux)] + fn test_arg_iterate_bad_encoding() { + let invalid = unsafe { std::str::from_utf8_unchecked(b"\x80\x81") }; + // this arises from a conversion from OsString to &str + assert!( + arg_iterate(vec![OsString::from("head"), OsString::from(invalid)].into_iter()).is_err() + ); + } + #[test] + fn rbuf_early_exit() { + let mut empty = std::io::BufReader::new(std::io::Cursor::new(Vec::new())); + assert!(rbuf_n_bytes(&mut empty, 0).is_ok()); + assert!(rbuf_n_lines(&mut empty, 0, false).is_ok()); + } } diff --git a/src/uu/head/src/parse.rs b/src/uu/head/src/parse.rs new file mode 100644 index 000000000..470d821e0 --- /dev/null +++ b/src/uu/head/src/parse.rs @@ -0,0 +1,282 @@ +use std::convert::TryFrom; +use std::ffi::OsString; + +#[derive(PartialEq, Debug)] +pub enum ParseError { + Syntax, + Overflow, +} +/// Parses obsolete syntax +/// head -NUM[kmzv] +pub fn parse_obsolete(src: &str) -> Option, ParseError>> { + let mut chars = src.char_indices(); + if let Some((_, '-')) = chars.next() { + let mut num_end = 0usize; + let mut has_num = false; + let mut last_char = 0 as char; + while let Some((n, c)) = chars.next() { + if c.is_numeric() { + has_num = true; + num_end = n; + } else { + last_char = c; + break; + } + } + if has_num { + match src[1..=num_end].parse::() { + Ok(num) => { + let mut quiet = false; + let mut verbose = false; + let mut zero_terminated = false; + let mut multiplier = None; + let mut c = last_char; + loop { + // not that here, we only match lower case 'k', 'c', and 'm' + match c { + // we want to preserve order + // this also saves us 1 heap allocation + 'q' => { + quiet = true; + verbose = false + } + 'v' => { + verbose = true; + quiet = false + } + 'z' => zero_terminated = true, + 'c' => multiplier = Some(1), + 'b' => multiplier = Some(512), + 'k' => multiplier = Some(1024), + 'm' => multiplier = Some(1024 * 1024), + '\0' => {} + _ => return Some(Err(ParseError::Syntax)), + } + if let Some((_, next)) = chars.next() { + c = next + } else { + break; + } + } + let mut options = Vec::new(); + if quiet { + options.push(OsString::from("-q")) + } + if verbose { + options.push(OsString::from("-v")) + } + if zero_terminated { + options.push(OsString::from("-z")) + } + if let Some(n) = multiplier { + options.push(OsString::from("-c")); + let num = match num.checked_mul(n) { + Some(n) => n, + None => return Some(Err(ParseError::Overflow)), + }; + options.push(OsString::from(format!("{}", num))); + } else { + options.push(OsString::from("-n")); + options.push(OsString::from(format!("{}", num))); + } + Some(Ok(options.into_iter())) + } + Err(_) => Some(Err(ParseError::Overflow)), + } + } else { + None + } + } else { + None + } +} +/// Parses an -c or -n argument, +/// the bool specifies whether to read from the end +pub fn parse_num(src: &str) -> Result<(usize, bool), ParseError> { + let mut num_start = 0; + let mut chars = src.char_indices(); + let (mut chars, all_but_last) = match chars.next() { + Some((_, c)) => { + if c == '-' { + num_start += 1; + (chars, true) + } else { + (src.char_indices(), false) + } + } + None => return Err(ParseError::Syntax), + }; + let mut num_end = 0usize; + let mut last_char = 0 as char; + let mut num_count = 0usize; + while let Some((n, c)) = chars.next() { + if c.is_numeric() { + num_end = n; + num_count += 1; + } else { + last_char = c; + break; + } + } + + let num = if num_count > 0 { + match src[num_start..=num_end].parse::() { + Ok(n) => Some(n), + Err(_) => return Err(ParseError::Overflow), + } + } else { + None + }; + + if last_char == 0 as char { + if let Some(n) = num { + Ok((n, all_but_last)) + } else { + Err(ParseError::Syntax) + } + } else { + let base: u128 = match chars.next() { + Some((_, c)) => { + let b = match c { + 'B' if last_char != 'b' => 1000, + 'i' if last_char != 'b' => { + if let Some((_, 'B')) = chars.next() { + 1024 + } else { + return Err(ParseError::Syntax); + } + } + _ => return Err(ParseError::Syntax), + }; + if chars.next().is_some() { + return Err(ParseError::Syntax); + } else { + b + } + } + None => 1024, + }; + let mul = match last_char.to_lowercase().next().unwrap() { + 'b' => 512, + 'k' => base.pow(1), + 'm' => base.pow(2), + 'g' => base.pow(3), + 't' => base.pow(4), + 'p' => base.pow(5), + 'e' => base.pow(6), + 'z' => base.pow(7), + 'y' => base.pow(8), + _ => return Err(ParseError::Syntax), + }; + let mul = match usize::try_from(mul) { + Ok(n) => n, + Err(_) => return Err(ParseError::Overflow), + }; + match num.unwrap_or(1).checked_mul(mul) { + Some(n) => Ok((n, all_but_last)), + None => Err(ParseError::Overflow), + } + } +} +#[cfg(test)] +mod tests { + use super::*; + fn obsolete(src: &str) -> Option, ParseError>> { + let r = parse_obsolete(src); + match r { + Some(s) => match s { + Ok(v) => Some(Ok(v.map(|s| s.to_str().unwrap().to_owned()).collect())), + Err(e) => Some(Err(e)), + }, + None => None, + } + } + fn obsolete_result(src: &[&str]) -> Option, ParseError>> { + Some(Ok(src.iter().map(|s| s.to_string()).collect())) + } + #[test] + #[cfg(not(target_pointer_width = "128"))] + fn test_parse_overflow_x64() { + assert_eq!(parse_num("1Y"), Err(ParseError::Overflow)); + assert_eq!(parse_num("1Z"), Err(ParseError::Overflow)); + assert_eq!(parse_num("100E"), Err(ParseError::Overflow)); + assert_eq!(parse_num("100000P"), Err(ParseError::Overflow)); + assert_eq!(parse_num("1000000000T"), Err(ParseError::Overflow)); + assert_eq!( + parse_num("10000000000000000000000"), + Err(ParseError::Overflow) + ); + } + #[test] + #[cfg(target_pointer_width = "32")] + fn test_parse_overflow_x32() { + assert_eq!(parse_num("1T"), Err(ParseError::Overflow)); + assert_eq!(parse_num("1000G"), Err(ParseError::Overflow)); + } + #[test] + fn test_parse_bad_syntax() { + assert_eq!(parse_num("5MiB nonsense"), Err(ParseError::Syntax)); + assert_eq!(parse_num("Nonsense string"), Err(ParseError::Syntax)); + assert_eq!(parse_num("5mib"), Err(ParseError::Syntax)); + assert_eq!(parse_num("biB"), Err(ParseError::Syntax)); + assert_eq!(parse_num("-"), Err(ParseError::Syntax)); + assert_eq!(parse_num(""), Err(ParseError::Syntax)); + } + #[test] + fn test_parse_numbers() { + assert_eq!(parse_num("k"), Ok((1024, false))); + assert_eq!(parse_num("MiB"), Ok((1024 * 1024, false))); + assert_eq!(parse_num("-5"), Ok((5, true))); + assert_eq!(parse_num("b"), Ok((512, false))); + assert_eq!(parse_num("-2GiB"), Ok((2 * 1024 * 1024 * 1024, true))); + assert_eq!(parse_num("5M"), Ok((5 * 1024 * 1024, false))); + assert_eq!(parse_num("5MB"), Ok((5 * 1000 * 1000, false))); + } + #[test] + fn test_parse_numbers_obsolete() { + assert_eq!(obsolete("-5"), obsolete_result(&["-n", "5"])); + assert_eq!(obsolete("-100"), obsolete_result(&["-n", "100"])); + assert_eq!(obsolete("-5m"), obsolete_result(&["-c", "5242880"])); + assert_eq!(obsolete("-1k"), obsolete_result(&["-c", "1024"])); + assert_eq!(obsolete("-2b"), obsolete_result(&["-c", "1024"])); + assert_eq!(obsolete("-1mmk"), obsolete_result(&["-c", "1024"])); + assert_eq!(obsolete("-1vz"), obsolete_result(&["-v", "-z", "-n", "1"])); + assert_eq!( + obsolete("-1vzqvq"), + obsolete_result(&["-q", "-z", "-n", "1"]) + ); + assert_eq!(obsolete("-1vzc"), obsolete_result(&["-v", "-z", "-c", "1"])); + assert_eq!( + obsolete("-105kzm"), + obsolete_result(&["-z", "-c", "110100480"]) + ); + } + #[test] + fn test_parse_errors_obsolete() { + assert_eq!(obsolete("-5n"), Some(Err(ParseError::Syntax))); + assert_eq!(obsolete("-5c5"), Some(Err(ParseError::Syntax))); + } + #[test] + fn test_parse_obsolete_nomatch() { + assert_eq!(obsolete("-k"), None); + assert_eq!(obsolete("asd"), None); + } + #[test] + #[cfg(target_pointer_width = "64")] + fn test_parse_obsolete_overflow_x64() { + assert_eq!( + obsolete("-1000000000000000m"), + Some(Err(ParseError::Overflow)) + ); + assert_eq!( + obsolete("-10000000000000000000000"), + Some(Err(ParseError::Overflow)) + ); + } + #[test] + #[cfg(target_pointer_width = "32")] + fn test_parse_obsolete_overflow_x32() { + assert_eq!(obsolete("-42949672960"), Some(Err(ParseError::Overflow))); + assert_eq!(obsolete("-42949672k"), Some(Err(ParseError::Overflow))); + } +} diff --git a/src/uu/head/src/split.rs b/src/uu/head/src/split.rs new file mode 100644 index 000000000..9e9a0c685 --- /dev/null +++ b/src/uu/head/src/split.rs @@ -0,0 +1,60 @@ +#[derive(Debug)] +pub enum Event<'a> { + Data(&'a [u8]), + Line, +} +/// Loops over the lines read from a BufRead. +/// # Arguments +/// * `input` the ReadBuf to read from +/// * `zero` whether to use 0u8 as a line delimiter +/// * `on_event` a closure receiving some bytes read in a slice, or +/// event signalling a line was just read. +/// this is guaranteed to be signalled *directly* after the +/// slice containing the (CR on win)LF / 0 is passed +/// +/// Return whether to continue +pub fn walk_lines( + input: &mut impl std::io::BufRead, + zero: bool, + mut on_event: F, +) -> std::io::Result<()> +where + F: FnMut(Event) -> std::io::Result, +{ + let mut buffer = [0u8; super::BUF_SIZE]; + loop { + let read = loop { + match input.read(&mut buffer) { + Ok(n) => break n, + Err(e) => match e.kind() { + std::io::ErrorKind::Interrupted => {} + _ => return Err(e), + }, + } + }; + if read == 0 { + return Ok(()); + } + let mut base = 0usize; + for (i, byte) in buffer[..read].iter().enumerate() { + match byte { + b'\n' if !zero => { + on_event(Event::Data(&buffer[base..=i]))?; + base = i + 1; + if !on_event(Event::Line)? { + return Ok(()); + } + } + 0u8 if zero => { + on_event(Event::Data(&buffer[base..=i]))?; + base = i + 1; + if !on_event(Event::Line)? { + return Ok(()); + } + } + _ => {} + } + } + on_event(Event::Data(&buffer[base..read]))?; + } +} diff --git a/src/uu/hostid/Cargo.toml b/src/uu/hostid/Cargo.toml index db833ace7..ab6954104 100644 --- a/src/uu/hostid/Cargo.toml +++ b/src/uu/hostid/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_hostid" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "hostid ~ (uutils) display the numeric identifier of the current host" @@ -16,8 +16,8 @@ path = "src/hostid.rs" [dependencies] libc = "0.2.42" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "hostid" diff --git a/src/uu/hostid/src/hostid.rs b/src/uu/hostid/src/hostid.rs index 162335b3a..5a4909c62 100644 --- a/src/uu/hostid/src/hostid.rs +++ b/src/uu/hostid/src/hostid.rs @@ -7,8 +7,6 @@ // spell-checker:ignore (ToDO) gethostid -extern crate libc; - #[macro_use] extern crate uucore; diff --git a/src/uu/hostname/Cargo.toml b/src/uu/hostname/Cargo.toml index c0b724acd..fb1d00682 100644 --- a/src/uu/hostname/Cargo.toml +++ b/src/uu/hostname/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_hostname" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "hostname ~ (uutils) display or set the host name of the current host" @@ -15,11 +15,11 @@ edition = "2018" path = "src/hostname.rs" [dependencies] -clap = "2.32" +clap = "2.33" libc = "0.2.42" hostname = { version = "0.3", features = ["set"] } -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary", features=["wide"] } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["wide"] } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } winapi = { version="0.3", features=["sysinfoapi", "winsock2"] } [[bin]] diff --git a/src/uu/hostname/src/hostname.rs b/src/uu/hostname/src/hostname.rs index 2e742da9a..d6f70be16 100644 --- a/src/uu/hostname/src/hostname.rs +++ b/src/uu/hostname/src/hostname.rs @@ -7,12 +7,6 @@ // spell-checker:ignore (ToDO) MAKEWORD addrs hashset -extern crate clap; -extern crate hostname; -extern crate libc; -#[cfg(windows)] -extern crate winapi; - #[macro_use] extern crate uucore; @@ -84,7 +78,7 @@ fn execute(args: impl uucore::Args) -> i32 { ) .arg(Arg::with_name(OPT_SHORT).short("s").long("short").help( "Display the short hostname (the portion before the first dot) if \ - possible", + possible", )) .arg(Arg::with_name(OPT_HOST)) .get_matches_from(args); @@ -123,7 +117,7 @@ fn display_hostname(matches: &ArgMatches) -> i32 { ip.truncate(len - 2); } output.push_str(&ip); - output.push_str(" "); + output.push(' '); hashset.insert(addr); } } diff --git a/src/uu/id/Cargo.toml b/src/uu/id/Cargo.toml index f82a76c44..308d6089d 100644 --- a/src/uu/id/Cargo.toml +++ b/src/uu/id/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_id" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "id ~ (uutils) display user and group information for USER" @@ -15,8 +15,9 @@ edition = "2018" path = "src/id.rs" [dependencies] -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary", features=["entries", "process"] } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +clap = "2.33" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["entries", "process"] } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "id" diff --git a/src/uu/id/src/id.rs b/src/uu/id/src/id.rs index 252f0b395..4536622c7 100644 --- a/src/uu/id/src/id.rs +++ b/src/uu/id/src/id.rs @@ -17,6 +17,8 @@ #[macro_use] extern crate uucore; + +use clap::{App, Arg}; use std::ffi::CStr; use uucore::entries::{self, Group, Locate, Passwd}; pub use uucore::libc; @@ -68,50 +70,100 @@ mod audit { } } -static SYNTAX: &str = "[OPTION]... [USER]"; -static SUMMARY: &str = "Print user and group information for the specified USER,\n or (when USER omitted) for the current user."; +static ABOUT: &str = "Display user and group information for the specified USER,\n or (when USER omitted) for the current user."; +static VERSION: &str = env!("CARGO_PKG_VERSION"); + +static OPT_AUDIT: &str = "audit"; +static OPT_EFFECTIVE_USER: &str = "effective-user"; +static OPT_GROUP: &str = "group"; +static OPT_GROUPS: &str = "groups"; +static OPT_HUMAN_READABLE: &str = "human-readable"; +static OPT_NAME: &str = "name"; +static OPT_PASSWORD: &str = "password"; +static OPT_REAL_ID: &str = "real-id"; + +static ARG_USERS: &str = "users"; + +fn get_usage() -> String { + format!("{0} [OPTION]... [USER]", executable!()) +} pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); + let usage = get_usage(); - let mut opts = app!(SYNTAX, SUMMARY, ""); - opts.optflag( - "A", - "", - "Display the process audit (not available on Linux)", - ); - opts.optflag("G", "groups", "Display the different group IDs"); - opts.optflag("g", "group", "Display the effective group ID as a number"); - opts.optflag( - "n", - "", - "Display the name of the user or group ID for the -G, -g and -u options", - ); - opts.optflag("P", "", "Display the id as a password file entry"); - opts.optflag("p", "", "Make the output human-readable"); - opts.optflag("r", "", "Display the real ID for the -g and -u options"); - opts.optflag("u", "user", "Display the effective user ID as a number"); + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(&usage[..]) + .arg( + Arg::with_name(OPT_AUDIT) + .short("A") + .help("Display the process audit (not available on Linux)"), + ) + .arg( + Arg::with_name(OPT_EFFECTIVE_USER) + .short("u") + .long("user") + .help("Display the effective user ID as a number"), + ) + .arg( + Arg::with_name(OPT_GROUP) + .short("g") + .long(OPT_GROUP) + .help("Display the effective group ID as a number"), + ) + .arg( + Arg::with_name(OPT_GROUPS) + .short("G") + .long(OPT_GROUPS) + .help("Display the different group IDs"), + ) + .arg( + Arg::with_name(OPT_HUMAN_READABLE) + .short("p") + .help("Make the output human-readable"), + ) + .arg( + Arg::with_name(OPT_NAME) + .short("n") + .help("Display the name of the user or group ID for the -G, -g and -u options"), + ) + .arg( + Arg::with_name(OPT_PASSWORD) + .short("P") + .help("Display the id as a password file entry"), + ) + .arg( + Arg::with_name(OPT_REAL_ID) + .short("r") + .help("Display the real ID for the -g and -u options"), + ) + .arg(Arg::with_name(ARG_USERS).multiple(true).takes_value(true)) + .get_matches_from(args); - let matches = opts.parse(args); + let users: Vec = matches + .values_of(ARG_USERS) + .map(|v| v.map(ToString::to_string).collect()) + .unwrap_or_default(); - if matches.opt_present("A") { + if matches.is_present(OPT_AUDIT) { auditid(); return 0; } - let possible_pw = if matches.free.is_empty() { + let possible_pw = if users.is_empty() { None } else { - match Passwd::locate(matches.free[0].as_str()) { + match Passwd::locate(users[0].as_str()) { Ok(p) => Some(p), - Err(_) => crash!(1, "No such user/group: {}", matches.free[0]), + Err(_) => crash!(1, "No such user/group: {}", users[0]), } }; - let nflag = matches.opt_present("n"); - let uflag = matches.opt_present("u"); - let gflag = matches.opt_present("g"); - let rflag = matches.opt_present("r"); + let nflag = matches.is_present(OPT_NAME); + let uflag = matches.is_present(OPT_EFFECTIVE_USER); + let gflag = matches.is_present(OPT_GROUP); + let rflag = matches.is_present(OPT_REAL_ID); if gflag { let id = possible_pw @@ -143,7 +195,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { return 0; } - if matches.opt_present("G") { + if matches.is_present(OPT_GROUPS) { println!( "{}", if nflag { @@ -167,12 +219,12 @@ pub fn uumain(args: impl uucore::Args) -> i32 { return 0; } - if matches.opt_present("P") { + if matches.is_present(OPT_PASSWORD) { pline(possible_pw.map(|v| v.uid())); return 0; }; - if matches.opt_present("p") { + if matches.is_present(OPT_HUMAN_READABLE) { pretty(possible_pw); return 0; } @@ -239,7 +291,7 @@ fn pretty(possible_pw: Option) { } } -#[cfg(any(target_os = "macos", target_os = "freebsd"))] +#[cfg(any(target_vendor = "apple", target_os = "freebsd"))] fn pline(possible_uid: Option) { let uid = possible_uid.unwrap_or_else(getuid); let pw = Passwd::locate(uid).unwrap(); diff --git a/src/uu/install/Cargo.toml b/src/uu/install/Cargo.toml index 3e00a9bd8..91463199a 100644 --- a/src/uu/install/Cargo.toml +++ b/src/uu/install/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_install" -version = "0.0.1" +version = "0.0.6" authors = [ "Ben Eills ", "uutils developers", @@ -18,10 +18,12 @@ edition = "2018" path = "src/install.rs" [dependencies] -getopts = "0.2.18" +clap = "2.33" +filetime = "0.2" +file_diff = "1.0.0" libc = ">= 0.2" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary", features=["mode"] } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["mode", "perms", "entries"] } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [dev-dependencies] time = "0.1.40" diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index 4970689fb..a4f1ca6e6 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -7,31 +7,36 @@ // spell-checker:ignore (ToDO) rwxr sourcepath targetpath -extern crate getopts; -extern crate libc; - mod mode; #[macro_use] extern crate uucore; +use clap::{App, Arg, ArgMatches}; +use file_diff::diff; +use filetime::{set_file_times, FileTime}; +use uucore::entries::{grp2gid, usr2uid}; +use uucore::perms::{wrap_chgrp, wrap_chown, Verbosity}; + +use libc::{getegid, geteuid}; use std::fs; +use std::fs::File; +use std::os::unix::fs::MetadataExt; use std::path::{Path, PathBuf}; use std::result::Result; -static NAME: &str = "install"; -static SUMMARY: &str = "Copy SOURCE to DEST or multiple SOURCE(s) to the existing - DIRECTORY, while setting permission modes and owner/group"; -static LONG_HELP: &str = ""; - -const DEFAULT_MODE: u32 = 755; +const DEFAULT_MODE: u32 = 0o755; #[allow(dead_code)] pub struct Behavior { main_function: MainFunction, specified_mode: Option, suffix: String, + owner: String, + group: String, verbose: bool, + preserve_timestamps: bool, + compare: bool, } #[derive(Clone, Eq, PartialEq)] @@ -52,14 +57,180 @@ impl Behavior { } } +static ABOUT: &str = "Copy SOURCE to DEST or multiple SOURCE(s) to the existing + DIRECTORY, while setting permission modes and owner/group"; +static VERSION: &str = env!("CARGO_PKG_VERSION"); + +static OPT_COMPARE: &str = "compare"; +static OPT_BACKUP: &str = "backup"; +static OPT_BACKUP_2: &str = "backup2"; +static OPT_DIRECTORY: &str = "directory"; +static OPT_IGNORED: &str = "ignored"; +static OPT_CREATED: &str = "created"; +static OPT_GROUP: &str = "group"; +static OPT_MODE: &str = "mode"; +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"; +static OPT_PRESERVE_CONTEXT: &str = "preserve-context"; +static OPT_CONTEXT: &str = "context"; + +static ARG_FILES: &str = "files"; + +fn get_usage() -> String { + format!("{0} [OPTION]... [FILE]...", executable!()) +} + /// Main install utility function, called from main.rs. /// /// Returns a program return code. /// pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); + let usage = get_usage(); - let matches = parse_opts(args); + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(&usage[..]) + .arg( + Arg::with_name(OPT_BACKUP) + .long(OPT_BACKUP) + .help("(unimplemented) make a backup of each existing destination file") + .value_name("CONTROL") + ) + .arg( + // TODO implement flag + Arg::with_name(OPT_BACKUP_2) + .short("b") + .help("(unimplemented) like --backup but does not accept an argument") + ) + .arg( + Arg::with_name(OPT_IGNORED) + .short("c") + .help("ignored") + ) + .arg( + Arg::with_name(OPT_COMPARE) + .short("C") + .long(OPT_COMPARE) + .help("compare each pair of source and destination files, and in some cases, do not modify the destination at all") + ) + .arg( + Arg::with_name(OPT_DIRECTORY) + .short("d") + .long(OPT_DIRECTORY) + .help("treat all arguments as directory names. create all components of the specified directories") + ) + + .arg( + // TODO implement flag + Arg::with_name(OPT_CREATED) + .short("D") + .help("(unimplemented) create all leading components of DEST except the last, then copy SOURCE to DEST") + ) + .arg( + Arg::with_name(OPT_GROUP) + .short("g") + .long(OPT_GROUP) + .help("set group ownership, instead of process's current group") + .value_name("GROUP") + .takes_value(true) + ) + .arg( + Arg::with_name(OPT_MODE) + .short("m") + .long(OPT_MODE) + .help("set permission mode (as in chmod), instead of rwxr-xr-x") + .value_name("MODE") + .takes_value(true) + ) + .arg( + Arg::with_name(OPT_OWNER) + .short("o") + .long(OPT_OWNER) + .help("set ownership (super-user only)") + .value_name("OWNER") + .takes_value(true) + ) + .arg( + Arg::with_name(OPT_PRESERVE_TIMESTAMPS) + .short("p") + .long(OPT_PRESERVE_TIMESTAMPS) + .help("apply access/modification times of SOURCE files to corresponding destination files") + ) + .arg( + // TODO implement flag + Arg::with_name(OPT_STRIP) + .short("s") + .long(OPT_STRIP) + .help("(unimplemented) strip symbol tables") + ) + .arg( + // TODO implement flag + Arg::with_name(OPT_STRIP_PROGRAM) + .long(OPT_STRIP_PROGRAM) + .help("(unimplemented) program used to strip binaries") + .value_name("PROGRAM") + ) + .arg( + // TODO implement flag + Arg::with_name(OPT_SUFFIX) + .short("S") + .long(OPT_SUFFIX) + .help("(unimplemented) override the usual backup suffix") + .value_name("SUFFIX") + .takes_value(true) + .min_values(1) + ) + .arg( + // TODO implement flag + Arg::with_name(OPT_TARGET_DIRECTORY) + .short("t") + .long(OPT_TARGET_DIRECTORY) + .help("(unimplemented) move all SOURCE arguments into DIRECTORY") + .value_name("DIRECTORY") + ) + .arg( + // TODO implement flag + Arg::with_name(OPT_NO_TARGET_DIRECTORY) + .short("T") + .long(OPT_NO_TARGET_DIRECTORY) + .help("(unimplemented) treat DEST as a normal file") + + ) + .arg( + Arg::with_name(OPT_VERBOSE) + .short("v") + .long(OPT_VERBOSE) + .help("explain what is being done") + ) + .arg( + // TODO implement flag + Arg::with_name(OPT_PRESERVE_CONTEXT) + .short("P") + .long(OPT_PRESERVE_CONTEXT) + .help("(unimplemented) preserve security context") + ) + .arg( + // TODO implement flag + Arg::with_name(OPT_CONTEXT) + .short("Z") + .long(OPT_CONTEXT) + .help("(unimplemented) set security context of files and directories") + .value_name("CONTEXT") + ) + .arg(Arg::with_name(ARG_FILES).multiple(true).takes_value(true).min_values(1)) + .get_matches_from(args); + + let paths: Vec = matches + .values_of(ARG_FILES) + .map(|v| v.map(ToString::to_string).collect()) + .unwrap_or_default(); if let Err(s) = check_unimplemented(&matches) { show_error!("Unimplemented feature: {}", s); @@ -73,146 +244,12 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } }; - let paths: Vec = { - #[allow(clippy::ptr_arg)] - fn string_to_path(s: &String) -> &Path { - Path::new(s) - }; - let to_owned = |p: &Path| p.to_owned(); - let arguments = matches.free.iter().map(string_to_path); - - arguments.map(to_owned).collect() - }; - match behavior.main_function { - MainFunction::Directory => directory(&paths[..], behavior), - MainFunction::Standard => standard(&paths[..], behavior), + MainFunction::Directory => directory(paths, behavior), + MainFunction::Standard => standard(paths, behavior), } } -/// Build a specification of the command line. -/// -/// Returns a getopts::Options struct. -/// -fn parse_opts(args: Vec) -> getopts::Matches { - let syntax = format!( - "SOURCE DEST - {} SOURCE... DIRECTORY", - NAME - ); - app!(&syntax, SUMMARY, LONG_HELP) - // TODO implement flag - .optflagopt( - "", - "backup", - "(unimplemented) make a backup of each existing destination\n \ - file", - "CONTROL", - ) - // TODO implement flag - .optflag( - "b", - "", - "(unimplemented) like --backup but does not accept an argument", - ) - .optflag("c", "", "ignored") - // TODO implement flag - .optflag( - "C", - "compare", - "(unimplemented) compare each pair of source and destination\n \ - files, and in some cases, do not modify the destination at all", - ) - .optflag( - "d", - "directory", - "treat all arguments as directory names.\n \ - create all components of the specified directories", - ) - // TODO implement flag - .optflag( - "D", - "", - "(unimplemented) create all leading components of DEST except the\n \ - last, then copy SOURCE to DEST", - ) - // TODO implement flag - .optflagopt( - "g", - "group", - "(unimplemented) set group ownership, instead of process's\n \ - current group", - "GROUP", - ) - .optflagopt( - "m", - "mode", - "set permission mode (as in chmod), instead\n \ - of rwxr-xr-x", - "MODE", - ) - // TODO implement flag - .optflagopt( - "o", - "owner", - "(unimplemented) set ownership (super-user only)", - "OWNER", - ) - // TODO implement flag - .optflag( - "p", - "preserve-timestamps", - "(unimplemented) apply access/modification times\n \ - of SOURCE files to corresponding destination files", - ) - // TODO implement flag - .optflag("s", "strip", "(unimplemented) strip symbol tables") - // TODO implement flag - .optflagopt( - "", - "strip-program", - "(unimplemented) program used to strip binaries", - "PROGRAM", - ) - // TODO implement flag - .optopt( - "S", - "suffix", - "(unimplemented) override the usual backup suffix", - "SUFFIX", - ) - // TODO implement flag - .optopt( - "t", - "target-directory", - "(unimplemented) move all SOURCE arguments into\n \ - DIRECTORY", - "DIRECTORY", - ) - // TODO implement flag - .optflag( - "T", - "no-target-directory", - "(unimplemented) treat DEST as a normal file", - ) - .optflag("v", "verbose", "explain what is being done") - // TODO implement flag - .optflag( - "P", - "preserve-context", - "(unimplemented) preserve security context", - ) - // TODO implement flag - .optflagopt( - "Z", - "context", - "(unimplemented) set security context of files and\n \ - directories", - "CONTEXT", - ) - .parse(args) -} - /// Check for unimplemented command line arguments. /// /// Either return the degenerate Ok value, or an Err with string. @@ -221,34 +258,27 @@ fn parse_opts(args: Vec) -> getopts::Matches { /// /// Error datum is a string of the unimplemented argument. /// -fn check_unimplemented(matches: &getopts::Matches) -> Result<(), &str> { - if matches.opt_present("backup") { +/// +fn check_unimplemented<'a>(matches: &ArgMatches) -> Result<(), &'a str> { + if matches.is_present(OPT_BACKUP) { Err("--backup") - } else if matches.opt_present("b") { + } else if matches.is_present(OPT_BACKUP_2) { Err("-b") - } else if matches.opt_present("compare") { - Err("--compare, -C") - } else if matches.opt_present("D") { + } else if matches.is_present(OPT_CREATED) { Err("-D") - } else if matches.opt_present("group") { - Err("--group, -g") - } else if matches.opt_present("owner") { - Err("--owner, -o") - } else if matches.opt_present("preserve-timestamps") { - Err("--preserve-timestamps, -p") - } else if matches.opt_present("strip") { + } else if matches.is_present(OPT_STRIP) { Err("--strip, -s") - } else if matches.opt_present("strip-program") { + } else if matches.is_present(OPT_STRIP_PROGRAM) { Err("--strip-program") - } else if matches.opt_present("suffix") { + } else if matches.is_present(OPT_SUFFIX) { Err("--suffix, -S") - } else if matches.opt_present("target-directory") { + } else if matches.is_present(OPT_TARGET_DIRECTORY) { Err("--target-directory, -t") - } else if matches.opt_present("no-target-directory") { + } else if matches.is_present(OPT_NO_TARGET_DIRECTORY) { Err("--no-target-directory, -T") - } else if matches.opt_present("preserve-context") { + } else if matches.is_present(OPT_PRESERVE_CONTEXT) { Err("--preserve-context, -P") - } else if matches.opt_present("context") { + } else if matches.is_present(OPT_CONTEXT) { Err("--context, -Z") } else { Ok(()) @@ -263,8 +293,8 @@ fn check_unimplemented(matches: &getopts::Matches) -> Result<(), &str> { /// /// In event of failure, returns an integer intended as a program return code. /// -fn behavior(matches: &getopts::Matches) -> Result { - let main_function = if matches.opt_present("directory") { +fn behavior(matches: &ArgMatches) -> Result { + let main_function = if matches.is_present(OPT_DIRECTORY) { MainFunction::Directory } else { MainFunction::Standard @@ -272,8 +302,8 @@ fn behavior(matches: &getopts::Matches) -> Result { let considering_dir: bool = MainFunction::Directory == main_function; - let specified_mode: Option = if matches.opt_present("mode") { - match matches.opt_str("mode") { + let specified_mode: Option = if matches.is_present(OPT_MODE) { + match matches.value_of(OPT_MODE) { Some(x) => match mode::parse(&x[..], considering_dir) { Ok(y) => Some(y), Err(err) => { @@ -282,11 +312,6 @@ fn behavior(matches: &getopts::Matches) -> Result { } }, None => { - show_error!( - "option '--mode' requires an argument\n \ - Try '{} --help' for more information.", - NAME - ); return Err(1); } } @@ -294,27 +319,26 @@ fn behavior(matches: &getopts::Matches) -> Result { None }; - let backup_suffix = if matches.opt_present("suffix") { - match matches.opt_str("suffix") { + let backup_suffix = if matches.is_present(OPT_SUFFIX) { + match matches.value_of(OPT_SUFFIX) { Some(x) => x, None => { - show_error!( - "option '--suffix' requires an argument\n\ - Try '{} --help' for more information.", - NAME - ); return Err(1); } } } else { - "~".to_owned() + "~" }; Ok(Behavior { main_function, specified_mode, - suffix: backup_suffix, - verbose: matches.opt_present("v"), + suffix: backup_suffix.to_string(), + 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), + preserve_timestamps: matches.is_present(OPT_PRESERVE_TIMESTAMPS), + compare: matches.is_present(OPT_COMPARE), }) } @@ -325,32 +349,36 @@ fn behavior(matches: &getopts::Matches) -> Result { /// /// Returns an integer intended as a program return code. /// -fn directory(paths: &[PathBuf], b: Behavior) -> i32 { +fn directory(paths: Vec, b: Behavior) -> i32 { if paths.is_empty() { - println!("{} with -d requires at least one argument.", NAME); + println!("{} with -d requires at least one argument.", executable!()); 1 } else { let mut all_successful = true; - for directory in paths.iter() { - let path = directory.as_path(); + for path in paths.iter().map(Path::new) { + // if the path already exist, don't try to create it again + if !path.exists() { + // Differently than the primary functionality (MainFunction::Standard), the directory + // functionality should create all ancestors (or components) of a directory regardless + // of the presence of the "-D" flag. + // NOTE: the GNU "install" sets the expected mode only for the target directory. All + // created ancestor directories will have 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) { + show_info!("{}: {}", path.display(), e); + all_successful = false; + continue; + } - if path.exists() { - show_info!("cannot create directory '{}': File exists", path.display()); - all_successful = false; - } - - if let Err(e) = fs::create_dir(directory) { - show_info!("{}: {}", path.display(), e.to_string()); - all_successful = false; + if b.verbose { + show_info!("creating directory '{}'", path.display()); + } } if mode::chmod(&path, b.mode()).is_err() { all_successful = false; - } - - if b.verbose { - show_info!("created directory '{}'", path.display()); + continue; } } if all_successful { @@ -361,29 +389,29 @@ fn directory(paths: &[PathBuf], b: Behavior) -> i32 { } } -/// Test if the path is a a new file path that can be +/// Test if the path is a new file path that can be /// created immediately fn is_new_file_path(path: &Path) -> bool { - path.is_file() || !path.exists() && path.parent().map(Path::is_dir).unwrap_or(true) + !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 } /// Perform an install, given a list of paths and behavior. /// /// Returns an integer intended as a program return code. /// -fn standard(paths: &[PathBuf], b: Behavior) -> i32 { - if paths.len() < 2 { - println!("{} requires at least 2 arguments.", NAME); - 1 - } else { - let sources = &paths[0..paths.len() - 1]; - let target = &paths[paths.len() - 1]; +fn standard(paths: Vec, b: Behavior) -> i32 { + let sources = &paths[0..paths.len() - 1] + .iter() + .map(PathBuf::from) + .collect::>(); + let target = Path::new(paths.last().unwrap()); - if (target.is_file() || is_new_file_path(target)) && sources.len() == 1 { - copy_file_to_file(&sources[0], target, &b) - } else { - copy_files_into_dir(sources, target, &b) - } + if (target.is_file() || is_new_file_path(target)) && sources.len() == 1 { + copy_file_to_file(&sources[0], &target.to_path_buf(), &b) + } else { + copy_files_into_dir(sources, &target.to_path_buf(), &b) } } @@ -399,24 +427,31 @@ fn standard(paths: &[PathBuf], b: Behavior) -> i32 { /// fn copy_files_into_dir(files: &[PathBuf], target_dir: &PathBuf, b: &Behavior) -> i32 { if !target_dir.is_dir() { - show_error!("target ‘{}’ is not a directory", target_dir.display()); + show_error!("target '{}' is not a directory", target_dir.display()); return 1; } let mut all_successful = true; for sourcepath in files.iter() { - let targetpath = match sourcepath.as_os_str().to_str() { - Some(name) => target_dir.join(name), - None => { - show_error!( - "cannot stat ‘{}’: No such file or directory", - sourcepath.display() - ); + if !sourcepath.exists() { + show_info!( + "cannot stat '{}': No such file or directory", + sourcepath.display() + ); - all_successful = false; - continue; - } - }; + all_successful = false; + continue; + } + + if sourcepath.is_dir() { + show_info!("omitting directory '{}'", sourcepath.display()); + all_successful = false; + continue; + } + + let mut targetpath = target_dir.clone().to_path_buf(); + let filename = sourcepath.components().last().unwrap(); + targetpath.push(filename); if copy(sourcepath, &targetpath, b).is_err() { all_successful = false; @@ -459,11 +494,26 @@ fn copy_file_to_file(file: &PathBuf, target: &PathBuf, b: &Behavior) -> i32 { /// If the copy system call fails, we print a verbose error and return an empty error value. /// fn copy(from: &PathBuf, to: &PathBuf, b: &Behavior) -> Result<(), ()> { - let io_result = fs::copy(from, to); + if b.compare && !need_copy(from, to, b) { + return Ok(()); + } - if let Err(err) = io_result { + if from.to_string_lossy() == "/dev/null" { + /* workaround a limitation of fs::copy + * https://github.com/rust-lang/rust/issues/79390 + */ + if let Err(err) = File::create(to) { + show_error!( + "install: cannot install '{}' to '{}': {}", + from.display(), + to.display(), + err + ); + return Err(()); + } + } else if let Err(err) = fs::copy(from, to) { show_error!( - "install: cannot install ‘{}’ to ‘{}’: {}", + "cannot install '{}' to '{}': {}", from.display(), to.display(), err @@ -475,9 +525,150 @@ fn copy(from: &PathBuf, to: &PathBuf, b: &Behavior) -> Result<(), ()> { return Err(()); } + if !b.owner.is_empty() { + let meta = match fs::metadata(to) { + Ok(meta) => meta, + Err(f) => crash!(1, "{}", f.to_string()), + }; + + let owner_id = match usr2uid(&b.owner) { + Ok(g) => g, + _ => crash!(1, "no such user: {}", b.owner), + }; + let gid = meta.gid(); + match wrap_chown( + to.as_path(), + &meta, + Some(owner_id), + Some(gid), + false, + Verbosity::Normal, + ) { + Ok(n) => { + if !n.is_empty() { + show_info!("{}", n); + } + } + Err(e) => show_info!("{}", e), + } + } + + if !b.group.is_empty() { + let meta = match fs::metadata(to) { + Ok(meta) => meta, + Err(f) => crash!(1, "{}", f.to_string()), + }; + + let group_id = match grp2gid(&b.group) { + Ok(g) => g, + _ => crash!(1, "no such group: {}", b.group), + }; + match wrap_chgrp(to.as_path(), &meta, group_id, false, Verbosity::Normal) { + Ok(n) => { + if !n.is_empty() { + show_info!("{}", n); + } + } + Err(e) => show_info!("{}", e), + } + } + + if b.preserve_timestamps { + let meta = match fs::metadata(from) { + Ok(meta) => meta, + Err(f) => crash!(1, "{}", f.to_string()), + }; + + let modified_time = FileTime::from_last_modification_time(&meta); + let accessed_time = FileTime::from_last_access_time(&meta); + + match set_file_times(to.as_path(), accessed_time, modified_time) { + Ok(_) => {} + Err(e) => show_info!("{}", e), + } + } + if b.verbose { show_info!("'{}' -> '{}'", from.display(), to.display()); } Ok(()) } + +/// Return true if a file is necessary to copy. This is the case when: +/// - _from_ or _to_ is nonexistent; +/// - either file has a sticky bit or set[ug]id bit, or the user specified one; +/// - either file isn't a regular file; +/// - the sizes of _from_ and _to_ differ; +/// - _to_'s owner differs from intended; or +/// - the contents of _from_ and _to_ differ. +/// +/// # Parameters +/// +/// _from_ and _to_, if existent, must be non-directories. +/// +/// # Errors +/// +/// Crashes the program if a nonexistent owner or group is specified in _b_. +/// +fn need_copy(from: &PathBuf, to: &PathBuf, b: &Behavior) -> bool { + let from_meta = match fs::metadata(from) { + Ok(meta) => meta, + Err(_) => return true, + }; + let to_meta = match fs::metadata(to) { + Ok(meta) => meta, + Err(_) => return true, + }; + + // setuid || setgid || sticky + let extra_mode: u32 = 0o7000; + + if b.specified_mode.unwrap_or(0) & extra_mode != 0 + || from_meta.mode() & extra_mode != 0 + || to_meta.mode() & extra_mode != 0 + { + return true; + } + + if !from_meta.is_file() || !to_meta.is_file() { + return true; + } + + if from_meta.len() != to_meta.len() { + return true; + } + + // TODO: if -P (#1809) and from/to contexts mismatch, return true. + + if !b.owner.is_empty() { + let owner_id = match usr2uid(&b.owner) { + Ok(id) => id, + _ => crash!(1, "no such user: {}", b.owner), + }; + if owner_id != to_meta.uid() { + return true; + } + } else if !b.group.is_empty() { + let group_id = match grp2gid(&b.group) { + Ok(id) => id, + _ => crash!(1, "no such group: {}", b.group), + }; + if group_id != to_meta.gid() { + return true; + } + } else { + #[cfg(not(target_os = "windows"))] + unsafe { + if to_meta.uid() != geteuid() || to_meta.gid() != getegid() { + return true; + } + } + } + + if !diff(from.to_str().unwrap(), to.to_str().unwrap()) { + return true; + } + + false +} diff --git a/src/uu/install/src/mode.rs b/src/uu/install/src/mode.rs index 9c8eb2de0..a3de40c68 100644 --- a/src/uu/install/src/mode.rs +++ b/src/uu/install/src/mode.rs @@ -1,5 +1,3 @@ -extern crate libc; - use std::fs; use std::path::Path; #[cfg(not(windows))] diff --git a/src/uu/join/Cargo.toml b/src/uu/join/Cargo.toml index 156b4429b..9371b7601 100644 --- a/src/uu/join/Cargo.toml +++ b/src/uu/join/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_join" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "join ~ (uutils) merge lines from inputs with matching join fields" @@ -15,9 +15,9 @@ edition = "2018" path = "src/join.rs" [dependencies] -clap = "2.32" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +clap = "2.33" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "join" diff --git a/src/uu/join/src/join.rs b/src/uu/join/src/join.rs index 23feae662..d02a54eb3 100644 --- a/src/uu/join/src/join.rs +++ b/src/uu/join/src/join.rs @@ -7,8 +7,6 @@ // spell-checker:ignore (ToDO) autoformat FILENUM whitespaces pairable unpairable nocheck -extern crate clap; - #[macro_use] extern crate uucore; @@ -696,10 +694,7 @@ fn get_field_number(keys: Option, key: Option) -> usize { return keys; } - match key { - Some(key) => key, - None => 0, - } + key.unwrap_or(0) } /// Parse the specified field string as a natural number and return diff --git a/src/uu/kill/Cargo.toml b/src/uu/kill/Cargo.toml index 1562ffb8a..6b66806bc 100644 --- a/src/uu/kill/Cargo.toml +++ b/src/uu/kill/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_kill" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "kill ~ (uutils) send a signal to a process" @@ -16,8 +16,8 @@ path = "src/kill.rs" [dependencies] libc = "0.2.42" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary", features=["signals"] } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["signals"] } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "kill" diff --git a/src/uu/kill/src/kill.rs b/src/uu/kill/src/kill.rs index d0df6fa3c..9af9c74f7 100644 --- a/src/uu/kill/src/kill.rs +++ b/src/uu/kill/src/kill.rs @@ -7,8 +7,6 @@ // spell-checker:ignore (ToDO) signalname pids -extern crate libc; - #[macro_use] extern crate uucore; diff --git a/src/uu/link/Cargo.toml b/src/uu/link/Cargo.toml index 9b9f08f94..13c3453cf 100644 --- a/src/uu/link/Cargo.toml +++ b/src/uu/link/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_link" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "link ~ (uutils) create a hard (file system) link to FILE" @@ -16,8 +16,8 @@ path = "src/link.rs" [dependencies] libc = "0.2.42" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "link" diff --git a/src/uu/ln/Cargo.toml b/src/uu/ln/Cargo.toml index 2a26ca938..c19d8fb52 100644 --- a/src/uu/ln/Cargo.toml +++ b/src/uu/ln/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_ln" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "ln ~ (uutils) create a (file system) link to TARGET" @@ -15,9 +15,10 @@ edition = "2018" path = "src/ln.rs" [dependencies] +clap = "2.33" libc = "0.2.42" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["fs"] } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "ln" diff --git a/src/uu/ln/src/ln.rs b/src/uu/ln/src/ln.rs index 078b9c2ea..96a0df813 100644 --- a/src/uu/ln/src/ln.rs +++ b/src/uu/ln/src/ln.rs @@ -10,6 +10,8 @@ #[macro_use] extern crate uucore; +use clap::{App, Arg}; + use std::borrow::Cow; use std::ffi::OsStr; use std::fs; @@ -22,27 +24,16 @@ use std::os::windows::fs::{symlink_dir, symlink_file}; use std::path::{Path, PathBuf}; use uucore::fs::{canonicalize, CanonicalizeMode}; -static NAME: &str = "ln"; -static SUMMARY: &str = ""; -static LONG_HELP: &str = " - In the 1st form, create a link to TARGET with the name LINK_NAME. - In the 2nd form, create a link to TARGET in the current directory. - In the 3rd and 4th forms, create links to each TARGET in DIRECTORY. - Create hard links by default, symbolic links with --symbolic. - By default, each destination (name of new link) should not already exist. - When creating hard links, each TARGET must exist. Symbolic links - can hold arbitrary text; if later resolved, a relative link is - interpreted in relation to its parent directory. -"; - pub struct Settings { overwrite: OverwriteMode, backup: BackupMode, + force: bool, suffix: String, symbolic: bool, relative: bool, target_dir: Option, no_target_dir: bool, + no_dereference: bool, verbose: bool, } @@ -61,143 +52,212 @@ pub enum BackupMode { ExistingBackup, } -pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); +fn get_usage() -> String { + format!( + "{0} [OPTION]... [-T] TARGET LINK_executable!() (1st form) + {0} [OPTION]... TARGET (2nd form) + {0} [OPTION]... TARGET... DIRECTORY (3rd form) + {0} [OPTION]... -t DIRECTORY TARGET... (4th form)", + executable!() + ) +} - let syntax = format!( - "[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)", - NAME - ); - let matches = app!(&syntax, SUMMARY, LONG_HELP) - .optflag( - "b", - "", +fn get_long_usage() -> String { + String::from( + " In the 1st form, create a link to TARGET with the name LINK_executable!(). + In the 2nd form, create a link to TARGET in the current directory. + In the 3rd and 4th forms, create links to each TARGET in DIRECTORY. + Create hard links by default, symbolic links with --symbolic. + By default, each destination (name of new link) should not already exist. + When creating hard links, each TARGET must exist. Symbolic links + can hold arbitrary text; if later resolved, a relative link is + interpreted in relation to its parent directory. + ", + ) +} + +static ABOUT: &str = "change file owner and group"; +static VERSION: &str = env!("CARGO_PKG_VERSION"); + +static OPT_B: &str = "b"; +static OPT_BACKUP: &str = "backup"; +static OPT_FORCE: &str = "force"; +static OPT_INTERACTIVE: &str = "interactive"; +static OPT_NO_DEREFERENCE: &str = "no-dereference"; +static OPT_SYMBOLIC: &str = "symbolic"; +static OPT_SUFFIX: &str = "suffix"; +static OPT_TARGET_DIRECTORY: &str = "target-directory"; +static OPT_NO_TARGET_DIRECTORY: &str = "no-target-directory"; +static OPT_RELATIVE: &str = "relative"; +static OPT_VERBOSE: &str = "verbose"; + +static ARG_FILES: &str = "files"; + +pub fn uumain(args: impl uucore::Args) -> i32 { + let usage = get_usage(); + let long_usage = get_long_usage(); + + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(&usage[..]) + .after_help(&long_usage[..]) + .arg(Arg::with_name(OPT_B).short(OPT_B).help( "make a backup of each file that would otherwise be overwritten or \ removed", + )) + .arg( + Arg::with_name(OPT_BACKUP) + .long(OPT_BACKUP) + .help( + "make a backup of each file that would otherwise be overwritten \ + or removed", + ) + .takes_value(true) + .possible_value("simple") + .possible_value("never") + .possible_value("numbered") + .possible_value("t") + .possible_value("existing") + .possible_value("nil") + .possible_value("none") + .possible_value("off") + .value_name("METHOD"), ) - .optflagopt( - "", - "backup", - "make a backup of each file that would otherwise be overwritten \ - or removed", - "METHOD", - ) - // TODO: opts.optflag("d", "directory", "allow users with appropriate privileges to attempt \ + // TODO: opts.arg( + // Arg::with_name(("d", "directory", "allow users with appropriate privileges to attempt \ // to make hard links to directories"); - .optflag("f", "force", "remove existing destination files") - .optflag( - "i", - "interactive", - "prompt whether to remove existing destination files", + .arg( + Arg::with_name(OPT_FORCE) + .short("f") + .long(OPT_FORCE) + .help("remove existing destination files"), ) - // TODO: opts.optflag("L", "logical", "dereference TARGETs that are symbolic links"); - // TODO: opts.optflag("n", "no-dereference", "treat LINK_NAME as a normal file if it is a \ - // symbolic link to a directory"); - // TODO: opts.optflag("P", "physical", "make hard links directly to symbolic links"); - .optflag("s", "symbolic", "make symbolic links instead of hard links") - .optopt("S", "suffix", "override the usual backup suffix", "SUFFIX") - .optopt( - "t", - "target-directory", - "specify the DIRECTORY in which to create the links", - "DIRECTORY", + .arg( + Arg::with_name(OPT_INTERACTIVE) + .short("i") + .long(OPT_INTERACTIVE) + .help("prompt whether to remove existing destination files"), ) - .optflag( - "T", - "no-target-directory", - "treat LINK_NAME as a normal file always", + .arg( + Arg::with_name(OPT_NO_DEREFERENCE) + .short("n") + .long(OPT_NO_DEREFERENCE) + .help( + "treat LINK_executable!() as a normal file if it is a \ + symbolic link to a directory", + ), ) - .optflag( - "r", - "relative", - "create symbolic links relative to link location", + // TODO: opts.arg( + // Arg::with_name(("L", "logical", "dereference TARGETs that are symbolic links"); + // + // TODO: opts.arg( + // Arg::with_name(("P", "physical", "make hard links directly to symbolic links"); + .arg( + Arg::with_name(OPT_SYMBOLIC) + .short("s") + .long("symbolic") + .help("make symbolic links instead of hard links"), ) - .optflag("v", "verbose", "print name of each linked file") - .parse(args); + .arg( + Arg::with_name(OPT_SUFFIX) + .short("S") + .long(OPT_SUFFIX) + .help("override the usual backup suffix") + .value_name("SUFFIX") + .takes_value(true), + ) + .arg( + Arg::with_name(OPT_TARGET_DIRECTORY) + .short("t") + .long(OPT_TARGET_DIRECTORY) + .help("specify the DIRECTORY in which to create the links") + .value_name("DIRECTORY") + .conflicts_with(OPT_NO_TARGET_DIRECTORY), + ) + .arg( + Arg::with_name(OPT_NO_TARGET_DIRECTORY) + .short("T") + .long(OPT_NO_TARGET_DIRECTORY) + .help("treat LINK_executable!() as a normal file always"), + ) + .arg( + Arg::with_name(OPT_RELATIVE) + .short("r") + .long(OPT_RELATIVE) + .help("create symbolic links relative to link location"), + ) + .arg( + Arg::with_name(OPT_VERBOSE) + .short("v") + .long(OPT_VERBOSE) + .help("print name of each linked file"), + ) + .arg( + Arg::with_name(ARG_FILES) + .multiple(true) + .takes_value(true) + .required(true) + .min_values(1), + ) + .get_matches_from(args); - let overwrite_mode = if matches.opt_present("force") { + /* the list of files */ + + let paths: Vec = matches + .values_of(ARG_FILES) + .unwrap() + .map(PathBuf::from) + .collect(); + + let overwrite_mode = if matches.is_present(OPT_FORCE) { OverwriteMode::Force - } else if matches.opt_present("interactive") { + } else if matches.is_present(OPT_INTERACTIVE) { OverwriteMode::Interactive } else { OverwriteMode::NoClobber }; - let backup_mode = if matches.opt_present("b") { + let backup_mode = if matches.is_present(OPT_B) { BackupMode::ExistingBackup - } else if matches.opt_present("backup") { - match matches.opt_str("backup") { + } else if matches.is_present(OPT_BACKUP) { + match matches.value_of(OPT_BACKUP) { None => BackupMode::ExistingBackup, - Some(mode) => match &mode[..] { + Some(mode) => match mode { "simple" | "never" => BackupMode::SimpleBackup, "numbered" | "t" => BackupMode::NumberedBackup, "existing" | "nil" => BackupMode::ExistingBackup, "none" | "off" => BackupMode::NoBackup, - x => { - show_error!( - "invalid argument '{}' for 'backup method'\n\ - Try '{} --help' for more information.", - x, - NAME - ); - return 1; - } + _ => panic!(), // cannot happen as it is managed by clap }, } } else { BackupMode::NoBackup }; - let backup_suffix = if matches.opt_present("suffix") { - match matches.opt_str("suffix") { - Some(x) => x, - None => { - show_error!( - "option '--suffix' requires an argument\n\ - Try '{} --help' for more information.", - NAME - ); - return 1; - } - } + let backup_suffix = if matches.is_present(OPT_SUFFIX) { + matches.value_of(OPT_SUFFIX).unwrap() } else { - "~".to_owned() + "~" }; - if matches.opt_present("T") && matches.opt_present("t") { - show_error!("cannot combine --target-directory (-t) and --no-target-directory (-T)"); - return 1; - } - let settings = Settings { overwrite: overwrite_mode, backup: backup_mode, - suffix: backup_suffix, - symbolic: matches.opt_present("s"), - relative: matches.opt_present("r"), - target_dir: matches.opt_str("t"), - no_target_dir: matches.opt_present("T"), - verbose: matches.opt_present("v"), + force: matches.is_present(OPT_FORCE), + suffix: backup_suffix.to_string(), + symbolic: matches.is_present(OPT_SYMBOLIC), + relative: matches.is_present(OPT_RELATIVE), + target_dir: matches.value_of(OPT_TARGET_DIRECTORY).map(String::from), + no_target_dir: matches.is_present(OPT_NO_TARGET_DIRECTORY), + no_dereference: matches.is_present(OPT_NO_DEREFERENCE), + verbose: matches.is_present(OPT_VERBOSE), }; - let string_to_path = |s: &String| PathBuf::from(s); - let paths: Vec = matches.free.iter().map(string_to_path).collect(); - exec(&paths[..], &settings) } fn exec(files: &[PathBuf], settings: &Settings) -> i32 { - if files.is_empty() { - show_error!( - "missing file operand\nTry '{} --help' for more information.", - NAME - ); - return 1; - } - // Handle cases where we create links in a directory first. if let Some(ref name) = settings.target_dir { // 4th form: a directory is specified by -t. @@ -228,7 +288,7 @@ fn exec(files: &[PathBuf], settings: &Settings) -> i32 { show_error!( "extra operand '{}'\nTry '{} --help' for more information.", files[2].display(), - NAME + executable!() ); return 1; } @@ -251,24 +311,45 @@ fn link_files_in_dir(files: &[PathBuf], target_dir: &PathBuf, settings: &Setting let mut all_successful = true; for srcpath in files.iter() { - let targetpath = match srcpath.as_os_str().to_str() { - Some(name) => { - match Path::new(name).file_name() { - Some(basename) => target_dir.join(basename), - // This can be None only for "." or "..". Trying - // to create a link with such name will fail with - // EEXIST, which agrees with the behavior of GNU - // coreutils. - None => target_dir.join(name), + let targetpath = if settings.no_dereference && settings.force { + // In that case, we don't want to do link resolution + // We need to clean the target + if is_symlink(target_dir) { + if target_dir.is_file() { + if let Err(e) = fs::remove_file(target_dir) { + show_error!("Could not update {}: {}", target_dir.display(), e) + }; + } + if target_dir.is_dir() { + // Not sure why but on Windows, the symlink can be + // considered as a dir + // See test_ln::test_symlink_no_deref_dir + if let Err(e) = fs::remove_dir(target_dir) { + show_error!("Could not update {}: {}", target_dir.display(), e) + }; } } - None => { - show_error!( - "cannot stat '{}': No such file or directory", - srcpath.display() - ); - all_successful = false; - continue; + target_dir.clone() + } else { + match srcpath.as_os_str().to_str() { + Some(name) => { + match Path::new(name).file_name() { + Some(basename) => target_dir.join(basename), + // This can be None only for "." or "..". Trying + // to create a link with such name will fail with + // EEXIST, which agrees with the behavior of GNU + // coreutils. + None => target_dir.join(name), + } + } + None => { + show_error!( + "cannot stat '{}': No such file or directory", + srcpath.display() + ); + all_successful = false; + continue; + } } }; @@ -321,7 +402,7 @@ fn link(src: &PathBuf, dst: &PathBuf, settings: &Settings) -> Result<()> { match settings.overwrite { OverwriteMode::NoClobber => {} OverwriteMode::Interactive => { - print!("{}: overwrite '{}'? ", NAME, dst.display()); + print!("{}: overwrite '{}'? ", executable!(), dst.display()); if !read_yes() { return Ok(()); } @@ -341,6 +422,10 @@ fn link(src: &PathBuf, dst: &PathBuf, settings: &Settings) -> Result<()> { } } + if settings.no_dereference && settings.force && dst.exists() { + fs::remove_file(dst)?; + } + if settings.symbolic { symlink(&source, dst)?; } else { diff --git a/src/uu/logname/Cargo.toml b/src/uu/logname/Cargo.toml index 450ac4571..416f817d7 100644 --- a/src/uu/logname/Cargo.toml +++ b/src/uu/logname/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_logname" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "logname ~ (uutils) display the login name of the current user" @@ -16,8 +16,8 @@ path = "src/logname.rs" [dependencies] libc = "0.2.42" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "logname" diff --git a/src/uu/logname/src/logname.rs b/src/uu/logname/src/logname.rs index 4537bb9cd..c1f0c31aa 100644 --- a/src/uu/logname/src/logname.rs +++ b/src/uu/logname/src/logname.rs @@ -9,8 +9,6 @@ // spell-checker:ignore (ToDO) getlogin userlogin -extern crate libc; - #[macro_use] extern crate uucore; diff --git a/src/uu/ls/Cargo.toml b/src/uu/ls/Cargo.toml index f35f90dc1..dacdc7cd9 100644 --- a/src/uu/ls/Cargo.toml +++ b/src/uu/ls/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_ls" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "ls ~ (uutils) display directory contents" @@ -15,16 +15,19 @@ edition = "2018" path = "src/ls.rs" [dependencies] -getopts = "0.2.18" -isatty = "0.1" +clap = "2.33" lazy_static = "1.0.1" -number_prefix = "0.2.8" +number_prefix = "0.4" term_grid = "0.1.5" termsize = "0.1.6" time = "0.1.40" unicode-width = "0.1.5" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary", features=["entries", "fs"] } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +globset = "0.4.6" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["entries", "fs"] } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } + +[target.'cfg(unix)'.dependencies] +atty = "0.2" [[bin]] name = "ls" diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index d6c7d0d7b..fdc11144a 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -7,25 +7,19 @@ // spell-checker:ignore (ToDO) cpio svgz webm somegroup nlink rmvb xspf -extern crate getopts; -#[cfg(unix)] -extern crate isatty; -extern crate number_prefix; -extern crate term_grid; -extern crate termsize; -extern crate time; -extern crate unicode_width; - #[cfg(unix)] #[macro_use] extern crate lazy_static; #[macro_use] extern crate uucore; -#[cfg(unix)] -use isatty::stdout_isatty; -use number_prefix::{decimal_prefix, Prefixed, Standalone}; -use std::cmp::Reverse; +mod quoting_style; +mod version_cmp; + +use clap::{App, Arg}; +use globset::{self, Glob, GlobSet, GlobSetBuilder}; +use number_prefix::NumberPrefix; +use quoting_style::{escape_name, QuotingStyle}; #[cfg(unix)] use std::collections::HashMap; use std::fs; @@ -37,6 +31,11 @@ use std::os::unix::fs::MetadataExt; #[cfg(windows)] use std::os::windows::fs::MetadataExt; use std::path::{Path, PathBuf}; +#[cfg(unix)] +use std::time::Duration; +use std::time::{SystemTime, UNIX_EPOCH}; +use std::{cmp::Reverse, process::exit}; + use term_grid::{Cell, Direction, Filling, Grid, GridOptions}; use time::{strftime, Timespec}; #[cfg(unix)] @@ -44,14 +43,17 @@ use unicode_width::UnicodeWidthStr; #[cfg(unix)] use uucore::libc::{mode_t, S_ISGID, S_ISUID, S_ISVTX, S_IWOTH, S_IXGRP, S_IXOTH, S_IXUSR}; -static NAME: &str = "ls"; -static SUMMARY: &str = ""; -static LONG_HELP: &str = " +static VERSION: &str = env!("CARGO_PKG_VERSION"); +static ABOUT: &str = " By default, ls will list the files and contents of any directories on the command line, expect that it will ignore files and directories whose names start with '.' "; +fn get_usage() -> String { + format!("{0} [OPTION]... [FILE]...", executable!()) +} + #[cfg(unix)] static DEFAULT_COLORS: &str = "rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.jpg=01;35:*.jpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.m4a=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.oga=00;36:*.opus=00;36:*.spx=00;36:*.xspf=00;36:"; @@ -63,7 +65,7 @@ lazy_static! { let codes = LS_COLORS.split(':'); let mut map = HashMap::new(); for c in codes { - let p: Vec<_> = c.split('=').collect(); + let p: Vec<_> = c.splitn(2, '=').collect(); if p.len() == 2 { map.insert(p[0], p[1]); } @@ -76,118 +78,926 @@ lazy_static! { static ref END_CODE: &'static str = COLOR_MAP.get("ec").unwrap_or(&""); } +pub mod options { + pub mod format { + pub static ONELINE: &str = "1"; + pub static LONG: &str = "long"; + pub static COLUMNS: &str = "C"; + pub static ACROSS: &str = "x"; + pub static COMMAS: &str = "m"; + pub static LONG_NO_OWNER: &str = "g"; + pub static LONG_NO_GROUP: &str = "o"; + pub static LONG_NUMERIC_UID_GID: &str = "numeric-uid-gid"; + } + pub mod files { + pub static ALL: &str = "all"; + pub static ALMOST_ALL: &str = "almost-all"; + } + pub mod sort { + pub static SIZE: &str = "S"; + pub static TIME: &str = "t"; + pub static NONE: &str = "U"; + pub static VERSION: &str = "v"; + } + pub mod time { + pub static ACCESS: &str = "u"; + pub static CHANGE: &str = "c"; + } + pub mod size { + pub static HUMAN_READABLE: &str = "human-readable"; + pub static SI: &str = "si"; + } + pub mod quoting { + pub static ESCAPE: &str = "escape"; + pub static LITERAL: &str = "literal"; + pub static C: &str = "quote-name"; + } + pub static QUOTING_STYLE: &str = "quoting-style"; + + pub mod indicator_style { + pub static NONE: &str = "none"; + pub static SLASH: &str = "slash"; + pub static FILE_TYPE: &str = "file-type"; + pub static CLASSIFY: &str = "classify"; + } + pub static HIDE_CONTROL_CHARS: &str = "hide-control-chars"; + pub static SHOW_CONTROL_CHARS: &str = "show-control-chars"; + pub static WIDTH: &str = "width"; + pub static AUTHOR: &str = "author"; + pub static NO_GROUP: &str = "no-group"; + pub static FORMAT: &str = "format"; + pub static SORT: &str = "sort"; + pub static TIME: &str = "time"; + pub static IGNORE_BACKUPS: &str = "ignore-backups"; + pub static DIRECTORY: &str = "directory"; + pub static CLASSIFY: &str = "classify"; + pub static FILE_TYPE: &str = "file-type"; + pub static SLASH: &str = "p"; + pub static INODE: &str = "inode"; + pub static DEREFERENCE: &str = "dereference"; + pub static REVERSE: &str = "reverse"; + pub static RECURSIVE: &str = "recursive"; + pub static COLOR: &str = "color"; + pub static PATHS: &str = "paths"; + pub static INDICATOR_STYLE: &str = "indicator-style"; + pub static HIDE: &str = "hide"; + pub static IGNORE: &str = "ignore"; +} + +#[derive(PartialEq, Eq)] +enum Format { + Columns, + Long, + OneLine, + Across, + Commas, +} + +enum Sort { + None, + Name, + Size, + Time, + Version, +} + +enum SizeFormat { + Bytes, + Binary, // Powers of 1024, --human-readable, -h + Decimal, // Powers of 1000, --si +} + +#[derive(PartialEq, Eq)] +enum Files { + All, + AlmostAll, + Normal, +} + +enum Time { + Modification, + Access, + Change, +} + +#[derive(PartialEq, Eq)] +enum IndicatorStyle { + None, + Slash, + FileType, + Classify, +} + +struct Config { + format: Format, + files: Files, + sort: Sort, + recursive: bool, + reverse: bool, + dereference: bool, + ignore_patterns: GlobSet, + size_format: SizeFormat, + directory: bool, + time: Time, + #[cfg(unix)] + inode: bool, + #[cfg(unix)] + color: bool, + long: LongFormat, + width: Option, + quoting_style: QuotingStyle, + indicator_style: IndicatorStyle, +} + +// Fields that can be removed or added to the long format +struct LongFormat { + author: bool, + group: bool, + owner: bool, + #[cfg(unix)] + numeric_uid_gid: bool, +} + +impl Config { + fn from(options: clap::ArgMatches) -> Config { + let (mut format, opt) = if let Some(format_) = options.value_of(options::FORMAT) { + ( + match format_ { + "long" | "verbose" => Format::Long, + "single-column" => Format::OneLine, + "columns" | "vertical" => Format::Columns, + "across" | "horizontal" => Format::Across, + "commas" => Format::Commas, + // below should never happen as clap already restricts the values. + _ => unreachable!("Invalid field for --format"), + }, + options::FORMAT, + ) + } else if options.is_present(options::format::LONG) { + (Format::Long, options::format::LONG) + } else if options.is_present(options::format::ACROSS) { + (Format::Across, options::format::ACROSS) + } else if options.is_present(options::format::COMMAS) { + (Format::Commas, options::format::COMMAS) + } else { + (Format::Columns, options::format::COLUMNS) + }; + + // The -o, -n and -g options are tricky. They cannot override with each + // other because it's possible to combine them. For example, the option + // -og should hide both owner and group. Furthermore, they are not + // reset if -l or --format=long is used. So these should just show the + // group: -gl or "-g --format=long". Finally, they are also not reset + // when switching to a different format option inbetween like this: + // -ogCl or "-og --format=vertical --format=long". + // + // -1 has a similar issue: it does nothing if the format is long. This + // actually makes it distinct from the --format=singe-column option, + // which always applies. + // + // The idea here is to not let these options override with the other + // options, but manually whether they have an index that's greater than + // the other format options. If so, we set the appropriate format. + if format != Format::Long { + let idx = options + .indices_of(opt) + .map(|x| x.max().unwrap()) + .unwrap_or(0); + if [ + options::format::LONG_NO_OWNER, + options::format::LONG_NO_GROUP, + options::format::LONG_NUMERIC_UID_GID, + ] + .iter() + .flat_map(|opt| options.indices_of(opt)) + .flatten() + .any(|i| i >= idx) + { + format = Format::Long; + } else if let Some(mut indices) = options.indices_of(options::format::ONELINE) { + if indices.any(|i| i > idx) { + format = Format::OneLine; + } + } + } + + let files = if options.is_present(options::files::ALL) { + Files::All + } else if options.is_present(options::files::ALMOST_ALL) { + Files::AlmostAll + } else { + Files::Normal + }; + + let sort = if let Some(field) = options.value_of(options::SORT) { + match field { + "none" => Sort::None, + "name" => Sort::Name, + "time" => Sort::Time, + "size" => Sort::Size, + "version" => Sort::Version, + // below should never happen as clap already restricts the values. + _ => unreachable!("Invalid field for --sort"), + } + } else if options.is_present(options::sort::TIME) { + Sort::Time + } else if options.is_present(options::sort::SIZE) { + Sort::Size + } else if options.is_present(options::sort::NONE) { + Sort::None + } else if options.is_present(options::sort::VERSION) { + Sort::Version + } else { + Sort::Name + }; + + let time = if let Some(field) = options.value_of(options::TIME) { + match field { + "ctime" | "status" => Time::Change, + "access" | "atime" | "use" => Time::Access, + // below should never happen as clap already restricts the values. + _ => unreachable!("Invalid field for --time"), + } + } else if options.is_present(options::time::ACCESS) { + Time::Access + } else if options.is_present(options::time::CHANGE) { + Time::Change + } else { + Time::Modification + }; + + #[cfg(unix)] + let color = match options.value_of(options::COLOR) { + None => options.is_present(options::COLOR), + Some(val) => match val { + "" | "always" | "yes" | "force" => true, + "auto" | "tty" | "if-tty" => atty::is(atty::Stream::Stdout), + /* "never" | "no" | "none" | */ _ => false, + }, + }; + + let size_format = if options.is_present(options::size::HUMAN_READABLE) { + SizeFormat::Binary + } else if options.is_present(options::size::SI) { + SizeFormat::Decimal + } else { + SizeFormat::Bytes + }; + + let long = { + let author = options.is_present(options::AUTHOR); + let group = !options.is_present(options::NO_GROUP) + && !options.is_present(options::format::LONG_NO_GROUP); + let owner = !options.is_present(options::format::LONG_NO_OWNER); + #[cfg(unix)] + let numeric_uid_gid = options.is_present(options::format::LONG_NUMERIC_UID_GID); + LongFormat { + author, + group, + owner, + #[cfg(unix)] + numeric_uid_gid, + } + }; + + let width = options + .value_of(options::WIDTH) + .map(|x| { + x.parse::().unwrap_or_else(|_e| { + show_error!("invalid line width: ‘{}’", x); + exit(2); + }) + }) + .or_else(|| termsize::get().map(|s| s.cols)); + + let show_control = if options.is_present(options::HIDE_CONTROL_CHARS) { + false + } else if options.is_present(options::SHOW_CONTROL_CHARS) { + true + } else { + false // TODO: only if output is a terminal and the program is `ls` + }; + + let quoting_style = if let Some(style) = options.value_of(options::QUOTING_STYLE) { + match style { + "literal" => QuotingStyle::Literal { show_control }, + "shell" => QuotingStyle::Shell { + escape: false, + always_quote: false, + show_control, + }, + "shell-always" => QuotingStyle::Shell { + escape: false, + always_quote: true, + show_control, + }, + "shell-escape" => QuotingStyle::Shell { + escape: true, + always_quote: false, + show_control, + }, + "shell-escape-always" => QuotingStyle::Shell { + escape: true, + always_quote: true, + show_control, + }, + "c" => QuotingStyle::C { + quotes: quoting_style::Quotes::Double, + }, + "escape" => QuotingStyle::C { + quotes: quoting_style::Quotes::None, + }, + _ => unreachable!("Should have been caught by Clap"), + } + } else if options.is_present(options::quoting::LITERAL) { + QuotingStyle::Literal { show_control } + } else if options.is_present(options::quoting::ESCAPE) { + QuotingStyle::C { + quotes: quoting_style::Quotes::None, + } + } else if options.is_present(options::quoting::C) { + QuotingStyle::C { + quotes: quoting_style::Quotes::Double, + } + } else { + // TODO: use environment variable if available + QuotingStyle::Shell { + escape: true, + always_quote: false, + show_control, + } + }; + + let indicator_style = if let Some(field) = options.value_of(options::INDICATOR_STYLE) { + match field { + "none" => IndicatorStyle::None, + "file-type" => IndicatorStyle::FileType, + "classify" => IndicatorStyle::Classify, + "slash" => IndicatorStyle::Slash, + &_ => IndicatorStyle::None, + } + } else if options.is_present(options::indicator_style::NONE) { + IndicatorStyle::None + } else if options.is_present(options::indicator_style::CLASSIFY) + || options.is_present(options::CLASSIFY) + { + IndicatorStyle::Classify + } else if options.is_present(options::indicator_style::SLASH) + || options.is_present(options::SLASH) + { + IndicatorStyle::Slash + } else if options.is_present(options::indicator_style::FILE_TYPE) + || options.is_present(options::FILE_TYPE) + { + IndicatorStyle::FileType + } else { + IndicatorStyle::None + }; + + let mut ignore_patterns = GlobSetBuilder::new(); + if options.is_present(options::IGNORE_BACKUPS) { + ignore_patterns.add(Glob::new("*~").unwrap()); + ignore_patterns.add(Glob::new(".*~").unwrap()); + } + + for pattern in options.values_of(options::IGNORE).into_iter().flatten() { + match Glob::new(pattern) { + Ok(p) => { + ignore_patterns.add(p); + } + Err(_) => show_warning!("Invalid pattern for ignore: '{}'", pattern), + } + } + + if files == Files::Normal { + for pattern in options.values_of(options::HIDE).into_iter().flatten() { + match Glob::new(pattern) { + Ok(p) => { + ignore_patterns.add(p); + } + Err(_) => show_warning!("Invalid pattern for hide: '{}'", pattern), + } + } + } + + let ignore_patterns = ignore_patterns.build().unwrap(); + + Config { + format, + files, + sort, + recursive: options.is_present(options::RECURSIVE), + reverse: options.is_present(options::REVERSE), + dereference: options.is_present(options::DEREFERENCE), + ignore_patterns, + size_format, + directory: options.is_present(options::DIRECTORY), + time, + #[cfg(unix)] + color, + #[cfg(unix)] + inode: options.is_present(options::INODE), + long, + width, + quoting_style, + indicator_style, + } + } +} + pub fn uumain(args: impl uucore::Args) -> i32 { let args = args.collect_str(); - let syntax = format!( - "[OPTION]... DIRECTORY - {0} [OPTION]... [FILE]...", - NAME - ); - let matches = app!(&syntax, SUMMARY, LONG_HELP) - .optflag("1", "", "list one file per line.") - .optflag( - "a", - "all", - "Do not ignore hidden files (files with names that start with '.').", - ) - .optflag( - "A", - "almost-all", - "In a directory, do not ignore all file names that start with '.', only ignore \ - '.' and '..'.", - ) - .optflag("B", "ignore-backups", "Ignore entries which end with ~.") - .optflag( - "c", - "", - "If the long listing format (e.g., -l, -o) is being used, print the status \ - change time (the ‘ctime’ in the inode) instead of the modification time. When \ - explicitly sorting by time (--sort=time or -t) or when not using a long listing \ - format, sort according to the status change time.", - ) - .optflag( - "d", - "directory", - "Only list the names of directories, rather than listing directory contents. \ - This will not follow symbolic links unless one of `--dereference-command-line \ - (-H)`, `--dereference (-L)`, or `--dereference-command-line-symlink-to-dir` is \ - specified.", - ) - .optflag( - "F", - "classify", - "Append a character to each file name indicating the file type. Also, for \ - regular files that are executable, append '*'. The file type indicators are \ - '/' for directories, '@' for symbolic links, '|' for FIFOs, '=' for sockets, \ - '>' for doors, and nothing for regular files.", - ) - .optflag( - "h", - "human-readable", - "Print human readable file sizes (e.g. 1K 234M 56G).", - ) - .optflag("i", "inode", "print the index number of each file") - .optflag( - "L", - "dereference", - "When showing file information for a symbolic link, show information for the \ - file the link references rather than the link itself.", - ) - .optflag("l", "long", "Display detailed information.") - .optflag("n", "numeric-uid-gid", "-l with numeric UIDs and GIDs.") - .optflag( - "r", - "reverse", - "Reverse whatever the sorting method is--e.g., list files in reverse \ - alphabetical order, youngest first, smallest first, or whatever.", - ) - .optflag( - "R", - "recursive", - "List the contents of all directories recursively.", - ) - .optflag("S", "", "Sort by file size, largest first.") - .optflag( - "t", - "", - "Sort by modification time (the 'mtime' in the inode), newest first.", - ) - .optflag( - "U", - "", - "Do not sort; list the files in whatever order they are stored in the \ - directory. This is especially useful when listing very large directories, \ - since not doing any sorting can be noticeably faster.", - ) - .optflagopt( - "", - "color", - "Color output based on file type.", - "always|auto|never", - ) - .parse(args); + let usage = get_usage(); - list(matches); - 0 + let app = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(&usage[..]) + + // Format arguments + .arg( + Arg::with_name(options::FORMAT) + .long(options::FORMAT) + .help("Set the display format.") + .takes_value(true) + .possible_values(&["long", "verbose", "single-column", "columns", "vertical", "across", "horizontal", "commas"]) + .hide_possible_values(true) + .require_equals(true) + .overrides_with_all(&[ + options::FORMAT, + options::format::COLUMNS, + options::format::LONG, + options::format::ACROSS, + options::format::COLUMNS, + ]), + ) + .arg( + Arg::with_name(options::format::COLUMNS) + .short(options::format::COLUMNS) + .help("Display the files in columns.") + .overrides_with_all(&[ + options::FORMAT, + options::format::COLUMNS, + options::format::LONG, + options::format::ACROSS, + options::format::COLUMNS, + ]), + ) + .arg( + Arg::with_name(options::format::LONG) + .short("l") + .long(options::format::LONG) + .help("Display detailed information.") + .overrides_with_all(&[ + options::FORMAT, + options::format::COLUMNS, + options::format::LONG, + options::format::ACROSS, + options::format::COLUMNS, + ]), + ) + .arg( + Arg::with_name(options::format::ACROSS) + .short(options::format::ACROSS) + .help("List entries in rows instead of in columns.") + .overrides_with_all(&[ + options::FORMAT, + options::format::COLUMNS, + options::format::LONG, + options::format::ACROSS, + options::format::COLUMNS, + ]), + ) + .arg( + Arg::with_name(options::format::COMMAS) + .short(options::format::COMMAS) + .help("List entries separated by commas.") + .overrides_with_all(&[ + options::FORMAT, + options::format::COLUMNS, + options::format::LONG, + options::format::ACROSS, + options::format::COLUMNS, + ]), + ) + // The next four arguments do not override with the other format + // options, see the comment in Config::from for the reason. + // Ideally, they would use Arg::override_with, with their own name + // but that doesn't seem to work in all cases. Example: + // ls -1g1 + // even though `ls -11` and `ls -1 -g -1` work. + .arg( + Arg::with_name(options::format::ONELINE) + .short(options::format::ONELINE) + .help("List one file per line.") + .multiple(true) + ) + .arg( + Arg::with_name(options::format::LONG_NO_GROUP) + .short(options::format::LONG_NO_GROUP) + .help("Long format without group information. Identical to --format=long with --no-group.") + .multiple(true) + ) + .arg( + Arg::with_name(options::format::LONG_NO_OWNER) + .short(options::format::LONG_NO_OWNER) + .help("Long format without owner information.") + .multiple(true) + ) + .arg( + Arg::with_name(options::format::LONG_NUMERIC_UID_GID) + .short("n") + .long(options::format::LONG_NUMERIC_UID_GID) + .help("-l with numeric UIDs and GIDs.") + .multiple(true) + ) + + // Quoting style + .arg( + Arg::with_name(options::QUOTING_STYLE) + .long(options::QUOTING_STYLE) + .takes_value(true) + .help("Set quoting style.") + .possible_values(&["literal", "shell", "shell-always", "shell-escape", "shell-escape-always", "c", "escape"]) + .overrides_with_all(&[ + options::QUOTING_STYLE, + options::quoting::LITERAL, + options::quoting::ESCAPE, + options::quoting::C, + ]) + ) + .arg( + Arg::with_name(options::quoting::LITERAL) + .short("N") + .long(options::quoting::LITERAL) + .help("Use literal quoting style. Equivalent to `--quoting-style=literal`") + .overrides_with_all(&[ + options::QUOTING_STYLE, + options::quoting::LITERAL, + options::quoting::ESCAPE, + options::quoting::C, + ]) + ) + .arg( + Arg::with_name(options::quoting::ESCAPE) + .short("b") + .long(options::quoting::ESCAPE) + .help("Use escape quoting style. Equivalent to `--quoting-style=escape`") + .overrides_with_all(&[ + options::QUOTING_STYLE, + options::quoting::LITERAL, + options::quoting::ESCAPE, + options::quoting::C, + ]) + ) + .arg( + Arg::with_name(options::quoting::C) + .short("Q") + .long(options::quoting::C) + .help("Use C quoting style. Equivalent to `--quoting-style=c`") + .overrides_with_all(&[ + options::QUOTING_STYLE, + options::quoting::LITERAL, + options::quoting::ESCAPE, + options::quoting::C, + ]) + ) + + // Control characters + .arg( + Arg::with_name(options::HIDE_CONTROL_CHARS) + .short("q") + .long(options::HIDE_CONTROL_CHARS) + .help("Replace control characters with '?' if they are not escaped.") + .overrides_with_all(&[ + options::HIDE_CONTROL_CHARS, + options::SHOW_CONTROL_CHARS, + ]) + ) + .arg( + Arg::with_name(options::SHOW_CONTROL_CHARS) + .long(options::SHOW_CONTROL_CHARS) + .help("Show control characters 'as is' if they are not escaped.") + .overrides_with_all(&[ + options::HIDE_CONTROL_CHARS, + options::SHOW_CONTROL_CHARS, + ]) + ) + + // Time arguments + .arg( + Arg::with_name(options::TIME) + .long(options::TIME) + .help("Show time in :\n\ + \taccess time (-u): atime, access, use;\n\ + \tchange time (-t): ctime, status.") + .value_name("field") + .takes_value(true) + .possible_values(&["atime", "access", "use", "ctime", "status"]) + .hide_possible_values(true) + .require_equals(true) + .overrides_with_all(&[ + options::TIME, + options::time::ACCESS, + options::time::CHANGE, + ]) + ) + .arg( + Arg::with_name(options::time::CHANGE) + .short(options::time::CHANGE) + .help("If the long listing format (e.g., -l, -o) is being used, print the status \ + change time (the ‘ctime’ in the inode) instead of the modification time. When \ + explicitly sorting by time (--sort=time or -t) or when not using a long listing \ + format, sort according to the status change time.") + .overrides_with_all(&[ + options::TIME, + options::time::ACCESS, + options::time::CHANGE, + ]) + ) + .arg( + Arg::with_name(options::time::ACCESS) + .short(options::time::ACCESS) + .help("If the long listing format (e.g., -l, -o) is being used, print the status \ + access time instead of the modification time. When explicitly sorting by time \ + (--sort=time or -t) or when not using a long listing format, sort according to the \ + access time.") + .overrides_with_all(&[ + options::TIME, + options::time::ACCESS, + options::time::CHANGE, + ]) + ) + + // Hide and ignore + .arg( + Arg::with_name(options::HIDE) + .long(options::HIDE) + .takes_value(true) + .multiple(true) + ) + .arg( + Arg::with_name(options::IGNORE) + .short("I") + .long(options::IGNORE) + .takes_value(true) + .multiple(true) + ) + .arg( + Arg::with_name(options::IGNORE_BACKUPS) + .short("B") + .long(options::IGNORE_BACKUPS) + .help("Ignore entries which end with ~."), + ) + + // Sort arguments + .arg( + Arg::with_name(options::SORT) + .long(options::SORT) + .help("Sort by : name, none (-U), time (-t) or size (-S)") + .value_name("field") + .takes_value(true) + .possible_values(&["name", "none", "time", "size", "version"]) + .require_equals(true) + .overrides_with_all(&[ + options::SORT, + options::sort::SIZE, + options::sort::TIME, + options::sort::NONE, + options::sort::VERSION, + ]) + ) + .arg( + Arg::with_name(options::sort::SIZE) + .short(options::sort::SIZE) + .help("Sort by file size, largest first.") + .overrides_with_all(&[ + options::SORT, + options::sort::SIZE, + options::sort::TIME, + options::sort::NONE, + options::sort::VERSION, + ]) + ) + .arg( + Arg::with_name(options::sort::TIME) + .short(options::sort::TIME) + .help("Sort by modification time (the 'mtime' in the inode), newest first.") + .overrides_with_all(&[ + options::SORT, + options::sort::SIZE, + options::sort::TIME, + options::sort::NONE, + options::sort::VERSION, + ]) + ) + .arg( + Arg::with_name(options::sort::VERSION) + .short(options::sort::VERSION) + .help("Natural sort of (version) numbers in the filenames.") + .overrides_with_all(&[ + options::SORT, + options::sort::SIZE, + options::sort::TIME, + options::sort::NONE, + options::sort::VERSION, + ]) + ) + .arg( + Arg::with_name(options::sort::NONE) + .short(options::sort::NONE) + .help("Do not sort; list the files in whatever order they are stored in the \ + directory. This is especially useful when listing very large directories, \ + since not doing any sorting can be noticeably faster.") + .overrides_with_all(&[ + options::SORT, + options::sort::SIZE, + options::sort::TIME, + options::sort::NONE, + options::sort::VERSION, + ]) + ) + + // Long format options + .arg( + Arg::with_name(options::NO_GROUP) + .long(options::NO_GROUP) + .short("-G") + .help("Do not show group in long format.") + ) + .arg( + Arg::with_name(options::AUTHOR) + .long(options::AUTHOR) + .help("Show author in long format. On the supported platforms, the author \ + always matches the file owner.") + ) + // Other Flags + .arg( + Arg::with_name(options::files::ALL) + .short("a") + .long(options::files::ALL) + .help("Do not ignore hidden files (files with names that start with '.')."), + ) + .arg( + Arg::with_name(options::files::ALMOST_ALL) + .short("A") + .long(options::files::ALMOST_ALL) + .help( + "In a directory, do not ignore all file names that start with '.', only ignore \ + '.' and '..'.", + ), + ) + .arg( + Arg::with_name(options::DIRECTORY) + .short("d") + .long(options::DIRECTORY) + .help( + "Only list the names of directories, rather than listing directory contents. \ + This will not follow symbolic links unless one of `--dereference-command-line \ + (-H)`, `--dereference (-L)`, or `--dereference-command-line-symlink-to-dir` is \ + specified.", + ), + ) + .arg( + Arg::with_name(options::size::HUMAN_READABLE) + .short("h") + .long(options::size::HUMAN_READABLE) + .help("Print human readable file sizes (e.g. 1K 234M 56G).") + .overrides_with(options::size::SI), + ) + .arg( + Arg::with_name(options::size::SI) + .long(options::size::SI) + .help("Print human readable file sizes using powers of 1000 instead of 1024.") + ) + .arg( + Arg::with_name(options::INODE) + .short("i") + .long(options::INODE) + .help("print the index number of each file"), + ) + .arg( + Arg::with_name(options::DEREFERENCE) + .short("L") + .long(options::DEREFERENCE) + .help( + "When showing file information for a symbolic link, show information for the \ + file the link references rather than the link itself.", + ), + ) + .arg( + Arg::with_name(options::REVERSE) + .short("r") + .long(options::REVERSE) + .help("Reverse whatever the sorting method is--e.g., list files in reverse \ + alphabetical order, youngest first, smallest first, or whatever.", + )) + .arg( + Arg::with_name(options::RECURSIVE) + .short("R") + .long(options::RECURSIVE) + .help("List the contents of all directories recursively."), + ) + .arg( + Arg::with_name(options::WIDTH) + .long(options::WIDTH) + .short("w") + .help("Assume that the terminal is COLS columns wide.") + .value_name("COLS") + .takes_value(true) + ) + .arg( + Arg::with_name(options::COLOR) + .long(options::COLOR) + .help("Color output based on file type.") + .takes_value(true) + .require_equals(true) + .min_values(0), + ) + .arg( + Arg::with_name(options::INDICATOR_STYLE) + .long(options::INDICATOR_STYLE) + .help(" append indicator with style WORD to entry names: none (default), slash\ + (-p), file-type (--file-type), classify (-F)") + .takes_value(true) + .possible_values(&["none", "slash", "file-type", "classify"]) + .overrides_with_all(&[ + options::FILE_TYPE, + options::SLASH, + options::CLASSIFY, + options::INDICATOR_STYLE, + ])) + .arg( + Arg::with_name(options::CLASSIFY) + .short("F") + .long(options::CLASSIFY) + .help("Append a character to each file name indicating the file type. Also, for \ + regular files that are executable, append '*'. The file type indicators are \ + '/' for directories, '@' for symbolic links, '|' for FIFOs, '=' for sockets, \ + '>' for doors, and nothing for regular files.") + .overrides_with_all(&[ + options::FILE_TYPE, + options::SLASH, + options::CLASSIFY, + options::INDICATOR_STYLE, + ]) + ) + .arg( + Arg::with_name(options::FILE_TYPE) + .long(options::FILE_TYPE) + .help("Same as --classify, but do not append '*'") + .overrides_with_all(&[ + options::FILE_TYPE, + options::SLASH, + options::CLASSIFY, + options::INDICATOR_STYLE, + ])) + .arg( + Arg::with_name(options::SLASH) + .short(options::SLASH) + .help("Append / indicator to directories." + ) + .overrides_with_all(&[ + options::FILE_TYPE, + options::SLASH, + options::CLASSIFY, + options::INDICATOR_STYLE, + ])) + + // Positional arguments + .arg(Arg::with_name(options::PATHS).multiple(true).takes_value(true)); + + let matches = app.get_matches_from(args); + + let locs = matches + .values_of(options::PATHS) + .map(|v| v.map(ToString::to_string).collect()) + .unwrap_or_else(|| vec![String::from(".")]); + + list(locs, Config::from(matches)) } -fn list(options: getopts::Matches) { - let locs: Vec = if options.free.is_empty() { - vec![String::from(".")] - } else { - options.free.to_vec() - }; +fn list(locs: Vec, config: Config) -> i32 { + let number_of_locs = locs.len(); let mut files = Vec::::new(); let mut dirs = Vec::::new(); + let mut has_failed = false; for loc in locs { let p = PathBuf::from(&loc); + if !p.exists() { + show_error!("'{}': {}", &loc, "No such file or directory"); + // We found an error, the return code of ls should not be 0 + // And no need to continue the execution + has_failed = true; + continue; + } let mut dir = false; - if p.is_dir() && !options.opt_present("d") { + if p.is_dir() && !config.directory { dir = true; - if options.opt_present("l") && !(options.opt_present("L")) { + if config.format == Format::Long && !config.dereference { if let Ok(md) = p.symlink_metadata() { if md.file_type().is_symlink() && !p.ends_with("/") { dir = false; @@ -201,126 +1011,109 @@ fn list(options: getopts::Matches) { files.push(p); } } - sort_entries(&mut files, &options); - display_items(&files, None, &options); + sort_entries(&mut files, &config); + display_items(&files, None, &config); - sort_entries(&mut dirs, &options); + sort_entries(&mut dirs, &config); for dir in dirs { - if options.free.len() > 1 { + if number_of_locs > 1 { println!("\n{}:", dir.to_string_lossy()); } - enter_directory(&dir, &options); + enter_directory(&dir, &config); + } + if has_failed { + 1 + } else { + 0 } } -#[cfg(any(unix, target_os = "redox"))] -fn sort_entries(entries: &mut Vec, options: &getopts::Matches) { - let mut reverse = options.opt_present("r"); - if options.opt_present("t") { - if options.opt_present("c") { - entries.sort_by_key(|k| { - Reverse(get_metadata(k, options).map(|md| md.ctime()).unwrap_or(0)) - }); - } else { - entries.sort_by_key(|k| { - // Newest first - Reverse( - get_metadata(k, options) - .and_then(|md| md.modified()) - .unwrap_or(std::time::UNIX_EPOCH), - ) - }); - } - } else if options.opt_present("S") { - entries.sort_by_key(|k| get_metadata(k, options).map(|md| md.size()).unwrap_or(0)); - reverse = !reverse; - } else if !options.opt_present("U") { - entries.sort(); +fn sort_entries(entries: &mut Vec, config: &Config) { + match config.sort { + Sort::Time => entries.sort_by_key(|k| { + Reverse( + get_metadata(k, config) + .ok() + .and_then(|md| get_system_time(&md, config)) + .unwrap_or(UNIX_EPOCH), + ) + }), + Sort::Size => entries + .sort_by_key(|k| Reverse(get_metadata(k, config).map(|md| md.len()).unwrap_or(0))), + // The default sort in GNU ls is case insensitive + Sort::Name => entries.sort_by_key(|k| k.to_string_lossy().to_lowercase()), + Sort::Version => entries.sort_by(version_cmp::version_cmp), + Sort::None => {} } - if reverse { + if config.reverse { entries.reverse(); } } #[cfg(windows)] -fn sort_entries(entries: &mut Vec, options: &getopts::Matches) { - let mut reverse = options.opt_present("r"); - if options.opt_present("t") { - entries.sort_by_key(|k| { - // Newest first - Reverse( - get_metadata(k, options) - .and_then(|md| md.modified()) - .unwrap_or(std::time::UNIX_EPOCH), - ) - }); - } else if options.opt_present("S") { - entries.sort_by_key(|k| { - get_metadata(k, options) - .map(|md| md.file_size()) - .unwrap_or(0) - }); - reverse = !reverse; - } else if !options.opt_present("U") { - entries.sort(); - } - - if reverse { - entries.reverse(); - } +fn is_hidden(file_path: &DirEntry) -> bool { + let metadata = fs::metadata(file_path.path()).unwrap(); + let attr = metadata.file_attributes(); + ((attr & 0x2) > 0) || file_path.file_name().to_string_lossy().starts_with('.') } -fn should_display(entry: &DirEntry, options: &getopts::Matches) -> bool { +#[cfg(unix)] +fn is_hidden(file_path: &DirEntry) -> bool { + file_path.file_name().to_string_lossy().starts_with('.') +} + +fn should_display(entry: &DirEntry, config: &Config) -> bool { let ffi_name = entry.file_name(); - let name = ffi_name.to_string_lossy(); - if !options.opt_present("a") && !options.opt_present("A") && name.starts_with('.') { + + if config.files == Files::Normal && is_hidden(entry) { return false; } - if options.opt_present("B") && name.ends_with('~') { + + if config.ignore_patterns.is_match(&ffi_name) { return false; } true } -fn enter_directory(dir: &PathBuf, options: &getopts::Matches) { +fn enter_directory(dir: &PathBuf, config: &Config) { let mut entries: Vec<_> = safe_unwrap!(fs::read_dir(dir).and_then(Iterator::collect)); - entries.retain(|e| should_display(e, options)); + entries.retain(|e| should_display(e, config)); let mut entries: Vec<_> = entries.iter().map(DirEntry::path).collect(); - sort_entries(&mut entries, options); + sort_entries(&mut entries, config); - if options.opt_present("a") { + if config.files == Files::All { let mut display_entries = entries.clone(); display_entries.insert(0, dir.join("..")); display_entries.insert(0, dir.join(".")); - display_items(&display_entries, Some(dir), options); + display_items(&display_entries, Some(dir), config); } else { - display_items(&entries, Some(dir), options); + display_items(&entries, Some(dir), config); } - if options.opt_present("R") { + if config.recursive { for e in entries.iter().filter(|p| p.is_dir()) { println!("\n{}:", e.to_string_lossy()); - enter_directory(&e, options); + enter_directory(&e, config); } } } -fn get_metadata(entry: &PathBuf, options: &getopts::Matches) -> std::io::Result { - if options.opt_present("L") { +fn get_metadata(entry: &PathBuf, config: &Config) -> std::io::Result { + if config.dereference { entry.metadata().or_else(|_| entry.symlink_metadata()) } else { entry.symlink_metadata() } } -fn display_dir_entry_size(entry: &PathBuf, options: &getopts::Matches) -> (usize, usize) { - if let Ok(md) = get_metadata(entry, options) { +fn display_dir_entry_size(entry: &PathBuf, config: &Config) -> (usize, usize) { + if let Ok(md) = get_metadata(entry, config) { ( display_symlink_count(&md).len(), - display_file_size(&md, options).len(), + display_file_size(&md, config).len(), ) } else { (0, 0) @@ -328,65 +1121,83 @@ fn display_dir_entry_size(entry: &PathBuf, options: &getopts::Matches) -> (usize } fn pad_left(string: String, count: usize) -> String { - if count > string.len() { - let pad = count - string.len(); - let pad = String::from_utf8(vec![b' '; pad]).unwrap(); - format!("{}{}", pad, string) - } else { - string - } + format!("{:>width$}", string, width = count) } -fn display_items(items: &[PathBuf], strip: Option<&Path>, options: &getopts::Matches) { - if options.opt_present("long") || options.opt_present("numeric-uid-gid") { +fn display_items(items: &[PathBuf], strip: Option<&Path>, config: &Config) { + if config.format == Format::Long { let (mut max_links, mut max_size) = (1, 1); for item in items { - let (links, size) = display_dir_entry_size(item, options); + let (links, size) = display_dir_entry_size(item, config); max_links = links.max(max_links); max_size = size.max(max_size); } for item in items { - display_item_long(item, strip, max_links, max_size, options); + display_item_long(item, strip, max_links, max_size, config); } } else { - if !options.opt_present("1") { - let names = items.iter().filter_map(|i| { - let md = get_metadata(i, options); - match md { - Err(e) => { - let filename = get_file_name(i, strip); - show_error!("{}: {}", filename, e); - None - } - Ok(md) => Some(display_file_name(&i, strip, &md, options)), + let names = items.iter().filter_map(|i| { + let md = get_metadata(i, config); + match md { + Err(e) => { + let filename = get_file_name(i, strip); + show_error!("'{}': {}", filename, e); + None } - }); - - if let Some(size) = termsize::get() { - let mut grid = Grid::new(GridOptions { - filling: Filling::Spaces(2), - direction: Direction::TopToBottom, - }); + Ok(md) => Some(display_file_name(&i, strip, &md, config)), + } + }); + match (&config.format, config.width) { + (Format::Columns, Some(width)) => display_grid(names, width, Direction::TopToBottom), + (Format::Across, Some(width)) => display_grid(names, width, Direction::LeftToRight), + (Format::Commas, width_opt) => { + let term_width = width_opt.unwrap_or(1); + let mut current_col = 0; + let mut names = names; + if let Some(name) = names.next() { + print!("{}", name.contents); + current_col = name.width as u16 + 2; + } for name in names { - grid.add(name); + let name_width = name.width as u16; + if current_col + name_width + 1 > term_width { + current_col = name_width + 2; + print!(",\n{}", name.contents); + } else { + current_col += name_width + 2; + print!(", {}", name.contents); + } } - - if let Some(output) = grid.fit_into_width(size.cols as usize) { - print!("{}", output); - return; + // Current col is never zero again if names have been printed. + // So we print a newline. + if current_col > 0 { + println!(); + } + } + _ => { + for name in names { + println!("{}", name.contents); } } } + } +} - // Couldn't display a grid, either because we don't know - // the terminal width or because fit_into_width failed - for i in items { - let md = get_metadata(i, options); - if let Ok(md) = md { - println!("{}", display_file_name(&i, strip, &md, options).contents); - } - } +fn display_grid(names: impl Iterator, width: u16, direction: Direction) { + let mut grid = Grid::new(GridOptions { + filling: Filling::Spaces(2), + direction, + }); + + for name in names { + grid.add(name); + } + + match grid.fit_into_width(width as usize) { + Some(output) => print!("{}", output), + // Width is too small for the grid, so we fit it in one column + None => print!("{}", grid.fit_into_columns(1)), } } @@ -397,9 +1208,9 @@ fn display_item_long( strip: Option<&Path>, max_links: usize, max_size: usize, - options: &getopts::Matches, + config: &Config, ) { - let md = match get_metadata(item, options) { + let md = match get_metadata(item, config) { Err(e) => { let filename = get_file_name(&item, strip); show_error!("{}: {}", filename, e); @@ -408,32 +1219,45 @@ fn display_item_long( Ok(md) => md, }; - println!( - "{}{}{} {} {} {} {} {} {}", - get_inode(&md, options), + #[cfg(unix)] + { + if config.inode { + print!("{} ", get_inode(&md)); + } + } + + print!( + "{}{} {}", display_file_type(md.file_type()), display_permissions(&md), pad_left(display_symlink_count(&md), max_links), - display_uname(&md, options), - display_group(&md, options), - pad_left(display_file_size(&md, options), max_size), - display_date(&md, options), - display_file_name(&item, strip, &md, options).contents + ); + + if config.long.owner { + print!(" {}", display_uname(&md, config)); + } + + if config.long.group { + print!(" {}", display_group(&md, config)); + } + + // Author is only different from owner on GNU/Hurd, so we reuse + // the owner, since GNU/Hurd is not currently supported by Rust. + if config.long.author { + print!(" {}", display_uname(&md, config)); + } + + println!( + " {} {} {}", + pad_left(display_file_size(&md, config), max_size), + display_date(&md, config), + display_file_name(&item, strip, &md, config).contents, ); } #[cfg(unix)] -fn get_inode(metadata: &Metadata, options: &getopts::Matches) -> String { - if options.opt_present("inode") { - format!("{:8} ", metadata.ino()) - } else { - "".to_string() - } -} - -#[cfg(not(unix))] -fn get_inode(_metadata: &Metadata, _options: &getopts::Matches) -> String { - "".to_string() +fn get_inode(metadata: &Metadata) -> String { + format!("{:8}", metadata.ino()) } // Currently getpwuid is `linux` target only. If it's broken out into @@ -442,8 +1266,8 @@ fn get_inode(_metadata: &Metadata, _options: &getopts::Matches) -> String { use uucore::entries; #[cfg(unix)] -fn display_uname(metadata: &Metadata, options: &getopts::Matches) -> String { - if options.opt_present("numeric-uid-gid") { +fn display_uname(metadata: &Metadata, config: &Config) -> String { + if config.long.numeric_uid_gid { metadata.uid().to_string() } else { entries::uid2usr(metadata.uid()).unwrap_or_else(|_| metadata.uid().to_string()) @@ -451,8 +1275,8 @@ fn display_uname(metadata: &Metadata, options: &getopts::Matches) -> String { } #[cfg(unix)] -fn display_group(metadata: &Metadata, options: &getopts::Matches) -> String { - if options.opt_present("numeric-uid-gid") { +fn display_group(metadata: &Metadata, config: &Config) -> String { + if config.long.numeric_uid_gid { metadata.gid().to_string() } else { entries::gid2grp(metadata.gid()).unwrap_or_else(|_| metadata.gid().to_string()) @@ -460,53 +1284,83 @@ fn display_group(metadata: &Metadata, options: &getopts::Matches) -> String { } #[cfg(not(unix))] -#[allow(unused_variables)] -fn display_uname(metadata: &Metadata, _options: &getopts::Matches) -> String { +fn display_uname(_metadata: &Metadata, _config: &Config) -> String { "somebody".to_string() } #[cfg(not(unix))] #[allow(unused_variables)] -fn display_group(metadata: &Metadata, _options: &getopts::Matches) -> String { +fn display_group(_metadata: &Metadata, _config: &Config) -> String { "somegroup".to_string() } +// The implementations for get_time are separated because some options, such +// as ctime will not be available #[cfg(unix)] -fn display_date(metadata: &Metadata, options: &getopts::Matches) -> String { - let secs = if options.opt_present("c") { - metadata.ctime() - } else { - metadata.mtime() - }; - let time = time::at(Timespec::new(secs, 0)); - strftime("%F %R", &time).unwrap() -} - -#[cfg(not(unix))] -#[allow(unused_variables)] -fn display_date(metadata: &Metadata, options: &getopts::Matches) -> String { - if let Ok(mtime) = metadata.modified() { - let time = time::at(Timespec::new( - mtime - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs() as i64, - 0, - )); - strftime("%F %R", &time).unwrap() - } else { - "???".to_string() +fn get_system_time(md: &Metadata, config: &Config) -> Option { + match config.time { + Time::Change => Some(UNIX_EPOCH + Duration::new(md.ctime() as u64, md.ctime_nsec() as u32)), + Time::Modification => md.modified().ok(), + Time::Access => md.accessed().ok(), } } -fn display_file_size(metadata: &Metadata, options: &getopts::Matches) -> String { - if options.opt_present("human-readable") { - match decimal_prefix(metadata.len() as f64) { - Standalone(bytes) => bytes.to_string(), - Prefixed(prefix, bytes) => format!("{:.2}{}", bytes, prefix).to_uppercase(), +#[cfg(not(unix))] +fn get_system_time(md: &Metadata, config: &Config) -> Option { + match config.time { + Time::Modification => md.modified().ok(), + Time::Access => md.accessed().ok(), + _ => None, + } +} + +fn get_time(md: &Metadata, config: &Config) -> Option { + let duration = get_system_time(md, config)? + .duration_since(UNIX_EPOCH) + .ok()?; + let secs = duration.as_secs() as i64; + let nsec = duration.subsec_nanos() as i32; + Some(time::at(Timespec::new(secs, nsec))) +} + +fn display_date(metadata: &Metadata, config: &Config) -> String { + match get_time(metadata, config) { + Some(time) => strftime("%F %R", &time).unwrap(), + None => "???".into(), + } +} + +// There are a few peculiarities to how GNU formats the sizes: +// 1. One decimal place is given if and only if the size is smaller than 10 +// 2. It rounds sizes up. +// 3. The human-readable format uses powers for 1024, but does not display the "i" +// that is commonly used to denote Kibi, Mebi, etc. +// 4. Kibi and Kilo are denoted differently ("k" and "K", respectively) +fn format_prefixed(prefixed: NumberPrefix) -> String { + match prefixed { + NumberPrefix::Standalone(bytes) => bytes.to_string(), + NumberPrefix::Prefixed(prefix, bytes) => { + // Remove the "i" from "Ki", "Mi", etc. if present + let prefix_str = prefix.symbol().trim_end_matches('i'); + + // Check whether we get more than 10 if we round up to the first decimal + // because we want do display 9.81 as "9.9", not as "10". + if (10.0 * bytes).ceil() >= 100.0 { + format!("{:.0}{}", bytes.ceil(), prefix_str) + } else { + format!("{:.1}{}", (10.0 * bytes).ceil() / 10.0, prefix_str) + } } - } else { - metadata.len().to_string() + } +} + +fn display_file_size(metadata: &Metadata, config: &Config) -> String { + // NOTE: The human-readable behaviour deviates from the GNU ls. + // The GNU ls uses binary prefixes by default. + match config.size_format { + SizeFormat::Binary => format_prefixed(NumberPrefix::binary(metadata.len() as f64)), + SizeFormat::Decimal => format_prefixed(NumberPrefix::decimal(metadata.len() as f64)), + SizeFormat::Bytes => metadata.len().to_string(), } } @@ -536,24 +1390,29 @@ fn display_file_name( path: &Path, strip: Option<&Path>, metadata: &Metadata, - options: &getopts::Matches, + config: &Config, ) -> Cell { - let mut name = get_file_name(path, strip); + let mut name = escape_name(get_file_name(path, strip), &config.quoting_style); + let file_type = metadata.file_type(); - if !options.opt_present("long") { - name = get_inode(metadata, options) + &name; - } - - if options.opt_present("classify") { - let file_type = metadata.file_type(); - if file_type.is_dir() { - name.push('/'); - } else if file_type.is_symlink() { - name.push('@'); + match config.indicator_style { + IndicatorStyle::Classify | IndicatorStyle::FileType => { + if file_type.is_dir() { + name.push('/'); + } + if file_type.is_symlink() { + name.push('@'); + } } - } + IndicatorStyle::Slash => { + if file_type.is_dir() { + name.push('/'); + } + } + _ => (), + }; - if options.opt_present("long") && metadata.file_type().is_symlink() { + if config.format == Format::Long && metadata.file_type().is_symlink() { if let Ok(target) = path.read_link() { // We don't bother updating width here because it's not used for long listings let target_name = target.to_string_lossy().to_string(); @@ -598,26 +1457,16 @@ fn display_file_name( path: &Path, strip: Option<&Path>, metadata: &Metadata, - options: &getopts::Matches, + config: &Config, ) -> Cell { - let mut name = get_file_name(path, strip); - if !options.opt_present("long") { - name = get_inode(metadata, options) + &name; + let mut name = escape_name(get_file_name(path, strip), &config.quoting_style); + if config.format != Format::Long && config.inode { + name = get_inode(metadata) + " " + &name; } let mut width = UnicodeWidthStr::width(&*name); - let color = match options.opt_str("color") { - None => true, - Some(val) => match val.as_ref() { - "always" | "yes" | "force" => true, - "auto" | "tty" | "if-tty" => stdout_isatty(), - /* "never" | "no" | "none" | */ _ => false, - }, - }; - let classify = options.opt_present("classify"); let ext; - - if color || classify { + if config.color || config.indicator_style != IndicatorStyle::None { let file_type = metadata.file_type(); let (code, sym) = if file_type.is_dir() { @@ -667,18 +1516,36 @@ fn display_file_name( ("", None) }; - if color { + if config.color { name = color_name(name, code); } - if classify { - if let Some(s) = sym { - name.push(s); - width += 1; + + let char_opt = match config.indicator_style { + IndicatorStyle::Classify => sym, + IndicatorStyle::FileType => { + // Don't append an asterisk. + match sym { + Some('*') => None, + _ => sym, + } } + IndicatorStyle::Slash => { + // Append only a slash. + match sym { + Some('/') => Some('/'), + _ => None, + } + } + IndicatorStyle::None => None, + }; + + if let Some(c) = char_opt { + name.push(c); + width += 1; } } - if options.opt_present("long") && metadata.file_type().is_symlink() { + if config.format == Format::Long && metadata.file_type().is_symlink() { if let Ok(target) = path.read_link() { // We don't bother updating width here because it's not used for long listings let code = if target.exists() { "fi" } else { "mi" }; @@ -695,8 +1562,7 @@ fn display_file_name( } #[cfg(not(unix))] -#[allow(unused_variables)] -fn display_symlink_count(metadata: &Metadata) -> String { +fn display_symlink_count(_metadata: &Metadata) -> String { // Currently not sure of how to get this on Windows, so I'm punting. // Git Bash looks like it may do the same thing. String::from("1") diff --git a/src/uu/ls/src/quoting_style.rs b/src/uu/ls/src/quoting_style.rs new file mode 100644 index 000000000..ceb54466c --- /dev/null +++ b/src/uu/ls/src/quoting_style.rs @@ -0,0 +1,630 @@ +use std::char::from_digit; + +const SPECIAL_SHELL_CHARS: &str = "~`#$&*()\\|[]{};'\"<>?! "; + +pub(super) enum QuotingStyle { + Shell { + escape: bool, + always_quote: bool, + show_control: bool, + }, + C { + quotes: Quotes, + }, + Literal { + show_control: bool, + }, +} + +#[derive(Clone, Copy)] +pub(super) enum Quotes { + None, + Single, + Double, + // TODO: Locale +} + +// This implementation is heavily inspired by the std::char::EscapeDefault implementation +// in the Rust standard library. This custom implementation is needed because the +// characters \a, \b, \e, \f & \v are not recognized by Rust. +#[derive(Clone, Debug)] +struct EscapedChar { + state: EscapeState, +} + +#[derive(Clone, Debug)] +enum EscapeState { + Done, + Char(char), + Backslash(char), + ForceQuote(char), + Octal(EscapeOctal), +} + +#[derive(Clone, Debug)] +struct EscapeOctal { + c: char, + state: EscapeOctalState, + idx: usize, +} + +#[derive(Clone, Debug)] +enum EscapeOctalState { + Done, + Backslash, + Value, +} + +impl Iterator for EscapeOctal { + type Item = char; + + fn next(&mut self) -> Option { + match self.state { + EscapeOctalState::Done => None, + EscapeOctalState::Backslash => { + self.state = EscapeOctalState::Value; + Some('\\') + } + EscapeOctalState::Value => { + let octal_digit = ((self.c as u32) >> (self.idx * 3)) & 0o7; + if self.idx == 0 { + self.state = EscapeOctalState::Done; + } else { + self.idx -= 1; + } + Some(from_digit(octal_digit, 8).unwrap()) + } + } + } +} + +impl EscapeOctal { + fn from(c: char) -> EscapeOctal { + EscapeOctal { + c, + idx: 2, + state: EscapeOctalState::Backslash, + } + } +} + +impl EscapedChar { + fn new_literal(c: char) -> Self { + Self { + state: EscapeState::Char(c), + } + } + + fn new_c(c: char, quotes: Quotes) -> Self { + use EscapeState::*; + let init_state = match c { + '\x07' => Backslash('a'), + '\x08' => Backslash('b'), + '\t' => Backslash('t'), + '\n' => Backslash('n'), + '\x0B' => Backslash('v'), + '\x0C' => Backslash('f'), + '\r' => Backslash('r'), + '\\' => Backslash('\\'), + '\'' => match quotes { + Quotes::Single => Backslash('\''), + _ => Char('\''), + }, + '"' => match quotes { + Quotes::Double => Backslash('"'), + _ => Char('"'), + }, + ' ' => match quotes { + Quotes::None => Backslash(' '), + _ => Char(' '), + }, + _ if c.is_ascii_control() => Octal(EscapeOctal::from(c)), + _ => Char(c), + }; + Self { state: init_state } + } + + fn new_shell(c: char, escape: bool, quotes: Quotes) -> Self { + use EscapeState::*; + let init_state = match c { + _ if !escape && c.is_control() => Char(c), + '\x07' => Backslash('a'), + '\x08' => Backslash('b'), + '\t' => Backslash('t'), + '\n' => Backslash('n'), + '\x0B' => Backslash('v'), + '\x0C' => Backslash('f'), + '\r' => Backslash('r'), + '\\' => Backslash('\\'), + '\x00'..='\x1F' | '\x7F' => Octal(EscapeOctal::from(c)), + '\'' => match quotes { + Quotes::Single => Backslash('\''), + _ => Char('\''), + }, + _ if SPECIAL_SHELL_CHARS.contains(c) => ForceQuote(c), + _ => Char(c), + }; + Self { state: init_state } + } + + fn hide_control(self) -> Self { + match self.state { + EscapeState::Char(c) if c.is_control() => Self { + state: EscapeState::Char('?'), + }, + _ => self, + } + } +} + +impl Iterator for EscapedChar { + type Item = char; + + fn next(&mut self) -> Option { + match self.state { + EscapeState::Backslash(c) => { + self.state = EscapeState::Char(c); + Some('\\') + } + EscapeState::Char(c) | EscapeState::ForceQuote(c) => { + self.state = EscapeState::Done; + Some(c) + } + EscapeState::Done => None, + EscapeState::Octal(ref mut iter) => iter.next(), + } + } +} + +fn shell_without_escape(name: String, quotes: Quotes, show_control_chars: bool) -> (String, bool) { + let mut must_quote = false; + let mut escaped_str = String::with_capacity(name.len()); + + for c in name.chars() { + let escaped = { + let ec = EscapedChar::new_shell(c, false, quotes); + if show_control_chars { + ec + } else { + ec.hide_control() + } + }; + + match escaped.state { + EscapeState::Backslash('\'') => escaped_str.push_str("'\\''"), + EscapeState::ForceQuote(x) => { + must_quote = true; + escaped_str.push(x); + } + _ => { + for char in escaped { + escaped_str.push(char); + } + } + } + } + (escaped_str, must_quote) +} + +fn shell_with_escape(name: String, quotes: Quotes) -> (String, bool) { + // We need to keep track of whether we are in a dollar expression + // because e.g. \b\n is escaped as $'\b\n' and not like $'b'$'n' + let mut in_dollar = false; + let mut must_quote = false; + let mut escaped_str = String::with_capacity(name.len()); + + for c in name.chars() { + let escaped = EscapedChar::new_shell(c, true, quotes); + match escaped.state { + EscapeState::Char(x) => { + if in_dollar { + escaped_str.push_str("''"); + in_dollar = false; + } + escaped_str.push(x); + } + EscapeState::ForceQuote(x) => { + if in_dollar { + escaped_str.push_str("''"); + in_dollar = false; + } + must_quote = true; + escaped_str.push(x); + } + // Single quotes are not put in dollar expressions, but are escaped + // if the string also contains double quotes. In that case, they must + // be handled separately. + EscapeState::Backslash('\'') => { + must_quote = true; + in_dollar = false; + escaped_str.push_str("'\\''"); + } + _ => { + if !in_dollar { + escaped_str.push_str("'$'"); + in_dollar = true; + } + must_quote = true; + for char in escaped { + escaped_str.push(char); + } + } + } + } + (escaped_str, must_quote) +} + +pub(super) fn escape_name(name: String, style: &QuotingStyle) -> String { + match style { + QuotingStyle::Literal { show_control } => { + if !show_control { + name.chars() + .flat_map(|c| EscapedChar::new_literal(c).hide_control()) + .collect() + } else { + name + } + } + QuotingStyle::C { quotes } => { + let escaped_str: String = name + .chars() + .flat_map(|c| EscapedChar::new_c(c, *quotes)) + .collect(); + + match quotes { + Quotes::Single => format!("'{}'", escaped_str), + Quotes::Double => format!("\"{}\"", escaped_str), + _ => escaped_str, + } + } + QuotingStyle::Shell { + escape, + always_quote, + show_control, + } => { + let (quotes, must_quote) = if name.contains('"') { + (Quotes::Single, true) + } else if name.contains('\'') { + (Quotes::Double, true) + } else if *always_quote { + (Quotes::Single, true) + } else { + (Quotes::Single, false) + }; + + let (escaped_str, contains_quote_chars) = if *escape { + shell_with_escape(name, quotes) + } else { + shell_without_escape(name, quotes, *show_control) + }; + + match (must_quote | contains_quote_chars, quotes) { + (true, Quotes::Single) => format!("'{}'", escaped_str), + (true, Quotes::Double) => format!("\"{}\"", escaped_str), + _ => escaped_str, + } + } + } +} + +#[cfg(test)] +mod tests { + use crate::quoting_style::{escape_name, Quotes, QuotingStyle}; + fn get_style(s: &str) -> QuotingStyle { + match s { + "literal" => QuotingStyle::Literal { + show_control: false, + }, + "literal-show" => QuotingStyle::Literal { show_control: true }, + "escape" => QuotingStyle::C { + quotes: Quotes::None, + }, + "c" => QuotingStyle::C { + quotes: Quotes::Double, + }, + "shell" => QuotingStyle::Shell { + escape: false, + always_quote: false, + show_control: false, + }, + "shell-show" => QuotingStyle::Shell { + escape: false, + always_quote: false, + show_control: true, + }, + "shell-always" => QuotingStyle::Shell { + escape: false, + always_quote: true, + show_control: false, + }, + "shell-always-show" => QuotingStyle::Shell { + escape: false, + always_quote: true, + show_control: true, + }, + "shell-escape" => QuotingStyle::Shell { + escape: true, + always_quote: false, + show_control: false, + }, + "shell-escape-always" => QuotingStyle::Shell { + escape: true, + always_quote: true, + show_control: false, + }, + _ => panic!("Invalid name!"), + } + } + + fn check_names(name: &str, map: Vec<(&str, &str)>) { + assert_eq!( + map.iter() + .map(|(_, style)| escape_name(name.to_string(), &get_style(style))) + .collect::>(), + map.iter() + .map(|(correct, _)| correct.to_string()) + .collect::>() + ); + } + + #[test] + fn test_simple_names() { + check_names( + "one_two", + vec![ + ("one_two", "literal"), + ("one_two", "literal-show"), + ("one_two", "escape"), + ("\"one_two\"", "c"), + ("one_two", "shell"), + ("one_two", "shell-show"), + ("\'one_two\'", "shell-always"), + ("\'one_two\'", "shell-always-show"), + ("one_two", "shell-escape"), + ("\'one_two\'", "shell-escape-always"), + ], + ); + } + + #[test] + fn test_spaces() { + check_names( + "one two", + vec![ + ("one two", "literal"), + ("one two", "literal-show"), + ("one\\ two", "escape"), + ("\"one two\"", "c"), + ("\'one two\'", "shell"), + ("\'one two\'", "shell-show"), + ("\'one two\'", "shell-always"), + ("\'one two\'", "shell-always-show"), + ("\'one two\'", "shell-escape"), + ("\'one two\'", "shell-escape-always"), + ], + ); + + check_names( + " one", + vec![ + (" one", "literal"), + (" one", "literal-show"), + ("\\ one", "escape"), + ("\" one\"", "c"), + ("' one'", "shell"), + ("' one'", "shell-show"), + ("' one'", "shell-always"), + ("' one'", "shell-always-show"), + ("' one'", "shell-escape"), + ("' one'", "shell-escape-always"), + ], + ); + } + + #[test] + fn test_quotes() { + // One double quote + check_names( + "one\"two", + vec![ + ("one\"two", "literal"), + ("one\"two", "literal-show"), + ("one\"two", "escape"), + ("\"one\\\"two\"", "c"), + ("'one\"two'", "shell"), + ("'one\"two'", "shell-show"), + ("'one\"two'", "shell-always"), + ("'one\"two'", "shell-always-show"), + ("'one\"two'", "shell-escape"), + ("'one\"two'", "shell-escape-always"), + ], + ); + + // One single quote + check_names( + "one\'two", + vec![ + ("one'two", "literal"), + ("one'two", "literal-show"), + ("one'two", "escape"), + ("\"one'two\"", "c"), + ("\"one'two\"", "shell"), + ("\"one'two\"", "shell-show"), + ("\"one'two\"", "shell-always"), + ("\"one'two\"", "shell-always-show"), + ("\"one'two\"", "shell-escape"), + ("\"one'two\"", "shell-escape-always"), + ], + ); + + // One single quote and one double quote + check_names( + "one'two\"three", + vec![ + ("one'two\"three", "literal"), + ("one'two\"three", "literal-show"), + ("one'two\"three", "escape"), + ("\"one'two\\\"three\"", "c"), + ("'one'\\''two\"three'", "shell"), + ("'one'\\''two\"three'", "shell-show"), + ("'one'\\''two\"three'", "shell-always"), + ("'one'\\''two\"three'", "shell-always-show"), + ("'one'\\''two\"three'", "shell-escape"), + ("'one'\\''two\"three'", "shell-escape-always"), + ], + ); + + // Consecutive quotes + check_names( + "one''two\"\"three", + vec![ + ("one''two\"\"three", "literal"), + ("one''two\"\"three", "literal-show"), + ("one''two\"\"three", "escape"), + ("\"one''two\\\"\\\"three\"", "c"), + ("'one'\\'''\\''two\"\"three'", "shell"), + ("'one'\\'''\\''two\"\"three'", "shell-show"), + ("'one'\\'''\\''two\"\"three'", "shell-always"), + ("'one'\\'''\\''two\"\"three'", "shell-always-show"), + ("'one'\\'''\\''two\"\"three'", "shell-escape"), + ("'one'\\'''\\''two\"\"three'", "shell-escape-always"), + ], + ); + } + + #[test] + fn test_control_chars() { + // A simple newline + check_names( + "one\ntwo", + vec![ + ("one?two", "literal"), + ("one\ntwo", "literal-show"), + ("one\\ntwo", "escape"), + ("\"one\\ntwo\"", "c"), + ("one?two", "shell"), + ("one\ntwo", "shell-show"), + ("'one?two'", "shell-always"), + ("'one\ntwo'", "shell-always-show"), + ("'one'$'\\n''two'", "shell-escape"), + ("'one'$'\\n''two'", "shell-escape-always"), + ], + ); + + // The first 16 control characters. NUL is also included, even though it is of + // no importance for file names. + check_names( + "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F", + vec![ + ("????????????????", "literal"), + ( + "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F", + "literal-show", + ), + ( + "\\000\\001\\002\\003\\004\\005\\006\\a\\b\\t\\n\\v\\f\\r\\016\\017", + "escape", + ), + ( + "\"\\000\\001\\002\\003\\004\\005\\006\\a\\b\\t\\n\\v\\f\\r\\016\\017\"", + "c", + ), + ("????????????????", "shell"), + ( + "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F", + "shell-show", + ), + ("'????????????????'", "shell-always"), + ( + "'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F'", + "shell-always-show", + ), + ( + "''$'\\000\\001\\002\\003\\004\\005\\006\\a\\b\\t\\n\\v\\f\\r\\016\\017'", + "shell-escape", + ), + ( + "''$'\\000\\001\\002\\003\\004\\005\\006\\a\\b\\t\\n\\v\\f\\r\\016\\017'", + "shell-escape-always", + ), + ], + ); + + // The last 16 control characters. + check_names( + "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F", + vec![ + ("????????????????", "literal"), + ( + "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F", + "literal-show", + ), + ( + "\\020\\021\\022\\023\\024\\025\\026\\027\\030\\031\\032\\033\\034\\035\\036\\037", + "escape", + ), + ( + "\"\\020\\021\\022\\023\\024\\025\\026\\027\\030\\031\\032\\033\\034\\035\\036\\037\"", + "c", + ), + ("????????????????", "shell"), + ( + "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F", + "shell-show", + ), + ("'????????????????'", "shell-always"), + ( + "'\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F'", + "shell-always-show", + ), + ( + "''$'\\020\\021\\022\\023\\024\\025\\026\\027\\030\\031\\032\\033\\034\\035\\036\\037'", + "shell-escape", + ), + ( + "''$'\\020\\021\\022\\023\\024\\025\\026\\027\\030\\031\\032\\033\\034\\035\\036\\037'", + "shell-escape-always", + ), + ], + ); + + // DEL + check_names( + "\x7F", + vec![ + ("?", "literal"), + ("\x7F", "literal-show"), + ("\\177", "escape"), + ("\"\\177\"", "c"), + ("?", "shell"), + ("\x7F", "shell-show"), + ("'?'", "shell-always"), + ("'\x7F'", "shell-always-show"), + ("''$'\\177'", "shell-escape"), + ("''$'\\177'", "shell-escape-always"), + ], + ); + } + + #[test] + fn test_question_mark() { + // A question mark must force quotes in shell and shell-always, unless + // it is in place of a control character (that case is already covered + // in other tests) + check_names( + "one?two", + vec![ + ("one?two", "literal"), + ("one?two", "literal-show"), + ("one?two", "escape"), + ("\"one?two\"", "c"), + ("'one?two'", "shell"), + ("'one?two'", "shell-show"), + ("'one?two'", "shell-always"), + ("'one?two'", "shell-always-show"), + ("'one?two'", "shell-escape"), + ("'one?two'", "shell-escape-always"), + ], + ); + } +} diff --git a/src/uu/ls/src/version_cmp.rs b/src/uu/ls/src/version_cmp.rs new file mode 100644 index 000000000..3cd5989f1 --- /dev/null +++ b/src/uu/ls/src/version_cmp.rs @@ -0,0 +1,304 @@ +use std::{cmp::Ordering, path::PathBuf}; + +/// Compare pathbufs in a way that matches the GNU version sort, meaning that +/// numbers get sorted in a natural way. +pub(crate) fn version_cmp(a: &PathBuf, b: &PathBuf) -> Ordering { + let a_string = a.to_string_lossy(); + let b_string = b.to_string_lossy(); + let mut a = a_string.chars().peekable(); + let mut b = b_string.chars().peekable(); + + // The order determined from the number of leading zeroes. + // This is used if the filenames are equivalent up to leading zeroes. + let mut leading_zeroes = Ordering::Equal; + + loop { + match (a.next(), b.next()) { + // If the characters are both numerical. We collect the rest of the number + // and parse them to u64's and compare them. + (Some(a_char @ '0'..='9'), Some(b_char @ '0'..='9')) => { + let mut a_leading_zeroes = 0; + if a_char == '0' { + a_leading_zeroes = 1; + while let Some('0') = a.peek() { + a_leading_zeroes += 1; + a.next(); + } + } + + let mut b_leading_zeroes = 0; + if b_char == '0' { + b_leading_zeroes = 1; + while let Some('0') = b.peek() { + b_leading_zeroes += 1; + b.next(); + } + } + // The first different number of leading zeros determines the order + // so if it's already been determined by a previous number, we leave + // it as that ordering. + // It's b.cmp(&a), because the *largest* number of leading zeros + // should go first + if leading_zeroes == Ordering::Equal { + leading_zeroes = b_leading_zeroes.cmp(&a_leading_zeroes); + } + + let mut a_str = String::new(); + let mut b_str = String::new(); + if a_char != '0' { + a_str.push(a_char); + } + if b_char != '0' { + b_str.push(b_char); + } + + // Unwrapping here is fine because we only call next if peek returns + // Some(_), so next should also return Some(_). + while let Some('0'..='9') = a.peek() { + a_str.push(a.next().unwrap()); + } + + while let Some('0'..='9') = b.peek() { + b_str.push(b.next().unwrap()); + } + + // Since the leading zeroes are stripped, the length can be + // used to compare the numbers. + match a_str.len().cmp(&b_str.len()) { + Ordering::Equal => {} + x => return x, + } + + // At this point, leading zeroes are stripped and the lengths + // are equal, meaning that the strings can be compared using + // the standard compare function. + match a_str.cmp(&b_str) { + Ordering::Equal => {} + x => return x, + } + } + // If there are two characters we just compare the characters + (Some(a_char), Some(b_char)) => match a_char.cmp(&b_char) { + Ordering::Equal => {} + x => return x, + }, + // Otherise, we compare the options (because None < Some(_)) + (a_opt, b_opt) => match a_opt.cmp(&b_opt) { + // If they are completely equal except for leading zeroes, we use the leading zeroes. + Ordering::Equal => return leading_zeroes, + x => return x, + }, + } + } +} + +#[cfg(test)] +mod tests { + use crate::version_cmp::version_cmp; + use std::cmp::Ordering; + use std::path::PathBuf; + #[test] + fn test_version_cmp() { + // Identical strings + assert_eq!( + version_cmp(&PathBuf::from("hello"), &PathBuf::from("hello")), + Ordering::Equal + ); + + assert_eq!( + version_cmp(&PathBuf::from("file12"), &PathBuf::from("file12")), + Ordering::Equal + ); + + assert_eq!( + version_cmp( + &PathBuf::from("file12-suffix"), + &PathBuf::from("file12-suffix") + ), + Ordering::Equal + ); + + assert_eq!( + version_cmp( + &PathBuf::from("file12-suffix24"), + &PathBuf::from("file12-suffix24") + ), + Ordering::Equal + ); + + // Shortened names + assert_eq!( + version_cmp(&PathBuf::from("world"), &PathBuf::from("wo")), + Ordering::Greater, + ); + + assert_eq!( + version_cmp(&PathBuf::from("hello10wo"), &PathBuf::from("hello10world")), + Ordering::Less, + ); + + // Simple names + assert_eq!( + version_cmp(&PathBuf::from("world"), &PathBuf::from("hello")), + Ordering::Greater, + ); + + assert_eq!( + version_cmp(&PathBuf::from("hello"), &PathBuf::from("world")), + Ordering::Less + ); + + assert_eq!( + version_cmp(&PathBuf::from("apple"), &PathBuf::from("ant")), + Ordering::Greater + ); + + assert_eq!( + version_cmp(&PathBuf::from("ant"), &PathBuf::from("apple")), + Ordering::Less + ); + + // Uppercase letters + assert_eq!( + version_cmp(&PathBuf::from("Beef"), &PathBuf::from("apple")), + Ordering::Less, + "Uppercase letters are sorted before all lowercase letters" + ); + + assert_eq!( + version_cmp(&PathBuf::from("Apple"), &PathBuf::from("apple")), + Ordering::Less + ); + + assert_eq!( + version_cmp(&PathBuf::from("apple"), &PathBuf::from("aPple")), + Ordering::Greater + ); + + // Numbers + assert_eq!( + version_cmp(&PathBuf::from("100"), &PathBuf::from("20")), + Ordering::Greater, + "Greater numbers are greater even if they start with a smaller digit", + ); + + assert_eq!( + version_cmp(&PathBuf::from("20"), &PathBuf::from("20")), + Ordering::Equal, + "Equal numbers are equal" + ); + + assert_eq!( + version_cmp(&PathBuf::from("15"), &PathBuf::from("200")), + Ordering::Less, + "Small numbers are smaller" + ); + + // Comparing numbers with other characters + assert_eq!( + version_cmp(&PathBuf::from("1000"), &PathBuf::from("apple")), + Ordering::Less, + "Numbers are sorted before other characters" + ); + + assert_eq!( + version_cmp(&PathBuf::from("file1000"), &PathBuf::from("fileapple")), + Ordering::Less, + "Numbers in the middle of the name are sorted before other characters" + ); + + // Leading zeroes + assert_eq!( + version_cmp(&PathBuf::from("012"), &PathBuf::from("12")), + Ordering::Less, + "A single leading zero can make a difference" + ); + + assert_eq!( + version_cmp(&PathBuf::from("000800"), &PathBuf::from("0000800")), + Ordering::Greater, + "Leading number of zeroes is used even if both non-zero number of zeros" + ); + + // Numbers and other characters combined + assert_eq!( + version_cmp(&PathBuf::from("ab10"), &PathBuf::from("aa11")), + Ordering::Greater + ); + + assert_eq!( + version_cmp(&PathBuf::from("aa10"), &PathBuf::from("aa11")), + Ordering::Less, + "Numbers after other characters are handled correctly." + ); + + assert_eq!( + version_cmp(&PathBuf::from("aa2"), &PathBuf::from("aa100")), + Ordering::Less, + "Numbers after alphabetical characters are handled correctly." + ); + + assert_eq!( + version_cmp(&PathBuf::from("aa10bb"), &PathBuf::from("aa11aa")), + Ordering::Less, + "Number is used even if alphabetical characters after it differ." + ); + + assert_eq!( + version_cmp(&PathBuf::from("aa10aa0010"), &PathBuf::from("aa11aa1")), + Ordering::Less, + "Second number is ignored if the first number differs." + ); + + assert_eq!( + version_cmp(&PathBuf::from("aa10aa0010"), &PathBuf::from("aa10aa1")), + Ordering::Greater, + "Second number is used if the rest is equal." + ); + + assert_eq!( + version_cmp(&PathBuf::from("aa10aa0010"), &PathBuf::from("aa00010aa1")), + Ordering::Greater, + "Second number is used if the rest is equal up to leading zeroes of the first number." + ); + + assert_eq!( + version_cmp(&PathBuf::from("aa10aa0022"), &PathBuf::from("aa010aa022")), + Ordering::Greater, + "The leading zeroes of the first number has priority." + ); + + assert_eq!( + version_cmp(&PathBuf::from("aa10aa0022"), &PathBuf::from("aa10aa022")), + Ordering::Less, + "The leading zeroes of other numbers than the first are used." + ); + + assert_eq!( + version_cmp(&PathBuf::from("file-1.4"), &PathBuf::from("file-1.13")), + Ordering::Less, + "Periods are handled as normal text, not as a decimal point." + ); + + // Greater than u64::Max + // u64 == 18446744073709551615 so this should be plenty: + // 20000000000000000000000 + assert_eq!( + version_cmp( + &PathBuf::from("aa2000000000000000000000bb"), + &PathBuf::from("aa002000000000000000000001bb") + ), + Ordering::Less, + "Numbers larger than u64::MAX are handled correctly without crashing" + ); + + assert_eq!( + version_cmp( + &PathBuf::from("aa2000000000000000000000bb"), + &PathBuf::from("aa002000000000000000000000bb") + ), + Ordering::Greater, + "Leading zeroes for numbers larger than u64::MAX are handled correctly without crashing" + ); + } +} diff --git a/src/uu/mkdir/Cargo.toml b/src/uu/mkdir/Cargo.toml index 89c391fb0..a8d374bf9 100644 --- a/src/uu/mkdir/Cargo.toml +++ b/src/uu/mkdir/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_mkdir" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "mkdir ~ (uutils) create DIRECTORY" @@ -15,10 +15,10 @@ edition = "2018" path = "src/mkdir.rs" [dependencies] -getopts = "0.2.18" +clap = "2.33" libc = "0.2.42" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary", features=["fs", "mode"] } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["fs", "mode"] } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "mkdir" diff --git a/src/uu/mkdir/src/mkdir.rs b/src/uu/mkdir/src/mkdir.rs index c1c03d8f2..6b9fd68ea 100644 --- a/src/uu/mkdir/src/mkdir.rs +++ b/src/uu/mkdir/src/mkdir.rs @@ -5,53 +5,77 @@ // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -extern crate getopts; - #[macro_use] extern crate uucore; +use clap::{App, Arg}; use std::fs; use std::path::Path; -static NAME: &str = "mkdir"; +static ABOUT: &str = "Create the given DIRECTORY(ies) if they do not exist"; static VERSION: &str = env!("CARGO_PKG_VERSION"); +static OPT_MODE: &str = "mode"; +static OPT_PARENTS: &str = "parents"; +static OPT_VERBOSE: &str = "verbose"; + +static ARG_DIRS: &str = "dirs"; + +fn get_usage() -> String { + format!("{0} [OPTION]... [USER]", executable!()) +} /** * Handles option parsing */ pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); - - let mut opts = getopts::Options::new(); + let usage = get_usage(); // Linux-specific options, not implemented // opts.optflag("Z", "context", "set SELinux security context" + // " of each created directory to CTX"), - opts.optopt("m", "mode", "set file mode", "755"); - opts.optflag("p", "parents", "make parent directories as needed"); - opts.optflag("v", "verbose", "print a message for each printed directory"); - opts.optflag("h", "help", "display this help"); - opts.optflag("V", "version", "display this version"); + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(&usage[..]) + .arg( + Arg::with_name(OPT_MODE) + .short("m") + .long(OPT_MODE) + .help("set file mode") + .default_value("755"), + ) + .arg( + Arg::with_name(OPT_PARENTS) + .short("p") + .long(OPT_PARENTS) + .alias("parent") + .help("make parent directories as needed"), + ) + .arg( + Arg::with_name(OPT_VERBOSE) + .short("v") + .long(OPT_VERBOSE) + .help("print a message for each printed directory"), + ) + .arg( + Arg::with_name(ARG_DIRS) + .multiple(true) + .takes_value(true) + .min_values(1), + ) + .get_matches_from(args); - let matches = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(f) => crash!(1, "Invalid options\n{}", f), - }; + let dirs: Vec = matches + .values_of(ARG_DIRS) + .map(|v| v.map(ToString::to_string).collect()) + .unwrap_or_default(); - if args.len() == 1 || matches.opt_present("help") { - print_help(&opts); - return 0; - } - if matches.opt_present("version") { - println!("{} {}", NAME, VERSION); - return 0; - } - let verbose = matches.opt_present("verbose"); - let recursive = matches.opt_present("parents"); + let verbose = matches.is_present(OPT_VERBOSE); + let recursive = matches.is_present(OPT_PARENTS); // Translate a ~str in octal form to u16, default to 755 // Not tested on Windows - let mode_match = matches.opts_str(&["mode".to_owned()]); + let mode_match = matches.value_of(OPT_MODE); let mode: u16 = match mode_match { Some(m) => { let res: Option = u16::from_str_radix(&m, 8).ok(); @@ -60,26 +84,12 @@ pub fn uumain(args: impl uucore::Args) -> i32 { _ => crash!(1, "no mode given"), } } - _ => 0o755 as u16, + _ => 0o755_u16, }; - let dirs = matches.free; - if dirs.is_empty() { - crash!(1, "missing operand"); - } exec(dirs, recursive, mode, verbose) } -fn print_help(opts: &getopts::Options) { - println!("{} {}", NAME, VERSION); - println!(); - println!("Usage:"); - print!( - "{}", - opts.usage("Create the given DIRECTORY(ies) if they do not exist") - ); -} - /** * Create the list of new directories */ @@ -120,7 +130,7 @@ fn mkdir(path: &Path, recursive: bool, mode: u16, verbose: bool) -> i32 { } if verbose { - show_info!("created directory '{}'", path.display()); + println!("{}: created directory '{}'", executable!(), path.display()); } #[cfg(any(unix, target_os = "redox"))] diff --git a/src/uu/mkfifo/Cargo.toml b/src/uu/mkfifo/Cargo.toml index c06be0dad..d66003b10 100644 --- a/src/uu/mkfifo/Cargo.toml +++ b/src/uu/mkfifo/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_mkfifo" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "mkfifo ~ (uutils) create FIFOs (named pipes)" @@ -15,10 +15,10 @@ edition = "2018" path = "src/mkfifo.rs" [dependencies] -getopts = "0.2.18" +clap = "2.33" libc = "0.2.42" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "mkfifo" diff --git a/src/uu/mkfifo/src/mkfifo.rs b/src/uu/mkfifo/src/mkfifo.rs index 1906e604a..14701af4d 100644 --- a/src/uu/mkfifo/src/mkfifo.rs +++ b/src/uu/mkfifo/src/mkfifo.rs @@ -5,62 +5,58 @@ // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -extern crate getopts; -extern crate libc; - #[macro_use] extern crate uucore; +use clap::{App, Arg}; use libc::mkfifo; use std::ffi::CString; -use std::io::Error; static NAME: &str = "mkfifo"; static VERSION: &str = env!("CARGO_PKG_VERSION"); +static USAGE: &str = "mkfifo [OPTION]... NAME..."; +static SUMMARY: &str = "Create a FIFO with the given name."; + +mod options { + pub static MODE: &str = "mode"; + pub static SE_LINUX_SECURITY_CONTEXT: &str = "Z"; + pub static CONTEXT: &str = "context"; + pub static FIFO: &str = "fifo"; +} pub fn uumain(args: impl uucore::Args) -> i32 { let args = args.collect_str(); - let mut opts = getopts::Options::new(); + let matches = App::new(executable!()) + .name(NAME) + .version(VERSION) + .usage(USAGE) + .about(SUMMARY) + .arg( + Arg::with_name(options::MODE) + .short("m") + .long(options::MODE) + .help("file permissions for the fifo") + .default_value("0666") + .value_name("0666"), + ) + .arg( + Arg::with_name(options::SE_LINUX_SECURITY_CONTEXT) + .short(options::SE_LINUX_SECURITY_CONTEXT) + .help("set the SELinux security context to default type") + ) + .arg(Arg::with_name(options::CONTEXT).long(options::CONTEXT).value_name("CTX").help("like -Z, or if CTX is specified then set the SELinux\nor SMACK security context to CTX")) + .arg(Arg::with_name(options::FIFO).hidden(true).multiple(true)) + .get_matches_from(args); - opts.optopt( - "m", - "mode", - "file permissions for the fifo", - "(default 0666)", - ); - opts.optflag("h", "help", "display this help and exit"); - opts.optflag("V", "version", "output version information and exit"); - - let matches = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(err) => panic!("{}", err), - }; - - if matches.opt_present("version") { - println!("{} {}", NAME, VERSION); - return 0; + if matches.is_present(options::CONTEXT) { + crash!(1, "--context is not implemented"); + } + if matches.is_present(options::SE_LINUX_SECURITY_CONTEXT) { + crash!(1, "-Z is not implemented"); } - if matches.opt_present("help") || matches.free.is_empty() { - let msg = format!( - "{0} {1} - -Usage: - {0} [OPTIONS] NAME... - -Create a FIFO with the given name.", - NAME, VERSION - ); - - print!("{}", opts.usage(&msg)); - if matches.free.is_empty() { - return 1; - } - return 0; - } - - let mode = match matches.opt_str("m") { + let mode = match matches.value_of(options::MODE) { Some(m) => match usize::from_str_radix(&m, 8) { Ok(m) => m, Err(e) => { @@ -71,21 +67,22 @@ Create a FIFO with the given name.", None => 0o666, }; - let mut exit_status = 0; - for f in &matches.free { + let fifos: Vec = match matches.values_of(options::FIFO) { + Some(v) => v.clone().map(|s| s.to_owned()).collect(), + None => crash!(1, "missing operand"), + }; + + let mut exit_code = 0; + for f in fifos { let err = unsafe { let name = CString::new(f.as_bytes()).unwrap(); mkfifo(name.as_ptr(), mode as libc::mode_t) }; if err == -1 { - show_error!( - "creating '{}': {}", - f, - Error::last_os_error().raw_os_error().unwrap() - ); - exit_status = 1; + show_error!("cannot create fifo '{}': File exists", f); + exit_code = 1; } } - exit_status + exit_code } diff --git a/src/uu/mknod/Cargo.toml b/src/uu/mknod/Cargo.toml index d7058b883..2c3ac8fb9 100644 --- a/src/uu/mknod/Cargo.toml +++ b/src/uu/mknod/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_mknod" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "mknod ~ (uutils) create special file NAME of TYPE" @@ -18,8 +18,8 @@ path = "src/mknod.rs" [dependencies] getopts = "0.2.18" libc = "^0.2.42" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary", features=["mode"] } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["mode"] } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "mknod" diff --git a/src/uu/mknod/src/mknod.rs b/src/uu/mknod/src/mknod.rs index 59d6b129f..1343501bb 100644 --- a/src/uu/mknod/src/mknod.rs +++ b/src/uu/mknod/src/mknod.rs @@ -7,9 +7,6 @@ // spell-checker:ignore (ToDO) parsemode makedev sysmacros makenod newmode perror IFBLK IFCHR IFIFO -extern crate getopts; -extern crate libc; - mod parsemode; #[macro_use] diff --git a/src/uu/mknod/src/parsemode.rs b/src/uu/mknod/src/parsemode.rs index b180bb8e5..8f8f9af61 100644 --- a/src/uu/mknod/src/parsemode.rs +++ b/src/uu/mknod/src/parsemode.rs @@ -1,6 +1,5 @@ // spell-checker:ignore (ToDO) fperm -extern crate libc; use libc::{mode_t, S_IRGRP, S_IROTH, S_IRUSR, S_IWGRP, S_IWOTH, S_IWUSR}; use uucore::mode; diff --git a/src/uu/mktemp/Cargo.toml b/src/uu/mktemp/Cargo.toml index c9a937ba1..c669f0acc 100644 --- a/src/uu/mktemp/Cargo.toml +++ b/src/uu/mktemp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_mktemp" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "mktemp ~ (uutils) create and display a temporary file or directory from TEMPLATE" @@ -15,11 +15,11 @@ edition = "2018" path = "src/mktemp.rs" [dependencies] -getopts = "0.2.18" +clap = "2.33" rand = "0.5" -tempfile = "3.0" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +tempfile = "3.1" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "mktemp" diff --git a/src/uu/mktemp/src/mktemp.rs b/src/uu/mktemp/src/mktemp.rs index 8cae1f70d..ed767ffe0 100644 --- a/src/uu/mktemp/src/mktemp.rs +++ b/src/uu/mktemp/src/mktemp.rs @@ -8,13 +8,11 @@ // spell-checker:ignore (ToDO) tempfile tempdir SUFF TMPDIR tmpname -extern crate getopts; -extern crate rand; -extern crate tempfile; - #[macro_use] extern crate uucore; +use clap::{App, Arg}; + use std::env; use std::iter; use std::mem::forget; @@ -25,78 +23,118 @@ use tempfile::Builder; mod tempdir; -static NAME: &str = "mktemp"; +static ABOUT: &str = "create a temporary file or directory."; static VERSION: &str = env!("CARGO_PKG_VERSION"); + static DEFAULT_TEMPLATE: &str = "tmp.XXXXXXXXXX"; +static OPT_DIRECTORY: &str = "directory"; +static OPT_DRY_RUN: &str = "dry-run"; +static OPT_QUIET: &str = "quiet"; +static OPT_SUFFIX: &str = "suffix"; +static OPT_TMPDIR: &str = "tmpdir"; +static OPT_T: &str = "t"; + +static ARG_TEMPLATE: &str = "template"; + +fn get_usage() -> String { + format!("{0} [OPTION]... [TEMPLATE]", executable!()) +} + pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); + let usage = get_usage(); - let mut opts = getopts::Options::new(); - opts.optflag("d", "directory", "Make a directory instead of a file"); - opts.optflag( - "u", - "dry-run", - "do not create anything; merely print a name (unsafe)", - ); - opts.optflag("q", "quiet", "Fail silently if an error occurs."); - opts.optopt( - "", - "suffix", - "append SUFF to TEMPLATE; SUFF must not contain a path separator. \ - This option is implied if TEMPLATE does not end with X.", - "SUFF", - ); - opts.optopt( - "p", - "tmpdir", - "interpret TEMPLATE relative to DIR; if DIR is not specified, use \ - $TMPDIR if set, else /tmp. With this option, TEMPLATE must not \ - be an absolute name; unlike with -t, TEMPLATE may contain \ - slashes, but mktemp creates only the final component", - "DIR", - ); - // deprecated option of GNU coreutils - // opts.optflag("t", "", "Generate a template (using the supplied prefix and TMPDIR if set) \ - // to create a filename template"); - opts.optflag("", "help", "Print this help and exit"); - opts.optflag("", "version", "print the version and exit"); + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(&usage[..]) + .arg( + Arg::with_name(OPT_DIRECTORY) + .short("d") + .long(OPT_DIRECTORY) + .help("Make a directory instead of a file"), + ) + .arg( + Arg::with_name(OPT_DRY_RUN) + .short("u") + .long(OPT_DRY_RUN) + .help("do not create anything; merely print a name (unsafe)"), + ) + .arg( + Arg::with_name(OPT_QUIET) + .short("q") + .long("quiet") + .help("Fail silently if an error occurs."), + ) + .arg( + Arg::with_name(OPT_SUFFIX) + .long(OPT_SUFFIX) + .help( + "append SUFF to TEMPLATE; SUFF must not contain a path separator. \ + This option is implied if TEMPLATE does not end with X.", + ) + .value_name("SUFF"), + ) + .arg( + Arg::with_name(OPT_TMPDIR) + .short("p") + .long(OPT_TMPDIR) + .help( + "interpret TEMPLATE relative to DIR; if DIR is not specified, use \ + $TMPDIR if set, else /tmp. With this option, TEMPLATE must not \ + be an absolute name; unlike with -t, TEMPLATE may contain \ + slashes, but mktemp creates only the final component", + ) + .value_name("DIR"), + ) + .arg(Arg::with_name(OPT_T).short(OPT_T).help( + "Generate a template (using the supplied prefix and TMPDIR if set) \ + to create a filename template [deprecated]", + )) + .arg( + Arg::with_name(ARG_TEMPLATE) + .multiple(false) + .takes_value(true) + .max_values(1) + .default_value(DEFAULT_TEMPLATE), + ) + .get_matches_from(args); - // >> early return options - let matches = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(f) => crash!(1, "Invalid options\n{}", f), - }; + let template = matches.value_of(ARG_TEMPLATE).unwrap(); + let tmpdir = matches.value_of(OPT_TMPDIR).unwrap_or_default(); - if matches.opt_present("help") { - print_help(&opts); - return 0; - } - if matches.opt_present("version") { - println!("{} {}", NAME, VERSION); - return 0; - } - - if 1 < matches.free.len() { - crash!(1, "Too many templates"); - } - // << - - let make_dir = matches.opt_present("directory"); - let dry_run = matches.opt_present("dry-run"); - let suffix_opt = matches.opt_str("suffix"); - let suppress_file_err = matches.opt_present("quiet"); - - let template = if matches.free.is_empty() { - DEFAULT_TEMPLATE + let (template, mut tmpdir) = if matches.is_present(OPT_TMPDIR) + && !PathBuf::from(tmpdir).is_dir() // if a temp dir is provided, it must be an actual path + && tmpdir.contains("XXX") + // If this is a template, it has to contain at least 3 X + && template == DEFAULT_TEMPLATE + // That means that clap does not think we provided a template + { + // Special case to workaround a limitation of clap when doing + // mktemp --tmpdir apt-key-gpghome.XXX + // The behavior should be + // mktemp --tmpdir $TMPDIR apt-key-gpghome.XX + // As --tmpdir is empty + // + // Fixed in clap 3 + // See https://github.com/clap-rs/clap/pull/1587 + let tmp = env::temp_dir(); + (tmpdir, tmp) + } else if !matches.is_present(OPT_TMPDIR) { + let tmp = env::temp_dir(); + (template, tmp) } else { - &matches.free[0][..] + (template, PathBuf::from(tmpdir)) }; + let make_dir = matches.is_present(OPT_DIRECTORY); + let dry_run = matches.is_present(OPT_DRY_RUN); + let suppress_file_err = matches.is_present(OPT_QUIET); + let (prefix, rand, suffix) = match parse_template(template) { - Some((p, r, s)) => match suffix_opt { + Some((p, r, s)) => match matches.value_of(OPT_SUFFIX) { Some(suf) => { - if s == "" { + if s.is_empty() { (p, r, suf) } else { crash!( @@ -105,9 +143,9 @@ pub fn uumain(args: impl uucore::Args) -> i32 { ) } } - None => (p, r, s.to_owned()), + None => (p, r, s), }, - None => ("", 0, "".to_owned()), + None => ("", 0, ""), }; if rand < 3 { @@ -118,18 +156,16 @@ pub fn uumain(args: impl uucore::Args) -> i32 { crash!(1, "suffix cannot contain any path separators"); } - let tmpdir = match matches.opt_str("tmpdir") { - Some(s) => { - if PathBuf::from(prefix).is_absolute() { - show_info!( - "invalid template, ‘{}’; with --tmpdir, it may not be absolute", - template - ); - return 1; - } - PathBuf::from(s) - } - None => env::temp_dir(), + if matches.is_present(OPT_TMPDIR) && PathBuf::from(prefix).is_absolute() { + show_info!( + "invalid template, ‘{}’; with --tmpdir, it may not be absolute", + template + ); + return 1; + }; + + if matches.is_present(OPT_T) { + tmpdir = env::temp_dir() }; if dry_run { @@ -139,21 +175,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } } -fn print_help(opts: &getopts::Options) { - let usage = format!( - " Create a temporary file or directory, safely, and print its name. -TEMPLATE must contain at least 3 consecutive 'X's in last component. -If TEMPLATE is not specified, use {}, and --tmpdir is implied", - DEFAULT_TEMPLATE - ); - - println!("{} {}", NAME, VERSION); - println!("SYNOPSIS"); - println!(" {} [OPTION]... [FILE]", NAME); - println!("Usage:"); - print!("{}", opts.usage(&usage[..])); -} - fn parse_template(temp: &str) -> Option<(&str, usize, &str)> { let right = match temp.rfind('X') { Some(r) => r + 1, @@ -208,18 +229,17 @@ fn exec( } Err(e) => { if !quiet { - show_info!("{}", e); + show_info!("{}: {}", e, tmpdir.display()); } return 1; } } } - let tmpfile = Builder::new() + .prefix(prefix) .rand_bytes(rand) .suffix(suffix) .tempfile_in(tmpdir); - let tmpfile = match tmpfile { Ok(f) => f, Err(e) => { diff --git a/src/uu/mktemp/src/tempdir.rs b/src/uu/mktemp/src/tempdir.rs index 10c0bb62a..1b6c9d7b3 100644 --- a/src/uu/mktemp/src/tempdir.rs +++ b/src/uu/mktemp/src/tempdir.rs @@ -2,7 +2,6 @@ // Mainly taken from crate `tempdir` -extern crate rand; use rand::distributions::Alphanumeric; use rand::{thread_rng, Rng}; diff --git a/src/uu/more/Cargo.toml b/src/uu/more/Cargo.toml index 56af9e2df..1f4bfed68 100644 --- a/src/uu/more/Cargo.toml +++ b/src/uu/more/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_more" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "more ~ (uutils) input perusal filter" @@ -15,16 +15,16 @@ edition = "2018" path = "src/more.rs" [dependencies] -getopts = "0.2.18" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +clap = "2.33" +uucore = { version = ">=0.0.7", package = "uucore", path = "../../uucore" } +uucore_procs = { version = ">=0.0.5", package = "uucore_procs", path = "../../uucore_procs" } [target.'cfg(target_os = "redox")'.dependencies] redox_termios = "0.1" redox_syscall = "0.1" [target.'cfg(all(unix, not(target_os = "fuchsia")))'.dependencies] -nix = "0.8.1" +nix = "<=0.13" [[bin]] name = "more" diff --git a/src/uu/more/src/more.rs b/src/uu/more/src/more.rs index 5e0646b78..59e7d0faa 100644 --- a/src/uu/more/src/more.rs +++ b/src/uu/more/src/more.rs @@ -7,97 +7,76 @@ // spell-checker:ignore (ToDO) lflag ICANON tcgetattr tcsetattr TCSADRAIN -extern crate getopts; - #[macro_use] extern crate uucore; -use getopts::Options; use std::fs::File; -use std::io::{stdout, Read, Write}; +use std::io::{stdin, stdout, BufRead, BufReader, Read, Write}; #[cfg(all(unix, not(target_os = "fuchsia")))] extern crate nix; #[cfg(all(unix, not(target_os = "fuchsia")))] -use nix::sys::termios; +use nix::sys::termios::{self, LocalFlags, SetArg}; #[cfg(target_os = "redox")] extern crate redox_termios; #[cfg(target_os = "redox")] extern crate syscall; -#[derive(Clone, Eq, PartialEq)] -pub enum Mode { - More, - Help, - Version, +use clap::{App, Arg, ArgMatches}; + +static VERSION: &str = env!("CARGO_PKG_VERSION"); +static ABOUT: &str = "A file perusal filter for CRT viewing."; + +mod options { + pub const FILE: &str = "file"; } -static NAME: &str = "more"; -static VERSION: &str = env!("CARGO_PKG_VERSION"); +fn get_usage() -> String { + format!("{} [options] ...", executable!()) +} pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); + let usage = get_usage(); - let mut opts = Options::new(); + let matches = App::new(executable!()) + .version(VERSION) + .usage(usage.as_str()) + .about(ABOUT) + .arg( + Arg::with_name(options::FILE) + .number_of_values(1) + .multiple(true), + ) + .get_matches_from(args); // FixME: fail without panic for now; but `more` should work with no arguments (ie, for piped input) - if args.len() < 2 { - println!("{}: incorrect usage", args[0]); + if let None | Some("-") = matches.value_of(options::FILE) { + show_usage_error!("Reading from stdin isn't supported yet."); return 1; } - opts.optflag("h", "help", "display this help and exit"); - opts.optflag("v", "version", "output version information and exit"); - - let matches = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(e) => { - show_error!("{}", e); - panic!() + if let Some(x) = matches.value_of(options::FILE) { + let path = std::path::Path::new(x); + if path.is_dir() { + show_usage_error!("'{}' is a directory.", x); + return 1; } - }; - let usage = opts.usage("more TARGET."); - let mode = if matches.opt_present("version") { - Mode::Version - } else if matches.opt_present("help") { - Mode::Help - } else { - Mode::More - }; - - match mode { - Mode::More => more(matches), - Mode::Help => help(&usage), - Mode::Version => version(), } + more(matches); + 0 } -fn version() { - println!("{} {}", NAME, VERSION); -} - -fn help(usage: &str) { - let msg = format!( - "{0} {1}\n\n\ - Usage: {0} TARGET\n \ - \n\ - {2}", - NAME, VERSION, usage - ); - println!("{}", msg); -} - #[cfg(all(unix, not(target_os = "fuchsia")))] fn setup_term() -> termios::Termios { let mut term = termios::tcgetattr(0).unwrap(); // Unset canonical mode, so we get characters immediately - term.c_lflag.remove(termios::ICANON); + term.local_flags.remove(LocalFlags::ICANON); // Disable local echo - term.c_lflag.remove(termios::ECHO); - termios::tcsetattr(0, termios::TCSADRAIN, &term).unwrap(); + term.local_flags.remove(LocalFlags::ECHO); + termios::tcsetattr(0, SetArg::TCSADRAIN, &term).unwrap(); term } @@ -112,8 +91,8 @@ fn setup_term() -> redox_termios::Termios { let mut term = redox_termios::Termios::default(); let fd = syscall::dup(0, b"termios").unwrap(); syscall::read(fd, &mut term).unwrap(); - term.c_lflag &= !redox_termios::ICANON; - term.c_lflag &= !redox_termios::ECHO; + term.local_flags &= !redox_termios::ICANON; + term.local_flags &= !redox_termios::ECHO; syscall::write(fd, &term).unwrap(); let _ = syscall::close(fd); term @@ -121,9 +100,9 @@ fn setup_term() -> redox_termios::Termios { #[cfg(all(unix, not(target_os = "fuchsia")))] fn reset_term(term: &mut termios::Termios) { - term.c_lflag.insert(termios::ICANON); - term.c_lflag.insert(termios::ECHO); - termios::tcsetattr(0, termios::TCSADRAIN, &term).unwrap(); + term.local_flags.insert(LocalFlags::ICANON); + term.local_flags.insert(LocalFlags::ECHO); + termios::tcsetattr(0, SetArg::TCSADRAIN, &term).unwrap(); } #[cfg(any(windows, target_os = "fuchsia"))] @@ -134,15 +113,17 @@ fn reset_term(_: &mut usize) {} fn reset_term(term: &mut redox_termios::Termios) { let fd = syscall::dup(0, b"termios").unwrap(); syscall::read(fd, term).unwrap(); - term.c_lflag |= redox_termios::ICANON; - term.c_lflag |= redox_termios::ECHO; + term.local_flags |= redox_termios::ICANON; + term.local_flags |= redox_termios::ECHO; syscall::write(fd, &term).unwrap(); let _ = syscall::close(fd); } -fn more(matches: getopts::Matches) { - let files = matches.free; - let mut f = File::open(files.first().unwrap()).unwrap(); +fn more(matches: ArgMatches) { + let mut f: Box = match matches.value_of(options::FILE) { + None | Some("-") => Box::new(BufReader::new(stdin())), + Some(filename) => Box::new(BufReader::new(File::open(filename).unwrap())), + }; let mut buffer = [0; 1024]; let mut term = setup_term(); diff --git a/src/uu/mv/Cargo.toml b/src/uu/mv/Cargo.toml index 708330800..8f1e7b9ee 100644 --- a/src/uu/mv/Cargo.toml +++ b/src/uu/mv/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_mv" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "mv ~ (uutils) move (rename) SOURCE to DESTINATION" @@ -15,10 +15,10 @@ edition = "2018" path = "src/mv.rs" [dependencies] -getopts = "0.2.18" +clap = "2.33" fs_extra = "1.1.0" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "mv" diff --git a/src/uu/mv/src/mv.rs b/src/uu/mv/src/mv.rs index d0d98dafa..b481aeebc 100644 --- a/src/uu/mv/src/mv.rs +++ b/src/uu/mv/src/mv.rs @@ -8,12 +8,10 @@ // spell-checker:ignore (ToDO) sourcepath targetpath -extern crate fs_extra; -extern crate getopts; - #[macro_use] extern crate uucore; +use clap::{App, Arg, ArgMatches}; use std::env; use std::fs; use std::io::{self, stdin}; @@ -25,9 +23,6 @@ use std::path::{Path, PathBuf}; use fs_extra::dir::{move_dir, CopyOptions as DirCopyOptions}; -static NAME: &str = "mv"; -static VERSION: &str = env!("CARGO_PKG_VERSION"); - pub struct Behavior { overwrite: OverwriteMode, backup: BackupMode, @@ -53,54 +48,128 @@ pub enum BackupMode { ExistingBackup, } +static ABOUT: &str = "Move SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY."; +static VERSION: &str = env!("CARGO_PKG_VERSION"); + +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"; +static OPT_VERBOSE: &str = "verbose"; + +static ARG_FILES: &str = "files"; + +fn get_usage() -> String { + format!( + "{0} [OPTION]... [-T] SOURCE DEST +{0} [OPTION]... SOURCE... DIRECTORY +{0} [OPTION]... -t DIRECTORY SOURCE...", + executable!() + ) +} + pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); + let usage = get_usage(); - let mut opts = getopts::Options::new(); + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(&usage[..]) + .arg( + Arg::with_name(OPT_BACKUP) + .long(OPT_BACKUP) + .help("make a backup of each existing destination file") + .takes_value(true) + .possible_value("simple") + .possible_value("never") + .possible_value("numbered") + .possible_value("t") + .possible_value("existing") + .possible_value("nil") + .possible_value("none") + .possible_value("off") + .value_name("CONTROL") + ) + .arg( + Arg::with_name(OPT_BACKUP_NO_ARG) + .short(OPT_BACKUP_NO_ARG) + .help("like --backup but does not accept an argument") + ) + .arg( + Arg::with_name(OPT_FORCE) + .short("f") + .long(OPT_FORCE) + .help("do not prompt before overwriting") + ) + .arg( + Arg::with_name(OPT_INTERACTIVE) + .short("i") + .long(OPT_INTERACTIVE) + .help("prompt before override") + ) + .arg( + Arg::with_name(OPT_NO_CLOBBER).short("n") + .long(OPT_NO_CLOBBER) + .help("do not overwrite an existing file") + ) + .arg( + Arg::with_name(OPT_STRIP_TRAILING_SLASHES) + .long(OPT_STRIP_TRAILING_SLASHES) + .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") + ) + .arg( + Arg::with_name(OPT_TARGET_DIRECTORY) + .short("t") + .long(OPT_TARGET_DIRECTORY) + .help("move all SOURCE arguments into DIRECTORY") + .takes_value(true) + .value_name("DIRECTORY") + .conflicts_with(OPT_NO_TARGET_DIRECTORY) + ) + .arg( + Arg::with_name(OPT_NO_TARGET_DIRECTORY) + .short("T") + .long(OPT_NO_TARGET_DIRECTORY). + help("treat DEST as a normal file") + ) + .arg( + Arg::with_name(OPT_UPDATE) + .short("u") + .long(OPT_UPDATE) + .help("move only when the SOURCE file is newer than the destination file or when the destination file is missing") + ) + .arg( + Arg::with_name(OPT_VERBOSE) + .short("v") + .long(OPT_VERBOSE).help("explain what is being done") + ) + .arg( + Arg::with_name(ARG_FILES) + .multiple(true) + .takes_value(true) + .min_values(2) + .required(true) + ) + .get_matches_from(args); - opts.optflagopt( - "", - "backup", - "make a backup of each existing destination file", - "CONTROL", - ); - opts.optflag("b", "", "like --backup but does not accept an argument"); - opts.optflag("f", "force", "do not prompt before overwriting"); - opts.optflag("i", "interactive", "prompt before override"); - opts.optflag("n", "no-clobber", "do not overwrite an existing file"); - opts.optflag( - "", - "strip-trailing-slashes", - "remove any trailing slashes from each SOURCE\n \ - argument", - ); - opts.optopt("S", "suffix", "override the usual backup suffix", "SUFFIX"); - opts.optopt( - "t", - "target-directory", - "move all SOURCE arguments into DIRECTORY", - "DIRECTORY", - ); - opts.optflag("T", "no-target-directory", "treat DEST as a normal file"); - opts.optflag( - "u", - "update", - "move only when the SOURCE file is newer\n \ - than the destination file or when the\n \ - destination file is missing", - ); - opts.optflag("v", "verbose", "explain what is being done"); - opts.optflag("h", "help", "display this help and exit"); - opts.optflag("V", "version", "output version information and exit"); - - let matches = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(f) => { - show_error!("Invalid options\n{}", f); - return 1; - } - }; - let usage = opts.usage("Move SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY."); + let files: Vec = matches + .values_of(ARG_FILES) + .map(|v| v.map(ToString::to_string).collect()) + .unwrap_or_default(); let overwrite_mode = determine_overwrite_mode(&matches); let backup_mode = determine_backup_mode(&matches); @@ -109,26 +178,21 @@ pub fn uumain(args: impl uucore::Args) -> i32 { show_error!( "options --backup and --no-clobber are mutually exclusive\n\ Try '{} --help' for more information.", - NAME + executable!() ); return 1; } let backup_suffix = determine_backup_suffix(backup_mode, &matches); - if matches.opt_present("T") && matches.opt_present("t") { - show_error!("cannot combine --target-directory (-t) and --no-target-directory (-T)"); - return 1; - } - let behavior = Behavior { overwrite: overwrite_mode, backup: backup_mode, suffix: backup_suffix, - update: matches.opt_present("u"), - target_dir: matches.opt_str("t"), - no_target_dir: matches.opt_present("T"), - verbose: matches.opt_present("v"), + update: matches.is_present(OPT_UPDATE), + target_dir: matches.value_of(OPT_TARGET_DIRECTORY).map(String::from), + no_target_dir: matches.is_present(OPT_NO_TARGET_DIRECTORY), + verbose: matches.is_present(OPT_VERBOSE), }; let paths: Vec = { @@ -136,60 +200,45 @@ pub fn uumain(args: impl uucore::Args) -> i32 { p.components().as_path() } let to_owned = |p: &Path| p.to_owned(); - let arguments = matches.free.iter().map(Path::new); - if matches.opt_present("strip-trailing-slashes") { - arguments.map(strip_slashes).map(to_owned).collect() + let paths = files.iter().map(Path::new); + + if matches.is_present(OPT_STRIP_TRAILING_SLASHES) { + paths.map(strip_slashes).map(to_owned).collect() } else { - arguments.map(to_owned).collect() + paths.map(to_owned).collect() } }; - if matches.opt_present("version") { - println!("{} {}", NAME, VERSION); - 0 - } else if matches.opt_present("help") { - help(&usage); - 0 - } else { - exec(&paths[..], behavior) - } + exec(&paths[..], behavior) } -fn determine_overwrite_mode(matches: &getopts::Matches) -> OverwriteMode { +fn determine_overwrite_mode(matches: &ArgMatches) -> OverwriteMode { // This does not exactly match the GNU implementation: // The GNU mv defaults to Force, but if more than one of the // overwrite options are supplied, only the last takes effect. // To default to no-clobber in that situation seems safer: // - if matches.opt_present("no-clobber") { + if matches.is_present(OPT_NO_CLOBBER) { OverwriteMode::NoClobber - } else if matches.opt_present("interactive") { + } else if matches.is_present(OPT_INTERACTIVE) { OverwriteMode::Interactive } else { OverwriteMode::Force } } -fn determine_backup_mode(matches: &getopts::Matches) -> BackupMode { - if matches.opt_present("b") { +fn determine_backup_mode(matches: &ArgMatches) -> BackupMode { + if matches.is_present(OPT_BACKUP_NO_ARG) { BackupMode::SimpleBackup - } else if matches.opt_present("backup") { - match matches.opt_str("backup") { + } else if matches.is_present(OPT_BACKUP) { + match matches.value_of(OPT_BACKUP).map(String::from) { None => BackupMode::SimpleBackup, Some(mode) => match &mode[..] { "simple" | "never" => BackupMode::SimpleBackup, "numbered" | "t" => BackupMode::NumberedBackup, "existing" | "nil" => BackupMode::ExistingBackup, "none" | "off" => BackupMode::NoBackup, - x => { - crash!( - 1, - "invalid argument ‘{}’ for ‘backup type’\n\ - Try '{} --help' for more information.", - x, - NAME - ); - } + _ => panic!(), // cannot happen as it is managed by clap }, } } else { @@ -197,19 +246,9 @@ fn determine_backup_mode(matches: &getopts::Matches) -> BackupMode { } } -fn determine_backup_suffix(backup_mode: BackupMode, matches: &getopts::Matches) -> String { - if matches.opt_present("suffix") { - match matches.opt_str("suffix") { - Some(x) => x, - None => { - crash!( - 1, - "option '--suffix' requires an argument\n\ - Try '{} --help' for more information.", - NAME - ); - } - } +fn determine_backup_suffix(backup_mode: BackupMode, matches: &ArgMatches) -> String { + if matches.is_present(OPT_SUFFIX) { + matches.value_of(OPT_SUFFIX).map(String::from).unwrap() } else if let (Ok(s), BackupMode::SimpleBackup) = (env::var("SIMPLE_BACKUP_SUFFIX"), backup_mode) { @@ -219,29 +258,12 @@ fn determine_backup_suffix(backup_mode: BackupMode, matches: &getopts::Matches) } } -fn help(usage: &str) { - println!( - "{0} {1}\n\n\ - Usage: {0} SOURCE DEST\n \ - or: {0} SOURCE... DIRECTORY\n\n\ - {2}", - NAME, VERSION, usage - ); -} - fn exec(files: &[PathBuf], b: Behavior) -> i32 { if let Some(ref name) = b.target_dir { return move_files_into_dir(files, &PathBuf::from(name), &b); } match files.len() { - 0 | 1 => { - show_error!( - "missing file operand\n\ - Try '{} --help' for more information.", - NAME - ); - return 1; - } + /* case 0/1 are not possible thanks to clap */ 2 => { let source = &files[0]; let target = &files[1]; @@ -302,7 +324,7 @@ fn exec(files: &[PathBuf], b: Behavior) -> i32 { "mv: extra operand ‘{}’\n\ Try '{} --help' for more information.", files[2].display(), - NAME + executable!() ); return 1; } @@ -358,7 +380,7 @@ fn rename(from: &PathBuf, to: &PathBuf, b: &Behavior) -> io::Result<()> { match b.overwrite { OverwriteMode::NoClobber => return Ok(()), OverwriteMode::Interactive => { - print!("{}: overwrite ‘{}’? ", NAME, to.display()); + println!("{}: overwrite ‘{}’? ", executable!(), to.display()); if !read_yes() { return Ok(()); } diff --git a/src/uu/nice/Cargo.toml b/src/uu/nice/Cargo.toml index e9ca8445a..279e79ae3 100644 --- a/src/uu/nice/Cargo.toml +++ b/src/uu/nice/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_nice" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "nice ~ (uutils) run PROGRAM with modified scheduling priority" @@ -15,10 +15,11 @@ edition = "2018" path = "src/nice.rs" [dependencies] -getopts = "0.2.18" +clap = "2.33" libc = "0.2.42" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +nix = { version="<=0.13" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "nice" diff --git a/src/uu/nice/src/nice.rs b/src/uu/nice/src/nice.rs index 5d5647a51..c1d3345af 100644 --- a/src/uu/nice/src/nice.rs +++ b/src/uu/nice/src/nice.rs @@ -7,9 +7,6 @@ // spell-checker:ignore (ToDO) getpriority execvp setpriority nstr PRIO cstrs ENOENT -extern crate getopts; -extern crate libc; - #[macro_use] extern crate uucore; @@ -18,7 +15,7 @@ use std::ffi::CString; use std::io::Error; use std::ptr; -const NAME: &str = "nice"; +use clap::{App, AppSettings, Arg}; const VERSION: &str = env!("CARGO_PKG_VERSION"); // XXX: PRIO_PROCESS is 0 on at least FreeBSD and Linux. Don't know about Mac OS X. @@ -29,64 +26,57 @@ extern "C" { fn setpriority(which: c_int, who: c_int, prio: c_int) -> c_int; } -pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); +pub mod options { + pub static ADJUSTMENT: &str = "adjustment"; + pub static COMMAND: &str = "COMMAND"; +} - let mut opts = getopts::Options::new(); - - opts.optopt( - "n", - "adjustment", - "add N to the niceness (default is 10)", - "N", - ); - opts.optflag("h", "help", "display this help and exit"); - opts.optflag("V", "version", "output version information and exit"); - - let matches = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(err) => { - show_error!("{}", err); - return 125; - } - }; - - if matches.opt_present("version") { - println!("{} {}", NAME, VERSION); - return 0; - } - - if matches.opt_present("help") { - let msg = format!( - "{0} {1} - -Usage: +fn get_usage() -> String { + format!( + " {0} [OPTIONS] [COMMAND [ARGS]] 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).", - NAME, VERSION - ); + executable!() + ) +} - print!("{}", opts.usage(&msg)); - return 0; - } +pub fn uumain(args: impl uucore::Args) -> i32 { + let usage = get_usage(); - let mut niceness = unsafe { getpriority(PRIO_PROCESS, 0) }; + let matches = App::new(executable!()) + .setting(AppSettings::TrailingVarArg) + .version(VERSION) + .usage(&usage[..]) + .arg( + Arg::with_name(options::ADJUSTMENT) + .short("n") + .long(options::ADJUSTMENT) + .help("add N to the niceness (default is 10)") + .takes_value(true) + .allow_hyphen_values(true), + ) + .arg(Arg::with_name(options::COMMAND).multiple(true)) + .get_matches_from(args); + + let mut niceness = unsafe { + nix::errno::Errno::clear(); + getpriority(PRIO_PROCESS, 0) + }; if Error::last_os_error().raw_os_error().unwrap() != 0 { - show_error!("{}", Error::last_os_error()); + show_error!("getpriority: {}", Error::last_os_error()); return 125; } - let adjustment = match matches.opt_str("adjustment") { + let adjustment = match matches.value_of(options::ADJUSTMENT) { Some(nstr) => { - if matches.free.is_empty() { + if !matches.is_present(options::COMMAND) { show_error!( - "A command must be given with an adjustment. - Try \"{} --help\" for more information.", - args[0] + "A command must be given with an adjustment.\nTry \"{} --help\" for more information.", + executable!() ); return 125; } @@ -99,34 +89,32 @@ process).", } } None => { - if matches.free.is_empty() { + if !matches.is_present(options::COMMAND) { println!("{}", niceness); return 0; } - 10 as c_int + 10_i32 } }; niceness += adjustment; - unsafe { - setpriority(PRIO_PROCESS, 0, niceness); - } - if Error::last_os_error().raw_os_error().unwrap() != 0 { - show_warning!("{}", Error::last_os_error()); + if unsafe { setpriority(PRIO_PROCESS, 0, niceness) } == -1 { + show_warning!("setpriority: {}", Error::last_os_error()); } let cstrs: Vec = matches - .free - .iter() + .values_of(options::COMMAND) + .unwrap() .map(|x| CString::new(x.as_bytes()).unwrap()) .collect(); + let mut args: Vec<*const c_char> = cstrs.iter().map(|s| s.as_ptr()).collect(); args.push(ptr::null::()); unsafe { execvp(args[0], args.as_mut_ptr()); } - show_error!("{}", Error::last_os_error()); + show_error!("execvp: {}", Error::last_os_error()); if Error::last_os_error().raw_os_error().unwrap() as c_int == libc::ENOENT { 127 } else { diff --git a/src/uu/nl/Cargo.toml b/src/uu/nl/Cargo.toml index 23d12af1e..a51a2555e 100644 --- a/src/uu/nl/Cargo.toml +++ b/src/uu/nl/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_nl" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "nl ~ (uutils) display input with added line numbers" @@ -15,14 +15,14 @@ edition = "2018" path = "src/nl.rs" [dependencies] +clap = "2.33.3" aho-corasick = "0.7.3" -getopts = "0.2.18" libc = "0.2.42" memchr = "2.2.0" regex = "1.0.1" regex-syntax = "0.6.7" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "nl" diff --git a/src/uu/nl/src/helper.rs b/src/uu/nl/src/helper.rs index d3b78ea04..e31477c62 100644 --- a/src/uu/nl/src/helper.rs +++ b/src/uu/nl/src/helper.rs @@ -1,7 +1,6 @@ // spell-checker:ignore (ToDO) conv -extern crate getopts; -extern crate regex; +use crate::options; // parse_style parses a style string into a NumberingStyle. fn parse_style(chars: &[char]) -> Result { @@ -14,7 +13,9 @@ fn parse_style(chars: &[char]) -> Result { } else if chars.len() > 1 && chars[0] == 'p' { let s: String = chars[1..].iter().cloned().collect(); match regex::Regex::new(&s) { - Ok(re) => Ok(crate::NumberingStyle::NumberForRegularExpression(re)), + Ok(re) => Ok(crate::NumberingStyle::NumberForRegularExpression(Box::new( + re, + ))), Err(_) => Err(String::from("Illegal regular expression")), } } else { @@ -24,19 +25,19 @@ fn parse_style(chars: &[char]) -> Result { // parse_options loads the options into the settings, returning an array of // error messages. -pub fn parse_options(settings: &mut crate::Settings, opts: &getopts::Matches) -> Vec { +pub fn parse_options(settings: &mut crate::Settings, opts: &clap::ArgMatches) -> Vec { // This vector holds error messages encountered. let mut errs: Vec = vec![]; - settings.renumber = !opts.opt_present("p"); - match opts.opt_str("s") { + settings.renumber = !opts.is_present(options::NO_RENUMBER); + match opts.value_of(options::NUMER_SEPARATOR) { None => {} Some(val) => { - settings.number_separator = val; + settings.number_separator = val.to_owned(); } } - match opts.opt_str("n") { + match opts.value_of(options::NUMBER_FORMAT) { None => {} - Some(val) => match val.as_ref() { + Some(val) => match val { "ln" => { settings.number_format = crate::NumberFormat::Left; } @@ -51,7 +52,7 @@ pub fn parse_options(settings: &mut crate::Settings, opts: &getopts::Matches) -> } }, } - match opts.opt_str("b") { + match opts.value_of(options::BODY_NUMBERING) { None => {} Some(val) => { let chars: Vec = val.chars().collect(); @@ -65,7 +66,7 @@ pub fn parse_options(settings: &mut crate::Settings, opts: &getopts::Matches) -> } } } - match opts.opt_str("f") { + match opts.value_of(options::FOOTER_NUMBERING) { None => {} Some(val) => { let chars: Vec = val.chars().collect(); @@ -79,7 +80,7 @@ pub fn parse_options(settings: &mut crate::Settings, opts: &getopts::Matches) -> } } } - match opts.opt_str("h") { + match opts.value_of(options::HEADER_NUMBERING) { None => {} Some(val) => { let chars: Vec = val.chars().collect(); @@ -93,7 +94,7 @@ pub fn parse_options(settings: &mut crate::Settings, opts: &getopts::Matches) -> } } } - match opts.opt_str("i") { + match opts.value_of(options::LINE_INCREMENT) { None => {} Some(val) => { let conv: Option = val.parse().ok(); @@ -105,7 +106,7 @@ pub fn parse_options(settings: &mut crate::Settings, opts: &getopts::Matches) -> } } } - match opts.opt_str("w") { + match opts.value_of(options::NUMBER_WIDTH) { None => {} Some(val) => { let conv: Option = val.parse().ok(); @@ -117,7 +118,7 @@ pub fn parse_options(settings: &mut crate::Settings, opts: &getopts::Matches) -> } } } - match opts.opt_str("v") { + match opts.value_of(options::STARTING_LINE_NUMER) { None => {} Some(val) => { let conv: Option = val.parse().ok(); @@ -129,7 +130,7 @@ pub fn parse_options(settings: &mut crate::Settings, opts: &getopts::Matches) -> } } } - match opts.opt_str("l") { + match opts.value_of(options::JOIN_BLANK_LINES) { None => {} Some(val) => { let conv: Option = val.parse().ok(); diff --git a/src/uu/nl/src/nl.rs b/src/uu/nl/src/nl.rs index 6bf134704..3b5b5a2e8 100644 --- a/src/uu/nl/src/nl.rs +++ b/src/uu/nl/src/nl.rs @@ -8,15 +8,10 @@ // spell-checker:ignore (ToDO) corasick memchr -extern crate aho_corasick; -extern crate getopts; -extern crate memchr; -extern crate regex; -extern crate regex_syntax; - #[macro_use] extern crate uucore; +use clap::{App, Arg}; use std::fs::File; use std::io::{stdin, BufRead, BufReader, Read}; use std::iter::repeat; @@ -61,7 +56,7 @@ enum NumberingStyle { NumberForAll, NumberForNonEmpty, NumberForNone, - NumberForRegularExpression(regex::Regex), + NumberForRegularExpression(Box), } // NumberFormat specifies how line numbers are output within their allocated @@ -73,78 +68,106 @@ enum NumberFormat { RightZero, } +pub mod options { + pub const FILE: &str = "file"; + pub const BODY_NUMBERING: &str = "body-numbering"; + pub const SECTION_DELIMITER: &str = "section-delimiter"; + pub const FOOTER_NUMBERING: &str = "footer-numbering"; + pub const HEADER_NUMBERING: &str = "header-numbering"; + pub const LINE_INCREMENT: &str = "line-increment"; + pub const JOIN_BLANK_LINES: &str = "join-blank-lines"; + pub const NUMBER_FORMAT: &str = "number-format"; + pub const NO_RENUMBER: &str = "no-renumber"; + pub const NUMER_SEPARATOR: &str = "number-separator"; + pub const STARTING_LINE_NUMER: &str = "starting-line-number"; + pub const NUMBER_WIDTH: &str = "number-width"; +} + pub fn uumain(args: impl uucore::Args) -> i32 { let args = args.collect_str(); - let mut opts = getopts::Options::new(); - - opts.optopt( - "b", - "body-numbering", - "use STYLE for numbering body lines", - "STYLE", - ); - opts.optopt( - "d", - "section-delimiter", - "use CC for separating logical pages", - "CC", - ); - opts.optopt( - "f", - "footer-numbering", - "use STYLE for numbering footer lines", - "STYLE", - ); - opts.optopt( - "h", - "header-numbering", - "use STYLE for numbering header lines", - "STYLE", - ); - opts.optopt( - "i", - "line-increment", - "line number increment at each line", - "", - ); - opts.optopt( - "l", - "join-blank-lines", - "group of NUMBER empty lines counted as one", - "NUMBER", - ); - opts.optopt( - "n", - "number-format", - "insert line numbers according to FORMAT", - "FORMAT", - ); - opts.optflag( - "p", - "no-renumber", - "do not reset line numbers at logical pages", - ); - opts.optopt( - "s", - "number-separator", - "add STRING after (possible) line number", - "STRING", - ); - opts.optopt( - "v", - "starting-line-number", - "first line number on each logical page", - "NUMBER", - ); - opts.optopt( - "w", - "number-width", - "use NUMBER columns for line numbers", - "NUMBER", - ); - opts.optflag("", "help", "display this help and exit"); - opts.optflag("V", "version", "version"); + let matches = App::new(executable!()) + .name(NAME) + .version(VERSION) + .usage(USAGE) + .arg(Arg::with_name(options::FILE).hidden(true).multiple(true)) + .arg( + Arg::with_name(options::BODY_NUMBERING) + .short("b") + .long(options::BODY_NUMBERING) + .help("use STYLE for numbering body lines") + .value_name("SYNTAX"), + ) + .arg( + Arg::with_name(options::SECTION_DELIMITER) + .short("d") + .long(options::SECTION_DELIMITER) + .help("use CC for separating logical pages") + .value_name("CC"), + ) + .arg( + Arg::with_name(options::FOOTER_NUMBERING) + .short("f") + .long(options::FOOTER_NUMBERING) + .help("use STYLE for numbering footer lines") + .value_name("STYLE"), + ) + .arg( + Arg::with_name(options::HEADER_NUMBERING) + .short("h") + .long(options::HEADER_NUMBERING) + .help("use STYLE for numbering header lines") + .value_name("STYLE"), + ) + .arg( + Arg::with_name(options::LINE_INCREMENT) + .short("i") + .long(options::LINE_INCREMENT) + .help("line number increment at each line") + .value_name("NUMBER"), + ) + .arg( + Arg::with_name(options::JOIN_BLANK_LINES) + .short("l") + .long(options::JOIN_BLANK_LINES) + .help("group of NUMBER empty lines counted as one") + .value_name("NUMBER"), + ) + .arg( + Arg::with_name(options::NUMBER_FORMAT) + .short("n") + .long(options::NUMBER_FORMAT) + .help("insert line numbers according to FORMAT") + .value_name("FORMAT"), + ) + .arg( + Arg::with_name(options::NO_RENUMBER) + .short("p") + .long(options::NO_RENUMBER) + .help("do not reset line numbers at logical pages"), + ) + .arg( + Arg::with_name(options::NUMER_SEPARATOR) + .short("s") + .long(options::NUMER_SEPARATOR) + .help("add STRING after (possible) line number") + .value_name("STRING"), + ) + .arg( + Arg::with_name(options::STARTING_LINE_NUMER) + .short("v") + .long(options::STARTING_LINE_NUMER) + .help("first line number on each logical page") + .value_name("NUMBER"), + ) + .arg( + Arg::with_name(options::NUMBER_WIDTH) + .short("w") + .long(options::NUMBER_WIDTH) + .help("use NUMBER columns for line numbers") + .value_name("NUMBER"), + ) + .get_matches_from(args); // A mutable settings object, initialized with the defaults. let mut settings = Settings { @@ -161,27 +184,9 @@ pub fn uumain(args: impl uucore::Args) -> i32 { number_separator: String::from("\t"), }; - let given_options = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(f) => { - show_error!("{}", f); - print_usage(&opts); - return 1; - } - }; - - if given_options.opt_present("help") { - print_usage(&opts); - return 0; - } - if given_options.opt_present("version") { - version(); - return 0; - } - // Update the settings from the command line options, and terminate the // program if some options could not successfully be parsed. - let parse_errors = helper::parse_options(&mut settings, &given_options); + let parse_errors = helper::parse_options(&mut settings, &matches); if !parse_errors.is_empty() { show_error!("Invalid arguments supplied."); for message in &parse_errors { @@ -190,8 +195,11 @@ pub fn uumain(args: impl uucore::Args) -> i32 { return 1; } - let files = given_options.free; - let mut read_stdin = files.is_empty(); + let mut read_stdin = false; + let files: Vec = match matches.values_of(options::FILE) { + Some(v) => v.clone().map(|v| v.to_owned()).collect(), + None => vec!["-".to_owned()], + }; for file in &files { if file == "-" { @@ -310,7 +318,7 @@ fn nl(reader: &mut BufReader, settings: &Settings) { continue; } // From this point on we format and print a "regular" line. - if line == "" { + if line.is_empty() { // The line is empty, which means that we have to care // about the --join-blank-lines parameter. empty_line_count += 1; @@ -376,11 +384,3 @@ fn pass_none(_: &str, _: ®ex::Regex) -> bool { fn pass_all(_: &str, _: ®ex::Regex) -> bool { true } - -fn print_usage(opts: &getopts::Options) { - println!("{}", opts.usage(USAGE)); -} - -fn version() { - println!("{} {}", NAME, VERSION); -} diff --git a/src/uu/nohup/Cargo.toml b/src/uu/nohup/Cargo.toml index d349fd22f..5bbbd9dff 100644 --- a/src/uu/nohup/Cargo.toml +++ b/src/uu/nohup/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_nohup" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "nohup ~ (uutils) run COMMAND, ignoring hangup signals" @@ -15,10 +15,10 @@ edition = "2018" path = "src/nohup.rs" [dependencies] -getopts = "0.2.18" +clap = "2.33" libc = "0.2.42" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary", features=["fs"] } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["fs"] } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "nohup" diff --git a/src/uu/nohup/src/nohup.rs b/src/uu/nohup/src/nohup.rs index 76919cd7e..afbf2541b 100644 --- a/src/uu/nohup/src/nohup.rs +++ b/src/uu/nohup/src/nohup.rs @@ -7,12 +7,10 @@ // spell-checker:ignore (ToDO) execvp SIGHUP cproc vprocmgr cstrs homeout -extern crate getopts; -extern crate libc; - #[macro_use] extern crate uucore; +use clap::{App, AppSettings, Arg}; use libc::{c_char, dup2, execvp, signal}; use libc::{SIGHUP, SIG_IGN}; use std::env; @@ -23,50 +21,42 @@ use std::os::unix::prelude::*; use std::path::{Path, PathBuf}; use uucore::fs::{is_stderr_interactive, is_stdin_interactive, is_stdout_interactive}; -static NAME: &str = "nohup"; static VERSION: &str = env!("CARGO_PKG_VERSION"); +static ABOUT: &str = "Run COMMAND ignoring hangup signals."; +static LONG_HELP: &str = " +If standard input is terminal, it'll be replaced with /dev/null. +If standard output is terminal, it'll be appended to nohup.out instead, +or $HOME/nohup.out, if nohup.out open failed. +If standard error is terminal, it'll be redirected to stdout. +"; +static NOHUP_OUT: &str = "nohup.out"; +// exit codes that match the GNU implementation +static EXIT_CANCELED: i32 = 125; +static EXIT_CANNOT_INVOKE: i32 = 126; +static EXIT_ENOENT: i32 = 127; +static POSIX_NOHUP_FAILURE: i32 = 127; -#[cfg(target_os = "macos")] -extern "C" { - fn _vprocmgr_detach_from_console(flags: u32) -> *const libc::c_int; -} - -#[cfg(any(target_os = "linux", target_os = "freebsd"))] -unsafe fn _vprocmgr_detach_from_console(_: u32) -> *const libc::c_int { - std::ptr::null() +mod options { + pub const CMD: &str = "cmd"; } pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); + let usage = get_usage(); - let mut opts = getopts::Options::new(); + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(&usage[..]) + .after_help(LONG_HELP) + .arg( + Arg::with_name(options::CMD) + .hidden(true) + .required(true) + .multiple(true), + ) + .setting(AppSettings::TrailingVarArg) + .get_matches_from(args); - opts.optflag("h", "help", "Show help and exit"); - opts.optflag("V", "version", "Show version and exit"); - - let matches = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(f) => { - show_error!("{}", f); - show_usage(&opts); - return 1; - } - }; - - if matches.opt_present("V") { - println!("{} {}", NAME, VERSION); - return 0; - } - if matches.opt_present("h") { - show_usage(&opts); - return 0; - } - - if matches.free.is_empty() { - show_error!("Missing operand: COMMAND"); - println!("Try `{} --help` for more information.", NAME); - return 1; - } replace_fds(); unsafe { signal(SIGHUP, SIG_IGN) }; @@ -76,13 +66,18 @@ pub fn uumain(args: impl uucore::Args) -> i32 { }; let cstrs: Vec = matches - .free - .iter() + .values_of(options::CMD) + .unwrap() .map(|x| CString::new(x.as_bytes()).unwrap()) .collect(); let mut args: Vec<*const c_char> = cstrs.iter().map(|s| s.as_ptr()).collect(); args.push(std::ptr::null()); - unsafe { execvp(args[0], args.as_mut_ptr()) } + + let ret = unsafe { execvp(args[0], args.as_mut_ptr()) }; + match ret { + libc::ENOENT => EXIT_ENOENT, + _ => EXIT_CANNOT_INVOKE, + } } fn replace_fds() { @@ -111,23 +106,32 @@ fn replace_fds() { } fn find_stdout() -> File { + let internal_failure_code = match std::env::var("POSIXLY_CORRECT") { + Ok(_) => POSIX_NOHUP_FAILURE, + Err(_) => EXIT_CANCELED, + }; + match OpenOptions::new() .write(true) .create(true) .append(true) - .open(Path::new("nohup.out")) + .open(Path::new(NOHUP_OUT)) { Ok(t) => { - show_warning!("Output is redirected to: nohup.out"); + show_info!("ignoring input and appending output to '{}'", NOHUP_OUT); t } - Err(e) => { + Err(e1) => { let home = match env::var("HOME") { - Err(_) => crash!(2, "Cannot replace STDOUT: {}", e), + Err(_) => { + show_info!("failed to open '{}': {}", NOHUP_OUT, e1); + exit!(internal_failure_code) + } Ok(h) => h, }; let mut homeout = PathBuf::from(home); - homeout.push("nohup.out"); + homeout.push(NOHUP_OUT); + let homeout_str = homeout.to_str().unwrap(); match OpenOptions::new() .write(true) .create(true) @@ -135,30 +139,29 @@ fn find_stdout() -> File { .open(&homeout) { Ok(t) => { - show_warning!("Output is redirected to: {:?}", homeout); + show_info!("ignoring input and appending output to '{}'", homeout_str); t } - Err(e) => crash!(2, "Cannot replace STDOUT: {}", e), + Err(e2) => { + show_info!("failed to open '{}': {}", NOHUP_OUT, e1); + show_info!("failed to open '{}': {}", homeout_str, e2); + exit!(internal_failure_code) + } } } } } -fn show_usage(opts: &getopts::Options) { - let msg = format!( - "{0} {1} - -Usage: - {0} COMMAND [ARG]... - {0} OPTION - -Run COMMAND ignoring hangup signals. -If standard input is terminal, it'll be replaced with /dev/null. -If standard output is terminal, it'll be appended to nohup.out instead, -or $HOME/nohup.out, if nohup.out open failed. -If standard error is terminal, it'll be redirected to stdout.", - NAME, VERSION - ); - - print!("{}", opts.usage(&msg)); +fn get_usage() -> String { + format!("{0} COMMAND [ARG]...\n {0} FLAG", executable!()) +} + +#[cfg(target_vendor = "apple")] +extern "C" { + fn _vprocmgr_detach_from_console(flags: u32) -> *const libc::c_int; +} + +#[cfg(any(target_os = "linux", target_os = "freebsd"))] +unsafe fn _vprocmgr_detach_from_console(_: u32) -> *const libc::c_int { + std::ptr::null() } diff --git a/src/uu/nproc/Cargo.toml b/src/uu/nproc/Cargo.toml index 1689f3db6..be9d8f2e3 100644 --- a/src/uu/nproc/Cargo.toml +++ b/src/uu/nproc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_nproc" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "nproc ~ (uutils) display the number of processing units available" @@ -15,12 +15,11 @@ edition = "2018" path = "src/nproc.rs" [dependencies] -getopts = "0.2.18" libc = "0.2.42" num_cpus = "1.10" clap = "2.33" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary", features=["fs"] } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["fs"] } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "nproc" diff --git a/src/uu/nproc/src/nproc.rs b/src/uu/nproc/src/nproc.rs index d18952276..285cf764f 100644 --- a/src/uu/nproc/src/nproc.rs +++ b/src/uu/nproc/src/nproc.rs @@ -7,13 +7,6 @@ // spell-checker:ignore (ToDO) NPROCESSORS nprocs numstr threadstr sysconf -extern crate clap; -extern crate getopts; -extern crate num_cpus; - -#[cfg(unix)] -extern crate libc; - #[macro_use] extern crate uucore; @@ -22,7 +15,7 @@ use std::env; #[cfg(target_os = "linux")] pub const _SC_NPROCESSORS_CONF: libc::c_int = 83; -#[cfg(target_os = "macos")] +#[cfg(target_vendor = "apple")] pub const _SC_NPROCESSORS_CONF: libc::c_int = libc::_SC_NPROCESSORS_CONF; #[cfg(target_os = "freebsd")] pub const _SC_NPROCESSORS_CONF: libc::c_int = 57; @@ -48,13 +41,13 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .arg( Arg::with_name(OPT_ALL) .short("") - .long("all") + .long(OPT_ALL) .help("print the number of cores available to the system"), ) .arg( Arg::with_name(OPT_IGNORE) .short("") - .long("ignore") + .long(OPT_IGNORE) .takes_value(true) .help("ignore up to N cores"), ) @@ -74,10 +67,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { if !matches.is_present(OPT_ALL) { // OMP_NUM_THREADS doesn't have an impact on --all ignore += match env::var("OMP_NUM_THREADS") { - Ok(threadstr) => match threadstr.parse() { - Ok(num) => num, - Err(_) => 0, - }, + Ok(threadstr) => threadstr.parse().unwrap_or(0), Err(_) => 0, }; } @@ -99,7 +89,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { #[cfg(any( target_os = "linux", - target_os = "macos", + target_vendor = "apple", target_os = "freebsd", target_os = "netbsd" ))] @@ -119,7 +109,7 @@ fn num_cpus_all() -> usize { // Other platforms (e.g., windows), num_cpus::get() directly. #[cfg(not(any( target_os = "linux", - target_os = "macos", + target_vendor = "apple", target_os = "freebsd", target_os = "netbsd" )))] diff --git a/src/uu/numfmt/Cargo.toml b/src/uu/numfmt/Cargo.toml index b965b6053..ac5266d68 100644 --- a/src/uu/numfmt/Cargo.toml +++ b/src/uu/numfmt/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_numfmt" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "numfmt ~ (uutils) reformat NUMBER" @@ -15,9 +15,9 @@ edition = "2018" path = "src/numfmt.rs" [dependencies] -getopts = "0.2.18" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +clap = "2.33" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "numfmt" diff --git a/src/uu/numfmt/src/format.rs b/src/uu/numfmt/src/format.rs new file mode 100644 index 000000000..ebe380569 --- /dev/null +++ b/src/uu/numfmt/src/format.rs @@ -0,0 +1,296 @@ +use crate::options::NumfmtOptions; +use crate::units::{ + DisplayableSuffix, RawSuffix, Result, Suffix, Transform, Unit, IEC_BASES, SI_BASES, +}; + +/// Iterate over a line's fields, where each field is a contiguous sequence of +/// non-whitespace, optionally prefixed with one or more characters of leading +/// whitespace. Fields are returned as tuples of `(prefix, field)`. +/// +/// # Examples: +/// +/// ``` +/// let mut fields = uu_numfmt::format::WhitespaceSplitter { s: Some(" 1234 5") }; +/// +/// assert_eq!(Some((" ", "1234")), fields.next()); +/// assert_eq!(Some((" ", "5")), fields.next()); +/// assert_eq!(None, fields.next()); +/// ``` +/// +/// Delimiters are included in the results; `prefix` will be empty only for +/// the first field of the line (including the case where the input line is +/// empty): +/// +/// ``` +/// let mut fields = uu_numfmt::format::WhitespaceSplitter { s: Some("first second") }; +/// +/// assert_eq!(Some(("", "first")), fields.next()); +/// assert_eq!(Some((" ", "second")), fields.next()); +/// +/// let mut fields = uu_numfmt::format::WhitespaceSplitter { s: Some("") }; +/// +/// assert_eq!(Some(("", "")), fields.next()); +/// ``` +pub struct WhitespaceSplitter<'a> { + pub s: Option<&'a str>, +} + +impl<'a> Iterator for WhitespaceSplitter<'a> { + type Item = (&'a str, &'a str); + + /// Yield the next field in the input string as a tuple `(prefix, field)`. + fn next(&mut self) -> Option { + let haystack = self.s?; + + let (prefix, field) = haystack.split_at( + haystack + .find(|c: char| !c.is_whitespace()) + .unwrap_or_else(|| haystack.len()), + ); + + let (field, rest) = field.split_at( + field + .find(|c: char| c.is_whitespace()) + .unwrap_or_else(|| field.len()), + ); + + self.s = if !rest.is_empty() { Some(rest) } else { None }; + + Some((prefix, field)) + } +} + +fn parse_suffix(s: &str) -> Result<(f64, Option)> { + if s.is_empty() { + return Err("invalid number: ‘’".to_string()); + } + + let with_i = s.ends_with('i'); + let mut iter = s.chars(); + if with_i { + iter.next_back(); + } + let suffix: Option = match iter.next_back() { + Some('K') => Ok(Some((RawSuffix::K, with_i))), + Some('M') => Ok(Some((RawSuffix::M, with_i))), + Some('G') => Ok(Some((RawSuffix::G, with_i))), + Some('T') => Ok(Some((RawSuffix::T, with_i))), + Some('P') => Ok(Some((RawSuffix::P, with_i))), + Some('E') => Ok(Some((RawSuffix::E, with_i))), + Some('Z') => Ok(Some((RawSuffix::Z, with_i))), + Some('Y') => Ok(Some((RawSuffix::Y, with_i))), + Some('0'..='9') => Ok(None), + _ => Err(format!("invalid suffix in input: ‘{}’", s)), + }?; + + let suffix_len = match suffix { + None => 0, + Some((_, false)) => 1, + Some((_, true)) => 2, + }; + + let number = s[..s.len() - suffix_len] + .parse::() + .map_err(|_| format!("invalid number: ‘{}’", s))?; + + Ok((number, suffix)) +} + +fn remove_suffix(i: f64, s: Option, u: &Unit) -> Result { + match (s, u) { + (None, _) => Ok(i), + (Some((raw_suffix, false)), &Unit::Auto) | (Some((raw_suffix, false)), &Unit::Si) => { + match raw_suffix { + RawSuffix::K => Ok(i * 1e3), + RawSuffix::M => Ok(i * 1e6), + RawSuffix::G => Ok(i * 1e9), + RawSuffix::T => Ok(i * 1e12), + RawSuffix::P => Ok(i * 1e15), + RawSuffix::E => Ok(i * 1e18), + RawSuffix::Z => Ok(i * 1e21), + RawSuffix::Y => Ok(i * 1e24), + } + } + (Some((raw_suffix, false)), &Unit::Iec(false)) + | (Some((raw_suffix, true)), &Unit::Auto) + | (Some((raw_suffix, true)), &Unit::Iec(true)) => match raw_suffix { + RawSuffix::K => Ok(i * IEC_BASES[1]), + RawSuffix::M => Ok(i * IEC_BASES[2]), + RawSuffix::G => Ok(i * IEC_BASES[3]), + RawSuffix::T => Ok(i * IEC_BASES[4]), + RawSuffix::P => Ok(i * IEC_BASES[5]), + RawSuffix::E => Ok(i * IEC_BASES[6]), + RawSuffix::Z => Ok(i * IEC_BASES[7]), + RawSuffix::Y => Ok(i * IEC_BASES[8]), + }, + (_, _) => Err("This suffix is unsupported for specified unit".to_owned()), + } +} + +fn transform_from(s: &str, opts: &Transform) -> Result { + let (i, suffix) = parse_suffix(s)?; + + remove_suffix(i, suffix, &opts.unit).map(|n| if n < 0.0 { -n.abs().ceil() } else { n.ceil() }) +} + +/// Divide numerator by denominator, with ceiling. +/// +/// If the result of the division is less than 10.0, truncate the result +/// to the next highest tenth. +/// +/// Otherwise, truncate the result to the next highest whole number. +/// +/// # Examples: +/// +/// ``` +/// use uu_numfmt::format::div_ceil; +/// +/// assert_eq!(div_ceil(1.01, 1.0), 1.1); +/// assert_eq!(div_ceil(999.1, 1000.), 1.0); +/// assert_eq!(div_ceil(1001., 10.), 101.); +/// assert_eq!(div_ceil(9991., 10.), 1000.); +/// assert_eq!(div_ceil(-12.34, 1.0), -13.0); +/// assert_eq!(div_ceil(1000.0, -3.14), -319.0); +/// assert_eq!(div_ceil(-271828.0, -271.0), 1004.0); +/// ``` +pub fn div_ceil(n: f64, d: f64) -> f64 { + let v = n / (d / 10.0); + let (v, sign) = if v < 0.0 { (v.abs(), -1.0) } else { (v, 1.0) }; + + if v < 100.0 { + v.ceil() / 10.0 * sign + } else { + (v / 10.0).ceil() * sign + } +} + +fn consider_suffix(n: f64, u: &Unit) -> Result<(f64, Option)> { + use crate::units::RawSuffix::*; + + let abs_n = n.abs(); + let suffixes = [K, M, G, T, P, E, Z, Y]; + + let (bases, with_i) = match *u { + Unit::Si => (&SI_BASES, false), + Unit::Iec(with_i) => (&IEC_BASES, with_i), + Unit::Auto => return Err("Unit 'auto' isn't supported with --to options".to_owned()), + Unit::None => return Ok((n, None)), + }; + + let i = match abs_n { + _ if abs_n <= bases[1] - 1.0 => return Ok((n, None)), + _ if abs_n < bases[2] => 1, + _ if abs_n < bases[3] => 2, + _ if abs_n < bases[4] => 3, + _ if abs_n < bases[5] => 4, + _ if abs_n < bases[6] => 5, + _ if abs_n < bases[7] => 6, + _ if abs_n < bases[8] => 7, + _ if abs_n < bases[9] => 8, + _ => return Err("Number is too big and unsupported".to_string()), + }; + + let v = div_ceil(n, bases[i]); + + // check if rounding pushed us into the next base + if v.abs() >= bases[1] { + Ok((v / bases[1], Some((suffixes[i], with_i)))) + } else { + Ok((v, Some((suffixes[i - 1], with_i)))) + } +} + +fn transform_to(s: f64, opts: &Transform) -> Result { + let (i2, s) = consider_suffix(s, &opts.unit)?; + Ok(match s { + None => format!("{}", i2), + Some(s) if i2.abs() < 10.0 => format!("{:.1}{}", i2, DisplayableSuffix(s)), + Some(s) => format!("{:.0}{}", i2, DisplayableSuffix(s)), + }) +} + +fn format_string( + source: &str, + options: &NumfmtOptions, + implicit_padding: Option, +) -> Result { + let number = transform_to( + transform_from(source, &options.transform.from)?, + &options.transform.to, + )?; + + Ok(match implicit_padding.unwrap_or(options.padding) { + p if p == 0 => number, + p if p > 0 => format!("{:>padding$}", number, padding = p as usize), + p => format!("{: Result<()> { + let delimiter = options.delimiter.as_ref().unwrap(); + + for (n, field) in (1..).zip(s.split(delimiter)) { + let field_selected = uucore::ranges::contain(&options.fields, n); + + // print delimiter before second and subsequent fields + if n > 1 { + print!("{}", delimiter); + } + + if field_selected { + print!("{}", format_string(&field.trim_start(), options, None)?); + } else { + // print unselected field without conversion + print!("{}", field); + } + } + + println!(); + + Ok(()) +} + +fn format_and_print_whitespace(s: &str, options: &NumfmtOptions) -> Result<()> { + for (n, (prefix, field)) in (1..).zip(WhitespaceSplitter { s: Some(s) }) { + let field_selected = uucore::ranges::contain(&options.fields, n); + + if field_selected { + let empty_prefix = prefix.is_empty(); + + // print delimiter before second and subsequent fields + let prefix = if n > 1 { + print!(" "); + &prefix[1..] + } else { + &prefix + }; + + let implicit_padding = if !empty_prefix && options.padding == 0 { + Some((prefix.len() + field.len()) as isize) + } else { + None + }; + + print!("{}", format_string(&field, options, implicit_padding)?); + } else { + // print unselected field without conversion + print!("{}{}", prefix, field); + } + } + + println!(); + + Ok(()) +} + +/// Format a line of text according to the selected options. +/// +/// Given a line of text `s`, split the line into fields, transform and format +/// any selected numeric fields, and print the result to stdout. Fields not +/// selected for conversion are passed through unmodified. +pub fn format_and_print(s: &str, options: &NumfmtOptions) -> Result<()> { + match &options.delimiter { + Some(_) => format_and_print_delimited(s, options), + None => format_and_print_whitespace(s, options), + } +} diff --git a/src/uu/numfmt/src/numfmt.rs b/src/uu/numfmt/src/numfmt.rs index bf7c51c9e..e9a476956 100644 --- a/src/uu/numfmt/src/numfmt.rs +++ b/src/uu/numfmt/src/numfmt.rs @@ -5,326 +5,23 @@ // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -extern crate getopts; +#[macro_use] +extern crate uucore; -use getopts::{Matches, Options}; -use std::fmt; -use std::io::BufRead; +use crate::format::format_and_print; +use crate::options::*; +use crate::units::{Result, Transform, Unit}; +use clap::{App, AppSettings, Arg, ArgMatches}; +use std::io::{BufRead, Write}; +use uucore::ranges::Range; + +pub mod format; +mod options; +mod units; -static NAME: &str = "numfmt"; static VERSION: &str = env!("CARGO_PKG_VERSION"); - -const IEC_BASES: [f64; 10] = [ - //premature optimization - 1., - 1_024., - 1_048_576., - 1_073_741_824., - 1_099_511_627_776., - 1_125_899_906_842_624., - 1_152_921_504_606_846_976., - 1_180_591_620_717_411_303_424., - 1_208_925_819_614_629_174_706_176., - 1_237_940_039_285_380_274_899_124_224., -]; - -type Result = std::result::Result; - -type WithI = bool; - -enum Unit { - Auto, - Si, - Iec(WithI), - None, -} - -enum RawSuffix { - K, - M, - G, - T, - P, - E, - Z, - Y, -} - -type Suffix = (RawSuffix, WithI); - -struct DisplayableSuffix(Suffix); - -impl fmt::Display for DisplayableSuffix { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let DisplayableSuffix((ref raw_suffix, ref with_i)) = *self; - match raw_suffix { - RawSuffix::K => write!(f, "K"), - RawSuffix::M => write!(f, "M"), - RawSuffix::G => write!(f, "G"), - RawSuffix::T => write!(f, "T"), - RawSuffix::P => write!(f, "P"), - RawSuffix::E => write!(f, "E"), - RawSuffix::Z => write!(f, "Z"), - RawSuffix::Y => write!(f, "Y"), - } - .and_then(|()| match with_i { - true => write!(f, "i"), - false => Ok(()), - }) - } -} - -fn parse_suffix(s: String) -> Result<(f64, Option)> { - let with_i = s.ends_with('i'); - let mut iter = s.chars(); - if with_i { - iter.next_back(); - } - let suffix: Option = match iter.next_back() { - Some('K') => Ok(Some((RawSuffix::K, with_i))), - Some('M') => Ok(Some((RawSuffix::M, with_i))), - Some('G') => Ok(Some((RawSuffix::G, with_i))), - Some('T') => Ok(Some((RawSuffix::T, with_i))), - Some('P') => Ok(Some((RawSuffix::P, with_i))), - Some('E') => Ok(Some((RawSuffix::E, with_i))), - Some('Z') => Ok(Some((RawSuffix::Z, with_i))), - Some('Y') => Ok(Some((RawSuffix::Y, with_i))), - Some('0'..='9') => Ok(None), - _ => Err("Failed to parse suffix"), - }?; - - let suffix_len = match suffix { - None => 0, - Some((_, false)) => 1, - Some((_, true)) => 2, - }; - - let number = s[..s.len() - suffix_len] - .parse::() - .map_err(|err| err.to_string())?; - - Ok((number, suffix)) -} - -fn parse_unit(s: String) -> Result { - match &s[..] { - "auto" => Ok(Unit::Auto), - "si" => Ok(Unit::Si), - "iec" => Ok(Unit::Iec(false)), - "iec-i" => Ok(Unit::Iec(true)), - "none" => Ok(Unit::None), - _ => Err("Unsupported unit is specified".to_owned()), - } -} - -struct TransformOptions { - from: Transform, - to: Transform, -} - -struct Transform { - unit: Unit, -} - -struct NumfmtOptions { - transform: TransformOptions, - padding: isize, - header: usize, -} - -fn remove_suffix(i: f64, s: Option, u: &Unit) -> Result { - match (s, u) { - (None, _) => Ok(i), - (Some((raw_suffix, false)), &Unit::Auto) | (Some((raw_suffix, false)), &Unit::Si) => { - match raw_suffix { - RawSuffix::K => Ok(i * 1e3), - RawSuffix::M => Ok(i * 1e6), - RawSuffix::G => Ok(i * 1e9), - RawSuffix::T => Ok(i * 1e12), - RawSuffix::P => Ok(i * 1e15), - RawSuffix::E => Ok(i * 1e18), - RawSuffix::Z => Ok(i * 1e21), - RawSuffix::Y => Ok(i * 1e24), - } - } - (Some((raw_suffix, false)), &Unit::Iec(false)) - | (Some((raw_suffix, true)), &Unit::Auto) - | (Some((raw_suffix, true)), &Unit::Iec(true)) => match raw_suffix { - RawSuffix::K => Ok(i * IEC_BASES[1]), - RawSuffix::M => Ok(i * IEC_BASES[2]), - RawSuffix::G => Ok(i * IEC_BASES[3]), - RawSuffix::T => Ok(i * IEC_BASES[4]), - RawSuffix::P => Ok(i * IEC_BASES[5]), - RawSuffix::E => Ok(i * IEC_BASES[6]), - RawSuffix::Z => Ok(i * IEC_BASES[7]), - RawSuffix::Y => Ok(i * IEC_BASES[8]), - }, - (_, _) => Err("This suffix is unsupported for specified unit".to_owned()), - } -} - -fn transform_from(s: String, opts: &Transform) -> Result { - let (i, suffix) = parse_suffix(s)?; - remove_suffix(i, suffix, &opts.unit).map(|n| n.round()) -} - -fn consider_suffix(i: f64, u: &Unit) -> Result<(f64, Option)> { - let j = i.abs(); - match *u { - Unit::Si => match j { - _ if j < 1e3 => Ok((i, None)), - _ if j < 1e6 => Ok((i / 1e3, Some((RawSuffix::K, false)))), - _ if j < 1e9 => Ok((i / 1e6, Some((RawSuffix::M, false)))), - _ if j < 1e12 => Ok((i / 1e9, Some((RawSuffix::G, false)))), - _ if j < 1e15 => Ok((i / 1e12, Some((RawSuffix::T, false)))), - _ if j < 1e18 => Ok((i / 1e15, Some((RawSuffix::P, false)))), - _ if j < 1e21 => Ok((i / 1e18, Some((RawSuffix::E, false)))), - _ if j < 1e24 => Ok((i / 1e21, Some((RawSuffix::Z, false)))), - _ if j < 1e27 => Ok((i / 1e24, Some((RawSuffix::Y, false)))), - _ => Err("Number is too big and unsupported".to_owned()), - }, - Unit::Iec(with_i) => match j { - _ if j < IEC_BASES[1] => Ok((i, None)), - _ if j < IEC_BASES[2] => Ok((i / IEC_BASES[1], Some((RawSuffix::K, with_i)))), - _ if j < IEC_BASES[3] => Ok((i / IEC_BASES[2], Some((RawSuffix::M, with_i)))), - _ if j < IEC_BASES[4] => Ok((i / IEC_BASES[3], Some((RawSuffix::G, with_i)))), - _ if j < IEC_BASES[5] => Ok((i / IEC_BASES[4], Some((RawSuffix::T, with_i)))), - _ if j < IEC_BASES[6] => Ok((i / IEC_BASES[5], Some((RawSuffix::P, with_i)))), - _ if j < IEC_BASES[7] => Ok((i / IEC_BASES[6], Some((RawSuffix::E, with_i)))), - _ if j < IEC_BASES[8] => Ok((i / IEC_BASES[7], Some((RawSuffix::Z, with_i)))), - _ if j < IEC_BASES[9] => Ok((i / IEC_BASES[8], Some((RawSuffix::Y, with_i)))), - _ => Err("Number is too big and unsupported".to_owned()), - }, - Unit::Auto => Err("Unit 'auto' isn't supported with --to options".to_owned()), - Unit::None => Ok((i, None)), - } -} - -fn transform_to(s: f64, opts: &Transform) -> Result { - let (i2, s) = consider_suffix(s, &opts.unit)?; - Ok(match s { - None => format!("{}", i2), - Some(s) => format!("{:.1}{}", i2, DisplayableSuffix(s)), - }) -} - -fn format_string(source: String, options: &NumfmtOptions) -> Result { - let number = transform_to( - transform_from(source, &options.transform.from)?, - &options.transform.to, - )?; - - Ok(match options.padding { - p if p == 0 => number, - p if p > 0 => format!("{:>padding$}", number, padding = p as usize), - p => format!("{: Result { - let transform = TransformOptions { - from: Transform { - unit: args - .opt_str("from") - .map(parse_unit) - .unwrap_or(Ok(Unit::None))?, - }, - to: Transform { - unit: args - .opt_str("to") - .map(parse_unit) - .unwrap_or(Ok(Unit::None))?, - }, - }; - - let padding = match args.opt_str("padding") { - Some(s) => s.parse::().map_err(|err| err.to_string()), - None => Ok(0), - }?; - - let header = match args.opt_default("header", "1") { - Some(s) => s.parse::().map_err(|err| err.to_string()), - None => Ok(0), - }?; - - Ok(NumfmtOptions { - transform, - padding, - header, - }) -} - -fn handle_args(args: &[String], options: NumfmtOptions) -> Result<()> { - for l in args { - println!("{}", format_string(l.clone(), &options)?) - } - Ok(()) -} - -fn handle_stdin(options: NumfmtOptions) -> Result<()> { - let stdin = std::io::stdin(); - let locked_stdin = stdin.lock(); - - let mut lines = locked_stdin.lines(); - for l in lines.by_ref().take(options.header) { - l.map(|s| println!("{}", s)).map_err(|e| e.to_string())? - } - - for l in lines { - l.map_err(|e| e.to_string()).and_then(|l| { - let l = format_string(l, &options)?; - println!("{}", l); - Ok(()) - })? - } - Ok(()) -} - -pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); - - let mut opts = Options::new(); - - opts.optflag("h", "help", "display this help and exit"); - opts.optflag("V", "version", "output version information and exit"); - opts.optopt( - "", - "from", - "auto-scale input numbers to UNITs; default is 'none'; see UNIT above", - "UNIT", - ); - opts.optopt( - "", - "to", - "auto-scale output numbers to UNITs; see Unit above", - "UNIT", - ); - opts.optopt( - "", - "padding", - "pad the output to N characters; positive N will right-align; negative N will left-align; padding is ignored if the output is wider than N", - "N" - ); - opts.optflagopt( - "", - "header", - "print (without converting) the first N header lines; N defaults to 1 if not specified", - "N", - ); - - let matches = opts.parse(&args[1..]).unwrap(); - if matches.opt_present("help") { - println!("{} {}", NAME, VERSION); - println!(); - println!("Usage:"); - println!(" {0} [STRING]... [OPTION]...", NAME); - println!(); - print!( - "{}", - opts.usage("Convert numbers from/to human-readable strings") - ); - println!( - "UNIT options: +static ABOUT: &str = "Convert numbers from/to human-readable strings"; +static LONG_HELP: &str = "UNIT options: none no auto-scaling is done; suffixes will trigger an error auto accept optional single/two letter suffix: @@ -341,23 +38,187 @@ pub fn uumain(args: impl uucore::Args) -> i32 { iec-i accept optional two-letter suffix: - 1Ki = 1024, 1Mi = 1048576, ..." - ); + 1Ki = 1024, 1Mi = 1048576, ... - return 0; - } - if matches.opt_present("version") { - println!("{} {}", NAME, VERSION); - return 0; +FIELDS supports cut(1) style field ranges: + N N'th field, counted from 1 + N- from N'th field, to end of line + N-M from N'th to M'th field (inclusive) + -M from first to M'th field (inclusive) + - all fields +Multiple fields/ranges can be separated with commas +"; + +fn get_usage() -> String { + format!("{0} [OPTION]... [NUMBER]...", executable!()) +} + +fn handle_args<'a>(args: impl Iterator, options: NumfmtOptions) -> Result<()> { + for l in args { + format_and_print(l, &options)?; } - let options = parse_options(&matches).unwrap(); + Ok(()) +} - if matches.free.is_empty() { - handle_stdin(options).unwrap() - } else { - handle_args(&matches.free, options).unwrap() +fn handle_stdin(options: NumfmtOptions) -> Result<()> { + let stdin = std::io::stdin(); + let locked_stdin = stdin.lock(); + + let mut lines = locked_stdin.lines(); + for l in lines.by_ref().take(options.header) { + l.map(|s| println!("{}", s)).map_err(|e| e.to_string())?; + } + + for l in lines { + l.map_err(|e| e.to_string()) + .and_then(|l| format_and_print(&l, &options))?; + } + + Ok(()) +} + +fn parse_unit(s: &str) -> Result { + match s { + "auto" => Ok(Unit::Auto), + "si" => Ok(Unit::Si), + "iec" => Ok(Unit::Iec(false)), + "iec-i" => Ok(Unit::Iec(true)), + "none" => Ok(Unit::None), + _ => Err("Unsupported unit is specified".to_owned()), + } +} + +fn parse_options(args: &ArgMatches) -> Result { + let from = parse_unit(args.value_of(options::FROM).unwrap())?; + let to = parse_unit(args.value_of(options::TO).unwrap())?; + + let transform = TransformOptions { + from: Transform { unit: from }, + to: Transform { unit: to }, }; - 0 + let padding = match args.value_of(options::PADDING) { + Some(s) => s.parse::().map_err(|err| err.to_string()), + None => Ok(0), + }?; + + let header = match args.occurrences_of(options::HEADER) { + 0 => Ok(0), + _ => { + let value = args.value_of(options::HEADER).unwrap(); + + value + .parse::() + .map_err(|_| value) + .and_then(|n| match n { + 0 => Err(value), + _ => Ok(n), + }) + .map_err(|value| format!("invalid header value ‘{}’", value)) + } + }?; + + let fields = match args.value_of(options::FIELD) { + Some("-") => vec![Range { + low: 1, + high: std::usize::MAX, + }], + Some(v) => Range::from_list(v)?, + None => unreachable!(), + }; + + let delimiter = args.value_of(options::DELIMITER).map_or(Ok(None), |arg| { + if arg.len() == 1 { + Ok(Some(arg.to_string())) + } else { + Err("the delimiter must be a single character".to_string()) + } + })?; + + Ok(NumfmtOptions { + transform, + padding, + header, + fields, + delimiter, + }) +} + +pub fn uumain(args: impl uucore::Args) -> i32 { + let usage = get_usage(); + + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(&usage[..]) + .after_help(LONG_HELP) + .setting(AppSettings::AllowNegativeNumbers) + .arg( + Arg::with_name(options::DELIMITER) + .short("d") + .long(options::DELIMITER) + .value_name("X") + .help("use X instead of whitespace for field delimiter"), + ) + .arg( + Arg::with_name(options::FIELD) + .long(options::FIELD) + .help("replace the numbers in these input fields (default=1) see FIELDS below") + .value_name("FIELDS") + .default_value(options::FIELD_DEFAULT), + ) + .arg( + Arg::with_name(options::FROM) + .long(options::FROM) + .help("auto-scale input numbers to UNITs; see UNIT below") + .value_name("UNIT") + .default_value(options::FROM_DEFAULT), + ) + .arg( + Arg::with_name(options::TO) + .long(options::TO) + .help("auto-scale output numbers to UNITs; see UNIT below") + .value_name("UNIT") + .default_value(options::TO_DEFAULT), + ) + .arg( + Arg::with_name(options::PADDING) + .long(options::PADDING) + .help( + "pad the output to N characters; positive N will \ + right-align; negative N will left-align; padding is \ + ignored if the output is wider than N; the default is \ + to automatically pad if a whitespace is found", + ) + .value_name("N"), + ) + .arg( + Arg::with_name(options::HEADER) + .long(options::HEADER) + .help( + "print (without converting) the first N header lines; \ + N defaults to 1 if not specified", + ) + .value_name("N") + .default_value(options::HEADER_DEFAULT) + .hide_default_value(true), + ) + .arg(Arg::with_name(options::NUMBER).hidden(true).multiple(true)) + .get_matches_from(args); + + let result = + parse_options(&matches).and_then(|options| match matches.values_of(options::NUMBER) { + Some(values) => handle_args(values, options), + None => handle_stdin(options), + }); + + match result { + Err(e) => { + std::io::stdout().flush().expect("error flushing stdout"); + show_info!("{}", e); + 1 + } + _ => 0, + } } diff --git a/src/uu/numfmt/src/options.rs b/src/uu/numfmt/src/options.rs new file mode 100644 index 000000000..17f0a6fbe --- /dev/null +++ b/src/uu/numfmt/src/options.rs @@ -0,0 +1,27 @@ +use crate::units::Transform; +use uucore::ranges::Range; + +pub const DELIMITER: &str = "delimiter"; +pub const FIELD: &str = "field"; +pub const FIELD_DEFAULT: &str = "1"; +pub const FROM: &str = "from"; +pub const FROM_DEFAULT: &str = "none"; +pub const HEADER: &str = "header"; +pub const HEADER_DEFAULT: &str = "1"; +pub const NUMBER: &str = "NUMBER"; +pub const PADDING: &str = "padding"; +pub const TO: &str = "to"; +pub const TO_DEFAULT: &str = "none"; + +pub struct TransformOptions { + pub from: Transform, + pub to: Transform, +} + +pub struct NumfmtOptions { + pub transform: TransformOptions, + pub padding: isize, + pub header: usize, + pub fields: Vec, + pub delimiter: Option, +} diff --git a/src/uu/numfmt/src/units.rs b/src/uu/numfmt/src/units.rs new file mode 100644 index 000000000..5f9907bdf --- /dev/null +++ b/src/uu/numfmt/src/units.rs @@ -0,0 +1,67 @@ +use std::fmt; + +pub const SI_BASES: [f64; 10] = [1., 1e3, 1e6, 1e9, 1e12, 1e15, 1e18, 1e21, 1e24, 1e27]; + +pub const IEC_BASES: [f64; 10] = [ + 1., + 1_024., + 1_048_576., + 1_073_741_824., + 1_099_511_627_776., + 1_125_899_906_842_624., + 1_152_921_504_606_846_976., + 1_180_591_620_717_411_303_424., + 1_208_925_819_614_629_174_706_176., + 1_237_940_039_285_380_274_899_124_224., +]; + +pub type WithI = bool; + +pub enum Unit { + Auto, + Si, + Iec(WithI), + None, +} + +pub struct Transform { + pub unit: Unit, +} + +pub type Result = std::result::Result; + +#[derive(Clone, Copy, Debug)] +pub enum RawSuffix { + K, + M, + G, + T, + P, + E, + Z, + Y, +} + +pub type Suffix = (RawSuffix, WithI); + +pub struct DisplayableSuffix(pub Suffix); + +impl fmt::Display for DisplayableSuffix { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let DisplayableSuffix((ref raw_suffix, ref with_i)) = *self; + match raw_suffix { + RawSuffix::K => write!(f, "K"), + RawSuffix::M => write!(f, "M"), + RawSuffix::G => write!(f, "G"), + RawSuffix::T => write!(f, "T"), + RawSuffix::P => write!(f, "P"), + RawSuffix::E => write!(f, "E"), + RawSuffix::Z => write!(f, "Z"), + RawSuffix::Y => write!(f, "Y"), + } + .and_then(|()| match with_i { + true => write!(f, "i"), + false => Ok(()), + }) + } +} diff --git a/src/uu/od/Cargo.toml b/src/uu/od/Cargo.toml index dd58a2b89..6f9a75318 100644 --- a/src/uu/od/Cargo.toml +++ b/src/uu/od/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_od" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "od ~ (uutils) display formatted representation of input" @@ -16,11 +16,11 @@ path = "src/od.rs" [dependencies] byteorder = "1.3.2" -getopts = "0.2.18" +clap = "2.33" half = "1.6" libc = "0.2.42" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "od" diff --git a/src/uu/od/src/inputdecoder.rs b/src/uu/od/src/inputdecoder.rs index 377f0c4ef..3b36c28fb 100644 --- a/src/uu/od/src/inputdecoder.rs +++ b/src/uu/od/src/inputdecoder.rs @@ -38,10 +38,7 @@ impl<'a, I> InputDecoder<'a, I> { peek_length: usize, byte_order: ByteOrder, ) -> InputDecoder { - let mut bytes: Vec = Vec::with_capacity(normal_length + peek_length); - unsafe { - bytes.set_len(normal_length + peek_length); - } // fast but uninitialized + let bytes = vec![0; normal_length + peek_length]; InputDecoder { input, diff --git a/src/uu/od/src/multifilereader.rs b/src/uu/od/src/multifilereader.rs index e41c9e39b..1255da66d 100644 --- a/src/uu/od/src/multifilereader.rs +++ b/src/uu/od/src/multifilereader.rs @@ -24,7 +24,7 @@ pub trait HasError { } impl<'b> MultifileReader<'b> { - pub fn new<'a>(fnames: Vec>) -> MultifileReader<'a> { + pub fn new(fnames: Vec) -> MultifileReader { let mut mf = MultifileReader { ni: fnames, curr_file: None, // normally this means done; call next_file() diff --git a/src/uu/od/src/od.rs b/src/uu/od/src/od.rs index 84848a72c..c3b39fca1 100644 --- a/src/uu/od/src/od.rs +++ b/src/uu/od/src/od.rs @@ -7,10 +7,6 @@ // spell-checker:ignore (ToDO) formatteriteminfo inputdecoder inputoffset mockstream nrofbytes partialreader odfunc multifile exitcode -extern crate byteorder; -extern crate getopts; -extern crate half; - #[macro_use] extern crate uucore; @@ -45,15 +41,18 @@ use crate::parse_nrofbytes::parse_number_of_bytes; use crate::partialreader::*; use crate::peekreader::*; use crate::prn_char::format_ascii_dump; +use clap::{self, AppSettings, Arg, ArgMatches}; static VERSION: &str = env!("CARGO_PKG_VERSION"); const PEEK_BUFFER_SIZE: usize = 4; // utf-8 can be 4 bytes +static ABOUT: &str = "dump files in octal and other formats"; -static USAGE: &str = r#"Usage: +static USAGE: &str = r#" od [OPTION]... [--] [FILENAME]... od [-abcdDefFhHiIlLoOsxX] [FILENAME] [[+][0x]OFFSET[.][b]] - od --traditional [OPTION]... [FILENAME] [[+][0x]OFFSET[.][b] [[+][0x]LABEL[.][b]]] + od --traditional [OPTION]... [FILENAME] [[+][0x]OFFSET[.][b] [[+][0x]LABEL[.][b]]]"#; +static LONG_HELP: &str = r#" Displays data in various human-readable formats. If multiple formats are specified, the output will contain all formats in the order they appear on the command line. Each format will be printed on a new line. Only the line @@ -92,85 +91,18 @@ Any type specification can have a "z" suffix, which will add a ASCII dump at If an error occurred, a diagnostic message will be printed to stderr, and the exitcode will be non-zero."#; -fn create_getopts_options() -> getopts::Options { - let mut opts = getopts::Options::new(); - - opts.optopt( - "A", - "address-radix", - "Select the base in which file offsets are printed.", - "RADIX", - ); - opts.optopt( - "j", - "skip-bytes", - "Skip bytes input bytes before formatting and writing.", - "BYTES", - ); - opts.optopt( - "N", - "read-bytes", - "limit dump to BYTES input bytes", - "BYTES", - ); - opts.optopt( - "", - "endian", - "byte order to use for multi-byte formats", - "big|little", - ); - opts.optopt( - "S", - "strings", - "output strings of at least BYTES graphic chars. 3 is assumed when \ - BYTES is not specified.", - "BYTES", - ); - opts.optflagmulti("a", "", "named characters, ignoring high-order bit"); - opts.optflagmulti("b", "", "octal bytes"); - opts.optflagmulti("c", "", "ASCII characters or backslash escapes"); - opts.optflagmulti("d", "", "unsigned decimal 2-byte units"); - opts.optflagmulti("D", "", "unsigned decimal 4-byte units"); - opts.optflagmulti("o", "", "octal 2-byte units"); - - opts.optflagmulti("I", "", "decimal 8-byte units"); - opts.optflagmulti("L", "", "decimal 8-byte units"); - opts.optflagmulti("i", "", "decimal 4-byte units"); - opts.optflagmulti("l", "", "decimal 8-byte units"); - opts.optflagmulti("x", "", "hexadecimal 2-byte units"); - opts.optflagmulti("h", "", "hexadecimal 2-byte units"); - - opts.optflagmulti("O", "", "octal 4-byte units"); - opts.optflagmulti("s", "", "decimal 2-byte units"); - opts.optflagmulti("X", "", "hexadecimal 4-byte units"); - opts.optflagmulti("H", "", "hexadecimal 4-byte units"); - - opts.optflagmulti("e", "", "floating point double precision (64-bit) units"); - opts.optflagmulti("f", "", "floating point single precision (32-bit) units"); - opts.optflagmulti("F", "", "floating point double precision (64-bit) units"); - - opts.optmulti("t", "format", "select output format or formats", "TYPE"); - opts.optflag( - "v", - "output-duplicates", - "do not use * to mark line suppression", - ); - opts.optflagopt( - "w", - "width", - "output BYTES bytes per output line. 32 is implied when BYTES is not \ - specified.", - "BYTES", - ); - opts.optflag("", "help", "display this help and exit."); - opts.optflag("", "version", "output version information and exit."); - opts.optflag( - "", - "traditional", - "compatibility mode with one input, offset and label.", - ); - - opts +pub(crate) mod options { + pub const ADDRESS_RADIX: &str = "address-radix"; + pub const SKIP_BYTES: &str = "skip-bytes"; + pub const READ_BYTES: &str = "read-bytes"; + pub const ENDIAN: &str = "endian"; + pub const STRINGS: &str = "strings"; + pub const FORMAT: &str = "format"; + pub const OUTPUT_DUPLICATES: &str = "output-duplicates"; + pub const TRADITIONAL: &str = "traditional"; + pub const WIDTH: &str = "width"; + pub const VERSION: &str = "version"; + pub const FILENAME: &str = "FILENAME"; } struct OdOptions { @@ -186,8 +118,8 @@ struct OdOptions { } impl OdOptions { - fn new(matches: getopts::Matches, args: Vec) -> Result { - let byte_order = match matches.opt_str("endian").as_ref().map(String::as_ref) { + fn new<'a>(matches: ArgMatches<'a>, args: Vec) -> Result { + let byte_order = match matches.value_of(options::ENDIAN) { None => ByteOrder::Native, Some("little") => ByteOrder::Little, Some("big") => ByteOrder::Big, @@ -196,7 +128,7 @@ impl OdOptions { } }; - let mut skip_bytes = match matches.opt_default("skip-bytes", "0") { + let mut skip_bytes = match matches.value_of(options::SKIP_BYTES) { None => 0, Some(s) => match parse_number_of_bytes(&s) { Ok(i) => i, @@ -227,12 +159,10 @@ impl OdOptions { } }; - let mut line_bytes = match matches.opt_default("w", "32") { + let mut line_bytes = match matches.value_of(options::WIDTH) { None => 16, - Some(s) => match s.parse::() { - Ok(i) => i, - Err(_) => 0, - }, + Some(_) if matches.occurrences_of(options::WIDTH) == 0 => 16, + Some(s) => s.parse::().unwrap_or(0), }; let min_bytes = formats.iter().fold(1, |max, next| { cmp::max(max, next.formatter_item_info.byte_size) @@ -242,9 +172,9 @@ impl OdOptions { line_bytes = min_bytes; } - let output_duplicates = matches.opt_present("v"); + let output_duplicates = matches.is_present(options::OUTPUT_DUPLICATES); - let read_bytes = match matches.opt_str("read-bytes") { + let read_bytes = match matches.value_of(options::READ_BYTES) { None => None, Some(s) => match parse_number_of_bytes(&s) { Ok(i) => Some(i), @@ -254,10 +184,10 @@ impl OdOptions { }, }; - let radix = match matches.opt_str("A") { + let radix = match matches.value_of(options::ADDRESS_RADIX) { None => Radix::Octal, Some(s) => { - let st = s.into_bytes(); + let st = s.as_bytes(); if st.len() != 1 { return Err("Radix must be one of [d, o, n, x]".to_string()); } else { @@ -293,26 +223,244 @@ impl OdOptions { pub fn uumain(args: impl uucore::Args) -> i32 { let args = args.collect_str(); - let opts = create_getopts_options(); + let clap_opts = clap::App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(USAGE) + .after_help(LONG_HELP) + .arg( + Arg::with_name(options::ADDRESS_RADIX) + .short("A") + .long(options::ADDRESS_RADIX) + .help("Select the base in which file offsets are printed.") + .value_name("RADIX"), + ) + .arg( + Arg::with_name(options::SKIP_BYTES) + .short("j") + .long(options::SKIP_BYTES) + .help("Skip bytes input bytes before formatting and writing.") + .value_name("BYTES"), + ) + .arg( + Arg::with_name(options::READ_BYTES) + .short("N") + .long(options::READ_BYTES) + .help("limit dump to BYTES input bytes") + .value_name("BYTES"), + ) + .arg( + Arg::with_name(options::ENDIAN) + .long(options::ENDIAN) + .help("byte order to use for multi-byte formats") + .possible_values(&["big", "little"]) + .value_name("big|little"), + ) + .arg( + Arg::with_name(options::STRINGS) + .short("S") + .long(options::STRINGS) + .help( + "output strings of at least BYTES graphic chars. 3 is assumed when \ + BYTES is not specified.", + ) + .default_value("3") + .value_name("BYTES"), + ) + .arg( + Arg::with_name("a") + .short("a") + .help("named characters, ignoring high-order bit") + .multiple(true) + .takes_value(false), + ) + .arg( + Arg::with_name("b") + .short("b") + .help("octal bytes") + .multiple(true) + .takes_value(false), + ) + .arg( + Arg::with_name("c") + .short("c") + .help("ASCII characters or backslash escapes") + .multiple(true) + .takes_value(false), + ) + .arg( + Arg::with_name("d") + .short("d") + .help("unsigned decimal 2-byte units") + .multiple(true) + .takes_value(false), + ) + .arg( + Arg::with_name("D") + .short("D") + .help("unsigned decimal 4-byte units") + .multiple(true) + .takes_value(false), + ) + .arg( + Arg::with_name("o") + .short("o") + .help("octal 2-byte units") + .multiple(true) + .takes_value(false), + ) + .arg( + Arg::with_name("I") + .short("I") + .help("decimal 8-byte units") + .multiple(true) + .takes_value(false), + ) + .arg( + Arg::with_name("L") + .short("L") + .help("decimal 8-byte units") + .multiple(true) + .takes_value(false), + ) + .arg( + Arg::with_name("i") + .short("i") + .help("decimal 4-byte units") + .multiple(true) + .takes_value(false), + ) + .arg( + Arg::with_name("l") + .short("l") + .help("decimal 8-byte units") + .multiple(true) + .takes_value(false), + ) + .arg( + Arg::with_name("x") + .short("x") + .help("hexadecimal 2-byte units") + .multiple(true) + .takes_value(false), + ) + .arg( + Arg::with_name("h") + .short("h") + .help("hexadecimal 2-byte units") + .multiple(true) + .takes_value(false), + ) + .arg( + Arg::with_name("O") + .short("O") + .help("octal 4-byte units") + .multiple(true) + .takes_value(false), + ) + .arg( + Arg::with_name("s") + .short("s") + .help("decimal 2-byte units") + .multiple(true) + .takes_value(false), + ) + .arg( + Arg::with_name("X") + .short("X") + .help("hexadecimal 4-byte units") + .multiple(true) + .takes_value(false), + ) + .arg( + Arg::with_name("H") + .short("H") + .help("hexadecimal 4-byte units") + .multiple(true) + .takes_value(false), + ) + .arg( + Arg::with_name("e") + .short("e") + .help("floating point double precision (64-bit) units") + .multiple(true) + .takes_value(false), + ) + .arg( + Arg::with_name("f") + .short("f") + .help("floating point double precision (32-bit) units") + .multiple(true) + .takes_value(false), + ) + .arg( + Arg::with_name("F") + .short("F") + .help("floating point double precision (64-bit) units") + .multiple(true) + .takes_value(false), + ) + .arg( + Arg::with_name(options::FORMAT) + .short("t") + .long(options::FORMAT) + .help("select output format or formats") + .multiple(true) + .value_name("TYPE"), + ) + .arg( + Arg::with_name(options::OUTPUT_DUPLICATES) + .short("v") + .long(options::OUTPUT_DUPLICATES) + .help("do not use * to mark line suppression") + .takes_value(false) + .possible_values(&["big", "little"]), + ) + .arg( + Arg::with_name(options::WIDTH) + .short("w") + .long(options::WIDTH) + .help( + "output BYTES bytes per output line. 32 is implied when BYTES is not \ + specified.", + ) + .default_value("32") + .value_name("BYTES"), + ) + .arg( + Arg::with_name(options::VERSION) + .long(options::VERSION) + .help("output version information and exit.") + .takes_value(false), + ) + .arg( + Arg::with_name(options::TRADITIONAL) + .long(options::TRADITIONAL) + .help("compatibility mode with one input, offset and label.") + .takes_value(false), + ) + .arg( + Arg::with_name(options::FILENAME) + .hidden(true) + .multiple(true), + ) + .settings(&[ + AppSettings::TrailingVarArg, + AppSettings::DontDelimitTrailingValues, + AppSettings::DisableVersion, + AppSettings::DeriveDisplayOrder, + ]); - let matches = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(f) => { - show_usage_error!("{}", f); - return 1; - } - }; + let clap_matches = clap_opts + .clone() // Clone to reuse clap_otps to print help + .get_matches_from(args.clone()); - if matches.opt_present("help") { - println!("{}", opts.usage(&USAGE)); - return 0; - } - if matches.opt_present("version") { + if clap_matches.is_present(options::VERSION) { println!("{} {}", executable!(), VERSION); return 0; } - let od_options = match OdOptions::new(matches, args) { + let od_options = match OdOptions::new(clap_matches, args) { Err(s) => { show_usage_error!("{}", s); return 1; @@ -479,11 +627,11 @@ fn print_bytes(prefix: &str, input_decoder: &MemoryDecoder, output_info: &Output /// /// `skip_bytes` is the number of bytes skipped from the input /// `read_bytes` is an optional limit to the number of bytes to read -fn open_input_peek_reader<'a>( - input_strings: &'a [String], +fn open_input_peek_reader( + input_strings: &[String], skip_bytes: usize, read_bytes: Option, -) -> PeekReader>> { +) -> PeekReader> { // should return "impl PeekRead + Read + HasError" when supported in (stable) rust let inputs = input_strings .iter() diff --git a/src/uu/od/src/parse_formats.rs b/src/uu/od/src/parse_formats.rs index 61be92c40..8b32d648c 100644 --- a/src/uu/od/src/parse_formats.rs +++ b/src/uu/od/src/parse_formats.rs @@ -85,6 +85,8 @@ fn od_format_type(type_char: FormatType, byte_size: u8) -> Option bool { + #[allow(clippy::match_like_matches_macro)] + // `matches!(...)` macro not stabilized until rust v1.42 match ch { 'A' | 'j' | 'N' | 'S' | 'w' => true, _ => false, diff --git a/src/uu/od/src/parse_inputs.rs b/src/uu/od/src/parse_inputs.rs index 89a833d94..915aa1d92 100644 --- a/src/uu/od/src/parse_inputs.rs +++ b/src/uu/od/src/parse_inputs.rs @@ -1,20 +1,24 @@ -use getopts::Matches; +use super::options; +use clap::ArgMatches; /// Abstraction for getopts pub trait CommandLineOpts { /// returns all command line parameters which do not belong to an option. - fn inputs(&self) -> Vec; + fn inputs(&self) -> Vec<&str>; /// tests if any of the specified options is present. fn opts_present(&self, _: &[&str]) -> bool; } /// Implementation for `getopts` -impl CommandLineOpts for Matches { - fn inputs(&self) -> Vec { - self.free.clone() +impl<'a> CommandLineOpts for ArgMatches<'a> { + fn inputs(&self) -> Vec<&str> { + self.values_of(options::FILENAME) + .map(|values| values.collect()) + .unwrap_or_default() } + fn opts_present(&self, opts: &[&str]) -> bool { - self.opts_present(&opts.iter().map(|s| (*s).to_string()).collect::>()) + opts.iter().any(|opt| self.is_present(opt)) } } @@ -39,7 +43,7 @@ pub enum CommandLineInputs { /// '-' is used as filename if stdin is meant. This is also returned if /// there is no input, as stdin is the default input. pub fn parse_inputs(matches: &dyn CommandLineOpts) -> Result { - let mut input_strings: Vec = matches.inputs(); + let mut input_strings = matches.inputs(); if matches.opts_present(&["traditional"]) { return parse_inputs_traditional(input_strings); @@ -59,7 +63,7 @@ pub fn parse_inputs(matches: &dyn CommandLineOpts) -> Result Result) -> Result { +pub fn parse_inputs_traditional(input_strings: Vec<&str>) -> Result { match input_strings.len() { 0 => Ok(CommandLineInputs::FileNames(vec!["-".to_string()])), 1 => { let offset0 = parse_offset_operand(&input_strings[0]); Ok(match offset0 { Ok(n) => CommandLineInputs::FileAndOffset(("-".to_string(), n, None)), - _ => CommandLineInputs::FileNames(input_strings), + _ => CommandLineInputs::FileNames( + input_strings.iter().map(|s| s.to_string()).collect(), + ), }) } 2 => { @@ -98,7 +106,7 @@ pub fn parse_inputs_traditional(input_strings: Vec) -> Result Ok(CommandLineInputs::FileAndOffset(( - input_strings[0].clone(), + input_strings[0].clone().to_owned(), m, None, ))), @@ -110,7 +118,7 @@ pub fn parse_inputs_traditional(input_strings: Vec) -> Result Ok(CommandLineInputs::FileAndOffset(( - input_strings[0].clone(), + input_strings[0].clone().to_owned(), n, Some(m), ))), @@ -178,8 +186,8 @@ mod tests { } impl<'a> CommandLineOpts for MockOptions<'a> { - fn inputs(&self) -> Vec { - self.inputs.clone() + fn inputs(&self) -> Vec<&str> { + self.inputs.iter().map(|s| s.as_str()).collect() } fn opts_present(&self, opts: &[&str]) -> bool { for expected in opts.iter() { diff --git a/src/uu/od/src/partialreader.rs b/src/uu/od/src/partialreader.rs index 7e906f515..ee3588830 100644 --- a/src/uu/od/src/partialreader.rs +++ b/src/uu/od/src/partialreader.rs @@ -8,7 +8,7 @@ use crate::multifilereader::HasError; /// When a large number of bytes must be skipped, it will be read into a /// dynamically allocated buffer. The buffer will be limited to this size. -const MAX_SKIP_BUFFER: usize = 64 * 1024; +const MAX_SKIP_BUFFER: usize = 16 * 1024; /// Wrapper for `std::io::Read` which can skip bytes at the beginning /// of the input, and it can limit the returned bytes to a particular @@ -31,21 +31,25 @@ impl PartialReader { impl Read for PartialReader { fn read(&mut self, out: &mut [u8]) -> io::Result { if self.skip > 0 { - let buf_size = cmp::min(self.skip, MAX_SKIP_BUFFER); - let mut bytes: Vec = Vec::with_capacity(buf_size); - unsafe { - bytes.set_len(buf_size); - } + let mut bytes = [0; MAX_SKIP_BUFFER]; while self.skip > 0 { - let skip_count = cmp::min(self.skip, buf_size); + let skip_count = cmp::min(self.skip, MAX_SKIP_BUFFER); - match self.inner.read_exact(&mut bytes[..skip_count]) { + match self.inner.read(&mut bytes[..skip_count]) { + Ok(0) => { + // this is an error as we still have more to skip + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "tried to skip past end of input", + )); + } + Ok(n) => self.skip -= n, Err(e) => return Err(e), - Ok(()) => self.skip -= skip_count, } } } + match self.limit { None => self.inner.read(out), Some(0) => Ok(0), diff --git a/src/uu/paste/Cargo.toml b/src/uu/paste/Cargo.toml index 200e189c3..4e9971368 100644 --- a/src/uu/paste/Cargo.toml +++ b/src/uu/paste/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_paste" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "paste ~ (uutils) merge lines from inputs" @@ -15,9 +15,9 @@ edition = "2018" path = "src/paste.rs" [dependencies] -getopts = "0.2.18" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +clap = "2.33.3" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "paste" diff --git a/src/uu/paste/src/paste.rs b/src/uu/paste/src/paste.rs index 345e3722a..751cc0a04 100644 --- a/src/uu/paste/src/paste.rs +++ b/src/uu/paste/src/paste.rs @@ -7,78 +7,85 @@ // spell-checker:ignore (ToDO) delim -extern crate getopts; - #[macro_use] extern crate uucore; +use clap::{App, Arg}; use std::fs::File; use std::io::{stdin, BufRead, BufReader, Read}; use std::iter::repeat; use std::path::Path; -static NAME: &str = "paste"; static VERSION: &str = env!("CARGO_PKG_VERSION"); +static ABOUT: &str = "Write lines consisting of the sequentially corresponding lines from each +FILE, separated by TABs, to standard output."; + +mod options { + pub const DELIMITER: &str = "delimiters"; + pub const SERIAL: &str = "serial"; + pub const FILE: &str = "file"; +} + +// Wraps BufReader and stdin +fn read_line( + reader: Option<&mut BufReader>, + buf: &mut String, +) -> std::io::Result { + match reader { + Some(reader) => reader.read_line(buf), + None => stdin().read_line(buf), + } +} pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .arg( + Arg::with_name(options::SERIAL) + .long(options::SERIAL) + .short("s") + .help("paste one file at a time instead of in parallel"), + ) + .arg( + Arg::with_name(options::DELIMITER) + .long(options::DELIMITER) + .short("d") + .help("reuse characters from LIST instead of TABs") + .value_name("LIST") + .default_value("\t") + .hide_default_value(true), + ) + .arg( + Arg::with_name(options::FILE) + .value_name("FILE") + .multiple(true) + .default_value("-"), + ) + .get_matches_from(args); - let mut opts = getopts::Options::new(); - - opts.optflag( - "s", - "serial", - "paste one file at a time instead of in parallel", - ); - opts.optopt( - "d", - "delimiters", - "reuse characters from LIST instead of TABs", - "LIST", - ); - opts.optflag("h", "help", "display this help and exit"); - opts.optflag("V", "version", "output version information and exit"); - - let matches = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(e) => crash!(1, "{}", e), - }; - - if matches.opt_present("help") { - let msg = format!( - "{0} {1} - -Usage: - {0} [OPTION]... [FILE]... - -Write lines consisting of the sequentially corresponding lines from each -FILE, separated by TABs, to standard output.", - NAME, VERSION - ); - print!("{}", opts.usage(&msg)); - } else if matches.opt_present("version") { - println!("{} {}", NAME, VERSION); - } else { - let serial = matches.opt_present("serial"); - let delimiters = matches - .opt_str("delimiters") - .unwrap_or_else(|| "\t".to_owned()); - paste(matches.free, serial, delimiters); - } + let serial = matches.is_present(options::SERIAL); + let delimiters = matches.value_of(options::DELIMITER).unwrap().to_owned(); + let files = matches + .values_of(options::FILE) + .unwrap() + .map(|s| s.to_owned()) + .collect(); + paste(files, serial, delimiters); 0 } fn paste(filenames: Vec, serial: bool, delimiters: String) { - let mut files: Vec>> = filenames + let mut files: Vec<_> = filenames .into_iter() .map(|name| { - BufReader::new(if name == "-" { - Box::new(stdin()) as Box + if name == "-" { + None } else { let r = crash_if_err!(1, File::open(Path::new(&name))); - Box::new(r) as Box - }) + Some(BufReader::new(r)) + } }) .collect(); @@ -93,7 +100,7 @@ fn paste(filenames: Vec, serial: bool, delimiters: String) { let mut output = String::new(); loop { let mut line = String::new(); - match file.read_line(&mut line) { + match read_line(file.as_mut(), &mut line) { Ok(0) => break, Ok(_) => { output.push_str(line.trim_end()); @@ -115,7 +122,7 @@ fn paste(filenames: Vec, serial: bool, delimiters: String) { eof_count += 1; } else { let mut line = String::new(); - match file.read_line(&mut line) { + match read_line(file.as_mut(), &mut line) { Ok(0) => { eof[i] = true; eof_count += 1; diff --git a/src/uu/pathchk/Cargo.toml b/src/uu/pathchk/Cargo.toml index 3db2a5750..8c4e61d2b 100644 --- a/src/uu/pathchk/Cargo.toml +++ b/src/uu/pathchk/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_pathchk" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "pathchk ~ (uutils) diagnose invalid or non-portable PATHNAME" @@ -15,10 +15,10 @@ edition = "2018" path = "src/pathchk.rs" [dependencies] -getopts = "0.2.18" +clap = "2.33" libc = "0.2.42" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "pathchk" diff --git a/src/uu/pathchk/src/pathchk.rs b/src/uu/pathchk/src/pathchk.rs index 972cd4873..c27e52513 100644 --- a/src/uu/pathchk/src/pathchk.rs +++ b/src/uu/pathchk/src/pathchk.rs @@ -9,13 +9,10 @@ // spell-checker:ignore (ToDO) lstat -extern crate getopts; -extern crate libc; - #[macro_use] extern crate uucore; -use getopts::Options; +use clap::{App, Arg}; use std::fs; use std::io::{ErrorKind, Write}; @@ -25,107 +22,94 @@ enum Mode { Basic, // check basic compatibility with POSIX Extra, // check for leading dashes and empty names Both, // a combination of `Basic` and `Extra` - Help, // show help - Version, // show version information } static NAME: &str = "pathchk"; static VERSION: &str = env!("CARGO_PKG_VERSION"); +static ABOUT: &str = "Check whether file names are valid or portable"; + +mod options { + pub const POSIX: &str = "posix"; + pub const POSIX_SPECIAL: &str = "posix-special"; + pub const PORTABILITY: &str = "portability"; + pub const PATH: &str = "path"; +} // a few global constants as used in the GNU implementation const POSIX_PATH_MAX: usize = 256; const POSIX_NAME_MAX: usize = 14; -pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); +fn get_usage() -> String { + format!("{0} [OPTION]... NAME...", executable!()) +} - // add options - let mut opts = Options::new(); - opts.optflag("p", "posix", "check for (most) POSIX systems"); - opts.optflag( - "P", - "posix-special", - "check for empty names and leading \"-\"", - ); - opts.optflag( - "", - "portability", - "check for all POSIX systems (equivalent to -p -P)", - ); - opts.optflag("h", "help", "display this help text and exit"); - opts.optflag("V", "version", "output version information and exit"); - let matches = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(e) => crash!(1, "{}", e), - }; +pub fn uumain(args: impl uucore::Args) -> i32 { + let usage = get_usage(); + + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(&usage[..]) + .arg( + Arg::with_name(options::POSIX) + .short("p") + .help("check for most POSIX systems"), + ) + .arg( + Arg::with_name(options::POSIX_SPECIAL) + .short("P") + .help(r#"check for empty names and leading "-""#), + ) + .arg( + Arg::with_name(options::PORTABILITY) + .long(options::PORTABILITY) + .help("check for all POSIX systems (equivalent to -p -P)"), + ) + .arg(Arg::with_name(options::PATH).hidden(true).multiple(true)) + .get_matches_from(args); // set working mode - let mode = if matches.opt_present("version") { - Mode::Version - } else if matches.opt_present("help") { - Mode::Help - } else if (matches.opt_present("posix") && matches.opt_present("posix-special")) - || matches.opt_present("portability") - { + let is_posix = matches.values_of(options::POSIX).is_some(); + let is_posix_special = matches.values_of(options::POSIX_SPECIAL).is_some(); + let is_portability = matches.values_of(options::PORTABILITY).is_some(); + + let mode = if (is_posix && is_posix_special) || is_portability { Mode::Both - } else if matches.opt_present("posix") { + } else if is_posix { Mode::Basic - } else if matches.opt_present("posix-special") { + } else if is_posix_special { Mode::Extra } else { Mode::Default }; // take necessary actions - match mode { - Mode::Help => { - help(opts); - 0 - } - Mode::Version => { - version(); - 0 - } - _ => { - let mut res = if matches.free.is_empty() { - show_error!("missing operand\nTry {} --help for more information", NAME); - false - } else { - true - }; - // free strings are path operands - // FIXME: TCS, seems inefficient and overly verbose (?) - for p in matches.free { - let mut path = Vec::new(); - for path_segment in p.split('/') { - path.push(path_segment.to_string()); - } - res &= check_path(&mode, &path); - } - // determine error code - if res { - 0 - } else { - 1 + let paths = matches.values_of(options::PATH); + let mut res = if paths.is_none() { + show_error!("missing operand\nTry {} --help for more information", NAME); + false + } else { + true + }; + + if res { + // free strings are path operands + // FIXME: TCS, seems inefficient and overly verbose (?) + for p in paths.unwrap() { + let mut path = Vec::new(); + for path_segment in p.split('/') { + path.push(path_segment.to_string()); } + res &= check_path(&mode, &path); } } -} -// print help -fn help(opts: Options) { - let msg = format!( - "Usage: {} [OPTION]... NAME...\n\n\ - Diagnose invalid or unportable file names.", - NAME - ); - - print!("{}", opts.usage(&msg)); -} - -// print version information -fn version() { - println!("{} {}", NAME, VERSION); + // determine error code + if res { + 0 + } else { + 1 + } } // check a path, given as a slice of it's components and an operating mode diff --git a/src/uu/pinky/Cargo.toml b/src/uu/pinky/Cargo.toml index 1f2bfc13b..3f4a75241 100644 --- a/src/uu/pinky/Cargo.toml +++ b/src/uu/pinky/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_pinky" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "pinky ~ (uutils) display user information" @@ -15,8 +15,8 @@ edition = "2018" path = "src/pinky.rs" [dependencies] -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary", features=["utmpx", "entries"] } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["utmpx", "entries"] } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "pinky" diff --git a/src/uu/printenv/Cargo.toml b/src/uu/printenv/Cargo.toml index d3c4376a6..be95b8157 100644 --- a/src/uu/printenv/Cargo.toml +++ b/src/uu/printenv/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_printenv" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "printenv ~ (uutils) display value of environment VAR" @@ -15,9 +15,9 @@ edition = "2018" path = "src/printenv.rs" [dependencies] -getopts = "0.2.18" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +clap = "2.33" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "printenv" diff --git a/src/uu/printenv/src/printenv.rs b/src/uu/printenv/src/printenv.rs index 16dd6f0b0..34571ddad 100644 --- a/src/uu/printenv/src/printenv.rs +++ b/src/uu/printenv/src/printenv.rs @@ -7,69 +7,65 @@ /* last synced with: printenv (GNU coreutils) 8.13 */ -extern crate getopts; - #[macro_use] extern crate uucore; +use clap::{App, Arg}; use std::env; -static NAME: &str = "printenv"; +static ABOUT: &str = "Display the values of the specified environment VARIABLE(s), or (with no VARIABLE) display name and value pairs for them all."; static VERSION: &str = env!("CARGO_PKG_VERSION"); -pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); +static OPT_NULL: &str = "null"; - let mut opts = getopts::Options::new(); - opts.optflag( - "0", - "null", - "end each output line with 0 byte rather than newline", - ); - opts.optflag("h", "help", "display this help and exit"); - opts.optflag("V", "version", "output version information and exit"); - let matches = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(f) => crash!(1, "Invalid options\n{}", f), - }; - if matches.opt_present("help") { - let msg = format!( - "{0} {1} +static ARG_VARIABLES: &str = "variables"; -Usage: - {0} [VARIABLE]... [OPTION]... - -Prints the given environment VARIABLE(s), otherwise prints them all.", - NAME, VERSION - ); - print!("{}", opts.usage(&msg)); - return 0; - } - if matches.opt_present("version") { - println!("{} {}", NAME, VERSION); - return 0; - } - let mut separator = "\n"; - if matches.opt_present("null") { - separator = "\x00"; - }; - - exec(matches.free, separator); - - 0 +fn get_usage() -> String { + format!("{0} [VARIABLE]... [OPTION]...", executable!()) } -pub fn exec(args: Vec, separator: &str) { - if args.is_empty() { +pub fn uumain(args: impl uucore::Args) -> i32 { + let usage = get_usage(); + + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(&usage[..]) + .arg( + Arg::with_name(OPT_NULL) + .short("0") + .long(OPT_NULL) + .help("end each output line with 0 byte rather than newline"), + ) + .arg( + Arg::with_name(ARG_VARIABLES) + .multiple(true) + .takes_value(true) + .min_values(1), + ) + .get_matches_from(args); + + let variables: Vec = matches + .values_of(ARG_VARIABLES) + .map(|v| v.map(ToString::to_string).collect()) + .unwrap_or_default(); + + let mut separator = "\n"; + if matches.is_present(OPT_NULL) { + separator = "\x00"; + } + + if variables.is_empty() { for (env_var, value) in env::vars() { print!("{}={}{}", env_var, value, separator); } - return; + return 0; } - for env_var in &args { + for env_var in variables { if let Ok(var) = env::var(env_var) { print!("{}{}", var, separator); } } + 0 } diff --git a/src/uu/printf/Cargo.toml b/src/uu/printf/Cargo.toml index 7ac01a597..bc77d31be 100644 --- a/src/uu/printf/Cargo.toml +++ b/src/uu/printf/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_printf" -version = "0.0.1" +version = "0.0.6" authors = [ "Nathan Ross", "uutils developers", @@ -19,8 +19,8 @@ path = "src/printf.rs" [dependencies] itertools = "0.8.0" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "printf" diff --git a/src/uu/printf/src/printf.rs b/src/uu/printf/src/printf.rs index 246a1dcea..c2952e5a9 100644 --- a/src/uu/printf/src/printf.rs +++ b/src/uu/printf/src/printf.rs @@ -3,15 +3,12 @@ // spell-checker:ignore (change!) each's // spell-checker:ignore (ToDO) LONGHELP FORMATSTRING templating parameterizing formatstr -extern crate itertools; -extern crate uucore; - mod cli; mod memo; mod tokenize; static NAME: &str = "printf"; -static VERSION: &str = "0.0.1"; +static VERSION: &str = env!("CARGO_PKG_VERSION"); static SHORT_USAGE: &str = "printf: usage: printf [-v var] format [arguments]"; static LONGHELP_LEAD: &str = "printf diff --git a/src/uu/printf/src/tokenize/num_format/formatters/cninetyninehexfloatf.rs b/src/uu/printf/src/tokenize/num_format/formatters/cninetyninehexfloatf.rs index 33c3a7af5..10e58cc32 100644 --- a/src/uu/printf/src/tokenize/num_format/formatters/cninetyninehexfloatf.rs +++ b/src/uu/printf/src/tokenize/num_format/formatters/cninetyninehexfloatf.rs @@ -52,15 +52,14 @@ fn get_primitive_hex( last_dec_place: usize, capitalized: bool, ) -> FormatPrimitive { - let mut f: FormatPrimitive = Default::default(); - f.prefix = Some(String::from(if inprefix.sign == -1 { "-0x" } else { "0x" })); + let prefix = Some(String::from(if inprefix.sign == -1 { "-0x" } else { "0x" })); // assign the digits before and after the decimal points // to separate slices. If no digits after decimal point, // assign 0 let (mut first_segment_raw, second_segment_raw) = match analysis.decimal_pos { Some(pos) => (&str_in[..pos], &str_in[pos + 1..]), - None => (&str_in[..], "0"), + None => (str_in, "0"), }; if first_segment_raw.is_empty() { first_segment_raw = "0"; @@ -97,7 +96,7 @@ fn get_primitive_hex( // conversion. The best way to do it is to just convert the floatnum // directly to base 2 and then at the end translate back to hex. let mantissa = 0; - f.suffix = Some({ + let suffix = Some({ let ind = if capitalized { "P" } else { "p" }; if mantissa >= 0 { format!("{}+{}", ind, mantissa) @@ -105,7 +104,11 @@ fn get_primitive_hex( format!("{}{}", ind, mantissa) } }); - f + FormatPrimitive { + prefix, + suffix, + ..Default::default() + } } fn to_hex(src: &str, before_decimal: bool) -> String { diff --git a/src/uu/printf/src/tokenize/num_format/formatters/float_common.rs b/src/uu/printf/src/tokenize/num_format/formatters/float_common.rs index 65fe5b6ea..a107078ae 100644 --- a/src/uu/printf/src/tokenize/num_format/formatters/float_common.rs +++ b/src/uu/printf/src/tokenize/num_format/formatters/float_common.rs @@ -218,7 +218,7 @@ pub fn get_primitive_dec( // assign 0 let (mut first_segment_raw, second_segment_raw) = match analysis.decimal_pos { Some(pos) => (&str_in[..pos], &str_in[pos + 1..]), - None => (&str_in[..], "0"), + None => (str_in, "0"), }; if first_segment_raw.is_empty() { first_segment_raw = "0"; diff --git a/src/uu/printf/src/tokenize/num_format/formatters/intf.rs b/src/uu/printf/src/tokenize/num_format/formatters/intf.rs index 489420c41..9231bd027 100644 --- a/src/uu/printf/src/tokenize/num_format/formatters/intf.rs +++ b/src/uu/printf/src/tokenize/num_format/formatters/intf.rs @@ -198,9 +198,10 @@ impl Formatter for Intf { // We always will have a format primitive to return Some(if convert_hints.len_digits == 0 || convert_hints.is_zero { // if non-digit or end is reached before a non-zero digit - let mut fmt_prim: FormatPrimitive = Default::default(); - fmt_prim.pre_decimal = Some(String::from("0")); - fmt_prim + FormatPrimitive { + pre_decimal: Some(String::from("0")), + ..Default::default() + } } else if !convert_hints.past_max { // if the number is or may be below the bounds limit let radix_out = match *field.field_char { 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 44afd2e66..9a519e95e 100644 --- a/src/uu/printf/src/tokenize/num_format/num_format.rs +++ b/src/uu/printf/src/tokenize/num_format/num_format.rs @@ -64,7 +64,7 @@ fn get_provided(str_in_opt: Option<&String>) -> Option { None => { let so_far = (qchar as u8 as char).to_string(); warn_expected_numeric(&so_far); - 0 as u8 + 0_u8 } }) } @@ -72,7 +72,7 @@ fn get_provided(str_in_opt: Option<&String>) -> Option { _ => None, // no first byte } } else { - Some(0 as u8) + Some(0_u8) } } None => Some(0), diff --git a/src/uu/printf/src/tokenize/sub.rs b/src/uu/printf/src/tokenize/sub.rs index bf3fea211..30d684393 100644 --- a/src/uu/printf/src/tokenize/sub.rs +++ b/src/uu/printf/src/tokenize/sub.rs @@ -163,8 +163,8 @@ impl SubParser { 'b', 'c', 'd', 'e', 'E', 'f', 'F', 'g', 'G', 'i', 'o', 's', 'u', 'x', 'X', ]; let mut specifiers = vec!['h', 'j', 'l', 'L', 't', 'z']; - legal_fields.sort(); - specifiers.sort(); + legal_fields.sort_unstable(); + specifiers.sort_unstable(); // divide substitution from %([0-9]+)?(.[0-9+])?([a-zA-Z]) // into min_width, second_field, field_char diff --git a/src/uu/printf/src/tokenize/unescaped_text.rs b/src/uu/printf/src/tokenize/unescaped_text.rs index b6fdd1aa1..3b9f0123e 100644 --- a/src/uu/printf/src/tokenize/unescaped_text.rs +++ b/src/uu/printf/src/tokenize/unescaped_text.rs @@ -85,10 +85,7 @@ impl UnescapedText { // to the passed byte_vec // in subs_mode change octal behavior fn handle_escaped(byte_vec: &mut Vec, it: &mut PutBackN, subs_mode: bool) { - let ch = match it.next() { - Some(x) => x, - None => '\\', - }; + let ch = it.next().unwrap_or('\\'); match ch { '0'..='9' | 'x' => { let min_len = 1; diff --git a/src/uu/ptx/Cargo.toml b/src/uu/ptx/Cargo.toml index e906981c6..eb4413cbd 100644 --- a/src/uu/ptx/Cargo.toml +++ b/src/uu/ptx/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_ptx" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "ptx ~ (uutils) display a permuted index of input" @@ -15,14 +15,14 @@ edition = "2018" path = "src/ptx.rs" [dependencies] +clap = "2.33" aho-corasick = "0.7.3" -getopts = "0.2.18" libc = "0.2.42" memchr = "2.2.0" regex = "1.0.1" regex-syntax = "0.6.7" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "ptx" diff --git a/src/uu/ptx/src/ptx.rs b/src/uu/ptx/src/ptx.rs index 6981eeacc..6b3dae27f 100644 --- a/src/uu/ptx/src/ptx.rs +++ b/src/uu/ptx/src/ptx.rs @@ -7,16 +7,10 @@ // spell-checker:ignore (ToDOs) corasick memchr Roff trunc oset iset -extern crate aho_corasick; -extern crate getopts; -extern crate memchr; -extern crate regex; -extern crate regex_syntax; - #[macro_use] extern crate uucore; -use getopts::{Matches, Options}; +use clap::{App, Arg}; use regex::Regex; use std::cmp; use std::collections::{BTreeSet, HashMap, HashSet}; @@ -26,6 +20,12 @@ use std::io::{stdin, stdout, BufRead, BufReader, BufWriter, Read, Write}; static NAME: &str = "ptx"; static VERSION: &str = env!("CARGO_PKG_VERSION"); +static BRIEF: &str = "Usage: ptx [OPTION]... [INPUT]... (without -G) or: \ + ptx -G [OPTION]... [INPUT [OUTPUT]] \n Output a permuted index, \ + including context, of the words in the input files. \n\n Mandatory \ + arguments to long options are mandatory for short options too.\n + With no FILE, or when FILE is -, read standard input. \ + Default is '-F /'."; #[derive(Debug)] enum OutFormat { @@ -67,8 +67,11 @@ impl Default for Config { } } -fn read_word_filter_file(matches: &Matches, option: &str) -> HashSet { - let filename = matches.opt_str(option).expect("parsing options failed!"); +fn read_word_filter_file(matches: &clap::ArgMatches, option: &str) -> HashSet { + let filename = matches + .value_of(option) + .expect("parsing options failed!") + .to_string(); let reader = BufReader::new(crash_if_err!(1, File::open(filename))); let mut words: HashSet = HashSet::new(); for word in reader.lines() { @@ -87,26 +90,41 @@ struct WordFilter { } impl WordFilter { - fn new(matches: &Matches, config: &Config) -> WordFilter { - let (o, oset): (bool, HashSet) = if matches.opt_present("o") { - (true, read_word_filter_file(matches, "o")) + fn new(matches: &clap::ArgMatches, config: &Config) -> WordFilter { + let (o, oset): (bool, HashSet) = if matches.is_present(options::ONLY_FILE) { + (true, read_word_filter_file(matches, options::ONLY_FILE)) } else { (false, HashSet::new()) }; - let (i, iset): (bool, HashSet) = if matches.opt_present("i") { - (true, read_word_filter_file(matches, "i")) + let (i, iset): (bool, HashSet) = if matches.is_present(options::IGNORE_FILE) { + (true, read_word_filter_file(matches, options::IGNORE_FILE)) } else { (false, HashSet::new()) }; - if matches.opt_present("b") { + if matches.is_present(options::BREAK_FILE) { crash!(1, "-b not implemented yet"); } - let reg = if matches.opt_present("W") { - matches.opt_str("W").expect("parsing options failed!") - } else if config.gnu_ext { - "\\w+".to_owned() + // Ignore empty string regex from cmd-line-args + let arg_reg: Option = if matches.is_present(options::WORD_REGEXP) { + match matches.value_of(options::WORD_REGEXP) { + Some(v) => match v.is_empty() { + true => None, + false => Some(v.to_string()), + }, + None => None, + } } else { - "[^ \t\n]+".to_owned() + None + }; + let reg = match arg_reg { + Some(arg_reg) => arg_reg, + None => { + if config.gnu_ext { + "\\w+".to_owned() + } else { + "[^ \t\n]+".to_owned() + } + } }; WordFilter { only_specified: o, @@ -128,55 +146,50 @@ struct WordRef { filename: String, } -fn print_version() { - println!("{} {}", NAME, VERSION); -} - -fn print_usage(opts: &Options) { - let brief = "Usage: ptx [OPTION]... [INPUT]... (without -G) or: \ - ptx -G [OPTION]... [INPUT [OUTPUT]] \n Output a permuted index, \ - including context, of the words in the input files. \n\n Mandatory \ - arguments to long options are mandatory for short options too."; - let explanation = "With no FILE, or when FILE is -, read standard input. \ - Default is '-F /'."; - println!("{}\n{}", opts.usage(&brief), explanation); -} - -fn get_config(matches: &Matches) -> Config { +fn get_config(matches: &clap::ArgMatches) -> Config { let mut config: Config = Default::default(); let err_msg = "parsing options failed"; - if matches.opt_present("G") { + if matches.is_present(options::TRADITIONAL) { config.gnu_ext = false; config.format = OutFormat::Roff; config.context_regex = "[^ \t\n]+".to_owned(); } else { crash!(1, "GNU extensions not implemented yet"); } - if matches.opt_present("S") { + if matches.is_present(options::SENTENCE_REGEXP) { crash!(1, "-S not implemented yet"); } - config.auto_ref = matches.opt_present("A"); - config.input_ref = matches.opt_present("r"); - config.right_ref &= matches.opt_present("R"); - config.ignore_case = matches.opt_present("f"); - if matches.opt_present("M") { - config.macro_name = matches.opt_str("M").expect(err_msg); + config.auto_ref = matches.is_present(options::AUTO_REFERENCE); + config.input_ref = matches.is_present(options::REFERENCES); + config.right_ref &= matches.is_present(options::RIGHT_SIDE_REFS); + config.ignore_case = matches.is_present(options::IGNORE_CASE); + if matches.is_present(options::MACRO_NAME) { + config.macro_name = matches + .value_of(options::MACRO_NAME) + .expect(err_msg) + .to_string(); } - if matches.opt_present("F") { - config.trunc_str = matches.opt_str("F").expect(err_msg); + if matches.is_present(options::IGNORE_CASE) { + config.trunc_str = matches + .value_of(options::IGNORE_CASE) + .expect(err_msg) + .to_string(); } - if matches.opt_present("w") { - let width_str = matches.opt_str("w").expect(err_msg); + if matches.is_present(options::WIDTH) { + let width_str = matches.value_of(options::WIDTH).expect(err_msg).to_string(); config.line_width = crash_if_err!(1, usize::from_str_radix(&width_str, 10)); } - if matches.opt_present("g") { - let gap_str = matches.opt_str("g").expect(err_msg); + if matches.is_present(options::GAP_SIZE) { + let gap_str = matches + .value_of(options::GAP_SIZE) + .expect(err_msg) + .to_string(); config.gap_size = crash_if_err!(1, usize::from_str_radix(&gap_str, 10)); } - if matches.opt_present("O") { + if matches.is_present(options::FORMAT_ROFF) { config.format = OutFormat::Roff; } - if matches.opt_present("T") { + if matches.is_present(options::FORMAT_TEX) { config.format = OutFormat::Tex; } config @@ -557,102 +570,167 @@ fn write_traditional_output( } } +mod options { + pub static FILE: &str = "file"; + pub static AUTO_REFERENCE: &str = "auto-reference"; + pub static TRADITIONAL: &str = "traditional"; + pub static FLAG_TRUNCATION: &str = "flag-truncation"; + pub static MACRO_NAME: &str = "macro-name"; + pub static FORMAT_ROFF: &str = "format=roff"; + pub static RIGHT_SIDE_REFS: &str = "right-side-refs"; + pub static SENTENCE_REGEXP: &str = "sentence-regexp"; + pub static FORMAT_TEX: &str = "format=tex"; + pub static WORD_REGEXP: &str = "word-regexp"; + pub static BREAK_FILE: &str = "break-file"; + pub static IGNORE_CASE: &str = "ignore-case"; + pub static GAP_SIZE: &str = "gap-size"; + pub static IGNORE_FILE: &str = "ignore-file"; + pub static ONLY_FILE: &str = "only-file"; + pub static REFERENCES: &str = "references"; + pub static WIDTH: &str = "width"; +} + pub fn uumain(args: impl uucore::Args) -> i32 { let args = args.collect_str(); - let mut opts = Options::new(); - opts.optflag( - "A", - "auto-reference", - "output automatically generated references", - ); - opts.optflag("G", "traditional", "behave more like System V 'ptx'"); - opts.optopt( - "F", - "flag-truncation", - "use STRING for flagging line truncations", - "STRING", - ); - opts.optopt( - "M", - "macro-name", - "macro name to use instead of 'xx'", - "STRING", - ); - opts.optflag("O", "format=roff", "generate output as roff directives"); - opts.optflag( - "R", - "right-side-refs", - "put references at right, not counted in -w", - ); - opts.optopt( - "S", - "sentence-regexp", - "for end of lines or end of sentences", - "REGEXP", - ); - opts.optflag("T", "format=tex", "generate output as TeX directives"); - opts.optopt( - "W", - "word-regexp", - "use REGEXP to match each keyword", - "REGEXP", - ); - opts.optopt( - "b", - "break-file", - "word break characters in this FILE", - "FILE", - ); - opts.optflag( - "f", - "ignore-case", - "fold lower case to upper case for sorting", - ); - opts.optopt( - "g", - "gap-size", - "gap size in columns between output fields", - "NUMBER", - ); - opts.optopt( - "i", - "ignore-file", - "read ignore word list from FILE", - "FILE", - ); - opts.optopt( - "o", - "only-file", - "read only word list from this FILE", - "FILE", - ); - opts.optflag("r", "references", "first field of each line is a reference"); - opts.optopt( - "w", - "width", - "output width in columns, reference excluded", - "NUMBER", - ); - opts.optflag("", "help", "display this help and exit"); - opts.optflag("", "version", "output version information and exit"); + // let mut opts = Options::new(); + let matches = App::new(executable!()) + .name(NAME) + .version(VERSION) + .usage(BRIEF) + .arg(Arg::with_name(options::FILE).hidden(true).multiple(true)) + .arg( + Arg::with_name(options::AUTO_REFERENCE) + .short("A") + .long(options::AUTO_REFERENCE) + .help("output automatically generated references") + .takes_value(false), + ) + .arg( + Arg::with_name(options::TRADITIONAL) + .short("G") + .long(options::TRADITIONAL) + .help("behave more like System V 'ptx'"), + ) + .arg( + Arg::with_name(options::FLAG_TRUNCATION) + .short("F") + .long(options::FLAG_TRUNCATION) + .help("use STRING for flagging line truncations") + .value_name("STRING") + .takes_value(true), + ) + .arg( + Arg::with_name(options::MACRO_NAME) + .short("M") + .long(options::MACRO_NAME) + .help("macro name to use instead of 'xx'") + .value_name("STRING") + .takes_value(true), + ) + .arg( + Arg::with_name(options::FORMAT_ROFF) + .short("O") + .long(options::FORMAT_ROFF) + .help("generate output as roff directives"), + ) + .arg( + Arg::with_name(options::RIGHT_SIDE_REFS) + .short("R") + .long(options::RIGHT_SIDE_REFS) + .help("put references at right, not counted in -w") + .takes_value(false), + ) + .arg( + Arg::with_name(options::SENTENCE_REGEXP) + .short("S") + .long(options::SENTENCE_REGEXP) + .help("for end of lines or end of sentences") + .value_name("REGEXP") + .takes_value(true), + ) + .arg( + Arg::with_name(options::FORMAT_TEX) + .short("T") + .long(options::FORMAT_TEX) + .help("generate output as TeX directives"), + ) + .arg( + Arg::with_name(options::WORD_REGEXP) + .short("W") + .long(options::WORD_REGEXP) + .help("use REGEXP to match each keyword") + .value_name("REGEXP") + .takes_value(true), + ) + .arg( + Arg::with_name(options::BREAK_FILE) + .short("b") + .long(options::BREAK_FILE) + .help("word break characters in this FILE") + .value_name("FILE") + .takes_value(true), + ) + .arg( + Arg::with_name(options::IGNORE_CASE) + .short("f") + .long(options::IGNORE_CASE) + .help("fold lower case to upper case for sorting") + .takes_value(false), + ) + .arg( + Arg::with_name(options::GAP_SIZE) + .short("g") + .long(options::GAP_SIZE) + .help("gap size in columns between output fields") + .value_name("NUMBER") + .takes_value(true), + ) + .arg( + Arg::with_name(options::IGNORE_FILE) + .short("i") + .long(options::IGNORE_FILE) + .help("read ignore word list from FILE") + .value_name("FILE") + .takes_value(true), + ) + .arg( + Arg::with_name(options::ONLY_FILE) + .short("o") + .long(options::ONLY_FILE) + .help("read only word list from this FILE") + .value_name("FILE") + .takes_value(true), + ) + .arg( + Arg::with_name(options::REFERENCES) + .short("r") + .long(options::REFERENCES) + .help("first field of each line is a reference") + .value_name("FILE") + .takes_value(false), + ) + .arg( + Arg::with_name(options::WIDTH) + .short("w") + .long(options::WIDTH) + .help("output width in columns, reference excluded") + .value_name("NUMBER") + .takes_value(true), + ) + .get_matches_from(args); - let matches = return_if_err!(1, opts.parse(&args[1..])); + let input_files: Vec = match &matches.values_of(options::FILE) { + Some(v) => v.clone().map(|v| v.to_owned()).collect(), + None => vec!["-".to_string()], + }; - if matches.opt_present("help") { - print_usage(&opts); - return 0; - } - if matches.opt_present("version") { - print_version(); - return 0; - } let config = get_config(&matches); let word_filter = WordFilter::new(&matches, &config); - let file_map = read_input(&matches.free, &config); + let file_map = read_input(&input_files, &config); let word_set = create_word_set(&config, &word_filter, &file_map); - let output_file = if !config.gnu_ext && matches.free.len() == 2 { - matches.free[1].clone() + let output_file = if !config.gnu_ext && matches.args.len() == 2 { + matches.value_of(options::FILE).unwrap_or("-").to_string() } else { "-".to_owned() }; diff --git a/src/uu/pwd/Cargo.toml b/src/uu/pwd/Cargo.toml index ef25017d1..f4350d54c 100644 --- a/src/uu/pwd/Cargo.toml +++ b/src/uu/pwd/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_pwd" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "pwd ~ (uutils) display current working directory" @@ -15,9 +15,9 @@ edition = "2018" path = "src/pwd.rs" [dependencies] -getopts = "0.2.18" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +clap = "2.33" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "pwd" diff --git a/src/uu/pwd/src/pwd.rs b/src/uu/pwd/src/pwd.rs index 1861c8084..1786d33ee 100644 --- a/src/uu/pwd/src/pwd.rs +++ b/src/uu/pwd/src/pwd.rs @@ -5,17 +5,18 @@ // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -extern crate getopts; - #[macro_use] extern crate uucore; +use clap::{App, Arg}; use std::env; use std::io; use std::path::{Path, PathBuf}; -static NAME: &str = "pwd"; +static ABOUT: &str = "Display the full filename of the current working directory."; static VERSION: &str = env!("CARGO_PKG_VERSION"); +static OPT_LOGICAL: &str = "logical"; +static OPT_PHYSICAL: &str = "physical"; pub fn absolute_path(path: &Path) -> io::Result { let path_buf = path.canonicalize()?; @@ -32,53 +33,44 @@ pub fn absolute_path(path: &Path) -> io::Result { Ok(path_buf) } +fn get_usage() -> String { + format!("{0} [OPTION]... FILE...", executable!()) +} + pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); + let usage = get_usage(); - let mut opts = getopts::Options::new(); + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(&usage[..]) + .arg( + Arg::with_name(OPT_LOGICAL) + .short("L") + .long(OPT_LOGICAL) + .help("use PWD from environment, even if it contains symlinks"), + ) + .arg( + Arg::with_name(OPT_PHYSICAL) + .short("P") + .long(OPT_PHYSICAL) + .help("avoid all symlinks"), + ) + .get_matches_from(args); - opts.optflag("", "help", "display this help and exit"); - opts.optflag("", "version", "output version information and exit"); - opts.optflag( - "L", - "logical", - "use PWD from environment, even if it contains symlinks", - ); - opts.optflag("P", "physical", "avoid all symlinks"); - - let matches = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(f) => crash!(1, "Invalid options\n{}", f), - }; - - if matches.opt_present("help") { - let msg = format!( - "{0} {1} - -Usage: - {0} [OPTION]... - -Print the full filename of the current working directory.", - NAME, VERSION - ); - print!("{}", opts.usage(&msg)); - } else if matches.opt_present("version") { - println!("{} {}", NAME, VERSION); - } else { - match env::current_dir() { - Ok(logical_path) => { - if matches.opt_present("logical") { - println!("{}", logical_path.display()); - } else { - match absolute_path(&logical_path) { - Ok(physical_path) => println!("{}", physical_path.display()), - Err(e) => crash!(1, "failed to get absolute path {}", e), - }; - } + match env::current_dir() { + Ok(logical_path) => { + if matches.is_present(OPT_LOGICAL) { + println!("{}", logical_path.display()); + } else { + match absolute_path(&logical_path) { + Ok(physical_path) => println!("{}", physical_path.display()), + Err(e) => crash!(1, "failed to get absolute path {}", e), + }; } - Err(e) => crash!(1, "failed to get current directory {}", e), - }; - } + } + Err(e) => crash!(1, "failed to get current directory {}", e), + }; 0 } diff --git a/src/uu/readlink/Cargo.toml b/src/uu/readlink/Cargo.toml index 058b778a0..6e4be4dd8 100644 --- a/src/uu/readlink/Cargo.toml +++ b/src/uu/readlink/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_readlink" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "readlink ~ (uutils) display resolved path of PATHNAME" @@ -15,10 +15,10 @@ edition = "2018" path = "src/readlink.rs" [dependencies] -getopts = "0.2.18" +clap = "2.33" libc = "0.2.42" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary", features=["fs"] } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["fs"] } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "readlink" diff --git a/src/uu/readlink/src/readlink.rs b/src/uu/readlink/src/readlink.rs index c4721c58f..727c2cce5 100644 --- a/src/uu/readlink/src/readlink.rs +++ b/src/uu/readlink/src/readlink.rs @@ -7,11 +7,10 @@ // spell-checker:ignore (ToDO) errno -extern crate getopts; - #[macro_use] extern crate uucore; +use clap::{App, Arg}; use std::fs; use std::io::{stdout, Write}; use std::path::PathBuf; @@ -19,68 +18,107 @@ use uucore::fs::{canonicalize, CanonicalizeMode}; const NAME: &str = "readlink"; const VERSION: &str = env!("CARGO_PKG_VERSION"); +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"; +const OPT_CANONICALIZE_EXISTING: &str = "canonicalize-existing"; +const OPT_NO_NEWLINE: &str = "no-newline"; +const OPT_QUIET: &str = "quiet"; +const OPT_SILENT: &str = "silent"; +const OPT_VERBOSE: &str = "verbose"; +const OPT_ZERO: &str = "zero"; + +const ARG_FILES: &str = "files"; + +fn get_usage() -> String { + format!("{0} [OPTION]... [FILE]...", executable!()) +} pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); + let usage = get_usage(); + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(&usage[..]) + .arg( + Arg::with_name(OPT_CANONICALIZE) + .short("f") + .long(OPT_CANONICALIZE) + .help( + "canonicalize by following every symlink in every component of the \ + given name recursively; all but the last component must exist", + ), + ) + .arg( + Arg::with_name(OPT_CANONICALIZE_EXISTING) + .short("e") + .long("canonicalize-existing") + .help( + "canonicalize by following every symlink in every component of the \ + given name recursively, all components must exist", + ), + ) + .arg( + Arg::with_name(OPT_CANONICALIZE_MISSING) + .short("m") + .long(OPT_CANONICALIZE_MISSING) + .help( + "canonicalize by following every symlink in every component of the \ + given name recursively, without requirements on components existence", + ), + ) + .arg( + Arg::with_name(OPT_NO_NEWLINE) + .short("n") + .long(OPT_NO_NEWLINE) + .help("do not output the trailing delimiter"), + ) + .arg( + Arg::with_name(OPT_QUIET) + .short("q") + .long(OPT_QUIET) + .help("suppress most error messages"), + ) + .arg( + Arg::with_name(OPT_SILENT) + .short("s") + .long(OPT_SILENT) + .help("suppress most error messages"), + ) + .arg( + Arg::with_name(OPT_VERBOSE) + .short("v") + .long(OPT_VERBOSE) + .help("report error message"), + ) + .arg( + Arg::with_name(OPT_ZERO) + .short("z") + .long(OPT_ZERO) + .help("separate output with NUL rather than newline"), + ) + .arg(Arg::with_name(ARG_FILES).multiple(true).takes_value(true)) + .get_matches_from(args); - let mut opts = getopts::Options::new(); + let mut no_newline = matches.is_present(OPT_NO_NEWLINE); + let use_zero = matches.is_present(OPT_ZERO); + let silent = matches.is_present(OPT_SILENT) || matches.is_present(OPT_QUIET); + let verbose = matches.is_present(OPT_VERBOSE); - opts.optflag( - "f", - "canonicalize", - "canonicalize by following every symlink in every component of the \ - given name recursively; all but the last component must exist", - ); - opts.optflag( - "e", - "canonicalize-existing", - "canonicalize by following every symlink in every component of the \ - given name recursively, all components must exist", - ); - opts.optflag( - "m", - "canonicalize-missing", - "canonicalize by following every symlink in every component of the \ - given name recursively, without requirements on components existence", - ); - opts.optflag("n", "no-newline", "do not output the trailing delimiter"); - opts.optflag("q", "quiet", "suppress most error messages"); - opts.optflag("s", "silent", "suppress most error messages"); - opts.optflag("v", "verbose", "report error message"); - opts.optflag("z", "zero", "separate output with NUL rather than newline"); - opts.optflag("", "help", "display this help and exit"); - opts.optflag("", "version", "output version information and exit"); - - let matches = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(f) => crash!(1, "Invalid options\n{}", f), - }; - if matches.opt_present("help") { - show_usage(&opts); - return 0; - } - - if matches.opt_present("version") { - println!("{} {}", NAME, VERSION); - return 0; - } - - let mut no_newline = matches.opt_present("no-newline"); - let use_zero = matches.opt_present("zero"); - let silent = matches.opt_present("silent") || matches.opt_present("quiet"); - let verbose = matches.opt_present("verbose"); - - let can_mode = if matches.opt_present("canonicalize") { + let can_mode = if matches.is_present(OPT_CANONICALIZE) { CanonicalizeMode::Normal - } else if matches.opt_present("canonicalize-existing") { + } else if matches.is_present(OPT_CANONICALIZE_EXISTING) { CanonicalizeMode::Existing - } else if matches.opt_present("canonicalize-missing") { + } else if matches.is_present(OPT_CANONICALIZE_MISSING) { CanonicalizeMode::Missing } else { CanonicalizeMode::None }; - let files = matches.free; + let files: Vec = matches + .values_of(ARG_FILES) + .map(|v| v.map(ToString::to_string).collect()) + .unwrap_or_default(); if files.is_empty() { crash!( 1, @@ -133,11 +171,3 @@ fn show(path: &PathBuf, no_newline: bool, use_zero: bool) { } crash_if_err!(1, stdout().flush()); } - -fn show_usage(opts: &getopts::Options) { - println!("{} {}", NAME, VERSION); - println!(); - println!("Usage: {0} [OPTION]... [FILE]...", NAME); - print!("Print value of a symbolic link or canonical file name"); - print!("{}", opts.usage("")); -} diff --git a/src/uu/realpath/Cargo.toml b/src/uu/realpath/Cargo.toml index 4cd083c54..327a875f8 100644 --- a/src/uu/realpath/Cargo.toml +++ b/src/uu/realpath/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_realpath" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "realpath ~ (uutils) display resolved absolute path of PATHNAME" @@ -15,9 +15,9 @@ edition = "2018" path = "src/realpath.rs" [dependencies] -getopts = "0.2.18" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary", features=["fs"] } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +clap = "2.33" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["fs"] } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "realpath" diff --git a/src/uu/realpath/src/realpath.rs b/src/uu/realpath/src/realpath.rs index db7ce846b..5cc8f3d9a 100644 --- a/src/uu/realpath/src/realpath.rs +++ b/src/uu/realpath/src/realpath.rs @@ -7,66 +7,74 @@ // spell-checker:ignore (ToDO) retcode -extern crate getopts; - #[macro_use] extern crate uucore; +use clap::{App, Arg}; use std::fs; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use uucore::fs::{canonicalize, CanonicalizeMode}; -static NAME: &str = "realpath"; +static ABOUT: &str = "print the resolved path"; static VERSION: &str = env!("CARGO_PKG_VERSION"); +static OPT_QUIET: &str = "quiet"; +static OPT_STRIP: &str = "strip"; +static OPT_ZERO: &str = "zero"; + +static ARG_FILES: &str = "files"; + +fn get_usage() -> String { + format!("{0} [OPTION]... FILE...", executable!()) +} + pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); + let usage = get_usage(); - let mut opts = getopts::Options::new(); + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(&usage[..]) + .arg( + Arg::with_name(OPT_QUIET) + .short("q") + .long(OPT_QUIET) + .help("Do not print warnings for invalid paths"), + ) + .arg( + Arg::with_name(OPT_STRIP) + .short("s") + .long(OPT_STRIP) + .help("Only strip '.' and '..' components, but don't resolve symbolic links"), + ) + .arg( + Arg::with_name(OPT_ZERO) + .short("z") + .long(OPT_ZERO) + .help("Separate output filenames with \\0 rather than newline"), + ) + .arg( + Arg::with_name(ARG_FILES) + .multiple(true) + .takes_value(true) + .required(true) + .min_values(1), + ) + .get_matches_from(args); - opts.optflag("h", "help", "Show help and exit"); - opts.optflag("V", "version", "Show version and exit"); - opts.optflag( - "s", - "strip", - "Only strip '.' and '..' components, but don't resolve symbolic links", - ); - opts.optflag( - "z", - "zero", - "Separate output filenames with \\0 rather than newline", - ); - opts.optflag("q", "quiet", "Do not print warnings for invalid paths"); + /* the list of files */ - let matches = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(f) => { - show_error!("{}", f); - show_usage(&opts); - return 1; - } - }; + let paths: Vec = matches + .values_of(ARG_FILES) + .unwrap() + .map(PathBuf::from) + .collect(); - if matches.opt_present("V") { - version(); - return 0; - } - if matches.opt_present("h") { - show_usage(&opts); - return 0; - } - - if matches.free.is_empty() { - show_error!("Missing operand: FILENAME, at least one is required"); - println!("Try `{} --help` for more information.", NAME); - return 1; - } - - let strip = matches.opt_present("s"); - let zero = matches.opt_present("z"); - let quiet = matches.opt_present("q"); + let strip = matches.is_present(OPT_STRIP); + let zero = matches.is_present(OPT_ZERO); + let quiet = matches.is_present(OPT_QUIET); let mut retcode = 0; - for path in &matches.free { + for path in &paths { if !resolve_path(path, strip, zero, quiet) { retcode = 1 }; @@ -74,15 +82,14 @@ pub fn uumain(args: impl uucore::Args) -> i32 { retcode } -fn resolve_path(path: &str, strip: bool, zero: bool, quiet: bool) -> bool { - let p = Path::new(path).to_path_buf(); +fn resolve_path(p: &PathBuf, strip: bool, zero: bool, quiet: bool) -> bool { let abs = canonicalize(p, CanonicalizeMode::Normal).unwrap(); if strip { if zero { - print!("{}\0", abs.display()); + print!("{}\0", p.display()); } else { - println!("{}", abs.display()) + println!("{}", p.display()) } return true; } @@ -95,7 +102,7 @@ fn resolve_path(path: &str, strip: bool, zero: bool, quiet: bool) -> bool { loop { if links_left == 0 { if !quiet { - show_error!("Too many symbolic links: {}", path) + show_error!("Too many symbolic links: {}", p.display()) }; return false; } @@ -111,7 +118,7 @@ fn resolve_path(path: &str, strip: bool, zero: bool, quiet: bool) -> bool { } _ => { if !quiet { - show_error!("Invalid path: {}", path) + show_error!("Invalid path: {}", p.display()) }; return false; } @@ -129,23 +136,3 @@ fn resolve_path(path: &str, strip: bool, zero: bool, quiet: bool) -> bool { true } - -fn version() { - println!("{} {}", NAME, VERSION) -} - -fn show_usage(opts: &getopts::Options) { - version(); - println!(); - println!("Usage:"); - println!(" {} [-s|--strip] [-z|--zero] FILENAME...", NAME); - println!(" {} -V|--version", NAME); - println!(" {} -h|--help", NAME); - println!(); - print!("{}", opts.usage( - "Convert each FILENAME to the absolute path.\n\ - All the symbolic links will be resolved, resulting path will contain no special components like '.' or '..'.\n\ - Each path component must exist or resolution will fail and non-zero exit status returned.\n\ - Each resolved FILENAME will be written to the standard output, one per line.") - ); -} diff --git a/src/uu/relpath/Cargo.toml b/src/uu/relpath/Cargo.toml index ff2f3120e..7a316c29c 100644 --- a/src/uu/relpath/Cargo.toml +++ b/src/uu/relpath/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_relpath" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "relpath ~ (uutils) display relative path of PATHNAME_TO from PATHNAME_FROM" @@ -15,9 +15,9 @@ edition = "2018" path = "src/relpath.rs" [dependencies] -getopts = "0.2.18" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary", features=["fs"] } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +clap = "2.33.3" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["fs"] } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "relpath" diff --git a/src/uu/relpath/src/relpath.rs b/src/uu/relpath/src/relpath.rs index e48d43b44..82779107a 100644 --- a/src/uu/relpath/src/relpath.rs +++ b/src/uu/relpath/src/relpath.rs @@ -7,67 +7,63 @@ // spell-checker:ignore (ToDO) subpath absto absfrom absbase -extern crate getopts; - #[macro_use] extern crate uucore; +use clap::{App, Arg}; use std::env; use std::path::{Path, PathBuf}; use uucore::fs::{canonicalize, CanonicalizeMode}; -static NAME: &str = "relpath"; static VERSION: &str = env!("CARGO_PKG_VERSION"); +static ABOUT: &str = "Convert TO destination to the relative path from the FROM dir. +If FROM path is omitted, current working dir will be used."; + +mod options { + pub const DIR: &str = "DIR"; + pub const TO: &str = "TO"; + pub const FROM: &str = "FROM"; +} + +fn get_usage() -> String { + format!("{} [-d DIR] TO [FROM]", executable!()) +} pub fn uumain(args: impl uucore::Args) -> i32 { let args = args.collect_str(); + let usage = get_usage(); - let mut opts = getopts::Options::new(); + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(&usage[..]) + .arg( + Arg::with_name(options::DIR) + .short("d") + .takes_value(true) + .help("If any of FROM and TO is not subpath of DIR, output absolute path instead of relative"), + ) + .arg( + Arg::with_name(options::TO) + .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name(options::FROM) + .takes_value(true), + ) + .get_matches_from(args); - opts.optflag("h", "help", "Show help and exit"); - opts.optflag("V", "version", "Show version and exit"); - opts.optopt( - "d", - "", - "If any of FROM and TO is not subpath of DIR, output absolute path instead of relative", - "DIR", - ); - - let matches = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(f) => { - show_error!("{}", f); - show_usage(&opts); - return 1; - } - }; - - if matches.opt_present("V") { - version(); - return 0; - } - if matches.opt_present("h") { - show_usage(&opts); - return 0; - } - - if matches.free.is_empty() { - show_error!("Missing operand: TO"); - println!("Try `{} --help` for more information.", NAME); - return 1; - } - - let to = Path::new(&matches.free[0]); - let from = if matches.free.len() > 1 { - Path::new(&matches.free[1]).to_path_buf() - } else { - env::current_dir().unwrap() + let to = Path::new(matches.value_of(options::TO).unwrap()).to_path_buf(); // required + let from = match matches.value_of(options::FROM) { + 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(); - if matches.opt_present("d") { - let base = Path::new(&matches.opt_str("d").unwrap()).to_path_buf(); + 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(); if !absto.as_path().starts_with(absbase.as_path()) || !absfrom.as_path().starts_with(absbase.as_path()) @@ -101,24 +97,3 @@ pub fn uumain(args: impl uucore::Args) -> i32 { println!("{}", result.display()); 0 } - -fn version() { - println!("{} {}", NAME, VERSION) -} - -fn show_usage(opts: &getopts::Options) { - version(); - println!(); - println!("Usage:"); - println!(" {} [-d DIR] TO [FROM]", NAME); - println!(" {} -V|--version", NAME); - println!(" {} -h|--help", NAME); - println!(); - print!( - "{}", - opts.usage( - "Convert TO destination to the relative path from the FROM dir.\n\ - If FROM path is omitted, current working dir will be used." - ) - ); -} diff --git a/src/uu/rm/Cargo.toml b/src/uu/rm/Cargo.toml index 8fbd14663..9974111aa 100644 --- a/src/uu/rm/Cargo.toml +++ b/src/uu/rm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_rm" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "rm ~ (uutils) remove PATHNAME" @@ -15,11 +15,12 @@ edition = "2018" path = "src/rm.rs" [dependencies] -getopts = "0.2.18" -walkdir = "2.2.8" +clap = "2.33" +walkdir = "2.2" remove_dir_all = "0.5.1" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } + +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["fs"] } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "rm" diff --git a/src/uu/rm/src/rm.rs b/src/uu/rm/src/rm.rs index 42c7be557..09671768b 100644 --- a/src/uu/rm/src/rm.rs +++ b/src/uu/rm/src/rm.rs @@ -7,19 +7,16 @@ // spell-checker:ignore (ToDO) bitor ulong -extern crate getopts; -extern crate remove_dir_all; -extern crate walkdir; - #[macro_use] extern crate uucore; +use clap::{App, Arg}; use remove_dir_all::remove_dir_all; use std::collections::VecDeque; use std::fs; use std::io::{stderr, stdin, BufRead, Write}; use std::ops::BitOr; -use std::path::Path; +use std::path::{Path, PathBuf}; use walkdir::{DirEntry, WalkDir}; #[derive(Eq, PartialEq, Clone, Copy)] @@ -40,83 +37,146 @@ struct Options { verbose: bool, } -static NAME: &str = "rm"; +static ABOUT: &str = "Remove (unlink) the FILE(s)"; static VERSION: &str = env!("CARGO_PKG_VERSION"); +static OPT_DIR: &str = "dir"; +static OPT_INTERACTIVE: &str = "interactive"; +static OPT_FORCE: &str = "force"; +static OPT_NO_PRESERVE_ROOT: &str = "no-preserve-root"; +static OPT_ONE_FILE_SYSTEM: &str = "one-file-system"; +static OPT_PRESERVE_ROOT: &str = "preserve-root"; +static OPT_PROMPT: &str = "prompt"; +static OPT_PROMPT_MORE: &str = "prompt-more"; +static OPT_RECURSIVE: &str = "recursive"; +static OPT_RECURSIVE_R: &str = "recursive_R"; +static OPT_VERBOSE: &str = "verbose"; + +static ARG_FILES: &str = "files"; + +fn get_usage() -> String { + format!("{0} [OPTION]... FILE...", executable!()) +} + +fn get_long_usage() -> String { + String::from( + "By default, rm does not remove directories. Use the --recursive (-r or -R) + option to remove each listed directory, too, along with all of its contents + + To remove a file whose name starts with a '-', for example '-foo', + use one of these commands: + rm -- -foo + + rm ./-foo + + Note that if you use rm to remove a file, it might be possible to recover + some of its contents, given sufficient expertise and/or time. For greater + assurance that the contents are truly unrecoverable, consider using shred.", + ) +} pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); + let usage = get_usage(); + let long_usage = get_long_usage(); - // TODO: make getopts support -R in addition to -r - let mut opts = getopts::Options::new(); + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(&usage[..]) + .after_help(&long_usage[..]) - opts.optflag( - "f", - "force", - "ignore nonexistent files and arguments, never prompt", - ); - opts.optflag("i", "", "prompt before every removal"); - opts.optflag("I", "", "prompt once before removing more than three files, or when removing recursively. Less intrusive than -i, while still giving some protection against most mistakes"); - opts.optflagopt( - "", - "interactive", - "prompt according to WHEN: never, once (-I), or always (-i). Without WHEN, prompts always", - "WHEN", - ); - opts.optflag("", "one-file-system", "when removing a hierarchy recursively, skip any directory that is on a file system different from that of the corresponding command line argument (NOT IMPLEMENTED)"); - opts.optflag("", "no-preserve-root", "do not treat '/' specially"); - opts.optflag("", "preserve-root", "do not remove '/' (default)"); - opts.optflag( - "r", - "recursive", - "remove directories and their contents recursively", - ); - opts.optflag("d", "dir", "remove empty directories"); - opts.optflag("v", "verbose", "explain what is being done"); - opts.optflag("h", "help", "display this help and exit"); - opts.optflag("V", "version", "output version information and exit"); + .arg( + Arg::with_name(OPT_FORCE) + .short("f") + .long(OPT_FORCE) + .multiple(true) + .help("ignore nonexistent files and arguments, never prompt") + ) + .arg( + Arg::with_name(OPT_PROMPT) + .short("i") + .long("prompt before every removal") + ) + .arg( + Arg::with_name(OPT_PROMPT_MORE) + .short("I") + .help("prompt once before removing more than three files, or when removing recursively. Less intrusive than -i, while still giving some protection against most mistakes") + ) + .arg( + Arg::with_name(OPT_INTERACTIVE) + .long(OPT_INTERACTIVE) + .help("prompt according to WHEN: never, once (-I), or always (-i). Without WHEN, prompts always") + .value_name("WHEN") + .takes_value(true) + ) + .arg( + Arg::with_name(OPT_ONE_FILE_SYSTEM) + .long(OPT_ONE_FILE_SYSTEM) + .help("when removing a hierarchy recursively, skip any directory that is on a file system different from that of the corresponding command line argument (NOT IMPLEMENTED)") + ) + .arg( + Arg::with_name(OPT_NO_PRESERVE_ROOT) + .long(OPT_NO_PRESERVE_ROOT) + .help("do not treat '/' specially") + ) + .arg( + Arg::with_name(OPT_PRESERVE_ROOT) + .long(OPT_PRESERVE_ROOT) + .help("do not remove '/' (default)") + ) + .arg( + Arg::with_name(OPT_RECURSIVE).short("r") + .long(OPT_RECURSIVE) + .help("remove directories and their contents recursively") + ) + .arg( + // To mimic GNU's behavior we also want the '-R' flag. However, using clap's + // alias method 'visible_alias("R")' would result in a long '--R' flag. + Arg::with_name(OPT_RECURSIVE_R).short("R") + .help("Equivalent to -r") + ) + .arg( + Arg::with_name(OPT_DIR) + .short("d") + .long(OPT_DIR) + .help("remove empty directories") + ) + .arg( + Arg::with_name(OPT_VERBOSE) + .short("v") + .long(OPT_VERBOSE) + .help("explain what is being done") + ) + .arg( + Arg::with_name(ARG_FILES) + .multiple(true) + .takes_value(true) + .min_values(1) + ) + .get_matches_from(args); - let matches = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(f) => crash!(1, "{}", f), - }; + let files: Vec = matches + .values_of(ARG_FILES) + .map(|v| v.map(ToString::to_string).collect()) + .unwrap_or_default(); - let force = matches.opt_present("force"); + let force = matches.is_present(OPT_FORCE); - if matches.opt_present("help") { - println!("{} {}", NAME, VERSION); - println!(); - println!("Usage:"); - println!(" {0} [OPTION]... [FILE]...", NAME); - println!(); - println!("{}", opts.usage("Remove (unlink) the FILE(s).")); - println!("By default, rm does not remove directories. Use the --recursive (-r)"); - println!("option to remove each listed directory, too, along with all of its contents"); - println!(); - println!("To remove a file whose name starts with a '-', for example '-foo',"); - println!("use one of these commands:"); - println!("rm -- -foo"); - println!(); - println!("rm ./-foo"); - println!(); - println!("Note that if you use rm to remove a file, it might be possible to recover"); - println!("some of its contents, given sufficient expertise and/or time. For greater"); - println!("assurance that the contents are truly unrecoverable, consider using shred."); - } else if matches.opt_present("version") { - println!("{} {}", NAME, VERSION); - } else if matches.free.is_empty() && !force { + if files.is_empty() && !force { + // Still check by hand and not use clap + // Because "rm -f" is a thing show_error!("missing an argument"); - show_error!("for help, try '{0} --help'", NAME); + show_error!("for help, try '{0} --help'", executable!()); return 1; } else { let options = Options { force, interactive: { - if matches.opt_present("i") { + if matches.is_present(OPT_PROMPT) { InteractiveMode::Always - } else if matches.opt_present("I") { + } else if matches.is_present(OPT_PROMPT_MORE) { InteractiveMode::Once - } else if matches.opt_present("interactive") { - match &matches.opt_str("interactive").unwrap()[..] { + } else if matches.is_present(OPT_INTERACTIVE) { + match &matches.value_of(OPT_INTERACTIVE).unwrap()[..] { "none" => InteractiveMode::None, "once" => InteractiveMode::Once, "always" => InteractiveMode::Always, @@ -126,15 +186,13 @@ pub fn uumain(args: impl uucore::Args) -> i32 { InteractiveMode::None } }, - one_fs: matches.opt_present("one-file-system"), - preserve_root: !matches.opt_present("no-preserve-root"), - recursive: matches.opt_present("recursive"), - dir: matches.opt_present("dir"), - verbose: matches.opt_present("verbose"), + one_fs: matches.is_present(OPT_ONE_FILE_SYSTEM), + preserve_root: !matches.is_present(OPT_NO_PRESERVE_ROOT), + recursive: matches.is_present(OPT_RECURSIVE) || matches.is_present(OPT_RECURSIVE_R), + dir: matches.is_present(OPT_DIR), + verbose: matches.is_present(OPT_VERBOSE), }; - if options.interactive == InteractiveMode::Once - && (options.recursive || matches.free.len() > 3) - { + if options.interactive == InteractiveMode::Once && (options.recursive || files.len() > 3) { let msg = if options.recursive { "Remove all arguments recursively? " } else { @@ -145,7 +203,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } } - if remove(matches.free, options) { + if remove(files, options) { return 1; } } @@ -175,7 +233,7 @@ 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!("no such file or directory '{}'", filename); + show_error!("cannot remove '{}': No such file or directory", filename); true } else { false @@ -193,7 +251,7 @@ fn handle_dir(path: &Path, options: &Options) -> bool { let is_root = path.has_root() && path.parent().is_none(); if options.recursive && (!is_root || !options.preserve_root) { - if options.interactive != InteractiveMode::Always { + if options.interactive != InteractiveMode::Always && !options.verbose { // we need the extra crate because apparently fs::remove_dir_all() does not function // correctly on Windows if let Err(e) = remove_dir_all(path) { @@ -231,7 +289,7 @@ fn handle_dir(path: &Path, options: &Options) -> bool { had_err = true; } else { show_error!( - "could not remove directory '{}' (did you mean to pass '-r'?)", + "cannot remove '{}': Is a directory", // GNU's rm error message does not include help path.display() ); had_err = true; @@ -247,16 +305,34 @@ fn remove_dir(path: &Path, options: &Options) -> bool { true }; if response { - match fs::remove_dir(path) { - Ok(_) => { - if options.verbose { - println!("removed '{}'", path.display()); + if let Ok(mut read_dir) = fs::read_dir(path) { + if options.dir || options.recursive { + if read_dir.next().is_none() { + match fs::remove_dir(path) { + Ok(_) => { + if options.verbose { + println!("removed directory '{}'", normalize(path).display()); + } + } + Err(e) => { + show_error!("cannot remove '{}': {}", path.display(), e); + return true; + } + } + } else { + // directory can be read but is not empty + show_error!("cannot remove '{}': Directory not empty", path.display()); + return true; } - } - Err(e) => { - show_error!("removing '{}': {}", path.display(), e); + } else { + // called to remove a symlink_dir (windows) without "-r"/"-R" or "-d" + show_error!("cannot remove '{}': Is a directory", path.display()); 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()); + return true; } } @@ -273,7 +349,7 @@ fn remove_file(path: &Path, options: &Options) -> bool { match fs::remove_file(path) { Ok(_) => { if options.verbose { - println!("removed '{}'", path.display()); + println!("removed '{}'", normalize(path).display()); } } Err(e) => { @@ -294,6 +370,14 @@ fn prompt_file(path: &Path, is_dir: bool) -> bool { } } +fn normalize(path: &Path) -> PathBuf { + // 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 + // TODO: replace this once that lands + uucore::fs::normalize_path(path) +} + fn prompt(msg: &str) -> bool { let _ = stderr().write_all(msg.as_bytes()); let _ = stderr().flush(); @@ -302,6 +386,8 @@ fn prompt(msg: &str) -> bool { let stdin = stdin(); let mut stdin = stdin.lock(); + #[allow(clippy::match_like_matches_macro)] + // `matches!(...)` macro not stabilized until rust v1.42 match stdin.read_until(b'\n', &mut buf) { Ok(x) if x > 0 => match buf[0] { b'y' | b'Y' => true, diff --git a/src/uu/rmdir/Cargo.toml b/src/uu/rmdir/Cargo.toml index 1a8705d7c..b6e04f71c 100644 --- a/src/uu/rmdir/Cargo.toml +++ b/src/uu/rmdir/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_rmdir" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "rmdir ~ (uutils) remove empty DIRECTORY" @@ -15,9 +15,9 @@ edition = "2018" path = "src/rmdir.rs" [dependencies] -getopts = "0.2.18" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +clap = "2.33" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "rmdir" diff --git a/src/uu/rmdir/src/rmdir.rs b/src/uu/rmdir/src/rmdir.rs index 65ea630d1..7a7e8fc9b 100644 --- a/src/uu/rmdir/src/rmdir.rs +++ b/src/uu/rmdir/src/rmdir.rs @@ -5,69 +5,73 @@ // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -extern crate getopts; - #[macro_use] extern crate uucore; +use clap::{App, Arg}; use std::fs; use std::path::Path; -static NAME: &str = "rmdir"; static VERSION: &str = env!("CARGO_PKG_VERSION"); +static ABOUT: &str = "Remove the DIRECTORY(ies), if they are empty."; +static OPT_IGNORE_FAIL_NON_EMPTY: &str = "ignore-fail-on-non-empty"; +static OPT_PARENTS: &str = "parents"; +static OPT_VERBOSE: &str = "verbose"; + +static ARG_DIRS: &str = "dirs"; + +fn get_usage() -> String { + format!("{0} [OPTION]... DIRECTORY...", executable!()) +} pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); + let usage = get_usage(); - let mut opts = getopts::Options::new(); + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(&usage[..]) + .arg( + Arg::with_name(OPT_IGNORE_FAIL_NON_EMPTY) + .long(OPT_IGNORE_FAIL_NON_EMPTY) + .help("ignore each failure that is solely because a directory is non-empty"), + ) + .arg( + Arg::with_name(OPT_PARENTS) + .short("p") + .long(OPT_PARENTS) + .help( + "remove DIRECTORY and its ancestors; e.g., + 'rmdir -p a/b/c' is similar to rmdir a/b/c a/b a", + ), + ) + .arg( + Arg::with_name(OPT_VERBOSE) + .short("v") + .long(OPT_VERBOSE) + .help("output a diagnostic for every directory processed"), + ) + .arg( + Arg::with_name(ARG_DIRS) + .multiple(true) + .takes_value(true) + .min_values(1) + .required(true), + ) + .get_matches_from(args); - opts.optflag( - "", - "ignore-fail-on-non-empty", - "ignore each failure that is solely because a directory is non-empty", - ); - opts.optflag("p", "parents", "remove DIRECTORY and its ancestors; e.g., 'rmdir -p a/b/c' is similar to rmdir a/b/c a/b a"); - opts.optflag( - "v", - "verbose", - "output a diagnostic for every directory processed", - ); - opts.optflag("h", "help", "print this help and exit"); - opts.optflag("V", "version", "output version information and exit"); + let dirs: Vec = matches + .values_of(ARG_DIRS) + .map(|v| v.map(ToString::to_string).collect()) + .unwrap_or_default(); - let matches = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(f) => { - show_error!("{}", f); - return 1; - } - }; + let ignore = matches.is_present(OPT_IGNORE_FAIL_NON_EMPTY); + let parents = matches.is_present(OPT_PARENTS); + let verbose = matches.is_present(OPT_VERBOSE); - if matches.opt_present("help") { - let msg = format!( - "{0} {1} - -Usage: - {0} [OPTION]... DIRECTORY... - -Remove the DIRECTORY(ies), if they are empty.", - NAME, VERSION - ); - print!("{}", opts.usage(&msg)); - } else if matches.opt_present("version") { - println!("{} {}", NAME, VERSION); - } else if matches.free.is_empty() { - show_error!("missing an argument"); - show_error!("for help, try '{0} --help'", NAME); - return 1; - } else { - let ignore = matches.opt_present("ignore-fail-on-non-empty"); - let parents = matches.opt_present("parents"); - let verbose = matches.opt_present("verbose"); - match remove(matches.free, ignore, parents, verbose) { - Ok(()) => ( /* pass */ ), - Err(e) => return e, - } + match remove(dirs, ignore, parents, verbose) { + Ok(()) => ( /* pass */ ), + Err(e) => return e, } 0 @@ -115,7 +119,7 @@ fn remove_dir(path: &Path, ignore: bool, verbose: bool) -> Result<(), i32> { show_error!("removing directory '{}': {}", path.display(), e); r = Err(1); } - Ok(_) if verbose => println!("Removed directory '{}'", path.display()), + Ok(_) if verbose => println!("removing directory, '{}'", path.display()), _ => (), } } else if !ignore { diff --git a/src/uu/seq/Cargo.toml b/src/uu/seq/Cargo.toml index 29d1eb1cf..96c629c68 100644 --- a/src/uu/seq/Cargo.toml +++ b/src/uu/seq/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_seq" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "seq ~ (uutils) display a sequence of numbers" @@ -15,9 +15,9 @@ edition = "2018" path = "src/seq.rs" [dependencies] -getopts = "0.2.18" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +clap = "2.33" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "seq" diff --git a/src/uu/seq/src/seq.rs b/src/uu/seq/src/seq.rs index 57890ab28..671dd7e1c 100644 --- a/src/uu/seq/src/seq.rs +++ b/src/uu/seq/src/seq.rs @@ -3,17 +3,29 @@ // spell-checker:ignore (ToDO) istr chiter argptr ilen -extern crate getopts; - #[macro_use] extern crate uucore; +use clap::{App, AppSettings, Arg}; use std::cmp; use std::io::{stdout, Write}; -static NAME: &str = "seq"; static VERSION: &str = env!("CARGO_PKG_VERSION"); +static ABOUT: &str = "Display numbers from FIRST to LAST, in steps of INCREMENT."; +static OPT_SEPARATOR: &str = "separator"; +static OPT_TERMINATOR: &str = "terminator"; +static OPT_WIDTHS: &str = "widths"; +static ARG_NUMBERS: &str = "numbers"; + +fn get_usage() -> String { + format!( + "{0} [OPTION]... LAST + {0} [OPTION]... FIRST LAST + {0} [OPTION]... FIRST INCREMENT LAST", + executable!() + ) +} #[derive(Clone)] struct SeqOptions { separator: String, @@ -38,162 +50,59 @@ fn escape_sequences(s: &str) -> String { s.replace("\\n", "\n").replace("\\t", "\t") } -fn parse_options(args: Vec, options: &mut SeqOptions) -> Result, i32> { - let mut seq_args = vec![]; - let mut iter = args.into_iter().skip(1); - while let Some(arg) = iter.next() { - match &arg[..] { - "--help" | "-h" => { - print_help(); - return Err(0); - } - "--version" | "-V" => { - print_version(); - return Err(0); - } - "-s" | "--separator" => match iter.next() { - Some(sep) => options.separator = sep, - None => { - show_error!("expected a separator after {}", arg); - return Err(1); - } - }, - "-t" | "--terminator" => match iter.next() { - Some(term) => options.terminator = Some(term), - None => { - show_error!("expected a terminator after '{}'", arg); - return Err(1); - } - }, - "-w" | "--widths" => options.widths = true, - "--" => { - seq_args.extend(iter); - break; - } - _ => { - if arg.len() > 1 && arg.starts_with('-') { - let argptr: *const String = &arg; // escape from the borrow checker - let mut chiter = unsafe { &(*argptr)[..] }.chars().skip(1); - let mut ch = ' '; - while match chiter.next() { - Some(m) => { - ch = m; - true - } - None => false, - } { - match ch { - 'h' => { - print_help(); - return Err(0); - } - 'V' => { - print_version(); - return Err(0); - } - 's' => match iter.next() { - Some(sep) => { - options.separator = sep; - let next = chiter.next(); - if let Some(n) = next { - show_error!("unexpected character ('{}')", n); - return Err(1); - } - } - None => { - show_error!("expected a separator after {}", arg); - return Err(1); - } - }, - 't' => match iter.next() { - Some(term) => { - options.terminator = Some(term); - let next = chiter.next(); - if let Some(n) = next { - show_error!("unexpected character ('{}')", n); - return Err(1); - } - } - None => { - show_error!("expected a terminator after {}", arg); - return Err(1); - } - }, - 'w' => options.widths = true, - _ => { - seq_args.push(arg); - break; - } - } - } - } else { - seq_args.push(arg); - } - } - }; - } - Ok(seq_args) -} - -fn print_help() { - let mut opts = getopts::Options::new(); - - opts.optopt( - "s", - "separator", - "Separator character (defaults to \\n)", - "", - ); - opts.optopt( - "t", - "terminator", - "Terminator character (defaults to separator)", - "", - ); - opts.optflag( - "w", - "widths", - "Equalize widths of all numbers by padding with zeros", - ); - opts.optflag("h", "help", "print this help text and exit"); - opts.optflag("V", "version", "print version and exit"); - - println!("{} {}\n", NAME, VERSION); - println!( - "Usage:\n {} [-w] [-s string] [-t string] [first [step]] last\n", - NAME - ); - println!("{}", opts.usage("Print sequences of numbers")); -} - -fn print_version() { - println!("{} {}", NAME, VERSION); -} - pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); + let usage = get_usage(); + let matches = App::new(executable!()) + .setting(AppSettings::AllowLeadingHyphen) + .version(VERSION) + .about(ABOUT) + .usage(&usage[..]) + .arg( + Arg::with_name(OPT_SEPARATOR) + .short("s") + .long("separator") + .help("Separator character (defaults to \\n)") + .takes_value(true) + .number_of_values(1), + ) + .arg( + Arg::with_name(OPT_TERMINATOR) + .short("t") + .long("terminator") + .help("Terminator character (defaults to separator)") + .takes_value(true) + .number_of_values(1), + ) + .arg( + Arg::with_name(OPT_WIDTHS) + .short("w") + .long("widths") + .help("Equalize widths of all numbers by padding with zeros"), + ) + .arg( + Arg::with_name(ARG_NUMBERS) + .multiple(true) + .takes_value(true) + .allow_hyphen_values(true) + .max_values(3), + ) + .get_matches_from(args); + + let numbers = matches.values_of(ARG_NUMBERS).unwrap().collect::>(); let mut options = SeqOptions { separator: "\n".to_owned(), terminator: None, widths: false, }; - let free = match parse_options(args, &mut options) { - Ok(m) => m, - Err(f) => return f, - }; - if free.is_empty() || free.len() > 3 { - crash!( - 1, - "too {} operands.\nTry '{} --help' for more information.", - if free.is_empty() { "few" } else { "many" }, - NAME - ); - } + options.separator = matches.value_of(OPT_SEPARATOR).unwrap_or("\n").to_string(); + options.terminator = matches.value_of(OPT_TERMINATOR).map(String::from); + options.widths = matches.is_present(OPT_WIDTHS); + let mut largest_dec = 0; let mut padding = 0; - let first = if free.len() > 1 { - let slice = &free[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; @@ -208,8 +117,8 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } else { 1.0 }; - let step = if free.len() > 2 { - let slice = &free[1][..]; + 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); @@ -224,8 +133,12 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } else { 1.0 }; + if increment == 0.0 { + show_error!("increment value: '{}'", &numbers[1][..]); + return 1; + } let last = { - let slice = &free[free.len() - 1][..]; + let slice = &numbers[numbers.len() - 1][..]; padding = cmp::max(padding, slice.find('.').unwrap_or_else(|| slice.len())); match parse_float(slice) { Ok(n) => n, @@ -245,7 +158,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { }; print_seq( first, - step, + increment, last, largest_dec, separator, @@ -257,8 +170,8 @@ pub fn uumain(args: impl uucore::Args) -> i32 { 0 } -fn done_printing(next: f64, step: f64, last: f64) -> bool { - if step >= 0f64 { +fn done_printing(next: f64, increment: f64, last: f64) -> bool { + if increment >= 0f64 { next > last } else { next < last @@ -268,7 +181,7 @@ fn done_printing(next: f64, step: f64, last: f64) -> bool { #[allow(clippy::too_many_arguments)] fn print_seq( first: f64, - step: f64, + increment: f64, last: f64, largest_dec: usize, separator: String, @@ -277,8 +190,8 @@ fn print_seq( padding: usize, ) { let mut i = 0isize; - let mut value = first + i as f64 * step; - while !done_printing(value, step, last) { + let mut value = first + i as f64 * increment; + while !done_printing(value, increment, last) { let istr = format!("{:.*}", largest_dec, value); let ilen = istr.len(); let before_dec = istr.find('.').unwrap_or(ilen); @@ -289,12 +202,12 @@ fn print_seq( } print!("{}", istr); i += 1; - value = first + i as f64 * step; - if !done_printing(value, step, last) { + value = first + i as f64 * increment; + if !done_printing(value, increment, last) { print!("{}", separator); } } - if (first >= last && step < 0f64) || (first <= last && step > 0f64) { + if (first >= last && increment < 0f64) || (first <= last && increment > 0f64) { print!("{}", terminator); } crash_if_err!(1, stdout().flush()); diff --git a/src/uu/shred/Cargo.toml b/src/uu/shred/Cargo.toml index 12cc3926e..dda68b45b 100644 --- a/src/uu/shred/Cargo.toml +++ b/src/uu/shred/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_shred" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "shred ~ (uutils) hide former FILE contents with repeated overwrites" @@ -15,13 +15,13 @@ edition = "2018" path = "src/shred.rs" [dependencies] +clap = "2.33" filetime = "0.2.1" -getopts = "0.2.18" libc = "0.2.42" rand = "0.5" time = "0.1.40" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "shred" diff --git a/src/uu/shred/src/shred.rs b/src/uu/shred/src/shred.rs index 307684375..d5f910297 100644 --- a/src/uu/shred/src/shred.rs +++ b/src/uu/shred/src/shred.rs @@ -8,9 +8,7 @@ // spell-checker:ignore (ToDO) NAMESET FILESIZE fstab coeff journaling writeback REiser journaled -extern crate getopts; -extern crate rand; - +use clap::{App, Arg}; use rand::{Rng, ThreadRng}; use std::cell::{Cell, RefCell}; use std::fs; @@ -69,10 +67,7 @@ struct FilenameGenerator { impl FilenameGenerator { fn new(name_len: usize) -> FilenameGenerator { - let mut indices: Vec = Vec::new(); - for _ in 0..name_len { - indices.push(0); - } + let indices: Vec = vec![0; name_len]; FilenameGenerator { name_len, nameset_indices: RefCell::new(indices), @@ -125,6 +120,7 @@ struct BytesGenerator<'a> { exact: bool, // if false, every block's size is block_size gen_type: PassType<'a>, rng: Option>, + bytes: [u8; BLOCK_SIZE], } impl<'a> BytesGenerator<'a> { @@ -134,6 +130,8 @@ impl<'a> BytesGenerator<'a> { _ => None, }; + let bytes = [0; BLOCK_SIZE]; + BytesGenerator { total_bytes, bytes_generated: Cell::new(0u64), @@ -141,25 +139,35 @@ impl<'a> BytesGenerator<'a> { exact, gen_type, rng, + bytes, } } -} -impl<'a> Iterator for BytesGenerator<'a> { - type Item = Box<[u8]>; + pub fn reset(&mut self, total_bytes: u64, gen_type: PassType<'a>) { + if let PassType::Random = gen_type { + if self.rng.is_none() { + self.rng = Some(RefCell::new(rand::thread_rng())); + } + } - fn next(&mut self) -> Option> { + self.total_bytes = total_bytes; + self.gen_type = gen_type; + + self.bytes_generated.set(0); + } + + pub fn next(&mut self) -> Option<&[u8]> { // We go over the total_bytes limit when !self.exact and total_bytes isn't a multiple // of self.block_size if self.bytes_generated.get() >= self.total_bytes { return None; } - let this_block_size: usize = { + let this_block_size = { if !self.exact { self.block_size } else { - let bytes_left: u64 = self.total_bytes - self.bytes_generated.get(); + let bytes_left = self.total_bytes - self.bytes_generated.get(); if bytes_left >= self.block_size as u64 { self.block_size } else { @@ -168,17 +176,12 @@ impl<'a> Iterator for BytesGenerator<'a> { } }; - let mut bytes: Vec = Vec::with_capacity(this_block_size); + let bytes = &mut self.bytes[..this_block_size]; match self.gen_type { PassType::Random => { - // This is ok because the vector was - // allocated with the same capacity - unsafe { - bytes.set_len(this_block_size); - } let mut rng = self.rng.as_ref().unwrap().borrow_mut(); - rng.fill(&mut bytes[..]); + rng.fill(bytes); } PassType::Pattern(pattern) => { let skip = { @@ -188,10 +191,17 @@ impl<'a> Iterator for BytesGenerator<'a> { (pattern.len() as u64 % self.bytes_generated.get()) as usize } }; - // Same range as 0..this_block_size but we start with the right index - for i in skip..this_block_size + skip { - let index = i % pattern.len(); - bytes.push(pattern[index]); + + // Copy the pattern in chunks rather than simply one byte at a time + let mut i = 0; + while i < this_block_size { + let start = (i + skip) % pattern.len(); + let end = (this_block_size - i).min(pattern.len()); + let len = end - start; + + bytes[i..i + len].copy_from_slice(&pattern[start..end]); + + i += len; } } }; @@ -199,142 +209,178 @@ impl<'a> Iterator for BytesGenerator<'a> { let new_bytes_generated = self.bytes_generated.get() + this_block_size as u64; self.bytes_generated.set(new_bytes_generated); - Some(bytes.into_boxed_slice()) + Some(bytes) } } +static ABOUT: &str = "Overwrite the specified FILE(s) repeatedly, in order to make it harder\n\ +for even very expensive hardware probing to recover the data. +"; + +fn get_usage() -> String { + format!("{} [OPTION]... FILE...", executable!()) +} + +static AFTER_HELP: &str = + "Delete FILE(s) if --remove (-u) is specified. The default is not to remove\n\ + the files because it is common to operate on device files like /dev/hda,\n\ + and those files usually should not be removed.\n\ + \n\ + CAUTION: Note that shred relies on a very important assumption:\n\ + that the file system overwrites data in place. This is the traditional\n\ + way to do things, but many modern file system designs do not satisfy this\n\ + assumption. The following are examples of file systems on which shred is\n\ + not effective, or is not guaranteed to be effective in all file system modes:\n\ + \n\ + * log-structured or journaled file systems, such as those supplied with\n\ + AIX and Solaris (and JFS, ReiserFS, XFS, Ext3, etc.)\n\ + \n\ + * file systems that write redundant data and carry on even if some writes\n\ + fail, such as RAID-based file systems\n\ + \n\ + * file systems that make snapshots, such as Network Appliance's NFS server\n\ + \n\ + * file systems that cache in temporary locations, such as NFS\n\ + version 3 clients\n\ + \n\ + * compressed file systems\n\ + \n\ + In the case of ext3 file systems, the above disclaimer applies\n\ + and shred is thus of limited effectiveness) only in data=journal mode,\n\ + which journals file data in addition to just metadata. In both the\n\ + data=ordered (default) and data=writeback modes, shred works as usual.\n\ + Ext3 journaling modes can be changed by adding the data=something option\n\ + to the mount options for a particular file system in the /etc/fstab file,\n\ + as documented in the mount man page (man mount).\n\ + \n\ + In addition, file system backups and remote mirrors may contain copies\n\ + of the file that cannot be removed, and that will allow a shredded file\n\ + to be recovered later.\n\ + "; + +pub mod options { + pub const FILE: &str = "file"; + pub const ITERATIONS: &str = "iterations"; + pub const SIZE: &str = "size"; + pub const REMOVE: &str = "remove"; + pub const VERBOSE: &str = "verbose"; + pub const EXACT: &str = "exact"; + pub const ZERO: &str = "zero"; +} + pub fn uumain(args: impl uucore::Args) -> i32 { let args = args.collect_str(); - let mut opts = getopts::Options::new(); + let usage = get_usage(); - // TODO: Add force option - opts.optopt( - "n", - "iterations", - "overwrite N times instead of the default (3)", - "N", - ); - opts.optopt( - "s", - "size", - "shred this many bytes (suffixes like K, M, G accepted)", - "FILESIZE", - ); - opts.optflag( - "u", - "remove", - "truncate and remove the file after overwriting; See below", - ); - opts.optflag("v", "verbose", "show progress"); - opts.optflag( - "x", - "exact", - "do not round file sizes up to the next full block; \ - this is the default for non-regular files", - ); - opts.optflag( - "z", - "zero", - "add a final overwrite with zeros to hide shredding", - ); - opts.optflag("", "help", "display this help and exit"); - opts.optflag("", "version", "output version information and exit"); + let app = App::new(executable!()) + .version(VERSION_STR) + .about(ABOUT) + .after_help(AFTER_HELP) + .usage(&usage[..]) + .arg( + Arg::with_name(options::ITERATIONS) + .long(options::ITERATIONS) + .short("n") + .help("overwrite N times instead of the default (3)") + .value_name("NUMBER") + .default_value("3"), + ) + .arg( + Arg::with_name(options::SIZE) + .long(options::SIZE) + .short("s") + .takes_value(true) + .value_name("N") + .help("shred this many bytes (suffixes like K, M, G accepted)"), + ) + .arg( + Arg::with_name(options::REMOVE) + .short("u") + .long(options::REMOVE) + .help("truncate and remove file after overwriting; See below"), + ) + .arg( + Arg::with_name(options::VERBOSE) + .long(options::VERBOSE) + .short("v") + .help("show progress"), + ) + .arg( + Arg::with_name(options::EXACT) + .long(options::EXACT) + .short("x") + .help( + "do not round file sizes up to the next full block;\n\ + this is the default for non-regular files", + ), + ) + .arg( + Arg::with_name(options::ZERO) + .long(options::ZERO) + .short("z") + .help("add a final overwrite with zeros to hide shredding"), + ) + // Positional arguments + .arg(Arg::with_name(options::FILE).hidden(true).multiple(true)); - let matches = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(e) => panic!("Invalid options\n{}", e), + let matches = app.get_matches_from(args); + + let mut errs: Vec = vec![]; + + if !matches.is_present(options::FILE) { + show_error!("Missing an argument"); + show_error!("For help, try '{} --help'", NAME); + return 0; + } + + let iterations = match matches.value_of(options::ITERATIONS) { + Some(s) => match s.parse::() { + Ok(u) => u, + Err(_) => { + errs.push(format!("invalid number of passes: '{}'", s)); + 0 + } + }, + None => unreachable!(), }; - if matches.opt_present("help") { - show_help(&opts); - return 0; - } else if matches.opt_present("version") { - println!("{} {}", NAME, VERSION_STR); - return 0; - } else if matches.free.is_empty() { - println!("{}: Missing an argument", NAME); - println!("For help, try '{} --help'", NAME); - return 0; - } else { - let iterations = match matches.opt_str("iterations") { - Some(s) => match s.parse::() { - Ok(u) => u, - Err(_) => { - println!("{}: Invalid number of passes", NAME); - return 1; - } - }, - None => 3, - }; - let remove = matches.opt_present("remove"); - let size = get_size(matches.opt_str("size")); - let exact = matches.opt_present("exact") && size.is_none(); // if -s is given, ignore -x - let zero = matches.opt_present("zero"); - let verbose = matches.opt_present("verbose"); - for path_str in matches.free.into_iter() { - wipe_file(&path_str, iterations, remove, size, exact, zero, verbose); + // TODO: implement --remove HOW + // The optional HOW parameter indicates how to remove a directory entry: + // - 'unlink' => use a standard unlink call. + // - 'wipe' => also first obfuscate bytes in the name. + // - 'wipesync' => also sync each obfuscated byte to disk. + // The default mode is 'wipesync', but note it can be expensive. + + // TODO: implement --random-source + + // TODO: implement --force + + let remove = matches.is_present(options::REMOVE); + let size_arg = match matches.value_of(options::SIZE) { + Some(s) => Some(s.to_string()), + None => None, + }; + let size = get_size(size_arg); + let exact = matches.is_present(options::EXACT) && size.is_none(); // if -s is given, ignore -x + let zero = matches.is_present(options::ZERO); + let verbose = matches.is_present(options::VERBOSE); + + if !errs.is_empty() { + show_error!("Invalid arguments supplied."); + for message in errs { + show_error!("{}", message); } + return 1; + } + + for path_str in matches.values_of(options::FILE).unwrap() { + wipe_file(&path_str, iterations, remove, size, exact, zero, verbose); } 0 } -fn show_help(opts: &getopts::Options) { - println!("Usage: {} [OPTION]... FILE...", NAME); - println!( - "Overwrite the specified FILE(s) repeatedly, in order to make it harder \ - for even very expensive hardware probing to recover the data." - ); - println!("{}", opts.usage("")); - println!("Delete FILE(s) if --remove (-u) is specified. The default is not to remove"); - println!("the files because it is common to operate on device files like /dev/hda,"); - println!("and those files usually should not be removed."); - println!(); - println!( - "CAUTION: Note that {} relies on a very important assumption:", - NAME - ); - println!("that the file system overwrites data in place. This is the traditional"); - println!("way to do things, but many modern file system designs do not satisfy this"); - println!( - "assumption. The following are examples of file systems on which {} is", - NAME - ); - println!("not effective, or is not guaranteed to be effective in all file system modes:"); - println!(); - println!("* log-structured or journaled file systems, such as those supplied with"); - println!("AIX and Solaris (and JFS, ReiserFS, XFS, Ext3, etc.)"); - println!(); - println!("* file systems that write redundant data and carry on even if some writes"); - println!("fail, such as RAID-based file systems"); - println!(); - println!("* file systems that make snapshots, such as Network Appliance's NFS server"); - println!(); - println!("* file systems that cache in temporary locations, such as NFS"); - println!("version 3 clients"); - println!(); - println!("* compressed file systems"); - println!(); - println!("In the case of ext3 file systems, the above disclaimer applies"); - println!( - "(and {} is thus of limited effectiveness) only in data=journal mode,", - NAME - ); - println!("which journals file data in addition to just metadata. In both the"); - println!( - "data=ordered (default) and data=writeback modes, {} works as usual.", - NAME - ); - println!("Ext3 journaling modes can be changed by adding the data=something option"); - println!("to the mount options for a particular file system in the /etc/fstab file,"); - println!("as documented in the mount man page (man mount)."); - println!(); - println!("In addition, file system backups and remote mirrors may contain copies"); - println!("of the file that cannot be removed, and that will allow a shredded file"); - println!("to be recovered later."); -} - // TODO: Add support for all postfixes here up to and including EiB // http://www.gnu.org/software/coreutils/manual/coreutils.html#Block-size fn get_size(size_str_opt: Option) -> Option { @@ -449,6 +495,10 @@ fn wipe_file( .open(path) .expect("Failed to open file for writing"); + // NOTE: it does not really matter what we set for total_bytes and gen_type here, so just + // use bogus values + let mut generator = BytesGenerator::new(0, PassType::Pattern(&[]), exact); + for (i, pass_type) in pass_sequence.iter().enumerate() { if verbose { let pass_name: String = pass_name(*pass_type); @@ -473,7 +523,8 @@ fn wipe_file( } } // size is an optional argument for exactly how many bytes we want to shred - do_pass(&mut file, path, *pass_type, size, exact).expect("File write pass failed"); + do_pass(&mut file, path, &mut generator, *pass_type, size) + .expect("File write pass failed"); // Ignore failed writes; just keep trying } } @@ -483,22 +534,22 @@ fn wipe_file( } } -fn do_pass( +fn do_pass<'a>( file: &mut File, path: &Path, - generator_type: PassType, + generator: &mut BytesGenerator<'a>, + generator_type: PassType<'a>, given_file_size: Option, - exact: bool, ) -> Result<(), io::Error> { file.seek(SeekFrom::Start(0))?; // Use the given size or the whole file if not specified let size: u64 = given_file_size.unwrap_or(get_file_size(path)?); - let generator = BytesGenerator::new(size, generator_type, exact); + generator.reset(size, generator_type); - for block in generator { - file.write_all(&*block)?; + while let Some(block) = generator.next() { + file.write_all(block)?; } file.sync_data()?; diff --git a/src/uu/shuf/Cargo.toml b/src/uu/shuf/Cargo.toml index 128fb939c..dbf559454 100644 --- a/src/uu/shuf/Cargo.toml +++ b/src/uu/shuf/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_shuf" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "shuf ~ (uutils) display random permutations of input lines" @@ -15,10 +15,10 @@ edition = "2018" path = "src/shuf.rs" [dependencies] -getopts = "0.2.18" +clap = "2.33" rand = "0.5" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "shuf" diff --git a/src/uu/shuf/src/shuf.rs b/src/uu/shuf/src/shuf.rs index 6006b7875..f7af05214 100644 --- a/src/uu/shuf/src/shuf.rs +++ b/src/uu/shuf/src/shuf.rs @@ -7,138 +7,165 @@ // spell-checker:ignore (ToDO) cmdline evec seps rvec fdata -extern crate getopts; -extern crate rand; - #[macro_use] extern crate uucore; +use clap::{App, Arg}; use rand::Rng; use std::fs::File; use std::io::{stdin, stdout, BufReader, BufWriter, Read, Write}; -use std::usize::MAX as MAX_USIZE; - -enum Mode { - Default, - Echo, - InputRange((usize, usize)), -} static NAME: &str = "shuf"; static VERSION: &str = env!("CARGO_PKG_VERSION"); +static USAGE: &str = r#"shuf [OPTION]... [FILE] + or: shuf -e [OPTION]... [ARG]... + or: shuf -i LO-HI [OPTION]... +Write a random permutation of the input lines to standard output. + +With no FILE, or when FILE is -, read standard input. +"#; +static TEMPLATE: &str = "Usage: {usage}\nMandatory arguments to long options are mandatory for short options too.\n{unified}"; + +struct Options { + head_count: usize, + output: Option, + random_source: Option, + repeat: bool, + sep: u8, +} + +enum Mode { + Default(String), + Echo(Vec), + InputRange((usize, usize)), +} + +mod options { + pub static ECHO: &str = "echo"; + pub static INPUT_RANGE: &str = "input-range"; + pub static HEAD_COUNT: &str = "head-count"; + pub static OUTPUT: &str = "output"; + pub static RANDOM_SOURCE: &str = "random-source"; + pub static REPEAT: &str = "repeat"; + pub static ZERO_TERMINATED: &str = "zero-terminated"; + pub static FILE: &str = "file"; +} pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); + let matches = App::new(executable!()) + .name(NAME) + .version(VERSION) + .template(TEMPLATE) + .usage(USAGE) + .arg( + Arg::with_name(options::ECHO) + .short("e") + .long(options::ECHO) + .takes_value(true) + .value_name("ARG") + .help("treat each ARG as an input line") + .multiple(true) + .use_delimiter(false) + .min_values(0) + .conflicts_with(options::INPUT_RANGE), + ) + .arg( + Arg::with_name(options::INPUT_RANGE) + .short("i") + .long(options::INPUT_RANGE) + .takes_value(true) + .value_name("LO-HI") + .help("treat each number LO through HI as an input line") + .conflicts_with(options::FILE), + ) + .arg( + Arg::with_name(options::HEAD_COUNT) + .short("n") + .long(options::HEAD_COUNT) + .takes_value(true) + .value_name("COUNT") + .help("output at most COUNT lines"), + ) + .arg( + Arg::with_name(options::OUTPUT) + .short("o") + .long(options::OUTPUT) + .takes_value(true) + .value_name("FILE") + .help("write result to FILE instead of standard output"), + ) + .arg( + Arg::with_name(options::RANDOM_SOURCE) + .long(options::RANDOM_SOURCE) + .takes_value(true) + .value_name("FILE") + .help("get random bytes from FILE"), + ) + .arg( + Arg::with_name(options::REPEAT) + .short("r") + .long(options::REPEAT) + .help("output lines can be repeated"), + ) + .arg( + Arg::with_name(options::ZERO_TERMINATED) + .short("z") + .long(options::ZERO_TERMINATED) + .help("line delimiter is NUL, not newline"), + ) + .arg(Arg::with_name(options::FILE).takes_value(true)) + .get_matches_from(args); - let mut opts = getopts::Options::new(); - opts.optflag("e", "echo", "treat each ARG as an input line"); - opts.optopt( - "i", - "input-range", - "treat each number LO through HI as an input line", - "LO-HI", - ); - opts.optopt("n", "head-count", "output at most COUNT lines", "COUNT"); - opts.optopt( - "o", - "output", - "write result to FILE instead of standard output", - "FILE", - ); - opts.optopt("", "random-source", "get random bytes from FILE", "FILE"); - opts.optflag("r", "repeat", "output lines can be repeated"); - opts.optflag("z", "zero-terminated", "end lines with 0 byte, not newline"); - opts.optflag("h", "help", "display this help and exit"); - opts.optflag("V", "version", "output version information and exit"); - let mut matches = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(f) => crash!(1, "{}", f), - }; - if matches.opt_present("help") { - let msg = format!( - "{0} {1} - -Usage: - {0} [OPTION]... [FILE] - {0} -e [OPTION]... [ARG]... - {0} -i LO-HI [OPTION]... - -Write a random permutation of the input lines to standard output. -With no FILE, or when FILE is -, read standard input.", - NAME, VERSION - ); - print!("{}", opts.usage(&msg)); - } else if matches.opt_present("version") { - println!("{} {}", NAME, VERSION); + let mode = if let Some(args) = matches.values_of(options::ECHO) { + Mode::Echo(args.map(String::from).collect()) + } else if let Some(range) = matches.value_of(options::INPUT_RANGE) { + match parse_range(range) { + Ok(m) => Mode::InputRange(m), + Err(msg) => { + crash!(1, "{}", msg); + } + } } else { - let echo = matches.opt_present("echo"); - let mode = match matches.opt_str("input-range") { - Some(range) => { - if echo { - show_error!("cannot specify more than one mode"); - return 1; - } - match parse_range(range) { - Ok(m) => Mode::InputRange(m), - Err(msg) => { - crash!(1, "{}", msg); - } - } - } - None => { - if echo { - Mode::Echo - } else { - if matches.free.is_empty() { - matches.free.push("-".to_owned()); - } else if matches.free.len() > 1 { - show_error!("extra operand '{}'", &matches.free[1][..]); - } - Mode::Default - } - } - }; - let repeat = matches.opt_present("repeat"); - let sep = if matches.opt_present("zero-terminated") { - 0x00 as u8 - } else { - 0x0a as u8 - }; - let count = match matches.opt_str("head-count") { - Some(cnt) => match cnt.parse::() { + Mode::Default(matches.value_of(options::FILE).unwrap_or("-").to_string()) + }; + + let options = Options { + head_count: match matches.value_of(options::HEAD_COUNT) { + Some(count) => match count.parse::() { Ok(val) => val, - Err(e) => { - show_error!("'{}' is not a valid count: {}", cnt, e); + Err(_) => { + show_error!("invalid line count: '{}'", count); return 1; } }, - None => MAX_USIZE, - }; - let output = matches.opt_str("output"); - let random = matches.opt_str("random-source"); + None => std::usize::MAX, + }, + output: matches.value_of(options::OUTPUT).map(String::from), + random_source: matches.value_of(options::RANDOM_SOURCE).map(String::from), + repeat: matches.is_present(options::REPEAT), + sep: if matches.is_present(options::ZERO_TERMINATED) { + 0x00_u8 + } else { + 0x0a_u8 + }, + }; - match mode { - Mode::Echo => { - // XXX: this doesn't correctly handle non-UTF-8 cmdline args - let mut evec = matches - .free - .iter() - .map(String::as_bytes) - .collect::>(); - find_seps(&mut evec, sep); - shuf_bytes(&mut evec, repeat, count, sep, output, random); - } - Mode::InputRange((b, e)) => { - let rvec = (b..e).map(|x| format!("{}", x)).collect::>(); - let mut rvec = rvec.iter().map(String::as_bytes).collect::>(); - shuf_bytes(&mut rvec, repeat, count, sep, output, random); - } - Mode::Default => { - let fdata = read_input_file(&matches.free[0][..]); - let mut fdata = vec![&fdata[..]]; - find_seps(&mut fdata, sep); - shuf_bytes(&mut fdata, repeat, count, sep, output, random); - } + match mode { + Mode::Echo(args) => { + let mut evec = args.iter().map(String::as_bytes).collect::>(); + find_seps(&mut evec, options.sep); + shuf_bytes(&mut evec, options); + } + Mode::InputRange((b, e)) => { + let rvec = (b..e).map(|x| format!("{}", x)).collect::>(); + let mut rvec = rvec.iter().map(String::as_bytes).collect::>(); + shuf_bytes(&mut rvec, options); + } + Mode::Default(filename) => { + let fdata = read_input_file(&filename); + let mut fdata = vec![&fdata[..]]; + find_seps(&mut fdata, options.sep); + shuf_bytes(&mut fdata, options); } } @@ -196,15 +223,8 @@ fn find_seps(data: &mut Vec<&[u8]>, sep: u8) { } } -fn shuf_bytes( - input: &mut Vec<&[u8]>, - repeat: bool, - count: usize, - sep: u8, - output: Option, - random: Option, -) { - let mut output = BufWriter::new(match output { +fn shuf_bytes(input: &mut Vec<&[u8]>, opts: Options) { + let mut output = BufWriter::new(match opts.output { None => Box::new(stdout()) as Box, Some(s) => match File::create(&s[..]) { Ok(f) => Box::new(f) as Box, @@ -212,7 +232,7 @@ fn shuf_bytes( }, }); - let mut rng = match random { + 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), @@ -228,7 +248,7 @@ fn shuf_bytes( len_mod <<= 1; } - let mut count = count; + let mut count = opts.head_count; while count > 0 && !input.is_empty() { let mut r = input.len(); while r >= input.len() { @@ -240,11 +260,11 @@ fn shuf_bytes( .write_all(input[r]) .unwrap_or_else(|e| crash!(1, "write failed: {}", e)); output - .write_all(&[sep]) + .write_all(&[opts.sep]) .unwrap_or_else(|e| crash!(1, "write failed: {}", e)); // if we do not allow repeats, remove the chosen value from the input vector - if !repeat { + if !opts.repeat { // shrink the mask if we will drop below a power of 2 if input.len() % 2 == 0 && len_mod > 2 { len_mod >>= 1; @@ -256,18 +276,18 @@ fn shuf_bytes( } } -fn parse_range(input_range: String) -> Result<(usize, usize), String> { +fn parse_range(input_range: &str) -> Result<(usize, usize), String> { let split: Vec<&str> = input_range.split('-').collect(); if split.len() != 2 { - Err("invalid range format".to_owned()) + Err(format!("invalid input range: '{}'", input_range)) } else { let begin = match split[0].parse::() { Ok(m) => m, - Err(e) => return Err(format!("{} is not a valid number: {}", split[0], e)), + Err(_) => return Err(format!("invalid input range: '{}'", split[0])), }; let end = match split[1].parse::() { Ok(m) => m, - Err(e) => return Err(format!("{} is not a valid number: {}", split[1], e)), + Err(_) => return Err(format!("invalid input range: '{}'", split[1])), }; Ok((begin, end + 1)) } diff --git a/src/uu/sleep/Cargo.toml b/src/uu/sleep/Cargo.toml index 09d3d1289..fe7ee2941 100644 --- a/src/uu/sleep/Cargo.toml +++ b/src/uu/sleep/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_sleep" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "sleep ~ (uutils) pause for DURATION" @@ -15,9 +15,9 @@ edition = "2018" path = "src/sleep.rs" [dependencies] -getopts = "0.2.18" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary", features=["parse_time"] } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +clap = "2.33" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["parse_time"] } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "sleep" diff --git a/src/uu/sleep/src/sleep.rs b/src/uu/sleep/src/sleep.rs index 1097da886..5c1f06e5e 100644 --- a/src/uu/sleep/src/sleep.rs +++ b/src/uu/sleep/src/sleep.rs @@ -5,63 +5,62 @@ // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -extern crate getopts; - #[macro_use] extern crate uucore; use std::thread; use std::time::Duration; -static NAME: &str = "sleep"; +use clap::{App, Arg}; + static VERSION: &str = env!("CARGO_PKG_VERSION"); - -pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); - - let mut opts = getopts::Options::new(); - opts.optflag("h", "help", "display this help and exit"); - opts.optflag("V", "version", "output version information and exit"); - - let matches = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(f) => { - show_error!("{}", f); - return 1; - } - }; - - if matches.opt_present("help") { - let msg = format!( - "{0} {1} - -Usage: - {0} NUMBER[SUFFIX] -or - {0} OPTION - -Pause for NUMBER seconds. SUFFIX may be 's' for seconds (the default), +static ABOUT: &str = "Pause for NUMBER seconds."; +static LONG_HELP: &str = "Pause for NUMBER seconds. SUFFIX may be 's' for seconds (the default), 'm' for minutes, 'h' for hours or 'd' for days. Unlike most implementations that require NUMBER be an integer, here NUMBER may be an arbitrary floating point number. Given two or more arguments, pause for the amount of time -specified by the sum of their values.", - NAME, VERSION - ); - print!("{}", opts.usage(&msg)); - } else if matches.opt_present("version") { - println!("{} {}", NAME, VERSION); - } else if matches.free.is_empty() { - show_error!("missing an argument"); - show_error!("for help, try '{0} --help'", NAME); - return 1; - } else { - sleep(matches.free); +specified by the sum of their values."; + +mod options { + pub const NUMBER: &str = "NUMBER"; +} + +fn get_usage() -> String { + format!( + "{0} {1}[SUFFIX]... \n {0} OPTION", + executable!(), + options::NUMBER + ) +} + +pub fn uumain(args: impl uucore::Args) -> i32 { + let usage = get_usage(); + + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(&usage[..]) + .after_help(LONG_HELP) + .arg( + Arg::with_name(options::NUMBER) + .long(options::NUMBER) + .help("pause for NUMBER seconds") + .value_name(options::NUMBER) + .index(1) + .multiple(true) + .required(true), + ) + .get_matches_from(args); + + if let Some(values) = matches.values_of(options::NUMBER) { + let numbers = values.collect(); + sleep(numbers); } 0 } -fn sleep(args: Vec) { +fn sleep(args: Vec<&str>) { let sleep_dur = args.iter().fold( Duration::new(0, 0), diff --git a/src/uu/sort/Cargo.toml b/src/uu/sort/Cargo.toml index c9e643cf3..7a6f95c41 100644 --- a/src/uu/sort/Cargo.toml +++ b/src/uu/sort/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_sort" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "sort ~ (uutils) sort input lines" @@ -15,11 +15,13 @@ edition = "2018" path = "src/sort.rs" [dependencies] -getopts = "0.2.18" +rand = "0.7" +clap = "2.33" +twox-hash = "1.6.0" itertools = "0.8.0" semver = "0.9.0" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary", features=["fs"] } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["fs"] } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "sort" diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index dc4dc1d90..6c29ad98d 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -1,36 +1,56 @@ // * This file is part of the uutils coreutils package. // * // * (c) Michael Yin +// * (c) Robert Swinford // * // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. #![allow(dead_code)] // spell-checker:ignore (ToDO) outfile nondictionary - -extern crate getopts; -extern crate semver; - -extern crate itertools; #[macro_use] extern crate uucore; +use clap::{App, Arg}; use itertools::Itertools; +use rand::distributions::Alphanumeric; +use rand::{thread_rng, Rng}; use semver::Version; use std::cmp::Ordering; use std::collections::BinaryHeap; use std::fs::File; +use std::hash::{Hash, Hasher}; use std::io::{stdin, stdout, BufRead, BufReader, BufWriter, Lines, Read, Write}; use std::mem::replace; use std::path::Path; +use twox_hash::XxHash64; use uucore::fs::is_stdin_interactive; // for Iterator::dedup() static NAME: &str = "sort"; +static ABOUT: &str = "Display sorted concatenation of all FILE(s)."; static VERSION: &str = env!("CARGO_PKG_VERSION"); -const DECIMAL_PT: char = '.'; -const THOUSANDS_SEP: char = ','; +static OPT_HUMAN_NUMERIC_SORT: &str = "human-numeric-sort"; +static OPT_MONTH_SORT: &str = "month-sort"; +static OPT_NUMERIC_SORT: &str = "numeric-sort"; +static OPT_VERSION_SORT: &str = "version-sort"; +static OPT_DICTIONARY_ORDER: &str = "dictionary-order"; +static OPT_MERGE: &str = "merge"; +static OPT_CHECK: &str = "check"; +static OPT_IGNORE_CASE: &str = "ignore-case"; +static OPT_IGNORE_BLANKS: &str = "ignore-blanks"; +static OPT_OUTPUT: &str = "output"; +static OPT_REVERSE: &str = "reverse"; +static OPT_STABLE: &str = "stable"; +static OPT_UNIQUE: &str = "unique"; +static OPT_RANDOM: &str = "random-sort"; + +static ARG_FILES: &str = "files"; + +static DECIMAL_PT: char = '.'; +static THOUSANDS_SEP: char = ','; +#[derive(Eq, Ord, PartialEq, PartialOrd)] enum SortMode { Numeric, HumanNumeric, @@ -47,8 +67,10 @@ struct Settings { stable: bool, unique: bool, check: bool, + random: bool, compare_fns: Vec Ordering>, transform_fns: Vec String>, + salt: String, } impl Default for Settings { @@ -61,8 +83,10 @@ impl Default for Settings { stable: false, unique: false, check: false, + random: false, compare_fns: Vec::new(), transform_fns: Vec::new(), + salt: String::new(), } } } @@ -143,114 +167,159 @@ impl<'a> Iterator for FileMerger<'a> { } } -pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); - - let mut settings: Settings = Default::default(); - let mut opts = getopts::Options::new(); - - opts.optflag( - "d", - "dictionary-order", - "consider only blanks and alphanumeric characters", - ); - opts.optflag( - "f", - "ignore-case", - "fold lower case to upper case characters", - ); - opts.optflag( - "n", - "numeric-sort", - "compare according to string numerical value", - ); - opts.optflag( - "h", - "human-numeric-sort", - "compare according to human readable sizes, eg 1M > 100k", - ); - opts.optflag( - "M", - "month-sort", - "compare according to month name abbreviation", - ); - opts.optflag("r", "reverse", "reverse the output"); - opts.optflag("h", "help", "display this help and exit"); - opts.optflag("", "version", "output version information and exit"); - opts.optflag("m", "merge", "merge already sorted files; do not sort"); - opts.optopt( - "o", - "output", - "write output to FILENAME instead of stdout", - "FILENAME", - ); - opts.optflag( - "s", - "stable", - "stabilize sort by disabling last-resort comparison", - ); - opts.optflag("u", "unique", "output only the first of an equal run"); - opts.optflag( - "V", - "version-sort", - "Sort by SemVer version number, eg 1.12.2 > 1.1.2", - ); - opts.optflag("c", "check", "check for sorted input; do not sort"); - - let matches = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(f) => crash!(1, "Invalid options\n{}", f), - }; - if matches.opt_present("help") { - let msg = format!( - "{0} {1} - +fn get_usage() -> String { + format!( + "{0} {1} Usage: {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, VERSION - ); - print!("{}", opts.usage(&msg)); - return 0; - } + NAME, VERSION + ) +} - if matches.opt_present("version") { - println!("{} {}", NAME, VERSION); - return 0; - } +pub fn uumain(args: impl uucore::Args) -> i32 { + let args = args.collect_str(); + let usage = get_usage(); + let mut settings: Settings = Default::default(); - settings.mode = if matches.opt_present("numeric-sort") { - SortMode::Numeric - } else if matches.opt_present("human-numeric-sort") { + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(&usage[..]) + .arg( + Arg::with_name(OPT_HUMAN_NUMERIC_SORT) + .short("h") + .long(OPT_HUMAN_NUMERIC_SORT) + .help("compare according to human readable sizes, eg 1M > 100k"), + ) + .arg( + Arg::with_name(OPT_MONTH_SORT) + .short("M") + .long(OPT_MONTH_SORT) + .help("compare according to month name abbreviation"), + ) + .arg( + Arg::with_name(OPT_NUMERIC_SORT) + .short("n") + .long(OPT_NUMERIC_SORT) + .help("compare according to string numerical value"), + ) + .arg( + Arg::with_name(OPT_VERSION_SORT) + .short("V") + .long(OPT_VERSION_SORT) + .help("Sort by SemVer version number, eg 1.12.2 > 1.1.2"), + ) + .arg( + Arg::with_name(OPT_DICTIONARY_ORDER) + .short("d") + .long(OPT_DICTIONARY_ORDER) + .help("consider only blanks and alphanumeric characters"), + ) + .arg( + Arg::with_name(OPT_MERGE) + .short("m") + .long(OPT_MERGE) + .help("merge already sorted files; do not sort"), + ) + .arg( + Arg::with_name(OPT_CHECK) + .short("c") + .long(OPT_CHECK) + .help("check for sorted input; do not sort"), + ) + .arg( + Arg::with_name(OPT_IGNORE_CASE) + .short("f") + .long(OPT_IGNORE_CASE) + .help("fold lower case to upper case characters"), + ) + .arg( + Arg::with_name(OPT_IGNORE_BLANKS) + .short("b") + .long(OPT_IGNORE_BLANKS) + .help("ignore leading blanks when finding sort keys in each line"), + ) + .arg( + Arg::with_name(OPT_OUTPUT) + .short("o") + .long(OPT_OUTPUT) + .help("write output to FILENAME instead of stdout") + .takes_value(true) + .value_name("FILENAME"), + ) + .arg( + Arg::with_name(OPT_RANDOM) + .short("R") + .long(OPT_RANDOM) + .help("shuffle in random order"), + ) + .arg( + Arg::with_name(OPT_REVERSE) + .short("r") + .long(OPT_REVERSE) + .help("reverse the output"), + ) + .arg( + Arg::with_name(OPT_STABLE) + .short("s") + .long(OPT_STABLE) + .help("stabilize sort by disabling last-resort comparison"), + ) + .arg( + Arg::with_name(OPT_UNIQUE) + .short("u") + .long(OPT_UNIQUE) + .help("output only the first of an equal run"), + ) + .arg(Arg::with_name(ARG_FILES).multiple(true).takes_value(true)) + .get_matches_from(args); + + let mut files: Vec = matches + .values_of(ARG_FILES) + .map(|v| v.map(ToString::to_string).collect()) + .unwrap_or_default(); + + settings.mode = if matches.is_present(OPT_HUMAN_NUMERIC_SORT) { SortMode::HumanNumeric - } else if matches.opt_present("month-sort") { + } else if matches.is_present(OPT_MONTH_SORT) { SortMode::Month - } else if matches.opt_present("version-sort") { + } else if matches.is_present(OPT_NUMERIC_SORT) { + SortMode::Numeric + } else if matches.is_present(OPT_VERSION_SORT) { SortMode::Version } else { SortMode::Default }; - settings.merge = matches.opt_present("merge"); - settings.reverse = matches.opt_present("reverse"); - settings.outfile = matches.opt_str("output"); - settings.stable = matches.opt_present("stable"); - settings.unique = matches.opt_present("unique"); - settings.check = matches.opt_present("check"); - - if matches.opt_present("dictionary-order") { + if matches.is_present(OPT_DICTIONARY_ORDER) { settings.transform_fns.push(remove_nondictionary_chars); } - if matches.opt_present("ignore-case") { + + settings.merge = matches.is_present(OPT_MERGE); + settings.check = matches.is_present(OPT_CHECK); + + if matches.is_present(OPT_IGNORE_CASE) { settings.transform_fns.push(|s| s.to_uppercase()); } - let mut files = matches.free; + if matches.is_present(OPT_IGNORE_BLANKS) { + settings.transform_fns.push(|s| s.trim_start().to_string()); + } + + settings.outfile = matches.value_of(OPT_OUTPUT).map(String::from); + settings.reverse = matches.is_present(OPT_REVERSE); + settings.stable = matches.is_present(OPT_STABLE); + settings.unique = matches.is_present(OPT_UNIQUE); + + if matches.is_present(OPT_RANDOM) { + settings.random = matches.is_present(OPT_RANDOM); + settings.salt = get_rand_string(); + } + + //let mut files = matches.free; if files.is_empty() { /* if no file, default to stdin */ files.push("-".to_owned()); @@ -273,10 +342,10 @@ With no FILE, or when FILE is -, read standard input.", } } - exec(files, &settings) + exec(files, &mut settings) } -fn exec(files: Vec, settings: &Settings) -> i32 { +fn exec(files: Vec, settings: &mut Settings) -> i32 { let mut lines = Vec::new(); let mut file_merger = FileMerger::new(&settings); @@ -311,6 +380,13 @@ fn exec(files: Vec, settings: &Settings) -> i32 { } else { print_sorted(file_merger, &settings.outfile) } + } else if settings.unique && settings.mode == SortMode::Numeric { + print_sorted( + lines + .iter() + .dedup_by(|a, b| num_sort_dedup(a) == num_sort_dedup(b)), + &settings.outfile, + ) } else if settings.unique { print_sorted(lines.iter().dedup(), &settings.outfile) } else { @@ -379,7 +455,11 @@ fn compare_by(a: &str, b: &str, settings: &Settings) -> Ordering { }; for compare_fn in &settings.compare_fns { - let cmp = compare_fn(a, b); + let cmp: Ordering = if settings.random { + random_shuffle(a, b, settings.salt.clone()) + } else { + compare_fn(a, b) + }; if cmp != Ordering::Equal { if settings.reverse { return cmp.reverse(); @@ -391,36 +471,60 @@ fn compare_by(a: &str, b: &str, settings: &Settings) -> Ordering { Ordering::Equal } -/// Parse the beginning string into an f64, returning -inf instead of NaN on errors. -fn permissive_f64_parse(a: &str) -> f64 { - // Maybe should be split on non-digit, but then 10e100 won't parse properly. - // On the flip side, this will give NEG_INFINITY for "1,234", which might be OK - // because there's no way to handle both CSV and thousands separators without a new flag. - // GNU sort treats "1,234" as "1" in numeric, so maybe it's fine. - // GNU sort treats "NaN" as non-number in numeric, so it needs special care. - match a.split_whitespace().next() { - None => std::f64::NEG_INFINITY, - Some(sa) => match sa.parse::() { - Ok(a) if a.is_nan() => std::f64::NEG_INFINITY, - Ok(a) => a, - Err(_) => std::f64::NEG_INFINITY, - }, - } -} - fn default_compare(a: &str, b: &str) -> Ordering { a.cmp(b) } -/// Compares two floating point numbers, with errors being assumed to be -inf. -/// Stops coercing at the first whitespace char, so 1e2 will parse as 100 but -/// 1,000 will parse as -inf. +fn get_leading_number(a: &str) -> &str { + let mut s = ""; + for c in a.chars() { + if !c.is_numeric() && !c.eq(&'-') && !c.eq(&' ') && !c.eq(&'.') && !c.eq(&',') { + s = a.trim().split(c).next().unwrap(); + break; + } + s = a.trim(); + } + return s; +} + +// Matches GNU behavior, see: +// https://www.gnu.org/software/coreutils/manual/html_node/sort-invocation.html +// Specifically *not* the same as sort -n | uniq +fn num_sort_dedup(a: &str) -> &str { + // Empty lines are dumped + if a.is_empty() { + return "0"; + // And lines that don't begin numerically are dumped + } else if !a.trim().chars().nth(0).unwrap_or('\0').is_numeric() { + return "0"; + } else { + // Prepare lines for comparison of only the numerical leading numbers + return get_leading_number(a); + }; +} + +/// Parse the beginning string into an f64, returning -inf instead of NaN on errors. +fn permissive_f64_parse(a: &str) -> f64 { + // GNU sort treats "NaN" as non-number in numeric, so it needs special care. + match a.parse::() { + Ok(a) if a.is_nan() => std::f64::NEG_INFINITY, + Ok(a) => a, + Err(_) => std::f64::NEG_INFINITY, + } +} + +/// Compares two floats, with errors and non-numerics assumed to be -inf. +/// Stops coercing at the first non-numeric char. fn numeric_compare(a: &str, b: &str) -> Ordering { #![allow(clippy::comparison_chain)] - let fa = permissive_f64_parse(a); - let fb = permissive_f64_parse(b); - // f64::cmp isn't implemented because NaN messes with it - // but we sidestep that with permissive_f64_parse so just fake it + + let sa = get_leading_number(a); + let sb = get_leading_number(b); + + let fa = permissive_f64_parse(sa); + let fb = permissive_f64_parse(sb); + + // f64::cmp isn't implemented (due to NaN issues); implement directly instead if fa > fb { Ordering::Greater } else if fa < fb { @@ -431,13 +535,10 @@ fn numeric_compare(a: &str, b: &str) -> Ordering { } fn human_numeric_convert(a: &str) -> f64 { - let int_str: String = a.chars().take_while(|c| c.is_numeric()).collect(); - let suffix = a.chars().find(|c| !c.is_numeric()); - let int_part = match int_str.parse::() { - Ok(i) => i, - Err(_) => -1f64, - } as f64; - let suffix: f64 = match suffix.unwrap_or('\0') { + let int_str = get_leading_number(a); + let (_, s) = a.split_at(int_str.len()); + let int_part = permissive_f64_parse(int_str); + let suffix: f64 = match s.parse().unwrap_or('\0') { 'K' => 1000f64, 'M' => 1E6, 'G' => 1E9, @@ -464,6 +565,30 @@ fn human_numeric_size_compare(a: &str, b: &str) -> Ordering { } } +fn random_shuffle(a: &str, b: &str, salt: String) -> Ordering { + #![allow(clippy::comparison_chain)] + let salt_slice = salt.as_str(); + + let da = hash(&[a, salt_slice].concat()); + let db = hash(&[b, salt_slice].concat()); + + da.cmp(&db) +} + +fn get_rand_string() -> String { + thread_rng() + .sample_iter(&Alphanumeric) + .take(16) + .map(char::from) + .collect::() +} + +fn hash(t: &T) -> u64 { + let mut s: XxHash64 = Default::default(); + t.hash(&mut s); + s.finish() +} + #[derive(Eq, Ord, PartialEq, PartialOrd)] enum Month { Unknown, @@ -550,10 +675,7 @@ where for line in iter { let str = format!("{}\n", line); - if let Err(e) = file.write_all(str.as_bytes()) { - show_error!("sort: {0}", e.to_string()); - panic!("Write failed"); - } + crash_if_err!(1, file.write_all(str.as_bytes())) } } @@ -572,3 +694,65 @@ fn open(path: &str) -> Option<(Box, bool)> { } } } + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_default_compare() { + let a = "your own"; + let b = "your place"; + + assert_eq!(Ordering::Less, default_compare(a, b)); + } + + #[test] + fn test_numeric_compare1() { + let a = "149:7"; + let b = "150:5"; + + assert_eq!(Ordering::Less, numeric_compare(a, b)); + } + + #[test] + fn test_numeric_compare2() { + let a = "-1.02"; + let b = "1"; + + assert_eq!(Ordering::Less, numeric_compare(a, b)); + } + + #[test] + fn test_human_numeric_compare() { + let a = "300K"; + let b = "1M"; + + assert_eq!(Ordering::Less, human_numeric_size_compare(a, b)); + } + + #[test] + fn test_month_compare() { + let a = "JaN"; + let b = "OCt"; + + assert_eq!(Ordering::Less, month_compare(a, b)); + } + #[test] + fn test_version_compare() { + let a = "1.2.3-alpha2"; + let b = "1.4.0"; + + assert_eq!(Ordering::Less, version_compare(a, b)); + } + + #[test] + fn test_random_compare() { + let a = "9"; + let b = "9"; + let c = get_rand_string(); + + assert_eq!(Ordering::Equal, random_shuffle(a, b, c)); + } +} diff --git a/src/uu/split/Cargo.toml b/src/uu/split/Cargo.toml index 90563345a..056fbe034 100644 --- a/src/uu/split/Cargo.toml +++ b/src/uu/split/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_split" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "split ~ (uutils) split input into output files" @@ -15,9 +15,9 @@ edition = "2018" path = "src/split.rs" [dependencies] -getopts = "0.2.18" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +clap = "2.33" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "split" diff --git a/src/uu/split/src/platform/mod.rs b/src/uu/split/src/platform/mod.rs new file mode 100644 index 000000000..020c01a4a --- /dev/null +++ b/src/uu/split/src/platform/mod.rs @@ -0,0 +1,11 @@ +#[cfg(unix)] +pub use self::unix::instantiate_current_writer; + +#[cfg(windows)] +pub use self::windows::instantiate_current_writer; + +#[cfg(unix)] +mod unix; + +#[cfg(windows)] +mod windows; diff --git a/src/uu/split/src/platform/unix.rs b/src/uu/split/src/platform/unix.rs new file mode 100644 index 000000000..20d9d637b --- /dev/null +++ b/src/uu/split/src/platform/unix.rs @@ -0,0 +1,123 @@ +use std::env; +use std::io::Write; +use std::io::{BufWriter, Result}; +use std::process::{Child, Command, Stdio}; +/// A writer that writes to a shell_process' stdin +/// +/// We use a shell process (not directly calling a sub-process) so we can forward the name of the +/// corresponding output file (xaa, xab, xac… ). This is the way it was implemented in GNU split. +struct FilterWriter { + /// Running shell process + shell_process: Child, +} + +impl Write for FilterWriter { + fn write(&mut self, buf: &[u8]) -> Result { + self.shell_process + .stdin + .as_mut() + .expect("failed to get shell stdin") + .write(buf) + } + fn flush(&mut self) -> Result<()> { + self.shell_process + .stdin + .as_mut() + .expect("failed to get shell stdin") + .flush() + } +} + +/// Have an environment variable set at a value during this lifetime +struct WithEnvVarSet { + /// Env var key + _previous_var_key: String, + /// Previous value set to this key + _previous_var_value: std::result::Result, +} +impl WithEnvVarSet { + /// Save previous value assigned to key, set key=value + fn new(key: &str, value: &str) -> WithEnvVarSet { + let previous_env_value = env::var(key); + env::set_var(key, value); + WithEnvVarSet { + _previous_var_key: String::from(key), + _previous_var_value: previous_env_value, + } + } +} + +impl Drop for WithEnvVarSet { + /// Restore previous value now that this is being dropped by context + fn drop(&mut self) { + if let Ok(ref prev_value) = self._previous_var_value { + env::set_var(&self._previous_var_key, &prev_value); + } else { + env::remove_var(&self._previous_var_key) + } + } +} +impl FilterWriter { + /// Create a new filter running a command with $FILE pointing at the output name + /// + /// #Arguments + /// + /// * `command` - The shell command to execute + /// * `filepath` - Path of the output file (forwarded to command as $FILE) + fn new(command: &str, filepath: &str) -> FilterWriter { + // set $FILE, save previous value (if there was one) + let _with_env_var_set = WithEnvVarSet::new("FILE", &filepath); + + let shell_process = + Command::new(env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_owned())) + .arg("-c") + .arg(command) + .stdin(Stdio::piped()) + .spawn() + .expect("Couldn't spawn filter command"); + + FilterWriter { shell_process } + } +} + +impl Drop for FilterWriter { + /// flush stdin, close it and wait on `shell_process` before dropping self + fn drop(&mut self) { + { + // close stdin by dropping it + let _stdin = self.shell_process.stdin.as_mut(); + } + let exit_status = self + .shell_process + .wait() + .expect("Couldn't wait for child process"); + if let Some(return_code) = exit_status.code() { + if return_code != 0 { + crash!(1, "Shell process returned {}", return_code); + } + } else { + crash!(1, "Shell process terminated by signal") + } + } +} + +/// Instantiate either a file writer or a "write to shell process's stdin" writer +pub fn instantiate_current_writer( + filter: &Option, + filename: &str, +) -> BufWriter> { + match filter { + None => BufWriter::new(Box::new( + // write to the next file + std::fs::OpenOptions::new() + .write(true) + .create(true) + .open(std::path::Path::new(&filename)) + .unwrap(), + ) as Box), + Some(ref filter_command) => BufWriter::new(Box::new( + // spawn a shell command and write to it + FilterWriter::new(&filter_command, &filename), + ) as Box), + } +} diff --git a/src/uu/split/src/platform/windows.rs b/src/uu/split/src/platform/windows.rs new file mode 100644 index 000000000..e67518f2a --- /dev/null +++ b/src/uu/split/src/platform/windows.rs @@ -0,0 +1,19 @@ +use std::io::BufWriter; +use std::io::Write; +/// Get a file writer +/// +/// Unlike the unix version of this function, this _always_ returns +/// a file writer +pub fn instantiate_current_writer( + _filter: &Option, + filename: &str, +) -> BufWriter> { + BufWriter::new(Box::new( + // write to the next file + std::fs::OpenOptions::new() + .write(true) + .create(true) + .open(std::path::Path::new(&filename)) + .unwrap(), + ) as Box) +} diff --git a/src/uu/split/src/split.rs b/src/uu/split/src/split.rs index ee6b8e9fd..4f80e25a3 100644 --- a/src/uu/split/src/split.rs +++ b/src/uu/split/src/split.rs @@ -7,86 +7,132 @@ // spell-checker:ignore (ToDO) PREFIXaa -extern crate getopts; - #[macro_use] extern crate uucore; +mod platform; + +use clap::{App, Arg}; use std::char; -use std::fs::{File, OpenOptions}; +use std::env; +use std::fs::File; use std::io::{stdin, stdout, BufRead, BufReader, BufWriter, Read, Write}; use std::path::Path; static NAME: &str = "split"; static VERSION: &str = env!("CARGO_PKG_VERSION"); -pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); +static OPT_BYTES: &str = "bytes"; +static OPT_LINE_BYTES: &str = "line-bytes"; +static OPT_LINES: &str = "lines"; +static OPT_ADDITIONAL_SUFFIX: &str = "additional-suffix"; +static OPT_FILTER: &str = "filter"; +static OPT_NUMERIC_SUFFIXES: &str = "numeric-suffixes"; +static OPT_SUFFIX_LENGTH: &str = "suffix-length"; +static OPT_DEFAULT_SUFFIX_LENGTH: usize = 2; +static OPT_VERBOSE: &str = "verbose"; - let mut opts = getopts::Options::new(); +static ARG_INPUT: &str = "input"; +static ARG_PREFIX: &str = "prefix"; - opts.optopt( - "a", - "suffix-length", - "use suffixes of length N (default 2)", - "N", - ); - opts.optopt("b", "bytes", "put SIZE bytes per output file", "SIZE"); - opts.optopt( - "C", - "line-bytes", - "put at most SIZE bytes of lines per output file", - "SIZE", - ); - opts.optflag( - "d", - "numeric-suffixes", - "use numeric suffixes instead of alphabetic", - ); - opts.optopt( - "", - "additional-suffix", - "additional suffix to append to output file names", - "SUFFIX", - ); - opts.optopt("l", "lines", "put NUMBER lines per output file", "NUMBER"); - opts.optflag( - "", - "verbose", - "print a diagnostic just before each output file is opened", - ); - opts.optflag("h", "help", "display help and exit"); - opts.optflag("V", "version", "output version information and exit"); - - let matches = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(f) => crash!(1, "{}", f), - }; - - if matches.opt_present("h") { - let msg = format!( - "{0} {1} - -Usage: - {0} [OPTION]... [INPUT [PREFIX]] +fn get_usage() -> String { + format!("{0} [OPTION]... [INPUT [PREFIX]]", NAME) +} +fn get_long_usage() -> String { + format!( + "Usage: + {0} 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.", - NAME, VERSION - ); + get_usage() + ) +} - println!( - "{}\nSIZE may have a multiplier suffix: b for 512, k for 1K, m for 1 Meg.", - opts.usage(&msg) - ); - return 0; - } +pub fn uumain(args: impl uucore::Args) -> i32 { + let usage = get_usage(); + let long_usage = get_long_usage(); + let default_suffix_length_str = OPT_DEFAULT_SUFFIX_LENGTH.to_string(); - if matches.opt_present("V") { - println!("{} {}", NAME, VERSION); - return 0; - } + let matches = App::new(executable!()) + .version(VERSION) + .about("Create output files containing consecutive or interleaved sections of input") + .usage(&usage[..]) + .after_help(&long_usage[..]) + // strategy (mutually exclusive) + .arg( + Arg::with_name(OPT_BYTES) + .short("b") + .long(OPT_BYTES) + .takes_value(true) + .default_value("2") + .help("use suffixes of length N (default 2)"), + ) + .arg( + Arg::with_name(OPT_LINE_BYTES) + .short("C") + .long(OPT_LINE_BYTES) + .takes_value(true) + .default_value("2") + .help("put at most SIZE bytes of lines per output file"), + ) + .arg( + Arg::with_name(OPT_LINES) + .short("l") + .long(OPT_LINES) + .takes_value(true) + .default_value("1000") + .help("write to shell COMMAND file name is $FILE (Currently not implemented for Windows)"), + ) + // rest of the arguments + .arg( + Arg::with_name(OPT_ADDITIONAL_SUFFIX) + .long(OPT_ADDITIONAL_SUFFIX) + .takes_value(true) + .default_value("") + .help("additional suffix to append to output file names"), + ) + .arg( + Arg::with_name(OPT_FILTER) + .long(OPT_FILTER) + .takes_value(true) + .help("write to shell COMMAND file name is $FILE (Currently not implemented for Windows)"), + ) + .arg( + Arg::with_name(OPT_NUMERIC_SUFFIXES) + .short("d") + .long(OPT_NUMERIC_SUFFIXES) + .takes_value(true) + .default_value("0") + .help("use numeric suffixes instead of alphabetic"), + ) + .arg( + Arg::with_name(OPT_SUFFIX_LENGTH) + .short("a") + .long(OPT_SUFFIX_LENGTH) + .takes_value(true) + .default_value(default_suffix_length_str.as_str()) + .help("use suffixes of length N (default 2)"), + ) + .arg( + Arg::with_name(OPT_VERBOSE) + .long(OPT_VERBOSE) + .help("print a diagnostic just before each output file is opened"), + ) + .arg( + Arg::with_name(ARG_INPUT) + .takes_value(true) + .default_value("-") + .index(1) + ) + .arg( + Arg::with_name(ARG_PREFIX) + .takes_value(true) + .default_value("x") + .index(2) + ) + .get_matches_from(args); let mut settings = Settings { prefix: "".to_owned(), @@ -94,51 +140,62 @@ size is 1000, and default PREFIX is 'x'. With no INPUT, or when INPUT is suffix_length: 0, additional_suffix: "".to_owned(), input: "".to_owned(), + filter: None, strategy: "".to_owned(), strategy_param: "".to_owned(), verbose: false, }; - settings.numeric_suffix = matches.opt_present("d"); + settings.suffix_length = matches + .value_of(OPT_SUFFIX_LENGTH) + .unwrap() + .parse() + .unwrap_or_else(|_| panic!("Invalid number for {}", OPT_SUFFIX_LENGTH)); - settings.suffix_length = match matches.opt_str("a") { - Some(n) => match n.parse() { - Ok(m) => m, - Err(e) => crash!(1, "cannot parse num: {}", e), - }, - None => 2, - }; + settings.numeric_suffix = matches.occurrences_of(OPT_NUMERIC_SUFFIXES) > 0; + settings.additional_suffix = matches.value_of(OPT_ADDITIONAL_SUFFIX).unwrap().to_owned(); - settings.additional_suffix = if matches.opt_present("additional-suffix") { - matches.opt_str("additional-suffix").unwrap() - } else { - "".to_owned() - }; + settings.verbose = matches.occurrences_of("verbose") > 0; + // check that the user is not specifying more than one strategy + // note: right now, this exact behaviour cannot be handled by ArgGroup since ArgGroup + // considers a default value Arg as "defined" + let explicit_strategies = + vec![OPT_LINE_BYTES, OPT_LINES, OPT_BYTES] + .into_iter() + .fold(0, |count, strat| { + if matches.occurrences_of(strat) > 0 { + count + 1 + } else { + count + } + }); + if explicit_strategies > 1 { + crash!(1, "cannot split in more than one way"); + } - settings.verbose = matches.opt_present("verbose"); - - settings.strategy = "l".to_owned(); - settings.strategy_param = "1000".to_owned(); - let strategies = vec!["b", "C", "l"]; - for e in &strategies { - if let Some(a) = matches.opt_str(*e) { - if settings.strategy == "l" { - settings.strategy = (*e).to_owned(); - settings.strategy_param = a; - } else { - crash!(1, "{}: cannot split in more than one way", NAME) - } + // default strategy (if no strategy is passed, use this one) + settings.strategy = String::from(OPT_LINES); + settings.strategy_param = matches.value_of(OPT_LINES).unwrap().to_owned(); + // take any (other) defined strategy + for strat in vec![OPT_LINE_BYTES, OPT_BYTES].into_iter() { + if matches.occurrences_of(strat) > 0 { + settings.strategy = String::from(strat); + settings.strategy_param = matches.value_of(strat).unwrap().to_owned(); } } - let mut v = matches.free.iter(); - let (input, prefix) = match (v.next(), v.next()) { - (Some(a), None) => (a.to_owned(), "x".to_owned()), - (Some(a), Some(b)) => (a.clone(), b.clone()), - (None, _) => ("-".to_owned(), "x".to_owned()), - }; - settings.input = input; - settings.prefix = prefix; + settings.input = matches.value_of(ARG_INPUT).unwrap().to_owned(); + settings.prefix = matches.value_of(ARG_PREFIX).unwrap().to_owned(); + + if matches.occurrences_of(OPT_FILTER) > 0 { + if cfg!(windows) { + // see https://github.com/rust-lang/rust/issues/29494 + show_error!("{} is currently not supported in this platform", OPT_FILTER); + exit!(-1); + } else { + settings.filter = Some(matches.value_of(OPT_FILTER).unwrap().to_owned()); + } + } split(&settings) } @@ -149,6 +206,8 @@ struct Settings { suffix_length: usize, additional_suffix: String, input: String, + /// When supplied, a shell command to output to instead of xaa, xab … + filter: Option, strategy: String, strategy_param: String, verbose: bool, @@ -305,9 +364,9 @@ fn split(settings: &Settings) -> i32 { Box::new(r) as Box }); - let mut splitter: Box = match settings.strategy.as_ref() { - "l" => Box::new(LineSplitter::new(settings)), - "b" | "C" => Box::new(ByteSplitter::new(settings)), + 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), }; @@ -325,7 +384,6 @@ fn split(settings: &Settings) -> i32 { _ => {} } } - if control.request_new_file { let mut filename = settings.prefix.clone(); filename.push_str( @@ -338,17 +396,9 @@ fn split(settings: &Settings) -> i32 { ); filename.push_str(settings.additional_suffix.as_ref()); - if fileno != 0 { - crash_if_err!(1, writer.flush()); - } + crash_if_err!(1, writer.flush()); fileno += 1; - writer = BufWriter::new(Box::new( - OpenOptions::new() - .write(true) - .create(true) - .open(Path::new(&filename)) - .unwrap(), - ) as Box); + writer = platform::instantiate_current_writer(&settings.filter, filename.as_str()); control.request_new_file = false; if settings.verbose { println!("creating file '{}'", filename); diff --git a/src/uu/stat/Cargo.toml b/src/uu/stat/Cargo.toml index ddc280f5e..96bf63ffe 100644 --- a/src/uu/stat/Cargo.toml +++ b/src/uu/stat/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_stat" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "stat ~ (uutils) display FILE status" @@ -15,10 +15,10 @@ edition = "2018" path = "src/stat.rs" [dependencies] -getopts = "0.2.18" +clap = "2.33" time = "0.1.40" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary", features=["entries", "libc"] } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["entries", "libc"] } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "stat" diff --git a/src/uu/stat/src/fsext.rs b/src/uu/stat/src/fsext.rs index 11c8f8095..d90099892 100644 --- a/src/uu/stat/src/fsext.rs +++ b/src/uu/stat/src/fsext.rs @@ -149,7 +149,7 @@ use std::path::Path; #[cfg(any( target_os = "linux", - target_os = "macos", + target_vendor = "apple", target_os = "android", target_os = "freebsd" ))] @@ -165,7 +165,7 @@ use uucore::libc::statvfs as Sstatfs; #[cfg(any( target_os = "linux", - target_os = "macos", + target_vendor = "apple", target_os = "android", target_os = "freebsd" ))] @@ -211,11 +211,11 @@ impl FsMeta for Sstatfs { fn free_fnodes(&self) -> u64 { self.f_ffree as u64 } - #[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))] + #[cfg(any(target_os = "linux", target_vendor = "apple", target_os = "freebsd"))] fn fs_type(&self) -> i64 { self.f_type as i64 } - #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "freebsd")))] + #[cfg(not(any(target_os = "linux", target_vendor = "apple", target_os = "freebsd")))] fn fs_type(&self) -> i64 { // FIXME: statvfs doesn't have an equivalent, so we need to do something else unimplemented!() @@ -225,12 +225,12 @@ impl FsMeta for Sstatfs { fn iosize(&self) -> u64 { self.f_frsize as u64 } - #[cfg(any(target_os = "macos", target_os = "freebsd"))] + #[cfg(any(target_vendor = "apple", target_os = "freebsd"))] fn iosize(&self) -> u64 { self.f_iosize as u64 } // XXX: dunno if this is right - #[cfg(not(any(target_os = "macos", target_os = "freebsd", target_os = "linux")))] + #[cfg(not(any(target_vendor = "apple", target_os = "freebsd", target_os = "linux")))] fn iosize(&self) -> u64 { self.f_bsize as u64 } @@ -241,13 +241,13 @@ impl FsMeta for Sstatfs { // // Solaris, Irix and POSIX have a system call statvfs(2) that returns a // struct statvfs, containing an unsigned long f_fsid - #[cfg(any(target_os = "macos", target_os = "freebsd", target_os = "linux"))] + #[cfg(any(target_vendor = "apple", target_os = "freebsd", target_os = "linux"))] fn fsid(&self) -> u64 { let f_fsid: &[u32; 2] = unsafe { &*(&self.f_fsid as *const uucore::libc::fsid_t as *const [u32; 2]) }; (u64::from(f_fsid[0])) << 32 | u64::from(f_fsid[1]) } - #[cfg(not(any(target_os = "macos", target_os = "freebsd", target_os = "linux")))] + #[cfg(not(any(target_vendor = "apple", target_os = "freebsd", target_os = "linux")))] fn fsid(&self) -> u64 { self.f_fsid as u64 } @@ -256,7 +256,7 @@ impl FsMeta for Sstatfs { fn namelen(&self) -> u64 { self.f_namelen as u64 } - #[cfg(target_os = "macos")] + #[cfg(target_vendor = "apple")] fn namelen(&self) -> u64 { 1024 } @@ -265,7 +265,7 @@ impl FsMeta for Sstatfs { self.f_namemax as u64 } // XXX: should everything just use statvfs? - #[cfg(not(any(target_os = "macos", target_os = "freebsd", target_os = "linux")))] + #[cfg(not(any(target_vendor = "apple", target_os = "freebsd", target_os = "linux")))] fn namelen(&self) -> u64 { self.f_namemax as u64 } diff --git a/src/uu/stat/src/stat.rs b/src/uu/stat/src/stat.rs index 859043d7e..5216fb293 100644 --- a/src/uu/stat/src/stat.rs +++ b/src/uu/stat/src/stat.rs @@ -7,9 +7,6 @@ // spell-checker:ignore (ToDO) mtab fsext showfs otype fmtstr prec ftype blocksize nlink rdev fnodes fsid namelen blksize inodes fstype iosize statfs gnulib NBLOCKSIZE -extern crate getopts; -use getopts::Options; - #[macro_use] mod fsext; pub use crate::fsext::*; @@ -18,6 +15,7 @@ pub use crate::fsext::*; extern crate uucore; use uucore::entries; +use clap::{App, Arg, ArgMatches}; use std::borrow::Cow; use std::convert::AsRef; use std::fs::File; @@ -86,9 +84,19 @@ macro_rules! print_adjusted { }; } -static NAME: &str = "stat"; +static ABOUT: &str = "Display file or file system status."; static VERSION: &str = env!("CARGO_PKG_VERSION"); +pub mod options { + pub static DEREFERENCE: &str = "dereference"; + pub static FILE_SYSTEM: &str = "file-system"; + pub static FORMAT: &str = "format"; + pub static PRINTF: &str = "printf"; + pub static TERSE: &str = "terse"; +} + +static ARG_FILES: &str = "files"; + const MOUNT_INFO: &str = "/etc/mtab"; pub const F_ALTER: u8 = 1; pub const F_ZERO: u8 = 1 << 1; @@ -332,7 +340,6 @@ impl Stater { let mut tokens = Vec::new(); let bound = fmtstr.len(); let chars = fmtstr.chars().collect::>(); - let mut i = 0_usize; while i < bound { match chars[i] { @@ -453,16 +460,23 @@ impl Stater { Ok(tokens) } - fn new(matches: getopts::Matches) -> Result { - let fmtstr = if matches.opt_present("printf") { - matches.opt_str("printf").expect("Invalid format string") + fn new(matches: ArgMatches) -> Result { + let files: Vec = matches + .values_of(ARG_FILES) + .map(|v| v.map(ToString::to_string).collect()) + .unwrap_or_default(); + + let fmtstr = if matches.is_present(options::PRINTF) { + matches + .value_of(options::PRINTF) + .expect("Invalid format string") } else { - matches.opt_str("format").unwrap_or_else(|| "".to_owned()) + matches.value_of(options::FORMAT).unwrap_or("") }; - let use_printf = matches.opt_present("printf"); - let terse = matches.opt_present("terse"); - let showfs = matches.opt_present("file-system"); + let use_printf = matches.is_present(options::PRINTF); + let terse = matches.is_present(options::TERSE); + let showfs = matches.is_present(options::FILE_SYSTEM); let default_tokens = if fmtstr.is_empty() { Stater::generate_tokens(&Stater::default_fmt(showfs, terse, false), use_printf).unwrap() @@ -491,10 +505,10 @@ impl Stater { }; Ok(Stater { - follow: matches.opt_present("dereference"), + follow: matches.is_present(options::DEREFERENCE), showfs, from_user: !fmtstr.is_empty(), - files: matches.free, + files, default_tokens, default_dev_tokens, mount_list, @@ -873,76 +887,13 @@ impl Stater { } } -pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); - - let mut opts = Options::new(); - - opts.optflag("h", "help", "display this help and exit"); - opts.optflag("", "version", "output version information and exit"); - - opts.optflag("L", "dereference", "follow links"); - opts.optflag( - "f", - "file-system", - "display file system status instead of file status", - ); - opts.optflag("t", "terse", "print the information in terse form"); - - // Omit the unused description as they are too long - opts.optopt("c", "format", "", "FORMAT"); - opts.optopt("", "printf", "", "FORMAT"); - - let matches = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(f) => { - show_usage_error!("{}", f); - return 1; - } - }; - - if matches.opt_present("help") { - return help(); - } else if matches.opt_present("version") { - return version(); - } - - if matches.free.is_empty() { - show_usage_error!("missing operand"); - return 1; - } - - match Stater::new(matches) { - Ok(stater) => stater.exec(), - Err(e) => { - show_info!("{}", e); - 1 - } - } +fn get_usage() -> String { + format!("{0} [OPTION]... FILE...", executable!()) } -fn version() -> i32 { - println!("{} {}", NAME, VERSION); - 0 -} - -fn help() -> i32 { - let msg = format!( - r#"Usage: {} [OPTION]... FILE... -Display file or file system status. - -Mandatory arguments to long options are mandatory for short options too. - -L, --dereference follow links - -f, --file-system display file system status instead of file status - -c --format=FORMAT use the specified FORMAT instead of the default; - output a newline after each use of FORMAT - --printf=FORMAT like --format, but interpret backslash escapes, - and do not output a mandatory trailing newline; - if you want a newline, include \n in FORMAT - -t, --terse print the information in terse form - --help display this help and exit - --version output version information and exit - +fn get_long_usage() -> String { + String::from( + " The valid format sequences for files (without --file-system): %a access rights in octal (note '#' and '0' printf flags) @@ -993,9 +944,71 @@ Valid format sequences for file systems: NOTE: your shell may have its own version of stat, which usually supersedes the version described here. Please refer to your shell's documentation -for details about the options it supports."#, - NAME - ); - println!("{}", msg); - 0 +for details about the options it supports. +", + ) +} + +pub fn uumain(args: impl uucore::Args) -> i32 { + let usage = get_usage(); + let long_usage = get_long_usage(); + + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(&usage[..]) + .after_help(&long_usage[..]) + .arg( + Arg::with_name(options::DEREFERENCE) + .short("L") + .long(options::DEREFERENCE) + .help("follow links"), + ) + .arg( + Arg::with_name(options::FILE_SYSTEM) + .short("f") + .long(options::FILE_SYSTEM) + .help("display file system status instead of file status"), + ) + .arg( + Arg::with_name(options::TERSE) + .short("t") + .long(options::TERSE) + .help("print the information in terse form"), + ) + .arg( + Arg::with_name(options::FORMAT) + .short("c") + .long(options::FORMAT) + .help( + "use the specified FORMAT instead of the default; + output a newline after each use of FORMAT", + ) + .value_name("FORMAT"), + ) + .arg( + Arg::with_name(options::PRINTF) + .long(options::PRINTF) + .value_name("FORMAT") + .help( + "like --format, but interpret backslash escapes, + and do not output a mandatory trailing newline; + if you want a newline, include \n in FORMAT", + ), + ) + .arg( + Arg::with_name(ARG_FILES) + .multiple(true) + .takes_value(true) + .min_values(1), + ) + .get_matches_from(args); + + match Stater::new(matches) { + Ok(stater) => stater.exec(), + Err(e) => { + show_info!("{}", e); + 1 + } + } } diff --git a/src/uu/stdbuf/Cargo.toml b/src/uu/stdbuf/Cargo.toml index 9da2e59c7..22ce4de6a 100644 --- a/src/uu/stdbuf/Cargo.toml +++ b/src/uu/stdbuf/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_stdbuf" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "stdbuf ~ (uutils) run COMMAND with modified standard stream buffering" @@ -17,11 +17,11 @@ path = "src/stdbuf.rs" [dependencies] getopts = "0.2.18" tempfile = "3.1" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [build-dependencies] -libstdbuf = { version="0.0.1", package="uu_stdbuf_libstdbuf", path="src/libstdbuf" } +libstdbuf = { version="0.0.6", package="uu_stdbuf_libstdbuf", path="src/libstdbuf" } [[bin]] name = "stdbuf" diff --git a/src/uu/stdbuf/build.rs b/src/uu/stdbuf/build.rs index c005072d9..b14d503cf 100644 --- a/src/uu/stdbuf/build.rs +++ b/src/uu/stdbuf/build.rs @@ -4,12 +4,12 @@ use std::env; use std::fs; use std::path::Path; -#[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "windows")))] +#[cfg(not(any(target_vendor = "apple", target_os = "windows")))] mod platform { pub const DYLIB_EXT: &str = ".so"; } -#[cfg(any(target_os = "macos", target_os = "ios"))] +#[cfg(any(target_vendor = "apple"))] mod platform { pub const DYLIB_EXT: &str = ".dylib"; } diff --git a/src/uu/stdbuf/src/libstdbuf/Cargo.toml b/src/uu/stdbuf/src/libstdbuf/Cargo.toml index 0db28c1c4..86eb09d46 100644 --- a/src/uu/stdbuf/src/libstdbuf/Cargo.toml +++ b/src/uu/stdbuf/src/libstdbuf/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_stdbuf_libstdbuf" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "stdbuf/libstdbuf ~ (uutils); dynamic library required for stdbuf" @@ -17,10 +17,10 @@ path = "src/libstdbuf.rs" crate-type = ["cdylib", "rlib"] # XXX: note: the rlib is just to prevent Cargo from spitting out a warning [dependencies] -cpp = "0.4" +cpp = "0.5" libc = "0.2" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../../../uucore_procs" } [build-dependencies] cpp_build = "0.4" diff --git a/src/uu/stdbuf/src/libstdbuf/build.rs b/src/uu/stdbuf/src/libstdbuf/build.rs index 95f55ed94..fc8ddeac8 100644 --- a/src/uu/stdbuf/src/libstdbuf/build.rs +++ b/src/uu/stdbuf/src/libstdbuf/build.rs @@ -1,7 +1,5 @@ // spell-checker:ignore (ToDO) libstdbuf -extern crate cpp_build; - use cpp_build::Config; fn main() { diff --git a/src/uu/stdbuf/src/libstdbuf/src/libstdbuf.rs b/src/uu/stdbuf/src/libstdbuf/src/libstdbuf.rs index 094c18ba6..fa36d4ab5 100644 --- a/src/uu/stdbuf/src/libstdbuf/src/libstdbuf.rs +++ b/src/uu/stdbuf/src/libstdbuf/src/libstdbuf.rs @@ -2,7 +2,6 @@ #[macro_use] extern crate cpp; -extern crate libc; #[macro_use] extern crate uucore; diff --git a/src/uu/stdbuf/src/stdbuf.rs b/src/uu/stdbuf/src/stdbuf.rs index dcae42d99..67ed9a838 100644 --- a/src/uu/stdbuf/src/stdbuf.rs +++ b/src/uu/stdbuf/src/stdbuf.rs @@ -7,9 +7,6 @@ // spell-checker:ignore (ToDO) tempdir dyld dylib dragonflybsd optgrps libstdbuf -extern crate getopts; -extern crate tempfile; - #[macro_use] extern crate uucore; @@ -60,7 +57,7 @@ fn preload_strings() -> (&'static str, &'static str) { ("LD_PRELOAD", "so") } -#[cfg(target_os = "macos")] +#[cfg(target_vendor = "apple")] fn preload_strings() -> (&'static str, &'static str) { ("DYLD_LIBRARY_PATH", "dylib") } @@ -70,7 +67,7 @@ fn preload_strings() -> (&'static str, &'static str) { target_os = "freebsd", target_os = "netbsd", target_os = "dragonflybsd", - target_os = "macos" + target_vendor = "apple" )))] fn preload_strings() -> (&'static str, &'static str) { crash!(1, "Command not supported for this operating system!") diff --git a/src/uu/sum/Cargo.toml b/src/uu/sum/Cargo.toml index 2216e905e..64b6d3de9 100644 --- a/src/uu/sum/Cargo.toml +++ b/src/uu/sum/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_sum" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "sum ~ (uutils) display checksum and block counts for input" @@ -15,9 +15,9 @@ edition = "2018" path = "src/sum.rs" [dependencies] -getopts = "0.2.18" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +clap = "2.33" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "sum" diff --git a/src/uu/sum/src/sum.rs b/src/uu/sum/src/sum.rs index 3fb28d15d..ed5655a3d 100644 --- a/src/uu/sum/src/sum.rs +++ b/src/uu/sum/src/sum.rs @@ -7,17 +7,19 @@ // spell-checker:ignore (ToDO) sysv -extern crate getopts; - #[macro_use] extern crate uucore; +use clap::{App, Arg}; use std::fs::File; use std::io::{stdin, Read, Result}; use std::path::Path; static NAME: &str = "sum"; static VERSION: &str = env!("CARGO_PKG_VERSION"); +static USAGE: &str = + "[OPTION]... [FILE]...\nWith no FILE, or when FILE is -, read standard input."; +static SUMMARY: &str = "Checksum and count the blocks in a file."; fn bsd_sum(mut reader: Box) -> (usize, u16) { let mut buf = [0; 1024]; @@ -66,55 +68,59 @@ fn open(name: &str) -> Result> { match name { "-" => Ok(Box::new(stdin()) as Box), _ => { - let f = File::open(&Path::new(name))?; + let path = &Path::new(name); + if path.is_dir() { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Is a directory", + )); + }; + if !path.metadata().is_ok() { + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "No such file or directory", + )); + }; + let f = File::open(path)?; Ok(Box::new(f) as Box) } } } +mod options { + pub static FILE: &str = "file"; + pub static BSD_COMPATIBLE: &str = "r"; + pub static SYSTEM_V_COMPATIBLE: &str = "sysv"; +} + pub fn uumain(args: impl uucore::Args) -> i32 { let args = args.collect_str(); - let mut opts = getopts::Options::new(); + let matches = App::new(executable!()) + .name(NAME) + .version(VERSION) + .usage(USAGE) + .about(SUMMARY) + .arg(Arg::with_name(options::FILE).multiple(true).hidden(true)) + .arg( + Arg::with_name(options::BSD_COMPATIBLE) + .short(options::BSD_COMPATIBLE) + .help("use the BSD compatible algorithm (default)"), + ) + .arg( + Arg::with_name(options::SYSTEM_V_COMPATIBLE) + .short("s") + .long(options::SYSTEM_V_COMPATIBLE) + .help("use the BSD compatible algorithm (default)"), + ) + .get_matches_from(args); - opts.optflag("r", "", "use the BSD compatible algorithm (default)"); - opts.optflag("s", "sysv", "use System V compatible algorithm"); - opts.optflag("h", "help", "show this help message"); - opts.optflag("v", "version", "print the version and exit"); - - let matches = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(f) => crash!(1, "Invalid options\n{}", f), + let files: Vec = match matches.values_of(options::FILE) { + Some(v) => v.clone().map(|v| v.to_owned()).collect(), + None => vec!["-".to_owned()], }; - if matches.opt_present("help") { - let msg = format!( - "{0} {1} - -Usage: - {0} [OPTION]... [FILE]... - -Checksum and count the blocks in a file.", - NAME, VERSION - ); - println!( - "{}\nWith no FILE, or when FILE is -, read standard input.", - opts.usage(&msg) - ); - return 0; - } - if matches.opt_present("version") { - println!("{} {}", NAME, VERSION); - return 0; - } - - let sysv = matches.opt_present("sysv"); - - let files = if matches.free.is_empty() { - vec!["-".to_owned()] - } else { - matches.free - }; + let sysv = matches.is_present(options::SYSTEM_V_COMPATIBLE); let print_names = if sysv { files.len() > 1 || files[0] != "-" @@ -122,10 +128,15 @@ Checksum and count the blocks in a file.", files.len() > 1 }; + let mut exit_code = 0; for file in &files { let reader = match open(file) { Ok(f) => f, - _ => crash!(1, "unable to open file"), + Err(error) => { + show_error!("'{}' {}", file, error); + exit_code = 2; + continue; + } }; let (blocks, sum) = if sysv { sysv_sum(reader) @@ -140,5 +151,5 @@ Checksum and count the blocks in a file.", } } - 0 + exit_code } diff --git a/src/uu/sync/Cargo.toml b/src/uu/sync/Cargo.toml index d6dab2788..fcff6002e 100644 --- a/src/uu/sync/Cargo.toml +++ b/src/uu/sync/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_sync" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "sync ~ (uutils) synchronize cache writes to storage" @@ -15,10 +15,10 @@ edition = "2018" path = "src/sync.rs" [dependencies] -getopts = "0.2.18" +clap = "2.33" libc = "0.2.42" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary", features=["wide"] } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["wide"] } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } winapi = { version = "0.3", features = ["errhandlingapi", "fileapi", "handleapi", "std", "winbase", "winerror"] } [[bin]] diff --git a/src/uu/sync/src/sync.rs b/src/uu/sync/src/sync.rs index bb6d8cd80..985e7580d 100644 --- a/src/uu/sync/src/sync.rs +++ b/src/uu/sync/src/sync.rs @@ -7,29 +7,55 @@ /* Last synced with: sync (GNU coreutils) 8.13 */ -extern crate getopts; extern crate libc; -#[cfg(windows)] #[macro_use] extern crate uucore; -#[cfg(not(windows))] -extern crate uucore; +use clap::{App, Arg}; +use std::path::Path; -static NAME: &str = "sync"; +static EXIT_ERR: i32 = 1; + +static ABOUT: &str = "Synchronize cached writes to persistent storage"; static VERSION: &str = env!("CARGO_PKG_VERSION"); +pub mod options { + pub static FILE_SYSTEM: &str = "file-system"; + pub static DATA: &str = "data"; +} + +static ARG_FILES: &str = "files"; #[cfg(unix)] mod platform { use super::libc; - - extern "C" { - fn sync() -> libc::c_void; - } + #[cfg(target_os = "linux")] + use std::fs::File; + #[cfg(target_os = "linux")] + use std::os::unix::io::AsRawFd; pub unsafe fn do_sync() -> isize { - sync(); + libc::sync(); + 0 + } + + #[cfg(target_os = "linux")] + pub unsafe fn do_syncfs(files: Vec) -> isize { + for path in files { + let f = File::open(&path).unwrap(); + let fd = f.as_raw_fd(); + libc::syscall(libc::SYS_syncfs, fd); + } + 0 + } + + #[cfg(target_os = "linux")] + pub unsafe fn do_fdatasync(files: Vec) -> isize { + for path in files { + let f = File::open(&path).unwrap(); + let fd = f.as_raw_fd(); + libc::syscall(libc::SYS_fdatasync, fd); + } 0 } } @@ -45,6 +71,7 @@ mod platform { use std::fs::OpenOptions; use std::mem; use std::os::windows::prelude::*; + use std::path::Path; use uucore::wide::{FromWide, ToWide}; unsafe fn flush_volume(name: &str) { @@ -116,59 +143,84 @@ mod platform { } 0 } + + pub unsafe fn do_syncfs(files: Vec) -> isize { + for path in files { + flush_volume( + Path::new(&path) + .components() + .next() + .unwrap() + .as_os_str() + .to_str() + .unwrap(), + ); + } + 0 + } +} + +fn get_usage() -> String { + format!("{0} [OPTION]... FILE...", executable!()) } pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); + let usage = get_usage(); - let mut opts = getopts::Options::new(); + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(&usage[..]) + .arg( + Arg::with_name(options::FILE_SYSTEM) + .short("f") + .long(options::FILE_SYSTEM) + .conflicts_with(options::DATA) + .help("sync the file systems that contain the files (Linux and Windows only)"), + ) + .arg( + Arg::with_name(options::DATA) + .short("d") + .long(options::DATA) + .conflicts_with(options::FILE_SYSTEM) + .help("sync only file data, no unneeded metadata (Linux only)"), + ) + .arg(Arg::with_name(ARG_FILES).multiple(true).takes_value(true)) + .get_matches_from(args); - opts.optflag("h", "help", "display this help and exit"); - opts.optflag("V", "version", "output version information and exit"); + let files: Vec = matches + .values_of(ARG_FILES) + .map(|v| v.map(ToString::to_string).collect()) + .unwrap_or_default(); - let matches = match opts.parse(&args[1..]) { - Ok(m) => m, - _ => { - help(&opts); - return 1; + for f in &files { + if !Path::new(&f).exists() { + crash!(EXIT_ERR, "cannot stat '{}': No such file or directory", f); } - }; - - if matches.opt_present("h") { - help(&opts); - return 0; } - if matches.opt_present("V") { - version(); - return 0; + if matches.is_present(options::FILE_SYSTEM) { + #[cfg(any(target_os = "linux", target_os = "windows"))] + syncfs(files); + } else if matches.is_present(options::DATA) { + #[cfg(target_os = "linux")] + fdatasync(files); + } else { + sync(); } - - sync(); 0 } -fn version() { - println!("{} (uutils) {}", NAME, VERSION); - println!("The MIT License"); - println!(); - println!("Author -- Alexander Fomin."); -} - -fn help(opts: &getopts::Options) { - let msg = format!( - "{0} {1} - -Usage: - {0} [OPTION] - -Force changed blocks to disk, update the super block.", - NAME, VERSION - ); - - print!("{}", opts.usage(&msg)); -} - fn sync() -> isize { unsafe { platform::do_sync() } } + +#[cfg(any(target_os = "linux", target_os = "windows"))] +fn syncfs(files: Vec) -> isize { + unsafe { platform::do_syncfs(files) } +} + +#[cfg(target_os = "linux")] +fn fdatasync(files: Vec) -> isize { + unsafe { platform::do_fdatasync(files) } +} diff --git a/src/uu/tac/Cargo.toml b/src/uu/tac/Cargo.toml index b5e3867ee..3a530d0ce 100644 --- a/src/uu/tac/Cargo.toml +++ b/src/uu/tac/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_tac" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "tac ~ (uutils) concatenate and display input lines in reverse order" @@ -15,9 +15,9 @@ edition = "2018" path = "src/tac.rs" [dependencies] -getopts = "0.2.18" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +clap = "2.33" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "tac" diff --git a/src/uu/tac/src/tac.rs b/src/uu/tac/src/tac.rs index 91bbea4d8..68dae94e2 100644 --- a/src/uu/tac/src/tac.rs +++ b/src/uu/tac/src/tac.rs @@ -7,84 +7,80 @@ // spell-checker:ignore (ToDO) sbytes slen -extern crate getopts; - #[macro_use] extern crate uucore; -use std::fs::File; +use clap::{App, Arg}; use std::io::{stdin, stdout, BufReader, Read, Stdout, Write}; +use std::{fs::File, path::Path}; static NAME: &str = "tac"; static VERSION: &str = env!("CARGO_PKG_VERSION"); +static USAGE: &str = "[OPTION]... [FILE]..."; +static SUMMARY: &str = "Write each file to standard output, last line first."; + +mod options { + pub static BEFORE: &str = "before"; + pub static REGEX: &str = "regex"; + pub static SEPARATOR: &str = "separator"; + pub static FILE: &str = "file"; +} pub fn uumain(args: impl uucore::Args) -> i32 { let args = args.collect_str(); - let mut opts = getopts::Options::new(); + let matches = App::new(executable!()) + .name(NAME) + .version(VERSION) + .usage(USAGE) + .about(SUMMARY) + .arg( + Arg::with_name(options::BEFORE) + .short("b") + .long(options::BEFORE) + .help("attach the separator before instead of after") + .takes_value(false), + ) + .arg( + Arg::with_name(options::REGEX) + .short("r") + .long(options::REGEX) + .help("interpret the sequence as a regular expression (NOT IMPLEMENTED)") + .takes_value(false), + ) + .arg( + Arg::with_name(options::SEPARATOR) + .short("s") + .long(options::SEPARATOR) + .help("use STRING as the separator instead of newline") + .takes_value(true), + ) + .arg(Arg::with_name(options::FILE).hidden(true).multiple(true)) + .get_matches_from(args); - opts.optflag( - "b", - "before", - "attach the separator before instead of after", - ); - opts.optflag( - "r", - "regex", - "interpret the sequence as a regular expression (NOT IMPLEMENTED)", - ); - opts.optopt( - "s", - "separator", - "use STRING as the separator instead of newline", - "STRING", - ); - opts.optflag("h", "help", "display this help and exit"); - opts.optflag("V", "version", "output version information and exit"); - - let matches = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(f) => crash!(1, "{}", f), - }; - if matches.opt_present("help") { - let msg = format!( - "{0} {1} - -Usage: - {0} [OPTION]... [FILE]... - -Write each file to standard output, last line first.", - NAME, VERSION - ); - - print!("{}", opts.usage(&msg)); - } else if matches.opt_present("version") { - println!("{} {}", NAME, VERSION); - } else { - let before = matches.opt_present("b"); - let regex = matches.opt_present("r"); - let separator = match matches.opt_str("s") { - Some(m) => { - if m.is_empty() { - crash!(1, "separator cannot be empty") - } else { - m - } + let before = matches.is_present(options::BEFORE); + let regex = matches.is_present(options::REGEX); + let separator = match matches.value_of(options::SEPARATOR) { + Some(m) => { + if m.is_empty() { + crash!(1, "separator cannot be empty") + } else { + m.to_owned() } - None => "\n".to_owned(), - }; - let files = if matches.free.is_empty() { - vec!["-".to_owned()] - } else { - matches.free - }; - tac(files, before, regex, &separator[..]); - } + } + None => "\n".to_owned(), + }; - 0 + let files: Vec = match matches.values_of(options::FILE) { + Some(v) => v.map(|v| v.to_owned()).collect(), + None => vec!["-".to_owned()], + }; + + tac(files, before, regex, &separator[..]) } -fn tac(filenames: Vec, before: bool, _: bool, separator: &str) { +fn tac(filenames: Vec, before: bool, _: bool, separator: &str) -> i32 { + let mut exit_code = 0; let mut out = stdout(); let sbytes = separator.as_bytes(); let slen = sbytes.len(); @@ -93,10 +89,19 @@ fn tac(filenames: Vec, before: bool, _: bool, separator: &str) { let mut file = BufReader::new(if filename == "-" { Box::new(stdin()) as Box } else { - match File::open(filename) { + let path = Path::new(filename); + if path.is_dir() || !path.metadata().is_ok() { + show_error!( + "failed to open '{}' for reading: No such file or directory", + filename + ); + continue; + } + match File::open(path) { Ok(f) => Box::new(f) as Box, Err(e) => { - show_warning!("failed to open '{}' for reading: {}", filename, e); + show_error!("failed to open '{}' for reading: {}", filename, e); + exit_code = 1; continue; } } @@ -104,7 +109,8 @@ fn tac(filenames: Vec, before: bool, _: bool, separator: &str) { let mut data = Vec::new(); if let Err(e) = file.read_to_end(&mut data) { - show_warning!("failed to read '{}': {}", filename, e); + show_error!("failed to read '{}': {}", filename, e); + exit_code = 1; continue; }; @@ -143,6 +149,8 @@ fn tac(filenames: Vec, before: bool, _: bool, separator: &str) { } show_line(&mut out, sbytes, &data[0..prev], before); } + + exit_code } fn show_line(out: &mut Stdout, sep: &[u8], dat: &[u8], before: bool) { diff --git a/src/uu/tail/Cargo.toml b/src/uu/tail/Cargo.toml index a7cc75d7b..d3f60e09b 100644 --- a/src/uu/tail/Cargo.toml +++ b/src/uu/tail/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_tail" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "tail ~ (uutils) display the last lines of input" @@ -15,10 +15,10 @@ edition = "2018" path = "src/tail.rs" [dependencies] -getopts = "0.2.18" +clap = "2.33" libc = "0.2.42" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } winapi = { version="0.3", features=["fileapi", "handleapi", "processthreadsapi", "synchapi", "winbase"] } [target.'cfg(target_os = "redox")'.dependencies] diff --git a/src/uu/tail/src/platform/redox.rs b/src/uu/tail/src/platform/redox.rs index 2c1efd318..bc6fb5220 100644 --- a/src/uu/tail/src/platform/redox.rs +++ b/src/uu/tail/src/platform/redox.rs @@ -1,7 +1,5 @@ // spell-checker:ignore (ToDO) ENOSYS EPERM -extern crate syscall; - use self::syscall::{Error, ENOSYS, EPERM}; pub type Pid = usize; diff --git a/src/uu/tail/src/platform/unix.rs b/src/uu/tail/src/platform/unix.rs index bbb1f1965..167f693e6 100644 --- a/src/uu/tail/src/platform/unix.rs +++ b/src/uu/tail/src/platform/unix.rs @@ -9,8 +9,6 @@ // spell-checker:ignore (ToDO) errno EPERM ENOSYS -extern crate libc; - use std::io::Error; pub type Pid = libc::pid_t; diff --git a/src/uu/tail/src/tail.rs b/src/uu/tail/src/tail.rs index 949c7e5ca..ffe27e26c 100644 --- a/src/uu/tail/src/tail.rs +++ b/src/uu/tail/src/tail.rs @@ -9,26 +9,39 @@ // spell-checker:ignore (ToDO) seekable seek'd tail'ing ringbuffer ringbuf -extern crate getopts; -extern crate libc; +#[macro_use] +extern crate clap; #[macro_use] extern crate uucore; mod platform; +use clap::{App, Arg}; use std::collections::VecDeque; use std::error::Error; use std::fmt; use std::fs::File; use std::io::{stdin, stdout, BufRead, BufReader, Read, Seek, SeekFrom, Write}; use std::path::Path; -use std::str::from_utf8; use std::thread::sleep; use std::time::Duration; -static NAME: &str = "tail"; -static VERSION: &str = env!("CARGO_PKG_VERSION"); +pub mod options { + pub mod verbosity { + pub static QUIET: &str = "quiet"; + pub static SILENT: &str = "silent"; + pub static VERBOSE: &str = "verbose"; + } + pub static BYTES: &str = "bytes"; + pub static FOLLOW: &str = "follow"; + pub static LINES: &str = "lines"; + pub static PID: &str = "pid"; + pub static SLEEP_INT: &str = "sleep-interval"; + pub static ZERO_TERM: &str = "zero-terminated"; +} + +static ARG_FILES: &str = "files"; enum FilterMode { Bytes(u64), @@ -57,65 +70,80 @@ impl Default for Settings { #[allow(clippy::cognitive_complexity)] pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); - let mut settings: Settings = Default::default(); - // handle obsolete -number syntax - let options = match obsolete(&args[1..]) { - (args, Some(n)) => { - settings.mode = FilterMode::Lines(n, b'\n'); - args - } - (args, None) => args, - }; + let app = App::new(executable!()) + .version(crate_version!()) + .about("output the last part of files") + .arg( + Arg::with_name(options::BYTES) + .short("c") + .long(options::BYTES) + .takes_value(true) + .allow_hyphen_values(true) + .help("Number of bytes to print"), + ) + .arg( + Arg::with_name(options::FOLLOW) + .short("f") + .long(options::FOLLOW) + .help("Print the file as it grows"), + ) + .arg( + Arg::with_name(options::LINES) + .short("n") + .long(options::LINES) + .takes_value(true) + .allow_hyphen_values(true) + .help("Number of lines to print"), + ) + .arg( + Arg::with_name(options::PID) + .long(options::PID) + .takes_value(true) + .help("with -f, terminate after process ID, PID dies"), + ) + .arg( + Arg::with_name(options::verbosity::QUIET) + .short("q") + .long(options::verbosity::QUIET) + .help("never output headers giving file names"), + ) + .arg( + Arg::with_name(options::verbosity::SILENT) + .long(options::verbosity::SILENT) + .help("synonym of --quiet"), + ) + .arg( + Arg::with_name(options::SLEEP_INT) + .short("s") + .long(options::SLEEP_INT) + .help("Number or seconds to sleep between polling the file when running with -f"), + ) + .arg( + Arg::with_name(options::verbosity::VERBOSE) + .short("v") + .long(options::verbosity::VERBOSE) + .help("always output headers giving file names"), + ) + .arg( + Arg::with_name(options::ZERO_TERM) + .short("z") + .long(options::ZERO_TERM) + .help("Line delimiter is NUL, not newline"), + ) + .arg( + Arg::with_name(ARG_FILES) + .multiple(true) + .takes_value(true) + .min_values(1), + ); - let args = options; + let matches = app.get_matches_from(args); - let mut opts = getopts::Options::new(); - - opts.optopt("c", "bytes", "Number of bytes to print", "k"); - opts.optopt("n", "lines", "Number of lines to print", "k"); - opts.optflag("f", "follow", "Print the file as it grows"); - opts.optopt( - "s", - "sleep-interval", - "Number or seconds to sleep between polling the file when running with -f", - "n", - ); - opts.optopt( - "", - "pid", - "with -f, terminate after process ID, PID dies", - "PID", - ); - opts.optflag("z", "zero-terminated", "Line delimiter is NUL, not newline"); - opts.optflag("h", "help", "help"); - opts.optflag("V", "version", "version"); - opts.optflag("v", "verbose", "always output headers giving file names"); - opts.optflag("q", "quiet", "never output headers giving file names"); - opts.optflag("", "silent", "synonym of --quiet"); - - let given_options = match opts.parse(&args) { - Ok(m) => m, - Err(_) => { - println!("{}", opts.usage("")); - return 1; - } - }; - - if given_options.opt_present("h") { - println!("{}", opts.usage("")); - return 0; - } - if given_options.opt_present("V") { - version(); - return 0; - } - - settings.follow = given_options.opt_present("f"); + settings.follow = matches.is_present(options::FOLLOW); if settings.follow { - if let Some(n) = given_options.opt_str("s") { + if let Some(n) = matches.value_of(options::SLEEP_INT) { let parsed: Option = n.parse().ok(); if let Some(m) = parsed { settings.sleep_msec = m * 1000 @@ -123,7 +151,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } } - if let Some(pid_str) = given_options.opt_str("pid") { + if let Some(pid_str) = matches.value_of(options::PID) { if let Ok(pid) = pid_str.parse() { settings.pid = pid; if pid != 0 { @@ -139,9 +167,9 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } } - match given_options.opt_str("n") { + match matches.value_of(options::LINES) { Some(n) => { - let mut slice: &str = n.as_ref(); + let mut slice: &str = n; if slice.chars().next().unwrap_or('_') == '+' { settings.beginning = true; slice = &slice[1..]; @@ -155,8 +183,8 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } } None => { - if let Some(n) = given_options.opt_str("c") { - let mut slice: &str = n.as_ref(); + if let Some(n) = matches.value_of(options::BYTES) { + let mut slice: &str = n; if slice.chars().next().unwrap_or('_') == '+' { settings.beginning = true; slice = &slice[1..]; @@ -172,16 +200,20 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } }; - if given_options.opt_present("z") { + if matches.is_present(options::ZERO_TERM) { if let FilterMode::Lines(count, _) = settings.mode { settings.mode = FilterMode::Lines(count, 0); } } - let verbose = given_options.opt_present("v"); - let quiet = given_options.opt_present("q") || given_options.opt_present("silent"); + let verbose = matches.is_present(options::verbosity::VERBOSE); + let quiet = matches.is_present(options::verbosity::QUIET) + || matches.is_present(options::verbosity::SILENT); - let files = given_options.free; + let files: Vec = matches + .values_of(ARG_FILES) + .map(|v| v.map(ToString::to_string).collect()) + .unwrap_or_default(); if files.is_empty() { let mut buffer = BufReader::new(stdin()); @@ -245,7 +277,11 @@ impl Error for ParseSizeErr { impl fmt::Display for ParseSizeErr { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { - write!(f, "{}", self.to_string()) + let s = match self { + ParseSizeErr::ParseFailure(s) => s, + ParseSizeErr::SizeTooBig(s) => s, + }; + write!(f, "{}", s) } } @@ -309,49 +345,13 @@ pub fn parse_size(mut size_slice: &str) -> Result { // sole B is not a valid suffix Err(ParseSizeErr::parse_failure(size_slice)) } else { - let value: Option = size_slice.parse().ok(); + let value: Option = size_slice.parse().ok(); value - .map(|v| Ok(multiplier * v)) + .map(|v| Ok((multiplier as i64 * v.abs()) as u64)) .unwrap_or_else(|| Err(ParseSizeErr::parse_failure(size_slice))) } } -// It searches for an option in the form of -123123 -// -// In case is found, the options vector will get rid of that object so that -// getopts works correctly. -fn obsolete(options: &[String]) -> (Vec, Option) { - let mut options: Vec = options.to_vec(); - let mut a = 0; - let b = options.len(); - - while a < b { - let current = options[a].clone(); - let current = current.as_bytes(); - - if current.len() > 1 && current[0] == b'-' { - let len = current.len(); - for pos in 1..len { - // Ensure that the argument is only made out of digits - if !(current[pos] as char).is_numeric() { - break; - } - - // If this is the last number - if pos == len - 1 { - options.remove(a); - let number: Option = from_utf8(¤t[1..len]).unwrap().parse().ok(); - return (options, Some(number.unwrap())); - } - } - } - - a += 1; - } - - (options, None) -} - /// When reading files in reverse in `bounded_tail`, this is the size of each /// block read at a time. const BLOCK_SIZE: u64 = 1 << 16; @@ -382,7 +382,7 @@ fn follow(readers: &mut [BufReader], filenames: &[String], settings: } print!("{}", datum); } - Err(err) => panic!(err), + Err(err) => panic!("{}", err), } } } @@ -509,7 +509,7 @@ fn unbounded_tail(reader: &mut BufReader, settings: &Settings) { ringbuf.push_back(datum); } } - Err(err) => panic!(err), + Err(err) => panic!("{}", err), } } let mut stdout = stdout(); @@ -540,7 +540,7 @@ fn unbounded_tail(reader: &mut BufReader, settings: &Settings) { ringbuf.push_back(datum[0]); } } - Err(err) => panic!(err), + Err(err) => panic!("{}", err), } } let mut stdout = stdout(); @@ -566,7 +566,3 @@ fn print_byte(stdout: &mut T, ch: u8) { fn print_string(_: &mut T, s: &str) { print!("{}", s); } - -fn version() { - println!("{} {}", NAME, VERSION); -} diff --git a/src/uu/tee/Cargo.toml b/src/uu/tee/Cargo.toml index 777b9a747..7ac81adc4 100644 --- a/src/uu/tee/Cargo.toml +++ b/src/uu/tee/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_tee" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "tee ~ (uutils) display input and copy to FILE" @@ -15,10 +15,11 @@ edition = "2018" path = "src/tee.rs" [dependencies] -getopts = "0.2.18" +clap = "2.33.3" libc = "0.2.42" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +retain_mut = "0.1.2" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["libc"] } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "tee" diff --git a/src/uu/tee/src/tee.rs b/src/uu/tee/src/tee.rs index f715f9e7c..7c6a86b4c 100644 --- a/src/uu/tee/src/tee.rs +++ b/src/uu/tee/src/tee.rs @@ -5,100 +5,121 @@ // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -extern crate getopts; - +#[macro_use] extern crate uucore; +use clap::{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}; -static NAME: &str = "tee"; +#[cfg(unix)] +use uucore::libc; + static VERSION: &str = env!("CARGO_PKG_VERSION"); +static ABOUT: &str = "Copy standard input to each FILE, and also to standard output."; + +mod options { + pub const APPEND: &str = "append"; + pub const IGNORE_INTERRUPTS: &str = "ignore-interrupts"; + pub const FILE: &str = "file"; +} + +#[allow(dead_code)] +struct Options { + append: bool, + ignore_interrupts: bool, + files: Vec, +} + +fn get_usage() -> String { + format!("{0} [OPTION]... [FILE]...", executable!()) +} pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); + let usage = get_usage(); - match options(&args).and_then(exec) { + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(&usage[..]) + .after_help("If a FILE is -, it refers to a file named - .") + .arg( + Arg::with_name(options::APPEND) + .long(options::APPEND) + .short("a") + .help("append to the given FILEs, do not overwrite"), + ) + .arg( + Arg::with_name(options::IGNORE_INTERRUPTS) + .long(options::IGNORE_INTERRUPTS) + .short("i") + .help("ignore interrupt signals (ignored on non-Unix platforms)"), + ) + .arg(Arg::with_name(options::FILE).multiple(true)) + .get_matches_from(args); + + let options = Options { + append: matches.is_present(options::APPEND), + ignore_interrupts: matches.is_present(options::IGNORE_INTERRUPTS), + files: matches + .values_of(options::FILE) + .map(|v| v.map(ToString::to_string).collect()) + .unwrap_or_default(), + }; + + match tee(options) { Ok(_) => 0, Err(_) => 1, } } -#[allow(dead_code)] -struct Options { - program: String, - append: bool, - ignore_interrupts: bool, - print_and_exit: Option, - files: Vec, -} - -fn options(args: &[String]) -> Result { - let mut opts = getopts::Options::new(); - - opts.optflag("a", "append", "append to the given FILEs, do not overwrite"); - opts.optflag("i", "ignore-interrupts", "ignore interrupt signals"); - opts.optflag("h", "help", "display this help and exit"); - opts.optflag("V", "version", "output version information and exit"); - - opts.parse(&args[1..]) - .map_err(|e| Error::new(ErrorKind::Other, format!("{}", e))) - .map(|m| { - let version = format!("{} {}", NAME, VERSION); - let arguments = "[OPTION]... [FILE]..."; - let brief = "Copy standard input to each FILE, and also to standard output."; - let comment = "If a FILE is -, it refers to a file named - ."; - let help = format!( - "{}\n\nUsage:\n {} {}\n\n{}\n{}", - version, - NAME, - arguments, - opts.usage(brief), - comment - ); - let names: Vec = m.free.clone().into_iter().collect(); - let to_print = if m.opt_present("help") { - Some(help) - } else if m.opt_present("version") { - Some(version) - } else { - None - }; - Options { - program: NAME.to_owned(), - append: m.opt_present("append"), - ignore_interrupts: m.opt_present("ignore-interrupts"), - print_and_exit: to_print, - files: names, - } - }) - .map_err(|message| warn(format!("{}", message).as_ref())) -} - -fn exec(options: Options) -> Result<()> { - match options.print_and_exit { - Some(text) => { - println!("{}", text); - Ok(()) - } - None => tee(options), +#[cfg(unix)] +fn ignore_interrupts() -> Result<()> { + let ret = unsafe { libc::signal(libc::SIGINT, libc::SIG_IGN) }; + if ret == libc::SIG_ERR { + return Err(Error::new(ErrorKind::Other, "")); } + Ok(()) +} + +#[cfg(not(unix))] +fn ignore_interrupts() -> Result<()> { + // Do nothing. + Ok(()) } fn tee(options: Options) -> Result<()> { - let mut writers: Vec> = options + if options.ignore_interrupts { + ignore_interrupts()? + } + let mut writers: Vec = options .files .clone() .into_iter() - .map(|file| open(file, options.append)) + .map(|file| NamedWriter { + name: file.clone(), + inner: open(file, options.append), + }) .collect(); - writers.push(Box::new(stdout())); - let output = &mut MultiWriter { writers }; + + writers.insert( + 0, + NamedWriter { + name: "'standard output'".to_owned(), + inner: Box::new(stdout()), + }, + ); + + let mut output = MultiWriter::new(writers); let input = &mut NamedReader { inner: Box::new(stdin()) as Box, }; - if copy(input, output).is_err() || output.flush().is_err() { + + // TODO: replaced generic 'copy' call to be able to stop copying + // if all outputs are closed (due to errors) + if copy(input, &mut output).is_err() || output.flush().is_err() || output.error_occured() { Err(Error::new(ErrorKind::Other, "")) } else { Ok(()) @@ -106,7 +127,7 @@ fn tee(options: Options) -> Result<()> { } fn open(name: String, append: bool) -> Box { - let path = PathBuf::from(name); + let path = PathBuf::from(name.clone()); let inner: Box = { let mut options = OpenOptions::new(); let mode = if append { @@ -119,55 +140,68 @@ fn open(name: String, append: bool) -> Box { Err(_) => Box::new(sink()), } }; - Box::new(NamedWriter { inner, path }) as Box + Box::new(NamedWriter { inner, name }) as Box } struct MultiWriter { - writers: Vec>, + writers: Vec, + initial_len: usize, +} + +impl MultiWriter { + fn new(writers: Vec) -> Self { + Self { + initial_len: writers.len(), + writers, + } + } + fn error_occured(&self) -> bool { + self.writers.len() != self.initial_len + } } impl Write for MultiWriter { fn write(&mut self, buf: &[u8]) -> Result { - for writer in &mut self.writers { - writer.write_all(buf)?; - } + self.writers.retain_mut(|writer| { + let result = writer.write_all(buf); + match result { + Err(f) => { + show_info!("{}: {}", writer.name, f.to_string()); + false + } + _ => true, + } + }); Ok(buf.len()) } fn flush(&mut self) -> Result<()> { - for writer in &mut self.writers { - writer.flush()?; - } + self.writers.retain_mut(|writer| { + let result = writer.flush(); + match result { + Err(f) => { + show_info!("{}: {}", writer.name, f.to_string()); + false + } + _ => true, + } + }); Ok(()) } } struct NamedWriter { inner: Box, - path: PathBuf, + pub name: String, } impl Write for NamedWriter { fn write(&mut self, buf: &[u8]) -> Result { - match self.inner.write(buf) { - Err(f) => { - self.inner = Box::new(sink()) as Box; - warn(format!("{}: {}", self.path.display(), f.to_string()).as_ref()); - Err(f) - } - okay => okay, - } + self.inner.write(buf) } fn flush(&mut self) -> Result<()> { - match self.inner.flush() { - Err(f) => { - self.inner = Box::new(sink()) as Box; - warn(format!("{}: {}", self.path.display(), f.to_string()).as_ref()); - Err(f) - } - okay => okay, - } + self.inner.flush() } } @@ -179,15 +213,10 @@ impl Read for NamedReader { fn read(&mut self, buf: &mut [u8]) -> Result { match self.inner.read(buf) { Err(f) => { - warn(format!("{}: {}", Path::new("stdin").display(), f.to_string()).as_ref()); + show_info!("{}: {}", Path::new("stdin").display(), f.to_string()); Err(f) } okay => okay, } } } - -fn warn(message: &str) -> Error { - eprintln!("{}: {}", NAME, message); - Error::new(ErrorKind::Other, format!("{}: {}", NAME, message)) -} diff --git a/src/uu/test/Cargo.toml b/src/uu/test/Cargo.toml index 8cc621a63..e1f6e62e7 100644 --- a/src/uu/test/Cargo.toml +++ b/src/uu/test/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_test" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "test ~ (uutils) evaluate comparison and file type expressions" @@ -16,8 +16,8 @@ path = "src/test.rs" [dependencies] libc = "0.2.42" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [target.'cfg(target_os = "redox")'.dependencies] redox_syscall = "0.1" diff --git a/src/uu/test/src/test.rs b/src/uu/test/src/test.rs index dafe1913c..4394e4a8e 100644 --- a/src/uu/test/src/test.rs +++ b/src/uu/test/src/test.rs @@ -7,10 +7,6 @@ // spell-checker:ignore (ToDO) retval paren prec subprec cond -extern crate libc; -#[cfg(target_os = "redox")] -extern crate syscall; - use std::collections::HashMap; use std::str::from_utf8; diff --git a/src/uu/timeout/Cargo.toml b/src/uu/timeout/Cargo.toml index bc94a4d5a..51ac0bc0e 100644 --- a/src/uu/timeout/Cargo.toml +++ b/src/uu/timeout/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_timeout" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "timeout ~ (uutils) run COMMAND with a DURATION time limit" @@ -17,9 +17,8 @@ path = "src/timeout.rs" [dependencies] getopts = "0.2.18" libc = "0.2.42" -time = "0.1.40" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary", features=["parse_time", "process", "signals"] } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["parse_time", "process", "signals"] } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "timeout" diff --git a/src/uu/timeout/src/timeout.rs b/src/uu/timeout/src/timeout.rs index 59bbeca8f..0dd7c2016 100644 --- a/src/uu/timeout/src/timeout.rs +++ b/src/uu/timeout/src/timeout.rs @@ -7,10 +7,6 @@ // spell-checker:ignore (ToDO) tstr sigstr cmdname setpgid -extern crate getopts; -extern crate libc; -extern crate time; - #[macro_use] extern crate uucore; diff --git a/src/uu/touch/Cargo.toml b/src/uu/touch/Cargo.toml index 7372c4347..0608a7b7c 100644 --- a/src/uu/touch/Cargo.toml +++ b/src/uu/touch/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_touch" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "touch ~ (uutils) change FILE timestamps" @@ -16,10 +16,10 @@ path = "src/touch.rs" [dependencies] filetime = "0.2.1" -getopts = "0.2.18" +clap = "2.33" time = "0.1.40" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary", features=["libc"] } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["libc"] } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "touch" diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index 48abd63a2..39405900e 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -9,142 +9,162 @@ // spell-checker:ignore (ToDO) filetime strptime utcoff strs datetime MMDDhhmm pub extern crate filetime; -extern crate getopts; -extern crate time; #[macro_use] extern crate uucore; +use clap::{App, Arg, ArgGroup}; use filetime::*; use std::fs::{self, File}; use std::io::Error; use std::path::Path; -static NAME: &str = "touch"; static VERSION: &str = env!("CARGO_PKG_VERSION"); +static ABOUT: &str = "Update the access and modification times of each FILE to the current time."; +pub mod options { + // Both SOURCES and sources are needed as we need to be able to refer to the ArgGroup. + pub static SOURCES: &str = "sources"; + pub mod sources { + pub static DATE: &str = "date"; + pub static REFERENCE: &str = "reference"; + pub static CURRENT: &str = "current"; + } + pub static ACCESS: &str = "access"; + pub static MODIFICATION: &str = "modification"; + pub static NO_CREATE: &str = "no-create"; + pub static NO_DEREF: &str = "no-dereference"; + pub static TIME: &str = "time"; +} -// Since touch's date/timestamp parsing doesn't account for timezone, the -// returned value from time::strptime() is UTC. We get system's timezone to -// localize the time. -macro_rules! to_local( - ($exp:expr) => ({ - let mut tm = $exp; - tm.tm_utcoff = time::now().tm_utcoff; - tm - }) -); +static ARG_FILES: &str = "files"; -macro_rules! local_tm_to_filetime( - ($exp:expr) => ({ - let ts = $exp.to_timespec(); - FileTime::from_unix_time(ts.sec as i64, ts.nsec as u32) - }) -); +fn to_local(mut tm: time::Tm) -> time::Tm { + tm.tm_utcoff = time::now().tm_utcoff; + tm +} + +fn local_tm_to_filetime(tm: time::Tm) -> FileTime { + let ts = tm.to_timespec(); + FileTime::from_unix_time(ts.sec as i64, ts.nsec as u32) +} + +fn get_usage() -> String { + format!("{0} [OPTION]... [USER]", executable!()) +} pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); + let usage = get_usage(); - let mut opts = getopts::Options::new(); - - opts.optflag("a", "", "change only the access time"); - opts.optflag("c", "no-create", "do not create any files"); - opts.optopt( - "d", - "date", - "parse argument and use it instead of current time", - "STRING", - ); - opts.optflag( - "h", - "no-dereference", - "affect each symbolic link instead of any referenced file \ - (only for systems that can change the timestamps of a symlink)", - ); - opts.optflag("m", "", "change only the modification time"); - opts.optopt( - "r", - "reference", - "use this file's times instead of the current time", - "FILE", - ); - opts.optopt( - "t", - "", - "use [[CC]YY]MMDDhhmm[.ss] instead of the current time", - "STAMP", - ); - opts.optopt( - "", - "time", - "change only the specified time: \"access\", \"atime\", or \ - \"use\" are equivalent to -a; \"modify\" or \"mtime\" are \ - equivalent to -m", - "WORD", - ); - opts.optflag("h", "help", "display this help and exit"); - opts.optflag("V", "version", "output version information and exit"); - - let matches = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(e) => panic!("Invalid options\n{}", e), - }; - - if matches.opt_present("version") { - println!("{} {}", NAME, VERSION); - return 0; - } - - if matches.opt_present("help") || matches.free.is_empty() { - println!("{} {}", NAME, VERSION); - println!(); - println!("Usage: {} [OPTION]... FILE...", NAME); - println!(); - println!( - "{}", - opts.usage( - "Update the access and modification times of \ - each FILE to the current time." - ) - ); - if matches.free.is_empty() { - return 1; - } - return 0; - } - - if matches.opt_present("date") - && matches.opts_present(&["reference".to_owned(), "t".to_owned()]) - || matches.opt_present("reference") - && matches.opts_present(&["date".to_owned(), "t".to_owned()]) - || matches.opt_present("t") - && matches.opts_present(&["date".to_owned(), "reference".to_owned()]) - { - panic!("Invalid options: cannot specify reference time from more than one source"); - } - - let (mut atime, mut mtime) = if matches.opt_present("reference") { - stat( - &matches.opt_str("reference").unwrap()[..], - !matches.opt_present("no-dereference"), + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(&usage[..]) + .arg( + Arg::with_name(options::ACCESS) + .short("a") + .help("change only the access time"), ) - } else if matches.opts_present(&["date".to_owned(), "t".to_owned()]) { - let timestamp = if matches.opt_present("date") { - parse_date(matches.opt_str("date").unwrap().as_ref()) + .arg( + Arg::with_name(options::sources::CURRENT) + .short("t") + .help("use [[CC]YY]MMDDhhmm[.ss] instead of the current time") + .value_name("STAMP") + .takes_value(true), + ) + .arg( + Arg::with_name(options::sources::DATE) + .short("d") + .long(options::sources::DATE) + .help("parse argument and use it instead of current time") + .value_name("STRING"), + ) + .arg( + Arg::with_name(options::MODIFICATION) + .short("m") + .help("change only the modification time"), + ) + .arg( + Arg::with_name(options::NO_CREATE) + .short("c") + .long(options::NO_CREATE) + .help("do not create any files"), + ) + .arg( + Arg::with_name(options::NO_DEREF) + .short("h") + .long(options::NO_DEREF) + .help( + "affect each symbolic link instead of any referenced file \ + (only for systems that can change the timestamps of a symlink)", + ), + ) + .arg( + Arg::with_name(options::sources::REFERENCE) + .short("r") + .long(options::sources::REFERENCE) + .help("use this file's times instead of the current time") + .value_name("FILE"), + ) + .arg( + Arg::with_name(options::TIME) + .long(options::TIME) + .help( + "change only the specified time: \"access\", \"atime\", or \ + \"use\" are equivalent to -a; \"modify\" or \"mtime\" are \ + equivalent to -m", + ) + .value_name("WORD") + .possible_values(&["access", "atime", "use"]) + .takes_value(true), + ) + .arg( + Arg::with_name(ARG_FILES) + .multiple(true) + .takes_value(true) + .min_values(1), + ) + .group(ArgGroup::with_name(options::SOURCES).args(&[ + options::sources::CURRENT, + options::sources::DATE, + options::sources::REFERENCE, + ])) + .get_matches_from(args); + + let files: Vec = matches + .values_of(ARG_FILES) + .map(|v| v.map(ToString::to_string).collect()) + .unwrap_or_default(); + + let (mut atime, mut mtime) = if matches.is_present(options::sources::REFERENCE) { + stat( + &matches.value_of(options::sources::REFERENCE).unwrap()[..], + !matches.is_present(options::NO_DEREF), + ) + } else if matches.is_present(options::sources::DATE) + || matches.is_present(options::sources::CURRENT) + { + let timestamp = if matches.is_present(options::sources::DATE) { + parse_date(matches.value_of(options::sources::DATE).unwrap().as_ref()) } else { - parse_timestamp(matches.opt_str("t").unwrap().as_ref()) + parse_timestamp( + matches + .value_of(options::sources::CURRENT) + .unwrap() + .as_ref(), + ) }; (timestamp, timestamp) } else { - let now = local_tm_to_filetime!(time::now()); + let now = local_tm_to_filetime(time::now()); (now, now) }; - for filename in &matches.free { + for filename in &files { let path = &filename[..]; if !Path::new(path).exists() { // no-dereference included here for compatibility - if matches.opts_present(&["no-create".to_owned(), "no-dereference".to_owned()]) { + if matches.is_present(options::NO_CREATE) || matches.is_present(options::NO_DEREF) { continue; } @@ -154,18 +174,21 @@ pub fn uumain(args: impl uucore::Args) -> i32 { }; // Minor optimization: if no reference time was specified, we're done. - if !matches.opts_present(&["date".to_owned(), "reference".to_owned(), "t".to_owned()]) { + if !matches.is_present(options::SOURCES) { continue; } } // If changing "only" atime or mtime, grab the existing value of the other. // Note that "-a" and "-m" may be passed together; this is not an xor. - if matches.opts_present(&["a".to_owned(), "m".to_owned(), "time".to_owned()]) { - let st = stat(path, !matches.opt_present("no-dereference")); - let time = matches.opt_strs("time"); + if matches.is_present(options::ACCESS) + || matches.is_present(options::MODIFICATION) + || matches.is_present(options::TIME) + { + let st = stat(path, !matches.is_present(options::NO_DEREF)); + let time = matches.value_of(options::TIME).unwrap_or(""); - if !(matches.opt_present("a") + if !(matches.is_present(options::ACCESS) || time.contains(&"access".to_owned()) || time.contains(&"atime".to_owned()) || time.contains(&"use".to_owned())) @@ -173,7 +196,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { atime = st.0; } - if !(matches.opt_present("m") + if !(matches.is_present(options::MODIFICATION) || time.contains(&"modify".to_owned()) || time.contains(&"mtime".to_owned())) { @@ -181,7 +204,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } } - if matches.opt_present("h") { + if matches.is_present(options::NO_DEREF) { if let Err(e) = set_symlink_file_times(path, atime, mtime) { show_warning!("cannot touch '{}': {}", path, e); } @@ -220,7 +243,7 @@ fn parse_date(str: &str) -> FileTime { // not about to implement GNU parse_datetime. // http://git.savannah.gnu.org/gitweb/?p=gnulib.git;a=blob_plain;f=lib/parse-datetime.y match time::strptime(str, "%c") { - Ok(tm) => local_tm_to_filetime!(to_local!(tm)), + Ok(tm) => local_tm_to_filetime(to_local(tm)), Err(e) => panic!("Unable to parse date\n{}", e), } } @@ -238,7 +261,7 @@ fn parse_timestamp(s: &str) -> FileTime { }; match time::strptime(&ts, format) { - Ok(tm) => local_tm_to_filetime!(to_local!(tm)), + Ok(tm) => local_tm_to_filetime(to_local(tm)), Err(e) => panic!("Unable to parse timestamp\n{}", e), } } diff --git a/src/uu/tr/Cargo.toml b/src/uu/tr/Cargo.toml index 6f356cc65..a3d066bfb 100644 --- a/src/uu/tr/Cargo.toml +++ b/src/uu/tr/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_tr" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "tr ~ (uutils) translate characters within input and display" @@ -17,9 +17,9 @@ path = "src/tr.rs" [dependencies] bit-set = "0.5.0" fnv = "1.0.5" -getopts = "0.2.18" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +clap = "2.33" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "tr" diff --git a/src/uu/tr/src/expand.rs b/src/uu/tr/src/expand.rs index 3291d57ae..e71cf262c 100644 --- a/src/uu/tr/src/expand.rs +++ b/src/uu/tr/src/expand.rs @@ -14,17 +14,46 @@ use std::cmp::min; use std::iter::Peekable; use std::ops::RangeInclusive; +/// Parse a backslash escape sequence to the corresponding character. Assumes +/// the string starts from the character _after_ the `\` and is not empty. +/// +/// Returns a tuple containing the character and the number of characters +/// consumed from the input. The alphabetic escape sequences consume 1 +/// character; octal escape sequences consume 1 to 3 octal digits. #[inline] -fn unescape_char(c: char) -> char { - match c { - 'a' => 0x07u8 as char, - 'b' => 0x08u8 as char, - 'f' => 0x0cu8 as char, - 'v' => 0x0bu8 as char, - 'n' => '\n', - 'r' => '\r', - 't' => '\t', - _ => c, +fn parse_sequence(s: &str) -> (char, usize) { + let c = s.chars().next().expect("invalid escape: empty string"); + + if '0' <= c && c <= '7' { + let mut v = c.to_digit(8).unwrap(); + let mut consumed = 1; + let bits_per_digit = 3; + + for c in s.chars().skip(1).take(2) { + match c.to_digit(8) { + Some(c) => { + v = (v << bits_per_digit) | c; + consumed += 1; + } + None => break, + } + } + + (from_u32(v).expect("invalid octal escape"), consumed) + } else { + ( + match c { + 'a' => 0x07u8 as char, + 'b' => 0x08u8 as char, + 'f' => 0x0cu8 as char, + 'v' => 0x0bu8 as char, + 'n' => '\n', + 'r' => '\r', + 't' => '\t', + c => c, + }, + 1, + ) } } @@ -52,8 +81,9 @@ impl<'a> Iterator for Unescape<'a> { '\\' if self.string.len() > 1 => { // yes---it's \ and it's not the last char in a string // we know that \ is 1 byte long so we can index into the string safely - let c = self.string[1..].chars().next().unwrap(); - (Some(unescape_char(c)), 1 + c.len_utf8()) + let (c, consumed) = parse_sequence(&self.string[1..]); + + (Some(c), 1 + consumed) } c => (Some(c), c.len_utf8()), // not an escape char }; diff --git a/src/uu/tr/src/tr.rs b/src/uu/tr/src/tr.rs index de65622a3..b94b11b9d 100644 --- a/src/uu/tr/src/tr.rs +++ b/src/uu/tr/src/tr.rs @@ -10,26 +10,33 @@ // spell-checker:ignore (ToDO) allocs bset dflag cflag sflag tflag -extern crate bit_set; -extern crate fnv; -extern crate getopts; - #[macro_use] extern crate uucore; mod expand; use bit_set::BitSet; +use clap::{App, Arg}; use fnv::FnvHashMap; -use getopts::Options; use std::io::{stdin, stdout, BufRead, BufWriter, Write}; use crate::expand::ExpandSet; static NAME: &str = "tr"; static VERSION: &str = env!("CARGO_PKG_VERSION"); +static ABOUT: &str = "translate or delete characters"; +static LONG_HELP: &str = "Translate, squeeze, and/or delete characters from standard input, +writing to standard output."; const BUFFER_LEN: usize = 1024; +mod options { + pub const COMPLEMENT: &str = "complement"; + pub const DELETE: &str = "delete"; + pub const SQUEEZE: &str = "squeeze-repeats"; + pub const TRUNCATE: &str = "truncate"; + pub const SETS: &str = "sets"; +} + trait SymbolTranslator { fn translate(&self, c: char, prev_c: char) -> Option; } @@ -174,56 +181,60 @@ fn translate_input( } } -fn usage(opts: &Options) { - println!("{} {}", NAME, VERSION); - println!(); - println!("Usage:"); - println!(" {} [OPTIONS] SET1 [SET2]", NAME); - println!(); - println!("{}", opts.usage("Translate or delete characters.")); +fn get_usage() -> String { + format!("{} [OPTION]... SET1 [SET2]", executable!()) } pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); + let usage = get_usage(); - let mut opts = Options::new(); + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(&usage[..]) + .after_help(LONG_HELP) + .arg( + Arg::with_name(options::COMPLEMENT) + .short("C") + .short("c") + .long(options::COMPLEMENT) + .help("use the complement of SET1"), + ) + .arg( + Arg::with_name(options::DELETE) + .short("d") + .long(options::DELETE) + .help("delete characters in SET1, do not translate"), + ) + .arg( + Arg::with_name(options::SQUEEZE) + .long(options::SQUEEZE) + .short("s") + .help( + "replace each sequence of a repeated character that is + listed in the last specified SET, with a single occurrence + of that character", + ), + ) + .arg( + Arg::with_name(options::TRUNCATE) + .long(options::TRUNCATE) + .short("t") + .help("first truncate SET1 to length of SET2"), + ) + .arg(Arg::with_name(options::SETS).multiple(true)) + .get_matches_from(args); - opts.optflag("c", "complement", "use the complement of SET1"); - opts.optflag("C", "", "same as -c"); - opts.optflag("d", "delete", "delete characters in SET1"); - opts.optflag("h", "help", "display this help and exit"); - opts.optflag("s", "squeeze", "replace each sequence of a repeated character that is listed in the last specified SET, with a single occurrence of that character"); - opts.optflag( - "t", - "truncate-set1", - "first truncate SET1 to length of SET2", - ); - opts.optflag("V", "version", "output version information and exit"); + let delete_flag = matches.is_present(options::DELETE); + let complement_flag = matches.is_present(options::COMPLEMENT); + let squeeze_flag = matches.is_present(options::SQUEEZE); + let truncate_flag = matches.is_present(options::TRUNCATE); - let matches = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(err) => { - show_error!("{}", err); - return 1; - } + let sets: Vec = match matches.values_of(options::SETS) { + Some(v) => v.map(|v| v.to_string()).collect(), + None => vec![], }; - if matches.opt_present("help") { - usage(&opts); - return 0; - } - - if matches.opt_present("version") { - println!("{} {}", NAME, VERSION); - return 0; - } - - let dflag = matches.opt_present("d"); - let cflag = matches.opts_present(&["c".to_owned(), "C".to_owned()]); - let sflag = matches.opt_present("s"); - let tflag = matches.opt_present("t"); - let sets = matches.free; - if sets.is_empty() { show_error!( "missing operand\nTry `{} --help` for more information.", @@ -232,7 +243,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { return 1; } - if !(dflag || sflag) && sets.len() < 2 { + if !(delete_flag || squeeze_flag) && sets.len() < 2 { show_error!( "missing operand after ‘{}’\nTry `{} --help` for more information.", sets[0], @@ -241,7 +252,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { return 1; } - if cflag && !dflag && !sflag { + if complement_flag && !delete_flag && !squeeze_flag { show_error!("-c is only supported with -d or -s"); return 1; } @@ -253,21 +264,21 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let mut buffered_stdout = BufWriter::new(locked_stdout); let set1 = ExpandSet::new(sets[0].as_ref()); - if dflag { - if sflag { + if delete_flag { + if squeeze_flag { let set2 = ExpandSet::new(sets[1].as_ref()); - let op = DeleteAndSqueezeOperation::new(set1, set2, cflag); + let op = DeleteAndSqueezeOperation::new(set1, set2, complement_flag); translate_input(&mut locked_stdin, &mut buffered_stdout, op); } else { - let op = DeleteOperation::new(set1, cflag); + let op = DeleteOperation::new(set1, complement_flag); translate_input(&mut locked_stdin, &mut buffered_stdout, op); } - } else if sflag { - let op = SqueezeOperation::new(set1, cflag); + } else if squeeze_flag { + let op = SqueezeOperation::new(set1, complement_flag); translate_input(&mut locked_stdin, &mut buffered_stdout, op); } else { let mut set2 = ExpandSet::new(sets[1].as_ref()); - let op = TranslateOperation::new(set1, &mut set2, tflag); + let op = TranslateOperation::new(set1, &mut set2, truncate_flag); translate_input(&mut locked_stdin, &mut buffered_stdout, op) } diff --git a/src/uu/true/Cargo.toml b/src/uu/true/Cargo.toml index a625e1c45..9f13318fd 100644 --- a/src/uu/true/Cargo.toml +++ b/src/uu/true/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_true" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "true ~ (uutils) do nothing and succeed" @@ -15,8 +15,8 @@ edition = "2018" path = "src/true.rs" [dependencies] -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "true" diff --git a/src/uu/truncate/Cargo.toml b/src/uu/truncate/Cargo.toml index 7bc4c34c4..e2c0afadc 100644 --- a/src/uu/truncate/Cargo.toml +++ b/src/uu/truncate/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_truncate" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "truncate ~ (uutils) truncate (or extend) FILE to SIZE" @@ -15,9 +15,9 @@ edition = "2018" path = "src/truncate.rs" [dependencies] -getopts = "0.2.18" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +clap = "2.33" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "truncate" diff --git a/src/uu/truncate/src/truncate.rs b/src/uu/truncate/src/truncate.rs index 6e36eb76e..9cd5865b7 100644 --- a/src/uu/truncate/src/truncate.rs +++ b/src/uu/truncate/src/truncate.rs @@ -7,13 +7,11 @@ // spell-checker:ignore (ToDO) RFILE refsize rfilename fsize tsize -extern crate getopts; - #[macro_use] extern crate uucore; +use clap::{App, Arg}; use std::fs::{metadata, File, OpenOptions}; -use std::io::Result; use std::path::Path; #[derive(Eq, PartialEq)] @@ -27,81 +25,99 @@ enum TruncateMode { RoundUp, } -static NAME: &str = "truncate"; +static ABOUT: &str = "Shrink or extend the size of each file to the specified size."; static VERSION: &str = env!("CARGO_PKG_VERSION"); +pub mod options { + pub static IO_BLOCKS: &str = "io-blocks"; + pub static NO_CREATE: &str = "no-create"; + pub static REFERENCE: &str = "reference"; + pub static SIZE: &str = "size"; +} + +static ARG_FILES: &str = "files"; + +fn get_usage() -> String { + format!("{0} [OPTION]... [FILE]...", executable!()) +} + +fn get_long_usage() -> String { + String::from( + " + SIZE is an integer with an optional prefix and optional unit. + The available units (K, M, G, T, P, E, Z, and Y) use the following format: + 'KB' => 1000 (kilobytes) + 'K' => 1024 (kibibytes) + 'MB' => 1000*1000 (megabytes) + 'M' => 1024*1024 (mebibytes) + 'GB' => 1000*1000*1000 (gigabytes) + 'G' => 1024*1024*1024 (gibibytes) + SIZE may also be prefixed by one of the following to adjust the size of each + file based on its current size: + '+' => extend by + '-' => reduce by + '<' => at most + '>' => at least + '/' => round down to multiple of + '%' => round up to multiple of", + ) +} + pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); + let usage = get_usage(); + let long_usage = get_long_usage(); - let mut opts = getopts::Options::new(); + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(&usage[..]) + .after_help(&long_usage[..]) + .arg( + Arg::with_name(options::IO_BLOCKS) + .short("o") + .long(options::IO_BLOCKS) + .help("treat SIZE as the number of I/O blocks of the file rather than bytes (NOT IMPLEMENTED)") + ) + .arg( + Arg::with_name(options::NO_CREATE) + .short("c") + .long(options::NO_CREATE) + .help("do not create files that do not exist") + ) + .arg( + Arg::with_name(options::REFERENCE) + .short("r") + .long(options::REFERENCE) + .help("base the size of each file on the size of RFILE") + .value_name("RFILE") + ) + .arg( + Arg::with_name(options::SIZE) + .short("s") + .long("size") + .help("set or adjust the size of each file according to SIZE, which is in bytes unless --io-blocks is specified") + .value_name("SIZE") + ) + .arg(Arg::with_name(ARG_FILES).multiple(true).takes_value(true).min_values(1)) + .get_matches_from(args); - opts.optflag("c", "no-create", "do not create files that do not exist"); - opts.optflag( - "o", - "io-blocks", - "treat SIZE as the number of I/O blocks of the file rather than bytes (NOT IMPLEMENTED)", - ); - opts.optopt( - "r", - "reference", - "base the size of each file on the size of RFILE", - "RFILE", - ); - opts.optopt("s", "size", "set or adjust the size of each file according to SIZE, which is in bytes unless --io-blocks is specified", "SIZE"); - opts.optflag("h", "help", "display this help and exit"); - opts.optflag("V", "version", "output version information and exit"); + let files: Vec = matches + .values_of(ARG_FILES) + .map(|v| v.map(ToString::to_string).collect()) + .unwrap_or_default(); - let matches = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(f) => crash!(1, "{}", f), - }; - - if matches.opt_present("help") { - println!("{} {}", NAME, VERSION); - println!(); - println!("Usage:"); - println!(" {} [OPTION]... FILE...", NAME); - println!(); - print!( - "{}", - opts.usage("Shrink or extend the size of each file to the specified size.") - ); - println!( - " -SIZE is an integer with an optional prefix and optional unit. -The available units (K, M, G, T, P, E, Z, and Y) use the following format: - 'KB' => 1000 (kilobytes) - 'K' => 1024 (kibibytes) - 'MB' => 1000*1000 (megabytes) - 'M' => 1024*1024 (mebibytes) - 'GB' => 1000*1000*1000 (gigabytes) - 'G' => 1024*1024*1024 (gibibytes) -SIZE may also be prefixed by one of the following to adjust the size of each -file based on its current size: - '+' => extend by - '-' => reduce by - '<' => at most - '>' => at least - '/' => round down to multiple of - '%' => round up to multiple of" - ); - } else if matches.opt_present("version") { - println!("{} {}", NAME, VERSION); - } else if matches.free.is_empty() { - show_error!("missing an argument"); + if files.is_empty() { + show_error!("Missing an argument"); return 1; } else { - let no_create = matches.opt_present("no-create"); - let io_blocks = matches.opt_present("io-blocks"); - let reference = matches.opt_str("reference"); - let size = matches.opt_str("size"); + let io_blocks = matches.is_present(options::IO_BLOCKS); + let no_create = matches.is_present(options::NO_CREATE); + let reference = matches.value_of(options::REFERENCE).map(String::from); + let size = matches.value_of(options::SIZE).map(String::from); if reference.is_none() && size.is_none() { crash!(1, "you must specify either --reference or --size"); } else { - match truncate(no_create, io_blocks, reference, size, matches.free) { - Ok(()) => ( /* pass */ ), - Err(_) => return 1, - } + truncate(no_create, io_blocks, reference, size, files); } } @@ -114,7 +130,7 @@ fn truncate( reference: Option, size: Option, filenames: Vec, -) -> Result<()> { +) { let (refsize, mode) = match reference { Some(rfilename) => { let _ = match File::open(Path::new(&rfilename)) { @@ -173,7 +189,6 @@ fn truncate( Err(f) => crash!(1, "{}", f.to_string()), } } - Ok(()) } fn parse_size(size: &str) -> (u64, TruncateMode) { diff --git a/src/uu/tsort/Cargo.toml b/src/uu/tsort/Cargo.toml index 74ea51a94..37f543012 100644 --- a/src/uu/tsort/Cargo.toml +++ b/src/uu/tsort/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_tsort" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "tsort ~ (uutils) topologically sort input (partially ordered) pairs" @@ -15,9 +15,9 @@ edition = "2018" path = "src/tsort.rs" [dependencies] -getopts = "0.2.18" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +clap= "2.33" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "tsort" diff --git a/src/uu/tsort/src/tsort.rs b/src/uu/tsort/src/tsort.rs index e8a1010e1..3440972a2 100644 --- a/src/uu/tsort/src/tsort.rs +++ b/src/uu/tsort/src/tsort.rs @@ -6,54 +6,38 @@ // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -extern crate getopts; - #[macro_use] extern crate uucore; +use clap::{App, Arg}; use std::collections::{HashMap, HashSet}; use std::fs::File; use std::io::{stdin, BufRead, BufReader, Read}; use std::path::Path; -static NAME: &str = "tsort"; static VERSION: &str = env!("CARGO_PKG_VERSION"); +static SUMMARY: &str = "Topological sort the strings in FILE. +Strings are defined as any sequence of tokens separated by whitespace (tab, space, or newline). +If FILE is not passed in, stdin is used instead."; +static USAGE: &str = "tsort [OPTIONS] FILE"; + +mod options { + pub const FILE: &str = "file"; +} pub fn uumain(args: impl uucore::Args) -> i32 { let args = args.collect_str(); - let mut opts = getopts::Options::new(); + let matches = App::new(executable!()) + .version(VERSION) + .usage(USAGE) + .about(SUMMARY) + .arg(Arg::with_name(options::FILE).hidden(true)) + .get_matches_from(args); - opts.optflag("h", "help", "display this help and exit"); - opts.optflag("V", "version", "output version information and exit"); - - let matches = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(f) => crash!(1, "{}", f), - }; - - if matches.opt_present("h") { - println!("{} {}", NAME, VERSION); - println!(); - println!("Usage:"); - println!(" {} [OPTIONS] FILE", NAME); - println!(); - println!("{}", opts.usage("Topological sort the strings in FILE. Strings are defined as any sequence of tokens separated by whitespace (tab, space, or newline). If FILE is not passed in, stdin is used instead.")); - return 0; - } - - if matches.opt_present("V") { - println!("{} {}", NAME, VERSION); - return 0; - } - - let files = matches.free.clone(); - let input = if files.len() > 1 { - crash!(1, "{}, extra operand '{}'", NAME, matches.free[1]); - } else if files.is_empty() { - "-".to_owned() - } else { - files[0].clone() + let input = match matches.value_of(options::FILE) { + Some(v) => v, + None => "-", }; let mut stdin_buf; @@ -131,7 +115,7 @@ impl Graph { } fn has_edge(&self, from: &str, to: &str) -> bool { - self.in_edges.get(to).unwrap().contains(from) + self.in_edges[to].contains(from) } fn init_node(&mut self, n: &str) { diff --git a/src/uu/tty/Cargo.toml b/src/uu/tty/Cargo.toml index ea521b431..7be27a900 100644 --- a/src/uu/tty/Cargo.toml +++ b/src/uu/tty/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_tty" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "tty ~ (uutils) display the name of the terminal connected to standard input" @@ -15,10 +15,10 @@ edition = "2018" path = "src/tty.rs" [dependencies] -getopts = "0.2.18" +clap = "2.33" libc = "0.2.42" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary", features=["fs"] } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["fs"] } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "tty" diff --git a/src/uu/tty/src/tty.rs b/src/uu/tty/src/tty.rs index 177fc8668..18d69db46 100644 --- a/src/uu/tty/src/tty.rs +++ b/src/uu/tty/src/tty.rs @@ -9,74 +9,65 @@ // spell-checker:ignore (ToDO) ttyname filedesc -extern crate getopts; -extern crate libc; - #[macro_use] extern crate uucore; +use clap::{App, Arg}; use std::ffi::CStr; use uucore::fs::is_stdin_interactive; -extern "C" { - fn ttyname(filedesc: libc::c_int) -> *const libc::c_char; +static VERSION: &str = env!("CARGO_PKG_VERSION"); +static ABOUT: &str = "Print the file name of the terminal connected to standard input."; + +mod options { + pub const SILENT: &str = "silent"; } -static NAME: &str = "tty"; -static VERSION: &str = env!("CARGO_PKG_VERSION"); +fn get_usage() -> String { + format!("{0} [OPTION]...", executable!()) +} pub fn uumain(args: impl uucore::Args) -> i32 { let args = args.collect_str(); + let usage = get_usage(); - let mut opts = getopts::Options::new(); + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(&usage[..]) + .arg( + Arg::with_name(options::SILENT) + .long(options::SILENT) + .visible_alias("quiet") + .short("s") + .help("print nothing, only return an exit status") + .required(false), + ) + .get_matches_from(args); - opts.optflag("s", "silent", "print nothing, only return an exit status"); - opts.optflag("h", "help", "display this help and exit"); - opts.optflag("V", "version", "output version information and exit"); + let silent = matches.is_present(options::SILENT); - let matches = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(f) => crash!(2, "{}", f), + // Call libc function ttyname + let tty = unsafe { + let ptr = libc::ttyname(libc::STDIN_FILENO); + if !ptr.is_null() { + String::from_utf8_lossy(CStr::from_ptr(ptr).to_bytes()).to_string() + } else { + "".to_owned() + } }; - if matches.opt_present("help") { - println!("{} {}", NAME, VERSION); - println!(); - println!("Usage:"); - println!(" {} [OPTION]...", NAME); - println!(); - print!( - "{}", - opts.usage("Print the file name of the terminal connected to standard input.") - ); - } else if matches.opt_present("version") { - println!("{} {}", NAME, VERSION); - } else { - let silent = matches.opt_present("s"); - - let tty = unsafe { - let ptr = ttyname(libc::STDIN_FILENO); - if !ptr.is_null() { - String::from_utf8_lossy(CStr::from_ptr(ptr).to_bytes()).to_string() - } else { - "".to_owned() - } - }; - - if !silent { - if !tty.chars().all(|c| c.is_whitespace()) { - println!("{}", tty); - } else { - println!("not a tty"); - } - } - - return if is_stdin_interactive() { - libc::EXIT_SUCCESS + if !silent { + if !tty.chars().all(|c| c.is_whitespace()) { + println!("{}", tty); } else { - libc::EXIT_FAILURE - }; + println!("not a tty"); + } } - 0 + return if is_stdin_interactive() { + libc::EXIT_SUCCESS + } else { + libc::EXIT_FAILURE + }; } diff --git a/src/uu/uname/Cargo.toml b/src/uu/uname/Cargo.toml index fd0a7ca82..9707d8444 100644 --- a/src/uu/uname/Cargo.toml +++ b/src/uu/uname/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_uname" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "uname ~ (uutils) display system information" @@ -15,10 +15,10 @@ edition = "2018" path = "src/uname.rs" [dependencies] -clap = "2.32" -platform-info = "0.0.1" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +clap = "2.33" +platform-info = "0.1" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "uname" diff --git a/src/uu/uname/src/uname.rs b/src/uu/uname/src/uname.rs index c81c9cfa1..4586a084f 100644 --- a/src/uu/uname/src/uname.rs +++ b/src/uu/uname/src/uname.rs @@ -10,8 +10,6 @@ // spell-checker:ignore (ToDO) nodename kernelname kernelrelease kernelversion sysname hwplatform mnrsv -extern crate clap; -extern crate platform_info; #[macro_use] extern crate uucore; @@ -21,17 +19,17 @@ use platform_info::*; const VERSION: &str = env!("CARGO_PKG_VERSION"); const ABOUT: &str = "Print certain system information. With no OPTION, same as -s."; -const OPT_ALL: &str = "all"; -const OPT_KERNELNAME: &str = "kernel-name"; -const OPT_NODENAME: &str = "nodename"; -const OPT_KERNELVERSION: &str = "kernel-version"; -const OPT_KERNELRELEASE: &str = "kernel-release"; -const OPT_MACHINE: &str = "machine"; - -//FIXME: unimplemented options -//const OPT_PROCESSOR: &'static str = "processor"; -//const OPT_HWPLATFORM: &'static str = "hardware-platform"; -const OPT_OS: &str = "operating-system"; +pub mod options { + pub static ALL: &str = "all"; + pub static KERNELNAME: &str = "kernel-name"; + pub static NODENAME: &str = "nodename"; + pub static KERNELVERSION: &str = "kernel-version"; + pub static KERNELRELEASE: &str = "kernel-release"; + pub static MACHINE: &str = "machine"; + pub static PROCESSOR: &str = "processor"; + pub static HWPLATFORM: &str = "hardware-platform"; + pub static OS: &str = "operating-system"; +} #[cfg(target_os = "linux")] const HOST_OS: &str = "GNU/Linux"; @@ -41,7 +39,7 @@ const HOST_OS: &str = "Windows NT"; const HOST_OS: &str = "FreeBSD"; #[cfg(target_os = "openbsd")] const HOST_OS: &str = "OpenBSD"; -#[cfg(target_os = "macos")] +#[cfg(target_vendor = "apple")] const HOST_OS: &str = "Darwin"; #[cfg(target_os = "fuchsia")] const HOST_OS: &str = "Fuchsia"; @@ -54,85 +52,105 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .version(VERSION) .about(ABOUT) .usage(&usage[..]) - .arg(Arg::with_name(OPT_ALL) + .arg(Arg::with_name(options::ALL) .short("a") - .long(OPT_ALL) + .long(options::ALL) .help("Behave as though all of the options -mnrsv were specified.")) - .arg(Arg::with_name(OPT_KERNELNAME) + .arg(Arg::with_name(options::KERNELNAME) .short("s") - .long(OPT_KERNELNAME) + .long(options::KERNELNAME) .alias("sysname") // Obsolescent option in GNU uname .help("print the kernel name.")) - .arg(Arg::with_name(OPT_NODENAME) + .arg(Arg::with_name(options::NODENAME) .short("n") - .long(OPT_NODENAME) + .long(options::NODENAME) .help("print the nodename (the nodename may be a name that the system is known by to a communications network).")) - .arg(Arg::with_name(OPT_KERNELRELEASE) + .arg(Arg::with_name(options::KERNELRELEASE) .short("r") - .long(OPT_KERNELRELEASE) + .long(options::KERNELRELEASE) .alias("release") // Obsolescent option in GNU uname .help("print the operating system release.")) - .arg(Arg::with_name(OPT_KERNELVERSION) + .arg(Arg::with_name(options::KERNELVERSION) .short("v") - .long(OPT_KERNELVERSION) + .long(options::KERNELVERSION) .help("print the operating system version.")) - - //FIXME: unimplemented options - // .arg(Arg::with_name(OPT_PROCESSOR) - // .short("p") - // .long(OPT_PROCESSOR) - // .help("print the processor type (non-portable)")) - // .arg(Arg::with_name(OPT_HWPLATFORM) - // .short("i") - // .long(OPT_HWPLATFORM) - // .help("print the hardware platform (non-portable)")) - .arg(Arg::with_name(OPT_MACHINE) + .arg(Arg::with_name(options::HWPLATFORM) + .short("i") + .long(options::HWPLATFORM) + .help("print the hardware platform (non-portable)")) + .arg(Arg::with_name(options::MACHINE) .short("m") - .long(OPT_MACHINE) + .long(options::MACHINE) .help("print the machine hardware name.")) - .arg(Arg::with_name(OPT_OS) + .arg(Arg::with_name(options::PROCESSOR) + .short("p") + .long(options::PROCESSOR) + .help("print the processor type (non-portable)")) + .arg(Arg::with_name(options::OS) .short("o") - .long(OPT_OS) + .long(options::OS) .help("print the operating system name.")) .get_matches_from(args); let uname = return_if_err!(1, PlatformInfo::new()); let mut output = String::new(); - let all = matches.is_present(OPT_ALL); - let kernelname = matches.is_present(OPT_KERNELNAME); - let nodename = matches.is_present(OPT_NODENAME); - let kernelrelease = matches.is_present(OPT_KERNELRELEASE); - let kernelversion = matches.is_present(OPT_KERNELVERSION); - let machine = matches.is_present(OPT_MACHINE); - let os = matches.is_present(OPT_OS); + let all = matches.is_present(options::ALL); + let kernelname = matches.is_present(options::KERNELNAME); + let nodename = matches.is_present(options::NODENAME); + let kernelrelease = matches.is_present(options::KERNELRELEASE); + let kernelversion = matches.is_present(options::KERNELVERSION); + let machine = matches.is_present(options::MACHINE); + let processor = matches.is_present(options::PROCESSOR); + let hwplatform = matches.is_present(options::HWPLATFORM); + let os = matches.is_present(options::OS); - let none = !(all || kernelname || nodename || kernelrelease || kernelversion || machine || os); + let none = !(all + || kernelname + || nodename + || kernelrelease + || kernelversion + || machine + || os + || processor + || hwplatform); if kernelname || all || none { output.push_str(&uname.sysname()); - output.push_str(" "); + output.push(' '); } if nodename || all { output.push_str(&uname.nodename()); - output.push_str(" "); + output.push(' '); } if kernelrelease || all { output.push_str(&uname.release()); - output.push_str(" "); + output.push(' '); } if kernelversion || all { output.push_str(&uname.version()); - output.push_str(" "); + output.push(' '); } if machine || all { output.push_str(&uname.machine()); - output.push_str(" "); + output.push(' '); + } + if processor || all { + // According to https://stackoverflow.com/posts/394271/revisions + // Most of the time, it returns unknown + output.push_str("unknown"); + output.push(' '); + } + if hwplatform || all { + // According to https://lists.gnu.org/archive/html/bug-coreutils/2005-09/msg00063.html + // Most of the time, it returns unknown + output.push_str("unknown"); + output.push(' '); } if os || all { output.push_str(HOST_OS); - output.push_str(" "); + output.push(' '); } println!("{}", output.trim_end()); diff --git a/src/uu/unexpand/Cargo.toml b/src/uu/unexpand/Cargo.toml index 933349a51..e39dd87ca 100644 --- a/src/uu/unexpand/Cargo.toml +++ b/src/uu/unexpand/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_unexpand" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "unexpand ~ (uutils) convert input spaces to tabs" @@ -15,10 +15,10 @@ edition = "2018" path = "src/unexpand.rs" [dependencies] -getopts = "0.2.18" +clap = "2.33" unicode-width = "0.1.5" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "unexpand" diff --git a/src/uu/unexpand/src/unexpand.rs b/src/uu/unexpand/src/unexpand.rs index 1ca12ba87..5b08c33cf 100644 --- a/src/uu/unexpand/src/unexpand.rs +++ b/src/uu/unexpand/src/unexpand.rs @@ -9,12 +9,9 @@ // spell-checker:ignore (ToDO) nums aflag uflag scol prevtab amode ctype cwidth nbytes lastcol pctype -extern crate getopts; -extern crate unicode_width; - #[macro_use] extern crate uucore; - +use clap::{App, Arg}; use std::fs::File; use std::io::{stdin, stdout, BufRead, BufReader, BufWriter, Read, Stdout, Write}; use std::str::from_utf8; @@ -22,14 +19,16 @@ use unicode_width::UnicodeWidthChar; static NAME: &str = "unexpand"; static VERSION: &str = env!("CARGO_PKG_VERSION"); +static USAGE: &str = "unexpand [OPTION]... [FILE]..."; +static SUMMARY: &str = "Convert blanks in each FILE to tabs, writing to standard output.\n + With no FILE, or when FILE is -, read standard input."; const DEFAULT_TABSTOP: usize = 8; fn tabstops_parse(s: String) -> Vec { - let words = s.split(',').collect::>(); + let words = s.split(','); let nums = words - .into_iter() .map(|sn| { sn.parse() .unwrap_or_else(|_| crash!(1, "{}\n", "tab size contains invalid character(s)")) @@ -50,6 +49,14 @@ fn tabstops_parse(s: String) -> Vec { nums } +mod options { + pub const FILE: &str = "file"; + pub const ALL: &str = "all"; + pub const FIRST_ONLY: &str = "first-only"; + pub const TABS: &str = "tabs"; + pub const NO_UTF8: &str = "no-utf8"; +} + struct Options { files: Vec, tabstops: Vec, @@ -58,20 +65,19 @@ struct Options { } impl Options { - fn new(matches: getopts::Matches) -> Options { - let tabstops = match matches.opt_str("t") { + fn new(matches: clap::ArgMatches) -> Options { + let tabstops = match matches.value_of(options::TABS) { None => vec![DEFAULT_TABSTOP], - Some(s) => tabstops_parse(s), + Some(s) => tabstops_parse(s.to_string()), }; - let aflag = (matches.opt_present("all") || matches.opt_present("tabs")) - && !matches.opt_present("first-only"); - let uflag = !matches.opt_present("U"); + let aflag = (matches.is_present(options::ALL) || matches.is_present(options::TABS)) + && !matches.is_present(options::FIRST_ONLY); + let uflag = !matches.is_present(options::NO_UTF8); - let files = if matches.free.is_empty() { - vec!["-".to_owned()] - } else { - matches.free + let files = match matches.value_of(options::FILE) { + Some(v) => vec![v.to_string()], + None => vec!["-".to_owned()], }; Options { @@ -86,60 +92,39 @@ impl Options { pub fn uumain(args: impl uucore::Args) -> i32 { let args = args.collect_str(); - let mut opts = getopts::Options::new(); - - opts.optflag( - "a", - "all", - "convert all blanks, instead of just initial blanks", - ); - opts.optflag( - "", - "first-only", - "convert only leading sequences of blanks (overrides -a)", - ); - opts.optopt( - "t", - "tabs", - "have tabs N characters apart instead of 8 (enables -a)", - "N", - ); - opts.optopt( - "t", - "tabs", - "use comma separated LIST of tab positions (enables -a)", - "LIST", - ); - opts.optflag( - "U", - "no-utf8", - "interpret input file as 8-bit ASCII rather than UTF-8", - ); - opts.optflag("h", "help", "display this help and exit"); - opts.optflag("V", "version", "output version information and exit"); - - let matches = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(f) => crash!(1, "{}", f), - }; - - if matches.opt_present("help") { - println!("{} {}\n", NAME, VERSION); - println!("Usage: {} [OPTION]... [FILE]...\n", NAME); - println!( - "{}", - opts.usage( - "Convert blanks in each FILE to tabs, writing to standard output.\n\ - With no FILE, or when FILE is -, read standard input." - ) - ); - return 0; - } - - if matches.opt_present("V") { - println!("{} {}", NAME, VERSION); - return 0; - } + let matches = App::new(executable!()) + .name(NAME) + .version(VERSION) + .usage(USAGE) + .about(SUMMARY) + .arg(Arg::with_name(options::FILE).hidden(true).multiple(true)) + .arg( + Arg::with_name(options::ALL) + .short("a") + .long(options::ALL) + .help("convert all blanks, instead of just initial blanks") + .takes_value(false), + ) + .arg( + Arg::with_name(options::FIRST_ONLY) + .long(options::FIRST_ONLY) + .help("convert only leading sequences of blanks (overrides -a)") + .takes_value(false), + ) + .arg( + Arg::with_name(options::TABS) + .short("t") + .long(options::TABS) + .long_help("use comma separated LIST of tab positions or have tabs N characters apart instead of 8 (enables -a)") + .takes_value(true) + ) + .arg( + Arg::with_name(options::NO_UTF8) + .short("U") + .long(options::NO_UTF8) + .takes_value(false) + .help("interpret input file as 8-bit ASCII rather than UTF-8")) + .get_matches_from(args); unexpand(Options::new(matches)); diff --git a/src/uu/uniq/Cargo.toml b/src/uu/uniq/Cargo.toml index 6de1088b5..3fe89b450 100644 --- a/src/uu/uniq/Cargo.toml +++ b/src/uu/uniq/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_uniq" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "uniq ~ (uutils) filter identical adjacent lines from input" @@ -15,9 +15,11 @@ edition = "2018" path = "src/uniq.rs" [dependencies] -getopts = "0.2.18" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +clap = "2.33" +strum = "0.20" +strum_macros = "0.20" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "uniq" diff --git a/src/uu/uniq/src/uniq.rs b/src/uu/uniq/src/uniq.rs index 4c4ed1ed4..a61a78a61 100644 --- a/src/uu/uniq/src/uniq.rs +++ b/src/uu/uniq/src/uniq.rs @@ -5,24 +5,40 @@ // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -extern crate getopts; - #[macro_use] extern crate uucore; -use getopts::{Matches, Options}; +use clap::{App, Arg, ArgMatches}; use std::fs::File; -use std::io::{stdin, stdout, BufRead, BufReader, BufWriter, Read, Write}; +use std::io::{stdin, stdout, BufRead, BufReader, BufWriter, Read, Result, Write}; use std::path::Path; use std::str::FromStr; +use strum_macros::{AsRefStr, EnumString}; -static NAME: &str = "uniq"; +static ABOUT: &str = "Report or omit repeated lines."; static VERSION: &str = env!("CARGO_PKG_VERSION"); +pub mod options { + pub static ALL_REPEATED: &str = "all-repeated"; + pub static CHECK_CHARS: &str = "check-chars"; + pub static COUNT: &str = "count"; + pub static IGNORE_CASE: &str = "ignore-case"; + pub static REPEATED: &str = "repeated"; + pub static SKIP_FIELDS: &str = "skip-fields"; + pub static SKIP_CHARS: &str = "skip-chars"; + pub static UNIQUE: &str = "unique"; + pub static ZERO_TERMINATED: &str = "zero-terminated"; + pub static GROUP: &str = "group"; +} -#[derive(PartialEq)] +static ARG_FILES: &str = "files"; + +#[derive(PartialEq, Clone, Copy, AsRefStr, EnumString)] +#[strum(serialize_all = "snake_case")] enum Delimiters { + Append, Prepend, Separate, + Both, None, } @@ -47,44 +63,51 @@ impl Uniq { ) { let mut lines: Vec = vec![]; let mut first_line_printed = false; - let delimiters = &self.delimiters; + let delimiters = self.delimiters; let line_terminator = self.get_line_terminator(); + // Don't print any delimiting lines before, after or between groups if delimiting method is 'none' + let no_delimiters = delimiters == Delimiters::None; + // The 'prepend' and 'both' delimit methods will cause output to start with delimiter line + let prepend_delimiter = delimiters == Delimiters::Prepend || delimiters == Delimiters::Both; + // The 'append' and 'both' delimit methods will cause output to end with delimiter line + let append_delimiter = delimiters == Delimiters::Append || delimiters == Delimiters::Both; - for io_line in reader.split(line_terminator) { - let line = String::from_utf8(crash_if_err!(1, io_line)).unwrap(); + for line in reader.split(line_terminator).map(get_line_string) { if !lines.is_empty() && self.cmp_keys(&lines[0], &line) { - let print_delimiter = delimiters == &Delimiters::Prepend - || (delimiters == &Delimiters::Separate && first_line_printed); + // Print delimiter if delimit method is not 'none' and any line has been output + // before or if we need to start output with delimiter + let print_delimiter = !no_delimiters && (prepend_delimiter || first_line_printed); first_line_printed |= self.print_lines(writer, &lines, print_delimiter); lines.truncate(0); } lines.push(line); } if !lines.is_empty() { - let print_delimiter = delimiters == &Delimiters::Prepend - || (delimiters == &Delimiters::Separate && first_line_printed); - self.print_lines(writer, &lines, print_delimiter); + // Print delimiter if delimit method is not 'none' and any line has been output + // before or if we need to start output with delimiter + let print_delimiter = !no_delimiters && (prepend_delimiter || first_line_printed); + first_line_printed |= self.print_lines(writer, &lines, print_delimiter); + } + if append_delimiter && first_line_printed { + crash_if_err!(1, writer.write_all(&[line_terminator])); } } fn skip_fields<'a>(&self, line: &'a str) -> &'a str { if let Some(skip_fields) = self.skip_fields { - if line.split_whitespace().count() > skip_fields { - let mut field = 0; - let mut i = 0; - while field < skip_fields && i < line.len() { - while i < line.len() && line.chars().nth(i).unwrap().is_whitespace() { - i += 1; - } - while i < line.len() && !line.chars().nth(i).unwrap().is_whitespace() { - i += 1; - } - field += 1; + let mut i = 0; + let mut char_indices = line.char_indices(); + for _ in 0..skip_fields { + if char_indices.find(|(_, c)| !c.is_whitespace()) == None { + return ""; + } + match char_indices.find(|(_, c)| c.is_whitespace()) { + None => return "", + + Some((next_field_i, _)) => i = next_field_i, } - &line[i..] - } else { - "" } + &line[i..] } else { line } @@ -120,10 +143,7 @@ impl Uniq { // fast path: avoid skipping if self.ignore_case && slice_start == 0 && slice_stop == len { - return closure(&mut fields_to_check.chars().map(|c| match c { - 'a'..='z' => ((c as u8) - 32) as char, - _ => c, - })); + return closure(&mut fields_to_check.chars().flat_map(|c| c.to_uppercase())); } // fast path: we can avoid mapping chars to upper-case, if we don't want to ignore the case @@ -136,10 +156,7 @@ impl Uniq { .chars() .skip(slice_start) .take(slice_stop) - .map(|c| match c { - 'a'..='z' => ((c as u8) - 32) as char, - _ => c, - }), + .flat_map(|c| c.to_uppercase()), ) } else { closure(&mut fields_to_check.chars()) @@ -194,116 +211,178 @@ impl Uniq { } } -fn opt_parsed(opt_name: &str, matches: &Matches) -> Option { - matches.opt_str(opt_name).map(|arg_str| { +fn get_line_string(io_line: Result>) -> String { + let line_bytes = crash_if_err!(1, io_line); + crash_if_err!(1, String::from_utf8(line_bytes)) +} + +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)) }) } +fn get_usage() -> String { + format!("{0} [OPTION]... [INPUT [OUTPUT]]...", executable!()) +} + +fn get_long_usage() -> String { + String::from( + "Filter adjacent matching lines from INPUT (or standard input),\n\ + writing to OUTPUT (or standard output). + Note: 'uniq' does not detect repeated lines unless they are adjacent.\n\ + You may want to sort the input first, or use 'sort -u' without 'uniq'.\n", + ) +} + pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); + let usage = get_usage(); + let long_usage = get_long_usage(); - let mut opts = Options::new(); + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(&usage[..]) + .after_help(&long_usage[..]) + .arg( + Arg::with_name(options::ALL_REPEATED) + .short("D") + .long(options::ALL_REPEATED) + .possible_values(&[ + Delimiters::None.as_ref(), Delimiters::Prepend.as_ref(), Delimiters::Separate.as_ref() + ]) + .help("print all duplicate lines. Delimiting is done with blank lines. [default: none]") + .value_name("delimit-method") + .min_values(0) + .max_values(1), + ) + .arg( + Arg::with_name(options::GROUP) + .long(options::GROUP) + .possible_values(&[ + Delimiters::Separate.as_ref(), Delimiters::Prepend.as_ref(), + Delimiters::Append.as_ref(), Delimiters::Both.as_ref() + ]) + .help("show all items, separating groups with an empty line. [default: separate]") + .value_name("group-method") + .min_values(0) + .max_values(1) + .conflicts_with_all(&[ + options::REPEATED, + options::ALL_REPEATED, + options::UNIQUE, + ]), + ) + .arg( + Arg::with_name(options::CHECK_CHARS) + .short("w") + .long(options::CHECK_CHARS) + .help("compare no more than N characters in lines") + .value_name("N"), + ) + .arg( + Arg::with_name(options::COUNT) + .short("c") + .long(options::COUNT) + .help("prefix lines by the number of occurrences"), + ) + .arg( + Arg::with_name(options::IGNORE_CASE) + .short("i") + .long(options::IGNORE_CASE) + .help("ignore differences in case when comparing"), + ) + .arg( + Arg::with_name(options::REPEATED) + .short("d") + .long(options::REPEATED) + .help("only print duplicate lines"), + ) + .arg( + Arg::with_name(options::SKIP_CHARS) + .short("s") + .long(options::SKIP_CHARS) + .help("avoid comparing the first N characters") + .value_name("N"), + ) + .arg( + Arg::with_name(options::SKIP_FIELDS) + .short("f") + .long(options::SKIP_FIELDS) + .help("avoid comparing the first N fields") + .value_name("N"), + ) + .arg( + Arg::with_name(options::UNIQUE) + .short("u") + .long(options::UNIQUE) + .help("only print unique lines"), + ) + .arg( + Arg::with_name(options::ZERO_TERMINATED) + .short("z") + .long(options::ZERO_TERMINATED) + .help("end lines with 0 byte, not newline"), + ) + .arg( + Arg::with_name(ARG_FILES) + .multiple(true) + .takes_value(true) + .max_values(2), + ) + .get_matches_from(args); - opts.optflag("c", "count", "prefix lines by the number of occurrences"); - opts.optflag("d", "repeated", "only print duplicate lines"); - opts.optflagopt( - "D", - "all-repeated", - "print all duplicate lines delimit-method={none(default),prepend,separate} Delimiting is done with blank lines", - "delimit-method" - ); - opts.optopt( - "f", - "skip-fields", - "avoid comparing the first N fields", - "N", - ); - opts.optopt( - "s", - "skip-chars", - "avoid comparing the first N characters", - "N", - ); - opts.optopt( - "w", - "check-chars", - "compare no more than N characters in lines", - "N", - ); - opts.optflag( - "i", - "ignore-case", - "ignore differences in case when comparing", - ); - opts.optflag("u", "unique", "only print unique lines"); - opts.optflag("z", "zero-terminated", "end lines with 0 byte, not newline"); - opts.optflag("h", "help", "display this help and exit"); - opts.optflag("V", "version", "output version information and exit"); + let files: Vec = matches + .values_of(ARG_FILES) + .map(|v| v.map(ToString::to_string).collect()) + .unwrap_or_default(); - let matches = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(f) => crash!(1, "{}", f), + let (in_file_name, out_file_name) = match files.len() { + 0 => ("-".to_owned(), "-".to_owned()), + 1 => (files[0].clone(), "-".to_owned()), + 2 => (files[0].clone(), files[1].clone()), + _ => { + // Cannot happen as clap will fail earlier + crash!(1, "Extra operand: {}", files[2]); + } }; - if matches.opt_present("help") { - println!("{} {}", NAME, VERSION); - println!(); - println!("Usage:"); - println!(" {0} [OPTION]... [FILE]...", NAME); - println!(); - print!( - "{}", - opts.usage( - "Filter adjacent matching lines from INPUT (or standard input),\n\ - writing to OUTPUT (or standard output)." - ) - ); - println!(); - println!( - "Note: '{0}' does not detect repeated lines unless they are adjacent.\n\ - You may want to sort the input first, or use 'sort -u' without '{0}'.\n", - NAME - ); - } else if matches.opt_present("version") { - println!("{} {}", NAME, VERSION); - } else { - let (in_file_name, out_file_name) = match matches.free.len() { - 0 => ("-".to_owned(), "-".to_owned()), - 1 => (matches.free[0].clone(), "-".to_owned()), - 2 => (matches.free[0].clone(), matches.free[1].clone()), - _ => { - crash!(1, "Extra operand: {}", matches.free[2]); - } - }; - let uniq = Uniq { - repeats_only: matches.opt_present("repeated") || matches.opt_present("all-repeated"), - uniques_only: matches.opt_present("unique"), - all_repeated: matches.opt_present("all-repeated"), - delimiters: match matches.opt_default("all-repeated", "none") { - Some(ref opt_arg) if opt_arg != "none" => match &(*opt_arg.as_str()) { - "prepend" => Delimiters::Prepend, - "separate" => Delimiters::Separate, - _ => crash!(1, "Incorrect argument for all-repeated: {}", opt_arg), - }, - _ => Delimiters::None, - }, - show_counts: matches.opt_present("count"), - skip_fields: opt_parsed("skip-fields", &matches), - slice_start: opt_parsed("skip-chars", &matches), - slice_stop: opt_parsed("check-chars", &matches), - ignore_case: matches.opt_present("ignore-case"), - zero_terminated: matches.opt_present("zero-terminated"), - }; - uniq.print_uniq( - &mut open_input_file(in_file_name), - &mut open_output_file(out_file_name), - ); - } + let uniq = Uniq { + repeats_only: matches.is_present(options::REPEATED) + || matches.is_present(options::ALL_REPEATED), + uniques_only: matches.is_present(options::UNIQUE), + all_repeated: matches.is_present(options::ALL_REPEATED) + || matches.is_present(options::GROUP), + delimiters: get_delimiter(&matches), + show_counts: matches.is_present(options::COUNT), + skip_fields: opt_parsed(options::SKIP_FIELDS, &matches), + slice_start: opt_parsed(options::SKIP_CHARS, &matches), + slice_stop: opt_parsed(options::CHECK_CHARS, &matches), + ignore_case: matches.is_present(options::IGNORE_CASE), + zero_terminated: matches.is_present(options::ZERO_TERMINATED), + }; + uniq.print_uniq( + &mut open_input_file(in_file_name), + &mut open_output_file(out_file_name), + ); + 0 } +fn get_delimiter(matches: &ArgMatches) -> Delimiters { + let value = matches + .value_of(options::ALL_REPEATED) + .or_else(|| matches.value_of(options::GROUP)); + if let Some(delimiter_arg) = value { + crash_if_err!(1, Delimiters::from_str(delimiter_arg)) + } else if matches.is_present(options::GROUP) { + Delimiters::Separate + } else { + Delimiters::None + } +} + fn open_input_file(in_file_name: String) -> BufReader> { let in_file = if in_file_name == "-" { Box::new(stdin()) as Box diff --git a/src/uu/unlink/Cargo.toml b/src/uu/unlink/Cargo.toml index 5d13953b9..b193bd1b5 100644 --- a/src/uu/unlink/Cargo.toml +++ b/src/uu/unlink/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_unlink" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "unlink ~ (uutils) remove a (file system) link to FILE" @@ -17,8 +17,8 @@ path = "src/unlink.rs" [dependencies] getopts = "0.2.18" libc = "0.2.42" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "unlink" diff --git a/src/uu/unlink/src/unlink.rs b/src/uu/unlink/src/unlink.rs index 6fad6ca48..b85b6ea94 100644 --- a/src/uu/unlink/src/unlink.rs +++ b/src/uu/unlink/src/unlink.rs @@ -9,9 +9,6 @@ // spell-checker:ignore (ToDO) lstat IFLNK IFMT IFREG -extern crate getopts; -extern crate libc; - #[macro_use] extern crate uucore; diff --git a/src/uu/uptime/Cargo.toml b/src/uu/uptime/Cargo.toml index ea1789ab3..1136e6420 100644 --- a/src/uu/uptime/Cargo.toml +++ b/src/uu/uptime/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_uptime" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "uptime ~ (uutils) display dynamic system information" @@ -15,12 +15,10 @@ edition = "2018" path = "src/uptime.rs" [dependencies] -getopts = "0.2.18" -time = "0.1.40" chrono = "0.4" -clap = "2.32" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary", features=["libc", "utmpx"] } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +clap = "2.33" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["libc", "utmpx"] } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "uptime" diff --git a/src/uu/uptime/src/uptime.rs b/src/uu/uptime/src/uptime.rs index 5c51a535b..418b8317f 100644 --- a/src/uu/uptime/src/uptime.rs +++ b/src/uu/uptime/src/uptime.rs @@ -6,14 +6,8 @@ // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -/* last synced with: cat (GNU coreutils) 8.13 */ - // spell-checker:ignore (ToDO) getloadavg upsecs updays nusers loadavg boottime uphours upmins -extern crate chrono; -extern crate clap; -extern crate time; - use chrono::{Local, TimeZone, Utc}; use clap::{App, Arg}; @@ -25,9 +19,11 @@ use uucore::libc::time_t; static VERSION: &str = env!("CARGO_PKG_VERSION"); 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."; -static OPT_SINCE: &str = "SINCE"; + 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."; +pub mod options { + pub static SINCE: &str = "since"; +} #[cfg(unix)] use uucore::libc::getloadavg; @@ -48,9 +44,9 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .about(ABOUT) .usage(&usage[..]) .arg( - Arg::with_name(OPT_SINCE) + Arg::with_name(options::SINCE) .short("s") - .long("since") + .long(options::SINCE) .help("system up since"), ) .get_matches_from(args); @@ -62,7 +58,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { 1 } else { - if matches.is_present(OPT_SINCE) { + 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; @@ -141,12 +137,9 @@ fn print_nusers(nusers: usize) { } fn print_time() { - let local_time = time::now(); + let local_time = Local::now().time(); - print!( - " {:02}:{:02}:{:02} ", - local_time.tm_hour, local_time.tm_min, local_time.tm_sec - ); + print!(" {} ", local_time.format("%H:%M:%S")); } #[cfg(unix)] @@ -154,25 +147,22 @@ fn get_uptime(boot_time: Option) -> i64 { use std::fs::File; use std::io::Read; - let mut proc_uptime = String::new(); + let mut proc_uptime_s = String::new(); - if let Some(n) = File::open("/proc/uptime") + let proc_uptime = File::open("/proc/uptime") .ok() - .and_then(|mut f| f.read_to_string(&mut proc_uptime).ok()) - .and_then(|_| proc_uptime.split_whitespace().next()) - .and_then(|s| s.split('.').next().unwrap_or("0").parse().ok()) - { - n - } else { - match boot_time { - Some(t) => { - let now = time::get_time().sec; - let boottime = t as i64; - now - boottime - } - _ => -1, + .and_then(|mut f| f.read_to_string(&mut proc_uptime_s).ok()) + .and_then(|_| proc_uptime_s.split_whitespace().next()) + .and_then(|s| s.split('.').next().unwrap_or("0").parse().ok()); + + proc_uptime.unwrap_or_else(|| match boot_time { + Some(t) => { + let now = Local::now().timestamp(); + let boottime = t as i64; + now - boottime } - } + None => -1, + }) } #[cfg(windows)] diff --git a/src/uu/users/Cargo.toml b/src/uu/users/Cargo.toml index e232839b3..84da13020 100644 --- a/src/uu/users/Cargo.toml +++ b/src/uu/users/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_users" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "users ~ (uutils) display names of currently logged-in users" @@ -15,9 +15,9 @@ edition = "2018" path = "src/users.rs" [dependencies] -getopts = "0.2.18" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary", features=["utmpx"] } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +clap = "2.33" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["utmpx"] } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "users" diff --git a/src/uu/users/src/users.rs b/src/uu/users/src/users.rs index 850b3112c..4bb628441 100644 --- a/src/uu/users/src/users.rs +++ b/src/uu/users/src/users.rs @@ -10,59 +10,43 @@ // Allow dead code here in order to keep all fields, constants here, for consistency. #![allow(dead_code)] -extern crate getopts; +#[macro_use] extern crate uucore; use uucore::utmpx::*; -use getopts::Options; +use clap::{App, Arg}; -static NAME: &str = "users"; +static ABOUT: &str = "Display who is currently logged in, according to FILE."; static VERSION: &str = env!("CARGO_PKG_VERSION"); +static ARG_FILES: &str = "files"; + +fn get_usage() -> String { + format!("{0} [FILE]", executable!()) +} + pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); + let usage = get_usage(); - let mut opts = Options::new(); + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(&usage[..]) + .arg(Arg::with_name(ARG_FILES).takes_value(true).max_values(1)) + .get_matches_from(args); - opts.optflag("h", "help", "display this help and exit"); - opts.optflag("V", "version", "output version information and exit"); + let files: Vec = matches + .values_of(ARG_FILES) + .map(|v| v.map(ToString::to_string).collect()) + .unwrap_or_default(); - let matches = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(f) => panic!("{}", f), - }; - - if matches.opt_present("help") { - println!("{} {}", NAME, VERSION); - println!(); - println!("Usage:"); - println!(" {} [OPTION]... [FILE]", NAME); - println!(); - println!( - "{}", - opts.usage("Output who is currently logged in according to FILE.") - ); - return 0; - } - - if matches.opt_present("version") { - println!("{} {}", NAME, VERSION); - return 0; - } - - let filename = if !matches.free.is_empty() { - matches.free[0].as_ref() + let filename = if !files.is_empty() { + files[0].as_ref() } else { DEFAULT_FILE }; - exec(filename); - - 0 -} - -fn exec(filename: &str) { let mut users = Utmpx::iter_all_records() .read_from(filename) .filter(Utmpx::is_user_process) @@ -73,4 +57,6 @@ fn exec(filename: &str) { users.sort(); println!("{}", users.join(" ")); } + + 0 } diff --git a/src/uu/wc/Cargo.toml b/src/uu/wc/Cargo.toml index 84ad1bb95..8ae79dc08 100644 --- a/src/uu/wc/Cargo.toml +++ b/src/uu/wc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_wc" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "wc ~ (uutils) display newline, word, and byte counts for input" @@ -15,9 +15,14 @@ edition = "2018" path = "src/wc.rs" [dependencies] -getopts = "0.2.18" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary" } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +clap = "2.33" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } +thiserror = "1.0" + +[target.'cfg(unix)'.dependencies] +nix = "0.20" +libc = "0.2" [[bin]] name = "wc" diff --git a/src/uu/wc/src/count_bytes.rs b/src/uu/wc/src/count_bytes.rs new file mode 100644 index 000000000..dc90f67cc --- /dev/null +++ b/src/uu/wc/src/count_bytes.rs @@ -0,0 +1,95 @@ +use super::{WcResult, WordCountable}; + +#[cfg(any(target_os = "linux", target_os = "android"))] +use std::fs::OpenOptions; +use std::io::ErrorKind; + +#[cfg(unix)] +use libc::S_IFREG; +#[cfg(unix)] +use nix::sys::stat::fstat; +#[cfg(any(target_os = "linux", target_os = "android"))] +use std::os::unix::io::{AsRawFd, RawFd}; + +#[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; + +const BUF_SIZE: usize = 16384; + +/// This is a Linux-specific function to count the number of bytes using the +/// `splice` system call, which is faster than using `read`. +#[inline] +#[cfg(any(target_os = "linux", target_os = "android"))] +fn count_bytes_using_splice(fd: RawFd) -> nix::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()?; + + 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(pipe_rd, None, null, None, res, SpliceFFlags::empty())?; + } + + Ok(byte_count) +} + +/// In the special case where we only need to count the number of bytes. There +/// are several optimizations we can do: +/// 1. On Unix, we can simply `stat` the file if it is regular. +/// 2. On Linux -- if the above did not work -- we can use splice to count +/// the number of bytes if the file is a FIFO. +/// 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 { + #[cfg(unix)] + { + let fd = handle.as_raw_fd(); + match fstat(fd) { + Ok(stat) => { + // 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); + } + #[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); + } + } + } + } + _ => {} + } + } + + // Fall back on `read`, but without the overhead of counting words and lines. + let mut buf = [0 as u8; BUF_SIZE]; + let mut byte_count = 0; + loop { + match handle.read(&mut buf) { + Ok(0) => return Ok(byte_count), + Ok(n) => { + byte_count += n; + } + Err(ref e) if e.kind() == ErrorKind::Interrupted => continue, + Err(e) => return Err(e.into()), + } + } +} diff --git a/src/uu/wc/src/wc.rs b/src/uu/wc/src/wc.rs index 3aa1b0510..22463caa4 100644 --- a/src/uu/wc/src/wc.rs +++ b/src/uu/wc/src/wc.rs @@ -7,19 +7,34 @@ // spell-checker:ignore (ToDO) fpath -extern crate getopts; - #[macro_use] extern crate uucore; -use getopts::{Matches, Options}; +mod count_bytes; +use count_bytes::count_bytes_fast; +use clap::{App, Arg, ArgMatches}; +use thiserror::Error; + +use std::cmp::max; use std::fs::File; -use std::io::{stdin, BufRead, BufReader, Read}; +use std::io::{self, BufRead, BufReader, Read, StdinLock, Write}; +use std::ops::{Add, AddAssign}; +#[cfg(unix)] +use std::os::unix::io::AsRawFd; use std::path::Path; -use std::result::Result as StdResult; use std::str::from_utf8; +#[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, @@ -29,13 +44,13 @@ struct Settings { } impl Settings { - fn new(matches: &Matches) -> Settings { + fn new(matches: &ArgMatches) -> Settings { let settings = Settings { - show_bytes: matches.opt_present("bytes"), - show_chars: matches.opt_present("chars"), - show_lines: matches.opt_present("lines"), - show_words: matches.opt_present("words"), - show_max_line_length: matches.opt_present("L"), + show_bytes: matches.is_present(options::BYTES), + show_chars: matches.is_present(options::CHAR), + show_lines: matches.is_present(options::LINES), + show_words: matches.is_present(options::WORDS), + show_max_line_length: matches.is_present(options::MAX_LINE_LENGTH), }; if settings.show_bytes @@ -55,10 +70,46 @@ impl Settings { show_max_line_length: false, } } + + fn number_enabled(&self) -> u32 { + let mut result = 0; + result += self.show_bytes as u32; + result += self.show_chars as u32; + result += self.show_lines as u32; + result += self.show_max_line_length as u32; + result += self.show_words as u32; + result + } } -struct Result { - title: String, +#[cfg(unix)] +trait WordCountable: AsRawFd + Read { + type Buffered: BufRead; + fn get_buffered(self) -> Self::Buffered; +} +#[cfg(not(unix))] +trait WordCountable: Read { + type Buffered: BufRead; + fn get_buffered(self) -> Self::Buffered; +} + +impl WordCountable for StdinLock<'_> { + type Buffered = Self; + + fn get_buffered(self) -> Self::Buffered { + self + } +} +impl WordCountable for File { + type Buffered = BufReader; + + fn get_buffered(self) -> Self::Buffered { + BufReader::new(self) + } +} + +#[derive(Debug, Default, Copy, Clone)] +struct WordCount { bytes: usize, chars: usize, lines: usize, @@ -66,224 +117,318 @@ struct Result { max_line_length: usize, } -static NAME: &str = "wc"; +impl Add for WordCount { + type Output = Self; + + fn add(self, other: Self) -> Self { + Self { + bytes: self.bytes + other.bytes, + chars: self.chars + other.chars, + lines: self.lines + other.lines, + words: self.words + other.words, + max_line_length: max(self.max_line_length, other.max_line_length), + } + } +} + +impl AddAssign for WordCount { + fn add_assign(&mut self, other: Self) { + *self = *self + other + } +} + +impl WordCount { + fn with_title<'a>(self, title: &'a str) -> TitledWordCount<'a> { + return TitledWordCount { + title: title, + count: self, + }; + } +} + +/// This struct supplements the actual word count with a title that is displayed +/// to the user at the end of the program. +/// The reason we don't simply include title in the `WordCount` struct is that +/// it would result in unneccesary copying of `String`. +#[derive(Debug, Default, Clone)] +struct TitledWordCount<'a> { + title: &'a str, + count: WordCount, +} + +static ABOUT: &str = "Display newline, word, and byte counts for each FILE, and a total line if +more than one FILE is specified."; static VERSION: &str = env!("CARGO_PKG_VERSION"); +pub mod options { + pub static BYTES: &str = "bytes"; + pub static CHAR: &str = "chars"; + pub static LINES: &str = "lines"; + pub static MAX_LINE_LENGTH: &str = "max-line-length"; + pub static WORDS: &str = "words"; +} + +static ARG_FILES: &str = "files"; + +fn get_usage() -> String { + format!( + "{0} [OPTION]... [FILE]... + With no FILE, or when FILE is -, read standard input.", + executable!() + ) +} + pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); + let usage = get_usage(); - let mut opts = Options::new(); + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(&usage[..]) + .arg( + Arg::with_name(options::BYTES) + .short("c") + .long(options::BYTES) + .help("print the byte counts"), + ) + .arg( + Arg::with_name(options::CHAR) + .short("m") + .long(options::CHAR) + .help("print the character counts"), + ) + .arg( + Arg::with_name(options::LINES) + .short("l") + .long(options::LINES) + .help("print the newline counts"), + ) + .arg( + Arg::with_name(options::MAX_LINE_LENGTH) + .short("L") + .long(options::MAX_LINE_LENGTH) + .help("print the length of the longest line"), + ) + .arg( + Arg::with_name(options::WORDS) + .short("w") + .long(options::WORDS) + .help("print the word counts"), + ) + .arg(Arg::with_name(ARG_FILES).multiple(true).takes_value(true)) + .get_matches_from(args); - opts.optflag("c", "bytes", "print the byte counts"); - opts.optflag("m", "chars", "print the character counts"); - opts.optflag("l", "lines", "print the newline counts"); - opts.optflag( - "L", - "max-line-length", - "print the length of the longest line", - ); - opts.optflag("w", "words", "print the word counts"); - opts.optflag("h", "help", "display this help and exit"); - opts.optflag("V", "version", "output version information and exit"); + let mut files: Vec = matches + .values_of(ARG_FILES) + .map(|v| v.map(ToString::to_string).collect()) + .unwrap_or_default(); - let mut matches = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(f) => crash!(1, "Invalid options\n{}", f), - }; - - if matches.opt_present("help") { - println!("{} {}", NAME, VERSION); - println!(); - println!("Usage:"); - println!(" {0} [OPTION]... [FILE]...", NAME); - println!(); - println!( - "{}", - opts.usage("Print newline, word and byte counts for each FILE") - ); - println!("With no FILE, or when FILE is -, read standard input."); - return 0; - } - - if matches.opt_present("version") { - println!("{} {}", NAME, VERSION); - return 0; - } - - if matches.free.is_empty() { - matches.free.push("-".to_owned()); + if files.is_empty() { + files.push("-".to_owned()); } let settings = Settings::new(&matches); - match wc(matches.free, &settings) { - Ok(()) => ( /* pass */ ), - Err(e) => return e, + if wc(files, &settings).is_ok() { + 0 + } else { + 1 } - - 0 } const CR: u8 = b'\r'; const LF: u8 = b'\n'; const SPACE: u8 = b' '; const TAB: u8 = b'\t'; -const SYN: u8 = 0x16 as u8; -const FF: u8 = 0x0C as u8; +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 } -fn wc(files: Vec, settings: &Settings) -> StdResult<(), i32> { - let mut total_line_count: usize = 0; - let mut total_word_count: usize = 0; - let mut total_char_count: usize = 0; - let mut total_byte_count: usize = 0; - let mut total_longest_line_length: usize = 0; - - let mut results = vec![]; - let mut max_width: usize = 0; +fn word_count_from_reader( + mut reader: T, + settings: &Settings, + path: &String, +) -> WcResult { + 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() + }); + } // 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; + let mut line_count: usize = 0; + let mut word_count: usize = 0; + let mut byte_count: usize = 0; + let mut char_count: usize = 0; + let mut longest_line_length: usize = 0; + let mut raw_line = Vec::new(); + let mut ends_lf: bool; + + // 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 + let mut buffered_reader = reader.get_buffered(); + loop { + match buffered_reader.read_until(LF, &mut raw_line) { + Ok(n) => { + if n == 0 { + break; + } + } + Err(ref e) => { + if !raw_line.is_empty() { + show_warning!("Error while reading {}: {}", path, e); + } else { + break; + } + } + }; + + // GNU 'wc' only counts lines that end in LF as lines + ends_lf = *raw_line.last().unwrap() == LF; + line_count += ends_lf as usize; + + byte_count += raw_line.len(); + + if decode_chars { + // try and convert the bytes to UTF-8 first + let current_char_count; + match from_utf8(&raw_line[..]) { + Ok(line) => { + word_count += line.split_whitespace().count(); + current_char_count = line.chars().count(); + } + Err(..) => { + word_count += raw_line.split(|&x| is_word_separator(x)).count(); + current_char_count = raw_line.iter().filter(|c| c.is_ascii()).count() + } + } + char_count += current_char_count; + if current_char_count > longest_line_length { + // -L is a GNU 'wc' extension so same behavior on LF + longest_line_length = current_char_count - (ends_lf as usize); + } + } + + raw_line.truncate(0); + } + + Ok(WordCount { + bytes: byte_count, + chars: char_count, + lines: line_count, + words: word_count, + max_line_length: longest_line_length, + }) +} + +fn word_count_from_path(path: &String, settings: &Settings) -> WcResult { + if path == "-" { + let stdin = io::stdin(); + let stdin_lock = stdin.lock(); + return Ok(word_count_from_reader(stdin_lock, settings, path)?); + } else { + let path_obj = Path::new(path); + if path_obj.is_dir() { + return Err(WcError::IsDirectory(path.clone())); + } else { + let file = File::open(path)?; + return Ok(word_count_from_reader(file, settings, path)?); + } + } +} + +fn wc(files: Vec, settings: &Settings) -> Result<(), u32> { + let mut total_word_count = WordCount::default(); + let mut results = vec![]; + let mut max_width: usize = 0; + let mut error_count = 0; + + let num_files = files.len(); + for path in &files { - let mut reader = open(&path[..])?; - - let mut line_count: usize = 0; - let mut word_count: usize = 0; - let mut byte_count: usize = 0; - let mut char_count: usize = 0; - let mut longest_line_length: usize = 0; - let mut raw_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 - while match reader.read_until(LF, &mut raw_line) { - Ok(n) if n > 0 => true, - Err(ref e) if !raw_line.is_empty() => { - show_warning!("Error while reading {}: {}", path, e); - !raw_line.is_empty() - } - _ => false, - } { - // GNU 'wc' only counts lines that end in LF as lines - if *raw_line.last().unwrap() == LF { - line_count += 1; - } - - byte_count += raw_line.len(); - - if decode_chars { - // try and convert the bytes to UTF-8 first - let current_char_count; - match from_utf8(&raw_line[..]) { - Ok(line) => { - word_count += line.split_whitespace().count(); - current_char_count = line.chars().count(); - } - Err(..) => { - word_count += raw_line.split(|&x| is_word_separator(x)).count(); - current_char_count = raw_line.iter().filter(|c| c.is_ascii()).count() - } - } - char_count += current_char_count; - - if current_char_count > longest_line_length { - // we subtract one here because `line.len()` includes the LF - // matches GNU 'wc' behavior - longest_line_length = current_char_count - 1; - } - } - - raw_line.truncate(0); - } - - results.push(Result { - title: path.clone(), - bytes: byte_count, - chars: char_count, - lines: line_count, - words: word_count, - max_line_length: longest_line_length, + let word_count = word_count_from_path(&path, settings).unwrap_or_else(|err| { + show_error!("{}", err); + error_count += 1; + WordCount::default() }); - - total_line_count += line_count; + max_width = max(max_width, word_count.bytes.to_string().len() + 1); total_word_count += word_count; - total_char_count += char_count; - total_byte_count += byte_count; - - if longest_line_length > total_longest_line_length { - total_longest_line_length = longest_line_length; - } - - // used for formatting - max_width = total_byte_count.to_string().len() + 1; + results.push(word_count.with_title(path)); } for result in &results { - print_stats(settings, &result, max_width); + if let Err(err) = print_stats(settings, &result, max_width) { + show_warning!("failed to print result for {}: {}", result.title, err); + error_count += 1; + } } - if files.len() > 1 { - let result = Result { - title: "total".to_owned(), - bytes: total_byte_count, - chars: total_char_count, - lines: total_line_count, - words: total_word_count, - max_line_length: total_longest_line_length, - }; - print_stats(settings, &result, max_width); + if num_files > 1 { + let total_result = total_word_count.with_title("total"); + if let Err(err) = print_stats(settings, &total_result, max_width) { + show_warning!("failed to print total: {}", err); + error_count += 1; + } + } + + if error_count == 0 { + Ok(()) + } else { + Err(error_count) + } +} + +fn print_stats( + settings: &Settings, + result: &TitledWordCount, + mut min_width: usize, +) -> WcResult<()> { + 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; + } + + if settings.show_lines { + write!(stdout_lock, "{:1$}", result.count.lines, min_width)?; + } + if settings.show_words { + write!(stdout_lock, "{:1$}", result.count.words, min_width)?; + } + if settings.show_bytes { + write!(stdout_lock, "{:1$}", result.count.bytes, min_width)?; + } + if settings.show_chars { + write!(stdout_lock, "{:1$}", result.count.chars, min_width)?; + } + if settings.show_max_line_length { + write!( + stdout_lock, + "{:1$}", + result.count.max_line_length, min_width + )?; + } + + if result.title == "-" { + writeln!(stdout_lock, "")?; + } else { + writeln!(stdout_lock, " {}", result.title)?; } Ok(()) } - -fn print_stats(settings: &Settings, result: &Result, max_width: usize) { - if settings.show_lines { - print!("{:1$}", result.lines, max_width); - } - if settings.show_words { - print!("{:1$}", result.words, max_width); - } - if settings.show_bytes { - print!("{:1$}", result.bytes, max_width); - } - if settings.show_chars { - print!("{:1$}", result.chars, max_width); - } - if settings.show_max_line_length { - print!("{:1$}", result.max_line_length, max_width); - } - - if result.title != "-" { - println!(" {}", result.title); - } else { - println!(); - } -} - -fn open(path: &str) -> StdResult>, i32> { - if "-" == path { - let reader = Box::new(stdin()) as Box; - return Ok(BufReader::new(reader)); - } - - let fpath = Path::new(path); - if fpath.is_dir() { - show_info!("{}: is a directory", path); - } - match File::open(&fpath) { - Ok(fd) => { - let reader = Box::new(fd) as Box; - Ok(BufReader::new(reader)) - } - Err(e) => { - show_error!("wc: {}: {}", path, e); - Err(1) - } - } -} diff --git a/src/uu/who/Cargo.toml b/src/uu/who/Cargo.toml index 6b3646356..c0cd63795 100644 --- a/src/uu/who/Cargo.toml +++ b/src/uu/who/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_who" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "who ~ (uutils) display information about currently logged-in users" @@ -15,8 +15,8 @@ edition = "2018" path = "src/who.rs" [dependencies] -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary", features=["utmpx"] } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["utmpx"] } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] name = "who" diff --git a/src/uu/who/src/who.rs b/src/uu/who/src/who.rs index b028be0a0..8c7ff3211 100644 --- a/src/uu/who/src/who.rs +++ b/src/uu/who/src/who.rs @@ -60,12 +60,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { "count", "all login names and number of users logged on", ); - #[cfg(any( - target_os = "macos", - target_os = "ios", - target_os = "linux", - target_os = "android" - ))] + #[cfg(any(target_vendor = "apple", target_os = "linux", target_os = "android"))] opts.optflag("r", "runlevel", "print current runlevel"); opts.optflag("s", "short", "print only name, line, and time (default)"); opts.optflag("t", "time", "print last system clock change"); @@ -305,12 +300,7 @@ impl Who { #[allow(unused_assignments)] let mut res = false; - #[cfg(any( - target_os = "macos", - target_os = "ios", - target_os = "linux", - target_os = "android" - ))] + #[cfg(any(target_vendor = "apple", target_os = "linux", target_os = "android"))] { res = record == utmpx::RUN_LVL; } diff --git a/src/uu/whoami/Cargo.toml b/src/uu/whoami/Cargo.toml index ecda1ae2d..f8dc01440 100644 --- a/src/uu/whoami/Cargo.toml +++ b/src/uu/whoami/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_whoami" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "whoami ~ (uutils) display user name of current effective user ID" @@ -15,10 +15,12 @@ edition = "2018" path = "src/whoami.rs" [dependencies] +clap = "2.33" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["entries", "wide"] } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } + +[target.'cfg(target_os = "windows")'.dependencies] advapi32-sys = "0.2.0" -clap = "2.32" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary", features=["entries", "wide"] } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } winapi = { version = "0.3", features = ["lmcons"] } [[bin]] diff --git a/src/uu/whoami/src/platform/windows.rs b/src/uu/whoami/src/platform/windows.rs index 7f9490a76..1d65281bd 100644 --- a/src/uu/whoami/src/platform/windows.rs +++ b/src/uu/whoami/src/platform/windows.rs @@ -9,8 +9,6 @@ // spell-checker:ignore (ToDO) advapi lmcons winnt getusername WCHAR UNLEN -extern crate advapi32; -extern crate uucore; extern crate winapi; use self::winapi::shared::lmcons; diff --git a/src/uu/yes/Cargo.toml b/src/uu/yes/Cargo.toml index 1424a15ed..4a843ddd8 100644 --- a/src/uu/yes/Cargo.toml +++ b/src/uu/yes/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_yes" -version = "0.0.1" +version = "0.0.6" authors = ["uutils developers"] license = "MIT" description = "yes ~ (uutils) repeatedly display a line with STRING (or 'y')" @@ -15,9 +15,9 @@ edition = "2018" path = "src/yes.rs" [dependencies] -clap = "2.32" -uucore = { version="0.0.4", package="uucore", git="https://github.com/uutils/uucore.git", branch="canary", features=["zero-copy"] } -uucore_procs = { version="0.0.4", package="uucore_procs", git="https://github.com/uutils/uucore.git", branch="canary" } +clap = "2.33" +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["zero-copy"] } +uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [features] default = [] diff --git a/src/uu/yes/src/yes.rs b/src/uu/yes/src/yes.rs index 7932a1486..1fc2d92bc 100644 --- a/src/uu/yes/src/yes.rs +++ b/src/uu/yes/src/yes.rs @@ -77,8 +77,8 @@ fn prepare_buffer<'a>(input: &'a str, _buffer: &'a mut [u8; BUF_SIZE]) -> &'a [u } pub fn exec(bytes: &[u8]) { - let mut stdin_raw = io::stdout(); - let mut writer = ZeroCopyWriter::with_default(&mut stdin_raw, |stdin| stdin.lock()); + 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(); diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml new file mode 100644 index 000000000..855e64b36 --- /dev/null +++ b/src/uucore/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "uucore" +version = "0.0.8" +authors = ["uutils developers"] +license = "MIT" +description = "uutils ~ 'core' uutils code library (cross-platform)" + +homepage = "https://github.com/uutils/coreutils" +repository = "https://github.com/uutils/coreutils/tree/master/src/uu/arch" +# readme = "README.md" +keywords = ["coreutils", "uutils", "cross-platform", "cli", "utility"] +categories = ["command-line-utilities"] +edition = "2018" + +[lib] +path="src/lib/lib.rs" + +[dependencies] +dunce = "1.0.0" +getopts = "<= 0.2.21" +wild = "2.0.4" +# * 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.42", optional=true } +# * "problem" dependencies (pinned) +data-encoding = { version="~2.1", optional=true } ## data-encoding: require v2.1; but v2.2.0 breaks the build for MinSRV v1.31.0 +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 + +[target.'cfg(target_os = "redox")'.dependencies] +termion = "1.5" + +[features] +default = [] +# * non-default features +encoding = ["data-encoding", "thiserror"] +entries = ["libc"] +fs = ["libc"] +mode = ["libc"] +parse_time = [] +perms = ["libc"] +process = ["libc"] +signals = [] +utf8 = [] +utmpx = ["time", "libc"] +wide = [] +zero-copy = ["nix", "libc", "lazy_static", "platform-info"] diff --git a/src/uucore/src/lib/features.rs b/src/uucore/src/lib/features.rs new file mode 100644 index 000000000..c26225cb7 --- /dev/null +++ b/src/uucore/src/lib/features.rs @@ -0,0 +1,36 @@ +// features ~ feature-gated modules (core/bundler file) + +#[cfg(feature = "encoding")] +pub mod encoding; +#[cfg(feature = "fs")] +pub mod fs; +#[cfg(feature = "parse_time")] +pub mod parse_time; +#[cfg(feature = "zero-copy")] +pub mod zero_copy; + +// * (platform-specific) feature-gated modules +// ** non-windows +#[cfg(all(not(windows), feature = "mode"))] +pub mod mode; + +// ** unix-only +#[cfg(all(unix, feature = "entries"))] +pub mod entries; +#[cfg(all(unix, feature = "perms"))] +pub mod perms; +#[cfg(all(unix, feature = "process"))] +pub mod process; + +#[cfg(all(unix, not(target_os = "fuchsia"), feature = "signals"))] +pub mod signals; +#[cfg(all( + unix, + not(target_os = "fuchsia"), + not(target_env = "musl"), + feature = "utmpx" +))] +pub mod utmpx; +// ** windows-only +#[cfg(all(windows, feature = "wide"))] +pub mod wide; diff --git a/src/uucore/src/lib/features/encoding.rs b/src/uucore/src/lib/features/encoding.rs new file mode 100644 index 000000000..03fa0ed8b --- /dev/null +++ b/src/uucore/src/lib/features/encoding.rs @@ -0,0 +1,121 @@ +// This file is part of the uutils coreutils package. +// +// (c) Jian Zeng +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +// spell-checker:ignore (strings) ABCDEFGHIJKLMNOPQRSTUVWXYZ + +extern crate data_encoding; + +use self::data_encoding::{DecodeError, BASE32, BASE64}; + +use std::io::{self, Read, Write}; + +#[cfg(feature = "thiserror")] +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum EncodingError { + #[error("{}", _0)] + Decode(#[from] DecodeError), + #[error("{}", _0)] + Io(#[from] io::Error), +} + +pub type DecodeResult = Result, EncodingError>; + +#[derive(Clone, Copy)] +pub enum Format { + Base32, + Base64, +} +use self::Format::*; + +pub fn encode(f: Format, input: &[u8]) -> String { + match f { + Base32 => BASE32.encode(input), + Base64 => BASE64.encode(input), + } +} + +pub fn decode(f: Format, input: &[u8]) -> DecodeResult { + Ok(match f { + Base32 => BASE32.decode(input)?, + Base64 => BASE64.decode(input)?, + }) +} + +pub struct Data { + line_wrap: usize, + ignore_garbage: bool, + input: R, + format: Format, + alphabet: &'static [u8], +} + +impl Data { + pub fn new(input: R, format: Format) -> Self { + Data { + line_wrap: 76, + ignore_garbage: false, + input, + format, + alphabet: match format { + Base32 => b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567=", + Base64 => b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789=+/", + }, + } + } + + pub fn line_wrap(mut self, wrap: usize) -> Self { + self.line_wrap = wrap; + self + } + + pub fn ignore_garbage(mut self, ignore: bool) -> Self { + self.ignore_garbage = ignore; + self + } + + pub fn decode(&mut self) -> DecodeResult { + let mut buf = vec![]; + self.input.read_to_end(&mut buf)?; + if self.ignore_garbage { + buf.retain(|c| self.alphabet.contains(c)); + } else { + buf.retain(|&c| c != b'\r' && c != b'\n'); + }; + decode(self.format, &buf) + } + + pub fn encode(&mut self) -> String { + let mut buf: Vec = vec![]; + self.input.read_to_end(&mut buf).unwrap(); + encode(self.format, buf.as_slice()) + } +} + +// NOTE: this will likely be phased out at some point +pub fn wrap_print(data: &Data, res: String) { + let stdout = io::stdout(); + wrap_write(stdout.lock(), data.line_wrap, res).unwrap(); +} + +pub fn wrap_write(mut writer: W, line_wrap: usize, res: String) -> io::Result<()> { + use std::cmp::min; + + if line_wrap == 0 { + return write!(writer, "{}", res); + } + + let mut start = 0; + while start < res.len() { + let end = min(start + line_wrap, res.len()); + writeln!(writer, "{}", &res[start..end])?; + start = end; + } + + Ok(()) +} diff --git a/src/uucore/src/lib/features/entries.rs b/src/uucore/src/lib/features/entries.rs new file mode 100644 index 000000000..d2dce2461 --- /dev/null +++ b/src/uucore/src/lib/features/entries.rs @@ -0,0 +1,270 @@ +// This file is part of the uutils coreutils package. +// +// (c) Jian Zeng +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +// spell-checker:ignore (vars) Passwd cstr fnam gecos ngroups + +//! Get password/group file entry +//! +//! # Examples: +//! +//! ``` +//! use uucore::entries::{self, Locate}; +//! +//! let root_group = if cfg!(any(target_os = "linux", target_os = "android")) { +//! "root" +//! } else { +//! "wheel" +//! }; +//! +//! assert_eq!("root", entries::uid2usr(0).unwrap()); +//! assert_eq!(0, entries::usr2uid("root").unwrap()); +//! assert!(entries::gid2grp(0).is_ok()); +//! assert!(entries::grp2gid(root_group).is_ok()); +//! +//! assert!(entries::Passwd::locate(0).is_ok()); +//! assert!(entries::Passwd::locate("0").is_ok()); +//! assert!(entries::Passwd::locate("root").is_ok()); +//! +//! assert!(entries::Group::locate(0).is_ok()); +//! assert!(entries::Group::locate("0").is_ok()); +//! assert!(entries::Group::locate(root_group).is_ok()); +//! ``` + +#[cfg(any(target_os = "freebsd", target_vendor = "apple"))] +use libc::time_t; +use libc::{c_char, c_int, gid_t, uid_t}; +use libc::{getgrgid, getgrnam, getgroups, getpwnam, getpwuid, group, passwd}; + +use std::borrow::Cow; +use std::ffi::{CStr, CString}; +use std::io::Error as IOError; +use std::io::ErrorKind; +use std::io::Result as IOResult; +use std::ptr; + +extern "C" { + fn getgrouplist( + name: *const c_char, + gid: gid_t, + groups: *mut gid_t, + ngroups: *mut c_int, + ) -> c_int; +} + +pub fn get_groups() -> IOResult> { + let ngroups = unsafe { getgroups(0, ptr::null_mut()) }; + if ngroups == -1 { + return Err(IOError::last_os_error()); + } + let mut groups = Vec::with_capacity(ngroups as usize); + let ngroups = unsafe { getgroups(ngroups, groups.as_mut_ptr()) }; + if ngroups == -1 { + Err(IOError::last_os_error()) + } else { + unsafe { + groups.set_len(ngroups as usize); + } + Ok(groups) + } +} + +pub struct Passwd { + inner: passwd, +} + +macro_rules! cstr2cow { + ($v:expr) => { + unsafe { CStr::from_ptr($v).to_string_lossy() } + }; +} + +impl Passwd { + /// AKA passwd.pw_name + pub fn name(&self) -> Cow { + cstr2cow!(self.inner.pw_name) + } + + /// AKA passwd.pw_uid + pub fn uid(&self) -> uid_t { + self.inner.pw_uid + } + + /// AKA passwd.pw_gid + pub fn gid(&self) -> gid_t { + self.inner.pw_gid + } + + /// AKA passwd.pw_gecos + pub fn user_info(&self) -> Cow { + cstr2cow!(self.inner.pw_gecos) + } + + /// AKA passwd.pw_shell + pub fn user_shell(&self) -> Cow { + cstr2cow!(self.inner.pw_shell) + } + + /// AKA passwd.pw_dir + pub fn user_dir(&self) -> Cow { + cstr2cow!(self.inner.pw_dir) + } + + /// AKA passwd.pw_passwd + pub fn user_passwd(&self) -> Cow { + cstr2cow!(self.inner.pw_passwd) + } + + /// AKA passwd.pw_class + #[cfg(any(target_os = "freebsd", target_vendor = "apple"))] + pub fn user_access_class(&self) -> Cow { + cstr2cow!(self.inner.pw_class) + } + + /// AKA passwd.pw_change + #[cfg(any(target_os = "freebsd", target_vendor = "apple"))] + pub fn passwd_change_time(&self) -> time_t { + self.inner.pw_change + } + + /// AKA passwd.pw_expire + #[cfg(any(target_os = "freebsd", target_vendor = "apple"))] + pub fn expiration(&self) -> time_t { + self.inner.pw_expire + } + + pub fn as_inner(&self) -> &passwd { + &self.inner + } + + pub fn into_inner(self) -> passwd { + self.inner + } + + pub fn belongs_to(&self) -> Vec { + let mut ngroups: c_int = 8; + let mut groups = Vec::with_capacity(ngroups as usize); + let gid = self.inner.pw_gid; + let name = self.inner.pw_name; + unsafe { + if getgrouplist(name, gid, groups.as_mut_ptr(), &mut ngroups) == -1 { + groups.resize(ngroups as usize, 0); + getgrouplist(name, gid, groups.as_mut_ptr(), &mut ngroups); + } + groups.set_len(ngroups as usize); + } + groups.truncate(ngroups as usize); + groups + } +} + +pub struct Group { + inner: group, +} + +impl Group { + /// AKA group.gr_name + pub fn name(&self) -> Cow { + cstr2cow!(self.inner.gr_name) + } + + /// AKA group.gr_gid + pub fn gid(&self) -> gid_t { + self.inner.gr_gid + } + + pub fn as_inner(&self) -> &group { + &self.inner + } + + pub fn into_inner(self) -> group { + self.inner + } +} + +/// Fetch desired entry. +pub trait Locate { + fn locate(key: K) -> IOResult + where + Self: ::std::marker::Sized; +} + +macro_rules! f { + ($fnam:ident, $fid:ident, $t:ident, $st:ident) => { + impl Locate<$t> for $st { + fn locate(k: $t) -> IOResult { + unsafe { + let data = $fid(k); + if !data.is_null() { + Ok($st { + inner: ptr::read(data as *const _), + }) + } else { + Err(IOError::new( + ErrorKind::NotFound, + format!("No such id: {}", k), + )) + } + } + } + } + + impl<'a> Locate<&'a str> for $st { + fn locate(k: &'a str) -> IOResult { + if let Ok(id) = k.parse::<$t>() { + let data = unsafe { $fid(id) }; + if !data.is_null() { + Ok($st { + inner: unsafe { ptr::read(data as *const _) }, + }) + } else { + Err(IOError::new( + ErrorKind::NotFound, + format!("No such id: {}", id), + )) + } + } else { + unsafe { + let data = $fnam(CString::new(k).unwrap().as_ptr()); + if !data.is_null() { + Ok($st { + inner: ptr::read(data as *const _), + }) + } else { + Err(IOError::new( + ErrorKind::NotFound, + format!("Not found: {}", k), + )) + } + } + } + } + } + }; +} + +f!(getpwnam, getpwuid, uid_t, Passwd); +f!(getgrnam, getgrgid, gid_t, Group); + +#[inline] +pub fn uid2usr(id: uid_t) -> IOResult { + Passwd::locate(id).map(|p| p.name().into_owned()) +} + +#[inline] +pub fn gid2grp(id: gid_t) -> IOResult { + Group::locate(id).map(|p| p.name().into_owned()) +} + +#[inline] +pub fn usr2uid(name: &str) -> IOResult { + Passwd::locate(name).map(|p| p.uid()) +} + +#[inline] +pub fn grp2gid(name: &str) -> IOResult { + Group::locate(name).map(|p| p.gid()) +} diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs new file mode 100644 index 000000000..a72d6ea82 --- /dev/null +++ b/src/uucore/src/lib/features/fs.rs @@ -0,0 +1,358 @@ +// This file is part of the uutils coreutils package. +// +// (c) Joseph Crail +// (c) Jian Zeng +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +#[cfg(unix)] +use libc::{ + mode_t, S_IRGRP, S_IROTH, S_IRUSR, S_ISGID, S_ISUID, S_ISVTX, S_IWGRP, S_IWOTH, S_IWUSR, + S_IXGRP, S_IXOTH, S_IXUSR, +}; +use std::borrow::Cow; +use std::env; +use std::fs; +#[cfg(target_os = "redox")] +use std::io; +use std::io::Result as IOResult; +use std::io::{Error, ErrorKind}; +#[cfg(any(unix, target_os = "redox"))] +use std::os::unix::fs::MetadataExt; +use std::path::{Component, Path, PathBuf}; + +#[cfg(unix)] +macro_rules! has { + ($mode:expr, $perm:expr) => { + $mode & ($perm as u32) != 0 + }; +} + +pub fn resolve_relative_path(path: &Path) -> Cow { + if path.components().all(|e| e != Component::ParentDir) { + return path.into(); + } + let root = Component::RootDir.as_os_str(); + let mut result = env::current_dir().unwrap_or_else(|_| PathBuf::from(root)); + for comp in path.components() { + match comp { + Component::ParentDir => { + if let Ok(p) = result.read_link() { + result = p; + } + result.pop(); + } + Component::CurDir => (), + Component::RootDir | Component::Normal(_) | Component::Prefix(_) => { + result.push(comp.as_os_str()) + } + } + } + result.into() +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum CanonicalizeMode { + None, + Normal, + Existing, + Missing, +} + +// 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 +// replace this once that lands +pub fn normalize_path(path: &Path) -> PathBuf { + let mut components = path.components().peekable(); + let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() { + components.next(); + PathBuf::from(c.as_os_str()) + } else { + PathBuf::new() + }; + + for component in components { + match component { + Component::Prefix(..) => unreachable!(), + Component::RootDir => { + ret.push(component.as_os_str()); + } + Component::CurDir => {} + Component::ParentDir => { + ret.pop(); + } + Component::Normal(c) => { + ret.push(c); + } + } + } + ret +} + +fn resolve>(original: P) -> IOResult { + const MAX_LINKS_FOLLOWED: u32 = 255; + let mut followed = 0; + let mut result = original.as_ref().to_path_buf(); + loop { + if followed == MAX_LINKS_FOLLOWED { + return Err(Error::new( + ErrorKind::InvalidInput, + "maximum links followed", + )); + } + + match fs::symlink_metadata(&result) { + Err(e) => return Err(e), + Ok(ref m) if !m.file_type().is_symlink() => break, + Ok(..) => { + followed += 1; + match fs::read_link(&result) { + Ok(path) => { + result.pop(); + result.push(path); + } + Err(e) => { + return Err(e); + } + } + } + } + } + Ok(result) +} + +pub fn canonicalize>(original: P, can_mode: CanonicalizeMode) -> IOResult { + // Create an absolute path + let original = original.as_ref(); + let original = if original.is_absolute() { + original.to_path_buf() + } else { + dunce::canonicalize(env::current_dir().unwrap()) + .unwrap() + .join(original) + }; + + let mut result = PathBuf::new(); + let mut parts = vec![]; + + // Split path by directory separator; add prefix (Windows-only) and root + // directory to final path buffer; add remaining parts to temporary + // vector for canonicalization. + for part in original.components() { + match part { + Component::Prefix(_) | Component::RootDir => { + result.push(part.as_os_str()); + } + Component::CurDir => (), + Component::ParentDir => { + parts.pop(); + } + Component::Normal(_) => { + parts.push(part.as_os_str()); + } + } + } + + // Resolve the symlinks where possible + if !parts.is_empty() { + for part in parts[..parts.len() - 1].iter() { + result.push(part); + + if can_mode == CanonicalizeMode::None { + continue; + } + + match resolve(&result) { + Err(e) => match can_mode { + CanonicalizeMode::Missing => continue, + _ => return Err(e), + }, + Ok(path) => { + result.pop(); + result.push(path); + } + } + } + + result.push(parts.last().unwrap()); + + match resolve(&result) { + Err(e) => { + if can_mode == CanonicalizeMode::Existing { + return Err(e); + } + } + Ok(path) => { + result.pop(); + result.push(path); + } + } + } + Ok(result) +} + +#[cfg(unix)] +pub fn is_stdin_interactive() -> bool { + unsafe { libc::isatty(libc::STDIN_FILENO) == 1 } +} + +#[cfg(windows)] +pub fn is_stdin_interactive() -> bool { + false +} + +#[cfg(target_os = "redox")] +pub fn is_stdin_interactive() -> bool { + termion::is_tty(&io::stdin()) +} + +#[cfg(unix)] +pub fn is_stdout_interactive() -> bool { + unsafe { libc::isatty(libc::STDOUT_FILENO) == 1 } +} + +#[cfg(windows)] +pub fn is_stdout_interactive() -> bool { + false +} + +#[cfg(target_os = "redox")] +pub fn is_stdout_interactive() -> bool { + termion::is_tty(&io::stdout()) +} + +#[cfg(unix)] +pub fn is_stderr_interactive() -> bool { + unsafe { libc::isatty(libc::STDERR_FILENO) == 1 } +} + +#[cfg(windows)] +pub fn is_stderr_interactive() -> bool { + false +} + +#[cfg(target_os = "redox")] +pub fn is_stderr_interactive() -> bool { + termion::is_tty(&io::stderr()) +} + +#[cfg(not(unix))] +#[allow(unused_variables)] +pub fn display_permissions(metadata: &fs::Metadata) -> String { + String::from("---------") +} + +#[cfg(unix)] +pub fn display_permissions(metadata: &fs::Metadata) -> String { + let mode: mode_t = metadata.mode() as mode_t; + display_permissions_unix(mode as u32) +} + +#[cfg(unix)] +pub fn display_permissions_unix(mode: u32) -> String { + let mut result = String::with_capacity(9); + result.push(if has!(mode, S_IRUSR) { 'r' } else { '-' }); + result.push(if has!(mode, S_IWUSR) { 'w' } else { '-' }); + result.push(if has!(mode, S_ISUID) { + if has!(mode, S_IXUSR) { + 's' + } else { + 'S' + } + } else if has!(mode, S_IXUSR) { + 'x' + } else { + '-' + }); + + result.push(if has!(mode, S_IRGRP) { 'r' } else { '-' }); + result.push(if has!(mode, S_IWGRP) { 'w' } else { '-' }); + result.push(if has!(mode, S_ISGID) { + if has!(mode, S_IXGRP) { + 's' + } else { + 'S' + } + } else if has!(mode, S_IXGRP) { + 'x' + } else { + '-' + }); + + result.push(if has!(mode, S_IROTH) { 'r' } else { '-' }); + result.push(if has!(mode, S_IWOTH) { 'w' } else { '-' }); + result.push(if has!(mode, S_ISVTX) { + if has!(mode, S_IXOTH) { + 't' + } else { + 'T' + } + } else if has!(mode, S_IXOTH) { + 'x' + } else { + '-' + }); + + result +} + +#[cfg(test)] +mod tests { + // Note this useful idiom: importing names from outer (for mod tests) scope. + use super::*; + + struct NormalizePathTestCase<'a> { + path: &'a str, + test: &'a str, + } + + const NORMALIZE_PATH_TESTS: [NormalizePathTestCase; 8] = [ + NormalizePathTestCase { + path: "./foo/bar.txt", + test: "foo/bar.txt", + }, + NormalizePathTestCase { + path: "bar/../foo/bar.txt", + test: "foo/bar.txt", + }, + NormalizePathTestCase { + path: "foo///bar.txt", + test: "foo/bar.txt", + }, + NormalizePathTestCase { + path: "foo///bar", + test: "foo/bar", + }, + NormalizePathTestCase { + path: "foo//./bar", + test: "foo/bar", + }, + NormalizePathTestCase { + path: "/foo//./bar", + test: "/foo/bar", + }, + NormalizePathTestCase { + path: r"C:/you/later/", + test: "C:/you/later", + }, + NormalizePathTestCase { + path: "\\networkshare/a//foo//./bar", + test: "\\networkshare/a/foo/bar", + }, + ]; + + #[test] + fn test_normalize_path() { + for test in NORMALIZE_PATH_TESTS.iter() { + let path = Path::new(test.path); + let normalized = normalize_path(path); + assert_eq!( + test.test + .replace("/", std::path::MAIN_SEPARATOR.to_string().as_str()), + normalized.to_str().expect("Path is not valid utf-8!") + ); + } + } +} diff --git a/src/uucore/src/lib/features/mode.rs b/src/uucore/src/lib/features/mode.rs new file mode 100644 index 000000000..8b5e71799 --- /dev/null +++ b/src/uucore/src/lib/features/mode.rs @@ -0,0 +1,131 @@ +// This file is part of the uutils coreutils package. +// +// (c) Alex Lyon +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +// spell-checker:ignore (vars) fperm srwx + +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)) + } 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()), + } + } +} + +pub fn parse_symbolic( + mut fperm: u32, + mut mode: &str, + considering_dir: bool, +) -> Result { + #[cfg(unix)] + use libc::umask; + + #[cfg(target_os = "redox")] + unsafe fn umask(_mask: u32) -> u32 { + // XXX Redox does not currently have umask + 0 + } + + 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)?; + mode = &mode[pos..]; + let (mut srwx, pos) = parse_change(mode, fperm, considering_dir); + if respect_umask { + srwx &= !(last_umask as u32); + } + mode = &mode[pos..]; + match op { + '+' => fperm |= srwx & mask, + '-' => fperm &= !(srwx & mask), + '=' => fperm = (fperm & !mask) | (srwx & mask), + _ => unreachable!(), + } + } + unsafe { + umask(last_umask); + } + Ok(fperm) +} + +fn parse_levels(mode: &str) -> (u32, usize) { + let mut mask = 0; + let mut pos = 0; + for ch in mode.chars() { + mask |= match ch { + 'u' => 0o7700, + 'g' => 0o7070, + 'o' => 0o7007, + 'a' => 0o7777, + _ => break, + }; + pos += 1; + } + if pos == 0 { + mask = 0o7777; // default to 'a' + } + (mask, pos) +} + +fn parse_op(mode: &str, default: Option) -> Result<(char, usize), String> { + match mode.chars().next() { + Some(ch) => match ch { + '+' | '-' | '=' => Ok((ch, 1)), + _ => match default { + Some(ch) => Ok((ch, 0)), + None => Err(format!( + "invalid operator (expected +, -, or =, but found {})", + ch + )), + }, + }, + None => Err("unexpected end of mode".to_owned()), + } +} + +fn parse_change(mode: &str, fperm: u32, considering_dir: bool) -> (u32, usize) { + let mut srwx = fperm & 0o7000; + let mut pos = 0; + for ch in mode.chars() { + match ch { + 'r' => srwx |= 0o444, + 'w' => srwx |= 0o222, + 'x' => srwx |= 0o111, + 'X' => { + if considering_dir || (fperm & 0o0111) != 0 { + srwx |= 0o111 + } + } + 's' => srwx |= 0o4000 | 0o2000, + 't' => srwx |= 0o1000, + 'u' => srwx = (fperm & 0o700) | ((fperm >> 3) & 0o070) | ((fperm >> 6) & 0o007), + 'g' => srwx = ((fperm << 3) & 0o700) | (fperm & 0o070) | ((fperm >> 3) & 0o007), + 'o' => srwx = ((fperm << 6) & 0o700) | ((fperm << 3) & 0o070) | (fperm & 0o007), + _ => break, + }; + pos += 1; + } + if pos == 0 { + srwx = 0; + } + (srwx, pos) +} diff --git a/src/uucore/src/lib/features/parse_time.rs b/src/uucore/src/lib/features/parse_time.rs new file mode 100644 index 000000000..8e822685b --- /dev/null +++ b/src/uucore/src/lib/features/parse_time.rs @@ -0,0 +1,43 @@ +// This file is part of the uutils coreutils package. +// +// (c) Alex Lyon +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +// spell-checker:ignore (vars) NANOS numstr + +use std::time::Duration; + +pub fn from_str(string: &str) -> Result { + let len = string.len(); + if len == 0 { + return Err("empty string".to_owned()); + } + let slice = &string[..len - 1]; + let (numstr, times) = match string.chars().next_back().unwrap() { + 's' | 'S' => (slice, 1), + 'm' | 'M' => (slice, 60), + 'h' | 'H' => (slice, 60 * 60), + 'd' | 'D' => (slice, 60 * 60 * 24), + val => { + if !val.is_alphabetic() { + (string, 1) + } else if string == "inf" || string == "infinity" { + ("inf", 1) + } else { + return Err(format!("invalid time interval '{}'", string)); + } + } + }; + let num = match numstr.parse::() { + Ok(m) => m, + Err(e) => return Err(format!("invalid time interval '{}': {}", string, e)), + }; + + const NANOS_PER_SEC: u32 = 1_000_000_000; + let whole_secs = num.trunc(); + let nanos = (num.fract() * (NANOS_PER_SEC as f64)).trunc(); + let duration = Duration::new(whole_secs as u64, nanos as u32); + Ok(duration * times) +} diff --git a/src/uucore/src/lib/features/perms.rs b/src/uucore/src/lib/features/perms.rs new file mode 100644 index 000000000..66db15451 --- /dev/null +++ b/src/uucore/src/lib/features/perms.rs @@ -0,0 +1,182 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +pub use crate::features::entries; +use libc::{self, gid_t, lchown, uid_t}; + +use std::io::Error as IOError; +use std::io::Result as IOResult; + +use std::ffi::CString; +use std::fs::Metadata; +use std::os::unix::fs::MetadataExt; + +use std::os::unix::ffi::OsStrExt; +use std::path::Path; + +/// The various level of verbosity +#[derive(PartialEq, Clone, Debug)] +pub enum Verbosity { + Silent, + Changes, + Verbose, + Normal, +} + +/// Actually perform the change of group on a path +fn chgrp>(path: P, dgid: 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 as gid_t).wrapping_sub(1), dgid) + } else { + lchown(s.as_ptr(), (0 as gid_t).wrapping_sub(1), dgid) + } + }; + 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: gid_t, + follow: bool, + verbosity: Verbosity, +) -> Result { + use self::Verbosity::*; + let path = path.as_ref(); + let mut out: String = String::new(); + + 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() + ); + } + } + Ok(out) +} + +/// Actually perform the change of owner on a path +fn chown>(path: P, duid: uid_t, dgid: 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(), duid, dgid) + } else { + lchown(s.as_ptr(), duid, dgid) + } + }; + if ret == 0 { + Ok(()) + } else { + Err(IOError::last_os_error()) + } +} + +/// Perform the change of owner on a path +/// with the various options +/// and error messages management +pub fn wrap_chown>( + path: P, + meta: &Metadata, + dest_uid: Option, + dest_gid: Option, + 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() + ); + }; + } + } + return Err(out); + } 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() + ); + } + _ => (), + }; + } else if verbosity == Verbose { + out = format!( + "ownership of '{}' retained as {}:{}", + path.display(), + entries::uid2usr(dest_uid).unwrap(), + entries::gid2grp(dest_gid).unwrap() + ); + } + } + Ok(out) +} diff --git a/src/uucore/src/lib/features/process.rs b/src/uucore/src/lib/features/process.rs new file mode 100644 index 000000000..078b782f5 --- /dev/null +++ b/src/uucore/src/lib/features/process.rs @@ -0,0 +1,129 @@ +// This file is part of the uutils coreutils package. +// +// (c) Maciej Dziardziel +// (c) Jian Zeng +// +// For the full copyright and license information, please view the LICENSE file +// that was distributed with this source code. + +// spell-checker:ignore (vars) cvar exitstatus +// spell-checker:ignore (sys/unix) WIFSIGNALED + +use libc::{gid_t, pid_t, uid_t}; +use std::fmt; +use std::io; +use std::process::Child; +use std::process::ExitStatus as StdExitStatus; +use std::thread; +use std::time::{Duration, Instant}; + +pub fn geteuid() -> uid_t { + unsafe { libc::geteuid() } +} + +pub fn getegid() -> gid_t { + unsafe { libc::getegid() } +} + +pub fn getgid() -> gid_t { + unsafe { libc::getgid() } +} + +pub fn getuid() -> uid_t { + unsafe { libc::getuid() } +} + +// This is basically sys::unix::process::ExitStatus +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +pub enum ExitStatus { + Code(i32), + Signal(i32), +} + +impl ExitStatus { + fn from_std_status(status: StdExitStatus) -> Self { + #[cfg(unix)] + { + use std::os::unix::process::ExitStatusExt; + + if let Some(signal) = status.signal() { + return ExitStatus::Signal(signal); + } + } + + // NOTE: this should never fail as we check if the program exited through a signal above + ExitStatus::Code(status.code().unwrap()) + } + + pub fn success(&self) -> bool { + match *self { + ExitStatus::Code(code) => code == 0, + _ => false, + } + } + + pub fn code(&self) -> Option { + match *self { + ExitStatus::Code(code) => Some(code), + _ => None, + } + } + + pub fn signal(&self) -> Option { + match *self { + ExitStatus::Signal(code) => Some(code), + _ => None, + } + } +} + +impl fmt::Display for ExitStatus { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + ExitStatus::Code(code) => write!(f, "exit code: {}", code), + ExitStatus::Signal(code) => write!(f, "exit code: {}", code), + } + } +} + +/// Missing methods for Child objects +pub trait ChildExt { + /// Send a signal to a Child process. + fn send_signal(&mut self, signal: usize) -> io::Result<()>; + + /// Wait for a process to finish or return after the specified duration. + fn wait_or_timeout(&mut self, timeout: Duration) -> io::Result>; +} + +impl ChildExt for Child { + fn send_signal(&mut self, signal: usize) -> io::Result<()> { + if unsafe { libc::kill(self.id() as pid_t, signal as i32) } != 0 { + Err(io::Error::last_os_error()) + } else { + Ok(()) + } + } + + fn wait_or_timeout(&mut self, timeout: Duration) -> io::Result> { + // .try_wait() doesn't drop stdin, so we do it manually + drop(self.stdin.take()); + + let start = Instant::now(); + loop { + if let Some(status) = self.try_wait()? { + return Ok(Some(ExitStatus::from_std_status(status))); + } + + if start.elapsed() >= timeout { + break; + } + + // XXX: this is kinda gross, but it's cleaner than starting a thread just to wait + // (which was the previous solution). We might want to use a different duration + // here as well + thread::sleep(Duration::from_millis(100)); + } + + Ok(None) + } +} diff --git a/src/uucore/src/lib/features/signals.rs b/src/uucore/src/lib/features/signals.rs new file mode 100644 index 000000000..d22fa1791 --- /dev/null +++ b/src/uucore/src/lib/features/signals.rs @@ -0,0 +1,390 @@ +// This file is part of the uutils coreutils package. +// +// (c) Maciej Dziardziel +// +// For the full copyright and license information, please view the LICENSE file +// that was distributed with this source code. + +// spell-checker:ignore (vars/api) fcntl setrlimit setitimer +// spell-checker:ignore (vars/signals) ABRT ALRM CHLD SEGV SIGABRT SIGALRM SIGBUS SIGCHLD SIGCONT SIGEMT SIGFPE SIGHUP SIGILL SIGINFO SIGINT SIGIO SIGIOT SIGKILL SIGPIPE SIGPROF SIGQUIT SIGSEGV SIGSTOP SIGSYS SIGTERM SIGTRAP SIGTSTP SIGTTIN SIGTTOU SIGURG SIGUSR SIGVTALRM SIGWINCH SIGXCPU SIGXFSZ STKFLT TSTP TTIN TTOU VTALRM XCPU XFSZ + +pub static DEFAULT_SIGNAL: usize = 15; + +pub struct Signal<'a> { + pub name: &'a str, + pub value: usize, +} + +/* + +Linux Programmer's Manual + + 1 HUP 2 INT 3 QUIT 4 ILL 5 TRAP 6 ABRT 7 BUS + 8 FPE 9 KILL 10 USR1 11 SEGV 12 USR2 13 PIPE 14 ALRM +15 TERM 16 STKFLT 17 CHLD 18 CONT 19 STOP 20 TSTP 21 TTIN +22 TTOU 23 URG 24 XCPU 25 XFSZ 26 VTALRM 27 PROF 28 WINCH +29 POLL 30 PWR 31 SYS + + +*/ + +#[cfg(target_os = "linux")] +pub static ALL_SIGNALS: [Signal<'static>; 31] = [ + Signal { + name: "HUP", + value: 1, + }, + Signal { + name: "INT", + value: 2, + }, + Signal { + name: "QUIT", + value: 3, + }, + Signal { + name: "ILL", + value: 4, + }, + Signal { + name: "TRAP", + value: 5, + }, + Signal { + name: "ABRT", + value: 6, + }, + Signal { + name: "BUS", + value: 7, + }, + Signal { + name: "FPE", + value: 8, + }, + Signal { + name: "KILL", + value: 9, + }, + Signal { + name: "USR1", + value: 10, + }, + Signal { + name: "SEGV", + value: 11, + }, + Signal { + name: "USR2", + value: 12, + }, + Signal { + name: "PIPE", + value: 13, + }, + Signal { + name: "ALRM", + value: 14, + }, + Signal { + name: "TERM", + value: 15, + }, + Signal { + name: "STKFLT", + value: 16, + }, + Signal { + name: "CHLD", + value: 17, + }, + Signal { + name: "CONT", + value: 18, + }, + Signal { + name: "STOP", + value: 19, + }, + Signal { + name: "TSTP", + value: 20, + }, + Signal { + name: "TTIN", + value: 21, + }, + Signal { + name: "TTOU", + value: 22, + }, + Signal { + name: "URG", + value: 23, + }, + Signal { + name: "XCPU", + value: 24, + }, + Signal { + name: "XFSZ", + value: 25, + }, + Signal { + name: "VTALRM", + value: 26, + }, + Signal { + name: "PROF", + value: 27, + }, + Signal { + name: "WINCH", + value: 28, + }, + Signal { + name: "POLL", + value: 29, + }, + Signal { + name: "PWR", + value: 30, + }, + Signal { + name: "SYS", + value: 31, + }, +]; + +/* + + +https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man3/signal.3.html + + +No Name Default Action Description +1 SIGHUP terminate process terminal line hangup +2 SIGINT terminate process interrupt program +3 SIGQUIT create core image quit program +4 SIGILL create core image illegal instruction +5 SIGTRAP create core image trace trap +6 SIGABRT create core image abort program (formerly SIGIOT) +7 SIGEMT create core image emulate instruction executed +8 SIGFPE create core image floating-point exception +9 SIGKILL terminate process kill program +10 SIGBUS create core image bus error +11 SIGSEGV create core image segmentation violation +12 SIGSYS create core image non-existent system call invoked +13 SIGPIPE terminate process write on a pipe with no reader +14 SIGALRM terminate process real-time timer expired +15 SIGTERM terminate process software termination signal +16 SIGURG discard signal urgent condition present on socket +17 SIGSTOP stop process stop (cannot be caught or ignored) +18 SIGTSTP stop process stop signal generated from keyboard +19 SIGCONT discard signal continue after stop +20 SIGCHLD discard signal child status has changed +21 SIGTTIN stop process background read attempted from control terminal +22 SIGTTOU stop process background write attempted to control terminal +23 SIGIO discard signal I/O is possible on a descriptor (see fcntl(2)) +24 SIGXCPU terminate process cpu time limit exceeded (see setrlimit(2)) +25 SIGXFSZ terminate process file size limit exceeded (see setrlimit(2)) +26 SIGVTALRM terminate process virtual time alarm (see setitimer(2)) +27 SIGPROF terminate process profiling timer alarm (see setitimer(2)) +28 SIGWINCH discard signal Window size change +29 SIGINFO discard signal status request from keyboard +30 SIGUSR1 terminate process User defined signal 1 +31 SIGUSR2 terminate process User defined signal 2 + +*/ + +#[cfg(any(target_vendor = "apple", target_os = "freebsd"))] +pub static ALL_SIGNALS: [Signal<'static>; 31] = [ + Signal { + name: "HUP", + value: 1, + }, + Signal { + name: "INT", + value: 2, + }, + Signal { + name: "QUIT", + value: 3, + }, + Signal { + name: "ILL", + value: 4, + }, + Signal { + name: "TRAP", + value: 5, + }, + Signal { + name: "ABRT", + value: 6, + }, + Signal { + name: "EMT", + value: 7, + }, + Signal { + name: "FPE", + value: 8, + }, + Signal { + name: "KILL", + value: 9, + }, + Signal { + name: "BUS", + value: 10, + }, + Signal { + name: "SEGV", + value: 11, + }, + Signal { + name: "SYS", + value: 12, + }, + Signal { + name: "PIPE", + value: 13, + }, + Signal { + name: "ALRM", + value: 14, + }, + Signal { + name: "TERM", + value: 15, + }, + Signal { + name: "URG", + value: 16, + }, + Signal { + name: "STOP", + value: 17, + }, + Signal { + name: "TSTP", + value: 18, + }, + Signal { + name: "CONT", + value: 19, + }, + Signal { + name: "CHLD", + value: 20, + }, + Signal { + name: "TTIN", + value: 21, + }, + Signal { + name: "TTOU", + value: 22, + }, + Signal { + name: "IO", + value: 23, + }, + Signal { + name: "XCPU", + value: 24, + }, + Signal { + name: "XFSZ", + value: 25, + }, + Signal { + name: "VTALRM", + value: 26, + }, + Signal { + name: "PROF", + value: 27, + }, + Signal { + name: "WINCH", + value: 28, + }, + Signal { + name: "INFO", + value: 29, + }, + Signal { + name: "USR1", + value: 30, + }, + Signal { + name: "USR2", + value: 31, + }, +]; + +pub fn signal_by_name_or_value(signal_name_or_value: &str) -> Option { + if let Ok(value) = signal_name_or_value.parse() { + if is_signal(value) { + return Some(value); + } else { + return None; + } + } + let signal_name = signal_name_or_value.trim_start_matches("SIG"); + + ALL_SIGNALS + .iter() + .find(|s| s.name == signal_name) + .map(|s| s.value) +} + +#[inline(always)] +pub fn is_signal(num: usize) -> bool { + // Named signals start at 1 + num <= ALL_SIGNALS.len() +} + +#[test] +fn signals_all_contiguous() { + for (i, signal) in ALL_SIGNALS.iter().enumerate() { + assert_eq!(signal.value, i + 1); + } +} + +#[test] +fn signals_all_are_signal() { + for signal in &ALL_SIGNALS { + assert!(is_signal(signal.value)); + } +} + +#[test] +fn signal_by_value() { + assert_eq!(signal_by_name_or_value("0"), Some(0)); + for signal in &ALL_SIGNALS { + assert_eq!( + signal_by_name_or_value(&signal.value.to_string()), + Some(signal.value) + ); + } +} + +#[test] +fn signal_by_short_name() { + for signal in &ALL_SIGNALS { + assert_eq!(signal_by_name_or_value(signal.name), Some(signal.value)); + } +} + +#[test] +fn signal_by_long_name() { + for signal in &ALL_SIGNALS { + assert_eq!( + signal_by_name_or_value(&format!("SIG{}", signal.name)), + Some(signal.value) + ); + } +} diff --git a/src/uucore/src/lib/features/utmpx.rs b/src/uucore/src/lib/features/utmpx.rs new file mode 100644 index 000000000..0308d8a5e --- /dev/null +++ b/src/uucore/src/lib/features/utmpx.rs @@ -0,0 +1,274 @@ +// This file is part of the uutils coreutils package. +// +// (c) Jian Zeng +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. +// +//! Aims to provide platform-independent methods to obtain login records +//! +//! **ONLY** support linux, macos and freebsd for the time being +//! +//! # Examples: +//! +//! ``` +//! use uucore::utmpx::Utmpx; +//! for ut in Utmpx::iter_all_records() { +//! if ut.is_user_process() { +//! println!("{}: {}", ut.host(), ut.user()) +//! } +//! } +//! ``` +//! +//! Specifying the path to login record: +//! +//! ``` +//! use uucore::utmpx::Utmpx; +//! for ut in Utmpx::iter_all_records().read_from("/some/where/else") { +//! if ut.is_user_process() { +//! println!("{}: {}", ut.host(), ut.user()) +//! } +//! } +//! ``` + +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::ptr; + +pub use self::ut::*; +use libc::utmpx; +// pub use libc::getutxid; +// pub use libc::getutxline; +// pub use libc::pututxline; +pub use libc::endutxent; +pub use libc::getutxent; +pub use libc::setutxent; +#[cfg(any(target_vendor = "apple", target_os = "linux"))] +pub use libc::utmpxname; +#[cfg(target_os = "freebsd")] +pub unsafe extern "C" fn utmpxname(_file: *const libc::c_char) -> libc::c_int { + 0 +} + +// In case the c_char array doesn't end with NULL +macro_rules! chars2string { + ($arr:expr) => { + $arr.iter() + .take_while(|i| **i > 0) + .map(|&i| i as u8 as char) + .collect::() + }; +} + +#[cfg(target_os = "linux")] +mod ut { + pub static DEFAULT_FILE: &str = "/var/run/utmp"; + + pub use libc::__UT_HOSTSIZE as UT_HOSTSIZE; + pub use libc::__UT_LINESIZE as UT_LINESIZE; + pub use libc::__UT_NAMESIZE as UT_NAMESIZE; + pub const UT_IDSIZE: usize = 4; + + pub use libc::ACCOUNTING; + pub use libc::BOOT_TIME; + 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::USER_PROCESS; +} + +#[cfg(target_vendor = "apple")] +mod ut { + pub static DEFAULT_FILE: &str = "/var/run/utmpx"; + + 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::BOOT_TIME; + 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::SHUTDOWN_TIME; + pub use libc::SIGNATURE; + pub use libc::USER_PROCESS; +} + +#[cfg(target_os = "freebsd")] +mod ut { + pub static DEFAULT_FILE: &str = ""; + + pub const UT_LINESIZE: usize = 16; + pub const UT_NAMESIZE: usize = 32; + pub const UT_IDSIZE: usize = 8; + pub const UT_HOSTSIZE: usize = 128; + + pub use libc::BOOT_TIME; + 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::SHUTDOWN_TIME; + pub use libc::USER_PROCESS; +} + +pub struct Utmpx { + inner: utmpx, +} + +impl Utmpx { + /// A.K.A. ut.ut_type + pub fn record_type(&self) -> i16 { + self.inner.ut_type as i16 + } + /// A.K.A. ut.ut_pid + pub fn pid(&self) -> i32 { + self.inner.ut_pid as i32 + } + /// A.K.A. ut.ut_id + pub fn terminal_suffix(&self) -> String { + chars2string!(self.inner.ut_id) + } + /// A.K.A. ut.ut_user + pub fn user(&self) -> String { + chars2string!(self.inner.ut_user) + } + /// A.K.A. ut.ut_host + pub fn host(&self) -> String { + chars2string!(self.inner.ut_host) + } + /// A.K.A. ut.ut_line + pub fn tty_device(&self) -> String { + chars2string!(self.inner.ut_line) + } + /// A.K.A. ut.ut_tv + pub fn login_time(&self) -> Tm { + time::at(Timespec::new( + self.inner.ut_tv.tv_sec as i64, + self.inner.ut_tv.tv_usec as i32, + )) + } + /// A.K.A. ut.ut_exit + /// + /// Return (e_termination, e_exit) + #[cfg(any(target_os = "linux", target_os = "android"))] + pub fn exit_status(&self) -> (i16, i16) { + (self.inner.ut_exit.e_termination, self.inner.ut_exit.e_exit) + } + /// A.K.A. ut.ut_exit + /// + /// Return (0, 0) on Non-Linux platform + #[cfg(not(any(target_os = "linux", target_os = "android")))] + pub fn exit_status(&self) -> (i16, i16) { + (0, 0) + } + /// Consumes the `Utmpx`, returning the underlying C struct utmpx + pub fn into_inner(self) -> utmpx { + self.inner + } + pub fn is_user_process(&self) -> bool { + !self.user().is_empty() && self.record_type() == USER_PROCESS + } + + /// Canonicalize host name using DNS + pub fn canon_host(&self) -> IOResult { + const AI_CANONNAME: libc::c_int = 0x2; + let host = self.host(); + let host = host.split(':').next().unwrap(); + let hints = libc::addrinfo { + ai_flags: AI_CANONNAME, + ai_family: 0, + ai_socktype: 0, + ai_protocol: 0, + ai_addrlen: 0, + ai_addr: ptr::null_mut(), + ai_canonname: ptr::null_mut(), + ai_next: ptr::null_mut(), + }; + let c_host = CString::new(host).unwrap(); + let mut res = ptr::null_mut(); + let status = unsafe { + libc::getaddrinfo( + c_host.as_ptr(), + ptr::null(), + &hints as *const _, + &mut res as *mut _, + ) + }; + if status == 0 { + let info: libc::addrinfo = unsafe { ptr::read(res as *const _) }; + // http://lists.gnu.org/archive/html/bug-coreutils/2006-09/msg00300.html + // says Darwin 7.9.0 getaddrinfo returns 0 but sets + // res->ai_canonname to NULL. + let ret = if info.ai_canonname.is_null() { + Ok(String::from(host)) + } else { + Ok(unsafe { CString::from_raw(info.ai_canonname).into_string().unwrap() }) + }; + unsafe { + libc::freeaddrinfo(res); + } + ret + } else { + Err(IOError::last_os_error()) + } + } + pub fn iter_all_records() -> UtmpxIter { + UtmpxIter + } +} + +/// Iterator of login records +pub struct UtmpxIter; + +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 cstr = CString::new(f).unwrap(); + utmpxname(cstr.as_ptr()) + }; + if res != 0 { + println!("Warning: {}", IOError::last_os_error()); + } + unsafe { + setutxent(); + } + self + } +} + +impl Iterator for UtmpxIter { + type Item = Utmpx; + fn next(&mut self) -> Option { + unsafe { + let res = getutxent(); + if !res.is_null() { + Some(Utmpx { + inner: ptr::read(res as *const _), + }) + } else { + endutxent(); + None + } + } + } +} diff --git a/src/uucore/src/lib/features/wide.rs b/src/uucore/src/lib/features/wide.rs new file mode 100644 index 000000000..49ce575a7 --- /dev/null +++ b/src/uucore/src/lib/features/wide.rs @@ -0,0 +1,39 @@ +// This file is part of the uutils coreutils package. +// +// (c) Peter Atashian +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use std::ffi::{OsStr, OsString}; +use std::os::windows::ffi::{OsStrExt, OsStringExt}; +pub trait ToWide { + fn to_wide(&self) -> Vec; + fn to_wide_null(&self) -> Vec; +} +impl ToWide for T +where + T: AsRef, +{ + fn to_wide(&self) -> Vec { + self.as_ref().encode_wide().collect() + } + fn to_wide_null(&self) -> Vec { + self.as_ref().encode_wide().chain(Some(0)).collect() + } +} +pub trait FromWide { + fn from_wide(wide: &[u16]) -> Self; + fn from_wide_null(wide: &[u16]) -> Self; +} +impl FromWide for String { + fn from_wide(wide: &[u16]) -> String { + OsString::from_wide(wide).to_string_lossy().into_owned() + } + fn from_wide_null(wide: &[u16]) -> String { + let len = wide.iter().take_while(|&&c| c != 0).count(); + OsString::from_wide(&wide[..len]) + .to_string_lossy() + .into_owned() + } +} diff --git a/src/uucore/src/lib/features/zero_copy.rs b/src/uucore/src/lib/features/zero_copy.rs new file mode 100644 index 000000000..1eb2c1547 --- /dev/null +++ b/src/uucore/src/lib/features/zero_copy.rs @@ -0,0 +1,148 @@ +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 new file mode 100644 index 000000000..67e4354c5 --- /dev/null +++ b/src/uucore/src/lib/features/zero_copy/platform.rs @@ -0,0 +1,21 @@ +#[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 new file mode 100644 index 000000000..47239a361 --- /dev/null +++ b/src/uucore/src/lib/features/zero_copy/platform/default.rs @@ -0,0 +1,21 @@ +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 new file mode 100644 index 000000000..e2bed3061 --- /dev/null +++ b/src/uucore/src/lib/features/zero_copy/platform/linux.rs @@ -0,0 +1,113 @@ +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 new file mode 100644 index 000000000..553549c9b --- /dev/null +++ b/src/uucore/src/lib/features/zero_copy/platform/unix.rs @@ -0,0 +1,18 @@ +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 new file mode 100644 index 000000000..8134bfda3 --- /dev/null +++ b/src/uucore/src/lib/features/zero_copy/platform/windows.rs @@ -0,0 +1,19 @@ +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 new file mode 100644 index 000000000..324095b6a --- /dev/null +++ b/src/uucore/src/lib/lib.rs @@ -0,0 +1,86 @@ +// library ~ (core/bundler file) + +// 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; +#[cfg(feature = "winapi")] +pub extern crate winapi; + +//## internal modules + +mod macros; // crate macros (macro_rules-type; exported to `crate::...`) + +mod features; // feature-gated code modules +mod mods; // core cross-platform modules + +// * cross-platform modules +pub use crate::mods::coreopts; +pub use crate::mods::panic; +pub use crate::mods::ranges; + +// * feature-gated modules +#[cfg(feature = "encoding")] +pub use crate::features::encoding; +#[cfg(feature = "fs")] +pub use crate::features::fs; +#[cfg(feature = "parse_time")] +pub use crate::features::parse_time; +#[cfg(feature = "zero-copy")] +pub use crate::features::zero_copy; + +// * (platform-specific) feature-gated modules +// ** non-windows +#[cfg(all(not(windows), feature = "mode"))] +pub use crate::features::mode; +// ** unix-only +#[cfg(all(unix, feature = "entries"))] +pub use crate::features::entries; +#[cfg(all(unix, feature = "perms"))] +pub use crate::features::perms; +#[cfg(all(unix, feature = "process"))] +pub use crate::features::process; +#[cfg(all(unix, not(target_os = "fuchsia"), feature = "signals"))] +pub use crate::features::signals; +#[cfg(all( + unix, + not(target_os = "fuchsia"), + not(target_env = "musl"), + feature = "utmpx" +))] +pub use crate::features::utmpx; +// ** windows-only +#[cfg(all(windows, feature = "wide"))] +pub use crate::features::wide; + +//## core functions + +use std::ffi::OsString; + +pub trait Args: Iterator + Sized { + fn collect_str(self) -> Vec { + // FIXME: avoid unwrap() + self.map(|s| s.into_string().unwrap()).collect() + } +} + +impl + Sized> Args for T {} + +// args() ... +pub fn args() -> impl Iterator { + wild::args() +} + +pub fn args_os() -> impl Iterator { + wild::args_os() +} diff --git a/src/uucore/src/lib/macros.rs b/src/uucore/src/lib/macros.rs new file mode 100644 index 000000000..6836f81aa --- /dev/null +++ b/src/uucore/src/lib/macros.rs @@ -0,0 +1,287 @@ +// This file is part of the uutils coreutils package. +// +// (c) Alex Lyon +// +// 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 + } + }) +); + +/// Show an error to stderr in a silimar style to GNU coreutils. +#[macro_export] +macro_rules! show_error( + ($($args:tt)+) => ({ + eprint!("{}: error: ", executable!()); + eprintln!($($args)+); + }) +); + +/// Show a warning to stderr in a silimar style to GNU coreutils. +#[macro_export] +macro_rules! show_warning( + ($($args:tt)+) => ({ + eprint!("{}: warning: ", executable!()); + eprintln!($($args)+); + }) +); + +/// Show an info message to stderr in a silimar style to GNU coreutils. +#[macro_export] +macro_rules! show_info( + ($($args:tt)+) => ({ + eprint!("{}: ", executable!()); + eprintln!($($args)+); + }) +); + +/// Show a bad inocation help message in a similar style to GNU coreutils. +#[macro_export] +macro_rules! show_usage_error( + ($($args:tt)+) => ({ + eprint!("{}: ", executable!()); + eprintln!($($args)+); + eprintln!("Try '{} --help' for more information.", executable!()); + }) +); + +/// Display the provided error message, then `exit()` with the provided exit code +#[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) + }) +); + +/// Unwraps the Result. Instead of panicking, it exists the program with the +/// provided exit code. +#[macro_export] +macro_rules! crash_if_err( + ($exit_code:expr, $exp:expr) => ( + match $exp { + Ok(m) => m, + Err(f) => 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( + ($fd:expr, $($args:tt)+) => ( + match write!($fd, $($args)+) { + Ok(_) => {} + Err(f) => panic!(f.to_string()) + } + ) +); + +#[macro_export] +macro_rules! safe_writeln( + ($fd:expr, $($args:tt)+) => ( + match writeln!($fd, $($args)+) { + Ok(_) => {} + Err(f) => panic!(f.to_string()) + } + ) +); + +/// 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 + +#[macro_export] +macro_rules! snippet_list_join_oxford { + ($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)*)) + ); +} + +#[macro_export] +macro_rules! snippet_list_join_or { + ($valOne:expr, $valTwo:expr) => ( + format!("{} or {}", $valOne, $valTwo) + ); + ($valOne:expr, $valTwo:expr $(, $remaining_values:expr)*) => ( + format!("{}, {}", $valOne, snippet_list_join_oxford!("or", $valTwo $(, $remaining_values)*)) + ); +} + +//-- message templates : invalid input + +#[macro_export] +macro_rules! msg_invalid_input { + ($reason: expr) => { + format!("invalid input: {}", $reason) + }; +} + +#[macro_export] +macro_rules! snippet_no_file_at_path { + ($path:expr) => { + format!("nonexistent path {}", $path) + }; +} + +// -- message templates : invalid input : flag + +#[macro_export] +macro_rules! msg_invalid_opt_use { + ($about:expr, $flag:expr) => { + msg_invalid_input!(format!("The '{}' option {}", $flag, $about)) + }; + ($about:expr, $long_flag:expr, $short_flag:expr) => { + msg_invalid_input!(format!( + "The '{}' ('{}') option {}", + $long_flag, $short_flag, $about + )) + }; +} + +#[macro_export] +macro_rules! msg_opt_only_usable_if { + ($clause:expr, $flag:expr) => { + msg_invalid_opt_use!(format!("only usable if {}", $clause), $flag) + }; + ($clause:expr, $long_flag:expr, $short_flag:expr) => { + msg_invalid_opt_use!( + format!("only usable if {}", $clause), + $long_flag, + $short_flag + ) + }; +} + +#[macro_export] +macro_rules! msg_opt_invalid_should_be { + ($expects:expr, $received:expr, $flag:expr) => { + 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!( + format!("expects {}, but was provided {}", $expects, $received), + $long_flag, + $short_flag + ) + }; +} + +// -- message templates : invalid input : args + +#[macro_export] +macro_rules! msg_arg_invalid_value { + ($expects:expr, $received:expr) => { + msg_invalid_input!(format!( + "expects its argument to be {}, but was provided {}", + $expects, $received + )) + }; +} + +#[macro_export] +macro_rules! msg_args_invalid_value { + ($expects:expr, $received:expr) => { + msg_invalid_input!(format!( + "expects its arguments to be {}, but was provided {}", + $expects, $received + )) + }; + ($msg:expr) => { + msg_invalid_input!($msg) + }; +} + +#[macro_export] +macro_rules! msg_args_nonexistent_file { + ($received:expr) => { + msg_args_invalid_value!("paths to files", snippet_no_file_at_path!($received)) + }; +} + +#[macro_export] +macro_rules! msg_wrong_number_of_arguments { + () => { + msg_args_invalid_value!("wrong number of arguments") + }; + ($min:expr, $max:expr) => { + msg_args_invalid_value!(format!("expects {}-{} arguments", $min, $max)) + }; + ($exact:expr) => { + if $exact == 1 { + msg_args_invalid_value!("expects 1 argument") + } else { + msg_args_invalid_value!(format!("expects {} arguments", $exact)) + } + }; +} + +// -- message templates : invalid input : input combinations + +#[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)*))) + ); +} + +#[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)*))) ; + ); +} diff --git a/src/uucore/src/lib/mods.rs b/src/uucore/src/lib/mods.rs new file mode 100644 index 000000000..c73909dcc --- /dev/null +++ b/src/uucore/src/lib/mods.rs @@ -0,0 +1,5 @@ +// mods ~ cross-platforms modules (core/bundler file) + +pub mod coreopts; +pub mod panic; +pub mod ranges; diff --git a/src/uucore/src/lib/mods/coreopts.rs b/src/uucore/src/lib/mods/coreopts.rs new file mode 100644 index 000000000..f3fb77335 --- /dev/null +++ b/src/uucore/src/lib/mods/coreopts.rs @@ -0,0 +1,141 @@ +pub struct HelpText<'a> { + pub name: &'a str, + pub version: &'a str, + pub syntax: &'a str, + pub summary: &'a str, + pub long_help: &'a str, + pub display_usage: bool, +} + +pub struct CoreOptions<'a> { + options: getopts::Options, + help_text: HelpText<'a>, +} + +impl<'a> CoreOptions<'a> { + pub fn new(help_text: HelpText<'a>) -> Self { + let mut ret = CoreOptions { + options: getopts::Options::new(), + help_text, + }; + ret.options + .optflag("", "help", "print usage information") + .optflag("", "version", "print name and version number"); + ret + } + pub fn optflagopt( + &mut self, + short_name: &str, + long_name: &str, + desc: &str, + hint: &str, + ) -> &mut CoreOptions<'a> { + self.options.optflagopt(short_name, long_name, desc, hint); + self + } + pub fn optflag( + &mut self, + short_name: &str, + long_name: &str, + desc: &str, + ) -> &mut CoreOptions<'a> { + self.options.optflag(short_name, long_name, desc); + self + } + pub fn optflagmulti( + &mut self, + short_name: &str, + long_name: &str, + desc: &str, + ) -> &mut CoreOptions<'a> { + self.options.optflagmulti(short_name, long_name, desc); + self + } + pub fn optopt( + &mut self, + short_name: &str, + long_name: &str, + desc: &str, + hint: &str, + ) -> &mut CoreOptions<'a> { + self.options.optopt(short_name, long_name, desc, hint); + self + } + pub fn optmulti( + &mut self, + short_name: &str, + long_name: &str, + desc: &str, + hint: &str, + ) -> &mut CoreOptions<'a> { + self.options.optmulti(short_name, long_name, desc, hint); + self + } + pub fn usage(&self, summary: &str) -> String { + self.options.usage(summary) + } + pub fn parse(&mut self, args: Vec) -> getopts::Matches { + let matches = match self.options.parse(&args[1..]) { + Ok(m) => Some(m), + Err(f) => { + eprint!("{}: error: ", self.help_text.name); + eprintln!("{}", f); + ::std::process::exit(1); + } + } + .unwrap(); + if matches.opt_present("help") { + let usage_str = if self.help_text.display_usage { + format!( + "\n {}\n\n Reference\n", + self.options.usage(self.help_text.summary) + ) + .replace("Options:", " Options:") + } else { + String::new() + }; + print!( + " + {0} {1} + + {0} {2} +{3}{4} +", + self.help_text.name, + self.help_text.version, + self.help_text.syntax, + usage_str, + self.help_text.long_help + ); + crate::exit!(0); + } else if matches.opt_present("version") { + println!("{} {}", self.help_text.name, self.help_text.version); + crate::exit!(0); + } + matches + } +} + +#[macro_export] +macro_rules! app { + ($syntax: expr, $summary: expr, $long_help: expr) => { + uucore::coreopts::CoreOptions::new(uucore::coreopts::HelpText { + name: executable!(), + version: env!("CARGO_PKG_VERSION"), + syntax: $syntax, + summary: $summary, + long_help: $long_help, + display_usage: true, + }) + }; + ($syntax: expr, $summary: expr, $long_help: expr, $display_usage: expr) => { + uucore::coreopts::CoreOptions::new(uucore::coreopts::HelpText { + name: executable!(), + version: env!("CARGO_PKG_VERSION"), + syntax: $syntax, + summary: $summary, + long_help: $long_help, + display_usage: $display_usage, + }) + }; +} diff --git a/src/uucore/src/lib/mods/panic.rs b/src/uucore/src/lib/mods/panic.rs new file mode 100644 index 000000000..6947df2ac --- /dev/null +++ b/src/uucore/src/lib/mods/panic.rs @@ -0,0 +1,17 @@ +use std::panic; + +//## SIGPIPE handling background/discussions ... +//* `uutils` ~ , +//* rust and `rg` ~ , , + +pub fn mute_sigpipe_panic() { + let hook = panic::take_hook(); + panic::set_hook(Box::new(move |info| { + if let Some(res) = info.payload().downcast_ref::() { + if res.contains("Broken pipe") { + return; + } + } + hook(info) + })); +} diff --git a/src/uu/cut/src/ranges.rs b/src/uucore/src/lib/mods/ranges.rs similarity index 83% rename from src/uu/cut/src/ranges.rs rename to src/uucore/src/lib/mods/ranges.rs index 74fec08e6..d4a6bf601 100644 --- a/src/uu/cut/src/ranges.rs +++ b/src/uucore/src/lib/mods/ranges.rs @@ -144,3 +144,31 @@ pub fn complement(ranges: &[Range]) -> Vec { complements } + +/// Test if at least one of the given Ranges contain the supplied value. +/// +/// Examples: +/// +/// ``` +/// let ranges = uucore::ranges::Range::from_list("11,2,6-8").unwrap(); +/// +/// assert!(!uucore::ranges::contain(&ranges, 0)); +/// assert!(!uucore::ranges::contain(&ranges, 1)); +/// assert!(!uucore::ranges::contain(&ranges, 5)); +/// assert!(!uucore::ranges::contain(&ranges, 10)); +/// +/// assert!(uucore::ranges::contain(&ranges, 2)); +/// assert!(uucore::ranges::contain(&ranges, 6)); +/// assert!(uucore::ranges::contain(&ranges, 7)); +/// assert!(uucore::ranges::contain(&ranges, 8)); +/// assert!(uucore::ranges::contain(&ranges, 11)); +/// ``` +pub fn contain(ranges: &[Range], n: usize) -> bool { + for range in ranges { + if n >= range.low && n <= range.high { + return true; + } + } + + false +} diff --git a/src/uucore_procs/Cargo.toml b/src/uucore_procs/Cargo.toml new file mode 100644 index 000000000..195912ff6 --- /dev/null +++ b/src/uucore_procs/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "uucore_procs" +version = "0.0.5" +authors = ["Roy Ivy III "] +license = "MIT" +description = "uutils ~ 'uucore' proc-macros" + +homepage = "https://github.com/uutils/uucore/uucore_procs" +repository = "https://github.com/uutils/uucore/uucore_procs" +# readme = "README.md" +keywords = ["cross-platform", "proc-macros", "uucore", "uutils"] +# categories = ["os"] +edition = "2018" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0" +quote = "1.0" +syn = { version="1.0" } + +[features] +default = [] +# * non-default features +debug = ["syn/extra-traits"] ## add Debug traits to syn structures (for `println!("{:?}", ...)`) diff --git a/src/uucore_procs/src/lib.rs b/src/uucore_procs/src/lib.rs new file mode 100644 index 000000000..10368a5bd --- /dev/null +++ b/src/uucore_procs/src/lib.rs @@ -0,0 +1,86 @@ +#![allow(dead_code)] // work-around for GH:rust-lang/rust#62127; maint: can be removed when MinSRV >= v1.38.0 +#![allow(unused_macros)] // work-around for GH:rust-lang/rust#62127; maint: can be removed when MinSRV >= v1.38.0 + +// Copyright (C) ~ Roy Ivy III ; MIT license + +extern crate proc_macro; + +//## rust proc-macro background info +//* ref: @@ +//* ref: [path construction from LitStr](https://oschwald.github.io/maxminddb-rust/syn/struct.LitStr.html) @@ + +//## proc_dbg macro +//* used to help debug the compile-time proc_macro code + +#[cfg(feature = "debug")] +macro_rules! proc_dbg { + ($x:expr) => { + dbg!($x) + }; +} +#[cfg(not(feature = "debug"))] +macro_rules! proc_dbg { + ($x:expr) => {}; +} + +//## main!() + +// main!( EXPR ) +// generates a `main()` function for utilities within the uutils group +// EXPR == syn::Expr::Lit::String | syn::Expr::Path::Ident ~ EXPR contains the lexical path to the utility `uumain()` function +//* NOTE: EXPR is ultimately expected to be a multi-segment lexical path (eg, `crate::func`); so, if a single segment path is provided, a trailing "::uumain" is automatically added +//* for more generic use (and future use of "eager" macros), EXPR may be in either STRING or IDENT form + +struct Tokens { + expr: syn::Expr, +} + +impl syn::parse::Parse for Tokens { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + Ok(Tokens { + expr: input.parse()?, + }) + } +} + +#[proc_macro] +#[cfg(not(test))] // work-around for GH:rust-lang/rust#62127; maint: can be removed when MinSRV >= v1.38.0 +pub fn main(stream: proc_macro::TokenStream) -> proc_macro::TokenStream { + let Tokens { expr } = syn::parse_macro_input!(stream as Tokens); + proc_dbg!(&expr); + + const ARG_PANIC_TEXT: &str = + "expected ident lexical path (or a literal string version) to 'uumain()' as argument"; + + // match EXPR as a string literal or an ident path, o/w panic!() + let mut expr = match expr { + syn::Expr::Lit(expr_lit) => match expr_lit.lit { + syn::Lit::Str(ref lit_str) => lit_str.parse::().unwrap(), + _ => panic!("{}", ARG_PANIC_TEXT), + }, + syn::Expr::Path(expr_path) => expr_path, + _ => panic!("{}", ARG_PANIC_TEXT), + }; + proc_dbg!(&expr); + + // for a single segment ExprPath argument, add trailing '::uumain' segment + if expr.path.segments.len() < 2 { + expr = syn::parse_quote!( #expr::uumain ); + }; + proc_dbg!(&expr); + + let f = quote::quote! { #expr(uucore::args_os()) }; + proc_dbg!(&f); + + // generate a uutils utility `main()` function, tailored for the calling utility + let result = quote::quote! { + fn main() { + use std::io::Write; + uucore::panic::mute_sigpipe_panic(); // suppress extraneous error output for SIGPIPE failures/panics + let code = #f; // execute utility code + std::io::stdout().flush().expect("could not flush stdout"); // (defensively) flush stdout for utility prior to exit; see + std::process::exit(code); + } + }; + proc_macro::TokenStream::from(result) +} diff --git a/tests/by-util/test_cat.rs b/tests/by-util/test_cat.rs index 20dfb479b..481b1683d 100644 --- a/tests/by-util/test_cat.rs +++ b/tests/by-util/test_cat.rs @@ -1,9 +1,16 @@ -extern crate tempfile; #[cfg(unix)] extern crate unix_socket; use crate::common::util::*; +#[test] +fn test_output_simple() { + new_ucmd!() + .args(&["alpha.txt"]) + .succeeds() + .stdout_only("abcde\nfghij\nklmno\npqrst\nuvwxyz\n"); +} + #[test] fn test_output_multi_files_print_all_chars() { new_ucmd!() @@ -11,16 +18,16 @@ fn test_output_multi_files_print_all_chars() { .succeeds() .stdout_only( " 1\tabcde$\n 2\tfghij$\n 3\tklmno$\n 4\tpqrst$\n \ - 5\tuvwxyz$\n 6\t^@^A^B^C^D^E^F^G^H^I$\n \ - 7\t^K^L^M^N^O^P^Q^R^S^T^U^V^W^X^Y^Z^[^\\^]^^^_ \ - !\"#$%&\'()*+,-./0123456789:;\ - <=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~^?M-^@M-^AM-^\ - BM-^CM-^DM-^EM-^FM-^GM-^HM-^IM-^JM-^KM-^LM-^MM-^NM-^OM-^PM-^QM-^RM-^SM-^TM-^UM-^V\ - M-^WM-^XM-^YM-^ZM-^[M-^\\M-^]M-^^M-^_M- \ - M-!M-\"M-#M-$M-%M-&M-\'M-(M-)M-*M-+M-,M--M-.M-/M-0M-1M-2M-3M-4M-5M-6M-7M-8M-9M-:\ - M-;M-M-?M-@M-AM-BM-CM-DM-EM-FM-GM-HM-IM-JM-KM-LM-MM-NM-OM-PM-QM-RM-SM-TM-U\ - M-VM-WM-XM-YM-ZM-[M-\\M-]M-^M-_M-`M-aM-bM-cM-dM-eM-fM-gM-hM-iM-jM-kM-lM-mM-nM-oM-\ - pM-qM-rM-sM-tM-uM-vM-wM-xM-yM-zM-{M-|M-}M-~M-^?", + 5\tuvwxyz$\n 6\t^@^A^B^C^D^E^F^G^H^I$\n \ + 7\t^K^L^M^N^O^P^Q^R^S^T^U^V^W^X^Y^Z^[^\\^]^^^_ \ + !\"#$%&\'()*+,-./0123456789:;\ + <=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~^?M-^@M-^AM-^\ + BM-^CM-^DM-^EM-^FM-^GM-^HM-^IM-^JM-^KM-^LM-^MM-^NM-^OM-^PM-^QM-^RM-^SM-^TM-^UM-^V\ + M-^WM-^XM-^YM-^ZM-^[M-^\\M-^]M-^^M-^_M- \ + M-!M-\"M-#M-$M-%M-&M-\'M-(M-)M-*M-+M-,M--M-.M-/M-0M-1M-2M-3M-4M-5M-6M-7M-8M-9M-:\ + M-;M-M-?M-@M-AM-BM-CM-DM-EM-FM-GM-HM-IM-JM-KM-LM-MM-NM-OM-PM-QM-RM-SM-TM-U\ + M-VM-WM-XM-YM-ZM-[M-\\M-]M-^M-_M-`M-aM-bM-cM-dM-eM-fM-gM-hM-iM-jM-kM-lM-mM-nM-oM-\ + pM-qM-rM-sM-tM-uM-vM-wM-xM-yM-zM-{M-|M-}M-~M-^?", ); } @@ -31,7 +38,7 @@ fn test_numbered_lines_no_trailing_newline() { .succeeds() .stdout_only( " 1\ttext without a trailing newlineabcde\n 2\tfghij\n \ - 3\tklmno\n 4\tpqrst\n 5\tuvwxyz\n", + 3\tklmno\n 4\tpqrst\n 5\tuvwxyz\n", ); } diff --git a/tests/by-util/test_chgrp.rs b/tests/by-util/test_chgrp.rs index 3bd0c69e5..613f52fd2 100644 --- a/tests/by-util/test_chgrp.rs +++ b/tests/by-util/test_chgrp.rs @@ -110,13 +110,12 @@ fn test_reference() { .arg("--reference=/etc/passwd") .arg("/etc") .fails() - .stderr_is("chgrp: changing group of '/etc': Operation not permitted (os error 1)\n") - .stdout_is("failed to change group of /etc from root to root\n"); + .stderr_is("chgrp: changing group of '/etc': Operation not permitted (os error 1)\nfailed to change group of '/etc' from root to root"); } } #[test] -#[cfg(target_os = "macos")] +#[cfg(target_vendor = "apple")] fn test_reference() { new_ucmd!() .arg("-v") diff --git a/tests/by-util/test_chmod.rs b/tests/by-util/test_chmod.rs index 612f0860f..b85567166 100644 --- a/tests/by-util/test_chmod.rs +++ b/tests/by-util/test_chmod.rs @@ -4,6 +4,8 @@ use std::os::unix::fs::{OpenOptionsExt, PermissionsExt}; use std::sync::Mutex; extern crate libc; +use self::chmod::strip_minus_from_mode; +extern crate chmod; use self::libc::umask; static TEST_FILE: &'static str = "file"; @@ -35,10 +37,10 @@ fn run_single_test(test: &TestCase, at: AtPath, mut ucmd: UCommand) { mkfile(&at.plus_as_string(TEST_FILE), test.before); let perms = at.metadata(TEST_FILE).permissions().mode(); if perms != test.before { - panic!(format!( + panic!( "{}: expected: {:o} got: {:o}", "setting permissions on test files before actual test run failed", test.after, perms - )); + ); } for arg in &test.args { @@ -47,15 +49,15 @@ fn run_single_test(test: &TestCase, at: AtPath, mut ucmd: UCommand) { let r = ucmd.run(); if !r.success { println!("{}", r.stderr); - panic!(format!("{:?}: failed", ucmd.raw)); + panic!("{:?}: failed", ucmd.raw); } let perms = at.metadata(TEST_FILE).permissions().mode(); if perms != test.after { - panic!(format!( + panic!( "{:?}: expected: {:o} got: {:o}", ucmd.raw, test.after, perms - )); + ); } } @@ -279,3 +281,207 @@ fn test_chmod_reference_file() { mkfile(&at.plus_as_string(REFERENCE_FILE), REFERENCE_PERMS); run_single_test(&tests[0], at, ucmd); } + +#[test] +fn test_chmod_recursive() { + let _guard = UMASK_MUTEX.lock(); + + let original_umask = unsafe { umask(0) }; + let (at, mut ucmd) = at_and_ucmd!(); + at.mkdir("a"); + at.mkdir("a/b"); + at.mkdir("a/b/c"); + at.mkdir("z"); + mkfile(&at.plus_as_string("a/a"), 0o100444); + mkfile(&at.plus_as_string("a/b/b"), 0o100444); + mkfile(&at.plus_as_string("a/b/c/c"), 0o100444); + mkfile(&at.plus_as_string("z/y"), 0o100444); + + let result = ucmd + .arg("-R") + .arg("--verbose") + .arg("-r,a+w") + .arg("a") + .arg("z") + .succeeds(); + + assert_eq!(at.metadata("z/y").permissions().mode(), 0o100222); + assert_eq!(at.metadata("a/a").permissions().mode(), 0o100222); + assert_eq!(at.metadata("a/b/b").permissions().mode(), 0o100222); + assert_eq!(at.metadata("a/b/c/c").permissions().mode(), 0o100222); + println!("mode {:o}", at.metadata("a").permissions().mode()); + assert_eq!(at.metadata("a").permissions().mode(), 0o40333); + assert_eq!(at.metadata("z").permissions().mode(), 0o40333); + assert!(result.stderr.contains("to 333 (-wx-wx-wx)")); + assert!(result.stderr.contains("to 222 (-w--w--w-)")); + + unsafe { + umask(original_umask); + } +} + +#[test] +fn test_chmod_non_existing_file() { + let (_at, mut ucmd) = at_and_ucmd!(); + let result = ucmd + .arg("-R") + .arg("--verbose") + .arg("-r,a+w") + .arg("dont-exist") + .fails(); + assert!(result + .stderr + .contains("cannot access 'dont-exist': No such file or directory")); +} + +#[test] +fn test_chmod_preserve_root() { + let (_at, mut ucmd) = at_and_ucmd!(); + let result = ucmd + .arg("-R") + .arg("--preserve-root") + .arg("755") + .arg("/") + .fails(); + assert!(result + .stderr + .contains("chmod: error: it is dangerous to operate recursively on '/'")); +} + +#[test] +fn test_chmod_symlink_non_existing_file() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let non_existing = "test_chmod_symlink_non_existing_file"; + let test_symlink = "test_chmod_symlink_non_existing_file_symlink"; + let expected_stdout = &format!( + "failed to change mode of '{}' from 0000 (---------) to 0000 (---------)", + test_symlink + ); + let expected_stderr = &format!("cannot operate on dangling symlink '{}'", test_symlink); + + at.symlink_file(non_existing, test_symlink); + let mut result; + + // this cannot succeed since the symbolic link dangles + result = scene.ucmd().arg("755").arg("-v").arg(test_symlink).fails(); + + println!("stdout = {:?}", result.stdout); + println!("stderr = {:?}", result.stderr); + + assert!(result.stdout.contains(expected_stdout)); + assert!(result.stderr.contains(expected_stderr)); + assert_eq!(result.code, Some(1)); + + // this should be the same than with just '-v' but without stderr + result = scene + .ucmd() + .arg("755") + .arg("-v") + .arg("-f") + .arg(test_symlink) + .fails(); + + println!("stdout = {:?}", result.stdout); + println!("stderr = {:?}", result.stderr); + + assert!(result.stdout.contains(expected_stdout)); + assert!(result.stderr.is_empty()); + assert_eq!(result.code, Some(1)); +} + +#[test] +fn test_chmod_symlink_non_existing_file_recursive() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let non_existing = "test_chmod_symlink_non_existing_file_recursive"; + let test_symlink = "test_chmod_symlink_non_existing_file_recursive_symlink"; + let test_directory = "test_chmod_symlink_non_existing_file_directory"; + + at.mkdir(test_directory); + at.symlink_file( + non_existing, + &format!("{}/{}", test_directory, test_symlink), + ); + let mut result; + + // this should succeed + result = scene + .ucmd() + .arg("-R") + .arg("755") + .arg(test_directory) + .succeeds(); + assert_eq!(result.code, Some(0)); + assert!(result.stdout.is_empty()); + assert!(result.stderr.is_empty()); + + let expected_stdout = &format!( + "mode of '{}' retained as 0755 (rwxr-xr-x)\nneither symbolic link '{}/{}' nor referent has been changed", + test_directory, test_directory, test_symlink + ); + + // '-v': this should succeed without stderr + result = scene + .ucmd() + .arg("-R") + .arg("-v") + .arg("755") + .arg(test_directory) + .succeeds(); + + println!("stdout = {:?}", result.stdout); + println!("stderr = {:?}", result.stderr); + + assert!(result.stdout.contains(expected_stdout)); + assert!(result.stderr.is_empty()); + assert_eq!(result.code, Some(0)); + + // '-vf': this should be the same than with just '-v' + result = scene + .ucmd() + .arg("-R") + .arg("-v") + .arg("-f") + .arg("755") + .arg(test_directory) + .succeeds(); + + println!("stdout = {:?}", result.stdout); + println!("stderr = {:?}", result.stderr); + + assert!(result.stdout.contains(expected_stdout)); + assert!(result.stderr.is_empty()); + assert_eq!(result.code, Some(0)); +} + +#[test] +fn test_chmod_strip_minus_from_mode() { + let tests = vec![ + // ( before, after ) + ("chmod -v -xw -R FILE", "chmod -v xw -R FILE"), + ("chmod g=rwx FILE -c", "chmod g=rwx FILE -c"), + ( + "chmod -c -R -w,o+w FILE --preserve-root", + "chmod -c -R w,o+w FILE --preserve-root", + ), + ("chmod -c -R +w FILE ", "chmod -c -R +w FILE "), + ("chmod a=r,=xX FILE", "chmod a=r,=xX FILE"), + ( + "chmod -v --reference RFILE -R FILE", + "chmod -v --reference RFILE -R FILE", + ), + ("chmod -Rvc -w-x FILE", "chmod -Rvc w-x FILE"), + ("chmod 755 -v FILE", "chmod 755 -v FILE"), + ("chmod -v +0004 FILE -R", "chmod -v +0004 FILE -R"), + ("chmod -v -0007 FILE -R", "chmod -v 0007 FILE -R"), + ]; + + for test in tests { + let mut args: Vec = test.0.split(" ").map(|v| v.to_string()).collect(); + let _mode_had_minus_prefix = strip_minus_from_mode(&mut args); + assert_eq!(test.1, args.join(" ")); + } +} diff --git a/tests/by-util/test_chown.rs b/tests/by-util/test_chown.rs index fb487cc16..7b663e9c9 100644 --- a/tests/by-util/test_chown.rs +++ b/tests/by-util/test_chown.rs @@ -1,7 +1,8 @@ use crate::common::util::*; +#[cfg(target_os = "linux")] +use rust_users::get_effective_uid; extern crate chown; -// pub use self::uu_chown::*; #[cfg(test)] mod test_passgrp { @@ -46,3 +47,353 @@ mod test_passgrp { fn test_invalid_option() { new_ucmd!().arg("-w").arg("-q").arg("/").fails(); } + +#[test] +fn test_chown_myself() { + // test chown username file.txt + let scene = TestScenario::new(util_name!()); + let result = scene.cmd("whoami").run(); + if is_ci() && result.stderr.contains("No such user/group") { + // In the CI, some server are failing to return whoami. + // As seems to be a configuration issue, ignoring it + return; + } + println!("results {}", result.stdout); + let username = result.stdout.trim_end(); + + let (at, mut ucmd) = at_and_ucmd!(); + let file1 = "test_install_target_dir_file_a1"; + + at.touch(file1); + let result = ucmd.arg(username).arg(file1).run(); + println!("results stdout {}", result.stdout); + println!("results stderr {}", result.stderr); + if is_ci() && result.stderr.contains("invalid user") { + // In the CI, some server are failing to return id. + // As seems to be a configuration issue, ignoring it + return; + } + assert!(result.success); +} + +#[test] +fn test_chown_myself_second() { + // test chown username: file.txt + let scene = TestScenario::new(util_name!()); + let result = scene.cmd("whoami").run(); + if is_ci() && result.stderr.contains("No such user/group") { + // In the CI, some server are failing to return whoami. + // As seems to be a configuration issue, ignoring it + return; + } + println!("results {}", result.stdout); + + let (at, mut ucmd) = at_and_ucmd!(); + let file1 = "test_install_target_dir_file_a1"; + + at.touch(file1); + let result = ucmd + .arg(result.stdout.trim_end().to_owned() + ":") + .arg(file1) + .run(); + + println!("result.stdout = {}", result.stdout); + println!("result.stderr = {}", result.stderr); + assert!(result.success); +} + +#[test] +fn test_chown_myself_group() { + // test chown username:group file.txt + let scene = TestScenario::new(util_name!()); + let result = scene.cmd("whoami").run(); + if is_ci() && result.stderr.contains("No such user/group") { + // In the CI, some server are failing to return whoami. + // As seems to be a configuration issue, ignoring it + return; + } + println!("user name = {}", result.stdout); + let username = result.stdout.trim_end(); + + let result = scene.cmd("id").arg("-gn").run(); + if is_ci() && result.stderr.contains("No such user/group") { + // In the CI, some server are failing to return whoami. + // As seems to be a configuration issue, ignoring it + return; + } + println!("group name = {}", result.stdout); + let group = result.stdout.trim_end(); + + let (at, mut ucmd) = at_and_ucmd!(); + let file1 = "test_install_target_dir_file_a1"; + let perm = username.to_owned() + ":" + group; + at.touch(file1); + let result = ucmd.arg(perm).arg(file1).run(); + println!("result.stdout = {}", result.stdout); + println!("result.stderr = {}", result.stderr); + if is_ci() && result.stderr.contains("chown: invalid group:") { + // With some Ubuntu into the CI, we can get this answer + return; + } + assert!(result.success); +} + +#[test] +fn test_chown_only_group() { + // test chown :group file.txt + let scene = TestScenario::new(util_name!()); + let result = scene.cmd("whoami").run(); + if is_ci() && result.stderr.contains("No such user/group") { + // In the CI, some server are failing to return whoami. + // As seems to be a configuration issue, ignoring it + return; + } + println!("results {}", result.stdout); + + let (at, mut ucmd) = at_and_ucmd!(); + let file1 = "test_install_target_dir_file_a1"; + let perm = ":".to_owned() + result.stdout.trim_end(); + at.touch(file1); + let result = ucmd.arg(perm).arg(file1).run(); + + println!("result.stdout = {}", result.stdout); + println!("result.stderr = {}", result.stderr); + + if is_ci() && result.stderr.contains("Operation not permitted") { + // With ubuntu with old Rust in the CI, we can get an error + return; + } + if is_ci() && result.stderr.contains("chown: invalid group:") { + // With mac into the CI, we can get this answer + return; + } + assert!(result.success); +} + +#[test] +fn test_chown_only_id() { + // test chown 1111 file.txt + let result = TestScenario::new("id").ucmd_keepenv().arg("-u").run(); + if is_ci() && result.stderr.contains("No such user/group") { + // In the CI, some server are failing to return whoami. + // As seems to be a configuration issue, ignoring it + return; + } + println!("result.stdout = {}", result.stdout); + println!("result.stderr = {}", result.stderr); + let id = String::from(result.stdout.trim()); + + let (at, mut ucmd) = at_and_ucmd!(); + let file1 = "test_install_target_dir_file_a1"; + + at.touch(file1); + let result = ucmd.arg(id).arg(file1).run(); + + println!("result.stdout = {}", result.stdout); + println!("result.stderr = {}", result.stderr); + if is_ci() && result.stderr.contains("chown: invalid user:") { + // With some Ubuntu into the CI, we can get this answer + return; + } + assert!(result.success); +} + +#[test] +fn test_chown_only_group_id() { + // test chown :1111 file.txt + let result = TestScenario::new("id").ucmd_keepenv().arg("-g").run(); + if is_ci() && result.stderr.contains("No such user/group") { + // In the CI, some server are failing to return whoami. + // As seems to be a configuration issue, ignoring it + return; + } + println!("result.stdout {}", result.stdout); + println!("result.stderr = {}", result.stderr); + let id = String::from(result.stdout.trim()); + + let (at, mut ucmd) = at_and_ucmd!(); + let file1 = "test_install_target_dir_file_a1"; + + at.touch(file1); + let perm = ":".to_owned() + &id; + + let result = ucmd.arg(perm).arg(file1).run(); + + println!("result.stdout = {}", result.stdout); + println!("result.stderr = {}", result.stderr); + if is_ci() && result.stderr.contains("chown: invalid group:") { + // With mac into the CI, we can get this answer + return; + } + assert!(result.success); +} + +#[test] +fn test_chown_both_id() { + // test chown 1111:1111 file.txt + let result = TestScenario::new("id").ucmd_keepenv().arg("-u").run(); + if is_ci() && result.stderr.contains("No such user/group") { + // In the CI, some server are failing to return whoami. + // As seems to be a configuration issue, ignoring it + return; + } + println!("result.stdout {}", result.stdout); + println!("result.stderr = {}", result.stderr); + let id_user = String::from(result.stdout.trim()); + + let result = TestScenario::new("id").ucmd_keepenv().arg("-g").run(); + if is_ci() && result.stderr.contains("No such user/group") { + // In the CI, some server are failing to return whoami. + // As seems to be a configuration issue, ignoring it + return; + } + println!("result.stdout {}", result.stdout); + println!("result.stderr = {}", result.stderr); + let id_group = String::from(result.stdout.trim()); + + let (at, mut ucmd) = at_and_ucmd!(); + let file1 = "test_install_target_dir_file_a1"; + + at.touch(file1); + let perm = id_user + &":".to_owned() + &id_group; + + let result = ucmd.arg(perm).arg(file1).run(); + println!("result.stdout {}", result.stdout); + println!("result.stderr = {}", result.stderr); + + if is_ci() && result.stderr.contains("invalid user") { + // In the CI, some server are failing to return id. + // As seems to be a configuration issue, ignoring it + return; + } + + assert!(result.success); +} + +#[test] +fn test_chown_both_mix() { + // test chown 1111:1111 file.txt + let result = TestScenario::new("id").ucmd_keepenv().arg("-u").run(); + if is_ci() && result.stderr.contains("No such user/group") { + // In the CI, some server are failing to return whoami. + // As seems to be a configuration issue, ignoring it + return; + } + println!("result.stdout {}", result.stdout); + println!("result.stderr = {}", result.stderr); + let id_user = String::from(result.stdout.trim()); + + let result = TestScenario::new("id").ucmd_keepenv().arg("-gn").run(); + if is_ci() && result.stderr.contains("No such user/group") { + // In the CI, some server are failing to return whoami. + // As seems to be a configuration issue, ignoring it + return; + } + println!("result.stdout {}", result.stdout); + println!("result.stderr = {}", result.stderr); + let group_name = String::from(result.stdout.trim()); + + let (at, mut ucmd) = at_and_ucmd!(); + let file1 = "test_install_target_dir_file_a1"; + + at.touch(file1); + let perm = id_user + &":".to_owned() + &group_name; + + let result = ucmd.arg(perm).arg(file1).run(); + + if is_ci() && result.stderr.contains("invalid user") { + // In the CI, some server are failing to return id. + // As seems to be a configuration issue, ignoring it + return; + } + assert!(result.success); +} + +#[test] +fn test_chown_recursive() { + let scene = TestScenario::new(util_name!()); + let result = scene.cmd("whoami").run(); + if is_ci() && result.stderr.contains("No such user/group") { + // In the CI, some server are failing to return whoami. + // As seems to be a configuration issue, ignoring it + return; + } + println!("result.stdout {}", result.stdout); + println!("result.stderr = {}", result.stderr); + let username = result.stdout.trim_end(); + + let (at, mut ucmd) = at_and_ucmd!(); + at.mkdir("a"); + at.mkdir("a/b"); + at.mkdir("a/b/c"); + at.mkdir("z"); + at.touch(&at.plus_as_string("a/a")); + at.touch(&at.plus_as_string("a/b/b")); + at.touch(&at.plus_as_string("a/b/c/c")); + at.touch(&at.plus_as_string("z/y")); + + let result = ucmd + .arg("-R") + .arg("--verbose") + .arg(username) + .arg("a") + .arg("z") + .run(); + println!("result.stdout {}", result.stdout); + println!("result.stderr = {}", result.stderr); + if is_ci() && result.stderr.contains("invalid user") { + // In the CI, some server are failing to return id. + // As seems to be a configuration issue, ignoring it + return; + } + + assert!(result.stderr.contains("ownership of 'a/a' retained as")); + assert!(result.stderr.contains("ownership of 'z/y' retained as")); + assert!(result.success); +} + +#[test] +fn test_root_preserve() { + let scene = TestScenario::new(util_name!()); + let result = scene.cmd("whoami").run(); + if is_ci() && result.stderr.contains("No such user/group") { + // In the CI, some server are failing to return whoami. + // As seems to be a configuration issue, ignoring it + return; + } + println!("result.stdout {}", result.stdout); + println!("result.stderr = {}", result.stderr); + let username = result.stdout.trim_end(); + + let result = new_ucmd!() + .arg("--preserve-root") + .arg("-R") + .arg(username) + .arg("/") + .fails(); + println!("result.stdout {}", result.stdout); + println!("result.stderr = {}", result.stderr); + if is_ci() && result.stderr.contains("invalid user") { + // In the CI, some server are failing to return id. + // As seems to be a configuration issue, ignoring it + return; + } + assert!(result + .stderr + .contains("chown: it is dangerous to operate recursively")); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_big_p() { + if get_effective_uid() != 0 { + new_ucmd!() + .arg("-RP") + .arg("bin") + .arg("/proc/self/cwd") + .fails() + .stderr_is( + "chown: changing ownership of '/proc/self/cwd': Operation not permitted (os error 1)\n", + ); + } +} diff --git a/tests/by-util/test_chroot.rs b/tests/by-util/test_chroot.rs index 651491045..9a8fb71dd 100644 --- a/tests/by-util/test_chroot.rs +++ b/tests/by-util/test_chroot.rs @@ -1 +1,98 @@ -// ToDO: add tests +use crate::common::util::*; + +#[test] +fn test_missing_operand() { + let result = new_ucmd!().run(); + + assert_eq!( + true, + result + .stderr + .starts_with("error: The following required arguments were not provided") + ); + + assert_eq!(true, result.stderr.contains("")); +} + +#[test] +fn test_enter_chroot_fails() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.mkdir("jail"); + + let result = ucmd.arg("jail").run(); + + assert_eq!( + true, + result.stderr.starts_with( + "chroot: error: cannot chroot to jail: Operation not permitted (os error 1)" + ) + ) +} + +#[test] +fn test_no_such_directory() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.touch(&at.plus_as_string("a")); + + ucmd.arg("a") + .fails() + .stderr_is("chroot: error: cannot change root directory to `a`: no such directory"); +} + +#[test] +fn test_invalid_user_spec() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.mkdir("a"); + + let result = ucmd.arg("a").arg("--userspec=ARABA:").run(); + + assert_eq!( + true, + result.stderr.starts_with("chroot: error: invalid userspec") + ); +} + +#[test] +fn test_preference_of_userspec() { + let scene = TestScenario::new(util_name!()); + let result = scene.cmd("whoami").run(); + if is_ci() && result.stderr.contains("No such user/group") { + // In the CI, some server are failing to return whoami. + // As seems to be a configuration issue, ignoring it + return; + } + println!("result.stdout {}", result.stdout); + println!("result.stderr = {}", result.stderr); + let username = result.stdout.trim_end(); + + let ts = TestScenario::new("id"); + let result = ts.cmd("id").arg("-g").arg("-n").run(); + println!("result.stdout {}", result.stdout); + println!("result.stderr = {}", result.stderr); + + if is_ci() && result.stderr.contains("cannot find name for user ID") { + // In the CI, some server are failing to return id. + // As seems to be a configuration issue, ignoring it + return; + } + + let group_name = result.stdout.trim_end(); + let (at, mut ucmd) = at_and_ucmd!(); + + at.mkdir("a"); + + let result = ucmd + .arg("a") + .arg("--user") + .arg("fake") + .arg("-G") + .arg("ABC,DEF") + .arg(format!("--userspec={}:{}", username, group_name)) + .run(); + + println!("result.stdout {}", result.stdout); + println!("result.stderr = {}", result.stderr); +} diff --git a/tests/by-util/test_cksum.rs b/tests/by-util/test_cksum.rs index d0bf5ffc8..8c8a551a0 100644 --- a/tests/by-util/test_cksum.rs +++ b/tests/by-util/test_cksum.rs @@ -24,3 +24,79 @@ fn test_stdin() { .succeeds() .stdout_is_fixture("stdin.expected"); } + +#[test] +fn test_empty() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.touch("a"); + + ucmd.arg("a").succeeds().stdout.ends_with("0 a"); +} + +#[test] +fn test_arg_overrides_stdin() { + let (at, mut ucmd) = at_and_ucmd!(); + let input = "foobarfoobar"; + + at.touch("a"); + + let result = ucmd.arg("a").pipe_in(input.as_bytes()).run(); + + println!("{}, {}", result.stdout, result.stderr); + + assert!(result.stdout.ends_with("0 a\n")) +} + +#[test] +fn test_invalid_file() { + let (_, mut ucmd) = at_and_ucmd!(); + + let ls = TestScenario::new("ls"); + let files = ls.cmd("ls").arg("-l").run(); + println!("{:?}", files.stdout); + println!("{:?}", files.stderr); + + let folder_name = "asdf".to_string(); + + let result = ucmd.arg(&folder_name).run(); + + println!("stdout: {:?}", result.stdout); + println!("stderr: {:?}", result.stderr); + assert!(result.stderr.contains("cksum: error: 'asdf'")); + assert!(!result.success); +} + +// Make sure crc is correct for files larger than 32 bytes +// but <128 bytes (1 fold pclmul) +#[test] +fn test_crc_for_bigger_than_32_bytes() { + let (_, mut ucmd) = at_and_ucmd!(); + + let result = ucmd.arg("chars.txt").run(); + + let mut stdout_splitted = result.stdout.split(" "); + + let cksum: i64 = stdout_splitted.next().unwrap().parse().unwrap(); + let bytes_cnt: i64 = stdout_splitted.next().unwrap().parse().unwrap(); + + assert!(result.success); + assert_eq!(cksum, 586047089); + assert_eq!(bytes_cnt, 16); +} + +#[test] +fn test_stdin_larger_than_128_bytes() { + let (_, mut ucmd) = at_and_ucmd!(); + + let result = ucmd.arg("larger_than_2056_bytes.txt").run(); + + let mut stdout_splitted = result.stdout.split(" "); + + let cksum: i64 = stdout_splitted.next().unwrap().parse().unwrap(); + let bytes_cnt: i64 = stdout_splitted.next().unwrap().parse().unwrap(); + + assert!(result.success); + assert_eq!(cksum, 945881979); + assert_eq!(bytes_cnt, 2058); +} diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index f56131a86..1fa8212ca 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -8,8 +8,16 @@ use std::os::unix::fs; #[cfg(windows)] use std::os::windows::fs::symlink_file; +#[cfg(target_os = "linux")] +use filetime::FileTime; #[cfg(not(windows))] use std::env; +#[cfg(target_os = "linux")] +use std::fs as std_fs; +#[cfg(target_os = "linux")] +use std::thread::sleep; +#[cfg(target_os = "linux")] +use std::time::Duration; static TEST_EXISTING_FILE: &str = "existing_file.txt"; static TEST_HELLO_WORLD_SOURCE: &str = "hello_world.txt"; @@ -23,6 +31,12 @@ static TEST_COPY_FROM_FOLDER: &str = "hello_dir_with_file/"; static TEST_COPY_FROM_FOLDER_FILE: &str = "hello_dir_with_file/hello_world.txt"; static TEST_COPY_TO_FOLDER_NEW: &str = "hello_dir_new"; static TEST_COPY_TO_FOLDER_NEW_FILE: &str = "hello_dir_new/hello_world.txt"; +#[cfg(any(target_os = "linux", target_os = "freebsd"))] +static TEST_MOUNT_COPY_FROM_FOLDER: &str = "dir_with_mount"; +#[cfg(any(target_os = "linux", target_os = "freebsd"))] +static TEST_MOUNT_MOUNTPOINT: &str = "mount"; +#[cfg(any(target_os = "linux", target_os = "freebsd"))] +static TEST_MOUNT_OTHER_FILESYSTEM_FILE: &str = "mount/DO_NOT_copy_me.txt"; #[test] fn test_cp_cp() { @@ -34,8 +48,7 @@ fn test_cp_cp() { .run(); // Check that the exit code represents a successful copy. - let exit_success = result.success; - assert!(exit_success); + assert!(result.success); // Check the content of the destination file that was copied. assert_eq!(at.read(TEST_HELLO_WORLD_DEST), "Hello, World!\n"); @@ -112,6 +125,8 @@ fn test_cp_multiple_files() { } #[test] +// FixME: for MacOS, this has intermittent failures; track repair progress at GH:uutils/coreutils/issues/1590 +#[cfg(not(macos))] fn test_cp_recurse() { let (at, mut ucmd) = at_and_ucmd!(); @@ -141,6 +156,8 @@ fn test_cp_with_dirs_t() { } #[test] +// FixME: for MacOS, this has intermittent failures; track repair progress at GH:uutils/coreutils/issues/1590 +#[cfg(not(macos))] fn test_cp_with_dirs() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; @@ -205,7 +222,7 @@ fn test_cp_arg_interactive() { } #[test] -#[cfg(target_os = "unix")] +#[cfg(target_os = "linux")] fn test_cp_arg_link() { use std::os::linux::fs::MetadataExt; @@ -244,7 +261,40 @@ fn test_cp_arg_no_clobber() { assert!(result.success); assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "How are you?\n"); - assert!(result.stderr.contains("Not overwriting")); +} + +#[test] +fn test_cp_arg_no_clobber_twice() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch("source.txt"); + let result = scene + .ucmd() + .arg("--no-clobber") + .arg("source.txt") + .arg("dest.txt") + .run(); + + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(result.success); + assert!(result.stderr.is_empty()); + assert_eq!(at.read("source.txt"), ""); + + at.append("source.txt", "some-content"); + let result = scene + .ucmd() + .arg("--no-clobber") + .arg("source.txt") + .arg("dest.txt") + .run(); + + assert!(result.success); + assert_eq!(at.read("source.txt"), "some-content"); + // Should be empty as the "no-clobber" should keep + // the previous version + assert_eq!(at.read("dest.txt"), ""); + assert!(!result.stderr.contains("Not overwriting")); } #[test] @@ -339,6 +389,59 @@ fn test_cp_arg_suffix() { ); } +#[test] +fn test_cp_deref_conflicting_options() { + let (_at, mut ucmd) = at_and_ucmd!(); + + ucmd.arg("-LP") + .arg(TEST_COPY_TO_FOLDER) + .arg(TEST_HELLO_WORLD_SOURCE) + .fails(); +} + +#[test] +fn test_cp_deref() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + #[cfg(not(windows))] + let _r = fs::symlink( + TEST_HELLO_WORLD_SOURCE, + at.subdir.join(TEST_HELLO_WORLD_SOURCE_SYMLINK), + ); + #[cfg(windows)] + let _r = symlink_file( + TEST_HELLO_WORLD_SOURCE, + at.subdir.join(TEST_HELLO_WORLD_SOURCE_SYMLINK), + ); + //using -L option + let result = scene + .ucmd() + .arg("-L") + .arg(TEST_HELLO_WORLD_SOURCE) + .arg(TEST_HELLO_WORLD_SOURCE_SYMLINK) + .arg(TEST_COPY_TO_FOLDER) + .run(); + + // Check that the exit code represents a successful copy. + assert!(result.success); + let path_to_new_symlink = at + .subdir + .join(TEST_COPY_TO_FOLDER) + .join(TEST_HELLO_WORLD_SOURCE_SYMLINK); + // unlike -P/--no-deref, we expect a file, not a link + assert!(at.file_exists( + &path_to_new_symlink + .clone() + .into_os_string() + .into_string() + .unwrap() + )); + // Check the content of the destination file that was copied. + assert_eq!(at.read(TEST_COPY_TO_FOLDER_FILE), "Hello, World!\n"); + let path_to_check = path_to_new_symlink.to_str().unwrap(); + assert_eq!(at.read(&path_to_check), "Hello, World!\n"); +} #[test] fn test_cp_no_deref() { let scene = TestScenario::new(util_name!()); @@ -364,8 +467,7 @@ fn test_cp_no_deref() { .run(); // Check that the exit code represents a successful copy. - let exit_success = result.success; - assert!(exit_success); + assert!(result.success); let path_to_new_symlink = at .subdir .join(TEST_COPY_TO_FOLDER) @@ -383,6 +485,195 @@ fn test_cp_no_deref() { assert_eq!(at.read(&path_to_check), "Hello, World!\n"); } +#[test] +fn test_cp_strip_trailing_slashes() { + let (at, mut ucmd) = at_and_ucmd!(); + + //using --strip-trailing-slashes option + let result = ucmd + .arg("--strip-trailing-slashes") + .arg(format!("{}/", TEST_HELLO_WORLD_SOURCE)) + .arg(TEST_HELLO_WORLD_DEST) + .run(); + + // Check that the exit code represents a successful copy. + assert!(result.success); + + // Check the content of the destination file that was copied. + assert_eq!(at.read(TEST_HELLO_WORLD_DEST), "Hello, World!\n"); +} + +#[test] +fn test_cp_parents() { + let (at, mut ucmd) = at_and_ucmd!(); + + let result = ucmd + .arg("--parents") + .arg(TEST_COPY_FROM_FOLDER_FILE) + .arg(TEST_COPY_TO_FOLDER) + .run(); + + assert!(result.success); + // Check the content of the destination file that was copied. + assert_eq!( + at.read(&format!( + "{}/{}", + TEST_COPY_TO_FOLDER, TEST_COPY_FROM_FOLDER_FILE + )), + "Hello, World!\n" + ); +} + +#[test] +fn test_cp_parents_multiple_files() { + let (at, mut ucmd) = at_and_ucmd!(); + + let result = ucmd + .arg("--parents") + .arg(TEST_COPY_FROM_FOLDER_FILE) + .arg(TEST_HOW_ARE_YOU_SOURCE) + .arg(TEST_COPY_TO_FOLDER) + .run(); + + assert!(result.success); + assert_eq!( + at.read(&format!( + "{}/{}", + TEST_COPY_TO_FOLDER, TEST_COPY_FROM_FOLDER_FILE + )), + "Hello, World!\n" + ); + assert_eq!( + at.read(&format!( + "{}/{}", + TEST_COPY_TO_FOLDER, TEST_HOW_ARE_YOU_SOURCE + )), + "How are you?\n" + ); +} + +#[test] +fn test_cp_parents_dest_not_directory() { + let (_, mut ucmd) = at_and_ucmd!(); + + let result = ucmd + .arg("--parents") + .arg(TEST_COPY_FROM_FOLDER_FILE) + .arg(TEST_HELLO_WORLD_DEST) + .run(); + println!("{:?}", result); + + // Check that we did not succeed in copying. + assert!(!result.success); + assert!(result + .stderr + .contains("with --parents, the destination must be a directory")); +} + +#[test] +// For now, disable the test on Windows. Symlinks aren't well support on Windows. +// It works on Unix for now and it works locally when run from a powershell +#[cfg(not(windows))] +fn test_cp_deref_folder_to_folder() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let cwd = env::current_dir().unwrap(); + + let path_to_new_symlink = at.subdir.join(TEST_COPY_FROM_FOLDER); + + // Change the cwd to have a correct symlink + assert!(env::set_current_dir(&path_to_new_symlink).is_ok()); + + #[cfg(not(windows))] + let _r = fs::symlink(TEST_HELLO_WORLD_SOURCE, TEST_HELLO_WORLD_SOURCE_SYMLINK); + #[cfg(windows)] + let _r = symlink_file(TEST_HELLO_WORLD_SOURCE, TEST_HELLO_WORLD_SOURCE_SYMLINK); + + // Back to the initial cwd (breaks the other tests) + assert!(env::set_current_dir(&cwd).is_ok()); + + //using -P -R option + let result = scene + .ucmd() + .arg("-L") + .arg("-R") + .arg("-v") + .arg(TEST_COPY_FROM_FOLDER) + .arg(TEST_COPY_TO_FOLDER_NEW) + .run(); + println!("cp output {}", result.stdout); + + // Check that the exit code represents a successful copy. + assert!(result.success); + + #[cfg(not(windows))] + { + let scene2 = TestScenario::new("ls"); + let result = scene2.cmd("ls").arg("-al").arg(path_to_new_symlink).run(); + println!("ls source {}", result.stdout); + + let path_to_new_symlink = at.subdir.join(TEST_COPY_TO_FOLDER_NEW); + + let result = scene2.cmd("ls").arg("-al").arg(path_to_new_symlink).run(); + println!("ls dest {}", result.stdout); + } + + #[cfg(windows)] + { + // No action as this test is disabled but kept in case we want to + // try to make it work in the future. + let a = Command::new("cmd").args(&["/C", "dir"]).output(); + println!("output {:#?}", a); + + let a = Command::new("cmd") + .args(&["/C", "dir", &at.as_string()]) + .output(); + println!("output {:#?}", a); + + let a = Command::new("cmd") + .args(&["/C", "dir", path_to_new_symlink.to_str().unwrap()]) + .output(); + println!("output {:#?}", a); + + let path_to_new_symlink = at.subdir.join(TEST_COPY_FROM_FOLDER); + + let a = Command::new("cmd") + .args(&["/C", "dir", path_to_new_symlink.to_str().unwrap()]) + .output(); + println!("output {:#?}", a); + + let path_to_new_symlink = at.subdir.join(TEST_COPY_TO_FOLDER_NEW); + + let a = Command::new("cmd") + .args(&["/C", "dir", path_to_new_symlink.to_str().unwrap()]) + .output(); + println!("output {:#?}", a); + } + + let path_to_new_symlink = at + .subdir + .join(TEST_COPY_TO_FOLDER_NEW) + .join(TEST_HELLO_WORLD_SOURCE_SYMLINK); + assert!(at.file_exists( + &path_to_new_symlink + .clone() + .into_os_string() + .into_string() + .unwrap() + )); + + let path_to_new = at.subdir.join(TEST_COPY_TO_FOLDER_NEW_FILE); + + // Check the content of the destination file that was copied. + let path_to_check = path_to_new.to_str().unwrap(); + assert_eq!(at.read(path_to_check), "Hello, World!\n"); + + // Check the content of the symlink + let path_to_check = path_to_new_symlink.to_str().unwrap(); + assert_eq!(at.read(&path_to_check), "Hello, World!\n"); +} + #[test] // For now, disable the test on Windows. Symlinks aren't well support on Windows. // It works on Unix for now and it works locally when run from a powershell @@ -418,8 +709,7 @@ fn test_cp_no_deref_folder_to_folder() { println!("cp output {}", result.stdout); // Check that the exit code represents a successful copy. - let exit_success = result.success; - assert!(exit_success); + assert!(result.success); #[cfg(not(windows))] { @@ -487,3 +777,300 @@ fn test_cp_no_deref_folder_to_folder() { let path_to_check = path_to_new_symlink.to_str().unwrap(); assert_eq!(at.read(&path_to_check), "Hello, World!\n"); } + +#[test] +#[cfg(target_os = "linux")] +fn test_cp_archive() { + let (at, mut ucmd) = at_and_ucmd!(); + let ts = time::now().to_timespec(); + let previous = FileTime::from_unix_time(ts.sec as i64 - 3600, ts.nsec as u32); + // set the file creation/modif an hour ago + filetime::set_file_times( + at.plus_as_string(TEST_HELLO_WORLD_SOURCE), + previous, + previous, + ) + .unwrap(); + let result = ucmd + .arg(TEST_HELLO_WORLD_SOURCE) + .arg("--archive") + .arg(TEST_HOW_ARE_YOU_SOURCE) + .run(); + + assert!(result.success); + assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n"); + + let metadata = std_fs::metadata(at.subdir.join(TEST_HELLO_WORLD_SOURCE)).unwrap(); + let creation = metadata.modified().unwrap(); + + let metadata2 = std_fs::metadata(at.subdir.join(TEST_HOW_ARE_YOU_SOURCE)).unwrap(); + let creation2 = metadata2.modified().unwrap(); + + let scene2 = TestScenario::new("ls"); + let result = scene2.cmd("ls").arg("-al").arg(at.subdir).run(); + + println!("ls dest {}", result.stdout); + assert_eq!(creation, creation2); + assert!(result.success); +} + +#[test] +#[cfg(target_os = "unix")] +fn test_cp_archive_recursive() { + let (at, mut ucmd) = at_and_ucmd!(); + let cwd = env::current_dir().unwrap(); + + // creates + // dir/1 + // dir/1.link => dir/1 + // dir/2 + // dir/2.link => dir/2 + + let file_1 = at.subdir.join(TEST_COPY_TO_FOLDER).join("1"); + let file_1_link = at.subdir.join(TEST_COPY_TO_FOLDER).join("1.link"); + let file_2 = at.subdir.join(TEST_COPY_TO_FOLDER).join("2"); + let file_2_link = at.subdir.join(TEST_COPY_TO_FOLDER).join("2.link"); + + at.touch(&file_1.to_string_lossy()); + at.touch(&file_2.to_string_lossy()); + + // Change the cwd to have a correct symlink + assert!(env::set_current_dir(&at.subdir.join(TEST_COPY_TO_FOLDER)).is_ok()); + + #[cfg(not(windows))] + { + let _r = fs::symlink("1", &file_1_link); + let _r = fs::symlink("2", &file_2_link); + } + #[cfg(windows)] + { + let _r = symlink_file("1", &file_1_link); + let _r = symlink_file("2", &file_2_link); + } + // Back to the initial cwd (breaks the other tests) + assert!(env::set_current_dir(&cwd).is_ok()); + + let resultg = ucmd + .arg("--archive") + .arg(TEST_COPY_TO_FOLDER) + .arg(TEST_COPY_TO_FOLDER_NEW) + .run(); + + let scene2 = TestScenario::new("ls"); + let result = scene2 + .cmd("ls") + .arg("-al") + .arg(&at.subdir.join(TEST_COPY_TO_FOLDER)) + .run(); + + println!("ls dest {}", result.stdout); + + let scene2 = TestScenario::new("ls"); + let result = scene2 + .cmd("ls") + .arg("-al") + .arg(&at.subdir.join(TEST_COPY_TO_FOLDER_NEW)) + .run(); + + println!("ls dest {}", result.stdout); + assert!(at.file_exists( + &at.subdir + .join(TEST_COPY_TO_FOLDER_NEW) + .join("1.link") + .to_string_lossy() + )); + assert!(at.file_exists( + &at.subdir + .join(TEST_COPY_TO_FOLDER_NEW) + .join("2.link") + .to_string_lossy() + )); + assert!(at.file_exists( + &at.subdir + .join(TEST_COPY_TO_FOLDER_NEW) + .join("1") + .to_string_lossy() + )); + assert!(at.file_exists( + &at.subdir + .join(TEST_COPY_TO_FOLDER_NEW) + .join("2") + .to_string_lossy() + )); + + assert!(at.is_symlink( + &at.subdir + .join(TEST_COPY_TO_FOLDER_NEW) + .join("1.link") + .to_string_lossy() + )); + assert!(at.is_symlink( + &at.subdir + .join(TEST_COPY_TO_FOLDER_NEW) + .join("2.link") + .to_string_lossy() + )); + + // fails for now + assert!(resultg.success); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_cp_preserve_timestamps() { + let (at, mut ucmd) = at_and_ucmd!(); + let ts = time::now().to_timespec(); + let previous = FileTime::from_unix_time(ts.sec as i64 - 3600, ts.nsec as u32); + // set the file creation/modif an hour ago + filetime::set_file_times( + at.plus_as_string(TEST_HELLO_WORLD_SOURCE), + previous, + previous, + ) + .unwrap(); + let result = ucmd + .arg(TEST_HELLO_WORLD_SOURCE) + .arg("--preserve=timestamps") + .arg(TEST_HOW_ARE_YOU_SOURCE) + .run(); + + assert!(result.success); + assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n"); + + let metadata = std_fs::metadata(at.subdir.join(TEST_HELLO_WORLD_SOURCE)).unwrap(); + let creation = metadata.modified().unwrap(); + + let metadata2 = std_fs::metadata(at.subdir.join(TEST_HOW_ARE_YOU_SOURCE)).unwrap(); + let creation2 = metadata2.modified().unwrap(); + + let scene2 = TestScenario::new("ls"); + let result = scene2.cmd("ls").arg("-al").arg(at.subdir).run(); + + println!("ls dest {}", result.stdout); + assert_eq!(creation, creation2); + assert!(result.success); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_cp_dont_preserve_timestamps() { + let (at, mut ucmd) = at_and_ucmd!(); + let ts = time::now().to_timespec(); + let previous = FileTime::from_unix_time(ts.sec as i64 - 3600, ts.nsec as u32); + // set the file creation/modif an hour ago + filetime::set_file_times( + at.plus_as_string(TEST_HELLO_WORLD_SOURCE), + previous, + previous, + ) + .unwrap(); + sleep(Duration::from_secs(3)); + + let result = ucmd + .arg(TEST_HELLO_WORLD_SOURCE) + .arg("--no-preserve=timestamps") + .arg(TEST_HOW_ARE_YOU_SOURCE) + .run(); + + assert!(result.success); + assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n"); + + let metadata = std_fs::metadata(at.subdir.join(TEST_HELLO_WORLD_SOURCE)).unwrap(); + let creation = metadata.modified().unwrap(); + + let metadata2 = std_fs::metadata(at.subdir.join(TEST_HOW_ARE_YOU_SOURCE)).unwrap(); + let creation2 = metadata2.modified().unwrap(); + + let scene2 = TestScenario::new("ls"); + let result = scene2.cmd("ls").arg("-al").arg(at.subdir).run(); + + println!("ls dest {}", result.stdout); + println!("creation {:?} / {:?}", creation, creation2); + + assert_ne!(creation, creation2); + let res = creation.elapsed().unwrap() - creation2.elapsed().unwrap(); + // Some margins with time check + assert!(res.as_secs() > 3595); + assert!(res.as_secs() < 3605); + assert!(result.success); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_cp_target_file_dev_null() { + let (at, mut ucmd) = at_and_ucmd!(); + let file1 = "/dev/null"; + let file2 = "test_cp_target_file_file_i2"; + + at.touch(file2); + ucmd.arg(file1).arg(file2).succeeds().no_stderr(); + + assert!(at.file_exists(file2)); +} + +#[test] +#[cfg(any(target_os = "linux", target_os = "freebsd"))] +fn test_cp_one_file_system() { + use crate::common::util::AtPath; + use walkdir::WalkDir; + + let scene = TestScenario::new(util_name!()); + + // Test must be run as root (or with `sudo -E`) + if scene.cmd("whoami").run().stdout != "root\n" { + return; + } + + let at = scene.fixtures.clone(); + let at_src = AtPath::new(&at.plus(TEST_MOUNT_COPY_FROM_FOLDER)); + let at_dst = AtPath::new(&at.plus(TEST_COPY_TO_FOLDER_NEW)); + + // Prepare the mount + at_src.mkdir(TEST_MOUNT_MOUNTPOINT); + let mountpoint_path = &at_src.plus_as_string(TEST_MOUNT_MOUNTPOINT); + + let _r = scene + .cmd("mount") + .arg("-t") + .arg("tmpfs") + .arg("-o") + .arg("size=640k") // ought to be enough + .arg("tmpfs") + .arg(mountpoint_path) + .run(); + assert!(_r.code == Some(0), "{}", _r.stderr); + + at_src.touch(TEST_MOUNT_OTHER_FILESYSTEM_FILE); + + // Begin testing -x flag + let result = scene + .ucmd() + .arg("-rx") + .arg(TEST_MOUNT_COPY_FROM_FOLDER) + .arg(TEST_COPY_TO_FOLDER_NEW) + .run(); + + // Ditch the mount before the asserts + let _r = scene.cmd("umount").arg(mountpoint_path).run(); + assert!(_r.code == Some(0), "{}", _r.stderr); + + assert!(result.success); + assert!(!at_dst.file_exists(TEST_MOUNT_OTHER_FILESYSTEM_FILE)); + // Check if the other files were copied from the source folder hirerarchy + for entry in WalkDir::new(at_src.as_string()) { + let entry = entry.unwrap(); + let relative_src = entry + .path() + .strip_prefix(at_src.as_string()) + .unwrap() + .to_str() + .unwrap(); + + let ft = entry.file_type(); + match (ft.is_dir(), ft.is_file(), ft.is_symlink()) { + (true, _, _) => assert!(at_dst.dir_exists(relative_src)), + (_, true, _) => assert!(at_dst.file_exists(relative_src)), + (_, _, _) => panic!(), + } + } +} diff --git a/tests/by-util/test_csplit.rs b/tests/by-util/test_csplit.rs new file mode 100644 index 000000000..51cab483c --- /dev/null +++ b/tests/by-util/test_csplit.rs @@ -0,0 +1,1335 @@ +use crate::common::util::*; +use glob::glob; + +/// Returns a string of numbers with the given range, each on a new line. +/// The upper bound is not included. +fn generate(from: u32, to: u32) -> String { + (from..to).fold(String::new(), |acc, v| format!("{}{}\n", acc, v)) +} + +#[test] +fn test_stdin() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["-", "10"]) + .pipe_in(generate(1, 51)) + .succeeds() + .stdout_only("18\n123\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 2); + assert_eq!(at.read("xx00"), generate(1, 10)); + assert_eq!(at.read("xx01"), generate(10, 51)); +} + +#[test] +fn test_up_to_line() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "10"]) + .succeeds() + .stdout_only("18\n123\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 2); + assert_eq!(at.read("xx00"), generate(1, 10)); + assert_eq!(at.read("xx01"), generate(10, 51)); +} + +#[test] +fn test_up_to_line_repeat_twice() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "10", "{2}"]) + .succeeds() + .stdout_only("18\n30\n30\n63\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 4); + assert_eq!(at.read("xx00"), generate(1, 10)); + assert_eq!(at.read("xx01"), generate(10, 20)); + assert_eq!(at.read("xx02"), generate(20, 30)); + assert_eq!(at.read("xx03"), generate(30, 51)); +} + +#[test] +fn test_up_to_line_sequence() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "10", "25"]) + .succeeds() + .stdout_only("18\n45\n78\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 3); + assert_eq!(at.read("xx00"), generate(1, 10)); + assert_eq!(at.read("xx01"), generate(10, 25)); + assert_eq!(at.read("xx02"), generate(25, 51)); +} + +#[test] +fn test_up_to_match() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/9$/"]) + .succeeds() + .stdout_only("16\n125\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 2); + assert_eq!(at.read("xx00"), generate(1, 9)); + assert_eq!(at.read("xx01"), generate(9, 51)); +} + +#[test] +fn test_up_to_match_repeat_twice() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/9$/", "{2}"]) + .succeeds() + .stdout_only("16\n29\n30\n66\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 4); + assert_eq!(at.read("xx00"), generate(1, 9)); + assert_eq!(at.read("xx01"), generate(9, 19)); + assert_eq!(at.read("xx02"), generate(19, 29)); + assert_eq!(at.read("xx03"), generate(29, 51)); +} + +#[test] +fn test_up_to_match_sequence() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/9$/", "/5$/"]) + .succeeds() + .stdout_only("16\n17\n108\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 3); + assert_eq!(at.read("xx00"), generate(1, 9)); + assert_eq!(at.read("xx01"), generate(9, 15)); + assert_eq!(at.read("xx02"), generate(15, 51)); +} + +#[test] +fn test_up_to_match_offset() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/9$/+3"]) + .succeeds() + .stdout_only("24\n117\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 2); + assert_eq!(at.read("xx00"), generate(1, 12)); + assert_eq!(at.read("xx01"), generate(12, 51)); +} + +#[test] +fn test_up_to_match_offset_repeat_twice() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/9$/+3", "{2}"]) + .succeeds() + .stdout_only("24\n30\n30\n57\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 4); + assert_eq!(at.read("xx00"), generate(1, 12)); + assert_eq!(at.read("xx01"), generate(12, 22)); + assert_eq!(at.read("xx02"), generate(22, 32)); + assert_eq!(at.read("xx03"), generate(32, 51)); +} + +#[test] +fn test_up_to_match_negative_offset() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/9$/-3"]) + .succeeds() + .stdout_only("10\n131\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 2); + assert_eq!(at.read("xx00"), generate(1, 6)); + assert_eq!(at.read("xx01"), generate(6, 51)); +} + +#[test] +fn test_up_to_match_negative_offset_repeat_twice() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/9$/-3", "{2}"]) + .succeeds() + .stdout_only("10\n26\n30\n75\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 4); + assert_eq!(at.read("xx00"), generate(1, 6)); + assert_eq!(at.read("xx01"), generate(6, 16)); + assert_eq!(at.read("xx02"), generate(16, 26)); + assert_eq!(at.read("xx03"), generate(26, 51)); +} + +#[test] +fn test_up_to_match_repeat_always() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/9$/", "{*}"]) + .succeeds() + .stdout_only("16\n29\n30\n30\n30\n6\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 6); + assert_eq!(at.read("xx00"), generate(1, 9)); + assert_eq!(at.read("xx01"), generate(9, 19)); + assert_eq!(at.read("xx02"), generate(19, 29)); + assert_eq!(at.read("xx03"), generate(29, 39)); + assert_eq!(at.read("xx04"), generate(39, 49)); + assert_eq!(at.read("xx05"), generate(49, 51)); +} + +#[test] +fn test_up_to_match_repeat_over() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/9$/", "{50}"]) + .fails() + .stdout_is("16\n29\n30\n30\n30\n6\n") + .stderr_is("csplit: error: '/9$/': match not found on repetition 5"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 0); + + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/9$/", "{50}", "-k"]) + .fails() + .stdout_is("16\n29\n30\n30\n30\n6\n") + .stderr_is("csplit: error: '/9$/': match not found on repetition 5"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 6); + assert_eq!(at.read("xx00"), generate(1, 9)); + assert_eq!(at.read("xx01"), generate(9, 19)); + assert_eq!(at.read("xx02"), generate(19, 29)); + assert_eq!(at.read("xx03"), generate(29, 39)); + assert_eq!(at.read("xx04"), generate(39, 49)); + assert_eq!(at.read("xx05"), generate(49, 51)); +} + +#[test] +fn test_skip_to_match() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "%23%"]) + .succeeds() + .stdout_only("84\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 1); + assert_eq!(at.read("xx00"), generate(23, 51)); +} + +#[test] +fn test_skip_to_match_sequence1() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "%0$%", "%^4%"]) + .succeeds() + .stdout_only("33\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 1); + assert_eq!(at.read("xx00"), generate(40, 51)); +} + +#[test] +fn test_skip_to_match_sequence2() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "%0$%", "{1}", "%^4%"]) + .succeeds() + .stdout_only("33\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 1); + assert_eq!(at.read("xx00"), generate(40, 51)); +} + +#[test] +fn test_skip_to_match_sequence3() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "%0$%", "{1}", "/^4/"]) + .succeeds() + .stdout_only("60\n33\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 2); + assert_eq!(at.read("xx00"), generate(20, 40)); + assert_eq!(at.read("xx01"), generate(40, 51)); +} + +#[test] +fn test_skip_to_match_sequence4() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "%0$%", "/^4/"]) + .succeeds() + .stdout_only("90\n33\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 2); + assert_eq!(at.read("xx00"), generate(10, 40)); + assert_eq!(at.read("xx01"), generate(40, 51)); +} + +#[test] +fn test_skip_to_match_offset() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "%23%+3"]) + .succeeds() + .stdout_only("75\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 1); + assert_eq!(at.read("xx00"), generate(26, 51)); +} + +#[test] +fn test_skip_to_match_negative_offset() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "%23%-3"]) + .succeeds() + .stdout_only("93\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 1); + assert_eq!(at.read("xx00"), generate(20, 51)); +} + +#[test] +fn test_skip_to_match_repeat_always() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "%0$%", "{*}"]) + .succeeds() + .no_stdout(); + + let count = glob(&at.plus_as_string("xx*")).unwrap().count(); + assert_eq!(count, 0); +} + +#[test] +fn test_mix() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "13", "%25%", "/0$/"]) + .succeeds() + .stdout_only("27\n15\n63\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 3); + assert_eq!(at.read("xx00"), generate(1, 13)); + assert_eq!(at.read("xx01"), generate(25, 30)); + assert_eq!(at.read("xx02"), generate(30, 51)); +} + +#[test] +fn test_option_keep() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["-k", "numbers50.txt", "/20/", "/nope/"]) + .fails() + .stderr_is("csplit: error: '/nope/': match not found") + .stdout_is("48\n93\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 2); + assert_eq!(at.read("xx00"), generate(1, 20)); + assert_eq!(at.read("xx01"), generate(20, 51)); +} + +#[test] +fn test_option_quiet() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["--quiet", "numbers50.txt", "13", "%25%", "/0$/"]) + .succeeds() + .no_stdout(); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 3); + assert_eq!(at.read("xx00"), generate(1, 13)); + assert_eq!(at.read("xx01"), generate(25, 30)); + assert_eq!(at.read("xx02"), generate(30, 51)); +} + +#[test] +fn test_option_prefix() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["--prefix", "dog", "numbers50.txt", "13", "%25%", "/0$/"]) + .succeeds() + .stdout_only("27\n15\n63\n"); + + let count = glob(&at.plus_as_string("dog*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 3); + assert_eq!(at.read("dog00"), generate(1, 13)); + assert_eq!(at.read("dog01"), generate(25, 30)); + assert_eq!(at.read("dog02"), generate(30, 51)); +} + +#[test] +fn test_negative_offset_at_start() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["-", "/a/-1", "{*}"]) + .pipe_in("\na\n") + .succeeds() + .stdout_only("0\n3\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 2); + assert_eq!(at.read("xx00"), ""); + assert_eq!(at.read("xx01"), "\na\n"); +} + +#[test] +fn test_up_to_match_option_suppress_matched() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "--suppress-matched", "/0$/", "{*}"]) + .succeeds() + .stdout_only("18\n27\n27\n27\n27\n0\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 6); + assert_eq!(at.read("xx00"), generate(1, 10)); + assert_eq!(at.read("xx01"), generate(11, 20)); + assert_eq!(at.read("xx02"), generate(21, 30)); + assert_eq!(at.read("xx03"), generate(31, 40)); + assert_eq!(at.read("xx04"), generate(41, 50)); + assert_eq!(at.read("xx05"), ""); +} + +#[test] +fn test_up_to_match_offset_option_suppress_matched() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "--suppress-matched", "/10/+4"]) + .succeeds() + .stdout_only("27\n111\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 2); + assert_eq!(at.read("xx00"), generate(1, 10) + &generate(11, 14)); + assert_eq!(at.read("xx01"), generate(14, 51)); +} + +#[test] +fn test_up_to_match_negative_offset_option_suppress_matched() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "--suppress-matched", "/10/-4"]) + .succeeds() + .stdout_only("10\n128\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 2); + assert_eq!(at.read("xx00"), generate(1, 6)); + assert_eq!(at.read("xx01"), generate(6, 10) + &generate(11, 51)); +} + +#[test] +fn test_up_to_line_option_suppress_matched() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "--suppress-matched", "10"]) + .succeeds() + .stdout_only("18\n120\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 2); + assert_eq!(at.read("xx00"), generate(1, 10)); + assert_eq!(at.read("xx01"), generate(11, 51)); +} + +#[test] +fn test_skip_to_match_option_suppress_matched() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "--suppress-matched", "%0$%"]) + .succeeds() + .stdout_only("120\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 1); + assert_eq!(at.read("xx00"), generate(11, 51)); +} + +#[test] +fn test_option_elide_empty_file1() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "--suppress-matched", "-z", "/0$/", "{*}"]) + .succeeds() + .stdout_only("18\n27\n27\n27\n27\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 5); + assert_eq!(at.read("xx00"), generate(1, 10)); + assert_eq!(at.read("xx01"), generate(11, 20)); + assert_eq!(at.read("xx02"), generate(21, 30)); + assert_eq!(at.read("xx03"), generate(31, 40)); + assert_eq!(at.read("xx04"), generate(41, 50)); +} + +#[test] +fn test_option_elide_empty_file2() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["-", "-z", "/a/-1", "{*}"]) + .pipe_in("\na\n") + .succeeds() + .stdout_only("3\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 1); + assert_eq!(at.read("xx00"), "\na\n"); +} + +#[test] +fn test_up_to_match_context_overflow() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/45/+10"]) + .fails() + .stdout_is("141\n") + .stderr_is("csplit: error: '/45/+10': line number out of range"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("counting splits") + .count(); + assert_eq!(count, 0); + let (at, mut ucmd) = at_and_ucmd!(); + + ucmd.args(&["numbers50.txt", "/45/+10", "-k"]) + .fails() + .stdout_is("141\n") + .stderr_is("csplit: error: '/45/+10': line number out of range"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("counting splits") + .count(); + assert_eq!(count, 1); + assert_eq!(at.read("xx00"), generate(1, 51)); +} + +#[test] +fn test_skip_to_match_context_underflow() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "%5%-10"]) + .fails() + .stdout_is("141\n") + .stderr_is("csplit: error: '%5%-10': line number out of range"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("counting splits") + .count(); + assert_eq!(count, 0); + + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "%5%-10", "-k"]) + .fails() + .stdout_is("141\n") + .stderr_is("csplit: error: '%5%-10': line number out of range"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("counting splits") + .count(); + assert_eq!(count, 1); + assert_eq!(at.read("xx00"), generate(1, 51)); +} + +#[test] +fn test_skip_to_match_context_overflow() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "%45%+10"]) + .fails() + .stderr_only("csplit: error: '%45%+10': line number out of range"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("counting splits") + .count(); + assert_eq!(count, 0); + + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "%45%+10", "-k"]) + .fails() + .stderr_only("csplit: error: '%45%+10': line number out of range"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("counting splits") + .count(); + assert_eq!(count, 0); +} + +#[test] +fn test_up_to_no_match1() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/4/", "/nope/"]) + .fails() + .stdout_is("6\n135\n") + .stderr_is("csplit: error: '/nope/': match not found"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("counting splits") + .count(); + assert_eq!(count, 0); + + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/4/", "/nope/", "-k"]) + .fails() + .stdout_is("6\n135\n") + .stderr_is("csplit: error: '/nope/': match not found"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("counting splits") + .count(); + assert_eq!(count, 2); + assert_eq!(at.read("xx00"), generate(1, 4)); + assert_eq!(at.read("xx01"), generate(4, 51)); +} + +#[test] +fn test_up_to_no_match2() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/4/", "/nope/", "{50}"]) + .fails() + .stdout_is("6\n135\n") + .stderr_is("csplit: error: '/nope/': match not found"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("counting splits") + .count(); + assert_eq!(count, 0); + + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/4/", "/nope/", "{50}", "-k"]) + .fails() + .stdout_is("6\n135\n") + .stderr_is("csplit: error: '/nope/': match not found"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("counting splits") + .count(); + assert_eq!(count, 2); + assert_eq!(at.read("xx00"), generate(1, 4)); + assert_eq!(at.read("xx01"), generate(4, 51)); +} + +#[test] +fn test_up_to_no_match3() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/0$/", "{50}"]) + .fails() + .stdout_is("18\n30\n30\n30\n30\n3\n") + .stderr_is("csplit: error: '/0$/': match not found on repetition 5"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("counting splits") + .count(); + assert_eq!(count, 0); + + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/0$/", "{50}", "-k"]) + .fails() + .stdout_is("18\n30\n30\n30\n30\n3\n") + .stderr_is("csplit: error: '/0$/': match not found on repetition 5"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("counting splits") + .count(); + assert_eq!(count, 6); + assert_eq!(at.read("xx00"), generate(1, 10)); + assert_eq!(at.read("xx01"), generate(10, 20)); + assert_eq!(at.read("xx02"), generate(20, 30)); + assert_eq!(at.read("xx03"), generate(30, 40)); + assert_eq!(at.read("xx04"), generate(40, 50)); + assert_eq!(at.read("xx05"), "50\n"); +} + +#[test] +fn test_up_to_no_match4() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/nope/", "/4/"]) + .fails() + .stdout_is("141\n") + .stderr_is("csplit: error: '/nope/': match not found"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("counting splits") + .count(); + assert_eq!(count, 0); + + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/nope/", "/4/", "-k"]) + .fails() + .stdout_is("141\n") + .stderr_is("csplit: error: '/nope/': match not found"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("counting splits") + .count(); + assert_eq!(count, 1); + assert_eq!(at.read("xx00"), generate(1, 51)); +} + +#[test] +fn test_up_to_no_match5() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/nope/", "{*}"]) + .succeeds() + .stdout_only("141\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("counting splits") + .count(); + assert_eq!(count, 1); + assert_eq!(at.read("xx00"), generate(1, 51)); +} + +#[test] +fn test_up_to_no_match6() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/nope/-5"]) + .fails() + .stdout_is("141\n") + .stderr_is("csplit: error: '/nope/-5': match not found"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("counting splits") + .count(); + assert_eq!(count, 0); + + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/nope/-5", "-k"]) + .fails() + .stdout_is("141\n") + .stderr_is("csplit: error: '/nope/-5': match not found"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("counting splits") + .count(); + assert_eq!(count, 1); + assert_eq!(at.read("xx00"), generate(1, 51)); +} + +#[test] +fn test_up_to_no_match7() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/nope/+5"]) + .fails() + .stdout_is("141\n") + .stderr_is("csplit: error: '/nope/+5': match not found"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("counting splits") + .count(); + assert_eq!(count, 0); + + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/nope/+5", "-k"]) + .fails() + .stdout_is("141\n") + .stderr_is("csplit: error: '/nope/+5': match not found"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("counting splits") + .count(); + assert_eq!(count, 1); + assert_eq!(at.read("xx00"), generate(1, 51)); +} + +#[test] +fn test_skip_to_no_match1() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "%nope%"]) + .fails() + .stderr_only("csplit: error: '%nope%': match not found"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("counting splits") + .count(); + assert_eq!(count, 0); +} + +#[test] +fn test_skip_to_no_match2() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "%nope%", "{50}"]) + .fails() + .stderr_only("csplit: error: '%nope%': match not found"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("counting splits") + .count(); + assert_eq!(count, 0); +} + +#[test] +fn test_skip_to_no_match3() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "%0$%", "{50}"]) + .fails() + .stderr_only("csplit: error: '%0$%': match not found on repetition 5"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("counting splits") + .count(); + assert_eq!(count, 0); +} + +#[test] +fn test_skip_to_no_match4() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "%nope%", "/4/"]) + .fails() + .stderr_only("csplit: error: '%nope%': match not found"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("counting splits") + .count(); + assert_eq!(count, 0); +} + +#[test] +fn test_skip_to_no_match5() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "%nope%", "{*}"]) + .succeeds() + .no_stderr() + .no_stdout(); + + let count = glob(&at.plus_as_string("xx*")) + .expect("counting splits") + .count(); + assert_eq!(count, 0); +} + +#[test] +fn test_skip_to_no_match6() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "%nope%-5"]) + .fails() + .stderr_only("csplit: error: '%nope%-5': match not found"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("counting splits") + .count(); + assert_eq!(count, 0); +} + +#[test] +fn test_skip_to_no_match7() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "%nope%+5"]) + .fails() + .stderr_only("csplit: error: '%nope%+5': match not found"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("counting splits") + .count(); + assert_eq!(count, 0); +} + +#[test] +fn test_no_match() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "%nope%"]) + .fails() + .stderr_only("csplit: error: '%nope%': match not found"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("counting splits") + .count(); + assert_eq!(count, 0); + + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/nope/"]) + .fails() + .stdout_is("141\n") + .stderr_is("csplit: error: '/nope/': match not found"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("counting splits") + .count(); + assert_eq!(count, 0); +} + +#[test] +fn test_too_small_linenum() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/20/", "10", "/40/"]) + .succeeds() + .stdout_only("48\n0\n60\n33\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 4); + assert_eq!(at.read("xx00"), generate(1, 20)); + assert_eq!(at.read("xx01"), ""); + assert_eq!(at.read("xx02"), generate(20, 40)); + assert_eq!(at.read("xx03"), generate(40, 51)); +} + +#[test] +fn test_too_small_linenum_equal() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/20/", "20"]) + .succeeds() + .stdout_only("48\n0\n93\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 3); + assert_eq!(at.read("xx00"), generate(1, 20)); + assert_eq!(at.read("xx01"), ""); + assert_eq!(at.read("xx02"), generate(20, 51)); +} + +#[test] +fn test_too_small_linenum_elided() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "-z", "/20/", "10", "/40/"]) + .succeeds() + .stdout_only("48\n60\n33\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 3); + assert_eq!(at.read("xx00"), generate(1, 20)); + assert_eq!(at.read("xx01"), generate(20, 40)); + assert_eq!(at.read("xx02"), generate(40, 51)); +} + +#[test] +fn test_too_small_linenum_negative_offset() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/20/-5", "10", "/40/"]) + .succeeds() + .stdout_only("33\n0\n75\n33\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 4); + assert_eq!(at.read("xx00"), generate(1, 15)); + assert_eq!(at.read("xx01"), ""); + assert_eq!(at.read("xx02"), generate(15, 40)); + assert_eq!(at.read("xx03"), generate(40, 51)); +} + +#[test] +fn test_too_small_linenum_twice() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/20/", "10", "15", "/40/"]) + .succeeds() + .stdout_only("48\n0\n0\n60\n33\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 5); + assert_eq!(at.read("xx00"), generate(1, 20)); + assert_eq!(at.read("xx01"), ""); + assert_eq!(at.read("xx02"), ""); + assert_eq!(at.read("xx03"), generate(20, 40)); + assert_eq!(at.read("xx04"), generate(40, 51)); +} + +#[test] +fn test_too_small_linenum_repeat() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/20/", "10", "{*}"]) + .fails() + .stderr_is("csplit: error: '10': line number out of range on repetition 5") + .stdout_is("48\n0\n0\n30\n30\n30\n3\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 0); + + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/20/", "10", "{*}", "-k"]) + .fails() + .stderr_is("csplit: error: '10': line number out of range on repetition 5") + .stdout_is("48\n0\n0\n30\n30\n30\n3\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 7); + assert_eq!(at.read("xx00"), generate(1, 20)); + assert_eq!(at.read("xx01"), ""); + assert_eq!(at.read("xx02"), ""); + assert_eq!(at.read("xx03"), generate(20, 30)); + assert_eq!(at.read("xx04"), generate(30, 40)); + assert_eq!(at.read("xx05"), generate(40, 50)); + assert_eq!(at.read("xx06"), "50\n"); +} + +#[test] +fn test_linenum_out_of_range1() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "100"]) + .fails() + .stdout_is("141\n") + .stderr_is("csplit: error: '100': line number out of range"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 0); + + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "100", "-k"]) + .fails() + .stdout_is("141\n") + .stderr_is("csplit: error: '100': line number out of range"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 1); + assert_eq!(at.read("xx00"), generate(1, 51)); +} + +#[test] +fn test_linenum_out_of_range2() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "10", "100"]) + .fails() + .stdout_is("18\n123\n") + .stderr_is("csplit: error: '100': line number out of range"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 0); + + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "10", "100", "-k"]) + .fails() + .stdout_is("18\n123\n") + .stderr_is("csplit: error: '100': line number out of range"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 2); + assert_eq!(at.read("xx00"), generate(1, 10)); + assert_eq!(at.read("xx01"), generate(10, 51)); +} + +#[test] +fn test_linenum_out_of_range3() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "40", "{2}"]) + .fails() + .stdout_is("108\n33\n") + .stderr_is("csplit: error: '40': line number out of range on repetition 1"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 0); + + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "40", "{2}", "-k"]) + .fails() + .stdout_is("108\n33\n") + .stderr_is("csplit: error: '40': line number out of range on repetition 1"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 2); + assert_eq!(at.read("xx00"), generate(1, 40)); + assert_eq!(at.read("xx01"), generate(40, 51)); +} + +#[test] +fn test_linenum_out_of_range4() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "40", "{*}"]) + .fails() + .stdout_is("108\n33\n") + .stderr_is("csplit: error: '40': line number out of range on repetition 1"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 0); + + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "40", "{*}", "-k"]) + .fails() + .stdout_is("108\n33\n") + .stderr_is("csplit: error: '40': line number out of range on repetition 1"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 2); + assert_eq!(at.read("xx00"), generate(1, 40)); + assert_eq!(at.read("xx01"), generate(40, 51)); +} + +#[test] +fn test_skip_to_match_negative_offset_before_a_match() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/20/-10", "/15/"]) + .fails() + .stdout_is("18\n123\n") + .stderr_is("csplit: error: '/15/': match not found"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 0); +} + +#[test] +fn test_skip_to_match_negative_offset_before_a_linenum() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/20/-10", "15"]) + .succeeds() + .stdout_only("18\n15\n108\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 3); + assert_eq!(at.read("xx00"), generate(1, 10)); + assert_eq!(at.read("xx01"), generate(10, 15)); + assert_eq!(at.read("xx02"), generate(15, 51)); +} + +#[test] +fn test_corner_case1() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/10/", "11"]) + .succeeds() + .stdout_only("18\n3\n120\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 3); + assert_eq!(at.read("xx00"), generate(1, 10)); + assert_eq!(at.read("xx01"), "10\n"); + assert_eq!(at.read("xx02"), generate(11, 51)); +} + +#[test] +fn test_corner_case2() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/10/-5", "/10/"]) + .fails() + .stderr_is("csplit: error: '/10/': match not found") + .stdout_is("8\n133\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 0); +} + +#[test] +fn test_corner_case3() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/15/-3", "14", "/15/"]) + .fails() + .stderr_is("csplit: error: '/15/': match not found") + .stdout_is("24\n6\n111\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 0); +} + +#[test] +fn test_corner_case4() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/20/-10", "/30/-4"]) + .succeeds() + .stdout_only("18\n48\n75\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 3); + assert_eq!(at.read("xx00"), generate(1, 10)); + assert_eq!(at.read("xx01"), generate(10, 26)); + assert_eq!(at.read("xx02"), generate(26, 51)); +} + +// NOTE: differs from gnu's output: the empty split is not written +#[test] +fn test_up_to_match_context_underflow() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/5/-10"]) + .fails() + .stdout_is("0\n141\n") + .stderr_is("csplit: error: '/5/-10': line number out of range"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("counting splits") + .count(); + assert_eq!(count, 0); + + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/5/-10", "-k"]) + .fails() + .stdout_is("0\n141\n") + .stderr_is("csplit: error: '/5/-10': line number out of range"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("counting splits") + .count(); + assert_eq!(count, 2); + assert_eq!(at.read("xx00"), ""); + assert_eq!(at.read("xx01"), generate(1, 51)); +} + +// the offset is out of range because of the first pattern +// NOTE: output different than gnu's: the empty split is written but the rest of the input file is not +#[test] +fn test_linenum_range_with_up_to_match1() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "10", "/12/-5"]) + .fails() + .stderr_is("csplit: error: '/12/-5': line number out of range") + .stdout_is("18\n0\n123\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 0); + + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "10", "/12/-5", "-k"]) + .fails() + .stderr_is("csplit: error: '/12/-5': line number out of range") + .stdout_is("18\n0\n123\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 3); + assert_eq!(at.read("xx00"), generate(1, 10)); + assert_eq!(at.read("xx01"), ""); + assert_eq!(at.read("xx02"), generate(10, 51)); +} + +// the offset is out of range because more lines are needed than physically available +// NOTE: output different than gnu's: the empty split is not written but the rest of the input file is +#[test] +fn test_linenum_range_with_up_to_match2() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "10", "/12/-15"]) + .fails() + .stderr_is("csplit: error: '/12/-15': line number out of range") + .stdout_is("18\n0\n123\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 0); + + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "10", "/12/-15", "-k"]) + .fails() + .stderr_is("csplit: error: '/12/-15': line number out of range") + .stdout_is("18\n0\n123\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 3); + assert_eq!(at.read("xx00"), generate(1, 10)); + assert_eq!(at.read("xx01"), ""); + assert_eq!(at.read("xx02"), generate(10, 51)); +} + +// NOTE: output different than gnu's: the pattern /10/ is matched but should not +#[test] +fn test_linenum_range_with_up_to_match3() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "10", "/10/", "-k"]) + .fails() + .stderr_is("csplit: error: '/10/': match not found") + .stdout_is("18\n123\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 2); + assert_eq!(at.read("xx00"), generate(1, 10)); + assert_eq!(at.read("xx01"), generate(10, 51)); + + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", "/10/", "10"]) + .succeeds() + .stdout_only("18\n0\n123\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 3); + assert_eq!(at.read("xx00"), generate(1, 10)); + assert_eq!(at.read("xx01"), ""); + assert_eq!(at.read("xx02"), generate(10, 51)); +} diff --git a/tests/by-util/test_cut.rs b/tests/by-util/test_cut.rs index fc0f1b1f9..875317721 100644 --- a/tests/by-util/test_cut.rs +++ b/tests/by-util/test_cut.rs @@ -139,3 +139,21 @@ fn test_zero_terminated_only_delimited() { .succeeds() .stdout_only("82\n7\0"); } + +#[test] +fn test_directory_and_no_such_file() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.mkdir("some"); + + ucmd.arg("-b1") + .arg("some") + .run() + .stderr_is("cut: error: some: Is a directory\n"); + + new_ucmd!() + .arg("-b1") + .arg("some") + .run() + .stderr_is("cut: error: some: No such file or directory\n"); +} diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 0837878b2..5619aed94 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -2,6 +2,8 @@ extern crate regex; use self::regex::Regex; use crate::common::util::*; +#[cfg(all(unix, not(target_os = "macos")))] +use rust_users::*; #[test] fn test_date_email() { @@ -131,3 +133,92 @@ fn test_date_format_full_day() { let re = Regex::new(r"\S+ \d{4}-\d{2}-\d{2}").unwrap(); assert!(re.is_match(&result.stdout.trim())); } + +#[test] +#[cfg(all(unix, not(target_os = "macos")))] +fn test_date_set_valid() { + if get_effective_uid() == 0 { + let (_, mut ucmd) = at_and_ucmd!(); + let result = ucmd + .arg("--set") + .arg("2020-03-12 13:30:00+08:00") + .succeeds(); + result.no_stdout().no_stderr(); + } +} + +#[test] +#[cfg(any(windows, all(unix, not(target_os = "macos"))))] +fn test_date_set_invalid() { + let (_, mut ucmd) = at_and_ucmd!(); + let result = ucmd.arg("--set").arg("123abcd").fails(); + let result = result.no_stdout(); + assert!(result.stderr.starts_with("date: invalid date ")); +} + +#[test] +#[cfg(all(unix, not(target_os = "macos")))] +fn test_date_set_permissions_error() { + if !(get_effective_uid() == 0 || is_wsl()) { + let (_, mut ucmd) = at_and_ucmd!(); + let result = ucmd.arg("--set").arg("2020-03-11 21:45:00+08:00").fails(); + let result = result.no_stdout(); + assert!(result.stderr.starts_with("date: cannot set date: ")); + } +} + +#[test] +#[cfg(target_os = "macos")] +fn test_date_set_mac_unavailable() { + let (_, mut ucmd) = at_and_ucmd!(); + let result = ucmd.arg("--set").arg("2020-03-11 21:45:00+08:00").fails(); + let result = result.no_stdout(); + assert!(result + .stderr + .starts_with("date: setting the date is not supported by macOS")); +} + +#[test] +#[cfg(all(unix, not(target_os = "macos")))] +/// TODO: expected to fail currently; change to succeeds() when required. +fn test_date_set_valid_2() { + if get_effective_uid() == 0 { + let (_, mut ucmd) = at_and_ucmd!(); + let result = ucmd + .arg("--set") + .arg("Sat 20 Mar 2021 14:53:01 AWST") + .fails(); + let result = result.no_stdout(); + assert!(result.stderr.starts_with("date: invalid date ")); + } +} + +#[test] +#[cfg(all(unix, not(target_os = "macos")))] +/// TODO: expected to fail currently; change to succeeds() when required. +fn test_date_set_valid_3() { + if get_effective_uid() == 0 { + let (_, mut ucmd) = at_and_ucmd!(); + let result = ucmd + .arg("--set") + .arg("Sat 20 Mar 2021 14:53:01") // Local timezone + .fails(); + let result = result.no_stdout(); + assert!(result.stderr.starts_with("date: invalid date ")); + } +} + +#[test] +#[cfg(all(unix, not(target_os = "macos")))] +/// TODO: expected to fail currently; change to succeeds() when required. +fn test_date_set_valid_4() { + if get_effective_uid() == 0 { + let (_, mut ucmd) = at_and_ucmd!(); + let result = ucmd + .arg("--set") + .arg("2020-03-11 21:45:00") // Local timezone + .fails(); + let result = result.no_stdout(); + assert!(result.stderr.starts_with("date: invalid date ")); + } +} diff --git a/tests/by-util/test_du.rs b/tests/by-util/test_du.rs index c9704a658..b3b1b3465 100644 --- a/tests/by-util/test_du.rs +++ b/tests/by-util/test_du.rs @@ -12,7 +12,7 @@ fn test_du_basics() { assert!(result.success); assert_eq!(result.stderr, ""); } -#[cfg(target_os = "macos")] +#[cfg(target_vendor = "apple")] fn _du_basics(s: String) { let answer = "32\t./subdir 8\t./subdir/deeper @@ -21,7 +21,7 @@ fn _du_basics(s: String) { "; assert_eq!(s, answer); } -#[cfg(not(target_os = "macos"))] +#[cfg(not(target_vendor = "apple"))] fn _du_basics(s: String) { let answer = "28\t./subdir 8\t./subdir/deeper @@ -41,11 +41,15 @@ fn test_du_basics_subdir() { _du_basics_subdir(result.stdout); } -#[cfg(target_os = "macos")] +#[cfg(target_vendor = "apple")] fn _du_basics_subdir(s: String) { assert_eq!(s, "4\tsubdir/deeper\n"); } -#[cfg(not(target_os = "macos"))] +#[cfg(target_os = "windows")] +fn _du_basics_subdir(s: String) { + assert_eq!(s, "0\tsubdir/deeper\n"); +} +#[cfg(all(not(target_vendor = "apple"), not(target_os = "windows")))] fn _du_basics_subdir(s: String) { // MS-WSL linux has altered expected output if !is_wsl() { @@ -71,7 +75,7 @@ fn test_du_basics_bad_name() { fn test_du_soft_link() { let ts = TestScenario::new("du"); - let link = ts.cmd("ln").arg("-s").arg(SUB_FILE).arg(SUB_LINK).run(); + let link = ts.ccmd("ln").arg("-s").arg(SUB_FILE).arg(SUB_LINK).run(); assert!(link.success); let result = ts.ucmd().arg(SUB_DIR_LINKS).run(); @@ -80,12 +84,16 @@ fn test_du_soft_link() { _du_soft_link(result.stdout); } -#[cfg(target_os = "macos")] +#[cfg(target_vendor = "apple")] fn _du_soft_link(s: String) { // 'macos' host variants may have `du` output variation for soft links assert!((s == "12\tsubdir/links\n") || (s == "16\tsubdir/links\n")); } -#[cfg(not(target_os = "macos"))] +#[cfg(target_os = "windows")] +fn _du_soft_link(s: String) { + assert_eq!(s, "8\tsubdir/links\n"); +} +#[cfg(all(not(target_vendor = "apple"), not(target_os = "windows")))] fn _du_soft_link(s: String) { // MS-WSL linux has altered expected output if !is_wsl() { @@ -99,7 +107,7 @@ fn _du_soft_link(s: String) { fn test_du_hard_link() { let ts = TestScenario::new("du"); - let link = ts.cmd("ln").arg(SUB_FILE).arg(SUB_LINK).run(); + let link = ts.ccmd("ln").arg(SUB_FILE).arg(SUB_LINK).run(); assert!(link.success); let result = ts.ucmd().arg(SUB_DIR_LINKS).run(); @@ -109,11 +117,15 @@ fn test_du_hard_link() { _du_hard_link(result.stdout); } -#[cfg(target_os = "macos")] +#[cfg(target_vendor = "apple")] fn _du_hard_link(s: String) { assert_eq!(s, "12\tsubdir/links\n") } -#[cfg(not(target_os = "macos"))] +#[cfg(target_os = "windows")] +fn _du_hard_link(s: String) { + assert_eq!(s, "8\tsubdir/links\n") +} +#[cfg(all(not(target_vendor = "apple"), not(target_os = "windows")))] fn _du_hard_link(s: String) { // MS-WSL linux has altered expected output if !is_wsl() { @@ -133,11 +145,15 @@ fn test_du_d_flag() { _du_d_flag(result.stdout); } -#[cfg(target_os = "macos")] +#[cfg(target_vendor = "apple")] fn _du_d_flag(s: String) { assert_eq!(s, "16\t./subdir\n20\t./\n"); } -#[cfg(not(target_os = "macos"))] +#[cfg(target_os = "windows")] +fn _du_d_flag(s: String) { + assert_eq!(s, "8\t./subdir\n8\t./\n"); +} +#[cfg(all(not(target_vendor = "apple"), not(target_os = "windows")))] fn _du_d_flag(s: String) { // MS-WSL linux has altered expected output if !is_wsl() { @@ -146,3 +162,13 @@ fn _du_d_flag(s: String) { assert_eq!(s, "8\t./subdir\n8\t./\n"); } } + +#[test] +fn test_du_h_flag_empty_file() { + let ts = TestScenario::new("du"); + + let result = ts.ucmd().arg("-h").arg("empty.txt").run(); + assert!(result.success); + assert_eq!(result.stderr, ""); + assert_eq!(result.stdout, "0\tempty.txt\n"); +} diff --git a/tests/by-util/test_echo.rs b/tests/by-util/test_echo.rs index 2d073d60b..7394ffc1e 100644 --- a/tests/by-util/test_echo.rs +++ b/tests/by-util/test_echo.rs @@ -173,3 +173,58 @@ fn test_disable_escapes() { .succeeds() .stdout_only(format!("{}\n", input_str)); } + +#[test] +fn test_hyphen_value() { + new_ucmd!().arg("-abc").succeeds().stdout_is("-abc\n"); +} + +#[test] +fn test_multiple_hyphen_values() { + new_ucmd!() + .args(&["-abc", "-def", "-edf"]) + .succeeds() + .stdout_is("-abc -def -edf\n"); +} + +#[test] +fn test_hyphen_values_inside_string() { + new_ucmd!() + .arg("'\"\n'CXXFLAGS=-g -O2'\n\"'") + .succeeds() + .stdout + .contains("CXXFLAGS"); +} + +#[test] +fn test_hyphen_values_at_start() { + let result = new_ucmd!() + .arg("-E") + .arg("-test") + .arg("araba") + .arg("-merci") + .run(); + + assert!(result.success); + assert_eq!(false, result.stdout.contains("-E")); + assert_eq!(result.stdout, "-test araba -merci\n"); +} + +#[test] +fn test_hyphen_values_between() { + let result = new_ucmd!().arg("test").arg("-E").arg("araba").run(); + + assert!(result.success); + assert_eq!(result.stdout, "test -E araba\n"); + + let result = new_ucmd!() + .arg("dumdum ") + .arg("dum dum dum") + .arg("-e") + .arg("dum") + .run(); + + assert!(result.success); + assert_eq!(result.stdout, "dumdum dum dum dum -e dum\n"); + assert_eq!(true, result.stdout.contains("-e")); +} diff --git a/tests/by-util/test_env.rs b/tests/by-util/test_env.rs index f747f041e..2ffb2bc48 100644 --- a/tests/by-util/test_env.rs +++ b/tests/by-util/test_env.rs @@ -1,11 +1,10 @@ #[cfg(not(windows))] use std::fs; -use std::path::Path; -extern crate tempfile; -use self::tempfile::tempdir; use crate::common::util::*; use std::env; +use std::path::Path; +use tempfile::tempdir; #[test] fn test_env_help() { diff --git a/tests/by-util/test_expand.rs b/tests/by-util/test_expand.rs index 121ccccec..801bf9d98 100644 --- a/tests/by-util/test_expand.rs +++ b/tests/by-util/test_expand.rs @@ -46,3 +46,13 @@ fn test_with_space() { assert!(result.success); assert!(result.stdout.contains(" return")); } + +#[test] +fn test_with_multiple_files() { + let (_, mut ucmd) = at_and_ucmd!(); + + let result = ucmd.arg("with-spaces.txt").arg("with-tab.txt").run(); + assert!(result.success); + assert!(result.stdout.contains(" return")); + assert!(result.stdout.contains(" ")); +} diff --git a/tests/by-util/test_factor.rs b/tests/by-util/test_factor.rs index 358f38fd3..5bde17cdb 100644 --- a/tests/by-util/test_factor.rs +++ b/tests/by-util/test_factor.rs @@ -6,34 +6,64 @@ // that was distributed with this source code. use crate::common::util::*; +use std::time::SystemTime; #[path = "../../src/uu/factor/sieve.rs"] mod sieve; + +extern crate conv; +extern crate rand; + +use self::rand::distributions::{Distribution, Uniform}; +use self::rand::{rngs::SmallRng, Rng, SeedableRng}; use self::sieve::Sieve; -extern crate rand; -use self::rand::distributions::{Distribution, Uniform}; -use self::rand::{rngs::SmallRng, FromEntropy, Rng}; - const NUM_PRIMES: usize = 10000; -const LOG_PRIMES: f64 = 14.0; // ceil(log2(NUM_PRIMES)) - const NUM_TESTS: usize = 100; +#[test] +fn test_first_100000_integers() { + extern crate sha1; + + let n_integers = 100_000; + let mut instring = String::new(); + for i in 0..=n_integers { + instring.push_str(&(format!("{} ", i))[..]); + } + + println!("STDIN='{}'", instring); + let result = new_ucmd!().pipe_in(instring.as_bytes()).run(); + let stdout = result.stdout; + + assert!(result.success); + + // `seq 0 100000 | factor | sha1sum` => "4ed2d8403934fa1c76fe4b84c5d4b8850299c359" + let hash_check = sha1::Sha1::from(stdout.as_bytes()).hexdigest(); + assert_eq!(hash_check, "4ed2d8403934fa1c76fe4b84c5d4b8850299c359"); +} + #[test] fn test_random() { + use conv::prelude::*; + + let log_num_primes = f64::value_from(NUM_PRIMES).unwrap().log2().ceil(); let primes = Sieve::primes().take(NUM_PRIMES).collect::>(); - let mut rng = SmallRng::from_entropy(); - println!("(seed) rng={:?}", rng); + let rng_seed = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + println!("rng_seed={:?}", rng_seed); + let mut rng = SmallRng::seed_from_u64(rng_seed); + let mut rand_gt = move |min: u64| { - let mut product = 1u64; + let mut product = 1_u64; let mut factors = Vec::new(); while product < min { // log distribution---higher probability for lower numbers let factor; loop { - let next = rng.gen_range(0f64, LOG_PRIMES).exp2().floor() as usize; + let next = rng.gen_range(0_f64, log_num_primes).exp2().floor() as usize; if next < NUM_PRIMES { factor = primes[next]; break; @@ -73,9 +103,14 @@ fn test_random() { #[test] fn test_random_big() { - let mut rng = SmallRng::from_entropy(); - println!("(seed) rng={:?}", rng); - let bitrange_1 = Uniform::new(14usize, 51); + let rng_seed = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + println!("rng_seed={:?}", rng_seed); + let mut rng = SmallRng::seed_from_u64(rng_seed); + + let bitrange_1 = Uniform::new(14_usize, 51); let mut rand_64 = move || { // first, choose a random number of bits for the first factor let f_bit_1 = bitrange_1.sample(&mut rng); @@ -85,11 +120,11 @@ fn test_random_big() { // we will have a number of additional factors equal to nfacts + 1 // where nfacts is in [0, floor(rem/14) ) NOTE half-open interval // Each prime factor is at least 14 bits, hence floor(rem/14) - let nfacts = Uniform::new(0usize, rem / 14).sample(&mut rng); + let nfacts = Uniform::new(0_usize, rem / 14).sample(&mut rng); // we have to distribute extrabits among the (nfacts + 1) values let extrabits = rem - (nfacts + 1) * 14; // (remember, a Range is a half-open interval) - let extrarange = Uniform::new(0usize, extrabits + 1); + let extrarange = Uniform::new(0_usize, extrabits + 1); // to generate an even split of this range, generate n-1 random elements // in the range, add the desired total value to the end, sort this list, @@ -116,7 +151,7 @@ fn test_random_big() { let f_bits = f_bits; let mut nbits = 0; - let mut product = 1u64; + let mut product = 1_u64; let mut factors = Vec::new(); for bit in f_bits { assert!(bit < 37); @@ -161,6 +196,8 @@ fn test_big_primes() { } fn run(instring: &[u8], outstring: &[u8]) { + println!("STDIN='{}'", String::from_utf8_lossy(instring)); + println!("STDOUT(expected)='{}'", String::from_utf8_lossy(outstring)); // now run factor new_ucmd!() .pipe_in(instring) diff --git a/tests/by-util/test_fmt.rs b/tests/by-util/test_fmt.rs index e7990a760..4533cdf24 100644 --- a/tests/by-util/test_fmt.rs +++ b/tests/by-util/test_fmt.rs @@ -20,6 +20,19 @@ fn test_fmt_q() { ); } +#[test] +fn test_fmt_w_too_big() { + let result = new_ucmd!() + .arg("-w") + .arg("2501") + .arg("one-word-per-line.txt") + .run(); + //.stdout_is_fixture("call_graph.expected"); + assert_eq!( + result.stderr.trim(), + "fmt: error: invalid width: '2501': Numerical result out of range" + ); +} /* #[test] Fails for now, see https://github.com/uutils/coreutils/issues/1501 fn test_fmt_w() { diff --git a/tests/by-util/test_fold.rs b/tests/by-util/test_fold.rs index 57f90cee0..cc92c8ff3 100644 --- a/tests/by-util/test_fold.rs +++ b/tests/by-util/test_fold.rs @@ -23,3 +23,377 @@ fn test_40_column_word_boundary() { .run() .stdout_is_fixture("lorem_ipsum_40_column_word.expected"); } + +#[test] +fn test_default_wrap_with_newlines() { + new_ucmd!() + .arg("lorem_ipsum_new_line.txt") + .run() + .stdout_is_fixture("lorem_ipsum_new_line_80_column.expected"); +} + +#[test] +fn test_should_preserve_empty_line_without_final_newline() { + new_ucmd!() + .arg("-w2") + .pipe_in("12\n\n34") + .succeeds() + .stdout_is("12\n\n34"); +} + +#[test] +fn test_should_preserve_empty_line_and_final_newline() { + new_ucmd!() + .arg("-w2") + .pipe_in("12\n\n34\n") + .succeeds() + .stdout_is("12\n\n34\n"); +} + +#[test] +fn test_should_preserve_empty_lines() { + new_ucmd!().pipe_in("\n").succeeds().stdout_is("\n"); + + new_ucmd!() + .arg("-w1") + .pipe_in("0\n1\n\n2\n\n\n") + .succeeds() + .stdout_is("0\n1\n\n2\n\n\n"); +} + +#[test] +fn test_word_boundary_split_should_preserve_empty_lines() { + new_ucmd!() + .arg("-s") + .pipe_in("\n") + .succeeds() + .stdout_is("\n"); + + new_ucmd!() + .args(&["-w1", "-s"]) + .pipe_in("0\n1\n\n2\n\n\n") + .succeeds() + .stdout_is("0\n1\n\n2\n\n\n"); +} + +#[test] +fn test_should_not_add_newline_when_line_less_than_fold() { + new_ucmd!().pipe_in("1234").succeeds().stdout_is("1234"); +} + +#[test] +fn test_should_not_add_newline_when_line_longer_than_fold() { + new_ucmd!() + .arg("-w2") + .pipe_in("1234") + .succeeds() + .stdout_is("12\n34"); +} + +#[test] +fn test_should_not_add_newline_when_line_equal_to_fold() { + new_ucmd!() + .arg("-w1") + .pipe_in(" ") + .succeeds() + .stdout_is(" "); +} + +#[test] +fn test_should_preserve_final_newline_when_line_less_than_fold() { + new_ucmd!().pipe_in("1234\n").succeeds().stdout_is("1234\n"); +} + +#[test] +fn test_should_preserve_final_newline_when_line_longer_than_fold() { + new_ucmd!() + .arg("-w2") + .pipe_in("1234\n") + .succeeds() + .stdout_is("12\n34\n"); +} + +#[test] +fn test_should_preserve_final_newline_when_line_equal_to_fold() { + new_ucmd!() + .arg("-w2") + .pipe_in("1\n") + .succeeds() + .stdout_is("1\n"); +} + +#[test] +fn test_single_tab_should_not_add_extra_newline() { + new_ucmd!() + .arg("-w1") + .pipe_in("\t") + .succeeds() + .stdout_is("\t"); +} + +#[test] +fn test_initial_tab_counts_as_8_columns() { + new_ucmd!() + .arg("-w8") + .pipe_in("\t1") + .succeeds() + .stdout_is("\t\n1"); +} + +#[test] +fn test_tab_should_advance_to_next_tab_stop() { + // tab advances the column count to the next tab stop, i.e. the width + // of the tab varies based on the leading text + new_ucmd!() + .args(&["-w8", "tab_stops.input"]) + .succeeds() + .stdout_is_fixture("tab_stops_w8.expected"); +} + +#[test] +fn test_all_tabs_should_advance_to_next_tab_stops() { + new_ucmd!() + .args(&["-w16", "tab_stops.input"]) + .succeeds() + .stdout_is_fixture("tab_stops_w16.expected"); +} + +#[test] +fn test_fold_before_tab_with_narrow_width() { + new_ucmd!() + .arg("-w7") + .pipe_in("a\t1") + .succeeds() + .stdout_is("a\n\t\n1"); +} + +#[test] +fn test_fold_at_word_boundary() { + new_ucmd!() + .args(&["-w4", "-s"]) + .pipe_in("one two") + .succeeds() + .stdout_is("one \ntwo"); +} + +#[test] +fn test_fold_at_leading_word_boundary() { + new_ucmd!() + .args(&["-w3", "-s"]) + .pipe_in(" aaa") + .succeeds() + .stdout_is(" \naaa"); +} + +#[test] +fn test_fold_at_word_boundary_preserve_final_newline() { + new_ucmd!() + .args(&["-w4", "-s"]) + .pipe_in("one two\n") + .succeeds() + .stdout_is("one \ntwo\n"); +} + +#[test] +fn test_fold_at_tab() { + new_ucmd!() + .arg("-w8") + .pipe_in("a\tbbb\n") + .succeeds() + .stdout_is("a\t\nbbb\n"); +} + +#[test] +fn test_fold_after_tab() { + new_ucmd!() + .arg("-w10") + .pipe_in("a\tbbb\n") + .succeeds() + .stdout_is("a\tbb\nb\n"); +} + +#[test] +fn test_fold_at_tab_as_word_boundary() { + new_ucmd!() + .args(&["-w8", "-s"]) + .pipe_in("a\tbbb\n") + .succeeds() + .stdout_is("a\t\nbbb\n"); +} + +#[test] +fn test_fold_after_tab_as_word_boundary() { + new_ucmd!() + .args(&["-w10", "-s"]) + .pipe_in("a\tbbb\n") + .succeeds() + .stdout_is("a\t\nbbb\n"); +} + +#[test] +fn test_fold_at_word_boundary_only_whitespace() { + new_ucmd!() + .args(&["-w2", "-s"]) + .pipe_in(" ") + .succeeds() + .stdout_is(" \n "); +} + +#[test] +fn test_fold_at_word_boundary_only_whitespace_preserve_final_newline() { + new_ucmd!() + .args(&["-w2", "-s"]) + .pipe_in(" \n") + .succeeds() + .stdout_is(" \n \n"); +} + +// +// bytewise tests + +#[test] +fn test_bytewise_should_preserve_empty_line_without_final_newline() { + new_ucmd!() + .args(&["-w2", "-b"]) + .pipe_in("123\n\n45") + .succeeds() + .stdout_is("12\n3\n\n45"); +} + +#[test] +fn test_bytewise_should_preserve_empty_line_and_final_newline() { + new_ucmd!() + .args(&["-w2", "-b"]) + .pipe_in("12\n\n34\n") + .succeeds() + .stdout_is("12\n\n34\n"); +} + +#[test] +fn test_bytewise_should_preserve_empty_lines() { + new_ucmd!() + .arg("-b") + .pipe_in("\n") + .succeeds() + .stdout_is("\n"); + + new_ucmd!() + .args(&["-w1", "-b"]) + .pipe_in("0\n1\n\n2\n\n\n") + .succeeds() + .stdout_is("0\n1\n\n2\n\n\n"); +} + +#[test] +fn test_bytewise_word_boundary_split_should_preserve_empty_lines() { + new_ucmd!() + .args(&["-s", "-b"]) + .pipe_in("\n") + .succeeds() + .stdout_is("\n"); + + new_ucmd!() + .args(&["-w1", "-s", "-b"]) + .pipe_in("0\n1\n\n2\n\n\n") + .succeeds() + .stdout_is("0\n1\n\n2\n\n\n"); +} + +#[test] +fn test_bytewise_should_not_add_newline_when_line_less_than_fold() { + new_ucmd!() + .arg("-b") + .pipe_in("1234") + .succeeds() + .stdout_is("1234"); +} + +#[test] +fn test_bytewise_should_not_add_newline_when_line_longer_than_fold() { + new_ucmd!() + .args(&["-w2", "-b"]) + .pipe_in("1234") + .succeeds() + .stdout_is("12\n34"); +} + +#[test] +fn test_bytewise_should_not_add_newline_when_line_equal_to_fold() { + new_ucmd!() + .args(&["-w1", "-b"]) + .pipe_in(" ") + .succeeds() + .stdout_is(" "); +} + +#[test] +fn test_bytewise_should_preserve_final_newline_when_line_less_than_fold() { + new_ucmd!() + .arg("-b") + .pipe_in("1234\n") + .succeeds() + .stdout_is("1234\n"); +} + +#[test] +fn test_bytewise_should_preserve_final_newline_when_line_longer_than_fold() { + new_ucmd!() + .args(&["-w2", "-b"]) + .pipe_in("1234\n") + .succeeds() + .stdout_is("12\n34\n"); +} + +#[test] +fn test_bytewise_should_preserve_final_newline_when_line_equal_to_fold() { + new_ucmd!() + .args(&["-w2", "-b"]) + .pipe_in("1\n") + .succeeds() + .stdout_is("1\n"); +} + +#[test] +fn test_bytewise_single_tab_should_not_add_extra_newline() { + new_ucmd!() + .args(&["-w1", "-b"]) + .pipe_in("\t") + .succeeds() + .stdout_is("\t"); +} + +#[test] +fn test_tab_counts_as_one_byte() { + new_ucmd!() + .args(&["-w2", "-b"]) + .pipe_in("1\t2\n") + .succeeds() + .stdout_is("1\t\n2\n"); +} + +#[test] +fn test_bytewise_fold_before_tab_with_narrow_width() { + new_ucmd!() + .args(&["-w7", "-b"]) + .pipe_in("a\t1") + .succeeds() + .stdout_is("a\t1"); +} + +#[test] +fn test_bytewise_fold_at_word_boundary_only_whitespace() { + new_ucmd!() + .args(&["-w2", "-s", "-b"]) + .pipe_in(" ") + .succeeds() + .stdout_is(" \n "); +} + +#[test] +fn test_bytewise_fold_at_word_boundary_only_whitespace_preserve_final_newline() { + new_ucmd!() + .args(&["-w2", "-s", "-b"]) + .pipe_in(" \n") + .succeeds() + .stdout_is(" \n \n"); +} diff --git a/tests/by-util/test_hashsum.rs b/tests/by-util/test_hashsum.rs index af4021af4..6e7d59107 100644 --- a/tests/by-util/test_hashsum.rs +++ b/tests/by-util/test_hashsum.rs @@ -43,4 +43,5 @@ test_digest! { sha3_512 sha3 512 shake128_256 shake128 256 shake256_512 shake256 512 + b2sum b2sum 512 } diff --git a/tests/by-util/test_head.rs b/tests/by-util/test_head.rs old mode 100644 new mode 100755 index 4324290cb..d91cc1289 --- a/tests/by-util/test_head.rs +++ b/tests/by-util/test_head.rs @@ -89,73 +89,114 @@ fn test_verbose() { #[test] #[ignore] fn test_spams_newline() { + //this test is does not mirror what GNU does new_ucmd!().pipe_in("a").succeeds().stdout_is("a\n"); } #[test] -#[ignore] -fn test_unsupported_byte_syntax() { +fn test_byte_syntax() { new_ucmd!() .args(&["-1c"]) .pipe_in("abc") - .fails() - //GNU head returns "a" - .stdout_is("") - .stderr_is("head: error: Unrecognized option: \'1\'"); + .run() + .stdout_is("a"); } #[test] -#[ignore] -fn test_unsupported_line_syntax() { +fn test_line_syntax() { new_ucmd!() .args(&["-n", "2048m"]) .pipe_in("a\n") - .fails() - //.stdout_is("a\n"); What GNU head returns. - .stdout_is("") - .stderr_is("head: error: invalid line count \'2048m\': invalid digit found in string"); + .run() + .stdout_is("a\n"); } #[test] -#[ignore] -fn test_unsupported_zero_terminated_syntax() { +fn test_zero_terminated_syntax() { new_ucmd!() - .args(&["-z -n 1"]) + .args(&["-z", "-n", "1"]) .pipe_in("x\0y") - .fails() - //GNU Head returns "x\0" - .stderr_is("head: error: Unrecognized option: \'z\'"); + .run() + .stdout_is("x\0"); } #[test] -#[ignore] -fn test_unsupported_zero_terminated_syntax_2() { +fn test_zero_terminated_syntax_2() { new_ucmd!() - .args(&["-z -n 2"]) + .args(&["-z", "-n", "2"]) .pipe_in("x\0y") - .fails() - //GNU Head returns "x\0y" - .stderr_is("head: error: Unrecognized option: \'z\'"); + .run() + .stdout_is("x\0y"); } #[test] -#[ignore] -fn test_unsupported_negative_byte_syntax() { +fn test_negative_byte_syntax() { new_ucmd!() .args(&["--bytes=-2"]) .pipe_in("a\n") - .fails() - //GNU Head returns "" - .stderr_is("head: error: invalid byte count \'-2\': invalid digit found in string"); + .run() + .stdout_is(""); } #[test] -#[ignore] -fn test_bug_in_negative_zero_lines() { +fn test_negative_zero_lines() { new_ucmd!() .args(&["--lines=-0"]) .pipe_in("a\nb\n") .succeeds() - //GNU Head returns "a\nb\n" - .stdout_is(""); + .stdout_is("a\nb\n"); +} +#[test] +fn test_negative_zero_bytes() { + new_ucmd!() + .args(&["--bytes=-0"]) + .pipe_in("qwerty") + .succeeds() + .stdout_is("qwerty"); +} +#[test] +fn test_no_such_file_or_directory() { + let result = new_ucmd!().arg("no_such_file.toml").run(); + + assert_eq!( + true, + result + .stderr + .contains("cannot open 'no_such_file.toml' for reading: No such file or directory") + ) +} + +// there was a bug not caught by previous tests +// where for negative n > 3, the total amount of lines +// was correct, but it would eat from the second line +#[test] +fn test_sequence_fixture() { + new_ucmd!() + .args(&["-n", "-10", "sequence"]) + .run() + .stdout_is_fixture("sequence.expected"); +} +#[test] +fn test_file_backwards() { + new_ucmd!() + .args(&["-c", "-10", "lorem_ipsum.txt"]) + .run() + .stdout_is_fixture("lorem_ipsum_backwards_file.expected"); +} + +#[test] +fn test_zero_terminated() { + new_ucmd!() + .args(&["-z", "zero_terminated.txt"]) + .run() + .stdout_is_fixture("zero_terminated.expected"); +} + +#[test] +fn test_obsolete_extras() { + new_ucmd!() + .args(&["-5zv"]) + .pipe_in("1\02\03\04\05\06") + .succeeds() + .stdout_is("==> standard input <==\n1\02\03\04\05\0"); } diff --git a/tests/by-util/test_hostname.rs b/tests/by-util/test_hostname.rs index a526ddd88..804d47642 100644 --- a/tests/by-util/test_hostname.rs +++ b/tests/by-util/test_hostname.rs @@ -11,7 +11,7 @@ fn test_hostname() { } // FixME: fails for "MacOS" -#[cfg(not(target_os = "macos"))] +#[cfg(not(target_vendor = "apple"))] #[test] fn test_hostname_ip() { let result = new_ucmd!().arg("-i").run(); diff --git a/tests/by-util/test_id.rs b/tests/by-util/test_id.rs index 0541a2a89..116c73995 100644 --- a/tests/by-util/test_id.rs +++ b/tests/by-util/test_id.rs @@ -1,5 +1,17 @@ use crate::common::util::*; +fn return_whoami_username() -> String { + let scene = TestScenario::new("whoami"); + let result = scene.cmd("whoami").run(); + if is_ci() && result.stderr.contains("cannot find name for user ID") { + // In the CI, some server are failing to return whoami. + // As seems to be a configuration issue, ignoring it + return String::from(""); + } + + result.stdout.trim().to_string() +} + #[test] fn test_id() { let scene = TestScenario::new(util_name!()); @@ -29,18 +41,14 @@ fn test_id() { #[test] fn test_id_from_name() { - let mut scene = TestScenario::new("whoami"); - let result = scene.cmd("whoami").run(); - if is_ci() && result.stderr.contains("cannot find name for user ID") { - // In the CI, some server are failing to return whoami. - // As seems to be a configuration issue, ignoring it + let username = return_whoami_username(); + if username == "" { + // Sometimes, the CI is failing here return; } - let username = result.stdout.trim(); - - scene = TestScenario::new(util_name!()); - let result = scene.ucmd().arg(username).succeeds(); + let scene = TestScenario::new(util_name!()); + let result = scene.ucmd().arg(&username).succeeds(); println!("result.stdout = {}", result.stdout); println!("result.stderr = {}", result.stderr); assert!(result.success); @@ -139,3 +147,41 @@ fn test_id_user() { let s1 = String::from(result.stdout.trim()); assert!(s1.parse::().is_ok()); } + +#[test] +fn test_id_pretty_print() { + let username = return_whoami_username(); + if username == "" { + // Sometimes, the CI is failing here + return; + } + + let scene = TestScenario::new(util_name!()); + let result = scene.ucmd().arg("-p").run(); + if result.stdout.trim() == "" { + // Sometimes, the CI is failing here with + // old rust versions on Linux + return; + } + println!("result.stdout = {}", result.stdout); + println!("result.stderr = {}", result.stderr); + assert!(result.success); + assert!(result.stdout.contains(&username)); +} + +#[test] +fn test_id_password_style() { + let username = return_whoami_username(); + if username == "" { + // Sometimes, the CI is failing here + return; + } + let scene = TestScenario::new(util_name!()); + + let result = scene.ucmd().arg("-P").succeeds(); + + println!("result.stdout = {}", result.stdout); + println!("result.stderr = {}", result.stderr); + assert!(result.success); + assert!(result.stdout.starts_with(&username)); +} diff --git a/tests/by-util/test_install.rs b/tests/by-util/test_install.rs index e9e70bd93..8ac6396fd 100644 --- a/tests/by-util/test_install.rs +++ b/tests/by-util/test_install.rs @@ -1,5 +1,9 @@ use crate::common::util::*; +use filetime::FileTime; +use rust_users::*; use std::os::unix::fs::PermissionsExt; +#[cfg(target_os = "linux")] +use std::thread::sleep; #[test] fn test_install_help() { @@ -10,15 +14,15 @@ fn test_install_help() { .succeeds() .no_stderr() .stdout - .contains("Options:")); + .contains("FLAGS:")); } #[test] fn test_install_basic() { let (at, mut ucmd) = at_and_ucmd!(); - let dir = "test_install_target_dir_dir_a"; - let file1 = "test_install_target_dir_file_a1"; - let file2 = "test_install_target_dir_file_a2"; + let dir = "target_dir"; + let file1 = "source_file1"; + let file2 = "source_file2"; at.touch(file1); at.touch(file2); @@ -31,12 +35,24 @@ fn test_install_basic() { assert!(at.file_exists(&format!("{}/{}", dir, file2))); } +#[test] +fn test_install_twice_dir() { + let dir = "dir"; + let scene = TestScenario::new(util_name!()); + + scene.ucmd().arg("-d").arg(dir).succeeds(); + scene.ucmd().arg("-d").arg(dir).succeeds(); + let at = &scene.fixtures; + + assert!(at.dir_exists(dir)); +} + #[test] fn test_install_failing_not_dir() { let (at, mut ucmd) = at_and_ucmd!(); - let file1 = "test_install_target_dir_file_a1"; - let file2 = "test_install_target_dir_file_a2"; - let file3 = "test_install_target_dir_file_a3"; + let file1 = "file1"; + let file2 = "file2"; + let file3 = "file3"; at.touch(file1); at.touch(file2); @@ -53,8 +69,8 @@ fn test_install_failing_not_dir() { #[test] fn test_install_unimplemented_arg() { let (at, mut ucmd) = at_and_ucmd!(); - let dir = "test_install_target_dir_dir_b"; - let file = "test_install_target_dir_file_b"; + let dir = "target_dir"; + let file = "source_file"; let context_arg = "--context"; at.touch(file); @@ -71,60 +87,130 @@ fn test_install_unimplemented_arg() { } #[test] -fn test_install_component_directories() { +fn test_install_ancestors_directories() { let (at, mut ucmd) = at_and_ucmd!(); - let component1 = "test_install_target_dir_component_c1"; - let component2 = "test_install_target_dir_component_c2"; - let component3 = "test_install_target_dir_component_c3"; + let ancestor1 = "ancestor1"; + let ancestor2 = "ancestor1/ancestor2"; + let target_dir = "ancestor1/ancestor2/target_dir"; let directories_arg = "-d"; - ucmd.args(&[directories_arg, component1, component2, component3]) + ucmd.args(&[directories_arg, target_dir]) .succeeds() .no_stderr(); - assert!(at.dir_exists(component1)); - assert!(at.dir_exists(component2)); - assert!(at.dir_exists(component3)); + assert!(at.dir_exists(ancestor1)); + assert!(at.dir_exists(ancestor2)); + assert!(at.dir_exists(target_dir)); } #[test] -fn test_install_component_directories_failing() { +fn test_install_ancestors_mode_directories() { let (at, mut ucmd) = at_and_ucmd!(); - let component = "test_install_target_dir_component_d1"; + let ancestor1 = "ancestor1"; + let ancestor2 = "ancestor1/ancestor2"; + let target_dir = "ancestor1/ancestor2/target_dir"; + let directories_arg = "-d"; + let mode_arg = "--mode=700"; + + ucmd.args(&[mode_arg, directories_arg, target_dir]) + .succeeds() + .no_stderr(); + + assert!(at.dir_exists(ancestor1)); + assert!(at.dir_exists(ancestor2)); + assert!(at.dir_exists(target_dir)); + + assert_ne!(0o40700 as u32, at.metadata(ancestor1).permissions().mode()); + assert_ne!(0o40700 as u32, at.metadata(ancestor2).permissions().mode()); + + // Expected mode only on the target_dir. + assert_eq!(0o40700 as u32, at.metadata(target_dir).permissions().mode()); +} + +#[test] +fn test_install_parent_directories() { + let (at, mut ucmd) = at_and_ucmd!(); + let ancestor1 = "ancestor1"; + let ancestor2 = "ancestor1/ancestor2"; + let target_dir = "ancestor1/ancestor2/target_dir"; let directories_arg = "-d"; - at.mkdir(component); - assert!(ucmd - .arg(directories_arg) - .arg(component) - .fails() - .stderr - .contains("File exists")); + // Here one of the ancestors already exist and only the target_dir and + // its parent must be created. + at.mkdir(ancestor1); + + ucmd.args(&[directories_arg, target_dir]) + .succeeds() + .no_stderr(); + + assert!(at.dir_exists(ancestor2)); + assert!(at.dir_exists(target_dir)); +} + +#[test] +fn test_install_several_directories() { + let (at, mut ucmd) = at_and_ucmd!(); + let dir1 = "dir1"; + let dir2 = "dir2"; + let dir3 = "dir3"; + let directories_arg = "-d"; + + ucmd.args(&[directories_arg, dir1, dir2, dir3]) + .succeeds() + .no_stderr(); + + assert!(at.dir_exists(dir1)); + assert!(at.dir_exists(dir2)); + assert!(at.dir_exists(dir3)); } #[test] fn test_install_mode_numeric() { - let (at, mut ucmd) = at_and_ucmd!(); - let dir = "test_install_target_dir_dir_e"; - let file = "test_install_target_dir_file_e"; + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + let dir = "dir1"; + let dir2 = "dir2"; + + let file = "file"; let mode_arg = "--mode=333"; at.touch(file); at.mkdir(dir); - ucmd.arg(file).arg(dir).arg(mode_arg).succeeds().no_stderr(); + scene + .ucmd() + .arg(file) + .arg(dir) + .arg(mode_arg) + .succeeds() + .no_stderr(); let dest_file = &format!("{}/{}", dir, file); assert!(at.file_exists(file)); assert!(at.file_exists(dest_file)); let permissions = at.metadata(dest_file).permissions(); assert_eq!(0o100333 as u32, PermissionsExt::mode(&permissions)); + + let mode_arg = "-m 0333"; + at.mkdir(dir2); + + let result = scene.ucmd().arg(mode_arg).arg(file).arg(dir2).run(); + + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + + assert!(result.success); + let dest_file = &format!("{}/{}", dir2, file); + assert!(at.file_exists(file)); + assert!(at.file_exists(dest_file)); + let permissions = at.metadata(dest_file).permissions(); + assert_eq!(0o100333 as u32, PermissionsExt::mode(&permissions)); } #[test] fn test_install_mode_symbolic() { let (at, mut ucmd) = at_and_ucmd!(); - let dir = "test_install_target_dir_dir_f"; - let file = "test_install_target_dir_file_f"; + let dir = "target_dir"; + let file = "source_file"; let mode_arg = "--mode=o+wx"; at.touch(file); @@ -141,8 +227,8 @@ fn test_install_mode_symbolic() { #[test] fn test_install_mode_failing() { let (at, mut ucmd) = at_and_ucmd!(); - let dir = "test_install_target_dir_dir_g"; - let file = "test_install_target_dir_file_g"; + let dir = "target_dir"; + let file = "source_file"; let mode_arg = "--mode=999"; at.touch(file); @@ -163,7 +249,7 @@ fn test_install_mode_failing() { #[test] fn test_install_mode_directories() { let (at, mut ucmd) = at_and_ucmd!(); - let component = "test_install_target_dir_component_h"; + let component = "component"; let directories_arg = "-d"; let mode_arg = "--mode=333"; @@ -181,8 +267,8 @@ fn test_install_mode_directories() { #[test] fn test_install_target_file() { let (at, mut ucmd) = at_and_ucmd!(); - let file1 = "test_install_target_file_file_i1"; - let file2 = "test_install_target_file_file_i2"; + let file1 = "source_file"; + let file2 = "target_file"; at.touch(file1); at.touch(file2); @@ -195,8 +281,8 @@ fn test_install_target_file() { #[test] fn test_install_target_new_file() { let (at, mut ucmd) = at_and_ucmd!(); - let file = "test_install_target_new_filer_file_j"; - let dir = "test_install_target_new_file_dir_j"; + let file = "file"; + let dir = "target_dir"; at.touch(file); at.mkdir(dir); @@ -209,12 +295,72 @@ fn test_install_target_new_file() { assert!(at.file_exists(&format!("{}/{}", dir, file))); } +#[test] +fn test_install_target_new_file_with_group() { + let (at, mut ucmd) = at_and_ucmd!(); + let file = "file"; + let dir = "target_dir"; + let gid = get_effective_gid(); + + at.touch(file); + at.mkdir(dir); + let result = ucmd + .arg(file) + .arg("--group") + .arg(gid.to_string()) + .arg(format!("{}/{}", dir, file)) + .run(); + + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + + if is_ci() && result.stderr.contains("error: no such group:") { + // In the CI, some server are failing to return the group. + // As seems to be a configuration issue, ignoring it + return; + } + + assert!(result.success); + assert!(at.file_exists(file)); + assert!(at.file_exists(&format!("{}/{}", dir, file))); +} + +#[test] +fn test_install_target_new_file_with_owner() { + let (at, mut ucmd) = at_and_ucmd!(); + let file = "file"; + let dir = "target_dir"; + let uid = get_effective_uid(); + + at.touch(file); + at.mkdir(dir); + let result = ucmd + .arg(file) + .arg("--owner") + .arg(uid.to_string()) + .arg(format!("{}/{}", dir, file)) + .run(); + + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + + if is_ci() && result.stderr.contains("error: no such user:") { + // In the CI, some server are failing to return the user id. + // As seems to be a configuration issue, ignoring it + return; + } + + assert!(result.success); + assert!(at.file_exists(file)); + assert!(at.file_exists(&format!("{}/{}", dir, file))); +} + #[test] fn test_install_target_new_file_failing_nonexistent_parent() { let (at, mut ucmd) = at_and_ucmd!(); - let file1 = "test_install_target_new_file_failing_file_k1"; - let file2 = "test_install_target_new_file_failing_file_k2"; - let dir = "test_install_target_new_file_failing_dir_k"; + let file1 = "source_file"; + let file2 = "target_file"; + let dir = "target_dir"; at.touch(file1); @@ -226,3 +372,197 @@ fn test_install_target_new_file_failing_nonexistent_parent() { assert!(err.contains("not a directory")) } + +#[test] +fn test_install_preserve_timestamps() { + let (at, mut ucmd) = at_and_ucmd!(); + let file1 = "source_file"; + let file2 = "target_file"; + at.touch(file1); + + ucmd.arg(file1).arg(file2).arg("-p").succeeds().no_stderr(); + + assert!(at.file_exists(file1)); + assert!(at.file_exists(file2)); + + let file1_metadata = at.metadata(file1); + let file2_metadata = at.metadata(file2); + + assert_eq!( + file1_metadata.accessed().ok(), + file2_metadata.accessed().ok() + ); + assert_eq!( + file1_metadata.modified().ok(), + file2_metadata.modified().ok() + ); +} + +// These two tests are failing but should work +#[test] +fn test_install_copy_file() { + let (at, mut ucmd) = at_and_ucmd!(); + let file1 = "source_file"; + let file2 = "target_file"; + + at.touch(file1); + ucmd.arg(file1).arg(file2).succeeds().no_stderr(); + + assert!(at.file_exists(file1)); + assert!(at.file_exists(file2)); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_install_target_file_dev_null() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let file1 = "/dev/null"; + let file2 = "target_file"; + + let result = scene.ucmd().arg(file1).arg(file2).run(); + + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + + assert!(result.success); + + assert!(at.file_exists(file2)); +} + +#[test] +fn test_install_nested_paths_copy_file() { + let (at, mut ucmd) = at_and_ucmd!(); + let file1 = "source_file"; + let dir1 = "source_dir"; + let dir2 = "target_dir"; + + at.mkdir(dir1); + at.mkdir(dir2); + at.touch(&format!("{}/{}", dir1, file1)); + + ucmd.arg(format!("{}/{}", dir1, file1)) + .arg(dir2) + .succeeds() + .no_stderr(); + assert!(at.file_exists(&format!("{}/{}", dir2, file1))); +} + +#[test] +fn test_install_failing_omitting_directory() { + let (at, mut ucmd) = at_and_ucmd!(); + let file1 = "source_file"; + let dir1 = "source_dir"; + let dir2 = "target_dir"; + + at.mkdir(dir1); + at.mkdir(dir2); + at.touch(file1); + + let r = ucmd.arg(dir1).arg(file1).arg(dir2).run(); + assert!(r.code == Some(1)); + assert!(r.stderr.contains("omitting directory")); +} + +#[test] +fn test_install_failing_no_such_file() { + let (at, mut ucmd) = at_and_ucmd!(); + let file1 = "source_file"; + let file2 = "inexistent_file"; + let dir1 = "target_dir"; + + at.mkdir(dir1); + at.touch(file1); + + let r = ucmd.arg(file1).arg(file2).arg(dir1).run(); + assert!(r.code == Some(1)); + assert!(r.stderr.contains("No such file or directory")); +} + +#[test] +fn test_install_copy_then_compare_file() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + let file1 = "test_install_copy_then_compare_file_a1"; + let file2 = "test_install_copy_then_compare_file_a2"; + + at.touch(file1); + scene + .ucmd() + .arg("-C") + .arg(file1) + .arg(file2) + .succeeds() + .no_stderr(); + + let mut file2_meta = at.metadata(file2); + let before = FileTime::from_last_modification_time(&file2_meta); + + scene + .ucmd() + .arg("-C") + .arg(file1) + .arg(file2) + .succeeds() + .no_stderr(); + + file2_meta = at.metadata(file2); + let after = FileTime::from_last_modification_time(&file2_meta); + + assert!(before == after); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_install_copy_then_compare_file_with_extra_mode() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + // XXX: can't tests introspect on their own names? + let file1 = "test_install_copy_then_compare_file_with_extra_mode_a1"; + let file2 = "test_install_copy_then_compare_file_with_extra_mode_a2"; + + at.touch(file1); + scene + .ucmd() + .arg("-C") + .arg(file1) + .arg(file2) + .succeeds() + .no_stderr(); + + let mut file2_meta = at.metadata(file2); + let before = FileTime::from_last_modification_time(&file2_meta); + sleep(std::time::Duration::from_millis(1000)); + + scene + .ucmd() + .arg("-C") + .arg(file1) + .arg(file2) + .arg("-m") + .arg("1644") + .succeeds() + .no_stderr(); + + file2_meta = at.metadata(file2); + let after_install_sticky = FileTime::from_last_modification_time(&file2_meta); + + assert!(before != after_install_sticky); + + sleep(std::time::Duration::from_millis(1000)); + + // dest file still 1644, so need_copy ought to return `true` + scene + .ucmd() + .arg("-C") + .arg(file1) + .arg(file2) + .succeeds() + .no_stderr(); + + file2_meta = at.metadata(file2); + let after_install_sticky_again = FileTime::from_last_modification_time(&file2_meta); + + assert!(after_install_sticky != after_install_sticky_again); +} diff --git a/tests/by-util/test_ln.rs b/tests/by-util/test_ln.rs index a84927c4c..89261036d 100644 --- a/tests/by-util/test_ln.rs +++ b/tests/by-util/test_ln.rs @@ -321,12 +321,7 @@ fn test_symlink_errors() { // $ ln -T -t a b // ln: cannot combine --target-directory (-t) and --no-target-directory (-T) - ucmd.args(&["-T", "-t", dir, file_a, file_b]) - .fails() - .stderr_is( - "ln: error: cannot combine --target-directory (-t) and --no-target-directory \ - (-T)\n", - ); + ucmd.args(&["-T", "-t", dir, file_a, file_b]).fails(); } #[test] @@ -488,3 +483,95 @@ fn test_symlink_relative_dir() { assert!(at.is_symlink(link)); assert_eq!(at.resolve_link(link), dir); } + +#[test] +fn test_symlink_no_deref_dir() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let dir1 = "foo"; + let dir2 = "bar"; + let link = "baz"; + + at.mkdir(dir1); + at.mkdir(dir2); + scene + .ucmd() + .args(&["-s", dir2, link]) + .succeeds() + .no_stderr(); + assert!(at.dir_exists(dir1)); + assert!(at.dir_exists(dir2)); + assert!(at.is_symlink(link)); + assert_eq!(at.resolve_link(link), dir2); + + // try the normal behavior + scene + .ucmd() + .args(&["-sf", dir1, link]) + .succeeds() + .no_stderr(); + assert!(at.dir_exists(dir1)); + assert!(at.dir_exists(dir2)); + assert!(at.is_symlink("baz/foo")); + assert_eq!(at.resolve_link("baz/foo"), dir1); + + // Doesn't work without the force + scene.ucmd().args(&["-sn", dir1, link]).fails(); + + // Try with the no-deref + let result = scene.ucmd().args(&["-sfn", dir1, link]).run(); + println!("stdout {}", result.stdout); + println!("stderr {}", result.stderr); + assert!(result.success); + assert!(at.dir_exists(dir1)); + assert!(at.dir_exists(dir2)); + assert!(at.is_symlink(link)); + assert_eq!(at.resolve_link(link), dir1); +} + +#[test] +fn test_symlink_no_deref_file() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let file1 = "foo"; + let file2 = "bar"; + let link = "baz"; + + at.touch(file1); + at.touch(file2); + scene + .ucmd() + .args(&["-s", file2, link]) + .succeeds() + .no_stderr(); + assert!(at.file_exists(file1)); + assert!(at.file_exists(file2)); + assert!(at.is_symlink(link)); + assert_eq!(at.resolve_link(link), file2); + + // try the normal behavior + scene + .ucmd() + .args(&["-sf", file1, link]) + .succeeds() + .no_stderr(); + assert!(at.file_exists(file1)); + assert!(at.file_exists(file2)); + assert!(at.is_symlink("baz")); + assert_eq!(at.resolve_link("baz"), file1); + + // Doesn't work without the force + scene.ucmd().args(&["-sn", file1, link]).fails(); + + // Try with the no-deref + let result = scene.ucmd().args(&["-sfn", file1, link]).run(); + println!("stdout {}", result.stdout); + println!("stderr {}", result.stderr); + assert!(result.success); + assert!(at.file_exists(file1)); + assert!(at.file_exists(file2)); + assert!(at.is_symlink(link)); + assert_eq!(at.resolve_link(link), file1); +} diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index 18bd66d2e..64f8f57bf 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -1,19 +1,1390 @@ +#[cfg(unix)] +extern crate unix_socket; use crate::common::util::*; +extern crate regex; +use self::regex::Regex; + +use std::thread::sleep; +use std::time::Duration; + +#[cfg(not(windows))] +extern crate libc; +#[cfg(not(windows))] +use self::libc::umask; +#[cfg(not(windows))] +use std::path::PathBuf; +#[cfg(not(windows))] +use std::sync::Mutex; +#[cfg(not(windows))] +extern crate tempdir; +#[cfg(not(windows))] +use self::tempdir::TempDir; + +#[cfg(not(windows))] +lazy_static! { + static ref UMASK_MUTEX: Mutex<()> = Mutex::new(()); +} + #[test] fn test_ls_ls() { new_ucmd!().succeeds(); } #[test] -fn test_ls_ls_i() { +fn test_ls_i() { new_ucmd!().arg("-i").succeeds(); new_ucmd!().arg("-il").succeeds(); } #[test] -fn test_ls_ls_color() { - new_ucmd!().arg("--color").succeeds(); - new_ucmd!().arg("--color=always").succeeds(); - new_ucmd!().arg("--color=never").succeeds(); +fn test_ls_a() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch(".test-1"); + + let result = scene.ucmd().run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(result.success); + assert!(!result.stdout.contains(".test-1")); + assert!(!result.stdout.contains("..")); + + let result = scene.ucmd().arg("-a").run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(result.success); + assert!(result.stdout.contains(".test-1")); + assert!(result.stdout.contains("..")); + + let result = scene.ucmd().arg("-A").run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(result.success); + assert!(result.stdout.contains(".test-1")); + assert!(!result.stdout.contains("..")); +} + +#[test] +fn test_ls_width() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch(&at.plus_as_string("test-width-1")); + at.touch(&at.plus_as_string("test-width-2")); + at.touch(&at.plus_as_string("test-width-3")); + at.touch(&at.plus_as_string("test-width-4")); + + for option in &["-w 100", "-w=100", "--width=100", "--width 100"] { + let result = scene + .ucmd() + .args(&option.split(" ").collect::>()) + .run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert_eq!( + result.stdout, + "test-width-1 test-width-2 test-width-3 test-width-4\n", + ) + } + + for option in &["-w 50", "-w=50", "--width=50", "--width 50"] { + let result = scene + .ucmd() + .args(&option.split(" ").collect::>()) + .run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert_eq!( + result.stdout, + "test-width-1 test-width-3\ntest-width-2 test-width-4\n", + ) + } + + for option in &[ + "-w 25", + "-w=25", + "--width=25", + "--width 25", + "-w 0", + "-w=0", + "--width=0", + "--width 0", + ] { + let result = scene + .ucmd() + .args(&option.split(" ").collect::>()) + .run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert_eq!( + result.stdout, + "test-width-1\ntest-width-2\ntest-width-3\ntest-width-4\n", + ) + } +} + +#[test] +fn test_ls_columns() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch(&at.plus_as_string("test-columns-1")); + at.touch(&at.plus_as_string("test-columns-2")); + at.touch(&at.plus_as_string("test-columns-3")); + at.touch(&at.plus_as_string("test-columns-4")); + + // Columns is the default + let result = scene.ucmd().run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(result.success); + + #[cfg(not(windows))] + assert_eq!( + result.stdout, + "test-columns-1\ntest-columns-2\ntest-columns-3\ntest-columns-4\n" + ); + #[cfg(windows)] + assert_eq!( + result.stdout, + "test-columns-1 test-columns-2 test-columns-3 test-columns-4\n" + ); + + for option in &["-C", "--format=columns"] { + let result = scene.ucmd().arg(option).run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(result.success); + #[cfg(not(windows))] + assert_eq!( + result.stdout, + "test-columns-1\ntest-columns-2\ntest-columns-3\ntest-columns-4\n" + ); + #[cfg(windows)] + assert_eq!( + result.stdout, + "test-columns-1 test-columns-2 test-columns-3 test-columns-4\n" + ); + } + + for option in &["-C", "--format=columns"] { + let result = scene.ucmd().arg("-w=40").arg(option).run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(result.success); + assert_eq!( + result.stdout, + "test-columns-1 test-columns-3\ntest-columns-2 test-columns-4\n" + ); + } +} + +#[test] +fn test_ls_across() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch(&at.plus_as_string("test-across-1")); + at.touch(&at.plus_as_string("test-across-2")); + at.touch(&at.plus_as_string("test-across-3")); + at.touch(&at.plus_as_string("test-across-4")); + + for option in &["-x", "--format=across"] { + let result = scene.ucmd().arg(option).succeeds(); + // Because the test terminal has width 0, this is the same output as + // the columns option. + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + if cfg!(unix) { + assert_eq!( + result.stdout, + "test-across-1\ntest-across-2\ntest-across-3\ntest-across-4\n" + ); + } else { + assert_eq!( + result.stdout, + "test-across-1 test-across-2 test-across-3 test-across-4\n" + ); + } + } + + for option in &["-x", "--format=across"] { + let result = scene.ucmd().arg("-w=30").arg(option).run(); + // Because the test terminal has width 0, this is the same output as + // the columns option. + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert_eq!( + result.stdout, + "test-across-1 test-across-2\ntest-across-3 test-across-4\n" + ); + } +} + +#[test] +fn test_ls_commas() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch(&at.plus_as_string("test-commas-1")); + at.touch(&at.plus_as_string("test-commas-2")); + at.touch(&at.plus_as_string("test-commas-3")); + at.touch(&at.plus_as_string("test-commas-4")); + + for option in &["-m", "--format=commas"] { + let result = scene.ucmd().arg(option).succeeds(); + if cfg!(unix) { + assert_eq!( + result.stdout, + "test-commas-1,\ntest-commas-2,\ntest-commas-3,\ntest-commas-4\n" + ); + } else { + assert_eq!( + result.stdout, + "test-commas-1, test-commas-2, test-commas-3, test-commas-4\n" + ); + } + } + + for option in &["-m", "--format=commas"] { + let result = scene.ucmd().arg("-w=30").arg(option).succeeds(); + assert_eq!( + result.stdout, + "test-commas-1, test-commas-2,\ntest-commas-3, test-commas-4\n" + ); + } + for option in &["-m", "--format=commas"] { + let result = scene.ucmd().arg("-w=45").arg(option).succeeds(); + assert_eq!( + result.stdout, + "test-commas-1, test-commas-2, test-commas-3,\ntest-commas-4\n" + ); + } +} + +#[test] +fn test_ls_long() { + #[cfg(not(windows))] + let last; + #[cfg(not(windows))] + { + let _guard = UMASK_MUTEX.lock(); + last = unsafe { umask(0) }; + + unsafe { + umask(0o002); + } + } + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch(&at.plus_as_string("test-long")); + + for arg in &["-l", "--long", "--format=long", "--format=verbose"] { + let result = scene.ucmd().arg(arg).arg("test-long").succeeds(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + #[cfg(not(windows))] + assert!(result.stdout.contains("-rw-rw-r--")); + + #[cfg(windows)] + assert!(result.stdout.contains("---------- 1 somebody somegroup")); + } + + #[cfg(not(windows))] + { + unsafe { + umask(last); + } + } +} + +#[test] +fn test_ls_long_formats() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch(&at.plus_as_string("test-long-formats")); + + // Regex for three names, so all of author, group and owner + let re_three = Regex::new(r"[xrw-]{9} \d ([-0-9_a-z]+ ){3}0").unwrap(); + + #[cfg(unix)] + let re_three_num = Regex::new(r"[xrw-]{9} \d (\d+ ){3}0").unwrap(); + + // Regex for two names, either: + // - group and owner + // - author and owner + // - author and group + let re_two = Regex::new(r"[xrw-]{9} \d ([-0-9_a-z]+ ){2}0").unwrap(); + + #[cfg(unix)] + let re_two_num = Regex::new(r"[xrw-]{9} \d (\d+ ){2}0").unwrap(); + + // Regex for one name: author, group or owner + let re_one = Regex::new(r"[xrw-]{9} \d [-0-9_a-z]+ 0").unwrap(); + + #[cfg(unix)] + let re_one_num = Regex::new(r"[xrw-]{9} \d \d+ 0").unwrap(); + + // Regex for no names + let re_zero = Regex::new(r"[xrw-]{9} \d 0").unwrap(); + + let result = scene + .ucmd() + .arg("-l") + .arg("--author") + .arg("test-long-formats") + .run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(re_three.is_match(&result.stdout)); + + let result = scene + .ucmd() + .arg("-l1") + .arg("--author") + .arg("test-long-formats") + .run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(re_three.is_match(&result.stdout)); + + #[cfg(unix)] + { + let result = scene + .ucmd() + .arg("-n") + .arg("--author") + .arg("test-long-formats") + .succeeds(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(re_three_num.is_match(&result.stdout)); + } + + for arg in &[ + "-l", // only group and owner + "-g --author", // only author and group + "-o --author", // only author and owner + "-lG --author", // only author and owner + "-l --no-group --author", // only author and owner + ] { + let result = scene + .ucmd() + .args(&arg.split(" ").collect::>()) + .arg("test-long-formats") + .succeeds(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(re_two.is_match(&result.stdout)); + + #[cfg(unix)] + { + let result = scene + .ucmd() + .arg("-n") + .args(&arg.split(" ").collect::>()) + .arg("test-long-formats") + .succeeds(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(re_two_num.is_match(&result.stdout)); + } + } + + for arg in &[ + "-g", // only group + "-gl", // only group + "-o", // only owner + "-ol", // only owner + "-oG", // only owner + "-lG", // only owner + "-l --no-group", // only owner + "-gG --author", // only author + ] { + let result = scene + .ucmd() + .args(&arg.split(" ").collect::>()) + .arg("test-long-formats") + .succeeds(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(re_one.is_match(&result.stdout)); + + #[cfg(unix)] + { + let result = scene + .ucmd() + .arg("-n") + .args(&arg.split(" ").collect::>()) + .arg("test-long-formats") + .succeeds(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(re_one_num.is_match(&result.stdout)); + } + } + + for arg in &[ + "-og", + "-ogl", + "-lgo", + "-gG", + "-g --no-group", + "-og --no-group", + "-og --format=long", + "-ogCl", + "-og --format=vertical -l", + "-og1", + "-og1l", + ] { + let result = scene + .ucmd() + .args(&arg.split(" ").collect::>()) + .arg("test-long-formats") + .succeeds(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(re_zero.is_match(&result.stdout)); + + #[cfg(unix)] + { + let result = scene + .ucmd() + .arg("-n") + .args(&arg.split(" ").collect::>()) + .arg("test-long-formats") + .succeeds(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(re_zero.is_match(&result.stdout)); + } + } +} + +#[test] +fn test_ls_oneline() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch(&at.plus_as_string("test-oneline-1")); + at.touch(&at.plus_as_string("test-oneline-2")); + + // Bit of a weird situation: in the tests oneline and columns have the same output, + // except on Windows. + for option in &["-1", "--format=single-column"] { + let result = scene.ucmd().arg(option).run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(result.success); + assert_eq!(result.stdout, "test-oneline-1\ntest-oneline-2\n"); + } +} + +#[test] +fn test_ls_deref() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + let path_regexp = r"(.*)test-long.link -> (.*)test-long(.*)"; + let re = Regex::new(path_regexp).unwrap(); + + at.touch(&at.plus_as_string("test-long")); + at.symlink_file("test-long", "test-long.link"); + assert!(at.is_symlink("test-long.link")); + + let result = scene + .ucmd() + .arg("-l") + .arg("--color=never") + .arg("test-long") + .arg("test-long.link") + .run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(result.success); + assert!(re.is_match(&result.stdout.trim())); + + let result = scene + .ucmd() + .arg("-L") + .arg("--color=never") + .arg("test-long") + .arg("test-long.link") + .run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(result.success); + assert!(!re.is_match(&result.stdout.trim())); +} + +#[test] +fn test_ls_order_size() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("test-1"); + at.append("test-1", "1"); + + at.touch("test-2"); + at.append("test-2", "22"); + at.touch("test-3"); + at.append("test-3", "333"); + at.touch("test-4"); + at.append("test-4", "4444"); + + let result = scene.ucmd().arg("-al").run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(result.success); + + let result = scene.ucmd().arg("-S").run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(result.success); + #[cfg(not(windows))] + assert_eq!(result.stdout, "test-4\ntest-3\ntest-2\ntest-1\n"); + #[cfg(windows)] + assert_eq!(result.stdout, "test-4 test-3 test-2 test-1\n"); + + let result = scene.ucmd().arg("-S").arg("-r").run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(result.success); + #[cfg(not(windows))] + assert_eq!(result.stdout, "test-1\ntest-2\ntest-3\ntest-4\n"); + #[cfg(windows)] + assert_eq!(result.stdout, "test-1 test-2 test-3 test-4\n"); +} + +#[test] +fn test_ls_long_ctime() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("test-long-ctime-1"); + let result = scene.ucmd().arg("-lc").succeeds(); + + // Should show the time on Unix, but question marks on windows. + #[cfg(unix)] + assert!(result.stdout.contains(":")); + #[cfg(not(unix))] + assert!(result.stdout.contains("???")); +} + +#[test] +fn test_ls_order_time() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("test-1"); + at.append("test-1", "1"); + sleep(Duration::from_millis(100)); + at.touch("test-2"); + at.append("test-2", "22"); + + sleep(Duration::from_millis(100)); + at.touch("test-3"); + at.append("test-3", "333"); + sleep(Duration::from_millis(100)); + at.touch("test-4"); + at.append("test-4", "4444"); + sleep(Duration::from_millis(100)); + + // Read test-3, only changing access time + at.read("test-3"); + + // Set permissions of test-2, only changing ctime + std::fs::set_permissions( + at.plus_as_string("test-2"), + at.metadata("test-2").permissions(), + ) + .unwrap(); + + let result = scene.ucmd().arg("-al").run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(result.success); + + // ctime was changed at write, so the order is 4 3 2 1 + let result = scene.ucmd().arg("-t").run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(result.success); + #[cfg(not(windows))] + assert_eq!(result.stdout, "test-4\ntest-3\ntest-2\ntest-1\n"); + #[cfg(windows)] + assert_eq!(result.stdout, "test-4 test-3 test-2 test-1\n"); + + let result = scene.ucmd().arg("-tr").run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(result.success); + #[cfg(not(windows))] + assert_eq!(result.stdout, "test-1\ntest-2\ntest-3\ntest-4\n"); + #[cfg(windows)] + assert_eq!(result.stdout, "test-1 test-2 test-3 test-4\n"); + + // 3 was accessed last in the read + // So the order should be 2 3 4 1 + let result = scene.ucmd().arg("-tu").run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(result.success); + let file3_access = at.open("test-3").metadata().unwrap().accessed().unwrap(); + let file4_access = at.open("test-4").metadata().unwrap().accessed().unwrap(); + if file3_access > file4_access { + if cfg!(not(windows)) { + assert_eq!(result.stdout, "test-3\ntest-4\ntest-2\ntest-1\n"); + } else { + assert_eq!(result.stdout, "test-3 test-4 test-2 test-1\n"); + } + } else { + // Access time does not seem to be set on Windows and some other + // systems so the order is 4 3 2 1 + if cfg!(not(windows)) { + assert_eq!(result.stdout, "test-4\ntest-3\ntest-2\ntest-1\n"); + } else { + assert_eq!(result.stdout, "test-4 test-3 test-2 test-1\n"); + } + } + + // test-2 had the last ctime change when the permissions were set + // So the order should be 2 4 3 1 + #[cfg(unix)] + { + let result = scene.ucmd().arg("-tc").run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(result.success); + assert_eq!(result.stdout, "test-2\ntest-4\ntest-3\ntest-1\n"); + } +} + +#[test] +fn test_ls_non_existing() { + new_ucmd!().arg("doesntexist").fails(); +} + +#[test] +fn test_ls_files_dirs() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.mkdir("a"); + at.mkdir("a/b"); + at.mkdir("a/b/c"); + at.mkdir("z"); + at.touch(&at.plus_as_string("a/a")); + at.touch(&at.plus_as_string("a/b/b")); + + scene.ucmd().arg("a").succeeds(); + scene.ucmd().arg("a/a").succeeds(); + scene.ucmd().arg("a").arg("z").succeeds(); + + let result = scene.ucmd().arg("doesntexist").fails(); + // Doesn't exist + assert!(result + .stderr + .contains("error: 'doesntexist': No such file or directory")); + + let result = scene.ucmd().arg("a").arg("doesntexist").fails(); + // One exists, the other doesn't + assert!(result + .stderr + .contains("error: 'doesntexist': No such file or directory")); + assert!(result.stdout.contains("a:")); +} + +#[test] +fn test_ls_recursive() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.mkdir("a"); + at.mkdir("a/b"); + at.mkdir("a/b/c"); + at.mkdir("z"); + at.touch(&at.plus_as_string("a/a")); + at.touch(&at.plus_as_string("a/b/b")); + + scene.ucmd().arg("a").succeeds(); + scene.ucmd().arg("a/a").succeeds(); + let result = scene + .ucmd() + .arg("--color=never") + .arg("-R") + .arg("a") + .arg("z") + .succeeds(); + + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(result.success); + #[cfg(not(windows))] + assert!(result.stdout.contains("a/b:\nb")); + #[cfg(windows)] + assert!(result.stdout.contains("a\\b:\nb")); +} + +#[cfg(unix)] +#[test] +fn test_ls_ls_color() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.mkdir("a"); + at.mkdir("a/nested_dir"); + at.mkdir("z"); + at.touch(&at.plus_as_string("a/nested_file")); + at.touch("test-color"); + + let a_with_colors = "\x1b[01;34ma\x1b[0m"; + let z_with_colors = "\x1b[01;34mz\x1b[0m"; + let nested_dir_with_colors = "\x1b[01;34mnested_dir\x1b[0m"; + + // Color is disabled by default + let result = scene.ucmd().succeeds(); + assert!(!result.stdout.contains(a_with_colors)); + assert!(!result.stdout.contains(z_with_colors)); + + // Color should be enabled + let result = scene.ucmd().arg("--color").succeeds(); + assert!(result.stdout.contains(a_with_colors)); + assert!(result.stdout.contains(z_with_colors)); + + // Color should be enabled + let result = scene.ucmd().arg("--color=always").succeeds(); + assert!(result.stdout.contains(a_with_colors)); + assert!(result.stdout.contains(z_with_colors)); + + // Color should be disabled + let result = scene.ucmd().arg("--color=never").succeeds(); + assert!(!result.stdout.contains(a_with_colors)); + assert!(!result.stdout.contains(z_with_colors)); + + // Nested dir should be shown and colored + let result = scene.ucmd().arg("--color").arg("a").succeeds(); + assert!(result.stdout.contains(nested_dir_with_colors)); + + // Color has no effect + let result = scene + .ucmd() + .arg("--color=always") + .arg("a/nested_file") + .succeeds(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(result.stdout.contains("a/nested_file\n")); + + // No output + let result = scene.ucmd().arg("--color=never").arg("z").succeeds(); + assert_eq!(result.stdout, ""); +} + +#[cfg(unix)] +#[test] +fn test_ls_inode() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let file = "test_inode"; + at.touch(file); + + let re_short = Regex::new(r" *(\d+) test_inode").unwrap(); + let re_long = Regex::new(r" *(\d+) [xrw-]{10} \d .+ test_inode").unwrap(); + + let result = scene.ucmd().arg("test_inode").arg("-i").run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(re_short.is_match(&result.stdout)); + let inode_short = re_short + .captures(&result.stdout) + .unwrap() + .get(1) + .unwrap() + .as_str(); + + let result = scene.ucmd().arg("test_inode").run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(!re_short.is_match(&result.stdout)); + assert!(!result.stdout.contains(inode_short)); + + let result = scene.ucmd().arg("-li").arg("test_inode").run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(re_long.is_match(&result.stdout)); + let inode_long = re_long + .captures(&result.stdout) + .unwrap() + .get(1) + .unwrap() + .as_str(); + + let result = scene.ucmd().arg("-l").arg("test_inode").run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(!re_long.is_match(&result.stdout)); + assert!(!result.stdout.contains(inode_long)); + + assert_eq!(inode_short, inode_long) +} + +#[test] +#[cfg(not(windows))] +fn test_ls_indicator_style() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + // Setup: Directory, Symlink, and Pipes. + at.mkdir("directory"); + assert!(at.dir_exists("directory")); + + at.touch(&at.plus_as_string("link-src")); + at.symlink_file("link-src", "link-dest.link"); + assert!(at.is_symlink("link-dest.link")); + + at.mkfifo("named-pipe.fifo"); + assert!(at.is_fifo("named-pipe.fifo")); + + // Classify, File-Type, and Slash all contain indicators for directories. + let options = vec!["classify", "file-type", "slash"]; + for opt in options { + // Verify that classify and file-type both contain indicators for symlinks. + let result = scene.ucmd().arg(format!("--indicator-style={}", opt)).run(); + println!("stdout = {:?}", result.stdout); + assert!(result.stdout.contains("/")); + } + + // Same test as above, but with the alternate flags. + let options = vec!["--classify", "--file-type", "-p"]; + for opt in options { + let result = scene.ucmd().arg(format!("{}", opt)).run(); + println!("stdout = {:?}", result.stdout); + assert!(result.stdout.contains("/")); + } + + // Classify and File-Type all contain indicators for pipes and links. + let options = vec!["classify", "file-type"]; + for opt in options { + // Verify that classify and file-type both contain indicators for symlinks. + let result = scene.ucmd().arg(format!("--indicator-style={}", opt)).run(); + println!("stdout = {}", result.stdout); + assert!(result.stdout.contains("@")); + assert!(result.stdout.contains("|")); + } + + // Test sockets. Because the canonical way of making sockets to test is with + // TempDir, we need a separate test. + { + use self::unix_socket::UnixListener; + + let dir = TempDir::new("unix_socket").expect("failed to create dir"); + let socket_path = dir.path().join("sock"); + let _listener = UnixListener::bind(&socket_path).expect("failed to create socket"); + + new_ucmd!() + .args(&[ + PathBuf::from(dir.path().to_str().unwrap()), + PathBuf::from("--indicator-style=classify"), + ]) + .succeeds() + .stdout_only("sock=\n"); + } +} + +// Essentially the same test as above, but only test symlinks and directories, +// not pipes or sockets. +#[test] +#[cfg(not(unix))] +fn test_ls_indicator_style() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + // Setup: Directory, Symlink. + at.mkdir("directory"); + assert!(at.dir_exists("directory")); + + at.touch(&at.plus_as_string("link-src")); + at.symlink_file("link-src", "link-dest.link"); + assert!(at.is_symlink("link-dest.link")); + + // Classify, File-Type, and Slash all contain indicators for directories. + let options = vec!["classify", "file-type", "slash"]; + for opt in options { + // Verify that classify and file-type both contain indicators for symlinks. + let result = scene.ucmd().arg(format!("--indicator-style={}", opt)).run(); + println!("stdout = {:?}", result.stdout); + assert!(result.stdout.contains("/")); + } + + // Same test as above, but with the alternate flags. + let options = vec!["--classify", "--file-type", "-p"]; + for opt in options { + let result = scene.ucmd().arg(format!("{}", opt)).run(); + println!("stdout = {:?}", result.stdout); + assert!(result.stdout.contains("/")); + } + + // Classify and File-Type all contain indicators for pipes and links. + let options = vec!["classify", "file-type"]; + for opt in options { + // Verify that classify and file-type both contain indicators for symlinks. + let result = scene.ucmd().arg(format!("--indicator-style={}", opt)).run(); + println!("stdout = {}", result.stdout); + assert!(result.stdout.contains("@")); + } +} + +#[cfg(not(any(target_vendor = "apple", target_os = "windows")))] // Truncate not available on mac or win +#[test] +fn test_ls_human_si() { + let scene = TestScenario::new(util_name!()); + let file1 = "test_human-1"; + let result = scene + .cmd("truncate") + .arg("-s") + .arg("+1000") + .arg(file1) + .run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + + let result = scene.ucmd().arg("-hl").arg(file1).run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(result.success); + assert!(result.stdout.contains(" 1000 ")); + + let result = scene.ucmd().arg("-l").arg("--si").arg(file1).run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(result.success); + assert!(result.stdout.contains(" 1.0k ")); + + scene + .cmd("truncate") + .arg("-s") + .arg("+1000k") + .arg(file1) + .run(); + + let result = scene.ucmd().arg("-hl").arg(file1).run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(result.success); + assert!(result.stdout.contains(" 1001K ")); + + let result = scene.ucmd().arg("-l").arg("--si").arg(file1).run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(result.success); + assert!(result.stdout.contains(" 1.1M ")); + + let file2 = "test-human-2"; + let result = scene + .cmd("truncate") + .arg("-s") + .arg("+12300k") + .arg(file2) + .run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(result.success); + let result = scene.ucmd().arg("-hl").arg(file2).run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(result.success); + // GNU rounds up, so we must too. + assert!(result.stdout.contains(" 13M ")); + + let result = scene.ucmd().arg("-l").arg("--si").arg(file2).run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + // GNU rounds up, so we must too. + assert!(result.stdout.contains(" 13M ")); + + let file3 = "test-human-3"; + let result = scene + .cmd("truncate") + .arg("-s") + .arg("+9999") + .arg(file3) + .run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(result.success); + + let result = scene.ucmd().arg("-hl").arg(file3).run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(result.success); + assert!(result.stdout.contains(" 9.8K ")); + + let result = scene.ucmd().arg("-l").arg("--si").arg(file3).run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(result.success); + assert!(result.stdout.contains(" 10k ")); +} + +#[cfg(windows)] +#[test] +fn test_ls_hidden_windows() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + let file = "hiddenWindowsFileNoDot"; + at.touch(file); + // hide the file + scene + .cmd("attrib") + .arg("+h") + .arg("+S") + .arg("+r") + .arg(file) + .run(); + let result = scene.ucmd().run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(result.success); + let result = scene.ucmd().arg("-a").run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(result.success); + assert!(result.stdout.contains(file)); +} + +#[test] +fn test_ls_version_sort() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + for filename in &[ + "a2", + "b1", + "b20", + "a1.4", + "a1.40", + "b3", + "b11", + "b20b", + "b20a", + "a100", + "a1.13", + "aa", + "a1", + "aaa", + "a1.00000040", + "abab", + "ab", + "a01.40", + "a001.001", + "a01.0000001", + "a01.001", + "a001.01", + ] { + at.touch(filename); + } + + let mut expected = vec![ + "a1", + "a001.001", + "a001.01", + "a01.0000001", + "a01.001", + "a1.4", + "a1.13", + "a01.40", + "a1.00000040", + "a1.40", + "a2", + "a100", + "aa", + "aaa", + "ab", + "abab", + "b1", + "b3", + "b11", + "b20", + "b20a", + "b20b", + "", // because of '\n' at the end of the output + ]; + + let result = scene.ucmd().arg("-1v").run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + + assert_eq!(result.stdout.split('\n').collect::>(), expected); + + let result = scene.ucmd().arg("-1").arg("--sort=version").run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + + assert_eq!(result.stdout.split('\n').collect::>(), expected); + + let result = scene.ucmd().arg("-a1v").run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + + expected.insert(0, ".."); + expected.insert(0, "."); + assert_eq!(result.stdout.split('\n').collect::>(), expected,) +} + +#[test] +fn test_ls_quoting_style() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("one two"); + at.touch("one"); + + // It seems that windows doesn't allow \n in filenames. + #[cfg(unix)] + { + at.touch("one\ntwo"); + // Default is shell-escape + let result = scene.ucmd().arg("one\ntwo").succeeds(); + assert_eq!(result.stdout, "'one'$'\\n''two'\n"); + + for (arg, correct) in &[ + ("--quoting-style=literal", "one?two"), + ("-N", "one?two"), + ("--literal", "one?two"), + ("--quoting-style=c", "\"one\\ntwo\""), + ("-Q", "\"one\\ntwo\""), + ("--quote-name", "\"one\\ntwo\""), + ("--quoting-style=escape", "one\\ntwo"), + ("-b", "one\\ntwo"), + ("--escape", "one\\ntwo"), + ("--quoting-style=shell-escape", "'one'$'\\n''two'"), + ("--quoting-style=shell-escape-always", "'one'$'\\n''two'"), + ("--quoting-style=shell", "one?two"), + ("--quoting-style=shell-always", "'one?two'"), + ] { + let result = scene.ucmd().arg(arg).arg("one\ntwo").run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert_eq!(result.stdout, 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'"), + ] { + let result = scene + .ucmd() + .arg(arg) + .arg("--hide-control-chars") + .arg("one\ntwo") + .run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert_eq!(result.stdout, format!("{}\n", correct)); + } + + for (arg, correct) in &[ + ("--quoting-style=literal", "one\ntwo"), + ("-N", "one\ntwo"), + ("--literal", "one\ntwo"), + ("--quoting-style=shell", "one\ntwo"), + ("--quoting-style=shell-always", "'one\ntwo'"), + ] { + let result = scene + .ucmd() + .arg(arg) + .arg("--show-control-chars") + .arg("one\ntwo") + .run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert_eq!(result.stdout, format!("{}\n", correct)); + } + } + + let result = scene.ucmd().arg("one two").succeeds(); + assert_eq!(result.stdout, "'one two'\n"); + + for (arg, correct) in &[ + ("--quoting-style=literal", "one two"), + ("-N", "one two"), + ("--literal", "one two"), + ("--quoting-style=c", "\"one two\""), + ("-Q", "\"one two\""), + ("--quote-name", "\"one two\""), + ("--quoting-style=escape", "one\\ two"), + ("-b", "one\\ two"), + ("--escape", "one\\ two"), + ("--quoting-style=shell-escape", "'one two'"), + ("--quoting-style=shell-escape-always", "'one two'"), + ("--quoting-style=shell", "'one two'"), + ("--quoting-style=shell-always", "'one two'"), + ] { + let result = scene.ucmd().arg(arg).arg("one two").run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert_eq!(result.stdout, format!("{}\n", correct)); + } + + let result = scene.ucmd().arg("one").succeeds(); + assert_eq!(result.stdout, "one\n"); + + for (arg, correct) in &[ + ("--quoting-style=literal", "one"), + ("-N", "one"), + ("--quoting-style=c", "\"one\""), + ("-Q", "\"one\""), + ("--quote-name", "\"one\""), + ("--quoting-style=escape", "one"), + ("-b", "one"), + ("--quoting-style=shell-escape", "one"), + ("--quoting-style=shell-escape-always", "'one'"), + ("--quoting-style=shell", "one"), + ("--quoting-style=shell-always", "'one'"), + ] { + let result = scene.ucmd().arg(arg).arg("one").run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert_eq!(result.stdout, format!("{}\n", correct)); + } +} + +#[test] +fn test_ls_ignore_hide() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("README.md"); + at.touch("CONTRIBUTING.md"); + at.touch("some_other_file"); + at.touch("READMECAREFULLY.md"); + + scene + .ucmd() + .arg("--hide") + .arg("*") + .arg("-1") + .succeeds() + .stdout_is(""); + + scene + .ucmd() + .arg("--ignore") + .arg("*") + .arg("-1") + .succeeds() + .stdout_is(""); + + scene + .ucmd() + .arg("--ignore") + .arg("irrelevant pattern") + .arg("-1") + .succeeds() + .stdout_is("CONTRIBUTING.md\nREADME.md\nREADMECAREFULLY.md\nsome_other_file\n"); + + scene + .ucmd() + .arg("--ignore") + .arg("README*.md") + .arg("-1") + .succeeds() + .stdout_is("CONTRIBUTING.md\nsome_other_file\n"); + + scene + .ucmd() + .arg("--hide") + .arg("README*.md") + .arg("-1") + .succeeds() + .stdout_is("CONTRIBUTING.md\nsome_other_file\n"); + + scene + .ucmd() + .arg("--ignore") + .arg("*.md") + .arg("-1") + .succeeds() + .stdout_is("some_other_file\n"); + + scene + .ucmd() + .arg("-a") + .arg("--ignore") + .arg("*.md") + .arg("-1") + .succeeds() + .stdout_is(".\n..\nsome_other_file\n"); + + scene + .ucmd() + .arg("-a") + .arg("--hide") + .arg("*.md") + .arg("-1") + .succeeds() + .stdout_is(".\n..\nCONTRIBUTING.md\nREADME.md\nREADMECAREFULLY.md\nsome_other_file\n"); + + scene + .ucmd() + .arg("-A") + .arg("--ignore") + .arg("*.md") + .arg("-1") + .succeeds() + .stdout_is("some_other_file\n"); + + scene + .ucmd() + .arg("-A") + .arg("--hide") + .arg("*.md") + .arg("-1") + .succeeds() + .stdout_is("CONTRIBUTING.md\nREADME.md\nREADMECAREFULLY.md\nsome_other_file\n"); + + // Stacking multiple patterns + scene + .ucmd() + .arg("--ignore") + .arg("README*") + .arg("--ignore") + .arg("CONTRIBUTING*") + .arg("-1") + .succeeds() + .stdout_is("some_other_file\n"); + + scene + .ucmd() + .arg("--hide") + .arg("README*") + .arg("--ignore") + .arg("CONTRIBUTING*") + .arg("-1") + .succeeds() + .stdout_is("some_other_file\n"); + + scene + .ucmd() + .arg("--hide") + .arg("README*") + .arg("--hide") + .arg("CONTRIBUTING*") + .arg("-1") + .succeeds() + .stdout_is("some_other_file\n"); + + // Invalid patterns + scene + .ucmd() + .arg("--ignore") + .arg("READ[ME") + .arg("-1") + .succeeds() + .stderr_contains(&"Invalid pattern"); + + scene + .ucmd() + .arg("--hide") + .arg("READ[ME") + .arg("-1") + .succeeds() + .stderr_contains(&"Invalid pattern"); } diff --git a/tests/by-util/test_mkdir.rs b/tests/by-util/test_mkdir.rs index 70e4e414c..ef3226c41 100644 --- a/tests/by-util/test_mkdir.rs +++ b/tests/by-util/test_mkdir.rs @@ -13,6 +13,16 @@ fn test_mkdir_mkdir() { new_ucmd!().arg(TEST_DIR1).succeeds(); } +#[test] +fn test_mkdir_verbose() { + let expected = "mkdir: created directory 'mkdir_test1'\n"; + new_ucmd!() + .arg(TEST_DIR1) + .arg("-v") + .run() + .stdout_is(expected); +} + #[test] fn test_mkdir_dup_dir() { let scene = TestScenario::new(util_name!()); @@ -30,6 +40,8 @@ fn test_mkdir_parent() { let scene = TestScenario::new(util_name!()); scene.ucmd().arg("-p").arg(TEST_DIR4).succeeds(); scene.ucmd().arg("-p").arg(TEST_DIR4).succeeds(); + scene.ucmd().arg("--parent").arg(TEST_DIR4).succeeds(); + scene.ucmd().arg("--parents").arg(TEST_DIR4).succeeds(); } #[test] diff --git a/tests/by-util/test_mkfifo.rs b/tests/by-util/test_mkfifo.rs index 651491045..f60c0a4b8 100644 --- a/tests/by-util/test_mkfifo.rs +++ b/tests/by-util/test_mkfifo.rs @@ -1 +1,48 @@ -// ToDO: add tests +use crate::common::util::*; + +#[test] +fn test_create_fifo_missing_operand() { + new_ucmd!() + .fails() + .stderr_is("mkfifo: error: missing operand"); +} + +#[test] +fn test_create_one_fifo() { + new_ucmd!().arg("abc").succeeds(); +} + +#[test] +fn test_create_one_fifo_with_invalid_mode() { + new_ucmd!() + .arg("abcd") + .arg("-m") + .arg("invalid") + .fails() + .stderr + .contains("invalid mode"); +} + +#[test] +fn test_create_multiple_fifos() { + new_ucmd!() + .arg("abcde") + .arg("def") + .arg("sed") + .arg("dum") + .succeeds(); +} + +#[test] +fn test_create_one_fifo_with_mode() { + new_ucmd!().arg("abcde").arg("-m600").succeeds(); +} + +#[test] +fn test_create_one_fifo_already_exists() { + new_ucmd!() + .arg("abcdef") + .arg("abcdef") + .fails() + .stderr_is("mkfifo: error: cannot create fifo 'abcdef': File exists"); +} diff --git a/tests/by-util/test_mktemp.rs b/tests/by-util/test_mktemp.rs index bf75d31bf..2639a2c2f 100644 --- a/tests/by-util/test_mktemp.rs +++ b/tests/by-util/test_mktemp.rs @@ -1,7 +1,7 @@ use crate::common::util::*; -extern crate tempfile; -use self::tempfile::tempdir; +use std::path::PathBuf; +use tempfile::tempdir; static TEST_TEMPLATE1: &'static str = "tempXXXXXX"; static TEST_TEMPLATE2: &'static str = "temp"; @@ -65,6 +65,67 @@ fn test_mktemp_mktemp() { .fails(); } +#[test] +fn test_mktemp_mktemp_t() { + let scene = TestScenario::new(util_name!()); + + let pathname = scene.fixtures.as_string(); + + scene + .ucmd() + .env(TMPDIR, &pathname) + .arg("-t") + .arg(TEST_TEMPLATE1) + .succeeds(); + scene + .ucmd() + .env(TMPDIR, &pathname) + .arg("-t") + .arg(TEST_TEMPLATE2) + .fails(); + scene + .ucmd() + .env(TMPDIR, &pathname) + .arg("-t") + .arg(TEST_TEMPLATE3) + .fails(); + scene + .ucmd() + .env(TMPDIR, &pathname) + .arg("-t") + .arg(TEST_TEMPLATE4) + .fails(); + scene + .ucmd() + .env(TMPDIR, &pathname) + .arg("-t") + .arg(TEST_TEMPLATE5) + .succeeds(); + scene + .ucmd() + .env(TMPDIR, &pathname) + .arg("-t") + .arg(TEST_TEMPLATE6) + .succeeds(); + scene + .ucmd() + .env(TMPDIR, &pathname) + .arg("-t") + .arg(TEST_TEMPLATE7) + .succeeds(); + let result = scene + .ucmd() + .env(TMPDIR, &pathname) + .arg("-t") + .arg(TEST_TEMPLATE8) + .fails(); + println!("stdout {}", result.stdout); + println!("stderr {}", result.stderr); + assert!(result + .stderr + .contains("error: suffix cannot contain any path separators")); +} + #[test] fn test_mktemp_make_temp_dir() { let scene = TestScenario::new(util_name!()); @@ -320,3 +381,34 @@ fn test_mktemp_tmpdir() { .arg(TEST_TEMPLATE8) .fails(); } + +#[test] +fn test_mktemp_tmpdir_one_arg() { + let scene = TestScenario::new(util_name!()); + + let result = scene + .ucmd() + .arg("--tmpdir") + .arg("apt-key-gpghome.XXXXXXXXXX") + .succeeds(); + println!("stdout {}", result.stdout); + println!("stderr {}", result.stderr); + assert!(result.stdout.contains("apt-key-gpghome.")); + assert!(PathBuf::from(result.stdout.trim()).is_file()); +} + +#[test] +fn test_mktemp_directory_tmpdir() { + let scene = TestScenario::new(util_name!()); + + let result = scene + .ucmd() + .arg("--directory") + .arg("--tmpdir") + .arg("apt-key-gpghome.XXXXXXXXXX") + .succeeds(); + println!("stdout {}", result.stdout); + println!("stderr {}", result.stderr); + assert!(result.stdout.contains("apt-key-gpghome.")); + assert!(PathBuf::from(result.stdout.trim()).is_dir()); +} diff --git a/tests/by-util/test_more.rs b/tests/by-util/test_more.rs index 3b7cfa9a8..736fb6956 100644 --- a/tests/by-util/test_more.rs +++ b/tests/by-util/test_more.rs @@ -6,3 +6,14 @@ fn test_more_no_arg() { let result = ucmd.run(); assert!(!result.success); } + +#[test] +fn test_more_dir_arg() { + let (_, mut ucmd) = at_and_ucmd!(); + ucmd.arg("."); + let result = ucmd.run(); + assert!(!result.success); + const EXPECTED_ERROR_MESSAGE: &str = + "more: '.' is a directory.\nTry 'more --help' for more information."; + assert_eq!(result.stderr.trim(), EXPECTED_ERROR_MESSAGE); +} diff --git a/tests/by-util/test_mv.rs b/tests/by-util/test_mv.rs index 3fde65f99..0caeb1ef1 100644 --- a/tests/by-util/test_mv.rs +++ b/tests/by-util/test_mv.rs @@ -17,6 +17,16 @@ fn test_mv_rename_dir() { assert!(at.dir_exists(dir2)); } +#[test] +fn test_mv_fail() { + let (at, mut ucmd) = at_and_ucmd!(); + let dir1 = "test_mv_rename_dir"; + + at.mkdir(dir1); + + ucmd.arg(dir1).fails(); +} + #[test] fn test_mv_rename_file() { let (at, mut ucmd) = at_and_ucmd!(); @@ -301,6 +311,63 @@ fn test_mv_backup_numbering() { assert!(at.file_exists(&format!("{}.~1~", file_b))); } +#[test] +fn test_mv_backup_existing() { + let (at, mut ucmd) = at_and_ucmd!(); + let file_a = "test_mv_backup_numbering_file_a"; + let file_b = "test_mv_backup_numbering_file_b"; + + at.touch(file_a); + at.touch(file_b); + ucmd.arg("--backup=existing") + .arg(file_a) + .arg(file_b) + .succeeds() + .no_stderr(); + + assert!(!at.file_exists(file_a)); + assert!(at.file_exists(file_b)); + assert!(at.file_exists(&format!("{}~", file_b))); +} + +#[test] +fn test_mv_backup_simple() { + let (at, mut ucmd) = at_and_ucmd!(); + let file_a = "test_mv_backup_numbering_file_a"; + let file_b = "test_mv_backup_numbering_file_b"; + + at.touch(file_a); + at.touch(file_b); + ucmd.arg("--backup=simple") + .arg(file_a) + .arg(file_b) + .succeeds() + .no_stderr(); + + assert!(!at.file_exists(file_a)); + assert!(at.file_exists(file_b)); + assert!(at.file_exists(&format!("{}~", file_b))); +} + +#[test] +fn test_mv_backup_none() { + let (at, mut ucmd) = at_and_ucmd!(); + let file_a = "test_mv_backup_numbering_file_a"; + let file_b = "test_mv_backup_numbering_file_b"; + + at.touch(file_a); + at.touch(file_b); + ucmd.arg("--backup=none") + .arg(file_a) + .arg(file_b) + .succeeds() + .no_stderr(); + + assert!(!at.file_exists(file_a)); + assert!(at.file_exists(file_b)); + assert!(!at.file_exists(&format!("{}~", file_b))); +} + #[test] fn test_mv_existing_backup() { let (at, mut ucmd) = at_and_ucmd!(); @@ -459,17 +526,15 @@ fn test_mv_errors() { // $ mv -T -t a b // mv: cannot combine --target-directory (-t) and --no-target-directory (-T) - scene + let result = scene .ucmd() .arg("-T") .arg("-t") .arg(dir) .arg(file_a) .arg(file_b) - .fails() - .stderr_is( - "mv: error: cannot combine --target-directory (-t) and --no-target-directory (-T)\n", - ); + .fails(); + assert!(result.stderr.contains("cannot be used with")); // $ at.touch file && at.mkdir dir // $ mv -T file dir diff --git a/tests/by-util/test_nice.rs b/tests/by-util/test_nice.rs index 651491045..7e704fc00 100644 --- a/tests/by-util/test_nice.rs +++ b/tests/by-util/test_nice.rs @@ -1 +1,58 @@ -// ToDO: add tests +use crate::common::util::*; + +#[test] +fn test_get_current_niceness() { + // NOTE: this assumes the test suite is being run with a default niceness + // of 0, which may not necessarily be true + new_ucmd!().run().stdout_is("0\n"); +} + +#[test] +fn test_negative_adjustment() { + // This assumes the test suite is run as a normal (non-root) user, and as + // such attempting to set a negative niceness value will be rejected by + // the OS. If it gets denied, then we know a negative value was parsed + // correctly. + + let res = new_ucmd!().args(&["-n", "-1", "true"]).run(); + assert!(res + .stderr + .starts_with("nice: warning: setpriority: Permission denied")); +} + +#[test] +fn test_adjustment_with_no_command_should_error() { + new_ucmd!() + .args(&["-n", "19"]) + .run() + .stderr_is("nice: error: A command must be given with an adjustment.\nTry \"nice --help\" for more information.\n"); +} + +#[test] +fn test_command_with_no_adjustment() { + new_ucmd!().args(&["echo", "a"]).run().stdout_is("a\n"); +} + +#[test] +fn test_command_with_no_args() { + new_ucmd!() + .args(&["-n", "19", "echo"]) + .run() + .stdout_is("\n"); +} + +#[test] +fn test_command_with_args() { + new_ucmd!() + .args(&["-n", "19", "echo", "a", "b", "c"]) + .run() + .stdout_is("a b c\n"); +} + +#[test] +fn test_command_where_command_takes_n_flag() { + new_ucmd!() + .args(&["-n", "19", "echo", "-n", "a"]) + .run() + .stdout_is("a"); +} diff --git a/tests/by-util/test_nl.rs b/tests/by-util/test_nl.rs index fca73c37b..3ca039d19 100644 --- a/tests/by-util/test_nl.rs +++ b/tests/by-util/test_nl.rs @@ -23,8 +23,8 @@ fn test_padding_without_overflow() { .run() .stdout_is( "000001xL1\n001001xL2\n002001xL3\n003001xL4\n004001xL5\n005001xL6\n006001xL7\n0070\ - 01xL8\n008001xL9\n009001xL10\n010001xL11\n011001xL12\n012001xL13\n013001xL14\n014\ - 001xL15\n", + 01xL8\n008001xL9\n009001xL10\n010001xL11\n011001xL12\n012001xL13\n013001xL14\n014\ + 001xL15\n", ); } @@ -35,7 +35,7 @@ fn test_padding_with_overflow() { .run() .stdout_is( "0001xL1\n1001xL2\n2001xL3\n3001xL4\n4001xL5\n5001xL6\n6001xL7\n7001xL8\n8001xL9\n\ - 9001xL10\n10001xL11\n11001xL12\n12001xL13\n13001xL14\n14001xL15\n", + 9001xL10\n10001xL11\n11001xL12\n12001xL13\n13001xL14\n14001xL15\n", ); } @@ -45,15 +45,15 @@ fn test_sections_and_styles() { ( "section.txt", "\nHEADER1\nHEADER2\n\n1 |BODY1\n2 \ - |BODY2\n\nFOOTER1\nFOOTER2\n\nNEXTHEADER1\nNEXTHEADER2\n\n1 \ - |NEXTBODY1\n2 |NEXTBODY2\n\nNEXTFOOTER1\nNEXTFOOTER2\n", + |BODY2\n\nFOOTER1\nFOOTER2\n\nNEXTHEADER1\nNEXTHEADER2\n\n1 \ + |NEXTBODY1\n2 |NEXTBODY2\n\nNEXTFOOTER1\nNEXTFOOTER2\n", ), ( "joinblanklines.txt", "1 |Nonempty\n2 |Nonempty\n3 |Followed by 10x empty\n\n\n\n\n4 \ - |\n\n\n\n\n5 |\n6 |Followed by 5x empty\n\n\n\n\n7 |\n8 \ - |Followed by 4x empty\n\n\n\n\n9 |Nonempty\n10 |Nonempty\n11 \ - |Nonempty.\n", + |\n\n\n\n\n5 |\n6 |Followed by 5x empty\n\n\n\n\n7 |\n8 \ + |Followed by 4x empty\n\n\n\n\n9 |Nonempty\n10 |Nonempty\n11 \ + |Nonempty.\n", ), ] { new_ucmd!() diff --git a/tests/by-util/test_nohup.rs b/tests/by-util/test_nohup.rs index 651491045..b98ae007c 100644 --- a/tests/by-util/test_nohup.rs +++ b/tests/by-util/test_nohup.rs @@ -1 +1,19 @@ -// ToDO: add tests +use crate::common::util::*; +use std::thread::sleep; + +// General observation: nohup.out will not be created in tests run by cargo test +// because stdin/stdout is not attached to a TTY. +// All that can be tested is the side-effects. + +#[test] +#[cfg(any(target_os = "linux", target_os = "freebsd", target_vendor = "apple"))] +fn test_nohup_multiple_args_and_flags() { + let (at, mut ucmd) = at_and_ucmd!(); + + ucmd.args(&["touch", "-t", "1006161200", "file1", "file2"]) + .succeeds(); + sleep(std::time::Duration::from_millis(10)); + + assert!(at.file_exists("file1")); + assert!(at.file_exists("file2")); +} diff --git a/tests/by-util/test_numfmt.rs b/tests/by-util/test_numfmt.rs index 3aa7ab53b..64fc5360d 100644 --- a/tests/by-util/test_numfmt.rs +++ b/tests/by-util/test_numfmt.rs @@ -15,16 +15,25 @@ fn test_from_iec() { .args(&["--from=iec"]) .pipe_in("1024\n1.1M\n0.1G") .run() - .stdout_is("1024\n1153434\n107374182\n"); + .stdout_is("1024\n1153434\n107374183\n"); } #[test] fn test_from_iec_i() { new_ucmd!() .args(&["--from=iec-i"]) - .pipe_in("1024\n1.1Mi\n0.1Gi") + .pipe_in("1.1Mi\n0.1Gi") .run() - .stdout_is("1024\n1153434\n107374182\n"); + .stdout_is("1153434\n107374183\n"); +} + +#[test] +#[ignore] // FIXME: GNU from iec-i requires suffix +fn test_from_iec_i_requires_suffix() { + new_ucmd!() + .args(&["--from=iec-i", "1024"]) + .fails() + .stderr_is("numfmt: missing 'i' suffix in input: ‘1024’ (e.g Ki/Mi/Gi)"); } #[test] @@ -42,7 +51,7 @@ fn test_to_si() { .args(&["--to=si"]) .pipe_in("1000\n1100000\n100000000") .run() - .stdout_is("1.0K\n1.1M\n100.0M\n"); + .stdout_is("1.0K\n1.1M\n100M\n"); } #[test] @@ -51,7 +60,7 @@ fn test_to_iec() { .args(&["--to=iec"]) .pipe_in("1024\n1153434\n107374182") .run() - .stdout_is("1.0K\n1.1M\n102.4M\n"); + .stdout_is("1.0K\n1.2M\n103M\n"); } #[test] @@ -60,7 +69,7 @@ fn test_to_iec_i() { .args(&["--to=iec-i"]) .pipe_in("1024\n1153434\n107374182") .run() - .stdout_is("1.0Ki\n1.1Mi\n102.4Mi\n"); + .stdout_is("1.0Ki\n1.2Mi\n103Mi\n"); } #[test] @@ -107,6 +116,30 @@ fn test_header_default() { .stdout_is("header\n1000\n1100000\n100000000\n"); } +#[test] +fn test_header_error_if_non_numeric() { + new_ucmd!() + .args(&["--header=two"]) + .run() + .stderr_is("numfmt: invalid header value ‘two’"); +} + +#[test] +fn test_header_error_if_0() { + new_ucmd!() + .args(&["--header=0"]) + .run() + .stderr_is("numfmt: invalid header value ‘0’"); +} + +#[test] +fn test_header_error_if_negative() { + new_ucmd!() + .args(&["--header=-3"]) + .run() + .stderr_is("numfmt: invalid header value ‘-3’"); +} + #[test] fn test_negative() { new_ucmd!() @@ -118,7 +151,7 @@ fn test_negative() { .args(&["--to=iec-i"]) .pipe_in("-1024\n-1153434\n-107374182") .run() - .stdout_is("-1.0Ki\n-1.1Mi\n-102.4Mi\n"); + .stdout_is("-1.0Ki\n-1.2Mi\n-103Mi\n"); } #[test] @@ -135,7 +168,7 @@ fn test_normalize() { .args(&["--from=si", "--to=si"]) .pipe_in("10000000K\n0.001K") .run() - .stdout_is("10.0G\n1\n"); + .stdout_is("10G\n1\n"); } #[test] @@ -143,5 +176,305 @@ fn test_si_to_iec() { new_ucmd!() .args(&["--from=si", "--to=iec", "15334263563K"]) .run() - .stdout_is("13.9T\n"); + .stdout_is("14T\n"); +} + +#[test] +fn test_should_report_invalid_empty_number_on_empty_stdin() { + new_ucmd!() + .args(&["--from=auto"]) + .pipe_in("\n") + .run() + .stderr_is("numfmt: invalid number: ‘’\n"); +} + +#[test] +fn test_should_report_invalid_empty_number_on_blank_stdin() { + new_ucmd!() + .args(&["--from=auto"]) + .pipe_in(" \t \n") + .run() + .stderr_is("numfmt: invalid number: ‘’\n"); +} + +#[test] +fn test_should_report_invalid_suffix_on_stdin() { + new_ucmd!() + .args(&["--from=auto"]) + .pipe_in("1k") + .run() + .stderr_is("numfmt: invalid suffix in input: ‘1k’\n"); + + // GNU numfmt reports this one as “invalid number” + new_ucmd!() + .args(&["--from=auto"]) + .pipe_in("NaN") + .run() + .stderr_is("numfmt: invalid suffix in input: ‘NaN’\n"); +} + +#[test] +fn test_should_report_invalid_number_with_interior_junk() { + // GNU numfmt reports this as “invalid suffix” + new_ucmd!() + .args(&["--from=auto"]) + .pipe_in("1x0K") + .run() + .stderr_is("numfmt: invalid number: ‘1x0K’\n"); +} + +#[test] +fn test_should_skip_leading_space_from_stdin() { + new_ucmd!() + .args(&["--from=auto"]) + .pipe_in(" 2Ki") + .run() + .stdout_is("2048\n"); + + // multiline + new_ucmd!() + .args(&["--from=auto"]) + .pipe_in("\t1Ki\n 2K") + .run() + .stdout_is("1024\n2000\n"); +} + +#[test] +fn test_should_convert_only_first_number_in_line() { + new_ucmd!() + .args(&["--from=auto"]) + .pipe_in("1Ki 2M 3G") + .run() + .stdout_is("1024 2M 3G\n"); +} + +#[test] +fn test_leading_whitespace_should_imply_padding() { + new_ucmd!() + .args(&["--from=auto"]) + .pipe_in(" 1K") + .run() + .stdout_is(" 1000\n"); + + new_ucmd!() + .args(&["--from=auto"]) + .pipe_in(" 202Ki") + .run() + .stdout_is(" 206848\n"); +} + +#[test] +fn test_should_calculate_implicit_padding_per_line() { + new_ucmd!() + .args(&["--from=auto"]) + .pipe_in(" 1Ki\n 2K") + .run() + .stdout_is(" 1024\n 2000\n"); +} + +#[test] +fn test_leading_whitespace_in_free_argument_should_imply_padding() { + new_ucmd!() + .args(&["--from=auto", " 1Ki"]) + .run() + .stdout_is(" 1024\n"); +} + +#[test] +fn test_should_calculate_implicit_padding_per_free_argument() { + new_ucmd!() + .args(&["--from=auto", " 1Ki", " 2K"]) + .pipe_in(" 1Ki\n 2K") + .run() + .stdout_is(" 1024\n 2000\n"); +} + +#[test] +fn test_to_si_should_truncate_output() { + new_ucmd!() + .args(&["--to=si"]) + .pipe_in_fixture("gnutest_si_input.txt") + .succeeds() + .stdout_is_fixture("gnutest_si_result.txt"); +} + +#[test] +fn test_to_iec_should_truncate_output() { + new_ucmd!() + .args(&["--to=iec"]) + .pipe_in_fixture("gnutest_iec_input.txt") + .succeeds() + .stdout_is_fixture("gnutest_iec_result.txt"); +} + +#[test] +fn test_to_iec_i_should_truncate_output() { + new_ucmd!() + .args(&["--to=iec-i"]) + .pipe_in_fixture("gnutest_iec_input.txt") + .succeeds() + .stdout_is_fixture("gnutest_iec-i_result.txt"); +} + +#[test] +fn test_format_selected_field() { + new_ucmd!() + .args(&["--from=auto", "--field", "3", "1K 2K 3K"]) + .succeeds() + .stdout_only("1K 2K 3000\n"); + new_ucmd!() + .args(&["--from=auto", "--field", "2", "1K 2K 3K"]) + .succeeds() + .stdout_only("1K 2000 3K\n"); +} + +#[test] +fn test_format_selected_fields() { + new_ucmd!() + .args(&["--from=auto", "--field", "1,4,3", "1K 2K 3K 4K 5K 6K"]) + .succeeds() + .stdout_only("1000 2K 3000 4000 5K 6K\n"); +} + +#[test] +fn test_should_succeed_if_selected_field_out_of_range() { + new_ucmd!() + .args(&["--from=auto", "--field", "9", "1K 2K 3K"]) + .succeeds() + .stdout_only("1K 2K 3K\n"); +} + +#[test] +fn test_format_selected_field_range() { + new_ucmd!() + .args(&["--from=auto", "--field", "2-5", "1K 2K 3K 4K 5K 6K"]) + .succeeds() + .stdout_only("1K 2000 3000 4000 5000 6K\n"); +} + +#[test] +fn test_should_succeed_if_range_out_of_bounds() { + new_ucmd!() + .args(&["--from=auto", "--field", "5-10", "1K 2K 3K 4K 5K 6K"]) + .succeeds() + .stdout_only("1K 2K 3K 4K 5000 6000\n"); +} + +#[test] +fn test_implied_initial_field_value() { + new_ucmd!() + .args(&["--from=auto", "--field", "-2", "1K 2K 3K"]) + .succeeds() + .stdout_only("1000 2000 3K\n"); + + // same as above but with the equal sign + new_ucmd!() + .args(&["--from=auto", "--field=-2", "1K 2K 3K"]) + .succeeds() + .stdout_only("1000 2000 3K\n"); +} + +#[test] +fn test_field_df_example() { + // df -B1 | numfmt --header --field 2-4 --to=si + new_ucmd!() + .args(&["--header", "--field", "2-4", "--to=si"]) + .pipe_in_fixture("df_input.txt") + .succeeds() + .stdout_is_fixture("df_expected.txt"); +} + +#[test] +fn test_delimiter_must_not_be_empty() { + new_ucmd!().args(&["-d"]).fails(); +} + +#[test] +fn test_delimiter_must_not_be_more_than_one_character() { + new_ucmd!() + .args(&["--delimiter", "sad"]) + .fails() + .stderr_is("numfmt: the delimiter must be a single character"); +} + +#[test] +fn test_delimiter_only() { + new_ucmd!() + .args(&["-d", ","]) + .pipe_in("1234,56") + .succeeds() + .stdout_only("1234,56\n"); +} + +#[test] +fn test_line_is_field_with_no_delimiter() { + new_ucmd!() + .args(&["-d,", "--to=iec"]) + .pipe_in("123456") + .succeeds() + .stdout_only("121K\n"); +} + +#[test] +fn test_delimiter_to_si() { + new_ucmd!() + .args(&["-d=,", "--to=si"]) + .pipe_in("1234,56") + .succeeds() + .stdout_only("1.3K,56\n"); +} + +#[test] +fn test_delimiter_skips_leading_whitespace() { + new_ucmd!() + .args(&["-d=,", "--to=si"]) + .pipe_in(" \t 1234,56") + .succeeds() + .stdout_only("1.3K,56\n"); +} + +#[test] +fn test_delimiter_preserves_leading_whitespace_in_unselected_fields() { + new_ucmd!() + .args(&["-d=|", "--to=si"]) + .pipe_in(" 1000| 2000") + .succeeds() + .stdout_only("1.0K| 2000\n"); +} + +#[test] +fn test_delimiter_from_si() { + new_ucmd!() + .args(&["-d=,", "--from=si"]) + .pipe_in("1.2K,56") + .succeeds() + .stdout_only("1200,56\n"); +} + +#[test] +fn test_delimiter_overrides_whitespace_separator() { + // GNU numfmt reports this as “invalid suffix” + new_ucmd!() + .args(&["-d,"]) + .pipe_in("1 234,56") + .fails() + .stderr_is("numfmt: invalid number: ‘1 234’\n"); +} + +#[test] +fn test_delimiter_with_padding() { + new_ucmd!() + .args(&["-d=|", "--to=si", "--padding=5"]) + .pipe_in("1000|2000") + .succeeds() + .stdout_only(" 1.0K|2000\n"); +} + +#[test] +fn test_delimiter_with_padding_and_fields() { + new_ucmd!() + .args(&["-d=|", "--to=si", "--padding=5", "--field=-"]) + .pipe_in("1000|2000") + .succeeds() + .stdout_only(" 1.0K| 2.0K\n"); } diff --git a/tests/by-util/test_od.rs b/tests/by-util/test_od.rs index f3b766c26..b49e6f0ca 100644 --- a/tests/by-util/test_od.rs +++ b/tests/by-util/test_od.rs @@ -21,6 +21,7 @@ static ALPHA_OUT: &'static str = " // Test that od can read one file and dump with default format #[test] fn test_file() { + // TODO: Can this be replaced by AtPath? use std::env; let temp = env::temp_dir(); let tmpdir = Path::new(&temp); @@ -33,15 +34,12 @@ fn test_file() { } } - let result = new_ucmd!() + new_ucmd!() .arg("--endian=little") .arg(file.as_os_str()) - .run(); - - assert_empty_stderr!(result); - assert!(result.success); - assert_eq!(result.stdout, unindent(ALPHA_OUT)); - + .succeeds() + .no_stderr() + .stdout_is(unindent(ALPHA_OUT)); let _ = remove_file(file); } @@ -64,16 +62,14 @@ fn test_2files() { } } - let result = new_ucmd!() + new_ucmd!() .arg("--endian=little") .arg(file1.as_os_str()) .arg(file2.as_os_str()) - .run(); - - assert_empty_stderr!(result); - assert!(result.success); - assert_eq!(result.stdout, unindent(ALPHA_OUT)); - + .succeeds() + .no_stderr() + .stdout_is(unindent(ALPHA_OUT)); + // TODO: Handle errors? let _ = remove_file(file1); let _ = remove_file(file2); } @@ -85,22 +81,19 @@ fn test_no_file() { let tmpdir = Path::new(&temp); let file = tmpdir.join("}surely'none'would'thus'a'file'name"); - let result = new_ucmd!().arg(file.as_os_str()).run(); - - assert!(!result.success); + new_ucmd!().arg(file.as_os_str()).fails(); } // Test that od reads from stdin instead of a file #[test] fn test_from_stdin() { let input = "abcdefghijklmnopqrstuvwxyz\n"; - let result = new_ucmd!() + new_ucmd!() .arg("--endian=little") - .run_piped_stdin(input.as_bytes()); - - assert_empty_stderr!(result); - assert!(result.success); - assert_eq!(result.stdout, unindent(ALPHA_OUT)); + .run_piped_stdin(input.as_bytes()) + .success() + .no_stderr() + .stdout_is(unindent(ALPHA_OUT)); } // Test that od reads from stdin and also from files @@ -119,40 +112,35 @@ fn test_from_mixed() { } } - let result = new_ucmd!() + new_ucmd!() .arg("--endian=little") .arg(file1.as_os_str()) .arg("-") .arg(file3.as_os_str()) - .run_piped_stdin(data2.as_bytes()); - - assert_empty_stderr!(result); - assert!(result.success); - assert_eq!(result.stdout, unindent(ALPHA_OUT)); + .run_piped_stdin(data2.as_bytes()) + .success() + .no_stderr() + .stdout_is(unindent(ALPHA_OUT)); } #[test] fn test_multiple_formats() { let input = "abcdefghijklmnopqrstuvwxyz\n"; - let result = new_ucmd!() + new_ucmd!() .arg("-c") .arg("-b") - .run_piped_stdin(input.as_bytes()); - - assert_empty_stderr!(result); - assert!(result.success); - assert_eq!( - result.stdout, - unindent( + .run_piped_stdin(input.as_bytes()) + .success() + .no_stderr() + .stdout_is(unindent( " 0000000 a b c d e f g h i j k l m n o p 141 142 143 144 145 146 147 150 151 152 153 154 155 156 157 160 0000020 q r s t u v w x y z \\n 161 162 163 164 165 166 167 170 171 172 012 0000033 - " - ) - ); + ", + )); } #[test] @@ -166,14 +154,13 @@ fn test_dec() { 0000016 ", ); - let result = new_ucmd!() + new_ucmd!() .arg("--endian=little") .arg("-s") - .run_piped_stdin(&input[..]); - - assert_empty_stderr!(result); - assert!(result.success); - assert_eq!(result.stdout, expected_output); + .run_piped_stdin(&input[..]) + .success() + .no_stderr() + .stdout_is(expected_output); } #[test] @@ -185,14 +172,13 @@ fn test_hex16() { 0000011 ", ); - let result = new_ucmd!() + new_ucmd!() .arg("--endian=little") .arg("-x") - .run_piped_stdin(&input[..]); - - assert_empty_stderr!(result); - assert!(result.success); - assert_eq!(result.stdout, expected_output); + .run_piped_stdin(&input[..]) + .success() + .no_stderr() + .stdout_is(expected_output); } #[test] @@ -204,14 +190,13 @@ fn test_hex32() { 0000011 ", ); - let result = new_ucmd!() + new_ucmd!() .arg("--endian=little") .arg("-X") - .run_piped_stdin(&input[..]); - - assert_empty_stderr!(result); - assert!(result.success); - assert_eq!(result.stdout, expected_output); + .run_piped_stdin(&input[..]) + .success() + .no_stderr() + .stdout_is(expected_output); } #[test] @@ -232,15 +217,14 @@ fn test_f16() { 0000016 ", ); - let result = new_ucmd!() + new_ucmd!() .arg("--endian=little") .arg("-tf2") .arg("-w8") - .run_piped_stdin(&input[..]); - - assert_empty_stderr!(result); - assert!(result.success); - assert_eq!(result.stdout, expected_output); + .run_piped_stdin(&input[..]) + .success() + .no_stderr() + .stdout_is(expected_output); } #[test] @@ -261,14 +245,13 @@ fn test_f32() { 0000034 ", ); - let result = new_ucmd!() + new_ucmd!() .arg("--endian=little") .arg("-f") - .run_piped_stdin(&input[..]); - - assert_empty_stderr!(result); - assert!(result.success); - assert_eq!(result.stdout, expected_output); + .run_piped_stdin(&input[..]) + .success() + .no_stderr() + .stdout_is(expected_output); } #[test] @@ -291,36 +274,31 @@ fn test_f64() { 0000050 ", ); - let result = new_ucmd!() + new_ucmd!() .arg("--endian=little") .arg("-F") - .run_piped_stdin(&input[..]); - - assert_empty_stderr!(result); - assert!(result.success); - assert_eq!(result.stdout, expected_output); + .run_piped_stdin(&input[..]) + .success() + .no_stderr() + .stdout_is(expected_output); } #[test] fn test_multibyte() { - let result = new_ucmd!() + new_ucmd!() .arg("-c") .arg("-w12") - .run_piped_stdin("Universität Tübingen \u{1B000}".as_bytes()); - - assert_empty_stderr!(result); - assert!(result.success); - assert_eq!( - result.stdout, - unindent( + .run_piped_stdin("Universität Tübingen \u{1B000}".as_bytes()) + .success() + .no_stderr() + .stdout_is(unindent( " 0000000 U n i v e r s i t ä ** t 0000014 T ü ** b i n g e n \u{1B000} 0000030 ** ** ** 0000033 - " - ) - ); + ", + )); } #[test] @@ -334,11 +312,13 @@ fn test_width() { ", ); - let result = new_ucmd!().arg("-w4").arg("-v").run_piped_stdin(&input[..]); - - assert_empty_stderr!(result); - assert!(result.success); - assert_eq!(result.stdout, expected_output); + new_ucmd!() + .arg("-w4") + .arg("-v") + .run_piped_stdin(&input[..]) + .success() + .no_stderr() + .stdout_is(expected_output); } #[test] @@ -352,14 +332,13 @@ fn test_invalid_width() { ", ); - let result = new_ucmd!().arg("-w5").arg("-v").run_piped_stdin(&input[..]); - - assert_eq!( - result.stderr, - "od: warning: invalid width 5; using 2 instead\n" - ); - assert!(result.success); - assert_eq!(result.stdout, expected_output); + new_ucmd!() + .arg("-w5") + .arg("-v") + .run_piped_stdin(&input[..]) + .success() + .stderr_is_bytes("od: warning: invalid width 5; using 2 instead\n".as_bytes()) + .stdout_is(expected_output); } #[test] @@ -373,14 +352,13 @@ fn test_zero_width() { ", ); - let result = new_ucmd!().arg("-w0").arg("-v").run_piped_stdin(&input[..]); - - assert_eq!( - result.stderr, - "od: warning: invalid width 0; using 2 instead\n" - ); - assert!(result.success); - assert_eq!(result.stdout, expected_output); + new_ucmd!() + .arg("-w0") + .arg("-v") + .run_piped_stdin(&input[..]) + .success() + .stderr_is_bytes("od: warning: invalid width 0; using 2 instead\n".as_bytes()) + .stdout_is(expected_output); } #[test] @@ -392,11 +370,12 @@ fn test_width_without_value() { 0000050 "); - let result = new_ucmd!().arg("-w").run_piped_stdin(&input[..]); - - assert_empty_stderr!(result); - assert!(result.success); - assert_eq!(result.stdout, expected_output); + new_ucmd!() + .arg("-w") + .run_piped_stdin(&input[..]) + .success() + .no_stderr() + .stdout_is(expected_output); } #[test] @@ -421,15 +400,14 @@ fn test_suppress_duplicates() { ", ); - let result = new_ucmd!() + new_ucmd!() .arg("-w4") .arg("-O") .arg("-x") - .run_piped_stdin(&input[..]); - - assert_empty_stderr!(result); - assert!(result.success); - assert_eq!(result.stdout, expected_output); + .run_piped_stdin(&input[..]) + .no_stderr() + .success() + .stdout_is(expected_output); } #[test] @@ -446,17 +424,16 @@ fn test_big_endian() { ", ); - let result = new_ucmd!() + new_ucmd!() .arg("--endian=big") .arg("-F") .arg("-f") .arg("-X") .arg("-x") - .run_piped_stdin(&input[..]); - - assert_empty_stderr!(result); - assert!(result.success); - assert_eq!(result.stdout, expected_output); + .run_piped_stdin(&input[..]) + .no_stderr() + .success() + .stdout_is(expected_output); } #[test] @@ -474,16 +451,15 @@ fn test_alignment_Xxa() { ); // in this case the width of the -a (8-bit) determines the alignment for the other fields - let result = new_ucmd!() + new_ucmd!() .arg("--endian=little") .arg("-X") .arg("-x") .arg("-a") - .run_piped_stdin(&input[..]); - - assert_empty_stderr!(result); - assert!(result.success); - assert_eq!(result.stdout, expected_output); + .run_piped_stdin(&input[..]) + .no_stderr() + .success() + .stdout_is(expected_output); } #[test] @@ -500,15 +476,14 @@ fn test_alignment_Fx() { ); // in this case the width of the -F (64-bit) determines the alignment for the other field - let result = new_ucmd!() + new_ucmd!() .arg("--endian=little") .arg("-F") .arg("-x") - .run_piped_stdin(&input[..]); - - assert_empty_stderr!(result); - assert!(result.success); - assert_eq!(result.stdout, expected_output); + .run_piped_stdin(&input[..]) + .no_stderr() + .success() + .stdout_is(expected_output); } #[test] @@ -528,16 +503,15 @@ fn test_maxuint() { ", ); - let result = new_ucmd!() + new_ucmd!() .arg("--format=o8") .arg("-Oobtu8") .arg("-Dd") .arg("--format=u1") - .run_piped_stdin(&input[..]); - - assert_empty_stderr!(result); - assert!(result.success); - assert_eq!(result.stdout, expected_output); + .run_piped_stdin(&input[..]) + .no_stderr() + .success() + .stdout_is(expected_output); } #[test] @@ -553,15 +527,14 @@ fn test_hex_offset() { ", ); - let result = new_ucmd!() + new_ucmd!() .arg("-Ax") .arg("-X") .arg("-X") - .run_piped_stdin(&input[..]); - - assert_empty_stderr!(result); - assert!(result.success); - assert_eq!(result.stdout, expected_output); + .run_piped_stdin(&input[..]) + .no_stderr() + .success() + .stdout_is(expected_output); } #[test] @@ -577,15 +550,14 @@ fn test_dec_offset() { ", ); - let result = new_ucmd!() + new_ucmd!() .arg("-Ad") .arg("-X") .arg("-X") - .run_piped_stdin(&input[..]); - - assert_empty_stderr!(result); - assert!(result.success); - assert_eq!(result.stdout, expected_output); + .run_piped_stdin(&input[..]) + .no_stderr() + .success() + .stdout_is(expected_output); } #[test] @@ -594,66 +566,57 @@ fn test_no_offset() { const LINE: &'static str = " 00000000 00000000 00000000 00000000\n"; let expected_output = [LINE, LINE, LINE, LINE].join(""); - let result = new_ucmd!() + new_ucmd!() .arg("-An") .arg("-X") .arg("-X") - .run_piped_stdin(&input[..]); - - assert_empty_stderr!(result); - assert!(result.success); - assert_eq!(result.stdout, expected_output); + .run_piped_stdin(&input[..]) + .no_stderr() + .success() + .stdout_is(expected_output); } #[test] fn test_invalid_offset() { - let result = new_ucmd!().arg("-Ab").run(); - - assert!(!result.success); + new_ucmd!().arg("-Ab").fails(); } #[test] fn test_skip_bytes() { let input = "abcdefghijklmnopq"; - let result = new_ucmd!() + new_ucmd!() .arg("-c") .arg("--skip-bytes=5") - .run_piped_stdin(input.as_bytes()); - - assert_empty_stderr!(result); - assert!(result.success); - assert_eq!( - result.stdout, - unindent( + .run_piped_stdin(input.as_bytes()) + .no_stderr() + .success() + .stdout_is(unindent( " 0000005 f g h i j k l m n o p q 0000021 - " - ) - ); + ", + )); } #[test] fn test_skip_bytes_error() { let input = "12345"; - let result = new_ucmd!() + new_ucmd!() .arg("--skip-bytes=10") - .run_piped_stdin(input.as_bytes()); - - assert!(!result.success); + .run_piped_stdin(input.as_bytes()) + .failure(); } #[test] fn test_read_bytes() { let input = "abcdefghijklmnopqrstuvwxyz\n12345678"; - let result = new_ucmd!() + new_ucmd!() .arg("--endian=little") .arg("--read-bytes=27") - .run_piped_stdin(input.as_bytes()); - - assert_empty_stderr!(result); - assert!(result.success); - assert_eq!(result.stdout, unindent(ALPHA_OUT)); + .run_piped_stdin(input.as_bytes()) + .no_stderr() + .success() + .stdout_is(unindent(ALPHA_OUT)); } #[test] @@ -662,13 +625,12 @@ fn test_ascii_dump() { 0x00, 0x01, 0x0a, 0x0d, 0x10, 0x1f, 0x20, 0x61, 0x62, 0x63, 0x7d, 0x7e, 0x7f, 0x80, 0x90, 0xa0, 0xb0, 0xc0, 0xd0, 0xe0, 0xf0, 0xff, ]; - let result = new_ucmd!().arg("-tx1zacz").run_piped_stdin(&input[..]); - - assert_empty_stderr!(result); - assert!(result.success); - assert_eq!( - result.stdout, - unindent( + new_ucmd!() + .arg("-tx1zacz") + .run_piped_stdin(&input[..]) + .no_stderr() + .success() + .stdout_is(unindent( r" 0000000 00 01 0a 0d 10 1f 20 61 62 63 7d 7e 7f 80 90 a0 >...... abc}~....< nul soh nl cr dle us sp a b c } ~ del nul dle sp @@ -677,9 +639,8 @@ fn test_ascii_dump() { 0 @ P ` p del ** 300 320 340 360 377 >......< 0000026 - " - ) - ); + ", + )); } #[test] @@ -687,159 +648,136 @@ fn test_filename_parsing() { // files "a" and "x" both exists, but are no filenames in the commandline below // "-f" must be treated as a filename, it contains the text: minus lowercase f // so "-f" should not be interpreted as a formatting option. - let result = new_ucmd!() + new_ucmd!() .arg("--format") .arg("a") .arg("-A") .arg("x") .arg("--") .arg("-f") - .run(); - - assert_empty_stderr!(result); - assert!(result.success); - assert_eq!( - result.stdout, - unindent( + .succeeds() + .no_stderr() + .stdout_is(unindent( " 000000 m i n u s sp l o w e r c a s e sp 000010 f nl 000012 - " - ) - ); + ", + )); } #[test] fn test_stdin_offset() { let input = "abcdefghijklmnopq"; - let result = new_ucmd!() + new_ucmd!() .arg("-c") .arg("+5") - .run_piped_stdin(input.as_bytes()); - - assert_empty_stderr!(result); - assert!(result.success); - assert_eq!( - result.stdout, - unindent( + .run_piped_stdin(input.as_bytes()) + .no_stderr() + .success() + .stdout_is(unindent( " 0000005 f g h i j k l m n o p q 0000021 - " - ) - ); + ", + )); } #[test] fn test_file_offset() { - let result = new_ucmd!().arg("-c").arg("--").arg("-f").arg("10").run(); - - assert_empty_stderr!(result); - assert!(result.success); - assert_eq!( - result.stdout, - unindent( + new_ucmd!() + .arg("-c") + .arg("--") + .arg("-f") + .arg("10") + .succeeds() + .no_stderr() + .stdout_is(unindent( r" 0000010 w e r c a s e f \n 0000022 - " - ) - ); + ", + )); } #[test] fn test_traditional() { // note gnu od does not align both lines let input = "abcdefghijklmnopq"; - let result = new_ucmd!() + new_ucmd!() .arg("--traditional") .arg("-a") .arg("-c") .arg("-") .arg("10") .arg("0") - .run_piped_stdin(input.as_bytes()); - - assert_empty_stderr!(result); - assert!(result.success); - assert_eq!( - result.stdout, - unindent( + .run_piped_stdin(input.as_bytes()) + .no_stderr() + .success() + .stdout_is(unindent( r" 0000010 (0000000) i j k l m n o p q i j k l m n o p q 0000021 (0000011) - " - ) - ); + ", + )); } #[test] fn test_traditional_with_skip_bytes_override() { // --skip-bytes is ignored in this case let input = "abcdefghijklmnop"; - let result = new_ucmd!() + new_ucmd!() .arg("--traditional") .arg("--skip-bytes=10") .arg("-c") .arg("0") - .run_piped_stdin(input.as_bytes()); - - assert_empty_stderr!(result); - assert!(result.success); - assert_eq!( - result.stdout, - unindent( + .run_piped_stdin(input.as_bytes()) + .no_stderr() + .success() + .stdout_is(unindent( r" 0000000 a b c d e f g h i j k l m n o p 0000020 - " - ) - ); + ", + )); } #[test] fn test_traditional_with_skip_bytes_non_override() { // no offset specified in the traditional way, so --skip-bytes is used let input = "abcdefghijklmnop"; - let result = new_ucmd!() + new_ucmd!() .arg("--traditional") .arg("--skip-bytes=10") .arg("-c") - .run_piped_stdin(input.as_bytes()); - - assert_empty_stderr!(result); - assert!(result.success); - assert_eq!( - result.stdout, - unindent( + .run_piped_stdin(input.as_bytes()) + .no_stderr() + .success() + .stdout_is(unindent( r" 0000012 k l m n o p 0000020 - " - ) - ); + ", + )); } #[test] fn test_traditional_error() { // file "0" exists - don't fail on that, but --traditional only accepts a single input - let result = new_ucmd!() + new_ucmd!() .arg("--traditional") .arg("0") .arg("0") .arg("0") .arg("0") - .run(); - - assert!(!result.success); + .fails(); } #[test] fn test_traditional_only_label() { let input = "abcdefghijklmnopqrstuvwxyz"; - let result = new_ucmd!() + new_ucmd!() .arg("-An") .arg("--traditional") .arg("-a") @@ -847,20 +785,16 @@ fn test_traditional_only_label() { .arg("-") .arg("10") .arg("0x10") - .run_piped_stdin(input.as_bytes()); - - assert_empty_stderr!(result); - assert!(result.success); - assert_eq!( - result.stdout, - unindent( + .run_piped_stdin(input.as_bytes()) + .no_stderr() + .success() + .stdout_is(unindent( r" (0000020) i j k l m n o p q r s t u v w x i j k l m n o p q r s t u v w x (0000040) y z y z (0000042) - " - ) - ); + ", + )); } diff --git a/tests/by-util/test_paste.rs b/tests/by-util/test_paste.rs index 27de0b445..4604c5cf5 100644 --- a/tests/by-util/test_paste.rs +++ b/tests/by-util/test_paste.rs @@ -1,9 +1,104 @@ use crate::common::util::*; +struct TestData<'b> { + name: &'b str, + args: &'b [&'b str], + ins: &'b [&'b str], + out: &'b str, +} + +static EXAMPLE_DATA: &'static [TestData<'static>] = &[ + // Ensure that paste properly handles files lacking a final newline. + TestData { + name: "no-nl-1", + args: &[], + ins: &["a", "b"], + out: "a\tb\n", + }, + TestData { + name: "no-nl-2", + args: &[], + ins: &["a\n", "b"], + out: "a\tb\n", + }, + TestData { + name: "no-nl-3", + args: &[], + ins: &["a", "b\n"], + out: "a\tb\n", + }, + TestData { + name: "no-nl-4", + args: &[], + ins: &["a\n", "b\n"], + out: "a\tb\n", + }, + // Same as above, but with a two lines in each input file and the + // addition of the -d option to make SPACE be the output + // delimiter. + TestData { + name: "no-nla-1", + args: &["-d", " "], + ins: &["1\na", "2\nb"], + out: "1 2\na b\n", + }, + TestData { + name: "no-nla-2", + args: &["-d", " "], + ins: &["1\na\n", "2\nb"], + out: "1 2\na b\n", + }, + TestData { + name: "no-nla-3", + args: &["-d", " "], + ins: &["1\na", "2\nb\n"], + out: "1 2\na b\n", + }, + TestData { + name: "no-nla-4", + args: &["-d", " "], + ins: &["1\na\n", "2\nb\n"], + out: "1 2\na b\n", + }, +]; + #[test] fn test_combine_pairs_of_lines() { - new_ucmd!() - .args(&["-s", "-d", "\t\n", "html_colors.txt"]) - .run() - .stdout_is_fixture("html_colors.expected"); + for s in vec!["-s", "--serial"] { + for d in vec!["-d", "--delimiters"] { + new_ucmd!() + .args(&[s, d, "\t\n", "html_colors.txt"]) + .run() + .stdout_is_fixture("html_colors.expected"); + } + } +} + +#[test] +fn test_multi_stdin() { + for d in vec!["-d", "--delimiters"] { + new_ucmd!() + .args(&[d, "\t\n", "-", "-"]) + .pipe_in_fixture("html_colors.txt") + .succeeds() + .stdout_is_fixture("html_colors.expected"); + } +} + +#[test] +fn test_data() { + for example in EXAMPLE_DATA { + let (at, mut ucmd) = at_and_ucmd!(); + let mut ins = vec![]; + for (i, _in) in example.ins.iter().enumerate() { + let file = format!("in{}", i); + at.write(&file, _in); + ins.push(file); + } + println!("{}", example.name); + ucmd.args(example.args) + .args(&ins) + .succeeds() + .stdout_is(example.out); + } } diff --git a/tests/by-util/test_pathchk.rs b/tests/by-util/test_pathchk.rs index bdce377b3..3bc12f0b6 100644 --- a/tests/by-util/test_pathchk.rs +++ b/tests/by-util/test_pathchk.rs @@ -5,11 +5,152 @@ fn test_default_mode() { // test the default mode // accept some reasonable default - new_ucmd!().args(&["abc/def"]).succeeds().no_stdout(); + new_ucmd!().args(&["dir/file"]).succeeds().no_stdout(); - // fail on long inputs + // accept non-portable chars + new_ucmd!().args(&["dir#/$file"]).succeeds().no_stdout(); + + // accept empty path + new_ucmd!().args(&[""]).succeeds().no_stdout(); + + // fail on long path new_ucmd!() - .args(&[repeat_str("test", 20000)]) + .args(&["dir".repeat(libc::PATH_MAX as usize + 1)]) + .fails() + .no_stdout(); + + // fail on long filename + new_ucmd!() + .args(&[format!( + "dir/{}", + "file".repeat(libc::FILENAME_MAX as usize + 1) + )]) .fails() .no_stdout(); } + +#[test] +fn test_posix_mode() { + // test the posix mode + + // accept some reasonable default + new_ucmd!().args(&["-p", "dir/file"]).succeeds().no_stdout(); + + // fail on long path + new_ucmd!() + .args(&["-p", &"dir".repeat(libc::PATH_MAX as usize + 1).as_str()]) + .fails() + .no_stdout(); + + // fail on long filename + new_ucmd!() + .args(&[ + "-p", + &format!("dir/{}", "file".repeat(libc::FILENAME_MAX as usize + 1)).as_str(), + ]) + .fails() + .no_stdout(); + + // fail on non-portable chars + new_ucmd!().args(&["-p", "dir#/$file"]).fails().no_stdout(); +} + +#[test] +fn test_posix_special() { + // test the posix special mode + + // accept some reasonable default + new_ucmd!().args(&["-P", "dir/file"]).succeeds().no_stdout(); + + // accept non-portable chars + new_ucmd!() + .args(&["-P", "dir#/$file"]) + .succeeds() + .no_stdout(); + + // accept non-leading hyphen + new_ucmd!() + .args(&["-P", "dir/file-name"]) + .succeeds() + .no_stdout(); + + // fail on long path + new_ucmd!() + .args(&["-P", &"dir".repeat(libc::PATH_MAX as usize + 1).as_str()]) + .fails() + .no_stdout(); + + // fail on long filename + new_ucmd!() + .args(&[ + "-P", + &format!("dir/{}", "file".repeat(libc::FILENAME_MAX as usize + 1)).as_str(), + ]) + .fails() + .no_stdout(); + + // fail on leading hyphen char + new_ucmd!().args(&["-P", "dir/-file"]).fails().no_stdout(); + + // fail on empty path + new_ucmd!().args(&["-P", ""]).fails().no_stdout(); +} + +#[test] +fn test_posix_all() { + // test the posix special mode + + // accept some reasonable default + new_ucmd!() + .args(&["-p", "-P", "dir/file"]) + .succeeds() + .no_stdout(); + + // accept non-leading hyphen + new_ucmd!() + .args(&["-p", "-P", "dir/file-name"]) + .succeeds() + .no_stdout(); + + // fail on long path + new_ucmd!() + .args(&[ + "-p", + "-P", + &"dir".repeat(libc::PATH_MAX as usize + 1).as_str(), + ]) + .fails() + .no_stdout(); + + // fail on long filename + new_ucmd!() + .args(&[ + "-p", + "-P", + &format!("dir/{}", "file".repeat(libc::FILENAME_MAX as usize + 1)).as_str(), + ]) + .fails() + .no_stdout(); + + // fail on non-portable chars + new_ucmd!() + .args(&["-p", "-P", "dir#/$file"]) + .fails() + .no_stdout(); + + // fail on leading hyphen char + new_ucmd!() + .args(&["-p", "-P", "dir/-file"]) + .fails() + .no_stdout(); + + // fail on empty path + new_ucmd!().args(&["-p", "-P", ""]).fails().no_stdout(); +} + +#[test] +fn test_args_parsing() { + // fail on no args + let empty_args: [String; 0] = []; + new_ucmd!().args(&empty_args).fails().no_stdout(); +} diff --git a/tests/by-util/test_ptx.rs b/tests/by-util/test_ptx.rs index 77117c5c0..e44943bfa 100644 --- a/tests/by-util/test_ptx.rs +++ b/tests/by-util/test_ptx.rs @@ -8,6 +8,22 @@ fn gnu_ext_disabled_roff_no_ref() { .stdout_only_fixture("gnu_ext_disabled_roff_no_ref.expected"); } +#[test] +fn gnu_ext_disabled_roff_no_ref_empty_word_regexp() { + new_ucmd!() + .args(&["-G", "-R", "-W", "", "input"]) + .succeeds() + .stdout_only_fixture("gnu_ext_disabled_roff_no_ref.expected"); +} + +#[test] +fn gnu_ext_disabled_roff_no_ref_word_regexp_exc_space() { + new_ucmd!() + .args(&["-G", "-R", "-W", "[^\t\n]+", "input"]) + .succeeds() + .stdout_only_fixture("gnu_ext_disabled_roff_no_ref_word_regexp_exc_space.expected"); +} + #[test] fn gnu_ext_disabled_roff_input_ref() { new_ucmd!() diff --git a/tests/by-util/test_pwd.rs b/tests/by-util/test_pwd.rs index aecb700da..b6a6c87a4 100644 --- a/tests/by-util/test_pwd.rs +++ b/tests/by-util/test_pwd.rs @@ -5,3 +5,9 @@ fn test_default() { let (at, mut ucmd) = at_and_ucmd!(); ucmd.run().stdout_is(at.root_dir_resolved() + "\n"); } + +#[test] +fn test_failed() { + let (_at, mut ucmd) = at_and_ucmd!(); + ucmd.arg("willfail").fails(); +} diff --git a/tests/by-util/test_realpath.rs b/tests/by-util/test_realpath.rs index 1d5b67e68..e1384ac74 100644 --- a/tests/by-util/test_realpath.rs +++ b/tests/by-util/test_realpath.rs @@ -1,34 +1,108 @@ use crate::common::util::*; #[test] -fn test_current_directory() { +fn test_realpath_current_directory() { let (at, mut ucmd) = at_and_ucmd!(); - let actual = ucmd.arg(".").run().stdout; let expect = at.root_dir_resolved() + "\n"; - println!("actual: {:?}", actual); - println!("expect: {:?}", expect); - assert_eq!(actual, expect); + ucmd.arg(".").succeeds().stdout_is(expect); } #[test] -fn test_long_redirection_to_current_dir() { +fn test_realpath_long_redirection_to_current_dir() { let (at, mut ucmd) = at_and_ucmd!(); // Create a 256-character path to current directory let dir = path_concat!(".", ..128); - let actual = ucmd.arg(dir).run().stdout; let expect = at.root_dir_resolved() + "\n"; - println!("actual: {:?}", actual); - println!("expect: {:?}", expect); - assert_eq!(actual, expect); + ucmd.arg(dir).succeeds().stdout_is(expect); } #[test] -fn test_long_redirection_to_root() { +fn test_realpath_long_redirection_to_root() { // Create a 255-character path to root let dir = path_concat!("..", ..85); - let actual = new_ucmd!().arg(dir).run().stdout; let expect = get_root_path().to_owned() + "\n"; - println!("actual: {:?}", actual); - println!("expect: {:?}", expect); - assert_eq!(actual, expect); + new_ucmd!().arg(dir).succeeds().stdout_is(expect); +} + +#[test] +fn test_realpath_file_and_links() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("foo"); + at.symlink_file("foo", "bar"); + + scene.ucmd().arg("foo").succeeds().stdout_contains("foo\n"); + scene.ucmd().arg("bar").succeeds().stdout_contains("foo\n"); +} + +#[test] +fn test_realpath_file_and_links_zero() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("foo"); + at.symlink_file("foo", "bar"); + + scene + .ucmd() + .arg("foo") + .arg("-z") + .succeeds() + .stdout_contains("foo\u{0}"); + + scene + .ucmd() + .arg("bar") + .arg("-z") + .succeeds() + .stdout_contains("foo\u{0}"); +} + +#[test] +fn test_realpath_file_and_links_strip() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("foo"); + at.symlink_file("foo", "bar"); + + scene + .ucmd() + .arg("foo") + .arg("-s") + .succeeds() + .stdout_contains("foo\n"); + + scene + .ucmd() + .arg("bar") + .arg("-s") + .succeeds() + .stdout_contains("bar\n"); +} + +#[test] +fn test_realpath_file_and_links_strip_zero() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("foo"); + at.symlink_file("foo", "bar"); + + scene + .ucmd() + .arg("foo") + .arg("-s") + .arg("-z") + .succeeds() + .stdout_contains("foo\u{0}"); + + scene + .ucmd() + .arg("bar") + .arg("-s") + .arg("-z") + .succeeds() + .stdout_contains("bar\u{0}"); } diff --git a/tests/by-util/test_relpath.rs b/tests/by-util/test_relpath.rs index 651491045..690531896 100644 --- a/tests/by-util/test_relpath.rs +++ b/tests/by-util/test_relpath.rs @@ -1 +1,165 @@ -// ToDO: add tests +use crate::common::util::*; +use std::borrow::Cow; +use std::path::Path; + +struct TestCase<'a> { + from: &'a str, + to: &'a str, + expected: &'a str, +} + +const TESTS: [TestCase; 10] = [ + TestCase { + from: "A/B/C", + to: "A", + expected: "../..", + }, + TestCase { + from: "A/B/C", + to: "A/B", + expected: "..", + }, + TestCase { + from: "A/B/C", + to: "A/B/C", + expected: "", + }, + TestCase { + from: "A/B/C", + to: "A/B/C/D", + expected: "D", + }, + TestCase { + from: "A/B/C", + to: "A/B/C/D/E", + expected: "D/E", + }, + TestCase { + from: "A/B/C", + to: "A/B/D", + expected: "../D", + }, + TestCase { + from: "A/B/C", + to: "A/B/D/E", + expected: "../D/E", + }, + TestCase { + from: "A/B/C", + to: "A/D", + expected: "../../D", + }, + TestCase { + from: "A/B/C", + to: "D/E/F", + expected: "../../../D/E/F", + }, + TestCase { + from: "A/B/C", + to: "A/D/E", + expected: "../../D/E", + }, +]; + +fn convert_path<'a>(path: &'a str) -> Cow<'a, str> { + #[cfg(windows)] + return path.replace("/", "\\").into(); + #[cfg(not(windows))] + return path.into(); +} + +#[test] +fn test_relpath_with_from_no_d() { + for test in TESTS.iter() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let from: &str = &convert_path(test.from); + let to: &str = &convert_path(test.to); + let expected: &str = &convert_path(test.expected); + + at.mkdir_all(to); + at.mkdir_all(from); + + scene + .ucmd() + .arg(to) + .arg(from) + .succeeds() + .stdout_only(&format!("{}\n", expected)); + } +} + +#[test] +fn test_relpath_with_from_with_d() { + for test in TESTS.iter() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let from: &str = &convert_path(test.from); + let to: &str = &convert_path(test.to); + let pwd = at.as_string(); + at.mkdir_all(to); + at.mkdir_all(from); + + // d is part of subpath -> expect relative path + let mut result = scene + .ucmd() + .arg(to) + .arg(from) + .arg(&format!("-d{}", pwd)) + .run(); + assert!(result.success); + // relax rules for windows test environment + #[cfg(not(windows))] + assert!(Path::new(&result.stdout).is_relative()); + + // d is not part of subpath -> expect absolut path + result = scene.ucmd().arg(to).arg(from).arg("-dnon_existing").run(); + assert!(result.success); + assert!(Path::new(&result.stdout).is_absolute()); + } +} + +#[test] +fn test_relpath_no_from_no_d() { + for test in TESTS.iter() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let to: &str = &convert_path(test.to); + at.mkdir_all(to); + + let result = scene.ucmd().arg(to).run(); + assert!(result.success); + #[cfg(not(windows))] + assert_eq!(result.stdout, format!("{}\n", to)); + // relax rules for windows test environment + #[cfg(windows)] + assert!(result.stdout.ends_with(&format!("{}\n", to))); + } +} + +#[test] +fn test_relpath_no_from_with_d() { + for test in TESTS.iter() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let to: &str = &convert_path(test.to); + let pwd = at.as_string(); + at.mkdir_all(to); + + // d is part of subpath -> expect relative path + let mut result = scene.ucmd().arg(to).arg(&format!("-d{}", pwd)).run(); + assert!(result.success); + // relax rules for windows test environment + #[cfg(not(windows))] + assert!(Path::new(&result.stdout).is_relative()); + + // d is not part of subpath -> expect absolut path + result = scene.ucmd().arg(to).arg("-dnon_existing").run(); + assert!(result.success); + assert!(Path::new(&result.stdout).is_absolute()); + } +} diff --git a/tests/by-util/test_rm.rs b/tests/by-util/test_rm.rs index 88e70946a..9a068887c 100644 --- a/tests/by-util/test_rm.rs +++ b/tests/by-util/test_rm.rs @@ -12,6 +12,17 @@ fn test_rm_one_file() { assert!(!at.file_exists(file)); } +#[test] +fn test_rm_failed() { + let (_at, mut ucmd) = at_and_ucmd!(); + let file = "test_rm_one_file"; // Doesn't exist + + ucmd.arg(file).fails().stderr_contains(&format!( + "cannot remove '{}': No such file or directory", + file + )); +} + #[test] fn test_rm_multiple_files() { let (at, mut ucmd) = at_and_ucmd!(); @@ -77,6 +88,24 @@ fn test_rm_force() { assert!(!at.file_exists(file_b)); } +#[test] +fn test_rm_force_multiple() { + let (at, mut ucmd) = at_and_ucmd!(); + let file_a = "test_rm_force_a"; + let file_b = "test_rm_force_b"; + + ucmd.arg("-f") + .arg("-f") + .arg("-f") + .arg(file_a) + .arg(file_b) + .succeeds() + .no_stderr(); + + assert!(!at.file_exists(file_a)); + assert!(!at.file_exists(file_b)); +} + #[test] fn test_rm_empty_directory() { let (at, mut ucmd) = at_and_ucmd!(); @@ -89,6 +118,39 @@ fn test_rm_empty_directory() { assert!(!at.dir_exists(dir)); } +#[test] +fn test_rm_empty_directory_verbose() { + let (at, mut ucmd) = at_and_ucmd!(); + let dir = "test_rm_empty_directory_verbose"; + + at.mkdir(dir); + + ucmd.arg("-d") + .arg("-v") + .arg(dir) + .succeeds() + .stdout_only(format!("removed directory '{}'\n", dir)); + + assert!(!at.dir_exists(dir)); +} + +#[test] +fn test_rm_non_empty_directory() { + let (at, mut ucmd) = at_and_ucmd!(); + let dir = "test_rm_non_empty_dir"; + let file_a = &format!("{}/test_rm_non_empty_file_a", dir); + + at.mkdir(dir); + at.touch(file_a); + + ucmd.arg("-d") + .arg(dir) + .fails() + .stderr_contains(&format!("cannot remove '{}': Directory not empty", dir)); + assert!(at.file_exists(file_a)); + assert!(at.dir_exists(dir)); +} + #[test] fn test_rm_recursive() { let (at, mut ucmd) = at_and_ucmd!(); @@ -108,22 +170,15 @@ fn test_rm_recursive() { } #[test] -fn test_rm_errors() { +fn test_rm_directory_without_flag() { let (at, mut ucmd) = at_and_ucmd!(); - let dir = "test_rm_errors_directory"; - let file_a = "test_rm_errors_directory/test_rm_errors_file_a"; - let file_b = "test_rm_errors_directory/test_rm_errors_file_b"; + let dir = "test_rm_directory_without_flag_dir"; at.mkdir(dir); - at.touch(file_a); - at.touch(file_b); - // $ rm test_rm_errors_directory - // rm: error: could not remove directory 'test_rm_errors_directory' (did you mean to pass '-r'?) - ucmd.arg(dir).fails().stderr_is( - "rm: error: could not remove directory 'test_rm_errors_directory' (did you mean \ - to pass '-r'?)\n", - ); + ucmd.arg(dir) + .fails() + .stderr_contains(&format!("cannot remove '{}': Is a directory", dir)); } #[test] @@ -143,10 +198,13 @@ fn test_rm_verbose() { } #[test] -fn test_rm_dir_symlink() { +#[cfg(not(windows))] +// on unix symlink_dir is a file +fn test_rm_symlink_dir() { let (at, mut ucmd) = at_and_ucmd!(); - let dir = "test_rm_dir_symlink_dir"; - let link = "test_rm_dir_symlink_link"; + + let dir = "test_rm_symlink_dir_directory"; + let link = "test_rm_symlink_dir_link"; at.mkdir(dir); at.symlink_dir(dir, link); @@ -154,6 +212,30 @@ fn test_rm_dir_symlink() { ucmd.arg(link).succeeds(); } +#[test] +#[cfg(windows)] +// on windows removing symlink_dir requires "-r" or "-d" +fn test_rm_symlink_dir() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let dir = "test_rm_symlink_dir_directory"; + let link = "test_rm_symlink_dir_link"; + + at.mkdir(dir); + at.symlink_dir(dir, link); + + scene + .ucmd() + .arg(link) + .fails() + .stderr_contains(&format!("cannot remove '{}': Is a directory", link)); + + assert!(at.dir_exists(link)); + + scene.ucmd().arg("-r").arg(link).succeeds(); +} + #[test] fn test_rm_invalid_symlink() { let (at, mut ucmd) = at_and_ucmd!(); @@ -178,3 +260,32 @@ fn test_rm_no_operand() { ucmd.fails() .stderr_is("rm: error: missing an argument\nrm: error: for help, try 'rm --help'\n"); } + +#[test] +fn test_rm_verbose_slash() { + let (at, mut ucmd) = at_and_ucmd!(); + let dir = "test_rm_verbose_slash_directory"; + let file_a = &format!("{}/test_rm_verbose_slash_file_a", dir); + + at.mkdir(dir); + at.touch(file_a); + + let file_a_normalized = &format!( + "{}{}test_rm_verbose_slash_file_a", + dir, + std::path::MAIN_SEPARATOR + ); + + ucmd.arg("-r") + .arg("-f") + .arg("-v") + .arg(&format!("{}///", dir)) + .succeeds() + .stdout_only(format!( + "removed '{}'\nremoved directory '{}'\n", + file_a_normalized, dir + )); + + assert!(!at.dir_exists(dir)); + assert!(!at.file_exists(file_a)); +} diff --git a/tests/by-util/test_rmdir.rs b/tests/by-util/test_rmdir.rs index 5f87b5af6..34531cf22 100644 --- a/tests/by-util/test_rmdir.rs +++ b/tests/by-util/test_rmdir.rs @@ -40,7 +40,7 @@ fn test_rmdir_nonempty_directory_no_parents() { ucmd.arg(dir).fails().stderr_is( "rmdir: error: failed to remove 'test_rmdir_nonempty_no_parents': Directory not \ - empty\n", + empty\n", ); assert!(at.dir_exists(dir)); @@ -60,9 +60,9 @@ fn test_rmdir_nonempty_directory_with_parents() { ucmd.arg("-p").arg(dir).fails().stderr_is( "rmdir: error: failed to remove 'test_rmdir_nonempty/with/parents': Directory not \ - empty\nrmdir: error: failed to remove 'test_rmdir_nonempty/with': Directory not \ - empty\nrmdir: error: failed to remove 'test_rmdir_nonempty': Directory not \ - empty\n", + empty\nrmdir: error: failed to remove 'test_rmdir_nonempty/with': Directory not \ + empty\nrmdir: error: failed to remove 'test_rmdir_nonempty': Directory not \ + empty\n", ); assert!(at.dir_exists(dir)); diff --git a/tests/by-util/test_seq.rs b/tests/by-util/test_seq.rs index 440a2bc98..a74938377 100644 --- a/tests/by-util/test_seq.rs +++ b/tests/by-util/test_seq.rs @@ -14,6 +14,10 @@ fn test_count_down() { .args(&["--", "5", "-1", "1"]) .run() .stdout_is("5\n4\n3\n2\n1\n"); + new_ucmd!() + .args(&["5", "-1", "1"]) + .run() + .stdout_is("5\n4\n3\n2\n1\n"); } #[test] @@ -31,3 +35,13 @@ fn test_equalize_widths() { .run() .stdout_is("05\n06\n07\n08\n09\n10\n"); } + +#[test] +fn test_seq_wrong_arg() { + new_ucmd!().args(&["-w", "5", "10", "33", "32"]).fails(); +} + +#[test] +fn test_zero_step() { + new_ucmd!().args(&["10", "0", "32"]).fails(); +} diff --git a/tests/by-util/test_shuf.rs b/tests/by-util/test_shuf.rs index 651491045..717971bd4 100644 --- a/tests/by-util/test_shuf.rs +++ b/tests/by-util/test_shuf.rs @@ -1 +1,215 @@ -// ToDO: add tests +use crate::common::util::*; + +#[test] +fn test_output_is_random_permutation() { + let input_seq = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + let input = input_seq + .iter() + .map(|x| x.to_string()) + .collect::>() + .join("\n"); + + let result = new_ucmd!() + .pipe_in(input.as_bytes()) + .succeeds() + .no_stderr() + .stdout + .clone(); + + let mut result_seq: Vec = result + .split("\n") + .filter(|x| !x.is_empty()) + .map(|x| x.parse().unwrap()) + .collect(); + result_seq.sort(); + assert_ne!(result, input, "Output is not randomised"); + assert_eq!(result_seq, input_seq, "Output is not a permutation"); +} + +#[test] +fn test_zero_termination() { + let input_seq = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + let result = new_ucmd!() + .arg("-z") + .arg("-i1-10") + .succeeds() + .no_stderr() + .stdout + .clone(); + + let mut result_seq: Vec = result + .split("\0") + .filter(|x| !x.is_empty()) + .map(|x| x.parse().unwrap()) + .collect(); + result_seq.sort(); + assert_eq!(result_seq, input_seq, "Output is not a permutation"); +} + +#[test] +fn test_echo() { + let input_seq = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + let result = new_ucmd!() + .arg("-e") + .args( + &input_seq + .iter() + .map(|x| x.to_string()) + .collect::>(), + ) + .succeeds() + .no_stderr() + .stdout + .clone(); + + let mut result_seq: Vec = result + .split("\n") + .filter(|x| !x.is_empty()) + .map(|x| x.parse().unwrap()) + .collect(); + result_seq.sort(); + assert_eq!(result_seq, input_seq, "Output is not a permutation"); +} + +#[test] +fn test_head_count() { + let repeat_limit = 5; + let input_seq = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + let input = input_seq + .iter() + .map(|x| x.to_string()) + .collect::>() + .join("\n"); + + let result = new_ucmd!() + .args(&["-n", &repeat_limit.to_string()]) + .pipe_in(input.as_bytes()) + .succeeds() + .no_stderr() + .stdout + .clone(); + + let mut result_seq: Vec = result + .split("\n") + .filter(|x| !x.is_empty()) + .map(|x| x.parse().unwrap()) + .collect(); + result_seq.sort(); + assert_eq!(result_seq.len(), repeat_limit, "Output is not limited"); + assert!( + result_seq.iter().all(|x| input_seq.contains(x)), + "Output includes element not from input: {}", + result + ) +} + +#[test] +fn test_repeat() { + let repeat_limit = 15000; + let input_seq = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + let input = input_seq + .iter() + .map(|x| x.to_string()) + .collect::>() + .join("\n"); + + let result = new_ucmd!() + .arg("-r") + .args(&["-n", &repeat_limit.to_string()]) + .pipe_in(input.as_bytes()) + .succeeds() + .no_stderr() + .stdout + .clone(); + + let result_seq: Vec = result + .split("\n") + .filter(|x| !x.is_empty()) + .map(|x| x.parse().unwrap()) + .collect(); + assert_eq!( + result_seq.len(), + repeat_limit, + "Output is not repeating forever" + ); + assert!( + result_seq.iter().all(|x| input_seq.contains(x)), + "Output includes element not from input: {:?}", + result_seq + .iter() + .filter(|x| !input_seq.contains(x)) + .collect::>() + ) +} + +#[test] +fn test_file_input() { + let expected_seq = vec![11, 12, 13, 14, 15, 16, 17, 18, 19, 20]; + + let result = new_ucmd!() + .arg("file_input.txt") + .succeeds() + .no_stderr() + .stdout + .clone(); + + let mut result_seq: Vec = result + .split("\n") + .filter(|x| !x.is_empty()) + .map(|x| x.parse().unwrap()) + .collect(); + result_seq.sort(); + assert_eq!(result_seq, expected_seq, "Output is not a permutation"); +} + +#[test] +fn test_shuf_echo_and_input_range_not_allowed() { + let result = new_ucmd!().args(&["-e", "0", "-i", "0-2"]).run(); + + assert!(!result.success); + assert!(result + .stderr + .contains("The argument '--input-range ' cannot be used with '--echo ...'")); +} + +#[test] +fn test_shuf_input_range_and_file_not_allowed() { + let result = new_ucmd!().args(&["-i", "0-9", "file"]).run(); + + assert!(!result.success); + assert!(result + .stderr + .contains("The argument '' cannot be used with '--input-range '")); +} + +#[test] +fn test_shuf_invalid_input_range_one() { + let result = new_ucmd!().args(&["-i", "0"]).run(); + + assert!(!result.success); + assert!(result.stderr.contains("invalid input range")); +} + +#[test] +fn test_shuf_invalid_input_range_two() { + let result = new_ucmd!().args(&["-i", "a-9"]).run(); + + assert!(!result.success); + assert!(result.stderr.contains("invalid input range: 'a'")); +} + +#[test] +fn test_shuf_invalid_input_range_three() { + let result = new_ucmd!().args(&["-i", "0-b"]).run(); + + assert!(!result.success); + assert!(result.stderr.contains("invalid input range: 'b'")); +} + +#[test] +fn test_shuf_invalid_input_line_count() { + let result = new_ucmd!().args(&["-n", "a"]).run(); + + assert!(!result.success); + assert!(result.stderr.contains("invalid line count: 'a'")); +} diff --git a/tests/by-util/test_sleep.rs b/tests/by-util/test_sleep.rs index 651491045..a17beddf6 100644 --- a/tests/by-util/test_sleep.rs +++ b/tests/by-util/test_sleep.rs @@ -1 +1,112 @@ -// ToDO: add tests +use crate::common::util::*; + +use std::time::{Duration, Instant}; + +#[test] +fn test_sleep_no_suffix() { + let millis_100 = Duration::from_millis(100); + let before_test = Instant::now(); + + new_ucmd!().args(&["0.1"]).succeeds().stdout_only(""); + + let duration = before_test.elapsed(); + assert!(duration >= millis_100); +} + +#[test] +fn test_sleep_s_suffix() { + let millis_100 = Duration::from_millis(100); + let before_test = Instant::now(); + + new_ucmd!().args(&["0.1s"]).succeeds().stdout_only(""); + + let duration = before_test.elapsed(); + assert!(duration >= millis_100); +} + +#[test] +fn test_sleep_m_suffix() { + let millis_600 = Duration::from_millis(600); + let before_test = Instant::now(); + + new_ucmd!().args(&["0.01m"]).succeeds().stdout_only(""); + + let duration = before_test.elapsed(); + assert!(duration >= millis_600); +} + +#[test] +fn test_sleep_h_suffix() { + let millis_360 = Duration::from_millis(360); + let before_test = Instant::now(); + + new_ucmd!().args(&["0.0001h"]).succeeds().stdout_only(""); + + let duration = before_test.elapsed(); + assert!(duration >= millis_360); +} + +#[test] +fn test_sleep_negative_duration() { + new_ucmd!().args(&["-1"]).fails(); + new_ucmd!().args(&["-1s"]).fails(); + new_ucmd!().args(&["-1m"]).fails(); + new_ucmd!().args(&["-1h"]).fails(); + new_ucmd!().args(&["-1d"]).fails(); +} + +#[test] +fn test_sleep_zero_duration() { + new_ucmd!().args(&["0"]).succeeds().stdout_only(""); + new_ucmd!().args(&["0s"]).succeeds().stdout_only(""); + new_ucmd!().args(&["0m"]).succeeds().stdout_only(""); + new_ucmd!().args(&["0h"]).succeeds().stdout_only(""); + new_ucmd!().args(&["0d"]).succeeds().stdout_only(""); +} + +#[test] +fn test_sleep_no_argument() { + new_ucmd!().fails(); +} + +#[test] +fn test_sleep_sum_duration_same_suffix() { + let millis_200 = Duration::from_millis(100 + 100); + let before_test = Instant::now(); + + new_ucmd!() + .args(&["0.1s", "0.1s"]) + .succeeds() + .stdout_only(""); + + let duration = before_test.elapsed(); + assert!(duration >= millis_200); +} + +#[test] +fn test_sleep_sum_duration_different_suffix() { + let millis_700 = Duration::from_millis(100 + 600); + let before_test = Instant::now(); + + new_ucmd!() + .args(&["0.1s", "0.01m"]) + .succeeds() + .stdout_only(""); + + let duration = before_test.elapsed(); + assert!(duration >= millis_700); +} + +#[test] +fn test_sleep_sum_duration_many() { + let millis_900 = Duration::from_millis(100 + 100 + 300 + 400); + let before_test = Instant::now(); + + new_ucmd!() + .args(&["0.1s", "0.1s", "0.3s", "0.4s"]) + .succeeds() + .stdout_only(""); + + let duration = before_test.elapsed(); + assert!(duration >= millis_900); +} diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index d3a4e6397..2bac71def 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -2,22 +2,43 @@ use crate::common::util::*; #[test] fn test_numeric_floats_and_ints() { - test_helper("numeric_floats_and_ints", "-n"); + for numeric_sort_param in vec!["-n", "--numeric-sort"] { + let input = "1.444\n8.013\n1\n-8\n1.04\n-1"; + new_ucmd!() + .arg(numeric_sort_param) + .pipe_in(input) + .succeeds() + .stdout_only("-8\n-1\n1\n1.04\n1.444\n8.013\n"); + } } #[test] fn test_numeric_floats() { - test_helper("numeric_floats", "-n"); + for numeric_sort_param in vec!["-n", "--numeric-sort"] { + let input = "1.444\n8.013\n1.58590\n-8.90880\n1.040000000\n-.05"; + new_ucmd!() + .arg(numeric_sort_param) + .pipe_in(input) + .succeeds() + .stdout_only("-8.90880\n-.05\n1.040000000\n1.444\n1.58590\n8.013\n"); + } } #[test] fn test_numeric_floats_with_nan() { - test_helper("numeric_floats_with_nan", "-n"); + for numeric_sort_param in vec!["-n", "--numeric-sort"] { + let input = "1.444\n1.0/0.0\n1.58590\n-8.90880\n1.040000000\n-.05"; + new_ucmd!() + .arg(numeric_sort_param) + .pipe_in(input) + .succeeds() + .stdout_only("-8.90880\n-.05\n1.0/0.0\n1.040000000\n1.444\n1.58590\n"); + } } #[test] fn test_numeric_unfixed_floats() { - test_helper("numeric_unfixed_floats", "-n"); + test_helper("numeric_fixed_floats", "-n"); } #[test] @@ -32,12 +53,26 @@ fn test_numeric_unsorted_ints() { #[test] fn test_human_block_sizes() { - test_helper("human_block_sizes", "-h"); + for human_numeric_sort_param in vec!["-h", "--human-numeric-sort"] { + let input = "8981K\n909991M\n-8T\n21G\n0.8M"; + new_ucmd!() + .arg(human_numeric_sort_param) + .pipe_in(input) + .succeeds() + .stdout_only("-8T\n0.8M\n8981K\n21G\n909991M\n"); + } } #[test] fn test_month_default() { - test_helper("month_default", "-M"); + for month_sort_param in vec!["-M", "--month-sort"] { + let input = "JAn\nMAY\n000may\nJun\nFeb"; + new_ucmd!() + .arg(month_sort_param) + .pipe_in(input) + .succeeds() + .stdout_only("000may\nJAn\nFeb\nMAY\nJun\n"); + } } #[test] @@ -47,12 +82,23 @@ fn test_month_stable() { #[test] fn test_default_unsorted_ints() { - test_helper("default_unsorted_ints", ""); + let input = "9\n1909888\n000\n1\n2"; + new_ucmd!() + .pipe_in(input) + .succeeds() + .stdout_only("000\n1\n1909888\n2\n9\n"); } #[test] fn test_numeric_unique_ints() { - test_helper("numeric_unsorted_ints_unique", "-nu"); + for numeric_unique_sort_param in vec!["-nu"] { + let input = "9\n9\n8\n1\n"; + new_ucmd!() + .arg(numeric_unique_sort_param) + .pipe_in(input) + .succeeds() + .stdout_only("1\n8\n9\n"); + } } #[test] @@ -118,6 +164,19 @@ fn test_merge_reversed() { .stdout_only_fixture("merge_ints_reversed.expected"); } +#[test] +fn test_pipe() { + // TODO: issue 1608 reports a panic when we attempt to read from stdin, + // which was closed by the other side of the pipe. This test does not + // protect against regressions in that case; we should add one at some + // point. + new_ucmd!() + .pipe_in("one\ntwo\nfour") + .succeeds() + .stdout_is("four\none\ntwo\n") + .stderr_is(""); +} + #[test] fn test_check() { new_ucmd!() diff --git a/tests/by-util/test_split.rs b/tests/by-util/test_split.rs index 5ed1a271d..521cbbe9a 100644 --- a/tests/by-util/test_split.rs +++ b/tests/by-util/test_split.rs @@ -4,6 +4,8 @@ extern crate regex; use self::rand::{thread_rng, Rng}; use self::regex::Regex; use crate::common::util::*; +#[cfg(not(windows))] +use std::env; use std::fs::{read_dir, File}; use std::io::Write; use std::path::Path; @@ -32,6 +34,7 @@ impl Glob { self.collect().len() } + /// Get all files in `self.directory` that match `self.regex` fn collect(&self) -> Vec { read_dir(Path::new(&self.directory.subdir)) .unwrap() @@ -49,6 +52,7 @@ impl Glob { .collect() } + /// Accumulate bytes of all files in `self.collect()` fn collate(&self) -> Vec { let mut files = self.collect(); files.sort(); @@ -60,11 +64,16 @@ impl Glob { } } +/// File handle that user can add random bytes (line-formatted or not) to struct RandomFile { inner: File, } impl RandomFile { + /// Size of each line that's being generated + const LINESIZE: usize = 32; + + /// `create()` file handle located at `at` / `name` fn new(at: &AtPath, name: &str) -> RandomFile { RandomFile { inner: File::create(&at.plus(name)).unwrap(), @@ -81,11 +90,11 @@ impl RandomFile { let _ = write!(self.inner, "{}", random_chars(n)); } + /// Add n lines each of size `RandomFile::LINESIZE` fn add_lines(&mut self, lines: usize) { - let line_size: usize = 32; let mut n = lines; while n > 0 { - let _ = writeln!(self.inner, "{}", random_chars(line_size)); + let _ = writeln!(self.inner, "{}", random_chars(RandomFile::LINESIZE)); n -= 1; } } @@ -103,12 +112,17 @@ fn test_split_default() { } #[test] -fn test_split_num_prefixed_chunks_by_bytes() { +fn test_split_numeric_prefixed_chunks_by_bytes() { let (at, mut ucmd) = at_and_ucmd!(); let name = "split_num_prefixed_chunks_by_bytes"; let glob = Glob::new(&at, ".", r"a\d\d$"); RandomFile::new(&at, name).add_bytes(10000); - ucmd.args(&["-d", "-b", "1000", name, "a"]).succeeds(); + ucmd.args(&[ + "-d", // --numeric-suffixes + "-b", // --bytes + "1000", name, "a", + ]) + .succeeds(); assert_eq!(glob.count(), 10); assert_eq!(glob.collate(), at.read(name).into_bytes()); } @@ -156,3 +170,64 @@ fn test_split_additional_suffix() { assert_eq!(glob.count(), 2); assert_eq!(glob.collate(), at.read(name).into_bytes()); } + +// note: the test_filter* tests below are unix-only +// windows support has been waived for now because of the difficulty of getting +// the `cmd` call right +// see https://github.com/rust-lang/rust/issues/29494 + +#[test] +#[cfg(unix)] +fn test_filter() { + // like `test_split_default()` but run a command before writing + let (at, mut ucmd) = at_and_ucmd!(); + let name = "filtered"; + let glob = Glob::new(&at, ".", r"x[[:alpha:]][[:alpha:]]$"); + let n_lines = 3; + RandomFile::new(&at, name).add_lines(n_lines); + + // change all characters to 'i' + ucmd.args(&["--filter=sed s/./i/g > $FILE", name]) + .succeeds(); + // assert all characters are 'i' / no character is not 'i' + // (assert that command succeded) + assert!( + glob.collate().iter().find(|&&c| { + // is not i + c != ('i' as u8) + // is not newline + && c != ('\n' as u8) + }) == None + ); +} + +#[test] +#[cfg(unix)] +fn test_filter_with_env_var_set() { + // This test will ensure that if $FILE env var was set before running --filter, it'll stay that + // way + // implemented like `test_split_default()` but run a command before writing + let (at, mut ucmd) = at_and_ucmd!(); + let name = "filtered"; + let glob = Glob::new(&at, ".", r"x[[:alpha:]][[:alpha:]]$"); + let n_lines = 3; + RandomFile::new(&at, name).add_lines(n_lines); + + let env_var_value = "somevalue"; + env::set_var("FILE", &env_var_value); + ucmd.args(&[format!("--filter={}", "cat > $FILE").as_str(), name]) + .succeeds(); + assert_eq!(glob.collate(), at.read(name).into_bytes()); + assert!(env::var("FILE").unwrap_or("var was unset".to_owned()) == env_var_value); +} + +#[test] +#[cfg(unix)] +fn test_filter_command_fails() { + let (at, mut ucmd) = at_and_ucmd!(); + let name = "filter-will-fail"; + RandomFile::new(&at, name).add_lines(4); + + ucmd.args(&["--filter=/a/path/that/totally/does/not/exist", name]) + .fails(); +} diff --git a/tests/by-util/test_sum.rs b/tests/by-util/test_sum.rs index 83cf689ac..d12455749 100644 --- a/tests/by-util/test_sum.rs +++ b/tests/by-util/test_sum.rs @@ -52,3 +52,23 @@ fn test_sysv_stdin() { .succeeds() .stdout_only_fixture("sysv_stdin.expected"); } + +#[test] +fn test_invalid_file() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.mkdir("a"); + + ucmd.arg("a") + .fails() + .stderr_is("sum: error: 'a' Is a directory"); +} + +#[test] +fn test_invalid_metadata() { + let (_, mut ucmd) = at_and_ucmd!(); + + ucmd.arg("b") + .fails() + .stderr_is("sum: error: 'b' No such file or directory"); +} diff --git a/tests/by-util/test_sync.rs b/tests/by-util/test_sync.rs index 651491045..ddd6969a3 100644 --- a/tests/by-util/test_sync.rs +++ b/tests/by-util/test_sync.rs @@ -1 +1,38 @@ -// ToDO: add tests +use crate::common::util::*; +extern crate tempfile; +use std::fs; +use tempfile::tempdir; + +#[test] +fn test_sync_default() { + let result = new_ucmd!().run(); + assert!(result.success); +} + +#[test] +fn test_sync_incorrect_arg() { + new_ucmd!().arg("--foo").fails(); +} + +#[test] +fn test_sync_fs() { + let temporary_directory = tempdir().unwrap(); + let temporary_path = fs::canonicalize(temporary_directory.path()).unwrap(); + let result = new_ucmd!().arg("--file-system").arg(&temporary_path).run(); + assert!(result.success); +} + +#[test] +fn test_sync_data() { + // Todo add a second arg + let temporary_directory = tempdir().unwrap(); + let temporary_path = fs::canonicalize(temporary_directory.path()).unwrap(); + let result = new_ucmd!().arg("--data").arg(&temporary_path).run(); + assert!(result.success); +} + +#[test] +fn test_sync_no_existing_files() { + let result = new_ucmd!().arg("--data").arg("do-no-exist").fails(); + assert!(result.stderr.contains("error: cannot stat")); +} diff --git a/tests/by-util/test_tac.rs b/tests/by-util/test_tac.rs index d46f9aec6..3733adbec 100644 --- a/tests/by-util/test_tac.rs +++ b/tests/by-util/test_tac.rs @@ -49,3 +49,21 @@ fn test_single_non_newline_separator_before() { .run() .stdout_is_fixture("delimited_primes_before.expected"); } + +#[test] +fn test_invalid_input() { + let (_, mut ucmd) = at_and_ucmd!(); + + ucmd.arg("b") + .run() + .stderr + .contains("tac: error: failed to open 'b' for reading"); + + let (at, mut ucmd) = at_and_ucmd!(); + + at.mkdir("a"); + ucmd.arg("a") + .run() + .stderr + .contains("tac: error: failed to read 'a'"); +} diff --git a/tests/by-util/test_tail.rs b/tests/by-util/test_tail.rs index 422ea1986..5edff4d55 100644 --- a/tests/by-util/test_tail.rs +++ b/tests/by-util/test_tail.rs @@ -95,8 +95,8 @@ fn test_follow_stdin() { .stdout_is_fixture("follow_stdin.expected"); } -// FixME: test PASSES for usual windows builds, but fails for coverage testing builds (likely related to the specific RUSTFLAGS '-Zpanic_abort_tests -Cpanic=abort') -#[cfg(not(windows))] +// FixME: test PASSES for usual windows builds, but fails for coverage testing builds (likely related to the specific RUSTFLAGS '-Zpanic_abort_tests -Cpanic=abort') This test also breaks tty settings under bash requiring a 'stty sane' or reset. +#[cfg(disable_until_fixed)] #[test] fn test_follow_with_pid() { use std::process::{Command, Stdio}; @@ -329,3 +329,17 @@ fn test_multiple_input_quiet_flag_overrides_verbose_flag_for_suppressing_headers .run() .stdout_is_fixture("foobar_multiple_quiet.expected"); } + +#[test] +fn test_negative_indexing() { + let positive_lines_index = new_ucmd!().arg("-n").arg("5").arg(FOOBAR_TXT).run(); + + let negative_lines_index = new_ucmd!().arg("-n").arg("-5").arg(FOOBAR_TXT).run(); + + let positive_bytes_index = new_ucmd!().arg("-c").arg("20").arg(FOOBAR_TXT).run(); + + let negative_bytes_index = new_ucmd!().arg("-c").arg("-20").arg(FOOBAR_TXT).run(); + + assert_eq!(positive_lines_index.stdout, negative_lines_index.stdout); + assert_eq!(positive_bytes_index.stdout, negative_bytes_index.stdout); +} diff --git a/tests/by-util/test_tee.rs b/tests/by-util/test_tee.rs index 651491045..f2587a11f 100644 --- a/tests/by-util/test_tee.rs +++ b/tests/by-util/test_tee.rs @@ -1 +1,104 @@ -// ToDO: add tests +use crate::common::util::*; + +// tests for basic tee functionality. +// inspired by: +// https://github.com/coreutils/coreutils/tests/misc/tee.sh + +#[test] +fn test_tee_processing_multiple_operands() { + // POSIX says: "Processing of at least 13 file operands shall be supported." + + let content = "tee_sample_content"; + for &n in [1, 2, 12, 13].iter() { + let files = (1..=n).map(|x| x.to_string()).collect::>(); + let (at, mut ucmd) = at_and_ucmd!(); + + ucmd.args(&files) + .pipe_in(content) + .succeeds() + .stdout_is(content); + + for file in files.iter() { + assert!(at.file_exists(file)); + assert_eq!(at.read(file), content); + } + } +} + +#[test] +fn test_tee_treat_minus_as_filename() { + // Ensure tee treats '-' as the name of a file, as mandated by POSIX. + + let (at, mut ucmd) = at_and_ucmd!(); + let content = "tee_sample_content"; + let file = "-"; + + ucmd.arg("-").pipe_in(content).succeeds().stdout_is(content); + + assert!(at.file_exists(file)); + assert_eq!(at.read(file), content); +} + +#[test] +fn test_tee_append() { + let (at, mut ucmd) = at_and_ucmd!(); + let content = "tee_sample_content"; + let file = "tee_out"; + + at.touch(file); + at.write(file, content); + assert_eq!(at.read(file), content); + + ucmd.arg("-a") + .arg(file) + .pipe_in(content) + .succeeds() + .stdout_is(content); + assert!(at.file_exists(file)); + assert_eq!(at.read(file), content.repeat(2)); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_tee_no_more_writeable_1() { + // equals to 'tee /dev/full out2 (); + let file_out = "tee_file_out"; + + ucmd.arg("/dev/full") + .arg(file_out) + .pipe_in(&content[..]) + .fails() + .stdout_contains(&content) + .stderr_contains(&"No space left on device"); + + assert_eq!(at.read(file_out), content); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_tee_no_more_writeable_2() { + // should be equals to 'tee out1 out2 >/dev/full (); + let file_out_a = "tee_file_out_a"; + let file_out_b = "tee_file_out_b"; + + let _result = ucmd + .arg(file_out_a) + .arg(file_out_b) + .pipe_in("/dev/full") + .succeeds(); // TODO: expected to succeed currently; change to fails() when required + + // TODO: comment in after https://github.com/uutils/coreutils/issues/1805 is fixed + // assert_eq!(at.read(file_out_a), content); + // assert_eq!(at.read(file_out_b), content); + // assert!(result.stderr.contains("No space left on device")); +} diff --git a/tests/by-util/test_timeout.rs b/tests/by-util/test_timeout.rs index 651491045..592516cca 100644 --- a/tests/by-util/test_timeout.rs +++ b/tests/by-util/test_timeout.rs @@ -1 +1,11 @@ -// ToDO: add tests +use crate::common::util::*; + +// FIXME: this depends on the system having true and false in PATH +// the best solution is probably to generate some test binaries that we can call for any +// utility that requires executing another program (kill, for instance) +#[test] +fn test_subcommand_retcode() { + new_ucmd!().arg("1").arg("true").succeeds(); + + new_ucmd!().arg("1").arg("false").run().status_code(1); +} diff --git a/tests/by-util/test_touch.rs b/tests/by-util/test_touch.rs index 84263376a..9921c16b5 100644 --- a/tests/by-util/test_touch.rs +++ b/tests/by-util/test_touch.rs @@ -195,6 +195,61 @@ fn test_touch_set_only_atime() { assert_eq!(atime.unix_seconds() - start_of_year.unix_seconds(), 45240); } +#[test] +fn test_touch_set_only_mtime_failed() { + let (_at, mut ucmd) = at_and_ucmd!(); + let file = "test_touch_set_only_mtime"; + + ucmd.args(&["-t", "2015010112342", "-m", file]).fails(); +} + +#[test] +fn test_touch_set_both_time_and_reference() { + let (at, mut ucmd) = at_and_ucmd!(); + let ref_file = "test_touch_reference"; + let file = "test_touch_set_both_time_and_reference"; + + let start_of_year = str_to_filetime("%Y%m%d%H%M", "201501010000"); + + at.touch(ref_file); + set_file_times(&at, ref_file, start_of_year, start_of_year); + assert!(at.file_exists(ref_file)); + + ucmd.args(&["-t", "2015010112342", "-r", ref_file, file]) + .fails(); +} + +#[test] +fn test_touch_set_both_date_and_reference() { + let (at, mut ucmd) = at_and_ucmd!(); + let ref_file = "test_touch_reference"; + let file = "test_touch_set_both_date_and_reference"; + + let start_of_year = str_to_filetime("%Y%m%d%H%M", "201501010000"); + + at.touch(ref_file); + set_file_times(&at, ref_file, start_of_year, start_of_year); + assert!(at.file_exists(ref_file)); + + ucmd.args(&["-d", "Thu Jan 01 12:34:00 2015", "-r", ref_file, file]) + .fails(); +} + +#[test] +fn test_touch_set_both_time_and_date() { + let (_at, mut ucmd) = at_and_ucmd!(); + let file = "test_touch_set_both_time_and_date"; + + ucmd.args(&[ + "-t", + "2015010112342", + "-d", + "Thu Jan 01 12:34:00 2015", + file, + ]) + .fails(); +} + #[test] fn test_touch_set_only_mtime() { let (at, mut ucmd) = at_and_ucmd!(); diff --git a/tests/by-util/test_tr.rs b/tests/by-util/test_tr.rs index b32d98d29..a1500bcf6 100644 --- a/tests/by-util/test_tr.rs +++ b/tests/by-util/test_tr.rs @@ -134,3 +134,66 @@ fn missing_required_second_arg_fails() { assert!(!result.success); assert!(result.stderr.contains("missing operand after")); } + +#[test] +fn test_interpret_backslash_escapes() { + new_ucmd!() + .args(&["abfnrtv", r"\a\b\f\n\r\t\v"]) + .pipe_in("abfnrtv") + .succeeds() + .stdout_is("\u{7}\u{8}\u{c}\n\r\t\u{b}"); +} + +#[test] +fn test_interpret_unrecognized_backslash_escape_as_character() { + new_ucmd!() + .args(&["qcz+=~-", r"\q\c\z\+\=\~\-"]) + .pipe_in("qcz+=~-") + .succeeds() + .stdout_is("qcz+=~-"); +} + +#[test] +fn test_interpret_single_octal_escape() { + new_ucmd!() + .args(&["X", r"\015"]) + .pipe_in("X") + .succeeds() + .stdout_is("\r"); +} + +#[test] +fn test_interpret_one_and_two_digit_octal_escape() { + new_ucmd!() + .args(&["XYZ", r"\0\11\77"]) + .pipe_in("XYZ") + .succeeds() + .stdout_is("\0\t?"); +} + +#[test] +fn test_octal_escape_is_at_most_three_digits() { + new_ucmd!() + .args(&["XY", r"\0156"]) + .pipe_in("XY") + .succeeds() + .stdout_is("\r6"); +} + +#[test] +fn test_non_octal_digit_ends_escape() { + new_ucmd!() + .args(&["rust", r"\08\11956"]) + .pipe_in("rust") + .succeeds() + .stdout_is("\08\t9"); +} + +#[test] +fn test_interpret_backslash_at_eol_literally() { + new_ucmd!() + .args(&["X", r"\"]) + .pipe_in("X") + .succeeds() + .stdout_is("\\"); +} diff --git a/tests/by-util/test_truncate.rs b/tests/by-util/test_truncate.rs index 7bb171386..ce7964d57 100644 --- a/tests/by-util/test_truncate.rs +++ b/tests/by-util/test_truncate.rs @@ -14,6 +14,35 @@ fn test_increase_file_size() { assert!(file.seek(SeekFrom::Current(0)).unwrap() == 5 * 1024); } +#[test] +fn test_increase_file_size_kb() { + let (at, mut ucmd) = at_and_ucmd!(); + let mut file = at.make_file(TFILE1); + ucmd.args(&["-s", "+5KB", TFILE1]).succeeds(); + + file.seek(SeekFrom::End(0)).unwrap(); + assert!(file.seek(SeekFrom::Current(0)).unwrap() == 5 * 1000); +} + +#[test] +fn test_reference() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + let mut file = at.make_file(TFILE2); + + scene.ucmd().arg("-s").arg("+5KB").arg(TFILE1).run(); + + scene + .ucmd() + .arg("--reference") + .arg(TFILE1) + .arg(TFILE2) + .run(); + + file.seek(SeekFrom::End(0)).unwrap(); + assert!(file.seek(SeekFrom::Current(0)).unwrap() == 5 * 1000); +} + #[test] fn test_decrease_file_size() { let (at, mut ucmd) = at_and_ucmd!(); @@ -23,3 +52,20 @@ fn test_decrease_file_size() { file.seek(SeekFrom::End(0)).unwrap(); assert!(file.seek(SeekFrom::Current(0)).unwrap() == 6); } + +#[test] +fn test_failed() { + new_ucmd!().fails(); +} + +#[test] +fn test_failed_2() { + let (_at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&[TFILE1]).fails(); +} + +#[test] +fn test_failed_incorrect_arg() { + let (_at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["-s", "+5A", TFILE1]).fails(); +} diff --git a/tests/by-util/test_tsort.rs b/tests/by-util/test_tsort.rs index c743868ec..159b80025 100644 --- a/tests/by-util/test_tsort.rs +++ b/tests/by-util/test_tsort.rs @@ -15,3 +15,36 @@ fn test_sort_self_loop() { .succeeds() .stdout_only("first\nsecond\n"); } + +#[test] +fn test_no_such_file() { + let result = new_ucmd!().arg("invalid_file_txt").run(); + + assert_eq!(true, result.stderr.contains("No such file or directory")); +} + +#[test] +fn test_version_flag() { + let version_short = new_ucmd!().arg("-V").run(); + let version_long = new_ucmd!().arg("--version").run(); + + assert_eq!(version_short.stdout, version_long.stdout); +} + +#[test] +fn test_help_flag() { + let help_short = new_ucmd!().arg("-h").run(); + let help_long = new_ucmd!().arg("--help").run(); + + assert_eq!(help_short.stdout, help_long.stdout); +} + +#[test] +fn test_multiple_arguments() { + let result = new_ucmd!() + .arg("call_graph.txt") + .arg("invalid_file.txt") + .run(); + + assert_eq!(true, result.stderr.contains("error: Found argument 'invalid_file.txt' which wasn't expected, or isn't valid in this context")) +} diff --git a/tests/by-util/test_tty.rs b/tests/by-util/test_tty.rs index 651491045..6bca54e03 100644 --- a/tests/by-util/test_tty.rs +++ b/tests/by-util/test_tty.rs @@ -1 +1,57 @@ -// ToDO: add tests +use crate::common::util::*; + +#[test] +#[cfg(not(windows))] +fn test_dev_null() { + new_ucmd!() + .pipe_in(" ( if $cond.stderr.len() > 0 { - panic!(format!("stderr: {}", $cond.stderr)) + panic!("stderr: {}", $cond.stderr_str()) } ); ); +/// Assertion helper macro for [`CmdResult`] types +/// +/// [`CmdResult`]: crate::tests::common::util::CmdResult #[macro_export] macro_rules! assert_empty_stdout( ($cond:expr) => ( if $cond.stdout.len() > 0 { - panic!(format!("stdout: {}", $cond.stdout)) + panic!("stdout: {}", $cond.stdout_str()) } ); ); +/// Assertion helper macro for [`CmdResult`] types +/// +/// [`CmdResult`]: crate::tests::common::util::CmdResult #[macro_export] macro_rules! assert_no_error( ($cond:expr) => ( assert!($cond.success); if $cond.stderr.len() > 0 { - panic!(format!("stderr: {}", $cond.stderr)) + panic!("stderr: {}", $cond.stderr_str()) } ); ); +/// Platform-independent helper for constructing a PathBuf from individual elements #[macro_export] macro_rules! path_concat { ($e:expr, ..$n:expr) => {{ @@ -47,6 +57,9 @@ macro_rules! path_concat { }}; } +/// Deduce the name of the test binary from the test filename. +/// +/// e.g.: `tests/by-util/test_cat.rs` -> `cat` #[macro_export] macro_rules! util_name { () => { @@ -54,6 +67,16 @@ macro_rules! util_name { }; } +/// Convenience macro for acquiring a [`UCommand`] builder. +/// +/// Returns the following: +/// - a [`UCommand`] builder for invoking the binary to be tested +/// +/// This macro is intended for quick, single-call tests. For more complex tests +/// that require multiple invocations of the tested binary, see [`TestScenario`] +/// +/// [`UCommand`]: crate::tests::common::util::UCommand +/// [`TestScenario]: crate::tests::common::util::TestScenario #[macro_export] macro_rules! new_ucmd { () => { @@ -61,6 +84,18 @@ macro_rules! new_ucmd { }; } +/// Convenience macro for acquiring a [`UCommand`] builder and a test path. +/// +/// Returns a tuple containing the following: +/// - an [`AsPath`] that points to a unique temporary test directory +/// - a [`UCommand`] builder for invoking the binary to be tested +/// +/// This macro is intended for quick, single-call tests. For more complex tests +/// that require multiple invocations of the tested binary, see [`TestScenario`] +/// +/// [`UCommand`]: crate::tests::common::util::UCommand +/// [`AsPath`]: crate::tests::common::util::AsPath +/// [`TestScenario]: crate::tests::common::util::TestScenario #[macro_export] macro_rules! at_and_ucmd { () => {{ diff --git a/tests/common/util.rs b/tests/common/util.rs index b581b8de9..708b8dbba 100644 --- a/tests/common/util.rs +++ b/tests/common/util.rs @@ -1,8 +1,10 @@ #![allow(dead_code)] -extern crate tempfile; -use self::tempfile::TempDir; +#[cfg(not(windows))] +use libc; use std::env; +#[cfg(not(windows))] +use std::ffi::CString; use std::ffi::OsStr; use std::fs::{self, File, OpenOptions}; use std::io::{Read, Result, Write}; @@ -16,6 +18,7 @@ use std::rc::Rc; use std::str::from_utf8; use std::thread::sleep; use std::time::Duration; +use tempfile::TempDir; #[cfg(windows)] static PROGNAME: &str = concat!(env!("CARGO_PKG_NAME"), ".exe"); @@ -26,11 +29,11 @@ static TESTS_DIR: &str = "tests"; static FIXTURES_DIR: &str = "fixtures"; static ALREADY_RUN: &str = " you have already run this UCommand, if you want to run \ - another command in the same test, use TestScenario::new instead of \ - testing();"; + another command in the same test, use TestScenario::new instead of \ + testing();"; static MULTIPLE_STDIN_MEANINGLESS: &str = "Ucommand is designed around a typical use case of: provide args and input stream -> spawn process -> block until completion -> return output streams. For verifying that a particular section of the input stream is what causes a particular behavior, use the Command type directly."; -/// Test if the program are running under CI +/// Test if the program is running under CI pub fn is_ci() -> bool { std::env::var("CI") .unwrap_or(String::from("false")) @@ -53,17 +56,10 @@ pub fn is_wsl() -> bool { false } -fn read_scenario_fixture>(tmpd: &Option>, file_rel_path: S) -> String { +/// Read a test scenario fixture, returning its bytes +fn read_scenario_fixture>(tmpd: &Option>, file_rel_path: S) -> Vec { let tmpdir_path = tmpd.as_ref().unwrap().as_ref().path(); - AtPath::new(tmpdir_path).read(file_rel_path.as_ref().to_str().unwrap()) -} - -pub fn repeat_str(s: &str, n: u32) -> String { - let mut repeated = String::new(); - for _ in 0..n { - repeated.push_str(s); - } - repeated + AtPath::new(tmpdir_path).read_bytes(file_rel_path.as_ref().to_str().unwrap()) } /// A command result is the outputs of a command (streams and status code) @@ -72,22 +68,98 @@ pub fn repeat_str(s: &str, n: u32) -> String { pub struct CmdResult { //tmpd is used for convenience functions for asserts against fixtures tmpd: Option>, + /// exit status for command (if there is one) + pub code: Option, + /// zero-exit from running the Command? + /// see [`success`] pub success: bool, + /// captured standard output after running the Command pub stdout: String, + /// captured standard error after running the Command pub stderr: String, } impl CmdResult { + /// Returns a reference to the program's standard output as a slice of bytes + pub fn stdout(&self) -> &[u8] { + &self.stdout.as_bytes() + } + + /// Returns the program's standard output as a string slice + pub fn stdout_str(&self) -> &str { + &self.stdout + } + + /// Returns the program's standard output as a string + /// consumes self + pub fn stdout_move_str(self) -> String { + self.stdout + } + + /// Returns the program's standard output as a vec of bytes + /// consumes self + pub fn stdout_move_bytes(self) -> Vec { + Vec::from(self.stdout) + } + + /// Returns a reference to the program's standard error as a slice of bytes + pub fn stderr(&self) -> &[u8] { + &self.stderr.as_bytes() + } + + /// Returns the program's standard error as a string slice + pub fn stderr_str(&self) -> &str { + &self.stderr + } + + /// Returns the program's standard error as a string + /// consumes self + pub fn stderr_move_str(self) -> String { + self.stderr + } + + /// Returns the program's standard error as a vec of bytes + /// consumes self + pub fn stderr_move_bytes(self) -> Vec { + Vec::from(self.stderr) + } + + /// Returns the program's exit code + /// Panics if not run + pub fn code(&self) -> i32 { + self.code.expect("Program must be run first") + } + + /// Returns the program's TempDir + /// Panics if not present + pub fn tmpd(&self) -> Rc { + match &self.tmpd { + Some(ptr) => ptr.clone(), + None => panic!("Command not associated with a TempDir"), + } + } + + /// Returns whether the program succeeded + pub fn succeeded(&self) -> bool { + self.success + } + /// asserts that the command resulted in a success (zero) status code - pub fn success(&self) -> Box<&CmdResult> { + pub fn success(&self) -> &CmdResult { assert!(self.success); - Box::new(self) + self } /// asserts that the command resulted in a failure (non-zero) status code - pub fn failure(&self) -> Box<&CmdResult> { + pub fn failure(&self) -> &CmdResult { assert!(!self.success); - Box::new(self) + self + } + + /// asserts that the command's exit code is the same as the given one + pub fn status_code(&self, code: i32) -> &CmdResult { + assert!(self.code == Some(code)); + self } /// asserts that the command resulted in empty (zero-length) stderr stream output @@ -95,9 +167,9 @@ impl CmdResult { /// but you might find yourself using this function if /// 1. you can not know exactly what stdout will be /// or 2. you know that stdout will also be empty - pub fn no_stderr(&self) -> Box<&CmdResult> { - assert_eq!(self.stderr, ""); - Box::new(self) + pub fn no_stderr(&self) -> &CmdResult { + assert!(self.stderr.is_empty()); + self } /// asserts that the command resulted in empty (zero-length) stderr stream output @@ -106,74 +178,102 @@ impl CmdResult { /// but you might find yourself using this function if /// 1. you can not know exactly what stderr will be /// or 2. you know that stderr will also be empty - pub fn no_stdout(&self) -> Box<&CmdResult> { - assert_eq!(self.stdout, ""); - Box::new(self) + pub fn no_stdout(&self) -> &CmdResult { + assert!(self.stdout.is_empty()); + self } /// asserts that the command resulted in stdout stream output that equals the /// passed in value, trailing whitespace are kept to force strict comparison (#1235) /// stdout_only is a better choice unless stderr may or will be non-empty - pub fn stdout_is>(&self, msg: T) -> Box<&CmdResult> { + pub fn stdout_is>(&self, msg: T) -> &CmdResult { assert_eq!(self.stdout, String::from(msg.as_ref())); - Box::new(self) + self + } + + /// asserts that the command resulted in stdout stream output, + /// whose bytes equal those of the passed in slice + pub fn stdout_is_bytes>(&self, msg: T) -> &CmdResult { + assert_eq!(self.stdout.as_bytes(), msg.as_ref()); + self } /// like stdout_is(...), but expects the contents of the file at the provided relative path - pub fn stdout_is_fixture>(&self, file_rel_path: T) -> Box<&CmdResult> { + pub fn stdout_is_fixture>(&self, file_rel_path: T) -> &CmdResult { let contents = read_scenario_fixture(&self.tmpd, file_rel_path); - self.stdout_is(contents) + self.stdout_is_bytes(contents) } /// asserts that the command resulted in stderr stream output that equals the /// passed in value, when both are trimmed of trailing whitespace /// stderr_only is a better choice unless stdout may or will be non-empty - pub fn stderr_is>(&self, msg: T) -> Box<&CmdResult> { + pub fn stderr_is>(&self, msg: T) -> &CmdResult { assert_eq!( self.stderr.trim_end(), String::from(msg.as_ref()).trim_end() ); - Box::new(self) + self } - /// like stderr_is(...), but expects the contents of the file at the provided relative path - pub fn stderr_is_fixture>(&self, file_rel_path: T) -> Box<&CmdResult> { - let contents = read_scenario_fixture(&self.tmpd, file_rel_path); - self.stderr_is(contents) + /// asserts that the command resulted in stderr stream output, + /// whose bytes equal those of the passed in slice + pub fn stderr_is_bytes>(&self, msg: T) -> &CmdResult { + assert_eq!(self.stderr.as_bytes(), msg.as_ref()); + self } /// asserts that /// 1. the command resulted in stdout stream output that equals the /// passed in value, when both are trimmed of trailing whitespace /// and 2. the command resulted in empty (zero-length) stderr stream output - pub fn stdout_only>(&self, msg: T) -> Box<&CmdResult> { + pub fn stdout_only>(&self, msg: T) -> &CmdResult { self.no_stderr().stdout_is(msg) } + /// asserts that + /// 1. the command resulted in a stdout stream whose bytes + /// equal those of the passed in value + /// 2. the command resulted in an empty stderr stream + pub fn stdout_only_bytes>(&self, msg: T) -> &CmdResult { + self.no_stderr().stdout_is_bytes(msg) + } + /// like stdout_only(...), but expects the contents of the file at the provided relative path - pub fn stdout_only_fixture>(&self, file_rel_path: T) -> Box<&CmdResult> { + pub fn stdout_only_fixture>(&self, file_rel_path: T) -> &CmdResult { let contents = read_scenario_fixture(&self.tmpd, file_rel_path); - self.stdout_only(contents) + self.stdout_only_bytes(contents) } /// asserts that /// 1. the command resulted in stderr stream output that equals the /// passed in value, when both are trimmed of trailing whitespace /// and 2. the command resulted in empty (zero-length) stdout stream output - pub fn stderr_only>(&self, msg: T) -> Box<&CmdResult> { + pub fn stderr_only>(&self, msg: T) -> &CmdResult { self.no_stdout().stderr_is(msg) } - /// like stderr_only(...), but expects the contents of the file at the provided relative path - pub fn stderr_only_fixture>(&self, file_rel_path: T) -> Box<&CmdResult> { - let contents = read_scenario_fixture(&self.tmpd, file_rel_path); - self.stderr_only(contents) + /// asserts that + /// 1. the command resulted in a stderr stream whose bytes equal the ones + /// of the passed value + /// 2. the command resulted in an empty stdout stream + pub fn stderr_only_bytes>(&self, msg: T) -> &CmdResult { + self.no_stderr().stderr_is_bytes(msg) } - pub fn fails_silently(&self) -> Box<&CmdResult> { + pub fn fails_silently(&self) -> &CmdResult { assert!(!self.success); - assert_eq!(self.stderr, ""); - Box::new(self) + assert!(self.stderr.is_empty()); + self + } + + pub fn stdout_contains>(&self, cmp: T) -> &CmdResult { + assert!(self.stdout_str().contains(cmp.as_ref())); + self + } + + pub fn stderr_contains>(&self, cmp: &T) -> &CmdResult { + assert!(self.stderr_str().contains(cmp.as_ref())); + self } } @@ -259,13 +359,29 @@ impl AtPath { pub fn read(&self, name: &str) -> String { let mut f = self.open(name); let mut contents = String::new(); - let _ = f.read_to_string(&mut contents); + f.read_to_string(&mut contents) + .unwrap_or_else(|e| panic!("Couldn't read {}: {}", name, e)); + contents + } + + pub fn read_bytes(&self, name: &str) -> Vec { + let mut f = self.open(name); + let mut contents = Vec::new(); + f.read_to_end(&mut contents) + .unwrap_or_else(|e| panic!("Couldn't read {}: {}", name, e)); contents } pub fn write(&self, name: &str, contents: &str) { - let mut f = self.open(name); - let _ = f.write(contents.as_bytes()); + log_info("open(write)", self.plus_as_string(name)); + std::fs::write(self.plus(name), contents) + .unwrap_or_else(|e| panic!("Couldn't write {}: {}", name, e)); + } + + pub fn write_bytes(&self, name: &str, contents: &[u8]) { + log_info("open(write)", self.plus_as_string(name)); + std::fs::write(self.plus(name), contents) + .unwrap_or_else(|e| panic!("Couldn't write {}: {}", name, e)); } pub fn append(&self, name: &str, contents: &str) { @@ -275,7 +391,19 @@ impl AtPath { .append(true) .open(self.plus(name)) .unwrap(); - let _ = f.write(contents.as_bytes()); + f.write(contents.as_bytes()) + .unwrap_or_else(|e| panic!("Couldn't write {}: {}", name, e)); + } + + pub fn append_bytes(&self, name: &str, contents: &[u8]) { + log_info("open(append)", self.plus_as_string(name)); + let mut f = OpenOptions::new() + .write(true) + .append(true) + .open(self.plus(name)) + .unwrap(); + f.write_all(contents) + .unwrap_or_else(|e| panic!("Couldn't append to {}: {}", name, e)); } pub fn mkdir(&self, dir: &str) { @@ -299,6 +427,29 @@ impl AtPath { File::create(&self.plus(file)).unwrap(); } + #[cfg(not(windows))] + pub fn mkfifo(&self, fifo: &str) { + let full_path = self.plus_as_string(fifo); + log_info("mkfifo", &full_path); + unsafe { + let fifo_name: CString = CString::new(full_path).expect("CString creation failed."); + libc::mkfifo(fifo_name.as_ptr(), libc::S_IWUSR | libc::S_IRUSR); + } + } + + #[cfg(not(windows))] + pub fn is_fifo(&self, fifo: &str) -> bool { + unsafe { + let name = CString::new(self.plus_as_string(fifo)).unwrap(); + let mut stat: libc::stat = std::mem::zeroed(); + if libc::stat(name.as_ptr(), &mut stat) >= 0 { + libc::S_IFIFO & stat.st_mode != 0 + } else { + false + } + } + } + pub fn symlink_file(&self, src: &str, dst: &str) { log_info( "symlink", @@ -359,22 +510,6 @@ impl AtPath { } } - pub fn cleanup(&self, path: &'static str) { - let p = &self.plus(path); - if let Ok(m) = fs::metadata(p) { - if m.is_file() { - fs::remove_file(&p).unwrap(); - } else { - fs::remove_dir(&p).unwrap(); - } - } - } - - pub fn root_dir(&self) -> String { - log_info("current_directory", ""); - self.subdir.to_str().unwrap().to_owned() - } - pub fn root_dir_resolved(&self) -> String { log_info("current_directory_resolved", ""); let s = self @@ -403,8 +538,10 @@ impl AtPath { /// An environment for running a single uutils test case, serves three functions: /// 1. centralizes logic for locating the uutils binary and calling the utility -/// 2. provides a temporary directory for the test case +/// 2. provides a unique temporary directory for the test case /// 3. copies over fixtures for the utility to the temporary directory +/// +/// Fixtures can be found under `tests/fixtures/$util_name/` pub struct TestScenario { bin_path: PathBuf, util_name: String, @@ -439,16 +576,28 @@ impl TestScenario { ts } + /// Returns builder for invoking the target uutils binary. Paths given are + /// treated relative to the environment's unique temporary test directory. pub fn ucmd(&self) -> UCommand { let mut cmd = self.cmd(&self.bin_path); cmd.arg(&self.util_name); cmd } + /// Returns builder for invoking any system command. Paths given are treated + /// relative to the environment's unique temporary test directory. pub fn cmd>(&self, bin: S) -> UCommand { UCommand::new_from_tmp(bin, self.tmpd.clone(), true) } + /// Returns builder for invoking any uutils command. Paths given are treated + /// relative to the environment's unique temporary test directory. + pub fn ccmd>(&self, bin: S) -> UCommand { + let mut cmd = self.cmd(&self.bin_path); + cmd.arg(bin); + cmd + } + // different names are used rather than an argument // because the need to keep the environment is exceedingly rare. pub fn ucmd_keepenv(&self) -> UCommand { @@ -514,25 +663,23 @@ impl UCommand { ucmd } - pub fn arg>(&mut self, arg: S) -> Box<&mut UCommand> { + /// Add a parameter to the invocation. Path arguments are treated relative + /// to the test environment directory. + pub fn arg>(&mut self, arg: S) -> &mut UCommand { if self.has_run { - panic!(ALREADY_RUN); + panic!("{}", ALREADY_RUN); } self.comm_string.push_str(" "); self.comm_string.push_str(arg.as_ref().to_str().unwrap()); self.raw.arg(arg.as_ref()); - Box::new(self) + self } - /// like arg(...), but uses the contents of the file at the provided relative path as the argument - pub fn arg_fixture>(&mut self, file_rel_path: S) -> Box<&mut UCommand> { - let contents = read_scenario_fixture(&self.tmpd, file_rel_path); - self.arg(contents) - } - - pub fn args>(&mut self, args: &[S]) -> Box<&mut UCommand> { + /// Add multiple parameters to the invocation. Path arguments are treated relative + /// to the test environment directory. + pub fn args>(&mut self, args: &[S]) -> &mut UCommand { if self.has_run { - panic!(MULTIPLE_STDIN_MEANINGLESS); + panic!("{}", MULTIPLE_STDIN_MEANINGLESS); } for s in args { self.comm_string.push_str(" "); @@ -540,41 +687,41 @@ impl UCommand { } self.raw.args(args.as_ref()); - Box::new(self) + self } /// provides stdinput to feed in to the command when spawned - pub fn pipe_in>>(&mut self, input: T) -> Box<&mut UCommand> { + pub fn pipe_in>>(&mut self, input: T) -> &mut UCommand { if self.stdin.is_some() { - panic!(MULTIPLE_STDIN_MEANINGLESS); + panic!("{}", MULTIPLE_STDIN_MEANINGLESS); } self.stdin = Some(input.into()); - Box::new(self) + self } /// like pipe_in(...), but uses the contents of the file at the provided relative path as the piped in data - pub fn pipe_in_fixture>(&mut self, file_rel_path: S) -> Box<&mut UCommand> { + pub fn pipe_in_fixture>(&mut self, file_rel_path: S) -> &mut UCommand { let contents = read_scenario_fixture(&self.tmpd, file_rel_path); self.pipe_in(contents) } - pub fn env(&mut self, key: K, val: V) -> Box<&mut UCommand> + pub fn env(&mut self, key: K, val: V) -> &mut UCommand where K: AsRef, V: AsRef, { if self.has_run { - panic!(ALREADY_RUN); + panic!("{}", ALREADY_RUN); } self.raw.env(key, val); - Box::new(self) + self } /// Spawns the command, feeds the stdin if any, and returns the /// child process immediately. pub fn run_no_wait(&mut self) -> Child { if self.has_run { - panic!(ALREADY_RUN); + panic!("{}", ALREADY_RUN); } self.has_run = true; log_info("run", &self.comm_string); @@ -606,6 +753,7 @@ impl UCommand { CmdResult { tmpd: self.tmpd.clone(), + code: prog.status.code(), success: prog.status.success(), stdout: from_utf8(&prog.stdout).unwrap().to_string(), stderr: from_utf8(&prog.stderr).unwrap().to_string(), @@ -645,7 +793,7 @@ pub fn read_size(child: &mut Child, size: usize) -> String { .stdout .as_mut() .unwrap() - .read(output.as_mut_slice()) + .read_exact(output.as_mut_slice()) .unwrap(); String::from_utf8(output).unwrap() } diff --git a/tests/fixtures/cksum/chars.txt b/tests/fixtures/cksum/chars.txt new file mode 100644 index 000000000..26eec03fe --- /dev/null +++ b/tests/fixtures/cksum/chars.txt @@ -0,0 +1 @@ +123456789:;<=>?@ \ No newline at end of file diff --git a/tests/fixtures/cksum/larger_than_2056_bytes.txt b/tests/fixtures/cksum/larger_than_2056_bytes.txt new file mode 100644 index 000000000..668de449a --- /dev/null +++ b/tests/fixtures/cksum/larger_than_2056_bytes.txt @@ -0,0 +1 @@ +\000\001\002\003\004\005\006\007\010\011\012\013\014\015\016\017\020\021\022\023\024\025\026\027\030\031\032\033\034\035\036\037\040\041\042\043\044\045\046\047\050\051\052\053\054\055\056\057\060\061\062\063\064\065\066\067\070\071\072\073\074\075\076\077\100\101\102\103\104\105\106\107\110\111\112\113\114\115\116\117\120\121\122\123\124\125\126\127\130\131\132\133\134\135\136\137\140\141\142\143\144\145\146\147\150\151\152\153\154\155\156\157\160\161\162\163\164\165\166\167\170\171\172\173\174\175\176\177\200\201\202\203\204\205\206\207\210\211\212\213\214\215\216\217\220\221\222\223\224\225\226\227\230\231\232\233\234\235\236\237\240\241\242\243\244\245\246\247\250\251\252\253\254\255\256\257\260\261\262\263\264\265\266\267\270\271\272\273\274\275\276\277\300\301\302\303\304\305\306\307\310\311\312\313\314\315\316\317\320\321\322\323\324\325\326\327\330\331\332\333\334\335\336\337\340\341\342\343\344\345\346\347\350\351\352\353\354\355\356\357\360\361\362\363\364\365\366\367\370\371\372\373\374\375\376\377\400\401\402\403\404\405\406\407\410\411\412\413\414\415\416\417\420\421\422\423\424\425\426\427\430\431\432\433\434\435\436\437\440\441\442\443\444\445\446\447\450\451\452\453\454\455\456\457\460\461\462\463\464\465\466\467\470\471\472\473\474\475\476\477\500\501\502\503\504\505\506\507\510\511\512\513\514\515\516\517\520\521\522\523\524\525\526\527\530\531\532\533\534\535\536\537\540\541\542\543\544\545\546\547\550\551\552\553\554\555\556\557\560\561\562\563\564\565\566\567\570\571\572\573\574\575\576\577\600\601\602\603\604\605\606\607\610\611\612\613\614\615\616\617\620\621\622\623\624\625\626\627\630\631\632\633\634\635\636\637\640\641\642\643\644\645\646\647\650\651\652\653\654\655\656\657\660\661\662\663\664\665\666\667\670\671\672\673\674\675\676\677\700\701\702\703\704\705\706\707\710\711\712\713\714\715\716\717\720\721\722\723\724\725\726\727\730\731\732\733\734\735\736\737\740\741\742\743\744\745\746\747\750\751\752\753\754\755\756\757\760\761\762\763\764\765\766\767\770\771\772\773\774\775\776\777\1000\1001 \ No newline at end of file diff --git a/tests/fixtures/cp/dir_with_mount/copy_me.txt b/tests/fixtures/cp/dir_with_mount/copy_me.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fixtures/cp/dir_with_mount/copy_me/copy_me.txt b/tests/fixtures/cp/dir_with_mount/copy_me/copy_me.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fixtures/csplit/numbers50.txt b/tests/fixtures/csplit/numbers50.txt new file mode 100644 index 000000000..96cc55885 --- /dev/null +++ b/tests/fixtures/csplit/numbers50.txt @@ -0,0 +1,50 @@ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 diff --git a/tests/fixtures/du/empty.txt b/tests/fixtures/du/empty.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fixtures/fold/lorem_ipsum_new_line.txt b/tests/fixtures/fold/lorem_ipsum_new_line.txt new file mode 100644 index 000000000..a844b83d7 --- /dev/null +++ b/tests/fixtures/fold/lorem_ipsum_new_line.txt @@ -0,0 +1,2 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc interdum suscipit sem vel ornare. Proin euismod, justo sed mollis dictum, eros urna ultricies augue, eu pharetra mi ex id ante. Duis convallis porttitor aliquam. Nunc vitae tincidunt ex. Suspendisse iaculis ligula ac diam consectetur lacinia. Donec vel velit dui. Etiam fringilla, dolor quis tempor vehicula, lacus turpis bibendum velit, et pellentesque elit odio a magna. +Cras vulputate tortor non libero vehicula euismod. Aliquam tincidunt nisl eget enim cursus, viverra sagittis magna commodo. Cras rhoncus egestas leo nec blandit. Suspendisse potenti. Etiam ullamcorper leo vel lacus vestibulum, cursus semper eros efficitur. In hac habitasse platea dictumst. Phasellus scelerisque vehicula fringilla. diff --git a/tests/fixtures/fold/lorem_ipsum_new_line_80_column.expected b/tests/fixtures/fold/lorem_ipsum_new_line_80_column.expected new file mode 100644 index 000000000..16ea4bab6 --- /dev/null +++ b/tests/fixtures/fold/lorem_ipsum_new_line_80_column.expected @@ -0,0 +1,11 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc interdum suscipit +sem vel ornare. Proin euismod, justo sed mollis dictum, eros urna ultricies augu +e, eu pharetra mi ex id ante. Duis convallis porttitor aliquam. Nunc vitae tinci +dunt ex. Suspendisse iaculis ligula ac diam consectetur lacinia. Donec vel velit + dui. Etiam fringilla, dolor quis tempor vehicula, lacus turpis bibendum velit, +et pellentesque elit odio a magna. +Cras vulputate tortor non libero vehicula euismod. Aliquam tincidunt nisl eget e +nim cursus, viverra sagittis magna commodo. Cras rhoncus egestas leo nec blandit +. Suspendisse potenti. Etiam ullamcorper leo vel lacus vestibulum, cursus semper + eros efficitur. In hac habitasse platea dictumst. Phasellus scelerisque vehicul +a fringilla. diff --git a/tests/fixtures/fold/tab_stops.input b/tests/fixtures/fold/tab_stops.input new file mode 100644 index 000000000..a96a378ea --- /dev/null +++ b/tests/fixtures/fold/tab_stops.input @@ -0,0 +1,11 @@ +1 +12 +123 +1234 +12345 +123456 +1234567 +12345678 +123456781 +12345678 2 +12345678 2 4 diff --git a/tests/fixtures/fold/tab_stops_w16.expected b/tests/fixtures/fold/tab_stops_w16.expected new file mode 100644 index 000000000..8122ac16d --- /dev/null +++ b/tests/fixtures/fold/tab_stops_w16.expected @@ -0,0 +1,13 @@ +1 +12 +123 +1234 +12345 +123456 +1234567 +12345678 +123456781 +12345678 +2 +12345678 +2 4 diff --git a/tests/fixtures/fold/tab_stops_w8.expected b/tests/fixtures/fold/tab_stops_w8.expected new file mode 100644 index 000000000..3173a4a22 --- /dev/null +++ b/tests/fixtures/fold/tab_stops_w8.expected @@ -0,0 +1,18 @@ +1 +12 +123 +1234 +12345 +123456 +1234567 +12345678 + +12345678 +1 +12345678 + +2 +12345678 + +2 +4 diff --git a/tests/fixtures/hashsum/b2sum.expected b/tests/fixtures/hashsum/b2sum.expected new file mode 100644 index 000000000..a0dae0db4 --- /dev/null +++ b/tests/fixtures/hashsum/b2sum.expected @@ -0,0 +1 @@ +7355dd5276c21cfe0c593b5063b96af3f96a454b33216f58314f44c3ade92e9cd6cec4210a0836246780e9baf927cc50b9a3d7073e8f9bd12780fddbcb930c6d \ No newline at end of file diff --git a/tests/fixtures/head/lorem_ipsum_backwards_file.expected b/tests/fixtures/head/lorem_ipsum_backwards_file.expected new file mode 100644 index 000000000..fcf432187 --- /dev/null +++ b/tests/fixtures/head/lorem_ipsum_backwards_file.expected @@ -0,0 +1,24 @@ +Lorem ipsum dolor sit amet, +consectetur adipiscing elit. +Nunc interdum suscipit sem vel ornare. +Proin euismod, +justo sed mollis dictum, +eros urna ultricies augue, +eu pharetra mi ex id ante. +Duis convallis porttitor aliquam. +Nunc vitae tincidunt ex. +Suspendisse iaculis ligula ac diam consectetur lacinia. +Donec vel velit dui. +Etiam fringilla, +dolor quis tempor vehicula, +lacus turpis bibendum velit, +et pellentesque elit odio a magna. +Cras vulputate tortor non libero vehicula euismod. +Aliquam tincidunt nisl eget enim cursus, +viverra sagittis magna commodo. +Cras rhoncus egestas leo nec blandit. +Suspendisse potenti. +Etiam ullamcorper leo vel lacus vestibulum, +cursus semper eros efficitur. +In hac habitasse platea dictumst. +Phasellus scelerisque vehicula f \ No newline at end of file diff --git a/tests/fixtures/head/sequence b/tests/fixtures/head/sequence new file mode 100644 index 000000000..190423f88 --- /dev/null +++ b/tests/fixtures/head/sequence @@ -0,0 +1,100 @@ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 diff --git a/tests/fixtures/head/sequence.expected b/tests/fixtures/head/sequence.expected new file mode 100644 index 000000000..17d2a1390 --- /dev/null +++ b/tests/fixtures/head/sequence.expected @@ -0,0 +1,90 @@ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 diff --git a/tests/fixtures/head/zero_terminated.expected b/tests/fixtures/head/zero_terminated.expected new file mode 100644 index 000000000..3c4cc058c Binary files /dev/null and b/tests/fixtures/head/zero_terminated.expected differ diff --git a/tests/fixtures/head/zero_terminated.txt b/tests/fixtures/head/zero_terminated.txt new file mode 100644 index 000000000..6c7968122 Binary files /dev/null and b/tests/fixtures/head/zero_terminated.txt differ diff --git a/tests/fixtures/numfmt/df_expected.txt b/tests/fixtures/numfmt/df_expected.txt new file mode 100644 index 000000000..ea8c3d79f --- /dev/null +++ b/tests/fixtures/numfmt/df_expected.txt @@ -0,0 +1,8 @@ +Filesystem 1B-blocks Used Available Use% Mounted on +udev 8.2G 0 8.2G 0% /dev +tmpfs 1.7G 2.1M 1.7G 1% /run +/dev/nvme0n1p2 1.1T 433G 523G 46% / +tmpfs 8.3G 145M 8.1G 2% /dev/shm +tmpfs 5.3M 4.1K 5.3M 1% /run/lock +tmpfs 8.3G 0 8.3G 0% /sys/fs/cgroup +/dev/nvme0n1p1 536M 8.2M 528M 2% /boot/efi diff --git a/tests/fixtures/numfmt/df_input.txt b/tests/fixtures/numfmt/df_input.txt new file mode 100644 index 000000000..4c094d54f --- /dev/null +++ b/tests/fixtures/numfmt/df_input.txt @@ -0,0 +1,8 @@ +Filesystem 1B-blocks Used Available Use% Mounted on +udev 8192688128 0 8192688128 0% /dev +tmpfs 1643331584 2015232 1641316352 1% /run +/dev/nvme0n1p2 1006530654208 432716689408 522613624832 46% / +tmpfs 8216649728 144437248 8072212480 2% /dev/shm +tmpfs 5242880 4096 5238784 1% /run/lock +tmpfs 8216649728 0 8216649728 0% /sys/fs/cgroup +/dev/nvme0n1p1 535805952 8175616 527630336 2% /boot/efi diff --git a/tests/fixtures/numfmt/gnutest_iec-i_result.txt b/tests/fixtures/numfmt/gnutest_iec-i_result.txt new file mode 100644 index 000000000..b4c286fc6 --- /dev/null +++ b/tests/fixtures/numfmt/gnutest_iec-i_result.txt @@ -0,0 +1,49 @@ +-1.1Ki +-1.1Ki +-1.0Ki +-1.0Ki +-1023 +0 +1 +1023 +1.0Ki +1.1Ki +1.1Ki +1.2Ki +1.5Ki +1.6Ki +1.9Ki +2.0Ki +2.0Ki +2.0Ki +2.0Ki +2.1Ki +10Ki +10Ki +10Ki +100Ki +100Ki +100Ki +949Ki +950Ki +950Ki +951Ki +951Ki +952Ki +990Ki +991Ki +995Ki +995Ki +996Ki +996Ki +997Ki +999Ki +1000Ki +1023Ki +1.0Mi +1.0Mi +1.0Mi +1.1Mi +1.0Gi +1.0Gi +1.1Gi diff --git a/tests/fixtures/numfmt/gnutest_iec_input.txt b/tests/fixtures/numfmt/gnutest_iec_input.txt new file mode 100644 index 000000000..bd7e867d5 --- /dev/null +++ b/tests/fixtures/numfmt/gnutest_iec_input.txt @@ -0,0 +1,49 @@ +-1025 +-1024.1 +-1024 +-1023.1 +-1023 +0 +1 +1023 +1024 +1025 +1126 +1127 +1536 +1537 +1945 +1946 +1996 +1997 +2048 +2049 +10188 +10189 +10240 +102348 +102349 +102400 +971776 +972288 +972800 +972801 +973824 +973825 +1013760 +1013761 +1018879 +1018880 +1018881 +1019904 +1019905 +1022976 +1022977 +1047552 +1047553 +1048575 +1048576 +1048577 +1073741823 +1073741824 +1073741825 diff --git a/tests/fixtures/numfmt/gnutest_iec_result.txt b/tests/fixtures/numfmt/gnutest_iec_result.txt new file mode 100644 index 000000000..61c5515b8 --- /dev/null +++ b/tests/fixtures/numfmt/gnutest_iec_result.txt @@ -0,0 +1,49 @@ +-1.1K +-1.1K +-1.0K +-1.0K +-1023 +0 +1 +1023 +1.0K +1.1K +1.1K +1.2K +1.5K +1.6K +1.9K +2.0K +2.0K +2.0K +2.0K +2.1K +10K +10K +10K +100K +100K +100K +949K +950K +950K +951K +951K +952K +990K +991K +995K +995K +996K +996K +997K +999K +1000K +1023K +1.0M +1.0M +1.0M +1.1M +1.0G +1.0G +1.1G diff --git a/tests/fixtures/numfmt/gnutest_si_input.txt b/tests/fixtures/numfmt/gnutest_si_input.txt new file mode 100644 index 000000000..3fcf4f9ff --- /dev/null +++ b/tests/fixtures/numfmt/gnutest_si_input.txt @@ -0,0 +1,39 @@ +-1001 +-999.1 +-999 +1 +500 +999 +999.1 +1000 +1000.1 +1001 +9900 +9901 +9949 +9950 +9951 +10000 +10001 +10500 +10999 +50000 +99000 +99001 +99900 +99949 +99950 +100000 +100001 +100999 +101000 +101001 +999000 +999001 +999949 +999950 +999999 +1000000 +1000001 +999000000.1 +999000001 diff --git a/tests/fixtures/numfmt/gnutest_si_result.txt b/tests/fixtures/numfmt/gnutest_si_result.txt new file mode 100644 index 000000000..7238ba40c --- /dev/null +++ b/tests/fixtures/numfmt/gnutest_si_result.txt @@ -0,0 +1,39 @@ +-1.1K +-1.0K +-999 +1 +500 +999 +1.0K +1.0K +1.1K +1.1K +9.9K +10K +10K +10K +10K +10K +11K +11K +11K +50K +99K +100K +100K +100K +100K +100K +101K +101K +101K +102K +999K +1.0M +1.0M +1.0M +1.0M +1.0M +1.1M +1.0G +1.0G diff --git a/tests/fixtures/ptx/gnu_ext_disabled_roff_no_ref_word_regexp_exc_space.expected b/tests/fixtures/ptx/gnu_ext_disabled_roff_no_ref_word_regexp_exc_space.expected new file mode 100644 index 000000000..141e6c9f8 --- /dev/null +++ b/tests/fixtures/ptx/gnu_ext_disabled_roff_no_ref_word_regexp_exc_space.expected @@ -0,0 +1,7 @@ +.xx "" "" """quotes"", for roff" "" +.xx "" "" "and some other like %a, b#, c$c" "" +.xx "" "" "hello world!" "" +.xx "" "" "let's check special characters:" "" +.xx "" "" "maybe also~or^" "" +.xx "" "" "oh, and back\slash" "" +.xx "" "" "{brackets} for tex" "" diff --git a/tests/fixtures/shuf/file_input.txt b/tests/fixtures/shuf/file_input.txt new file mode 100644 index 000000000..fc316949e --- /dev/null +++ b/tests/fixtures/shuf/file_input.txt @@ -0,0 +1,10 @@ +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 diff --git a/tests/fixtures/unexpand/with_spaces.txt b/tests/fixtures/unexpand/with_spaces.txt new file mode 100644 index 000000000..3f39671a1 --- /dev/null +++ b/tests/fixtures/unexpand/with_spaces.txt @@ -0,0 +1,2 @@ + abc d e f g \t\t A + \ No newline at end of file diff --git a/tests/fixtures/uniq/group-append.expected b/tests/fixtures/uniq/group-append.expected new file mode 100644 index 000000000..62f53e69f --- /dev/null +++ b/tests/fixtures/uniq/group-append.expected @@ -0,0 +1,26 @@ + aaaaa ⅰ + + bbbbb ⅱ + bbbbb ⅱ + + ccccc ⅲ + ccccc ⅲ + ccccc ⅲ + + ddddd ⅲ + ddddd ⅲ + ddddd ⅲ + ddddd ⅲ + + eeeee ⅲ + + fffff ⅲ + fffff ⅲ + + ggggg ⅲ + ggggg ⅲ + ggggg ⅲ + + GGGGG ⅲ + GGGGG ⅲ + diff --git a/tests/fixtures/uniq/group-both.expected b/tests/fixtures/uniq/group-both.expected new file mode 100644 index 000000000..8a0f06bf2 --- /dev/null +++ b/tests/fixtures/uniq/group-both.expected @@ -0,0 +1,27 @@ + + aaaaa ⅰ + + bbbbb ⅱ + bbbbb ⅱ + + ccccc ⅲ + ccccc ⅲ + ccccc ⅲ + + ddddd ⅲ + ddddd ⅲ + ddddd ⅲ + ddddd ⅲ + + eeeee ⅲ + + fffff ⅲ + fffff ⅲ + + ggggg ⅲ + ggggg ⅲ + ggggg ⅲ + + GGGGG ⅲ + GGGGG ⅲ + diff --git a/tests/fixtures/uniq/group-prepend.expected b/tests/fixtures/uniq/group-prepend.expected new file mode 100644 index 000000000..5209f7fbe --- /dev/null +++ b/tests/fixtures/uniq/group-prepend.expected @@ -0,0 +1,26 @@ + + aaaaa ⅰ + + bbbbb ⅱ + bbbbb ⅱ + + ccccc ⅲ + ccccc ⅲ + ccccc ⅲ + + ddddd ⅲ + ddddd ⅲ + ddddd ⅲ + ddddd ⅲ + + eeeee ⅲ + + fffff ⅲ + fffff ⅲ + + ggggg ⅲ + ggggg ⅲ + ggggg ⅲ + + GGGGG ⅲ + GGGGG ⅲ diff --git a/tests/fixtures/uniq/group.expected b/tests/fixtures/uniq/group.expected new file mode 100644 index 000000000..145a78011 --- /dev/null +++ b/tests/fixtures/uniq/group.expected @@ -0,0 +1,25 @@ + aaaaa ⅰ + + bbbbb ⅱ + bbbbb ⅱ + + ccccc ⅲ + ccccc ⅲ + ccccc ⅲ + + ddddd ⅲ + ddddd ⅲ + ddddd ⅲ + ddddd ⅲ + + eeeee ⅲ + + fffff ⅲ + fffff ⅲ + + ggggg ⅲ + ggggg ⅲ + ggggg ⅲ + + GGGGG ⅲ + GGGGG ⅲ diff --git a/tests/fixtures/uniq/not-utf8-sequence.txt b/tests/fixtures/uniq/not-utf8-sequence.txt new file mode 100644 index 000000000..78c146ffd --- /dev/null +++ b/tests/fixtures/uniq/not-utf8-sequence.txt @@ -0,0 +1,2 @@ +Next line contains two bytes - 0xCC and 0xCD - which are not a valid utf-8 sequence + \ No newline at end of file diff --git a/tests/fixtures/uniq/skip-2-fields.expected b/tests/fixtures/uniq/skip-2-fields.expected index 8cffc2ebc..b971c2b2b 100644 --- a/tests/fixtures/uniq/skip-2-fields.expected +++ b/tests/fixtures/uniq/skip-2-fields.expected @@ -1,2 +1,2 @@ - aaa aa a + aaa ⟪⟫ a aa a diff --git a/tests/fixtures/uniq/skip-fields.txt b/tests/fixtures/uniq/skip-fields.txt index f84e377a0..4ec2744c6 100644 --- a/tests/fixtures/uniq/skip-fields.txt +++ b/tests/fixtures/uniq/skip-fields.txt @@ -1,4 +1,4 @@ - aaa aa a + aaa ⟪⟫ a ZZZ aa a ZZZ aa a ZZZ bb a diff --git a/tests/fixtures/wc/UTF_8_test.txt b/tests/fixtures/wc/UTF_8_test.txt new file mode 100644 index 000000000..a5b5d50e6 Binary files /dev/null and b/tests/fixtures/wc/UTF_8_test.txt differ diff --git a/util/publish.sh b/util/publish.sh new file mode 100644 index 000000000..0936238a8 --- /dev/null +++ b/util/publish.sh @@ -0,0 +1,24 @@ +#!/bin/bash +set -e + +ARG="" +if test "$1" != "--do-it"; then + ARG="--dry-run --allow-dirty" +fi + +cd src/uucore/ +cargo publish $ARG +cd - + +cd src/uu/stdbuf/src/libstdbuf/ +cargo publish $ARG +cd - + +PROGS=$(ls -1d src/uu/*/) +for p in $PROGS; do + cd $p + cargo publish $ARG + cd - +done + +cargo publish $ARG diff --git a/util/update-version.sh b/util/update-version.sh new file mode 100644 index 000000000..042d43b71 --- /dev/null +++ b/util/update-version.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# This is a stupid helper. I will mass replace all versions (including other crates) +# So, it should be triple-checked + + +FROM="0.0.5" +TO="0.0.6" + +UUCORE_FROM="0.0.7" +UUCORE_TO="0.0.8" + +PROGS=$(ls -1d src/uu/*/Cargo.toml src/uu/stdbuf/src/libstdbuf/Cargo.toml Cargo.toml) + +# update the version of all programs +sed -i -e "s|version = \"$FROM\"|version = \"$TO\"|" $PROGS + +# Update the stbuff stuff +sed -i -e "s|libstdbuf = { version=\"$FROM\"|libstdbuf = { version=\"$TO\"|" src/uu/stdbuf/Cargo.toml +sed -i -e "s|= { optional=true, version=\"$FROM\", package=\"uu_|= { optional=true, version=\"$TO\", package=\"uu_|g" Cargo.toml + +# Update uucore itself +#sed -i -e "s|version = \"$UUCORE_FROM\"|version = \"$UUCORE_TO\"|" src/uucore/Cargo.toml +# Update crates using uucore +#sed -i -e "s|uucore = { version=\">=$UUCORE_FROM\",|uucore = { version=\">=$UUCORE_TO\",|" $PROGS + +