1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-08-01 13:37:48 +00:00

Merge branch 'master' into implement-more

This commit is contained in:
Sylvestre Ledru 2021-05-28 19:49:48 +02:00 committed by GitHub
commit fe42808e9b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
146 changed files with 6701 additions and 4057 deletions

View file

@ -1,7 +1,14 @@
env:
# Temporary workaround for error `error: sysinfo not supported on
# this platform` seen on FreeBSD platforms, affecting Rustup
#
# References: https://github.com/rust-lang/rustup/issues/2774
RUSTUP_IO_THREADS: 1
task: task:
name: stable x86_64-unknown-freebsd-12 name: stable x86_64-unknown-freebsd-12
freebsd_instance: freebsd_instance:
image: freebsd-12-1-release-amd64 image: freebsd-12-2-release-amd64
setup_script: setup_script:
- pkg install -y curl gmake - pkg install -y curl gmake
- curl https://sh.rustup.rs -sSf --output rustup.sh - curl https://sh.rustup.rs -sSf --output rustup.sh

View file

@ -11,7 +11,7 @@ env:
PROJECT_NAME: coreutils PROJECT_NAME: coreutils
PROJECT_DESC: "Core universal (cross-platform) utilities" PROJECT_DESC: "Core universal (cross-platform) utilities"
PROJECT_AUTH: "uutils" PROJECT_AUTH: "uutils"
RUST_MIN_SRV: "1.40.0" ## v1.40.0 RUST_MIN_SRV: "1.43.1" ## v1.43.0
RUST_COV_SRV: "2020-08-01" ## (~v1.47.0) supported rust version for code coverage; (date required/used by 'coverage') ## !maint: refactor when code coverage support is included in the stable channel RUST_COV_SRV: "2020-08-01" ## (~v1.47.0) supported rust version for code coverage; (date required/used by 'coverage') ## !maint: refactor when code coverage support is included in the stable channel
on: [push, pull_request] on: [push, pull_request]
@ -235,6 +235,9 @@ jobs:
arm-unknown-linux-gnueabihf) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;; arm-unknown-linux-gnueabihf) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;;
aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;; aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;;
esac esac
case '${{ matrix.job.os }}' in
macos-latest) brew install coreutils ;; # needed for testing
esac
- name: Initialize workflow variables - name: Initialize workflow variables
id: vars id: vars
shell: bash shell: bash
@ -360,6 +363,10 @@ jobs:
mkdir -p '${{ steps.vars.outputs.STAGING }}/dpkg' mkdir -p '${{ steps.vars.outputs.STAGING }}/dpkg'
- name: rust toolchain ~ install - name: rust toolchain ~ install
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
env:
# Override auto-detection of RAM for Rustc install.
# https://github.com/rust-lang/rustup/issues/2229#issuecomment-585855925
RUSTUP_UNPACK_RAM: "21474836480"
with: with:
toolchain: ${{ steps.vars.outputs.TOOLCHAIN }} toolchain: ${{ steps.vars.outputs.TOOLCHAIN }}
target: ${{ matrix.job.target }} target: ${{ matrix.job.target }}
@ -486,6 +493,13 @@ jobs:
- { os: windows-latest , features: windows } - { os: windows-latest , features: windows }
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- name: Install/setup prerequisites
shell: bash
run: |
## install/setup prerequisites
case '${{ matrix.job.os }}' in
macos-latest) brew install coreutils ;; # needed for testing
esac
# - name: Reattach HEAD ## may be needed for accurate code coverage info # - name: Reattach HEAD ## may be needed for accurate code coverage info
# run: git checkout ${{ github.head_ref }} # run: git checkout ${{ github.head_ref }}
- name: Initialize workflow variables - name: Initialize workflow variables

View file

@ -12,16 +12,18 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
with: with:
path: 'uutils' path: 'uutils'
- name: Chechout GNU coreutils - name: Checkout GNU coreutils
uses: actions/checkout@v2 uses: actions/checkout@v2
with: with:
repository: 'coreutils/coreutils' repository: 'coreutils/coreutils'
path: 'gnu' path: 'gnu'
- name: Chechout GNU corelib ref: v8.32
- name: Checkout GNU corelib
uses: actions/checkout@v2 uses: actions/checkout@v2
with: with:
repository: 'coreutils/gnulib' repository: 'coreutils/gnulib'
path: 'gnulib' path: 'gnulib'
ref: 8e99f24c0931a38880c6ee9b8287c7da80b0036b
fetch-depth: 0 # gnu gets upset if gnulib is a shallow checkout fetch-depth: 0 # gnu gets upset if gnulib is a shallow checkout
- name: Install `rust` toolchain - name: Install `rust` toolchain
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
@ -30,103 +32,43 @@ jobs:
default: true default: true
profile: minimal # minimal component installation (ie, no documentation) profile: minimal # minimal component installation (ie, no documentation)
components: rustfmt components: rustfmt
- name: Build binaries - name: Install deps
shell: bash shell: bash
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install autoconf autopoint bison texinfo gperf gcc g++ gdb python-pyinotify python3-sphinx sudo apt-get install autoconf autopoint bison texinfo gperf gcc g++ gdb python-pyinotify python3-sphinx jq
pushd uutils - name: Build binaries
make PROFILE=release shell: bash
BUILDDIR="$PWD/target/release/" run: |
cp "${BUILDDIR}/install" "${BUILDDIR}/ginstall" # The GNU tests rename this script before running, to avoid confusion with the make target cd uutils
# Create *sum binaries bash util/build-gnu.sh
for sum in b2sum md5sum sha1sum sha224sum sha256sum sha384sum sha512sum
do
sum_path="${BUILDDIR}/${sum}"
test -f "${sum_path}" || cp "${BUILDDIR}/hashsum" "${sum_path}"
done
test -f "${BUILDDIR}/[" || cp "${BUILDDIR}/test" "${BUILDDIR}/["
popd
GNULIB_SRCDIR="$PWD/gnulib"
pushd gnu/
# Any binaries that aren't built become `false` so their tests fail
for binary in $(./build-aux/gen-lists-of-programs.sh --list-progs)
do
bin_path="${BUILDDIR}/${binary}"
test -f "${bin_path}" || { echo "'${binary}' was not built with uutils, using the 'false' program"; cp "${BUILDDIR}/false" "${bin_path}"; }
done
./bootstrap --gnulib-srcdir="$GNULIB_SRCDIR"
./configure --quiet --disable-gcc-warnings
#Add timeout to to protect against hangs
sed -i 's|"\$@|/usr/bin/timeout 600 "\$@|' build-aux/test-driver
# Change the PATH in the Makefile to test the uutils coreutils instead of the GNU coreutils
sed -i "s/^[[:blank:]]*PATH=.*/ PATH='${BUILDDIR//\//\\/}\$(PATH_SEPARATOR)'\"\$\$PATH\" \\\/" Makefile
sed -i 's| tr | /usr/bin/tr |' tests/init.sh
make
# Generate the factor tests, so they can be fixed
for i in {00..36}
do
make tests/factor/t${i}.sh
done
grep -rl 'path_prepend_' tests/* | xargs sed -i 's|path_prepend_ ./src||'
sed -i -e 's|^seq |/usr/bin/seq |' -e 's|sha1sum |/usr/bin/sha1sum |' tests/factor/t*sh
# Remove tests checking for --version & --help
# Not really interesting for us and logs are too big
sed -i -e '/tests\/misc\/invalid-opt.pl/ D' \
-e '/tests\/misc\/help-version.sh/ D' \
-e '/tests\/misc\/help-version-getopt.sh/ D' \
Makefile
# printf doesn't limit the values used in its arg, so this produced ~2GB of output
sed -i '/INT_OFLOW/ D' tests/misc/printf.sh
# Use the system coreutils where the test fails due to error in a util that is not the one being tested
sed -i 's|stat|/usr/bin/stat|' tests/chgrp/basic.sh tests/cp/existing-perm-dir.sh tests/touch/60-seconds.sh tests/misc/sort-compress-proc.sh
sed -i 's|ls -|/usr/bin/ls -|' tests/chgrp/posix-H.sh tests/chown/deref.sh tests/cp/same-file.sh tests/misc/mknod.sh tests/mv/part-symlink.sh tests/du/8gb.sh
sed -i 's|mkdir |/usr/bin/mkdir |' tests/cp/existing-perm-dir.sh tests/rm/empty-inacc.sh
sed -i 's|timeout \([[:digit:]]\)| /usr/bin/timeout \1|' tests/tail-2/inotify-rotate.sh tests/tail-2/inotify-dir-recreate.sh tests/tail-2/inotify-rotate-resources.sh tests/cp/parent-perm-race.sh tests/ls/infloop.sh tests/misc/sort-exit-early.sh tests/misc/sort-NaN-infloop.sh tests/misc/uniq-perf.sh tests/tail-2/inotify-only-regular.sh tests/tail-2/pipe-f2.sh tests/tail-2/retry.sh tests/tail-2/symlink.sh tests/tail-2/wait.sh tests/tail-2/pid.sh tests/dd/stats.sh tests/tail-2/follow-name.sh tests/misc/shuf.sh # Don't break the function called 'grep_timeout'
sed -i 's|chmod |/usr/bin/chmod |' tests/du/inacc-dir.sh tests/mkdir/p-3.sh tests/tail-2/tail-n0f.sh tests/cp/fail-perm.sh tests/du/inaccessible-cwd.sh tests/mv/i-2.sh tests/chgrp/basic.sh tests/misc/shuf.sh
sed -i 's|sort |/usr/bin/sort |' tests/ls/hyperlink.sh tests/misc/test-N.sh
sed -i 's|split |/usr/bin/split |' tests/misc/factor-parallel.sh
sed -i 's|truncate |/usr/bin/truncate |' tests/split/fail.sh
sed -i 's|dd |/usr/bin/dd |' tests/du/8gb.sh tests/tail-2/big-4gb.sh tests/cp/fiemap-2.sh init.cfg
sed -i 's|id -|/usr/bin/id -|' tests/misc/runcon-no-reorder.sh
sed -i 's|touch |/usr/bin/touch |' tests/cp/preserve-link.sh tests/cp/reflink-perm.sh tests/ls/block-size.sh tests/ls/abmon-align.sh tests/ls/rt-1.sh tests/mv/update.sh tests/misc/ls-time.sh tests/misc/stat-nanoseconds.sh tests/misc/time-style.sh tests/misc/test-N.sh
sed -i 's|ln -|/usr/bin/ln -|' tests/cp/link-deref.sh
sed -i 's|printf |/usr/bin/printf |' tests/dd/ascii.sh
sed -i 's|cp |/usr/bin/cp |' tests/mv/hard-2.sh
sed -i 's|paste |/usr/bin/paste |' tests/misc/od-endian.sh
sed -i 's|seq |/usr/bin/seq |' tests/misc/sort-discrim.sh
#Add specific timeout to tests that currently hang to limit time spent waiting
sed -i 's|seq \$|/usr/bin/timeout 0.1 seq \$|' tests/misc/seq-precision.sh tests/misc/seq-long-double.sh
sed -i 's|cat |/usr/bin/timeout 0.1 cat |' tests/misc/cat-self.sh
test -f "${BUILDDIR}/getlimits" || cp src/getlimits "${BUILDDIR}"
- name: Run GNU tests - name: Run GNU tests
shell: bash shell: bash
run: | run: |
BUILDDIR="${PWD}/uutils/target/release" bash uutils/util/run-gnu-test.sh
GNULIB_DIR="${PWD}/gnulib"
pushd gnu
timeout -sKILL 2h make -j "$(nproc)" check SUBDIRS=. RUN_EXPENSIVE_TESTS=yes RUN_VERY_EXPENSIVE_TESTS=yes VERBOSE=no || : # Kill after 4 hours in case something gets stuck in make
- name: Extract tests info - name: Extract tests info
shell: bash shell: bash
run: | run: |
if test -f gnu/tests/test-suite.log LOG_FILE=gnu/tests/test-suite.log
if test -f "$LOG_FILE"
then then
TOTAL=$( grep "# TOTAL:" gnu/tests/test-suite.log|cut -d' ' -f2-) TOTAL=$(sed -n "s/.*# TOTAL: \(.*\)/\1/p" "$LOG_FILE"|tr -d '\r'|head -n1)
PASS=$( grep "# PASS:" gnu/tests/test-suite.log|cut -d' ' -f2-) PASS=$(sed -n "s/.*# PASS: \(.*\)/\1/p" "$LOG_FILE"|tr -d '\r'|head -n1)
SKIP=$( grep "# SKIP:" gnu/tests/test-suite.log|cut -d' ' -f2-) SKIP=$(sed -n "s/.*# SKIP: \(.*\)/\1/p" "$LOG_FILE"|tr -d '\r'|head -n1)
FAIL=$( grep "# FAIL:" gnu/tests/test-suite.log|cut -d' ' -f2-) FAIL=$(sed -n "s/.*# FAIL: \(.*\)/\1/p" "$LOG_FILE"|tr -d '\r'|head -n1)
XPASS=$( grep "# XPASS:" gnu/tests/test-suite.log|cut -d' ' -f2-) XPASS=$(sed -n "s/.*# XPASS: \(.*\)/\1/p" "$LOG_FILE"|tr -d '\r'|head -n1)
ERROR=$( grep "# ERROR:" gnu/tests/test-suite.log|cut -d' ' -f2-) ERROR=$(sed -n "s/.*# ERROR: \(.*\)/\1/p" "$LOG_FILE"|tr -d '\r'|head -n1)
echo "::warning ::GNU testsuite = $TOTAL / $PASS / $FAIL / $ERROR" echo "::warning ::GNU testsuite = TOTAL: $TOTAL / PASS: $PASS / FAIL: $FAIL / ERROR: $ERROR"
jq -n \
--arg date "$(date --rfc-email)" \
--arg sha "$GITHUB_SHA" \
--arg total "$TOTAL" \
--arg pass "$PASS" \
--arg skip "$SKIP" \
--arg fail "$FAIL" \
--arg xpass "$XPASS" \
--arg error "$ERROR" \
'{($date): { sha: $sha, total: $total, pass: $pass, skip: $skip, fail: $fail, xpass: $xpass, error: $error, }}' > gnu-result.json
else else
echo "::error ::Failed to get summary of test results" echo "::error ::Failed to get summary of test results"
fi fi
@ -135,3 +77,8 @@ jobs:
with: with:
name: test-report name: test-report
path: gnu/tests/**/*.log path: gnu/tests/**/*.log
- uses: actions/upload-artifact@v2
with:
name: gnu-result
path: gnu-result.json

3
.gitignore vendored
View file

@ -12,3 +12,6 @@ target/
Cargo.lock Cargo.lock
lib*.a lib*.a
/docs/_build /docs/_build
*.iml
### macOS ###
.DS_Store

View file

@ -1,72 +0,0 @@
language: rust
rust:
- stable
- beta
os:
- linux
# - osx
env:
# sphinx v1.8.0 is bugged & fails for linux builds; so, force specific `sphinx` version
global: FEATURES='' TEST_INSTALL='' SPHINX_VERSIONED='sphinx==1.7.8'
matrix:
allow_failures:
- rust: beta
- rust: nightly
fast_finish: true
include:
- rust: 1.40.0
env: FEATURES=unix
# - rust: stable
# os: linux
# env: FEATURES=unix TEST_INSTALL=true
# - rust: stable
# os: osx
# env: FEATURES=macos TEST_INSTALL=true
- rust: nightly
os: linux
env: FEATURES=nightly,unix TEST_INSTALL=true
- rust: nightly
os: osx
env: FEATURES=nightly,macos TEST_INSTALL=true
- rust: nightly
os: linux
env: FEATURES=nightly,feat_os_unix_redox CC=x86_64-unknown-redox-gcc CARGO_ARGS='--no-default-features --target=x86_64-unknown-redox' REDOX=1
cache:
directories:
- $HOME/.cargo
sudo: true
before_install:
- if [ $REDOX ]; then ./.travis/redox-toolchain.sh; fi
install:
- if [ $TRAVIS_OS_NAME = linux ]; then sudo apt-get install python-pip && sudo pip install $SPHINX_VERSIONED; fi
- |
if [ $TRAVIS_OS_NAME = osx ]; then
brew update
brew upgrade python
pip3 install $SPHINX_VERSIONED
fi
script:
- cargo build $CARGO_ARGS --features "$FEATURES"
- if [ ! $REDOX ]; then cargo test $CARGO_ARGS -p uucore -p coreutils --features "$FEATURES" --no-fail-fast; fi
- if [ -n "$TEST_INSTALL" ]; then mkdir installdir_test; DESTDIR=installdir_test make install; [ `ls installdir_test/usr/local/bin | wc -l` -gt 0 ]; fi
addons:
apt:
packages:
- libssl-dev
after_success: |
if [ "$TRAVIS_OS_NAME" = linux -a "$TRAVIS_RUST_VERSION" = stable ]; then
bash <(curl https://raw.githubusercontent.com/xd009642/tarpaulin/master/travis-install.sh)
cargo tarpaulin --out Xml
bash <(curl -s https://codecov.io/bash)
fi

View file

@ -70,10 +70,6 @@ lines for non-utility modules include:
README: add help README: add help
``` ```
```
travis: fix build
```
``` ```
uucore: add new modules uucore: add new modules
``` ```

373
Cargo.lock generated
View file

@ -1,5 +1,11 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
[[package]]
name = "Inflector"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
[[package]] [[package]]
name = "advapi32-sys" name = "advapi32-sys"
version = "0.2.0" version = "0.2.0"
@ -37,6 +43,12 @@ dependencies = [
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]]
name = "array-init"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6945cc5422176fc5e602e590c2878d2c2acd9a4fe20a4baa7c28022521698ec6"
[[package]] [[package]]
name = "arrayvec" name = "arrayvec"
version = "0.4.12" version = "0.4.12"
@ -63,6 +75,15 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "binary-heap-plus"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f068638f8ff9e118a9361e66a411eff410e7fb3ecaa23bf9272324f8fc606d7"
dependencies = [
"compare",
]
[[package]] [[package]]
name = "bit-set" name = "bit-set"
version = "0.5.2" version = "0.5.2"
@ -136,9 +157,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]] [[package]]
name = "cast" name = "cast"
version = "0.2.5" version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc38c385bfd7e444464011bb24820f40dd1c76bcdfa1b78611cb7c2e5cafab75" checksum = "57cdfa5d50aad6cb4d44dcab6101a7f79925bd59d82ca42f38a9856a28865374"
dependencies = [ dependencies = [
"rustc_version", "rustc_version",
] ]
@ -198,6 +219,12 @@ dependencies = [
"bitflags", "bitflags",
] ]
[[package]]
name = "compare"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "120133d4db2ec47efe2e26502ee984747630c67f51974fca0b6c1340cf2368d3"
[[package]] [[package]]
name = "constant_time_eq" name = "constant_time_eq"
version = "0.1.5" version = "0.1.5"
@ -228,7 +255,6 @@ dependencies = [
"rand 0.7.3", "rand 0.7.3",
"regex", "regex",
"sha1", "sha1",
"tempdir",
"tempfile", "tempfile",
"textwrap", "textwrap",
"time", "time",
@ -259,6 +285,7 @@ dependencies = [
"uu_expand", "uu_expand",
"uu_expr", "uu_expr",
"uu_factor", "uu_factor",
"uu_factor_benches",
"uu_false", "uu_false",
"uu_fmt", "uu_fmt",
"uu_fold", "uu_fold",
@ -602,6 +629,18 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "dns-lookup"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "093d88961fd18c4ecacb8c80cd0b356463ba941ba11e0e01f9cf5271380b79dc"
dependencies = [
"cfg-if 1.0.0",
"libc",
"socket2",
"winapi 0.3.9",
]
[[package]] [[package]]
name = "dunce" name = "dunce"
version = "1.0.1" version = "1.0.1"
@ -644,7 +683,7 @@ checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
"libc", "libc",
"redox_syscall 0.2.7", "redox_syscall 0.2.8",
"winapi 0.3.9", "winapi 0.3.9",
] ]
@ -693,7 +732,18 @@ checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
"libc", "libc",
"wasi", "wasi 0.9.0+wasi-snapshot-preview1",
]
[[package]]
name = "getrandom"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
dependencies = [
"cfg-if 1.0.0",
"libc",
"wasi 0.10.2+wasi-snapshot-preview1",
] ]
[[package]] [[package]]
@ -809,9 +859,9 @@ checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.50" version = "0.3.51"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d99f9e3e84b8f67f846ef5b4cbbc3b1c29f6c759fcbce6f01aa0e73d932a24c" checksum = "83bdfbace3a0e81a4253f73b49e960b053e396a11012cbd49b9b74d6a2b67062"
dependencies = [ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
@ -1053,6 +1103,29 @@ version = "11.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575"
[[package]]
name = "ouroboros"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc1f52300b81ac4eeeb6c00c20f7e86556c427d9fb2d92b68fc73c22f331cd15"
dependencies = [
"ouroboros_macro",
"stable_deref_trait",
]
[[package]]
name = "ouroboros_macro"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41db02c8f8731cdd7a72b433c7900cce4bf245465b452c364bfd21f4566ab055"
dependencies = [
"Inflector",
"proc-macro-error",
"proc-macro2",
"quote 1.0.9",
"syn",
]
[[package]] [[package]]
name = "output_vt100" name = "output_vt100"
version = "0.1.2" version = "0.1.2"
@ -1105,6 +1178,15 @@ dependencies = [
"proc-macro-hack", "proc-macro-hack",
] ]
[[package]]
name = "pest"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53"
dependencies = [
"ucd-trie",
]
[[package]] [[package]]
name = "pkg-config" name = "pkg-config"
version = "0.3.19" version = "0.3.19"
@ -1123,9 +1205,9 @@ dependencies = [
[[package]] [[package]]
name = "plotters" name = "plotters"
version = "0.3.0" version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45ca0ae5f169d0917a7c7f5a9c1a3d3d9598f18f529dd2b8373ed988efea307a" checksum = "32a3fd9ec30b9749ce28cd91f255d569591cdf937fe280c312143e3c4bad6f2a"
dependencies = [ dependencies = [
"num-traits", "num-traits",
"plotters-backend", "plotters-backend",
@ -1167,6 +1249,30 @@ dependencies = [
"output_vt100", "output_vt100",
] ]
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote 1.0.9",
"syn",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote 1.0.9",
"version_check",
]
[[package]] [[package]]
name = "proc-macro-hack" name = "proc-macro-hack"
version = "0.5.19" version = "0.5.19"
@ -1175,9 +1281,9 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.26" version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec" checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038"
dependencies = [ dependencies = [
"unicode-xid 0.2.2", "unicode-xid 0.2.2",
] ]
@ -1215,19 +1321,6 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "rand"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
dependencies = [
"fuchsia-cprng",
"libc",
"rand_core 0.3.1",
"rdrand",
"winapi 0.3.9",
]
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.5.6" version = "0.5.6"
@ -1247,14 +1340,26 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.1.16",
"libc", "libc",
"rand_chacha", "rand_chacha 0.2.2",
"rand_core 0.5.1", "rand_core 0.5.1",
"rand_hc", "rand_hc 0.2.0",
"rand_pcg", "rand_pcg",
] ]
[[package]]
name = "rand"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e"
dependencies = [
"libc",
"rand_chacha 0.3.0",
"rand_core 0.6.2",
"rand_hc 0.3.0",
]
[[package]] [[package]]
name = "rand_chacha" name = "rand_chacha"
version = "0.2.2" version = "0.2.2"
@ -1265,6 +1370,16 @@ dependencies = [
"rand_core 0.5.1", "rand_core 0.5.1",
] ]
[[package]]
name = "rand_chacha"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d"
dependencies = [
"ppv-lite86",
"rand_core 0.6.2",
]
[[package]] [[package]]
name = "rand_core" name = "rand_core"
version = "0.3.1" version = "0.3.1"
@ -1286,7 +1401,16 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.1.16",
]
[[package]]
name = "rand_core"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7"
dependencies = [
"getrandom 0.2.3",
] ]
[[package]] [[package]]
@ -1298,6 +1422,15 @@ dependencies = [
"rand_core 0.5.1", "rand_core 0.5.1",
] ]
[[package]]
name = "rand_hc"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73"
dependencies = [
"rand_core 0.6.2",
]
[[package]] [[package]]
name = "rand_pcg" name = "rand_pcg"
version = "0.2.1" version = "0.2.1"
@ -1309,9 +1442,9 @@ dependencies = [
[[package]] [[package]]
name = "rayon" name = "rayon"
version = "1.5.0" version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b0d8e0819fadc20c74ea8373106ead0600e3a67ef1fe8da56e39b9ae7275674" checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"crossbeam-deque", "crossbeam-deque",
@ -1321,9 +1454,9 @@ dependencies = [
[[package]] [[package]]
name = "rayon-core" name = "rayon-core"
version = "1.9.0" version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ab346ac5921dc62ffa9f89b7a773907511cdfa5490c572ae9be1be33e8afa4a" checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e"
dependencies = [ dependencies = [
"crossbeam-channel", "crossbeam-channel",
"crossbeam-deque", "crossbeam-deque",
@ -1332,15 +1465,6 @@ dependencies = [
"num_cpus", "num_cpus",
] ]
[[package]]
name = "rdrand"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
dependencies = [
"rand_core 0.3.1",
]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.1.57" version = "0.1.57"
@ -1349,9 +1473,9 @@ checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.2.7" version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85dd92e586f7355c633911e11f77f3d12f04b1b1bd76a198bd34ae3af8341ef2" checksum = "742739e41cd49414de871ea5e549afb7e2a3ac77b589bcbebe8c82fab37147fc"
dependencies = [ dependencies = [
"bitflags", "bitflags",
] ]
@ -1362,14 +1486,14 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f" checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f"
dependencies = [ dependencies = [
"redox_syscall 0.2.7", "redox_syscall 0.2.8",
] ]
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.5.3" version = "1.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce5f1ceb7f74abbce32601642fcf8e8508a8a8991e0621c7d750295b9095702b" checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr 2.4.0", "memchr 2.4.0",
@ -1402,9 +1526,9 @@ dependencies = [
[[package]] [[package]]
name = "retain_mut" name = "retain_mut"
version = "0.1.2" version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53552c6c49e1e13f1a203ef0080ab3bbef0beb570a528993e83df057a9d9bba1" checksum = "e9c17925a9027d298a4603d286befe3f9dc0e8ed02523141914eb628798d6e5b"
[[package]] [[package]]
name = "rust-ini" name = "rust-ini"
@ -1414,11 +1538,11 @@ checksum = "3e52c148ef37f8c375d49d5a73aa70713125b7f19095948a923f80afdeb22ec2"
[[package]] [[package]]
name = "rustc_version" name = "rustc_version"
version = "0.2.3" version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee"
dependencies = [ dependencies = [
"semver", "semver 0.11.0",
] ]
[[package]] [[package]]
@ -1448,7 +1572,16 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
dependencies = [ dependencies = [
"semver-parser", "semver-parser 0.7.0",
]
[[package]]
name = "semver"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6"
dependencies = [
"semver-parser 0.10.2",
] ]
[[package]] [[package]]
@ -1458,14 +1591,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]] [[package]]
name = "serde" name = "semver-parser"
version = "1.0.125" version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171" checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7"
dependencies = [ dependencies = [
"serde_derive", "pest",
] ]
[[package]]
name = "serde"
version = "1.0.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03"
[[package]] [[package]]
name = "serde_cbor" name = "serde_cbor"
version = "0.11.1" version = "0.11.1"
@ -1478,9 +1617,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.125" version = "1.0.126"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b093b7a2bb58203b5da3056c05b4ec1fed827dcfdb37347a8841695263b3d06d" checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.9", "quote 1.0.9",
@ -1559,14 +1698,22 @@ dependencies = [
] ]
[[package]] [[package]]
name = "smallvec" name = "socket2"
version = "1.6.1" version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e"
dependencies = [ dependencies = [
"serde", "cfg-if 1.0.0",
"libc",
"winapi 0.3.9",
] ]
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.8.0" version = "0.8.0"
@ -1602,26 +1749,16 @@ dependencies = [
"unicode-xid 0.2.2", "unicode-xid 0.2.2",
] ]
[[package]]
name = "tempdir"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8"
dependencies = [
"rand 0.4.6",
"remove_dir_all",
]
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.1.0" version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
dependencies = [ dependencies = [
"cfg-if 0.1.10", "cfg-if 1.0.0",
"libc", "libc",
"rand 0.7.3", "rand 0.8.3",
"redox_syscall 0.1.57", "redox_syscall 0.2.8",
"remove_dir_all", "remove_dir_all",
"winapi 0.3.9", "winapi 0.3.9",
] ]
@ -1653,7 +1790,7 @@ checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e"
dependencies = [ dependencies = [
"libc", "libc",
"numtoa", "numtoa",
"redox_syscall 0.2.7", "redox_syscall 0.2.8",
"redox_termios", "redox_termios",
] ]
@ -1726,6 +1863,12 @@ version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06"
[[package]]
name = "ucd-trie"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
[[package]] [[package]]
name = "unicode-segmentation" name = "unicode-segmentation"
version = "1.7.1" version = "1.7.1"
@ -1780,6 +1923,7 @@ dependencies = [
name = "uu_arch" name = "uu_arch"
version = "0.0.6" version = "0.0.6"
dependencies = [ dependencies = [
"clap",
"platform-info", "platform-info",
"uucore", "uucore",
"uucore_procs", "uucore_procs",
@ -1941,11 +2085,9 @@ name = "uu_df"
version = "0.0.6" version = "0.0.6"
dependencies = [ dependencies = [
"clap", "clap",
"libc",
"number_prefix", "number_prefix",
"uucore", "uucore",
"uucore_procs", "uucore_procs",
"winapi 0.3.9",
] ]
[[package]] [[package]]
@ -1972,6 +2114,7 @@ name = "uu_du"
version = "0.0.6" version = "0.0.6"
dependencies = [ dependencies = [
"chrono", "chrono",
"clap",
"uucore", "uucore",
"uucore_procs", "uucore_procs",
"winapi 0.3.9", "winapi 0.3.9",
@ -2022,17 +2165,26 @@ name = "uu_factor"
version = "0.0.6" version = "0.0.6"
dependencies = [ dependencies = [
"coz", "coz",
"criterion",
"num-traits", "num-traits",
"paste", "paste",
"quickcheck", "quickcheck",
"rand 0.7.3", "rand 0.7.3",
"rand_chacha", "smallvec",
"smallvec 0.6.14",
"uucore", "uucore",
"uucore_procs", "uucore_procs",
] ]
[[package]]
name = "uu_factor_benches"
version = "0.0.0"
dependencies = [
"array-init",
"criterion",
"rand 0.7.3",
"rand_chacha 0.2.2",
"uu_factor",
]
[[package]] [[package]]
name = "uu_false" name = "uu_false"
version = "0.0.6" version = "0.0.6"
@ -2184,6 +2336,7 @@ dependencies = [
name = "uu_logname" name = "uu_logname"
version = "0.0.6" version = "0.0.6"
dependencies = [ dependencies = [
"clap",
"libc", "libc",
"uucore", "uucore",
"uucore_procs", "uucore_procs",
@ -2233,7 +2386,7 @@ dependencies = [
name = "uu_mknod" name = "uu_mknod"
version = "0.0.6" version = "0.0.6"
dependencies = [ dependencies = [
"getopts", "clap",
"libc", "libc",
"uucore", "uucore",
"uucore_procs", "uucore_procs",
@ -2503,16 +2656,17 @@ dependencies = [
name = "uu_sort" name = "uu_sort"
version = "0.0.6" version = "0.0.6"
dependencies = [ dependencies = [
"binary-heap-plus",
"clap", "clap",
"compare",
"fnv", "fnv",
"itertools 0.10.0", "itertools 0.10.0",
"memchr 2.4.0",
"ouroboros",
"rand 0.7.3", "rand 0.7.3",
"rayon", "rayon",
"semver", "semver 0.9.0",
"serde", "tempfile",
"serde_json",
"smallvec 1.6.1",
"tempdir",
"unicode-width", "unicode-width",
"uucore", "uucore",
"uucore_procs", "uucore_procs",
@ -2532,7 +2686,6 @@ name = "uu_stat"
version = "0.0.6" version = "0.0.6"
dependencies = [ dependencies = [
"clap", "clap",
"time",
"uucore", "uucore",
"uucore_procs", "uucore_procs",
] ]
@ -2796,6 +2949,7 @@ name = "uucore"
version = "0.0.8" version = "0.0.8"
dependencies = [ dependencies = [
"data-encoding", "data-encoding",
"dns-lookup",
"dunce", "dunce",
"getopts", "getopts",
"lazy_static", "lazy_static",
@ -2806,6 +2960,7 @@ dependencies = [
"thiserror", "thiserror",
"time", "time",
"wild", "wild",
"winapi 0.3.9",
] ]
[[package]] [[package]]
@ -2823,6 +2978,12 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]]
name = "version_check"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
[[package]] [[package]]
name = "void" name = "void"
version = "1.0.2" version = "1.0.2"
@ -2847,10 +3008,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
[[package]] [[package]]
name = "wasm-bindgen" name = "wasi"
version = "0.2.73" version = "0.10.2+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83240549659d187488f91f33c0f8547cbfef0b2088bc470c116d1d260ef623d9" checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
[[package]]
name = "wasm-bindgen"
version = "0.2.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d54ee1d4ed486f78874278e63e4069fc1ab9f6a18ca492076ffb90c5eb2997fd"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
"wasm-bindgen-macro", "wasm-bindgen-macro",
@ -2858,9 +3025,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-backend" name = "wasm-bindgen-backend"
version = "0.2.73" version = "0.2.74"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae70622411ca953215ca6d06d3ebeb1e915f0f6613e3b495122878d7ebec7dae" checksum = "3b33f6a0694ccfea53d94db8b2ed1c3a8a4c86dd936b13b9f0a15ec4a451b900"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"lazy_static", "lazy_static",
@ -2873,9 +3040,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.73" version = "0.2.74"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e734d91443f177bfdb41969de821e15c516931c3c3db3d318fa1b68975d0f6f" checksum = "088169ca61430fe1e58b8096c24975251700e7b1f6fd91cc9d59b04fb9b18bd4"
dependencies = [ dependencies = [
"quote 1.0.9", "quote 1.0.9",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@ -2883,9 +3050,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.73" version = "0.2.74"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d53739ff08c8a68b0fdbcd54c372b8ab800b1449ab3c9d706503bc7dd1621b2c" checksum = "be2241542ff3d9f241f5e2cb6dd09b37efe786df8851c54957683a49f0987a97"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.9", "quote 1.0.9",
@ -2896,15 +3063,15 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.73" version = "0.2.74"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9a543ae66aa233d14bb765ed9af4a33e81b8b58d1584cf1b47ff8cd0b9e4489" checksum = "d7cff876b8f18eed75a66cf49b65e7f967cb354a7aa16003fb55dbfd25b44b4f"
[[package]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.50" version = "0.3.51"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a905d57e488fec8861446d3393670fb50d27a262344013181c2cdf9fff5481be" checksum = "e828417b379f3df7111d3a2a9e5753706cae29c41f7c4029ee9fd77f3e09e582"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",

View file

@ -324,6 +324,9 @@ wc = { optional=true, version="0.0.6", package="uu_wc", path="src/uu/wc" }
who = { optional=true, version="0.0.6", package="uu_who", path="src/uu/who" } who = { optional=true, version="0.0.6", package="uu_who", path="src/uu/who" }
whoami = { optional=true, version="0.0.6", package="uu_whoami", path="src/uu/whoami" } whoami = { optional=true, version="0.0.6", package="uu_whoami", path="src/uu/whoami" }
yes = { optional=true, version="0.0.6", package="uu_yes", path="src/uu/yes" } yes = { optional=true, version="0.0.6", package="uu_yes", path="src/uu/yes" }
factor_benches = { optional = true, version = "0.0.0", package = "uu_factor_benches", path = "tests/benches/factor" }
# #
# * pinned transitive dependencies # * pinned transitive dependencies
# Not needed for now. Keep as examples: # Not needed for now. Keep as examples:
@ -339,15 +342,11 @@ pretty_assertions = "0.7.2"
rand = "0.7" rand = "0.7"
regex = "1.0" regex = "1.0"
sha1 = { version="0.6", features=["std"] } sha1 = { version="0.6", features=["std"] }
## tempfile 3.2 depends on recent version of rand which depends on getrandom v0.2 which has compiler errors for MinRustV v1.32.0 tempfile = "3.2.0"
## min dep for tempfile = Rustc 1.40
tempfile = "= 3.1.0"
time = "0.1" time = "0.1"
unindent = "0.1" unindent = "0.1"
uucore = { version=">=0.0.8", package="uucore", path="src/uucore", features=["entries"] } uucore = { version=">=0.0.8", package="uucore", path="src/uucore", features=["entries"] }
walkdir = "2.2" walkdir = "2.2"
tempdir = "0.3"
atty = "0.2.14"
[target.'cfg(unix)'.dev-dependencies] [target.'cfg(unix)'.dev-dependencies]
rust-users = { version="0.10", package="users" } rust-users = { version="0.10", package="users" }

View file

@ -6,7 +6,6 @@
[![LOC](https://tokei.rs/b1/github/uutils/coreutils?category=code)](https://github.com/Aaronepower/tokei) [![LOC](https://tokei.rs/b1/github/uutils/coreutils?category=code)](https://github.com/Aaronepower/tokei)
[![dependency status](https://deps.rs/repo/github/uutils/coreutils/status.svg)](https://deps.rs/repo/github/uutils/coreutils) [![dependency status](https://deps.rs/repo/github/uutils/coreutils/status.svg)](https://deps.rs/repo/github/uutils/coreutils)
[![Build Status](https://api.travis-ci.org/uutils/coreutils.svg?branch=master)](https://travis-ci.org/uutils/coreutils)
[![Build Status (FreeBSD)](https://api.cirrus-ci.com/github/uutils/coreutils.svg)](https://cirrus-ci.com/github/uutils/coreutils/master) [![Build Status (FreeBSD)](https://api.cirrus-ci.com/github/uutils/coreutils.svg)](https://cirrus-ci.com/github/uutils/coreutils/master)
[![CodeCov](https://codecov.io/gh/uutils/coreutils/branch/master/graph/badge.svg)](https://codecov.io/gh/uutils/coreutils) [![CodeCov](https://codecov.io/gh/uutils/coreutils/branch/master/graph/badge.svg)](https://codecov.io/gh/uutils/coreutils)
@ -40,7 +39,7 @@ to compile anywhere, and this is as good a way as any to try and learn it.
### Rust Version ### Rust Version
uutils follows Rust's release channels and is tested against stable, beta and nightly. uutils follows Rust's release channels and is tested against stable, beta and nightly.
The current oldest supported version of the Rust compiler is `1.40.0`. The current oldest supported version of the Rust compiler is `1.43.1`.
On both Windows and Redox, only the nightly version is tested currently. On both Windows and Redox, only the nightly version is tested currently.
@ -319,6 +318,16 @@ To pass an argument like "-v" to the busybox test runtime
$ make UTILS='UTILITY_1 UTILITY_2' RUNTEST_ARGS='-v' busytest $ make UTILS='UTILITY_1 UTILITY_2' RUNTEST_ARGS='-v' busytest
``` ```
## Comparing with GNU
![Evolution over time](https://github.com/uutils/coreutils-tracking/blob/main/gnu-results.png?raw=true)
To run locally:
```bash
$ bash util/build-gnu.sh
$ bash util/run-gnu-test.sh
```
## Contribute ## Contribute
To contribute to uutils, please see [CONTRIBUTING](CONTRIBUTING.md). To contribute to uutils, please see [CONTRIBUTING](CONTRIBUTING.md).

View file

@ -16,6 +16,7 @@ path = "src/arch.rs"
[dependencies] [dependencies]
platform-info = "0.1" platform-info = "0.1"
clap = "2.33"
uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore = { version=">=0.0.8", package="uucore", path="../../uucore" }
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }

View file

@ -10,17 +10,20 @@
extern crate uucore; extern crate uucore;
use platform_info::*; use platform_info::*;
use uucore::InvalidEncodingHandling;
static SYNTAX: &str = "Display machine architecture"; use clap::App;
static VERSION: &str = env!("CARGO_PKG_VERSION");
static ABOUT: &str = "Display machine architecture";
static SUMMARY: &str = "Determine architecture name for current machine."; static SUMMARY: &str = "Determine architecture name for current machine.";
static LONG_HELP: &str = "";
pub fn uumain(args: impl uucore::Args) -> i32 { pub fn uumain(args: impl uucore::Args) -> i32 {
app!(SYNTAX, SUMMARY, LONG_HELP).parse( App::new(executable!())
args.collect_str(InvalidEncodingHandling::ConvertLossy) .version(VERSION)
.accept_any(), .about(ABOUT)
); .after_help(SUMMARY)
.get_matches_from(args);
let uts = return_if_err!(1, PlatformInfo::new()); let uts = return_if_err!(1, PlatformInfo::new());
println!("{}", uts.machine().trim()); println!("{}", uts.machine().trim());
0 0

View file

@ -347,7 +347,7 @@ fn cat_files(files: Vec<String>, options: &OutputOptions) -> Result<(), u32> {
for path in &files { for path in &files {
if let Err(err) = cat_path(path, &options, &mut state) { if let Err(err) = cat_path(path, &options, &mut state) {
show_info!("{}: {}", path, err); show_error!("{}: {}", path, err);
error_count += 1; error_count += 1;
} }
} }

View file

@ -97,7 +97,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
if recursive { if recursive {
if bit_flag == FTS_PHYSICAL { if bit_flag == FTS_PHYSICAL {
if derefer == 1 { if derefer == 1 {
show_info!("-R --dereference requires -H or -L"); show_error!("-R --dereference requires -H or -L");
return 1; return 1;
} }
derefer = 0; derefer = 0;
@ -132,7 +132,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
dest_gid = meta.gid(); dest_gid = meta.gid();
} }
Err(e) => { Err(e) => {
show_info!("failed to get attributes of '{}': {}", file, e); show_error!("failed to get attributes of '{}': {}", file, e);
return 1; return 1;
} }
} }
@ -143,7 +143,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
dest_gid = g; dest_gid = g;
} }
_ => { _ => {
show_info!("invalid group: {}", matches.free[0].as_str()); show_error!("invalid group: {}", matches.free[0].as_str());
return 1; return 1;
} }
} }
@ -235,8 +235,8 @@ impl Chgrper {
if let Some(p) = may_exist { if let Some(p) = may_exist {
if p.parent().is_none() || self.is_bind_root(p) { if p.parent().is_none() || self.is_bind_root(p) {
show_info!("it is dangerous to operate recursively on '/'"); show_error!("it is dangerous to operate recursively on '/'");
show_info!("use --no-preserve-root to override this failsafe"); show_error!("use --no-preserve-root to override this failsafe");
return 1; return 1;
} }
} }
@ -250,12 +250,12 @@ impl Chgrper {
self.verbosity.clone(), self.verbosity.clone(),
) { ) {
Ok(n) => { Ok(n) => {
show_info!("{}", n); show_error!("{}", n);
0 0
} }
Err(e) => { Err(e) => {
if self.verbosity != Verbosity::Silent { if self.verbosity != Verbosity::Silent {
show_info!("{}", e); show_error!("{}", e);
} }
1 1
} }
@ -275,7 +275,7 @@ impl Chgrper {
for entry in WalkDir::new(root).follow_links(follow).min_depth(1) { for entry in WalkDir::new(root).follow_links(follow).min_depth(1) {
let entry = unwrap!(entry, e, { let entry = unwrap!(entry, e, {
ret = 1; ret = 1;
show_info!("{}", e); show_error!("{}", e);
continue; continue;
}); });
let path = entry.path(); let path = entry.path();
@ -290,13 +290,13 @@ impl Chgrper {
ret = match wrap_chgrp(path, &meta, self.dest_gid, follow, self.verbosity.clone()) { ret = match wrap_chgrp(path, &meta, self.dest_gid, follow, self.verbosity.clone()) {
Ok(n) => { Ok(n) => {
if !n.is_empty() { if !n.is_empty() {
show_info!("{}", n); show_error!("{}", n);
} }
0 0
} }
Err(e) => { Err(e) => {
if self.verbosity != Verbosity::Silent { if self.verbosity != Verbosity::Silent {
show_info!("{}", e); show_error!("{}", e);
} }
1 1
} }
@ -313,7 +313,7 @@ impl Chgrper {
unwrap!(path.metadata(), e, { unwrap!(path.metadata(), e, {
match self.verbosity { match self.verbosity {
Silent => (), Silent => (),
_ => show_info!("cannot access '{}': {}", path.display(), e), _ => show_error!("cannot access '{}': {}", path.display(), e),
} }
return None; return None;
}) })
@ -321,7 +321,7 @@ impl Chgrper {
unwrap!(path.symlink_metadata(), e, { unwrap!(path.symlink_metadata(), e, {
match self.verbosity { match self.verbosity {
Silent => (), Silent => (),
_ => show_info!("cannot dereference '{}': {}", path.display(), e), _ => show_error!("cannot dereference '{}': {}", path.display(), e),
} }
return None; return None;
}) })

View file

@ -15,6 +15,7 @@ use std::fs;
use std::os::unix::fs::{MetadataExt, PermissionsExt}; use std::os::unix::fs::{MetadataExt, PermissionsExt};
use std::path::Path; use std::path::Path;
use uucore::fs::display_permissions_unix; use uucore::fs::display_permissions_unix;
use uucore::libc::mode_t;
#[cfg(not(windows))] #[cfg(not(windows))]
use uucore::mode; use uucore::mode;
use uucore::InvalidEncodingHandling; use uucore::InvalidEncodingHandling;
@ -261,8 +262,10 @@ impl Chmoder {
); );
} }
return Ok(()); return Ok(());
} else if err.kind() == std::io::ErrorKind::PermissionDenied {
show_error!("'{}': Permission denied", file.display());
} else { } else {
show_error!("{}: '{}'", err, file.display()); show_error!("'{}': {}", file.display(), err);
} }
return Err(1); return Err(1);
} }
@ -306,7 +309,7 @@ impl Chmoder {
"mode of '{}' retained as {:04o} ({})", "mode of '{}' retained as {:04o} ({})",
file.display(), file.display(),
fperm, fperm,
display_permissions_unix(fperm), display_permissions_unix(fperm as mode_t, false),
); );
} }
Ok(()) Ok(())
@ -315,25 +318,25 @@ impl Chmoder {
show_error!("{}", err); show_error!("{}", err);
} }
if self.verbose { if self.verbose {
show_info!( show_error!(
"failed to change mode of file '{}' from {:o} ({}) to {:o} ({})", "failed to change mode of file '{}' from {:o} ({}) to {:o} ({})",
file.display(), file.display(),
fperm, fperm,
display_permissions_unix(fperm), display_permissions_unix(fperm as mode_t, false),
mode, mode,
display_permissions_unix(mode) display_permissions_unix(mode as mode_t, false)
); );
} }
Err(1) Err(1)
} else { } else {
if self.verbose || self.changes { if self.verbose || self.changes {
show_info!( show_error!(
"mode of '{}' changed from {:o} ({}) to {:o} ({})", "mode of '{}' changed from {:o} ({}) to {:o} ({})",
file.display(), file.display(),
fperm, fperm,
display_permissions_unix(fperm), display_permissions_unix(fperm as mode_t, false),
mode, mode,
display_permissions_unix(mode) display_permissions_unix(mode as mode_t, false)
); );
} }
Ok(()) Ok(())

View file

@ -199,7 +199,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
if recursive { if recursive {
if bit_flag == FTS_PHYSICAL { if bit_flag == FTS_PHYSICAL {
if derefer == 1 { if derefer == 1 {
show_info!("-R --dereference requires -H or -L"); show_error!("-R --dereference requires -H or -L");
return 1; return 1;
} }
derefer = 0; derefer = 0;
@ -227,7 +227,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
Ok((Some(uid), Some(gid))) => IfFrom::UserGroup(uid, gid), Ok((Some(uid), Some(gid))) => IfFrom::UserGroup(uid, gid),
Ok((None, None)) => IfFrom::All, Ok((None, None)) => IfFrom::All,
Err(e) => { Err(e) => {
show_info!("{}", e); show_error!("{}", e);
return 1; return 1;
} }
} }
@ -244,7 +244,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
dest_uid = Some(meta.uid()); dest_uid = Some(meta.uid());
} }
Err(e) => { Err(e) => {
show_info!("failed to get attributes of '{}': {}", file, e); show_error!("failed to get attributes of '{}': {}", file, e);
return 1; return 1;
} }
} }
@ -255,7 +255,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
dest_gid = g; dest_gid = g;
} }
Err(e) => { Err(e) => {
show_info!("{}", e); show_error!("{}", e);
return 1; return 1;
} }
} }
@ -377,8 +377,8 @@ impl Chowner {
if let Some(p) = may_exist { if let Some(p) = may_exist {
if p.parent().is_none() { if p.parent().is_none() {
show_info!("it is dangerous to operate recursively on '/'"); show_error!("it is dangerous to operate recursively on '/'");
show_info!("use --no-preserve-root to override this failsafe"); show_error!("use --no-preserve-root to override this failsafe");
return 1; return 1;
} }
} }
@ -395,13 +395,13 @@ impl Chowner {
) { ) {
Ok(n) => { Ok(n) => {
if !n.is_empty() { if !n.is_empty() {
show_info!("{}", n); show_error!("{}", n);
} }
0 0
} }
Err(e) => { Err(e) => {
if self.verbosity != Verbosity::Silent { if self.verbosity != Verbosity::Silent {
show_info!("{}", e); show_error!("{}", e);
} }
1 1
} }
@ -424,7 +424,7 @@ impl Chowner {
for entry in WalkDir::new(root).follow_links(follow).min_depth(1) { for entry in WalkDir::new(root).follow_links(follow).min_depth(1) {
let entry = unwrap!(entry, e, { let entry = unwrap!(entry, e, {
ret = 1; ret = 1;
show_info!("{}", e); show_error!("{}", e);
continue; continue;
}); });
let path = entry.path(); let path = entry.path();
@ -450,13 +450,13 @@ impl Chowner {
) { ) {
Ok(n) => { Ok(n) => {
if !n.is_empty() { if !n.is_empty() {
show_info!("{}", n); show_error!("{}", n);
} }
0 0
} }
Err(e) => { Err(e) => {
if self.verbosity != Verbosity::Silent { if self.verbosity != Verbosity::Silent {
show_info!("{}", e); show_error!("{}", e);
} }
1 1
} }
@ -472,7 +472,7 @@ impl Chowner {
unwrap!(path.metadata(), e, { unwrap!(path.metadata(), e, {
match self.verbosity { match self.verbosity {
Silent => (), Silent => (),
_ => show_info!("cannot access '{}': {}", path.display(), e), _ => show_error!("cannot access '{}': {}", path.display(), e),
} }
return None; return None;
}) })
@ -480,7 +480,7 @@ impl Chowner {
unwrap!(path.symlink_metadata(), e, { unwrap!(path.symlink_metadata(), e, {
match self.verbosity { match self.verbosity {
Silent => (), Silent => (),
_ => show_info!("cannot dereference '{}': {}", path.display(), e), _ => show_error!("cannot dereference '{}': {}", path.display(), e),
} }
return None; return None;
}) })

View file

@ -47,6 +47,7 @@ use std::os::windows::ffi::OsStrExt;
use std::path::{Path, PathBuf, StripPrefixError}; use std::path::{Path, PathBuf, StripPrefixError};
use std::str::FromStr; use std::str::FromStr;
use std::string::ToString; use std::string::ToString;
use uucore::backup_control::{self, BackupMode};
use uucore::fs::resolve_relative_path; use uucore::fs::resolve_relative_path;
use uucore::fs::{canonicalize, CanonicalizeMode}; use uucore::fs::{canonicalize, CanonicalizeMode};
use walkdir::WalkDir; use walkdir::WalkDir;
@ -169,14 +170,6 @@ pub enum TargetType {
File, File,
} }
#[derive(Clone, Eq, PartialEq)]
pub enum BackupMode {
ExistingBackup,
NoBackup,
NumberedBackup,
SimpleBackup,
}
pub enum CopyMode { pub enum CopyMode {
Link, Link,
SymLink, SymLink,
@ -201,7 +194,7 @@ pub enum Attribute {
#[allow(dead_code)] #[allow(dead_code)]
pub struct Options { pub struct Options {
attributes_only: bool, attributes_only: bool,
backup: bool, backup: BackupMode,
copy_contents: bool, copy_contents: bool,
copy_mode: CopyMode, copy_mode: CopyMode,
dereference: bool, dereference: bool,
@ -222,6 +215,7 @@ pub struct Options {
static VERSION: &str = env!("CARGO_PKG_VERSION"); static VERSION: &str = env!("CARGO_PKG_VERSION");
static ABOUT: &str = "Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY."; static ABOUT: &str = "Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY.";
static LONG_HELP: &str = "";
static EXIT_OK: i32 = 0; static EXIT_OK: i32 = 0;
static EXIT_ERR: i32 = 1; static EXIT_ERR: i32 = 1;
@ -238,6 +232,7 @@ fn get_usage() -> String {
static OPT_ARCHIVE: &str = "archive"; static OPT_ARCHIVE: &str = "archive";
static OPT_ATTRIBUTES_ONLY: &str = "attributes-only"; static OPT_ATTRIBUTES_ONLY: &str = "attributes-only";
static OPT_BACKUP: &str = "backup"; static OPT_BACKUP: &str = "backup";
static OPT_BACKUP_NO_ARG: &str = "b";
static OPT_CLI_SYMBOLIC_LINKS: &str = "cli-symbolic-links"; static OPT_CLI_SYMBOLIC_LINKS: &str = "cli-symbolic-links";
static OPT_CONTEXT: &str = "context"; static OPT_CONTEXT: &str = "context";
static OPT_COPY_CONTENTS: &str = "copy-contents"; static OPT_COPY_CONTENTS: &str = "copy-contents";
@ -301,6 +296,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
let matches = App::new(executable!()) let matches = App::new(executable!())
.version(VERSION) .version(VERSION)
.about(ABOUT) .about(ABOUT)
.after_help(&*format!("{}\n{}", LONG_HELP, backup_control::BACKUP_CONTROL_LONG_HELP))
.usage(&usage[..]) .usage(&usage[..])
.arg(Arg::with_name(OPT_TARGET_DIRECTORY) .arg(Arg::with_name(OPT_TARGET_DIRECTORY)
.short("t") .short("t")
@ -362,14 +358,22 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
.help("remove each existing destination file before attempting to open it \ .help("remove each existing destination file before attempting to open it \
(contrast with --force). On Windows, current only works for writeable files.")) (contrast with --force). On Windows, current only works for writeable files."))
.arg(Arg::with_name(OPT_BACKUP) .arg(Arg::with_name(OPT_BACKUP)
.short("b")
.long(OPT_BACKUP) .long(OPT_BACKUP)
.help("make a backup of each existing destination file")) .help("make a backup of each existing destination file")
.takes_value(true)
.require_equals(true)
.min_values(0)
.possible_values(backup_control::BACKUP_CONTROL_VALUES)
.value_name("CONTROL")
)
.arg(Arg::with_name(OPT_BACKUP_NO_ARG)
.short(OPT_BACKUP_NO_ARG)
.help("like --backup but does not accept an argument")
)
.arg(Arg::with_name(OPT_SUFFIX) .arg(Arg::with_name(OPT_SUFFIX)
.short("S") .short("S")
.long(OPT_SUFFIX) .long(OPT_SUFFIX)
.takes_value(true) .takes_value(true)
.default_value("~")
.value_name("SUFFIX") .value_name("SUFFIX")
.help("override the usual backup suffix")) .help("override the usual backup suffix"))
.arg(Arg::with_name(OPT_UPDATE) .arg(Arg::with_name(OPT_UPDATE)
@ -463,6 +467,12 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
.get_matches_from(args); .get_matches_from(args);
let options = crash_if_err!(EXIT_ERR, Options::from_matches(&matches)); let options = crash_if_err!(EXIT_ERR, Options::from_matches(&matches));
if options.overwrite == OverwriteMode::NoClobber && options.backup != BackupMode::NoBackup {
show_usage_error!("options --backup and --no-clobber are mutually exclusive");
return 1;
}
let paths: Vec<String> = matches let paths: Vec<String> = matches
.values_of(OPT_PATHS) .values_of(OPT_PATHS)
.map(|v| v.map(ToString::to_string).collect()) .map(|v| v.map(ToString::to_string).collect())
@ -585,7 +595,13 @@ impl Options {
|| matches.is_present(OPT_RECURSIVE_ALIAS) || matches.is_present(OPT_RECURSIVE_ALIAS)
|| matches.is_present(OPT_ARCHIVE); || matches.is_present(OPT_ARCHIVE);
let backup = matches.is_present(OPT_BACKUP) || (matches.occurrences_of(OPT_SUFFIX) > 0); let backup_mode = backup_control::determine_backup_mode(
matches.is_present(OPT_BACKUP_NO_ARG) || matches.is_present(OPT_BACKUP),
matches.value_of(OPT_BACKUP),
);
let backup_suffix = backup_control::determine_backup_suffix(matches.value_of(OPT_SUFFIX));
let overwrite = OverwriteMode::from_matches(matches);
// Parse target directory options // Parse target directory options
let no_target_dir = matches.is_present(OPT_NO_TARGET_DIRECTORY); let no_target_dir = matches.is_present(OPT_NO_TARGET_DIRECTORY);
@ -631,9 +647,7 @@ impl Options {
|| matches.is_present(OPT_NO_DEREFERENCE_PRESERVE_LINKS) || matches.is_present(OPT_NO_DEREFERENCE_PRESERVE_LINKS)
|| matches.is_present(OPT_ARCHIVE), || matches.is_present(OPT_ARCHIVE),
one_file_system: matches.is_present(OPT_ONE_FILE_SYSTEM), one_file_system: matches.is_present(OPT_ONE_FILE_SYSTEM),
overwrite: OverwriteMode::from_matches(matches),
parents: matches.is_present(OPT_PARENTS), parents: matches.is_present(OPT_PARENTS),
backup_suffix: matches.value_of(OPT_SUFFIX).unwrap().to_string(),
update: matches.is_present(OPT_UPDATE), update: matches.is_present(OPT_UPDATE),
verbose: matches.is_present(OPT_VERBOSE), verbose: matches.is_present(OPT_VERBOSE),
strip_trailing_slashes: matches.is_present(OPT_STRIP_TRAILING_SLASHES), strip_trailing_slashes: matches.is_present(OPT_STRIP_TRAILING_SLASHES),
@ -654,7 +668,9 @@ impl Options {
ReflinkMode::Never ReflinkMode::Never
} }
}, },
backup, backup: backup_mode,
backup_suffix: backup_suffix,
overwrite: overwrite,
no_target_dir, no_target_dir,
preserve_attributes, preserve_attributes,
recursive, recursive,
@ -1090,14 +1106,10 @@ fn context_for(src: &Path, dest: &Path) -> String {
format!("'{}' -> '{}'", src.display(), dest.display()) format!("'{}' -> '{}'", src.display(), dest.display())
} }
/// Implements a relatively naive backup that is not as full featured /// Implements a simple backup copy for the destination file.
/// as GNU cp. No CONTROL version control method argument is taken /// TODO: for the backup, should this function be replaced by `copy_file(...)`?
/// for backups. fn backup_dest(dest: &Path, backup_path: &PathBuf) -> CopyResult<PathBuf> {
/// TODO: Add version control methods fs::copy(dest, &backup_path)?;
fn backup_file(path: &Path, suffix: &str) -> CopyResult<PathBuf> {
let mut backup_path = path.to_path_buf().into_os_string();
backup_path.push(suffix);
fs::copy(path, &backup_path)?;
Ok(backup_path.into()) Ok(backup_path.into())
} }
@ -1108,8 +1120,9 @@ fn handle_existing_dest(source: &Path, dest: &Path, options: &Options) -> CopyRe
options.overwrite.verify(dest)?; options.overwrite.verify(dest)?;
if options.backup { let backup_path = backup_control::get_backup_path(options.backup, dest, &options.backup_suffix);
backup_file(dest, &options.backup_suffix)?; if let Some(backup_path) = backup_path {
backup_dest(dest, &backup_path)?;
} }
match options.overwrite { match options.overwrite {

View file

@ -207,11 +207,15 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
.alias(OPT_UNIVERSAL_2) .alias(OPT_UNIVERSAL_2)
.help("print or set Coordinated Universal Time (UTC)"), .help("print or set Coordinated Universal Time (UTC)"),
) )
.arg(Arg::with_name(OPT_FORMAT).multiple(true)) .arg(Arg::with_name(OPT_FORMAT).multiple(false))
.get_matches_from(args); .get_matches_from(args);
let format = if let Some(form) = matches.value_of(OPT_FORMAT) { let format = if let Some(form) = matches.value_of(OPT_FORMAT) {
let form = form[1..].into(); if !form.starts_with('+') {
eprintln!("date: invalid date {}", form);
return 1;
}
let form = form[1..].to_string();
Format::Custom(form) Format::Custom(form)
} else if let Some(fmt) = matches } else if let Some(fmt) = matches
.values_of(OPT_ISO_8601) .values_of(OPT_ISO_8601)
@ -237,7 +241,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
let set_to = match matches.value_of(OPT_SET).map(parse_date) { let set_to = match matches.value_of(OPT_SET).map(parse_date) {
None => None, None => None,
Some(Err((input, _err))) => { Some(Err((input, _err))) => {
eprintln!("date: invalid date '{}'", input); eprintln!("date: invalid date {}", input);
return 1; return 1;
} }
Some(Ok(date)) => Some(date), Some(Ok(date)) => Some(date),
@ -297,11 +301,13 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
for date in dates { for date in dates {
match date { match date {
Ok(date) => { Ok(date) => {
let formatted = date.format(format_string); // GNU `date` uses `%N` for nano seconds, however crate::chrono uses `%f`
let format_string = &format_string.replace("%N", "%f");
let formatted = date.format(format_string).to_string().replace("%f", "%N");
println!("{}", formatted); println!("{}", formatted);
} }
Err((input, _err)) => { Err((input, _err)) => {
println!("date: invalid date '{}'", input); println!("date: invalid date {}", input);
} }
} }
} }

View file

@ -16,14 +16,10 @@ path = "src/df.rs"
[dependencies] [dependencies]
clap = "2.33" clap = "2.33"
libc = "0.2"
number_prefix = "0.4" number_prefix = "0.4"
uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["libc", "fsext"] }
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }
[target.'cfg(target_os = "windows")'.dependencies]
winapi = { version = "0.3", features = ["errhandlingapi", "fileapi", "handleapi", "winerror"] }
[[bin]] [[bin]]
name = "df" name = "df"
path = "src/main.rs" path = "src/main.rs"

View file

@ -6,22 +6,17 @@
// For the full copyright and license information, please view the LICENSE file // For the full copyright and license information, please view the LICENSE file
// that was distributed with this source code. // that was distributed with this source code.
// spell-checker:ignore (ToDO) mountinfo mtab BLOCKSIZE getmntinfo fobj mptr noatime Iused overmounted // spell-checker:ignore (ToDO) mountinfo BLOCKSIZE fobj mptr noatime Iused overmounted
// spell-checker:ignore (libc/fs) asyncreads asyncwrites autofs bavail bfree bsize charspare cifs debugfs devfs devpts ffree frsize fsid fstypename fusectl inode inodes iosize kernfs mntbufp mntfromname mntonname mqueue namemax pipefs smbfs statfs statvfs subfs syncreads syncwrites sysfs wcslen // spell-checker:ignore (libc/fs) asyncreads asyncwrites autofs bavail bfree bsize charspare cifs debugfs devfs devpts ffree frsize fsid fstypename fusectl inode inodes iosize kernfs mntbufp mntfromname mntonname mqueue namemax pipefs smbfs statvfs subfs syncreads syncwrites sysfs wcslen
#[macro_use] #[macro_use]
extern crate uucore; extern crate uucore;
#[cfg(unix)]
use uucore::fsext::statfs_fn;
use uucore::fsext::{read_fs_list, FsUsage, MountInfo};
use clap::{App, Arg}; use clap::{App, Arg};
#[cfg(windows)]
use winapi::um::errhandlingapi::GetLastError;
#[cfg(windows)]
use winapi::um::fileapi::{
FindFirstVolumeW, FindNextVolumeW, FindVolumeClose, GetDriveTypeW, GetVolumeInformationW,
GetVolumePathNamesForVolumeNameW, QueryDosDeviceW,
};
use number_prefix::NumberPrefix; use number_prefix::NumberPrefix;
use std::cell::Cell; use std::cell::Cell;
use std::collections::HashMap; use std::collections::HashMap;
@ -32,41 +27,11 @@ use std::ffi::CString;
#[cfg(unix)] #[cfg(unix)]
use std::mem; use std::mem;
#[cfg(any(target_vendor = "apple", target_os = "freebsd"))]
use libc::c_int;
#[cfg(target_vendor = "apple")]
use libc::statfs;
#[cfg(any(target_vendor = "apple", target_os = "freebsd"))]
use std::ffi::CStr;
#[cfg(any(target_vendor = "apple", target_os = "freebsd", target_os = "windows"))]
use std::ptr;
#[cfg(any(target_vendor = "apple", target_os = "freebsd"))]
use std::slice;
#[cfg(target_os = "freebsd")] #[cfg(target_os = "freebsd")]
use libc::{c_char, fsid_t, uid_t}; use uucore::libc::{c_char, fsid_t, uid_t};
#[cfg(target_os = "linux")]
use std::fs::File;
#[cfg(target_os = "linux")]
use std::io::{BufRead, BufReader};
#[cfg(windows)]
use std::ffi::OsString;
#[cfg(windows)]
use std::os::windows::ffi::OsStrExt;
#[cfg(windows)]
use std::os::windows::ffi::OsStringExt;
#[cfg(windows)] #[cfg(windows)]
use std::path::Path; use std::path::Path;
#[cfg(windows)]
use winapi::shared::minwindef::DWORD;
#[cfg(windows)]
use winapi::um::fileapi::GetDiskFreeSpaceW;
#[cfg(windows)]
use winapi::um::handleapi::INVALID_HANDLE_VALUE;
#[cfg(windows)]
use winapi::um::winbase::DRIVE_REMOTE;
static VERSION: &str = env!("CARGO_PKG_VERSION"); static VERSION: &str = env!("CARGO_PKG_VERSION");
static ABOUT: &str = "Show information about the file system on which each FILE resides,\n\ static ABOUT: &str = "Show information about the file system on which each FILE resides,\n\
@ -75,14 +40,6 @@ static ABOUT: &str = "Show information about the file system on which each FILE
static EXIT_OK: i32 = 0; static EXIT_OK: i32 = 0;
static EXIT_ERR: i32 = 1; static EXIT_ERR: i32 = 1;
#[cfg(windows)]
const MAX_PATH: usize = 266;
#[cfg(target_os = "linux")]
static LINUX_MOUNTINFO: &str = "/proc/self/mountinfo";
#[cfg(target_os = "linux")]
static LINUX_MTAB: &str = "/etc/mtab";
static OPT_ALL: &str = "all"; static OPT_ALL: &str = "all";
static OPT_BLOCKSIZE: &str = "blocksize"; static OPT_BLOCKSIZE: &str = "blocksize";
static OPT_DIRECT: &str = "direct"; static OPT_DIRECT: &str = "direct";
@ -101,8 +58,6 @@ static OPT_TYPE: &str = "type";
static OPT_PRINT_TYPE: &str = "print-type"; static OPT_PRINT_TYPE: &str = "print-type";
static OPT_EXCLUDE_TYPE: &str = "exclude-type"; static OPT_EXCLUDE_TYPE: &str = "exclude-type";
static MOUNT_OPT_BIND: &str = "bind";
/// Store names of file systems as a selector. /// Store names of file systems as a selector.
/// Note: `exclude` takes priority over `include`. /// Note: `exclude` takes priority over `include`.
struct FsSelector { struct FsSelector {
@ -121,136 +76,16 @@ struct Options {
fs_selector: FsSelector, fs_selector: FsSelector,
} }
#[derive(Debug, Clone)]
struct MountInfo {
// it stores `volume_name` in windows platform and `dev_id` in unix platform
dev_id: String,
dev_name: String,
fs_type: String,
mount_dir: String,
mount_option: String, // we only care "bind" option
mount_root: String,
remote: bool,
dummy: bool,
}
#[cfg(all(
target_os = "freebsd",
not(all(target_vendor = "apple", target_arch = "x86_64"))
))]
#[repr(C)]
#[derive(Copy, Clone)]
#[allow(non_camel_case_types)]
struct statfs {
f_version: u32,
f_type: u32,
f_flags: u64,
f_bsize: u64,
f_iosize: u64,
f_blocks: u64,
f_bfree: u64,
f_bavail: i64,
f_files: u64,
f_ffree: i64,
f_syncwrites: u64,
f_asyncwrites: u64,
f_syncreads: u64,
f_asyncreads: u64,
f_spare: [u64; 10usize],
f_namemax: u32,
f_owner: uid_t,
f_fsid: fsid_t,
f_charspare: [c_char; 80usize],
f_fstypename: [c_char; 16usize],
f_mntfromname: [c_char; 88usize],
f_mntonname: [c_char; 88usize],
}
#[derive(Debug, Clone)]
struct FsUsage {
blocksize: u64,
blocks: u64,
bfree: u64,
bavail: u64,
bavail_top_bit_set: bool,
files: u64,
ffree: u64,
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct Filesystem { struct Filesystem {
mountinfo: MountInfo, mountinfo: MountInfo,
usage: FsUsage, usage: FsUsage,
} }
#[cfg(windows)]
macro_rules! String2LPWSTR {
($str: expr) => {
OsString::from($str.clone())
.as_os_str()
.encode_wide()
.chain(Some(0))
.collect::<Vec<u16>>()
.as_ptr()
};
}
#[cfg(windows)]
#[allow(non_snake_case)]
fn LPWSTR2String(buf: &[u16]) -> String {
let len = unsafe { libc::wcslen(buf.as_ptr()) };
OsString::from_wide(&buf[..len as usize])
.into_string()
.unwrap()
}
fn get_usage() -> String { fn get_usage() -> String {
format!("{0} [OPTION]... [FILE]...", executable!()) format!("{0} [OPTION]... [FILE]...", executable!())
} }
#[cfg(any(target_os = "freebsd", target_vendor = "apple"))]
extern "C" {
#[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_vendor = "apple", target_arch = "aarch64")
))]
fn getmntinfo(mntbufp: *mut *mut statfs, flags: c_int) -> c_int;
}
#[cfg(any(target_os = "freebsd", target_vendor = "apple"))]
impl From<statfs> for MountInfo {
fn from(statfs: statfs) -> Self {
let mut info = MountInfo {
dev_id: "".to_string(),
dev_name: unsafe {
CStr::from_ptr(&statfs.f_mntfromname[0])
.to_string_lossy()
.into_owned()
},
fs_type: unsafe {
CStr::from_ptr(&statfs.f_fstypename[0])
.to_string_lossy()
.into_owned()
},
mount_dir: unsafe {
CStr::from_ptr(&statfs.f_mntonname[0])
.to_string_lossy()
.into_owned()
},
mount_root: "".to_string(),
mount_option: "".to_string(),
remote: false,
dummy: false,
};
info.set_missing_fields();
info
}
}
impl FsSelector { impl FsSelector {
fn new() -> FsSelector { fn new() -> FsSelector {
FsSelector { FsSelector {
@ -295,239 +130,6 @@ impl Options {
} }
} }
impl MountInfo {
fn set_missing_fields(&mut self) {
#[cfg(unix)]
{
// We want to keep the dev_id on Windows
// but set dev_id
let path = CString::new(self.mount_dir.clone()).unwrap();
unsafe {
let mut stat = mem::zeroed();
if libc::stat(path.as_ptr(), &mut stat) == 0 {
self.dev_id = (stat.st_dev as i32).to_string();
} else {
self.dev_id = "".to_string();
}
}
}
// set MountInfo::dummy
match self.fs_type.as_ref() {
"autofs" | "proc" | "subfs"
/* for Linux 2.6/3.x */
| "debugfs" | "devpts" | "fusectl" | "mqueue" | "rpc_pipefs" | "sysfs"
/* FreeBSD, Linux 2.4 */
| "devfs"
/* for NetBSD 3.0 */
| "kernfs"
/* for Irix 6.5 */
| "ignore" => self.dummy = true,
_ => self.dummy = self.fs_type == "none"
&& self.mount_option.find(MOUNT_OPT_BIND).is_none(),
}
// set MountInfo::remote
#[cfg(windows)]
{
self.remote = DRIVE_REMOTE == unsafe { GetDriveTypeW(String2LPWSTR!(self.mount_root)) };
}
#[cfg(unix)]
{
if self.dev_name.find(':').is_some()
|| (self.dev_name.starts_with("//") && self.fs_type == "smbfs"
|| self.fs_type == "cifs")
|| self.dev_name == "-hosts"
{
self.remote = true;
} else {
self.remote = false;
}
}
}
#[cfg(target_os = "linux")]
fn new(file_name: &str, raw: Vec<&str>) -> Option<MountInfo> {
match file_name {
// Format: 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue
// "man proc" for more details
"/proc/self/mountinfo" => {
let mut m = MountInfo {
dev_id: "".to_string(),
dev_name: raw[9].to_string(),
fs_type: raw[8].to_string(),
mount_root: raw[3].to_string(),
mount_dir: raw[4].to_string(),
mount_option: raw[5].to_string(),
remote: false,
dummy: false,
};
m.set_missing_fields();
Some(m)
}
"/etc/mtab" => {
let mut m = MountInfo {
dev_id: "".to_string(),
dev_name: raw[0].to_string(),
fs_type: raw[2].to_string(),
mount_root: "".to_string(),
mount_dir: raw[1].to_string(),
mount_option: raw[3].to_string(),
remote: false,
dummy: false,
};
m.set_missing_fields();
Some(m)
}
_ => None,
}
}
#[cfg(windows)]
fn new(mut volume_name: String) -> Option<MountInfo> {
let mut dev_name_buf = [0u16; MAX_PATH];
volume_name.pop();
unsafe {
QueryDosDeviceW(
OsString::from(volume_name.clone())
.as_os_str()
.encode_wide()
.chain(Some(0))
.skip(4)
.collect::<Vec<u16>>()
.as_ptr(),
dev_name_buf.as_mut_ptr(),
dev_name_buf.len() as DWORD,
)
};
volume_name.push('\\');
let dev_name = LPWSTR2String(&dev_name_buf);
let mut mount_root_buf = [0u16; MAX_PATH];
let success = unsafe {
GetVolumePathNamesForVolumeNameW(
String2LPWSTR!(volume_name),
mount_root_buf.as_mut_ptr(),
mount_root_buf.len() as DWORD,
ptr::null_mut(),
)
};
if 0 == success {
// TODO: support the case when `GetLastError()` returns `ERROR_MORE_DATA`
return None;
}
let mount_root = LPWSTR2String(&mount_root_buf);
let mut fs_type_buf = [0u16; MAX_PATH];
let success = unsafe {
GetVolumeInformationW(
String2LPWSTR!(mount_root),
ptr::null_mut(),
0 as DWORD,
ptr::null_mut(),
ptr::null_mut(),
ptr::null_mut(),
fs_type_buf.as_mut_ptr(),
fs_type_buf.len() as DWORD,
)
};
let fs_type = if 0 != success {
Some(LPWSTR2String(&fs_type_buf))
} else {
None
};
let mut mn_info = MountInfo {
dev_id: volume_name,
dev_name,
fs_type: fs_type.unwrap_or_else(|| "".to_string()),
mount_root,
mount_dir: "".to_string(),
mount_option: "".to_string(),
remote: false,
dummy: false,
};
mn_info.set_missing_fields();
Some(mn_info)
}
}
impl FsUsage {
#[cfg(unix)]
fn new(statvfs: libc::statvfs) -> FsUsage {
{
FsUsage {
blocksize: if statvfs.f_frsize != 0 {
statvfs.f_frsize as u64
} else {
statvfs.f_bsize as u64
},
blocks: statvfs.f_blocks as u64,
bfree: statvfs.f_bfree as u64,
bavail: statvfs.f_bavail as u64,
bavail_top_bit_set: ((statvfs.f_bavail as u64) & (1u64.rotate_right(1))) != 0,
files: statvfs.f_files as u64,
ffree: statvfs.f_ffree as u64,
}
}
}
#[cfg(not(unix))]
fn new(path: &Path) -> FsUsage {
let mut root_path = [0u16; MAX_PATH];
let success = unsafe {
GetVolumePathNamesForVolumeNameW(
//path_utf8.as_ptr(),
String2LPWSTR!(path.as_os_str()),
root_path.as_mut_ptr(),
root_path.len() as DWORD,
ptr::null_mut(),
)
};
if 0 == success {
crash!(
EXIT_ERR,
"GetVolumePathNamesForVolumeNameW failed: {}",
unsafe { GetLastError() }
);
}
let mut sectors_per_cluster = 0;
let mut bytes_per_sector = 0;
let mut number_of_free_clusters = 0;
let mut total_number_of_clusters = 0;
let success = unsafe {
GetDiskFreeSpaceW(
String2LPWSTR!(path.as_os_str()),
&mut sectors_per_cluster,
&mut bytes_per_sector,
&mut number_of_free_clusters,
&mut total_number_of_clusters,
)
};
if 0 == success {
// Fails in case of CD for example
//crash!(EXIT_ERR, "GetDiskFreeSpaceW failed: {}", unsafe {
//GetLastError()
//});
}
let bytes_per_cluster = sectors_per_cluster as u64 * bytes_per_sector as u64;
FsUsage {
// f_bsize File system block size.
blocksize: bytes_per_cluster as u64,
// f_blocks - Total number of blocks on the file system, in units of f_frsize.
// frsize = Fundamental file system block size (fragment size).
blocks: total_number_of_clusters as u64,
// Total number of free blocks.
bfree: number_of_free_clusters as u64,
// Total number of free blocks available to non-privileged processes.
bavail: 0 as u64,
bavail_top_bit_set: ((bytes_per_sector as u64) & (1u64.rotate_right(1))) != 0,
// Total number of file nodes (inodes) on the file system.
files: 0 as u64, // Not available on windows
// Total number of free file nodes (inodes).
ffree: 4096 as u64, // Meaningless on Windows
}
}
}
impl Filesystem { impl Filesystem {
// TODO: resolve uuid in `mountinfo.dev_name` if exists // TODO: resolve uuid in `mountinfo.dev_name` if exists
fn new(mountinfo: MountInfo) -> Option<Filesystem> { fn new(mountinfo: MountInfo) -> Option<Filesystem> {
@ -548,7 +150,7 @@ impl Filesystem {
unsafe { unsafe {
let path = CString::new(_stat_path).unwrap(); let path = CString::new(_stat_path).unwrap();
let mut statvfs = mem::zeroed(); let mut statvfs = mem::zeroed();
if libc::statvfs(path.as_ptr(), &mut statvfs) < 0 { if statfs_fn(path.as_ptr(), &mut statvfs) < 0 {
None None
} else { } else {
Some(Filesystem { Some(Filesystem {
@ -565,80 +167,6 @@ impl Filesystem {
} }
} }
/// Read file system list.
fn read_fs_list() -> Vec<MountInfo> {
#[cfg(target_os = "linux")]
{
let (file_name, fobj) = File::open(LINUX_MOUNTINFO)
.map(|f| (LINUX_MOUNTINFO, f))
.or_else(|_| File::open(LINUX_MTAB).map(|f| (LINUX_MTAB, f)))
.expect("failed to find mount list files");
let reader = BufReader::new(fobj);
reader
.lines()
.filter_map(|line| line.ok())
.filter_map(|line| {
let raw_data = line.split_whitespace().collect::<Vec<&str>>();
MountInfo::new(file_name, raw_data)
})
.collect::<Vec<_>>()
}
#[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) };
if len < 0 {
crash!(EXIT_ERR, "getmntinfo failed");
}
let mounts = unsafe { slice::from_raw_parts(mptr, len as usize) };
mounts
.iter()
.map(|m| MountInfo::from(*m))
.collect::<Vec<_>>()
}
#[cfg(windows)]
{
let mut volume_name_buf = [0u16; MAX_PATH];
// As recommended in the MS documentation, retrieve the first volume before the others
let find_handle = unsafe {
FindFirstVolumeW(volume_name_buf.as_mut_ptr(), volume_name_buf.len() as DWORD)
};
if INVALID_HANDLE_VALUE == find_handle {
crash!(EXIT_ERR, "FindFirstVolumeW failed: {}", unsafe {
GetLastError()
});
}
let mut mounts = Vec::<MountInfo>::new();
loop {
let volume_name = LPWSTR2String(&volume_name_buf);
if !volume_name.starts_with("\\\\?\\") || !volume_name.ends_with('\\') {
show_warning!("A bad path was skipped: {}", volume_name);
continue;
}
if let Some(m) = MountInfo::new(volume_name) {
mounts.push(m);
}
if 0 == unsafe {
FindNextVolumeW(
find_handle,
volume_name_buf.as_mut_ptr(),
volume_name_buf.len() as DWORD,
)
} {
let err = unsafe { GetLastError() };
if err != winapi::shared::winerror::ERROR_NO_MORE_FILES {
crash!(EXIT_ERR, "FindNextVolumeW failed: {}", err);
}
break;
}
}
unsafe {
FindVolumeClose(find_handle);
}
mounts
}
}
fn filter_mount_list(vmi: Vec<MountInfo>, paths: &[String], opt: &Options) -> Vec<MountInfo> { fn filter_mount_list(vmi: Vec<MountInfo>, paths: &[String], opt: &Options) -> Vec<MountInfo> {
vmi.into_iter() vmi.into_iter()
.filter_map(|mi| { .filter_map(|mi| {

View file

@ -105,7 +105,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
if out_format == OutputFmt::Unknown { if out_format == OutputFmt::Unknown {
match guess_syntax() { match guess_syntax() {
OutputFmt::Unknown => { OutputFmt::Unknown => {
show_info!("no SHELL environment variable, and no shell type option given"); show_error!("no SHELL environment variable, and no shell type option given");
return 1; return 1;
} }
fmt => out_format = fmt, fmt => out_format = fmt,
@ -130,7 +130,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
) )
} }
Err(e) => { Err(e) => {
show_info!("{}: {}", matches.free[0], e); show_error!("{}: {}", matches.free[0], e);
return 1; return 1;
} }
} }
@ -141,7 +141,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
0 0
} }
Err(s) => { Err(s) => {
show_info!("{}", s); show_error!("{}", s);
1 1
} }
} }

View file

@ -15,6 +15,7 @@ edition = "2018"
path = "src/du.rs" path = "src/du.rs"
[dependencies] [dependencies]
clap = "2.33"
chrono = "0.4" chrono = "0.4"
uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore = { version=">=0.0.8", package="uucore", path="../../uucore" }
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }

View file

@ -12,6 +12,7 @@ extern crate uucore;
use chrono::prelude::DateTime; use chrono::prelude::DateTime;
use chrono::Local; use chrono::Local;
use clap::{App, Arg};
use std::collections::HashSet; use std::collections::HashSet;
use std::env; use std::env;
use std::fs; use std::fs;
@ -37,6 +38,27 @@ use winapi::um::winbase::GetFileInformationByHandleEx;
#[cfg(windows)] #[cfg(windows)]
use winapi::um::winnt::{FILE_ID_128, ULONGLONG}; use winapi::um::winnt::{FILE_ID_128, ULONGLONG};
mod options {
pub const NULL: &str = "0";
pub const ALL: &str = "all";
pub const APPARENT_SIZE: &str = "apparent-size";
pub const BLOCK_SIZE: &str = "B";
pub const BYTES: &str = "b";
pub const TOTAL: &str = "c";
pub const MAX_DEPTH: &str = "d";
pub const HUMAN_READABLE: &str = "h";
pub const BLOCK_SIZE_1K: &str = "k";
pub const COUNT_LINKS: &str = "l";
pub const BLOCK_SIZE_1M: &str = "m";
pub const SEPARATE_DIRS: &str = "S";
pub const SUMMARIZE: &str = "s";
pub const SI: &str = "si";
pub const TIME: &str = "time";
pub const TIME_STYLE: &str = "time-style";
pub const FILE: &str = "FILE";
}
const VERSION: &str = env!("CARGO_PKG_VERSION");
const NAME: &str = "du"; const NAME: &str = "du";
const SUMMARY: &str = "estimate file space usage"; const SUMMARY: &str = "estimate file space usage";
const LONG_HELP: &str = " const LONG_HELP: &str = "
@ -220,14 +242,14 @@ fn unit_string_to_number(s: &str) -> Option<u64> {
Some(number * multiple.pow(unit)) Some(number * multiple.pow(unit))
} }
fn translate_to_pure_number(s: &Option<String>) -> Option<u64> { fn translate_to_pure_number(s: &Option<&str>) -> Option<u64> {
match *s { match *s {
Some(ref s) => unit_string_to_number(s), Some(ref s) => unit_string_to_number(s),
None => None, None => None,
} }
} }
fn read_block_size(s: Option<String>) -> u64 { fn read_block_size(s: Option<&str>) -> u64 {
match translate_to_pure_number(&s) { match translate_to_pure_number(&s) {
Some(v) => v, Some(v) => v,
None => { None => {
@ -236,7 +258,8 @@ fn read_block_size(s: Option<String>) -> u64 {
}; };
for env_var in &["DU_BLOCK_SIZE", "BLOCK_SIZE", "BLOCKSIZE"] { for env_var in &["DU_BLOCK_SIZE", "BLOCK_SIZE", "BLOCKSIZE"] {
if let Some(quantity) = translate_to_pure_number(&env::var(env_var).ok()) { let env_size = env::var(env_var).ok();
if let Some(quantity) = translate_to_pure_number(&env_size.as_deref()) {
return quantity; return quantity;
} }
} }
@ -361,126 +384,189 @@ fn convert_size_other(size: u64, _multiplier: u64, block_size: u64) -> String {
format!("{}", ((size as f64) / (block_size as f64)).ceil()) format!("{}", ((size as f64) / (block_size as f64)).ceil())
} }
fn get_usage() -> String {
format!(
"{0} [OPTION]... [FILE]...
{0} [OPTION]... --files0-from=F",
executable!()
)
}
#[allow(clippy::cognitive_complexity)] #[allow(clippy::cognitive_complexity)]
pub fn uumain(args: impl uucore::Args) -> i32 { pub fn uumain(args: impl uucore::Args) -> i32 {
let args = args let args = args
.collect_str(InvalidEncodingHandling::Ignore) .collect_str(InvalidEncodingHandling::Ignore)
.accept_any(); .accept_any();
let syntax = format!( let usage = get_usage();
"[OPTION]... [FILE]...
{0} [OPTION]... --files0-from=F",
NAME
);
let matches = app!(&syntax, SUMMARY, LONG_HELP)
// In task
.optflag(
"a",
"all",
" write counts for all files, not just directories",
)
// In main
.optflag(
"",
"apparent-size",
"print apparent sizes, rather than disk usage
although the apparent size is usually smaller, it may be larger due to holes
in ('sparse') files, internal fragmentation, indirect blocks, and the like",
)
// In main
.optopt(
"B",
"block-size",
"scale sizes by SIZE before printing them.
E.g., '-BM' prints sizes in units of 1,048,576 bytes. See SIZE format below.",
"SIZE",
)
// In main
.optflag(
"b",
"bytes",
"equivalent to '--apparent-size --block-size=1'",
)
// In main
.optflag("c", "total", "produce a grand total")
// In task
// opts.optflag("D", "dereference-args", "dereference only symlinks that are listed
// on the command line"),
// In main
// opts.optopt("", "files0-from", "summarize disk usage of the NUL-terminated file
// names specified in file F;
// If F is - then read names from standard input", "F"),
// // In task
// opts.optflag("H", "", "equivalent to --dereference-args (-D)"),
// In main
.optflag(
"h",
"human-readable",
"print sizes in human readable format (e.g., 1K 234M 2G)",
)
// In main
.optflag("", "si", "like -h, but use powers of 1000 not 1024")
// In main
.optflag("k", "", "like --block-size=1K")
// In task
.optflag("l", "count-links", "count sizes many times if hard linked")
// // In main
.optflag("m", "", "like --block-size=1M")
// // In task
// opts.optflag("L", "dereference", "dereference all symbolic links"),
// // In task
// opts.optflag("P", "no-dereference", "don't follow any symbolic links (this is the default)"),
// // In main
.optflag(
"0",
"null",
"end each output line with 0 byte rather than newline",
)
// In main
.optflag(
"S",
"separate-dirs",
"do not include size of subdirectories",
)
// In main
.optflag("s", "summarize", "display only a total for each argument")
// // In task
// opts.optflag("x", "one-file-system", "skip directories on different file systems"),
// // In task
// opts.optopt("X", "exclude-from", "exclude files that match any pattern in FILE", "FILE"),
// // In task
// opts.optopt("", "exclude", "exclude files that match PATTERN", "PATTERN"),
// In main
.optopt(
"d",
"max-depth",
"print the total for a directory (or file, with --all)
only if it is N or fewer levels below the command
line argument; --max-depth=0 is the same as --summarize",
"N",
)
// In main
.optflagopt(
"",
"time",
"show time of the last modification of any file in the
directory, or any of its subdirectories. If WORD is given, show time as WORD instead
of modification time: atime, access, use, ctime or status",
"WORD",
)
// In main
.optopt(
"",
"time-style",
"show times using style STYLE:
full-iso, long-iso, iso, +FORMAT FORMAT is interpreted like 'date'",
"STYLE",
)
.parse(args);
let summarize = matches.opt_present("summarize"); let matches = App::new(executable!())
.version(VERSION)
.about(SUMMARY)
.usage(&usage[..])
.after_help(LONG_HELP)
.arg(
Arg::with_name(options::ALL)
.short("a")
.long(options::ALL)
.help("write counts for all files, not just directories"),
)
.arg(
Arg::with_name(options::APPARENT_SIZE)
.long(options::APPARENT_SIZE)
.help(
"print apparent sizes, rather than disk usage \
although the apparent size is usually smaller, it may be larger due to holes \
in ('sparse') files, internal fragmentation, indirect blocks, and the like"
)
)
.arg(
Arg::with_name(options::BLOCK_SIZE)
.short("B")
.long("block-size")
.value_name("SIZE")
.help(
"scale sizes by SIZE before printing them. \
E.g., '-BM' prints sizes in units of 1,048,576 bytes. See SIZE format below."
)
)
.arg(
Arg::with_name(options::BYTES)
.short("b")
.long("bytes")
.help("equivalent to '--apparent-size --block-size=1'")
)
.arg(
Arg::with_name(options::TOTAL)
.long("total")
.short("c")
.help("produce a grand total")
)
.arg(
Arg::with_name(options::MAX_DEPTH)
.short("d")
.long("max-depth")
.value_name("N")
.help(
"print the total for a directory (or file, with --all) \
only if it is N or fewer levels below the command \
line argument; --max-depth=0 is the same as --summarize"
)
)
.arg(
Arg::with_name(options::HUMAN_READABLE)
.long("human-readable")
.short("h")
.help("print sizes in human readable format (e.g., 1K 234M 2G)")
)
.arg(
Arg::with_name("inodes")
.long("inodes")
.help(
"list inode usage information instead of block usage like --block-size=1K"
)
)
.arg(
Arg::with_name(options::BLOCK_SIZE_1K)
.short("k")
.help("like --block-size=1K")
)
.arg(
Arg::with_name(options::COUNT_LINKS)
.short("l")
.long("count-links")
.help("count sizes many times if hard linked")
)
// .arg(
// Arg::with_name("dereference")
// .short("L")
// .long("dereference")
// .help("dereference all symbolic links")
// )
// .arg(
// Arg::with_name("no-dereference")
// .short("P")
// .long("no-dereference")
// .help("don't follow any symbolic links (this is the default)")
// )
.arg(
Arg::with_name(options::BLOCK_SIZE_1M)
.short("m")
.help("like --block-size=1M")
)
.arg(
Arg::with_name(options::NULL)
.short("0")
.long("null")
.help("end each output line with 0 byte rather than newline")
)
.arg(
Arg::with_name(options::SEPARATE_DIRS)
.short("S")
.long("separate-dirs")
.help("do not include size of subdirectories")
)
.arg(
Arg::with_name(options::SUMMARIZE)
.short("s")
.long("summarize")
.help("display only a total for each argument")
)
.arg(
Arg::with_name(options::SI)
.long(options::SI)
.help("like -h, but use powers of 1000 not 1024")
)
// .arg(
// Arg::with_name("one-file-system")
// .short("x")
// .long("one-file-system")
// .help("skip directories on different file systems")
// )
// .arg(
// Arg::with_name("")
// .short("x")
// .long("exclude-from")
// .value_name("FILE")
// .help("exclude files that match any pattern in FILE")
// )
// .arg(
// Arg::with_name("exclude")
// .long("exclude")
// .value_name("PATTERN")
// .help("exclude files that match PATTERN")
// )
.arg(
Arg::with_name(options::TIME)
.long(options::TIME)
.value_name("WORD")
.require_equals(true)
.min_values(0)
.help(
"show time of the last modification of any file in the \
directory, or any of its subdirectories. If WORD is given, show time as WORD instead \
of modification time: atime, access, use, ctime or status"
)
)
.arg(
Arg::with_name(options::TIME_STYLE)
.long(options::TIME_STYLE)
.value_name("STYLE")
.help(
"show times using style STYLE: \
full-iso, long-iso, iso, +FORMAT FORMAT is interpreted like 'date'"
)
)
.arg(
Arg::with_name(options::FILE)
.hidden(true)
.multiple(true)
)
.get_matches_from(args);
let max_depth_str = matches.opt_str("max-depth"); let summarize = matches.is_present(options::SUMMARIZE);
let max_depth_str = matches.value_of(options::MAX_DEPTH);
let max_depth = max_depth_str.as_ref().and_then(|s| s.parse::<usize>().ok()); let max_depth = max_depth_str.as_ref().and_then(|s| s.parse::<usize>().ok());
match (max_depth_str, max_depth) { match (max_depth_str, max_depth) {
(Some(ref s), _) if summarize => { (Some(ref s), _) if summarize => {
@ -495,34 +581,35 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
} }
let options = Options { let options = Options {
all: matches.opt_present("all"), all: matches.is_present(options::ALL),
program_name: NAME.to_owned(), program_name: NAME.to_owned(),
max_depth, max_depth,
total: matches.opt_present("total"), total: matches.is_present(options::TOTAL),
separate_dirs: matches.opt_present("S"), separate_dirs: matches.is_present(options::SEPARATE_DIRS),
}; };
let strs = if matches.free.is_empty() { let strs = match matches.value_of(options::FILE) {
vec!["./".to_owned()] // TODO: gnu `du` doesn't use trailing "/" here Some(_) => matches.values_of(options::FILE).unwrap().collect(),
} else { None => {
matches.free.clone() vec!["./"] // TODO: gnu `du` doesn't use trailing "/" here
}
}; };
let block_size = read_block_size(matches.opt_str("block-size")); let block_size = read_block_size(matches.value_of(options::BLOCK_SIZE));
let multiplier: u64 = if matches.opt_present("si") { let multiplier: u64 = if matches.is_present(options::SI) {
1000 1000
} else { } else {
1024 1024
}; };
let convert_size_fn = { let convert_size_fn = {
if matches.opt_present("human-readable") || matches.opt_present("si") { if matches.is_present(options::HUMAN_READABLE) || matches.is_present(options::SI) {
convert_size_human convert_size_human
} else if matches.opt_present("b") { } else if matches.is_present(options::BYTES) {
convert_size_b convert_size_b
} else if matches.opt_present("k") { } else if matches.is_present(options::BLOCK_SIZE_1K) {
convert_size_k convert_size_k
} else if matches.opt_present("m") { } else if matches.is_present(options::BLOCK_SIZE_1M) {
convert_size_m convert_size_m
} else { } else {
convert_size_other convert_size_other
@ -530,8 +617,8 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
}; };
let convert_size = |size| convert_size_fn(size, multiplier, block_size); let convert_size = |size| convert_size_fn(size, multiplier, block_size);
let time_format_str = match matches.opt_str("time-style") { let time_format_str = match matches.value_of("time-style") {
Some(s) => match &s[..] { Some(s) => match s {
"full-iso" => "%Y-%m-%d %H:%M:%S.%f %z", "full-iso" => "%Y-%m-%d %H:%M:%S.%f %z",
"long-iso" => "%Y-%m-%d %H:%M", "long-iso" => "%Y-%m-%d %H:%M",
"iso" => "%Y-%m-%d", "iso" => "%Y-%m-%d",
@ -552,7 +639,11 @@ Try '{} --help' for more information.",
None => "%Y-%m-%d %H:%M", None => "%Y-%m-%d %H:%M",
}; };
let line_separator = if matches.opt_present("0") { "\0" } else { "\n" }; let line_separator = if matches.is_present(options::NULL) {
"\0"
} else {
"\n"
};
let mut grand_total = 0; let mut grand_total = 0;
for path_str in strs { for path_str in strs {
@ -565,18 +656,20 @@ Try '{} --help' for more information.",
let (_, len) = iter.size_hint(); let (_, len) = iter.size_hint();
let len = len.unwrap(); let len = len.unwrap();
for (index, stat) in iter.enumerate() { for (index, stat) in iter.enumerate() {
let size = if matches.opt_present("apparent-size") || matches.opt_present("b") { let size = if matches.is_present(options::APPARENT_SIZE)
|| matches.is_present(options::BYTES)
{
stat.size stat.size
} else { } else {
// C's stat is such that each block is assume to be 512 bytes // C's stat is such that each block is assume to be 512 bytes
// See: http://linux.die.net/man/2/stat // See: http://linux.die.net/man/2/stat
stat.blocks * 512 stat.blocks * 512
}; };
if matches.opt_present("time") { if matches.is_present(options::TIME) {
let tm = { let tm = {
let secs = { let secs = {
match matches.opt_str("time") { match matches.value_of(options::TIME) {
Some(s) => match &s[..] { Some(s) => match s {
"accessed" => stat.accessed, "accessed" => stat.accessed,
"created" => stat.created, "created" => stat.created,
"modified" => stat.modified, "modified" => stat.modified,
@ -649,8 +742,8 @@ mod test_du {
(Some("900KB".to_string()), Some(900 * 1000)), (Some("900KB".to_string()), Some(900 * 1000)),
(Some("BAD_STRING".to_string()), None), (Some("BAD_STRING".to_string()), None),
]; ];
for it in test_data.into_iter() { for it in test_data.iter() {
assert_eq!(translate_to_pure_number(&it.0), it.1); assert_eq!(translate_to_pure_number(&it.0.as_deref()), it.1);
} }
} }
@ -661,8 +754,8 @@ mod test_du {
(None, 1024), (None, 1024),
(Some("BAD_STRING".to_string()), 1024), (Some("BAD_STRING".to_string()), 1024),
]; ];
for it in test_data.into_iter() { for it in test_data.iter() {
assert_eq!(read_block_size(it.0.clone()), it.1); assert_eq!(read_block_size(it.0.as_deref()), it.1);
} }
} }
} }

View file

@ -153,7 +153,7 @@ impl AstNode {
":" | "match" => operator_match(&operand_values), ":" | "match" => operator_match(&operand_values),
"length" => Ok(prefix_operator_length(&operand_values)), "length" => Ok(prefix_operator_length(&operand_values)),
"index" => Ok(prefix_operator_index(&operand_values)), "index" => Ok(prefix_operator_index(&operand_values)),
"substr" => prefix_operator_substr(&operand_values), "substr" => Ok(prefix_operator_substr(&operand_values)),
_ => Err(format!("operation not implemented: {}", op_type)), _ => Err(format!("operation not implemented: {}", op_type)),
}, },
@ -522,35 +522,23 @@ fn prefix_operator_index(values: &[String]) -> String {
"0".to_string() "0".to_string()
} }
fn prefix_operator_substr(values: &[String]) -> Result<String, String> { fn prefix_operator_substr(values: &[String]) -> String {
assert!(values.len() == 3); assert!(values.len() == 3);
let subj = &values[0]; let subj = &values[0];
let mut idx = match values[1].parse::<i64>() { let idx = match values[1]
Ok(i) => i, .parse::<usize>()
Err(_) => return Err("expected integer as POS arg to 'substr'".to_string()), .ok()
.and_then(|v| v.checked_sub(1))
{
Some(i) => i,
None => return String::new(),
}; };
let mut len = match values[2].parse::<i64>() { let len = match values[2].parse::<usize>() {
Ok(i) => i, Ok(i) => i,
Err(_) => return Err("expected integer as LENGTH arg to 'substr'".to_string()), Err(_) => return String::new(),
}; };
if idx <= 0 || len <= 0 { subj.chars().skip(idx).take(len).collect()
return Ok("".to_string());
}
let mut out_str = String::new();
for ch in subj.chars() {
idx -= 1;
if idx <= 0 {
if len <= 0 {
break;
}
len -= 1;
out_str.push(ch);
}
}
Ok(out_str)
} }
fn bool_as_int(b: bool) -> i64 { fn bool_as_int(b: bool) -> i64 {

View file

@ -0,0 +1,116 @@
# Benchmarking `factor`
The benchmarks for `factor` are located under `tests/benches/factor`
and can be invoked with `cargo bench` in that directory.
They are located outside the `uu_factor` crate, as they do not comply
with the project's minimum supported Rust version, *i.e.* may require
a newer version of `rustc`.
## Microbenchmarking deterministic functions
We currently use [`criterion`] to benchmark deterministic functions,
such as `gcd` and `table::factor`.
However, µbenchmarks are by nature unstable: not only are they specific to
the hardware, operating system version, etc., but they are noisy and affected
by other tasks on the system (browser, compile jobs, etc.), which can cause
`criterion` to report spurious performance improvements and regressions.
This can be mitigated by getting as close to [idealised conditions][lemire]
as possible:
- minimize the amount of computation and I/O running concurrently to the
benchmark, *i.e.* close your browser and IM clients, don't compile at the
same time, etc. ;
- ensure the CPU's [frequency stays constant] during the benchmark ;
- [isolate a **physical** core], set it to `nohz_full`, and pin the benchmark
to it, so it won't be preempted in the middle of a measurement ;
- disable ASLR by running `setarch -R cargo bench`, so we can compare results
across multiple executions.
[`criterion`]: https://bheisler.github.io/criterion.rs/book/index.html
[lemire]: https://lemire.me/blog/2018/01/16/microbenchmarking-calls-for-idealized-conditions/
[isolate a **physical** core]: https://pyperf.readthedocs.io/en/latest/system.html#isolate-cpus-on-linux
[frequency stays constant]: XXXTODO
### Guidance for designing µbenchmarks
*Note:* this guidance is specific to `factor` and takes its application domain
into account; do not expect it to generalise to other projects. It is based
on Daniel Lemire's [*Microbenchmarking calls for idealized conditions*][lemire],
which I recommend reading if you want to add benchmarks to `factor`.
1. Select a small, self-contained, deterministic component
`gcd` and `table::factor` are good example of such:
- no I/O or access to external data structures ;
- no call into other components ;
- behaviour is deterministic: no RNG, no concurrency, ... ;
- the test's body is *fast* (~100ns for `gcd`, ~10µs for `factor::table`),
so each sample takes a very short time, minimizing variability and
maximizing the numbers of samples we can take in a given time.
2. Benchmarks are immutable (once merged in `uutils`)
Modifying a benchmark means previously-collected values cannot meaningfully
be compared, silently giving nonsensical results. If you must modify an
existing benchmark, rename it.
3. Test common cases
We are interested in overall performance, rather than specific edge-cases;
use **reproducibly-randomised inputs**, sampling from either all possible
input values or some subset of interest.
4. Use [`criterion`], `criterion::black_box`, ...
`criterion` isn't perfect, but it is also much better than ad-hoc
solutions in each benchmark.
## Wishlist
### Configurable statistical estimators
`criterion` always uses the arithmetic average as estimator; in µbenchmarks,
where the code under test is fully deterministic and the measurements are
subject to additive, positive noise, [the minimum is more appropriate][lemire].
### CI & reproducible performance testing
Measuring performance on real hardware is important, as it relates directly
to what users of `factor` experience; however, such measurements are subject
to the constraints of the real-world, and aren't perfectly reproducible.
Moreover, the mitigations for it (described above) aren't achievable in
virtualized, multi-tenant environments such as CI.
Instead, we could run the µbenchmarks in a simulated CPU with [`cachegrind`],
measure execution “time” in that model (in CI), and use it to detect and report
performance improvements and regressions.
[`iai`] is an implementation of this idea for Rust.
[`cachegrind`]: https://www.valgrind.org/docs/manual/cg-manual.html
[`iai`]: https://bheisler.github.io/criterion.rs/book/iai/iai.html
### Comparing randomised implementations across multiple inputs
`factor` is a challenging target for system benchmarks as it combines two
characteristics:
1. integer factoring algorithms are randomised, with large variance in
execution time ;
2. various inputs also have large differences in factoring time, that
corresponds to no natural, linear ordering of the inputs.
If (1) was untrue (i.e. if execution time wasn't random), we could faithfully
compare 2 implementations (2 successive versions, or `uutils` and GNU) using
a scatter plot, where each axis corresponds to the perf. of one implementation.
Similarly, without (2) we could plot numbers on the X axis and their factoring
time on the Y axis, using multiple lines for various quantiles. The large
differences in factoring times for successive numbers, mean that such a plot
would be unreadable.

View file

@ -17,20 +17,15 @@ num-traits = "0.2.13" # used in src/numerics.rs, which is included by build.rs
[dependencies] [dependencies]
coz = { version = "0.1.3", optional = true } coz = { version = "0.1.3", optional = true }
num-traits = "0.2.13" # Needs at least version 0.2.13 for "OverflowingAdd" num-traits = "0.2.13" # Needs at least version 0.2.13 for "OverflowingAdd"
rand = { version="0.7", features=["small_rng"] } rand = { version = "0.7", features = ["small_rng"] }
smallvec = { version="0.6.14, < 1.0" } smallvec = { version = "0.6.14, < 1.0" }
uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore = { version = ">=0.0.8", package = "uucore", path = "../../uucore" }
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } uucore_procs = { version = ">=0.0.5", package = "uucore_procs", path = "../../uucore_procs" }
[dev-dependencies] [dev-dependencies]
criterion = "0.3"
paste = "0.1.18" paste = "0.1.18"
quickcheck = "0.9.2" quickcheck = "0.9.2"
rand_chacha = "0.2.2"
[[bench]]
name = "gcd"
harness = false
[[bin]] [[bin]]
name = "factor" name = "factor"

View file

@ -13,13 +13,13 @@ use std::error::Error;
use std::io::{self, stdin, stdout, BufRead, Write}; use std::io::{self, stdin, stdout, BufRead, Write};
mod factor; mod factor;
pub(crate) use factor::*; pub use factor::*;
use uucore::InvalidEncodingHandling; use uucore::InvalidEncodingHandling;
mod miller_rabin; mod miller_rabin;
pub mod numeric; pub mod numeric;
mod rho; mod rho;
mod table; pub mod table;
static SYNTAX: &str = "[OPTION] [NUMBER]..."; static SYNTAX: &str = "[OPTION] [NUMBER]...";
static SUMMARY: &str = "Print the prime factors of the given number(s). static SUMMARY: &str = "Print the prime factors of the given number(s).

View file

@ -161,8 +161,9 @@ pub fn factor(mut n: u64) -> Factors {
return factors; return factors;
} }
let (factors, n) = table::factor(n, factors); table::factor(&mut n, &mut factors);
#[allow(clippy::let_and_return)]
let r = if n < (1 << 32) { let r = if n < (1 << 32) {
_factor::<Montgomery<u32>>(n, factors) _factor::<Montgomery<u32>>(n, factors)
} else { } else {
@ -238,9 +239,13 @@ mod tests {
} }
#[cfg(test)] #[cfg(test)]
impl quickcheck::Arbitrary for Factors { use rand::{
fn arbitrary<G: quickcheck::Gen>(gen: &mut G) -> Self { distributions::{Distribution, Standard},
use rand::Rng; Rng,
};
#[cfg(test)]
impl Distribution<Factors> for Standard {
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Factors {
let mut f = Factors::one(); let mut f = Factors::one();
let mut g = 1u64; let mut g = 1u64;
let mut n = u64::MAX; let mut n = u64::MAX;
@ -251,7 +256,7 @@ impl quickcheck::Arbitrary for Factors {
// See Generating Random Factored Numbers, Easily, J. Cryptology (2003) // See Generating Random Factored Numbers, Easily, J. Cryptology (2003)
'attempt: loop { 'attempt: loop {
while n > 1 { while n > 1 {
n = gen.gen_range(1, n); n = rng.gen_range(1, n);
if miller_rabin::is_prime(n) { if miller_rabin::is_prime(n) {
if let Some(h) = g.checked_mul(n) { if let Some(h) = g.checked_mul(n) {
f.push(n); f.push(n);
@ -268,6 +273,13 @@ impl quickcheck::Arbitrary for Factors {
} }
} }
#[cfg(test)]
impl quickcheck::Arbitrary for Factors {
fn arbitrary<G: quickcheck::Gen>(g: &mut G) -> Self {
g.gen()
}
}
#[cfg(test)] #[cfg(test)]
impl std::ops::BitXor<Exponent> for Factors { impl std::ops::BitXor<Exponent> for Factors {
type Output = Self; type Output = Self;
@ -280,6 +292,6 @@ impl std::ops::BitXor<Exponent> for Factors {
} }
debug_assert_eq!(r.product(), self.product().pow(rhs.into())); debug_assert_eq!(r.product(), self.product().pow(rhs.into()));
return r; r
} }
} }

View file

@ -8,15 +8,13 @@
// spell-checker: ignore (ToDO) INVS // spell-checker: ignore (ToDO) INVS
use std::num::Wrapping;
use crate::Factors; use crate::Factors;
include!(concat!(env!("OUT_DIR"), "/prime_table.rs")); include!(concat!(env!("OUT_DIR"), "/prime_table.rs"));
pub(crate) fn factor(mut num: u64, mut factors: Factors) -> (Factors, u64) { pub fn factor(num: &mut u64, factors: &mut Factors) {
for &(prime, inv, ceil) in P_INVS_U64 { for &(prime, inv, ceil) in P_INVS_U64 {
if num == 1 { if *num == 1 {
break; break;
} }
@ -27,11 +25,11 @@ pub(crate) fn factor(mut num: u64, mut factors: Factors) -> (Factors, u64) {
// for a nice explanation. // for a nice explanation.
let mut k = 0; let mut k = 0;
loop { loop {
let Wrapping(x) = Wrapping(num) * Wrapping(inv); let x = num.wrapping_mul(inv);
// While prime divides num // While prime divides num
if x <= ceil { if x <= ceil {
num = x; *num = x;
k += 1; k += 1;
#[cfg(feature = "coz")] #[cfg(feature = "coz")]
coz::progress!("factor found"); coz::progress!("factor found");
@ -43,6 +41,61 @@ pub(crate) fn factor(mut num: u64, mut factors: Factors) -> (Factors, u64) {
} }
} }
} }
}
(factors, num)
pub const CHUNK_SIZE: usize = 8;
pub fn factor_chunk(n_s: &mut [u64; CHUNK_SIZE], f_s: &mut [Factors; CHUNK_SIZE]) {
for &(prime, inv, ceil) in P_INVS_U64 {
if n_s[0] == 1 && n_s[1] == 1 && n_s[2] == 1 && n_s[3] == 1 {
break;
}
for (num, factors) in n_s.iter_mut().zip(f_s.iter_mut()) {
if *num == 1 {
continue;
}
let mut k = 0;
loop {
let x = num.wrapping_mul(inv);
// While prime divides num
if x <= ceil {
*num = x;
k += 1;
} else {
if k > 0 {
factors.add(prime, k);
}
break;
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Factors;
use quickcheck::quickcheck;
use rand::{rngs::SmallRng, Rng, SeedableRng};
quickcheck! {
fn chunk_vs_iter(seed: u64) -> () {
let mut rng = SmallRng::seed_from_u64(seed);
let mut n_c: [u64; CHUNK_SIZE] = rng.gen();
let mut f_c: [Factors; CHUNK_SIZE] = rng.gen();
let mut n_i = n_c.clone();
let mut f_i = f_c.clone();
for (n, f) in n_i.iter_mut().zip(f_i.iter_mut()) {
factor(n, f);
}
factor_chunk(&mut n_c, &mut f_c);
assert_eq!(n_i, n_c);
assert_eq!(f_i, f_c);
}
}
} }

View file

@ -296,7 +296,7 @@ fn find_kp_breakpoints<'a, T: Iterator<Item = &'a WordInfo<'a>>>(
(0, 0.0) (0, 0.0)
} else { } else {
compute_demerits( compute_demerits(
(args.opts.goal - tlen) as isize, args.opts.goal as isize - tlen as isize,
stretch, stretch,
w.word_nchars as isize, w.word_nchars as isize,
active.prev_rat, active.prev_rat,

View file

@ -16,7 +16,7 @@ path = "src/head.rs"
[dependencies] [dependencies]
clap = "2.33" clap = "2.33"
uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["ringbuffer"] }
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }
[[bin]] [[bin]]

View file

@ -1,8 +1,8 @@
use clap::{App, Arg}; use clap::{App, Arg};
use std::convert::TryFrom; use std::convert::TryFrom;
use std::ffi::OsString; use std::ffi::OsString;
use std::io::{ErrorKind, Read, Seek, SeekFrom, Write}; use std::io::{self, ErrorKind, Read, Seek, SeekFrom, Write};
use uucore::{crash, executable, show_error}; use uucore::{crash, executable, show_error, show_error_custom_description};
const EXIT_FAILURE: i32 = 1; const EXIT_FAILURE: i32 = 1;
const EXIT_SUCCESS: i32 = 0; const EXIT_SUCCESS: i32 = 0;
@ -27,8 +27,12 @@ mod options {
pub const ZERO_NAME: &str = "ZERO"; pub const ZERO_NAME: &str = "ZERO";
pub const FILES_NAME: &str = "FILE"; pub const FILES_NAME: &str = "FILE";
} }
mod lines;
mod parse; mod parse;
mod split; mod split;
mod take;
use lines::zlines;
use take::take_all_but;
fn app<'a>() -> App<'a, 'a> { fn app<'a>() -> App<'a, 'a> {
App::new(executable!()) App::new(executable!())
@ -206,38 +210,20 @@ impl Default for HeadOptions {
} }
} }
fn rbuf_n_bytes(input: &mut impl std::io::BufRead, n: usize) -> std::io::Result<()> { fn rbuf_n_bytes<R>(input: R, n: usize) -> std::io::Result<()>
if n == 0 { where
return Ok(()); R: Read,
} {
let mut readbuf = [0u8; BUF_SIZE]; // Read the first `n` bytes from the `input` reader.
let mut i = 0usize; let mut reader = input.take(n as u64);
// Write those bytes to `stdout`.
let stdout = std::io::stdout(); let stdout = std::io::stdout();
let mut stdout = stdout.lock(); let mut stdout = stdout.lock();
loop { io::copy(&mut reader, &mut stdout)?;
let read = loop {
match input.read(&mut readbuf) { Ok(())
Ok(n) => break n,
Err(e) => match e.kind() {
ErrorKind::Interrupted => {}
_ => return Err(e),
},
}
};
if read == 0 {
// might be unexpected if
// we haven't read `n` bytes
// but this mirrors GNU's behavior
return Ok(());
}
stdout.write_all(&readbuf[..read.min(n - i)])?;
i += read.min(n - i);
if i == n {
return Ok(());
}
}
} }
fn rbuf_n_lines(input: &mut impl std::io::BufRead, n: usize, zero: bool) -> std::io::Result<()> { fn rbuf_n_lines(input: &mut impl std::io::BufRead, n: usize, zero: bool) -> std::io::Result<()> {
@ -311,36 +297,22 @@ fn rbuf_but_last_n_bytes(input: &mut impl std::io::BufRead, n: usize) -> std::io
} }
fn rbuf_but_last_n_lines( fn rbuf_but_last_n_lines(
input: &mut impl std::io::BufRead, input: impl std::io::BufRead,
n: usize, n: usize,
zero: bool, zero: bool,
) -> std::io::Result<()> { ) -> std::io::Result<()> {
if n == 0 { if zero {
//prints everything let stdout = std::io::stdout();
return rbuf_n_bytes(input, std::usize::MAX); let mut stdout = stdout.lock();
for bytes in take_all_but(zlines(input), n) {
stdout.write_all(&bytes?)?;
}
} else {
for line in take_all_but(input.lines(), n) {
println!("{}", line?);
}
} }
let mut ringbuf = vec![Vec::new(); n]; Ok(())
let stdout = std::io::stdout();
let mut stdout = stdout.lock();
let mut line = Vec::new();
let mut lines = 0usize;
split::walk_lines(input, zero, |e| match e {
split::Event::Data(dat) => {
line.extend_from_slice(dat);
Ok(true)
}
split::Event::Line => {
if lines < n {
ringbuf[lines] = std::mem::replace(&mut line, Vec::new());
lines += 1;
} else {
stdout.write_all(&ringbuf[0])?;
ringbuf.rotate_left(1);
ringbuf[n - 1] = std::mem::replace(&mut line, Vec::new());
}
Ok(true)
}
})
} }
fn head_backwards_file(input: &mut std::fs::File, options: &HeadOptions) -> std::io::Result<()> { fn head_backwards_file(input: &mut std::fs::File, options: &HeadOptions) -> std::io::Result<()> {
@ -418,12 +390,13 @@ fn head_file(input: &mut std::fs::File, options: &HeadOptions) -> std::io::Resul
} }
} }
fn uu_head(options: &HeadOptions) { fn uu_head(options: &HeadOptions) -> Result<(), u32> {
let mut error_count = 0;
let mut first = true; let mut first = true;
for fname in &options.files { for fname in &options.files {
let res = match fname.as_str() { let res = match fname.as_str() {
"-" => { "-" => {
if options.verbose { if (options.files.len() > 1 && !options.quiet) || options.verbose {
if !first { if !first {
println!(); println!();
} }
@ -451,53 +424,49 @@ fn uu_head(options: &HeadOptions) {
name => { name => {
let mut file = match std::fs::File::open(name) { let mut file = match std::fs::File::open(name) {
Ok(f) => f, Ok(f) => f,
Err(err) => match err.kind() { Err(err) => {
ErrorKind::NotFound => { let prefix = format!("cannot open '{}' for reading", name);
crash!( match err.kind() {
EXIT_FAILURE, ErrorKind::NotFound => {
"head: cannot open '{}' for reading: No such file or directory", show_error_custom_description!(prefix, "No such file or directory");
name }
); ErrorKind::PermissionDenied => {
show_error_custom_description!(prefix, "Permission denied");
}
_ => {
show_error_custom_description!(prefix, "{}", err);
}
} }
ErrorKind::PermissionDenied => { error_count += 1;
crash!( continue;
EXIT_FAILURE, }
"head: cannot open '{}' for reading: Permission denied",
name
);
}
_ => {
crash!(
EXIT_FAILURE,
"head: cannot open '{}' for reading: {}",
name,
err
);
}
},
}; };
if (options.files.len() > 1 && !options.quiet) || options.verbose { if (options.files.len() > 1 && !options.quiet) || options.verbose {
if !first {
println!();
}
println!("==> {} <==", name) println!("==> {} <==", name)
} }
head_file(&mut file, options) head_file(&mut file, options)
} }
}; };
if res.is_err() { if res.is_err() {
if fname.as_str() == "-" { let name = if fname.as_str() == "-" {
crash!( "standard input"
EXIT_FAILURE,
"head: error reading standard input: Input/output error"
);
} else { } else {
crash!( fname
EXIT_FAILURE, };
"head: error reading {}: Input/output error", let prefix = format!("error reading {}", name);
fname show_error_custom_description!(prefix, "Input/output error");
); error_count += 1;
}
} }
first = false; first = false;
} }
if error_count > 0 {
Err(error_count)
} else {
Ok(())
}
} }
pub fn uumain(args: impl uucore::Args) -> i32 { pub fn uumain(args: impl uucore::Args) -> i32 {
@ -507,9 +476,10 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
crash!(EXIT_FAILURE, "head: {}", s); crash!(EXIT_FAILURE, "head: {}", s);
} }
}; };
uu_head(&args); match uu_head(&args) {
Ok(_) => EXIT_SUCCESS,
EXIT_SUCCESS Err(_) => EXIT_FAILURE,
}
} }
#[cfg(test)] #[cfg(test)]

73
src/uu/head/src/lines.rs Normal file
View file

@ -0,0 +1,73 @@
//! Iterate over zero-terminated lines.
use std::io::BufRead;
/// The zero byte, representing the null character.
const ZERO: u8 = 0;
/// Returns an iterator over the lines of the given reader.
///
/// The iterator returned from this function will yield instances of
/// [`io::Result`]<[`Vec`]<[`u8`]>>, representing the bytes of the line
/// *including* the null character (with the possible exception of the
/// last line, which may not have one).
///
/// # Examples
///
/// ```rust,ignore
/// use std::io::Cursor;
///
/// let cursor = Cursor::new(b"x\0y\0z\0");
/// let mut iter = zlines(cursor).map(|l| l.unwrap());
/// assert_eq!(iter.next(), Some(b"x\0".to_vec()));
/// assert_eq!(iter.next(), Some(b"y\0".to_vec()));
/// assert_eq!(iter.next(), Some(b"z\0".to_vec()));
/// assert_eq!(iter.next(), None);
/// ```
pub fn zlines<B>(buf: B) -> ZLines<B> {
ZLines { buf }
}
/// An iterator over the zero-terminated lines of an instance of `BufRead`.
pub struct ZLines<B> {
buf: B,
}
impl<B: BufRead> Iterator for ZLines<B> {
type Item = std::io::Result<Vec<u8>>;
fn next(&mut self) -> Option<std::io::Result<Vec<u8>>> {
let mut buf = Vec::new();
match self.buf.read_until(ZERO, &mut buf) {
Ok(0) => None,
Ok(_) => Some(Ok(buf)),
Err(e) => Some(Err(e)),
}
}
}
#[cfg(test)]
mod tests {
use crate::lines::zlines;
use std::io::Cursor;
#[test]
fn test_null_terminated() {
let cursor = Cursor::new(b"x\0y\0z\0");
let mut iter = zlines(cursor).map(|l| l.unwrap());
assert_eq!(iter.next(), Some(b"x\0".to_vec()));
assert_eq!(iter.next(), Some(b"y\0".to_vec()));
assert_eq!(iter.next(), Some(b"z\0".to_vec()));
assert_eq!(iter.next(), None);
}
#[test]
fn test_not_null_terminated() {
let cursor = Cursor::new(b"x\0y\0z");
let mut iter = zlines(cursor).map(|l| l.unwrap());
assert_eq!(iter.next(), Some(b"x\0".to_vec()));
assert_eq!(iter.next(), Some(b"y\0".to_vec()));
assert_eq!(iter.next(), Some(b"z".to_vec()));
assert_eq!(iter.next(), None);
}
}

93
src/uu/head/src/take.rs Normal file
View file

@ -0,0 +1,93 @@
//! Take all but the last elements of an iterator.
use uucore::ringbuffer::RingBuffer;
/// Create an iterator over all but the last `n` elements of `iter`.
///
/// # Examples
///
/// ```rust,ignore
/// let data = [1, 2, 3, 4, 5];
/// let n = 2;
/// let mut iter = take_all_but(data.iter(), n);
/// assert_eq!(Some(4), iter.next());
/// assert_eq!(Some(5), iter.next());
/// assert_eq!(None, iter.next());
/// ```
pub fn take_all_but<I: Iterator>(iter: I, n: usize) -> TakeAllBut<I> {
TakeAllBut::new(iter, n)
}
/// An iterator that only iterates over the last elements of another iterator.
pub struct TakeAllBut<I: Iterator> {
iter: I,
buf: RingBuffer<<I as Iterator>::Item>,
}
impl<I: Iterator> TakeAllBut<I> {
pub fn new(mut iter: I, n: usize) -> TakeAllBut<I> {
// Create a new ring buffer and fill it up.
//
// If there are fewer than `n` elements in `iter`, then we
// exhaust the iterator so that whenever `TakeAllBut::next()` is
// called, it will return `None`, as expected.
let mut buf = RingBuffer::new(n);
for _ in 0..n {
let value = match iter.next() {
None => {
break;
}
Some(x) => x,
};
buf.push_back(value);
}
TakeAllBut { iter, buf }
}
}
impl<I: Iterator> Iterator for TakeAllBut<I>
where
I: Iterator,
{
type Item = <I as Iterator>::Item;
fn next(&mut self) -> Option<<I as Iterator>::Item> {
match self.iter.next() {
Some(value) => self.buf.push_back(value),
None => None,
}
}
}
#[cfg(test)]
mod tests {
use crate::take::take_all_but;
#[test]
fn test_fewer_elements() {
let mut iter = take_all_but([0, 1, 2].iter(), 2);
assert_eq!(Some(&0), iter.next());
assert_eq!(None, iter.next());
}
#[test]
fn test_same_number_of_elements() {
let mut iter = take_all_but([0, 1].iter(), 2);
assert_eq!(None, iter.next());
}
#[test]
fn test_more_elements() {
let mut iter = take_all_but([0].iter(), 2);
assert_eq!(None, iter.next());
}
#[test]
fn test_zero_elements() {
let mut iter = take_all_but([0, 1, 2].iter(), 0);
assert_eq!(Some(&0), iter.next());
assert_eq!(Some(&1), iter.next());
assert_eq!(Some(&2), iter.next());
assert_eq!(None, iter.next());
}
}

View file

@ -370,13 +370,13 @@ fn directory(paths: Vec<String>, b: Behavior) -> i32 {
// created ancestor directories will have the default mode. Hence it is safe to use // created ancestor directories will have the default mode. Hence it is safe to use
// fs::create_dir_all and then only modify the target's dir mode. // fs::create_dir_all and then only modify the target's dir mode.
if let Err(e) = fs::create_dir_all(path) { if let Err(e) = fs::create_dir_all(path) {
show_info!("{}: {}", path.display(), e); show_error!("{}: {}", path.display(), e);
all_successful = false; all_successful = false;
continue; continue;
} }
if b.verbose { if b.verbose {
show_info!("creating directory '{}'", path.display()); show_error!("creating directory '{}'", path.display());
} }
} }
@ -461,7 +461,7 @@ fn copy_files_into_dir(files: &[PathBuf], target_dir: &Path, b: &Behavior) -> i3
let mut all_successful = true; let mut all_successful = true;
for sourcepath in files.iter() { for sourcepath in files.iter() {
if !sourcepath.exists() { if !sourcepath.exists() {
show_info!( show_error!(
"cannot stat '{}': No such file or directory", "cannot stat '{}': No such file or directory",
sourcepath.display() sourcepath.display()
); );
@ -471,7 +471,7 @@ fn copy_files_into_dir(files: &[PathBuf], target_dir: &Path, b: &Behavior) -> i3
} }
if sourcepath.is_dir() { if sourcepath.is_dir() {
show_info!("omitting directory '{}'", sourcepath.display()); show_error!("omitting directory '{}'", sourcepath.display());
all_successful = false; all_successful = false;
continue; continue;
} }
@ -588,10 +588,10 @@ fn copy(from: &Path, to: &Path, b: &Behavior) -> Result<(), ()> {
) { ) {
Ok(n) => { Ok(n) => {
if !n.is_empty() { if !n.is_empty() {
show_info!("{}", n); show_error!("{}", n);
} }
} }
Err(e) => show_info!("{}", e), Err(e) => show_error!("{}", e),
} }
} }
@ -608,10 +608,10 @@ fn copy(from: &Path, to: &Path, b: &Behavior) -> Result<(), ()> {
match wrap_chgrp(to, &meta, group_id, false, Verbosity::Normal) { match wrap_chgrp(to, &meta, group_id, false, Verbosity::Normal) {
Ok(n) => { Ok(n) => {
if !n.is_empty() { if !n.is_empty() {
show_info!("{}", n); show_error!("{}", n);
} }
} }
Err(e) => show_info!("{}", e), Err(e) => show_error!("{}", e),
} }
} }
@ -626,12 +626,12 @@ fn copy(from: &Path, to: &Path, b: &Behavior) -> Result<(), ()> {
match set_file_times(to, accessed_time, modified_time) { match set_file_times(to, accessed_time, modified_time) {
Ok(_) => {} Ok(_) => {}
Err(e) => show_info!("{}", e), Err(e) => show_error!("{}", e),
} }
} }
if b.verbose { if b.verbose {
show_info!("'{}' -> '{}'", from.display(), to.display()); show_error!("'{}' -> '{}'", from.display(), to.display());
} }
Ok(()) Ok(())

View file

@ -23,7 +23,7 @@ pub fn parse(mode_string: &str, considering_dir: bool) -> Result<u32, String> {
pub fn chmod(path: &Path, mode: u32) -> Result<(), ()> { pub fn chmod(path: &Path, mode: u32) -> Result<(), ()> {
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
fs::set_permissions(path, fs::Permissions::from_mode(mode)).map_err(|err| { fs::set_permissions(path, fs::Permissions::from_mode(mode)).map_err(|err| {
show_info!("{}: chmod failed with error {}", path.display(), err); show_error!("{}: chmod failed with error {}", path.display(), err);
}) })
} }

View file

@ -16,6 +16,7 @@ path = "src/logname.rs"
[dependencies] [dependencies]
libc = "0.2.42" libc = "0.2.42"
clap = "2.33"
uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore = { version=">=0.0.8", package="uucore", path="../../uucore" }
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }

View file

@ -15,6 +15,8 @@ extern crate uucore;
use std::ffi::CStr; use std::ffi::CStr;
use uucore::InvalidEncodingHandling; use uucore::InvalidEncodingHandling;
use clap::App;
extern "C" { extern "C" {
// POSIX requires using getlogin (or equivalent code) // POSIX requires using getlogin (or equivalent code)
pub fn getlogin() -> *const libc::c_char; pub fn getlogin() -> *const libc::c_char;
@ -31,15 +33,24 @@ fn get_userlogin() -> Option<String> {
} }
} }
static SYNTAX: &str = "";
static SUMMARY: &str = "Print user's login name"; static SUMMARY: &str = "Print user's login name";
static LONG_HELP: &str = ""; static VERSION: &str = env!("CARGO_PKG_VERSION");
fn get_usage() -> String {
String::from(executable!())
}
pub fn uumain(args: impl uucore::Args) -> i32 { pub fn uumain(args: impl uucore::Args) -> i32 {
app!(SYNTAX, SUMMARY, LONG_HELP).parse( let args = args
args.collect_str(InvalidEncodingHandling::ConvertLossy) .collect_str(InvalidEncodingHandling::Ignore)
.accept_any(), .accept_any();
);
let usage = get_usage();
let _ = App::new(executable!())
.version(VERSION)
.about(SUMMARY)
.usage(&usage[..])
.get_matches_from(args);
match get_userlogin() { match get_userlogin() {
Some(userlogin) => println!("{}", userlogin), Some(userlogin) => println!("{}", userlogin),

View file

@ -1110,7 +1110,7 @@ struct PathData {
md: OnceCell<Option<Metadata>>, md: OnceCell<Option<Metadata>>,
ft: OnceCell<Option<FileType>>, ft: OnceCell<Option<FileType>>,
// Name of the file - will be empty for . or .. // Name of the file - will be empty for . or ..
file_name: String, display_name: String,
// PathBuf that all above data corresponds to // PathBuf that all above data corresponds to
p_buf: PathBuf, p_buf: PathBuf,
must_dereference: bool, must_dereference: bool,
@ -1126,14 +1126,18 @@ impl PathData {
) -> Self { ) -> Self {
// We cannot use `Path::ends_with` or `Path::Components`, because they remove occurrences of '.' // We cannot use `Path::ends_with` or `Path::Components`, because they remove occurrences of '.'
// For '..', the filename is None // For '..', the filename is None
let name = if let Some(name) = file_name { let display_name = if let Some(name) = file_name {
name name
} else { } else {
p_buf let display_osstr = if command_line {
.file_name() p_buf.as_os_str()
.unwrap_or_else(|| p_buf.iter().next_back().unwrap()) } else {
.to_string_lossy() p_buf
.into_owned() .file_name()
.unwrap_or_else(|| p_buf.iter().next_back().unwrap())
};
display_osstr.to_string_lossy().into_owned()
}; };
let must_dereference = match &config.dereference { let must_dereference = match &config.dereference {
Dereference::All => true, Dereference::All => true,
@ -1159,7 +1163,7 @@ impl PathData {
Self { Self {
md: OnceCell::new(), md: OnceCell::new(),
ft, ft,
file_name: name, display_name,
p_buf, p_buf,
must_dereference, must_dereference,
} }
@ -1179,31 +1183,32 @@ impl PathData {
} }
fn list(locs: Vec<String>, config: Config) -> i32 { fn list(locs: Vec<String>, config: Config) -> i32 {
let number_of_locs = locs.len();
let mut files = Vec::<PathData>::new(); let mut files = Vec::<PathData>::new();
let mut dirs = Vec::<PathData>::new(); let mut dirs = Vec::<PathData>::new();
let mut has_failed = false; let mut has_failed = false;
let mut out = BufWriter::new(stdout()); let mut out = BufWriter::new(stdout());
for loc in locs { for loc in &locs {
let p = PathBuf::from(&loc); let p = PathBuf::from(&loc);
if !p.exists() { if !p.exists() {
show_error!("'{}': {}", &loc, "No such file or directory"); show_error!("'{}': {}", &loc, "No such file or directory");
// We found an error, the return code of ls should not be 0 /*
// And no need to continue the execution We found an error, the return code of ls should not be 0
And no need to continue the execution
*/
has_failed = true; has_failed = true;
continue; continue;
} }
let path_data = PathData::new(p, None, None, &config, true); let path_data = PathData::new(p, None, None, &config, true);
let show_dir_contents = if let Some(ft) = path_data.file_type() { let show_dir_contents = match path_data.file_type() {
!config.directory && ft.is_dir() Some(ft) => !config.directory && ft.is_dir(),
} else { None => {
has_failed = true; has_failed = true;
false false
}
}; };
if show_dir_contents { if show_dir_contents {
@ -1217,7 +1222,7 @@ fn list(locs: Vec<String>, config: Config) -> i32 {
sort_entries(&mut dirs, &config); sort_entries(&mut dirs, &config);
for dir in dirs { for dir in dirs {
if number_of_locs > 1 { if locs.len() > 1 {
let _ = writeln!(out, "\n{}:", dir.p_buf.display()); let _ = writeln!(out, "\n{}:", dir.p_buf.display());
} }
enter_directory(&dir, &config, &mut out); enter_directory(&dir, &config, &mut out);
@ -1242,7 +1247,7 @@ fn sort_entries(entries: &mut Vec<PathData>, config: &Config) {
entries.sort_by_key(|k| Reverse(k.md().as_ref().map(|md| md.len()).unwrap_or(0))) entries.sort_by_key(|k| Reverse(k.md().as_ref().map(|md| md.len()).unwrap_or(0)))
} }
// The default sort in GNU ls is case insensitive // The default sort in GNU ls is case insensitive
Sort::Name => entries.sort_by(|a, b| a.file_name.cmp(&b.file_name)), Sort::Name => entries.sort_by(|a, b| a.display_name.cmp(&b.display_name)),
Sort::Version => entries.sort_by(|a, b| version_cmp::version_cmp(&a.p_buf, &b.p_buf)), Sort::Version => entries.sort_by(|a, b| version_cmp::version_cmp(&a.p_buf, &b.p_buf)),
Sort::Extension => entries.sort_by(|a, b| { Sort::Extension => entries.sort_by(|a, b| {
a.p_buf a.p_buf
@ -1331,7 +1336,7 @@ fn display_dir_entry_size(entry: &PathData, config: &Config) -> (usize, usize) {
if let Some(md) = entry.md() { if let Some(md) = entry.md() {
( (
display_symlink_count(&md).len(), display_symlink_count(&md).len(),
display_file_size(&md, config).len(), display_size_or_rdev(&md, config).len(),
) )
} else { } else {
(0, 0) (0, 0)
@ -1344,14 +1349,22 @@ fn pad_left(string: String, count: usize) -> String {
fn display_items(items: &[PathData], config: &Config, out: &mut BufWriter<Stdout>) { fn display_items(items: &[PathData], config: &Config, out: &mut BufWriter<Stdout>) {
if config.format == Format::Long { if config.format == Format::Long {
let (mut max_links, mut max_size) = (1, 1); let (mut max_links, mut max_width) = (1, 1);
let mut total_size = 0;
for item in items { for item in items {
let (links, size) = display_dir_entry_size(item, config); let (links, width) = display_dir_entry_size(item, config);
max_links = links.max(max_links); max_links = links.max(max_links);
max_size = size.max(max_size); max_width = width.max(max_width);
total_size += item.md().map_or(0, |md| get_block_size(md, config));
} }
if total_size > 0 {
let _ = writeln!(out, "total {}", display_size(total_size, config));
}
for item in items { for item in items {
display_item_long(item, max_links, max_size, config, out); display_item_long(item, max_links, max_width, config, out);
} }
} else { } else {
let names = items.iter().filter_map(|i| display_file_name(&i, config)); let names = items.iter().filter_map(|i| display_file_name(&i, config));
@ -1396,6 +1409,29 @@ fn display_items(items: &[PathData], config: &Config, out: &mut BufWriter<Stdout
} }
} }
fn get_block_size(md: &Metadata, config: &Config) -> u64 {
/* GNU ls will display sizes in terms of block size
md.len() will differ from this value when the file has some holes
*/
#[cfg(unix)]
{
// hard-coded for now - enabling setting this remains a TODO
let ls_block_size = 1024;
match config.size_format {
SizeFormat::Binary => md.blocks() * 512,
SizeFormat::Decimal => md.blocks() * 512,
SizeFormat::Bytes => md.blocks() * 512 / ls_block_size,
}
}
#[cfg(not(unix))]
{
let _ = config;
// no way to get block size for windows, fall-back to file size
md.len()
}
}
fn display_grid( fn display_grid(
names: impl Iterator<Item = Cell>, names: impl Iterator<Item = Cell>,
width: u16, width: u16,
@ -1448,9 +1484,8 @@ fn display_item_long(
let _ = write!( let _ = write!(
out, out,
"{}{} {}", "{} {}",
display_file_type(md.file_type()), display_permissions(&md, true),
display_permissions(&md),
pad_left(display_symlink_count(&md), max_links), pad_left(display_symlink_count(&md), max_links),
); );
@ -1471,7 +1506,7 @@ fn display_item_long(
let _ = writeln!( let _ = writeln!(
out, out,
" {} {} {}", " {} {} {}",
pad_left(display_file_size(&md, config), max_size), pad_left(display_size_or_rdev(md, config), max_size),
display_date(&md, config), display_date(&md, config),
// unwrap is fine because it fails when metadata is not available // unwrap is fine because it fails when metadata is not available
// but we already know that it is because it's checked at the // but we already know that it is because it's checked at the
@ -1626,23 +1661,28 @@ fn format_prefixed(prefixed: NumberPrefix<f64>) -> String {
} }
} }
fn display_file_size(metadata: &Metadata, config: &Config) -> String { fn display_size_or_rdev(metadata: &Metadata, config: &Config) -> String {
#[cfg(unix)]
{
let ft = metadata.file_type();
if ft.is_char_device() || ft.is_block_device() {
let dev: u64 = metadata.rdev();
let major = (dev >> 8) as u8;
let minor = dev as u8;
return format!("{}, {}", major, minor);
}
}
display_size(metadata.len(), config)
}
fn display_size(size: u64, config: &Config) -> String {
// NOTE: The human-readable behaviour deviates from the GNU ls. // NOTE: The human-readable behaviour deviates from the GNU ls.
// The GNU ls uses binary prefixes by default. // The GNU ls uses binary prefixes by default.
match config.size_format { match config.size_format {
SizeFormat::Binary => format_prefixed(NumberPrefix::binary(metadata.len() as f64)), SizeFormat::Binary => format_prefixed(NumberPrefix::binary(size as f64)),
SizeFormat::Decimal => format_prefixed(NumberPrefix::decimal(metadata.len() as f64)), SizeFormat::Decimal => format_prefixed(NumberPrefix::decimal(size as f64)),
SizeFormat::Bytes => metadata.len().to_string(), SizeFormat::Bytes => size.to_string(),
}
}
fn display_file_type(file_type: FileType) -> char {
if file_type.is_dir() {
'd'
} else if file_type.is_symlink() {
'l'
} else {
'-'
} }
} }
@ -1683,7 +1723,7 @@ fn classify_file(path: &PathData) -> Option<char> {
} }
fn display_file_name(path: &PathData, config: &Config) -> Option<Cell> { fn display_file_name(path: &PathData, config: &Config) -> Option<Cell> {
let mut name = escape_name(&path.file_name, &config.quoting_style); let mut name = escape_name(&path.display_name, &config.quoting_style);
#[cfg(unix)] #[cfg(unix)]
{ {

View file

@ -101,7 +101,7 @@ fn exec(dirs: Vec<String>, recursive: bool, mode: u16, verbose: bool) -> i32 {
if !recursive { if !recursive {
if let Some(parent) = path.parent() { if let Some(parent) = path.parent() {
if parent != empty && !parent.exists() { if parent != empty && !parent.exists() {
show_info!( show_error!(
"cannot create directory '{}': No such file or directory", "cannot create directory '{}': No such file or directory",
path.display() path.display()
); );
@ -125,7 +125,7 @@ fn mkdir(path: &Path, recursive: bool, mode: u16, verbose: bool) -> i32 {
fs::create_dir fs::create_dir
}; };
if let Err(e) = create_dir(path) { if let Err(e) = create_dir(path) {
show_info!("{}: {}", path.display(), e.to_string()); show_error!("{}: {}", path.display(), e.to_string());
return 1; return 1;
} }

View file

@ -16,7 +16,7 @@ name = "uu_mknod"
path = "src/mknod.rs" path = "src/mknod.rs"
[dependencies] [dependencies]
getopts = "0.2.18" clap = "2.33"
libc = "^0.2.42" libc = "^0.2.42"
uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["mode"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["mode"] }
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }

View file

@ -5,21 +5,41 @@
// For the full copyright and license information, please view the LICENSE // For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code. // file that was distributed with this source code.
// spell-checker:ignore (ToDO) parsemode makedev sysmacros makenod newmode perror IFBLK IFCHR IFIFO // spell-checker:ignore (ToDO) parsemode makedev sysmacros perror IFBLK IFCHR IFIFO
#[macro_use] #[macro_use]
extern crate uucore; extern crate uucore;
use std::ffi::CString;
use clap::{App, Arg, ArgMatches};
use libc::{dev_t, mode_t}; use libc::{dev_t, mode_t};
use libc::{S_IFBLK, S_IFCHR, S_IFIFO, S_IRGRP, S_IROTH, S_IRUSR, S_IWGRP, S_IWOTH, S_IWUSR}; use libc::{S_IFBLK, S_IFCHR, S_IFIFO, S_IRGRP, S_IROTH, S_IRUSR, S_IWGRP, S_IWOTH, S_IWUSR};
use getopts::Options;
use std::ffi::CString;
use uucore::InvalidEncodingHandling; use uucore::InvalidEncodingHandling;
static NAME: &str = "mknod"; static NAME: &str = "mknod";
static VERSION: &str = env!("CARGO_PKG_VERSION"); static VERSION: &str = env!("CARGO_PKG_VERSION");
static ABOUT: &str = "Create the special file NAME of the given TYPE.";
static USAGE: &str = "mknod [OPTION]... NAME TYPE [MAJOR MINOR]";
static LONG_HELP: &str = "Mandatory arguments to long options are mandatory for short options too.
-m, --mode=MODE set file permission bits to MODE, not a=rw - umask
--help display this help and exit
--version output version information and exit
Both MAJOR and MINOR must be specified when TYPE is b, c, or u, and they
must be omitted when TYPE is p. If MAJOR or MINOR begins with 0x or 0X,
it is interpreted as hexadecimal; otherwise, if it begins with 0, as octal;
otherwise, as decimal. TYPE may be:
b create a block (buffered) special file
c, u create a character (unbuffered) special file
p create a FIFO
NOTE: your shell may have its own version of mknod, which usually supersedes
the version described here. Please refer to your shell's documentation
for details about the options it supports.
";
const MODE_RW_UGO: mode_t = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH; const MODE_RW_UGO: mode_t = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
@ -30,13 +50,35 @@ fn makedev(maj: u64, min: u64) -> dev_t {
} }
#[cfg(windows)] #[cfg(windows)]
fn _makenod(path: CString, mode: mode_t, dev: dev_t) -> i32 { fn _makenod(file_name: &str, mode: mode_t, dev: dev_t) -> i32 {
panic!("Unsupported for windows platform") panic!("Unsupported for windows platform")
} }
#[cfg(unix)] #[cfg(unix)]
fn _makenod(path: CString, mode: mode_t, dev: dev_t) -> i32 { fn _makenod(file_name: &str, mode: mode_t, dev: dev_t) -> i32 {
unsafe { libc::mknod(path.as_ptr(), mode, dev) } let c_str = CString::new(file_name).expect("Failed to convert to CString");
// the user supplied a mode
let set_umask = mode & MODE_RW_UGO != MODE_RW_UGO;
unsafe {
// store prev umask
let last_umask = if set_umask { libc::umask(0) } else { 0 };
let errno = libc::mknod(c_str.as_ptr(), mode, dev);
// set umask back to original value
if set_umask {
libc::umask(last_umask);
}
if errno == -1 {
let c_str = CString::new(NAME).expect("Failed to convert to CString");
// shows the error from the mknod syscall
libc::perror(c_str.as_ptr());
}
errno
}
} }
#[allow(clippy::cognitive_complexity)] #[allow(clippy::cognitive_complexity)]
@ -44,156 +86,136 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
let args = args let args = args
.collect_str(InvalidEncodingHandling::Ignore) .collect_str(InvalidEncodingHandling::Ignore)
.accept_any(); .accept_any();
let mut opts = Options::new();
// Linux-specific options, not implemented // Linux-specific options, not implemented
// opts.optflag("Z", "", "set the SELinux security context to default type"); // opts.optflag("Z", "", "set the SELinux security context to default type");
// opts.optopt("", "context", "like -Z, or if CTX is specified then set the SELinux or SMACK security context to CTX"); // opts.optopt("", "context", "like -Z, or if CTX is specified then set the SELinux or SMACK security context to CTX");
opts.optopt(
"m",
"mode",
"set file permission bits to MODE, not a=rw - umask",
"MODE",
);
opts.optflag("", "help", "display this help and exit"); let matches = App::new(executable!())
opts.optflag("", "version", "output version information and exit"); .version(VERSION)
.usage(USAGE)
.after_help(LONG_HELP)
.about(ABOUT)
.arg(
Arg::with_name("mode")
.short("m")
.long("mode")
.value_name("MODE")
.help("set file permission bits to MODE, not a=rw - umask"),
)
.arg(
Arg::with_name("name")
.value_name("NAME")
.help("name of the new file")
.required(true)
.index(1),
)
.arg(
Arg::with_name("type")
.value_name("TYPE")
.help("type of the new file (b, c, u or p)")
.required(true)
.validator(valid_type)
.index(2),
)
.arg(
Arg::with_name("major")
.value_name("MAJOR")
.help("major file type")
.validator(valid_u64)
.index(3),
)
.arg(
Arg::with_name("minor")
.value_name("MINOR")
.help("minor file type")
.validator(valid_u64)
.index(4),
)
.get_matches_from(args);
let matches = match opts.parse(&args[1..]) { let mode = match get_mode(&matches) {
Ok(m) => m, Ok(mode) => mode,
Err(f) => crash!(1, "{}\nTry '{} --help' for more information.", f, NAME), Err(err) => {
show_error!("{}", err);
return 1;
}
}; };
if matches.opt_present("help") { let file_name = matches.value_of("name").expect("Missing argument 'NAME'");
println!(
"Usage: {0} [OPTION]... NAME TYPE [MAJOR MINOR]
Mandatory arguments to long options are mandatory for short options too. // Only check the first character, to allow mnemonic usage like
-m, --mode=MODE set file permission bits to MODE, not a=rw - umask // 'mknod /dev/rst0 character 18 0'.
--help display this help and exit let ch = matches
--version output version information and exit .value_of("type")
.expect("Missing argument 'TYPE'")
.chars()
.next()
.expect("Failed to get the first char");
Both MAJOR and MINOR must be specified when TYPE is b, c, or u, and they if ch == 'p' {
must be omitted when TYPE is p. If MAJOR or MINOR begins with 0x or 0X, if matches.is_present("major") || matches.is_present("minor") {
it is interpreted as hexadecimal; otherwise, if it begins with 0, as octal; eprintln!("Fifos do not have major and minor device numbers.");
otherwise, as decimal. TYPE may be: eprintln!("Try '{} --help' for more information.", NAME);
1
b create a block (buffered) special file } else {
c, u create a character (unbuffered) special file _makenod(file_name, S_IFIFO | mode, 0)
p create a FIFO
NOTE: your shell may have its own version of mknod, which usually supersedes
the version described here. Please refer to your shell's documentation
for details about the options it supports.",
NAME
);
return 0;
}
if matches.opt_present("version") {
println!("{} {}", NAME, VERSION);
return 0;
}
let mut last_umask: mode_t = 0;
let mut newmode: mode_t = MODE_RW_UGO;
if matches.opt_present("mode") {
match uucore::mode::parse_mode(matches.opt_str("mode")) {
Ok(parsed) => {
if parsed > 0o777 {
show_info!("mode must specify only file permission bits");
return 1;
}
newmode = parsed;
}
Err(e) => {
show_info!("{}", e);
return 1;
}
} }
unsafe { } else {
last_umask = libc::umask(0); match (matches.value_of("major"), matches.value_of("minor")) {
} (None, None) | (_, None) | (None, _) => {
} eprintln!("Special files require major and minor device numbers.");
eprintln!("Try '{} --help' for more information.", NAME);
1
}
(Some(major), Some(minor)) => {
let major = major.parse::<u64>().expect("validated by clap");
let minor = minor.parse::<u64>().expect("validated by clap");
let mut ret = 0i32; let dev = makedev(major, minor);
match matches.free.len() {
0 => show_usage_error!("missing operand"),
1 => show_usage_error!("missing operand after {}", matches.free[0]),
_ => {
let args = &matches.free;
let c_str = CString::new(args[0].as_str()).expect("Failed to convert to CString");
// Only check the first character, to allow mnemonic usage like
// 'mknod /dev/rst0 character 18 0'.
let ch = args[1]
.chars()
.next()
.expect("Failed to get the first char");
if ch == 'p' {
if args.len() > 2 {
show_info!("{}: extra operand {}", NAME, args[2]);
if args.len() == 4 {
eprintln!("Fifos do not have major and minor device numbers.");
}
eprintln!("Try '{} --help' for more information.", NAME);
return 1;
}
ret = _makenod(c_str, S_IFIFO | newmode, 0);
} else {
if args.len() < 4 {
show_info!("missing operand after {}", args[args.len() - 1]);
if args.len() == 2 {
eprintln!("Special files require major and minor device numbers.");
}
eprintln!("Try '{} --help' for more information.", NAME);
return 1;
} else if args.len() > 4 {
show_usage_error!("extra operand {}", args[4]);
return 1;
} else if !"bcu".contains(ch) {
show_usage_error!("invalid device type {}", args[1]);
return 1;
}
let maj = args[2].parse::<u64>();
let min = args[3].parse::<u64>();
if maj.is_err() {
show_info!("invalid major device number {}", args[2]);
return 1;
} else if min.is_err() {
show_info!("invalid minor device number {}", args[3]);
return 1;
}
let (maj, min) = (maj.unwrap(), min.unwrap());
let dev = makedev(maj, min);
if ch == 'b' { if ch == 'b' {
// block special file // block special file
ret = _makenod(c_str, S_IFBLK | newmode, dev); _makenod(file_name, S_IFBLK | mode, dev)
} else { } else if ch == 'c' || ch == 'u' {
// char special file // char special file
ret = _makenod(c_str, S_IFCHR | newmode, dev); _makenod(file_name, S_IFCHR | mode, dev)
} else {
unreachable!("{} was validated to be only b, c or u", ch);
} }
} }
} }
} }
}
if last_umask != 0 {
unsafe { fn get_mode(matches: &ArgMatches) -> Result<mode_t, String> {
libc::umask(last_umask); match matches.value_of("mode") {
} None => Ok(MODE_RW_UGO),
} Some(str_mode) => uucore::mode::parse_mode(str_mode)
if ret == -1 { .map_err(|e| format!("invalid mode ({})", e))
let c_str = CString::new(format!("{}: {}", NAME, matches.free[0]).as_str()) .and_then(|mode| {
.expect("Failed to convert to CString"); if mode > 0o777 {
unsafe { Err("mode must specify only file permission bits".to_string())
libc::perror(c_str.as_ptr()); } else {
} Ok(mode)
} }
}),
ret }
}
fn valid_type(tpe: String) -> Result<(), String> {
// Only check the first character, to allow mnemonic usage like
// 'mknod /dev/rst0 character 18 0'.
tpe.chars()
.next()
.ok_or_else(|| "missing device type".to_string())
.and_then(|first_char| {
if vec!['b', 'c', 'u', 'p'].contains(&first_char) {
Ok(())
} else {
Err(format!("invalid device type {}", tpe))
}
})
}
fn valid_u64(num: String) -> Result<(), String> {
num.parse::<u64>().map(|_| ()).map_err(|_| num)
} }

View file

@ -0,0 +1,54 @@
// spell-checker:ignore (ToDO) fperm
use libc::{mode_t, S_IRGRP, S_IROTH, S_IRUSR, S_IWGRP, S_IWOTH, S_IWUSR};
use uucore::mode;
pub const MODE_RW_UGO: mode_t = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
pub fn parse_mode(mode: &str) -> Result<mode_t, String> {
let arr: &[char] = &['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
let result = if mode.contains(arr) {
mode::parse_numeric(MODE_RW_UGO as u32, mode)
} else {
mode::parse_symbolic(MODE_RW_UGO as u32, mode, true)
};
result.map(|mode| mode as mode_t)
}
#[cfg(test)]
mod test {
/// Test if the program is running under WSL
// ref: <https://github.com/microsoft/WSL/issues/4555> @@ <https://archive.is/dP0bz>
// ToDO: test on WSL2 which likely doesn't need special handling; plan change to `is_wsl_1()` if WSL2 is less needy
pub fn is_wsl() -> bool {
#[cfg(target_os = "linux")]
{
if let Ok(b) = std::fs::read("/proc/sys/kernel/osrelease") {
if let Ok(s) = std::str::from_utf8(&b) {
let a = s.to_ascii_lowercase();
return a.contains("microsoft") || a.contains("wsl");
}
}
}
false
}
#[test]
fn symbolic_modes() {
assert_eq!(super::parse_mode("u+x").unwrap(), 0o766);
assert_eq!(
super::parse_mode("+x").unwrap(),
if !is_wsl() { 0o777 } else { 0o776 }
);
assert_eq!(super::parse_mode("a-w").unwrap(), 0o444);
assert_eq!(super::parse_mode("g-r").unwrap(), 0o626);
}
#[test]
fn numeric_modes() {
assert_eq!(super::parse_mode("644").unwrap(), 0o644);
assert_eq!(super::parse_mode("+100").unwrap(), 0o766);
assert_eq!(super::parse_mode("-4").unwrap(), 0o662);
}
}

View file

@ -15,14 +15,11 @@ use clap::{App, Arg};
use std::env; use std::env;
use std::iter; use std::iter;
use std::mem::forget;
use std::path::{is_separator, PathBuf}; use std::path::{is_separator, PathBuf};
use rand::Rng; use rand::Rng;
use tempfile::Builder; use tempfile::Builder;
mod tempdir;
static ABOUT: &str = "create a temporary file or directory."; static ABOUT: &str = "create a temporary file or directory.";
static VERSION: &str = env!("CARGO_PKG_VERSION"); static VERSION: &str = env!("CARGO_PKG_VERSION");
@ -157,7 +154,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
} }
if matches.is_present(OPT_TMPDIR) && PathBuf::from(prefix).is_absolute() { if matches.is_present(OPT_TMPDIR) && PathBuf::from(prefix).is_absolute() {
show_info!( show_error!(
"invalid template, {}; with --tmpdir, it may not be absolute", "invalid template, {}; with --tmpdir, it may not be absolute",
template template
); );
@ -214,49 +211,54 @@ pub fn dry_exec(mut tmpdir: PathBuf, prefix: &str, rand: usize, suffix: &str) ->
} }
fn exec( fn exec(
tmpdir: PathBuf, dir: PathBuf,
prefix: &str, prefix: &str,
rand: usize, rand: usize,
suffix: &str, suffix: &str,
make_dir: bool, make_dir: bool,
quiet: bool, quiet: bool,
) -> i32 { ) -> i32 {
if make_dir { let res = if make_dir {
match tempdir::new_in(&tmpdir, prefix, rand, suffix) { let tmpdir = Builder::new()
Ok(ref f) => { .prefix(prefix)
println!("{}", f); .rand_bytes(rand)
return 0; .suffix(suffix)
} .tempdir_in(&dir);
Err(e) => {
if !quiet { // `into_path` consumes the TempDir without removing it
show_info!("{}: {}", e, tmpdir.display()); tmpdir.map(|d| d.into_path().to_string_lossy().to_string())
} else {
let tmpfile = Builder::new()
.prefix(prefix)
.rand_bytes(rand)
.suffix(suffix)
.tempfile_in(&dir);
match tmpfile {
Ok(f) => {
// `keep` ensures that the file is not deleted
match f.keep() {
Ok((_, p)) => Ok(p.to_string_lossy().to_string()),
Err(e) => {
show_error!("'{}': {}", dir.display(), e);
return 1;
}
} }
return 1;
} }
} Err(x) => Err(x)
}
let tmpfile = Builder::new()
.prefix(prefix)
.rand_bytes(rand)
.suffix(suffix)
.tempfile_in(tmpdir);
let tmpfile = match tmpfile {
Ok(f) => f,
Err(e) => {
if !quiet {
show_info!("failed to create tempfile: {}", e);
}
return 1;
} }
}; };
let tmpname = tmpfile.path().to_string_lossy().to_string(); match res {
Ok(ref f) => {
println!("{}", tmpname); println!("{}", f);
0
// CAUTION: Not to call `drop` of tmpfile, which removes the tempfile, }
// I call a dangerous function `forget`. Err(e) => {
forget(tmpfile); if !quiet {
show_error!("{}: {}", e, dir.display());
0 }
1
}
}
} }

View file

@ -1,51 +0,0 @@
// spell-checker:ignore (ToDO) tempdir tmpdir
// Mainly taken from crate `tempdir`
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
use std::io::Result as IOResult;
use std::io::{Error, ErrorKind};
use std::path::Path;
// How many times should we (re)try finding an unused random name? It should be
// enough that an attacker will run out of luck before we run out of patience.
const NUM_RETRIES: u32 = 1 << 31;
#[cfg(any(unix, target_os = "redox"))]
fn create_dir<P: AsRef<Path>>(path: P) -> IOResult<()> {
use std::fs::DirBuilder;
use std::os::unix::fs::DirBuilderExt;
DirBuilder::new().mode(0o700).create(path)
}
#[cfg(windows)]
fn create_dir<P: AsRef<Path>>(path: P) -> IOResult<()> {
::std::fs::create_dir(path)
}
pub fn new_in<P: AsRef<Path>>(
tmpdir: P,
prefix: &str,
rand: usize,
suffix: &str,
) -> IOResult<String> {
let mut rng = thread_rng();
for _ in 0..NUM_RETRIES {
let rand_chars: String = rng.sample_iter(&Alphanumeric).take(rand).collect();
let leaf = format!("{}{}{}", prefix, rand_chars, suffix);
let path = tmpdir.as_ref().join(&leaf);
match create_dir(&path) {
Ok(_) => return Ok(path.to_string_lossy().into_owned()),
Err(ref e) if e.kind() == ErrorKind::AlreadyExists => {}
Err(e) => return Err(e),
}
}
Err(Error::new(
ErrorKind::AlreadyExists,
"too many temporary directories already exist",
))
}

View file

@ -20,6 +20,7 @@ use std::os::unix;
#[cfg(windows)] #[cfg(windows)]
use std::os::windows; use std::os::windows;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use uucore::backup_control::{self, BackupMode};
use fs_extra::dir::{move_dir, CopyOptions as DirCopyOptions}; use fs_extra::dir::{move_dir, CopyOptions as DirCopyOptions};
@ -40,16 +41,9 @@ pub enum OverwriteMode {
Force, Force,
} }
#[derive(Clone, Copy, Eq, PartialEq)]
pub enum BackupMode {
NoBackup,
SimpleBackup,
NumberedBackup,
ExistingBackup,
}
static ABOUT: &str = "Move SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY."; static ABOUT: &str = "Move SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY.";
static VERSION: &str = env!("CARGO_PKG_VERSION"); static VERSION: &str = env!("CARGO_PKG_VERSION");
static LONG_HELP: &str = "";
static OPT_BACKUP: &str = "backup"; static OPT_BACKUP: &str = "backup";
static OPT_BACKUP_NO_ARG: &str = "b"; static OPT_BACKUP_NO_ARG: &str = "b";
@ -80,20 +74,16 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
let matches = App::new(executable!()) let matches = App::new(executable!())
.version(VERSION) .version(VERSION)
.about(ABOUT) .about(ABOUT)
.after_help(&*format!("{}\n{}", LONG_HELP, backup_control::BACKUP_CONTROL_LONG_HELP))
.usage(&usage[..]) .usage(&usage[..])
.arg( .arg(
Arg::with_name(OPT_BACKUP) Arg::with_name(OPT_BACKUP)
.long(OPT_BACKUP) .long(OPT_BACKUP)
.help("make a backup of each existing destination file") .help("make a backup of each existing destination file")
.takes_value(true) .takes_value(true)
.possible_value("simple") .require_equals(true)
.possible_value("never") .min_values(0)
.possible_value("numbered") .possible_values(backup_control::BACKUP_CONTROL_VALUES)
.possible_value("t")
.possible_value("existing")
.possible_value("nil")
.possible_value("none")
.possible_value("off")
.value_name("CONTROL") .value_name("CONTROL")
) )
.arg( .arg(
@ -172,18 +162,17 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
.unwrap_or_default(); .unwrap_or_default();
let overwrite_mode = determine_overwrite_mode(&matches); let overwrite_mode = determine_overwrite_mode(&matches);
let backup_mode = determine_backup_mode(&matches); let backup_mode = backup_control::determine_backup_mode(
matches.is_present(OPT_BACKUP_NO_ARG) || matches.is_present(OPT_BACKUP),
matches.value_of(OPT_BACKUP),
);
if overwrite_mode == OverwriteMode::NoClobber && backup_mode != BackupMode::NoBackup { if overwrite_mode == OverwriteMode::NoClobber && backup_mode != BackupMode::NoBackup {
show_error!( show_usage_error!("options --backup and --no-clobber are mutually exclusive");
"options --backup and --no-clobber are mutually exclusive\n\
Try '{} --help' for more information.",
executable!()
);
return 1; return 1;
} }
let backup_suffix = determine_backup_suffix(backup_mode, &matches); let backup_suffix = backup_control::determine_backup_suffix(matches.value_of(OPT_SUFFIX));
let behavior = Behavior { let behavior = Behavior {
overwrite: overwrite_mode, overwrite: overwrite_mode,
@ -227,37 +216,6 @@ fn determine_overwrite_mode(matches: &ArgMatches) -> OverwriteMode {
} }
} }
fn determine_backup_mode(matches: &ArgMatches) -> BackupMode {
if matches.is_present(OPT_BACKUP_NO_ARG) {
BackupMode::SimpleBackup
} else if matches.is_present(OPT_BACKUP) {
match matches.value_of(OPT_BACKUP).map(String::from) {
None => BackupMode::SimpleBackup,
Some(mode) => match &mode[..] {
"simple" | "never" => BackupMode::SimpleBackup,
"numbered" | "t" => BackupMode::NumberedBackup,
"existing" | "nil" => BackupMode::ExistingBackup,
"none" | "off" => BackupMode::NoBackup,
_ => panic!(), // cannot happen as it is managed by clap
},
}
} else {
BackupMode::NoBackup
}
}
fn determine_backup_suffix(backup_mode: BackupMode, matches: &ArgMatches) -> String {
if matches.is_present(OPT_SUFFIX) {
matches.value_of(OPT_SUFFIX).map(String::from).unwrap()
} else if let (Ok(s), BackupMode::SimpleBackup) =
(env::var("SIMPLE_BACKUP_SUFFIX"), backup_mode)
{
s
} else {
"~".to_owned()
}
}
fn exec(files: &[PathBuf], b: Behavior) -> i32 { fn exec(files: &[PathBuf], b: Behavior) -> i32 {
if let Some(ref name) = b.target_dir { if let Some(ref name) = b.target_dir {
return move_files_into_dir(files, &PathBuf::from(name), &b); return move_files_into_dir(files, &PathBuf::from(name), &b);
@ -295,7 +253,7 @@ fn exec(files: &[PathBuf], b: Behavior) -> i32 {
"cannot move {} to {}: {}", "cannot move {} to {}: {}",
source.display(), source.display(),
target.display(), target.display(),
e e.to_string()
); );
1 1
} }
@ -358,14 +316,15 @@ fn move_files_into_dir(files: &[PathBuf], target_dir: &Path, b: &Behavior) -> i3
if let Err(e) = rename(sourcepath, &targetpath, b) { if let Err(e) = rename(sourcepath, &targetpath, b) {
show_error!( show_error!(
"mv: cannot move {} to {}: {}", "cannot move {} to {}: {}",
sourcepath.display(), sourcepath.display(),
targetpath.display(), targetpath.display(),
e e.to_string()
); );
all_successful = false; all_successful = false;
} }
} }
if all_successful { if all_successful {
0 0
} else { } else {
@ -388,12 +347,7 @@ fn rename(from: &Path, to: &Path, b: &Behavior) -> io::Result<()> {
OverwriteMode::Force => {} OverwriteMode::Force => {}
}; };
backup_path = match b.backup { backup_path = backup_control::get_backup_path(b.backup, to, &b.suffix);
BackupMode::NoBackup => None,
BackupMode::SimpleBackup => Some(simple_backup_path(to, &b.suffix)),
BackupMode::NumberedBackup => Some(numbered_backup_path(to)),
BackupMode::ExistingBackup => Some(existing_backup_path(to, &b.suffix)),
};
if let Some(ref backup_path) = backup_path { if let Some(ref backup_path) = backup_path {
rename_with_fallback(to, backup_path)?; rename_with_fallback(to, backup_path)?;
} }
@ -452,7 +406,13 @@ fn rename_with_fallback(from: &Path, to: &Path) -> io::Result<()> {
..DirCopyOptions::new() ..DirCopyOptions::new()
}; };
if let Err(err) = move_dir(from, to, &options) { if let Err(err) = move_dir(from, to, &options) {
return Err(io::Error::new(io::ErrorKind::Other, format!("{:?}", err))); return match err.kind {
fs_extra::error::ErrorKind::PermissionDenied => Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"Permission denied",
)),
_ => Err(io::Error::new(io::ErrorKind::Other, format!("{:?}", err))),
};
} }
} else { } else {
fs::copy(from, to).and_then(|_| fs::remove_file(from))?; fs::copy(from, to).and_then(|_| fs::remove_file(from))?;
@ -507,28 +467,6 @@ fn read_yes() -> bool {
} }
} }
fn simple_backup_path(path: &Path, suffix: &str) -> PathBuf {
let mut p = path.to_string_lossy().into_owned();
p.push_str(suffix);
PathBuf::from(p)
}
fn numbered_backup_path(path: &Path) -> PathBuf {
(1_u64..)
.map(|i| path.with_extension(format!("~{}~", i)))
.find(|p| !p.exists())
.expect("cannot create backup")
}
fn existing_backup_path(path: &Path, suffix: &str) -> PathBuf {
let test_path = path.with_extension("~1~");
if test_path.exists() {
numbered_backup_path(path)
} else {
simple_backup_path(path, suffix)
}
}
fn is_empty_dir(path: &Path) -> bool { fn is_empty_dir(path: &Path) -> bool {
match fs::read_dir(path) { match fs::read_dir(path) {
Ok(contents) => contents.peekable().peek().is_none(), Ok(contents) => contents.peekable().peek().is_none(),

View file

@ -122,13 +122,13 @@ fn find_stdout() -> File {
.open(Path::new(NOHUP_OUT)) .open(Path::new(NOHUP_OUT))
{ {
Ok(t) => { Ok(t) => {
show_info!("ignoring input and appending output to '{}'", NOHUP_OUT); show_error!("ignoring input and appending output to '{}'", NOHUP_OUT);
t t
} }
Err(e1) => { Err(e1) => {
let home = match env::var("HOME") { let home = match env::var("HOME") {
Err(_) => { Err(_) => {
show_info!("failed to open '{}': {}", NOHUP_OUT, e1); show_error!("failed to open '{}': {}", NOHUP_OUT, e1);
exit!(internal_failure_code) exit!(internal_failure_code)
} }
Ok(h) => h, Ok(h) => h,
@ -143,12 +143,12 @@ fn find_stdout() -> File {
.open(&homeout) .open(&homeout)
{ {
Ok(t) => { Ok(t) => {
show_info!("ignoring input and appending output to '{}'", homeout_str); show_error!("ignoring input and appending output to '{}'", homeout_str);
t t
} }
Err(e2) => { Err(e2) => {
show_info!("failed to open '{}': {}", NOHUP_OUT, e1); show_error!("failed to open '{}': {}", NOHUP_OUT, e1);
show_info!("failed to open '{}': {}", homeout_str, e2); show_error!("failed to open '{}': {}", homeout_str, e2);
exit!(internal_failure_code) exit!(internal_failure_code)
} }
} }

View file

@ -216,7 +216,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
match result { match result {
Err(e) => { Err(e) => {
std::io::stdout().flush().expect("error flushing stdout"); std::io::stdout().flush().expect("error flushing stdout");
show_info!("{}", e); show_error!("{}", e);
1 1
} }
_ => 0, _ => 0,

View file

@ -48,7 +48,7 @@ fn get_usage() -> String {
fn get_long_usage() -> String { fn get_long_usage() -> String {
format!( format!(
"A lightweight 'finger' program; print user information.\n\ "A lightweight 'finger' program; print user information.\n\
The utmp file will be {}.", The utmp file will be {}.",
utmpx::DEFAULT_FILE utmpx::DEFAULT_FILE
) )
} }
@ -286,17 +286,10 @@ impl Pinky {
print!(" {}", time_string(&ut)); print!(" {}", time_string(&ut));
if self.include_where && !ut.host().is_empty() { let mut s = ut.host();
let ut_host = ut.host(); if self.include_where && !s.is_empty() {
let mut res = ut_host.splitn(2, ':'); s = safe_unwrap!(ut.canon_host());
let host = match res.next() { print!(" {}", s);
Some(_) => ut.canon_host().unwrap_or_else(|_| ut_host.clone()),
None => ut_host.clone(),
};
match res.next() {
Some(d) => print!(" {}:{}", host, d),
None => print!(" {}", host),
}
} }
println!(); println!();

View file

@ -11,7 +11,6 @@
extern crate uucore; extern crate uucore;
use clap::{App, Arg}; use clap::{App, Arg};
use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use uucore::fs::{canonicalize, CanonicalizeMode}; use uucore::fs::{canonicalize, CanonicalizeMode};
@ -75,64 +74,35 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
let quiet = matches.is_present(OPT_QUIET); let quiet = matches.is_present(OPT_QUIET);
let mut retcode = 0; let mut retcode = 0;
for path in &paths { for path in &paths {
if !resolve_path(path, strip, zero, quiet) { if let Err(e) = resolve_path(path, strip, zero) {
if !quiet {
show_error!("{}: {}", e, path.display());
}
retcode = 1 retcode = 1
}; };
} }
retcode retcode
} }
fn resolve_path(p: &Path, strip: bool, zero: bool, quiet: bool) -> bool { /// Resolve a path to an absolute form and print it.
let abs = canonicalize(p, CanonicalizeMode::Normal).unwrap(); ///
/// If `strip` is `true`, then this function does not attempt to resolve
if strip { /// symbolic links in the path. If `zero` is `true`, then this function
if zero { /// prints the path followed by the null byte (`'\0'`) instead of a
print!("{}\0", p.display()); /// newline character (`'\n'`).
} else { ///
println!("{}", p.display()) /// # Errors
} ///
return true; /// This function returns an error if there is a problem resolving
} /// symbolic links.
fn resolve_path(p: &Path, strip: bool, zero: bool) -> std::io::Result<()> {
let mut result = PathBuf::new(); let mode = if strip {
let mut links_left = 256; CanonicalizeMode::None
for part in abs.components() {
result.push(part.as_os_str());
loop {
if links_left == 0 {
if !quiet {
show_error!("Too many symbolic links: {}", p.display())
};
return false;
}
match fs::metadata(result.as_path()) {
Err(_) => break,
Ok(ref m) if !m.file_type().is_symlink() => break,
Ok(_) => {
links_left -= 1;
match fs::read_link(result.as_path()) {
Ok(x) => {
result.pop();
result.push(x.as_path());
}
_ => {
if !quiet {
show_error!("Invalid path: {}", p.display())
};
return false;
}
}
}
}
}
}
if zero {
print!("{}\0", result.display());
} else { } else {
println!("{}", result.display()); CanonicalizeMode::Normal
} };
let abs = canonicalize(p, mode)?;
true let line_ending = if zero { '\0' } else { '\n' };
print!("{}{}", abs.display(), line_ending);
Ok(())
} }

View file

@ -69,6 +69,28 @@ Run `cargo build --release` before benchmarking after you make a change!
- Benchmark numeric sorting with hyperfine: `hyperfine "target/release/coreutils sort shuffled_numbers_si.txt -h -o output.txt"`. - Benchmark numeric sorting with hyperfine: `hyperfine "target/release/coreutils sort shuffled_numbers_si.txt -h -o output.txt"`.
## External sorting
Try running commands with the `-S` option set to an amount of memory to be used, such as `1M`. Additionally, you could try sorting
huge files (ideally multiple Gigabytes) with `-S` (or without `-S` to benchmark with our default value).
Creating such a large file can be achieved by running `cat shuffled_wordlist.txt | sort -R >> shuffled_wordlist.txt`
multiple times (this will add the contents of `shuffled_wordlist.txt` to itself).
Example: Run `hyperfine './target/release/coreutils sort shuffled_wordlist.txt -S 1M' 'sort shuffled_wordlist.txt -S 1M'`
## Merging
"Merge" sort merges already sorted files. It is a sub-step of external sorting, so benchmarking it separately may be helpful.
- Splitting `shuffled_wordlist.txt` can be achieved by running `split shuffled_wordlist.txt shuffled_wordlist_slice_ --additional-suffix=.txt`
- Sort each part by running `for f in shuffled_wordlist_slice_*; do sort $f -o $f; done`
- Benchmark merging by running `hyperfine "target/release/coreutils sort -m shuffled_wordlist_slice_*"`
## Check
When invoked with -c, we simply check if the input is already ordered. The input for benchmarking should be an already sorted file.
- Benchmark checking by running `hyperfine "target/release/coreutils sort -c sorted_wordlist.txt"`
## Stdout and stdin performance ## Stdout and stdin performance
Try to run the above benchmarks by piping the input through stdin (standard input) and redirect the Try to run the above benchmarks by piping the input through stdin (standard input) and redirect the

View file

@ -15,19 +15,20 @@ edition = "2018"
path = "src/sort.rs" path = "src/sort.rs"
[dependencies] [dependencies]
serde_json = { version = "1.0.64", default-features = false, features = ["alloc"] } binary-heap-plus = "0.4.1"
serde = { version = "1.0", features = ["derive"] }
rayon = "1.5"
rand = "0.7"
clap = "2.33" clap = "2.33"
compare = "0.1.0"
fnv = "1.0.7" fnv = "1.0.7"
itertools = "0.10.0" itertools = "0.10.0"
memchr = "2.4.0"
ouroboros = "0.9.3"
rand = "0.7"
rayon = "1.5"
semver = "0.9.0" semver = "0.9.0"
smallvec = { version="1.6.1", features=["serde"] } tempfile = "3"
unicode-width = "0.1.8" unicode-width = "0.1.8"
uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["fs"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["fs"] }
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }
tempdir = "0.3.7"
[[bin]] [[bin]]
name = "sort" name = "sort"

103
src/uu/sort/src/check.rs Normal file
View file

@ -0,0 +1,103 @@
// * This file is part of the uutils coreutils package.
// *
// * (c) Michael Debertol <michael.debertol..AT..gmail.com>
// *
// * For the full copyright and license information, please view the LICENSE
// * file that was distributed with this source code.
//! Check if a file is ordered
use crate::{
chunks::{self, Chunk},
compare_by, open, GlobalSettings,
};
use itertools::Itertools;
use std::{
cmp::Ordering,
io::Read,
iter,
sync::mpsc::{sync_channel, Receiver, SyncSender},
thread,
};
/// Check if the file at `path` is ordered.
///
/// # Returns
///
/// The code we should exit with.
pub fn check(path: &str, settings: &GlobalSettings) -> i32 {
let file = open(path).expect("failed to open input file");
let (recycled_sender, recycled_receiver) = sync_channel(2);
let (loaded_sender, loaded_receiver) = sync_channel(2);
thread::spawn({
let settings = settings.clone();
move || reader(file, recycled_receiver, loaded_sender, &settings)
});
for _ in 0..2 {
recycled_sender
.send(Chunk::new(vec![0; 100 * 1024], |_| Vec::new()))
.unwrap();
}
let mut prev_chunk: Option<Chunk> = None;
let mut line_idx = 0;
for chunk in loaded_receiver.iter() {
line_idx += 1;
if let Some(prev_chunk) = prev_chunk.take() {
// Check if the first element of the new chunk is greater than the last
// element from the previous chunk
let prev_last = prev_chunk.borrow_lines().last().unwrap();
let new_first = chunk.borrow_lines().first().unwrap();
if compare_by(prev_last, new_first, &settings) == Ordering::Greater {
if !settings.check_silent {
println!("sort: {}:{}: disorder: {}", path, line_idx, new_first.line);
}
return 1;
}
recycled_sender.send(prev_chunk).ok();
}
for (a, b) in chunk.borrow_lines().iter().tuple_windows() {
line_idx += 1;
if compare_by(a, b, &settings) == Ordering::Greater {
if !settings.check_silent {
println!("sort: {}:{}: disorder: {}", path, line_idx, b.line);
}
return 1;
}
}
prev_chunk = Some(chunk);
}
0
}
/// The function running on the reader thread.
fn reader(
mut file: Box<dyn Read + Send>,
receiver: Receiver<Chunk>,
sender: SyncSender<Chunk>,
settings: &GlobalSettings,
) {
let mut sender = Some(sender);
let mut carry_over = vec![];
for chunk in receiver.iter() {
let (recycled_lines, recycled_buffer) = chunk.recycle();
chunks::read(
&mut sender,
recycled_buffer,
None,
&mut carry_over,
&mut file,
&mut iter::empty(),
if settings.zero_terminated {
b'\0'
} else {
b'\n'
},
recycled_lines,
settings,
)
}
}

229
src/uu/sort/src/chunks.rs Normal file
View file

@ -0,0 +1,229 @@
// * This file is part of the uutils coreutils package.
// *
// * (c) Michael Debertol <michael.debertol..AT..gmail.com>
// *
// * For the full copyright and license information, please view the LICENSE
// * file that was distributed with this source code.
//! Utilities for reading files as chunks.
use std::{
io::{ErrorKind, Read},
sync::mpsc::SyncSender,
};
use memchr::memchr_iter;
use ouroboros::self_referencing;
use crate::{GlobalSettings, Line};
/// The chunk that is passed around between threads.
/// `lines` consist of slices into `buffer`.
#[self_referencing(pub_extras)]
#[derive(Debug)]
pub struct Chunk {
pub buffer: Vec<u8>,
#[borrows(buffer)]
#[covariant]
pub lines: Vec<Line<'this>>,
}
impl Chunk {
/// Destroy this chunk and return its components to be reused.
///
/// # Returns
///
/// * The `lines` vector, emptied
/// * The `buffer` vector, **not** emptied
pub fn recycle(mut self) -> (Vec<Line<'static>>, Vec<u8>) {
let recycled_lines = self.with_lines_mut(|lines| {
lines.clear();
unsafe {
// SAFETY: It is safe to (temporarily) transmute to a vector of lines with a longer lifetime,
// because the vector is empty.
// Transmuting is necessary to make recycling possible. See https://github.com/rust-lang/rfcs/pull/2802
// for a rfc to make this unnecessary. Its example is similar to the code here.
std::mem::transmute::<Vec<Line<'_>>, Vec<Line<'static>>>(std::mem::take(lines))
}
});
(recycled_lines, self.into_heads().buffer)
}
}
/// Read a chunk, parse lines and send them.
///
/// No empty chunk will be sent. If we reach the end of the input, sender_option
/// is set to None. If this function however does not set sender_option to None,
/// it is not guaranteed that there is still input left: If the input fits _exactly_
/// into a buffer, we will only notice that there's nothing more to read at the next
/// invocation.
///
/// # Arguments
///
/// (see also `read_to_chunk` for a more detailed documentation)
///
/// * `sender_option`: The sender to send the lines to the sorter. If `None`, this function does nothing.
/// * `buffer`: The recycled buffer. All contents will be overwritten, but it must already be filled.
/// (i.e. `buffer.len()` should be equal to `buffer.capacity()`)
/// * `max_buffer_size`: How big `buffer` can be.
/// * `carry_over`: The bytes that must be carried over in between invocations.
/// * `file`: The current file.
/// * `next_files`: What `file` should be updated to next.
/// * `separator`: The line separator.
/// * `lines`: The recycled vector to fill with lines. Must be empty.
/// * `settings`: The global settings.
#[allow(clippy::too_many_arguments)]
pub fn read(
sender_option: &mut Option<SyncSender<Chunk>>,
mut buffer: Vec<u8>,
max_buffer_size: Option<usize>,
carry_over: &mut Vec<u8>,
file: &mut Box<dyn Read + Send>,
next_files: &mut impl Iterator<Item = Box<dyn Read + Send>>,
separator: u8,
lines: Vec<Line<'static>>,
settings: &GlobalSettings,
) {
assert!(lines.is_empty());
if let Some(sender) = sender_option {
if buffer.len() < carry_over.len() {
buffer.resize(carry_over.len() + 10 * 1024, 0);
}
buffer[..carry_over.len()].copy_from_slice(&carry_over);
let (read, should_continue) = read_to_buffer(
file,
next_files,
&mut buffer,
max_buffer_size,
carry_over.len(),
separator,
);
carry_over.clear();
carry_over.extend_from_slice(&buffer[read..]);
let payload = Chunk::new(buffer, |buf| {
let mut lines = unsafe {
// SAFETY: It is safe to transmute to a vector of lines with shorter lifetime,
// because it was only temporarily transmuted to a Vec<Line<'static>> to make recycling possible.
std::mem::transmute::<Vec<Line<'static>>, Vec<Line<'_>>>(lines)
};
let read = crash_if_err!(1, std::str::from_utf8(&buf[..read]));
parse_lines(read, &mut lines, separator, &settings);
lines
});
if !payload.borrow_lines().is_empty() {
sender.send(payload).unwrap();
}
if !should_continue {
*sender_option = None;
}
}
}
/// Split `read` into `Line`s, and add them to `lines`.
fn parse_lines<'a>(
mut read: &'a str,
lines: &mut Vec<Line<'a>>,
separator: u8,
settings: &GlobalSettings,
) {
// Strip a trailing separator. TODO: Once our MinRustV is 1.45 or above, use strip_suffix() instead.
if read.ends_with(separator as char) {
read = &read[..read.len() - 1];
}
lines.extend(
read.split(separator as char)
.map(|line| Line::create(line, settings)),
);
}
/// Read from `file` into `buffer`.
///
/// This function makes sure that at least two lines are read (unless we reach EOF and there's no next file),
/// growing the buffer if necessary.
/// The last line is likely to not have been fully read into the buffer. Its bytes must be copied to
/// the front of the buffer for the next invocation so that it can be continued to be read
/// (see the return values and `start_offset`).
///
/// # Arguments
///
/// * `file`: The file to start reading from.
/// * `next_files`: When `file` reaches EOF, it is updated to `next_files.next()` if that is `Some`,
/// and this function continues reading.
/// * `buffer`: The buffer that is filled with bytes. Its contents will mostly be overwritten (see `start_offset`
/// as well). It will be grown up to `max_buffer_size` if necessary, but it will always grow to read at least two lines.
/// * `max_buffer_size`: Grow the buffer to at most this length. If None, the buffer will not grow, unless needed to read at least two lines.
/// * `start_offset`: The amount of bytes at the start of `buffer` that were carried over
/// from the previous read and should not be overwritten.
/// * `separator`: The byte that separates lines.
///
/// # Returns
///
/// * The amount of bytes in `buffer` that can now be interpreted as lines.
/// The remaining bytes must be copied to the start of the buffer for the next invocation,
/// if another invocation is necessary, which is determined by the other return value.
/// * Whether this function should be called again.
fn read_to_buffer(
file: &mut Box<dyn Read + Send>,
next_files: &mut impl Iterator<Item = Box<dyn Read + Send>>,
buffer: &mut Vec<u8>,
max_buffer_size: Option<usize>,
start_offset: usize,
separator: u8,
) -> (usize, bool) {
let mut read_target = &mut buffer[start_offset..];
loop {
match file.read(read_target) {
Ok(0) => {
if read_target.is_empty() {
// chunk is full
if let Some(max_buffer_size) = max_buffer_size {
if max_buffer_size > buffer.len() {
// we can grow the buffer
let prev_len = buffer.len();
if buffer.len() < max_buffer_size / 2 {
buffer.resize(buffer.len() * 2, 0);
} else {
buffer.resize(max_buffer_size, 0);
}
read_target = &mut buffer[prev_len..];
continue;
}
}
let mut sep_iter = memchr_iter(separator, &buffer).rev();
let last_line_end = sep_iter.next();
if sep_iter.next().is_some() {
// We read enough lines.
let end = last_line_end.unwrap();
// We want to include the separator here, because it shouldn't be carried over.
return (end + 1, true);
} else {
// We need to read more lines
let len = buffer.len();
// resize the vector to 10 KB more
buffer.resize(len + 1024 * 10, 0);
read_target = &mut buffer[len..];
}
} else {
// This file is empty.
if let Some(next_file) = next_files.next() {
// There is another file.
*file = next_file;
} else {
// This was the last file.
let leftover_len = read_target.len();
return (buffer.len() - leftover_len, false);
}
}
}
Ok(n) => {
read_target = &mut read_target[n..];
}
Err(e) if e.kind() == ErrorKind::Interrupted => {
// retry
}
Err(e) => crash!(1, "{}", e),
}
}
}

201
src/uu/sort/src/ext_sort.rs Normal file
View file

@ -0,0 +1,201 @@
// * This file is part of the uutils coreutils package.
// *
// * (c) Michael Debertol <michael.debertol..AT..gmail.com>
// *
// * For the full copyright and license information, please view the LICENSE
// * file that was distributed with this source code.
//! Sort big files by using auxiliary files for storing intermediate chunks.
//!
//! Files are read into chunks of memory which are then sorted individually and
//! written to temporary files. There are two threads: One sorter, and one reader/writer.
//! The buffers for the individual chunks are recycled. There are two buffers.
use std::cmp::Ordering;
use std::io::{BufWriter, Write};
use std::path::Path;
use std::{
fs::OpenOptions,
io::Read,
sync::mpsc::{Receiver, SyncSender},
thread,
};
use itertools::Itertools;
use tempfile::TempDir;
use crate::{
chunks::{self, Chunk},
compare_by, merge, output_sorted_lines, sort_by, GlobalSettings,
};
const MIN_BUFFER_SIZE: usize = 8_000;
/// Sort files by using auxiliary files for storing intermediate chunks (if needed), and output the result.
pub fn ext_sort(files: &mut impl Iterator<Item = Box<dyn Read + Send>>, settings: &GlobalSettings) {
let tmp_dir = crash_if_err!(1, tempfile::Builder::new().prefix("uutils_sort").tempdir_in(&settings.tmp_dir));
let (sorted_sender, sorted_receiver) = std::sync::mpsc::sync_channel(1);
let (recycled_sender, recycled_receiver) = std::sync::mpsc::sync_channel(1);
thread::spawn({
let settings = settings.clone();
move || sorter(recycled_receiver, sorted_sender, settings)
});
let read_result = reader_writer(
files,
&tmp_dir,
if settings.zero_terminated {
b'\0'
} else {
b'\n'
},
// Heuristically chosen: Dividing by 10 seems to keep our memory usage roughly
// around settings.buffer_size as a whole.
settings.buffer_size / 10,
settings.clone(),
sorted_receiver,
recycled_sender,
);
match read_result {
ReadResult::WroteChunksToFile { chunks_written } => {
let files = (0..chunks_written)
.map(|chunk_num| tmp_dir.path().join(chunk_num.to_string()))
.collect::<Vec<_>>();
let mut merger = merge::merge(&files, settings);
merger.write_all(settings);
}
ReadResult::SortedSingleChunk(chunk) => {
output_sorted_lines(chunk.borrow_lines().iter(), settings);
}
ReadResult::SortedTwoChunks([a, b]) => {
let merged_iter = a
.borrow_lines()
.iter()
.merge_by(b.borrow_lines().iter(), |line_a, line_b| {
compare_by(line_a, line_b, settings) != Ordering::Greater
});
output_sorted_lines(merged_iter, settings);
}
ReadResult::EmptyInput => {
// don't output anything
}
}
}
/// The function that is executed on the sorter thread.
fn sorter(receiver: Receiver<Chunk>, sender: SyncSender<Chunk>, settings: GlobalSettings) {
while let Ok(mut payload) = receiver.recv() {
payload.with_lines_mut(|lines| sort_by(lines, &settings));
sender.send(payload).unwrap();
}
}
/// Describes how we read the chunks from the input.
enum ReadResult {
/// The input was empty. Nothing was read.
EmptyInput,
/// The input fits into a single Chunk, which was kept in memory.
SortedSingleChunk(Chunk),
/// The input fits into two chunks, which were kept in memory.
SortedTwoChunks([Chunk; 2]),
/// The input was read into multiple chunks, which were written to auxiliary files.
WroteChunksToFile {
/// The number of chunks written to auxiliary files.
chunks_written: usize,
},
}
/// The function that is executed on the reader/writer thread.
///
/// # Returns
/// * The number of chunks read.
fn reader_writer(
mut files: impl Iterator<Item = Box<dyn Read + Send>>,
tmp_dir: &TempDir,
separator: u8,
buffer_size: usize,
settings: GlobalSettings,
receiver: Receiver<Chunk>,
sender: SyncSender<Chunk>,
) -> ReadResult {
let mut sender_option = Some(sender);
let mut file = files.next().unwrap();
let mut carry_over = vec![];
// kick things off with two reads
for _ in 0..2 {
chunks::read(
&mut sender_option,
vec![0; MIN_BUFFER_SIZE],
Some(buffer_size),
&mut carry_over,
&mut file,
&mut files,
separator,
Vec::new(),
&settings,
);
if sender_option.is_none() {
// We have already read the whole input. Since we are in our first two reads,
// this means that we can fit the whole input into memory. Bypass writing below and
// handle this case in a more straightforward way.
return if let Ok(first_chunk) = receiver.recv() {
if let Ok(second_chunk) = receiver.recv() {
ReadResult::SortedTwoChunks([first_chunk, second_chunk])
} else {
ReadResult::SortedSingleChunk(first_chunk)
}
} else {
ReadResult::EmptyInput
};
}
}
let mut file_number = 0;
loop {
let mut chunk = match receiver.recv() {
Ok(it) => it,
_ => {
return ReadResult::WroteChunksToFile {
chunks_written: file_number,
}
}
};
write(
&mut chunk,
&tmp_dir.path().join(file_number.to_string()),
separator,
);
file_number += 1;
let (recycled_lines, recycled_buffer) = chunk.recycle();
chunks::read(
&mut sender_option,
recycled_buffer,
None,
&mut carry_over,
&mut file,
&mut files,
separator,
recycled_lines,
&settings,
);
}
}
/// Write the lines in `chunk` to `file`, separated by `separator`.
fn write(chunk: &mut Chunk, file: &Path, separator: u8) {
chunk.with_lines_mut(|lines| {
// Write the lines to the file
let file = crash_if_err!(1, OpenOptions::new().create(true).write(true).open(file));
let mut writer = BufWriter::new(file);
for s in lines.iter() {
crash_if_err!(1, writer.write_all(s.line.as_bytes()));
crash_if_err!(1, writer.write_all(&[separator]));
}
});
}

View file

@ -1,19 +0,0 @@
Copyright 2018 Battelle Memorial Institute
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,295 +0,0 @@
use std::clone::Clone;
use std::cmp::Ordering::Less;
use std::collections::VecDeque;
use std::error::Error;
use std::fs::{File, OpenOptions};
use std::io::SeekFrom::Start;
use std::io::{BufRead, BufReader, BufWriter, Seek, Write};
use std::marker::PhantomData;
use std::path::PathBuf;
use serde::de::DeserializeOwned;
use serde::Serialize;
use serde_json;
use tempdir::TempDir;
use super::{GlobalSettings, Line};
/// Trait for types that can be used by
/// [ExternalSorter](struct.ExternalSorter.html). Must be sortable, cloneable,
/// serializeable, and able to report on it's size
pub trait ExternallySortable: Clone + Serialize + DeserializeOwned {
/// Get the size, in bytes, of this object (used to constrain the buffer
/// used in the external sort).
fn get_size(&self) -> u64;
}
/// Iterator that provides sorted `T`s
pub struct ExtSortedIterator<Line> {
buffers: Vec<VecDeque<Line>>,
chunk_offsets: Vec<u64>,
max_per_chunk: u64,
chunks: u64,
tmp_dir: TempDir,
settings: GlobalSettings,
failed: bool,
}
impl Iterator for ExtSortedIterator<Line>
where
Line: ExternallySortable,
{
type Item = Result<Line, Box<dyn Error>>;
/// # Errors
///
/// This method can fail due to issues reading intermediate sorted chunks
/// from disk, or due to serde deserialization issues
fn next(&mut self) -> Option<Self::Item> {
if self.failed {
return None;
}
// fill up any empty buffers
let mut empty = true;
for chunk_num in 0..self.chunks {
if self.buffers[chunk_num as usize].is_empty() {
let mut f = match File::open(self.tmp_dir.path().join(chunk_num.to_string())) {
Ok(f) => f,
Err(e) => {
self.failed = true;
return Some(Err(Box::new(e)));
}
};
match f.seek(Start(self.chunk_offsets[chunk_num as usize])) {
Ok(_) => (),
Err(e) => {
self.failed = true;
return Some(Err(Box::new(e)));
}
}
let bytes_read =
match fill_buff(&mut self.buffers[chunk_num as usize], f, self.max_per_chunk) {
Ok(bytes_read) => bytes_read,
Err(e) => {
self.failed = true;
return Some(Err(e));
}
};
self.chunk_offsets[chunk_num as usize] += bytes_read;
if !self.buffers[chunk_num as usize].is_empty() {
empty = false;
}
} else {
empty = false;
}
}
if empty {
return None;
}
// find the next record to write
// check is_empty() before unwrap()ing
let mut idx = 0;
for chunk_num in 0..self.chunks as usize {
if !self.buffers[chunk_num].is_empty() {
if self.buffers[idx].is_empty()
|| (super::compare_by)(
self.buffers[chunk_num].front().unwrap(),
self.buffers[idx].front().unwrap(),
&self.settings,
) == Less
{
idx = chunk_num;
}
}
}
// unwrap due to checks above
let r = self.buffers[idx].pop_front().unwrap();
Some(Ok(r))
}
}
/// Perform an external sort on an unsorted stream of incoming data
pub struct ExternalSorter<Line>
where
Line: ExternallySortable,
{
tmp_dir: Option<PathBuf>,
buffer_bytes: u64,
phantom: PhantomData<Line>,
settings: GlobalSettings,
}
impl ExternalSorter<Line>
where
Line: ExternallySortable,
{
/// Create a new `ExternalSorter` with a specified memory buffer and
/// temporary directory
pub fn new(
buffer_bytes: u64,
tmp_dir: Option<PathBuf>,
settings: GlobalSettings,
) -> ExternalSorter<Line> {
ExternalSorter {
buffer_bytes,
tmp_dir,
phantom: PhantomData,
settings,
}
}
/// Sort (based on `compare`) the `T`s provided by `unsorted` and return an
/// iterator
///
/// # Errors
///
/// This method can fail due to issues writing intermediate sorted chunks
/// to disk, or due to serde serialization issues
pub fn sort_by<I>(
&self,
unsorted: I,
settings: GlobalSettings,
) -> Result<ExtSortedIterator<Line>, Box<dyn Error>>
where
I: Iterator<Item = Line>,
{
let tmp_dir = match self.tmp_dir {
Some(ref p) => TempDir::new_in(p, "uutils_sort")?,
None => TempDir::new("uutils_sort")?,
};
// creating the thing we need to return first due to the face that we need to
// borrow tmp_dir and move it out
let mut iter = ExtSortedIterator {
buffers: Vec::new(),
chunk_offsets: Vec::new(),
max_per_chunk: 0,
chunks: 0,
tmp_dir,
settings,
failed: false,
};
{
let mut total_read = 0;
let mut chunk = Vec::new();
// Initial buffer is specified by user
let mut adjusted_buffer_size = self.buffer_bytes;
let (iter_size, _) = unsorted.size_hint();
// make the initial chunks on disk
for seq in unsorted {
let seq_size = seq.get_size();
total_read += seq_size;
// GNU minimum is 16 * (sizeof struct + 2), but GNU uses about
// 1/10 the memory that we do. And GNU even says in the code it may
// not work on small buffer sizes.
//
// The following seems to work pretty well, and has about the same max
// RSS as lower minimum values.
//
let minimum_buffer_size: u64 = iter_size as u64 * seq_size / 8;
adjusted_buffer_size =
// Grow buffer size for a struct/Line larger than buffer
if adjusted_buffer_size < seq_size {
seq_size
} else if adjusted_buffer_size < minimum_buffer_size {
minimum_buffer_size
} else {
adjusted_buffer_size
};
chunk.push(seq);
if total_read >= adjusted_buffer_size {
super::sort_by(&mut chunk, &self.settings);
self.write_chunk(
&iter.tmp_dir.path().join(iter.chunks.to_string()),
&mut chunk,
)?;
chunk.clear();
total_read = 0;
iter.chunks += 1;
}
}
// write the last chunk
if chunk.len() > 0 {
super::sort_by(&mut chunk, &self.settings);
self.write_chunk(
&iter.tmp_dir.path().join(iter.chunks.to_string()),
&mut chunk,
)?;
iter.chunks += 1;
}
// initialize buffers for each chunk
//
// Having a right sized buffer for each chunk for smallish values seems silly to me?
//
// We will have to have the entire iter in memory sometime right?
// Set minimum to the size of the writer buffer, ~8K
//
const MINIMUM_READBACK_BUFFER: u64 = 8200;
let right_sized_buffer = adjusted_buffer_size
.checked_div(iter.chunks)
.unwrap_or(adjusted_buffer_size);
iter.max_per_chunk = if right_sized_buffer > MINIMUM_READBACK_BUFFER {
right_sized_buffer
} else {
MINIMUM_READBACK_BUFFER
};
iter.buffers = vec![VecDeque::new(); iter.chunks as usize];
iter.chunk_offsets = vec![0 as u64; iter.chunks as usize];
for chunk_num in 0..iter.chunks {
let offset = fill_buff(
&mut iter.buffers[chunk_num as usize],
File::open(iter.tmp_dir.path().join(chunk_num.to_string()))?,
iter.max_per_chunk,
)?;
iter.chunk_offsets[chunk_num as usize] = offset;
}
}
Ok(iter)
}
fn write_chunk(&self, file: &PathBuf, chunk: &mut Vec<Line>) -> Result<(), Box<dyn Error>> {
let new_file = OpenOptions::new().create(true).append(true).open(file)?;
let mut buf_write = Box::new(BufWriter::new(new_file)) as Box<dyn Write>;
for s in chunk {
let mut serialized = serde_json::to_string(&s).expect("JSON write error: ");
serialized.push_str("\n");
buf_write.write(serialized.as_bytes())?;
}
buf_write.flush()?;
Ok(())
}
}
fn fill_buff<Line>(
vec: &mut VecDeque<Line>,
file: File,
max_bytes: u64,
) -> Result<u64, Box<dyn Error>>
where
Line: ExternallySortable,
{
let mut total_read = 0;
let mut bytes_read = 0;
for line in BufReader::new(file).lines() {
let line_s = line?;
bytes_read += line_s.len() + 1;
// This is where the bad stuff happens usually
let deserialized: Line = serde_json::from_str(&line_s).expect("JSON read error: ");
total_read += deserialized.get_size();
vec.push_back(deserialized);
if total_read > max_bytes {
break;
}
}
Ok(bytes_read as u64)
}

224
src/uu/sort/src/merge.rs Normal file
View file

@ -0,0 +1,224 @@
//! Merge already sorted files.
//!
//! We achieve performance by splitting the tasks of sorting and writing, and reading and parsing between two threads.
//! The threads communicate over channels. There's one channel per file in the direction reader -> sorter, but only
//! one channel from the sorter back to the reader. The channels to the sorter are used to send the read chunks.
//! The sorter reads the next chunk from the channel whenever it needs the next chunk after running out of lines
//! from the previous read of the file. The channel back from the sorter to the reader has two purposes: To allow the reader
//! to reuse memory allocations and to tell the reader which file to read from next.
use std::{
cmp::Ordering,
ffi::OsStr,
io::{Read, Write},
iter,
rc::Rc,
sync::mpsc::{channel, sync_channel, Receiver, Sender, SyncSender},
thread,
};
use compare::Compare;
use crate::{
chunks::{self, Chunk},
compare_by, open, GlobalSettings,
};
// Merge already sorted files.
pub fn merge<'a>(files: &[impl AsRef<OsStr>], settings: &'a GlobalSettings) -> FileMerger<'a> {
let (request_sender, request_receiver) = channel();
let mut reader_files = Vec::with_capacity(files.len());
let mut loaded_receivers = Vec::with_capacity(files.len());
for (file_number, file) in files.iter().filter_map(open).enumerate() {
let (sender, receiver) = sync_channel(2);
loaded_receivers.push(receiver);
reader_files.push(ReaderFile {
file,
sender: Some(sender),
carry_over: vec![],
});
request_sender
.send((file_number, Chunk::new(vec![0; 8 * 1024], |_| Vec::new())))
.unwrap();
}
for file_number in 0..reader_files.len() {
request_sender
.send((file_number, Chunk::new(vec![0; 8 * 1024], |_| Vec::new())))
.unwrap();
}
thread::spawn({
let settings = settings.clone();
move || {
reader(
request_receiver,
&mut reader_files,
&settings,
if settings.zero_terminated {
b'\0'
} else {
b'\n'
},
)
}
});
let mut mergeable_files = vec![];
for (file_number, receiver) in loaded_receivers.into_iter().enumerate() {
mergeable_files.push(MergeableFile {
current_chunk: Rc::new(receiver.recv().unwrap()),
file_number,
line_idx: 0,
receiver,
})
}
FileMerger {
heap: binary_heap_plus::BinaryHeap::from_vec_cmp(
mergeable_files,
FileComparator { settings },
),
request_sender,
prev: None,
}
}
/// The struct on the reader thread representing an input file
struct ReaderFile {
file: Box<dyn Read + Send>,
sender: Option<SyncSender<Chunk>>,
carry_over: Vec<u8>,
}
/// The function running on the reader thread.
fn reader(
recycled_receiver: Receiver<(usize, Chunk)>,
files: &mut [ReaderFile],
settings: &GlobalSettings,
separator: u8,
) {
for (file_idx, chunk) in recycled_receiver.iter() {
let (recycled_lines, recycled_buffer) = chunk.recycle();
let ReaderFile {
file,
sender,
carry_over,
} = &mut files[file_idx];
chunks::read(
sender,
recycled_buffer,
None,
carry_over,
file,
&mut iter::empty(),
separator,
recycled_lines,
settings,
);
}
}
/// The struct on the main thread representing an input file
pub struct MergeableFile {
current_chunk: Rc<Chunk>,
line_idx: usize,
receiver: Receiver<Chunk>,
file_number: usize,
}
/// A struct to keep track of the previous line we encountered.
///
/// This is required for deduplication purposes.
struct PreviousLine {
chunk: Rc<Chunk>,
line_idx: usize,
file_number: usize,
}
/// Merges files together. This is **not** an iterator because of lifetime problems.
pub struct FileMerger<'a> {
heap: binary_heap_plus::BinaryHeap<MergeableFile, FileComparator<'a>>,
request_sender: Sender<(usize, Chunk)>,
prev: Option<PreviousLine>,
}
impl<'a> FileMerger<'a> {
/// Write the merged contents to the output file.
pub fn write_all(&mut self, settings: &GlobalSettings) {
let mut out = settings.out_writer();
while self.write_next(settings, &mut out) {}
}
fn write_next(&mut self, settings: &GlobalSettings, out: &mut impl Write) -> bool {
if let Some(file) = self.heap.peek() {
let prev = self.prev.replace(PreviousLine {
chunk: file.current_chunk.clone(),
line_idx: file.line_idx,
file_number: file.file_number,
});
file.current_chunk.with_lines(|lines| {
let current_line = &lines[file.line_idx];
if settings.unique {
if let Some(prev) = &prev {
let cmp = compare_by(
&prev.chunk.borrow_lines()[prev.line_idx],
current_line,
settings,
);
if cmp == Ordering::Equal {
return;
}
}
}
current_line.print(out, settings);
});
let was_last_line_for_file =
file.current_chunk.borrow_lines().len() == file.line_idx + 1;
if was_last_line_for_file {
if let Ok(next_chunk) = file.receiver.recv() {
let mut file = self.heap.peek_mut().unwrap();
file.current_chunk = Rc::new(next_chunk);
file.line_idx = 0;
} else {
self.heap.pop();
}
} else {
self.heap.peek_mut().unwrap().line_idx += 1;
}
if let Some(prev) = prev {
if let Ok(prev_chunk) = Rc::try_unwrap(prev.chunk) {
self.request_sender
.send((prev.file_number, prev_chunk))
.ok();
}
}
}
!self.heap.is_empty()
}
}
/// Compares files by their current line.
struct FileComparator<'a> {
settings: &'a GlobalSettings,
}
impl<'a> Compare<MergeableFile> for FileComparator<'a> {
fn compare(&self, a: &MergeableFile, b: &MergeableFile) -> Ordering {
let mut cmp = compare_by(
&a.current_chunk.borrow_lines()[a.line_idx],
&b.current_chunk.borrow_lines()[b.line_idx],
self.settings,
);
if cmp == Ordering::Equal {
// To make sorting stable, we need to consider the file number as well,
// as lines from a file with a lower number are to be considered "earlier".
cmp = a.file_number.cmp(&b.file_number);
}
// Our BinaryHeap is a max heap. We use it as a min heap, so we need to reverse the ordering.
cmp.reverse()
}
}

View file

@ -14,21 +14,20 @@
//! More specifically, exponent can be understood so that the original number is in (1..10)*10^exponent. //! More specifically, exponent can be understood so that the original number is in (1..10)*10^exponent.
//! From that follows the constraints of this algorithm: It is able to compare numbers in ±(1*10^[i64::MIN]..10*10^[i64::MAX]). //! From that follows the constraints of this algorithm: It is able to compare numbers in ±(1*10^[i64::MIN]..10*10^[i64::MAX]).
use serde::{Deserialize, Serialize};
use std::{cmp::Ordering, ops::Range}; use std::{cmp::Ordering, ops::Range};
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Clone)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
enum Sign { enum Sign {
Negative, Negative,
Positive, Positive,
} }
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[derive(Debug, PartialEq, Clone)]
pub struct NumInfo { pub struct NumInfo {
exponent: i64, exponent: i64,
sign: Sign, sign: Sign,
} }
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[derive(Debug, PartialEq, Clone)]
pub struct NumInfoParseSettings { pub struct NumInfoParseSettings {
pub accept_si_units: bool, pub accept_si_units: bool,
pub thousands_separator: Option<char>, pub thousands_separator: Option<char>,

File diff suppressed because it is too large Load diff

View file

@ -13,11 +13,11 @@ extern crate uucore;
mod platform; mod platform;
use clap::{App, Arg}; use clap::{App, Arg};
use std::char;
use std::env; use std::env;
use std::fs::File; use std::fs::File;
use std::io::{stdin, stdout, BufRead, BufReader, BufWriter, Read, Write}; use std::io::{stdin, BufRead, BufReader, BufWriter, Read, Write};
use std::path::Path; use std::path::Path;
use std::{char, fs::remove_file};
static NAME: &str = "split"; static NAME: &str = "split";
static VERSION: &str = env!("CARGO_PKG_VERSION"); static VERSION: &str = env!("CARGO_PKG_VERSION");
@ -213,107 +213,145 @@ struct Settings {
verbose: bool, verbose: bool,
} }
struct SplitControl {
current_line: String, // Don't touch
request_new_file: bool, // Splitter implementation requests new file
}
trait Splitter { trait Splitter {
// Consume the current_line and return the consumed string // Consume as much as possible from `reader` so as to saturate `writer`.
fn consume(&mut self, _: &mut SplitControl) -> String; // Equivalent to finishing one of the part files. Returns the number of
// bytes that have been moved.
fn consume(
&mut self,
reader: &mut BufReader<Box<dyn Read>>,
writer: &mut BufWriter<Box<dyn Write>>,
) -> u128;
} }
struct LineSplitter { struct LineSplitter {
saved_lines_to_write: usize, lines_per_split: usize,
lines_to_write: usize,
} }
impl LineSplitter { impl LineSplitter {
fn new(settings: &Settings) -> LineSplitter { fn new(settings: &Settings) -> LineSplitter {
let n = match settings.strategy_param.parse() {
Ok(a) => a,
Err(e) => crash!(1, "invalid number of lines: {}", e),
};
LineSplitter { LineSplitter {
saved_lines_to_write: n, lines_per_split: settings
lines_to_write: n, .strategy_param
.parse()
.unwrap_or_else(|e| crash!(1, "invalid number of lines: {}", e)),
} }
} }
} }
impl Splitter for LineSplitter { impl Splitter for LineSplitter {
fn consume(&mut self, control: &mut SplitControl) -> String { fn consume(
self.lines_to_write -= 1; &mut self,
if self.lines_to_write == 0 { reader: &mut BufReader<Box<dyn Read>>,
self.lines_to_write = self.saved_lines_to_write; writer: &mut BufWriter<Box<dyn Write>>,
control.request_new_file = true; ) -> u128 {
let mut bytes_consumed = 0u128;
let mut buffer = String::with_capacity(1024);
for _ in 0..self.lines_per_split {
let bytes_read = reader
.read_line(&mut buffer)
.unwrap_or_else(|_| crash!(1, "error reading bytes from input file"));
// If we ever read 0 bytes then we know we've hit EOF.
if bytes_read == 0 {
return bytes_consumed;
}
writer
.write_all(buffer.as_bytes())
.unwrap_or_else(|_| crash!(1, "error writing bytes to output file"));
// Empty out the String buffer since `read_line` appends instead of
// replaces.
buffer.clear();
bytes_consumed += bytes_read as u128;
} }
control.current_line.clone()
bytes_consumed
} }
} }
struct ByteSplitter { struct ByteSplitter {
saved_bytes_to_write: usize, bytes_per_split: u128,
bytes_to_write: usize,
break_on_line_end: bool,
require_whole_line: bool,
} }
impl ByteSplitter { impl ByteSplitter {
fn new(settings: &Settings) -> ByteSplitter { fn new(settings: &Settings) -> ByteSplitter {
let mut strategy_param: Vec<char> = settings.strategy_param.chars().collect(); // These multipliers are the same as supported by GNU coreutils.
let suffix = strategy_param.pop().unwrap(); let modifiers: Vec<(&str, u128)> = vec![
let multiplier = match suffix { ("K", 1024u128),
'0'..='9' => 1usize, ("M", 1024 * 1024),
'b' => 512usize, ("G", 1024 * 1024 * 1024),
'k' => 1024usize, ("T", 1024 * 1024 * 1024 * 1024),
'm' => 1024usize * 1024usize, ("P", 1024 * 1024 * 1024 * 1024 * 1024),
_ => crash!(1, "invalid number of bytes"), ("E", 1024 * 1024 * 1024 * 1024 * 1024 * 1024),
}; ("Z", 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024),
let n = if suffix.is_alphabetic() { ("Y", 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024),
match strategy_param ("KB", 1000),
.iter() ("MB", 1000 * 1000),
.cloned() ("GB", 1000 * 1000 * 1000),
.collect::<String>() ("TB", 1000 * 1000 * 1000 * 1000),
.parse::<usize>() ("PB", 1000 * 1000 * 1000 * 1000 * 1000),
{ ("EB", 1000 * 1000 * 1000 * 1000 * 1000 * 1000),
Ok(a) => a, ("ZB", 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000),
Err(e) => crash!(1, "invalid number of bytes: {}", e), ("YB", 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000),
} ];
} else {
match settings.strategy_param.parse::<usize>() { // This sequential find is acceptable since none of the modifiers are
Ok(a) => a, // suffixes of any other modifiers, a la Huffman codes.
Err(e) => crash!(1, "invalid number of bytes: {}", e), let (suffix, multiplier) = modifiers
} .iter()
}; .find(|(suffix, _)| settings.strategy_param.ends_with(suffix))
.unwrap_or(&("", 1));
// Try to parse the actual numeral.
let n = &settings.strategy_param[0..(settings.strategy_param.len() - suffix.len())]
.parse::<u128>()
.unwrap_or_else(|e| crash!(1, "invalid number of bytes: {}", e));
ByteSplitter { ByteSplitter {
saved_bytes_to_write: n * multiplier, bytes_per_split: n * multiplier,
bytes_to_write: n * multiplier,
break_on_line_end: settings.strategy == "b",
require_whole_line: false,
} }
} }
} }
impl Splitter for ByteSplitter { impl Splitter for ByteSplitter {
fn consume(&mut self, control: &mut SplitControl) -> String { fn consume(
let line = control.current_line.clone(); &mut self,
let n = std::cmp::min(line.chars().count(), self.bytes_to_write); reader: &mut BufReader<Box<dyn Read>>,
if self.require_whole_line && n < line.chars().count() { writer: &mut BufWriter<Box<dyn Write>>,
self.bytes_to_write = self.saved_bytes_to_write; ) -> u128 {
control.request_new_file = true; // We buffer reads and writes. We proceed until `bytes_consumed` is
self.require_whole_line = false; // equal to `self.bytes_per_split` or we reach EOF.
return "".to_owned(); let mut bytes_consumed = 0u128;
const BUFFER_SIZE: usize = 1024;
let mut buffer = [0u8; BUFFER_SIZE];
while bytes_consumed < self.bytes_per_split {
// Don't overshoot `self.bytes_per_split`! Note: Using std::cmp::min
// doesn't really work since we have to get types to match which
// can't be done in a way that keeps all conversions safe.
let bytes_desired = if (BUFFER_SIZE as u128) <= self.bytes_per_split - bytes_consumed {
BUFFER_SIZE
} else {
// This is a safe conversion since the difference must be less
// than BUFFER_SIZE in this branch.
(self.bytes_per_split - bytes_consumed) as usize
};
let bytes_read = reader
.read(&mut buffer[0..bytes_desired])
.unwrap_or_else(|_| crash!(1, "error reading bytes from input file"));
// If we ever read 0 bytes then we know we've hit EOF.
if bytes_read == 0 {
return bytes_consumed;
}
writer
.write_all(&buffer[0..bytes_read])
.unwrap_or_else(|_| crash!(1, "error writing bytes to output file"));
bytes_consumed += bytes_read as u128;
} }
self.bytes_to_write -= n;
if n == 0 { bytes_consumed
self.bytes_to_write = self.saved_bytes_to_write;
control.request_new_file = true;
}
if self.break_on_line_end && n == line.chars().count() {
self.require_whole_line = self.break_on_line_end;
}
line[..n].to_owned()
} }
} }
@ -353,14 +391,13 @@ fn split(settings: &Settings) -> i32 {
let mut reader = BufReader::new(if settings.input == "-" { let mut reader = BufReader::new(if settings.input == "-" {
Box::new(stdin()) as Box<dyn Read> Box::new(stdin()) as Box<dyn Read>
} else { } else {
let r = match File::open(Path::new(&settings.input)) { let r = File::open(Path::new(&settings.input)).unwrap_or_else(|_| {
Ok(a) => a, crash!(
Err(_) => crash!(
1, 1,
"cannot open '{}' for reading: No such file or directory", "cannot open '{}' for reading: No such file or directory",
settings.input settings.input
), )
}; });
Box::new(r) as Box<dyn Read> Box::new(r) as Box<dyn Read>
}); });
@ -370,48 +407,39 @@ fn split(settings: &Settings) -> i32 {
a => crash!(1, "strategy {} not supported", a), a => crash!(1, "strategy {} not supported", a),
}; };
let mut control = SplitControl {
current_line: "".to_owned(), // Request new line
request_new_file: true, // Request new file
};
let mut writer = BufWriter::new(Box::new(stdout()) as Box<dyn Write>);
let mut fileno = 0; let mut fileno = 0;
loop { loop {
if control.current_line.chars().count() == 0 { // Get a new part file set up, and construct `writer` for it.
match reader.read_line(&mut control.current_line) { let mut filename = settings.prefix.clone();
Ok(0) | Err(_) => break, filename.push_str(
_ => {} if settings.numeric_suffix {
num_prefix(fileno, settings.suffix_length)
} else {
str_prefix(fileno, settings.suffix_length)
} }
} .as_ref(),
if control.request_new_file { );
let mut filename = settings.prefix.clone(); filename.push_str(settings.additional_suffix.as_ref());
filename.push_str( let mut writer = platform::instantiate_current_writer(&settings.filter, filename.as_str());
if settings.numeric_suffix {
num_prefix(fileno, settings.suffix_length)
} else {
str_prefix(fileno, settings.suffix_length)
}
.as_ref(),
);
filename.push_str(settings.additional_suffix.as_ref());
crash_if_err!(1, writer.flush()); let bytes_consumed = splitter.consume(&mut reader, &mut writer);
fileno += 1; writer
writer = platform::instantiate_current_writer(&settings.filter, filename.as_str()); .flush()
control.request_new_file = false; .unwrap_or_else(|e| crash!(1, "error flushing to output file: {}", e));
if settings.verbose {
println!("creating file '{}'", filename); // If we didn't write anything we should clean up the empty file, and
// break from the loop.
if bytes_consumed == 0 {
// The output file is only ever created if --filter isn't used.
// Complicated, I know...
if settings.filter.is_none() {
remove_file(filename)
.unwrap_or_else(|e| crash!(1, "error removing empty file: {}", e));
} }
break;
} }
let consumed = splitter.consume(&mut control); fileno += 1;
crash_if_err!(1, writer.write_all(consumed.as_bytes()));
let advance = consumed.chars().count();
let clone = control.current_line.clone();
let sl = clone;
control.current_line = sl[advance..sl.chars().count()].to_owned();
} }
0 0
} }

View file

@ -16,8 +16,7 @@ path = "src/stat.rs"
[dependencies] [dependencies]
clap = "2.33" clap = "2.33"
time = "0.1.40" uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["entries", "libc", "fs", "fsext"] }
uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["entries", "libc"] }
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }
[[bin]] [[bin]]

View file

@ -1,415 +0,0 @@
// This file is part of the uutils coreutils package.
//
// (c) Jian Zeng <anonymousknight96@gmail.com>
//
// For the full copyright and license information, please view the LICENSE file
// that was distributed with this source code.
// spell-checker:ignore (ToDO) strerror IFBLK IFCHR IFDIR IFLNK IFIFO IFMT IFREG IFSOCK subsec nanos gnulib statfs Sstatfs bitrig statvfs iosize blksize fnodes fsid namelen bsize bfree bavail ffree frsize namemax errno fstype adfs acfs aufs affs autofs befs bdevfs binfmt ceph cgroups cifs configfs cramfs cgroupfs debugfs devfs devpts ecryptfs btrfs efivarfs exofs fhgfs fuseblk fusectl futexfs gpfs hfsx hostfs hpfs inodefs ibrix inotifyfs isofs jffs logfs hugetlbfs mqueue nsfs ntfs ocfs panfs pipefs ramfs romfs nfsd nilfs pstorefs reiserfs securityfs smackfs snfs sockfs squashfs sysfs sysv tempfs tracefs ubifs usbdevfs vmhgfs tmpfs vxfs wslfs xenfs vzfs openprom overlayfs
extern crate time;
use self::time::Timespec;
use std::time::UNIX_EPOCH;
pub use uucore::libc::{
c_int, mode_t, strerror, S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFMT, S_IFREG,
S_IFSOCK, S_IRGRP, S_IROTH, S_IRUSR, S_ISGID, S_ISUID, S_ISVTX, S_IWGRP, S_IWOTH, S_IWUSR,
S_IXGRP, S_IXOTH, S_IXUSR,
};
pub trait BirthTime {
fn pretty_birth(&self) -> String;
fn birth(&self) -> String;
}
use std::fs::Metadata;
impl BirthTime for Metadata {
fn pretty_birth(&self) -> String {
self.created()
.ok()
.and_then(|t| t.duration_since(UNIX_EPOCH).ok())
.map(|e| pretty_time(e.as_secs() as i64, i64::from(e.subsec_nanos())))
.unwrap_or_else(|| "-".to_owned())
}
fn birth(&self) -> String {
self.created()
.ok()
.and_then(|t| t.duration_since(UNIX_EPOCH).ok())
.map(|e| format!("{}", e.as_secs()))
.unwrap_or_else(|| "0".to_owned())
}
}
#[macro_export]
macro_rules! has {
($mode:expr, $perm:expr) => {
$mode & $perm != 0
};
}
pub fn pretty_time(sec: i64, nsec: i64) -> String {
// sec == seconds since UNIX_EPOCH
// nsec == nanoseconds since (UNIX_EPOCH + sec)
let tm = time::at(Timespec::new(sec, nsec as i32));
let res = time::strftime("%Y-%m-%d %H:%M:%S.%f %z", &tm).unwrap();
if res.ends_with(" -0000") {
res.replace(" -0000", " +0000")
} else {
res
}
}
pub fn pretty_filetype<'a>(mode: mode_t, size: u64) -> &'a str {
match mode & S_IFMT {
S_IFREG => {
if size != 0 {
"regular file"
} else {
"regular empty file"
}
}
S_IFDIR => "directory",
S_IFLNK => "symbolic link",
S_IFCHR => "character special file",
S_IFBLK => "block special file",
S_IFIFO => "fifo",
S_IFSOCK => "socket",
// TODO: Other file types
// See coreutils/gnulib/lib/file-type.c
_ => "weird file",
}
}
pub fn pretty_access(mode: mode_t) -> String {
let mut result = String::with_capacity(10);
result.push(match mode & S_IFMT {
S_IFDIR => 'd',
S_IFCHR => 'c',
S_IFBLK => 'b',
S_IFREG => '-',
S_IFIFO => 'p',
S_IFLNK => 'l',
S_IFSOCK => 's',
// TODO: Other file types
_ => '?',
});
result.push(if has!(mode, S_IRUSR) { 'r' } else { '-' });
result.push(if has!(mode, S_IWUSR) { 'w' } else { '-' });
result.push(if has!(mode, S_ISUID as mode_t) {
if has!(mode, S_IXUSR) {
's'
} else {
'S'
}
} else if has!(mode, S_IXUSR) {
'x'
} else {
'-'
});
result.push(if has!(mode, S_IRGRP) { 'r' } else { '-' });
result.push(if has!(mode, S_IWGRP) { 'w' } else { '-' });
result.push(if has!(mode, S_ISGID as mode_t) {
if has!(mode, S_IXGRP) {
's'
} else {
'S'
}
} else if has!(mode, S_IXGRP) {
'x'
} else {
'-'
});
result.push(if has!(mode, S_IROTH) { 'r' } else { '-' });
result.push(if has!(mode, S_IWOTH) { 'w' } else { '-' });
result.push(if has!(mode, S_ISVTX as mode_t) {
if has!(mode, S_IXOTH) {
't'
} else {
'T'
}
} else if has!(mode, S_IXOTH) {
'x'
} else {
'-'
});
result
}
use std::borrow::Cow;
use std::convert::{AsRef, From};
use std::ffi::CString;
use std::io::Error as IOError;
use std::mem;
use std::path::Path;
#[cfg(any(
target_os = "linux",
target_vendor = "apple",
target_os = "android",
target_os = "freebsd"
))]
use uucore::libc::statfs as Sstatfs;
#[cfg(any(
target_os = "openbsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "bitrig",
target_os = "dragonfly"
))]
use uucore::libc::statvfs as Sstatfs;
#[cfg(any(
target_os = "linux",
target_vendor = "apple",
target_os = "android",
target_os = "freebsd"
))]
use uucore::libc::statfs as statfs_fn;
#[cfg(any(
target_os = "openbsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "bitrig",
target_os = "dragonfly"
))]
use uucore::libc::statvfs as statfs_fn;
pub trait FsMeta {
fn fs_type(&self) -> i64;
fn iosize(&self) -> u64;
fn blksize(&self) -> i64;
fn total_blocks(&self) -> u64;
fn free_blocks(&self) -> u64;
fn avail_blocks(&self) -> u64;
fn total_fnodes(&self) -> u64;
fn free_fnodes(&self) -> u64;
fn fsid(&self) -> u64;
fn namelen(&self) -> u64;
}
impl FsMeta for Sstatfs {
fn blksize(&self) -> i64 {
self.f_bsize as i64
}
fn total_blocks(&self) -> u64 {
self.f_blocks as u64
}
fn free_blocks(&self) -> u64 {
self.f_bfree as u64
}
fn avail_blocks(&self) -> u64 {
self.f_bavail as u64
}
fn total_fnodes(&self) -> u64 {
self.f_files as u64
}
fn free_fnodes(&self) -> u64 {
self.f_ffree as u64
}
#[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_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!()
}
#[cfg(target_os = "linux")]
fn iosize(&self) -> u64 {
self.f_frsize as u64
}
#[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_vendor = "apple", target_os = "freebsd", target_os = "linux")))]
fn iosize(&self) -> u64 {
self.f_bsize as u64
}
// Linux, SunOS, HP-UX, 4.4BSD, FreeBSD have a system call statfs() that returns
// a struct statfs, containing a fsid_t f_fsid, where fsid_t is defined
// as struct { int val[2]; }
//
// Solaris, Irix and POSIX have a system call statvfs(2) that returns a
// struct statvfs, containing an unsigned long f_fsid
#[cfg(any(target_vendor = "apple", target_os = "freebsd", target_os = "linux"))]
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_vendor = "apple", target_os = "freebsd", target_os = "linux")))]
fn fsid(&self) -> u64 {
self.f_fsid as u64
}
#[cfg(target_os = "linux")]
fn namelen(&self) -> u64 {
self.f_namelen as u64
}
#[cfg(target_vendor = "apple")]
fn namelen(&self) -> u64 {
1024
}
#[cfg(target_os = "freebsd")]
fn namelen(&self) -> u64 {
self.f_namemax as u64
}
// XXX: should everything just use statvfs?
#[cfg(not(any(target_vendor = "apple", target_os = "freebsd", target_os = "linux")))]
fn namelen(&self) -> u64 {
self.f_namemax as u64
}
}
pub fn statfs<P: AsRef<Path>>(path: P) -> Result<Sstatfs, String>
where
Vec<u8>: From<P>,
{
match CString::new(path) {
Ok(p) => {
let mut buffer: Sstatfs = unsafe { mem::zeroed() };
unsafe {
match statfs_fn(p.as_ptr(), &mut buffer) {
0 => Ok(buffer),
_ => {
let errno = IOError::last_os_error().raw_os_error().unwrap_or(0);
Err(CString::from_raw(strerror(errno))
.into_string()
.unwrap_or_else(|_| "Unknown Error".to_owned()))
}
}
}
}
Err(e) => Err(e.to_string()),
}
}
pub fn pretty_fstype<'a>(fstype: i64) -> Cow<'a, str> {
match fstype {
0x6163_6673 => "acfs".into(),
0xADF5 => "adfs".into(),
0xADFF => "affs".into(),
0x5346_414F => "afs".into(),
0x0904_1934 => "anon-inode FS".into(),
0x6175_6673 => "aufs".into(),
0x0187 => "autofs".into(),
0x4246_5331 => "befs".into(),
0x6264_6576 => "bdevfs".into(),
0x1BAD_FACE => "bfs".into(),
0xCAFE_4A11 => "bpf_fs".into(),
0x4249_4E4D => "binfmt_misc".into(),
0x9123_683E => "btrfs".into(),
0x7372_7279 => "btrfs_test".into(),
0x00C3_6400 => "ceph".into(),
0x0027_E0EB => "cgroupfs".into(),
0xFF53_4D42 => "cifs".into(),
0x7375_7245 => "coda".into(),
0x012F_F7B7 => "coh".into(),
0x6265_6570 => "configfs".into(),
0x28CD_3D45 => "cramfs".into(),
0x453D_CD28 => "cramfs-wend".into(),
0x6462_6720 => "debugfs".into(),
0x1373 => "devfs".into(),
0x1CD1 => "devpts".into(),
0xF15F => "ecryptfs".into(),
0xDE5E_81E4 => "efivarfs".into(),
0x0041_4A53 => "efs".into(),
0x5DF5 => "exofs".into(),
0x137D => "ext".into(),
0xEF53 => "ext2/ext3".into(),
0xEF51 => "ext2".into(),
0xF2F5_2010 => "f2fs".into(),
0x4006 => "fat".into(),
0x1983_0326 => "fhgfs".into(),
0x6573_5546 => "fuseblk".into(),
0x6573_5543 => "fusectl".into(),
0x0BAD_1DEA => "futexfs".into(),
0x0116_1970 => "gfs/gfs2".into(),
0x4750_4653 => "gpfs".into(),
0x4244 => "hfs".into(),
0x482B => "hfs+".into(),
0x4858 => "hfsx".into(),
0x00C0_FFEE => "hostfs".into(),
0xF995_E849 => "hpfs".into(),
0x9584_58F6 => "hugetlbfs".into(),
0x1130_7854 => "inodefs".into(),
0x0131_11A8 => "ibrix".into(),
0x2BAD_1DEA => "inotifyfs".into(),
0x9660 => "isofs".into(),
0x4004 => "isofs".into(),
0x4000 => "isofs".into(),
0x07C0 => "jffs".into(),
0x72B6 => "jffs2".into(),
0x3153_464A => "jfs".into(),
0x6B41_4653 => "k-afs".into(),
0xC97E_8168 => "logfs".into(),
0x0BD0_0BD0 => "lustre".into(),
0x5346_314D => "m1fs".into(),
0x137F => "minix".into(),
0x138F => "minix (30 char.)".into(),
0x2468 => "minix v2".into(),
0x2478 => "minix v2 (30 char.)".into(),
0x4D5A => "minix3".into(),
0x1980_0202 => "mqueue".into(),
0x4D44 => "msdos".into(),
0x564C => "novell".into(),
0x6969 => "nfs".into(),
0x6E66_7364 => "nfsd".into(),
0x3434 => "nilfs".into(),
0x6E73_6673 => "nsfs".into(),
0x5346_544E => "ntfs".into(),
0x9FA1 => "openprom".into(),
0x7461_636F => "ocfs2".into(),
0x794C_7630 => "overlayfs".into(),
0xAAD7_AAEA => "panfs".into(),
0x5049_5045 => "pipefs".into(),
0x7C7C_6673 => "prl_fs".into(),
0x9FA0 => "proc".into(),
0x6165_676C => "pstorefs".into(),
0x002F => "qnx4".into(),
0x6819_1122 => "qnx6".into(),
0x8584_58F6 => "ramfs".into(),
0x5265_4973 => "reiserfs".into(),
0x7275 => "romfs".into(),
0x6759_6969 => "rpc_pipefs".into(),
0x7363_6673 => "securityfs".into(),
0xF97C_FF8C => "selinux".into(),
0x4341_5D53 => "smackfs".into(),
0x517B => "smb".into(),
0xFE53_4D42 => "smb2".into(),
0xBEEF_DEAD => "snfs".into(),
0x534F_434B => "sockfs".into(),
0x7371_7368 => "squashfs".into(),
0x6265_6572 => "sysfs".into(),
0x012F_F7B6 => "sysv2".into(),
0x012F_F7B5 => "sysv4".into(),
0x0102_1994 => "tmpfs".into(),
0x7472_6163 => "tracefs".into(),
0x2405_1905 => "ubifs".into(),
0x1501_3346 => "udf".into(),
0x0001_1954 => "ufs".into(),
0x5419_0100 => "ufs".into(),
0x9FA2 => "usbdevfs".into(),
0x0102_1997 => "v9fs".into(),
0xBACB_ACBC => "vmhgfs".into(),
0xA501_FCF5 => "vxfs".into(),
0x565A_4653 => "vzfs".into(),
0x5346_4846 => "wslfs".into(),
0xABBA_1974 => "xenfs".into(),
0x012F_F7B4 => "xenix".into(),
0x5846_5342 => "xfs".into(),
0x012F_D16D => "xia".into(),
0x2FC1_2FC1 => "zfs".into(),
other => format!("UNKNOWN ({:#x})", other).into(),
}
}

View file

@ -5,21 +5,20 @@
// For the full copyright and license information, please view the LICENSE file // For the full copyright and license information, please view the LICENSE file
// that was distributed with this source code. // that was distributed with this source code.
// spell-checker:ignore (ToDO) mtab fsext showfs otype fmtstr prec ftype blocksize nlink rdev fnodes fsid namelen blksize inodes fstype iosize statfs gnulib NBLOCKSIZE // spell-checker:ignore (ToDO) showfs otype fmtstr prec ftype blocksize nlink rdev fnodes fsid namelen blksize inodes fstype iosize statfs gnulib NBLOCKSIZE
#[macro_use]
mod fsext;
pub use crate::fsext::*;
#[macro_use] #[macro_use]
extern crate uucore; extern crate uucore;
use uucore::entries; use uucore::entries;
use uucore::fs::display_permissions;
use uucore::fsext::{
pretty_filetype, pretty_fstype, pretty_time, read_fs_list, statfs, BirthTime, FsMeta,
};
use uucore::libc::mode_t;
use clap::{App, Arg, ArgMatches}; use clap::{App, Arg, ArgMatches};
use std::borrow::Cow; use std::borrow::Cow;
use std::convert::AsRef; use std::convert::AsRef;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::os::unix::fs::{FileTypeExt, MetadataExt}; use std::os::unix::fs::{FileTypeExt, MetadataExt};
use std::path::Path; use std::path::Path;
use std::{cmp, fs, iter}; use std::{cmp, fs, iter};
@ -97,7 +96,6 @@ pub mod options {
static ARG_FILES: &str = "files"; static ARG_FILES: &str = "files";
const MOUNT_INFO: &str = "/etc/mtab";
pub const F_ALTER: u8 = 1; pub const F_ALTER: u8 = 1;
pub const F_ZERO: u8 = 1 << 1; pub const F_ZERO: u8 = 1 << 1;
pub const F_LEFT: u8 = 1 << 2; pub const F_LEFT: u8 = 1 << 2;
@ -490,13 +488,9 @@ impl Stater {
// mount points aren't displayed when showing filesystem information // mount points aren't displayed when showing filesystem information
None None
} else { } else {
let reader = BufReader::new( let mut mount_list = read_fs_list()
File::open(MOUNT_INFO).unwrap_or_else(|_| panic!("Failed to read {}", MOUNT_INFO)), .iter()
); .map(|mi| mi.mount_dir.clone())
let mut mount_list = reader
.lines()
.filter_map(Result::ok)
.filter_map(|line| line.split_whitespace().nth(1).map(ToOwned::to_owned))
.collect::<Vec<String>>(); .collect::<Vec<String>>();
// Reverse sort. The longer comes first. // Reverse sort. The longer comes first.
mount_list.sort(); mount_list.sort();
@ -575,7 +569,7 @@ impl Stater {
} }
// access rights in human readable form // access rights in human readable form
'A' => { 'A' => {
arg = pretty_access(meta.mode() as mode_t); arg = display_permissions(&meta, true);
otype = OutputType::Str; otype = OutputType::Str;
} }
// number of blocks allocated (see %B) // number of blocks allocated (see %B)
@ -663,7 +657,7 @@ impl Stater {
dst.to_string_lossy() dst.to_string_lossy()
); );
} else { } else {
arg = format!("`{}'", file); arg = file.to_string();
} }
otype = OutputType::Str; otype = OutputType::Str;
} }
@ -755,7 +749,7 @@ impl Stater {
} }
} }
Err(e) => { Err(e) => {
show_info!("cannot stat '{}': {}", file, e); show_error!("cannot stat '{}': {}", file, e);
return 1; return 1;
} }
} }
@ -848,7 +842,7 @@ impl Stater {
} }
} }
Err(e) => { Err(e) => {
show_info!("cannot read file system information for '{}': {}", file, e); show_error!("cannot read file system information for '{}': {}", file, e);
return 1; return 1;
} }
} }
@ -1007,7 +1001,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
match Stater::new(matches) { match Stater::new(matches) {
Ok(stater) => stater.exec(), Ok(stater) => stater.exec(),
Err(e) => { Err(e) => {
show_info!("{}", e); show_error!("{}", e);
1 1
} }
} }

View file

@ -1,76 +0,0 @@
// spell-checker:ignore (ToDO) scanutil qzxc dqzxc
pub use super::*;
#[test]
fn test_scanutil() {
assert_eq!(Some((-5, 2)), "-5zxc".scan_num::<i32>());
assert_eq!(Some((51, 2)), "51zxc".scan_num::<u32>());
assert_eq!(Some((192, 4)), "+192zxc".scan_num::<i32>());
assert_eq!(None, "z192zxc".scan_num::<i32>());
assert_eq!(Some(('a', 3)), "141zxc".scan_char(8));
assert_eq!(Some(('\n', 2)), "12qzxc".scan_char(8));
assert_eq!(Some(('\r', 1)), "dqzxc".scan_char(16));
assert_eq!(None, "z2qzxc".scan_char(8));
}
#[cfg(test)]
mod test_generate_tokens {
use super::*;
#[test]
fn test_normal_format() {
let s = "%10.2ac%-5.w\n";
let expected = vec![
Token::Directive {
flag: 0,
width: 10,
precision: 2,
format: 'a',
},
Token::Char('c'),
Token::Directive {
flag: F_LEFT,
width: 5,
precision: 0,
format: 'w',
},
Token::Char('\n'),
];
assert_eq!(&expected, &Stater::generate_tokens(s, false).unwrap());
}
#[test]
fn test_printf_format() {
let s = "%-# 15a\\r\\\"\\\\\\a\\b\\e\\f\\v%+020.-23w\\x12\\167\\132\\112\\n";
let expected = vec![
Token::Directive {
flag: F_LEFT | F_ALTER | F_SPACE,
width: 15,
precision: -1,
format: 'a',
},
Token::Char('\r'),
Token::Char('"'),
Token::Char('\\'),
Token::Char('\x07'),
Token::Char('\x08'),
Token::Char('\x1B'),
Token::Char('\x0C'),
Token::Char('\x0B'),
Token::Directive {
flag: F_SIGN | F_ZERO,
width: 20,
precision: -1,
format: 'w',
},
Token::Char('\x12'),
Token::Char('w'),
Token::Char('Z'),
Token::Char('J'),
Token::Char('\n'),
];
assert_eq!(&expected, &Stater::generate_tokens(s, true).unwrap());
}
}

View file

@ -24,18 +24,19 @@ use uucore::InvalidEncodingHandling;
static VERSION: &str = env!("CARGO_PKG_VERSION"); static VERSION: &str = env!("CARGO_PKG_VERSION");
static ABOUT: &str = static ABOUT: &str =
"Run COMMAND, with modified buffering operations for its standard streams.\n\n\ "Run COMMAND, with modified buffering operations for its standard streams.\n\n\
Mandatory arguments to long options are mandatory for short options too."; Mandatory arguments to long options are mandatory for short options too.";
static LONG_HELP: &str = "If MODE is 'L' the corresponding stream will be line buffered.\n\ static LONG_HELP: &str =
This option is invalid with standard input.\n\n\ "If MODE is 'L' the corresponding stream will be line buffered.\n\
If MODE is '0' the corresponding stream will be unbuffered.\n\n\ This option is invalid with standard input.\n\n\
Otherwise MODE is a number which may be followed by one of the following:\n\n\ If MODE is '0' the corresponding stream will be unbuffered.\n\n\
KB 1000, K 1024, MB 1000*1000, M 1024*1024, and so on for G, T, P, E, Z, Y.\n\ Otherwise MODE is a number which may be followed by one of the following:\n\n\
In this case the corresponding stream will be fully buffered with the buffer size set to \ KB 1000, K 1024, MB 1000*1000, M 1024*1024, and so on for G, T, P, E, Z, Y.\n\
MODE bytes.\n\n\ In this case the corresponding stream will be fully buffered with the buffer size set to \
NOTE: If COMMAND adjusts the buffering of its standard streams ('tee' does for e.g.) then \ MODE bytes.\n\n\
that will override corresponding settings changed by 'stdbuf'.\n\ NOTE: If COMMAND adjusts the buffering of its standard streams ('tee' does for e.g.) then \
Also some filters (like 'dd' and 'cat' etc.) don't use streams for I/O, \ that will override corresponding settings changed by 'stdbuf'.\n\
and are thus unaffected by 'stdbuf' settings.\n"; Also some filters (like 'dd' and 'cat' etc.) don't use streams for I/O, \
and are thus unaffected by 'stdbuf' settings.\n";
mod options { mod options {
pub const INPUT: &str = "input"; pub const INPUT: &str = "input";

View file

@ -17,7 +17,7 @@ path = "src/tail.rs"
[dependencies] [dependencies]
clap = "2.33" clap = "2.33"
libc = "0.2.42" libc = "0.2.42"
uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["ringbuffer"] }
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }
winapi = { version="0.3", features=["fileapi", "handleapi", "processthreadsapi", "synchapi", "winbase"] } winapi = { version="0.3", features=["fileapi", "handleapi", "processthreadsapi", "synchapi", "winbase"] }

83
src/uu/tail/src/chunks.rs Normal file
View file

@ -0,0 +1,83 @@
//! Iterating over a file by chunks, starting at the end of the file.
//!
//! Use [`ReverseChunks::new`] to create a new iterator over chunks of
//! bytes from the file.
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
/// When reading files in reverse in `bounded_tail`, this is the size of each
/// block read at a time.
pub const BLOCK_SIZE: u64 = 1 << 16;
/// An iterator over a file in non-overlapping chunks from the end of the file.
///
/// Each chunk is a [`Vec`]<[`u8`]> of size [`BLOCK_SIZE`] (except
/// possibly the last chunk, which might be smaller). Each call to
/// [`next`] will seek backwards through the given file.
pub struct ReverseChunks<'a> {
/// The file to iterate over, by blocks, from the end to the beginning.
file: &'a File,
/// The total number of bytes in the file.
size: u64,
/// The total number of blocks to read.
max_blocks_to_read: usize,
/// The index of the next block to read.
block_idx: usize,
}
impl<'a> ReverseChunks<'a> {
pub fn new(file: &'a mut File) -> ReverseChunks<'a> {
let size = file.seek(SeekFrom::End(0)).unwrap();
let max_blocks_to_read = (size as f64 / BLOCK_SIZE as f64).ceil() as usize;
let block_idx = 0;
ReverseChunks {
file,
size,
max_blocks_to_read,
block_idx,
}
}
}
impl<'a> Iterator for ReverseChunks<'a> {
type Item = Vec<u8>;
fn next(&mut self) -> Option<Self::Item> {
// If there are no more chunks to read, terminate the iterator.
if self.block_idx >= self.max_blocks_to_read {
return None;
}
// The chunk size is `BLOCK_SIZE` for all but the last chunk
// (that is, the chunk closest to the beginning of the file),
// which contains the remainder of the bytes.
let block_size = if self.block_idx == self.max_blocks_to_read - 1 {
self.size % BLOCK_SIZE
} else {
BLOCK_SIZE
};
// Seek backwards by the next chunk, read the full chunk into
// `buf`, and then seek back to the start of the chunk again.
let mut buf = vec![0; BLOCK_SIZE as usize];
let pos = self
.file
.seek(SeekFrom::Current(-(block_size as i64)))
.unwrap();
self.file
.read_exact(&mut buf[0..(block_size as usize)])
.unwrap();
let pos2 = self
.file
.seek(SeekFrom::Current(-(block_size as i64)))
.unwrap();
assert_eq!(pos, pos2);
self.block_idx += 1;
Some(buf[0..(block_size as usize)].to_vec())
}
}

View file

@ -15,7 +15,9 @@ extern crate clap;
#[macro_use] #[macro_use]
extern crate uucore; extern crate uucore;
mod chunks;
mod platform; mod platform;
use chunks::ReverseChunks;
use clap::{App, Arg}; use clap::{App, Arg};
use std::collections::VecDeque; use std::collections::VecDeque;
@ -26,6 +28,7 @@ use std::io::{stdin, stdout, BufRead, BufReader, Read, Seek, SeekFrom, Write};
use std::path::Path; use std::path::Path;
use std::thread::sleep; use std::thread::sleep;
use std::time::Duration; use std::time::Duration;
use uucore::ringbuffer::RingBuffer;
pub mod options { pub mod options {
pub mod verbosity { pub mod verbosity {
@ -239,7 +242,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
} }
let mut file = File::open(&path).unwrap(); let mut file = File::open(&path).unwrap();
if is_seekable(&mut file) { if is_seekable(&mut file) {
bounded_tail(&file, &settings); bounded_tail(&mut file, &settings);
if settings.follow { if settings.follow {
let reader = BufReader::new(file); let reader = BufReader::new(file);
readers.push(reader); readers.push(reader);
@ -353,10 +356,6 @@ pub fn parse_size(mut size_slice: &str) -> Result<u64, ParseSizeErr> {
} }
} }
/// When reading files in reverse in `bounded_tail`, this is the size of each
/// block read at a time.
const BLOCK_SIZE: u64 = 1 << 16;
fn follow<T: Read>(readers: &mut [BufReader<T>], filenames: &[String], settings: &Settings) { fn follow<T: Read>(readers: &mut [BufReader<T>], filenames: &[String], settings: &Settings) {
assert!(settings.follow); assert!(settings.follow);
let mut last = readers.len() - 1; let mut last = readers.len() - 1;
@ -394,48 +393,42 @@ fn follow<T: Read>(readers: &mut [BufReader<T>], filenames: &[String], settings:
} }
} }
/// Iterate over bytes in the file, in reverse, until `should_stop` returns /// Iterate over bytes in the file, in reverse, until we find the
/// true. The `file` is left seek'd to the position just after the byte that /// `num_delimiters` instance of `delimiter`. The `file` is left seek'd to the
/// `should_stop` returned true for. /// position just after that delimiter.
fn backwards_thru_file<F>( fn backwards_thru_file(file: &mut File, num_delimiters: usize, delimiter: u8) {
mut file: &File, // This variable counts the number of delimiters found in the file
size: u64, // so far (reading from the end of the file toward the beginning).
buf: &mut Vec<u8>, let mut counter = 0;
delimiter: u8,
should_stop: &mut F,
) where
F: FnMut(u8) -> bool,
{
assert!(buf.len() >= BLOCK_SIZE as usize);
let max_blocks_to_read = (size as f64 / BLOCK_SIZE as f64).ceil() as usize; for (block_idx, slice) in ReverseChunks::new(file).enumerate() {
// Iterate over each byte in the slice in reverse order.
let mut iter = slice.iter().enumerate().rev();
for block_idx in 0..max_blocks_to_read { // Ignore a trailing newline in the last block, if there is one.
let block_size = if block_idx == max_blocks_to_read - 1 { if block_idx == 0 {
size % BLOCK_SIZE if let Some(c) = slice.last() {
} else { if *c == delimiter {
BLOCK_SIZE iter.next();
}; }
// Seek backwards by the next block, read the full block into
// `buf`, and then seek back to the start of the block again.
let pos = file.seek(SeekFrom::Current(-(block_size as i64))).unwrap();
file.read_exact(&mut buf[0..(block_size as usize)]).unwrap();
let pos2 = file.seek(SeekFrom::Current(-(block_size as i64))).unwrap();
assert_eq!(pos, pos2);
// Iterate backwards through the bytes, calling `should_stop` on each
// one.
let slice = &buf[0..(block_size as usize)];
for (i, ch) in slice.iter().enumerate().rev() {
// Ignore one trailing newline.
if block_idx == 0 && i as u64 == block_size - 1 && *ch == delimiter {
continue;
} }
}
if should_stop(*ch) { // For each byte, increment the count of the number of
file.seek(SeekFrom::Current((i + 1) as i64)).unwrap(); // delimiters found. If we have found more than the specified
return; // number of delimiters, terminate the search and seek to the
// appropriate location in the file.
for (i, ch) in iter {
if *ch == delimiter {
counter += 1;
if counter >= num_delimiters {
// After each iteration of the outer loop, the
// cursor in the file is at the *beginning* of the
// block, so seeking forward by `i + 1` bytes puts
// us right after the found delimiter.
file.seek(SeekFrom::Current((i + 1) as i64)).unwrap();
return;
}
} }
} }
} }
@ -446,21 +439,11 @@ fn backwards_thru_file<F>(
/// end of the file, and then read the file "backwards" in blocks of size /// end of the file, and then read the file "backwards" in blocks of size
/// `BLOCK_SIZE` until we find the location of the first line/byte. This ends up /// `BLOCK_SIZE` until we find the location of the first line/byte. This ends up
/// being a nice performance win for very large files. /// being a nice performance win for very large files.
fn bounded_tail(mut file: &File, settings: &Settings) { fn bounded_tail(file: &mut File, settings: &Settings) {
let size = file.seek(SeekFrom::End(0)).unwrap();
let mut buf = vec![0; BLOCK_SIZE as usize];
// Find the position in the file to start printing from. // Find the position in the file to start printing from.
match settings.mode { match settings.mode {
FilterMode::Lines(mut count, delimiter) => { FilterMode::Lines(count, delimiter) => {
backwards_thru_file(&file, size, &mut buf, delimiter, &mut |byte| { backwards_thru_file(file, count as usize, delimiter);
if byte == delimiter {
count -= 1;
count == 0
} else {
false
}
});
} }
FilterMode::Bytes(count) => { FilterMode::Bytes(count) => {
file.seek(SeekFrom::End(-(count as i64))).unwrap(); file.seek(SeekFrom::End(-(count as i64))).unwrap();
@ -468,17 +451,37 @@ fn bounded_tail(mut file: &File, settings: &Settings) {
} }
// Print the target section of the file. // Print the target section of the file.
loop { let stdout = stdout();
let bytes_read = file.read(&mut buf).unwrap(); let mut stdout = stdout.lock();
std::io::copy(file, &mut stdout).unwrap();
}
let mut stdout = stdout(); /// Collect the last elements of an iterator into a `VecDeque`.
for b in &buf[0..bytes_read] { ///
print_byte(&mut stdout, *b); /// This function returns a [`VecDeque`] containing either the last
} /// `count` elements of `iter`, an [`Iterator`] over [`Result`]
/// instances, or all but the first `count` elements of `iter`. If
if bytes_read == 0 { /// `beginning` is `true`, then all but the first `count` elements are
break; /// returned.
} ///
/// # Panics
///
/// If any element of `iter` is an [`Err`], then this function panics.
fn unbounded_tail_collect<T, E>(
iter: impl Iterator<Item = Result<T, E>>,
count: u64,
beginning: bool,
) -> VecDeque<T>
where
E: fmt::Debug,
{
if beginning {
// GNU `tail` seems to index bytes and lines starting at 1, not
// at 0. It seems to treat `+0` and `+1` as the same thing.
let i = count.max(1) - 1;
iter.skip(i as usize).map(|r| r.unwrap()).collect()
} else {
RingBuffer::from_iter(iter.map(|r| r.unwrap()), count as usize).data
} }
} }
@ -487,66 +490,15 @@ fn unbounded_tail<T: Read>(reader: &mut BufReader<T>, settings: &Settings) {
// contains count lines/chars. When reaching the end of file, output the // contains count lines/chars. When reaching the end of file, output the
// data in the ringbuf. // data in the ringbuf.
match settings.mode { match settings.mode {
FilterMode::Lines(mut count, _delimiter) => { FilterMode::Lines(count, _) => {
let mut ringbuf: VecDeque<String> = VecDeque::new(); for line in unbounded_tail_collect(reader.lines(), count, settings.beginning) {
let mut skip = if settings.beginning { println!("{}", line);
let temp = count;
count = ::std::u64::MAX;
temp - 1
} else {
0
};
loop {
let mut datum = String::new();
match reader.read_line(&mut datum) {
Ok(0) => break,
Ok(_) => {
if skip > 0 {
skip -= 1;
} else {
if count <= ringbuf.len() as u64 {
ringbuf.pop_front();
}
ringbuf.push_back(datum);
}
}
Err(err) => panic!("{}", err),
}
}
let mut stdout = stdout();
for datum in &ringbuf {
print_string(&mut stdout, datum);
} }
} }
FilterMode::Bytes(mut count) => { FilterMode::Bytes(count) => {
let mut ringbuf: VecDeque<u8> = VecDeque::new(); for byte in unbounded_tail_collect(reader.bytes(), count, settings.beginning) {
let mut skip = if settings.beginning { let mut stdout = stdout();
let temp = count; print_byte(&mut stdout, byte);
count = ::std::u64::MAX;
temp - 1
} else {
0
};
loop {
let mut datum = [0; 1];
match reader.read(&mut datum) {
Ok(0) => break,
Ok(_) => {
if skip > 0 {
skip -= 1;
} else {
if count <= ringbuf.len() as u64 {
ringbuf.pop_front();
}
ringbuf.push_back(datum[0]);
}
}
Err(err) => panic!("{}", err),
}
}
let mut stdout = stdout();
for datum in &ringbuf {
print_byte(&mut stdout, *datum);
} }
} }
} }
@ -562,8 +514,3 @@ fn print_byte<T: Write>(stdout: &mut T, ch: u8) {
crash!(1, "{}", err); crash!(1, "{}", err);
} }
} }
#[inline]
fn print_string<T: Write>(_: &mut T, s: &str) {
print!("{}", s);
}

View file

@ -166,7 +166,7 @@ impl Write for MultiWriter {
let result = writer.write_all(buf); let result = writer.write_all(buf);
match result { match result {
Err(f) => { Err(f) => {
show_info!("{}: {}", writer.name, f.to_string()); show_error!("{}: {}", writer.name, f.to_string());
false false
} }
_ => true, _ => true,
@ -180,7 +180,7 @@ impl Write for MultiWriter {
let result = writer.flush(); let result = writer.flush();
match result { match result {
Err(f) => { Err(f) => {
show_info!("{}: {}", writer.name, f.to_string()); show_error!("{}: {}", writer.name, f.to_string());
false false
} }
_ => true, _ => true,
@ -213,7 +213,7 @@ impl Read for NamedReader {
fn read(&mut self, buf: &mut [u8]) -> Result<usize> { fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
match self.inner.read(buf) { match self.inner.read(buf) {
Err(f) => { Err(f) => {
show_info!("{}: {}", Path::new("stdin").display(), f.to_string()); show_error!("{}: {}", Path::new("stdin").display(), f.to_string());
Err(f) Err(f)
} }
okay => okay, okay => okay,

View file

@ -33,7 +33,7 @@ impl Symbol {
"(" => Symbol::LParen, "(" => Symbol::LParen,
"!" => Symbol::Bang, "!" => Symbol::Bang,
"-a" | "-o" => Symbol::BoolOp(s), "-a" | "-o" => Symbol::BoolOp(s),
"=" | "!=" => Symbol::StringOp(s), "=" | "==" | "!=" => Symbol::StringOp(s),
"-eq" | "-ge" | "-gt" | "-le" | "-lt" | "-ne" => Symbol::IntOp(s), "-eq" | "-ge" | "-gt" | "-le" | "-lt" | "-ne" => Symbol::IntOp(s),
"-ef" | "-nt" | "-ot" => Symbol::FileOp(s), "-ef" | "-nt" | "-ot" => Symbol::FileOp(s),
"-n" | "-z" => Symbol::StrlenOp(s), "-n" | "-z" => Symbol::StrlenOp(s),
@ -83,7 +83,7 @@ impl Symbol {
/// TERM → str OP str /// TERM → str OP str
/// TERM → str | 𝜖 /// TERM → str | 𝜖
/// OP → STRINGOP | INTOP | FILEOP /// OP → STRINGOP | INTOP | FILEOP
/// STRINGOP → = | != /// STRINGOP → = | == | !=
/// INTOP → -eq | -ge | -gt | -le | -lt | -ne /// INTOP → -eq | -ge | -gt | -le | -lt | -ne
/// FILEOP → -ef | -nt | -ot /// FILEOP → -ef | -nt | -ot
/// STRLEN → -n | -z /// STRLEN → -n | -z
@ -121,6 +121,8 @@ impl Parser {
/// Test if the next token in the stream is a BOOLOP (-a or -o), without /// Test if the next token in the stream is a BOOLOP (-a or -o), without
/// removing the token from the stream. /// removing the token from the stream.
fn peek_is_boolop(&mut self) -> bool { fn peek_is_boolop(&mut self) -> bool {
// TODO: change to `matches!(self.peek(), Symbol::BoolOp(_))` once MSRV is 1.42
// #[allow(clippy::match_like_matches_macro)] // needs MSRV 1.43
if let Symbol::BoolOp(_) = self.peek() { if let Symbol::BoolOp(_) = self.peek() {
true true
} else { } else {
@ -161,7 +163,7 @@ impl Parser {
match self.peek() { match self.peek() {
// lparen is a literal when followed by nothing or comparison // lparen is a literal when followed by nothing or comparison
Symbol::None | Symbol::StringOp(_) | Symbol::IntOp(_) | Symbol::FileOp(_) => { Symbol::None | Symbol::StringOp(_) | Symbol::IntOp(_) | Symbol::FileOp(_) => {
self.literal(Symbol::Literal(OsString::from("("))); self.literal(Symbol::LParen.into_literal());
} }
// empty parenthetical // empty parenthetical
Symbol::Literal(s) if s == ")" => {} Symbol::Literal(s) if s == ")" => {}
@ -181,27 +183,67 @@ impl Parser {
/// ///
/// * `! =`: negate the result of the implicit string length test of `=` /// * `! =`: negate the result of the implicit string length test of `=`
/// * `! = foo`: compare the literal strings `!` and `foo` /// * `! = foo`: compare the literal strings `!` and `foo`
/// * `! <expr>`: negate the result of the expression /// * `! = = str`: negate comparison of literal `=` and `str`
/// * `!`: bang followed by nothing is literal
/// * `! EXPR`: negate the result of the expression
///
/// Combined Boolean & negation:
///
/// * `! ( EXPR ) [BOOLOP EXPR]`: negate the parenthesized expression only
/// * `! UOP str BOOLOP EXPR`: negate the unary subexpression
/// * `! str BOOLOP str`: negate the entire Boolean expression
/// * `! str BOOLOP EXPR BOOLOP EXPR`: negate the value of the first `str` term
/// ///
fn bang(&mut self) { fn bang(&mut self) {
if let Symbol::StringOp(_) | Symbol::IntOp(_) | Symbol::FileOp(_) = self.peek() { match self.peek() {
// we need to peek ahead one more token to disambiguate the first Symbol::StringOp(_) | Symbol::IntOp(_) | Symbol::FileOp(_) | Symbol::BoolOp(_) => {
// two cases listed above: case 1 — `! <OP as literal>` — and // we need to peek ahead one more token to disambiguate the first
// case 2: `<! as literal> OP str`. // three cases listed above
let peek2 = self.tokens.clone().nth(1); let peek2 = Symbol::new(self.tokens.clone().nth(1));
if peek2.is_none() { match peek2 {
// op is literal // case 1: `! <OP as literal>`
let op = self.next_token().into_literal(); // case 3: `! = OP str`
self.stack.push(op); Symbol::StringOp(_) | Symbol::None => {
self.stack.push(Symbol::Bang); // op is literal
} else { let op = self.next_token().into_literal();
// bang is literal; parsing continues with op self.literal(op);
self.literal(Symbol::Literal(OsString::from("!"))); self.stack.push(Symbol::Bang);
}
// case 2: `<! as literal> OP str [BOOLOP EXPR]`.
_ => {
// bang is literal; parsing continues with op
self.literal(Symbol::Bang.into_literal());
self.maybe_boolop();
}
}
}
// bang followed by nothing is literal
Symbol::None => self.stack.push(Symbol::Bang.into_literal()),
_ => {
// peek ahead up to 4 tokens to determine if we need to negate
// the entire expression or just the first term
let peek4: Vec<Symbol> = self
.tokens
.clone()
.take(4)
.map(|token| Symbol::new(Some(token)))
.collect();
match peek4.as_slice() {
// we peeked ahead 4 but there were only 3 tokens left
[Symbol::Literal(_), Symbol::BoolOp(_), Symbol::Literal(_)] => {
self.expr();
self.stack.push(Symbol::Bang);
}
_ => {
self.term();
self.stack.push(Symbol::Bang);
}
}
} }
} else {
self.expr();
self.stack.push(Symbol::Bang);
} }
} }
@ -209,13 +251,14 @@ impl Parser {
/// as appropriate. /// as appropriate.
fn maybe_boolop(&mut self) { fn maybe_boolop(&mut self) {
if self.peek_is_boolop() { if self.peek_is_boolop() {
let token = self.tokens.next().unwrap(); // safe because we peeked let symbol = self.next_token();
// BoolOp by itself interpreted as Literal // BoolOp by itself interpreted as Literal
if let Symbol::None = self.peek() { if let Symbol::None = self.peek() {
self.literal(Symbol::Literal(token)) self.literal(symbol.into_literal());
} else { } else {
self.boolop(Symbol::BoolOp(token)) self.boolop(symbol);
self.maybe_boolop();
} }
} }
} }
@ -229,7 +272,6 @@ impl Parser {
if op == Symbol::BoolOp(OsString::from("-a")) { if op == Symbol::BoolOp(OsString::from("-a")) {
self.term(); self.term();
self.stack.push(op); self.stack.push(op);
self.maybe_boolop();
} else { } else {
self.expr(); self.expr();
self.stack.push(op); self.stack.push(op);

View file

@ -57,7 +57,7 @@ fn eval(stack: &mut Vec<Symbol>) -> Result<bool, String> {
Some(Symbol::StringOp(op)) => { Some(Symbol::StringOp(op)) => {
let b = stack.pop(); let b = stack.pop();
let a = stack.pop(); let a = stack.pop();
Ok(if op == "=" { a == b } else { a != b }) Ok(if op == "!=" { a != b } else { a == b })
} }
Some(Symbol::IntOp(op)) => { Some(Symbol::IntOp(op)) => {
let b = pop_literal!(); let b = pop_literal!();

View file

@ -11,7 +11,8 @@
extern crate uucore; extern crate uucore;
use clap::{App, Arg}; use clap::{App, Arg};
use std::fs::{metadata, File, OpenOptions}; use std::fs::{metadata, OpenOptions};
use std::io::ErrorKind;
use std::path::Path; use std::path::Path;
#[derive(Eq, PartialEq)] #[derive(Eq, PartialEq)]
@ -133,7 +134,35 @@ fn truncate(
filenames: Vec<String>, filenames: Vec<String>,
) { ) {
let (modsize, mode) = match size { let (modsize, mode) = match size {
Some(size_string) => parse_size(&size_string), Some(size_string) => {
// Trim any whitespace.
let size_string = size_string.trim();
// Get the modifier character from the size string, if any. For
// example, if the argument is "+123", then the modifier is '+'.
let c = size_string.chars().next().unwrap();
let mode = match c {
'+' => TruncateMode::Extend,
'-' => TruncateMode::Reduce,
'<' => TruncateMode::AtMost,
'>' => TruncateMode::AtLeast,
'/' => TruncateMode::RoundDown,
'%' => TruncateMode::RoundUp,
_ => TruncateMode::Absolute, /* assume that the size is just a number */
};
// If there was a modifier character, strip it.
let size_string = match mode {
TruncateMode::Absolute => size_string,
_ => &size_string[1..],
};
let num_bytes = match parse_size(size_string) {
Ok(b) => b,
Err(_) => crash!(1, "Invalid number: {}", size_string),
};
(num_bytes, mode)
}
None => (0, TruncateMode::Reference), None => (0, TruncateMode::Reference),
}; };
@ -146,13 +175,14 @@ fn truncate(
TruncateMode::Reduce => (), TruncateMode::Reduce => (),
_ => crash!(1, "you must specify a relative --size with --reference"), _ => crash!(1, "you must specify a relative --size with --reference"),
}; };
let _ = match File::open(Path::new(rfilename)) {
Ok(m) => m,
Err(f) => crash!(1, "{}", f.to_string()),
};
match metadata(rfilename) { match metadata(rfilename) {
Ok(meta) => meta.len(), Ok(meta) => meta.len(),
Err(f) => crash!(1, "{}", f.to_string()), Err(f) => match f.kind() {
ErrorKind::NotFound => {
crash!(1, "cannot stat '{}': No such file or directory", rfilename)
}
_ => crash!(1, "{}", f.to_string()),
},
} }
} }
None => 0, None => 0,
@ -181,20 +211,8 @@ fn truncate(
TruncateMode::Reference => fsize, TruncateMode::Reference => fsize,
TruncateMode::Extend => fsize + modsize, TruncateMode::Extend => fsize + modsize,
TruncateMode::Reduce => fsize - modsize, TruncateMode::Reduce => fsize - modsize,
TruncateMode::AtMost => { TruncateMode::AtMost => fsize.min(modsize),
if fsize > modsize { TruncateMode::AtLeast => fsize.max(modsize),
modsize
} else {
fsize
}
}
TruncateMode::AtLeast => {
if fsize < modsize {
modsize
} else {
fsize
}
}
TruncateMode::RoundDown => fsize - fsize % modsize, TruncateMode::RoundDown => fsize - fsize % modsize,
TruncateMode::RoundUp => fsize + fsize % modsize, TruncateMode::RoundUp => fsize + fsize % modsize,
}; };
@ -208,64 +226,89 @@ fn truncate(
} }
} }
fn parse_size(size: &str) -> (u64, TruncateMode) { /// Parse a size string into a number of bytes.
let clean_size = size.replace(" ", ""); ///
let mode = match clean_size.chars().next().unwrap() { /// A size string comprises an integer and an optional unit. The unit
'+' => TruncateMode::Extend, /// may be K, M, G, T, P, E, Z, or Y (powers of 1024) or KB, MB,
'-' => TruncateMode::Reduce, /// etc. (powers of 1000).
'<' => TruncateMode::AtMost, ///
'>' => TruncateMode::AtLeast, /// # Errors
'/' => TruncateMode::RoundDown, ///
'*' => TruncateMode::RoundUp, /// This function returns an error if the string does not begin with a
_ => TruncateMode::Absolute, /* assume that the size is just a number */ /// numeral, or if the unit is not one of the supported units described
/// in the preceding section.
///
/// # Examples
///
/// ```rust,ignore
/// assert_eq!(parse_size("123").unwrap(), 123);
/// assert_eq!(parse_size("123K").unwrap(), 123 * 1024);
/// assert_eq!(parse_size("123KB").unwrap(), 123 * 1000);
/// ```
fn parse_size(size: &str) -> Result<u64, ()> {
// Get the numeric part of the size argument. For example, if the
// argument is "123K", then the numeric part is "123".
let numeric_string: String = size.chars().take_while(|c| c.is_digit(10)).collect();
let number: u64 = match numeric_string.parse() {
Ok(n) => n,
Err(_) => return Err(()),
}; };
let bytes = {
let mut slice = if mode == TruncateMode::Absolute { // Get the alphabetic units part of the size argument and compute
&clean_size // the factor it represents. For example, if the argument is "123K",
} else { // then the unit part is "K" and the factor is 1024. This may be the
&clean_size[1..] // empty string, in which case, the factor is 1.
}; let n = numeric_string.len();
if slice.chars().last().unwrap().is_alphabetic() { let (base, exponent): (u64, u32) = match &size[n..] {
slice = &slice[..slice.len() - 1]; "" => (1, 0),
if !slice.is_empty() && slice.chars().last().unwrap().is_alphabetic() { "K" | "k" => (1024, 1),
slice = &slice[..slice.len() - 1]; "M" | "m" => (1024, 2),
} "G" | "g" => (1024, 3),
} "T" | "t" => (1024, 4),
slice "P" | "p" => (1024, 5),
} "E" | "e" => (1024, 6),
.to_owned(); "Z" | "z" => (1024, 7),
let mut number: u64 = match bytes.parse() { "Y" | "y" => (1024, 8),
Ok(num) => num, "KB" | "kB" => (1000, 1),
Err(e) => crash!(1, "'{}' is not a valid number: {}", size, e), "MB" | "mB" => (1000, 2),
"GB" | "gB" => (1000, 3),
"TB" | "tB" => (1000, 4),
"PB" | "pB" => (1000, 5),
"EB" | "eB" => (1000, 6),
"ZB" | "zB" => (1000, 7),
"YB" | "yB" => (1000, 8),
_ => return Err(()),
}; };
if clean_size.chars().last().unwrap().is_alphabetic() { let factor = base.pow(exponent);
number *= match clean_size.chars().last().unwrap().to_ascii_uppercase() { Ok(number * factor)
'B' => match clean_size }
.chars()
.nth(clean_size.len() - 2) #[cfg(test)]
.unwrap() mod tests {
.to_ascii_uppercase() use crate::parse_size;
{
'K' => 1000u64, #[test]
'M' => 1000u64.pow(2), fn test_parse_size_zero() {
'G' => 1000u64.pow(3), assert_eq!(parse_size("0").unwrap(), 0);
'T' => 1000u64.pow(4), assert_eq!(parse_size("0K").unwrap(), 0);
'P' => 1000u64.pow(5), assert_eq!(parse_size("0KB").unwrap(), 0);
'E' => 1000u64.pow(6), }
'Z' => 1000u64.pow(7),
'Y' => 1000u64.pow(8), #[test]
letter => crash!(1, "'{}B' is not a valid suffix.", letter), fn test_parse_size_without_factor() {
}, assert_eq!(parse_size("123").unwrap(), 123);
'K' => 1024u64, }
'M' => 1024u64.pow(2),
'G' => 1024u64.pow(3), #[test]
'T' => 1024u64.pow(4), fn test_parse_size_kilobytes() {
'P' => 1024u64.pow(5), assert_eq!(parse_size("123K").unwrap(), 123 * 1024);
'E' => 1024u64.pow(6), assert_eq!(parse_size("123KB").unwrap(), 123 * 1000);
'Z' => 1024u64.pow(7), }
'Y' => 1024u64.pow(8),
letter => crash!(1, "'{}' is not a valid suffix.", letter), #[test]
}; fn test_parse_size_megabytes() {
} assert_eq!(parse_size("123").unwrap(), 123);
(number, mode) assert_eq!(parse_size("123M").unwrap(), 123 * 1024 * 1024);
assert_eq!(parse_size("123MB").unwrap(), 123 * 1000 * 1000);
}
} }

View file

@ -12,18 +12,20 @@ extern crate uucore;
mod count_bytes; mod count_bytes;
mod countable; mod countable;
mod wordcount;
use count_bytes::count_bytes_fast; use count_bytes::count_bytes_fast;
use countable::WordCountable; use countable::WordCountable;
use wordcount::{TitledWordCount, WordCount};
use clap::{App, Arg, ArgMatches}; use clap::{App, Arg, ArgMatches};
use thiserror::Error; use thiserror::Error;
use std::cmp::max; use std::fs::{self, File};
use std::fs::File; use std::io::{self, ErrorKind, Write};
use std::io::{self, Write};
use std::ops::{Add, AddAssign};
use std::path::Path; use std::path::Path;
use std::str::from_utf8;
/// The minimum character width for formatting counts when reading from stdin.
const MINIMUM_WIDTH: usize = 7;
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum WcError { pub enum WcError {
@ -82,51 +84,6 @@ impl Settings {
} }
} }
#[derive(Debug, Default, Copy, Clone)]
struct WordCount {
bytes: usize,
chars: usize,
lines: usize,
words: usize,
max_line_length: usize,
}
impl Add for WordCount {
type Output = Self;
fn add(self, other: Self) -> Self {
Self {
bytes: self.bytes + other.bytes,
chars: self.chars + other.chars,
lines: self.lines + other.lines,
words: self.words + other.words,
max_line_length: max(self.max_line_length, other.max_line_length),
}
}
}
impl AddAssign for WordCount {
fn add_assign(&mut self, other: Self) {
*self = *self + other
}
}
impl WordCount {
fn with_title(self, title: &str) -> TitledWordCount {
TitledWordCount { title, count: self }
}
}
/// This struct supplements the actual word count with a title that is displayed
/// to the user at the end of the program.
/// The reason we don't simply include title in the `WordCount` struct is that
/// it would result in unneccesary copying of `String`.
#[derive(Debug, Default, Clone)]
struct TitledWordCount<'a> {
title: &'a str,
count: WordCount,
}
static ABOUT: &str = "Display newline, word, and byte counts for each FILE, and a total line if static ABOUT: &str = "Display newline, word, and byte counts for each FILE, and a total line if
more than one FILE is specified."; more than one FILE is specified.";
static VERSION: &str = env!("CARGO_PKG_VERSION"); static VERSION: &str = env!("CARGO_PKG_VERSION");
@ -149,6 +106,34 @@ fn get_usage() -> String {
) )
} }
enum StdinKind {
/// Stdin specified on command-line with "-".
Explicit,
/// Stdin implicitly specified on command-line by not passing any positional argument.
Implicit,
}
/// Supported inputs.
enum Input {
/// A regular file.
Path(String),
/// Standard input.
Stdin(StdinKind),
}
impl Input {
/// Converts input to title that appears in stats.
fn to_title(&self) -> Option<&str> {
match self {
Input::Path(path) => Some(path),
Input::Stdin(StdinKind::Explicit) => Some("-"),
Input::Stdin(StdinKind::Implicit) => None,
}
}
}
pub fn uumain(args: impl uucore::Args) -> i32 { pub fn uumain(args: impl uucore::Args) -> i32 {
let usage = get_usage(); let usage = get_usage();
@ -189,36 +174,33 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
.arg(Arg::with_name(ARG_FILES).multiple(true).takes_value(true)) .arg(Arg::with_name(ARG_FILES).multiple(true).takes_value(true))
.get_matches_from(args); .get_matches_from(args);
let mut files: Vec<String> = matches let mut inputs: Vec<Input> = matches
.values_of(ARG_FILES) .values_of(ARG_FILES)
.map(|v| v.map(ToString::to_string).collect()) .map(|v| {
v.map(|i| {
if i == "-" {
Input::Stdin(StdinKind::Explicit)
} else {
Input::Path(ToString::to_string(i))
}
})
.collect()
})
.unwrap_or_default(); .unwrap_or_default();
if files.is_empty() { if inputs.is_empty() {
files.push("-".to_owned()); inputs.push(Input::Stdin(StdinKind::Implicit));
} }
let settings = Settings::new(&matches); let settings = Settings::new(&matches);
if wc(files, &settings).is_ok() { if wc(inputs, &settings).is_ok() {
0 0
} else { } else {
1 1
} }
} }
const CR: u8 = b'\r';
const LF: u8 = b'\n';
const SPACE: u8 = b' ';
const TAB: u8 = b'\t';
const SYN: u8 = 0x16_u8;
const FF: u8 = 0x0C_u8;
#[inline(always)]
fn is_word_separator(byte: u8) -> bool {
byte == SPACE || byte == TAB || byte == CR || byte == SYN || byte == FF
}
fn word_count_from_reader<T: WordCountable>( fn word_count_from_reader<T: WordCountable>(
mut reader: T, mut reader: T,
settings: &Settings, settings: &Settings,
@ -239,104 +221,181 @@ fn word_count_from_reader<T: WordCountable>(
// we do not need to decode the byte stream if we're only counting bytes/newlines // we do not need to decode the byte stream if we're only counting bytes/newlines
let decode_chars = settings.show_chars || settings.show_words || settings.show_max_line_length; let decode_chars = settings.show_chars || settings.show_words || settings.show_max_line_length;
let mut line_count: usize = 0; // Sum the WordCount for each line. Show a warning for each line
let mut word_count: usize = 0; // that results in an IO error when trying to read it.
let mut byte_count: usize = 0; let total = reader
let mut char_count: usize = 0; .lines()
let mut longest_line_length: usize = 0; .filter_map(|res| match res {
let mut ends_lf: bool; Ok(line) => Some(line),
// reading from a TTY seems to raise a condition on, rather than return Some(0) like a file.
// hence the option wrapped in a result here
for line_result in reader.lines() {
let raw_line = match line_result {
Ok(l) => l,
Err(e) => { Err(e) => {
show_warning!("Error while reading {}: {}", path, e); show_warning!("Error while reading {}: {}", path, e);
continue; None
} }
}; })
.map(|line| WordCount::from_line(&line, decode_chars))
// GNU 'wc' only counts lines that end in LF as lines .sum();
ends_lf = *raw_line.last().unwrap() == LF; Ok(total)
line_count += ends_lf as usize;
byte_count += raw_line.len();
if decode_chars {
// try and convert the bytes to UTF-8 first
let current_char_count;
match from_utf8(&raw_line[..]) {
Ok(line) => {
word_count += line.split_whitespace().count();
current_char_count = line.chars().count();
}
Err(..) => {
word_count += raw_line.split(|&x| is_word_separator(x)).count();
current_char_count = raw_line.iter().filter(|c| c.is_ascii()).count()
}
}
char_count += current_char_count;
if current_char_count > longest_line_length {
// -L is a GNU 'wc' extension so same behavior on LF
longest_line_length = current_char_count - (ends_lf as usize);
}
}
}
Ok(WordCount {
bytes: byte_count,
chars: char_count,
lines: line_count,
words: word_count,
max_line_length: longest_line_length,
})
} }
fn word_count_from_path(path: &str, settings: &Settings) -> WcResult<WordCount> { fn word_count_from_input(input: &Input, settings: &Settings) -> WcResult<WordCount> {
if path == "-" { match input {
let stdin = io::stdin(); Input::Stdin(_) => {
let stdin_lock = stdin.lock(); let stdin = io::stdin();
word_count_from_reader(stdin_lock, settings, path) let stdin_lock = stdin.lock();
} else { word_count_from_reader(stdin_lock, settings, "-")
let path_obj = Path::new(path); }
if path_obj.is_dir() { Input::Path(path) => {
Err(WcError::IsDirectory(path.to_owned())) let path_obj = Path::new(path);
} else { if path_obj.is_dir() {
let file = File::open(path)?; Err(WcError::IsDirectory(path.to_owned()))
word_count_from_reader(file, settings, path) } else {
let file = File::open(path)?;
word_count_from_reader(file, settings, path)
}
} }
} }
} }
fn wc(files: Vec<String>, settings: &Settings) -> Result<(), u32> { /// Print a message appropriate for the particular error to `stderr`.
let mut total_word_count = WordCount::default(); ///
let mut results = vec![]; /// # Examples
let mut max_width: usize = 0; ///
/// This will print `wc: /tmp: Is a directory` to `stderr`.
///
/// ```rust,ignore
/// show_error(Input::Path("/tmp"), WcError::IsDirectory("/tmp"))
/// ```
fn show_error(input: &Input, err: WcError) {
match (input, err) {
(_, WcError::IsDirectory(path)) => {
show_error_custom_description!(path, "Is a directory");
}
(Input::Path(path), WcError::Io(e)) if e.kind() == ErrorKind::NotFound => {
show_error_custom_description!(path, "No such file or directory");
}
(_, e) => {
show_error!("{}", e);
}
};
}
/// Compute the number of digits needed to represent any count for this input.
///
/// If `input` is [`Input::Stdin`], then this function returns
/// [`MINIMUM_WIDTH`]. Otherwise, if metadata could not be read from
/// `input` then this function returns 1.
///
/// # Errors
///
/// This function will return an error if `input` is a [`Input::Path`]
/// and there is a problem accessing the metadata of the given `input`.
///
/// # Examples
///
/// A [`Input::Stdin`] gets a default minimum width:
///
/// ```rust,ignore
/// let input = Input::Stdin(StdinKind::Explicit);
/// assert_eq!(7, digit_width(input));
/// ```
fn digit_width(input: &Input) -> WcResult<Option<usize>> {
match input {
Input::Stdin(_) => Ok(Some(MINIMUM_WIDTH)),
Input::Path(filename) => {
let path = Path::new(filename);
let metadata = fs::metadata(path)?;
if metadata.is_file() {
// TODO We are now computing the number of bytes in a file
// twice: once here and once in `WordCount::from_line()` (or
// in `count_bytes_fast()` if that function is called
// instead). See GitHub issue #2201.
let num_bytes = metadata.len();
let num_digits = num_bytes.to_string().len();
Ok(Some(num_digits))
} else {
Ok(None)
}
}
}
}
/// Compute the number of digits needed to represent all counts in all inputs.
///
/// `inputs` may include zero or more [`Input::Stdin`] entries, each of
/// which represents reading from `stdin`. The presence of any such
/// entry causes this function to return a width that is at least
/// [`MINIMUM_WIDTH`].
///
/// If `input` is empty, then this function returns 1. If file metadata
/// could not be read from any of the [`Input::Path`] inputs and there
/// are no [`Input::Stdin`] inputs, then this function returns 1.
///
/// If there is a problem accessing the metadata, this function will
/// silently ignore the error and assume that the number of digits
/// needed to display the counts for that file is 1.
///
/// # Examples
///
/// An empty slice implies a width of 1:
///
/// ```rust,ignore
/// assert_eq!(1, max_width(&vec![]));
/// ```
///
/// The presence of [`Input::Stdin`] implies a minimum width:
///
/// ```rust,ignore
/// let inputs = vec![Input::Stdin(StdinKind::Explicit)];
/// assert_eq!(7, max_width(&inputs));
/// ```
fn max_width(inputs: &[Input]) -> usize {
let mut result = 1;
for input in inputs {
match digit_width(input) {
Ok(maybe_n) => {
if let Some(n) = maybe_n {
result = result.max(n);
}
}
Err(_) => continue,
}
}
result
}
fn wc(inputs: Vec<Input>, settings: &Settings) -> Result<(), u32> {
// Compute the width, in digits, to use when formatting counts.
//
// The width is the number of digits needed to print the number of
// bytes in the largest file. This is true regardless of whether
// the `settings` indicate that the bytes will be displayed.
let mut error_count = 0; let mut error_count = 0;
let max_width = max_width(&inputs);
let num_files = files.len(); let mut total_word_count = WordCount::default();
for path in &files { let num_inputs = inputs.len();
let word_count = word_count_from_path(&path, settings).unwrap_or_else(|err| {
show_error!("{}", err); for input in &inputs {
let word_count = word_count_from_input(&input, settings).unwrap_or_else(|err| {
show_error(&input, err);
error_count += 1; error_count += 1;
WordCount::default() WordCount::default()
}); });
max_width = max(max_width, word_count.bytes.to_string().len() + 1);
total_word_count += word_count; total_word_count += word_count;
results.push(word_count.with_title(path)); let result = word_count.with_title(input.to_title());
}
for result in &results {
if let Err(err) = print_stats(settings, &result, max_width) { if let Err(err) = print_stats(settings, &result, max_width) {
show_warning!("failed to print result for {}: {}", result.title, err); show_warning!(
"failed to print result for {}: {}",
result.title.unwrap_or("<stdin>"),
err
);
error_count += 1; error_count += 1;
} }
} }
if num_files > 1 { if num_inputs > 1 {
let total_result = total_word_count.with_title("total"); let total_result = total_word_count.with_title(Some("total"));
if let Err(err) = print_stats(settings, &total_result, max_width) { if let Err(err) = print_stats(settings, &total_result, max_width) {
show_warning!("failed to print total: {}", err); show_warning!("failed to print total: {}", err);
error_count += 1; error_count += 1;
@ -364,19 +423,40 @@ fn print_stats(
min_width = 0; min_width = 0;
} }
let mut is_first: bool = true;
if settings.show_lines { if settings.show_lines {
if !is_first {
write!(stdout_lock, " ")?;
}
write!(stdout_lock, "{:1$}", result.count.lines, min_width)?; write!(stdout_lock, "{:1$}", result.count.lines, min_width)?;
is_first = false;
} }
if settings.show_words { if settings.show_words {
if !is_first {
write!(stdout_lock, " ")?;
}
write!(stdout_lock, "{:1$}", result.count.words, min_width)?; write!(stdout_lock, "{:1$}", result.count.words, min_width)?;
is_first = false;
} }
if settings.show_bytes { if settings.show_bytes {
if !is_first {
write!(stdout_lock, " ")?;
}
write!(stdout_lock, "{:1$}", result.count.bytes, min_width)?; write!(stdout_lock, "{:1$}", result.count.bytes, min_width)?;
is_first = false;
} }
if settings.show_chars { if settings.show_chars {
if !is_first {
write!(stdout_lock, " ")?;
}
write!(stdout_lock, "{:1$}", result.count.chars, min_width)?; write!(stdout_lock, "{:1$}", result.count.chars, min_width)?;
is_first = false;
} }
if settings.show_max_line_length { if settings.show_max_line_length {
if !is_first {
write!(stdout_lock, " ")?;
}
write!( write!(
stdout_lock, stdout_lock,
"{:1$}", "{:1$}",
@ -384,10 +464,10 @@ fn print_stats(
)?; )?;
} }
if result.title == "-" { if let Some(title) = result.title {
writeln!(stdout_lock)?; writeln!(stdout_lock, " {}", title)?;
} else { } else {
writeln!(stdout_lock, " {}", result.title)?; writeln!(stdout_lock)?;
} }
Ok(()) Ok(())

131
src/uu/wc/src/wordcount.rs Normal file
View file

@ -0,0 +1,131 @@
use std::cmp::max;
use std::iter::Sum;
use std::ops::{Add, AddAssign};
use std::str::from_utf8;
const CR: u8 = b'\r';
const LF: u8 = b'\n';
const SPACE: u8 = b' ';
const TAB: u8 = b'\t';
const SYN: u8 = 0x16_u8;
const FF: u8 = 0x0C_u8;
#[inline(always)]
fn is_word_separator(byte: u8) -> bool {
byte == SPACE || byte == TAB || byte == CR || byte == SYN || byte == FF
}
#[derive(Debug, Default, Copy, Clone)]
pub struct WordCount {
pub bytes: usize,
pub chars: usize,
pub lines: usize,
pub words: usize,
pub max_line_length: usize,
}
impl Add for WordCount {
type Output = Self;
fn add(self, other: Self) -> Self {
Self {
bytes: self.bytes + other.bytes,
chars: self.chars + other.chars,
lines: self.lines + other.lines,
words: self.words + other.words,
max_line_length: max(self.max_line_length, other.max_line_length),
}
}
}
impl AddAssign for WordCount {
fn add_assign(&mut self, other: Self) {
*self = *self + other
}
}
impl Sum for WordCount {
fn sum<I>(iter: I) -> WordCount
where
I: Iterator<Item = WordCount>,
{
iter.fold(WordCount::default(), |acc, x| acc + x)
}
}
impl WordCount {
/// Count the characters and whitespace-separated words in the given bytes.
///
/// `line` is a slice of bytes that will be decoded as ASCII characters.
fn ascii_word_and_char_count(line: &[u8]) -> (usize, usize) {
let word_count = line.split(|&x| is_word_separator(x)).count();
let char_count = line.iter().filter(|c| c.is_ascii()).count();
(word_count, char_count)
}
/// Create a [`WordCount`] from a sequence of bytes representing a line.
///
/// If the last byte of `line` encodes a newline character (`\n`),
/// then the [`lines`] field will be set to 1. Otherwise, it will
/// be set to 0. The [`bytes`] field is simply the length of
/// `line`.
///
/// If `decode_chars` is `false`, the [`chars`] and [`words`]
/// fields will be set to 0. If it is `true`, this function will
/// attempt to decode the bytes first as UTF-8, and failing that,
/// as ASCII.
pub fn from_line(line: &[u8], decode_chars: bool) -> WordCount {
// GNU 'wc' only counts lines that end in LF as lines
let lines = (*line.last().unwrap() == LF) as usize;
let bytes = line.len();
let (words, chars) = if decode_chars {
WordCount::word_and_char_count(line)
} else {
(0, 0)
};
// -L is a GNU 'wc' extension so same behavior on LF
let max_line_length = if chars > 0 { chars - lines } else { 0 };
WordCount {
bytes,
chars,
lines,
words,
max_line_length,
}
}
/// Count the UTF-8 characters and words in the given string slice.
///
/// `s` is a string slice that is assumed to be a UTF-8 string.
fn utf8_word_and_char_count(s: &str) -> (usize, usize) {
let word_count = s.split_whitespace().count();
let char_count = s.chars().count();
(word_count, char_count)
}
pub fn with_title(self, title: Option<&str>) -> TitledWordCount {
TitledWordCount { title, count: self }
}
/// Count the characters and words in the given slice of bytes.
///
/// `line` is a slice of bytes that will be decoded as UTF-8
/// characters, or if that fails, as ASCII characters.
fn word_and_char_count(line: &[u8]) -> (usize, usize) {
// try and convert the bytes to UTF-8 first
match from_utf8(line) {
Ok(s) => WordCount::utf8_word_and_char_count(s),
Err(..) => WordCount::ascii_word_and_char_count(line),
}
}
}
/// This struct supplements the actual word count with an optional title that is
/// displayed to the user at the end of the program.
/// The reason we don't simply include title in the `WordCount` struct is that
/// it would result in unneccesary copying of `String`.
#[derive(Debug, Default, Clone)]
pub struct TitledWordCount<'a> {
pub title: Option<&'a str>,
pub count: WordCount,
}

View file

@ -29,7 +29,6 @@ mod options {
pub const ONLY_HOSTNAME_USER: &str = "only_hostname_user"; pub const ONLY_HOSTNAME_USER: &str = "only_hostname_user";
pub const PROCESS: &str = "process"; pub const PROCESS: &str = "process";
pub const COUNT: &str = "count"; pub const COUNT: &str = "count";
#[cfg(any(target_vendor = "apple", target_os = "linux", target_os = "android"))]
pub const RUNLEVEL: &str = "runlevel"; pub const RUNLEVEL: &str = "runlevel";
pub const SHORT: &str = "short"; pub const SHORT: &str = "short";
pub const TIME: &str = "time"; pub const TIME: &str = "time";
@ -41,14 +40,20 @@ mod options {
static VERSION: &str = env!("CARGO_PKG_VERSION"); static VERSION: &str = env!("CARGO_PKG_VERSION");
static ABOUT: &str = "Print information about users who are currently logged in."; static ABOUT: &str = "Print information about users who are currently logged in.";
#[cfg(any(target_os = "linux"))]
static RUNLEVEL_HELP: &str = "print current runlevel";
#[cfg(not(target_os = "linux"))]
static RUNLEVEL_HELP: &str = "print current runlevel (This is meaningless on non Linux)";
fn get_usage() -> String { fn get_usage() -> String {
format!("{0} [OPTION]... [ FILE | ARG1 ARG2 ]", executable!()) format!("{0} [OPTION]... [ FILE | ARG1 ARG2 ]", executable!())
} }
fn get_long_usage() -> String { fn get_long_usage() -> String {
String::from( format!(
"If FILE is not specified, use /var/run/utmp. /var/log/wtmp as FILE is common.\n\ "If FILE is not specified, use {}. /var/log/wtmp as FILE is common.\n\
If ARG1 ARG2 given, -m presumed: 'am i' or 'mom likes' are usual.", If ARG1 ARG2 given, -m presumed: 'am i' or 'mom likes' are usual.",
utmpx::DEFAULT_FILE,
) )
} }
@ -118,11 +123,10 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
.help("all login names and number of users logged on"), .help("all login names and number of users logged on"),
) )
.arg( .arg(
#[cfg(any(target_vendor = "apple", target_os = "linux", target_os = "android"))]
Arg::with_name(options::RUNLEVEL) Arg::with_name(options::RUNLEVEL)
.long(options::RUNLEVEL) .long(options::RUNLEVEL)
.short("r") .short("r")
.help("print current runlevel"), .help(RUNLEVEL_HELP),
) )
.arg( .arg(
Arg::with_name(options::SHORT) Arg::with_name(options::SHORT)
@ -383,15 +387,12 @@ fn current_tty() -> String {
impl Who { impl Who {
fn exec(&mut self) { fn exec(&mut self) {
let run_level_chk = |record: i16| { let run_level_chk = |_record: i16| {
#[allow(unused_assignments)] #[cfg(not(target_os = "linux"))]
let mut res = false; return false;
#[cfg(any(target_vendor = "apple", target_os = "linux", target_os = "android"))] #[cfg(target_os = "linux")]
{ return _record == utmpx::RUN_LVL;
res = record == utmpx::RUN_LVL;
}
res
}; };
let f = if self.args.len() == 1 { let f = if self.args.len() == 1 {
@ -424,7 +425,9 @@ impl Who {
if self.need_users && ut.is_user_process() { if self.need_users && ut.is_user_process() {
self.print_user(&ut); self.print_user(&ut);
} else if self.need_runlevel && run_level_chk(ut.record_type()) { } else if self.need_runlevel && run_level_chk(ut.record_type()) {
self.print_runlevel(&ut); if cfg!(target_os = "linux") {
self.print_runlevel(&ut);
}
} else if self.need_boottime && ut.record_type() == utmpx::BOOT_TIME { } else if self.need_boottime && ut.record_type() == utmpx::BOOT_TIME {
self.print_boottime(&ut); self.print_boottime(&ut);
} else if self.need_clockchange && ut.record_type() == utmpx::NEW_TIME { } else if self.need_clockchange && ut.record_type() == utmpx::NEW_TIME {
@ -548,20 +551,10 @@ impl Who {
" ?".into() " ?".into()
}; };
let mut buf = vec![]; let mut s = ut.host();
let ut_host = ut.host(); if self.do_lookup {
let mut res = ut_host.splitn(2, ':'); s = safe_unwrap!(ut.canon_host());
if let Some(h) = res.next() {
if self.do_lookup {
buf.push(ut.canon_host().unwrap_or_else(|_| h.to_owned()));
} else {
buf.push(h.to_owned());
}
} }
if let Some(h) = res.next() {
buf.push(h.to_owned());
}
let s = buf.join(":");
let hoststr = if s.is_empty() { s } else { format!("({})", s) }; let hoststr = if s.is_empty() { s } else { format!("({})", s) };
self.print_line( self.print_line(

View file

@ -16,6 +16,7 @@ edition = "2018"
path="src/lib/lib.rs" path="src/lib/lib.rs"
[dependencies] [dependencies]
dns-lookup = "1.0.5"
dunce = "1.0.0" dunce = "1.0.0"
getopts = "<= 0.2.21" getopts = "<= 0.2.21"
wild = "2.0.4" wild = "2.0.4"
@ -29,6 +30,9 @@ time = { version="<= 0.1.43", optional=true }
data-encoding = { version="~2.1", optional=true } ## data-encoding: require v2.1; but v2.2.0 breaks the build for MinSRV v1.31.0 data-encoding = { version="~2.1", optional=true } ## data-encoding: require v2.1; but v2.2.0 breaks the build for MinSRV v1.31.0
libc = { version="0.2.15, <= 0.2.85", optional=true } ## libc: initial utmp support added in v0.2.15; but v0.2.68 breaks the build for MinSRV v1.31.0 libc = { version="0.2.15, <= 0.2.85", optional=true } ## libc: initial utmp support added in v0.2.15; but v0.2.68 breaks the build for MinSRV v1.31.0
[target.'cfg(target_os = "windows")'.dependencies]
winapi = { version = "0.3", features = ["errhandlingapi", "fileapi", "handleapi", "winerror"] }
[target.'cfg(target_os = "redox")'.dependencies] [target.'cfg(target_os = "redox")'.dependencies]
termion = "1.5" termion = "1.5"
@ -38,10 +42,12 @@ default = []
encoding = ["data-encoding", "thiserror"] encoding = ["data-encoding", "thiserror"]
entries = ["libc"] entries = ["libc"]
fs = ["libc"] fs = ["libc"]
fsext = ["libc", "time"]
mode = ["libc"] mode = ["libc"]
parse_time = [] parse_time = []
perms = ["libc"] perms = ["libc"]
process = ["libc"] process = ["libc"]
ringbuffer = []
signals = [] signals = []
utf8 = [] utf8 = []
utmpx = ["time", "libc"] utmpx = ["time", "libc"]

View file

@ -4,8 +4,12 @@
pub mod encoding; pub mod encoding;
#[cfg(feature = "fs")] #[cfg(feature = "fs")]
pub mod fs; pub mod fs;
#[cfg(feature = "fsext")]
pub mod fsext;
#[cfg(feature = "parse_time")] #[cfg(feature = "parse_time")]
pub mod parse_time; pub mod parse_time;
#[cfg(feature = "ringbuffer")]
pub mod ringbuffer;
#[cfg(feature = "zero-copy")] #[cfg(feature = "zero-copy")]
pub mod zero_copy; pub mod zero_copy;

View file

@ -8,8 +8,9 @@
#[cfg(unix)] #[cfg(unix)]
use libc::{ use libc::{
mode_t, S_IRGRP, S_IROTH, S_IRUSR, S_ISGID, S_ISUID, S_ISVTX, S_IWGRP, S_IWOTH, S_IWUSR, mode_t, S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFMT, S_IFREG, S_IFSOCK, S_IRGRP,
S_IXGRP, S_IXOTH, S_IXUSR, S_IROTH, S_IRUSR, S_ISGID, S_ISUID, S_ISVTX, S_IWGRP, S_IWOTH, S_IWUSR, S_IXGRP, S_IXOTH,
S_IXUSR,
}; };
use std::borrow::Cow; use std::borrow::Cow;
use std::env; use std::env;
@ -23,9 +24,10 @@ use std::os::unix::fs::MetadataExt;
use std::path::{Component, Path, PathBuf}; use std::path::{Component, Path, PathBuf};
#[cfg(unix)] #[cfg(unix)]
#[macro_export]
macro_rules! has { macro_rules! has {
($mode:expr, $perm:expr) => { ($mode:expr, $perm:expr) => {
$mode & ($perm as u32) != 0 $mode & $perm != 0
}; };
} }
@ -52,11 +54,19 @@ pub fn resolve_relative_path(path: &Path) -> Cow<Path> {
result.into() result.into()
} }
/// Controls how symbolic links should be handled when canonicalizing a path.
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CanonicalizeMode { pub enum CanonicalizeMode {
/// Do not resolve any symbolic links.
None, None,
/// Resolve all symbolic links.
Normal, Normal,
/// Resolve symbolic links, ignoring errors on the final component.
Existing, Existing,
/// Resolve symbolic links, ignoring errors on the non-final components.
Missing, Missing,
} }
@ -123,6 +133,24 @@ fn resolve<P: AsRef<Path>>(original: P) -> IOResult<PathBuf> {
Ok(result) Ok(result)
} }
/// Return the canonical, absolute form of a path.
///
/// This function is a generalization of [`std::fs::canonicalize`] that
/// allows controlling how symbolic links are resolved and how to deal
/// with missing components. It returns the canonical, absolute form of
/// a path. The `can_mode` parameter controls how symbolic links are
/// resolved:
///
/// * [`CanonicalizeMode::Normal`] makes this function behave like
/// [`std::fs::canonicalize`], resolving symbolic links and returning
/// an error if the path does not exist.
/// * [`CanonicalizeMode::Missing`] makes this function ignore non-final
/// components of the path that could not be resolved.
/// * [`CanonicalizeMode::Existing`] makes this function return an error
/// if the final component of the path does not exist.
/// * [`CanonicalizeMode::None`] makes this function not try to resolve
/// any symbolic links.
///
pub fn canonicalize<P: AsRef<Path>>(original: P, can_mode: CanonicalizeMode) -> IOResult<PathBuf> { pub fn canonicalize<P: AsRef<Path>>(original: P, can_mode: CanonicalizeMode) -> IOResult<PathBuf> {
// Create an absolute path // Create an absolute path
let original = original.as_ref(); let original = original.as_ref();
@ -178,6 +206,10 @@ pub fn canonicalize<P: AsRef<Path>>(original: P, can_mode: CanonicalizeMode) ->
result.push(parts.last().unwrap()); result.push(parts.last().unwrap());
if can_mode == CanonicalizeMode::None {
return Ok(result);
}
match resolve(&result) { match resolve(&result) {
Err(e) => { Err(e) => {
if can_mode == CanonicalizeMode::Existing { if can_mode == CanonicalizeMode::Existing {
@ -240,22 +272,42 @@ pub fn is_stderr_interactive() -> bool {
#[cfg(not(unix))] #[cfg(not(unix))]
#[allow(unused_variables)] #[allow(unused_variables)]
pub fn display_permissions(metadata: &fs::Metadata) -> String { pub fn display_permissions(metadata: &fs::Metadata, display_file_type: bool) -> String {
if display_file_type {
return String::from("----------");
}
String::from("---------") String::from("---------")
} }
#[cfg(unix)] #[cfg(unix)]
pub fn display_permissions(metadata: &fs::Metadata) -> String { pub fn display_permissions(metadata: &fs::Metadata, display_file_type: bool) -> String {
let mode: mode_t = metadata.mode() as mode_t; let mode: mode_t = metadata.mode() as mode_t;
display_permissions_unix(mode as u32) display_permissions_unix(mode, display_file_type)
} }
#[cfg(unix)] #[cfg(unix)]
pub fn display_permissions_unix(mode: u32) -> String { pub fn display_permissions_unix(mode: mode_t, display_file_type: bool) -> String {
let mut result = String::with_capacity(9); let mut result;
if display_file_type {
result = String::with_capacity(10);
result.push(match mode & S_IFMT {
S_IFDIR => 'd',
S_IFCHR => 'c',
S_IFBLK => 'b',
S_IFREG => '-',
S_IFIFO => 'p',
S_IFLNK => 'l',
S_IFSOCK => 's',
// TODO: Other file types
_ => '?',
});
} else {
result = String::with_capacity(9);
}
result.push(if has!(mode, S_IRUSR) { 'r' } else { '-' }); result.push(if has!(mode, S_IRUSR) { 'r' } else { '-' });
result.push(if has!(mode, S_IWUSR) { 'w' } else { '-' }); result.push(if has!(mode, S_IWUSR) { 'w' } else { '-' });
result.push(if has!(mode, S_ISUID) { result.push(if has!(mode, S_ISUID as mode_t) {
if has!(mode, S_IXUSR) { if has!(mode, S_IXUSR) {
's' 's'
} else { } else {
@ -269,7 +321,7 @@ pub fn display_permissions_unix(mode: u32) -> String {
result.push(if has!(mode, S_IRGRP) { 'r' } else { '-' }); result.push(if has!(mode, S_IRGRP) { 'r' } else { '-' });
result.push(if has!(mode, S_IWGRP) { 'w' } else { '-' }); result.push(if has!(mode, S_IWGRP) { 'w' } else { '-' });
result.push(if has!(mode, S_ISGID) { result.push(if has!(mode, S_ISGID as mode_t) {
if has!(mode, S_IXGRP) { if has!(mode, S_IXGRP) {
's' 's'
} else { } else {
@ -283,7 +335,7 @@ pub fn display_permissions_unix(mode: u32) -> String {
result.push(if has!(mode, S_IROTH) { 'r' } else { '-' }); result.push(if has!(mode, S_IROTH) { 'r' } else { '-' });
result.push(if has!(mode, S_IWOTH) { 'w' } else { '-' }); result.push(if has!(mode, S_IWOTH) { 'w' } else { '-' });
result.push(if has!(mode, S_ISVTX) { result.push(if has!(mode, S_ISVTX as mode_t) {
if has!(mode, S_IXOTH) { if has!(mode, S_IXOTH) {
't' 't'
} else { } else {
@ -355,4 +407,57 @@ mod tests {
); );
} }
} }
#[cfg(unix)]
#[test]
fn test_display_permissions() {
assert_eq!(
"drwxr-xr-x",
display_permissions_unix(S_IFDIR | 0o755, true)
);
assert_eq!(
"rwxr-xr-x",
display_permissions_unix(S_IFDIR | 0o755, false)
);
assert_eq!(
"-rw-r--r--",
display_permissions_unix(S_IFREG | 0o644, true)
);
assert_eq!(
"srw-r-----",
display_permissions_unix(S_IFSOCK | 0o640, true)
);
assert_eq!(
"lrw-r-xr-x",
display_permissions_unix(S_IFLNK | 0o655, true)
);
assert_eq!("?rw-r-xr-x", display_permissions_unix(0o655, true));
assert_eq!(
"brwSr-xr-x",
display_permissions_unix(S_IFBLK | S_ISUID as mode_t | 0o655, true)
);
assert_eq!(
"brwsr-xr-x",
display_permissions_unix(S_IFBLK | S_ISUID as mode_t | 0o755, true)
);
assert_eq!(
"prw---sr--",
display_permissions_unix(S_IFIFO | S_ISGID as mode_t | 0o614, true)
);
assert_eq!(
"prw---Sr--",
display_permissions_unix(S_IFIFO | S_ISGID as mode_t | 0o604, true)
);
assert_eq!(
"c---r-xr-t",
display_permissions_unix(S_IFCHR | S_ISVTX as mode_t | 0o055, true)
);
assert_eq!(
"c---r-xr-T",
display_permissions_unix(S_IFCHR | S_ISVTX as mode_t | 0o054, true)
);
}
} }

View file

@ -0,0 +1,821 @@
// This file is part of the uutils coreutils package.
//
// (c) Jian Zeng <anonymousknight96@gmail.com>
// (c) Fangxu Hu <framlog@gmail.com>
// (c) Sylvestre Ledru <sylvestre@debian.org>
//
// For the full copyright and license information, please view the LICENSE file
// that was distributed with this source code.
// spell-checker:ignore (ToDO) strerror IFBLK IFCHR IFDIR IFLNK IFIFO IFMT IFREG IFSOCK subsec nanos gnulib statfs Sstatfs bitrig statvfs iosize blksize fnodes fsid namelen bsize bfree bavail ffree frsize namemax errno fstype adfs acfs aufs affs autofs befs bdevfs binfmt ceph cgroups cifs configfs cramfs cgroupfs debugfs devfs devpts ecryptfs btrfs efivarfs exofs fhgfs fuseblk fusectl futexfs gpfs hfsx hostfs hpfs inodefs ibrix inotifyfs isofs jffs logfs hugetlbfs mqueue nsfs ntfs ocfs panfs pipefs ramfs romfs nfsd nilfs pstorefs reiserfs securityfs smackfs snfs sockfs squashfs sysfs sysv tempfs tracefs ubifs usbdevfs vmhgfs tmpfs vxfs wslfs xenfs vzfs openprom overlayfs
extern crate time;
pub use crate::*; // import macros from `../../macros.rs`
#[cfg(target_os = "linux")]
const LINUX_MTAB: &str = "/etc/mtab";
#[cfg(target_os = "linux")]
const LINUX_MOUNTINFO: &str = "/proc/self/mountinfo";
static MOUNT_OPT_BIND: &str = "bind";
#[cfg(windows)]
const MAX_PATH: usize = 266;
#[cfg(not(unix))]
static EXIT_ERR: i32 = 1;
#[cfg(windows)]
use std::ffi::OsString;
#[cfg(windows)]
use std::os::windows::ffi::OsStrExt;
#[cfg(windows)]
use std::os::windows::ffi::OsStringExt;
#[cfg(windows)]
use winapi::shared::minwindef::DWORD;
#[cfg(windows)]
use winapi::um::errhandlingapi::GetLastError;
#[cfg(windows)]
use winapi::um::fileapi::GetDiskFreeSpaceW;
#[cfg(windows)]
use winapi::um::fileapi::{
FindFirstVolumeW, FindNextVolumeW, FindVolumeClose, GetDriveTypeW, GetVolumeInformationW,
GetVolumePathNamesForVolumeNameW, QueryDosDeviceW,
};
#[cfg(windows)]
use winapi::um::handleapi::INVALID_HANDLE_VALUE;
#[cfg(windows)]
use winapi::um::winbase::DRIVE_REMOTE;
#[cfg(windows)]
macro_rules! String2LPWSTR {
($str: expr) => {
OsString::from($str.clone())
.as_os_str()
.encode_wide()
.chain(Some(0))
.collect::<Vec<u16>>()
.as_ptr()
};
}
#[cfg(windows)]
#[allow(non_snake_case)]
fn LPWSTR2String(buf: &[u16]) -> String {
let len = unsafe { libc::wcslen(buf.as_ptr()) };
OsString::from_wide(&buf[..len as usize])
.into_string()
.unwrap()
}
use self::time::Timespec;
#[cfg(unix)]
use libc::{
mode_t, strerror, S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFMT, S_IFREG, S_IFSOCK,
};
use std::borrow::Cow;
use std::convert::{AsRef, From};
#[cfg(unix)]
use std::ffi::CString;
#[cfg(unix)]
use std::io::Error as IOError;
#[cfg(unix)]
use std::mem;
use std::path::Path;
use std::time::UNIX_EPOCH;
#[cfg(any(
target_os = "linux",
target_vendor = "apple",
target_os = "android",
target_os = "freebsd"
))]
pub use libc::statfs as Sstatfs;
#[cfg(any(
target_os = "openbsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "bitrig",
target_os = "dragonfly"
))]
pub use libc::statvfs as Sstatfs;
#[cfg(any(
target_os = "linux",
target_vendor = "apple",
target_os = "android",
target_os = "freebsd"
))]
pub use libc::statfs as statfs_fn;
#[cfg(any(
target_os = "openbsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "bitrig",
target_os = "dragonfly"
))]
pub use libc::statvfs as statfs_fn;
pub trait BirthTime {
fn pretty_birth(&self) -> String;
fn birth(&self) -> String;
}
use std::fs::Metadata;
impl BirthTime for Metadata {
fn pretty_birth(&self) -> String {
self.created()
.ok()
.and_then(|t| t.duration_since(UNIX_EPOCH).ok())
.map(|e| pretty_time(e.as_secs() as i64, i64::from(e.subsec_nanos())))
.unwrap_or_else(|| "-".to_owned())
}
fn birth(&self) -> String {
self.created()
.ok()
.and_then(|t| t.duration_since(UNIX_EPOCH).ok())
.map(|e| format!("{}", e.as_secs()))
.unwrap_or_else(|| "0".to_owned())
}
}
#[derive(Debug, Clone)]
pub struct MountInfo {
// it stores `volume_name` in windows platform and `dev_id` in unix platform
pub dev_id: String,
pub dev_name: String,
pub fs_type: String,
pub mount_dir: String,
pub mount_option: String, // we only care "bind" option
pub mount_root: String,
pub remote: bool,
pub dummy: bool,
}
impl MountInfo {
fn set_missing_fields(&mut self) {
#[cfg(unix)]
{
// We want to keep the dev_id on Windows
// but set dev_id
let path = CString::new(self.mount_dir.clone()).unwrap();
unsafe {
let mut stat = mem::zeroed();
if libc::stat(path.as_ptr(), &mut stat) == 0 {
self.dev_id = (stat.st_dev as i32).to_string();
} else {
self.dev_id = "".to_string();
}
}
}
// set MountInfo::dummy
match self.fs_type.as_ref() {
"autofs" | "proc" | "subfs"
/* for Linux 2.6/3.x */
| "debugfs" | "devpts" | "fusectl" | "mqueue" | "rpc_pipefs" | "sysfs"
/* FreeBSD, Linux 2.4 */
| "devfs"
/* for NetBSD 3.0 */
| "kernfs"
/* for Irix 6.5 */
| "ignore" => self.dummy = true,
_ => self.dummy = self.fs_type == "none"
&& self.mount_option.find(MOUNT_OPT_BIND).is_none(),
}
// set MountInfo::remote
#[cfg(windows)]
{
self.remote = DRIVE_REMOTE == unsafe { GetDriveTypeW(String2LPWSTR!(self.mount_root)) };
}
#[cfg(unix)]
{
if self.dev_name.find(':').is_some()
|| (self.dev_name.starts_with("//") && self.fs_type == "smbfs"
|| self.fs_type == "cifs")
|| self.dev_name == "-hosts"
{
self.remote = true;
} else {
self.remote = false;
}
}
}
#[cfg(target_os = "linux")]
fn new(file_name: &str, raw: Vec<&str>) -> Option<MountInfo> {
match file_name {
// Format: 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue
// "man proc" for more details
LINUX_MOUNTINFO => {
let mut m = MountInfo {
dev_id: "".to_string(),
dev_name: raw[9].to_string(),
fs_type: raw[8].to_string(),
mount_root: raw[3].to_string(),
mount_dir: raw[4].to_string(),
mount_option: raw[5].to_string(),
remote: false,
dummy: false,
};
m.set_missing_fields();
Some(m)
}
LINUX_MTAB => {
let mut m = MountInfo {
dev_id: "".to_string(),
dev_name: raw[0].to_string(),
fs_type: raw[2].to_string(),
mount_root: "".to_string(),
mount_dir: raw[1].to_string(),
mount_option: raw[3].to_string(),
remote: false,
dummy: false,
};
m.set_missing_fields();
Some(m)
}
_ => None,
}
}
#[cfg(windows)]
fn new(mut volume_name: String) -> Option<MountInfo> {
let mut dev_name_buf = [0u16; MAX_PATH];
volume_name.pop();
unsafe {
QueryDosDeviceW(
OsString::from(volume_name.clone())
.as_os_str()
.encode_wide()
.chain(Some(0))
.skip(4)
.collect::<Vec<u16>>()
.as_ptr(),
dev_name_buf.as_mut_ptr(),
dev_name_buf.len() as DWORD,
)
};
volume_name.push('\\');
let dev_name = LPWSTR2String(&dev_name_buf);
let mut mount_root_buf = [0u16; MAX_PATH];
let success = unsafe {
GetVolumePathNamesForVolumeNameW(
String2LPWSTR!(volume_name),
mount_root_buf.as_mut_ptr(),
mount_root_buf.len() as DWORD,
ptr::null_mut(),
)
};
if 0 == success {
// TODO: support the case when `GetLastError()` returns `ERROR_MORE_DATA`
return None;
}
let mount_root = LPWSTR2String(&mount_root_buf);
let mut fs_type_buf = [0u16; MAX_PATH];
let success = unsafe {
GetVolumeInformationW(
String2LPWSTR!(mount_root),
ptr::null_mut(),
0,
ptr::null_mut(),
ptr::null_mut(),
ptr::null_mut(),
fs_type_buf.as_mut_ptr(),
fs_type_buf.len() as DWORD,
)
};
let fs_type = if 0 != success {
Some(LPWSTR2String(&fs_type_buf))
} else {
None
};
let mut mn_info = MountInfo {
dev_id: volume_name,
dev_name,
fs_type: fs_type.unwrap_or_else(|| "".to_string()),
mount_root,
mount_dir: "".to_string(),
mount_option: "".to_string(),
remote: false,
dummy: false,
};
mn_info.set_missing_fields();
Some(mn_info)
}
}
#[cfg(any(target_vendor = "apple", target_os = "freebsd"))]
use std::ffi::CStr;
#[cfg(any(target_os = "freebsd", target_vendor = "apple"))]
impl From<Sstatfs> for MountInfo {
fn from(statfs: Sstatfs) -> Self {
let mut info = MountInfo {
dev_id: "".to_string(),
dev_name: unsafe {
CStr::from_ptr(&statfs.f_mntfromname[0])
.to_string_lossy()
.into_owned()
},
fs_type: unsafe {
CStr::from_ptr(&statfs.f_fstypename[0])
.to_string_lossy()
.into_owned()
},
mount_dir: unsafe {
CStr::from_ptr(&statfs.f_mntonname[0])
.to_string_lossy()
.into_owned()
},
mount_root: "".to_string(),
mount_option: "".to_string(),
remote: false,
dummy: false,
};
info.set_missing_fields();
info
}
}
#[cfg(any(target_os = "freebsd", target_vendor = "apple"))]
use libc::c_int;
#[cfg(any(target_os = "freebsd", target_vendor = "apple"))]
extern "C" {
#[cfg(all(target_vendor = "apple", target_arch = "x86_64"))]
#[link_name = "getmntinfo$INODE64"]
fn getmntinfo(mntbufp: *mut *mut Sstatfs, flags: c_int) -> c_int;
#[cfg(any(
all(target_os = "freebsd"),
all(target_vendor = "apple", target_arch = "aarch64")
))]
fn getmntinfo(mntbufp: *mut *mut Sstatfs, flags: c_int) -> c_int;
}
#[cfg(target_os = "linux")]
use std::fs::File;
#[cfg(target_os = "linux")]
use std::io::{BufRead, BufReader};
#[cfg(any(target_vendor = "apple", target_os = "freebsd", target_os = "windows"))]
use std::ptr;
#[cfg(any(target_vendor = "apple", target_os = "freebsd"))]
use std::slice;
/// Read file system list.
pub fn read_fs_list() -> Vec<MountInfo> {
#[cfg(target_os = "linux")]
{
let (file_name, fobj) = File::open(LINUX_MOUNTINFO)
.map(|f| (LINUX_MOUNTINFO, f))
.or_else(|_| File::open(LINUX_MTAB).map(|f| (LINUX_MTAB, f)))
.expect("failed to find mount list files");
let reader = BufReader::new(fobj);
reader
.lines()
.filter_map(|line| line.ok())
.filter_map(|line| {
let raw_data = line.split_whitespace().collect::<Vec<&str>>();
MountInfo::new(file_name, raw_data)
})
.collect::<Vec<_>>()
}
#[cfg(any(target_os = "freebsd", target_vendor = "apple"))]
{
let mut mptr: *mut Sstatfs = ptr::null_mut();
let len = unsafe { getmntinfo(&mut mptr, 1_i32) };
if len < 0 {
crash!(1, "getmntinfo failed");
}
let mounts = unsafe { slice::from_raw_parts(mptr, len as usize) };
mounts
.iter()
.map(|m| MountInfo::from(*m))
.collect::<Vec<_>>()
}
#[cfg(windows)]
{
let mut volume_name_buf = [0u16; MAX_PATH];
// As recommended in the MS documentation, retrieve the first volume before the others
let find_handle = unsafe {
FindFirstVolumeW(volume_name_buf.as_mut_ptr(), volume_name_buf.len() as DWORD)
};
if INVALID_HANDLE_VALUE == find_handle {
crash!(EXIT_ERR, "FindFirstVolumeW failed: {}", unsafe {
GetLastError()
});
}
let mut mounts = Vec::<MountInfo>::new();
loop {
let volume_name = LPWSTR2String(&volume_name_buf);
if !volume_name.starts_with("\\\\?\\") || !volume_name.ends_with('\\') {
show_warning!("A bad path was skipped: {}", volume_name);
continue;
}
if let Some(m) = MountInfo::new(volume_name) {
mounts.push(m);
}
if 0 == unsafe {
FindNextVolumeW(
find_handle,
volume_name_buf.as_mut_ptr(),
volume_name_buf.len() as DWORD,
)
} {
let err = unsafe { GetLastError() };
if err != winapi::shared::winerror::ERROR_NO_MORE_FILES {
crash!(EXIT_ERR, "FindNextVolumeW failed: {}", err);
}
break;
}
}
unsafe {
FindVolumeClose(find_handle);
}
mounts
}
}
#[derive(Debug, Clone)]
pub struct FsUsage {
pub blocksize: u64,
pub blocks: u64,
pub bfree: u64,
pub bavail: u64,
pub bavail_top_bit_set: bool,
pub files: u64,
pub ffree: u64,
}
impl FsUsage {
#[cfg(unix)]
pub fn new(statvfs: Sstatfs) -> FsUsage {
{
FsUsage {
blocksize: statvfs.f_bsize as u64, // or `statvfs.f_frsize` ?
blocks: statvfs.f_blocks as u64,
bfree: statvfs.f_bfree as u64,
bavail: statvfs.f_bavail as u64,
bavail_top_bit_set: ((statvfs.f_bavail as u64) & (1u64.rotate_right(1))) != 0,
files: statvfs.f_files as u64,
ffree: statvfs.f_ffree as u64,
}
}
}
#[cfg(not(unix))]
pub fn new(path: &Path) -> FsUsage {
let mut root_path = [0u16; MAX_PATH];
let success = unsafe {
GetVolumePathNamesForVolumeNameW(
//path_utf8.as_ptr(),
String2LPWSTR!(path.as_os_str()),
root_path.as_mut_ptr(),
root_path.len() as DWORD,
ptr::null_mut(),
)
};
if 0 == success {
crash!(
EXIT_ERR,
"GetVolumePathNamesForVolumeNameW failed: {}",
unsafe { GetLastError() }
);
}
let mut sectors_per_cluster = 0;
let mut bytes_per_sector = 0;
let mut number_of_free_clusters = 0;
let mut total_number_of_clusters = 0;
let success = unsafe {
GetDiskFreeSpaceW(
String2LPWSTR!(path.as_os_str()),
&mut sectors_per_cluster,
&mut bytes_per_sector,
&mut number_of_free_clusters,
&mut total_number_of_clusters,
)
};
if 0 == success {
// Fails in case of CD for example
//crash!(EXIT_ERR, "GetDiskFreeSpaceW failed: {}", unsafe {
//GetLastError()
//});
}
let bytes_per_cluster = sectors_per_cluster as u64 * bytes_per_sector as u64;
FsUsage {
// f_bsize File system block size.
blocksize: bytes_per_cluster as u64,
// f_blocks - Total number of blocks on the file system, in units of f_frsize.
// frsize = Fundamental file system block size (fragment size).
blocks: total_number_of_clusters as u64,
// Total number of free blocks.
bfree: number_of_free_clusters as u64,
// Total number of free blocks available to non-privileged processes.
bavail: 0,
bavail_top_bit_set: ((bytes_per_sector as u64) & (1u64.rotate_right(1))) != 0,
// Total number of file nodes (inodes) on the file system.
files: 0, // Not available on windows
// Total number of free file nodes (inodes).
ffree: 4096, // Meaningless on Windows
}
}
}
#[cfg(unix)]
pub trait FsMeta {
fn fs_type(&self) -> i64;
fn iosize(&self) -> u64;
fn blksize(&self) -> i64;
fn total_blocks(&self) -> u64;
fn free_blocks(&self) -> u64;
fn avail_blocks(&self) -> u64;
fn total_fnodes(&self) -> u64;
fn free_fnodes(&self) -> u64;
fn fsid(&self) -> u64;
fn namelen(&self) -> u64;
}
#[cfg(unix)]
impl FsMeta for Sstatfs {
fn blksize(&self) -> i64 {
self.f_bsize as i64
}
fn total_blocks(&self) -> u64 {
self.f_blocks as u64
}
fn free_blocks(&self) -> u64 {
self.f_bfree as u64
}
fn avail_blocks(&self) -> u64 {
self.f_bavail as u64
}
fn total_fnodes(&self) -> u64 {
self.f_files as u64
}
fn free_fnodes(&self) -> u64 {
self.f_ffree as u64
}
#[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_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!()
}
#[cfg(target_os = "linux")]
fn iosize(&self) -> u64 {
self.f_frsize as u64
}
#[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_vendor = "apple", target_os = "freebsd", target_os = "linux")))]
fn iosize(&self) -> u64 {
self.f_bsize as u64
}
// Linux, SunOS, HP-UX, 4.4BSD, FreeBSD have a system call statfs() that returns
// a struct statfs, containing a fsid_t f_fsid, where fsid_t is defined
// as struct { int val[2]; }
//
// Solaris, Irix and POSIX have a system call statvfs(2) that returns a
// struct statvfs, containing an unsigned long f_fsid
#[cfg(any(target_vendor = "apple", target_os = "freebsd", target_os = "linux"))]
fn fsid(&self) -> u64 {
let f_fsid: &[u32; 2] =
unsafe { &*(&self.f_fsid as *const libc::fsid_t as *const [u32; 2]) };
(u64::from(f_fsid[0])) << 32 | u64::from(f_fsid[1])
}
#[cfg(not(any(target_vendor = "apple", target_os = "freebsd", target_os = "linux")))]
fn fsid(&self) -> u64 {
self.f_fsid as u64
}
#[cfg(target_os = "linux")]
fn namelen(&self) -> u64 {
self.f_namelen as u64
}
#[cfg(target_vendor = "apple")]
fn namelen(&self) -> u64 {
1024
}
#[cfg(target_os = "freebsd")]
fn namelen(&self) -> u64 {
self.f_namemax as u64
}
// XXX: should everything just use statvfs?
#[cfg(not(any(target_vendor = "apple", target_os = "freebsd", target_os = "linux")))]
fn namelen(&self) -> u64 {
self.f_namemax as u64
}
}
#[cfg(unix)]
pub fn statfs<P: AsRef<Path>>(path: P) -> Result<Sstatfs, String>
where
Vec<u8>: From<P>,
{
match CString::new(path) {
Ok(p) => {
let mut buffer: Sstatfs = unsafe { mem::zeroed() };
unsafe {
match statfs_fn(p.as_ptr(), &mut buffer) {
0 => Ok(buffer),
_ => {
let errno = IOError::last_os_error().raw_os_error().unwrap_or(0);
Err(CString::from_raw(strerror(errno))
.into_string()
.unwrap_or_else(|_| "Unknown Error".to_owned()))
}
}
}
}
Err(e) => Err(e.to_string()),
}
}
pub fn pretty_time(sec: i64, nsec: i64) -> String {
// sec == seconds since UNIX_EPOCH
// nsec == nanoseconds since (UNIX_EPOCH + sec)
let tm = time::at(Timespec::new(sec, nsec as i32));
let res = time::strftime("%Y-%m-%d %H:%M:%S.%f %z", &tm).unwrap();
if res.ends_with(" -0000") {
res.replace(" -0000", " +0000")
} else {
res
}
}
#[cfg(unix)]
pub fn pretty_filetype<'a>(mode: mode_t, size: u64) -> &'a str {
match mode & S_IFMT {
S_IFREG => {
if size != 0 {
"regular file"
} else {
"regular empty file"
}
}
S_IFDIR => "directory",
S_IFLNK => "symbolic link",
S_IFCHR => "character special file",
S_IFBLK => "block special file",
S_IFIFO => "fifo",
S_IFSOCK => "socket",
// TODO: Other file types
// See coreutils/gnulib/lib/file-type.c
_ => "weird file",
}
}
pub fn pretty_fstype<'a>(fstype: i64) -> Cow<'a, str> {
match fstype {
0x6163_6673 => "acfs".into(),
0xADF5 => "adfs".into(),
0xADFF => "affs".into(),
0x5346_414F => "afs".into(),
0x0904_1934 => "anon-inode FS".into(),
0x6175_6673 => "aufs".into(),
0x0187 => "autofs".into(),
0x4246_5331 => "befs".into(),
0x6264_6576 => "bdevfs".into(),
0x1BAD_FACE => "bfs".into(),
0xCAFE_4A11 => "bpf_fs".into(),
0x4249_4E4D => "binfmt_misc".into(),
0x9123_683E => "btrfs".into(),
0x7372_7279 => "btrfs_test".into(),
0x00C3_6400 => "ceph".into(),
0x0027_E0EB => "cgroupfs".into(),
0xFF53_4D42 => "cifs".into(),
0x7375_7245 => "coda".into(),
0x012F_F7B7 => "coh".into(),
0x6265_6570 => "configfs".into(),
0x28CD_3D45 => "cramfs".into(),
0x453D_CD28 => "cramfs-wend".into(),
0x6462_6720 => "debugfs".into(),
0x1373 => "devfs".into(),
0x1CD1 => "devpts".into(),
0xF15F => "ecryptfs".into(),
0xDE5E_81E4 => "efivarfs".into(),
0x0041_4A53 => "efs".into(),
0x5DF5 => "exofs".into(),
0x137D => "ext".into(),
0xEF53 => "ext2/ext3".into(),
0xEF51 => "ext2".into(),
0xF2F5_2010 => "f2fs".into(),
0x4006 => "fat".into(),
0x1983_0326 => "fhgfs".into(),
0x6573_5546 => "fuseblk".into(),
0x6573_5543 => "fusectl".into(),
0x0BAD_1DEA => "futexfs".into(),
0x0116_1970 => "gfs/gfs2".into(),
0x4750_4653 => "gpfs".into(),
0x4244 => "hfs".into(),
0x482B => "hfs+".into(),
0x4858 => "hfsx".into(),
0x00C0_FFEE => "hostfs".into(),
0xF995_E849 => "hpfs".into(),
0x9584_58F6 => "hugetlbfs".into(),
0x1130_7854 => "inodefs".into(),
0x0131_11A8 => "ibrix".into(),
0x2BAD_1DEA => "inotifyfs".into(),
0x9660 => "isofs".into(),
0x4004 => "isofs".into(),
0x4000 => "isofs".into(),
0x07C0 => "jffs".into(),
0x72B6 => "jffs2".into(),
0x3153_464A => "jfs".into(),
0x6B41_4653 => "k-afs".into(),
0xC97E_8168 => "logfs".into(),
0x0BD0_0BD0 => "lustre".into(),
0x5346_314D => "m1fs".into(),
0x137F => "minix".into(),
0x138F => "minix (30 char.)".into(),
0x2468 => "minix v2".into(),
0x2478 => "minix v2 (30 char.)".into(),
0x4D5A => "minix3".into(),
0x1980_0202 => "mqueue".into(),
0x4D44 => "msdos".into(),
0x564C => "novell".into(),
0x6969 => "nfs".into(),
0x6E66_7364 => "nfsd".into(),
0x3434 => "nilfs".into(),
0x6E73_6673 => "nsfs".into(),
0x5346_544E => "ntfs".into(),
0x9FA1 => "openprom".into(),
0x7461_636F => "ocfs2".into(),
0x794C_7630 => "overlayfs".into(),
0xAAD7_AAEA => "panfs".into(),
0x5049_5045 => "pipefs".into(),
0x7C7C_6673 => "prl_fs".into(),
0x9FA0 => "proc".into(),
0x6165_676C => "pstorefs".into(),
0x002F => "qnx4".into(),
0x6819_1122 => "qnx6".into(),
0x8584_58F6 => "ramfs".into(),
0x5265_4973 => "reiserfs".into(),
0x7275 => "romfs".into(),
0x6759_6969 => "rpc_pipefs".into(),
0x7363_6673 => "securityfs".into(),
0xF97C_FF8C => "selinux".into(),
0x4341_5D53 => "smackfs".into(),
0x517B => "smb".into(),
0xFE53_4D42 => "smb2".into(),
0xBEEF_DEAD => "snfs".into(),
0x534F_434B => "sockfs".into(),
0x7371_7368 => "squashfs".into(),
0x6265_6572 => "sysfs".into(),
0x012F_F7B6 => "sysv2".into(),
0x012F_F7B5 => "sysv4".into(),
0x0102_1994 => "tmpfs".into(),
0x7472_6163 => "tracefs".into(),
0x2405_1905 => "ubifs".into(),
0x1501_3346 => "udf".into(),
0x0001_1954 => "ufs".into(),
0x5419_0100 => "ufs".into(),
0x9FA2 => "usbdevfs".into(),
0x0102_1997 => "v9fs".into(),
0xBACB_ACBC => "vmhgfs".into(),
0xA501_FCF5 => "vxfs".into(),
0x565A_4653 => "vzfs".into(),
0x5346_4846 => "wslfs".into(),
0xABBA_1974 => "xenfs".into(),
0x012F_F7B4 => "xenix".into(),
0x5846_5342 => "xfs".into(),
0x012F_D16D => "xia".into(),
0x2FC1_2FC1 => "zfs".into(),
other => format!("UNKNOWN ({:#x})", other).into(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg(unix)]
fn test_file_type() {
assert_eq!("block special file", pretty_filetype(S_IFBLK, 0));
assert_eq!("character special file", pretty_filetype(S_IFCHR, 0));
assert_eq!("regular file", pretty_filetype(S_IFREG, 1));
assert_eq!("regular empty file", pretty_filetype(S_IFREG, 0));
assert_eq!("weird file", pretty_filetype(0, 0));
}
#[test]
fn test_fs_type() {
assert_eq!("ext2/ext3", pretty_fstype(0xEF53));
assert_eq!("tmpfs", pretty_fstype(0x01021994));
assert_eq!("nfs", pretty_fstype(0x6969));
assert_eq!("btrfs", pretty_fstype(0x9123683e));
assert_eq!("xfs", pretty_fstype(0x58465342));
assert_eq!("zfs", pretty_fstype(0x2FC12FC1));
assert_eq!("ntfs", pretty_fstype(0x5346544e));
assert_eq!("fat", pretty_fstype(0x4006));
assert_eq!("UNKNOWN (0x1234)", pretty_fstype(0x1234));
}
}

View file

@ -132,19 +132,15 @@ fn parse_change(mode: &str, fperm: u32, considering_dir: bool) -> (u32, usize) {
(srwx, pos) (srwx, pos)
} }
pub fn parse_mode(mode: Option<String>) -> Result<mode_t, String> { pub fn parse_mode(mode: &str) -> Result<mode_t, String> {
let fperm = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH; let fperm = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
if let Some(mode) = mode { let arr: &[char] = &['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
let arr: &[char] = &['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; let result = if mode.contains(arr) {
let result = if mode.contains(arr) { parse_numeric(fperm as u32, mode)
parse_numeric(fperm as u32, mode.as_str())
} else {
parse_symbolic(fperm as u32, mode.as_str(), true)
};
result.map(|mode| mode as mode_t)
} else { } else {
Ok(fperm) parse_symbolic(fperm as u32, mode, true)
} };
result.map(|mode| mode as mode_t)
} }
#[cfg(test)] #[cfg(test)]
@ -152,20 +148,19 @@ mod test {
#[test] #[test]
fn symbolic_modes() { fn symbolic_modes() {
assert_eq!(super::parse_mode(Some("u+x".to_owned())).unwrap(), 0o766); assert_eq!(super::parse_mode("u+x").unwrap(), 0o766);
assert_eq!( assert_eq!(
super::parse_mode(Some("+x".to_owned())).unwrap(), super::parse_mode("+x").unwrap(),
if !crate::os::is_wsl_1() { 0o777 } else { 0o776 } if !crate::os::is_wsl_1() { 0o777 } else { 0o776 }
); );
assert_eq!(super::parse_mode(Some("a-w".to_owned())).unwrap(), 0o444); assert_eq!(super::parse_mode("a-w").unwrap(), 0o444);
assert_eq!(super::parse_mode(Some("g-r".to_owned())).unwrap(), 0o626); assert_eq!(super::parse_mode("g-r").unwrap(), 0o626);
} }
#[test] #[test]
fn numeric_modes() { fn numeric_modes() {
assert_eq!(super::parse_mode(Some("644".to_owned())).unwrap(), 0o644); assert_eq!(super::parse_mode("644").unwrap(), 0o644);
assert_eq!(super::parse_mode(Some("+100".to_owned())).unwrap(), 0o766); assert_eq!(super::parse_mode("+100").unwrap(), 0o766);
assert_eq!(super::parse_mode(Some("-4".to_owned())).unwrap(), 0o662); assert_eq!(super::parse_mode("-4").unwrap(), 0o662);
assert_eq!(super::parse_mode(None).unwrap(), 0o666);
} }
} }

View file

@ -0,0 +1,134 @@
//! A fixed-size ring buffer.
use std::collections::VecDeque;
/// A fixed-size ring buffer backed by a `VecDeque`.
///
/// If the ring buffer is not full, then calling the [`push_back`]
/// method appends elements, as in a [`VecDeque`]. If the ring buffer
/// is full, then calling [`push_back`] removes the element at the
/// front of the buffer (in a first-in, first-out manner) before
/// appending the new element to the back of the buffer.
///
/// Use [`from_iter`] to take the last `size` elements from an
/// iterator.
///
/// # Examples
///
/// After exceeding the size limit, the oldest elements are dropped in
/// favor of the newest element:
///
/// ```rust,ignore
/// let mut buffer: RingBuffer<u8> = RingBuffer::new(2);
/// buffer.push_back(0);
/// buffer.push_back(1);
/// buffer.push_back(2);
/// assert_eq!(vec![1, 2], buffer.data);
/// ```
///
/// Take the last `n` elements from an iterator:
///
/// ```rust,ignore
/// let iter = [0, 1, 2].iter();
/// let actual = RingBuffer::from_iter(iter, 2).data;
/// let expected = VecDeque::from_iter([1, 2].iter());
/// assert_eq!(expected, actual);
/// ```
pub struct RingBuffer<T> {
pub data: VecDeque<T>,
size: usize,
}
impl<T> RingBuffer<T> {
pub fn new(size: usize) -> RingBuffer<T> {
RingBuffer {
data: VecDeque::new(),
size,
}
}
pub fn from_iter(iter: impl Iterator<Item = T>, size: usize) -> RingBuffer<T> {
let mut ringbuf = RingBuffer::new(size);
for value in iter {
ringbuf.push_back(value);
}
ringbuf
}
/// Append a value to the end of the ring buffer.
///
/// If the ring buffer is not full, this method return [`None`]. If
/// the ring buffer is full, appending a new element will cause the
/// oldest element to be evicted. In that case this method returns
/// that element, or `None`.
///
/// In the special case where the size limit is zero, each call to
/// this method with input `value` returns `Some(value)`, because
/// the input is immediately evicted.
///
/// # Examples
///
/// Appending an element when the buffer is full returns the oldest
/// element:
///
/// ```rust,ignore
/// let mut buf = RingBuffer::new(3);
/// assert_eq!(None, buf.push_back(0));
/// assert_eq!(None, buf.push_back(1));
/// assert_eq!(None, buf.push_back(2));
/// assert_eq!(Some(0), buf.push_back(3));
/// ```
///
/// If the size limit is zero, then this method always returns the
/// input value:
///
/// ```rust,ignore
/// let mut buf = RingBuffer::new(0);
/// assert_eq!(Some(0), buf.push_back(0));
/// assert_eq!(Some(1), buf.push_back(1));
/// assert_eq!(Some(2), buf.push_back(2));
/// ```
pub fn push_back(&mut self, value: T) -> Option<T> {
if self.size == 0 {
return Some(value);
}
let result = if self.size <= self.data.len() {
self.data.pop_front()
} else {
None
};
self.data.push_back(value);
result
}
}
#[cfg(test)]
mod tests {
use crate::ringbuffer::RingBuffer;
use std::collections::VecDeque;
use std::iter::FromIterator;
#[test]
fn test_size_limit_zero() {
let mut buf = RingBuffer::new(0);
assert_eq!(Some(0), buf.push_back(0));
assert_eq!(Some(1), buf.push_back(1));
assert_eq!(Some(2), buf.push_back(2));
}
#[test]
fn test_evict_oldest() {
let mut buf = RingBuffer::new(2);
assert_eq!(None, buf.push_back(0));
assert_eq!(None, buf.push_back(1));
assert_eq!(Some(0), buf.push_back(2));
}
#[test]
fn test_from_iter() {
let iter = [0, 1, 2].iter();
let actual = RingBuffer::from_iter(iter, 2).data;
let expected = VecDeque::from_iter([1, 2].iter());
assert_eq!(expected, actual);
}
}

View file

@ -54,6 +54,8 @@ pub unsafe extern "C" fn utmpxname(_file: *const libc::c_char) -> libc::c_int {
0 0
} }
pub use crate::*; // import macros from `../../macros.rs`
// In case the c_char array doesn't end with NULL // In case the c_char array doesn't end with NULL
macro_rules! chars2string { macro_rules! chars2string {
($arr:expr) => { ($arr:expr) => {
@ -188,47 +190,40 @@ impl Utmpx {
/// Canonicalize host name using DNS /// Canonicalize host name using DNS
pub fn canon_host(&self) -> IOResult<String> { pub fn canon_host(&self) -> IOResult<String> {
const AI_CANONNAME: libc::c_int = 0x2;
let host = self.host(); let host = self.host();
let host = host.split(':').next().unwrap();
let hints = libc::addrinfo { // TODO: change to use `split_once` when MSRV hits 1.52.0
ai_flags: AI_CANONNAME, // let (hostname, display) = host.split_once(':').unwrap_or((&host, ""));
ai_family: 0, let mut h = host.split(':');
ai_socktype: 0, let hostname = h.next().unwrap_or(&host);
ai_protocol: 0, let display = h.next().unwrap_or("");
ai_addrlen: 0,
ai_addr: ptr::null_mut(), if !hostname.is_empty() {
ai_canonname: ptr::null_mut(), extern crate dns_lookup;
ai_next: ptr::null_mut(), use dns_lookup::{getaddrinfo, AddrInfoHints};
};
let c_host = CString::new(host).unwrap(); const AI_CANONNAME: i32 = 0x2;
let mut res = ptr::null_mut(); let hints = AddrInfoHints {
let status = unsafe { flags: AI_CANONNAME,
libc::getaddrinfo( ..AddrInfoHints::default()
c_host.as_ptr(),
ptr::null(),
&hints as *const _,
&mut res as *mut _,
)
};
if status == 0 {
let info: libc::addrinfo = unsafe { ptr::read(res as *const _) };
// http://lists.gnu.org/archive/html/bug-coreutils/2006-09/msg00300.html
// says Darwin 7.9.0 getaddrinfo returns 0 but sets
// res->ai_canonname to NULL.
let ret = if info.ai_canonname.is_null() {
Ok(String::from(host))
} else {
Ok(unsafe { CString::from_raw(info.ai_canonname).into_string().unwrap() })
}; };
unsafe { let sockets = getaddrinfo(Some(&hostname), None, Some(hints))
libc::freeaddrinfo(res); .unwrap()
.collect::<IOResult<Vec<_>>>()?;
for socket in sockets {
if let Some(ai_canonname) = socket.canonname {
return Ok(if display.is_empty() {
ai_canonname
} else {
format!("{}:{}", ai_canonname, display)
});
}
} }
ret
} else {
Err(IOError::last_os_error())
} }
Ok(host.to_string())
} }
pub fn iter_all_records() -> UtmpxIter { pub fn iter_all_records() -> UtmpxIter {
UtmpxIter UtmpxIter
} }
@ -247,7 +242,7 @@ impl UtmpxIter {
utmpxname(cstr.as_ptr()) utmpxname(cstr.as_ptr())
}; };
if res != 0 { if res != 0 {
println!("Warning: {}", IOError::last_os_error()); show_warning!("utmpxname: {}", IOError::last_os_error());
} }
unsafe { unsafe {
setutxent(); setutxent();

View file

@ -25,6 +25,7 @@ mod features; // feature-gated code modules
mod mods; // core cross-platform modules mod mods; // core cross-platform modules
// * cross-platform modules // * cross-platform modules
pub use crate::mods::backup_control;
pub use crate::mods::coreopts; pub use crate::mods::coreopts;
pub use crate::mods::os; pub use crate::mods::os;
pub use crate::mods::panic; pub use crate::mods::panic;
@ -35,8 +36,12 @@ pub use crate::mods::ranges;
pub use crate::features::encoding; pub use crate::features::encoding;
#[cfg(feature = "fs")] #[cfg(feature = "fs")]
pub use crate::features::fs; pub use crate::features::fs;
#[cfg(feature = "fsext")]
pub use crate::features::fsext;
#[cfg(feature = "parse_time")] #[cfg(feature = "parse_time")]
pub use crate::features::parse_time; pub use crate::features::parse_time;
#[cfg(feature = "ringbuffer")]
pub use crate::features::ringbuffer;
#[cfg(feature = "zero-copy")] #[cfg(feature = "zero-copy")]
pub use crate::features::zero_copy; pub use crate::features::zero_copy;
@ -187,6 +192,7 @@ mod tests {
vec.into_iter().collect_str(handling) vec.into_iter().collect_str(handling)
} }
#[cfg(any(unix, target_os = "redox"))]
fn test_invalid_utf8_args_lossy(os_str: &OsStr) { fn test_invalid_utf8_args_lossy(os_str: &OsStr) {
//assert our string is invalid utf8 //assert our string is invalid utf8
assert!(os_str.to_os_string().into_string().is_err()); assert!(os_str.to_os_string().into_string().is_err());
@ -210,6 +216,7 @@ mod tests {
); );
} }
#[cfg(any(unix, target_os = "redox"))]
fn test_invalid_utf8_args_ignore(os_str: &OsStr) { fn test_invalid_utf8_args_ignore(os_str: &OsStr) {
//assert our string is invalid utf8 //assert our string is invalid utf8
assert!(os_str.to_os_string().into_string().is_err()); assert!(os_str.to_os_string().into_string().is_err());
@ -234,7 +241,7 @@ mod tests {
//create a vector containing only correct encoding //create a vector containing only correct encoding
let test_vec = make_os_vec(&OsString::from("test2")); let test_vec = make_os_vec(&OsString::from("test2"));
//expect complete conversion without losses, even when lossy conversion is accepted //expect complete conversion without losses, even when lossy conversion is accepted
let _ = collect_os_str(test_vec.clone(), InvalidEncodingHandling::ConvertLossy) let _ = collect_os_str(test_vec, InvalidEncodingHandling::ConvertLossy)
.expect_complete("Lossy conversion not expected in this test"); .expect_complete("Lossy conversion not expected in this test");
} }

View file

@ -25,7 +25,7 @@ macro_rules! executable(
#[macro_export] #[macro_export]
macro_rules! show_error( macro_rules! show_error(
($($args:tt)+) => ({ ($($args:tt)+) => ({
eprint!("{}: error: ", executable!()); eprint!("{}: ", executable!());
eprintln!($($args)+); eprintln!($($args)+);
}) })
); );
@ -47,15 +47,6 @@ 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)+) => ({
eprint!("{}: ", executable!());
eprintln!($($args)+);
})
);
/// Show a bad inocation help message in a similar style to GNU coreutils. /// Show a bad inocation help message in a similar style to GNU coreutils.
#[macro_export] #[macro_export]
macro_rules! show_usage_error( macro_rules! show_usage_error(
@ -176,13 +167,6 @@ macro_rules! msg_invalid_input {
}; };
} }
#[macro_export]
macro_rules! snippet_no_file_at_path {
($path:expr) => {
format!("nonexistent path {}", $path)
};
}
// -- message templates : invalid input : flag // -- message templates : invalid input : flag
#[macro_export] #[macro_export]
@ -229,55 +213,6 @@ macro_rules! msg_opt_invalid_should_be {
}; };
} }
// -- message templates : invalid input : args
#[macro_export]
macro_rules! msg_arg_invalid_value {
($expects:expr, $received:expr) => {
msg_invalid_input!(format!(
"expects its argument to be {}, but was provided {}",
$expects, $received
))
};
}
#[macro_export]
macro_rules! msg_args_invalid_value {
($expects:expr, $received:expr) => {
msg_invalid_input!(format!(
"expects its arguments to be {}, but was provided {}",
$expects, $received
))
};
($msg:expr) => {
msg_invalid_input!($msg)
};
}
#[macro_export]
macro_rules! msg_args_nonexistent_file {
($received:expr) => {
msg_args_invalid_value!("paths to files", snippet_no_file_at_path!($received))
};
}
#[macro_export]
macro_rules! msg_wrong_number_of_arguments {
() => {
msg_args_invalid_value!("wrong number of arguments")
};
($min:expr, $max:expr) => {
msg_args_invalid_value!(format!("expects {}-{} arguments", $min, $max))
};
($exact:expr) => {
if $exact == 1 {
msg_args_invalid_value!("expects 1 argument")
} else {
msg_args_invalid_value!(format!("expects {} arguments", $exact))
}
};
}
// -- message templates : invalid input : input combinations // -- message templates : invalid input : input combinations
#[macro_export] #[macro_export]

View file

@ -1,5 +1,6 @@
// mods ~ cross-platforms modules (core/bundler file) // mods ~ cross-platforms modules (core/bundler file)
pub mod backup_control;
pub mod coreopts; pub mod coreopts;
pub mod os; pub mod os;
pub mod panic; pub mod panic;

View file

@ -0,0 +1,97 @@
use std::{
env,
path::{Path, PathBuf},
};
pub static BACKUP_CONTROL_VALUES: &[&str] = &[
"simple", "never", "numbered", "t", "existing", "nil", "none", "off",
];
pub static BACKUP_CONTROL_LONG_HELP: &str = "The backup suffix is '~', unless set with --suffix or SIMPLE_BACKUP_SUFFIX. Here are the version control values:
none, off
never make backups (even if --backup is given)
numbered, t
make numbered backups
existing, nil
numbered if numbered backups exist, simple otherwise
simple, never
always make simple backups";
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum BackupMode {
NoBackup,
SimpleBackup,
NumberedBackup,
ExistingBackup,
}
pub fn determine_backup_suffix(supplied_suffix: Option<&str>) -> String {
if let Some(suffix) = supplied_suffix {
String::from(suffix)
} else {
env::var("SIMPLE_BACKUP_SUFFIX").unwrap_or("~".to_owned())
}
}
pub fn determine_backup_mode(backup_opt_exists: bool, backup_opt: Option<&str>) -> BackupMode {
if backup_opt_exists {
match backup_opt.map(String::from) {
// default is existing, see:
// https://www.gnu.org/software/coreutils/manual/html_node/Backup-options.html
None => BackupMode::ExistingBackup,
Some(mode) => match &mode[..] {
"simple" | "never" => BackupMode::SimpleBackup,
"numbered" | "t" => BackupMode::NumberedBackup,
"existing" | "nil" => BackupMode::ExistingBackup,
"none" | "off" => BackupMode::NoBackup,
_ => panic!(), // cannot happen as it is managed by clap
},
}
} else {
BackupMode::NoBackup
}
}
pub fn get_backup_path(
backup_mode: BackupMode,
backup_path: &Path,
suffix: &str,
) -> Option<PathBuf> {
match backup_mode {
BackupMode::NoBackup => None,
BackupMode::SimpleBackup => Some(simple_backup_path(backup_path, suffix)),
BackupMode::NumberedBackup => Some(numbered_backup_path(backup_path)),
BackupMode::ExistingBackup => Some(existing_backup_path(backup_path, suffix)),
}
}
pub fn simple_backup_path(path: &Path, suffix: &str) -> PathBuf {
let mut p = path.to_string_lossy().into_owned();
p.push_str(suffix);
PathBuf::from(p)
}
pub fn numbered_backup_path(path: &Path) -> PathBuf {
for i in 1_u64.. {
let path_str = &format!("{}.~{}~", path.to_string_lossy(), i);
let path = Path::new(path_str);
if !path.exists() {
return path.to_path_buf();
}
}
panic!("cannot create backup")
}
pub fn existing_backup_path(path: &Path, suffix: &str) -> PathBuf {
let test_path_str = &format!("{}.~1~", path.to_string_lossy());
let test_path = Path::new(test_path_str);
if test_path.exists() {
numbered_backup_path(path)
} else {
simple_backup_path(path, suffix)
}
}

View file

@ -0,0 +1,26 @@
[package]
name = "uu_factor_benches"
version = "0.0.0"
authors = ["nicoo <nicoo@debian.org>"]
license = "MIT"
description = "Benchmarks for the uu_factor integer factorization tool"
homepage = "https://github.com/uutils/coreutils"
edition = "2018"
[dependencies]
uu_factor = { path = "../../../src/uu/factor" }
[dev-dependencies]
array-init = "2.0.0"
criterion = "0.3"
rand = "0.7"
rand_chacha = "0.2.2"
[[bench]]
name = "gcd"
harness = false
[[bench]]
name = "table"
harness = false

View file

@ -0,0 +1,78 @@
use array_init::array_init;
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use std::convert::TryInto;
use uu_factor::{table::*, Factors};
fn table(c: &mut Criterion) {
#[cfg(target_os = "linux")]
check_personality();
const INPUT_SIZE: usize = 128;
assert!(
INPUT_SIZE % CHUNK_SIZE == 0,
"INPUT_SIZE ({}) is not divisible by CHUNK_SIZE ({})",
INPUT_SIZE,
CHUNK_SIZE
);
let inputs = {
// Deterministic RNG; use an explicitely-named RNG to guarantee stability
use rand::{RngCore, SeedableRng};
use rand_chacha::ChaCha8Rng;
const SEED: u64 = 0xdead_bebe_ea75_cafe;
let mut rng = ChaCha8Rng::seed_from_u64(SEED);
std::iter::repeat_with(move || array_init::<_, _, INPUT_SIZE>(|_| rng.next_u64()))
};
let mut group = c.benchmark_group("table");
group.throughput(Throughput::Elements(INPUT_SIZE as _));
for a in inputs.take(10) {
let a_str = format!("{:?}", a);
group.bench_with_input(BenchmarkId::new("factor_chunk", &a_str), &a, |b, &a| {
b.iter(|| {
let mut n_s = a.clone();
let mut f_s: [_; INPUT_SIZE] = array_init(|_| Factors::one());
for (n_s, f_s) in n_s.chunks_mut(CHUNK_SIZE).zip(f_s.chunks_mut(CHUNK_SIZE)) {
factor_chunk(n_s.try_into().unwrap(), f_s.try_into().unwrap())
}
})
});
group.bench_with_input(BenchmarkId::new("factor", &a_str), &a, |b, &a| {
b.iter(|| {
let mut n_s = a.clone();
let mut f_s: [_; INPUT_SIZE] = array_init(|_| Factors::one());
for (n, f) in n_s.iter_mut().zip(f_s.iter_mut()) {
factor(n, f)
}
})
});
}
group.finish()
}
#[cfg(target_os = "linux")]
fn check_personality() {
use std::fs;
const ADDR_NO_RANDOMIZE: u64 = 0x0040000;
const PERSONALITY_PATH: &'static str = "/proc/self/personality";
let p_string = fs::read_to_string(PERSONALITY_PATH)
.expect(&format!("Couldn't read '{}'", PERSONALITY_PATH))
.strip_suffix("\n")
.unwrap()
.to_owned();
let personality = u64::from_str_radix(&p_string, 16).expect(&format!(
"Expected a hex value for personality, got '{:?}'",
p_string
));
if personality & ADDR_NO_RANDOMIZE == 0 {
eprintln!(
"WARNING: Benchmarking with ASLR enabled (personality is {:x}), results might not be reproducible.",
personality
);
}
}
criterion_group!(benches, table);
criterion_main!(benches);

View file

@ -98,7 +98,7 @@ fn test_wrap_bad_arg() {
.arg(wrap_param) .arg(wrap_param)
.arg("b") .arg("b")
.fails() .fails()
.stderr_only("base32: error: Invalid wrap size: b: invalid digit found in string\n"); .stderr_only("base32: Invalid wrap size: b: invalid digit found in string\n");
} }
} }
@ -109,7 +109,7 @@ fn test_base32_extra_operand() {
.arg("a.txt") .arg("a.txt")
.arg("a.txt") .arg("a.txt")
.fails() .fails()
.stderr_only("base32: error: extra operand a.txt"); .stderr_only("base32: extra operand a.txt");
} }
#[test] #[test]
@ -117,5 +117,5 @@ fn test_base32_file_not_found() {
new_ucmd!() new_ucmd!()
.arg("a.txt") .arg("a.txt")
.fails() .fails()
.stderr_only("base32: error: a.txt: No such file or directory"); .stderr_only("base32: a.txt: No such file or directory");
} }

View file

@ -88,7 +88,7 @@ fn test_wrap_bad_arg() {
.arg(wrap_param) .arg(wrap_param)
.arg("b") .arg("b")
.fails() .fails()
.stderr_only("base64: error: Invalid wrap size: b: invalid digit found in string\n"); .stderr_only("base64: Invalid wrap size: b: invalid digit found in string\n");
} }
} }
@ -99,7 +99,7 @@ fn test_base64_extra_operand() {
.arg("a.txt") .arg("a.txt")
.arg("a.txt") .arg("a.txt")
.fails() .fails()
.stderr_only("base64: error: extra operand a.txt"); .stderr_only("base64: extra operand a.txt");
} }
#[test] #[test]
@ -107,5 +107,5 @@ fn test_base64_file_not_found() {
new_ucmd!() new_ucmd!()
.arg("a.txt") .arg("a.txt")
.fails() .fails()
.stderr_only("base64: error: a.txt: No such file or directory"); .stderr_only("base64: a.txt: No such file or directory");
} }

View file

@ -1,4 +1,5 @@
use crate::common::util::*; use crate::common::util::*;
#[cfg(any(unix, target_os = "redox"))]
use std::ffi::OsStr; use std::ffi::OsStr;
#[test] #[test]
@ -108,7 +109,7 @@ fn test_no_args() {
fn test_no_args_output() { fn test_no_args_output() {
new_ucmd!() new_ucmd!()
.fails() .fails()
.stderr_is("basename: error: missing operand\nTry 'basename --help' for more information."); .stderr_is("basename: missing operand\nTry 'basename --help' for more information.");
} }
#[test] #[test]
@ -118,11 +119,13 @@ fn test_too_many_args() {
#[test] #[test]
fn test_too_many_args_output() { fn test_too_many_args_output() {
new_ucmd!().args(&["a", "b", "c"]).fails().stderr_is( new_ucmd!()
"basename: error: extra operand 'c'\nTry 'basename --help' for more information.", .args(&["a", "b", "c"])
); .fails()
.stderr_is("basename: extra operand 'c'\nTry 'basename --help' for more information.");
} }
#[cfg(any(unix, target_os = "redox"))]
fn test_invalid_utf8_args(os_str: &OsStr) { fn test_invalid_utf8_args(os_str: &OsStr) {
let test_vec = vec![os_str.to_os_string()]; let test_vec = vec![os_str.to_os_string()];
new_ucmd!().args(&test_vec).succeeds().stdout_is("fo<EFBFBD>o\n"); new_ucmd!().args(&test_vec).succeeds().stdout_is("fo<EFBFBD>o\n");

View file

@ -347,7 +347,13 @@ fn test_squeeze_blank_before_numbering() {
#[cfg(unix)] #[cfg(unix)]
fn test_dev_random() { fn test_dev_random() {
let mut buf = [0; 2048]; let mut buf = [0; 2048];
let mut proc = new_ucmd!().args(&["/dev/random"]).run_no_wait(); #[cfg(target_os = "linux")]
const DEV_RANDOM: &str = "/dev/urandom";
#[cfg(not(target_os = "linux"))]
const DEV_RANDOM: &str = "/dev/random";
let mut proc = new_ucmd!().args(&[DEV_RANDOM]).run_no_wait();
let mut proc_stdout = proc.stdout.take().unwrap(); let mut proc_stdout = proc.stdout.take().unwrap();
proc_stdout.read_exact(&mut buf).unwrap(); proc_stdout.read_exact(&mut buf).unwrap();
@ -395,14 +401,14 @@ fn test_dev_full_show_all() {
#[test] #[test]
#[cfg(unix)] #[cfg(unix)]
#[ignore]
fn test_domain_socket() { fn test_domain_socket() {
use std::io::prelude::*; use std::io::prelude::*;
use std::sync::{Arc, Barrier}; use std::sync::{Arc, Barrier};
use std::thread; use std::thread;
use tempdir::TempDir;
use unix_socket::UnixListener; use unix_socket::UnixListener;
let dir = TempDir::new("unix_socket").expect("failed to create dir"); let dir = tempfile::Builder::new().prefix("unix_socket").tempdir().expect("failed to create dir");
let socket_path = dir.path().join("sock"); let socket_path = dir.path().join("sock");
let listener = UnixListener::bind(&socket_path).expect("failed to create socket"); let listener = UnixListener::bind(&socket_path).expect("failed to create socket");

View file

@ -282,6 +282,26 @@ fn test_chmod_reference_file() {
run_single_test(&tests[0], at, ucmd); run_single_test(&tests[0], at, ucmd);
} }
#[test]
fn test_permission_denied() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
at.mkdir("d/");
at.mkdir("d/no-x");
at.mkdir("d/no-x/y");
scene.ucmd().arg("u=rw").arg("d/no-x").succeeds();
scene
.ucmd()
.arg("-R")
.arg("o=r")
.arg("d")
.fails()
.stderr_is("chmod: 'd/no-x/y': Permission denied");
}
#[test] #[test]
fn test_chmod_recursive() { fn test_chmod_recursive() {
let _guard = UMASK_MUTEX.lock(); let _guard = UMASK_MUTEX.lock();
@ -338,7 +358,7 @@ fn test_chmod_preserve_root() {
.arg("755") .arg("755")
.arg("/") .arg("/")
.fails() .fails()
.stderr_contains(&"chmod: error: it is dangerous to operate recursively on '/'"); .stderr_contains(&"chmod: it is dangerous to operate recursively on '/'");
} }
#[test] #[test]

View file

@ -21,7 +21,7 @@ fn test_enter_chroot_fails() {
assert!(result assert!(result
.stderr_str() .stderr_str()
.starts_with("chroot: error: cannot chroot to jail: Operation not permitted (os error 1)")); .starts_with("chroot: cannot chroot to jail: Operation not permitted (os error 1)"));
} }
#[test] #[test]
@ -32,7 +32,7 @@ fn test_no_such_directory() {
ucmd.arg("a") ucmd.arg("a")
.fails() .fails()
.stderr_is("chroot: error: cannot change root directory to `a`: no such directory"); .stderr_is("chroot: cannot change root directory to `a`: no such directory");
} }
#[test] #[test]
@ -43,9 +43,7 @@ fn test_invalid_user_spec() {
let result = ucmd.arg("a").arg("--userspec=ARABA:").fails(); let result = ucmd.arg("a").arg("--userspec=ARABA:").fails();
assert!(result assert!(result.stderr_str().starts_with("chroot: invalid userspec"));
.stderr_str()
.starts_with("chroot: error: invalid userspec"));
} }
#[test] #[test]

View file

@ -66,7 +66,7 @@ fn test_invalid_file() {
.arg(folder_name) .arg(folder_name)
.fails() .fails()
.no_stdout() .no_stdout()
.stderr_contains("cksum: error: 'asdf' No such file or directory"); .stderr_contains("cksum: 'asdf' No such file or directory");
// Then check when the file is of an invalid type // Then check when the file is of an invalid type
at.mkdir(folder_name); at.mkdir(folder_name);
@ -74,7 +74,7 @@ fn test_invalid_file() {
.arg(folder_name) .arg(folder_name)
.fails() .fails()
.no_stdout() .no_stdout()
.stderr_contains("cksum: error: 'asdf' Is a directory"); .stderr_contains("cksum: 'asdf' Is a directory");
} }
// Make sure crc is correct for files larger than 32 bytes // Make sure crc is correct for files larger than 32 bytes

View file

@ -214,8 +214,8 @@ fn test_cp_arg_symlink() {
fn test_cp_arg_no_clobber() { fn test_cp_arg_no_clobber() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
ucmd.arg(TEST_HELLO_WORLD_SOURCE) ucmd.arg(TEST_HELLO_WORLD_SOURCE)
.arg("--no-clobber")
.arg(TEST_HOW_ARE_YOU_SOURCE) .arg(TEST_HOW_ARE_YOU_SOURCE)
.arg("--no-clobber")
.succeeds(); .succeeds();
assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "How are you?\n"); assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "How are you?\n");
@ -305,7 +305,39 @@ fn test_cp_arg_backup() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
ucmd.arg(TEST_HELLO_WORLD_SOURCE) ucmd.arg(TEST_HELLO_WORLD_SOURCE)
.arg("--backup") .arg(TEST_HOW_ARE_YOU_SOURCE)
.arg("-b")
.succeeds();
assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n");
assert_eq!(
at.read(&*format!("{}~", TEST_HOW_ARE_YOU_SOURCE)),
"How are you?\n"
);
}
#[test]
fn test_cp_arg_backup_with_other_args() {
let (at, mut ucmd) = at_and_ucmd!();
ucmd.arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_HOW_ARE_YOU_SOURCE)
.arg("-vbL")
.succeeds();
assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n");
assert_eq!(
at.read(&*format!("{}~", TEST_HOW_ARE_YOU_SOURCE)),
"How are you?\n"
);
}
#[test]
fn test_cp_arg_backup_arg_first() {
let (at, mut ucmd) = at_and_ucmd!();
ucmd.arg("--backup")
.arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_HOW_ARE_YOU_SOURCE) .arg(TEST_HOW_ARE_YOU_SOURCE)
.succeeds(); .succeeds();
@ -321,6 +353,7 @@ fn test_cp_arg_suffix() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
ucmd.arg(TEST_HELLO_WORLD_SOURCE) ucmd.arg(TEST_HELLO_WORLD_SOURCE)
.arg("-b")
.arg("--suffix") .arg("--suffix")
.arg(".bak") .arg(".bak")
.arg(TEST_HOW_ARE_YOU_SOURCE) .arg(TEST_HOW_ARE_YOU_SOURCE)
@ -333,6 +366,207 @@ fn test_cp_arg_suffix() {
); );
} }
#[test]
fn test_cp_custom_backup_suffix_via_env() {
let (at, mut ucmd) = at_and_ucmd!();
let suffix = "super-suffix-of-the-century";
ucmd.arg("-b")
.env("SIMPLE_BACKUP_SUFFIX", suffix)
.arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_HOW_ARE_YOU_SOURCE)
.succeeds()
.no_stderr();
assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n");
assert_eq!(
at.read(&*format!("{}{}", TEST_HOW_ARE_YOU_SOURCE, suffix)),
"How are you?\n"
);
}
#[test]
fn test_cp_backup_numbered_with_t() {
let (at, mut ucmd) = at_and_ucmd!();
ucmd.arg("--backup=t")
.arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_HOW_ARE_YOU_SOURCE)
.succeeds()
.no_stderr();
assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n");
assert_eq!(
at.read(&*format!("{}.~1~", TEST_HOW_ARE_YOU_SOURCE)),
"How are you?\n"
);
}
#[test]
fn test_cp_backup_numbered() {
let (at, mut ucmd) = at_and_ucmd!();
ucmd.arg("--backup=numbered")
.arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_HOW_ARE_YOU_SOURCE)
.succeeds()
.no_stderr();
assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n");
assert_eq!(
at.read(&*format!("{}.~1~", TEST_HOW_ARE_YOU_SOURCE)),
"How are you?\n"
);
}
#[test]
fn test_cp_backup_existing() {
let (at, mut ucmd) = at_and_ucmd!();
ucmd.arg("--backup=existing")
.arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_HOW_ARE_YOU_SOURCE)
.succeeds()
.no_stderr();
assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n");
assert_eq!(
at.read(&*format!("{}~", TEST_HOW_ARE_YOU_SOURCE)),
"How are you?\n"
);
}
#[test]
fn test_cp_backup_nil() {
let (at, mut ucmd) = at_and_ucmd!();
ucmd.arg("--backup=nil")
.arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_HOW_ARE_YOU_SOURCE)
.succeeds()
.no_stderr();
assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n");
assert_eq!(
at.read(&*format!("{}~", TEST_HOW_ARE_YOU_SOURCE)),
"How are you?\n"
);
}
#[test]
fn test_cp_numbered_if_existing_backup_existing() {
let (at, mut ucmd) = at_and_ucmd!();
let existing_backup = &*format!("{}.~1~", TEST_HOW_ARE_YOU_SOURCE);
at.touch(existing_backup);
ucmd.arg("--backup=existing")
.arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_HOW_ARE_YOU_SOURCE)
.succeeds()
.no_stderr();
assert!(at.file_exists(TEST_HOW_ARE_YOU_SOURCE));
assert!(at.file_exists(existing_backup));
assert_eq!(
at.read(&*format!("{}.~2~", TEST_HOW_ARE_YOU_SOURCE)),
"How are you?\n"
);
}
#[test]
fn test_cp_numbered_if_existing_backup_nil() {
let (at, mut ucmd) = at_and_ucmd!();
let existing_backup = &*format!("{}.~1~", TEST_HOW_ARE_YOU_SOURCE);
at.touch(existing_backup);
ucmd.arg("--backup=nil")
.arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_HOW_ARE_YOU_SOURCE)
.succeeds()
.no_stderr();
assert!(at.file_exists(TEST_HOW_ARE_YOU_SOURCE));
assert!(at.file_exists(existing_backup));
assert_eq!(
at.read(&*format!("{}.~2~", TEST_HOW_ARE_YOU_SOURCE)),
"How are you?\n"
);
}
#[test]
fn test_cp_backup_simple() {
let (at, mut ucmd) = at_and_ucmd!();
ucmd.arg("--backup=simple")
.arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_HOW_ARE_YOU_SOURCE)
.succeeds()
.no_stderr();
assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n");
assert_eq!(
at.read(&*format!("{}~", TEST_HOW_ARE_YOU_SOURCE)),
"How are you?\n"
);
}
#[test]
fn test_cp_backup_never() {
let (at, mut ucmd) = at_and_ucmd!();
ucmd.arg("--backup=never")
.arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_HOW_ARE_YOU_SOURCE)
.succeeds()
.no_stderr();
assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n");
assert_eq!(
at.read(&*format!("{}~", TEST_HOW_ARE_YOU_SOURCE)),
"How are you?\n"
);
}
#[test]
fn test_cp_backup_none() {
let (at, mut ucmd) = at_and_ucmd!();
ucmd.arg("--backup=none")
.arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_HOW_ARE_YOU_SOURCE)
.succeeds()
.no_stderr();
assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n");
assert!(!at.file_exists(&format!("{}~", TEST_HOW_ARE_YOU_SOURCE)));
}
#[test]
fn test_cp_backup_off() {
let (at, mut ucmd) = at_and_ucmd!();
ucmd.arg("--backup=off")
.arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_HOW_ARE_YOU_SOURCE)
.succeeds()
.no_stderr();
assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n");
assert!(!at.file_exists(&format!("{}~", TEST_HOW_ARE_YOU_SOURCE)));
}
#[test]
fn test_cp_backup_no_clobber_conflicting_options() {
let (_, mut ucmd) = at_and_ucmd!();
ucmd.arg("--backup")
.arg("--no-clobber")
.arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_HOW_ARE_YOU_SOURCE)
.fails()
.stderr_is("cp: options --backup and --no-clobber are mutually exclusive\nTry 'cp --help' for more information.");
}
#[test] #[test]
fn test_cp_deref_conflicting_options() { fn test_cp_deref_conflicting_options() {
new_ucmd!() new_ucmd!()

View file

@ -208,7 +208,7 @@ fn test_up_to_match_repeat_over() {
ucmd.args(&["numbers50.txt", "/9$/", "{50}"]) ucmd.args(&["numbers50.txt", "/9$/", "{50}"])
.fails() .fails()
.stdout_is("16\n29\n30\n30\n30\n6\n") .stdout_is("16\n29\n30\n30\n30\n6\n")
.stderr_is("csplit: error: '/9$/': match not found on repetition 5"); .stderr_is("csplit: '/9$/': match not found on repetition 5");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
.expect("there should be splits created") .expect("there should be splits created")
@ -219,7 +219,7 @@ fn test_up_to_match_repeat_over() {
ucmd.args(&["numbers50.txt", "/9$/", "{50}", "-k"]) ucmd.args(&["numbers50.txt", "/9$/", "{50}", "-k"])
.fails() .fails()
.stdout_is("16\n29\n30\n30\n30\n6\n") .stdout_is("16\n29\n30\n30\n30\n6\n")
.stderr_is("csplit: error: '/9$/': match not found on repetition 5"); .stderr_is("csplit: '/9$/': match not found on repetition 5");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
.expect("there should be splits created") .expect("there should be splits created")
@ -365,7 +365,7 @@ fn test_option_keep() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
ucmd.args(&["-k", "numbers50.txt", "/20/", "/nope/"]) ucmd.args(&["-k", "numbers50.txt", "/20/", "/nope/"])
.fails() .fails()
.stderr_is("csplit: error: '/nope/': match not found") .stderr_is("csplit: '/nope/': match not found")
.stdout_is("48\n93\n"); .stdout_is("48\n93\n");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
@ -541,7 +541,7 @@ fn test_up_to_match_context_overflow() {
ucmd.args(&["numbers50.txt", "/45/+10"]) ucmd.args(&["numbers50.txt", "/45/+10"])
.fails() .fails()
.stdout_is("141\n") .stdout_is("141\n")
.stderr_is("csplit: error: '/45/+10': line number out of range"); .stderr_is("csplit: '/45/+10': line number out of range");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
.expect("counting splits") .expect("counting splits")
@ -552,7 +552,7 @@ fn test_up_to_match_context_overflow() {
ucmd.args(&["numbers50.txt", "/45/+10", "-k"]) ucmd.args(&["numbers50.txt", "/45/+10", "-k"])
.fails() .fails()
.stdout_is("141\n") .stdout_is("141\n")
.stderr_is("csplit: error: '/45/+10': line number out of range"); .stderr_is("csplit: '/45/+10': line number out of range");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
.expect("counting splits") .expect("counting splits")
@ -567,7 +567,7 @@ fn test_skip_to_match_context_underflow() {
ucmd.args(&["numbers50.txt", "%5%-10"]) ucmd.args(&["numbers50.txt", "%5%-10"])
.fails() .fails()
.stdout_is("141\n") .stdout_is("141\n")
.stderr_is("csplit: error: '%5%-10': line number out of range"); .stderr_is("csplit: '%5%-10': line number out of range");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
.expect("counting splits") .expect("counting splits")
@ -578,7 +578,7 @@ fn test_skip_to_match_context_underflow() {
ucmd.args(&["numbers50.txt", "%5%-10", "-k"]) ucmd.args(&["numbers50.txt", "%5%-10", "-k"])
.fails() .fails()
.stdout_is("141\n") .stdout_is("141\n")
.stderr_is("csplit: error: '%5%-10': line number out of range"); .stderr_is("csplit: '%5%-10': line number out of range");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
.expect("counting splits") .expect("counting splits")
@ -592,7 +592,7 @@ fn test_skip_to_match_context_overflow() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
ucmd.args(&["numbers50.txt", "%45%+10"]) ucmd.args(&["numbers50.txt", "%45%+10"])
.fails() .fails()
.stderr_only("csplit: error: '%45%+10': line number out of range"); .stderr_only("csplit: '%45%+10': line number out of range");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
.expect("counting splits") .expect("counting splits")
@ -602,7 +602,7 @@ fn test_skip_to_match_context_overflow() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
ucmd.args(&["numbers50.txt", "%45%+10", "-k"]) ucmd.args(&["numbers50.txt", "%45%+10", "-k"])
.fails() .fails()
.stderr_only("csplit: error: '%45%+10': line number out of range"); .stderr_only("csplit: '%45%+10': line number out of range");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
.expect("counting splits") .expect("counting splits")
@ -616,7 +616,7 @@ fn test_up_to_no_match1() {
ucmd.args(&["numbers50.txt", "/4/", "/nope/"]) ucmd.args(&["numbers50.txt", "/4/", "/nope/"])
.fails() .fails()
.stdout_is("6\n135\n") .stdout_is("6\n135\n")
.stderr_is("csplit: error: '/nope/': match not found"); .stderr_is("csplit: '/nope/': match not found");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
.expect("counting splits") .expect("counting splits")
@ -627,7 +627,7 @@ fn test_up_to_no_match1() {
ucmd.args(&["numbers50.txt", "/4/", "/nope/", "-k"]) ucmd.args(&["numbers50.txt", "/4/", "/nope/", "-k"])
.fails() .fails()
.stdout_is("6\n135\n") .stdout_is("6\n135\n")
.stderr_is("csplit: error: '/nope/': match not found"); .stderr_is("csplit: '/nope/': match not found");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
.expect("counting splits") .expect("counting splits")
@ -643,7 +643,7 @@ fn test_up_to_no_match2() {
ucmd.args(&["numbers50.txt", "/4/", "/nope/", "{50}"]) ucmd.args(&["numbers50.txt", "/4/", "/nope/", "{50}"])
.fails() .fails()
.stdout_is("6\n135\n") .stdout_is("6\n135\n")
.stderr_is("csplit: error: '/nope/': match not found"); .stderr_is("csplit: '/nope/': match not found");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
.expect("counting splits") .expect("counting splits")
@ -654,7 +654,7 @@ fn test_up_to_no_match2() {
ucmd.args(&["numbers50.txt", "/4/", "/nope/", "{50}", "-k"]) ucmd.args(&["numbers50.txt", "/4/", "/nope/", "{50}", "-k"])
.fails() .fails()
.stdout_is("6\n135\n") .stdout_is("6\n135\n")
.stderr_is("csplit: error: '/nope/': match not found"); .stderr_is("csplit: '/nope/': match not found");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
.expect("counting splits") .expect("counting splits")
@ -670,7 +670,7 @@ fn test_up_to_no_match3() {
ucmd.args(&["numbers50.txt", "/0$/", "{50}"]) ucmd.args(&["numbers50.txt", "/0$/", "{50}"])
.fails() .fails()
.stdout_is("18\n30\n30\n30\n30\n3\n") .stdout_is("18\n30\n30\n30\n30\n3\n")
.stderr_is("csplit: error: '/0$/': match not found on repetition 5"); .stderr_is("csplit: '/0$/': match not found on repetition 5");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
.expect("counting splits") .expect("counting splits")
@ -681,7 +681,7 @@ fn test_up_to_no_match3() {
ucmd.args(&["numbers50.txt", "/0$/", "{50}", "-k"]) ucmd.args(&["numbers50.txt", "/0$/", "{50}", "-k"])
.fails() .fails()
.stdout_is("18\n30\n30\n30\n30\n3\n") .stdout_is("18\n30\n30\n30\n30\n3\n")
.stderr_is("csplit: error: '/0$/': match not found on repetition 5"); .stderr_is("csplit: '/0$/': match not found on repetition 5");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
.expect("counting splits") .expect("counting splits")
@ -701,7 +701,7 @@ fn test_up_to_no_match4() {
ucmd.args(&["numbers50.txt", "/nope/", "/4/"]) ucmd.args(&["numbers50.txt", "/nope/", "/4/"])
.fails() .fails()
.stdout_is("141\n") .stdout_is("141\n")
.stderr_is("csplit: error: '/nope/': match not found"); .stderr_is("csplit: '/nope/': match not found");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
.expect("counting splits") .expect("counting splits")
@ -712,7 +712,7 @@ fn test_up_to_no_match4() {
ucmd.args(&["numbers50.txt", "/nope/", "/4/", "-k"]) ucmd.args(&["numbers50.txt", "/nope/", "/4/", "-k"])
.fails() .fails()
.stdout_is("141\n") .stdout_is("141\n")
.stderr_is("csplit: error: '/nope/': match not found"); .stderr_is("csplit: '/nope/': match not found");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
.expect("counting splits") .expect("counting splits")
@ -741,7 +741,7 @@ fn test_up_to_no_match6() {
ucmd.args(&["numbers50.txt", "/nope/-5"]) ucmd.args(&["numbers50.txt", "/nope/-5"])
.fails() .fails()
.stdout_is("141\n") .stdout_is("141\n")
.stderr_is("csplit: error: '/nope/-5': match not found"); .stderr_is("csplit: '/nope/-5': match not found");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
.expect("counting splits") .expect("counting splits")
@ -752,7 +752,7 @@ fn test_up_to_no_match6() {
ucmd.args(&["numbers50.txt", "/nope/-5", "-k"]) ucmd.args(&["numbers50.txt", "/nope/-5", "-k"])
.fails() .fails()
.stdout_is("141\n") .stdout_is("141\n")
.stderr_is("csplit: error: '/nope/-5': match not found"); .stderr_is("csplit: '/nope/-5': match not found");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
.expect("counting splits") .expect("counting splits")
@ -767,7 +767,7 @@ fn test_up_to_no_match7() {
ucmd.args(&["numbers50.txt", "/nope/+5"]) ucmd.args(&["numbers50.txt", "/nope/+5"])
.fails() .fails()
.stdout_is("141\n") .stdout_is("141\n")
.stderr_is("csplit: error: '/nope/+5': match not found"); .stderr_is("csplit: '/nope/+5': match not found");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
.expect("counting splits") .expect("counting splits")
@ -778,7 +778,7 @@ fn test_up_to_no_match7() {
ucmd.args(&["numbers50.txt", "/nope/+5", "-k"]) ucmd.args(&["numbers50.txt", "/nope/+5", "-k"])
.fails() .fails()
.stdout_is("141\n") .stdout_is("141\n")
.stderr_is("csplit: error: '/nope/+5': match not found"); .stderr_is("csplit: '/nope/+5': match not found");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
.expect("counting splits") .expect("counting splits")
@ -792,7 +792,7 @@ fn test_skip_to_no_match1() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
ucmd.args(&["numbers50.txt", "%nope%"]) ucmd.args(&["numbers50.txt", "%nope%"])
.fails() .fails()
.stderr_only("csplit: error: '%nope%': match not found"); .stderr_only("csplit: '%nope%': match not found");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
.expect("counting splits") .expect("counting splits")
@ -805,7 +805,7 @@ fn test_skip_to_no_match2() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
ucmd.args(&["numbers50.txt", "%nope%", "{50}"]) ucmd.args(&["numbers50.txt", "%nope%", "{50}"])
.fails() .fails()
.stderr_only("csplit: error: '%nope%': match not found"); .stderr_only("csplit: '%nope%': match not found");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
.expect("counting splits") .expect("counting splits")
@ -818,7 +818,7 @@ fn test_skip_to_no_match3() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
ucmd.args(&["numbers50.txt", "%0$%", "{50}"]) ucmd.args(&["numbers50.txt", "%0$%", "{50}"])
.fails() .fails()
.stderr_only("csplit: error: '%0$%': match not found on repetition 5"); .stderr_only("csplit: '%0$%': match not found on repetition 5");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
.expect("counting splits") .expect("counting splits")
@ -831,7 +831,7 @@ fn test_skip_to_no_match4() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
ucmd.args(&["numbers50.txt", "%nope%", "/4/"]) ucmd.args(&["numbers50.txt", "%nope%", "/4/"])
.fails() .fails()
.stderr_only("csplit: error: '%nope%': match not found"); .stderr_only("csplit: '%nope%': match not found");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
.expect("counting splits") .expect("counting splits")
@ -858,7 +858,7 @@ fn test_skip_to_no_match6() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
ucmd.args(&["numbers50.txt", "%nope%-5"]) ucmd.args(&["numbers50.txt", "%nope%-5"])
.fails() .fails()
.stderr_only("csplit: error: '%nope%-5': match not found"); .stderr_only("csplit: '%nope%-5': match not found");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
.expect("counting splits") .expect("counting splits")
@ -871,7 +871,7 @@ fn test_skip_to_no_match7() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
ucmd.args(&["numbers50.txt", "%nope%+5"]) ucmd.args(&["numbers50.txt", "%nope%+5"])
.fails() .fails()
.stderr_only("csplit: error: '%nope%+5': match not found"); .stderr_only("csplit: '%nope%+5': match not found");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
.expect("counting splits") .expect("counting splits")
@ -884,7 +884,7 @@ fn test_no_match() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
ucmd.args(&["numbers50.txt", "%nope%"]) ucmd.args(&["numbers50.txt", "%nope%"])
.fails() .fails()
.stderr_only("csplit: error: '%nope%': match not found"); .stderr_only("csplit: '%nope%': match not found");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
.expect("counting splits") .expect("counting splits")
@ -895,7 +895,7 @@ fn test_no_match() {
ucmd.args(&["numbers50.txt", "/nope/"]) ucmd.args(&["numbers50.txt", "/nope/"])
.fails() .fails()
.stdout_is("141\n") .stdout_is("141\n")
.stderr_is("csplit: error: '/nope/': match not found"); .stderr_is("csplit: '/nope/': match not found");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
.expect("counting splits") .expect("counting splits")
@ -992,7 +992,7 @@ fn test_too_small_linenum_repeat() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
ucmd.args(&["numbers50.txt", "/20/", "10", "{*}"]) ucmd.args(&["numbers50.txt", "/20/", "10", "{*}"])
.fails() .fails()
.stderr_is("csplit: error: '10': line number out of range on repetition 5") .stderr_is("csplit: '10': line number out of range on repetition 5")
.stdout_is("48\n0\n0\n30\n30\n30\n3\n"); .stdout_is("48\n0\n0\n30\n30\n30\n3\n");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
@ -1003,7 +1003,7 @@ fn test_too_small_linenum_repeat() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
ucmd.args(&["numbers50.txt", "/20/", "10", "{*}", "-k"]) ucmd.args(&["numbers50.txt", "/20/", "10", "{*}", "-k"])
.fails() .fails()
.stderr_is("csplit: error: '10': line number out of range on repetition 5") .stderr_is("csplit: '10': line number out of range on repetition 5")
.stdout_is("48\n0\n0\n30\n30\n30\n3\n"); .stdout_is("48\n0\n0\n30\n30\n30\n3\n");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
@ -1025,7 +1025,7 @@ fn test_linenum_out_of_range1() {
ucmd.args(&["numbers50.txt", "100"]) ucmd.args(&["numbers50.txt", "100"])
.fails() .fails()
.stdout_is("141\n") .stdout_is("141\n")
.stderr_is("csplit: error: '100': line number out of range"); .stderr_is("csplit: '100': line number out of range");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
.expect("there should be splits created") .expect("there should be splits created")
@ -1036,7 +1036,7 @@ fn test_linenum_out_of_range1() {
ucmd.args(&["numbers50.txt", "100", "-k"]) ucmd.args(&["numbers50.txt", "100", "-k"])
.fails() .fails()
.stdout_is("141\n") .stdout_is("141\n")
.stderr_is("csplit: error: '100': line number out of range"); .stderr_is("csplit: '100': line number out of range");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
.expect("there should be splits created") .expect("there should be splits created")
@ -1051,7 +1051,7 @@ fn test_linenum_out_of_range2() {
ucmd.args(&["numbers50.txt", "10", "100"]) ucmd.args(&["numbers50.txt", "10", "100"])
.fails() .fails()
.stdout_is("18\n123\n") .stdout_is("18\n123\n")
.stderr_is("csplit: error: '100': line number out of range"); .stderr_is("csplit: '100': line number out of range");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
.expect("there should be splits created") .expect("there should be splits created")
@ -1062,7 +1062,7 @@ fn test_linenum_out_of_range2() {
ucmd.args(&["numbers50.txt", "10", "100", "-k"]) ucmd.args(&["numbers50.txt", "10", "100", "-k"])
.fails() .fails()
.stdout_is("18\n123\n") .stdout_is("18\n123\n")
.stderr_is("csplit: error: '100': line number out of range"); .stderr_is("csplit: '100': line number out of range");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
.expect("there should be splits created") .expect("there should be splits created")
@ -1078,7 +1078,7 @@ fn test_linenum_out_of_range3() {
ucmd.args(&["numbers50.txt", "40", "{2}"]) ucmd.args(&["numbers50.txt", "40", "{2}"])
.fails() .fails()
.stdout_is("108\n33\n") .stdout_is("108\n33\n")
.stderr_is("csplit: error: '40': line number out of range on repetition 1"); .stderr_is("csplit: '40': line number out of range on repetition 1");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
.expect("there should be splits created") .expect("there should be splits created")
@ -1089,7 +1089,7 @@ fn test_linenum_out_of_range3() {
ucmd.args(&["numbers50.txt", "40", "{2}", "-k"]) ucmd.args(&["numbers50.txt", "40", "{2}", "-k"])
.fails() .fails()
.stdout_is("108\n33\n") .stdout_is("108\n33\n")
.stderr_is("csplit: error: '40': line number out of range on repetition 1"); .stderr_is("csplit: '40': line number out of range on repetition 1");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
.expect("there should be splits created") .expect("there should be splits created")
@ -1105,7 +1105,7 @@ fn test_linenum_out_of_range4() {
ucmd.args(&["numbers50.txt", "40", "{*}"]) ucmd.args(&["numbers50.txt", "40", "{*}"])
.fails() .fails()
.stdout_is("108\n33\n") .stdout_is("108\n33\n")
.stderr_is("csplit: error: '40': line number out of range on repetition 1"); .stderr_is("csplit: '40': line number out of range on repetition 1");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
.expect("there should be splits created") .expect("there should be splits created")
@ -1116,7 +1116,7 @@ fn test_linenum_out_of_range4() {
ucmd.args(&["numbers50.txt", "40", "{*}", "-k"]) ucmd.args(&["numbers50.txt", "40", "{*}", "-k"])
.fails() .fails()
.stdout_is("108\n33\n") .stdout_is("108\n33\n")
.stderr_is("csplit: error: '40': line number out of range on repetition 1"); .stderr_is("csplit: '40': line number out of range on repetition 1");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
.expect("there should be splits created") .expect("there should be splits created")
@ -1132,7 +1132,7 @@ fn test_skip_to_match_negative_offset_before_a_match() {
ucmd.args(&["numbers50.txt", "/20/-10", "/15/"]) ucmd.args(&["numbers50.txt", "/20/-10", "/15/"])
.fails() .fails()
.stdout_is("18\n123\n") .stdout_is("18\n123\n")
.stderr_is("csplit: error: '/15/': match not found"); .stderr_is("csplit: '/15/': match not found");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
.expect("there should be splits created") .expect("there should be splits created")
@ -1177,7 +1177,7 @@ fn test_corner_case2() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
ucmd.args(&["numbers50.txt", "/10/-5", "/10/"]) ucmd.args(&["numbers50.txt", "/10/-5", "/10/"])
.fails() .fails()
.stderr_is("csplit: error: '/10/': match not found") .stderr_is("csplit: '/10/': match not found")
.stdout_is("8\n133\n"); .stdout_is("8\n133\n");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
@ -1191,7 +1191,7 @@ fn test_corner_case3() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
ucmd.args(&["numbers50.txt", "/15/-3", "14", "/15/"]) ucmd.args(&["numbers50.txt", "/15/-3", "14", "/15/"])
.fails() .fails()
.stderr_is("csplit: error: '/15/': match not found") .stderr_is("csplit: '/15/': match not found")
.stdout_is("24\n6\n111\n"); .stdout_is("24\n6\n111\n");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
@ -1223,7 +1223,7 @@ fn test_up_to_match_context_underflow() {
ucmd.args(&["numbers50.txt", "/5/-10"]) ucmd.args(&["numbers50.txt", "/5/-10"])
.fails() .fails()
.stdout_is("0\n141\n") .stdout_is("0\n141\n")
.stderr_is("csplit: error: '/5/-10': line number out of range"); .stderr_is("csplit: '/5/-10': line number out of range");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
.expect("counting splits") .expect("counting splits")
@ -1234,7 +1234,7 @@ fn test_up_to_match_context_underflow() {
ucmd.args(&["numbers50.txt", "/5/-10", "-k"]) ucmd.args(&["numbers50.txt", "/5/-10", "-k"])
.fails() .fails()
.stdout_is("0\n141\n") .stdout_is("0\n141\n")
.stderr_is("csplit: error: '/5/-10': line number out of range"); .stderr_is("csplit: '/5/-10': line number out of range");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
.expect("counting splits") .expect("counting splits")
@ -1251,7 +1251,7 @@ fn test_linenum_range_with_up_to_match1() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
ucmd.args(&["numbers50.txt", "10", "/12/-5"]) ucmd.args(&["numbers50.txt", "10", "/12/-5"])
.fails() .fails()
.stderr_is("csplit: error: '/12/-5': line number out of range") .stderr_is("csplit: '/12/-5': line number out of range")
.stdout_is("18\n0\n123\n"); .stdout_is("18\n0\n123\n");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
@ -1262,7 +1262,7 @@ fn test_linenum_range_with_up_to_match1() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
ucmd.args(&["numbers50.txt", "10", "/12/-5", "-k"]) ucmd.args(&["numbers50.txt", "10", "/12/-5", "-k"])
.fails() .fails()
.stderr_is("csplit: error: '/12/-5': line number out of range") .stderr_is("csplit: '/12/-5': line number out of range")
.stdout_is("18\n0\n123\n"); .stdout_is("18\n0\n123\n");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
@ -1281,7 +1281,7 @@ fn test_linenum_range_with_up_to_match2() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
ucmd.args(&["numbers50.txt", "10", "/12/-15"]) ucmd.args(&["numbers50.txt", "10", "/12/-15"])
.fails() .fails()
.stderr_is("csplit: error: '/12/-15': line number out of range") .stderr_is("csplit: '/12/-15': line number out of range")
.stdout_is("18\n0\n123\n"); .stdout_is("18\n0\n123\n");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
@ -1292,7 +1292,7 @@ fn test_linenum_range_with_up_to_match2() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
ucmd.args(&["numbers50.txt", "10", "/12/-15", "-k"]) ucmd.args(&["numbers50.txt", "10", "/12/-15", "-k"])
.fails() .fails()
.stderr_is("csplit: error: '/12/-15': line number out of range") .stderr_is("csplit: '/12/-15': line number out of range")
.stdout_is("18\n0\n123\n"); .stdout_is("18\n0\n123\n");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))
@ -1310,7 +1310,7 @@ fn test_linenum_range_with_up_to_match3() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
ucmd.args(&["numbers50.txt", "10", "/10/", "-k"]) ucmd.args(&["numbers50.txt", "10", "/10/", "-k"])
.fails() .fails()
.stderr_is("csplit: error: '/10/': match not found") .stderr_is("csplit: '/10/': match not found")
.stdout_is("18\n123\n"); .stdout_is("18\n123\n");
let count = glob(&at.plus_as_string("xx*")) let count = glob(&at.plus_as_string("xx*"))

View file

@ -149,11 +149,11 @@ fn test_directory_and_no_such_file() {
ucmd.arg("-b1") ucmd.arg("-b1")
.arg("some") .arg("some")
.run() .run()
.stderr_is("cut: error: some: Is a directory\n"); .stderr_is("cut: some: Is a directory\n");
new_ucmd!() new_ucmd!()
.arg("-b1") .arg("-b1")
.arg("some") .arg("some")
.run() .run()
.stderr_is("cut: error: some: No such file or directory\n"); .stderr_is("cut: some: No such file or directory\n");
} }

View file

@ -104,6 +104,29 @@ fn test_date_format_full_day() {
.stdout_matches(&re); .stdout_matches(&re);
} }
#[test]
fn test_date_nano_seconds() {
// %N nanoseconds (000000000..999999999)
let re = Regex::new(r"^\d{1,9}$").unwrap();
new_ucmd!().arg("+%N").succeeds().stdout_matches(&re);
}
#[test]
fn test_date_format_without_plus() {
// [+FORMAT]
new_ucmd!()
.arg("%s")
.fails()
.stderr_contains("date: invalid date %s")
.code_is(1);
}
#[test]
fn test_date_format_literal() {
new_ucmd!().arg("+%%s").succeeds().stdout_is("%s\n");
new_ucmd!().arg("+%%N").succeeds().stdout_is("%N\n");
}
#[test] #[test]
#[cfg(all(unix, not(target_os = "macos")))] #[cfg(all(unix, not(target_os = "macos")))]
fn test_date_set_valid() { fn test_date_set_valid() {

Some files were not shown because too many files have changed in this diff Show more