diff --git a/.github/workflows/GNU.yml b/.github/workflows/GNU.yml index b1cbc1502..9eb5da2b9 100644 --- a/.github/workflows/GNU.yml +++ b/.github/workflows/GNU.yml @@ -34,46 +34,74 @@ jobs: shell: bash run: | sudo apt-get update - sudo apt-get install autoconf autopoint bison texinfo gperf gcc g++ gdb python-pyinotify expect python3-sphinx + 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 $(seq -w 1 36) + 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 |' tests/factor/t*sh - sed -i -e '/tests\/misc\/cat-self.sh/ D' Makefile # issue #1707 - sed -i -e '/tests\/misc\/numfmt.pl/ D' Makefile # issue #1708 - sed -i -e '/tests\/chown\/preserve-root.sh/ D' Makefile # issue #1709 - sed -i -e '/tests\/cp\/file-perm-race.sh/ D' Makefile # issue #1710 - sed -i -e '/tests\/cp\/special-f.sh/ D' Makefile # issue #1710 - sed -i -e '/tests\/mv\/mv-special-1.sh/ D' Makefile # issue #1710 - sed -i -e '/tests\/dd\/stats.sh/ D' Makefile # issue #1710 - sed -i -e '/tests\/cp\/existing-perm-race.sh/ D' Makefile # hangs, cp doesn't implement --copy-contents - # Remove tests that rely on seq with large numbers. See issue #1703 - sed -i -e '/tests\/misc\/seq.pl/ D' \ - -e '/tests\/misc\/seq-precision.sh/D' \ - Makefile + 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 - sed -i -e '/tests\/tail-2\/pid.sh/ D' Makefile # hangs on github, tail doesn't support -f - sed -i -e'/incompat4/ D' -e"/options '-co' are incompatible/ d" tests/misc/sort.pl # Sort doesn't correctly check for incompatible options, waits for input + + # 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 # 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 + 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 @@ -83,17 +111,22 @@ jobs: GNULIB_DIR="${PWD}/gnulib" pushd gnu - unbuffer timeout -sKILL 3600 make -j "$(nproc)" check SUBDIRS=. RUN_EXPENSIVE_TESTS=yes RUN_VERY_EXPENSIVE_TESTS=yes VERBOSE=no || : + 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: | - 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" + 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: diff --git a/Cargo.lock b/Cargo.lock index 9dcc23ff2..aabac3783 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -59,11 +59,6 @@ name = "bit-vec" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -[[package]] -name = "bitflags" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "bitflags" version = "1.2.1" @@ -95,7 +90,7 @@ dependencies = [ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "memchr 2.3.4 (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.124 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.125 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -193,7 +188,7 @@ dependencies = [ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.85 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.4.4 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.4.5 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", "same-file 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "sha1 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -300,6 +295,7 @@ dependencies = [ "uu_whoami 0.0.4", "uu_yes 0.0.4", "uucore 0.0.7", + "walkdir 2.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -404,13 +400,13 @@ dependencies = [ "oorandom 11.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "plotters 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "rayon 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.4.4 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.124 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.4.5 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.125 (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.124 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.125 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.64 (registry+https://github.com/rust-lang/crates.io-index)", "tinytemplate 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "walkdir 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "walkdir 2.3.2 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -472,7 +468,7 @@ dependencies = [ "csv-core 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "itoa 0.4.7 (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.124 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.125 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -517,7 +513,7 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "log 0.4.14 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.4.4 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.4.5 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -657,10 +653,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "js-sys" -version = "0.3.48" +version = "0.3.49" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "wasm-bindgen 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen 0.2.72 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -726,17 +722,6 @@ dependencies = [ "autocfg 1.0.1 (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.85 (registry+https://github.com/rust-lang/crates.io-index)", - "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "nix" version = "0.13.1" @@ -854,8 +839,8 @@ dependencies = [ "num-traits 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", "plotters-backend 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "plotters-svg 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", - "web-sys 0.3.48 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen 0.2.72 (registry+https://github.com/rust-lang/crates.io-index)", + "web-sys 0.3.49 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1035,7 +1020,7 @@ dependencies = [ [[package]] name = "regex" -version = "1.4.4" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "aho-corasick 0.7.15 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1115,7 +1100,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "serde" -version = "1.0.124" +version = "1.0.125" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -1124,12 +1109,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "half 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.124 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.125 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "serde_derive" -version = "1.0.124" +version = "1.0.125" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "proc-macro2 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1144,7 +1129,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "itoa 0.4.7 (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.124 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.125 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1301,7 +1286,7 @@ name = "tinytemplate" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "serde 1.0.124 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.125 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.64 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1397,7 +1382,7 @@ version = "0.0.4" dependencies = [ "uucore 0.0.7", "uucore_procs 0.0.5", - "walkdir 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "walkdir 2.3.2 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1407,7 +1392,7 @@ dependencies = [ "libc 0.2.85 (registry+https://github.com/rust-lang/crates.io-index)", "uucore 0.0.7", "uucore_procs 0.0.5", - "walkdir 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "walkdir 2.3.2 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1418,14 +1403,14 @@ dependencies = [ "glob 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "uucore 0.0.7", "uucore_procs 0.0.5", - "walkdir 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "walkdir 2.3.2 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "uu_chroot" version = "0.0.4" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", + "clap 2.33.3 (registry+https://github.com/rust-lang/crates.io-index)", "uucore 0.0.7", "uucore_procs 0.0.5", ] @@ -1460,7 +1445,7 @@ dependencies = [ "quick-error 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "uucore 0.0.7", "uucore_procs 0.0.5", - "walkdir 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "walkdir 2.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", "xattr 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1471,7 +1456,7 @@ version = "0.0.4" dependencies = [ "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", "glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.4.4 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.4.5 (registry+https://github.com/rust-lang/crates.io-index)", "thiserror 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)", "uucore 0.0.7", "uucore_procs 0.0.5", @@ -1491,8 +1476,10 @@ version = "0.0.4" dependencies = [ "chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.33.3 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.85 (registry+https://github.com/rust-lang/crates.io-index)", "uucore 0.0.7", "uucore_procs 0.0.5", + "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1538,6 +1525,7 @@ dependencies = [ name = "uu_echo" version = "0.0.4" dependencies = [ + "clap 2.33.3 (registry+https://github.com/rust-lang/crates.io-index)", "uucore 0.0.7", "uucore_procs 0.0.5", ] @@ -1557,7 +1545,7 @@ dependencies = [ name = "uu_expand" version = "0.0.4" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", + "clap 2.33.3 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-width 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", "uucore 0.0.7", "uucore_procs 0.0.5", @@ -1634,7 +1622,7 @@ dependencies = [ "hex 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.85 (registry+https://github.com/rust-lang/crates.io-index)", "md5 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.4.4 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.4.5 (registry+https://github.com/rust-lang/crates.io-index)", "regex-syntax 0.6.23 (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)", @@ -1745,7 +1733,7 @@ name = "uu_ls" version = "0.0.4" dependencies = [ "atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", + "clap 2.33.3 (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.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "term_grid 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1802,7 +1790,7 @@ name = "uu_more" version = "0.0.4" 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)", + "nix 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)", "redox_syscall 0.1.57 (registry+https://github.com/rust-lang/crates.io-index)", "redox_termios 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "uucore 0.0.7", @@ -1838,7 +1826,7 @@ dependencies = [ "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.85 (registry+https://github.com/rust-lang/crates.io-index)", "memchr 2.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.4.4 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.4.5 (registry+https://github.com/rust-lang/crates.io-index)", "regex-syntax 0.6.23 (registry+https://github.com/rust-lang/crates.io-index)", "uucore 0.0.7", "uucore_procs 0.0.5", @@ -1848,7 +1836,7 @@ dependencies = [ name = "uu_nohup" version = "0.0.4" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", + "clap 2.33.3 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.85 (registry+https://github.com/rust-lang/crates.io-index)", "uucore 0.0.7", "uucore_procs 0.0.5", @@ -1879,7 +1867,7 @@ name = "uu_od" version = "0.0.4" 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)", + "clap 2.33.3 (registry+https://github.com/rust-lang/crates.io-index)", "half 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.85 (registry+https://github.com/rust-lang/crates.io-index)", "uucore 0.0.7", @@ -1939,7 +1927,7 @@ dependencies = [ "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.85 (registry+https://github.com/rust-lang/crates.io-index)", "memchr 2.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.4.4 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.4.5 (registry+https://github.com/rust-lang/crates.io-index)", "regex-syntax 0.6.23 (registry+https://github.com/rust-lang/crates.io-index)", "uucore 0.0.7", "uucore_procs 0.0.5", @@ -1990,7 +1978,7 @@ dependencies = [ "remove_dir_all 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", "uucore 0.0.7", "uucore_procs 0.0.5", - "walkdir 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "walkdir 2.3.2 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -2140,7 +2128,7 @@ dependencies = [ name = "uu_tee" version = "0.0.4" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", + "clap 2.33.3 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.85 (registry+https://github.com/rust-lang/crates.io-index)", "uucore 0.0.7", "uucore_procs 0.0.5", @@ -2209,7 +2197,7 @@ dependencies = [ name = "uu_tsort" version = "0.0.4" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", + "clap 2.33.3 (registry+https://github.com/rust-lang/crates.io-index)", "uucore 0.0.7", "uucore_procs 0.0.5", ] @@ -2238,7 +2226,7 @@ dependencies = [ name = "uu_unexpand" version = "0.0.4" dependencies = [ - "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", + "clap 2.33.3 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-width 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", "uucore 0.0.7", "uucore_procs 0.0.5", @@ -2357,7 +2345,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "walkdir" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "same-file 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2372,16 +2360,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "wasm-bindgen" -version = "0.2.71" +version = "0.2.72" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-macro 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-macro 0.2.72 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.71" +version = "0.2.72" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bumpalo 3.6.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2390,42 +2378,42 @@ dependencies = [ "proc-macro2 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", "syn 1.0.64 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-shared 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-shared 0.2.72 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.71" +version = "0.2.72" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "quote 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-macro-support 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-macro-support 0.2.72 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.71" +version = "0.2.72" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "proc-macro2 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", "syn 1.0.64 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-backend 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-shared 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-backend 0.2.72 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-shared 0.2.72 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.71" +version = "0.2.72" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "web-sys" -version = "0.3.48" +version = "0.3.49" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "js-sys 0.3.48 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", + "js-sys 0.3.49 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen 0.2.72 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -2490,7 +2478,6 @@ dependencies = [ "checksum autocfg 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" "checksum bit-set 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de" "checksum bit-vec 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" -"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 blake2-rfc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)" = "5d6d530bdd2d52966a6d03b7a964add7ae1a288d25214066fd4b600f0f796400" "checksum block-buffer 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1339a1042f5d9f295737ad4d9a6ab6bf81c84a933dba110b9200cd6d1448b814" @@ -2549,7 +2536,7 @@ dependencies = [ "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.7 (registry+https://github.com/rust-lang/crates.io-index)" = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" -"checksum js-sys 0.3.48 (registry+https://github.com/rust-lang/crates.io-index)" = "dc9f84f9b115ce7843d60706df1422a916680bfdfcbdb0447c5614ff9d7e4d78" +"checksum js-sys 0.3.49 (registry+https://github.com/rust-lang/crates.io-index)" = "dc15e39392125075f60c95ba416f5381ff6c3a948ff02ab12464715adf56c821" "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.85 (registry+https://github.com/rust-lang/crates.io-index)" = "7ccac4b00700875e6a07c6cde370d44d32fa01c5a65cdd2fca6858c479d28bb3" @@ -2561,7 +2548,6 @@ dependencies = [ "checksum memchr 2.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" "checksum memoffset 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "157b4208e3059a8f9e78d559edc658e13df41410cb3ae03979c83130067fdd87" "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.44 (registry+https://github.com/rust-lang/crates.io-index)" = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" "checksum num-traits 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" @@ -2598,7 +2584,7 @@ dependencies = [ "checksum redox_syscall 0.1.57 (registry+https://github.com/rust-lang/crates.io-index)" = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" "checksum redox_syscall 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9" "checksum redox_termios 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f" -"checksum regex 1.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "54fd1046a3107eb58f42de31d656fee6853e5d276c455fd943742dce89fc3dd3" +"checksum regex 1.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "957056ecddbeba1b26965114e191d2e8589ce74db242b6ea25fc4062427a5c19" "checksum regex-automata 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "ae1ded71d66a4a97f5e961fd0cb25a5f366a42a41570d16a763a69c092c26ae4" "checksum regex-syntax 0.6.23 (registry+https://github.com/rust-lang/crates.io-index)" = "24d5f089152e60f62d28b835fbff2cd2e8dc0baf1ac13343bef92ab7eed84548" "checksum remove_dir_all 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" @@ -2610,9 +2596,9 @@ dependencies = [ "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.124 (registry+https://github.com/rust-lang/crates.io-index)" = "bd761ff957cb2a45fbb9ab3da6512de9de55872866160b23c25f1a841e99d29f" +"checksum serde 1.0.125 (registry+https://github.com/rust-lang/crates.io-index)" = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171" "checksum serde_cbor 0.11.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1e18acfa2f90e8b735b2836ab8d538de304cbb6729a7360729ea5a895d15a622" -"checksum serde_derive 1.0.124 (registry+https://github.com/rust-lang/crates.io-index)" = "1800f7693e94e186f5e25a28291ae1570da908aff7d97a095dec1e56ff99069b" +"checksum serde_derive 1.0.125 (registry+https://github.com/rust-lang/crates.io-index)" = "b093b7a2bb58203b5da3056c05b4ec1fed827dcfdb37347a8841695263b3d06d" "checksum serde_json 1.0.64 (registry+https://github.com/rust-lang/crates.io-index)" = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" "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" @@ -2640,14 +2626,14 @@ dependencies = [ "checksum users 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aa4227e95324a443c9fcb06e03d4d85e91aabe9a5a02aa818688b6918b6af486" "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 walkdir 2.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" "checksum wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)" = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" -"checksum wasm-bindgen 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)" = "7ee1280240b7c461d6a0071313e08f34a60b0365f14260362e5a2b17d1d31aa7" -"checksum wasm-bindgen-backend 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)" = "5b7d8b6942b8bb3a9b0e73fc79b98095a27de6fa247615e59d096754a3bc2aa8" -"checksum wasm-bindgen-macro 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)" = "e5ac38da8ef716661f0f36c0d8320b89028efe10c7c0afde65baffb496ce0d3b" -"checksum wasm-bindgen-macro-support 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)" = "cc053ec74d454df287b9374ee8abb36ffd5acb95ba87da3ba5b7d3fe20eb401e" -"checksum wasm-bindgen-shared 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)" = "7d6f8ec44822dd71f5f221a5847fb34acd9060535c1211b70a05844c0f6383b1" -"checksum web-sys 0.3.48 (registry+https://github.com/rust-lang/crates.io-index)" = "ec600b26223b2948cedfde2a0aa6756dcf1fef616f43d7b3097aaf53a6c4d92b" +"checksum wasm-bindgen 0.2.72 (registry+https://github.com/rust-lang/crates.io-index)" = "8fe8f61dba8e5d645a4d8132dc7a0a66861ed5e1045d2c0ed940fab33bac0fbe" +"checksum wasm-bindgen-backend 0.2.72 (registry+https://github.com/rust-lang/crates.io-index)" = "046ceba58ff062da072c7cb4ba5b22a37f00a302483f7e2a6cdc18fedbdc1fd3" +"checksum wasm-bindgen-macro 0.2.72 (registry+https://github.com/rust-lang/crates.io-index)" = "0ef9aa01d36cda046f797c57959ff5f3c615c9cc63997a8d545831ec7976819b" +"checksum wasm-bindgen-macro-support 0.2.72 (registry+https://github.com/rust-lang/crates.io-index)" = "96eb45c1b2ee33545a813a92dbb53856418bf7eb54ab34f7f7ff1448a5b3735d" +"checksum wasm-bindgen-shared 0.2.72 (registry+https://github.com/rust-lang/crates.io-index)" = "b7148f4696fb4960a346eaa60bbfb42a1ac4ebba21f750f75fc1375b098d5ffa" +"checksum web-sys 0.3.49 (registry+https://github.com/rust-lang/crates.io-index)" = "59fe19d70f5dacc03f6e46777213facae5ac3801575d56ca6cbd4c93dcd12310" "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.9 (registry+https://github.com/rust-lang/crates.io-index)" = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" diff --git a/Cargo.toml b/Cargo.toml index 9b55abe5c..bf11e66fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -347,6 +347,7 @@ tempfile = "= 3.1.0" time = "0.1" unindent = "0.1" uucore = { version=">=0.0.7", package="uucore", path="src/uucore", features=["entries"] } +walkdir = "2.2" [target.'cfg(unix)'.dev-dependencies] rust-users = { version="0.10", package="users" } diff --git a/README.md b/README.md index 9d25a1120..da92c80c2 100644 --- a/README.md +++ b/README.md @@ -23,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. @@ -301,7 +300,7 @@ Utilities | csplit | date | | | cut | join | | | dircolors | df | | -| dirname | | | +| dirname | tac | | | du | | | | echo | | | | env | | | @@ -354,7 +353,6 @@ Utilities | stdbuf | | | | sum | | | | sync | | | -| tac | | | | tee | | | | timeout | | | | touch | | | diff --git a/src/uu/chroot/Cargo.toml b/src/uu/chroot/Cargo.toml index 7ad2f0908..e967d4137 100644 --- a/src/uu/chroot/Cargo.toml +++ b/src/uu/chroot/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/chroot.rs" [dependencies] -getopts = "0.2.18" +clap= "2.33" uucore = { version=">=0.0.7", package="uucore", path="../../uucore", features=["entries"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/chroot/src/chroot.rs b/src/uu/chroot/src/chroot.rs index 773207363..2c3bcbca4 100644 --- a/src/uu/chroot/src/chroot.rs +++ b/src/uu/chroot/src/chroot.rs @@ -10,60 +10,81 @@ #[macro_use] extern crate uucore; -use uucore::entries; -use uucore::libc::{self, chroot, setgid, setgroups, setuid}; - +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}; +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. \ + .arg( + Arg::with_name(options::GROUPS) + .short("G") + .long(options::GROUPS) + .help("Comma-separated list of groups to switch to") + .value_name("GROUP1,GROUP2..."), + ) + .arg( + Arg::with_name(options::USERSPEC) + .long(options::USERSPEC) + .help( + "Colon-separated user and group to switch to. \ Same as -u USER -g GROUP. \ Userspec has higher preference than -u and/or -g", - "USER:GROUP", + ) + .value_name("USER:GROUP"), ) - .parse(args); - - if matches.free.is_empty() { - println!("Missing operand: NEWROOT"); - println!("Try `{} --help` for more information.", NAME); - return 1; - } + .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, @@ -72,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, @@ -80,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); @@ -97,30 +125,26 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } } -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); @@ -164,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()) } } 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..e1c75fffc 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -14,11 +14,101 @@ 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 SYNTAX: &str = "[OPTIONS] [FILE]..."; +const SUMMARY: &str = "Print CRC and size for each file"; +const LONG_HELP: &str = ""; + +// 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 { @@ -68,7 +158,6 @@ fn cksum(fname: &str) -> io::Result<(u32, usize)> { Err(err) => return Err(err), } } - //Ok((0 as u32,0 as usize)) } pub fn uumain(args: impl uucore::Args) -> i32 { diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 0922af241..569ee78bc 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -112,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)+) => ({ @@ -431,6 +431,10 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .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) @@ -442,10 +446,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .takes_value(true) .value_name("WHEN") .help("NotImplemented: control creation of sparse files. See below")) - .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) @@ -563,6 +563,7 @@ impl Options { let not_implemented_opts = vec![ OPT_COPY_CONTENTS, OPT_SPARSE, + #[cfg(not(any(windows, unix)))] OPT_ONE_FILE_SYSTEM, OPT_CONTEXT, #[cfg(windows)] @@ -937,7 +938,7 @@ 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 || options.dereference) && is_symlink { diff --git a/src/uu/date/Cargo.toml b/src/uu/date/Cargo.toml index 4e3227f02..c62cfe2b3 100644 --- a/src/uu/date/Cargo.toml +++ b/src/uu/date/Cargo.toml @@ -20,6 +20,12 @@ 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(unix)'.dependencies] +libc = "0.2" + +[target.'cfg(windows)'.dependencies] +winapi = { version = "0.3", features = ["minwinbase", "sysinfoapi", "minwindef"] } + [[bin]] name = "date" path = "src/main.rs" diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index 5ee4b1610..43573437d 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -12,13 +12,20 @@ #[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"; @@ -62,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, @@ -186,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) @@ -222,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; @@ -247,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) => { @@ -314,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/src/df.rs b/src/uu/df/src/df.rs index ed2865728..57caf7970 100644 --- a/src/uu/df/src/df.rs +++ b/src/uu/df/src/df.rs @@ -32,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")] @@ -137,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)] @@ -209,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(any( all(target_os = "freebsd"), - all(target_os = "macos", target_arch = "aarch64") + 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 { @@ -585,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) }; diff --git a/src/uu/echo/Cargo.toml b/src/uu/echo/Cargo.toml index a8742b68f..7b831fcb8 100644 --- a/src/uu/echo/Cargo.toml +++ b/src/uu/echo/Cargo.toml @@ -15,6 +15,7 @@ edition = "2018" path = "src/echo.rs" [dependencies] +clap = "2.33" uucore = { version=">=0.0.7", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/echo/src/echo.rs b/src/uu/echo/src/echo.rs index 93c395391..c991f5d3f 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 SUMMARY: &str = "display a line of text"; -const HELP: &str = r#" +static NAME: &str = "echo"; +static USAGE: &str = "[OPTIONS]... [STRING]..."; +static SUMMARY: &str = "display a line of text"; +static 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 static STRING: &str = "STRING"; + pub static NO_NEWLINE: &str = "no_newline"; + pub static ENABLE_BACKSLASH_ESCAPE: &str = "enable_backslash_escape"; + pub static DISABLE_BACKSLASH_ESCAPE: &str = "disable_backslash_escape"; +} + fn parse_code( input: &mut Peekable, base: u32, @@ -105,20 +115,54 @@ 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!()) + .usage(USAGE) + .about(SUMMARY) + .after_help(AFTER_HELP) + .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) + .hidden(true) + .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/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/factor/src/factor.rs b/src/uu/factor/src/factor.rs index 2715bad71..7d2e16a11 100644 --- a/src/uu/factor/src/factor.rs +++ b/src/uu/factor/src/factor.rs @@ -9,7 +9,7 @@ use smallvec::SmallVec; use std::cell::RefCell; use std::fmt; -use crate::numeric::{gcd, Arithmetic, Montgomery}; +use crate::numeric::{Arithmetic, Montgomery}; use crate::{miller_rabin, rho, table}; type Exponent = u8; @@ -29,20 +29,15 @@ impl Decomposition { fn add(&mut self, factor: u64, exp: Exponent) { debug_assert!(exp > 0); - // Assert the factor doesn't already exist in the Decomposition object - debug_assert_eq!(self.0.iter_mut().find(|(f, _)| *f == factor), None); - self.0.push((factor, exp)) - } - - fn is_one(&self) -> bool { - self.0.is_empty() - } - - fn pop(&mut self) -> Option<(u64, Exponent)> { - self.0.pop() + if let Some((_, e)) = self.0.iter_mut().find(|(f, _)| *f == factor) { + *e += exp; + } else { + self.0.push((factor, exp)) + } } + #[cfg(test)] fn product(&self) -> u64 { self.0 .iter() @@ -86,11 +81,11 @@ impl Factors { self.0.borrow_mut().add(prime, exp) } - #[cfg(test)] pub fn push(&mut self, prime: u64) { self.add(prime, 1) } + #[cfg(test)] fn product(&self) -> u64 { self.0.borrow().product() } @@ -111,116 +106,62 @@ impl fmt::Display for Factors { } } -fn _find_factor(num: u64) -> Option { +fn _factor(num: u64, f: Factors) -> Factors { use miller_rabin::Result::*; + // Shadow the name, so the recursion automatically goes from “Big” arithmetic to small. + let _factor = |n, f| { + if n < (1 << 32) { + _factor::>(n, f) + } else { + _factor::(n, f) + } + }; + + if num == 1 { + return f; + } + let n = A::new(num); - match miller_rabin::test::(n) { - Prime => None, - Composite(d) => Some(d), - Pseudoprime => Some(rho::find_divisor::(n)), - } + let divisor = match miller_rabin::test::(n) { + Prime => { + let mut r = f; + r.push(num); + return r; + } + + Composite(d) => d, + Pseudoprime => rho::find_divisor::(n), + }; + + let f = _factor(divisor, f); + _factor(num / divisor, f) } -fn find_factor(num: u64) -> Option { - if num < (1 << 32) { - _find_factor::>(num) - } else { - _find_factor::>(num) - } -} - -pub fn factor(num: u64) -> Factors { +pub fn factor(mut n: u64) -> Factors { let mut factors = Factors::one(); - if num < 2 { + if n < 2 { return factors; } - let mut n = num; - let n_zeros = num.trailing_zeros(); + let n_zeros = n.trailing_zeros(); if n_zeros > 0 { factors.add(2, n_zeros as Exponent); n >>= n_zeros; } - debug_assert_eq!(num, n * factors.product()); if n == 1 { return factors; } - table::factor(&mut n, &mut factors); - debug_assert_eq!(num, n * factors.product()); + let (factors, n) = table::factor(n, factors); - if n == 1 { - return factors; + if n < (1 << 32) { + _factor::>(n, factors) + } else { + _factor::>(n, factors) } - - let mut dec = Decomposition::one(); - dec.add(n, 1); - - while !dec.is_one() { - // Check correctness invariant - debug_assert_eq!(num, factors.product() * dec.product()); - - let (factor, exp) = dec.pop().unwrap(); - - if let Some(divisor) = find_factor(factor) { - let mut gcd_queue = Decomposition::one(); - - let quotient = factor / divisor; - let mut trivial_gcd = quotient == divisor; - if trivial_gcd { - gcd_queue.add(divisor, exp + 1); - } else { - gcd_queue.add(divisor, exp); - gcd_queue.add(quotient, exp); - } - - while !trivial_gcd { - debug_assert_eq!(factor, gcd_queue.product()); - - let mut tmp = Decomposition::one(); - trivial_gcd = true; - for i in 0..gcd_queue.0.len() - 1 { - let (mut a, exp_a) = gcd_queue.0[i]; - let (mut b, exp_b) = gcd_queue.0[i + 1]; - - if a == 1 { - continue; - } - - let g = gcd(a, b); - if g != 1 { - trivial_gcd = false; - a /= g; - b /= g; - } - if a != 1 { - tmp.add(a, exp_a); - } - if g != 1 { - tmp.add(g, exp_a + exp_b); - } - - if i + 1 != gcd_queue.0.len() - 1 { - gcd_queue.0[i + 1].0 = b; - } else if b != 1 { - tmp.add(b, exp_b); - } - } - gcd_queue = tmp; - } - - debug_assert_eq!(factor, gcd_queue.product()); - dec.0.extend(gcd_queue.0); - } else { - // factor is prime - factors.add(factor, exp); - } - } - - factors } #[cfg(test)] diff --git a/src/uu/factor/src/table.rs b/src/uu/factor/src/table.rs index b62e801cb..d6ef796fc 100644 --- a/src/uu/factor/src/table.rs +++ b/src/uu/factor/src/table.rs @@ -14,8 +14,7 @@ use crate::Factors; include!(concat!(env!("OUT_DIR"), "/prime_table.rs")); -pub(crate) fn factor(n: &mut u64, factors: &mut Factors) { - let mut num = *n; +pub(crate) fn factor(mut num: u64, mut factors: Factors) -> (Factors, u64) { for &(prime, inv, ceil) in P_INVS_U64 { if num == 1 { break; @@ -43,5 +42,5 @@ pub(crate) fn factor(n: &mut u64, factors: &mut Factors) { } } - *n = num; + (factors, num) } diff --git a/src/uu/head/src/head.rs b/src/uu/head/src/head.rs index 56d7e8452..0036dbba9 100644 --- a/src/uu/head/src/head.rs +++ b/src/uu/head/src/head.rs @@ -29,6 +29,7 @@ enum FilterMode { struct Settings { mode: FilterMode, verbose: bool, + zero_terminated: bool, } impl Default for Settings { @@ -36,6 +37,7 @@ impl Default for Settings { Settings { mode: FilterMode::Lines(10), verbose: false, + zero_terminated: false, } } } @@ -69,6 +71,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { ) .optflag("q", "quiet", "never print headers giving file names") .optflag("v", "verbose", "always print headers giving file names") + .optflag("z", "zero-terminated", "line delimiter is NUL, not newline") .optflag("h", "help", "display this help and exit") .optflag("V", "version", "output version information and exit") .parse(new_args); @@ -113,6 +116,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let quiet = matches.opt_present("q"); let verbose = matches.opt_present("v"); + settings.zero_terminated = matches.opt_present("z"); let files = matches.free; // GNU implementation allows multiple declarations of "-q" and "-v" with the @@ -145,6 +149,13 @@ pub fn uumain(args: impl uucore::Args) -> i32 { first_time = false; let path = Path::new(file); + if path.is_dir() || !path.metadata().is_ok() { + eprintln!( + "cannot open '{}' for reading: No such file or directory", + &path.to_str().unwrap() + ); + continue; + } let reader = File::open(&path).unwrap(); let mut buffer = BufReader::new(reader); if !head(&mut buffer, &settings) { @@ -203,8 +214,14 @@ fn head(reader: &mut BufReader, settings: &Settings) -> bool { } } FilterMode::Lines(count) => { - for line in reader.lines().take(count) { - println!("{}", line.unwrap()); + if settings.zero_terminated { + for line in reader.split(0).take(count) { + print!("{}\0", String::from_utf8(line.unwrap()).unwrap()) + } + } else { + for line in reader.lines().take(count) { + println!("{}", line.unwrap()); + } } } FilterMode::NLines(count) => { diff --git a/src/uu/id/src/id.rs b/src/uu/id/src/id.rs index f07a850fa..4536622c7 100644 --- a/src/uu/id/src/id.rs +++ b/src/uu/id/src/id.rs @@ -291,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/src/install.rs b/src/uu/install/src/install.rs index 9d1acdc7e..1ac9bc743 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -426,18 +426,25 @@ fn copy_files_into_dir(files: &[PathBuf], target_dir: &PathBuf, b: &Behavior) -> 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; diff --git a/src/uu/ls/Cargo.toml b/src/uu/ls/Cargo.toml index 10d7cc2da..59901f807 100644 --- a/src/uu/ls/Cargo.toml +++ b/src/uu/ls/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/ls.rs" [dependencies] -getopts = "0.2.18" +clap = "2.33" lazy_static = "1.0.1" number_prefix = "0.4" term_grid = "0.1.5" diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index b8defa397..8714a0fa1 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -13,8 +13,10 @@ extern crate lazy_static; #[macro_use] extern crate uucore; +mod version_cmp; + +use clap::{App, Arg}; use number_prefix::NumberPrefix; -use std::cmp::Reverse; #[cfg(unix)] use std::collections::HashMap; use std::fs; @@ -26,6 +28,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)] @@ -33,14 +40,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:"; @@ -52,7 +62,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]); } @@ -65,107 +75,639 @@ 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 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 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 INODE: &str = "inode"; + pub static DEREFERENCE: &str = "dereference"; + pub static NUMERIC_UID_GID: &str = "numeric-uid-gid"; + pub static REVERSE: &str = "reverse"; + pub static RECURSIVE: &str = "recursive"; + pub static COLOR: &str = "color"; + pub static PATHS: &str = "paths"; +} + +#[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, +} + +struct Config { + format: Format, + files: Files, + sort: Sort, + recursive: bool, + reverse: bool, + dereference: bool, + classify: bool, + ignore_backups: bool, + size_format: SizeFormat, + numeric_uid_gid: bool, + directory: bool, + time: Time, + #[cfg(unix)] + inode: bool, + #[cfg(unix)] + color: bool, + long: LongFormat, + width: Option, +} + +// Fields that can be removed or added to the long format +struct LongFormat { + author: bool, + group: bool, + owner: 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 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 check the last index they occur. If this index + // is larger than the index for the other format options, we apply the + // long format. + match options.indices_of(opt).map(|x| x.max().unwrap()) { + None => { + if options.is_present(options::format::LONG_NO_GROUP) + || options.is_present(options::format::LONG_NO_OWNER) + { + format = Format::Long; + } else if options.is_present(options::format::ONELINE) { + format = Format::OneLine; + } + } + Some(mut idx) => { + if let Some(indices) = options.indices_of(options::format::LONG_NO_OWNER) { + let i = indices.max().unwrap(); + if i > idx { + format = Format::Long; + idx = i; + } + } + if let Some(indices) = options.indices_of(options::format::LONG_NO_GROUP) { + let i = indices.max().unwrap(); + if i > idx { + format = Format::Long; + idx = i; + } + } + if let Some(indices) = options.indices_of(options::format::ONELINE) { + let i = indices.max().unwrap(); + if i > idx && format != Format::Long { + 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); + LongFormat { + author, + group, + owner, + } + }; + + 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)); + + Config { + format, + files, + sort, + recursive: options.is_present(options::RECURSIVE), + reverse: options.is_present(options::REVERSE), + dereference: options.is_present(options::DEREFERENCE), + classify: options.is_present(options::CLASSIFY), + ignore_backups: options.is_present(options::IGNORE_BACKUPS), + size_format, + numeric_uid_gid: options.is_present(options::NUMERIC_UID_GID), + directory: options.is_present(options::DIRECTORY), + time, + #[cfg(unix)] + color, + #[cfg(unix)] + inode: options.is_present(options::INODE), + long, + width, + } + } +} + 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) + 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 three arguments do not override with the other format + // options, see the comment in Config::from for the reason. + .arg( + Arg::with_name(options::format::ONELINE) + .short(options::format::ONELINE) + .help("List one file per line.") + ) + .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.") + ) + .arg( + Arg::with_name(options::format::LONG_NO_OWNER) + .short(options::format::LONG_NO_OWNER) + .help("Long format without owner information.") + ) + + // 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, + ]) + ) + + // 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::IGNORE_BACKUPS) + .short("B") + .long(options::IGNORE_BACKUPS) + .help("Ignore entries which end with ~."), + ) + .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::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.", + )) + .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::NUMERIC_UID_GID) + .short("n") + .long(options::NUMERIC_UID_GID) + .help("-l with numeric UIDs and GIDs."), + ) + .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), + ) + + // 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) -> i32 { - 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(); @@ -181,9 +723,9 @@ fn list(options: getopts::Matches) -> i32 { } 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; @@ -197,15 +739,15 @@ fn list(options: getopts::Matches) -> i32 { 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 @@ -214,126 +756,91 @@ fn list(options: getopts::Matches) -> i32 { } } -#[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 is_hidden(file_path: &DirEntry) -> std::io::Result { - let metadata = fs::metadata(file_path.path())?; +fn is_hidden(file_path: &DirEntry) -> bool { + let metadata = fs::metadata(file_path.path()).unwrap(); let attr = metadata.file_attributes(); - Ok(((attr & 0x2) > 0) || file_path.file_name().to_string_lossy().starts_with('.')) + ((attr & 0x2) > 0) || file_path.file_name().to_string_lossy().starts_with('.') } #[cfg(unix)] -fn is_hidden(file_path: &DirEntry) -> std::io::Result { - Ok(file_path.file_name().to_string_lossy().starts_with('.')) +fn is_hidden(file_path: &DirEntry) -> bool { + file_path.file_name().to_string_lossy().starts_with('.') } -#[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 should_display(entry: &DirEntry, options: &getopts::Matches) -> bool { +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") && is_hidden(entry).unwrap() { + if config.files == Files::Normal && is_hidden(entry) { return false; } - if options.opt_present("B") && name.ends_with('~') { + if config.ignore_backups && name.ends_with('~') { 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) @@ -341,65 +848,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 || config.numeric_uid_gid { 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)), } } @@ -410,9 +935,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); @@ -421,32 +946,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 @@ -455,8 +993,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.numeric_uid_gid { metadata.uid().to_string() } else { entries::uid2usr(metadata.uid()).unwrap_or_else(|_| metadata.uid().to_string()) @@ -464,8 +1002,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.numeric_uid_gid { metadata.gid().to_string() } else { entries::gid2grp(metadata.gid()).unwrap_or_else(|_| metadata.gid().to_string()) @@ -473,55 +1011,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 NumberPrefix::decimal(metadata.len() as f64) { - NumberPrefix::Standalone(bytes) => bytes.to_string(), - NumberPrefix::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(), } } @@ -551,15 +1117,11 @@ 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; - } - - if options.opt_present("classify") { + if config.classify { let file_type = metadata.file_type(); if file_type.is_dir() { name.push('/'); @@ -568,7 +1130,7 @@ fn display_file_name( } } - 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(); @@ -613,26 +1175,17 @@ 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; + 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 => atty::is(atty::Stream::Stdout), - Some(val) => match val.as_ref() { - "always" | "yes" | "force" => true, - "auto" | "tty" | "if-tty" => atty::is(atty::Stream::Stdout), - /* "never" | "no" | "none" | */ _ => false, - }, - }; - let classify = options.opt_present("classify"); let ext; - if color || classify { + if config.color || config.classify { let file_type = metadata.file_type(); let (code, sym) = if file_type.is_dir() { @@ -682,10 +1235,10 @@ fn display_file_name( ("", None) }; - if color { + if config.color { name = color_name(name, code); } - if classify { + if config.classify { if let Some(s) = sym { name.push(s); width += 1; @@ -693,7 +1246,7 @@ fn display_file_name( } } - 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" }; @@ -710,8 +1263,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/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/more/Cargo.toml b/src/uu/more/Cargo.toml index 3c1746bd4..acd9378b2 100644 --- a/src/uu/more/Cargo.toml +++ b/src/uu/more/Cargo.toml @@ -16,15 +16,15 @@ path = "src/more.rs" [dependencies] getopts = "0.2.18" -uucore = { version=">=0.0.7", package="uucore", path="../../uucore" } -uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } +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 de4e10187..524b0fbc4 100644 --- a/src/uu/more/src/more.rs +++ b/src/uu/more/src/more.rs @@ -17,7 +17,7 @@ use std::io::{stdout, 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; @@ -92,10 +92,10 @@ fn help(usage: &str) { 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 } @@ -110,8 +110,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 @@ -119,9 +119,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"))] @@ -132,8 +132,8 @@ 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); } diff --git a/src/uu/mv/src/mv.rs b/src/uu/mv/src/mv.rs index 6575ad37a..b481aeebc 100644 --- a/src/uu/mv/src/mv.rs +++ b/src/uu/mv/src/mv.rs @@ -380,7 +380,7 @@ fn rename(from: &PathBuf, to: &PathBuf, b: &Behavior) -> io::Result<()> { match b.overwrite { OverwriteMode::NoClobber => return Ok(()), OverwriteMode::Interactive => { - print!("{}: overwrite ‘{}’? ", executable!(), to.display()); + println!("{}: overwrite ‘{}’? ", executable!(), to.display()); if !read_yes() { return Ok(()); } diff --git a/src/uu/nohup/Cargo.toml b/src/uu/nohup/Cargo.toml index 0c5709a65..e9b6f8bd4 100644 --- a/src/uu/nohup/Cargo.toml +++ b/src/uu/nohup/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/nohup.rs" [dependencies] -getopts = "0.2.18" +clap = "2.33" libc = "0.2.42" uucore = { version=">=0.0.7", package="uucore", path="../../uucore", features=["fs"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/nohup/src/nohup.rs b/src/uu/nohup/src/nohup.rs index 67e281e38..afbf2541b 100644 --- a/src/uu/nohup/src/nohup.rs +++ b/src/uu/nohup/src/nohup.rs @@ -10,6 +10,7 @@ #[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; @@ -20,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) }; @@ -73,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() { @@ -108,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) @@ -132,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/src/nproc.rs b/src/uu/nproc/src/nproc.rs index 4eb538618..285cf764f 100644 --- a/src/uu/nproc/src/nproc.rs +++ b/src/uu/nproc/src/nproc.rs @@ -15,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; @@ -89,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" ))] @@ -109,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/od/Cargo.toml b/src/uu/od/Cargo.toml index 14aea59a7..e4db9faf0 100644 --- a/src/uu/od/Cargo.toml +++ b/src/uu/od/Cargo.toml @@ -16,7 +16,7 @@ path = "src/od.rs" [dependencies] byteorder = "1.3.2" -getopts = "0.2.18" +clap = "2.33" half = "1.6" libc = "0.2.42" uucore = { version=">=0.0.7", package="uucore", path="../../uucore" } diff --git a/src/uu/od/src/od.rs b/src/uu/od/src/od.rs index 47d3c29f8..c3b39fca1 100644 --- a/src/uu/od/src/od.rs +++ b/src/uu/od/src/od.rs @@ -41,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 @@ -88,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 { @@ -182,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, @@ -192,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, @@ -223,8 +159,9 @@ 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(_) if matches.occurrences_of(options::WIDTH) == 0 => 16, Some(s) => s.parse::().unwrap_or(0), }; let min_bytes = formats.iter().fold(1, |max, next| { @@ -235,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), @@ -247,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 { @@ -286,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; 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/pathchk/Cargo.toml b/src/uu/pathchk/Cargo.toml index 452199f4f..c1d0d0dfa 100644 --- a/src/uu/pathchk/Cargo.toml +++ b/src/uu/pathchk/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/pathchk.rs" [dependencies] -getopts = "0.2.18" +clap = "2.33" libc = "0.2.42" uucore = { version=">=0.0.7", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/pathchk/src/pathchk.rs b/src/uu/pathchk/src/pathchk.rs index 745d0d36c..c27e52513 100644 --- a/src/uu/pathchk/src/pathchk.rs +++ b/src/uu/pathchk/src/pathchk.rs @@ -12,7 +12,7 @@ #[macro_use] extern crate uucore; -use getopts::Options; +use clap::{App, Arg}; use std::fs; use std::io::{ErrorKind, Write}; @@ -22,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/ptx/Cargo.toml b/src/uu/ptx/Cargo.toml index 3a91feeb0..d1e0267b6 100644 --- a/src/uu/ptx/Cargo.toml +++ b/src/uu/ptx/Cargo.toml @@ -15,6 +15,7 @@ edition = "2018" path = "src/ptx.rs" [dependencies] +clap = "2.33" aho-corasick = "0.7.3" getopts = "0.2.18" libc = "0.2.42" diff --git a/src/uu/ptx/src/ptx.rs b/src/uu/ptx/src/ptx.rs index bfcd6699f..989ab52ef 100644 --- a/src/uu/ptx/src/ptx.rs +++ b/src/uu/ptx/src/ptx.rs @@ -10,7 +10,7 @@ #[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}; @@ -20,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 { @@ -61,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() { @@ -81,23 +90,29 @@ 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"); } // Ignore empty string regex from cmd-line-args - let arg_reg: Option = if matches.opt_present("W") { - matches.opt_str("W").filter(|reg| !reg.is_empty()) + 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 { None }; @@ -131,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 @@ -494,102 +504,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/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/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/stdbuf.rs b/src/uu/stdbuf/src/stdbuf.rs index 86523144c..67ed9a838 100644 --- a/src/uu/stdbuf/src/stdbuf.rs +++ b/src/uu/stdbuf/src/stdbuf.rs @@ -57,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") } @@ -67,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/tail/src/tail.rs b/src/uu/tail/src/tail.rs index cd391a53e..3a6b04b29 100644 --- a/src/uu/tail/src/tail.rs +++ b/src/uu/tail/src/tail.rs @@ -80,6 +80,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .short("c") .long(options::BYTES) .takes_value(true) + .allow_hyphen_values(true) .help("Number of bytes to print"), ) .arg( @@ -93,6 +94,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .short("n") .long(options::LINES) .takes_value(true) + .allow_hyphen_values(true) .help("Number of lines to print"), ) .arg( @@ -343,9 +345,9 @@ 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))) } } diff --git a/src/uu/tee/Cargo.toml b/src/uu/tee/Cargo.toml index ee18e888f..99a6ec23e 100644 --- a/src/uu/tee/Cargo.toml +++ b/src/uu/tee/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/tee.rs" [dependencies] -getopts = "0.2.18" +clap = "2.33.3" libc = "0.2.42" uucore = { version=">=0.0.7", package="uucore", path="../../uucore", features=["libc"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/tee/src/tee.rs b/src/uu/tee/src/tee.rs index e29945382..c54fa0d16 100644 --- a/src/uu/tee/src/tee.rs +++ b/src/uu/tee/src/tee.rs @@ -5,6 +5,10 @@ // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. +#[macro_use] +extern crate uucore; + +use clap::{App, Arg}; use std::fs::OpenOptions; use std::io::{copy, sink, stdin, stdout, Error, ErrorKind, Read, Result, Write}; use std::path::{Path, PathBuf}; @@ -12,80 +16,61 @@ use std::path::{Path, PathBuf}; #[cfg(unix)] use uucore::libc; -static NAME: &str = "tee"; static VERSION: &str = env!("CARGO_PKG_VERSION"); +static ABOUT: &str = "Copy standard input to each FILE, and also to standard output."; -pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); - - match options(&args).and_then(exec) { - Ok(_) => 0, - Err(_) => 1, - } +mod options { + pub const APPEND: &str = "append"; + pub const IGNORE_INTERRUPTS: &str = "ignore-interrupts"; + pub const FILE: &str = "file"; } #[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 (ignored on non-Unix platforms)", - ); - 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 get_usage() -> String { + format!("{0} [OPTION]... [FILE]...", executable!()) } -fn exec(options: Options) -> Result<()> { - match options.print_and_exit { - Some(text) => { - println!("{}", text); - Ok(()) - } - None => tee(options), +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("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, } } @@ -173,7 +158,7 @@ impl Write for NamedWriter { match self.inner.write(buf) { Err(f) => { self.inner = Box::new(sink()) as Box; - warn(format!("{}: {}", self.path.display(), f.to_string()).as_ref()); + show_warning!("{}: {}", self.path.display(), f.to_string()); Err(f) } okay => okay, @@ -184,7 +169,7 @@ impl Write for NamedWriter { match self.inner.flush() { Err(f) => { self.inner = Box::new(sink()) as Box; - warn(format!("{}: {}", self.path.display(), f.to_string()).as_ref()); + show_warning!("{}: {}", self.path.display(), f.to_string()); Err(f) } okay => okay, @@ -200,15 +185,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_warning!("{}: {}", 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/tsort/Cargo.toml b/src/uu/tsort/Cargo.toml index 10672a9e0..d870e0155 100644 --- a/src/uu/tsort/Cargo.toml +++ b/src/uu/tsort/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/tsort.rs" [dependencies] -getopts = "0.2.18" +clap= "2.33" uucore = { version=">=0.0.7", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/tsort/src/tsort.rs b/src/uu/tsort/src/tsort.rs index 0a0023031..3440972a2 100644 --- a/src/uu/tsort/src/tsort.rs +++ b/src/uu/tsort/src/tsort.rs @@ -9,49 +9,35 @@ #[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; diff --git a/src/uu/uname/src/uname.rs b/src/uu/uname/src/uname.rs index 6575aa9fd..4586a084f 100644 --- a/src/uu/uname/src/uname.rs +++ b/src/uu/uname/src/uname.rs @@ -39,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"; diff --git a/src/uu/unexpand/Cargo.toml b/src/uu/unexpand/Cargo.toml index ec6967e21..d66d335bf 100644 --- a/src/uu/unexpand/Cargo.toml +++ b/src/uu/unexpand/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/unexpand.rs" [dependencies] -getopts = "0.2.18" +clap = "2.33" unicode-width = "0.1.5" uucore = { version=">=0.0.7", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/unexpand/src/unexpand.rs b/src/uu/unexpand/src/unexpand.rs index 59a2dc6a0..5b08c33cf 100644 --- a/src/uu/unexpand/src/unexpand.rs +++ b/src/uu/unexpand/src/unexpand.rs @@ -11,7 +11,7 @@ #[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; @@ -19,6 +19,9 @@ 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; @@ -46,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, @@ -54,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 { @@ -82,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/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/uucore/src/lib/features/entries.rs b/src/uucore/src/lib/features/entries.rs index a921af2d0..d2dce2461 100644 --- a/src/uucore/src/lib/features/entries.rs +++ b/src/uucore/src/lib/features/entries.rs @@ -34,7 +34,7 @@ //! assert!(entries::Group::locate(root_group).is_ok()); //! ``` -#[cfg(any(target_os = "freebsd", target_os = "macos"))] +#[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}; @@ -119,19 +119,19 @@ impl Passwd { } /// AKA passwd.pw_class - #[cfg(any(target_os = "freebsd", target_os = "macos"))] + #[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_os = "macos"))] + #[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_os = "macos"))] + #[cfg(any(target_os = "freebsd", target_vendor = "apple"))] pub fn expiration(&self) -> time_t { self.inner.pw_expire } diff --git a/src/uucore/src/lib/features/signals.rs b/src/uucore/src/lib/features/signals.rs index 294669eab..d22fa1791 100644 --- a/src/uucore/src/lib/features/signals.rs +++ b/src/uucore/src/lib/features/signals.rs @@ -197,7 +197,7 @@ No Name Default Action Description */ -#[cfg(any(target_os = "macos", target_os = "freebsd"))] +#[cfg(any(target_vendor = "apple", target_os = "freebsd"))] pub static ALL_SIGNALS: [Signal<'static>; 31] = [ Signal { name: "HUP", diff --git a/src/uucore/src/lib/features/utmpx.rs b/src/uucore/src/lib/features/utmpx.rs index 31cd3b72c..0308d8a5e 100644 --- a/src/uucore/src/lib/features/utmpx.rs +++ b/src/uucore/src/lib/features/utmpx.rs @@ -47,7 +47,7 @@ use libc::utmpx; pub use libc::endutxent; pub use libc::getutxent; pub use libc::setutxent; -#[cfg(any(target_os = "macos", target_os = "linux"))] +#[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 { @@ -85,7 +85,7 @@ mod ut { pub use libc::USER_PROCESS; } -#[cfg(target_os = "macos")] +#[cfg(target_vendor = "apple")] mod ut { pub static DEFAULT_FILE: &str = "/var/run/utmpx"; diff --git a/src/uucore/src/lib/macros.rs b/src/uucore/src/lib/macros.rs index 0aa6d8f0a..6836f81aa 100644 --- a/src/uucore/src/lib/macros.rs +++ b/src/uucore/src/lib/macros.rs @@ -5,6 +5,9 @@ // 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( () => ({ @@ -18,6 +21,7 @@ macro_rules! executable( }) ); +/// Show an error to stderr in a silimar style to GNU coreutils. #[macro_export] macro_rules! show_error( ($($args:tt)+) => ({ @@ -26,6 +30,7 @@ macro_rules! show_error( }) ); +/// Show a warning to stderr in a silimar style to GNU coreutils. #[macro_export] macro_rules! show_warning( ($($args:tt)+) => ({ @@ -34,6 +39,7 @@ macro_rules! show_warning( }) ); +/// Show an info message to stderr in a silimar style to GNU coreutils. #[macro_export] macro_rules! show_info( ($($args:tt)+) => ({ @@ -42,6 +48,7 @@ macro_rules! show_info( }) ); +/// Show a bad inocation help message in a similar style to GNU coreutils. #[macro_export] macro_rules! show_usage_error( ($($args:tt)+) => ({ @@ -51,6 +58,7 @@ macro_rules! show_usage_error( }) ); +/// Display the provided error message, then `exit()` with the provided exit code #[macro_export] macro_rules! crash( ($exit_code:expr, $($args:tt)+) => ({ @@ -59,6 +67,7 @@ macro_rules! crash( }) ); +/// Calls `exit()` with the provided exit code. #[macro_export] macro_rules! exit( ($exit_code:expr) => ({ @@ -66,6 +75,8 @@ macro_rules! exit( }) ); +/// 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) => ( @@ -76,6 +87,9 @@ macro_rules! crash_if_err( ) ); +/// 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) => ( @@ -109,6 +123,8 @@ macro_rules! safe_writeln( ) ); +/// Unwraps the Result. Instead of panicking, it exists the program with exit +/// code 1. #[macro_export] macro_rules! safe_unwrap( ($exp:expr) => ( diff --git a/tests/by-util/test_cat.rs b/tests/by-util/test_cat.rs index a3e321139..b194eb9b0 100644 --- a/tests/by-util/test_cat.rs +++ b/tests/by-util/test_cat.rs @@ -10,16 +10,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-^?", ); } @@ -30,7 +30,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 d5afaf3a7..613f52fd2 100644 --- a/tests/by-util/test_chgrp.rs +++ b/tests/by-util/test_chgrp.rs @@ -115,7 +115,7 @@ fn test_reference() { } #[test] -#[cfg(target_os = "macos")] +#[cfg(target_vendor = "apple")] fn test_reference() { new_ucmd!() .arg("-v") 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_cp.rs b/tests/by-util/test_cp.rs index b96bd4e29..a00ed2fd2 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -31,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() { @@ -1001,3 +1007,70 @@ fn test_cp_target_file_dev_null() { 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_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..a79f820fb 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,11 @@ 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(not(target_vendor = "apple"))] fn _du_basics_subdir(s: String) { // MS-WSL linux has altered expected output if !is_wsl() { @@ -80,12 +80,12 @@ 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(not(target_vendor = "apple"))] fn _du_soft_link(s: String) { // MS-WSL linux has altered expected output if !is_wsl() { @@ -109,11 +109,11 @@ 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(not(target_vendor = "apple"))] fn _du_hard_link(s: String) { // MS-WSL linux has altered expected output if !is_wsl() { @@ -133,11 +133,11 @@ 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(not(target_vendor = "apple"))] fn _du_d_flag(s: String) { // MS-WSL linux has altered expected output if !is_wsl() { 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_head.rs b/tests/by-util/test_head.rs index 4324290cb..a1086c004 100644 --- a/tests/by-util/test_head.rs +++ b/tests/by-util/test_head.rs @@ -86,6 +86,14 @@ fn test_verbose() { .stdout_is_fixture("lorem_ipsum_verbose.expected"); } +#[test] +fn test_zero_terminated() { + new_ucmd!() + .args(&["-z", "zero_terminated.txt"]) + .run() + .stdout_is_fixture("zero_terminated.expected"); +} + #[test] #[ignore] fn test_spams_newline() { @@ -159,3 +167,15 @@ fn test_bug_in_negative_zero_lines() { //GNU Head returns "a\nb\n" .stdout_is(""); } + +#[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") + ) +} 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_install.rs b/tests/by-util/test_install.rs index 7b3706f9e..89dfb0e56 100644 --- a/tests/by-util/test_install.rs +++ b/tests/by-util/test_install.rs @@ -17,9 +17,9 @@ fn test_install_help() { #[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); @@ -34,7 +34,7 @@ fn test_install_basic() { #[test] fn test_install_twice_dir() { - let dir = "test_install_target_dir_dir_a"; + let dir = "dir"; let scene = TestScenario::new(util_name!()); scene.ucmd().arg("-d").arg(dir).succeeds(); @@ -47,9 +47,9 @@ fn test_install_twice_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); @@ -66,8 +66,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); @@ -86,9 +86,9 @@ fn test_install_unimplemented_arg() { #[test] fn test_install_component_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 component1 = "component1"; + let component2 = "component2"; + let component3 = "component3"; let directories_arg = "-d"; ucmd.args(&[directories_arg, component1, component2, component3]) @@ -104,10 +104,10 @@ fn test_install_component_directories() { fn test_install_mode_numeric() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; - let dir = "test_install_target_dir_dir_e"; - let dir2 = "test_install_target_dir_dir_e2"; + let dir = "dir1"; + let dir2 = "dir2"; - let file = "test_install_target_dir_file_e"; + let file = "file"; let mode_arg = "--mode=333"; at.touch(file); @@ -145,8 +145,8 @@ fn test_install_mode_numeric() { #[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); @@ -163,8 +163,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); @@ -185,7 +185,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"; @@ -203,8 +203,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); @@ -217,8 +217,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); @@ -234,8 +234,8 @@ fn test_install_target_new_file() { #[test] fn test_install_target_new_file_with_group() { 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"; let gid = get_effective_gid(); at.touch(file); @@ -264,8 +264,8 @@ fn test_install_target_new_file_with_group() { #[test] fn test_install_target_new_file_with_owner() { 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"; let uid = get_effective_uid(); at.touch(file); @@ -294,9 +294,9 @@ fn test_install_target_new_file_with_owner() { #[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); @@ -312,8 +312,8 @@ fn test_install_target_new_file_failing_nonexistent_parent() { #[test] fn test_install_preserve_timestamps() { let (at, mut ucmd) = at_and_ucmd!(); - let file1 = "test_install_target_dir_file_a1"; - let file2 = "test_install_target_dir_file_a2"; + let file1 = "source_file"; + let file2 = "target_file"; at.touch(file1); ucmd.arg(file1).arg(file2).arg("-p").succeeds().no_stderr(); @@ -338,8 +338,8 @@ fn test_install_preserve_timestamps() { #[test] fn test_install_copy_file() { let (at, mut ucmd) = at_and_ucmd!(); - let file1 = "test_install_target_dir_file_a1"; - let file2 = "test_install_target_dir_file_a2"; + let file1 = "source_file"; + let file2 = "target_file"; at.touch(file1); ucmd.arg(file1).arg(file2).succeeds().no_stderr(); @@ -353,8 +353,57 @@ fn test_install_copy_file() { fn test_install_target_file_dev_null() { let (at, mut ucmd) = at_and_ucmd!(); let file1 = "/dev/null"; - let file2 = "test_install_target_file_file_i2"; + let file2 = "target_file"; ucmd.arg(file1).arg(file2).succeeds().no_stderr(); 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")); +} diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index 422db8df9..091d47234 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -57,6 +57,200 @@ fn test_ls_a() { 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))] @@ -71,16 +265,20 @@ fn test_ls_long() { } } - let (at, mut ucmd) = at_and_ucmd!(); + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; at.touch(&at.plus_as_string("test-long")); - let result = ucmd.arg("-l").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")); + 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))] { @@ -90,6 +288,126 @@ fn test_ls_long() { } } +#[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(); + + // 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(); + + // Regex for one name: author, group or owner + let re_one = Regex::new(r"[xrw-]{9} \d [-0-9_a-z]+ 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)); + + 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)); + } + + 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)); + } + + 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)); + } +} + +#[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!()); @@ -166,27 +484,55 @@ fn test_ls_order_size() { } #[test] -fn test_ls_order_creation() { +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(500)); + sleep(Duration::from_millis(100)); at.touch("test-2"); at.append("test-2", "22"); - sleep(Duration::from_millis(500)); + + sleep(Duration::from_millis(100)); at.touch("test-3"); at.append("test-3", "333"); - sleep(Duration::from_millis(500)); + 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); @@ -196,7 +542,7 @@ fn test_ls_order_creation() { #[cfg(windows)] assert_eq!(result.stdout, "test-4 test-3 test-2 test-1\n"); - let result = scene.ucmd().arg("-t").arg("-r").run(); + let result = scene.ucmd().arg("-tr").run(); println!("stderr = {:?}", result.stderr); println!("stdout = {:?}", result.stdout); assert!(result.success); @@ -204,6 +550,41 @@ fn test_ls_order_creation() { 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] @@ -270,45 +651,197 @@ fn test_ls_recursive() { 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/a")); - scene.ucmd().arg("--color").succeeds(); - scene.ucmd().arg("--color=always").succeeds(); - scene.ucmd().arg("--color=never").succeeds(); - scene.ucmd().arg("--color").arg("a").succeeds(); - scene.ucmd().arg("--color=always").arg("a/a").succeeds(); - scene.ucmd().arg("--color=never").arg("z").succeeds(); + 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(); + 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(not(any(target_os = "macos", target_os = "windows")))] // Truncate not available on mac or win +#[cfg(unix)] #[test] -fn test_ls_human() { +fn test_ls_inode() { let scene = TestScenario::new(util_name!()); - let file = "test_human"; - let result = scene.cmd("truncate").arg("-s").arg("+1000").arg(file).run(); + 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); - let result = scene.ucmd().arg("-hl").arg(file).run(); + 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) +} + +#[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("1.00K")); + 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(file) + .arg(file1) .run(); - let result = scene.ucmd().arg("-hl").arg(file).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("1.02M")); + 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)] @@ -336,3 +869,81 @@ fn test_ls_hidden_windows() { 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,) +} diff --git a/tests/by-util/test_nice.rs b/tests/by-util/test_nice.rs index e10314f57..7e704fc00 100644 --- a/tests/by-util/test_nice.rs +++ b/tests/by-util/test_nice.rs @@ -15,7 +15,9 @@ fn test_negative_adjustment() { // correctly. let res = new_ucmd!().args(&["-n", "-1", "true"]).run(); - assert!(res.stderr.starts_with("nice: warning: setpriority: Permission denied")); + assert!(res + .stderr + .starts_with("nice: warning: setpriority: Permission denied")); } #[test] 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_pathchk.rs b/tests/by-util/test_pathchk.rs index bdce377b3..e24a464e0 100644 --- a/tests/by-util/test_pathchk.rs +++ b/tests/by-util/test_pathchk.rs @@ -9,7 +9,7 @@ fn test_default_mode() { // fail on long inputs new_ucmd!() - .args(&[repeat_str("test", 20000)]) + .args(&["test".repeat(20000)]) .fails() .no_stdout(); } diff --git a/tests/by-util/test_rm.rs b/tests/by-util/test_rm.rs index 149d509c5..40cc6839a 100644 --- a/tests/by-util/test_rm.rs +++ b/tests/by-util/test_rm.rs @@ -177,7 +177,7 @@ fn test_rm_directory_without_flag() { let dir = "test_rm_directory_without_flag_dir"; at.mkdir(dir); - + let result = ucmd.arg(dir).fails(); println!("{}", result.stderr); assert!(result 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_tail.rs b/tests/by-util/test_tail.rs index 458fc6aa7..5edff4d55 100644 --- a/tests/by-util/test_tail.rs +++ b/tests/by-util/test_tail.rs @@ -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_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_unexpand.rs b/tests/by-util/test_unexpand.rs index 93cc42d90..e8b880287 100644 --- a/tests/by-util/test_unexpand.rs +++ b/tests/by-util/test_unexpand.rs @@ -136,3 +136,22 @@ fn unexpand_spaces_after_fields() { .run() .stdout_is("\t\tA B C D\t\t A\t\n"); } + +#[test] +fn unexpand_read_from_file() { + new_ucmd!() + .arg("with_spaces.txt") + .arg("-t4") + .run() + .success(); +} + +#[test] +fn unexpand_read_from_two_file() { + new_ucmd!() + .arg("with_spaces.txt") + .arg("with_spaces.txt") + .arg("-t4") + .run() + .success(); +} diff --git a/tests/by-util/test_unlink.rs b/tests/by-util/test_unlink.rs index daac6d3b3..fa8f962c4 100644 --- a/tests/by-util/test_unlink.rs +++ b/tests/by-util/test_unlink.rs @@ -23,7 +23,7 @@ fn test_unlink_multiple_files() { ucmd.arg(file_a).arg(file_b).fails().stderr_is( "unlink: error: extra operand: 'test_unlink_multiple_file_b'\nTry 'unlink --help' \ - for more information.\n", + for more information.\n", ); } @@ -36,7 +36,7 @@ fn test_unlink_directory() { ucmd.arg(dir).fails().stderr_is( "unlink: error: cannot unlink 'test_unlink_empty_directory': Not a regular file \ - or symlink\n", + or symlink\n", ); } @@ -46,6 +46,6 @@ fn test_unlink_nonexistent() { new_ucmd!().arg(file).fails().stderr_is( "unlink: error: Cannot stat 'test_unlink_nonexistent': No such file or directory \ - (os error 2)\n", + (os error 2)\n", ); } diff --git a/tests/by-util/test_wc.rs b/tests/by-util/test_wc.rs index fc9c00ecc..b064d7e0e 100644 --- a/tests/by-util/test_wc.rs +++ b/tests/by-util/test_wc.rs @@ -81,6 +81,6 @@ fn test_multiple_default() { .run() .stdout_is( " 13 109 772 lorem_ipsum.txt\n 18 204 1115 moby_dick.txt\n 5 57 302 \ - alice_in_wonderland.txt\n 36 370 2189 total\n", + alice_in_wonderland.txt\n 36 370 2189 total\n", ); } diff --git a/tests/common/macros.rs b/tests/common/macros.rs index 645cfcc67..32ff7cbe8 100644 --- a/tests/common/macros.rs +++ b/tests/common/macros.rs @@ -1,3 +1,6 @@ +/// Assertion helper macro for [`CmdResult`] types +/// +/// [`CmdResult`]: crate::tests::common::util::CmdResult #[macro_export] macro_rules! assert_empty_stderr( ($cond:expr) => ( @@ -7,6 +10,9 @@ macro_rules! assert_empty_stderr( ); ); +/// Assertion helper macro for [`CmdResult`] types +/// +/// [`CmdResult`]: crate::tests::common::util::CmdResult #[macro_export] macro_rules! assert_empty_stdout( ($cond:expr) => ( @@ -16,6 +22,9 @@ macro_rules! assert_empty_stdout( ); ); +/// Assertion helper macro for [`CmdResult`] types +/// +/// [`CmdResult`]: crate::tests::common::util::CmdResult #[macro_export] macro_rules! assert_no_error( ($cond:expr) => ( @@ -26,6 +35,7 @@ macro_rules! assert_no_error( ); ); +/// 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 0f1acd49a..a2fab66c6 100644 --- a/tests/common/util.rs +++ b/tests/common/util.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] + use std::env; use std::ffi::OsStr; use std::fs::{self, File, OpenOptions}; @@ -27,7 +29,7 @@ static ALREADY_RUN: &str = " you have already run this UCommand, if you want to 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")) @@ -55,14 +57,6 @@ fn read_scenario_fixture>(tmpd: &Option>, file_rel_p 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 -} - /// A command result is the outputs of a command (streams and status code) /// within a struct which has convenience assertion functions about those outputs #[derive(Debug)] @@ -384,8 +378,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, @@ -420,12 +416,16 @@ 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) } @@ -495,6 +495,8 @@ impl UCommand { ucmd } + /// Add a parameter to the invocation. Path arguments are treated relative + /// to the test environment directory. pub fn arg>(&mut self, arg: S) -> Box<&mut UCommand> { if self.has_run { panic!(ALREADY_RUN); @@ -505,6 +507,8 @@ impl UCommand { Box::new(self) } + /// Add multiple parameters to the invocation. Path arguments are treated relative + /// to the test environment directory. pub fn args>(&mut self, args: &[S]) -> Box<&mut UCommand> { if self.has_run { panic!(MULTIPLE_STDIN_MEANINGLESS); 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/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/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