diff --git a/.cargo/config.toml b/.cargo/config.toml index c6aa207614f..02550b267d0 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -3,3 +3,8 @@ linker = "x86_64-unknown-redox-gcc" [env] PROJECT_NAME_FOR_VERSION_STRING = "uutils coreutils" + +# libstdbuf must be a shared library, so musl libc can't be linked statically +# https://github.com/rust-lang/rust/issues/82193 +[build] +rustflags = [ "-C", "target-feature=-crt-static" ] diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index fba140aa286..2fb8dc54e14 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -443,14 +443,14 @@ jobs: --arg multisize "$SIZE_MULTI" \ '{($date): { sha: $sha, size: $size, multisize: $multisize, }}' > size-result.json - name: Download the previous individual size result - uses: dawidd6/action-download-artifact@v9 + uses: dawidd6/action-download-artifact@v10 with: workflow: CICD.yml name: individual-size-result repo: uutils/coreutils path: dl - name: Download the previous size result - uses: dawidd6/action-download-artifact@v9 + uses: dawidd6/action-download-artifact@v10 with: workflow: CICD.yml name: size-result @@ -528,8 +528,7 @@ jobs: - { os: macos-latest , target: aarch64-apple-darwin , features: feat_os_macos, workspace-tests: true } # M1 CPU - { os: macos-13 , target: x86_64-apple-darwin , features: feat_os_macos, workspace-tests: true } - { os: windows-latest , target: i686-pc-windows-msvc , features: feat_os_windows } - # TODO: Re-enable after rust-onig release: https://github.com/rust-onig/rust-onig/issues/193 - # - { os: windows-latest , target: x86_64-pc-windows-gnu , features: feat_os_windows } + - { os: windows-latest , target: x86_64-pc-windows-gnu , features: feat_os_windows } - { os: windows-latest , target: x86_64-pc-windows-msvc , features: feat_os_windows } - { os: windows-latest , target: aarch64-pc-windows-msvc , features: feat_os_windows, use-cross: use-cross , skip-tests: true } steps: @@ -654,10 +653,6 @@ jobs: ;; esac outputs CARGO_TEST_OPTIONS - # ** pass needed environment into `cross` container (iff `cross` not already configured via "Cross.toml") - if [ "${CARGO_CMD}" = 'cross' ] && [ ! -e "Cross.toml" ] ; then - printf "[build.env]\npassthrough = [\"CI\", \"RUST_BACKTRACE\", \"CARGO_TERM_COLOR\"]\n" > Cross.toml - fi # * executable for `strip`? STRIP="strip" case ${{ matrix.job.target }} in @@ -694,11 +689,6 @@ jobs: sudo apt-get -y update sudo apt-get -y install fuse3 libfuse-dev ;; - # Update binutils if MinGW due to https://github.com/rust-lang/rust/issues/112368 - x86_64-pc-windows-gnu) - C:/msys64/usr/bin/pacman.exe -Sy --needed mingw-w64-x86_64-gcc --noconfirm - echo "C:\msys64\mingw64\bin" >> $GITHUB_PATH - ;; esac case '${{ matrix.job.os }}' in macos-latest) brew install coreutils ;; # needed for testing diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index b12dbb235aa..108bbc67d42 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -136,7 +136,7 @@ jobs: working-directory: ${{ steps.vars.outputs.path_GNU }} - name: Retrieve reference artifacts - uses: dawidd6/action-download-artifact@v9 + uses: dawidd6/action-download-artifact@v10 # ref: continue-on-error: true ## don't break the build for missing reference artifacts (may be expired or just not generated yet) with: @@ -162,6 +162,7 @@ jobs: sudo locale-gen sudo locale-gen --keep-existing fr_FR sudo locale-gen --keep-existing fr_FR.UTF-8 + sudo locale-gen --keep-existing es_ES.UTF-8 sudo locale-gen --keep-existing sv_SE sudo locale-gen --keep-existing sv_SE.UTF-8 sudo locale-gen --keep-existing en_US @@ -193,7 +194,7 @@ jobs: - name: Selinux - Build for selinux tests run: | - lima bash -c "cd ~/work/uutils/ && bash util/build-gnu.sh" + lima bash -c "cd ~/work/uutils/ && bash util/build-gnu.sh --release-build" lima bash -c "mkdir -p ~/work/gnu/tests-selinux/" - name: Selinux - Run selinux tests diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 6323d2d7841..a5bbf42af4d 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -78,7 +78,7 @@ jobs: fail-fast: false matrix: job: - - { os: ubuntu-latest , features: feat_os_unix } + - { os: ubuntu-latest , features: all , workspace: true } - { os: macos-latest , features: feat_os_macos } - { os: windows-latest , features: feat_os_windows } steps: @@ -104,6 +104,16 @@ jobs: *) FAIL_ON_FAULT=true ; FAULT_TYPE=error ;; esac; outputs FAIL_ON_FAULT FAULT_TYPE + - name: Install/setup prerequisites + shell: bash + run: | + ## Install/setup prerequisites + case '${{ matrix.job.os }}' in + ubuntu-*) + # selinux headers needed to enable all features + sudo apt-get -y install libselinux1-dev + ;; + esac - name: "`cargo clippy` lint testing" uses: nick-fields/retry@v3 with: @@ -117,7 +127,17 @@ jobs: fault_type="${{ steps.vars.outputs.FAULT_TYPE }}" fault_prefix=$(echo "$fault_type" | tr '[:lower:]' '[:upper:]') # * convert any warnings to GHA UI annotations; ref: - S=$(cargo clippy --all-targets --features ${{ matrix.job.features }} --tests -pcoreutils -- -D warnings 2>&1) && printf "%s\n" "$S" || { printf "%s\n" "$S" ; printf "%s" "$S" | sed -E -n -e '/^error:/{' -e "N; s/^error:[[:space:]]+(.*)\\n[[:space:]]+-->[[:space:]]+(.*):([0-9]+):([0-9]+).*$/::${fault_type} file=\2,line=\3,col=\4::${fault_prefix}: \`cargo clippy\`: \1 (file:'\2', line:\3)/p;" -e '}' ; fault=true ; } + if [[ "${{ matrix.job.features }}" == "all" ]]; then + extra="--all-features" + else + extra="--features ${{ matrix.job.features }}" + fi + case '${{ matrix.job.workspace-tests }}' in + 1|t|true|y|yes) + extra="${extra} --workspace" + ;; + esac + S=$(cargo clippy --all-targets $extra --tests -pcoreutils -- -D warnings 2>&1) && printf "%s\n" "$S" || { printf "%s\n" "$S" ; printf "%s" "$S" | sed -E -n -e '/^error:/{' -e "N; s/^error:[[:space:]]+(.*)\\n[[:space:]]+-->[[:space:]]+(.*):([0-9]+):([0-9]+).*$/::${fault_type} file=\2,line=\3,col=\4::${fault_prefix}: \`cargo clippy\`: \1 (file:'\2', line:\3)/p;" -e '}' ; fault=true ; } if [ -n "${{ steps.vars.outputs.FAIL_ON_FAULT }}" ] && [ -n "$fault" ]; then exit 1 ; fi style_spellcheck: diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index 6e96bfd9599..6ba684719ed 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -24,7 +24,7 @@ jobs: style: name: Style and Lint runs-on: ${{ matrix.job.os }} - timeout-minutes: 90 + timeout-minutes: 45 strategy: fail-fast: false matrix: @@ -117,7 +117,7 @@ jobs: test: name: Tests runs-on: ${{ matrix.job.os }} - timeout-minutes: 90 + timeout-minutes: 45 strategy: fail-fast: false matrix: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 53e879d09a5..534487abc8f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,13 +11,13 @@ repos: - id: rust-clippy name: Rust clippy description: Run cargo clippy on files included in the commit. - entry: cargo +stable clippy --workspace --all-targets --all-features -- + entry: cargo +stable clippy --workspace --all-targets --all-features -- -D warnings pass_filenames: false types: [file, rust] language: system - id: cspell name: Code spell checker (cspell) description: Run cspell to check for spelling errors. - entry: cspell -- + entry: cspell --no-must-find-files -- pass_filenames: true language: system diff --git a/.vscode/cSpell.json b/.vscode/cSpell.json index 01e192d59ba..199830c2d1d 100644 --- a/.vscode/cSpell.json +++ b/.vscode/cSpell.json @@ -26,7 +26,8 @@ "tests/**/fixtures/**", "src/uu/dd/test-resources/**", "vendor/**", - "**/*.svg" + "**/*.svg", + "src/uu/*/locales/*.ftl" ], "enableGlobDot": true, diff --git a/Cargo.lock b/Cargo.lock index 27ce051f179..810cc2a66d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,12 +88,12 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" dependencies = [ "anstyle", - "once_cell", + "once_cell_polyfill", "windows-sys 0.59.0", ] @@ -172,7 +172,7 @@ version = "0.71.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "cexpr", "clang-sys", "itertools 0.13.0", @@ -181,7 +181,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash 2.1.1", + "rustc-hash", "shlex", "syn", ] @@ -194,9 +194,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "bitvec" @@ -262,9 +262,9 @@ checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "bytecount" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" [[package]] name = "byteorder" @@ -274,9 +274,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" -version = "1.2.16" +version = "1.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" +checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951" dependencies = [ "shlex", ] @@ -314,27 +314,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "chrono-tz" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efdce149c370f133a071ca8ef6ea340b7b88748ab0810097a9e2976eaa34b4f3" -dependencies = [ - "chrono", - "chrono-tz-build", - "phf", -] - -[[package]] -name = "chrono-tz-build" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94fea34d77a245229e7746bd2beb786cd2a896f306ff491fb8cecb3074b10a7" -dependencies = [ - "parse-zoneinfo", - "phf_codegen", -] - [[package]] name = "clang-sys" version = "1.8.1" @@ -348,18 +327,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.38" +version = "4.5.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000" +checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.38" +version = "4.5.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120" +checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" dependencies = [ "anstream", "anstyle", @@ -370,9 +349,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.50" +version = "4.5.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91d3baa3bcd889d60e6ef28874126a0b384fd225ab83aa6d8a801c519194ce1" +checksum = "1a554639e42d0c838336fc4fbedb9e2df3ad1fa4acda149f9126b4ccfcd7900f" dependencies = [ "clap", ] @@ -433,7 +412,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", "once_cell", "tiny-keccak", ] @@ -478,7 +457,6 @@ dependencies = [ "phf", "phf_codegen", "pretty_assertions", - "procfs", "rand 0.9.1", "regex", "rlimit", @@ -601,66 +579,6 @@ dependencies = [ "zip", ] -[[package]] -name = "coz" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cef55b3fe2f5477d59e12bc792e8b3c95a25bd099eadcfae006ecea136de76e2" -dependencies = [ - "libc", - "once_cell", -] - -[[package]] -name = "cpp" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f36bcac3d8234c1fb813358e83d1bb6b0290a3d2b3b5efc6b88bfeaf9d8eec17" -dependencies = [ - "cpp_macros", -] - -[[package]] -name = "cpp_build" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27f8638c97fbd79cc6fc80b616e0e74b49bac21014faed590bbc89b7e2676c90" -dependencies = [ - "cc", - "cpp_common", - "lazy_static", - "proc-macro2", - "regex", - "syn", - "unicode-xid", -] - -[[package]] -name = "cpp_common" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25fcfea2ee05889597d35e986c2ad0169694320ae5cc8f6d2640a4bb8a884560" -dependencies = [ - "lazy_static", - "proc-macro2", - "syn", -] - -[[package]] -name = "cpp_macros" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d156158fe86e274820f5a53bc9edb0885a6e7113909497aa8d883b69dd171871" -dependencies = [ - "aho-corasick", - "byteorder", - "cpp_common", - "lazy_static", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "cpufeatures" version = "0.2.17" @@ -710,14 +628,14 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "crossterm_winapi", "derive_more", "document-features", "filedescriptor", "mio", "parking_lot", - "rustix 1.0.1", + "rustix", "signal-hook", "signal-hook-mio", "winapi", @@ -939,12 +857,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.10" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -953,7 +871,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22be12de19decddab85d09f251ec8363f060ccb22ec9c81bc157c0c8433946d8" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "log", "scopeguard", "uuid", @@ -1025,7 +943,7 @@ dependencies = [ "fluent-syntax", "intl-memoizer", "intl_pluralrules", - "rustc-hash 2.1.1", + "rustc-hash", "self_cell", "smallvec", "unic-langid", @@ -1058,9 +976,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "fs_extra" @@ -1154,9 +1072,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", @@ -1165,14 +1083,14 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", - "wasi 0.13.3+wasi-0.2.2", - "windows-targets 0.52.6", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] @@ -1199,9 +1117,9 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" dependencies = [ "allocator-api2", "equivalent", @@ -1257,12 +1175,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.1" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.15.3", ] [[package]] @@ -1284,7 +1202,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "inotify-sys", "libc", ] @@ -1347,6 +1265,47 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jiff" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a194df1107f33c79f4f93d02c80798520551949d59dfad22b6157048a88cca93" +dependencies = [ + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", + "windows-sys 0.52.0", +] + +[[package]] +name = "jiff-static" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + [[package]] name = "js-sys" version = "0.3.77" @@ -1368,9 +1327,9 @@ dependencies = [ [[package]] name = "kqueue" -version = "1.0.8" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" dependencies = [ "kqueue-sys", "libc", @@ -1386,12 +1345,6 @@ dependencies = [ "libc", ] -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - [[package]] name = "libc" version = "0.2.172" @@ -1400,19 +1353,19 @@ checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "libloading" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.53.0", ] [[package]] name = "libm" -version = "0.2.11" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" @@ -1420,7 +1373,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "libc", "redox_syscall", ] @@ -1434,12 +1387,6 @@ dependencies = [ "zlib-rs", ] -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - [[package]] name = "linux-raw-sys" version = "0.9.4" @@ -1454,25 +1401,19 @@ checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", ] -[[package]] -name = "lockfree-object-pool" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" - [[package]] name = "log" -version = "0.4.26" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "lru" @@ -1480,7 +1421,7 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown 0.15.2", + "hashbrown 0.15.3", ] [[package]] @@ -1526,23 +1467,23 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" dependencies = [ "adler2", ] [[package]] name = "mio" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1551,7 +1492,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "cfg-if", "cfg_aliases", "libc", @@ -1582,7 +1523,7 @@ version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "filetime", "fsevent-sys", "inotify", @@ -1689,17 +1630,23 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "once_cell" -version = "1.20.3" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "onig" -version = "6.4.0" +version = "6.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c4b31c8722ad9171c6d77d3557db078cab2bd50afcc9d09c8b315c59df8ca4f" +checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.9.1", "libc", "once_cell", "onig_sys", @@ -1707,9 +1654,9 @@ dependencies = [ [[package]] name = "onig_sys" -version = "69.8.1" +version = "69.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7" +checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc" dependencies = [ "cc", "pkg-config", @@ -1736,9 +1683,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", @@ -1746,9 +1693,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", @@ -1757,15 +1704,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "parse-zoneinfo" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" -dependencies = [ - "regex", -] - [[package]] name = "parse_datetime" version = "0.9.0" @@ -1849,6 +1787,15 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1857,11 +1804,11 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy", + "zerocopy 0.8.25", ] [[package]] @@ -1876,9 +1823,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.30" +version = "0.2.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1ccf34da56fc294e7d4ccf69a85992b7dfb826b7cf57bac6a70bba3494cc08a" +checksum = "9dee91521343f4c5c6a63edd65e54f31f5c92fe8978c40a4282f8372194c6a7d" dependencies = [ "proc-macro2", "syn", @@ -1902,34 +1849,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "procfs" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc5b72d8145275d844d4b5f6d4e1eef00c8cd889edb6035c21675d1bb1f45c9f" -dependencies = [ - "bitflags 2.9.0", - "hex", - "procfs-core", - "rustix 0.38.44", -] - -[[package]] -name = "procfs-core" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec" -dependencies = [ - "bitflags 2.9.0", - "hex", -] - -[[package]] -name = "quick-error" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" - [[package]] name = "quote" version = "1.0.40" @@ -1939,6 +1858,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + [[package]] name = "radium" version = "0.7.0" @@ -1992,7 +1917,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", ] [[package]] @@ -2001,7 +1926,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.1", + "getrandom 0.3.3", ] [[package]] @@ -2026,11 +1951,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.10" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", ] [[package]] @@ -2124,12 +2049,6 @@ dependencies = [ "trim-in-place", ] -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - [[package]] name = "rustc-hash" version = "2.1.1" @@ -2147,35 +2066,22 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags 2.9.0", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustix" -version = "1.0.1" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dade4812df5c384711475be5fcd8c162555352945401aed22a35bffeab61f657" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "errno", "libc", - "linux-raw-sys 0.9.4", - "windows-sys 0.59.0", + "linux-raw-sys", + "windows-sys 0.52.0", ] [[package]] name = "rustversion" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" [[package]] name = "same-file" @@ -2204,7 +2110,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e37f432dfe840521abd9a72fefdf88ed7ad0f43bbea7d9d1d3d80383e9f4ad13" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "libc", "once_cell", "parking_lot", @@ -2320,9 +2226,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" dependencies = [ "libc", ] @@ -2371,9 +2277,9 @@ checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" [[package]] name = "socket2" -version = "0.5.8" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", "windows-sys 0.52.0", @@ -2387,9 +2293,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.99" +version = "2.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02e925281e18ffd9d640e234264753c43edc62d64b2d4cf898f1bc5e75f3fc2" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" dependencies = [ "proc-macro2", "quote", @@ -2409,10 +2315,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ "fastrand", - "getrandom 0.3.1", + "getrandom 0.3.3", "once_cell", - "rustix 1.0.1", - "windows-sys 0.59.0", + "rustix", + "windows-sys 0.52.0", ] [[package]] @@ -2421,7 +2327,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" dependencies = [ - "rustix 1.0.1", + "rustix", "windows-sys 0.59.0", ] @@ -2531,15 +2437,15 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" [[package]] name = "toml_edit" -version = "0.22.24" +version = "0.22.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" dependencies = [ "indexmap", "toml_datetime", @@ -2554,11 +2460,11 @@ checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" [[package]] name = "type-map" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb68604048ff8fa93347f02441e4487594adc20bb8a084f9e564d2b827a0a9f" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" dependencies = [ - "rustc-hash 1.1.0", + "rustc-hash", ] [[package]] @@ -2615,12 +2521,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "unindent" version = "0.2.4" @@ -2650,7 +2550,7 @@ dependencies = [ "thiserror 1.0.69", "time", "utmp-classic-raw", - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -2660,7 +2560,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22c226537a3d6e01c440c1926ca0256dbee2d19b2229ede6fc4863a6493dd831" dependencies = [ "cfg-if", - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -2790,9 +2690,9 @@ dependencies = [ "filetime", "indicatif", "libc", - "linux-raw-sys 0.9.4", - "quick-error", + "linux-raw-sys", "selinux", + "thiserror 2.0.12", "uucore", "walkdir", "xattr", @@ -2824,6 +2724,7 @@ version = "0.1.0" dependencies = [ "chrono", "clap", + "jiff", "libc", "parse_datetime", "uucore", @@ -2937,7 +2838,6 @@ name = "uu_factor" version = "0.1.0" dependencies = [ "clap", - "coz", "num-bigint", "num-prime", "num-traits", @@ -3096,6 +2996,7 @@ dependencies = [ "clap", "glob", "hostname", + "jiff", "lscolors", "number_prefix", "selinux", @@ -3436,8 +3337,7 @@ dependencies = [ name = "uu_stdbuf_libstdbuf" version = "0.1.0" dependencies = [ - "cpp", - "cpp_build", + "ctor", "libc", ] @@ -3692,7 +3592,6 @@ dependencies = [ "blake2b_simd", "blake3", "chrono", - "chrono-tz", "clap", "crc32fast", "data-encoding", @@ -3702,9 +3601,9 @@ dependencies = [ "dunce", "fluent", "fluent-bundle", + "fluent-syntax", "glob", "hex", - "iana-time-zone", "itertools 0.14.0", "libc", "md-5", @@ -3748,9 +3647,13 @@ version = "0.1.0" [[package]] name = "uuid" -version = "1.15.1" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] [[package]] name = "uutests" @@ -3809,9 +3712,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasi" -version = "0.13.3+wasi-0.2.2" +version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ "wit-bindgen-rt", ] @@ -3926,9 +3829,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.60.1" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca21a92a9cae9bf4ccae5cf8368dce0837100ddf6e6d57936749e85f152f6247" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", @@ -3939,9 +3842,9 @@ dependencies = [ [[package]] name = "windows-implement" -version = "0.59.0" +version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83577b051e2f49a058c308f17f273b570a6a758386fc291b5f6a934dd84e48c1" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", @@ -3967,18 +3870,18 @@ checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" [[package]] name = "windows-result" -version = "0.3.1" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06374efe858fab7e4f881500e6e86ec8bc28f9462c47e5a9941a0142ad86b189" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" -version = "0.3.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ "windows-link", ] @@ -4034,13 +3937,29 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -4053,6 +3972,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -4065,6 +3990,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -4077,12 +4008,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -4095,6 +4038,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -4107,6 +4056,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -4119,6 +4074,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -4131,22 +4092,28 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + [[package]] name = "winnow" -version = "0.7.3" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1" +checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" dependencies = [ "memchr", ] [[package]] name = "wit-bindgen-rt" -version = "0.33.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", ] [[package]] @@ -4165,7 +4132,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e" dependencies = [ "libc", - "rustix 1.0.1", + "rustix", ] [[package]] @@ -4187,7 +4154,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", - "zerocopy-derive", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +dependencies = [ + "zerocopy-derive 0.8.25", ] [[package]] @@ -4201,6 +4177,17 @@ dependencies = [ "syn", ] +[[package]] +name = "zerocopy-derive" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zerofrom" version = "0.1.6" @@ -4238,14 +4225,12 @@ checksum = "868b928d7949e09af2f6086dfc1e01936064cc7a819253bce650d4e2a2d63ba8" [[package]] name = "zopfli" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" +checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" dependencies = [ "bumpalo", "crc32fast", - "lockfree-object-pool", "log", - "once_cell", "simd-adler32", ] diff --git a/Cargo.toml b/Cargo.toml index a4d64f6ad19..752c0cfef85 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ # coreutils (uutils) # * see the repository LICENSE, README, and CONTRIBUTING files for more information -# spell-checker:ignore (libs) bigdecimal datetime serde bincode gethostid kqueue libselinux mangen memmap procfs uuhelp startswith constness expl +# spell-checker:ignore (libs) bigdecimal datetime serde bincode gethostid kqueue libselinux mangen memmap uuhelp startswith constness expl [package] name = "coreutils" @@ -158,7 +158,6 @@ feat_os_macos = [ feat_os_unix = [ "feat_Tier1", # - "feat_require_crate_cpp", "feat_require_unix", "feat_require_unix_utmpx", "feat_require_unix_hostid", @@ -185,9 +184,7 @@ feat_os_unix_android = [ # # ** NOTE: these `feat_require_...` sets should be minimized as much as possible to encourage cross-platform availability of utilities # -# "feat_require_crate_cpp" == set of utilities requiring the `cpp` crate (which fail to compile on several platforms; as of 2020-04-23) -feat_require_crate_cpp = ["stdbuf"] -# "feat_require_unix" == set of utilities requiring support which is only available on unix platforms (as of 2020-04-23) +# "feat_require_unix" == set of utilities requiring support which is only available on unix platforms feat_require_unix = [ "chgrp", "chmod", @@ -204,6 +201,7 @@ feat_require_unix = [ "nohup", "pathchk", "stat", + "stdbuf", "stty", "timeout", "tty", @@ -220,8 +218,6 @@ feat_require_selinux = ["chcon", "runcon"] feat_os_unix_fuchsia = [ "feat_common_core", # - "feat_require_crate_cpp", - # "chgrp", "chmod", "chown", @@ -282,13 +278,12 @@ chrono = { version = "0.4.41", default-features = false, features = [ "alloc", "clock", ] } -chrono-tz = "0.10.0" clap = { version = "4.5", features = ["wrap_help", "cargo"] } clap_complete = "4.4" clap_mangen = "0.2" compare = "0.1.0" -coz = { version = "0.1.3" } crossterm = "0.29.0" +ctor = "0.4.1" ctrlc = { version = "3.4.7", features = ["termination"] } dns-lookup = { version = "2.0.4" } exacl = "0.12.0" @@ -301,9 +296,13 @@ gcd = "2.3" glob = "0.3.1" half = "2.4.1" hostname = "0.4" -iana-time-zone = "0.1.57" indicatif = "0.17.8" itertools = "0.14.0" +jiff = { version = "0.2.10", default-features = false, features = [ + "std", + "alloc", + "tz-system", +] } libc = "0.2.172" linux-raw-sys = "0.9" lscolors = { version = "0.20.0", default-features = false, features = [ @@ -318,12 +317,11 @@ num-bigint = "0.4.4" num-prime = "0.4.4" num-traits = "0.2.19" number_prefix = "0.4" -onig = { version = "~6.4", default-features = false } +onig = { version = "~6.5.1", default-features = false } parse_datetime = "0.9.0" phf = "0.11.2" phf_codegen = "0.11.2" platform-info = "2.0.3" -quick-error = "2.0.1" rand = { version = "0.9.0", features = ["small_rng"] } rand_core = "0.9.0" rayon = "1.10" @@ -366,6 +364,7 @@ digest = "0.10.7" fluent-bundle = "0.16.0" fluent = "0.17.0" unic-langid = "0.9.6" +fluent-syntax = "0.12.0" uucore = { version = "0.1.0", package = "uucore", path = "src/uucore" } uucore_procs = { version = "0.1.0", package = "uucore_procs", path = "src/uucore_procs" } @@ -500,6 +499,7 @@ yes = { optional = true, version = "0.1.0", package = "uu_yes", path = "src/uu/y [dev-dependencies] chrono = { workspace = true } +ctor = { workspace = true } filetime = { workspace = true } glob = { workspace = true } libc = { workspace = true } @@ -522,10 +522,6 @@ uucore = { workspace = true, features = [ walkdir = { workspace = true } hex-literal = "1.0.0" rstest = { workspace = true } -ctor = "0.4.1" - -[target.'cfg(any(target_os = "linux", target_os = "android"))'.dev-dependencies] -procfs = { version = "0.17", default-features = false } [target.'cfg(unix)'.dev-dependencies] nix = { workspace = true, features = ["process", "signal", "user", "term"] } diff --git a/Cross.toml b/Cross.toml new file mode 100644 index 00000000000..52f5bad21dd --- /dev/null +++ b/Cross.toml @@ -0,0 +1,7 @@ +# spell-checker:ignore (misc) dpkg noninteractive tzdata +[build] +pre-build = [ + "apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install tzdata", +] +[build.env] +passthrough = ["CI", "RUST_BACKTRACE", "CARGO_TERM_COLOR"] diff --git a/GNUmakefile b/GNUmakefile index f46126a82f5..dbcdcd0ce08 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -5,6 +5,7 @@ PROFILE ?= debug MULTICALL ?= n COMPLETIONS ?= y MANPAGES ?= y +LOCALES ?= y INSTALL ?= install ifneq (,$(filter install, $(MAKECMDGOALS))) override PROFILE:=release @@ -127,6 +128,7 @@ PROGS := \ sleep \ sort \ split \ + stty \ sum \ sync \ tac \ @@ -299,7 +301,7 @@ else endif endif -build-coreutils: +build-coreutils: locales ${CARGO} build ${CARGOFLAGS} --features "${EXES} $(BUILD_SPEC_FEATURE)" ${PROFILE_CMD} --no-default-features build: build-coreutils build-pkgs @@ -395,7 +397,32 @@ else install-completions: endif -install: build install-manpages install-completions +ifeq ($(LOCALES),y) +locales: + $(foreach prog, $(INSTALLEES), \ + if [ -d "$(BASEDIR)/src/uu/$(prog)/locales" ]; then \ + mkdir -p "$(BUILDDIR)/locales/$(prog)"; \ + for locale_file in "$(BASEDIR)"/src/uu/$(prog)/locales/*.ftl; do \ + $(INSTALL) -v "$$locale_file" "$(BUILDDIR)/locales/$(prog)/"; \ + done; \ + fi $(newline) \ + ) + + +install-locales: + $(foreach prog, $(INSTALLEES), \ + if [ -d "$(BASEDIR)/src/uu/$(prog)/locales" ]; then \ + mkdir -p "$(DESTDIR)$(DATAROOTDIR)/locales/$(prog)"; \ + for locale_file in "$(BASEDIR)"/src/uu/$(prog)/locales/*.ftl; do \ + $(INSTALL) -v "$$locale_file" "$(DESTDIR)$(DATAROOTDIR)/locales/$(prog)/"; \ + done; \ + fi $(newline) \ + ) +else +install-locales: +endif + +install: build install-manpages install-completions install-locales mkdir -p $(INSTALLDIR_BIN) ifeq (${MULTICALL}, y) $(INSTALL) $(BUILDDIR)/coreutils $(INSTALLDIR_BIN)/$(PROG_PREFIX)coreutils diff --git a/deny.toml b/deny.toml index 0881a09f3dd..c15502149b1 100644 --- a/deny.toml +++ b/deny.toml @@ -58,22 +58,42 @@ skip = [ { name = "windows-sys", version = "0.48.0" }, # mio, nu-ansi-term, socket2 { name = "windows-sys", version = "0.52.0" }, + # anstyle-query + { name = "windows-sys", version = "0.59.0" }, # windows-sys - { name = "windows-targets", version = "0.48.0" }, + { name = "windows-targets", version = "0.48.5" }, + # parking_lot_core + { name = "windows-targets", version = "0.52.6" }, # windows-targets - { name = "windows_aarch64_gnullvm", version = "0.48.0" }, + { name = "windows_aarch64_gnullvm", version = "0.48.5" }, # windows-targets - { name = "windows_aarch64_msvc", version = "0.48.0" }, + { name = "windows_aarch64_gnullvm", version = "0.52.6" }, # windows-targets - { name = "windows_i686_gnu", version = "0.48.0" }, + { name = "windows_aarch64_msvc", version = "0.48.5" }, # windows-targets - { name = "windows_i686_msvc", version = "0.48.0" }, + { name = "windows_aarch64_msvc", version = "0.52.6" }, # windows-targets - { name = "windows_x86_64_gnu", version = "0.48.0" }, + { name = "windows_i686_gnu", version = "0.48.5" }, # windows-targets - { name = "windows_x86_64_gnullvm", version = "0.48.0" }, + { name = "windows_i686_gnu", version = "0.52.6" }, # windows-targets - { name = "windows_x86_64_msvc", version = "0.48.0" }, + { name = "windows_i686_gnullvm", version = "0.52.6" }, + # windows-targets + { name = "windows_i686_msvc", version = "0.48.5" }, + # windows-targets + { name = "windows_i686_msvc", version = "0.52.6" }, + # windows-targets + { name = "windows_x86_64_gnu", version = "0.48.5" }, + # windows-targets + { name = "windows_x86_64_gnu", version = "0.52.6" }, + # windows-targets + { name = "windows_x86_64_gnullvm", version = "0.48.5" }, + # windows-targets + { name = "windows_x86_64_gnullvm", version = "0.52.6" }, + # windows-targets + { name = "windows_x86_64_msvc", version = "0.48.5" }, + # windows-targets + { name = "windows_x86_64_msvc", version = "0.52.6" }, # kqueue-sys, onig { name = "bitflags", version = "1.3.2" }, # ansi-width @@ -84,8 +104,6 @@ skip = [ { name = "thiserror-impl", version = "1.0.69" }, # bindgen { name = "itertools", version = "0.13.0" }, - # fluent-bundle - { name = "rustc-hash", version = "1.1.0" }, # ordered-multimap { name = "hashbrown", version = "0.14.5" }, # cexpr (via bindgen) @@ -100,10 +118,8 @@ skip = [ { name = "rand_chacha", version = "0.3.1" }, # rand { name = "rand_core", version = "0.6.4" }, - # crossterm, procfs, terminal_size - { name = "rustix", version = "0.38.43" }, - # rustix - { name = "linux-raw-sys", version = "0.4.15" }, + # utmp-classic + { name = "zerocopy", version = "0.7.35" }, ] # spell-checker: enable diff --git a/docs/src/extensions.md b/docs/src/extensions.md index 1e715f729c1..af6119da51a 100644 --- a/docs/src/extensions.md +++ b/docs/src/extensions.md @@ -1,3 +1,5 @@ + + # Extensions over GNU Though the main goal of the project is compatibility, uutils supports a few @@ -71,8 +73,84 @@ feature is adopted from [FreeBSD](https://www.freebsd.org/cgi/man.cgi?cut). mail headers in the input. `-q`/`--quick` breaks lines more quickly. And `-T`/`--tab-width` defines the number of spaces representing a tab when determining the line length. +## `printf` + +`printf` uses arbitrary precision decimal numbers to parse and format floating point +numbers. GNU coreutils uses `long double`, whose actual size may be [double precision +64-bit float](https://en.wikipedia.org/wiki/Double-precision_floating-point_format) +(e.g 32-bit arm), [extended precision 80-bit float](https://en.wikipedia.org/wiki/Extended_precision) +(x86(-64)), or +[quadruple precision 128-bit float](https://en.wikipedia.org/wiki/Quadruple-precision_floating-point_format) (e.g. arm64). + +Practically, this means that printing a number with a large precision will stay exact: +``` +printf "%.48f\n" 0.1 +0.100000000000000000000000000000000000000000000000 << uutils on all platforms +0.100000000000000000001355252715606880542509316001 << GNU coreutils on x86(-64) +0.100000000000000000000000000000000004814824860968 << GNU coreutils on arm64 +0.100000000000000005551115123125782702118158340454 << GNU coreutils on armv7 (32-bit) +``` + +### Hexadecimal floats + +For hexadecimal float format (`%a`), POSIX only states that one hexadecimal number +should be present left of the decimal point (`0xh.hhhhp±d` [1]), but does not say how +many _bits_ should be included (between 1 and 4). On x86(-64), the first digit always +includes 4 bits, so its value is always between `0x8` and `0xf`, while on other +architectures, only 1 bit is included, so the value is always `0x1`. + +However, the first digit will of course be `0x0` if the number is zero. Also, +rounding numbers may cause the first digit to be `0x1` on x86(-64) (e.g. +`0xf.fffffffp-5` rounds to `0x1.00p-1`), or `0x2` on other architectures. + +We chose to replicate x86-64 behavior on all platforms. + +Additionally, the default precision of the hexadecimal float format (`%a` without +any specifier) is expected to be "sufficient for exact representation of the value" [1]. +This is not possible in uutils as we store arbitrary precision numbers that may be +periodic in hexadecimal form (`0.1 = 0xc.ccc...p-7`), so we revert +to the number of digits that would be required to exactly print an +[extended precision 80-bit float](https://en.wikipedia.org/wiki/Extended_precision), +emulating GNU coreutils behavior on x86(-64). An 80-bit float has 64 bits in its +integer and fractional part, so 16 hexadecimal digits are printed in total (1 digit +before the decimal point, 15 after). + +Practically, this means that the default hexadecimal floating point output is +identical to x86(-64) GNU coreutils: +``` +printf "%a\n" 0.1 +0xc.ccccccccccccccdp-7 << uutils on all platforms +0xc.ccccccccccccccdp-7 << GNU coreutils on x86-64 +0x1.999999999999999999999999999ap-4 << GNU coreutils on arm64 +0x1.999999999999ap-4 << GNU coreutils on armv7 (32-bit) +``` + +We _can_ print an arbitrary number of digits if a larger precision is requested, +and the leading digit will still be in the `0x8`-`0xf` range: +``` +printf "%.32a\n" 0.1 +0xc.cccccccccccccccccccccccccccccccdp-7 << uutils on all platforms +0xc.ccccccccccccccd00000000000000000p-7 << GNU coreutils on x86-64 +0x1.999999999999999999999999999a0000p-4 << GNU coreutils on arm64 +0x1.999999999999a0000000000000000000p-4 << GNU coreutils on armv7 (32-bit) +``` + +***Note: The architecture-specific behavior on non-x86(-64) platforms may change in +the future.*** + ## `seq` +Unlike GNU coreutils, `seq` always uses arbitrary precision decimal numbers, no +matter the parameters (integers, decimal numbers, positive or negative increments, +format specified, etc.), so its output will be more correct than GNU coreutils for +some inputs (e.g. small fractional increments where GNU coreutils uses `long double`). + +The only limitation is that the position of the decimal point is stored in a `i64`, +so values smaller than 10**(-2**63) will underflow to 0, and some values larger +than 10**(2**63) may overflow to infinity. + +See also comments under `printf` for formatting precision and differences. + `seq` provides `-t`/`--terminator` to set the terminator character. ## `ls` diff --git a/flake.nix b/flake.nix index 79c69c4901e..9c99b7b72e1 100644 --- a/flake.nix +++ b/flake.nix @@ -1,4 +1,4 @@ -# spell-checker:ignore bintools gnum gperf ldflags libclang nixpkgs numtide pkgs texinfo +# spell-checker:ignore bintools gnum gperf ldflags libclang nixpkgs numtide pkgs texinfo gettext { inputs = { nixpkgs.url = "github:nixos/nixpkgs"; @@ -30,6 +30,7 @@ rustup pre-commit + nodePackages.cspell # debugging gdb diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 53fa8709934..765d3c0a9b5 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -122,12 +122,6 @@ dependencies = [ "compare", ] -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.9.1" @@ -225,27 +219,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "chrono-tz" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efdce149c370f133a071ca8ef6ea340b7b88748ab0810097a9e2976eaa34b4f3" -dependencies = [ - "chrono", - "chrono-tz-build", - "phf", -] - -[[package]] -name = "chrono-tz-build" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f10f8c9340e31fc120ff885fcdb54a0b48e474bbd77cab557f0c30a3e569402" -dependencies = [ - "parse-zoneinfo", - "phf_codegen", -] - [[package]] name = "clap" version = "4.5.38" @@ -650,6 +623,47 @@ dependencies = [ "either", ] +[[package]] +name = "jiff" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27e77966151130221b079bcec80f1f34a9e414fa489d99152a201c07fd2182bc" +dependencies = [ + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", + "windows-sys", +] + +[[package]] +name = "jiff-static" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97265751f8a9a4228476f2fc17874a9e7e70e96b893368e42619880fe143b48a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + [[package]] name = "jobserver" version = "0.1.33" @@ -735,7 +749,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.9.1", + "bitflags", "cfg-if", "cfg_aliases", "libc", @@ -798,11 +812,11 @@ checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "onig" -version = "6.4.0" +version = "6.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c4b31c8722ad9171c6d77d3557db078cab2bd50afcc9d09c8b315c59df8ca4f" +checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" dependencies = [ - "bitflags 1.3.2", + "bitflags", "libc", "once_cell", "onig_sys", @@ -810,9 +824,9 @@ dependencies = [ [[package]] name = "onig_sys" -version = "69.8.1" +version = "69.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7" +checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc" dependencies = [ "cc", "pkg-config", @@ -837,15 +851,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "parse-zoneinfo" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" -dependencies = [ - "regex", -] - [[package]] name = "parse_datetime" version = "0.9.0" @@ -858,49 +863,26 @@ dependencies = [ ] [[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_shared", -] - -[[package]] -name = "phf_codegen" -version = "0.11.3" +name = "pkg-config" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" -dependencies = [ - "phf_generator", - "phf_shared", -] +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] -name = "phf_generator" -version = "0.11.3" +name = "portable-atomic" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared", - "rand 0.8.5", -] +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" [[package]] -name = "phf_shared" -version = "0.11.3" +name = "portable-atomic-util" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" dependencies = [ - "siphasher", + "portable-atomic", ] -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - [[package]] name = "ppv-lite86" version = "0.2.21" @@ -934,15 +916,6 @@ version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "rand_core 0.6.4", -] - [[package]] name = "rand" version = "0.9.1" @@ -950,7 +923,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" dependencies = [ "rand_chacha", - "rand_core 0.9.3", + "rand_core", ] [[package]] @@ -960,15 +933,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core", ] -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" - [[package]] name = "rand_core" version = "0.9.3" @@ -1056,7 +1023,7 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" dependencies = [ - "bitflags 2.9.1", + "bitflags", "errno", "libc", "linux-raw-sys", @@ -1139,12 +1106,6 @@ version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" -[[package]] -name = "siphasher" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" - [[package]] name = "sm3" version = "0.4.2" @@ -1322,6 +1283,7 @@ version = "0.1.0" dependencies = [ "chrono", "clap", + "jiff", "libc", "parse_datetime", "uucore", @@ -1391,7 +1353,7 @@ dependencies = [ "itertools", "memchr", "nix", - "rand 0.9.1", + "rand", "rayon", "self_cell", "tempfile", @@ -1448,8 +1410,6 @@ dependencies = [ "bigdecimal", "blake2b_simd", "blake3", - "chrono", - "chrono-tz", "clap", "crc32fast", "data-encoding", @@ -1458,9 +1418,9 @@ dependencies = [ "dunce", "fluent", "fluent-bundle", + "fluent-syntax", "glob", "hex", - "iana-time-zone", "itertools", "libc", "md-5", @@ -1487,7 +1447,7 @@ name = "uucore-fuzz" version = "0.0.0" dependencies = [ "libfuzzer-sys", - "rand 0.9.1", + "rand", "uu_cksum", "uu_cut", "uu_date", @@ -1520,7 +1480,7 @@ version = "0.1.0" dependencies = [ "console", "libc", - "rand 0.9.1", + "rand", "similar", "tempfile", "uucore", @@ -1765,7 +1725,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.9.1", + "bitflags", ] [[package]] diff --git a/src/bin/coreutils.rs b/src/bin/coreutils.rs index b29e7ea2337..2821988c2e6 100644 --- a/src/bin/coreutils.rs +++ b/src/bin/coreutils.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore manpages mangen +// spell-checker:ignore manpages mangen prefixcat testcat use clap::{Arg, Command}; use clap_complete::Shell; @@ -14,6 +14,7 @@ use std::io::{self, Write}; use std::path::{Path, PathBuf}; use std::process; use uucore::display::Quotable; +use uucore::locale; const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -50,6 +51,48 @@ fn name(binary_path: &Path) -> Option<&str> { binary_path.file_stem()?.to_str() } +fn get_canonical_util_name(util_name: &str) -> &str { + match util_name { + // uu_test aliases - '[' is an alias for test + "[" => "test", + + // hashsum aliases - all these hash commands are aliases for hashsum + "md5sum" | "sha1sum" | "sha224sum" | "sha256sum" | "sha384sum" | "sha512sum" + | "sha3sum" | "sha3-224sum" | "sha3-256sum" | "sha3-384sum" | "sha3-512sum" + | "shake128sum" | "shake256sum" | "b2sum" | "b3sum" => "hashsum", + + "dir" => "ls", // dir is an alias for ls + + // Default case - return the util name as is + _ => util_name, + } +} + +fn find_prefixed_util<'a>( + binary_name: &str, + mut util_keys: impl Iterator, +) -> Option<&'a str> { + util_keys.find(|util| { + binary_name.ends_with(*util) + && binary_name.len() > util.len() // Ensure there's actually a prefix + && !binary_name[..binary_name.len() - (*util).len()] + .ends_with(char::is_alphanumeric) + }) +} + +fn setup_localization_or_exit(util_name: &str) { + locale::setup_localization(get_canonical_util_name(util_name)).unwrap_or_else(|err| { + match err { + uucore::locale::LocalizationError::ParseResource { + error: err_msg, + snippet, + } => eprintln!("Localization parse error at {snippet}: {err_msg}"), + other => eprintln!("Could not init the localization system: {other}"), + } + process::exit(99) + }); +} + #[allow(clippy::cognitive_complexity)] fn main() { uucore::panic::mute_sigpipe_panic(); @@ -70,13 +113,10 @@ fn main() { // binary name equals prefixed util name? // * prefix/stem may be any string ending in a non-alphanumeric character - let util_name = if let Some(util) = utils.keys().find(|util| { - binary_as_util.ends_with(*util) - && !binary_as_util[..binary_as_util.len() - (*util).len()] - .ends_with(char::is_alphanumeric) - }) { + // For example, if the binary is named `uu_test`, it will match `test` as a utility. + let util_name = if let Some(util) = find_prefixed_util(binary_as_util, utils.keys().copied()) { // prefixed util => replace 0th (aka, executable name) argument - Some(OsString::from(*util)) + Some(OsString::from(util)) } else { // unmatched binary name => regard as multi-binary container and advance argument list uucore::set_utility_is_second_arg(); @@ -111,6 +151,12 @@ fn main() { match utils.get(util) { Some(&(uumain, _)) => { + // TODO: plug the deactivation of the translation + // and load the English strings directly at compilation time in the + // binary to avoid the load of the flt + // Could be something like: + // #[cfg(not(feature = "only_english"))] + setup_localization_or_exit(util); process::exit(uumain(vec![util_os].into_iter().chain(args))); } None => { @@ -213,6 +259,7 @@ fn gen_manpage( let command = if utility == "coreutils" { gen_coreutils_app(util_map) } else { + setup_localization_or_exit(utility); util_map.get(utility).unwrap().1() }; @@ -239,3 +286,70 @@ fn gen_coreutils_app(util_map: &UtilityMap) -> Command { } command } + +#[cfg(test)] +mod tests { + use super::*; + use std::path::Path; + + #[test] + fn test_get_canonical_util_name() { + // Test a few key aliases + assert_eq!(get_canonical_util_name("["), "test"); + assert_eq!(get_canonical_util_name("md5sum"), "hashsum"); + assert_eq!(get_canonical_util_name("dir"), "ls"); + + // Test passthrough case + assert_eq!(get_canonical_util_name("cat"), "cat"); + } + + #[test] + fn test_name() { + // Test normal executable name + assert_eq!(name(Path::new("/usr/bin/ls")), Some("ls")); + assert_eq!(name(Path::new("cat")), Some("cat")); + assert_eq!( + name(Path::new("./target/debug/coreutils")), + Some("coreutils") + ); + + // Test with extensions + assert_eq!(name(Path::new("program.exe")), Some("program")); + assert_eq!(name(Path::new("/path/to/utility.bin")), Some("utility")); + + // Test edge cases + assert_eq!(name(Path::new("")), None); + assert_eq!(name(Path::new("/")), None); + } + + #[test] + fn test_find_prefixed_util() { + let utils = ["test", "cat", "ls", "cp"]; + + // Test exact prefixed matches + assert_eq!( + find_prefixed_util("uu_test", utils.iter().copied()), + Some("test") + ); + assert_eq!( + find_prefixed_util("my-cat", utils.iter().copied()), + Some("cat") + ); + assert_eq!( + find_prefixed_util("prefix_ls", utils.iter().copied()), + Some("ls") + ); + + // Test non-alphanumeric separator requirement + assert_eq!(find_prefixed_util("prefixcat", utils.iter().copied()), None); // no separator + assert_eq!(find_prefixed_util("testcat", utils.iter().copied()), None); // no separator + + // Test no match + assert_eq!(find_prefixed_util("unknown", utils.iter().copied()), None); + assert_eq!(find_prefixed_util("", utils.iter().copied()), None); + + // Test exact util name (should not match as prefixed) + assert_eq!(find_prefixed_util("test", utils.iter().copied()), None); + assert_eq!(find_prefixed_util("cat", utils.iter().copied()), None); + } +} diff --git a/src/uu/arch/arch.md b/src/uu/arch/arch.md deleted file mode 100644 index a4ba2e75fce..00000000000 --- a/src/uu/arch/arch.md +++ /dev/null @@ -1,11 +0,0 @@ -# arch - -``` -arch -``` - -Display machine architecture - -## After Help - -Determine architecture name for current machine. diff --git a/src/uu/arch/locales/en-US.ftl b/src/uu/arch/locales/en-US.ftl new file mode 100644 index 00000000000..1646e50030d --- /dev/null +++ b/src/uu/arch/locales/en-US.ftl @@ -0,0 +1,5 @@ +# Error message when system architecture information cannot be retrieved +cannot-get-system = cannot get system name + +arch-about = Display machine architecture +arch-after-help = Determine architecture name for current machine. diff --git a/src/uu/arch/locales/fr-FR.ftl b/src/uu/arch/locales/fr-FR.ftl new file mode 100644 index 00000000000..f08462a87b4 --- /dev/null +++ b/src/uu/arch/locales/fr-FR.ftl @@ -0,0 +1,5 @@ +# Error message when system architecture information cannot be retrieved +cannot-get-system = impossible d'obtenir le nom du système + +arch-about = Afficher l'architecture de la machine +arch-after-help = Déterminer le nom de l'architecture pour la machine actuelle. diff --git a/src/uu/arch/src/arch.rs b/src/uu/arch/src/arch.rs index 590def48fb9..aad79b430d1 100644 --- a/src/uu/arch/src/arch.rs +++ b/src/uu/arch/src/arch.rs @@ -7,16 +7,14 @@ use platform_info::*; use clap::Command; use uucore::error::{UResult, USimpleError}; -use uucore::{help_about, help_section}; - -static ABOUT: &str = help_about!("arch.md"); -static SUMMARY: &str = help_section!("after help", "arch.md"); +use uucore::locale::get_message; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { uu_app().try_get_matches_from(args)?; - let uts = PlatformInfo::new().map_err(|_e| USimpleError::new(1, "cannot get system name"))?; + let uts = + PlatformInfo::new().map_err(|_e| USimpleError::new(1, get_message("cannot-get-system")))?; println!("{}", uts.machine().to_string_lossy().trim()); Ok(()) @@ -25,7 +23,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .after_help(SUMMARY) + .about(get_message("arch-about")) + .after_help(get_message("arch-after-help")) .infer_long_args(true) } diff --git a/src/uu/base32/base32.md b/src/uu/base32/base32.md deleted file mode 100644 index 1805d433bd3..00000000000 --- a/src/uu/base32/base32.md +++ /dev/null @@ -1,14 +0,0 @@ -# base32 - -``` -base32 [OPTION]... [FILE] -``` - -encode/decode data and print to standard output -With no FILE, or when FILE is -, read standard input. - -The data are encoded as described for the base32 alphabet in RFC 4648. -When decoding, the input may contain newlines in addition -to the bytes of the formal base32 alphabet. Use --ignore-garbage -to attempt to recover from any other non-alphabet bytes in the -encoded stream. diff --git a/src/uu/base32/locales/en-US.ftl b/src/uu/base32/locales/en-US.ftl new file mode 100644 index 00000000000..e3d2beb98a6 --- /dev/null +++ b/src/uu/base32/locales/en-US.ftl @@ -0,0 +1,9 @@ +base32-about = encode/decode data and print to standard output + With no FILE, or when FILE is -, read standard input. + + The data are encoded as described for the base32 alphabet in RFC 4648. + When decoding, the input may contain newlines in addition + to the bytes of the formal base32 alphabet. Use --ignore-garbage + to attempt to recover from any other non-alphabet bytes in the + encoded stream. +base32-usage = base32 [OPTION]... [FILE] diff --git a/src/uu/base32/src/base32.rs b/src/uu/base32/src/base32.rs index e14e83921e2..37bf01e3219 100644 --- a/src/uu/base32/src/base32.rs +++ b/src/uu/base32/src/base32.rs @@ -5,24 +5,25 @@ pub mod base_common; -use base_common::ReadSeek; use clap::Command; -use uucore::{encoding::Format, error::UResult, help_about, help_usage}; - -const ABOUT: &str = help_about!("base32.md"); -const USAGE: &str = help_usage!("base32.md"); +use uucore::{encoding::Format, error::UResult, locale::get_message}; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let format = Format::Base32; - - let config = base_common::parse_base_cmd_args(args, ABOUT, USAGE)?; - - let mut input: Box = base_common::get_input(&config)?; - + let (about, usage) = get_info(); + let config = base_common::parse_base_cmd_args(args, about, usage)?; + let mut input = base_common::get_input(&config)?; base_common::handle_input(&mut input, format, config) } pub fn uu_app() -> Command { - base_common::base_app(ABOUT, USAGE) + let (about, usage) = get_info(); + base_common::base_app(about, usage) +} + +fn get_info() -> (&'static str, &'static str) { + let about: &'static str = Box::leak(get_message("base32-about").into_boxed_str()); + let usage: &'static str = Box::leak(get_message("base32-usage").into_boxed_str()); + (about, usage) } diff --git a/src/uu/base64/base64.md b/src/uu/base64/base64.md deleted file mode 100644 index ed3aa4f7638..00000000000 --- a/src/uu/base64/base64.md +++ /dev/null @@ -1,14 +0,0 @@ -# base64 - -``` -base64 [OPTION]... [FILE] -``` - -encode/decode data and print to standard output -With no FILE, or when FILE is -, read standard input. - -The data are encoded as described for the base64 alphabet in RFC 3548. -When decoding, the input may contain newlines in addition -to the bytes of the formal base64 alphabet. Use --ignore-garbage -to attempt to recover from any other non-alphabet bytes in the -encoded stream. diff --git a/src/uu/base64/locales/en-US.ftl b/src/uu/base64/locales/en-US.ftl new file mode 100644 index 00000000000..8919205654c --- /dev/null +++ b/src/uu/base64/locales/en-US.ftl @@ -0,0 +1,9 @@ +base64-about = encode/decode data and print to standard output + With no FILE, or when FILE is -, read standard input. + + The data are encoded as described for the base64 alphabet in RFC 3548. + When decoding, the input may contain newlines in addition + to the bytes of the formal base64 alphabet. Use --ignore-garbage + to attempt to recover from any other non-alphabet bytes in the + encoded stream. +base64-usage = base64 [OPTION]... [FILE] diff --git a/src/uu/base64/src/base64.rs b/src/uu/base64/src/base64.rs index 86eb75bf119..cd3ab66bded 100644 --- a/src/uu/base64/src/base64.rs +++ b/src/uu/base64/src/base64.rs @@ -5,22 +5,24 @@ use clap::Command; use uu_base32::base_common; -use uucore::{encoding::Format, error::UResult, help_about, help_usage}; - -const ABOUT: &str = help_about!("base64.md"); -const USAGE: &str = help_usage!("base64.md"); +use uucore::{encoding::Format, error::UResult, locale::get_message}; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let format = Format::Base64; - - let config = base_common::parse_base_cmd_args(args, ABOUT, USAGE)?; - + let (about, usage) = get_info(); + let config = base_common::parse_base_cmd_args(args, about, usage)?; let mut input = base_common::get_input(&config)?; - base_common::handle_input(&mut input, format, config) } pub fn uu_app() -> Command { - base_common::base_app(ABOUT, USAGE) + let (about, usage) = get_info(); + base_common::base_app(about, usage) +} + +fn get_info() -> (&'static str, &'static str) { + let about: &'static str = Box::leak(get_message("base64-about").into_boxed_str()); + let usage: &'static str = Box::leak(get_message("base64-usage").into_boxed_str()); + (about, usage) } diff --git a/src/uu/basename/basename.md b/src/uu/basename/basename.md deleted file mode 100644 index ee87fa76d4e..00000000000 --- a/src/uu/basename/basename.md +++ /dev/null @@ -1,9 +0,0 @@ -# basename - -``` -basename [-z] NAME [SUFFIX] -basename OPTION... NAME... -``` - -Print NAME with any leading directory components removed -If specified, also remove a trailing SUFFIX diff --git a/src/uu/basename/locales/en-US.ftl b/src/uu/basename/locales/en-US.ftl new file mode 100644 index 00000000000..fd0a8335b42 --- /dev/null +++ b/src/uu/basename/locales/en-US.ftl @@ -0,0 +1,4 @@ +basename-about = Print NAME with any leading directory components removed + If specified, also remove a trailing SUFFIX +basename-usage = basename [-z] NAME [SUFFIX] + basename OPTION... NAME... diff --git a/src/uu/basename/src/basename.rs b/src/uu/basename/src/basename.rs index a40fcc18534..c39c329df87 100644 --- a/src/uu/basename/src/basename.rs +++ b/src/uu/basename/src/basename.rs @@ -9,12 +9,10 @@ use clap::{Arg, ArgAction, Command}; use std::path::{PathBuf, is_separator}; use uucore::display::Quotable; use uucore::error::{UResult, UUsageError}; +use uucore::format_usage; use uucore::line_ending::LineEnding; -use uucore::{format_usage, help_about, help_usage}; -static ABOUT: &str = help_about!("basename.md"); - -const USAGE: &str = help_usage!("basename.md"); +use uucore::locale::get_message; pub mod options { pub static MULTIPLE: &str = "multiple"; @@ -77,8 +75,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("basename-about")) + .override_usage(format_usage(&get_message("basename-usage"))) .infer_long_args(true) .arg( Arg::new(options::MULTIPLE) diff --git a/src/uu/basenc/basenc.md b/src/uu/basenc/basenc.md deleted file mode 100644 index 001babe9e6b..00000000000 --- a/src/uu/basenc/basenc.md +++ /dev/null @@ -1,12 +0,0 @@ -# basenc - -``` -basenc [OPTION]... [FILE] -``` - -Encode/decode data and print to standard output -With no FILE, or when FILE is -, read standard input. - -When decoding, the input may contain newlines in addition to the bytes of -the formal alphabet. Use --ignore-garbage to attempt to recover -from any other non-alphabet bytes in the encoded stream. diff --git a/src/uu/basenc/locales/en-US.ftl b/src/uu/basenc/locales/en-US.ftl new file mode 100644 index 00000000000..520292c85dd --- /dev/null +++ b/src/uu/basenc/locales/en-US.ftl @@ -0,0 +1,7 @@ +basenc-about = Encode/decode data and print to standard output + With no FILE, or when FILE is -, read standard input. + + When decoding, the input may contain newlines in addition to the bytes of + the formal alphabet. Use --ignore-garbage to attempt to recover + from any other non-alphabet bytes in the encoded stream. +basenc-usage = basenc [OPTION]... [FILE] diff --git a/src/uu/basenc/src/basenc.rs b/src/uu/basenc/src/basenc.rs index 10090765232..9da71e9deae 100644 --- a/src/uu/basenc/src/basenc.rs +++ b/src/uu/basenc/src/basenc.rs @@ -8,15 +8,11 @@ use clap::{Arg, ArgAction, Command}; use uu_base32::base_common::{self, BASE_CMD_PARSE_ERROR, Config}; use uucore::error::UClapError; +use uucore::locale::get_message; use uucore::{ encoding::Format, error::{UResult, UUsageError}, }; -use uucore::{help_about, help_usage}; - -const ABOUT: &str = help_about!("basenc.md"); -const USAGE: &str = help_usage!("basenc.md"); - const ENCODINGS: &[(&str, Format, &str)] = &[ ("base64", Format::Base64, "same as 'base64' program"), ("base64url", Format::Base64Url, "file- and url-safe base64"), @@ -47,7 +43,10 @@ const ENCODINGS: &[(&str, Format, &str)] = &[ ]; pub fn uu_app() -> Command { - let mut command = base_common::base_app(ABOUT, USAGE); + let about: &'static str = Box::leak(get_message("basenc-about").into_boxed_str()); + let usage: &'static str = Box::leak(get_message("basenc-usage").into_boxed_str()); + + let mut command = base_common::base_app(about, usage); for encoding in ENCODINGS { let raw_arg = Arg::new(encoding.0) .long(encoding.0) diff --git a/src/uu/cat/cat.md b/src/uu/cat/cat.md deleted file mode 100644 index efcd317eb41..00000000000 --- a/src/uu/cat/cat.md +++ /dev/null @@ -1,8 +0,0 @@ -# cat - -``` -cat [OPTION]... [FILE]... -``` - -Concatenate FILE(s), or standard input, to standard output -With no FILE, or when FILE is -, read standard input. diff --git a/src/uu/cat/locales/en-US.ftl b/src/uu/cat/locales/en-US.ftl new file mode 100644 index 00000000000..108000d5aa1 --- /dev/null +++ b/src/uu/cat/locales/en-US.ftl @@ -0,0 +1,3 @@ +cat-about = Concatenate FILE(s), or standard input, to standard output + With no FILE, or when FILE is -, read standard input. +cat-usage = cat [OPTION]... [FILE]... diff --git a/src/uu/cat/src/cat.rs b/src/uu/cat/src/cat.rs index 45fbe6cebf3..cd89c3bc385 100644 --- a/src/uu/cat/src/cat.rs +++ b/src/uu/cat/src/cat.rs @@ -24,15 +24,13 @@ use thiserror::Error; use uucore::display::Quotable; use uucore::error::UResult; use uucore::fs::FileInformation; -use uucore::{fast_inc::fast_inc_one, format_usage, help_about, help_usage}; +use uucore::locale::get_message; +use uucore::{fast_inc::fast_inc_one, format_usage}; /// Linux splice support #[cfg(any(target_os = "linux", target_os = "android"))] mod splice; -const USAGE: &str = help_usage!("cat.md"); -const ABOUT: &str = help_about!("cat.md"); - // Allocate 32 digits for the line number. // An estimate is that we can print about 1e8 lines/seconds, so 32 digits // would be enough for billions of universe lifetimes. @@ -275,8 +273,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .override_usage(format_usage(USAGE)) - .about(ABOUT) + .override_usage(format_usage(&get_message("cat-usage"))) + .about(get_message("cat-about")) .infer_long_args(true) .args_override_self(true) .arg( diff --git a/src/uu/chcon/chcon.md b/src/uu/chcon/chcon.md deleted file mode 100644 index 64c64f47be7..00000000000 --- a/src/uu/chcon/chcon.md +++ /dev/null @@ -1,11 +0,0 @@ - -# chcon - -``` -chcon [OPTION]... CONTEXT FILE... -chcon [OPTION]... [-u USER] [-r ROLE] [-l RANGE] [-t TYPE] FILE... -chcon [OPTION]... --reference=RFILE FILE... -``` - -Change the SELinux security context of each FILE to CONTEXT. -With --reference, change the security context of each FILE to that of RFILE. diff --git a/src/uu/chcon/locales/en-US.ftl b/src/uu/chcon/locales/en-US.ftl new file mode 100644 index 00000000000..66c51f5af5a --- /dev/null +++ b/src/uu/chcon/locales/en-US.ftl @@ -0,0 +1,5 @@ +chcon-about = Change the SELinux security context of each FILE to CONTEXT. + With --reference, change the security context of each FILE to that of RFILE. +chcon-usage = chcon [OPTION]... CONTEXT FILE... + chcon [OPTION]... [-u USER] [-r ROLE] [-l RANGE] [-t TYPE] FILE... + chcon [OPTION]... --reference=RFILE FILE... diff --git a/src/uu/chcon/src/chcon.rs b/src/uu/chcon/src/chcon.rs index 2b1ff2e8f97..be1bc2d5be0 100644 --- a/src/uu/chcon/src/chcon.rs +++ b/src/uu/chcon/src/chcon.rs @@ -8,7 +8,7 @@ use clap::builder::ValueParser; use uucore::error::{UResult, USimpleError, UUsageError}; -use uucore::{display::Quotable, format_usage, help_about, help_usage, show_error, show_warning}; +use uucore::{display::Quotable, format_usage, show_error, show_warning}; use clap::{Arg, ArgAction, Command}; use selinux::{OpaqueSecurityContext, SecurityContext}; @@ -24,8 +24,7 @@ mod fts; use errors::*; -const ABOUT: &str = help_about!("chcon.md"); -const USAGE: &str = help_usage!("chcon.md"); +use uucore::locale::get_message; pub mod options { pub static HELP: &str = "help"; @@ -151,8 +150,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("chcon-about")) + .override_usage(format_usage(&get_message("chcon-usage"))) .infer_long_args(true) .disable_help_flag(true) .args_override_self(true) diff --git a/src/uu/chgrp/chgrp.md b/src/uu/chgrp/chgrp.md deleted file mode 100644 index 79bc068d2d3..00000000000 --- a/src/uu/chgrp/chgrp.md +++ /dev/null @@ -1,10 +0,0 @@ - - -# chgrp - -``` -chgrp [OPTION]... GROUP FILE... -chgrp [OPTION]... --reference=RFILE FILE... -``` - -Change the group of each FILE to GROUP. diff --git a/src/uu/chgrp/locales/en-US.ftl b/src/uu/chgrp/locales/en-US.ftl new file mode 100644 index 00000000000..5d399aa4da2 --- /dev/null +++ b/src/uu/chgrp/locales/en-US.ftl @@ -0,0 +1,3 @@ +chgrp-about = Change the group of each FILE to GROUP. +chgrp-usage = chgrp [OPTION]... GROUP FILE... + chgrp [OPTION]... --reference=RFILE FILE... diff --git a/src/uu/chgrp/src/chgrp.rs b/src/uu/chgrp/src/chgrp.rs index 1763bbfeb73..0fb0489034e 100644 --- a/src/uu/chgrp/src/chgrp.rs +++ b/src/uu/chgrp/src/chgrp.rs @@ -8,16 +8,15 @@ use uucore::display::Quotable; pub use uucore::entries; use uucore::error::{FromIo, UResult, USimpleError}; +use uucore::format_usage; use uucore::perms::{GidUidOwnerFilter, IfFrom, chown_base, options}; -use uucore::{format_usage, help_about, help_usage}; use clap::{Arg, ArgAction, ArgMatches, Command}; use std::fs; use std::os::unix::fs::MetadataExt; -const ABOUT: &str = help_about!("chgrp.md"); -const USAGE: &str = help_usage!("chgrp.md"); +use uucore::locale::get_message; fn parse_gid_from_str(group: &str) -> Result { if let Some(gid_str) = group.strip_prefix(':') { @@ -99,8 +98,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("chgrp-about")) + .override_usage(format_usage(&get_message("chgrp-usage"))) .infer_long_args(true) .disable_help_flag(true) .arg( diff --git a/src/uu/chmod/chmod.md b/src/uu/chmod/chmod.md deleted file mode 100644 index 10ddb48a2ed..00000000000 --- a/src/uu/chmod/chmod.md +++ /dev/null @@ -1,16 +0,0 @@ - - -# chmod - -``` -chmod [OPTION]... MODE[,MODE]... FILE... -chmod [OPTION]... OCTAL-MODE FILE... -chmod [OPTION]... --reference=RFILE FILE... -``` - -Change the mode of each FILE to MODE. -With --reference, change the mode of each FILE to that of RFILE. - -## After Help - -Each MODE is of the form `[ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=]?[0-7]+`. diff --git a/src/uu/chmod/locales/en-US.ftl b/src/uu/chmod/locales/en-US.ftl new file mode 100644 index 00000000000..c0d4d39abff --- /dev/null +++ b/src/uu/chmod/locales/en-US.ftl @@ -0,0 +1,6 @@ +chmod-about = Change the mode of each FILE to MODE. + With --reference, change the mode of each FILE to that of RFILE. +chmod-usage = chmod [OPTION]... MODE[,MODE]... FILE... + chmod [OPTION]... OCTAL-MODE FILE... + chmod [OPTION]... --reference=RFILE FILE... +chmod-after-help = Each MODE is of the form [ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=]?[0-7]+. diff --git a/src/uu/chmod/src/chmod.rs b/src/uu/chmod/src/chmod.rs index dfe30485919..7367d4b76ff 100644 --- a/src/uu/chmod/src/chmod.rs +++ b/src/uu/chmod/src/chmod.rs @@ -17,11 +17,9 @@ use uucore::libc::mode_t; #[cfg(not(windows))] use uucore::mode; use uucore::perms::{TraverseSymlinks, configure_symlink_and_recursion}; -use uucore::{format_usage, help_about, help_section, help_usage, show, show_error}; +use uucore::{format_usage, show, show_error}; -const ABOUT: &str = help_about!("chmod.md"); -const USAGE: &str = help_usage!("chmod.md"); -const LONG_USAGE: &str = help_section!("after help", "chmod.md"); +use uucore::locale::get_message; mod options { pub const HELP: &str = "help"; @@ -94,7 +92,9 @@ fn extract_negative_modes(mut args: impl uucore::Args) -> (Option, Vec UResult<()> { let (parsed_cmode, args) = extract_negative_modes(args.skip(1)); // skip binary name - let matches = uu_app().after_help(LONG_USAGE).try_get_matches_from(args)?; + let matches = uu_app() + .after_help(get_message("chmod-after-help")) + .try_get_matches_from(args)?; let changes = matches.get_flag(options::CHANGES); let quiet = matches.get_flag(options::QUIET); @@ -159,8 +159,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("chmod-about")) + .override_usage(format_usage(&get_message("chmod-usage"))) .args_override_self(true) .infer_long_args(true) .no_binary_name(true) diff --git a/src/uu/chown/chown.md b/src/uu/chown/chown.md deleted file mode 100644 index 83101c74c73..00000000000 --- a/src/uu/chown/chown.md +++ /dev/null @@ -1,9 +0,0 @@ - -# chown - -``` -chown [OPTION]... [OWNER][:[GROUP]] FILE... -chown [OPTION]... --reference=RFILE FILE... -``` - -Change file owner and group diff --git a/src/uu/chown/locales/en-US.ftl b/src/uu/chown/locales/en-US.ftl new file mode 100644 index 00000000000..4c8f2a46af3 --- /dev/null +++ b/src/uu/chown/locales/en-US.ftl @@ -0,0 +1,3 @@ +chown-about = Change file owner and group +chown-usage = chown [OPTION]... [OWNER][:[GROUP]] FILE... + chown [OPTION]... --reference=RFILE FILE... diff --git a/src/uu/chown/src/chown.rs b/src/uu/chown/src/chown.rs index 4389d92f663..bf820c11ea3 100644 --- a/src/uu/chown/src/chown.rs +++ b/src/uu/chown/src/chown.rs @@ -7,8 +7,8 @@ use uucore::display::Quotable; pub use uucore::entries::{self, Group, Locate, Passwd}; +use uucore::format_usage; use uucore::perms::{GidUidOwnerFilter, IfFrom, chown_base, options}; -use uucore::{format_usage, help_about, help_usage}; use uucore::error::{FromIo, UResult, USimpleError}; @@ -17,9 +17,7 @@ use clap::{Arg, ArgAction, ArgMatches, Command}; use std::fs; use std::os::unix::fs::MetadataExt; -static ABOUT: &str = help_about!("chown.md"); - -const USAGE: &str = help_usage!("chown.md"); +use uucore::locale::get_message; fn parse_gid_uid_and_filter(matches: &ArgMatches) -> UResult { let filter = if let Some(spec) = matches.get_one::(options::FROM) { @@ -79,8 +77,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("chown-about")) + .override_usage(format_usage(&get_message("chown-usage"))) .infer_long_args(true) .disable_help_flag(true) .arg( diff --git a/src/uu/chroot/chroot.md b/src/uu/chroot/chroot.md deleted file mode 100644 index 3967d08f963..00000000000 --- a/src/uu/chroot/chroot.md +++ /dev/null @@ -1,8 +0,0 @@ - -# chroot - -``` -chroot [OPTION]... NEWROOT [COMMAND [ARG]...] -``` - -Run COMMAND with root directory set to NEWROOT. diff --git a/src/uu/chroot/locales/en-US.ftl b/src/uu/chroot/locales/en-US.ftl new file mode 100644 index 00000000000..86687fea8b6 --- /dev/null +++ b/src/uu/chroot/locales/en-US.ftl @@ -0,0 +1,2 @@ +chroot-about = Run COMMAND with root directory set to NEWROOT. +chroot-usage = chroot [OPTION]... NEWROOT [COMMAND [ARG]...] diff --git a/src/uu/chroot/src/chroot.rs b/src/uu/chroot/src/chroot.rs index 3f7c0886b5f..0b04e79ae47 100644 --- a/src/uu/chroot/src/chroot.rs +++ b/src/uu/chroot/src/chroot.rs @@ -17,10 +17,9 @@ use uucore::entries::{Locate, Passwd, grp2gid, usr2uid}; use uucore::error::{UClapError, UResult, UUsageError, set_exit_code}; use uucore::fs::{MissingHandling, ResolveMode, canonicalize}; use uucore::libc::{self, chroot, setgid, setgroups, setuid}; -use uucore::{format_usage, help_about, help_usage, show}; +use uucore::{format_usage, show}; -static ABOUT: &str = help_about!("chroot.md"); -static USAGE: &str = help_usage!("chroot.md"); +use uucore::locale::get_message; mod options { pub const NEWROOT: &str = "newroot"; @@ -237,8 +236,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("chroot-about")) + .override_usage(format_usage(&get_message("chroot-usage"))) .infer_long_args(true) .trailing_var_arg(true) .arg( @@ -439,7 +438,7 @@ fn enter_chroot(root: &Path, skip_chdir: bool) -> UResult<()> { let err = unsafe { chroot( CString::new(root.as_os_str().as_bytes().to_vec()) - .unwrap() + .map_err(|e| ChrootError::CannotEnter("root".to_string(), e.into()))? .as_bytes_with_nul() .as_ptr() .cast::(), @@ -448,7 +447,7 @@ fn enter_chroot(root: &Path, skip_chdir: bool) -> UResult<()> { if err == 0 { if !skip_chdir { - std::env::set_current_dir("/").unwrap(); + std::env::set_current_dir("/")?; } Ok(()) } else { diff --git a/src/uu/cksum/cksum.md b/src/uu/cksum/cksum.md deleted file mode 100644 index 5ca83b40150..00000000000 --- a/src/uu/cksum/cksum.md +++ /dev/null @@ -1,24 +0,0 @@ -# cksum - -``` -cksum [OPTIONS] [FILE]... -``` - -Print CRC and size for each file - -## After Help - -DIGEST determines the digest algorithm and default output format: - -- `sysv`: (equivalent to sum -s) -- `bsd`: (equivalent to sum -r) -- `crc`: (equivalent to cksum) -- `crc32b`: (only available through cksum) -- `md5`: (equivalent to md5sum) -- `sha1`: (equivalent to sha1sum) -- `sha224`: (equivalent to sha224sum) -- `sha256`: (equivalent to sha256sum) -- `sha384`: (equivalent to sha384sum) -- `sha512`: (equivalent to sha512sum) -- `blake2b`: (equivalent to b2sum) -- `sm3`: (only available through cksum) diff --git a/src/uu/cksum/locales/en-US.ftl b/src/uu/cksum/locales/en-US.ftl new file mode 100644 index 00000000000..75a4bc8d045 --- /dev/null +++ b/src/uu/cksum/locales/en-US.ftl @@ -0,0 +1,16 @@ +cksum-about = Print CRC and size for each file +cksum-usage = cksum [OPTIONS] [FILE]... +cksum-after-help = DIGEST determines the digest algorithm and default output format: + + - sysv: (equivalent to sum -s) + - bsd: (equivalent to sum -r) + - crc: (equivalent to cksum) + - crc32b: (only available through cksum) + - md5: (equivalent to md5sum) + - sha1: (equivalent to sha1sum) + - sha224: (equivalent to sha224sum) + - sha256: (equivalent to sha256sum) + - sha384: (equivalent to sha384sum) + - sha512: (equivalent to sha512sum) + - blake2b: (equivalent to b2sum) + - sm3: (only available through cksum) diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index a1a9115d9a0..0ded6c634de 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -17,19 +17,16 @@ use uucore::checksum::{ ChecksumVerbose, SUPPORTED_ALGORITHMS, calculate_blake2b_length, detect_algo, digest_reader, perform_checksum_validation, }; +use uucore::locale::get_message; use uucore::{ encoding, error::{FromIo, UResult, USimpleError}, - format_usage, help_about, help_section, help_usage, + format_usage, line_ending::LineEnding, os_str_as_bytes, show, sum::Digest, }; -const USAGE: &str = help_usage!("cksum.md"); -const ABOUT: &str = help_about!("cksum.md"); -const AFTER_HELP: &str = help_section!("after help", "cksum.md"); - #[derive(Debug, PartialEq)] enum OutputFormat { Hexadecimal, @@ -343,8 +340,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("cksum-about")) + .override_usage(format_usage(&get_message("cksum-usage"))) .infer_long_args(true) .args_override_self(true) .arg( @@ -468,7 +465,7 @@ pub fn uu_app() -> Command { ) .action(ArgAction::SetTrue), ) - .after_help(AFTER_HELP) + .after_help(get_message("cksum-after-help")) } #[cfg(test)] diff --git a/src/uu/comm/comm.md b/src/uu/comm/comm.md deleted file mode 100644 index 91f4467d443..00000000000 --- a/src/uu/comm/comm.md +++ /dev/null @@ -1,13 +0,0 @@ -# comm - -``` -comm [OPTION]... FILE1 FILE2 -``` - -Compare two sorted files line by line. - -When FILE1 or FILE2 (not both) is -, read standard input. - -With no options, produce three-column output. Column one contains -lines unique to FILE1, column two contains lines unique to FILE2, -and column three contains lines common to both files. diff --git a/src/uu/comm/locales/en-US.ftl b/src/uu/comm/locales/en-US.ftl new file mode 100644 index 00000000000..56b67be37dd --- /dev/null +++ b/src/uu/comm/locales/en-US.ftl @@ -0,0 +1,8 @@ +comm-about = Compare two sorted files line by line. + + When FILE1 or FILE2 (not both) is -, read standard input. + + With no options, produce three-column output. Column one contains + lines unique to FILE1, column two contains lines unique to FILE2, + and column three contains lines common to both files. +comm-usage = comm [OPTION]... FILE1 FILE2 diff --git a/src/uu/comm/src/comm.rs b/src/uu/comm/src/comm.rs index 11752c331a5..d595b463d2d 100644 --- a/src/uu/comm/src/comm.rs +++ b/src/uu/comm/src/comm.rs @@ -9,14 +9,13 @@ use std::cmp::Ordering; use std::fs::{File, metadata}; use std::io::{self, BufRead, BufReader, Read, Stdin, stdin}; use uucore::error::{FromIo, UResult, USimpleError}; +use uucore::format_usage; use uucore::fs::paths_refer_to_same_file; use uucore::line_ending::LineEnding; -use uucore::{format_usage, help_about, help_usage}; use clap::{Arg, ArgAction, ArgMatches, Command}; -const ABOUT: &str = help_about!("comm.md"); -const USAGE: &str = help_usage!("comm.md"); +use uucore::locale::get_message; mod options { pub const COLUMN_1: &str = "1"; @@ -314,8 +313,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("comm-about")) + .override_usage(format_usage(&get_message("comm-usage"))) .infer_long_args(true) .args_override_self(true) .arg( diff --git a/src/uu/cp/Cargo.toml b/src/uu/cp/Cargo.toml index fd5b4696e03..2f57835907f 100644 --- a/src/uu/cp/Cargo.toml +++ b/src/uu/cp/Cargo.toml @@ -22,7 +22,6 @@ clap = { workspace = true } filetime = { workspace = true } libc = { workspace = true } linux-raw-sys = { workspace = true } -quick-error = { workspace = true } selinux = { workspace = true, optional = true } uucore = { workspace = true, features = [ "backup-control", @@ -37,6 +36,7 @@ uucore = { workspace = true, features = [ ] } walkdir = { workspace = true } indicatif = { workspace = true } +thiserror = { workspace = true } [target.'cfg(unix)'.dependencies] xattr = { workspace = true } diff --git a/src/uu/cp/cp.md b/src/uu/cp/cp.md deleted file mode 100644 index 7485340f2ac..00000000000 --- a/src/uu/cp/cp.md +++ /dev/null @@ -1,25 +0,0 @@ -# cp - -``` -cp [OPTION]... [-T] SOURCE DEST -cp [OPTION]... SOURCE... DIRECTORY -cp [OPTION]... -t DIRECTORY SOURCE... -``` - -Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY. - -## After Help - -Do not copy a non-directory that has an existing destination with the same or newer modification timestamp; -instead, silently skip the file without failing. If timestamps are being preserved, the comparison is to the -source timestamp truncated to the resolutions of the destination file system and of the system calls used to -update timestamps; this avoids duplicate work if several `cp -pu` commands are executed with the same source -and destination. This option is ignored if the `-n` or `--no-clobber` option is also specified. Also, if -`--preserve=links` is also specified (like with `cp -au` for example), that will take precedence; consequently, -depending on the order that files are processed from the source, newer files in the destination may be replaced, -to mirror hard links in the source. which gives more control over which existing files in the destination are -replaced, and its value can be one of the following: - -* `all` This is the default operation when an `--update` option is not specified, and results in all existing files in the destination being replaced. -* `none` This is similar to the `--no-clobber` option, in that no files in the destination are replaced, but also skipping a file does not induce a failure. -* `older` This is the default operation when `--update` is specified, and results in files being replaced if they’re older than the corresponding source file. diff --git a/src/uu/cp/locales/en-US.ftl b/src/uu/cp/locales/en-US.ftl new file mode 100644 index 00000000000..7150d1b883b --- /dev/null +++ b/src/uu/cp/locales/en-US.ftl @@ -0,0 +1,17 @@ +cp-about = Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY. +cp-usage = cp [OPTION]... [-T] SOURCE DEST + cp [OPTION]... SOURCE... DIRECTORY + cp [OPTION]... -t DIRECTORY SOURCE... +cp-after-help = Do not copy a non-directory that has an existing destination with the same or newer modification timestamp; + instead, silently skip the file without failing. If timestamps are being preserved, the comparison is to the + source timestamp truncated to the resolutions of the destination file system and of the system calls used to + update timestamps; this avoids duplicate work if several cp -pu commands are executed with the same source + and destination. This option is ignored if the -n or --no-clobber option is also specified. Also, if + --preserve=links is also specified (like with cp -au for example), that will take precedence; consequently, + depending on the order that files are processed from the source, newer files in the destination may be replaced, + to mirror hard links in the source. which gives more control over which existing files in the destination are + replaced, and its value can be one of the following: + + - all This is the default operation when an --update option is not specified, and results in all existing files in the destination being replaced. + - none This is similar to the --no-clobber option, in that no files in the destination are replaced, but also skipping a file does not induce a failure. + - older This is the default operation when --update is specified, and results in files being replaced if they’re older than the corresponding source file. diff --git a/src/uu/cp/src/copydir.rs b/src/uu/cp/src/copydir.rs index d2e367c5c19..be81b260f28 100644 --- a/src/uu/cp/src/copydir.rs +++ b/src/uu/cp/src/copydir.rs @@ -26,7 +26,7 @@ use uucore::uio_error; use walkdir::{DirEntry, WalkDir}; use crate::{ - CopyResult, Error, Options, aligned_ancestors, context_for, copy_attributes, copy_file, + CopyResult, CpError, Options, aligned_ancestors, context_for, copy_attributes, copy_file, copy_link, }; @@ -266,7 +266,7 @@ fn copy_direntry( // TODO What other kinds of errors, if any, should // cause us to continue walking the directory? match err { - Error::IoErrContext(e, _) if e.kind() == io::ErrorKind::PermissionDenied => { + CpError::IoErrContext(e, _) if e.kind() == io::ErrorKind::PermissionDenied => { show!(uio_error!( e, "cannot open {} for reading", diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 06f0b79657d..d2966c5c4c2 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -4,22 +4,22 @@ // file that was distributed with this source code. // spell-checker:ignore (ToDO) copydir ficlone fiemap ftruncate linkgs lstat nlink nlinks pathbuf pwrite reflink strs xattrs symlinked deduplicated advcpmv nushell IRWXG IRWXO IRWXU IRWXUGO IRWXU IRWXG IRWXO IRWXUGO -use quick_error::quick_error; use std::cmp::Ordering; use std::collections::{HashMap, HashSet}; use std::ffi::OsString; +use std::fmt::Display; use std::fs::{self, Metadata, OpenOptions, Permissions}; -use std::io; #[cfg(unix)] use std::os::unix::fs::{FileTypeExt, PermissionsExt}; use std::path::{Path, PathBuf, StripPrefixError}; +use std::{fmt, io}; #[cfg(all(unix, not(target_os = "android")))] use uucore::fsxattr::copy_xattrs; use clap::{Arg, ArgAction, ArgMatches, Command, builder::ValueParser, value_parser}; use filetime::FileTime; use indicatif::{ProgressBar, ProgressStyle}; -use quick_error::ResultExt; +use thiserror::Error; use platform::copy_on_write; use uucore::display::Quotable; @@ -36,74 +36,104 @@ use uucore::{backup_control, update_control}; // requires these enum. pub use uucore::{backup_control::BackupMode, update_control::UpdateMode}; use uucore::{ - format_usage, help_about, help_section, help_usage, - parser::shortcut_value_parser::ShortcutValueParser, prompt_yes, show_error, show_warning, + format_usage, parser::shortcut_value_parser::ShortcutValueParser, prompt_yes, show_error, + show_warning, }; use crate::copydir::copy_directory; +use uucore::locale::get_message; mod copydir; mod platform; -quick_error! { - #[derive(Debug)] - pub enum Error { - /// Simple io::Error wrapper - IoErr(err: io::Error) { from() source(err) display("{err}")} +#[derive(Debug, Error)] +pub enum CpError { + /// Simple io::Error wrapper + #[error("{0}")] + IoErr(#[from] io::Error), - /// Wrapper for io::Error with path context - IoErrContext(err: io::Error, path: String) { - display("{path}: {err}") - context(path: &'a str, err: io::Error) -> (err, path.to_owned()) - context(context: String, err: io::Error) -> (err, context) - source(err) - } + /// Wrapper for io::Error with path context + #[error("{1}: {0}")] + IoErrContext(io::Error, String), - /// General copy error - Error(err: String) { - display("{err}") - from(err: String) -> (err) - from(err: &'static str) -> (err.to_string()) - } + /// General copy error + #[error("{0}")] + Error(String), + + /// Represents the state when a non-fatal error has occurred + /// and not all files were copied. + #[error("Not all files were copied")] + NotAllFilesCopied, - /// Represents the state when a non-fatal error has occurred - /// and not all files were copied. - NotAllFilesCopied {} + /// Simple walkdir::Error wrapper + #[error("{0}")] + WalkDirErr(#[from] walkdir::Error), - /// Simple walkdir::Error wrapper - WalkDirErr(err: walkdir::Error) { from() display("{err}") source(err) } + /// Simple std::path::StripPrefixError wrapper + #[error(transparent)] + StripPrefixError(#[from] StripPrefixError), - /// Simple std::path::StripPrefixError wrapper - StripPrefixError(err: StripPrefixError) { from() } + /// Result of a skipped file + /// Currently happens when "no" is selected in interactive mode or when + /// `no-clobber` flag is set and destination is already present. + /// `exit with error` is used to determine which exit code should be returned. + #[error("Skipped copying file (exit with error = {0})")] + Skipped(bool), - /// Result of a skipped file - /// Currently happens when "no" is selected in interactive mode or when - /// `no-clobber` flag is set and destination is already present. - /// `exit with error` is used to determine which exit code should be returned. - Skipped(exit_with_error:bool) { } + /// Result of a skipped file + #[error("{0}")] + InvalidArgument(String), - /// Result of a skipped file - InvalidArgument(description: String) { display("{description}") } + /// All standard options are included as an an implementation + /// path, but those that are not implemented yet should return + /// a NotImplemented error. + #[error("Option '{0}' not yet implemented.")] + NotImplemented(String), - /// All standard options are included as an an implementation - /// path, but those that are not implemented yet should return - /// a NotImplemented error. - NotImplemented(opt: String) { display("Option '{opt}' not yet implemented.") } + /// Invalid arguments to backup + #[error(transparent)] + Backup(#[from] BackupError), + + #[error("'{}' is not a directory", .0.display())] + NotADirectory(PathBuf), +} - /// Invalid arguments to backup - Backup(description: String) { display("{description}\nTry '{} --help' for more information.", uucore::execution_phrase()) } +// Manual impl for &str +impl From<&'static str> for CpError { + fn from(s: &'static str) -> Self { + Self::Error(s.to_string()) + } +} - NotADirectory(path: PathBuf) { display("'{}' is not a directory", path.display()) } +impl From for CpError { + fn from(s: String) -> Self { + Self::Error(s) } } -impl UError for Error { +#[derive(Debug)] +pub struct BackupError(String); + +impl Display for BackupError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "cp: {}\nTry '{} --help' for more information.", + self.0, + uucore::execution_phrase() + ) + } +} + +impl std::error::Error for BackupError {} + +impl UError for CpError { fn code(&self) -> i32 { EXIT_ERR } } -pub type CopyResult = Result; +pub type CopyResult = Result; /// Specifies how to overwrite files. #[derive(Debug, Clone, Copy, Eq, PartialEq, Default)] @@ -422,10 +452,6 @@ fn show_debug(copy_debug: &CopyDebug) { ); } -const ABOUT: &str = help_about!("cp.md"); -const USAGE: &str = help_usage!("cp.md"); -const AFTER_HELP: &str = help_section!("after help", "cp.md"); - static EXIT_ERR: i32 = 1; // Argument constants @@ -494,10 +520,11 @@ pub fn uu_app() -> Command { ]; Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("cp-about")) + .override_usage(format_usage(&get_message("cp-usage"))) .after_help(format!( - "{AFTER_HELP}\n\n{}", + "{}\n\n{}", + get_message("cp-after-help"), backup_control::BACKUP_CONTROL_LONG_HELP )) .infer_long_args(true) @@ -803,7 +830,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { match error { // Error::NotAllFilesCopied is non-fatal, but the error // code should still be EXIT_ERR as does GNU cp - Error::NotAllFilesCopied => {} + CpError::NotAllFilesCopied => {} // Else we caught a fatal bubbled-up error, log it to stderr _ => show_error!("{error}"), }; @@ -940,7 +967,7 @@ impl Attributes { } } - pub fn parse_iter(values: impl Iterator) -> Result + pub fn parse_iter(values: impl Iterator) -> CopyResult where T: AsRef, { @@ -953,7 +980,7 @@ impl Attributes { /// Tries to match string containing a parameter to preserve with the corresponding entry in the /// Attributes struct. - fn parse_single_string(value: &str) -> Result { + fn parse_single_string(value: &str) -> CopyResult { let value = value.to_lowercase(); if value == "all" { @@ -970,7 +997,7 @@ impl Attributes { "link" | "links" => &mut new.links, "xattr" => &mut new.xattr, _ => { - return Err(Error::InvalidArgument(format!( + return Err(CpError::InvalidArgument(format!( "invalid attribute {}", value.quote() ))); @@ -998,14 +1025,14 @@ impl Options { && matches.value_source(not_implemented_opt) == Some(clap::parser::ValueSource::CommandLine) { - return Err(Error::NotImplemented(not_implemented_opt.to_string())); + return Err(CpError::NotImplemented(not_implemented_opt.to_string())); } } let recursive = matches.get_flag(options::RECURSIVE) || matches.get_flag(options::ARCHIVE); let backup_mode = match backup_control::determine_backup_mode(matches) { - Err(e) => return Err(Error::Backup(format!("{e}"))), + Err(e) => return Err(CpError::Backup(BackupError(format!("{e}")))), Ok(mode) => mode, }; let update_mode = update_control::determine_update_mode(matches); @@ -1015,7 +1042,7 @@ impl Options { .get_one::(update_control::arguments::OPT_UPDATE) .is_some_and(|v| v == "none" || v == "none-fail") { - return Err(Error::InvalidArgument( + return Err(CpError::InvalidArgument( "--backup is mutually exclusive with -n or --update=none-fail".to_string(), )); } @@ -1032,7 +1059,7 @@ impl Options { if let Some(dir) = &target_dir { if !dir.is_dir() { - return Err(Error::NotADirectory(dir.clone())); + return Err(CpError::NotADirectory(dir.clone())); } }; // cp follows POSIX conventions for overriding options such as "-a", @@ -1120,7 +1147,7 @@ impl Options { #[cfg(not(feature = "selinux"))] if let Preserve::Yes { required } = attributes.context { let selinux_disabled_error = - Error::Error("SELinux was not enabled during the compile time!".to_string()); + CpError::Error("SELinux was not enabled during the compile time!".to_owned()); if required { return Err(selinux_disabled_error); } else { @@ -1163,7 +1190,7 @@ impl Options { "auto" => ReflinkMode::Auto, "never" => ReflinkMode::Never, value => { - return Err(Error::InvalidArgument(format!( + return Err(CpError::InvalidArgument(format!( "invalid argument {} for \'reflink\'", value.quote() ))); @@ -1180,7 +1207,7 @@ impl Options { "auto" => SparseMode::Auto, "never" => SparseMode::Never, _ => { - return Err(Error::InvalidArgument(format!( + return Err(CpError::InvalidArgument(format!( "invalid argument {val} for \'sparse\'" ))); } @@ -1298,14 +1325,14 @@ fn parse_path_args( } /// When handling errors, we don't always want to show them to the user. This function handles that. -fn show_error_if_needed(error: &Error) { +fn show_error_if_needed(error: &CpError) { match error { // When using --no-clobber, we don't want to show // an error message - Error::NotAllFilesCopied => { + CpError::NotAllFilesCopied => { // Need to return an error code } - Error::Skipped(_) => { + CpError::Skipped(_) => { // touch a b && echo "n"|cp -i a b && echo $? // should return an error from GNU 9.2 } @@ -1382,7 +1409,7 @@ pub fn copy(sources: &[PathBuf], target: &Path, options: &Options) -> CopyResult // There is already a file and it isn't a symlink (managed in a different place) if copied_destinations.contains(&dest) && options.backup != BackupMode::Numbered { // If the target file was already created in this cp call, do not overwrite - return Err(Error::Error(format!( + return Err(CpError::Error(format!( "will not overwrite just-created '{}' with '{}'", dest.display(), source.display() @@ -1401,7 +1428,7 @@ pub fn copy(sources: &[PathBuf], target: &Path, options: &Options) -> CopyResult &mut copied_files, ) { show_error_if_needed(&error); - if !matches!(error, Error::Skipped(false)) { + if !matches!(error, CpError::Skipped(false)) { non_fatal_errors = true; } } else { @@ -1416,7 +1443,7 @@ pub fn copy(sources: &[PathBuf], target: &Path, options: &Options) -> CopyResult } if non_fatal_errors { - Err(Error::NotAllFilesCopied) + Err(CpError::NotAllFilesCopied) } else { Ok(()) } @@ -1564,7 +1591,7 @@ impl OverwriteMode { if debug { println!("skipped {}", path.quote()); } - Err(Error::Skipped(false)) + Err(CpError::Skipped(false)) } Self::Interactive(_) => { let prompt_yes_result = if let Some((octal, human_readable)) = @@ -1581,7 +1608,7 @@ impl OverwriteMode { if prompt_yes_result { Ok(()) } else { - Err(Error::Skipped(true)) + Err(CpError::Skipped(true)) } } Self::Clobber(_) => Ok(()), @@ -1650,7 +1677,8 @@ pub(crate) fn copy_attributes( attributes: &Attributes, ) -> CopyResult<()> { let context = &*format!("{} -> {}", source.quote(), dest.quote()); - let source_metadata = fs::symlink_metadata(source).context(context)?; + let source_metadata = + fs::symlink_metadata(source).map_err(|e| CpError::IoErrContext(e, context.to_owned()))?; // Ownership must be changed first to avoid interfering with mode change. #[cfg(unix)] @@ -1665,7 +1693,9 @@ pub(crate) fn copy_attributes( // gnu compatibility: cp doesn't report an error if it fails to set the ownership. let _ = wrap_chown( dest, - &dest.symlink_metadata().context(context)?, + &dest + .symlink_metadata() + .map_err(|e| CpError::IoErrContext(e, context.to_owned()))?, Some(dest_uid), Some(dest_gid), false, @@ -1684,12 +1714,13 @@ pub(crate) fn copy_attributes( // do nothing, since every symbolic link has the same // permissions. if !dest.is_symlink() { - fs::set_permissions(dest, source_metadata.permissions()).context(context)?; + fs::set_permissions(dest, source_metadata.permissions()) + .map_err(|e| CpError::IoErrContext(e, context.to_owned()))?; // FIXME: Implement this for windows as well #[cfg(feature = "feat_acl")] exacl::getfacl(source, None) .and_then(|acl| exacl::setfacl(&[dest], &acl, None)) - .map_err(|err| Error::Error(err.to_string()))?; + .map_err(|err| CpError::Error(err.to_string()))?; } Ok(()) @@ -1713,14 +1744,14 @@ pub(crate) fn copy_attributes( if let Ok(context) = selinux::SecurityContext::of_path(source, false, false) { if let Some(context) = context { if let Err(e) = context.set_for_path(dest, false, false) { - return Err(Error::Error(format!( + return Err(CpError::Error(format!( "failed to set the security context of {}: {e}", dest.display() ))); } } } else { - return Err(Error::Error(format!( + return Err(CpError::Error(format!( "failed to get security context of {}", source.display() ))); @@ -1760,19 +1791,29 @@ fn symlink_file( ) -> CopyResult<()> { #[cfg(not(windows))] { - std::os::unix::fs::symlink(source, dest).context(format!( - "cannot create symlink {} to {}", - get_filename(dest).unwrap_or("invalid file name").quote(), - get_filename(source).unwrap_or("invalid file name").quote() - ))?; + std::os::unix::fs::symlink(source, dest).map_err(|e| { + CpError::IoErrContext( + e, + format!( + "cannot create symlink {} to {}", + get_filename(dest).unwrap_or("invalid file name").quote(), + get_filename(source).unwrap_or("invalid file name").quote() + ), + ) + })?; } #[cfg(windows)] { - std::os::windows::fs::symlink_file(source, dest).context(format!( - "cannot create symlink {} to {}", - get_filename(dest).unwrap_or("invalid file name").quote(), - get_filename(source).unwrap_or("invalid file name").quote() - ))?; + std::os::windows::fs::symlink_file(source, dest).map_err(|e| { + CpError::IoErrContext( + e, + format!( + "cannot create symlink {} to {}", + get_filename(dest).unwrap_or("invalid file name").quote(), + get_filename(source).unwrap_or("invalid file name").quote() + ), + ) + })?; } if let Ok(file_info) = FileInformation::from_path(dest, false) { symlinked_files.insert(file_info); @@ -1867,7 +1908,7 @@ fn handle_existing_dest( if options.debug { println!("skipped {}", dest.quote()); } - return Err(Error::Skipped(false)); + return Err(CpError::Skipped(false)); } if options.update != UpdateMode::IfOlder { @@ -1947,7 +1988,7 @@ fn delete_dest_if_needed_and_allowed( &FileInformation::from_path( source, options.dereference(source_in_command_line) - ).context(format!("cannot stat {}", source.quote()))? + ).map_err(|e| CpError::IoErrContext(e, format!("cannot stat {}", source.quote())))? ) } } @@ -2088,11 +2129,16 @@ fn handle_copy_mode( } else { fs::hard_link(source, dest) } - .context(format!( - "cannot create hard link {} to {}", - get_filename(dest).unwrap_or("invalid file name").quote(), - get_filename(source).unwrap_or("invalid file name").quote() - ))?; + .map_err(|e| { + CpError::IoErrContext( + e, + format!( + "cannot create hard link {} to {}", + get_filename(dest).unwrap_or("invalid file name").quote(), + get_filename(source).unwrap_or("invalid file name").quote() + ), + ) + })?; } CopyMode::Copy => { copy_helper( @@ -2137,7 +2183,10 @@ fn handle_copy_mode( return Ok(PerformedAction::Skipped); } UpdateMode::NoneFail => { - return Err(Error::Error(format!("not replacing '{}'", dest.display()))); + return Err(CpError::Error(format!( + "not replacing '{}'", + dest.display() + ))); } UpdateMode::IfOlder => { let dest_metadata = fs::symlink_metadata(dest)?; @@ -2209,7 +2258,10 @@ fn calculate_dest_permissions( context: &str, ) -> CopyResult { if dest.exists() { - Ok(dest.symlink_metadata().context(context)?.permissions()) + Ok(dest + .symlink_metadata() + .map_err(|e| CpError::IoErrContext(e, context.to_owned()))? + .permissions()) } else { #[cfg(unix)] { @@ -2259,7 +2311,7 @@ fn copy_file( .map(|info| symlinked_files.contains(&info)) .unwrap_or(false) { - return Err(Error::Error(format!( + return Err(CpError::Error(format!( "will not copy '{}' through just-created symlink '{}'", source.display(), dest.display() @@ -2269,7 +2321,7 @@ fn copy_file( // Example: "cp file1 dir1/file1 tmp" where "tmp" is a directory containing a symlink "file1" pointing to a file named "foo". // foo will contain the contents of "file1" and "dir1/file1" will not be copied over to "tmp/file1" if copied_destinations.contains(dest) { - return Err(Error::Error(format!( + return Err(CpError::Error(format!( "will not copy '{}' through just-created symlink '{}'", source.display(), dest.display() @@ -2286,7 +2338,7 @@ fn copy_file( && !is_symlink_loop(dest) && std::env::var_os("POSIXLY_CORRECT").is_none() { - return Err(Error::Error(format!( + return Err(CpError::Error(format!( "not writing through dangling symlink '{}'", dest.display() ))); @@ -2368,7 +2420,7 @@ fn copy_file( // in the destination tree. if let Some(new_source) = copied_files.get( &FileInformation::from_path(source, options.dereference(source_in_command_line)) - .context(format!("cannot stat {}", source.quote()))?, + .map_err(|e| CpError::IoErrContext(e, format!("cannot stat {}", source.quote())))?, ) { fs::hard_link(new_source, dest)?; @@ -2462,7 +2514,7 @@ fn copy_file( if let Err(e) = uucore::selinux::set_selinux_security_context(dest, options.context.as_ref()) { - return Err(Error::Error(format!("SELinux error: {}", e))); + return Err(CpError::Error(format!("SELinux error: {}", e))); } } @@ -2542,7 +2594,7 @@ fn copy_helper( } if path_ends_with_terminator(dest) && !dest.is_dir() { - return Err(Error::NotADirectory(dest.to_path_buf())); + return Err(CpError::NotADirectory(dest.to_path_buf())); } if source_is_fifo && options.recursive && !options.copy_contents { diff --git a/src/uu/cp/src/platform/linux.rs b/src/uu/cp/src/platform/linux.rs index 9bf257f8276..c4606d43649 100644 --- a/src/uu/cp/src/platform/linux.rs +++ b/src/uu/cp/src/platform/linux.rs @@ -14,11 +14,11 @@ use std::os::unix::io::AsRawFd; use std::path::Path; use uucore::buf_copy; -use quick_error::ResultExt; - use uucore::mode::get_umask; -use crate::{CopyDebug, CopyResult, OffloadReflinkDebug, ReflinkMode, SparseDebug, SparseMode}; +use crate::{ + CopyDebug, CopyResult, CpError, OffloadReflinkDebug, ReflinkMode, SparseDebug, SparseMode, +}; /// The fallback behavior for [`clone`] on failed system call. #[derive(Clone, Copy)] @@ -404,7 +404,7 @@ pub(crate) fn copy_on_write( return Err("`--reflink=always` can be used only with --sparse=auto".into()); } }; - result.context(context)?; + result.map_err(|e| CpError::IoErrContext(e, context.to_owned()))?; Ok(copy_debug) } diff --git a/src/uu/cp/src/platform/macos.rs b/src/uu/cp/src/platform/macos.rs index 35879c29df7..be2e43a9562 100644 --- a/src/uu/cp/src/platform/macos.rs +++ b/src/uu/cp/src/platform/macos.rs @@ -9,11 +9,12 @@ use std::os::unix::ffi::OsStrExt; use std::os::unix::fs::OpenOptionsExt; use std::path::Path; -use quick_error::ResultExt; use uucore::buf_copy; use uucore::mode::get_umask; -use crate::{CopyDebug, CopyResult, OffloadReflinkDebug, ReflinkMode, SparseDebug, SparseMode}; +use crate::{ + CopyDebug, CopyResult, CpError, OffloadReflinkDebug, ReflinkMode, SparseDebug, SparseMode, +}; /// Copies `source` to `dest` using copy-on-write if possible. /// @@ -104,14 +105,15 @@ pub(crate) fn copy_on_write( let context = buf_copy::copy_stream(&mut src_file, &mut dst_file) .map_err(|_| std::io::Error::from(std::io::ErrorKind::Other)) - .context(context)?; + .map_err(|e| CpError::IoErrContext(e, context.to_owned()))?; if source_is_fifo { dst_file.set_permissions(src_file.metadata()?.permissions())?; } context } else { - fs::copy(source, dest).context(context)? + fs::copy(source, dest) + .map_err(|e| CpError::IoErrContext(e, context.to_owned()))? } } }; diff --git a/src/uu/cp/src/platform/other.rs b/src/uu/cp/src/platform/other.rs index 7ca1a5ded7c..9df2f99939a 100644 --- a/src/uu/cp/src/platform/other.rs +++ b/src/uu/cp/src/platform/other.rs @@ -6,9 +6,9 @@ use std::fs; use std::path::Path; -use quick_error::ResultExt; - -use crate::{CopyDebug, CopyResult, OffloadReflinkDebug, ReflinkMode, SparseDebug, SparseMode}; +use crate::{ + CopyDebug, CopyResult, CpError, OffloadReflinkDebug, ReflinkMode, SparseDebug, SparseMode, +}; /// Copies `source` to `dest` for systems without copy-on-write pub(crate) fn copy_on_write( @@ -31,7 +31,7 @@ pub(crate) fn copy_on_write( reflink: OffloadReflinkDebug::Unsupported, sparse_detection: SparseDebug::Unsupported, }; - fs::copy(source, dest).context(context)?; + fs::copy(source, dest).map_err(|e| CpError::IoErrContext(e, context.to_owned()))?; Ok(copy_debug) } diff --git a/src/uu/cp/src/platform/other_unix.rs b/src/uu/cp/src/platform/other_unix.rs index aa8fed3fab1..94799139ac5 100644 --- a/src/uu/cp/src/platform/other_unix.rs +++ b/src/uu/cp/src/platform/other_unix.rs @@ -7,11 +7,12 @@ use std::fs::{self, File, OpenOptions}; use std::os::unix::fs::OpenOptionsExt; use std::path::Path; -use quick_error::ResultExt; use uucore::buf_copy; use uucore::mode::get_umask; -use crate::{CopyDebug, CopyResult, OffloadReflinkDebug, ReflinkMode, SparseDebug, SparseMode}; +use crate::{ + CopyDebug, CopyResult, CpError, OffloadReflinkDebug, ReflinkMode, SparseDebug, SparseMode, +}; /// Copies `source` to `dest` for systems without copy-on-write pub(crate) fn copy_on_write( @@ -48,7 +49,7 @@ pub(crate) fn copy_on_write( buf_copy::copy_stream(&mut src_file, &mut dst_file) .map_err(|_| std::io::Error::from(std::io::ErrorKind::Other)) - .context(context)?; + .map_err(|e| CpError::IoErrContext(e, context.to_owned()))?; if source_is_fifo { dst_file.set_permissions(src_file.metadata()?.permissions())?; @@ -56,7 +57,7 @@ pub(crate) fn copy_on_write( return Ok(copy_debug); } - fs::copy(source, dest).context(context)?; + fs::copy(source, dest).map_err(|e| CpError::IoErrContext(e, context.to_owned()))?; Ok(copy_debug) } diff --git a/src/uu/csplit/csplit.md b/src/uu/csplit/csplit.md deleted file mode 100644 index 1d428fc8ee6..00000000000 --- a/src/uu/csplit/csplit.md +++ /dev/null @@ -1,11 +0,0 @@ -# csplit - -``` -csplit [OPTION]... FILE PATTERN... -``` - -Split a file into sections determined by context lines - -## After Help - -Output pieces of FILE separated by PATTERN(s) to files 'xx00', 'xx01', ..., and output byte counts of each piece to standard output. diff --git a/src/uu/csplit/locales/en-US.ftl b/src/uu/csplit/locales/en-US.ftl new file mode 100644 index 00000000000..0198b521b6d --- /dev/null +++ b/src/uu/csplit/locales/en-US.ftl @@ -0,0 +1,3 @@ +csplit-about = Split a file into sections determined by context lines +csplit-usage = csplit [OPTION]... FILE PATTERN... +csplit-after-help = Output pieces of FILE separated by PATTERN(s) to files 'xx00', 'xx01', ..., and output byte counts of each piece to standard output. diff --git a/src/uu/csplit/src/csplit.rs b/src/uu/csplit/src/csplit.rs index 621823aebba..5b2478ccd58 100644 --- a/src/uu/csplit/src/csplit.rs +++ b/src/uu/csplit/src/csplit.rs @@ -16,7 +16,7 @@ use clap::{Arg, ArgAction, ArgMatches, Command}; use regex::Regex; use uucore::display::Quotable; use uucore::error::{FromIo, UResult}; -use uucore::{format_usage, help_about, help_section, help_usage}; +use uucore::format_usage; mod csplit_error; mod patterns; @@ -25,9 +25,7 @@ mod split_name; use crate::csplit_error::CsplitError; use crate::split_name::SplitName; -const ABOUT: &str = help_about!("csplit.md"); -const AFTER_HELP: &str = help_section!("after help", "csplit.md"); -const USAGE: &str = help_usage!("csplit.md"); +use uucore::locale::get_message; mod options { pub const SUFFIX_FORMAT: &str = "suffix-format"; @@ -631,8 +629,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("csplit-about")) + .override_usage(format_usage(&get_message("csplit-usage"))) .args_override_self(true) .infer_long_args(true) .arg( @@ -697,7 +695,7 @@ pub fn uu_app() -> Command { .action(ArgAction::Append) .required(true), ) - .after_help(AFTER_HELP) + .after_help(get_message("csplit-after-help")) } #[cfg(test)] diff --git a/src/uu/csplit/src/patterns.rs b/src/uu/csplit/src/patterns.rs index bdf15b51197..9326ea3795e 100644 --- a/src/uu/csplit/src/patterns.rs +++ b/src/uu/csplit/src/patterns.rs @@ -106,8 +106,8 @@ pub fn get_patterns(args: &[String]) -> Result, CsplitError> { fn extract_patterns(args: &[String]) -> Result, CsplitError> { let mut patterns = Vec::with_capacity(args.len()); let to_match_reg = - Regex::new(r"^(/(?P.+)/|%(?P.+)%)(?P[\+-]?\d+)?$").unwrap(); - let execute_ntimes_reg = Regex::new(r"^\{(?P\d+)|\*\}$").unwrap(); + Regex::new(r"^(/(?P.+)/|%(?P.+)%)(?P[\+-]?[0-9]+)?$").unwrap(); + let execute_ntimes_reg = Regex::new(r"^\{(?P[0-9]+)|\*\}$").unwrap(); let mut iter = args.iter().peekable(); while let Some(arg) = iter.next() { diff --git a/src/uu/cut/cut.md b/src/uu/cut/cut.md deleted file mode 100644 index 5c21d23dcf9..00000000000 --- a/src/uu/cut/cut.md +++ /dev/null @@ -1,112 +0,0 @@ -# cut - - - -``` -cut OPTION... [FILE]... -``` - -Prints specified byte or field columns from each line of stdin or the input files - -## After Help - -Each call must specify a mode (what to use for columns), -a sequence (which columns to print), and provide a data source - -### Specifying a mode - -Use `--bytes` (`-b`) or `--characters` (`-c`) to specify byte mode - -Use `--fields` (`-f`) to specify field mode, where each line is broken into -fields identified by a delimiter character. For example for a typical CSV -you could use this in combination with setting comma as the delimiter - -### Specifying a sequence - -A sequence is a group of 1 or more numbers or inclusive ranges separated -by a commas. - -``` -cut -f 2,5-7 some_file.txt -``` - -will display the 2nd, 5th, 6th, and 7th field for each source line - -Ranges can extend to the end of the row by excluding the second number - -``` -cut -f 3- some_file.txt -``` - -will display the 3rd field and all fields after for each source line - -The first number of a range can be excluded, and this is effectively the -same as using 1 as the first number: it causes the range to begin at the -first column. Ranges can also display a single column - -``` -cut -f 1,3-5 some_file.txt -``` - -will display the 1st, 3rd, 4th, and 5th field for each source line - -The `--complement` option, when used, inverts the effect of the sequence - -``` -cut --complement -f 4-6 some_file.txt -``` - -will display the every field but the 4th, 5th, and 6th - -### Specifying a data source - -If no `sourcefile` arguments are specified, stdin is used as the source of -lines to print - -If `sourcefile` arguments are specified, stdin is ignored and all files are -read in consecutively if a `sourcefile` is not successfully read, a warning -will print to stderr, and the eventual status code will be 1, but cut -will continue to read through proceeding `sourcefiles` - -To print columns from both STDIN and a file argument, use `-` (dash) as a -`sourcefile` argument to represent stdin. - -### Field Mode options - -The fields in each line are identified by a delimiter (separator) - -#### Set the delimiter - -Set the delimiter which separates fields in the file using the -`--delimiter` (`-d`) option. Setting the delimiter is optional. -If not set, a default delimiter of Tab will be used. - -If the `-w` option is provided, fields will be separated by any number -of whitespace characters (Space and Tab). The output delimiter will -be a Tab unless explicitly specified. Only one of `-d` or `-w` option can be specified. -This is an extension adopted from FreeBSD. - -#### Optionally Filter based on delimiter - -If the `--only-delimited` (`-s`) flag is provided, only lines which -contain the delimiter will be printed - -#### Replace the delimiter - -If the `--output-delimiter` option is provided, the argument used for -it will replace the delimiter character in each line printed. This is -useful for transforming tabular data - e.g. to convert a CSV to a -TSV (tab-separated file) - -### Line endings - -When the `--zero-terminated` (`-z`) option is used, cut sees \\0 (null) as the -'line ending' character (both for the purposes of reading lines and -separating printed lines) instead of \\n (newline). This is useful for -tabular data where some of the cells may contain newlines - -``` -echo 'ab\\0cd' | cut -z -c 1 -``` - -will result in 'a\\0c\\0' diff --git a/src/uu/cut/locales/en-US.ftl b/src/uu/cut/locales/en-US.ftl new file mode 100644 index 00000000000..b7feb048cec --- /dev/null +++ b/src/uu/cut/locales/en-US.ftl @@ -0,0 +1,92 @@ +cut-about = Prints specified byte or field columns from each line of stdin or the input files +cut-usage = cut OPTION... [FILE]... +cut-after-help = Each call must specify a mode (what to use for columns), + a sequence (which columns to print), and provide a data source + + ### Specifying a mode + + Use --bytes (-b) or --characters (-c) to specify byte mode + + Use --fields (-f) to specify field mode, where each line is broken into + fields identified by a delimiter character. For example for a typical CSV + you could use this in combination with setting comma as the delimiter + + ### Specifying a sequence + + A sequence is a group of 1 or more numbers or inclusive ranges separated + by a commas. + + cut -f 2,5-7 some_file.txt + + will display the 2nd, 5th, 6th, and 7th field for each source line + + Ranges can extend to the end of the row by excluding the second number + + cut -f 3- some_file.txt + + will display the 3rd field and all fields after for each source line + + The first number of a range can be excluded, and this is effectively the + same as using 1 as the first number: it causes the range to begin at the + first column. Ranges can also display a single column + + cut -f 1,3-5 some_file.txt + + will display the 1st, 3rd, 4th, and 5th field for each source line + + The --complement option, when used, inverts the effect of the sequence + + cut --complement -f 4-6 some_file.txt + + will display the every field but the 4th, 5th, and 6th + + ### Specifying a data source + + If no sourcefile arguments are specified, stdin is used as the source of + lines to print + + If sourcefile arguments are specified, stdin is ignored and all files are + read in consecutively if a sourcefile is not successfully read, a warning + will print to stderr, and the eventual status code will be 1, but cut + will continue to read through proceeding sourcefiles + + To print columns from both STDIN and a file argument, use - (dash) as a + sourcefile argument to represent stdin. + + ### Field Mode options + + The fields in each line are identified by a delimiter (separator) + + #### Set the delimiter + + Set the delimiter which separates fields in the file using the + --delimiter (-d) option. Setting the delimiter is optional. + If not set, a default delimiter of Tab will be used. + + If the -w option is provided, fields will be separated by any number + of whitespace characters (Space and Tab). The output delimiter will + be a Tab unless explicitly specified. Only one of -d or -w option can be specified. + This is an extension adopted from FreeBSD. + + #### Optionally Filter based on delimiter + + If the --only-delimited (-s) flag is provided, only lines which + contain the delimiter will be printed + + #### Replace the delimiter + + If the --output-delimiter option is provided, the argument used for + it will replace the delimiter character in each line printed. This is + useful for transforming tabular data - e.g. to convert a CSV to a + TSV (tab-separated file) + + ### Line endings + + When the --zero-terminated (-z) option is used, cut sees \\0 (null) as the + 'line ending' character (both for the purposes of reading lines and + separating printed lines) instead of \\n (newline). This is useful for + tabular data where some of the cells may contain newlines + + echo 'ab\\0cd' | cut -z -c 1 + + will result in 'a\\0c\\0' diff --git a/src/uu/cut/src/cut.rs b/src/uu/cut/src/cut.rs index 49f5445f36f..19d5d7c0a3e 100644 --- a/src/uu/cut/src/cut.rs +++ b/src/uu/cut/src/cut.rs @@ -18,16 +18,13 @@ use uucore::os_str_as_bytes; use self::searcher::Searcher; use matcher::{ExactMatcher, Matcher, WhitespaceMatcher}; +use uucore::locale::get_message; use uucore::ranges::Range; -use uucore::{format_usage, help_about, help_section, help_usage, show_error, show_if_err}; +use uucore::{format_usage, show_error, show_if_err}; mod matcher; mod searcher; -const USAGE: &str = help_usage!("cut.md"); -const ABOUT: &str = help_about!("cut.md"); -const AFTER_HELP: &str = help_section!("after help", "cut.md"); - struct Options<'a> { out_delimiter: Option<&'a [u8]>, line_ending: LineEnding, @@ -580,9 +577,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .override_usage(format_usage(USAGE)) - .about(ABOUT) - .after_help(AFTER_HELP) + .override_usage(format_usage(&get_message("cut-usage"))) + .about(get_message("cut-about")) + .after_help(get_message("cut-after-help")) .infer_long_args(true) // While `args_override_self(true)` for some arguments, such as `-d` // and `--output-delimiter`, is consistent to the behavior of GNU cut, diff --git a/src/uu/date/Cargo.toml b/src/uu/date/Cargo.toml index 087d4befc7e..af1e87fb4aa 100644 --- a/src/uu/date/Cargo.toml +++ b/src/uu/date/Cargo.toml @@ -1,4 +1,4 @@ -# spell-checker:ignore datetime +# spell-checker:ignore datetime tzdb zoneinfo [package] name = "uu_date" description = "date ~ (uutils) display or set the current time" @@ -19,9 +19,14 @@ workspace = true path = "src/date.rs" [dependencies] -chrono = { workspace = true } clap = { workspace = true } -uucore = { workspace = true, features = ["custom-tz-fmt", "parser"] } +chrono = { workspace = true } # TODO: Eventually we'll want to remove this +jiff = { workspace = true, features = [ + "tzdb-bundle-platform", + "tzdb-zoneinfo", + "tzdb-concatenated", +] } +uucore = { workspace = true, features = ["parser"] } parse_datetime = { workspace = true } [target.'cfg(unix)'.dependencies] diff --git a/src/uu/date/date-usage.md b/src/uu/date/date-usage.md deleted file mode 100644 index 109bfd3988b..00000000000 --- a/src/uu/date/date-usage.md +++ /dev/null @@ -1,81 +0,0 @@ -# `date` usage - - - -FORMAT controls the output. Interpreted sequences are: - -| Sequence | Description | Example | -| -------- | -------------------------------------------------------------------- | ---------------------- | -| %% | a literal % | % | -| %a | locale's abbreviated weekday name | Sun | -| %A | locale's full weekday name | Sunday | -| %b | locale's abbreviated month name | Jan | -| %B | locale's full month name | January | -| %c | locale's date and time | Thu Mar 3 23:05:25 2005| -| %C | century; like %Y, except omit last two digits | 20 | -| %d | day of month | 01 | -| %D | date; same as %m/%d/%y | 12/31/99 | -| %e | day of month, space padded; same as %_d | 3 | -| %F | full date; same as %Y-%m-%d | 2005-03-03 | -| %g | last two digits of year of ISO week number (see %G) | 05 | -| %G | year of ISO week number (see %V); normally useful only with %V | 2005 | -| %h | same as %b | Jan | -| %H | hour (00..23) | 23 | -| %I | hour (01..12) | 11 | -| %j | day of year (001..366) | 062 | -| %k | hour, space padded ( 0..23); same as %_H | 3 | -| %l | hour, space padded ( 1..12); same as %_I | 9 | -| %m | month (01..12) | 03 | -| %M | minute (00..59) | 30 | -| %n | a newline | \n | -| %N | nanoseconds (000000000..999999999) | 123456789 | -| %p | locale's equivalent of either AM or PM; blank if not known | PM | -| %P | like %p, but lower case | pm | -| %q | quarter of year (1..4) | 1 | -| %r | locale's 12-hour clock time | 11:11:04 PM | -| %R | 24-hour hour and minute; same as %H:%M | 23:30 | -| %s | seconds since 1970-01-01 00:00:00 UTC | 1615432800 | -| %S | second (00..60) | 30 | -| %t | a tab | \t | -| %T | time; same as %H:%M:%S | 23:30:30 | -| %u | day of week (1..7); 1 is Monday | 4 | -| %U | week number of year, with Sunday as first day of week (00..53) | 10 | -| %V | ISO week number, with Monday as first day of week (01..53) | 12 | -| %w | day of week (0..6); 0 is Sunday | 4 | -| %W | week number of year, with Monday as first day of week (00..53) | 11 | -| %x | locale's date representation | 03/03/2005 | -| %X | locale's time representation | 23:30:30 | -| %y | last two digits of year (00..99) | 05 | -| %Y | year | 2005 | -| %z | +hhmm numeric time zone | -0400 | -| %:z | +hh:mm numeric time zone | -04:00 | -| %::z | +hh:mm:ss numeric time zone | -04:00:00 | -| %:::z | numeric time zone with : to necessary precision | -04, +05:30 | -| %Z | alphabetic time zone abbreviation | EDT | - -By default, date pads numeric fields with zeroes. -The following optional flags may follow '%': - -* `-` (hyphen) do not pad the field -* `_` (underscore) pad with spaces -* `0` (zero) pad with zeros -* `^` use upper case if possible -* `#` use opposite case if possible - -After any flags comes an optional field width, as a decimal number; -then an optional modifier, which is either -E to use the locale's alternate representations if available, or -O to use the locale's alternate numeric symbols if available. - -Examples: -Convert seconds since the epoch (1970-01-01 UTC) to a date - -``` -date --date='@2147483647' -``` - -Show the time on the west coast of the US (use tzselect(1) to find TZ) - -``` -TZ='America/Los_Angeles' date -``` diff --git a/src/uu/date/date.md b/src/uu/date/date.md deleted file mode 100644 index 97f1340169b..00000000000 --- a/src/uu/date/date.md +++ /dev/null @@ -1,10 +0,0 @@ - - -# date - -``` -date [OPTION]... [+FORMAT]... -date [OPTION]... [MMDDhhmm[[CC]YY][.ss]] -``` - -Print or set the system date and time diff --git a/src/uu/date/locales/en-US.ftl b/src/uu/date/locales/en-US.ftl new file mode 100644 index 00000000000..8889943beb1 --- /dev/null +++ b/src/uu/date/locales/en-US.ftl @@ -0,0 +1,76 @@ +date-about = + Print or set the system date and time + +date-usage = + date [OPTION]... [+FORMAT]... + date [OPTION]... [MMDDhhmm[[CC]YY][.ss]] + + FORMAT controls the output. Interpreted sequences are: + { "| Sequence | Description | Example |" } + { "| -------- | -------------------------------------------------------------------- | ---------------------- |" } + { "| %% | a literal % | % |" } + { "| %a | locale's abbreviated weekday name | Sun |" } + { "| %A | locale's full weekday name | Sunday |" } + { "| %b | locale's abbreviated month name | Jan |" } + { "| %B | locale's full month name | January |" } + { "| %c | locale's date and time | Thu Mar 3 23:05:25 2005|" } + { "| %C | century; like %Y, except omit last two digits | 20 |" } + { "| %d | day of month | 01 |" } + { "| %D | date; same as %m/%d/%y | 12/31/99 |" } + { "| %e | day of month, space padded; same as %_d | 3 |" } + { "| %F | full date; same as %Y-%m-%d | 2005-03-03 |" } + { "| %g | last two digits of year of ISO week number (see %G) | 05 |" } + { "| %G | year of ISO week number (see %V); normally useful only with %V | 2005 |" } + { "| %h | same as %b | Jan |" } + { "| %H | hour (00..23) | 23 |" } + { "| %I | hour (01..12) | 11 |" } + { "| %j | day of year (001..366) | 062 |" } + { "| %k | hour, space padded ( 0..23); same as %_H | 3 |" } + { "| %l | hour, space padded ( 1..12); same as %_I | 9 |" } + { "| %m | month (01..12) | 03 |" } + { "| %M | minute (00..59) | 30 |" } + { "| %n | a newline | \\n |" } + { "| %N | nanoseconds (000000000..999999999) | 123456789 |" } + { "| %p | locale's equivalent of either AM or PM; blank if not known | PM |" } + { "| %P | like %p, but lower case | pm |" } + { "| %q | quarter of year (1..4) | 1 |" } + { "| %r | locale's 12-hour clock time | 11:11:04 PM |" } + { "| %R | 24-hour hour and minute; same as %H:%M | 23:30 |" } + { "| %s | seconds since 1970-01-01 00:00:00 UTC | 1615432800 |" } + { "| %S | second (00..60) | 30 |" } + { "| %t | a tab | \\t |" } + { "| %T | time; same as %H:%M:%S | 23:30:30 |" } + { "| %u | day of week (1..7); 1 is Monday | 4 |" } + { "| %U | week number of year, with Sunday as first day of week (00..53) | 10 |" } + { "| %V | ISO week number, with Monday as first day of week (01..53) | 12 |" } + { "| %w | day of week (0..6); 0 is Sunday | 4 |" } + { "| %W | week number of year, with Monday as first day of week (00..53) | 11 |" } + { "| %x | locale's date representation | 03/03/2005 |" } + { "| %X | locale's time representation | 23:30:30 |" } + { "| %y | last two digits of year (00..99) | 05 |" } + { "| %Y | year | 2005 |" } + { "| %z | +hhmm numeric time zone | -0400 |" } + { "| %:z | +hh:mm numeric time zone | -04:00 |" } + { "| %::z | +hh:mm:ss numeric time zone | -04:00:00 |" } + { "| %:::z | numeric time zone with : to necessary precision | -04, +05:30 |" } + { "| %Z | alphabetic time zone abbreviation | EDT |" } + + By default, date pads numeric fields with zeroes. + The following optional flags may follow '%': + { "* `-` (hyphen) do not pad the field" } + { "* `_` (underscore) pad with spaces" } + { "* `0` (zero) pad with zeros" } + { "* `^` use upper case if possible" } + { "* `#` use opposite case if possible" } + After any flags comes an optional field width, as a decimal number; + then an optional modifier, which is either + { "* `E` to use the locale's alternate representations if available, or" } + { "* `O` to use the locale's alternate numeric symbols if available." } + Examples: + Convert seconds since the epoch (1970-01-01 UTC) to a date + + date --date='@2147483647' + + Show the time on the west coast of the US (use tzselect(1) to find TZ) + + TZ='America/Los_Angeles' date diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index f4c9313cb62..9f8b4c095c4 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -3,26 +3,25 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (chrono) Datelike Timelike ; (format) DATEFILE MMDDhhmm ; (vars) datetime datetimes +// spell-checker:ignore strtime ; (format) DATEFILE MMDDhhmm ; (vars) datetime datetimes -use chrono::format::{Item, StrftimeItems}; -use chrono::{DateTime, FixedOffset, Local, Offset, TimeDelta, Utc}; -#[cfg(windows)] -use chrono::{Datelike, Timelike}; use clap::{Arg, ArgAction, Command}; +use jiff::fmt::strtime; +use jiff::tz::TimeZone; +use jiff::{SignedDuration, Timestamp, Zoned}; #[cfg(all(unix, not(target_os = "macos"), not(target_os = "redox")))] use libc::{CLOCK_REALTIME, clock_settime, timespec}; use std::fs::File; use std::io::{BufRead, BufReader}; use std::path::PathBuf; -use uucore::custom_tz_fmt::custom_time_format; use uucore::display::Quotable; use uucore::error::FromIo; use uucore::error::{UResult, USimpleError}; -use uucore::{format_usage, help_about, help_usage, show}; +use uucore::{format_usage, show}; #[cfg(windows)] use windows_sys::Win32::{Foundation::SYSTEMTIME, System::SystemInformation::SetSystemTime}; +use uucore::locale::get_message; use uucore::parser::shortcut_value_parser::ShortcutValueParser; // Options @@ -32,9 +31,6 @@ const MINUTES: &str = "minutes"; const SECONDS: &str = "seconds"; const NS: &str = "ns"; -const ABOUT: &str = help_about!("date.md"); -const USAGE: &str = help_usage!("date.md"); - const OPT_DATE: &str = "date"; const OPT_FORMAT: &str = "format"; const OPT_FILE: &str = "file"; @@ -75,7 +71,7 @@ struct Settings { utc: bool, format: Format, date_source: DateSource, - set_to: Option>, + set_to: Option, } /// Various ways of displaying the date @@ -93,7 +89,7 @@ enum DateSource { Custom(String), File(PathBuf), Stdin, - Human(TimeDelta), + Human(SignedDuration), } enum Iso8601Format { @@ -167,9 +163,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { }; let date_source = if let Some(date) = matches.get_one::(OPT_DATE) { - let ref_time = Local::now(); - if let Ok(new_time) = parse_datetime::parse_datetime_at_date(ref_time, date.as_str()) { - let duration = new_time.signed_duration_since(ref_time); + if let Ok(duration) = parse_offset(date.as_str()) { DateSource::Human(duration) } else { DateSource::Custom(date.into()) @@ -203,39 +197,37 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { if let Some(date) = settings.set_to { // All set time functions expect UTC datetimes. - let date: DateTime = if settings.utc { - date.with_timezone(&Utc) + let date = if settings.utc { + date.with_time_zone(TimeZone::UTC) } else { - date.into() + date }; return set_system_datetime(date); } else { // Get the current time, either in the local time zone or UTC. - let now: DateTime = if settings.utc { - let now = Utc::now(); - now.with_timezone(&now.offset().fix()) + let now = if settings.utc { + Timestamp::now().to_zoned(TimeZone::UTC) } else { - let now = Local::now(); - now.with_timezone(now.offset()) + Zoned::now() }; // Iterate over all dates - whether it's a single date or a file. let dates: Box> = match settings.date_source { DateSource::Custom(ref input) => { - let date = parse_date(input.clone()); + let date = parse_date(input); let iter = std::iter::once(date); Box::new(iter) } DateSource::Human(relative_time) => { // Double check the result is overflow or not of the current_time + relative_time // it may cause a panic of chrono::datetime::DateTime add - match now.checked_add_signed(relative_time) { - Some(date) => { + match now.checked_add(relative_time) { + Ok(date) => { let iter = std::iter::once(Ok(date)); Box::new(iter) } - None => { + Err(_) => { return Err(USimpleError::new( 1, format!("invalid date {relative_time}"), @@ -272,23 +264,16 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // Format all the dates for date in dates { match date { - Ok(date) => { - let format_string = custom_time_format(format_string); - // Hack to work around panic in chrono, - // TODO - remove when a fix for https://github.com/chronotope/chrono/issues/623 is released - let format_items = StrftimeItems::new(format_string.as_str()); - if format_items.clone().any(|i| i == Item::Error) { + // TODO: Switch to lenient formatting. + Ok(date) => match strtime::format(format_string, &date) { + Ok(s) => println!("{s}"), + Err(e) => { return Err(USimpleError::new( 1, - format!("invalid format {}", format_string.replace("%f", "%N")), + format!("invalid format {} ({e})", format_string), )); } - let formatted = date - .format_with_items(format_items) - .to_string() - .replace("%f", "%N"); - println!("{formatted}"); - } + }, Err((input, _err)) => show!(USimpleError::new( 1, format!("invalid date {}", input.quote()) @@ -303,8 +288,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("date-about")) + .override_usage(format_usage(&get_message("date-usage"))) .infer_long_args(true) .arg( Arg::new(OPT_DATE) @@ -388,13 +373,13 @@ fn make_format_string(settings: &Settings) -> &str { Iso8601Format::Hours => "%FT%H%:z", Iso8601Format::Minutes => "%FT%H:%M%:z", Iso8601Format::Seconds => "%FT%T%:z", - Iso8601Format::Ns => "%FT%T,%f%:z", + Iso8601Format::Ns => "%FT%T,%N%:z", }, Format::Rfc5322 => "%a, %d %h %Y %T %z", Format::Rfc3339(ref fmt) => match *fmt { Rfc3339Format::Date => "%F", Rfc3339Format::Seconds => "%F %T%:z", - Rfc3339Format::Ns => "%F %T.%f%:z", + Rfc3339Format::Ns => "%F %T.%N%:z", }, Format::Custom(ref fmt) => fmt, Format::Default => "%a %b %e %X %Z %Y", @@ -403,19 +388,43 @@ fn make_format_string(settings: &Settings) -> &str { /// Parse a `String` into a `DateTime`. /// If it fails, return a tuple of the `String` along with its `ParseError`. +// TODO: Convert `parse_datetime` to jiff and remove wrapper from chrono to jiff structures. fn parse_date + Clone>( s: S, -) -> Result, (String, parse_datetime::ParseDateTimeError)> { - parse_datetime::parse_datetime(s.as_ref()).map_err(|e| (s.as_ref().into(), e)) +) -> Result { + match parse_datetime::parse_datetime(s.as_ref()) { + Ok(date) => { + let timestamp = + Timestamp::new(date.timestamp(), date.timestamp_subsec_nanos() as i32).unwrap(); + Ok(Zoned::new(timestamp, TimeZone::UTC)) + } + Err(e) => Err((s.as_ref().into(), e)), + } +} + +// TODO: Convert `parse_datetime` to jiff and remove wrapper from chrono to jiff structures. +// Also, consider whether parse_datetime::parse_datetime_at_date can be renamed to something +// like parse_datetime::parse_offset, instead of doing some addition/subtraction. +fn parse_offset(date: &str) -> Result { + let ref_time = chrono::Local::now(); + if let Ok(new_time) = parse_datetime::parse_datetime_at_date(ref_time, date) { + let duration = new_time.signed_duration_since(ref_time); + Ok(SignedDuration::new( + duration.num_seconds(), + duration.subsec_nanos(), + )) + } else { + Err(()) + } } #[cfg(not(any(unix, windows)))] -fn set_system_datetime(_date: DateTime) -> UResult<()> { +fn set_system_datetime(_date: Zoned) -> UResult<()> { unimplemented!("setting date not implemented (unsupported target)"); } #[cfg(target_os = "macos")] -fn set_system_datetime(_date: DateTime) -> UResult<()> { +fn set_system_datetime(_date: Zoned) -> UResult<()> { Err(USimpleError::new( 1, "setting the date is not supported by macOS".to_string(), @@ -423,7 +432,7 @@ fn set_system_datetime(_date: DateTime) -> UResult<()> { } #[cfg(target_os = "redox")] -fn set_system_datetime(_date: DateTime) -> UResult<()> { +fn set_system_datetime(_date: Zoned) -> UResult<()> { Err(USimpleError::new( 1, "setting the date is not supported by Redox".to_string(), @@ -436,10 +445,11 @@ fn set_system_datetime(_date: DateTime) -> UResult<()> { /// `` /// `` /// `` -fn set_system_datetime(date: DateTime) -> UResult<()> { +fn set_system_datetime(date: Zoned) -> UResult<()> { + let ts = date.timestamp(); let timespec = timespec { - tv_sec: date.timestamp() as _, - tv_nsec: date.timestamp_subsec_nanos() as _, + tv_sec: ts.as_second() as _, + tv_nsec: ts.subsec_nanosecond() as _, }; let result = unsafe { clock_settime(CLOCK_REALTIME, ×pec) }; @@ -456,7 +466,7 @@ fn set_system_datetime(date: DateTime) -> UResult<()> { /// See here for more: /// https://docs.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-setsystemtime /// https://docs.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-systemtime -fn set_system_datetime(date: DateTime) -> UResult<()> { +fn set_system_datetime(date: Zoned) -> UResult<()> { let system_time = SYSTEMTIME { wYear: date.year() as u16, wMonth: date.month() as u16, @@ -467,7 +477,7 @@ fn set_system_datetime(date: DateTime) -> UResult<()> { wMinute: date.minute() as u16, wSecond: date.second() as u16, // TODO: be careful of leap seconds - valid range is [0, 999] - how to handle? - wMilliseconds: ((date.nanosecond() / 1_000_000) % 1000) as u16, + wMilliseconds: ((date.subsec_nanosecond() / 1_000_000) % 1000) as u16, }; let result = unsafe { SetSystemTime(&system_time) }; diff --git a/src/uu/dd/dd.md b/src/uu/dd/dd.md deleted file mode 100644 index 504910884c2..00000000000 --- a/src/uu/dd/dd.md +++ /dev/null @@ -1,126 +0,0 @@ -# dd - - - -``` -dd [OPERAND]... -dd OPTION -``` - -Copy, and optionally convert, a file system resource - -## After Help - -### Operands - -- `bs=BYTES` : read and write up to BYTES bytes at a time (default: 512); - overwrites `ibs` and `obs`. -- `cbs=BYTES` : the 'conversion block size' in bytes. Applies to the - `conv=block`, and `conv=unblock` operations. -- `conv=CONVS` : a comma-separated list of conversion options or (for legacy - reasons) file flags. -- `count=N` : stop reading input after N ibs-sized read operations rather - than proceeding until EOF. See `iflag=count_bytes` if stopping after N bytes - is preferred -- `ibs=N` : the size of buffer used for reads (default: 512) -- `if=FILE` : the file used for input. When not specified, stdin is used instead -- `iflag=FLAGS` : a comma-separated list of input flags which specify how the - input source is treated. FLAGS may be any of the input-flags or general-flags - specified below. -- `skip=N` (or `iseek=N`) : skip N ibs-sized records into input before beginning - copy/convert operations. See iflag=seek_bytes if seeking N bytes is preferred. -- `obs=N` : the size of buffer used for writes (default: 512) -- `of=FILE` : the file used for output. When not specified, stdout is used - instead -- `oflag=FLAGS` : comma separated list of output flags which specify how the - output source is treated. FLAGS may be any of the output flags or general - flags specified below -- `seek=N` (or `oseek=N`) : seeks N obs-sized records into output before - beginning copy/convert operations. See oflag=seek_bytes if seeking N bytes is - preferred -- `status=LEVEL` : controls whether volume and performance stats are written to - stderr. - - When unspecified, dd will print stats upon completion. An example is below. - - ```plain - 6+0 records in - 16+0 records out - 8192 bytes (8.2 kB, 8.0 KiB) copied, 0.00057009 s, - 14.4 MB/s - ``` - - The first two lines are the 'volume' stats and the final line is the - 'performance' stats. - The volume stats indicate the number of complete and partial ibs-sized reads, - or obs-sized writes that took place during the copy. The format of the volume - stats is `+`. If records have been truncated (see - `conv=block`), the volume stats will contain the number of truncated records. - - Possible LEVEL values are: - - `progress` : Print periodic performance stats as the copy proceeds. - - `noxfer` : Print final volume stats, but not performance stats. - - `none` : Do not print any stats. - - Printing performance stats is also triggered by the INFO signal (where supported), - or the USR1 signal. Setting the POSIXLY_CORRECT environment variable to any value - (including an empty value) will cause the USR1 signal to be ignored. - -### Conversion Options - -- `ascii` : convert from EBCDIC to ASCII. This is the inverse of the `ebcdic` - option. Implies `conv=unblock`. -- `ebcdic` : convert from ASCII to EBCDIC. This is the inverse of the `ascii` - option. Implies `conv=block`. -- `ibm` : convert from ASCII to EBCDIC, applying the conventions for `[`, `]` - and `~` specified in POSIX. Implies `conv=block`. - -- `ucase` : convert from lower-case to upper-case. -- `lcase` : converts from upper-case to lower-case. - -- `block` : for each newline less than the size indicated by cbs=BYTES, remove - the newline and pad with spaces up to cbs. Lines longer than cbs are truncated. -- `unblock` : for each block of input of the size indicated by cbs=BYTES, remove - right-trailing spaces and replace with a newline character. - -- `sparse` : attempts to seek the output when an obs-sized block consists of - only zeros. -- `swab` : swaps each adjacent pair of bytes. If an odd number of bytes is - present, the final byte is omitted. -- `sync` : pad each ibs-sided block with zeros. If `block` or `unblock` is - specified, pad with spaces instead. -- `excl` : the output file must be created. Fail if the output file is already - present. -- `nocreat` : the output file will not be created. Fail if the output file in - not already present. -- `notrunc` : the output file will not be truncated. If this option is not - present, output will be truncated when opened. -- `noerror` : all read errors will be ignored. If this option is not present, - dd will only ignore Error::Interrupted. -- `fdatasync` : data will be written before finishing. -- `fsync` : data and metadata will be written before finishing. - -### Input flags - -- `count_bytes` : a value to `count=N` will be interpreted as bytes. -- `skip_bytes` : a value to `skip=N` will be interpreted as bytes. -- `fullblock` : wait for ibs bytes from each read. zero-length reads are still - considered EOF. - -### Output flags - -- `append` : open file in append mode. Consider setting conv=notrunc as well. -- `seek_bytes` : a value to seek=N will be interpreted as bytes. - -### General Flags - -- `direct` : use direct I/O for data. -- `directory` : fail unless the given input (if used as an iflag) or - output (if used as an oflag) is a directory. -- `dsync` : use synchronized I/O for data. -- `sync` : use synchronized I/O for data and metadata. -- `nonblock` : use non-blocking I/O. -- `noatime` : do not update access time. -- `nocache` : request that OS drop cache. -- `noctty` : do not assign a controlling tty. -- `nofollow` : do not follow system links. diff --git a/src/uu/dd/locales/en-US.ftl b/src/uu/dd/locales/en-US.ftl new file mode 100644 index 00000000000..56f391c79b8 --- /dev/null +++ b/src/uu/dd/locales/en-US.ftl @@ -0,0 +1,115 @@ +dd-about = Copy, and optionally convert, a file system resource +dd-usage = dd [OPERAND]... + dd OPTION +dd-after-help = ### Operands + + - bs=BYTES : read and write up to BYTES bytes at a time (default: 512); + overwrites ibs and obs. + - cbs=BYTES : the 'conversion block size' in bytes. Applies to the + conv=block, and conv=unblock operations. + - conv=CONVS : a comma-separated list of conversion options or (for legacy + reasons) file flags. + - count=N : stop reading input after N ibs-sized read operations rather + than proceeding until EOF. See iflag=count_bytes if stopping after N bytes + is preferred + - ibs=N : the size of buffer used for reads (default: 512) + - if=FILE : the file used for input. When not specified, stdin is used instead + - iflag=FLAGS : a comma-separated list of input flags which specify how the + input source is treated. FLAGS may be any of the input-flags or general-flags + specified below. + - skip=N (or iseek=N) : skip N ibs-sized records into input before beginning + copy/convert operations. See iflag=seek_bytes if seeking N bytes is preferred. + - obs=N : the size of buffer used for writes (default: 512) + - of=FILE : the file used for output. When not specified, stdout is used + instead + - oflag=FLAGS : comma separated list of output flags which specify how the + output source is treated. FLAGS may be any of the output flags or general + flags specified below + - seek=N (or oseek=N) : seeks N obs-sized records into output before + beginning copy/convert operations. See oflag=seek_bytes if seeking N bytes is + preferred + - status=LEVEL : controls whether volume and performance stats are written to + stderr. + + When unspecified, dd will print stats upon completion. An example is below. + + ```plain + 6+0 records in + 16+0 records out + 8192 bytes (8.2 kB, 8.0 KiB) copied, 0.00057009 s, + 14.4 MB/s + + The first two lines are the 'volume' stats and the final line is the + 'performance' stats. + The volume stats indicate the number of complete and partial ibs-sized reads, + or obs-sized writes that took place during the copy. The format of the volume + stats is +. If records have been truncated (see + conv=block), the volume stats will contain the number of truncated records. + + Possible LEVEL values are: + - progress : Print periodic performance stats as the copy proceeds. + - noxfer : Print final volume stats, but not performance stats. + - none : Do not print any stats. + + Printing performance stats is also triggered by the INFO signal (where supported), + or the USR1 signal. Setting the POSIXLY_CORRECT environment variable to any value + (including an empty value) will cause the USR1 signal to be ignored. + + ### Conversion Options + + - ascii : convert from EBCDIC to ASCII. This is the inverse of the ebcdic + option. Implies conv=unblock. + - ebcdic : convert from ASCII to EBCDIC. This is the inverse of the ascii + option. Implies conv=block. + - ibm : convert from ASCII to EBCDIC, applying the conventions for [, ] + and ~ specified in POSIX. Implies conv=block. + + - ucase : convert from lower-case to upper-case. + - lcase : converts from upper-case to lower-case. + + - block : for each newline less than the size indicated by cbs=BYTES, remove + the newline and pad with spaces up to cbs. Lines longer than cbs are truncated. + - unblock : for each block of input of the size indicated by cbs=BYTES, remove + right-trailing spaces and replace with a newline character. + + - sparse : attempts to seek the output when an obs-sized block consists of + only zeros. + - swab : swaps each adjacent pair of bytes. If an odd number of bytes is + present, the final byte is omitted. + - sync : pad each ibs-sided block with zeros. If block or unblock is + specified, pad with spaces instead. + - excl : the output file must be created. Fail if the output file is already + present. + - nocreat : the output file will not be created. Fail if the output file in + not already present. + - notrunc : the output file will not be truncated. If this option is not + present, output will be truncated when opened. + - noerror : all read errors will be ignored. If this option is not present, + dd will only ignore Error::Interrupted. + - fdatasync : data will be written before finishing. + - fsync : data and metadata will be written before finishing. + + ### Input flags + + - count_bytes : a value to count=N will be interpreted as bytes. + - skip_bytes : a value to skip=N will be interpreted as bytes. + - fullblock : wait for ibs bytes from each read. zero-length reads are still + considered EOF. + + ### Output flags + + - append : open file in append mode. Consider setting conv=notrunc as well. + - seek_bytes : a value to seek=N will be interpreted as bytes. + + ### General Flags + + - direct : use direct I/O for data. + - directory : fail unless the given input (if used as an iflag) or + output (if used as an oflag) is a directory. + - dsync : use synchronized I/O for data. + - sync : use synchronized I/O for data and metadata. + - nonblock : use non-blocking I/O. + - noatime : do not update access time. + - nocache : request that OS drop cache. + - noctty : do not assign a controlling tty. + - nofollow : do not follow system links. diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index 4de05246f43..c0e37263fa2 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -60,11 +60,9 @@ use uucore::error::{FromIo, UResult}; use uucore::error::{USimpleError, set_exit_code}; #[cfg(target_os = "linux")] use uucore::show_if_err; -use uucore::{format_usage, help_about, help_section, help_usage, show_error}; +use uucore::{format_usage, show_error}; -const ABOUT: &str = help_about!("dd.md"); -const AFTER_HELP: &str = help_section!("after help", "dd.md"); -const USAGE: &str = help_usage!("dd.md"); +use uucore::locale::get_message; const BUF_INIT_BYTE: u8 = 0xDD; /// Final settings after parsing @@ -1427,9 +1425,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) - .after_help(AFTER_HELP) + .about(get_message("dd-about")) + .override_usage(format_usage(&get_message("dd-usage"))) + .after_help(get_message("dd-after-help")) .infer_long_args(true) .arg(Arg::new(options::OPERANDS).num_args(1..)) } diff --git a/src/uu/df/df.md b/src/uu/df/df.md deleted file mode 100644 index 1a192f8fd01..00000000000 --- a/src/uu/df/df.md +++ /dev/null @@ -1,18 +0,0 @@ -# df - -``` -df [OPTION]... [FILE]... -``` - -Show information about the file system on which each FILE resides, -or all file systems by default. - -## After Help - -Display values are in units of the first available SIZE from --block-size, -and the DF_BLOCK_SIZE, BLOCK_SIZE and BLOCKSIZE environment variables. -Otherwise, units default to 1024 bytes (or 512 if POSIXLY_CORRECT is set). - -SIZE is an integer and optional unit (example: 10M is 10*1024*1024). -Units are K, M, G, T, P, E, Z, Y (powers of 1024) or KB, MB,... (powers -of 1000). diff --git a/src/uu/df/locales/en-US.ftl b/src/uu/df/locales/en-US.ftl new file mode 100644 index 00000000000..cbc69df76d8 --- /dev/null +++ b/src/uu/df/locales/en-US.ftl @@ -0,0 +1,10 @@ +df-about = Show information about the file system on which each FILE resides, + or all file systems by default. +df-usage = df [OPTION]... [FILE]... +df-after-help = Display values are in units of the first available SIZE from --block-size, + and the DF_BLOCK_SIZE, BLOCK_SIZE and BLOCKSIZE environment variables. + Otherwise, units default to 1024 bytes (or 512 if POSIXLY_CORRECT is set). + + SIZE is an integer and optional unit (example: 10M is 10*1024*1024). + Units are K, M, G, T, P, E, Z, Y (powers of 1024) or KB, MB,... (powers + of 1000). diff --git a/src/uu/df/src/df.rs b/src/uu/df/src/df.rs index 9e2bb6920af..c346cf16396 100644 --- a/src/uu/df/src/df.rs +++ b/src/uu/df/src/df.rs @@ -15,7 +15,7 @@ use uucore::display::Quotable; use uucore::error::{UError, UResult, USimpleError, get_exit_code}; use uucore::fsext::{MountInfo, read_fs_list}; use uucore::parser::parse_size::ParseSizeError; -use uucore::{format_usage, help_about, help_section, help_usage, show}; +use uucore::{format_usage, show}; use clap::{Arg, ArgAction, ArgMatches, Command, parser::ValueSource}; @@ -29,9 +29,7 @@ use crate::filesystem::Filesystem; use crate::filesystem::FsError; use crate::table::Table; -const ABOUT: &str = help_about!("df.md"); -const USAGE: &str = help_usage!("df.md"); -const AFTER_HELP: &str = help_section!("after help", "df.md"); +use uucore::locale::get_message; static OPT_HELP: &str = "help"; static OPT_ALL: &str = "all"; @@ -452,9 +450,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) - .after_help(AFTER_HELP) + .about(get_message("df-about")) + .override_usage(format_usage(&get_message("df-usage"))) + .after_help(get_message("df-after-help")) .infer_long_args(true) .disable_help_flag(true) .arg( diff --git a/src/uu/dircolors/dircolors.md b/src/uu/dircolors/dircolors.md deleted file mode 100644 index 959b8fd19bc..00000000000 --- a/src/uu/dircolors/dircolors.md +++ /dev/null @@ -1,13 +0,0 @@ -# dircolors - -``` -dircolors [OPTION]... [FILE] -``` - -Output commands to set the LS_COLORS environment variable. - -## After Help - -If FILE is specified, read it to determine which colors to use for which -file types and extensions. Otherwise, a precompiled database is used. -For details on the format of these files, run 'dircolors --print-database' diff --git a/src/uu/dircolors/locales/en-US.ftl b/src/uu/dircolors/locales/en-US.ftl new file mode 100644 index 00000000000..65b0b874c3d --- /dev/null +++ b/src/uu/dircolors/locales/en-US.ftl @@ -0,0 +1,5 @@ +dircolors-about = Output commands to set the LS_COLORS environment variable. +dircolors-usage = dircolors [OPTION]... [FILE] +dircolors-after-help = If FILE is specified, read it to determine which colors to use for which + file types and extensions. Otherwise, a precompiled database is used. + For details on the format of these files, run 'dircolors --print-database' diff --git a/src/uu/dircolors/src/dircolors.rs b/src/uu/dircolors/src/dircolors.rs index 4fb9228eb5f..ae9b7617763 100644 --- a/src/uu/dircolors/src/dircolors.rs +++ b/src/uu/dircolors/src/dircolors.rs @@ -16,7 +16,8 @@ use clap::{Arg, ArgAction, Command}; use uucore::colors::{FILE_ATTRIBUTE_CODES, FILE_COLORS, FILE_TYPES, TERMS}; use uucore::display::Quotable; use uucore::error::{UResult, USimpleError, UUsageError}; -use uucore::{format_usage, help_about, help_section, help_usage, parser::parse_glob}; +use uucore::locale::get_message; +use uucore::{format_usage, parser::parse_glob}; mod options { pub const BOURNE_SHELL: &str = "bourne-shell"; @@ -26,10 +27,6 @@ mod options { pub const FILE: &str = "FILE"; } -const USAGE: &str = help_usage!("dircolors.md"); -const ABOUT: &str = help_about!("dircolors.md"); -const AFTER_HELP: &str = help_section!("after help", "dircolors.md"); - #[derive(PartialEq, Eq, Debug)] pub enum OutputFmt { Shell, @@ -245,9 +242,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .after_help(AFTER_HELP) - .override_usage(format_usage(USAGE)) + .about(get_message("dircolors-about")) + .after_help(get_message("dircolors-after-help")) + .override_usage(format_usage(&get_message("dircolors-usage"))) .args_override_self(true) .infer_long_args(true) .arg( diff --git a/src/uu/dirname/dirname.md b/src/uu/dirname/dirname.md deleted file mode 100644 index f08c1c4c9ad..00000000000 --- a/src/uu/dirname/dirname.md +++ /dev/null @@ -1,12 +0,0 @@ -# dirname - -``` -dirname [OPTION] NAME... -``` - -Strip last component from file name - -## After Help - -Output each NAME with its last non-slash component and trailing slashes -removed; if NAME contains no /'s, output '.' (meaning the current directory). diff --git a/src/uu/dirname/locales/en-US.ftl b/src/uu/dirname/locales/en-US.ftl new file mode 100644 index 00000000000..37f33df4786 --- /dev/null +++ b/src/uu/dirname/locales/en-US.ftl @@ -0,0 +1,4 @@ +dirname-about = Strip last component from file name +dirname-usage = dirname [OPTION] NAME... +dirname-after-help = Output each NAME with its last non-slash component and trailing slashes + removed; if NAME contains no /'s, output '.' (meaning the current directory). diff --git a/src/uu/dirname/src/dirname.rs b/src/uu/dirname/src/dirname.rs index de8740f8970..02a6ea9384b 100644 --- a/src/uu/dirname/src/dirname.rs +++ b/src/uu/dirname/src/dirname.rs @@ -7,12 +7,10 @@ use clap::{Arg, ArgAction, Command}; use std::path::Path; use uucore::display::print_verbatim; use uucore::error::{UResult, UUsageError}; +use uucore::format_usage; use uucore::line_ending::LineEnding; -use uucore::{format_usage, help_about, help_section, help_usage}; -const ABOUT: &str = help_about!("dirname.md"); -const USAGE: &str = help_usage!("dirname.md"); -const AFTER_HELP: &str = help_section!("after help", "dirname.md"); +use uucore::locale::get_message; mod options { pub const ZERO: &str = "zero"; @@ -21,7 +19,9 @@ mod options { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().after_help(AFTER_HELP).try_get_matches_from(args)?; + let matches = uu_app() + .after_help(get_message("dirname-after-help")) + .try_get_matches_from(args)?; let line_ending = LineEnding::from_zero_flag(matches.get_flag(options::ZERO)); @@ -61,9 +61,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) - .about(ABOUT) + .about(get_message("dirname-about")) .version(uucore::crate_version!()) - .override_usage(format_usage(USAGE)) + .override_usage(format_usage(&get_message("dirname-usage"))) .args_override_self(true) .infer_long_args(true) .arg( diff --git a/src/uu/du/du.md b/src/uu/du/du.md deleted file mode 100644 index ae3319ad6b6..00000000000 --- a/src/uu/du/du.md +++ /dev/null @@ -1,24 +0,0 @@ -# du - -``` -du [OPTION]... [FILE]... -du [OPTION]... --files0-from=F -``` - -Estimate file space usage - -## After Help - -Display values are in units of the first available SIZE from --block-size, -and the DU_BLOCK_SIZE, BLOCK_SIZE and BLOCKSIZE environment variables. -Otherwise, units default to 1024 bytes (or 512 if POSIXLY_CORRECT is set). - -SIZE is an integer and optional unit (example: 10M is 10*1024*1024). -Units are K, M, G, T, P, E, Z, Y (powers of 1024) or KB, MB,... (powers -of 1000). - -PATTERN allows some advanced exclusions. For example, the following syntaxes -are supported: -`?` will match only one character -`*` will match zero or more characters -`{a,b}` will match a or b diff --git a/src/uu/du/locales/en-US.ftl b/src/uu/du/locales/en-US.ftl new file mode 100644 index 00000000000..9f409ffa372 --- /dev/null +++ b/src/uu/du/locales/en-US.ftl @@ -0,0 +1,16 @@ +du-about = Estimate file space usage +du-usage = du [OPTION]... [FILE]... + du [OPTION]... --files0-from=F +du-after-help = Display values are in units of the first available SIZE from --block-size, + and the DU_BLOCK_SIZE, BLOCK_SIZE and BLOCKSIZE environment variables. + Otherwise, units default to 1024 bytes (or 512 if POSIXLY_CORRECT is set). + + SIZE is an integer and optional unit (example: 10M is 10*1024*1024). + Units are K, M, G, T, P, E, Z, Y (powers of 1024) or KB, MB,... (powers + of 1000). + + PATTERN allows some advanced exclusions. For example, the following syntaxes + are supported: + ? will match only one character + { "*" } will match zero or more characters + {"{"}a,b{"}"} will match a or b diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index 0b268888136..08a7c7b91d6 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -27,10 +27,11 @@ use thiserror::Error; use uucore::display::{Quotable, print_verbatim}; use uucore::error::{FromIo, UError, UResult, USimpleError, set_exit_code}; use uucore::line_ending::LineEnding; +use uucore::locale::get_message; use uucore::parser::parse_glob; use uucore::parser::parse_size::{ParseSizeError, parse_size_u64}; use uucore::parser::shortcut_value_parser::ShortcutValueParser; -use uucore::{format_usage, help_about, help_section, help_usage, show, show_error, show_warning}; +use uucore::{format_usage, show, show_error, show_warning}; #[cfg(windows)] use windows_sys::Win32::Foundation::HANDLE; #[cfg(windows)] @@ -70,10 +71,6 @@ mod options { pub const FILE: &str = "FILE"; } -const ABOUT: &str = help_about!("du.md"); -const AFTER_HELP: &str = help_section!("after help", "du.md"); -const USAGE: &str = help_usage!("du.md"); - struct TraversalOptions { all: bool, separate_dirs: bool, @@ -824,9 +821,9 @@ fn parse_depth(max_depth_str: Option<&str>, summarize: bool) -> UResult Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .after_help(AFTER_HELP) - .override_usage(format_usage(USAGE)) + .about(get_message("du-about")) + .after_help(get_message("du-after-help")) + .override_usage(format_usage(&get_message("du-usage"))) .infer_long_args(true) .disable_help_flag(true) .arg( diff --git a/src/uu/echo/echo.md b/src/uu/echo/echo.md deleted file mode 100644 index f6f6f37ffcd..00000000000 --- a/src/uu/echo/echo.md +++ /dev/null @@ -1,26 +0,0 @@ -# echo - -``` -echo [OPTIONS]... [STRING]... -``` - -Display a line of text - -## After Help - -Echo the STRING(s) to standard output. - -If -e is in effect, the following sequences are recognized: - -- `\` backslash -- `\a` alert (BEL) -- `\b` backspace -- `\c` produce no further output -- `\e` escape -- `\f` form feed -- `\n` new line -- `\r` carriage return -- `\t` horizontal tab -- `\v` vertical tab -- `\0NNN` byte with octal value NNN (1 to 3 digits) -- `\xHH` byte with hexadecimal value HH (1 to 2 digits) diff --git a/src/uu/echo/locales/en-US.ftl b/src/uu/echo/locales/en-US.ftl new file mode 100644 index 00000000000..05bf06cd26b --- /dev/null +++ b/src/uu/echo/locales/en-US.ftl @@ -0,0 +1,18 @@ +echo-about = Display a line of text +echo-usage = echo [OPTIONS]... [STRING]... +echo-after-help = Echo the STRING(s) to standard output. + + If -e is in effect, the following sequences are recognized: + + - \ backslash + - \a alert (BEL) + - \b backspace + - \c produce no further output + - \e escape + - \f form feed + - \n new line + - \r carriage return + - \t horizontal tab + - \v vertical tab + - \0NNN byte with octal value NNN (1 to 3 digits) + - \xHH byte with hexadecimal value HH (1 to 2 digits) diff --git a/src/uu/echo/src/echo.rs b/src/uu/echo/src/echo.rs index 4df76634843..d1306dd3e9d 100644 --- a/src/uu/echo/src/echo.rs +++ b/src/uu/echo/src/echo.rs @@ -10,11 +10,9 @@ use std::ffi::{OsStr, OsString}; use std::io::{self, StdoutLock, Write}; use uucore::error::{UResult, USimpleError}; use uucore::format::{FormatChar, OctalParsing, parse_escape_only}; -use uucore::{format_usage, help_about, help_section, help_usage}; +use uucore::format_usage; -const ABOUT: &str = help_about!("echo.md"); -const USAGE: &str = help_usage!("echo.md"); -const AFTER_HELP: &str = help_section!("after help", "echo.md"); +use uucore::locale::get_message; mod options { pub const STRING: &str = "STRING"; @@ -141,9 +139,9 @@ pub fn uu_app() -> Command { .trailing_var_arg(true) .allow_hyphen_values(true) .version(uucore::crate_version!()) - .about(ABOUT) - .after_help(AFTER_HELP) - .override_usage(format_usage(USAGE)) + .about(get_message("echo-about")) + .after_help(get_message("echo-after-help")) + .override_usage(format_usage(&get_message("echo-usage"))) .arg( Arg::new(options::NO_NEWLINE) .short('n') diff --git a/src/uu/env/env.md b/src/uu/env/env.md deleted file mode 100644 index fc3341a61f7..00000000000 --- a/src/uu/env/env.md +++ /dev/null @@ -1,13 +0,0 @@ -# env - -``` -env [OPTION]... [-] [NAME=VALUE]... [COMMAND [ARG]...] -``` - - -Set each NAME to VALUE in the environment and run COMMAND - - -## After Help - -A mere - implies -i. If no COMMAND, print the resulting environment. diff --git a/src/uu/env/locales/en-US.ftl b/src/uu/env/locales/en-US.ftl new file mode 100644 index 00000000000..4204cb2e407 --- /dev/null +++ b/src/uu/env/locales/en-US.ftl @@ -0,0 +1,3 @@ +env-about = Set each NAME to VALUE in the environment and run COMMAND +env-usage = env [OPTION]... [-] [NAME=VALUE]... [COMMAND [ARG]...] +env-after-help = A mere - implies -i. If no COMMAND, print the resulting environment. diff --git a/src/uu/env/src/env.rs b/src/uu/env/src/env.rs index 51479b5902f..dbf5f1d56a8 100644 --- a/src/uu/env/src/env.rs +++ b/src/uu/env/src/env.rs @@ -36,7 +36,7 @@ use uucore::error::{ExitCode, UError, UResult, USimpleError, UUsageError}; use uucore::line_ending::LineEnding; #[cfg(unix)] use uucore::signals::signal_by_name_or_value; -use uucore::{format_usage, help_about, help_section, help_usage, show_warning}; +use uucore::{format_usage, show_warning}; use thiserror::Error; @@ -74,9 +74,7 @@ impl From for EnvError { } } -const ABOUT: &str = help_about!("env.md"); -const USAGE: &str = help_usage!("env.md"); -const AFTER_HELP: &str = help_section!("after help", "env.md"); +use uucore::locale::get_message; mod options { pub const IGNORE_ENVIRONMENT: &str = "ignore-environment"; @@ -222,9 +220,9 @@ fn load_config_file(opts: &mut Options) -> UResult<()> { pub fn uu_app() -> Command { Command::new(crate_name!()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) - .after_help(AFTER_HELP) + .about(get_message("env-about")) + .override_usage(format_usage(&get_message("env-usage"))) + .after_help(get_message("env-after-help")) .infer_long_args(true) .trailing_var_arg(true) .arg( diff --git a/src/uu/expand/expand.md b/src/uu/expand/expand.md deleted file mode 100644 index df7294c30d2..00000000000 --- a/src/uu/expand/expand.md +++ /dev/null @@ -1,8 +0,0 @@ -# expand - -``` -expand [OPTION]... [FILE]... -``` - -Convert tabs in each `FILE` to spaces, writing to standard output. -With no `FILE`, or when `FILE` is `-`, read standard input. diff --git a/src/uu/expand/locales/en-US.ftl b/src/uu/expand/locales/en-US.ftl new file mode 100644 index 00000000000..ad15acf3953 --- /dev/null +++ b/src/uu/expand/locales/en-US.ftl @@ -0,0 +1,3 @@ +expand-about = Convert tabs in each FILE to spaces, writing to standard output. + With no FILE, or when FILE is -, read standard input. +expand-usage = expand [OPTION]... [FILE]... diff --git a/src/uu/expand/src/expand.rs b/src/uu/expand/src/expand.rs index 4c37393b4ac..2e8d1b296c0 100644 --- a/src/uu/expand/src/expand.rs +++ b/src/uu/expand/src/expand.rs @@ -16,10 +16,9 @@ use thiserror::Error; use unicode_width::UnicodeWidthChar; use uucore::display::Quotable; use uucore::error::{FromIo, UError, UResult, set_exit_code}; -use uucore::{format_usage, help_about, help_usage, show_error}; +use uucore::{format_usage, show_error}; -const ABOUT: &str = help_about!("expand.md"); -const USAGE: &str = help_usage!("expand.md"); +use uucore::locale::get_message; pub mod options { pub static TABS: &str = "tabs"; @@ -253,9 +252,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) + .about(get_message("expand-about")) .after_help(LONG_HELP) - .override_usage(format_usage(USAGE)) + .override_usage(format_usage(&get_message("expand-usage"))) .infer_long_args(true) .args_override_self(true) .arg( diff --git a/src/uu/expr/expr.md b/src/uu/expr/expr.md deleted file mode 100644 index ab082465149..00000000000 --- a/src/uu/expr/expr.md +++ /dev/null @@ -1,53 +0,0 @@ -# expr - -``` -expr [EXPRESSION] -expr [OPTIONS] -``` - -Print the value of `EXPRESSION` to standard output - -## After help - -Print the value of `EXPRESSION` to standard output. A blank line below -separates increasing precedence groups. - -`EXPRESSION` may be: - -- `ARG1 | ARG2`: `ARG1` if it is neither null nor 0, otherwise `ARG2` -- `ARG1 & ARG2`: `ARG1` if neither argument is null or 0, otherwise 0 -- `ARG1 < ARG2`: `ARG1` is less than `ARG2` -- `ARG1 <= ARG2`: `ARG1` is less than or equal to `ARG2` -- `ARG1 = ARG2`: `ARG1` is equal to `ARG2` -- `ARG1 != ARG2`: `ARG1` is unequal to `ARG2` -- `ARG1 >= ARG2`: `ARG1` is greater than or equal to `ARG2` -- `ARG1 > ARG2`: `ARG1` is greater than `ARG2` -- `ARG1 + ARG2`: arithmetic sum of `ARG1` and `ARG2` -- `ARG1 - ARG2`: arithmetic difference of `ARG1` and `ARG2` -- `ARG1 * ARG2`: arithmetic product of `ARG1` and `ARG2` -- `ARG1 / ARG2`: arithmetic quotient of `ARG1` divided by `ARG2` -- `ARG1 % ARG2`: arithmetic remainder of `ARG1` divided by `ARG2` -- `STRING : REGEXP`: anchored pattern match of `REGEXP` in `STRING` -- `match STRING REGEXP`: same as `STRING : REGEXP` -- `substr STRING POS LENGTH`: substring of `STRING`, `POS` counted from 1 -- `index STRING CHARS`: index in `STRING` where any `CHARS` is found, or 0 -- `length STRING`: length of `STRING` -- `+ TOKEN`: interpret `TOKEN` as a string, even if it is a keyword like `match` - or an operator like `/` -- `( EXPRESSION )`: value of `EXPRESSION` - -Beware that many operators need to be escaped or quoted for shells. -Comparisons are arithmetic if both ARGs are numbers, else lexicographical. -Pattern matches return the string matched between \( and \) or null; if -\( and \) are not used, they return the number of characters matched or 0. - -Exit status is `0` if `EXPRESSION` is neither null nor `0`, `1` if `EXPRESSION` -is null or `0`, `2` if `EXPRESSION` is syntactically invalid, and `3` if an -error occurred. - -Environment variables: - -- `EXPR_DEBUG_TOKENS=1`: dump expression's tokens -- `EXPR_DEBUG_RPN=1`: dump expression represented in reverse polish notation -- `EXPR_DEBUG_SYA_STEP=1`: dump each parser step -- `EXPR_DEBUG_AST=1`: dump expression represented abstract syntax tree diff --git a/src/uu/expr/locales/en-US.ftl b/src/uu/expr/locales/en-US.ftl new file mode 100644 index 00000000000..f334d58ee1c --- /dev/null +++ b/src/uu/expr/locales/en-US.ftl @@ -0,0 +1,45 @@ +expr-about = Print the value of EXPRESSION to standard output +expr-usage = expr [EXPRESSION] + expr [OPTIONS] +expr-after-help = Print the value of EXPRESSION to standard output. A blank line below + separates increasing precedence groups. + + EXPRESSION may be: + + - ARG1 | ARG2: ARG1 if it is neither null nor 0, otherwise ARG2 + - ARG1 & ARG2: ARG1 if neither argument is null or 0, otherwise 0 + - ARG1 < ARG2: ARG1 is less than ARG2 + - ARG1 <= ARG2: ARG1 is less than or equal to ARG2 + - ARG1 = ARG2: ARG1 is equal to ARG2 + - ARG1 != ARG2: ARG1 is unequal to ARG2 + - ARG1 >= ARG2: ARG1 is greater than or equal to ARG2 + - ARG1 > ARG2: ARG1 is greater than ARG2 + - ARG1 + ARG2: arithmetic sum of ARG1 and ARG2 + - ARG1 - ARG2: arithmetic difference of ARG1 and ARG2 + - ARG1 * ARG2: arithmetic product of ARG1 and ARG2 + - ARG1 / ARG2: arithmetic quotient of ARG1 divided by ARG2 + - ARG1 % ARG2: arithmetic remainder of ARG1 divided by ARG2 + - STRING : REGEXP: anchored pattern match of REGEXP in STRING + - match STRING REGEXP: same as STRING : REGEXP + - substr STRING POS LENGTH: substring of STRING, POS counted from 1 + - index STRING CHARS: index in STRING where any CHARS is found, or 0 + - length STRING: length of STRING + - + TOKEN: interpret TOKEN as a string, even if it is a keyword like match + or an operator like / + - ( EXPRESSION ): value of EXPRESSION + + Beware that many operators need to be escaped or quoted for shells. + Comparisons are arithmetic if both ARGs are numbers, else lexicographical. + Pattern matches return the string matched between \( and \) or null; if + \( and \) are not used, they return the number of characters matched or 0. + + Exit status is 0 if EXPRESSION is neither null nor 0, 1 if EXPRESSION + is null or 0, 2 if EXPRESSION is syntactically invalid, and 3 if an + error occurred. + + Environment variables: + + - EXPR_DEBUG_TOKENS=1: dump expression's tokens + - EXPR_DEBUG_RPN=1: dump expression represented in reverse polish notation + - EXPR_DEBUG_SYA_STEP=1: dump each parser step + - EXPR_DEBUG_AST=1: dump expression represented abstract syntax tree diff --git a/src/uu/expr/src/expr.rs b/src/uu/expr/src/expr.rs index 073bf501a0b..e43b5a36264 100644 --- a/src/uu/expr/src/expr.rs +++ b/src/uu/expr/src/expr.rs @@ -6,10 +6,11 @@ use clap::{Arg, ArgAction, Command}; use syntax_tree::{AstNode, is_truthy}; use thiserror::Error; +use uucore::locale::get_message; use uucore::{ display::Quotable, error::{UError, UResult}, - format_usage, help_about, help_section, help_usage, + format_usage, }; mod syntax_tree; @@ -46,10 +47,12 @@ pub enum ExprError { UnmatchedClosingParenthesis, #[error("Unmatched \\{{")] UnmatchedOpeningBrace, - #[error("Unmatched ) or \\}}")] - UnmatchedClosingBrace, #[error("Invalid content of \\{{\\}}")] InvalidBracketContent, + #[error("Trailing backslash")] + TrailingBackslash, + #[error("Regular expression too big")] + TooBigRangeQuantifierIndex, } impl UError for ExprError { @@ -65,9 +68,9 @@ impl UError for ExprError { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(help_about!("expr.md")) - .override_usage(format_usage(help_usage!("expr.md"))) - .after_help(help_section!("after help", "expr.md")) + .about(get_message("expr-about")) + .override_usage(format_usage(&get_message("expr-usage"))) + .after_help(get_message("expr-after-help")) .infer_long_args(true) .disable_help_flag(true) .disable_version_flag(true) diff --git a/src/uu/expr/src/syntax_tree.rs b/src/uu/expr/src/syntax_tree.rs index 106b4bd6830..0950020c955 100644 --- a/src/uu/expr/src/syntax_tree.rs +++ b/src/uu/expr/src/syntax_tree.rs @@ -8,7 +8,7 @@ use std::{cell::Cell, collections::BTreeMap}; use num_bigint::{BigInt, ParseBigIntError}; -use num_traits::{ToPrimitive, Zero}; +use num_traits::ToPrimitive; use onig::{Regex, RegexOptions, Syntax}; use crate::{ExprError, ExprResult}; @@ -151,60 +151,62 @@ impl StringOp { let right = right?.eval_as_string(); check_posix_regex_errors(&right)?; - // All patterns are anchored so they begin with a caret (^) + // Transpile the input pattern from BRE syntax to `onig` crate's `Syntax::grep` let mut re_string = String::with_capacity(right.len() + 1); - re_string.push('^'); - - // Handle first character from the input pattern let mut pattern_chars = right.chars().peekable(); - let first = pattern_chars.next(); - match first { - Some('^') => {} // Start of string anchor is already added - Some('*') => re_string.push_str(r"\*"), - Some(char) => re_string.push(char), - None => return Ok(0.into()), - }; - - // Handle the rest of the input pattern. - let mut prev = first.unwrap_or_default(); + let mut prev = '\0'; let mut prev_is_escaped = false; + let mut is_start_of_expression = true; + + // All patterns are anchored so they begin with a caret (^) + if pattern_chars.peek() != Some(&'^') { + re_string.push('^'); + } + while let Some(curr) = pattern_chars.next() { + let curr_is_escaped = prev == '\\' && !prev_is_escaped; + let is_first_character = prev == '\0'; + match curr { - '^' => match (prev, prev_is_escaped) { - // Start of a capturing group - ('(', true) - // Start of an alternative pattern - | ('|', true) - // Character class negation "[^a]" - | ('[', false) - // Explicitly escaped caret - | ('\\', false) => re_string.push(curr), - _ => re_string.push_str(r"\^"), - }, - '$' => { - if let Some('\\') = pattern_chars.peek() { - // The next character was checked to be a backslash - let backslash = pattern_chars.next().unwrap_or_default(); - match pattern_chars.peek() { - // End of a capturing group - Some(')') => re_string.push('$'), - // End of an alternative pattern - Some('|') => re_string.push('$'), - _ => re_string.push_str(r"\$"), + // Character class negation "[^a]" + // Explicitly escaped caret "\^" + '^' if !is_start_of_expression && !matches!(prev, '[' | '\\') => { + re_string.push_str(r"\^"); + } + '$' if !curr_is_escaped && !is_end_of_expression(&pattern_chars) => { + re_string.push_str(r"\$"); + } + '\\' if !curr_is_escaped && pattern_chars.peek().is_none() => { + return Err(ExprError::TrailingBackslash); + } + '{' if curr_is_escaped => { + // Handle '{' literally at the start of an expression + if is_start_of_expression { + if re_string.ends_with('\\') { + let _ = re_string.pop(); } - re_string.push(backslash); - } else if (prev_is_escaped || prev != '\\') - && pattern_chars.peek().is_some() - { - re_string.push_str(r"\$"); + re_string.push(curr); } else { - re_string.push('$'); + // Check if the following section is a valid range quantifier + verify_range_quantifier(&pattern_chars)?; + + re_string.push(curr); + // Set the lower bound of range quantifier to 0 if it is missing + if pattern_chars.peek() == Some(&',') { + re_string.push('0'); + } } } _ => re_string.push(curr), } - prev_is_escaped = prev == '\\' && !prev_is_escaped; + // Capturing group "\(abc\)" + // Alternative pattern "a\|b" + is_start_of_expression = curr == '\\' && is_first_character + || curr_is_escaped && matches!(curr, '(' | '|') + || curr == '\\' && prev_is_escaped && matches!(prev, '(' | '|'); + + prev_is_escaped = curr_is_escaped; prev = curr; } @@ -213,7 +215,14 @@ impl StringOp { RegexOptions::REGEX_OPTION_SINGLELINE, Syntax::grep(), ) - .map_err(|_| ExprError::InvalidRegexExpression)?; + .map_err(|error| match error.code() { + // "invalid repeat range {lower,upper}" + -123 => ExprError::InvalidBracketContent, + // "too big number for repeat range" + -201 => ExprError::TooBigRangeQuantifierIndex, + _ => ExprError::InvalidRegexExpression, + })?; + Ok(if re.captures_len() > 0 { re.captures(&left) .and_then(|captures| captures.at(1)) @@ -241,6 +250,78 @@ impl StringOp { } } +/// Check if regex pattern character iterator is at the end of a regex expression or subexpression +fn is_end_of_expression(pattern_chars: &I) -> bool +where + I: Iterator + Clone, +{ + let mut pattern_chars_clone = pattern_chars.clone(); + match pattern_chars_clone.next() { + Some('\\') => matches!(pattern_chars_clone.next(), Some(')' | '|')), + None => true, // No characters left + _ => false, + } +} + +/// Check if regex pattern character iterator is at the start of a valid range quantifier. +/// The iterator's start position is expected to be after the opening brace. +/// Range quantifier ends to closing brace. +/// +/// # Examples of valid range quantifiers +/// +/// - `r"\{3\}"` +/// - `r"\{3,\}"` +/// - `r"\{,6\}"` +/// - `r"\{3,6\}"` +/// - `r"\{,\}"` +fn verify_range_quantifier(pattern_chars: &I) -> Result<(), ExprError> +where + I: Iterator + Clone, +{ + let mut pattern_chars_clone = pattern_chars.clone().peekable(); + if pattern_chars_clone.peek().is_none() { + return Err(ExprError::UnmatchedOpeningBrace); + } + + // Parse the string between braces + let mut quantifier = String::new(); + let mut prev = '\0'; + let mut curr_is_escaped = false; + while let Some(curr) = pattern_chars_clone.next() { + curr_is_escaped = prev == '\\' && !curr_is_escaped; + if curr_is_escaped && curr == '}' { + break; + } + if pattern_chars_clone.peek().is_none() { + return Err(ExprError::UnmatchedOpeningBrace); + } + if prev != '\0' { + quantifier.push(prev); + } + prev = curr; + } + + // Check if parsed quantifier is valid + let re = Regex::new(r"^([0-9]*,[0-9]*|[0-9]+)$").expect("valid regular expression"); + if let Some(captures) = re.captures(&quantifier) { + let matched = captures.at(0).unwrap_or_default(); + match matched.split_once(',') { + Some(("", "")) => Ok(()), + Some((x, "") | ("", x)) if x.parse::().is_ok() => Ok(()), + Some((_, "") | ("", _)) => Err(ExprError::TooBigRangeQuantifierIndex), + Some((f, l)) => match (f.parse::(), l.parse::()) { + (Ok(f), Ok(l)) if f > l => Err(ExprError::InvalidBracketContent), + (Ok(_), Ok(_)) => Ok(()), + _ => Err(ExprError::TooBigRangeQuantifierIndex), + }, + None if matched.parse::().is_ok() => Ok(()), + None => Err(ExprError::TooBigRangeQuantifierIndex), + } + } else { + Err(ExprError::InvalidBracketContent) + } +} + /// Check for errors in a supplied regular expression /// /// GNU coreutils shows messages for invalid regular expressions @@ -256,81 +337,26 @@ impl StringOp { /// has specific error messages. fn check_posix_regex_errors(pattern: &str) -> ExprResult<()> { let mut escaped_parens: u64 = 0; - let mut escaped_braces: u64 = 0; - let mut escaped = false; - - let mut repeating_pattern_text = String::new(); - let mut invalid_content_error = false; + let mut prev = '\0'; + let mut curr_is_escaped = false; - for c in pattern.chars() { - match (escaped, c) { + for curr in pattern.chars() { + curr_is_escaped = prev == '\\' && !curr_is_escaped; + match (curr_is_escaped, curr) { + (true, '(') => escaped_parens += 1, (true, ')') => { escaped_parens = escaped_parens .checked_sub(1) .ok_or(ExprError::UnmatchedClosingParenthesis)?; } - (true, '(') => { - escaped_parens += 1; - } - (true, '}') => { - escaped_braces = escaped_braces - .checked_sub(1) - .ok_or(ExprError::UnmatchedClosingBrace)?; - let mut repetition = - repeating_pattern_text[..repeating_pattern_text.len() - 1].splitn(2, ','); - match ( - repetition - .next() - .expect("splitn always returns at least one string"), - repetition.next(), - ) { - ("", None) => { - // Empty repeating pattern - invalid_content_error = true; - } - (x, None | Some("")) => { - if x.parse::().is_err() { - invalid_content_error = true; - } - } - ("", Some(x)) => { - if x.parse::().is_err() { - invalid_content_error = true; - } - } - (f, Some(l)) => { - if let (Ok(f), Ok(l)) = (f.parse::(), l.parse::()) { - invalid_content_error = invalid_content_error || f > l; - } else { - invalid_content_error = true; - } - } - } - repeating_pattern_text.clear(); - } - (true, '{') => { - escaped_braces += 1; - } - _ => { - if escaped_braces > 0 && repeating_pattern_text.len() <= 13 { - repeating_pattern_text.push(c); - } - if escaped_braces > 0 && !(c.is_ascii_digit() || c == '\\' || c == ',') { - invalid_content_error = true; - } - } + _ => {} } - escaped = !escaped && c == '\\'; - } - match ( - escaped_parens.is_zero(), - escaped_braces.is_zero(), - invalid_content_error, - ) { - (true, true, false) => Ok(()), - (_, false, _) => Err(ExprError::UnmatchedOpeningBrace), - (false, _, _) => Err(ExprError::UnmatchedOpeningParenthesis), - (true, true, true) => Err(ExprError::InvalidBracketContent), + prev = curr; + } + + match escaped_parens { + 0 => Ok(()), + _ => Err(ExprError::UnmatchedOpeningParenthesis), } } @@ -746,7 +772,7 @@ pub fn is_truthy(s: &NumOrStr) -> bool { #[cfg(test)] mod test { use crate::ExprError; - use crate::ExprError::InvalidBracketContent; + use crate::syntax_tree::verify_range_quantifier; use super::{ AstNode, AstNodeInner, BinOp, NumericOp, RelationOp, StringOp, check_posix_regex_errors, @@ -939,11 +965,6 @@ mod test { check_posix_regex_errors(r"\(abc"), Err(ExprError::UnmatchedOpeningParenthesis) ); - - assert_eq!( - check_posix_regex_errors(r"\{1,2"), - Err(ExprError::UnmatchedOpeningBrace) - ); } #[test] @@ -952,47 +973,51 @@ mod test { check_posix_regex_errors(r"abc\)"), Err(ExprError::UnmatchedClosingParenthesis) ); - - assert_eq!( - check_posix_regex_errors(r"abc\}"), - Err(ExprError::UnmatchedClosingBrace) - ); } #[test] - fn check_regex_empty_repeating_pattern() { + fn test_is_valid_range_quantifier() { + assert!(verify_range_quantifier(&"3\\}".chars()).is_ok()); + assert!(verify_range_quantifier(&"3,\\}".chars()).is_ok()); + assert!(verify_range_quantifier(&",6\\}".chars()).is_ok()); + assert!(verify_range_quantifier(&"3,6\\}".chars()).is_ok()); + assert!(verify_range_quantifier(&",\\}".chars()).is_ok()); + assert!(verify_range_quantifier(&"32767\\}anything".chars()).is_ok()); assert_eq!( - check_posix_regex_errors("ab\\{\\}"), - Err(InvalidBracketContent) + verify_range_quantifier(&"\\{3,6\\}".chars()), + Err(ExprError::InvalidBracketContent) + ); + assert_eq!( + verify_range_quantifier(&"\\}".chars()), + Err(ExprError::InvalidBracketContent) ); - } - - #[test] - fn check_regex_intervals_two_numbers() { assert_eq!( - // out of order - check_posix_regex_errors("ab\\{1,0\\}"), - Err(InvalidBracketContent) + verify_range_quantifier(&"".chars()), + Err(ExprError::UnmatchedOpeningBrace) + ); + assert_eq!( + verify_range_quantifier(&"3".chars()), + Err(ExprError::UnmatchedOpeningBrace) ); assert_eq!( - check_posix_regex_errors("ab\\{1,a\\}"), - Err(InvalidBracketContent) + verify_range_quantifier(&"3,".chars()), + Err(ExprError::UnmatchedOpeningBrace) ); assert_eq!( - check_posix_regex_errors("ab\\{a,3\\}"), - Err(InvalidBracketContent) + verify_range_quantifier(&",6".chars()), + Err(ExprError::UnmatchedOpeningBrace) ); assert_eq!( - check_posix_regex_errors("ab\\{a,b\\}"), - Err(InvalidBracketContent) + verify_range_quantifier(&"3,6".chars()), + Err(ExprError::UnmatchedOpeningBrace) ); assert_eq!( - check_posix_regex_errors("ab\\{a,\\}"), - Err(InvalidBracketContent) + verify_range_quantifier(&",".chars()), + Err(ExprError::UnmatchedOpeningBrace) ); assert_eq!( - check_posix_regex_errors("ab\\{,b\\}"), - Err(InvalidBracketContent) + verify_range_quantifier(&"32768\\}".chars()), + Err(ExprError::TooBigRangeQuantifierIndex) ); } } diff --git a/src/uu/factor/Cargo.toml b/src/uu/factor/Cargo.toml index 6b875074ec5..2bc850250ae 100644 --- a/src/uu/factor/Cargo.toml +++ b/src/uu/factor/Cargo.toml @@ -19,7 +19,6 @@ num-traits = { workspace = true } # used in src/numerics.rs, which is included b [dependencies] clap = { workspace = true } -coz = { workspace = true, optional = true } num-traits = { workspace = true } rand = { workspace = true } smallvec = { workspace = true } diff --git a/src/uu/factor/factor.md b/src/uu/factor/factor.md deleted file mode 100644 index ccce4f2529c..00000000000 --- a/src/uu/factor/factor.md +++ /dev/null @@ -1,8 +0,0 @@ -# factor - -``` -factor [OPTION]... [NUMBER]... -``` - -Print the prime factors of the given NUMBER(s). -If none are specified, read from standard input. diff --git a/src/uu/factor/locales/en-US.ftl b/src/uu/factor/locales/en-US.ftl new file mode 100644 index 00000000000..07e7920c5c6 --- /dev/null +++ b/src/uu/factor/locales/en-US.ftl @@ -0,0 +1,3 @@ +factor-about = Print the prime factors of the given NUMBER(s). + If none are specified, read from standard input. +factor-usage = factor [OPTION]... [NUMBER]... diff --git a/src/uu/factor/src/factor.rs b/src/uu/factor/src/factor.rs index 2c8d1661c05..4820190f4f1 100644 --- a/src/uu/factor/src/factor.rs +++ b/src/uu/factor/src/factor.rs @@ -14,10 +14,9 @@ use num_bigint::BigUint; use num_traits::FromPrimitive; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError, set_exit_code}; -use uucore::{format_usage, help_about, help_usage, show_error, show_warning}; +use uucore::{format_usage, show_error, show_warning}; -const ABOUT: &str = help_about!("factor.md"); -const USAGE: &str = help_usage!("factor.md"); +use uucore::locale::get_message; mod options { pub static EXPONENTS: &str = "exponents"; @@ -122,8 +121,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("factor-about")) + .override_usage(format_usage(&get_message("factor-usage"))) .infer_long_args(true) .disable_help_flag(true) .args_override_self(true) diff --git a/src/uu/false/false.md b/src/uu/false/false.md deleted file mode 100644 index 3f0594767f2..00000000000 --- a/src/uu/false/false.md +++ /dev/null @@ -1,11 +0,0 @@ -# false - -``` -false -``` - -Returns false, an unsuccessful exit status. - -Immediately returns with the exit status `1`. When invoked with one of the recognized options it -will try to write the help or version text. Any IO error during this operation is diagnosed, yet -the program will also return `1`. diff --git a/src/uu/false/locales/en-US.ftl b/src/uu/false/locales/en-US.ftl new file mode 100644 index 00000000000..8b642a69fa2 --- /dev/null +++ b/src/uu/false/locales/en-US.ftl @@ -0,0 +1,5 @@ +false-about = Returns false, an unsuccessful exit status. + + Immediately returns with the exit status 1. When invoked with one of the recognized options it + will try to write the help or version text. Any IO error during this operation is diagnosed, yet + the program will also return 1. diff --git a/src/uu/false/src/false.rs b/src/uu/false/src/false.rs index adf3593ea0d..b8bf3a8253a 100644 --- a/src/uu/false/src/false.rs +++ b/src/uu/false/src/false.rs @@ -5,9 +5,8 @@ use clap::{Arg, ArgAction, Command}; use std::{ffi::OsString, io::Write}; use uucore::error::{UResult, set_exit_code}; -use uucore::help_about; -const ABOUT: &str = help_about!("false.md"); +use uucore::locale::get_message; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { @@ -46,7 +45,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) + .about(get_message("false-about")) // We provide our own help and version options, to ensure maximum compatibility with GNU. .disable_help_flag(true) .disable_version_flag(true) diff --git a/src/uu/fmt/fmt.md b/src/uu/fmt/fmt.md deleted file mode 100644 index 6ed7d3048a5..00000000000 --- a/src/uu/fmt/fmt.md +++ /dev/null @@ -1,7 +0,0 @@ -# fmt - -``` -fmt [-WIDTH] [OPTION]... [FILE]... -``` - -Reformat paragraphs from input files (or stdin) to stdout. diff --git a/src/uu/fmt/locales/en-US.ftl b/src/uu/fmt/locales/en-US.ftl new file mode 100644 index 00000000000..c572e7f5c2a --- /dev/null +++ b/src/uu/fmt/locales/en-US.ftl @@ -0,0 +1,2 @@ +fmt-about = Reformat paragraphs from input files (or stdin) to stdout. +fmt-usage = fmt [-WIDTH] [OPTION]... [FILE]... diff --git a/src/uu/fmt/src/fmt.rs b/src/uu/fmt/src/fmt.rs index 8366af6cdba..bb79c2c591d 100644 --- a/src/uu/fmt/src/fmt.rs +++ b/src/uu/fmt/src/fmt.rs @@ -10,16 +10,15 @@ use std::fs::File; use std::io::{BufReader, BufWriter, Read, Stdout, Write, stdin, stdout}; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError, UUsageError}; -use uucore::{format_usage, help_about, help_usage}; +use uucore::format_usage; use linebreak::break_lines; use parasplit::ParagraphStream; +use uucore::locale::get_message; mod linebreak; mod parasplit; -const ABOUT: &str = help_about!("fmt.md"); -const USAGE: &str = help_usage!("fmt.md"); const MAX_WIDTH: usize = 2500; const DEFAULT_GOAL: usize = 70; const DEFAULT_WIDTH: usize = 75; @@ -336,8 +335,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("fmt-about")) + .override_usage(format_usage(&get_message("fmt-usage"))) .infer_long_args(true) .args_override_self(true) .arg( diff --git a/src/uu/fold/fold.md b/src/uu/fold/fold.md deleted file mode 100644 index 13062c8587d..00000000000 --- a/src/uu/fold/fold.md +++ /dev/null @@ -1,8 +0,0 @@ -# fold - -``` -fold [OPTION]... [FILE]... -``` - -Writes each file (or standard input if no files are given) -to standard output whilst breaking long lines diff --git a/src/uu/fold/locales/en-US.ftl b/src/uu/fold/locales/en-US.ftl new file mode 100644 index 00000000000..5d5bb40a627 --- /dev/null +++ b/src/uu/fold/locales/en-US.ftl @@ -0,0 +1,3 @@ +fold-about = Writes each file (or standard input if no files are given) + to standard output whilst breaking long lines +fold-usage = fold [OPTION]... [FILE]... diff --git a/src/uu/fold/src/fold.rs b/src/uu/fold/src/fold.rs index 0aba9c57ee4..465f964334a 100644 --- a/src/uu/fold/src/fold.rs +++ b/src/uu/fold/src/fold.rs @@ -11,13 +11,11 @@ use std::io::{BufRead, BufReader, Read, stdin}; use std::path::Path; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError}; -use uucore::{format_usage, help_about, help_usage}; +use uucore::format_usage; +use uucore::locale::get_message; const TAB_WIDTH: usize = 8; -const USAGE: &str = help_usage!("fold.md"); -const ABOUT: &str = help_about!("fold.md"); - mod options { pub const BYTES: &str = "bytes"; pub const SPACES: &str = "spaces"; @@ -60,8 +58,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .override_usage(format_usage(USAGE)) - .about(ABOUT) + .override_usage(format_usage(&get_message("fold-usage"))) + .about(get_message("fold-about")) .infer_long_args(true) .arg( Arg::new(options::BYTES) diff --git a/src/uu/groups/groups.md b/src/uu/groups/groups.md deleted file mode 100644 index 1d11d5e46bb..00000000000 --- a/src/uu/groups/groups.md +++ /dev/null @@ -1,8 +0,0 @@ -# groups - -``` -groups [OPTION]... [USERNAME]... -``` - -Print group memberships for each `USERNAME` or, if no `USERNAME` is specified, for -the current process (which may differ if the groups data‐base has changed). diff --git a/src/uu/groups/locales/en-US.ftl b/src/uu/groups/locales/en-US.ftl new file mode 100644 index 00000000000..ec6cc885556 --- /dev/null +++ b/src/uu/groups/locales/en-US.ftl @@ -0,0 +1,3 @@ +groups-about = Print group memberships for each USERNAME or, if no USERNAME is specified, for + the current process (which may differ if the groups data‐base has changed). +groups-usage = groups [OPTION]... [USERNAME]... diff --git a/src/uu/groups/src/groups.rs b/src/uu/groups/src/groups.rs index 6f7fbf5fed2..54f5afbe3f1 100644 --- a/src/uu/groups/src/groups.rs +++ b/src/uu/groups/src/groups.rs @@ -10,16 +10,15 @@ use uucore::{ display::Quotable, entries::{Locate, Passwd, get_groups_gnu, gid2grp}, error::{UError, UResult}, - format_usage, help_about, help_usage, show, + format_usage, show, }; use clap::{Arg, ArgAction, Command}; +use uucore::locale::get_message; mod options { pub const USERS: &str = "USERNAME"; } -const ABOUT: &str = help_about!("groups.md"); -const USAGE: &str = help_usage!("groups.md"); #[derive(Debug, Error)] enum GroupsError { @@ -82,8 +81,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("groups-about")) + .override_usage(format_usage(&get_message("groups-usage"))) .infer_long_args(true) .arg( Arg::new(options::USERS) diff --git a/src/uu/hashsum/hashsum.md b/src/uu/hashsum/hashsum.md deleted file mode 100644 index 35ec840a39c..00000000000 --- a/src/uu/hashsum/hashsum.md +++ /dev/null @@ -1,7 +0,0 @@ -# hashsum - -``` -hashsum -- [OPTIONS]... [FILE]... -``` - -Compute and check message digests. diff --git a/src/uu/hashsum/locales/en-US.ftl b/src/uu/hashsum/locales/en-US.ftl new file mode 100644 index 00000000000..790171034bc --- /dev/null +++ b/src/uu/hashsum/locales/en-US.ftl @@ -0,0 +1,2 @@ +hashsum-about = Compute and check message digests. +hashsum-usage = hashsum -- [OPTIONS]... [FILE]... diff --git a/src/uu/hashsum/src/hashsum.rs b/src/uu/hashsum/src/hashsum.rs index cd8ca912df5..621ec5a0b8f 100644 --- a/src/uu/hashsum/src/hashsum.rs +++ b/src/uu/hashsum/src/hashsum.rs @@ -26,12 +26,11 @@ use uucore::checksum::digest_reader; use uucore::checksum::escape_filename; use uucore::checksum::perform_checksum_validation; use uucore::error::{FromIo, UResult}; +use uucore::format_usage; +use uucore::locale::get_message; use uucore::sum::{Digest, Sha3_224, Sha3_256, Sha3_384, Sha3_512, Shake128, Shake256}; -use uucore::{format_usage, help_about, help_usage}; const NAME: &str = "hashsum"; -const ABOUT: &str = help_about!("hashsum.md"); -const USAGE: &str = help_usage!("hashsum.md"); struct Options { algoname: &'static str, @@ -318,8 +317,8 @@ pub fn uu_app_common() -> Command { const TEXT_HELP: &str = "read in text mode (default)"; Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("hashsum-about")) + .override_usage(format_usage(&get_message("hashsum-usage"))) .infer_long_args(true) .args_override_self(true) .arg( diff --git a/src/uu/head/head.md b/src/uu/head/head.md deleted file mode 100644 index 476f3affc30..00000000000 --- a/src/uu/head/head.md +++ /dev/null @@ -1,11 +0,0 @@ -# head - -``` -head [FLAG]... [FILE]... -``` - -Print the first 10 lines of each `FILE` to standard output. -With more than one `FILE`, precede each with a header giving the file name. -With no `FILE`, or when `FILE` is `-`, read standard input. - -Mandatory arguments to long flags are mandatory for short flags too. diff --git a/src/uu/head/locales/en-US.ftl b/src/uu/head/locales/en-US.ftl new file mode 100644 index 00000000000..4b702b6bc2a --- /dev/null +++ b/src/uu/head/locales/en-US.ftl @@ -0,0 +1,6 @@ +head-about = Print the first 10 lines of each FILE to standard output. + With more than one FILE, precede each with a header giving the file name. + With no FILE, or when FILE is -, read standard input. + + Mandatory arguments to long flags are mandatory for short flags too. +head-usage = head [FLAG]... [FILE]... diff --git a/src/uu/head/src/head.rs b/src/uu/head/src/head.rs index 573926a7bb2..f6711d96094 100644 --- a/src/uu/head/src/head.rs +++ b/src/uu/head/src/head.rs @@ -16,13 +16,10 @@ use thiserror::Error; use uucore::display::Quotable; use uucore::error::{FromIo, UError, UResult}; use uucore::line_ending::LineEnding; -use uucore::{format_usage, help_about, help_usage, show}; +use uucore::{format_usage, show}; const BUF_SIZE: usize = 65536; -const ABOUT: &str = help_about!("head.md"); -const USAGE: &str = help_usage!("head.md"); - mod options { pub const BYTES_NAME: &str = "BYTES"; pub const LINES_NAME: &str = "LINES"; @@ -38,6 +35,7 @@ mod take; use take::copy_all_but_n_bytes; use take::copy_all_but_n_lines; use take::take_lines; +use uucore::locale::get_message; #[derive(Error, Debug)] enum HeadError { @@ -72,8 +70,8 @@ type HeadResult = Result; pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("head-about")) + .override_usage(format_usage(&get_message("head-usage"))) .infer_long_args(true) .arg( Arg::new(options::BYTES_NAME) diff --git a/src/uu/hostid/hostid.md b/src/uu/hostid/hostid.md deleted file mode 100644 index 940cf5e5d36..00000000000 --- a/src/uu/hostid/hostid.md +++ /dev/null @@ -1,7 +0,0 @@ -# hostid - -``` -hostid [options] -``` - -Print the numeric identifier (in hexadecimal) for the current host diff --git a/src/uu/hostid/locales/en-US.ftl b/src/uu/hostid/locales/en-US.ftl new file mode 100644 index 00000000000..25abe94f734 --- /dev/null +++ b/src/uu/hostid/locales/en-US.ftl @@ -0,0 +1,2 @@ +hostid-about = Print the numeric identifier (in hexadecimal) for the current host +hostid-usage = hostid [options] diff --git a/src/uu/hostid/src/hostid.rs b/src/uu/hostid/src/hostid.rs index a01151dde17..aa29bd8fa84 100644 --- a/src/uu/hostid/src/hostid.rs +++ b/src/uu/hostid/src/hostid.rs @@ -7,10 +7,9 @@ use clap::Command; use libc::{c_long, gethostid}; -use uucore::{error::UResult, format_usage, help_about, help_usage}; +use uucore::{error::UResult, format_usage}; -const USAGE: &str = help_usage!("hostid.md"); -const ABOUT: &str = help_about!("hostid.md"); +use uucore::locale::get_message; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { @@ -22,8 +21,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("hostid-about")) + .override_usage(format_usage(&get_message("hostid-usage"))) .infer_long_args(true) } diff --git a/src/uu/hostname/hostname.md b/src/uu/hostname/hostname.md deleted file mode 100644 index f9e852c4b9e..00000000000 --- a/src/uu/hostname/hostname.md +++ /dev/null @@ -1,7 +0,0 @@ -# hostname - -``` -hostname [OPTION]... [HOSTNAME] -``` - -Display or set the system's host name. diff --git a/src/uu/hostname/locales/en-US.ftl b/src/uu/hostname/locales/en-US.ftl new file mode 100644 index 00000000000..8ad9ab52e1f --- /dev/null +++ b/src/uu/hostname/locales/en-US.ftl @@ -0,0 +1,2 @@ +hostname-about = Display or set the system's host name. +hostname-usage = hostname [OPTION]... [HOSTNAME] diff --git a/src/uu/hostname/src/hostname.rs b/src/uu/hostname/src/hostname.rs index 29b2bb6ba1e..05252883e60 100644 --- a/src/uu/hostname/src/hostname.rs +++ b/src/uu/hostname/src/hostname.rs @@ -15,15 +15,13 @@ use clap::{Arg, ArgAction, ArgMatches, Command}; #[cfg(any(target_os = "freebsd", target_os = "openbsd"))] use dns_lookup::lookup_host; +use uucore::locale::get_message; use uucore::{ error::{FromIo, UResult}, - format_usage, help_about, help_usage, + format_usage, }; -const ABOUT: &str = help_about!("hostname.md"); -const USAGE: &str = help_usage!("hostname.md"); - static OPT_DOMAIN: &str = "domain"; static OPT_IP_ADDRESS: &str = "ip-address"; static OPT_FQDN: &str = "fqdn"; @@ -76,8 +74,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("hostname-about")) + .override_usage(format_usage(&get_message("hostname-usage"))) .infer_long_args(true) .arg( Arg::new(OPT_DOMAIN) diff --git a/src/uu/id/id.md b/src/uu/id/id.md deleted file mode 100644 index bb7330314ab..00000000000 --- a/src/uu/id/id.md +++ /dev/null @@ -1,18 +0,0 @@ -# id - -``` -id [OPTION]... [USER]... -``` - -Print user and group information for each specified `USER`, -or (when `USER` omitted) for the current user. - -## After help - -The id utility displays the user and group names and numeric IDs, of the -calling process, to the standard output. If the real and effective IDs are -different, both are displayed, otherwise only the real ID is displayed. - -If a user (login name or user ID) is specified, the user and group IDs of -that user are displayed. In this case, the real and effective IDs are -assumed to be the same. diff --git a/src/uu/id/locales/en-US.ftl b/src/uu/id/locales/en-US.ftl new file mode 100644 index 00000000000..fd7b8348b1f --- /dev/null +++ b/src/uu/id/locales/en-US.ftl @@ -0,0 +1,10 @@ +id-about = Print user and group information for each specified USER, + or (when USER omitted) for the current user. +id-usage = id [OPTION]... [USER]... +id-after-help = The id utility displays the user and group names and numeric IDs, of the + calling process, to the standard output. If the real and effective IDs are + different, both are displayed, otherwise only the real ID is displayed. + + If a user (login name or user ID) is specified, the user and group IDs of + that user are displayed. In this case, the real and effective IDs are + assumed to be the same. diff --git a/src/uu/id/src/id.rs b/src/uu/id/src/id.rs index 314d12d6804..a0831474817 100644 --- a/src/uu/id/src/id.rs +++ b/src/uu/id/src/id.rs @@ -42,8 +42,9 @@ use uucore::error::{USimpleError, set_exit_code}; pub use uucore::libc; use uucore::libc::{getlogin, uid_t}; use uucore::line_ending::LineEnding; +use uucore::locale::get_message; use uucore::process::{getegid, geteuid, getgid, getuid}; -use uucore::{format_usage, help_about, help_section, help_usage, show_error}; +use uucore::{format_usage, show_error}; macro_rules! cstr2cow { ($v:expr) => { @@ -59,10 +60,6 @@ macro_rules! cstr2cow { }; } -const ABOUT: &str = help_about!("id.md"); -const USAGE: &str = help_usage!("id.md"); -const AFTER_HELP: &str = help_section!("after help", "id.md"); - #[cfg(not(feature = "selinux"))] static CONTEXT_HELP_TEXT: &str = "print only the security context of the process (not enabled)"; #[cfg(feature = "selinux")] @@ -119,7 +116,9 @@ struct State { #[uucore::main] #[allow(clippy::cognitive_complexity)] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().after_help(AFTER_HELP).try_get_matches_from(args)?; + let matches = uu_app() + .after_help(get_message("id-after-help")) + .try_get_matches_from(args)?; let users: Vec = matches .get_many::(options::ARG_USERS) @@ -327,8 +326,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("id-about")) + .override_usage(format_usage(&get_message("id-usage"))) .infer_long_args(true) .args_override_self(true) .arg( diff --git a/src/uu/install/install.md b/src/uu/install/install.md deleted file mode 100644 index ac6a70f4219..00000000000 --- a/src/uu/install/install.md +++ /dev/null @@ -1,8 +0,0 @@ -# install - -``` -install [OPTION]... [FILE]... -``` - -Copy SOURCE to DEST or multiple SOURCE(s) to the existing -DIRECTORY, while setting permission modes and owner/group diff --git a/src/uu/install/locales/en-US.ftl b/src/uu/install/locales/en-US.ftl new file mode 100644 index 00000000000..b5781abfbe5 --- /dev/null +++ b/src/uu/install/locales/en-US.ftl @@ -0,0 +1,3 @@ +install-about = Copy SOURCE to DEST or multiple SOURCE(s) to the existing + DIRECTORY, while setting permission modes and owner/group +install-usage = install [OPTION]... [FILE]... diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index c4590240bea..4d93a3fc4ac 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -27,12 +27,13 @@ use uucore::perms::{Verbosity, VerbosityLevel, wrap_chown}; use uucore::process::{getegid, geteuid}; #[cfg(feature = "selinux")] use uucore::selinux::{contexts_differ, set_selinux_security_context}; -use uucore::{format_usage, help_about, help_usage, show, show_error, show_if_err}; +use uucore::{format_usage, show, show_error, show_if_err}; #[cfg(unix)] use std::os::unix::fs::{FileTypeExt, MetadataExt}; #[cfg(unix)] use std::os::unix::prelude::OsStrExt; +use uucore::locale::get_message; const DEFAULT_MODE: u32 = 0o755; const DEFAULT_STRIP_PROGRAM: &str = "strip"; @@ -140,9 +141,6 @@ impl Behavior { } } -const ABOUT: &str = help_about!("install.md"); -const USAGE: &str = help_usage!("install.md"); - static OPT_COMPARE: &str = "compare"; static OPT_DIRECTORY: &str = "directory"; static OPT_IGNORED: &str = "ignored"; @@ -185,8 +183,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("install-about")) + .override_usage(format_usage(&get_message("install-usage"))) .infer_long_args(true) .arg(backup_control::arguments::backup()) .arg(backup_control::arguments::backup_no_args()) @@ -516,7 +514,11 @@ fn standard(mut paths: Vec, b: &Behavior) -> UResult<()> { return Err(UUsageError::new(1, "missing file operand")); } if b.no_target_dir && paths.len() > 2 { - return Err(InstallError::ExtraOperand(paths[2].clone(), format_usage(USAGE)).into()); + return Err(InstallError::ExtraOperand( + paths[2].clone(), + format_usage(&get_message("install-usage")), + ) + .into()); } // get the target from either "-t foo" param or from the last given paths argument diff --git a/src/uu/join/join.md b/src/uu/join/join.md deleted file mode 100644 index 04275342a6e..00000000000 --- a/src/uu/join/join.md +++ /dev/null @@ -1,10 +0,0 @@ -# join - -``` -join [OPTION]... FILE1 FILE2 -``` - -For each pair of input lines with identical join fields, write a line to -standard output. The default join field is the first, delimited by blanks. - -When `FILE1` or `FILE2` (not both) is `-`, read standard input. diff --git a/src/uu/join/locales/en-US.ftl b/src/uu/join/locales/en-US.ftl new file mode 100644 index 00000000000..e5d0053846d --- /dev/null +++ b/src/uu/join/locales/en-US.ftl @@ -0,0 +1,5 @@ +join-about = For each pair of input lines with identical join fields, write a line to + standard output. The default join field is the first, delimited by blanks. + + When FILE1 or FILE2 (not both) is -, read standard input. +join-usage = join [OPTION]... FILE1 FILE2 diff --git a/src/uu/join/src/join.rs b/src/uu/join/src/join.rs index 0c6816cb649..e676dadaa77 100644 --- a/src/uu/join/src/join.rs +++ b/src/uu/join/src/join.rs @@ -18,11 +18,10 @@ use std::os::unix::ffi::OsStrExt; use thiserror::Error; use uucore::display::Quotable; use uucore::error::{FromIo, UError, UResult, USimpleError, set_exit_code}; +use uucore::format_usage; use uucore::line_ending::LineEnding; -use uucore::{format_usage, help_about, help_usage}; -const ABOUT: &str = help_about!("join.md"); -const USAGE: &str = help_usage!("join.md"); +use uucore::locale::get_message; #[derive(Debug, Error)] enum JoinError { @@ -855,8 +854,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("join-about")) + .override_usage(format_usage(&get_message("join-usage"))) .infer_long_args(true) .arg( Arg::new("a") diff --git a/src/uu/kill/kill.md b/src/uu/kill/kill.md deleted file mode 100644 index 1e3e2b3d094..00000000000 --- a/src/uu/kill/kill.md +++ /dev/null @@ -1,7 +0,0 @@ -# kill - -``` -kill [OPTIONS]... PID... -``` - -Send signal to processes or list information about signals. diff --git a/src/uu/kill/locales/en-US.ftl b/src/uu/kill/locales/en-US.ftl new file mode 100644 index 00000000000..3038adaed0c --- /dev/null +++ b/src/uu/kill/locales/en-US.ftl @@ -0,0 +1,2 @@ +kill-about = Send signal to processes or list information about signals. +kill-usage = kill [OPTIONS]... PID... diff --git a/src/uu/kill/src/kill.rs b/src/uu/kill/src/kill.rs index 8d8aa0b614d..6c67aede95b 100644 --- a/src/uu/kill/src/kill.rs +++ b/src/uu/kill/src/kill.rs @@ -12,10 +12,9 @@ use std::io::Error; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError}; use uucore::signals::{ALL_SIGNALS, signal_by_name_or_value, signal_name_by_value}; -use uucore::{format_usage, help_about, help_usage, show}; +use uucore::{format_usage, show}; -static ABOUT: &str = help_about!("kill.md"); -const USAGE: &str = help_usage!("kill.md"); +use uucore::locale::get_message; // When the -l option is selected, the program displays the type of signal related to a certain // value or string. In case of a value, the program should control the lower 8 bits, but there is @@ -104,8 +103,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("kill-about")) + .override_usage(format_usage(&get_message("kill-usage"))) .infer_long_args(true) .allow_negative_numbers(true) .arg( diff --git a/src/uu/link/link.md b/src/uu/link/link.md deleted file mode 100644 index ea6a531b987..00000000000 --- a/src/uu/link/link.md +++ /dev/null @@ -1,7 +0,0 @@ -# link - -``` -link FILE1 FILE2 -``` - -Call the link function to create a link named FILE2 to an existing FILE1. diff --git a/src/uu/link/locales/en-US.ftl b/src/uu/link/locales/en-US.ftl new file mode 100644 index 00000000000..7a18c090015 --- /dev/null +++ b/src/uu/link/locales/en-US.ftl @@ -0,0 +1,2 @@ +link-about = Call the link function to create a link named FILE2 to an existing FILE1. +link-usage = link FILE1 FILE2 diff --git a/src/uu/link/src/link.rs b/src/uu/link/src/link.rs index 31f1239d86c..5ff1760ba78 100644 --- a/src/uu/link/src/link.rs +++ b/src/uu/link/src/link.rs @@ -9,10 +9,9 @@ use std::fs::hard_link; use std::path::Path; use uucore::display::Quotable; use uucore::error::{FromIo, UResult}; -use uucore::{format_usage, help_about, help_usage}; +use uucore::format_usage; -static ABOUT: &str = help_about!("link.md"); -const USAGE: &str = help_usage!("link.md"); +use uucore::locale::get_message; pub mod options { pub static FILES: &str = "FILES"; @@ -36,8 +35,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("link-about")) + .override_usage(format_usage(&get_message("link-usage"))) .infer_long_args(true) .arg( Arg::new(options::FILES) diff --git a/src/uu/ln/ln.md b/src/uu/ln/ln.md deleted file mode 100644 index 6bd6ee01619..00000000000 --- a/src/uu/ln/ln.md +++ /dev/null @@ -1,21 +0,0 @@ -# ln - -``` -ln [OPTION]... [-T] TARGET LINK_NAME -ln [OPTION]... TARGET -ln [OPTION]... TARGET... DIRECTORY -ln [OPTION]... -t DIRECTORY TARGET... -``` - -Make links between files. - -## After Help - -In the 1st form, create a link to TARGET with the name LINK_NAME. -In the 2nd form, create a link to TARGET in the current directory. -In the 3rd and 4th forms, create links to each TARGET in DIRECTORY. -Create hard links by default, symbolic links with --symbolic. -By default, each destination (name of new link) should not already exist. -When creating hard links, each TARGET must exist. Symbolic links -can hold arbitrary text; if later resolved, a relative link is -interpreted in relation to its parent directory. diff --git a/src/uu/ln/locales/en-US.ftl b/src/uu/ln/locales/en-US.ftl new file mode 100644 index 00000000000..7de0c171eb2 --- /dev/null +++ b/src/uu/ln/locales/en-US.ftl @@ -0,0 +1,13 @@ +ln-about = Make links between files. +ln-usage = ln [OPTION]... [-T] TARGET LINK_NAME + ln [OPTION]... TARGET + ln [OPTION]... TARGET... DIRECTORY + ln [OPTION]... -t DIRECTORY TARGET... +ln-after-help = In the 1st form, create a link to TARGET with the name LINK_NAME. + In the 2nd form, create a link to TARGET in the current directory. + In the 3rd and 4th forms, create links to each TARGET in DIRECTORY. + Create hard links by default, symbolic links with --symbolic. + By default, each destination (name of new link) should not already exist. + When creating hard links, each TARGET must exist. Symbolic links + can hold arbitrary text; if later resolved, a relative link is + interpreted in relation to its parent directory. diff --git a/src/uu/ln/src/ln.rs b/src/uu/ln/src/ln.rs index 3b8ff0d7069..960085200ec 100644 --- a/src/uu/ln/src/ln.rs +++ b/src/uu/ln/src/ln.rs @@ -9,7 +9,7 @@ use clap::{Arg, ArgAction, Command}; use uucore::display::Quotable; use uucore::error::{FromIo, UError, UResult}; use uucore::fs::{make_path_relative_to, paths_refer_to_same_file}; -use uucore::{format_usage, help_about, help_section, help_usage, prompt_yes, show_error}; +use uucore::{format_usage, prompt_yes, show_error}; use std::borrow::Cow; use std::collections::HashSet; @@ -24,6 +24,7 @@ use std::os::windows::fs::{symlink_dir, symlink_file}; use std::path::{Path, PathBuf}; use uucore::backup_control::{self, BackupMode}; use uucore::fs::{MissingHandling, ResolveMode, canonicalize}; +use uucore::locale::get_message; pub struct Settings { overwrite: OverwriteMode, @@ -48,7 +49,7 @@ pub enum OverwriteMode { #[derive(Error, Debug)] enum LnError { #[error("target {} is not a directory", _0.quote())] - TargetIsDirectory(PathBuf), + TargetIsNotADirectory(PathBuf), #[error("")] SomeLinksFailed, @@ -70,10 +71,6 @@ impl UError for LnError { } } -const ABOUT: &str = help_about!("ln.md"); -const USAGE: &str = help_usage!("ln.md"); -const AFTER_HELP: &str = help_section!("after help", "ln.md"); - mod options { pub const FORCE: &str = "force"; //pub const DIRECTORY: &str = "directory"; @@ -93,7 +90,8 @@ static ARG_FILES: &str = "files"; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let after_help = format!( - "{AFTER_HELP}\n\n{}", + "{}\n\n{}", + get_message("ln-after-help"), backup_control::BACKUP_CONTROL_LONG_HELP ); @@ -144,8 +142,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("ln-about")) + .override_usage(format_usage(&get_message("ln-usage"))) .infer_long_args(true) .arg(backup_control::arguments::backup()) .arg(backup_control::arguments::backup_no_args()) @@ -283,7 +281,7 @@ fn exec(files: &[PathBuf], settings: &Settings) -> UResult<()> { #[allow(clippy::cognitive_complexity)] fn link_files_in_dir(files: &[PathBuf], target_dir: &Path, settings: &Settings) -> UResult<()> { if !target_dir.is_dir() { - return Err(LnError::TargetIsDirectory(target_dir.to_owned()).into()); + return Err(LnError::TargetIsNotADirectory(target_dir.to_owned()).into()); } // remember the linked destinations for further usage let mut linked_destinations: HashSet = HashSet::with_capacity(files.len()); @@ -301,6 +299,7 @@ fn link_files_in_dir(files: &[PathBuf], target_dir: &Path, settings: &Settings) show_error!("Could not update {}: {e}", target_dir.quote()); }; } + #[cfg(windows)] if target_dir.is_dir() { // Not sure why but on Windows, the symlink can be // considered as a dir diff --git a/src/uu/logname/locales/en-US.ftl b/src/uu/logname/locales/en-US.ftl new file mode 100644 index 00000000000..038b757bc29 --- /dev/null +++ b/src/uu/logname/locales/en-US.ftl @@ -0,0 +1 @@ +logname-about = Print user's login name diff --git a/src/uu/logname/logname.md b/src/uu/logname/logname.md deleted file mode 100644 index b997c206be8..00000000000 --- a/src/uu/logname/logname.md +++ /dev/null @@ -1,7 +0,0 @@ -# logname - -``` -logname -``` - -Print user's login name diff --git a/src/uu/logname/src/logname.rs b/src/uu/logname/src/logname.rs index 5437bbae344..1ed1efb5ff3 100644 --- a/src/uu/logname/src/logname.rs +++ b/src/uu/logname/src/logname.rs @@ -7,7 +7,8 @@ use clap::Command; use std::ffi::CStr; -use uucore::{error::UResult, format_usage, help_about, help_usage, show_error}; +use uucore::locale::get_message; +use uucore::{error::UResult, show_error}; unsafe extern "C" { // POSIX requires using getlogin (or equivalent code) @@ -25,9 +26,6 @@ fn get_userlogin() -> Option { } } -const ABOUT: &str = help_about!("logname.md"); -const USAGE: &str = help_usage!("logname.md"); - #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let _ = uu_app().try_get_matches_from(args)?; @@ -43,7 +41,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .override_usage(format_usage(USAGE)) - .about(ABOUT) + .override_usage(uucore::util_name()) + .about(get_message("logname-about")) .infer_long_args(true) } diff --git a/src/uu/ls/Cargo.toml b/src/uu/ls/Cargo.toml index ff00175e747..ffb90a9403c 100644 --- a/src/uu/ls/Cargo.toml +++ b/src/uu/ls/Cargo.toml @@ -1,3 +1,5 @@ +# spell-checker:ignore tzdb zoneinfo + [package] name = "uu_ls" description = "ls ~ (uutils) display directory contents" @@ -23,6 +25,11 @@ chrono = { workspace = true } clap = { workspace = true, features = ["env"] } glob = { workspace = true } hostname = { workspace = true } +jiff = { workspace = true, features = [ + "tzdb-bundle-platform", + "tzdb-zoneinfo", + "tzdb-concatenated", +] } lscolors = { workspace = true } number_prefix = { workspace = true } selinux = { workspace = true, optional = true } @@ -30,7 +37,6 @@ terminal_size = { workspace = true } thiserror = { workspace = true } uucore = { workspace = true, features = [ "colors", - "custom-tz-fmt", "entries", "format", "fs", diff --git a/src/uu/ls/locales/en-US.ftl b/src/uu/ls/locales/en-US.ftl new file mode 100644 index 00000000000..b21aefa2257 --- /dev/null +++ b/src/uu/ls/locales/en-US.ftl @@ -0,0 +1,4 @@ +ls-about = List directory contents. + Ignore files and directories starting with a '.' by default +ls-usage = ls [OPTION]... [FILE]... +ls-after-help = The TIME_STYLE argument can be full-iso, long-iso, iso, locale or +FORMAT. FORMAT is interpreted like in date. Also the TIME_STYLE environment variable sets the default style to use. diff --git a/src/uu/ls/ls.md b/src/uu/ls/ls.md deleted file mode 100644 index 01eec7e5052..00000000000 --- a/src/uu/ls/ls.md +++ /dev/null @@ -1,12 +0,0 @@ -# ls - -``` -ls [OPTION]... [FILE]... -``` - -List directory contents. -Ignore files and directories starting with a '.' by default - -## After help - -The TIME_STYLE argument can be full-iso, long-iso, iso, locale or +FORMAT. FORMAT is interpreted like in date. Also the TIME_STYLE environment variable sets the default style to use. diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 0297a569cef..f3884f3023f 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (ToDO) somegroup nlink tabsize dired subdired dtype colorterm stringly nohash +// spell-checker:ignore (ToDO) somegroup nlink tabsize dired subdired dtype colorterm stringly nohash strtime use std::iter; #[cfg(windows)] @@ -16,29 +16,32 @@ use std::{ fs::{self, DirEntry, FileType, Metadata, ReadDir}, io::{BufWriter, ErrorKind, Stdout, Write, stdout}, path::{Path, PathBuf}, - time::{SystemTime, UNIX_EPOCH}, + time::{Duration, SystemTime, UNIX_EPOCH}, }; #[cfg(unix)] use std::{ collections::HashMap, os::unix::fs::{FileTypeExt, MetadataExt}, - time::Duration, }; use std::{collections::HashSet, io::IsTerminal}; use ansi_width::ansi_width; -use chrono::format::{Item, StrftimeItems}; -use chrono::{DateTime, Local, TimeDelta}; use clap::{ Arg, ArgAction, Command, builder::{NonEmptyStringValueParser, PossibleValue, ValueParser}, }; use glob::{MatchOptions, Pattern}; +use jiff::fmt::StdIoWrite; +use jiff::fmt::strtime::BrokenDownTime; +use jiff::{Timestamp, Zoned}; use lscolors::LsColors; use term_grid::{DEFAULT_SEPARATOR_SIZE, Direction, Filling, Grid, GridOptions, SPACES_IN_TAB}; use thiserror::Error; +#[cfg(unix)] +use uucore::entries; use uucore::error::USimpleError; use uucore::format::human::{SizeFormat, human_readable}; +use uucore::fs::FileInformation; #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] use uucore::fsxattr::has_acl; #[cfg(unix)] @@ -57,9 +60,9 @@ use uucore::libc::{S_IXGRP, S_IXOTH, S_IXUSR}; ))] use uucore::libc::{dev_t, major, minor}; use uucore::line_ending::LineEnding; +use uucore::locale::get_message; use uucore::quoting_style::{self, QuotingStyle, escape_name}; use uucore::{ - custom_tz_fmt, display::Quotable, error::{UError, UResult, set_exit_code}, format_usage, @@ -69,9 +72,7 @@ use uucore::{ parser::shortcut_value_parser::ShortcutValueParser, version_cmp::version_cmp, }; -use uucore::{ - help_about, help_section, help_usage, parser::parse_glob, show, show_error, show_warning, -}; +use uucore::{parser::parse_glob, show, show_error, show_warning}; mod dired; use dired::{DiredOutput, is_dired_arg_present}; @@ -83,10 +84,6 @@ static CONTEXT_HELP_TEXT: &str = "print any security context of each file (not e #[cfg(feature = "selinux")] static CONTEXT_HELP_TEXT: &str = "print any security context of each file"; -const ABOUT: &str = help_about!("ls.md"); -const AFTER_HELP: &str = help_section!("after help", "ls.md"); -const USAGE: &str = help_usage!("ls.md"); - pub mod options { pub mod format { pub static ONE_LINE: &str = "1"; @@ -274,64 +271,37 @@ enum TimeStyle { Format(String), } -/// A struct/impl used to format a file DateTime, precomputing the format for performance reasons. -struct TimeStyler { - // default format, always specified. - default: Vec>, - // format for a recent time, only specified it is is different from the default - recent: Option>>, - // If `recent` is set, cache the threshold time when we switch from recent to default format. - recent_time_threshold: Option>, -} - -impl TimeStyler { - /// Create a TimeStyler based on a TimeStyle specification. - fn new(style: &TimeStyle) -> TimeStyler { - let default: Vec> = match style { - TimeStyle::FullIso => StrftimeItems::new("%Y-%m-%d %H:%M:%S.%f %z").parse(), - TimeStyle::LongIso => StrftimeItems::new("%Y-%m-%d %H:%M").parse(), - TimeStyle::Iso => StrftimeItems::new("%Y-%m-%d ").parse(), - // In this version of chrono translating can be done - // The function is chrono::datetime::DateTime::format_localized - // However it's currently still hard to get the current pure-rust-locale - // So it's not yet implemented - TimeStyle::Locale => StrftimeItems::new("%b %e %Y").parse(), - TimeStyle::Format(fmt) => { - StrftimeItems::new_lenient(custom_tz_fmt::custom_time_format(fmt).as_str()) - .parse_to_owned() - } - } - .unwrap(); - let recent = match style { - TimeStyle::Iso => Some(StrftimeItems::new("%m-%d %H:%M")), - // See comment above about locale - TimeStyle::Locale => Some(StrftimeItems::new("%b %e %H:%M")), - _ => None, - } - .map(|x| x.collect()); - let recent_time_threshold = if recent.is_some() { - // According to GNU a Gregorian year has 365.2425 * 24 * 60 * 60 == 31556952 seconds on the average. - Some(Local::now() - TimeDelta::try_seconds(31_556_952 / 2).unwrap()) - } else { - None +/// Whether the given date is considered recent (i.e., in the last 6 months). +fn is_recent(time: Timestamp, state: &mut ListState) -> bool { + // According to GNU a Gregorian year has 365.2425 * 24 * 60 * 60 == 31556952 seconds on the average. + time > state.recent_time_threshold +} + +impl TimeStyle { + /// Format the given time according to this time format style. + fn format( + &self, + date: Zoned, + out: &mut Vec, + state: &mut ListState, + ) -> Result<(), jiff::Error> { + let recent = is_recent(date.timestamp(), state); + let tm = BrokenDownTime::from(&date); + let mut out = StdIoWrite(out); + let config = jiff::fmt::strtime::Config::new().lenient(true); + + let fmt = match (self, recent) { + (Self::FullIso, _) => "%Y-%m-%d %H:%M:%S.%f %z", + (Self::LongIso, _) => "%Y-%m-%d %H:%M", + (Self::Iso, true) => "%m-%d %H:%M", + (Self::Iso, false) => "%Y-%m-%d ", + // TODO: Using correct locale string is not implemented. + (Self::Locale, true) => "%b %e %H:%M", + (Self::Locale, false) => "%b %e %Y", + (Self::Format(fmt), _) => fmt, }; - TimeStyler { - default, - recent, - recent_time_threshold, - } - } - - /// Format a DateTime, using `recent` format if available, and the DateTime - /// is recent enough. - fn format(&self, time: DateTime) -> String { - if self.recent.is_none() || time <= self.recent_time_threshold.unwrap() { - time.format_with_items(self.default.iter()) - } else { - time.format_with_items(self.recent.as_ref().unwrap().iter()) - } - .to_string() + tm.format_with_config(&config, fmt, &mut out) } } @@ -1207,8 +1177,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .override_usage(format_usage(USAGE)) - .about(ABOUT) + .override_usage(format_usage(&get_message("ls-usage"))) + .about(get_message("ls-about")) .infer_long_args(true) .disable_help_flag(true) .args_override_self(true) @@ -1897,7 +1867,7 @@ pub fn uu_app() -> Command { .value_hint(clap::ValueHint::AnyPath) .value_parser(ValueParser::os_string()), ) - .after_help(AFTER_HELP) + .after_help(get_message("ls-after-help")) } /// Represents a Path along with it's associated data. @@ -2093,8 +2063,7 @@ struct ListState<'a> { uid_cache: HashMap, #[cfg(unix)] gid_cache: HashMap, - - time_styler: TimeStyler, + recent_time_threshold: Timestamp, } #[allow(clippy::cognitive_complexity)] @@ -2111,7 +2080,7 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> { uid_cache: HashMap::new(), #[cfg(unix)] gid_cache: HashMap::new(), - time_styler: TimeStyler::new(&config.time_style), + recent_time_threshold: Timestamp::now() - Duration::new(31_556_952 / 2, 0), }; for loc in locs { @@ -2907,7 +2876,7 @@ fn display_item_long( }; output_display.extend(b" "); - output_display.extend(display_date(md, config, state).as_bytes()); + display_date(md, config, state, &mut output_display)?; output_display.extend(b" "); let item_name = display_item_name( @@ -3045,10 +3014,6 @@ fn get_inode(metadata: &Metadata) -> String { // Currently getpwuid is `linux` target only. If it's broken state.out into // a posix-compliant attribute this can be updated... -#[cfg(unix)] -use uucore::entries; -use uucore::fs::FileInformation; - #[cfg(unix)] fn display_uname<'a>(metadata: &Metadata, config: &Config, state: &'a mut ListState) -> &'a String { let uid = metadata.uid(); @@ -3106,15 +3071,27 @@ fn get_system_time(md: &Metadata, config: &Config) -> Option { } } -fn get_time(md: &Metadata, config: &Config) -> Option> { +fn get_time(md: &Metadata, config: &Config) -> Option { let time = get_system_time(md, config)?; - Some(time.into()) + time.try_into().ok() } -fn display_date(metadata: &Metadata, config: &Config, state: &mut ListState) -> String { +fn display_date( + metadata: &Metadata, + config: &Config, + state: &mut ListState, + out: &mut Vec, +) -> UResult<()> { match get_time(metadata, config) { - Some(time) => state.time_styler.format(time), - None => "???".into(), + // TODO: Some fancier error conversion might be nice. + Some(time) => config + .time_style + .format(time, out, state) + .map_err(|x| USimpleError::new(1, x.to_string())), + None => { + out.extend(b"???"); + Ok(()) + } } } diff --git a/src/uu/mkdir/locales/en-US.ftl b/src/uu/mkdir/locales/en-US.ftl new file mode 100644 index 00000000000..04232924409 --- /dev/null +++ b/src/uu/mkdir/locales/en-US.ftl @@ -0,0 +1,3 @@ +mkdir-about = Create the given DIRECTORY(ies) if they do not exist +mkdir-usage = mkdir [OPTION]... DIRECTORY... +mkdir-after-help = Each MODE is of the form [ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=]?[0-7]+. diff --git a/src/uu/mkdir/mkdir.md b/src/uu/mkdir/mkdir.md deleted file mode 100644 index f5dbb25440b..00000000000 --- a/src/uu/mkdir/mkdir.md +++ /dev/null @@ -1,13 +0,0 @@ -# mkdir - - - -``` -mkdir [OPTION]... DIRECTORY... -``` - -Create the given DIRECTORY(ies) if they do not exist - -## After Help - -Each MODE is of the form `[ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=]?[0-7]+`. diff --git a/src/uu/mkdir/src/mkdir.rs b/src/uu/mkdir/src/mkdir.rs index adef62eee7a..7fceb36ce3f 100644 --- a/src/uu/mkdir/src/mkdir.rs +++ b/src/uu/mkdir/src/mkdir.rs @@ -13,17 +13,14 @@ use std::path::{Path, PathBuf}; #[cfg(not(windows))] use uucore::error::FromIo; use uucore::error::{UResult, USimpleError}; +use uucore::locale::get_message; #[cfg(not(windows))] use uucore::mode; use uucore::{display::Quotable, fs::dir_strip_dot_for_creation}; -use uucore::{format_usage, help_about, help_section, help_usage, show_if_err}; +use uucore::{format_usage, show_if_err}; static DEFAULT_PERM: u32 = 0o777; -const ABOUT: &str = help_about!("mkdir.md"); -const USAGE: &str = help_usage!("mkdir.md"); -const AFTER_HELP: &str = help_section!("after help", "mkdir.md"); - mod options { pub const MODE: &str = "mode"; pub const PARENTS: &str = "parents"; @@ -103,7 +100,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // Linux-specific options, not implemented // opts.optflag("Z", "context", "set SELinux security context" + // " of each created directory to CTX"), - let matches = uu_app().after_help(AFTER_HELP).try_get_matches_from(args)?; + let matches = uu_app() + .after_help(get_message("mkdir-after-help")) + .try_get_matches_from(args)?; let dirs = matches .get_many::(options::DIRS) @@ -133,8 +132,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("mkdir-about")) + .override_usage(format_usage(&get_message("mkdir-usage"))) .infer_long_args(true) .arg( Arg::new(options::MODE) diff --git a/src/uu/mkfifo/locales/en-US.ftl b/src/uu/mkfifo/locales/en-US.ftl new file mode 100644 index 00000000000..aee4bba9288 --- /dev/null +++ b/src/uu/mkfifo/locales/en-US.ftl @@ -0,0 +1,2 @@ +mkfifo-about = Create a FIFO with the given name. +mkfifo-usage = mkfifo [OPTION]... NAME... diff --git a/src/uu/mkfifo/mkfifo.md b/src/uu/mkfifo/mkfifo.md deleted file mode 100644 index b6343782922..00000000000 --- a/src/uu/mkfifo/mkfifo.md +++ /dev/null @@ -1,7 +0,0 @@ -# mkfifo - -``` -mkfifo [OPTION]... NAME... -``` - -Create a FIFO with the given name. diff --git a/src/uu/mkfifo/src/mkfifo.rs b/src/uu/mkfifo/src/mkfifo.rs index 24c057ebc94..ea5880438aa 100644 --- a/src/uu/mkfifo/src/mkfifo.rs +++ b/src/uu/mkfifo/src/mkfifo.rs @@ -10,10 +10,9 @@ use std::fs; use std::os::unix::fs::PermissionsExt; use uucore::display::Quotable; use uucore::error::{UResult, USimpleError}; -use uucore::{format_usage, help_about, help_usage, show}; +use uucore::{format_usage, show}; -static USAGE: &str = help_usage!("mkfifo.md"); -static ABOUT: &str = help_about!("mkfifo.md"); +use uucore::locale::get_message; mod options { pub static MODE: &str = "mode"; @@ -86,8 +85,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .override_usage(format_usage(USAGE)) - .about(ABOUT) + .override_usage(format_usage(&get_message("mkfifo-usage"))) + .about(get_message("mkfifo-about")) .infer_long_args(true) .arg( Arg::new(options::MODE) diff --git a/src/uu/mknod/locales/en-US.ftl b/src/uu/mknod/locales/en-US.ftl new file mode 100644 index 00000000000..c0e391381b9 --- /dev/null +++ b/src/uu/mknod/locales/en-US.ftl @@ -0,0 +1,17 @@ +mknod-about = Create the special file NAME of the given TYPE. +mknod-usage = mknod [OPTION]... NAME TYPE [MAJOR MINOR] +mknod-after-help = Mandatory arguments to long options are mandatory for short options too. + -m, --mode=MODE set file permission bits to MODE, not a=rw - umask + + 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. diff --git a/src/uu/mknod/mknod.md b/src/uu/mknod/mknod.md deleted file mode 100644 index 78da970252e..00000000000 --- a/src/uu/mknod/mknod.md +++ /dev/null @@ -1,25 +0,0 @@ -# mknod - -``` -mknod [OPTION]... NAME TYPE [MAJOR MINOR] -``` - -Create the special file NAME of the given TYPE. - -## After Help - -Mandatory arguments to long options are mandatory for short options too. -`-m`, `--mode=MODE` set file permission bits to `MODE`, not `a=rw - umask` - -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. diff --git a/src/uu/mknod/src/mknod.rs b/src/uu/mknod/src/mknod.rs index 076e639ccfb..94aa043ca80 100644 --- a/src/uu/mknod/src/mknod.rs +++ b/src/uu/mknod/src/mknod.rs @@ -12,11 +12,9 @@ use std::ffi::CString; use uucore::display::Quotable; use uucore::error::{UResult, USimpleError, UUsageError, set_exit_code}; -use uucore::{format_usage, help_about, help_section, help_usage}; +use uucore::format_usage; -const ABOUT: &str = help_about!("mknod.md"); -const USAGE: &str = help_usage!("mknod.md"); -const AFTER_HELP: &str = help_section!("after help", "mknod.md"); +use uucore::locale::get_message; const MODE_RW_UGO: mode_t = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH; @@ -159,9 +157,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .override_usage(format_usage(USAGE)) - .after_help(AFTER_HELP) - .about(ABOUT) + .override_usage(format_usage(&get_message("mknod-usage"))) + .after_help(get_message("mknod-after-help")) + .about(get_message("mknod-about")) .infer_long_args(true) .arg( Arg::new(options::MODE) diff --git a/src/uu/mktemp/locales/en-US.ftl b/src/uu/mktemp/locales/en-US.ftl new file mode 100644 index 00000000000..0e290ad970c --- /dev/null +++ b/src/uu/mktemp/locales/en-US.ftl @@ -0,0 +1,2 @@ +mktemp-about = Create a temporary file or directory. +mktemp-usage = mktemp [OPTION]... [TEMPLATE] diff --git a/src/uu/mktemp/mktemp.md b/src/uu/mktemp/mktemp.md deleted file mode 100644 index 75b56d66751..00000000000 --- a/src/uu/mktemp/mktemp.md +++ /dev/null @@ -1,7 +0,0 @@ -# mktemp - -``` -mktemp [OPTION]... [TEMPLATE] -``` - -Create a temporary file or directory. diff --git a/src/uu/mktemp/src/mktemp.rs b/src/uu/mktemp/src/mktemp.rs index d689115a6c6..ec649508425 100644 --- a/src/uu/mktemp/src/mktemp.rs +++ b/src/uu/mktemp/src/mktemp.rs @@ -8,7 +8,7 @@ use clap::{Arg, ArgAction, ArgMatches, Command, builder::ValueParser}; use uucore::display::{Quotable, println_verbatim}; use uucore::error::{FromIo, UError, UResult, UUsageError}; -use uucore::{format_usage, help_about, help_usage}; +use uucore::format_usage; use std::env; use std::ffi::OsStr; @@ -25,8 +25,7 @@ use rand::Rng; use tempfile::Builder; use thiserror::Error; -const ABOUT: &str = help_about!("mktemp.md"); -const USAGE: &str = help_usage!("mktemp.md"); +use uucore::locale::get_message; static DEFAULT_TEMPLATE: &str = "tmp.XXXXXXXXXX"; @@ -347,8 +346,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("mktemp-about")) + .override_usage(format_usage(&get_message("mktemp-usage"))) .infer_long_args(true) .arg( Arg::new(OPT_DIRECTORY) diff --git a/src/uu/more/locales/en-US.ftl b/src/uu/more/locales/en-US.ftl new file mode 100644 index 00000000000..9d2d24c869b --- /dev/null +++ b/src/uu/more/locales/en-US.ftl @@ -0,0 +1,2 @@ +more-about = Display the contents of a text file +more-usage = more [OPTIONS] FILE... diff --git a/src/uu/more/more.md b/src/uu/more/more.md deleted file mode 100644 index e63ca1a87fb..00000000000 --- a/src/uu/more/more.md +++ /dev/null @@ -1,8 +0,0 @@ -# more - -``` -more [OPTIONS] FILE... -``` - -Display the contents of a text file - diff --git a/src/uu/more/src/more.rs b/src/uu/more/src/more.rs index 97d911f4f3e..fc268944895 100644 --- a/src/uu/more/src/more.rs +++ b/src/uu/more/src/more.rs @@ -23,11 +23,10 @@ use crossterm::{ }; use uucore::error::{UResult, USimpleError, UUsageError}; +use uucore::format_usage; use uucore::{display::Quotable, show}; -use uucore::{format_usage, help_about, help_usage}; -const ABOUT: &str = help_about!("more.md"); -const USAGE: &str = help_usage!("more.md"); +use uucore::locale::get_message; const BELL: char = '\x07'; // Printing this character will ring the bell // The prompt to be displayed at the top of the screen when viewing multiple files, @@ -155,8 +154,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("more-about")) + .override_usage(format_usage(&get_message("more-usage"))) .version(uucore::crate_version!()) .infer_long_args(true) .arg( diff --git a/src/uu/mv/locales/en-US.ftl b/src/uu/mv/locales/en-US.ftl new file mode 100644 index 00000000000..7e8b2bd28e8 --- /dev/null +++ b/src/uu/mv/locales/en-US.ftl @@ -0,0 +1,16 @@ +mv-about = Move SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY. +mv-usage = mv [OPTION]... [-T] SOURCE DEST + mv [OPTION]... SOURCE... DIRECTORY + mv [OPTION]... -t DIRECTORY SOURCE... +mv-after-help = When specifying more than one of -i, -f, -n, only the final one will take effect. + + Do not move a non-directory that has an existing destination with the same or newer modification timestamp; + instead, silently skip the file without failing. If the move is across file system boundaries, the comparison is + to the source timestamp truncated to the resolutions of the destination file system and of the system calls used + to update timestamps; this avoids duplicate work if several mv -u commands are executed with the same source + and destination. This option is ignored if the -n or --no-clobber option is also specified. which gives more control + over which existing files in the destination are replaced, and its value can be one of the following: + + - all This is the default operation when an --update option is not specified, and results in all existing files in the destination being replaced. + - none This is similar to the --no-clobber option, in that no files in the destination are replaced, but also skipping a file does not induce a failure. + - older This is the default operation when --update is specified, and results in files being replaced if they’re older than the corresponding source file. diff --git a/src/uu/mv/mv.md b/src/uu/mv/mv.md deleted file mode 100644 index 6fcee46973f..00000000000 --- a/src/uu/mv/mv.md +++ /dev/null @@ -1,23 +0,0 @@ -# mv - -``` -mv [OPTION]... [-T] SOURCE DEST -mv [OPTION]... SOURCE... DIRECTORY -mv [OPTION]... -t DIRECTORY SOURCE... -``` -Move `SOURCE` to `DEST`, or multiple `SOURCE`(s) to `DIRECTORY`. - -## After Help - -When specifying more than one of -i, -f, -n, only the final one will take effect. - -Do not move a non-directory that has an existing destination with the same or newer modification timestamp; -instead, silently skip the file without failing. If the move is across file system boundaries, the comparison is -to the source timestamp truncated to the resolutions of the destination file system and of the system calls used -to update timestamps; this avoids duplicate work if several `mv -u` commands are executed with the same source -and destination. This option is ignored if the `-n` or `--no-clobber` option is also specified. which gives more control -over which existing files in the destination are replaced, and its value can be one of the following: - -* `all` This is the default operation when an `--update` option is not specified, and results in all existing files in the destination being replaced. -* `none` This is similar to the `--no-clobber` option, in that no files in the destination are replaced, but also skipping a file does not induce a failure. -* `older` This is the default operation when `--update` is specified, and results in files being replaced if they’re older than the corresponding source file. diff --git a/src/uu/mv/src/mv.rs b/src/uu/mv/src/mv.rs index edfa505c521..82af08b051b 100644 --- a/src/uu/mv/src/mv.rs +++ b/src/uu/mv/src/mv.rs @@ -40,7 +40,7 @@ use uucore::update_control; // These are exposed for projects (e.g. nushell) that want to create an `Options` value, which // requires these enums pub use uucore::{backup_control::BackupMode, update_control::UpdateMode}; -use uucore::{format_usage, help_about, help_section, help_usage, prompt_yes, show}; +use uucore::{format_usage, prompt_yes, show}; use fs_extra::dir::{ CopyOptions as DirCopyOptions, TransitProcess, TransitProcessResult, get_size as dir_get_size, @@ -48,6 +48,7 @@ use fs_extra::dir::{ }; use crate::error::MvError; +use uucore::locale::get_message; /// Options contains all the possible behaviors and flags for mv. /// @@ -123,10 +124,6 @@ pub enum OverwriteMode { Force, } -const ABOUT: &str = help_about!("mv.md"); -const USAGE: &str = help_usage!("mv.md"); -const AFTER_HELP: &str = help_section!("after help", "mv.md"); - static OPT_FORCE: &str = "force"; static OPT_INTERACTIVE: &str = "interactive"; static OPT_NO_CLOBBER: &str = "no-clobber"; @@ -205,10 +202,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("mv-about")) + .override_usage(format_usage(&get_message("mv-usage"))) .after_help(format!( - "{AFTER_HELP}\n\n{}", + "{}\n\n{}", + get_message("mv-after-help"), backup_control::BACKUP_CONTROL_LONG_HELP )) .infer_long_args(true) @@ -526,7 +524,7 @@ fn move_files_into_dir(files: &[PathBuf], target_dir: &Path, options: &Options) }; for sourcepath in files { - if !sourcepath.exists() { + if sourcepath.symlink_metadata().is_err() { show!(MvError::NoSuchFile(sourcepath.quote().to_string())); continue; } diff --git a/src/uu/nice/locales/en-US.ftl b/src/uu/nice/locales/en-US.ftl new file mode 100644 index 00000000000..d243ef94e47 --- /dev/null +++ b/src/uu/nice/locales/en-US.ftl @@ -0,0 +1,5 @@ +nice-about = Run COMMAND with an adjusted niceness, which affects process scheduling. + With no COMMAND, print the current niceness. Niceness values range from at + least -20 (most favorable to the process) to 19 (least favorable to the + process). +nice-usage = nice [OPTIONS] [COMMAND [ARGS]] diff --git a/src/uu/nice/nice.md b/src/uu/nice/nice.md deleted file mode 100644 index 872c239cab1..00000000000 --- a/src/uu/nice/nice.md +++ /dev/null @@ -1,10 +0,0 @@ -# nice - -``` -nice [OPTIONS] [COMMAND [ARGS]] -``` - -Run `COMMAND` with an adjusted niceness, which affects process scheduling. -With no `COMMAND`, print the current niceness. Niceness values range from at -least -20 (most favorable to the process) to 19 (least favorable to the -process). diff --git a/src/uu/nice/src/nice.rs b/src/uu/nice/src/nice.rs index 05ae2fa94da..bb86d7778c5 100644 --- a/src/uu/nice/src/nice.rs +++ b/src/uu/nice/src/nice.rs @@ -11,9 +11,10 @@ use std::io::{Error, Write}; use std::ptr; use clap::{Arg, ArgAction, Command}; +use uucore::locale::get_message; use uucore::{ error::{UClapError, UResult, USimpleError, UUsageError, set_exit_code}, - format_usage, help_about, help_usage, show_error, + format_usage, show_error, }; pub mod options { @@ -21,9 +22,6 @@ pub mod options { pub static COMMAND: &str = "COMMAND"; } -const ABOUT: &str = help_about!("nice.md"); -const USAGE: &str = help_usage!("nice.md"); - fn is_prefix_of(maybe_prefix: &str, target: &str, min_match: usize) -> bool { if maybe_prefix.len() < min_match || maybe_prefix.len() > target.len() { return false; @@ -186,8 +184,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("nice-about")) + .override_usage(format_usage(&get_message("nice-usage"))) .trailing_var_arg(true) .infer_long_args(true) .version(uucore::crate_version!()) diff --git a/src/uu/nl/locales/en-US.ftl b/src/uu/nl/locales/en-US.ftl new file mode 100644 index 00000000000..7880de562db --- /dev/null +++ b/src/uu/nl/locales/en-US.ftl @@ -0,0 +1,15 @@ +nl-about = Number lines of files +nl-usage = nl [OPTION]... [FILE]... +nl-after-help = STYLE is one of: + + - a number all lines + - t number only nonempty lines + - n number no lines + - pBRE number only lines that contain a match for the basic regular + expression, BRE + + FORMAT is one of: + + - ln left justified, no leading zeros + - rn right justified, no leading zeros + - rz right justified, leading zeros diff --git a/src/uu/nl/nl.md b/src/uu/nl/nl.md deleted file mode 100644 index bf3952cb2aa..00000000000 --- a/src/uu/nl/nl.md +++ /dev/null @@ -1,23 +0,0 @@ -# nl - -``` -nl [OPTION]... [FILE]... -``` - -Number lines of files - -## After Help - -`STYLE` is one of: - -* `a` number all lines -* `t` number only nonempty lines -* `n` number no lines -* `pBRE` number only lines that contain a match for the basic regular - expression, `BRE` - -`FORMAT` is one of: - -* `ln` left justified, no leading zeros -* `rn` right justified, no leading zeros -* `rz` right justified, leading zeros diff --git a/src/uu/nl/src/nl.rs b/src/uu/nl/src/nl.rs index 6380417e0e8..0b6397dfe3d 100644 --- a/src/uu/nl/src/nl.rs +++ b/src/uu/nl/src/nl.rs @@ -8,14 +8,11 @@ use std::fs::File; use std::io::{BufRead, BufReader, Read, stdin}; use std::path::Path; use uucore::error::{FromIo, UResult, USimpleError, set_exit_code}; -use uucore::{format_usage, help_about, help_section, help_usage, show_error}; +use uucore::locale::get_message; +use uucore::{format_usage, show_error}; mod helper; -const ABOUT: &str = help_about!("nl.md"); -const AFTER_HELP: &str = help_section!("after help", "nl.md"); -const USAGE: &str = help_usage!("nl.md"); - // Settings store options used by nl to produce its output. pub struct Settings { // The variables corresponding to the options -h, -b, and -f. @@ -222,10 +219,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) - .about(ABOUT) + .about(get_message("nl-about")) .version(uucore::crate_version!()) - .override_usage(format_usage(USAGE)) - .after_help(AFTER_HELP) + .override_usage(format_usage(&get_message("nl-usage"))) + .after_help(get_message("nl-after-help")) .infer_long_args(true) .disable_help_flag(true) .arg( diff --git a/src/uu/nohup/locales/en-US.ftl b/src/uu/nohup/locales/en-US.ftl new file mode 100644 index 00000000000..f209a7a90b1 --- /dev/null +++ b/src/uu/nohup/locales/en-US.ftl @@ -0,0 +1,7 @@ +nohup-about = Run COMMAND ignoring hangup signals. +nohup-usage = nohup COMMAND [ARG]... + nohup OPTION +nohup-after-help = If standard input is terminal, it'll be replaced with /dev/null. + If standard output is terminal, it'll be appended to nohup.out instead, + or $HOME/nohup.out, if nohup.out open failed. + If standard error is terminal, it'll be redirected to stdout. diff --git a/src/uu/nohup/nohup.md b/src/uu/nohup/nohup.md deleted file mode 100644 index 65ffaec873d..00000000000 --- a/src/uu/nohup/nohup.md +++ /dev/null @@ -1,15 +0,0 @@ -# nohup - -``` -nohup COMMAND [ARG]... -nohup OPTION -``` - -Run COMMAND ignoring hangup signals. - -## After Help - -If standard input is terminal, it'll be replaced with /dev/null. -If standard output is terminal, it'll be appended to nohup.out instead, -or $HOME/nohup.out, if nohup.out open failed. -If standard error is terminal, it'll be redirected to stdout. diff --git a/src/uu/nohup/src/nohup.rs b/src/uu/nohup/src/nohup.rs index 73003a16416..a855b01f17d 100644 --- a/src/uu/nohup/src/nohup.rs +++ b/src/uu/nohup/src/nohup.rs @@ -17,11 +17,9 @@ use std::path::{Path, PathBuf}; use thiserror::Error; use uucore::display::Quotable; use uucore::error::{UClapError, UError, UResult, set_exit_code}; -use uucore::{format_usage, help_about, help_section, help_usage, show_error}; +use uucore::{format_usage, show_error}; -const ABOUT: &str = help_about!("nohup.md"); -const AFTER_HELP: &str = help_section!("after help", "nohup.md"); -const USAGE: &str = help_usage!("nohup.md"); +use uucore::locale::get_message; static NOHUP_OUT: &str = "nohup.out"; // exit codes that match the GNU implementation static EXIT_CANCELED: i32 = 125; @@ -92,9 +90,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .after_help(AFTER_HELP) - .override_usage(format_usage(USAGE)) + .about(get_message("nohup-about")) + .after_help(get_message("nohup-after-help")) + .override_usage(format_usage(&get_message("nohup-usage"))) .arg( Arg::new(options::CMD) .hide(true) diff --git a/src/uu/nproc/locales/en-US.ftl b/src/uu/nproc/locales/en-US.ftl new file mode 100644 index 00000000000..42522997e61 --- /dev/null +++ b/src/uu/nproc/locales/en-US.ftl @@ -0,0 +1,4 @@ +nproc-about = Print the number of cores available to the current process. + If the OMP_NUM_THREADS or OMP_THREAD_LIMIT environment variables are set, then + they will determine the minimum and maximum returned value respectively. +nproc-usage = nproc [OPTIONS]... diff --git a/src/uu/nproc/nproc.md b/src/uu/nproc/nproc.md deleted file mode 100644 index fee958a92fc..00000000000 --- a/src/uu/nproc/nproc.md +++ /dev/null @@ -1,9 +0,0 @@ -# nproc - -``` -nproc [OPTIONS]... -``` - -Print the number of cores available to the current process. -If the `OMP_NUM_THREADS` or `OMP_THREAD_LIMIT` environment variables are set, then -they will determine the minimum and maximum returned value respectively. diff --git a/src/uu/nproc/src/nproc.rs b/src/uu/nproc/src/nproc.rs index a3a80724dc9..beed928349f 100644 --- a/src/uu/nproc/src/nproc.rs +++ b/src/uu/nproc/src/nproc.rs @@ -9,7 +9,8 @@ use clap::{Arg, ArgAction, Command}; use std::{env, thread}; use uucore::display::Quotable; use uucore::error::{UResult, USimpleError}; -use uucore::{format_usage, help_about, help_usage}; +use uucore::format_usage; +use uucore::locale::get_message; #[cfg(any(target_os = "linux", target_os = "android"))] pub const _SC_NPROCESSORS_CONF: libc::c_int = 83; @@ -23,9 +24,6 @@ pub const _SC_NPROCESSORS_CONF: libc::c_int = 1001; static OPT_ALL: &str = "all"; static OPT_IGNORE: &str = "ignore"; -const ABOUT: &str = help_about!("nproc.md"); -const USAGE: &str = help_usage!("nproc.md"); - #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().try_get_matches_from(args)?; @@ -94,8 +92,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("nproc-about")) + .override_usage(format_usage(&get_message("nproc-usage"))) .infer_long_args(true) .arg( Arg::new(OPT_ALL) diff --git a/src/uu/numfmt/locales/en-US.ftl b/src/uu/numfmt/locales/en-US.ftl new file mode 100644 index 00000000000..588a835a45d --- /dev/null +++ b/src/uu/numfmt/locales/en-US.ftl @@ -0,0 +1,36 @@ +numfmt-about = Convert numbers from/to human-readable strings +numfmt-usage = numfmt [OPTION]... [NUMBER]... +numfmt-after-help = UNIT options: + + - none: no auto-scaling is done; suffixes will trigger an error + - auto: accept optional single/two letter suffix: + + 1K = 1000, 1Ki = 1024, 1M = 1000000, 1Mi = 1048576, + + - si: accept optional single letter suffix: + + 1K = 1000, 1M = 1000000, ... + + - iec: accept optional single letter suffix: + + 1K = 1024, 1M = 1048576, ... + + - iec-i: accept optional two-letter suffix: + + 1Ki = 1024, 1Mi = 1048576, ... + + - FIELDS supports cut(1) style field ranges: + + N N'th field, counted from 1 + N- from N'th field, to end of line + N-M from N'th to M'th field (inclusive) + -M from first to M'th field (inclusive) + - all fields + + Multiple fields/ranges can be separated with commas + + FORMAT must be suitable for printing one floating-point argument %f. + Optional quote (%'f) will enable --grouping (if supported by current locale). + Optional width value (%10f) will pad output. Optional zero (%010f) width + will zero pad the number. Optional negative values (%-10f) will left align. + Optional precision (%.1f) will override the input determined precision. diff --git a/src/uu/numfmt/numfmt.md b/src/uu/numfmt/numfmt.md deleted file mode 100644 index 7c75736c115..00000000000 --- a/src/uu/numfmt/numfmt.md +++ /dev/null @@ -1,46 +0,0 @@ -# numfmt - - - -``` -numfmt [OPTION]... [NUMBER]... -``` - -Convert numbers from/to human-readable strings - -## After Help - -`UNIT` options: - -- `none`: no auto-scaling is done; suffixes will trigger an error -- `auto`: accept optional single/two letter suffix: - - 1K = 1000, 1Ki = 1024, 1M = 1000000, 1Mi = 1048576, - -- `si`: accept optional single letter suffix: - - 1K = 1000, 1M = 1000000, ... - -- `iec`: accept optional single letter suffix: - - 1K = 1024, 1M = 1048576, ... - -- `iec-i`: accept optional two-letter suffix: - - 1Ki = 1024, 1Mi = 1048576, ... - -- `FIELDS` supports `cut(1)` style field ranges: - - N N'th field, counted from 1 - N- from N'th field, to end of line - N-M from N'th to M'th field (inclusive) - -M from first to M'th field (inclusive) - - all fields - -Multiple fields/ranges can be separated with commas - -`FORMAT` must be suitable for printing one floating-point argument `%f`. -Optional quote (`%'f`) will enable --grouping (if supported by current locale). -Optional width value (`%10f`) will pad output. Optional zero (`%010f`) width -will zero pad the number. Optional negative values (`%-10f`) will left align. -Optional precision (`%.1f`) will override the input determined precision. diff --git a/src/uu/numfmt/src/numfmt.rs b/src/uu/numfmt/src/numfmt.rs index cfa7c30d898..de977d03587 100644 --- a/src/uu/numfmt/src/numfmt.rs +++ b/src/uu/numfmt/src/numfmt.rs @@ -15,19 +15,16 @@ use std::str::FromStr; use units::{IEC_BASES, SI_BASES}; use uucore::display::Quotable; use uucore::error::UResult; +use uucore::locale::get_message; use uucore::parser::shortcut_value_parser::ShortcutValueParser; use uucore::ranges::Range; -use uucore::{format_usage, help_about, help_section, help_usage, show, show_error}; +use uucore::{format_usage, show, show_error}; pub mod errors; pub mod format; pub mod options; mod units; -const ABOUT: &str = help_about!("numfmt.md"); -const AFTER_HELP: &str = help_section!("after help", "numfmt.md"); -const USAGE: &str = help_usage!("numfmt.md"); - fn handle_args<'a>(args: impl Iterator, options: &NumfmtOptions) -> UResult<()> { for l in args { format_and_handle_validation(l, options)?; @@ -277,9 +274,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .after_help(AFTER_HELP) - .override_usage(format_usage(USAGE)) + .about(get_message("numfmt-about")) + .after_help(get_message("numfmt-after-help")) + .override_usage(format_usage(&get_message("numfmt-usage"))) .allow_negative_numbers(true) .infer_long_args(true) .arg( diff --git a/src/uu/od/locales/en-US.ftl b/src/uu/od/locales/en-US.ftl new file mode 100644 index 00000000000..d451b30915e --- /dev/null +++ b/src/uu/od/locales/en-US.ftl @@ -0,0 +1,41 @@ +od-about = Dump files in octal and other formats +od-usage = od [OPTION]... [--] [FILENAME]... + od [-abcdDefFhHiIlLoOsxX] [FILENAME] [[+][0x]OFFSET[.][b]] + od --traditional [OPTION]... [FILENAME] [[+][0x]OFFSET[.][b] [[+][0x]LABEL[.][b]]] +od-after-help = Displays data in various human-readable formats. If multiple formats are + specified, the output will contain all formats in the order they appear on the + command line. Each format will be printed on a new line. Only the line + containing the first format will be prefixed with the offset. + + If no filename is specified, or it is "-", stdin will be used. After a "--", no + more options will be recognized. This allows for filenames starting with a "-". + + If a filename is a valid number which can be used as an offset in the second + form, you can force it to be recognized as a filename if you include an option + like "-j0", which is only valid in the first form. + + RADIX is one of o,d,x,n for octal, decimal, hexadecimal or none. + + BYTES is decimal by default, octal if prefixed with a "0", or hexadecimal if + prefixed with "0x". The suffixes b, KB, K, MB, M, GB, G, will multiply the + number with 512, 1000, 1024, 1000^2, 1024^2, 1000^3, 1024^3, 1000^2, 1024^2. + + OFFSET and LABEL are octal by default, hexadecimal if prefixed with "0x" or + decimal if a "." suffix is added. The "b" suffix will multiply with 512. + + TYPE contains one or more format specifications consisting of: + a for printable 7-bits ASCII + c for utf-8 characters or octal for undefined characters + d[SIZE] for signed decimal + f[SIZE] for floating point + o[SIZE] for octal + u[SIZE] for unsigned decimal + x[SIZE] for hexadecimal + SIZE is the number of bytes which can be the number 1, 2, 4, 8 or 16, + or C, I, S, L for 1, 2, 4, 8 bytes for integer types, + or F, D, L for 4, 8, 16 bytes for floating point. + Any type specification can have a "z" suffix, which will add a ASCII dump at + the end of the line. + + If an error occurred, a diagnostic message will be printed to stderr, and the + exit code will be non-zero. diff --git a/src/uu/od/od.md b/src/uu/od/od.md deleted file mode 100644 index 5cd9ac3b5e7..00000000000 --- a/src/uu/od/od.md +++ /dev/null @@ -1,49 +0,0 @@ -# od - -``` -od [OPTION]... [--] [FILENAME]... -od [-abcdDefFhHiIlLoOsxX] [FILENAME] [[+][0x]OFFSET[.][b]] -od --traditional [OPTION]... [FILENAME] [[+][0x]OFFSET[.][b] [[+][0x]LABEL[.][b]]] -``` - -Dump files in octal and other formats - -## After Help - -Displays data in various human-readable formats. If multiple formats are -specified, the output will contain all formats in the order they appear on the -command line. Each format will be printed on a new line. Only the line -containing the first format will be prefixed with the offset. - -If no filename is specified, or it is "-", stdin will be used. After a "--", no -more options will be recognized. This allows for filenames starting with a "-". - -If a filename is a valid number which can be used as an offset in the second -form, you can force it to be recognized as a filename if you include an option -like "-j0", which is only valid in the first form. - -RADIX is one of o,d,x,n for octal, decimal, hexadecimal or none. - -BYTES is decimal by default, octal if prefixed with a "0", or hexadecimal if -prefixed with "0x". The suffixes b, KB, K, MB, M, GB, G, will multiply the -number with 512, 1000, 1024, 1000^2, 1024^2, 1000^3, 1024^3, 1000^2, 1024^2. - -OFFSET and LABEL are octal by default, hexadecimal if prefixed with "0x" or -decimal if a "." suffix is added. The "b" suffix will multiply with 512. - -TYPE contains one or more format specifications consisting of: - a for printable 7-bits ASCII - c for utf-8 characters or octal for undefined characters - d[SIZE] for signed decimal - f[SIZE] for floating point - o[SIZE] for octal - u[SIZE] for unsigned decimal - x[SIZE] for hexadecimal -SIZE is the number of bytes which can be the number 1, 2, 4, 8 or 16, - or C, I, S, L for 1, 2, 4, 8 bytes for integer types, - or F, D, L for 4, 8, 16 bytes for floating point. -Any type specification can have a "z" suffix, which will add a ASCII dump at - the end of the line. - -If an error occurred, a diagnostic message will be printed to stderr, and the -exit code will be non-zero. diff --git a/src/uu/od/src/od.rs b/src/uu/od/src/od.rs index 652a0ce3f51..a84adece255 100644 --- a/src/uu/od/src/od.rs +++ b/src/uu/od/src/od.rs @@ -44,16 +44,13 @@ use clap::ArgAction; use clap::{Arg, ArgMatches, Command, parser::ValueSource}; use uucore::display::Quotable; use uucore::error::{UResult, USimpleError}; +use uucore::locale::get_message; use uucore::parser::parse_size::ParseSizeError; use uucore::parser::shortcut_value_parser::ShortcutValueParser; -use uucore::{format_usage, help_about, help_section, help_usage, show_error, show_warning}; +use uucore::{format_usage, show_error, show_warning}; const PEEK_BUFFER_SIZE: usize = 4; // utf-8 can be 4 bytes -const ABOUT: &str = help_about!("od.md"); -const USAGE: &str = help_usage!("od.md"); -const AFTER_HELP: &str = help_section!("after help", "od.md"); - pub(crate) mod options { pub const HELP: &str = "help"; pub const ADDRESS_RADIX: &str = "address-radix"; @@ -253,9 +250,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) - .after_help(AFTER_HELP) + .about(get_message("od-about")) + .override_usage(format_usage(&get_message("od-usage"))) + .after_help(get_message("od-after-help")) .trailing_var_arg(true) .dont_delimit_trailing_values(true) .infer_long_args(true) diff --git a/src/uu/paste/locales/en-US.ftl b/src/uu/paste/locales/en-US.ftl new file mode 100644 index 00000000000..15c39b0a62f --- /dev/null +++ b/src/uu/paste/locales/en-US.ftl @@ -0,0 +1,3 @@ +paste-about = Write lines consisting of the sequentially corresponding lines from each + FILE, separated by TABs, to standard output. +paste-usage = paste [OPTIONS] [FILE]... diff --git a/src/uu/paste/paste.md b/src/uu/paste/paste.md deleted file mode 100644 index 74160588894..00000000000 --- a/src/uu/paste/paste.md +++ /dev/null @@ -1,8 +0,0 @@ -# paste - -``` -paste [OPTIONS] [FILE]... -``` - -Write lines consisting of the sequentially corresponding lines from each -`FILE`, separated by `TAB`s, to standard output. diff --git a/src/uu/paste/src/paste.rs b/src/uu/paste/src/paste.rs index 98679b74635..9bb94294fc0 100644 --- a/src/uu/paste/src/paste.rs +++ b/src/uu/paste/src/paste.rs @@ -11,11 +11,10 @@ use std::iter::Cycle; use std::rc::Rc; use std::slice::Iter; use uucore::error::{UResult, USimpleError}; +use uucore::format_usage; use uucore::line_ending::LineEnding; -use uucore::{format_usage, help_about, help_usage}; -const ABOUT: &str = help_about!("paste.md"); -const USAGE: &str = help_usage!("paste.md"); +use uucore::locale::get_message; mod options { pub const DELIMITER: &str = "delimiters"; @@ -43,8 +42,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("paste-about")) + .override_usage(format_usage(&get_message("paste-usage"))) .infer_long_args(true) .arg( Arg::new(options::SERIAL) diff --git a/src/uu/pathchk/locales/en-US.ftl b/src/uu/pathchk/locales/en-US.ftl new file mode 100644 index 00000000000..c75b172622d --- /dev/null +++ b/src/uu/pathchk/locales/en-US.ftl @@ -0,0 +1,2 @@ +pathchk-about = Check whether file names are valid or portable +pathchk-usage = pathchk [OPTION]... NAME... diff --git a/src/uu/pathchk/pathchk.md b/src/uu/pathchk/pathchk.md deleted file mode 100644 index 2e5eca95953..00000000000 --- a/src/uu/pathchk/pathchk.md +++ /dev/null @@ -1,7 +0,0 @@ -# pathchk - -``` -pathchk [OPTION]... NAME... -``` - -Check whether file names are valid or portable diff --git a/src/uu/pathchk/src/pathchk.rs b/src/uu/pathchk/src/pathchk.rs index 183d67a0beb..cdd6418c4ad 100644 --- a/src/uu/pathchk/src/pathchk.rs +++ b/src/uu/pathchk/src/pathchk.rs @@ -10,7 +10,8 @@ use std::fs; use std::io::{ErrorKind, Write}; use uucore::display::Quotable; use uucore::error::{UResult, UUsageError, set_exit_code}; -use uucore::{format_usage, help_about, help_usage}; +use uucore::format_usage; +use uucore::locale::get_message; // operating mode enum Mode { @@ -20,9 +21,6 @@ enum Mode { Both, // a combination of `Basic` and `Extra` } -const ABOUT: &str = help_about!("pathchk.md"); -const USAGE: &str = help_usage!("pathchk.md"); - mod options { pub const POSIX: &str = "posix"; pub const POSIX_SPECIAL: &str = "posix-special"; @@ -80,8 +78,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("pathchk-about")) + .override_usage(format_usage(&get_message("pathchk-usage"))) .infer_long_args(true) .arg( Arg::new(options::POSIX) diff --git a/src/uu/pinky/locales/en-US.ftl b/src/uu/pinky/locales/en-US.ftl new file mode 100644 index 00000000000..d3b62d123c2 --- /dev/null +++ b/src/uu/pinky/locales/en-US.ftl @@ -0,0 +1,6 @@ +pinky-about = Displays brief user information on Unix-based systems +pinky-usage = pinky [OPTION]... [USER]... +pinky-about-musl-warning = Warning: When built with musl libc, the `pinky` utility may show incomplete + or missing user information due to musl's stub implementation of `utmpx` + functions. This limitation affects the ability to retrieve accurate details + about logged-in users. diff --git a/src/uu/pinky/pinky.md b/src/uu/pinky/pinky.md deleted file mode 100644 index 965ae4cd090..00000000000 --- a/src/uu/pinky/pinky.md +++ /dev/null @@ -1,7 +0,0 @@ -# pinky - -``` -pinky [OPTION]... [USER]... -``` - -Displays brief user information on Unix-based systems diff --git a/src/uu/pinky/src/pinky.rs b/src/uu/pinky/src/pinky.rs index 8246f86556e..3f0f13d975e 100644 --- a/src/uu/pinky/src/pinky.rs +++ b/src/uu/pinky/src/pinky.rs @@ -6,24 +6,11 @@ // spell-checker:ignore (ToDO) BUFSIZE gecos fullname, mesg iobuf use clap::{Arg, ArgAction, Command}; -use uucore::{format_usage, help_about, help_usage}; +use uucore::format_usage; +use uucore::locale::get_message; mod platform; -#[cfg(target_env = "musl")] -const ABOUT: &str = concat!( - help_about!("pinky.md"), - "\n\nWarning: When built with musl libc, the `pinky` utility may show incomplete \n", - "or missing user information due to musl's stub implementation of `utmpx` \n", - "functions. This limitation affects the ability to retrieve accurate details \n", - "about logged-in users." -); - -#[cfg(not(target_env = "musl"))] -const ABOUT: &str = help_about!("pinky.md"); - -const USAGE: &str = help_usage!("pinky.md"); - mod options { pub const LONG_FORMAT: &str = "long_format"; pub const OMIT_HOME_DIR: &str = "omit_home_dir"; @@ -42,10 +29,15 @@ mod options { use platform::uumain; pub fn uu_app() -> Command { + #[cfg(not(target_env = "musl"))] + let about = get_message("pinky-about"); + #[cfg(target_env = "musl")] + let about = get_message("pinky-about") + &get_message("pinky-about-musl-warning"); + Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(about) + .override_usage(format_usage(&get_message("pinky-usage"))) .infer_long_args(true) .disable_help_flag(true) .arg( diff --git a/src/uu/pr/locales/en-US.ftl b/src/uu/pr/locales/en-US.ftl new file mode 100644 index 00000000000..cd11a164dbd --- /dev/null +++ b/src/uu/pr/locales/en-US.ftl @@ -0,0 +1,19 @@ +pr-about = Write content of given file or standard input to standard output with pagination filter +pr-usage = pr [OPTION]... [FILE]... +pr-after-help = +PAGE Begin output at page number page of the formatted input. + -COLUMN Produce multi-column output. See --column + + The pr utility is a printing and pagination filter for text files. + When multiple input files are specified, each is read, formatted, and written to standard output. + By default, the input is separated into 66-line pages, each with + + - A 5-line header with the page number, date, time, and the pathname of the file. + - A 5-line trailer consisting of blank lines. + + If standard output is associated with a terminal, diagnostic messages are suppressed until the pr + utility has completed processing. + + When multiple column output is specified, text columns are of equal width. + By default, text columns are separated by at least one . + Input lines that do not fit into a text column are truncated. + Lines are not truncated under single column output. diff --git a/src/uu/pr/pr.md b/src/uu/pr/pr.md deleted file mode 100644 index cd2f552f3de..00000000000 --- a/src/uu/pr/pr.md +++ /dev/null @@ -1,27 +0,0 @@ -# pr - -``` -pr [OPTION]... [FILE]... -``` - -Write content of given file or standard input to standard output with pagination filter - -## After help - -`+PAGE` Begin output at page number page of the formatted input. -`-COLUMN` Produce multi-column output. See `--column` - -The pr utility is a printing and pagination filter for text files. -When multiple input files are specified, each is read, formatted, and written to standard output. -By default, the input is separated into 66-line pages, each with - -* A 5-line header with the page number, date, time, and the pathname of the file. -* A 5-line trailer consisting of blank lines. - -If standard output is associated with a terminal, diagnostic messages are suppressed until the pr -utility has completed processing. - -When multiple column output is specified, text columns are of equal width. -By default, text columns are separated by at least one ``. -Input lines that do not fit into a text column are truncated. -Lines are not truncated under single column output. diff --git a/src/uu/pr/src/pr.rs b/src/uu/pr/src/pr.rs index e24f9cf18e5..4ca97e784a1 100644 --- a/src/uu/pr/src/pr.rs +++ b/src/uu/pr/src/pr.rs @@ -18,11 +18,9 @@ use thiserror::Error; use uucore::display::Quotable; use uucore::error::UResult; -use uucore::{format_usage, help_about, help_section, help_usage}; +use uucore::format_usage; -const ABOUT: &str = help_about!("pr.md"); -const USAGE: &str = help_usage!("pr.md"); -const AFTER_HELP: &str = help_section!("after help", "pr.md"); +use uucore::locale::get_message; const TAB: char = '\t'; const LINES_PER_PAGE: usize = 66; const LINES_PER_PAGE_FOR_FORM_FEED: usize = 63; @@ -153,9 +151,9 @@ enum PrError { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .after_help(AFTER_HELP) - .override_usage(format_usage(USAGE)) + .about(get_message("pr-about")) + .after_help(get_message("pr-after-help")) + .override_usage(format_usage(&get_message("pr-usage"))) .infer_long_args(true) .args_override_self(true) .disable_help_flag(true) diff --git a/src/uu/printenv/locales/en-US.ftl b/src/uu/printenv/locales/en-US.ftl new file mode 100644 index 00000000000..b60962e1a6e --- /dev/null +++ b/src/uu/printenv/locales/en-US.ftl @@ -0,0 +1,2 @@ +printenv-about = Display the values of the specified environment VARIABLE(s), or (with no VARIABLE) display name and value pairs for them all. +printenv-usage = printenv [OPTION]... [VARIABLE]... diff --git a/src/uu/printenv/printenv.md b/src/uu/printenv/printenv.md deleted file mode 100644 index f67d46a23dd..00000000000 --- a/src/uu/printenv/printenv.md +++ /dev/null @@ -1,7 +0,0 @@ -# printenv - -``` -printenv [OPTION]... [VARIABLE]... -``` - -Display the values of the specified environment VARIABLE(s), or (with no VARIABLE) display name and value pairs for them all. diff --git a/src/uu/printenv/src/printenv.rs b/src/uu/printenv/src/printenv.rs index 4584a09b858..15e39a678e9 100644 --- a/src/uu/printenv/src/printenv.rs +++ b/src/uu/printenv/src/printenv.rs @@ -5,10 +5,9 @@ use clap::{Arg, ArgAction, Command}; use std::env; -use uucore::{error::UResult, format_usage, help_about, help_usage}; +use uucore::{error::UResult, format_usage}; -const ABOUT: &str = help_about!("printenv.md"); -const USAGE: &str = help_usage!("printenv.md"); +use uucore::locale::get_message; static OPT_NULL: &str = "null"; @@ -56,8 +55,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("printenv-about")) + .override_usage(format_usage(&get_message("printenv-usage"))) .infer_long_args(true) .arg( Arg::new(OPT_NULL) diff --git a/src/uu/printf/locales/en-US.ftl b/src/uu/printf/locales/en-US.ftl new file mode 100644 index 00000000000..6f58a07758a --- /dev/null +++ b/src/uu/printf/locales/en-US.ftl @@ -0,0 +1,249 @@ +printf-about = Print output based off of the format string and proceeding arguments. +printf-usage = printf FORMAT [ARGUMENT]... + printf OPTION +printf-after-help = basic anonymous string templating: + + prints format string at least once, repeating as long as there are remaining arguments + output prints escaped literals in the format string as character literals + output replaces anonymous fields with the next unused argument, formatted according to the field. + + Prints the , replacing escaped character sequences with character literals + and substitution field sequences with passed arguments + + literally, with the exception of the below + escaped character sequences, and the substitution sequences described further down. + + ### ESCAPE SEQUENCES + + The following escape sequences, organized here in alphabetical order, + will print the corresponding character literal: + + - \" double quote + + - \\ backslash + + - \\a alert (BEL) + + - \\b backspace + + - \\c End-of-Input + + - \\e escape + + - \\f form feed + + - \\n new line + + - \\r carriage return + + - \\t horizontal tab + + - \\v vertical tab + + - \\NNN byte with value expressed in octal value NNN (1 to 3 digits) + values greater than 256 will be treated + + - \\xHH byte with value expressed in hexadecimal value NN (1 to 2 digits) + + - \\uHHHH Unicode (IEC 10646) character with value expressed in hexadecimal value HHHH (4 digits) + + - \\uHHHH Unicode character with value expressed in hexadecimal value HHHH (8 digits) + + - %% a single % + + ### SUBSTITUTIONS + + #### SUBSTITUTION QUICK REFERENCE + + Fields + + - %s: string + - %b: string parsed for literals second parameter is max length + + - %c: char no second parameter + + - %i or %d: 64-bit integer + - %u: 64 bit unsigned integer + - %x or %X: 64-bit unsigned integer as hex + - %o: 64-bit unsigned integer as octal + second parameter is min-width, integer + output below that width is padded with leading zeroes + + - %q: ARGUMENT is printed in a format that can be reused as shell input, escaping non-printable + characters with the proposed POSIX $'' syntax. + + - %f or %F: decimal floating point value + - %e or %E: scientific notation floating point value + - %g or %G: shorter of specially interpreted decimal or SciNote floating point value. + second parameter is + -max places after decimal point for floating point output + -max number of significant digits for scientific notation output + + parameterizing fields + + examples: + + printf '%4.3i' 7 + + It has a first parameter of 4 and a second parameter of 3 and will result in ' 007' + + printf '%.1s' abcde + + It has no first parameter and a second parameter of 1 and will result in 'a' + + printf '%4c' q + + It has a first parameter of 4 and no second parameter and will result in ' q' + + The first parameter of a field is the minimum width to pad the output to + if the output is less than this absolute value of this width, + it will be padded with leading spaces, or, if the argument is negative, + with trailing spaces. the default is zero. + + The second parameter of a field is particular to the output field type. + defaults can be found in the full substitution help below + + special prefixes to numeric arguments + + - 0: (e.g. 010) interpret argument as octal (integer output fields only) + - 0x: (e.g. 0xABC) interpret argument as hex (numeric output fields only) + - \': (e.g. \'a) interpret argument as a character constant + + #### HOW TO USE SUBSTITUTIONS + + Substitutions are used to pass additional argument(s) into the FORMAT string, to be formatted a + particular way. E.g. + + printf 'the letter %X comes before the letter %X' 10 11 + + will print + + the letter A comes before the letter B + + because the substitution field %X means + 'take an integer argument and write it as a hexadecimal number' + + Passing more arguments than are in the format string will cause the format string to be + repeated for the remaining substitutions + + printf 'it is %i F in %s \n' 22 Portland 25 Boston 27 New York + + will print + + it is 22 F in Portland + it is 25 F in Boston + it is 27 F in Boston + + If a format string is printed but there are less arguments remaining + than there are substitution fields, substitution fields without + an argument will default to empty strings, or for numeric fields + the value 0 + + #### AVAILABLE SUBSTITUTIONS + + This program, like GNU coreutils printf, + interprets a modified subset of the POSIX C printf spec, + a quick reference to substitutions is below. + + #### STRING SUBSTITUTIONS + + All string fields have a 'max width' parameter + %.3s means 'print no more than three characters of the original input' + + - %s: string + + - %b: escaped string - the string will be checked for any escaped literals from + the escaped literal list above, and translate them to literal characters. + e.g. \\n will be transformed into a newline character. + One special rule about %b mode is that octal literals are interpreted differently + In arguments passed by %b, pass octal-interpreted literals must be in the form of \\0NNN + instead of \\NNN. (Although, for legacy reasons, octal literals in the form of \\NNN will + still be interpreted and not throw a warning, you will have problems if you use this for a + literal whose code begins with zero, as it will be viewed as in \\0NNN form.) + + - %q: escaped string - the string in a format that can be reused as input by most shells. + Non-printable characters are escaped with the POSIX proposed ‘$''’ syntax, + and shell meta-characters are quoted appropriately. + This is an equivalent format to ls --quoting=shell-escape output. + + #### CHAR SUBSTITUTIONS + + The character field does not have a secondary parameter. + + - %c: a single character + + #### INTEGER SUBSTITUTIONS + + All integer fields have a 'pad with zero' parameter + %.4i means an integer which if it is less than 4 digits in length, + is padded with leading zeros until it is 4 digits in length. + + - %d or %i: 64-bit integer + + - %u: 64-bit unsigned integer + + - %x or %X: 64-bit unsigned integer printed in Hexadecimal (base 16) + %X instead of %x means to use uppercase letters for 'a' through 'f' + + - %o: 64-bit unsigned integer printed in octal (base 8) + + #### FLOATING POINT SUBSTITUTIONS + + All floating point fields have a 'max decimal places / max significant digits' parameter + %.10f means a decimal floating point with 7 decimal places past 0 + %.10e means a scientific notation number with 10 significant digits + %.10g means the same behavior for decimal and Sci. Note, respectively, and provides the shortest + of each's output. + + Like with GNU coreutils, the value after the decimal point is these outputs is parsed as a + double first before being rendered to text. For both implementations do not expect meaningful + precision past the 18th decimal place. When using a number of decimal places that is 18 or + higher, you can expect variation in output between GNU coreutils printf and this printf at the + 18th decimal place of +/- 1 + + - %f: floating point value presented in decimal, truncated and displayed to 6 decimal places by + default. There is not past-double behavior parity with Coreutils printf, values are not + estimated or adjusted beyond input values. + + - %e or %E: floating point value presented in scientific notation + 7 significant digits by default + %E means use to use uppercase E for the mantissa. + + - %g or %G: floating point value presented in the shortest of decimal and scientific notation + behaves differently from %f and %E, please see posix printf spec for full details, + some examples of different behavior: + Sci Note has 6 significant digits by default + Trailing zeroes are removed + Instead of being truncated, digit after last is rounded + + Like other behavior in this utility, the design choices of floating point + behavior in this utility is selected to reproduce in exact + the behavior of GNU coreutils' printf from an inputs and outputs standpoint. + + ### USING PARAMETERS + + Most substitution fields can be parameterized using up to 2 numbers that can + be passed to the field, between the % sign and the field letter. + + The 1st parameter always indicates the minimum width of output, it is useful for creating + columnar output. Any output that would be less than this minimum width is padded with + leading spaces + The 2nd parameter is proceeded by a dot. + You do not have to use parameters + + ### SPECIAL FORMS OF INPUT + + For numeric input, the following additional forms of input are accepted besides decimal: + + Octal (only with integer): if the argument begins with a 0 the proceeding characters + will be interpreted as octal (base 8) for integer fields + + Hexadecimal: if the argument begins with 0x the proceeding characters will be interpreted + will be interpreted as hex (base 16) for any numeric fields + for float fields, hexadecimal input results in a precision + limit (in converting input past the decimal point) of 10^-15 + + Character Constant: if the argument begins with a single quote character, the first byte + of the next character will be interpreted as an 8-bit unsigned integer. If there are + additional bytes, they will throw an error (unless the environment variable POSIXLY_CORRECT + is set) diff --git a/src/uu/printf/printf.md b/src/uu/printf/printf.md deleted file mode 100644 index 791cc8be1f2..00000000000 --- a/src/uu/printf/printf.md +++ /dev/null @@ -1,273 +0,0 @@ - - -# printf - -``` -printf FORMAT [ARGUMENT]... -printf OPTION -``` - -Print output based off of the format string and proceeding arguments. - -## After Help - -basic anonymous string templating: - -prints format string at least once, repeating as long as there are remaining arguments -output prints escaped literals in the format string as character literals -output replaces anonymous fields with the next unused argument, formatted according to the field. - -Prints the `,` replacing escaped character sequences with character literals -and substitution field sequences with passed arguments - -literally, with the exception of the below -escaped character sequences, and the substitution sequences described further down. - -### ESCAPE SEQUENCES - -The following escape sequences, organized here in alphabetical order, -will print the corresponding character literal: - -* `\"` double quote - -* `\\\\` backslash - -* `\\a` alert (BEL) - -* `\\b` backspace - -* `\\c` End-of-Input - -* `\\e` escape - -* `\\f` form feed - -* `\\n` new line - -* `\\r` carriage return - -* `\\t` horizontal tab - -* `\\v` vertical tab - -* `\\NNN` byte with value expressed in octal value NNN (1 to 3 digits) - values greater than 256 will be treated - -* `\\xHH` byte with value expressed in hexadecimal value NN (1 to 2 digits) - -* `\\uHHHH` Unicode (IEC 10646) character with value expressed in hexadecimal value HHHH (4 digits) - -* `\\uHHHH` Unicode character with value expressed in hexadecimal value HHHH (8 digits) - -* `%%` a single % - -### SUBSTITUTIONS - -#### SUBSTITUTION QUICK REFERENCE - -Fields - -* `%s`: string -* `%b`: string parsed for literals second parameter is max length - -* `%c`: char no second parameter - -* `%i` or `%d`: 64-bit integer -* `%u`: 64 bit unsigned integer -* `%x` or `%X`: 64-bit unsigned integer as hex -* `%o`: 64-bit unsigned integer as octal - second parameter is min-width, integer - output below that width is padded with leading zeroes - -* `%q`: ARGUMENT is printed in a format that can be reused as shell input, escaping non-printable - characters with the proposed POSIX $'' syntax. - -* `%f` or `%F`: decimal floating point value -* `%e` or `%E`: scientific notation floating point value -* `%g` or `%G`: shorter of specially interpreted decimal or SciNote floating point value. - second parameter is - `-max` places after decimal point for floating point output - `-max` number of significant digits for scientific notation output - -parameterizing fields - -examples: - -``` -printf '%4.3i' 7 -``` - -It has a first parameter of 4 and a second parameter of 3 and will result in ' 007' - -``` -printf '%.1s' abcde -``` - -It has no first parameter and a second parameter of 1 and will result in 'a' - -``` -printf '%4c' q -``` - -It has a first parameter of 4 and no second parameter and will result in ' q' - -The first parameter of a field is the minimum width to pad the output to -if the output is less than this absolute value of this width, -it will be padded with leading spaces, or, if the argument is negative, -with trailing spaces. the default is zero. - -The second parameter of a field is particular to the output field type. -defaults can be found in the full substitution help below - -special prefixes to numeric arguments - -* `0`: (e.g. 010) interpret argument as octal (integer output fields only) -* `0x`: (e.g. 0xABC) interpret argument as hex (numeric output fields only) -* `\'`: (e.g. \'a) interpret argument as a character constant - -#### HOW TO USE SUBSTITUTIONS - -Substitutions are used to pass additional argument(s) into the FORMAT string, to be formatted a -particular way. E.g. - -``` -printf 'the letter %X comes before the letter %X' 10 11 -``` - -will print - -``` -the letter A comes before the letter B -``` - -because the substitution field `%X` means -'take an integer argument and write it as a hexadecimal number' - -Passing more arguments than are in the format string will cause the format string to be -repeated for the remaining substitutions - -``` -printf 'it is %i F in %s \n' 22 Portland 25 Boston 27 New York -``` - -will print - -``` -it is 22 F in Portland -it is 25 F in Boston -it is 27 F in Boston -``` - -If a format string is printed but there are less arguments remaining -than there are substitution fields, substitution fields without -an argument will default to empty strings, or for numeric fields -the value 0 - -#### AVAILABLE SUBSTITUTIONS - -This program, like GNU coreutils printf, -interprets a modified subset of the POSIX C printf spec, -a quick reference to substitutions is below. - -#### STRING SUBSTITUTIONS - -All string fields have a 'max width' parameter -`%.3s` means 'print no more than three characters of the original input' - -* `%s`: string - -* `%b`: escaped string - the string will be checked for any escaped literals from - the escaped literal list above, and translate them to literal characters. - e.g. `\\n` will be transformed into a newline character. - One special rule about `%b` mode is that octal literals are interpreted differently - In arguments passed by `%b`, pass octal-interpreted literals must be in the form of `\\0NNN` - instead of `\\NNN`. (Although, for legacy reasons, octal literals in the form of `\\NNN` will - still be interpreted and not throw a warning, you will have problems if you use this for a - literal whose code begins with zero, as it will be viewed as in `\\0NNN` form.) - -* `%q`: escaped string - the string in a format that can be reused as input by most shells. - Non-printable characters are escaped with the POSIX proposed ‘$''’ syntax, - and shell meta-characters are quoted appropriately. - This is an equivalent format to ls --quoting=shell-escape output. - -#### CHAR SUBSTITUTIONS - -The character field does not have a secondary parameter. - -* `%c`: a single character - -#### INTEGER SUBSTITUTIONS - -All integer fields have a 'pad with zero' parameter -`%.4i` means an integer which if it is less than 4 digits in length, -is padded with leading zeros until it is 4 digits in length. - -* `%d` or `%i`: 64-bit integer - -* `%u`: 64-bit unsigned integer - -* `%x` or `%X`: 64-bit unsigned integer printed in Hexadecimal (base 16) - `%X` instead of `%x` means to use uppercase letters for 'a' through 'f' - -* `%o`: 64-bit unsigned integer printed in octal (base 8) - -#### FLOATING POINT SUBSTITUTIONS - -All floating point fields have a 'max decimal places / max significant digits' parameter -`%.10f` means a decimal floating point with 7 decimal places past 0 -`%.10e` means a scientific notation number with 10 significant digits -`%.10g` means the same behavior for decimal and Sci. Note, respectively, and provides the shortest -of each's output. - -Like with GNU coreutils, the value after the decimal point is these outputs is parsed as a -double first before being rendered to text. For both implementations do not expect meaningful -precision past the 18th decimal place. When using a number of decimal places that is 18 or -higher, you can expect variation in output between GNU coreutils printf and this printf at the -18th decimal place of +/- 1 - -* `%f`: floating point value presented in decimal, truncated and displayed to 6 decimal places by - default. There is not past-double behavior parity with Coreutils printf, values are not - estimated or adjusted beyond input values. - -* `%e` or `%E`: floating point value presented in scientific notation - 7 significant digits by default - `%E` means use to use uppercase E for the mantissa. - -* `%g` or `%G`: floating point value presented in the shortest of decimal and scientific notation - behaves differently from `%f` and `%E`, please see posix printf spec for full details, - some examples of different behavior: - Sci Note has 6 significant digits by default - Trailing zeroes are removed - Instead of being truncated, digit after last is rounded - -Like other behavior in this utility, the design choices of floating point -behavior in this utility is selected to reproduce in exact -the behavior of GNU coreutils' printf from an inputs and outputs standpoint. - -### USING PARAMETERS - -Most substitution fields can be parameterized using up to 2 numbers that can -be passed to the field, between the % sign and the field letter. - -The 1st parameter always indicates the minimum width of output, it is useful for creating -columnar output. Any output that would be less than this minimum width is padded with -leading spaces -The 2nd parameter is proceeded by a dot. -You do not have to use parameters - -### SPECIAL FORMS OF INPUT - -For numeric input, the following additional forms of input are accepted besides decimal: - -Octal (only with integer): if the argument begins with a 0 the proceeding characters -will be interpreted as octal (base 8) for integer fields - -Hexadecimal: if the argument begins with 0x the proceeding characters will be interpreted -will be interpreted as hex (base 16) for any numeric fields -for float fields, hexadecimal input results in a precision -limit (in converting input past the decimal point) of 10^-15 - -Character Constant: if the argument begins with a single quote character, the first byte -of the next character will be interpreted as an 8-bit unsigned integer. If there are -additional bytes, they will throw an error (unless the environment variable POSIXLY_CORRECT -is set) diff --git a/src/uu/printf/src/printf.rs b/src/uu/printf/src/printf.rs index 887ad4107a7..a28fc6aaea2 100644 --- a/src/uu/printf/src/printf.rs +++ b/src/uu/printf/src/printf.rs @@ -7,13 +7,11 @@ use std::io::stdout; use std::ops::ControlFlow; use uucore::error::{UResult, UUsageError}; use uucore::format::{FormatArgument, FormatArguments, FormatItem, parse_spec_and_escape}; -use uucore::{format_usage, help_about, help_section, help_usage, os_str_as_bytes, show_warning}; +use uucore::locale::get_message; +use uucore::{format_usage, os_str_as_bytes, show_warning}; const VERSION: &str = "version"; const HELP: &str = "help"; -const USAGE: &str = help_usage!("printf.md"); -const ABOUT: &str = help_about!("printf.md"); -const AFTER_HELP: &str = help_section!("after help", "printf.md"); mod options { pub const FORMAT: &str = "FORMAT"; @@ -81,9 +79,9 @@ pub fn uu_app() -> Command { Command::new(uucore::util_name()) .allow_hyphen_values(true) .version(uucore::crate_version!()) - .about(ABOUT) - .after_help(AFTER_HELP) - .override_usage(format_usage(USAGE)) + .about(get_message("printf-about")) + .after_help(get_message("printf-after-help")) + .override_usage(format_usage(&get_message("printf-usage"))) .disable_help_flag(true) .disable_version_flag(true) .arg( diff --git a/src/uu/ptx/locales/en-US.ftl b/src/uu/ptx/locales/en-US.ftl new file mode 100644 index 00000000000..87bc95530d1 --- /dev/null +++ b/src/uu/ptx/locales/en-US.ftl @@ -0,0 +1,6 @@ +ptx-about = Produce a permuted index of file contents + Output a permuted index, including context, of the words in the input files. + Mandatory arguments to long options are mandatory for short options too. + With no FILE, or when FILE is -, read standard input. Default is '-F /'. +ptx-usage = ptx [OPTION]... [INPUT]... + ptx -G [OPTION]... [INPUT [OUTPUT]] diff --git a/src/uu/ptx/ptx.md b/src/uu/ptx/ptx.md deleted file mode 100644 index b8717776cb6..00000000000 --- a/src/uu/ptx/ptx.md +++ /dev/null @@ -1,11 +0,0 @@ -# ptx - -``` -ptx [OPTION]... [INPUT]... -ptx -G [OPTION]... [INPUT [OUTPUT]] -``` - -Produce a permuted index of file contents -Output a permuted index, including context, of the words in the input files. -Mandatory arguments to long options are mandatory for short options too. -With no FILE, or when FILE is -, read standard input. Default is '-F /'. \ No newline at end of file diff --git a/src/uu/ptx/src/ptx.rs b/src/uu/ptx/src/ptx.rs index bb5b2928283..856183a5499 100644 --- a/src/uu/ptx/src/ptx.rs +++ b/src/uu/ptx/src/ptx.rs @@ -17,10 +17,9 @@ use regex::Regex; use thiserror::Error; use uucore::display::Quotable; use uucore::error::{FromIo, UError, UResult, UUsageError}; -use uucore::{format_usage, help_about, help_usage}; +use uucore::format_usage; -const USAGE: &str = help_usage!("ptx.md"); -const ABOUT: &str = help_about!("ptx.md"); +use uucore::locale::get_message; #[derive(Debug)] enum OutFormat { @@ -765,9 +764,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) - .about(ABOUT) + .about(get_message("ptx-about")) .version(uucore::crate_version!()) - .override_usage(format_usage(USAGE)) + .override_usage(format_usage(&get_message("ptx-usage"))) .infer_long_args(true) .arg( Arg::new(options::FILE) diff --git a/src/uu/pwd/locales/en-US.ftl b/src/uu/pwd/locales/en-US.ftl new file mode 100644 index 00000000000..52a4a2283c0 --- /dev/null +++ b/src/uu/pwd/locales/en-US.ftl @@ -0,0 +1,2 @@ +pwd-about = Display the full filename of the current working directory. +pwd-usage = pwd [OPTION]... [FILE]... diff --git a/src/uu/pwd/pwd.md b/src/uu/pwd/pwd.md deleted file mode 100644 index 89ccee2db8e..00000000000 --- a/src/uu/pwd/pwd.md +++ /dev/null @@ -1,7 +0,0 @@ -# pwd - -``` -pwd [OPTION]... [FILE]... -``` - -Display the full filename of the current working directory. diff --git a/src/uu/pwd/src/pwd.rs b/src/uu/pwd/src/pwd.rs index b924af241fe..f1f398d9b0c 100644 --- a/src/uu/pwd/src/pwd.rs +++ b/src/uu/pwd/src/pwd.rs @@ -8,13 +8,12 @@ use clap::{Arg, Command}; use std::env; use std::io; use std::path::PathBuf; -use uucore::{format_usage, help_about, help_usage}; +use uucore::format_usage; use uucore::display::println_verbatim; use uucore::error::{FromIo, UResult}; -const ABOUT: &str = help_about!("pwd.md"); -const USAGE: &str = help_usage!("pwd.md"); +use uucore::locale::get_message; const OPT_LOGICAL: &str = "logical"; const OPT_PHYSICAL: &str = "physical"; @@ -141,8 +140,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("pwd-about")) + .override_usage(format_usage(&get_message("pwd-usage"))) .infer_long_args(true) .arg( Arg::new(OPT_LOGICAL) diff --git a/src/uu/readlink/locales/en-US.ftl b/src/uu/readlink/locales/en-US.ftl new file mode 100644 index 00000000000..ddb80665173 --- /dev/null +++ b/src/uu/readlink/locales/en-US.ftl @@ -0,0 +1,2 @@ +readlink-about = Print value of a symbolic link or canonical file name. +readlink-usage = readlink [OPTION]... [FILE]... diff --git a/src/uu/readlink/readlink.md b/src/uu/readlink/readlink.md deleted file mode 100644 index 7215acbec09..00000000000 --- a/src/uu/readlink/readlink.md +++ /dev/null @@ -1,7 +0,0 @@ -# readlink - -``` -readlink [OPTION]... [FILE]... -``` - -Print value of a symbolic link or canonical file name. diff --git a/src/uu/readlink/src/readlink.rs b/src/uu/readlink/src/readlink.rs index 211422e035f..f41e076a353 100644 --- a/src/uu/readlink/src/readlink.rs +++ b/src/uu/readlink/src/readlink.rs @@ -13,10 +13,9 @@ use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError, UUsageError}; use uucore::fs::{MissingHandling, ResolveMode, canonicalize}; use uucore::line_ending::LineEnding; -use uucore::{format_usage, help_about, help_usage, show_error}; +use uucore::{format_usage, show_error}; -const ABOUT: &str = help_about!("readlink.md"); -const USAGE: &str = help_usage!("readlink.md"); +use uucore::locale::get_message; const OPT_CANONICALIZE: &str = "canonicalize"; const OPT_CANONICALIZE_MISSING: &str = "canonicalize-missing"; const OPT_CANONICALIZE_EXISTING: &str = "canonicalize-existing"; @@ -102,8 +101,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("readlink-about")) + .override_usage(format_usage(&get_message("readlink-usage"))) .infer_long_args(true) .arg( Arg::new(OPT_CANONICALIZE) diff --git a/src/uu/realpath/locales/en-US.ftl b/src/uu/realpath/locales/en-US.ftl new file mode 100644 index 00000000000..90910bf5301 --- /dev/null +++ b/src/uu/realpath/locales/en-US.ftl @@ -0,0 +1,2 @@ +realpath-about = Print the resolved path +realpath-usage = realpath [OPTION]... FILE... diff --git a/src/uu/realpath/realpath.md b/src/uu/realpath/realpath.md deleted file mode 100644 index 25e4a37260d..00000000000 --- a/src/uu/realpath/realpath.md +++ /dev/null @@ -1,7 +0,0 @@ -# realpath - -``` -realpath [OPTION]... FILE... -``` - -Print the resolved path diff --git a/src/uu/realpath/src/realpath.rs b/src/uu/realpath/src/realpath.rs index 94532b75505..4868ba68ade 100644 --- a/src/uu/realpath/src/realpath.rs +++ b/src/uu/realpath/src/realpath.rs @@ -11,19 +11,16 @@ use std::{ path::{Path, PathBuf}, }; use uucore::fs::make_path_relative_to; +use uucore::locale::get_message; use uucore::{ display::{Quotable, print_verbatim}, error::{FromIo, UClapError, UResult}, format_usage, fs::{MissingHandling, ResolveMode, canonicalize}, - help_about, help_usage, line_ending::LineEnding, show_if_err, }; -static ABOUT: &str = help_about!("realpath.md"); -const USAGE: &str = help_usage!("realpath.md"); - static OPT_QUIET: &str = "quiet"; static OPT_STRIP: &str = "strip"; static OPT_ZERO: &str = "zero"; @@ -89,8 +86,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("realpath-about")) + .override_usage(format_usage(&get_message("realpath-usage"))) .infer_long_args(true) .arg( Arg::new(OPT_QUIET) diff --git a/src/uu/rm/locales/en-US.ftl b/src/uu/rm/locales/en-US.ftl new file mode 100644 index 00000000000..31f0d008329 --- /dev/null +++ b/src/uu/rm/locales/en-US.ftl @@ -0,0 +1,14 @@ +rm-about = Remove (unlink) the FILE(s) +rm-usage = rm [OPTION]... FILE... +rm-after-help = By default, rm does not remove directories. Use the --recursive (-r or -R) + option to remove each listed directory, too, along with all of its contents + + To remove a file whose name starts with a '-', for example '-foo', + use one of these commands: + rm -- -foo + + rm ./-foo + + Note that if you use rm to remove a file, it might be possible to recover + some of its contents, given sufficient expertise and/or time. For greater + assurance that the contents are truly unrecoverable, consider using shred. diff --git a/src/uu/rm/rm.md b/src/uu/rm/rm.md deleted file mode 100644 index 7acc46363b8..00000000000 --- a/src/uu/rm/rm.md +++ /dev/null @@ -1,22 +0,0 @@ -# rm - -``` -rm [OPTION]... FILE... -``` - -Remove (unlink) the FILE(s) - -## After Help - -By default, rm does not remove directories. Use the --recursive (-r or -R) -option to remove each listed directory, too, along with all of its contents - -To remove a file whose name starts with a '-', for example '-foo', -use one of these commands: -rm -- -foo - -rm ./-foo - -Note that if you use rm to remove a file, it might be possible to recover -some of its contents, given sufficient expertise and/or time. For greater -assurance that the contents are truly unrecoverable, consider using shred. diff --git a/src/uu/rm/src/rm.rs b/src/uu/rm/src/rm.rs index 863336f5d14..d7340faa1c7 100644 --- a/src/uu/rm/src/rm.rs +++ b/src/uu/rm/src/rm.rs @@ -18,9 +18,8 @@ use std::path::MAIN_SEPARATOR; use std::path::{Path, PathBuf}; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError, UUsageError}; -use uucore::{ - format_usage, help_about, help_section, help_usage, os_str_as_bytes, prompt_yes, show_error, -}; +use uucore::locale::get_message; +use uucore::{format_usage, os_str_as_bytes, prompt_yes, show_error}; #[derive(Eq, PartialEq, Clone, Copy)] /// Enum, determining when the `rm` will prompt the user about the file deletion @@ -90,18 +89,14 @@ impl Default for Options { } } -const ABOUT: &str = help_about!("rm.md"); -const USAGE: &str = help_usage!("rm.md"); -const AFTER_HELP: &str = help_section!("after help", "rm.md"); - static OPT_DIR: &str = "dir"; static OPT_INTERACTIVE: &str = "interactive"; static OPT_FORCE: &str = "force"; static OPT_NO_PRESERVE_ROOT: &str = "no-preserve-root"; static OPT_ONE_FILE_SYSTEM: &str = "one-file-system"; static OPT_PRESERVE_ROOT: &str = "preserve-root"; -static OPT_PROMPT: &str = "prompt"; -static OPT_PROMPT_MORE: &str = "prompt-more"; +static OPT_PROMPT_ALWAYS: &str = "prompt-always"; +static OPT_PROMPT_ONCE: &str = "prompt-once"; static OPT_RECURSIVE: &str = "recursive"; static OPT_VERBOSE: &str = "verbose"; static PRESUME_INPUT_TTY: &str = "-presume-input-tty"; @@ -110,19 +105,25 @@ static ARG_FILES: &str = "files"; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().after_help(AFTER_HELP).try_get_matches_from(args)?; + let matches = uu_app().try_get_matches_from(args)?; - let files: Vec<&OsStr> = matches + let files: Vec<_> = matches .get_many::(ARG_FILES) .map(|v| v.map(OsString::as_os_str).collect()) .unwrap_or_default(); let force_flag = matches.get_flag(OPT_FORCE); + if files.is_empty() && !force_flag { + // Still check by hand and not use clap + // Because "rm -f" is a thing + return Err(UUsageError::new(1, "missing operand")); + } + // If -f(--force) is before any -i (or variants) we want prompts else no prompts - let force_prompt_never: bool = force_flag && { + let force_prompt_never = force_flag && { let force_index = matches.index_of(OPT_FORCE).unwrap_or(0); - ![OPT_PROMPT, OPT_PROMPT_MORE, OPT_INTERACTIVE] + ![OPT_PROMPT_ALWAYS, OPT_PROMPT_ONCE, OPT_INTERACTIVE] .iter() .any(|flag| { matches.value_source(flag) == Some(ValueSource::CommandLine) @@ -130,79 +131,75 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { }) }; - if files.is_empty() && !force_flag { - // Still check by hand and not use clap - // Because "rm -f" is a thing - return Err(UUsageError::new(1, "missing operand")); - } else { - let options = Options { - force: force_flag, - interactive: { - if force_prompt_never { - InteractiveMode::Never - } else if matches.get_flag(OPT_PROMPT) { - InteractiveMode::Always - } else if matches.get_flag(OPT_PROMPT_MORE) { - InteractiveMode::Once - } else if matches.contains_id(OPT_INTERACTIVE) { - match matches.get_one::(OPT_INTERACTIVE).unwrap().as_str() { - "never" => InteractiveMode::Never, - "once" => InteractiveMode::Once, - "always" => InteractiveMode::Always, - val => { - return Err(USimpleError::new( - 1, - format!("Invalid argument to interactive ({val})"), - )); - } + let options = Options { + force: force_flag, + interactive: { + if force_prompt_never { + InteractiveMode::Never + } else if matches.get_flag(OPT_PROMPT_ALWAYS) { + InteractiveMode::Always + } else if matches.get_flag(OPT_PROMPT_ONCE) { + InteractiveMode::Once + } else if matches.contains_id(OPT_INTERACTIVE) { + match matches.get_one::(OPT_INTERACTIVE).unwrap().as_str() { + "never" => InteractiveMode::Never, + "once" => InteractiveMode::Once, + "always" => InteractiveMode::Always, + val => { + return Err(USimpleError::new( + 1, + format!("Invalid argument to interactive ({val})"), + )); } - } else { - InteractiveMode::PromptProtected } - }, - one_fs: matches.get_flag(OPT_ONE_FILE_SYSTEM), - preserve_root: !matches.get_flag(OPT_NO_PRESERVE_ROOT), - recursive: matches.get_flag(OPT_RECURSIVE), - dir: matches.get_flag(OPT_DIR), - verbose: matches.get_flag(OPT_VERBOSE), - __presume_input_tty: if matches.get_flag(PRESUME_INPUT_TTY) { - Some(true) } else { - None + InteractiveMode::PromptProtected + } + }, + one_fs: matches.get_flag(OPT_ONE_FILE_SYSTEM), + preserve_root: !matches.get_flag(OPT_NO_PRESERVE_ROOT), + recursive: matches.get_flag(OPT_RECURSIVE), + dir: matches.get_flag(OPT_DIR), + verbose: matches.get_flag(OPT_VERBOSE), + __presume_input_tty: if matches.get_flag(PRESUME_INPUT_TTY) { + Some(true) + } else { + None + }, + }; + if options.interactive == InteractiveMode::Once && (options.recursive || files.len() > 3) { + let msg: String = format!( + "remove {} {}{}", + files.len(), + if files.len() > 1 { + "arguments" + } else { + "argument" }, - }; - if options.interactive == InteractiveMode::Once && (options.recursive || files.len() > 3) { - let msg: String = format!( - "remove {} {}{}", - files.len(), - if files.len() > 1 { - "arguments" - } else { - "argument" - }, - if options.recursive { - " recursively?" - } else { - "?" - } - ); - if !prompt_yes!("{msg}") { - return Ok(()); + if options.recursive { + " recursively?" + } else { + "?" } + ); + if !prompt_yes!("{msg}") { + return Ok(()); } + } - if remove(&files, &options) { - return Err(1.into()); - } + if remove(&files, &options) { + return Err(1.into()); } + Ok(()) } pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("rm-about")) + .override_usage(format_usage(&get_message("rm-usage"))) + .after_help(get_message("rm-after-help")) .infer_long_args(true) .args_override_self(true) .arg( @@ -213,18 +210,18 @@ pub fn uu_app() -> Command { .action(ArgAction::SetTrue), ) .arg( - Arg::new(OPT_PROMPT) + Arg::new(OPT_PROMPT_ALWAYS) .short('i') .help("prompt before every removal") - .overrides_with_all([OPT_PROMPT_MORE, OPT_INTERACTIVE]) + .overrides_with_all([OPT_PROMPT_ONCE, OPT_INTERACTIVE]) .action(ArgAction::SetTrue), ) .arg( - Arg::new(OPT_PROMPT_MORE) + Arg::new(OPT_PROMPT_ONCE) .short('I') .help("prompt once before removing more than three files, or when removing recursively. \ Less intrusive than -i, while still giving some protection against most mistakes") - .overrides_with_all([OPT_PROMPT, OPT_INTERACTIVE]) + .overrides_with_all([OPT_PROMPT_ALWAYS, OPT_INTERACTIVE]) .action(ArgAction::SetTrue), ) .arg( @@ -238,7 +235,7 @@ pub fn uu_app() -> Command { .num_args(0..=1) .require_equals(true) .default_missing_value("always") - .overrides_with_all([OPT_PROMPT, OPT_PROMPT_MORE]), + .overrides_with_all([OPT_PROMPT_ALWAYS, OPT_PROMPT_ONCE]), ) .arg( Arg::new(OPT_ONE_FILE_SYSTEM) diff --git a/src/uu/rmdir/locales/en-US.ftl b/src/uu/rmdir/locales/en-US.ftl new file mode 100644 index 00000000000..e2c1ca0ec19 --- /dev/null +++ b/src/uu/rmdir/locales/en-US.ftl @@ -0,0 +1,2 @@ +rmdir-about = Remove the DIRECTORY(ies), if they are empty. +rmdir-usage = rmdir [OPTION]... DIRECTORY... diff --git a/src/uu/rmdir/rmdir.md b/src/uu/rmdir/rmdir.md deleted file mode 100644 index 16f7bf82bea..00000000000 --- a/src/uu/rmdir/rmdir.md +++ /dev/null @@ -1,7 +0,0 @@ -# rmdir - -``` -rmdir [OPTION]... DIRECTORY... -``` - -Remove the DIRECTORY(ies), if they are empty. diff --git a/src/uu/rmdir/src/rmdir.rs b/src/uu/rmdir/src/rmdir.rs index 8c9dc12b665..228b085ec21 100644 --- a/src/uu/rmdir/src/rmdir.rs +++ b/src/uu/rmdir/src/rmdir.rs @@ -14,10 +14,9 @@ use std::path::Path; use uucore::display::Quotable; use uucore::error::{UResult, set_exit_code, strip_errno}; -use uucore::{format_usage, help_about, help_usage, show_error, util_name}; +use uucore::{format_usage, show_error, util_name}; -static ABOUT: &str = help_about!("rmdir.md"); -const USAGE: &str = help_usage!("rmdir.md"); +use uucore::locale::get_message; static OPT_IGNORE_FAIL_NON_EMPTY: &str = "ignore-fail-on-non-empty"; static OPT_PARENTS: &str = "parents"; static OPT_VERBOSE: &str = "verbose"; @@ -165,8 +164,8 @@ struct Opts { pub fn uu_app() -> Command { Command::new(util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("rmdir-about")) + .override_usage(format_usage(&get_message("rmdir-usage"))) .infer_long_args(true) .arg( Arg::new(OPT_IGNORE_FAIL_NON_EMPTY) diff --git a/src/uu/runcon/locales/en-US.ftl b/src/uu/runcon/locales/en-US.ftl new file mode 100644 index 00000000000..37d977a5b90 --- /dev/null +++ b/src/uu/runcon/locales/en-US.ftl @@ -0,0 +1,10 @@ +runcon-about = Run command with specified security context under SELinux enabled systems. +runcon-usage = runcon CONTEXT COMMAND [ARG...] + runcon [-c] [-u USER] [-r ROLE] [-t TYPE] [-l RANGE] COMMAND [ARG...] +runcon-after-help = Run COMMAND with completely-specified CONTEXT, or with current or transitioned security context modified by one or more of LEVEL, ROLE, TYPE, and USER. + + If none of --compute, --type, --user, --role or --range is specified, then the first argument is used as the complete context. + + Note that only carefully-chosen contexts are likely to successfully run. + + If neither CONTEXT nor COMMAND is specified, the current security context is printed. diff --git a/src/uu/runcon/runcon.md b/src/uu/runcon/runcon.md deleted file mode 100644 index 53884b703be..00000000000 --- a/src/uu/runcon/runcon.md +++ /dev/null @@ -1,18 +0,0 @@ -# runcon - -``` -runcon CONTEXT COMMAND [ARG...] -runcon [-c] [-u USER] [-r ROLE] [-t TYPE] [-l RANGE] COMMAND [ARG...] -``` - -Run command with specified security context under SELinux enabled systems. - -## After Help - -Run COMMAND with completely-specified CONTEXT, or with current or transitioned security context modified by one or more of LEVEL, ROLE, TYPE, and USER. - -If none of --compute, --type, --user, --role or --range is specified, then the first argument is used as the complete context. - -Note that only carefully-chosen contexts are likely to successfully run. - -If neither CONTEXT nor COMMAND is specified, the current security context is printed. diff --git a/src/uu/runcon/src/runcon.rs b/src/uu/runcon/src/runcon.rs index 658aa33b252..ee67a92201a 100644 --- a/src/uu/runcon/src/runcon.rs +++ b/src/uu/runcon/src/runcon.rs @@ -10,7 +10,7 @@ use uucore::error::{UClapError, UError, UResult}; use clap::{Arg, ArgAction, Command}; use selinux::{OpaqueSecurityContext, SecurityClass, SecurityContext}; -use uucore::{format_usage, help_about, help_section, help_usage}; +use uucore::format_usage; use std::borrow::Cow; use std::ffi::{CStr, CString, OsStr, OsString}; @@ -23,9 +23,7 @@ mod errors; use errors::error_exit_status; use errors::{Error, Result, RunconError}; -const ABOUT: &str = help_about!("runcon.md"); -const USAGE: &str = help_usage!("runcon.md"); -const DESCRIPTION: &str = help_section!("after help", "runcon.md"); +use uucore::locale::get_message; pub mod options { pub const COMPUTE: &str = "compute"; @@ -90,9 +88,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .after_help(DESCRIPTION) - .override_usage(format_usage(USAGE)) + .about(get_message("runcon-about")) + .after_help(get_message("runcon-after-help")) + .override_usage(format_usage(&get_message("runcon-usage"))) .infer_long_args(true) .arg( Arg::new(options::COMPUTE) diff --git a/src/uu/seq/locales/en-US.ftl b/src/uu/seq/locales/en-US.ftl new file mode 100644 index 00000000000..818ebc86c1e --- /dev/null +++ b/src/uu/seq/locales/en-US.ftl @@ -0,0 +1,4 @@ +seq-about = Display numbers from FIRST to LAST, in steps of INCREMENT. +seq-usage = seq [OPTION]... LAST + seq [OPTION]... FIRST LAST + seq [OPTION]... FIRST INCREMENT LAST diff --git a/src/uu/seq/seq.md b/src/uu/seq/seq.md deleted file mode 100644 index d747e4a0263..00000000000 --- a/src/uu/seq/seq.md +++ /dev/null @@ -1,9 +0,0 @@ -# seq - -``` -seq [OPTION]... LAST -seq [OPTION]... FIRST LAST -seq [OPTION]... FIRST INCREMENT LAST -``` - -Display numbers from FIRST to LAST, in steps of INCREMENT. diff --git a/src/uu/seq/src/seq.rs b/src/uu/seq/src/seq.rs index af7ca2f84d0..2dd5f17b81d 100644 --- a/src/uu/seq/src/seq.rs +++ b/src/uu/seq/src/seq.rs @@ -15,7 +15,7 @@ use uucore::error::{FromIo, UResult}; use uucore::extendedbigdecimal::ExtendedBigDecimal; use uucore::format::num_format::FloatVariant; use uucore::format::{Format, num_format}; -use uucore::{fast_inc::fast_inc, format_usage, help_about, help_usage}; +use uucore::{fast_inc::fast_inc, format_usage}; mod error; @@ -28,8 +28,7 @@ mod numberparse; use crate::error::SeqError; use crate::number::PreciseNumber; -const ABOUT: &str = help_about!("seq.md"); -const USAGE: &str = help_usage!("seq.md"); +use uucore::locale::get_message; const OPT_SEPARATOR: &str = "separator"; const OPT_TERMINATOR: &str = "terminator"; @@ -222,8 +221,8 @@ pub fn uu_app() -> Command { .trailing_var_arg(true) .infer_long_args(true) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("seq-about")) + .override_usage(format_usage(&get_message("seq-usage"))) .arg( Arg::new(OPT_SEPARATOR) .short('s') diff --git a/src/uu/shred/locales/en-US.ftl b/src/uu/shred/locales/en-US.ftl new file mode 100644 index 00000000000..90245a08f0b --- /dev/null +++ b/src/uu/shred/locales/en-US.ftl @@ -0,0 +1,37 @@ +shred-about = Overwrite the specified FILE(s) repeatedly, in order to make it harder for even + very expensive hardware probing to recover the data. +shred-usage = shred [OPTION]... FILE... +shred-after-help = Delete FILE(s) if --remove (-u) is specified. The default is not to remove + the files because it is common to operate on device files like /dev/hda, and + those files usually should not be removed. + + CAUTION: Note that shred relies on a very important assumption: that the file + system overwrites data in place. This is the traditional way to do things, but + many modern file system designs do not satisfy this assumption. The following + are examples of file systems on which shred is not effective, or is not + guaranteed to be effective in all file system modes: + + - log-structured or journal file systems, such as those supplied with + AIX and Solaris (and JFS, ReiserFS, XFS, Ext3, etc.) + + - file systems that write redundant data and carry on even if some writes + fail, such as RAID-based file systems + + - file systems that make snapshots, such as Network Appliance's NFS server + + - file systems that cache in temporary locations, such as NFS + version 3 clients + + - compressed file systems + + In the case of ext3 file systems, the above disclaimer applies (and shred is + thus of limited effectiveness) only in data=journal mode, which journals file + data in addition to just metadata. In both the data=ordered (default) and + data=writeback modes, shred works as usual. Ext3 journal modes can be changed + by adding the data=something option to the mount options for a particular + file system in the /etc/fstab file, as documented in the mount man page (`man + mount`). + + In addition, file system backups and remote mirrors may contain copies of + the file that cannot be removed, and that will allow a shredded file to be + recovered later. diff --git a/src/uu/shred/shred.md b/src/uu/shred/shred.md deleted file mode 100644 index 6d6398cce10..00000000000 --- a/src/uu/shred/shred.md +++ /dev/null @@ -1,47 +0,0 @@ -# shred - - - -``` -shred [OPTION]... FILE... -``` - -Overwrite the specified FILE(s) repeatedly, in order to make it harder for even -very expensive hardware probing to recover the data. - -## After help - -Delete `FILE(s)` if `--remove` (`-u`) is specified. The default is not to remove -the files because it is common to operate on device files like `/dev/hda`, and -those files usually should not be removed. - -CAUTION: Note that shred relies on a very important assumption: that the file -system overwrites data in place. This is the traditional way to do things, but -many modern file system designs do not satisfy this assumption. The following -are examples of file systems on which shred is not effective, or is not -guaranteed to be effective in all file system modes: - - * log-structured or journal file systems, such as those supplied with - AIX and Solaris (and JFS, ReiserFS, XFS, Ext3, etc.) - - * file systems that write redundant data and carry on even if some writes - fail, such as RAID-based file systems - - * file systems that make snapshots, such as Network Appliance's NFS server - - * file systems that cache in temporary locations, such as NFS - version 3 clients - - * compressed file systems - -In the case of ext3 file systems, the above disclaimer applies (and shred is -thus of limited effectiveness) only in `data=journal` mode, which journals file -data in addition to just metadata. In both the `data=ordered` (default) and -`data=writeback` modes, shred works as usual. Ext3 journal modes can be changed -by adding the `data=something` option to the mount options for a particular -file system in the `/etc/fstab` file, as documented in the mount man page (`man -mount`). - -In addition, file system backups and remote mirrors may contain copies of -the file that cannot be removed, and that will allow a shredded file to be -recovered later. diff --git a/src/uu/shred/src/shred.rs b/src/uu/shred/src/shred.rs index 71e3fcef3b0..0cfafd8627c 100644 --- a/src/uu/shred/src/shred.rs +++ b/src/uu/shred/src/shred.rs @@ -10,7 +10,7 @@ use clap::{Arg, ArgAction, Command}; use libc::S_IWUSR; use rand::{Rng, SeedableRng, rngs::StdRng, seq::SliceRandom}; use std::fs::{self, File, OpenOptions}; -use std::io::{self, Seek, Write}; +use std::io::{self, Read, Seek, Write}; #[cfg(unix)] use std::os::unix::prelude::PermissionsExt; use std::path::{Path, PathBuf}; @@ -18,11 +18,9 @@ use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError, UUsageError}; use uucore::parser::parse_size::parse_size_u64; use uucore::parser::shortcut_value_parser::ShortcutValueParser; -use uucore::{format_usage, help_about, help_section, help_usage, show_error, show_if_err}; +use uucore::{format_usage, show_error, show_if_err}; -const ABOUT: &str = help_about!("shred.md"); -const USAGE: &str = help_usage!("shred.md"); -const AFTER_HELP: &str = help_section!("after help", "shred.md"); +use uucore::locale::get_message; pub mod options { pub const FORCE: &str = "force"; @@ -34,6 +32,7 @@ pub mod options { pub const VERBOSE: &str = "verbose"; pub const EXACT: &str = "exact"; pub const ZERO: &str = "zero"; + pub const RANDOM_SOURCE: &str = "random-source"; pub mod remove { pub const UNLINK: &str = "unlink"; @@ -152,16 +151,25 @@ impl Iterator for FilenameIter { } } +enum RandomSource { + System, + Read(File), +} + /// Used to generate blocks of bytes of size <= BLOCK_SIZE based on either a give pattern /// or randomness // The lint warns about a large difference because StdRng is big, but the buffers are much // larger anyway, so it's fine. #[allow(clippy::large_enum_variant)] -enum BytesWriter { +enum BytesWriter<'a> { Random { rng: StdRng, buffer: [u8; BLOCK_SIZE], }, + RandomFile { + rng_file: &'a File, + buffer: [u8; BLOCK_SIZE], + }, // To write patterns we only write to the buffer once. To be able to do // this, we need to extend the buffer with 2 bytes. We can then easily // obtain a buffer starting with any character of the pattern that we @@ -177,12 +185,18 @@ enum BytesWriter { }, } -impl BytesWriter { - fn from_pass_type(pass: &PassType) -> Self { +impl<'a> BytesWriter<'a> { + fn from_pass_type(pass: &PassType, random_source: &'a RandomSource) -> Self { match pass { - PassType::Random => Self::Random { - rng: StdRng::from_os_rng(), - buffer: [0; BLOCK_SIZE], + PassType::Random => match random_source { + RandomSource::System => Self::Random { + rng: StdRng::from_os_rng(), + buffer: [0; BLOCK_SIZE], + }, + RandomSource::Read(file) => Self::RandomFile { + rng_file: file, + buffer: [0; BLOCK_SIZE], + }, }, PassType::Pattern(pattern) => { // Copy the pattern in chunks rather than simply one byte at a time @@ -203,17 +217,22 @@ impl BytesWriter { } } - fn bytes_for_pass(&mut self, size: usize) -> &[u8] { + fn bytes_for_pass(&mut self, size: usize) -> Result<&[u8], io::Error> { match self { Self::Random { rng, buffer } => { let bytes = &mut buffer[..size]; rng.fill(bytes); - bytes + Ok(bytes) + } + Self::RandomFile { rng_file, buffer } => { + let bytes = &mut buffer[..size]; + rng_file.read_exact(bytes)?; + Ok(bytes) } Self::Pattern { offset, buffer } => { let bytes = &buffer[*offset..size + *offset]; *offset = (*offset + size) % PATTERN_LENGTH; - bytes + Ok(bytes) } } } @@ -240,6 +259,15 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { None => unreachable!(), }; + let random_source = match matches.get_one::(options::RANDOM_SOURCE) { + Some(filepath) => RandomSource::Read(File::open(filepath).map_err(|_| { + USimpleError::new( + 1, + format!("cannot open random source: {}", filepath.quote()), + ) + })?), + None => RandomSource::System, + }; // TODO: implement --random-source let remove_method = if matches.get_flag(options::WIPESYNC) { @@ -275,6 +303,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { size, exact, zero, + &random_source, verbose, force, )); @@ -285,9 +314,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .after_help(AFTER_HELP) - .override_usage(format_usage(USAGE)) + .about(get_message("shred-about")) + .after_help(get_message("shred-after-help")) + .override_usage(format_usage(&get_message("shred-usage"))) .infer_long_args(true) .arg( Arg::new(options::FORCE) @@ -356,6 +385,13 @@ pub fn uu_app() -> Command { .help("add a final overwrite with zeros to hide shredding") .action(ArgAction::SetTrue), ) + .arg( + Arg::new(options::RANDOM_SOURCE) + .long(options::RANDOM_SOURCE) + .help("take random bytes from FILE") + .value_hint(clap::ValueHint::FilePath) + .action(ArgAction::Set), + ) // Positional arguments .arg( Arg::new(options::FILE) @@ -395,6 +431,7 @@ fn wipe_file( size: Option, exact: bool, zero: bool, + random_source: &RandomSource, verbose: bool, force: bool, ) -> UResult<()> { @@ -501,7 +538,7 @@ fn wipe_file( // size is an optional argument for exactly how many bytes we want to shred // Ignore failed writes; just keep trying show_if_err!( - do_pass(&mut file, &pass_type, exact, size) + do_pass(&mut file, &pass_type, exact, random_source, size) .map_err_context(|| format!("{}: File write pass failed", path.maybe_quote())) ); } @@ -534,22 +571,23 @@ fn do_pass( file: &mut File, pass_type: &PassType, exact: bool, + random_source: &RandomSource, file_size: u64, ) -> Result<(), io::Error> { // We might be at the end of the file due to a previous iteration, so rewind. file.rewind()?; - let mut writer = BytesWriter::from_pass_type(pass_type); + let mut writer = BytesWriter::from_pass_type(pass_type, random_source); let (number_of_blocks, bytes_left) = split_on_blocks(file_size, exact); // We start by writing BLOCK_SIZE times as many time as possible. for _ in 0..number_of_blocks { - let block = writer.bytes_for_pass(BLOCK_SIZE); + let block = writer.bytes_for_pass(BLOCK_SIZE)?; file.write_all(block)?; } // Then we write remaining data which is smaller than the BLOCK_SIZE - let block = writer.bytes_for_pass(bytes_left as usize); + let block = writer.bytes_for_pass(bytes_left as usize)?; file.write_all(block)?; file.sync_data()?; diff --git a/src/uu/shuf/locales/en-US.ftl b/src/uu/shuf/locales/en-US.ftl new file mode 100644 index 00000000000..5348f1f7e1b --- /dev/null +++ b/src/uu/shuf/locales/en-US.ftl @@ -0,0 +1,6 @@ +shuf-about = Shuffle the input by outputting a random permutation of input lines. + Each output permutation is equally likely. + With no FILE, or when FILE is -, read standard input. +shuf-usage = shuf [OPTION]... [FILE] + shuf -e [OPTION]... [ARG]... + shuf -i LO-HI [OPTION]... diff --git a/src/uu/shuf/shuf.md b/src/uu/shuf/shuf.md deleted file mode 100644 index 7bc1e0a6d60..00000000000 --- a/src/uu/shuf/shuf.md +++ /dev/null @@ -1,11 +0,0 @@ -# shuf - -``` -shuf [OPTION]... [FILE] -shuf -e [OPTION]... [ARG]... -shuf -i LO-HI [OPTION]... -``` - -Shuffle the input by outputting a random permutation of input lines. -Each output permutation is equally likely. -With no FILE, or when FILE is -, read standard input. diff --git a/src/uu/shuf/src/shuf.rs b/src/uu/shuf/src/shuf.rs index 9c08ea28d6f..db44636150d 100644 --- a/src/uu/shuf/src/shuf.rs +++ b/src/uu/shuf/src/shuf.rs @@ -19,7 +19,8 @@ use std::path::{Path, PathBuf}; use std::str::FromStr; use uucore::display::{OsWrite, Quotable}; use uucore::error::{FromIo, UResult, USimpleError, UUsageError}; -use uucore::{format_usage, help_about, help_usage}; +use uucore::format_usage; +use uucore::locale::get_message; mod rand_read_adapter; @@ -29,9 +30,6 @@ enum Mode { InputRange(RangeInclusive), } -static USAGE: &str = help_usage!("shuf.md"); -static ABOUT: &str = help_about!("shuf.md"); - struct Options { head_count: usize, output: Option, @@ -143,9 +141,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) - .about(ABOUT) + .about(get_message("shuf-about")) .version(uucore::crate_version!()) - .override_usage(format_usage(USAGE)) + .override_usage(format_usage(&get_message("shuf-usage"))) .infer_long_args(true) .arg( Arg::new(options::ECHO) diff --git a/src/uu/sleep/locales/en-US.ftl b/src/uu/sleep/locales/en-US.ftl new file mode 100644 index 00000000000..c9e7925babe --- /dev/null +++ b/src/uu/sleep/locales/en-US.ftl @@ -0,0 +1,8 @@ +sleep-about = Pause for NUMBER seconds. +sleep-usage = sleep NUMBER[SUFFIX]... + sleep OPTION +sleep-after-help = Pause for NUMBER seconds. SUFFIX may be 's' for seconds (the default), + 'm' for minutes, 'h' for hours or 'd' for days. Unlike most implementations + that require NUMBER be an integer, here NUMBER may be an arbitrary floating + point number. Given two or more arguments, pause for the amount of time + specified by the sum of their values. diff --git a/src/uu/sleep/sleep.md b/src/uu/sleep/sleep.md deleted file mode 100644 index b5c07fb493e..00000000000 --- a/src/uu/sleep/sleep.md +++ /dev/null @@ -1,16 +0,0 @@ -# sleep - -``` -sleep NUMBER[SUFFIX]... -sleep OPTION -``` - -Pause for NUMBER seconds. - -## After Help - -Pause for NUMBER seconds. SUFFIX may be 's' for seconds (the default), -'m' for minutes, 'h' for hours or 'd' for days. Unlike most implementations -that require NUMBER be an integer, here NUMBER may be an arbitrary floating -point number. Given two or more arguments, pause for the amount of time -specified by the sum of their values. diff --git a/src/uu/sleep/src/sleep.rs b/src/uu/sleep/src/sleep.rs index 2b533eadebb..9b307b1e8a6 100644 --- a/src/uu/sleep/src/sleep.rs +++ b/src/uu/sleep/src/sleep.rs @@ -8,16 +8,14 @@ use std::time::Duration; use uucore::{ error::{UResult, USimpleError, UUsageError}, - format_usage, help_about, help_section, help_usage, + format_usage, parser::parse_time, show_error, }; use clap::{Arg, ArgAction, Command}; -static ABOUT: &str = help_about!("sleep.md"); -const USAGE: &str = help_usage!("sleep.md"); -static AFTER_HELP: &str = help_section!("after help", "sleep.md"); +use uucore::locale::get_message; mod options { pub const NUMBER: &str = "NUMBER"; @@ -47,9 +45,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .after_help(AFTER_HELP) - .override_usage(format_usage(USAGE)) + .about(get_message("sleep-about")) + .after_help(get_message("sleep-after-help")) + .override_usage(format_usage(&get_message("sleep-usage"))) .infer_long_args(true) .arg( Arg::new(options::NUMBER) diff --git a/src/uu/sort/locales/en-US.ftl b/src/uu/sort/locales/en-US.ftl new file mode 100644 index 00000000000..7b53e9df023 --- /dev/null +++ b/src/uu/sort/locales/en-US.ftl @@ -0,0 +1,11 @@ +sort-about = Display sorted concatenation of all FILE(s). With no FILE, or when FILE is -, read standard input. +sort-usage = sort [OPTION]... [FILE]... +sort-after-help = The key format is FIELD[.CHAR][OPTIONS][,FIELD[.CHAR]][OPTIONS]. + + Fields by default are separated by the first whitespace after a non-whitespace character. Use -t to specify a custom separator. + In the default case, whitespace is appended at the beginning of each field. Custom separators however are not included in fields. + + FIELD and CHAR both start at 1 (i.e. they are 1-indexed). If there is no end specified after a comma, the end will be the end of the line. + If CHAR is set 0, it means the end of the field. CHAR defaults to 1 for the start position and to 0 for the end position. + + Valid options are: MbdfhnRrV. They override the global options for this key. diff --git a/src/uu/sort/sort.md b/src/uu/sort/sort.md deleted file mode 100644 index 1d1aa5d5ffe..00000000000 --- a/src/uu/sort/sort.md +++ /dev/null @@ -1,21 +0,0 @@ - - -# sort - -``` -sort [OPTION]... [FILE]... -``` - -Display sorted concatenation of all FILE(s). With no FILE, or when FILE is -, read standard input. - -## After help - -The key format is `FIELD[.CHAR][OPTIONS][,FIELD[.CHAR]][OPTIONS]`. - -Fields by default are separated by the first whitespace after a non-whitespace character. Use `-t` to specify a custom separator. -In the default case, whitespace is appended at the beginning of each field. Custom separators however are not included in fields. - -`FIELD` and `CHAR` both start at 1 (i.e. they are 1-indexed). If there is no end specified after a comma, the end will be the end of the line. -If `CHAR` is set 0, it means the end of the field. `CHAR` defaults to 1 for the start position and to 0 for the end position. - -Valid options are: `MbdfhnRrV`. They override the global options for this key. diff --git a/src/uu/sort/src/merge.rs b/src/uu/sort/src/merge.rs index fb7e2c8bf11..bbd0c2c839e 100644 --- a/src/uu/sort/src/merge.rs +++ b/src/uu/sort/src/merge.rs @@ -13,7 +13,7 @@ use std::{ cmp::Ordering, - ffi::OsString, + ffi::{OsStr, OsString}, fs::{self, File}, io::{BufWriter, Read, Write}, iter, @@ -38,7 +38,7 @@ use crate::{ /// and replace its occurrences in the inputs with that copy. fn replace_output_file_in_input_files( files: &mut [OsString], - output: Option<&str>, + output: Option<&OsStr>, tmp_dir: &mut TmpDirWrapper, ) -> UResult<()> { let mut copy: Option = None; diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index 19baead3045..c97d07a6aa1 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -48,13 +48,11 @@ use uucore::line_ending::LineEnding; use uucore::parser::parse_size::{ParseSizeError, Parser}; use uucore::parser::shortcut_value_parser::ShortcutValueParser; use uucore::version_cmp::version_cmp; -use uucore::{format_usage, help_about, help_section, help_usage, show_error}; +use uucore::{format_usage, show_error}; use crate::tmp_dir::TmpDirWrapper; -const ABOUT: &str = help_about!("sort.md"); -const USAGE: &str = help_usage!("sort.md"); -const AFTER_HELP: &str = help_section!("after help", "sort.md"); +use uucore::locale::get_message; mod options { pub mod modes { @@ -131,7 +129,10 @@ pub enum SortError { }, #[error("open failed: {}: {}", .path.maybe_quote(), strip_errno(.error))] - OpenFailed { path: String, error: std::io::Error }, + OpenFailed { + path: PathBuf, + error: std::io::Error, + }, #[error("failed to parse key {}: {}", .key.quote(), .msg)] ParseKeyError { key: String, msg: String }, @@ -154,8 +155,23 @@ pub enum SortError { #[error("cannot create temporary file in '{}':", .path.display())] TmpFileCreationFailed { path: PathBuf }, + #[error("extra operand '{}'\nfile operands cannot be combined with --files0-from\nTry '{} --help' for more information.", .file.display(), uucore::execution_phrase())] + FileOperandsCombined { file: PathBuf }, + #[error("{error}")] Uft8Error { error: Utf8Error }, + + #[error("multiple output files specified")] + MultipleOutputFiles, + + #[error("when reading file names from stdin, no file name of '-' allowed")] + MinusInStdIn, + + #[error("no input from '{}'", .file.display())] + EmptyInputFile { file: PathBuf }, + + #[error("{}:{}: invalid zero-length file name", .file.display(), .line_num)] + ZeroLengthFileName { file: PathBuf, line_num: usize }, } impl UError for SortError { @@ -201,24 +217,25 @@ impl SortMode { } pub struct Output { - file: Option<(String, File)>, + file: Option<(OsString, File)>, } impl Output { - fn new(name: Option<&str>) -> UResult { + fn new(name: Option<&OsStr>) -> UResult { let file = if let Some(name) = name { + let path = Path::new(name); // This is different from `File::create()` because we don't truncate the output yet. // This allows using the output file as an input file. #[allow(clippy::suspicious_open_options)] let file = OpenOptions::new() .write(true) .create(true) - .open(name) + .open(path) .map_err(|e| SortError::OpenFailed { - path: name.to_owned(), + path: path.to_owned(), error: e, })?; - Some((name.to_owned(), file)) + Some((name.to_os_string(), file)) } else { None }; @@ -236,9 +253,9 @@ impl Output { }) } - fn as_output_name(&self) -> Option<&str> { + fn as_output_name(&self) -> Option<&OsStr> { match &self.file { - Some((name, _file)) => Some(name), + Some((name, _file)) => Some(name.as_os_str()), None => None, } } @@ -1013,6 +1030,8 @@ fn get_rlimit() -> UResult { } } +const STDIN_FILE: &str = "-"; + #[uucore::main] #[allow(clippy::cognitive_complexity)] pub fn uumain(args: impl uucore::Args) -> UResult<()> { @@ -1034,25 +1053,57 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } }; + // Prevent -o/--output to be specified multiple times + if matches + .get_occurrences::(options::OUTPUT) + .is_some_and(|out| out.len() > 1) + { + return Err(SortError::MultipleOutputFiles.into()); + } + settings.debug = matches.get_flag(options::DEBUG); // check whether user specified a zero terminated list of files for input, otherwise read files from args let mut files: Vec = if matches.contains_id(options::FILES0_FROM) { - let files0_from: Vec = matches - .get_many::(options::FILES0_FROM) - .map(|v| v.map(ToOwned::to_owned).collect()) + let files0_from: PathBuf = matches + .get_one::(options::FILES0_FROM) + .map(|v| v.into()) .unwrap_or_default(); + // Cannot combine FILES with FILES0_FROM + if let Some(s) = matches.get_one::(options::FILES) { + return Err(SortError::FileOperandsCombined { file: s.into() }.into()); + } + let mut files = Vec::new(); - for path in &files0_from { - let reader = open(path)?; - let buf_reader = BufReader::new(reader); - for line in buf_reader.split(b'\0').flatten() { - files.push(OsString::from( - std::str::from_utf8(&line) - .expect("Could not parse string from zero terminated input."), - )); + + // sort errors with "cannot open: [...]" instead of "cannot read: [...]" here + let reader = open_with_open_failed_error(&files0_from)?; + let buf_reader = BufReader::new(reader); + for (line_num, line) in buf_reader.split(b'\0').flatten().enumerate() { + let f = std::str::from_utf8(&line) + .expect("Could not parse string from zero terminated input."); + match f { + STDIN_FILE => { + return Err(SortError::MinusInStdIn.into()); + } + "" => { + return Err(SortError::ZeroLengthFileName { + file: files0_from, + line_num: line_num + 1, + } + .into()); + } + _ => {} } + + files.push(OsString::from( + std::str::from_utf8(&line) + .expect("Could not parse string from zero terminated input."), + )); + } + if files.is_empty() { + return Err(SortError::EmptyInputFile { file: files0_from }.into()); } files } else { @@ -1201,7 +1252,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { if files.is_empty() { /* if no file, default to stdin */ - files.push("-".to_string().into()); + files.push(OsString::from(STDIN_FILE)); } else if settings.check && files.len() != 1 { return Err(UUsageError::new( 2, @@ -1271,8 +1322,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let output = Output::new( matches - .get_one::(options::OUTPUT) - .map(|s| s.as_str()), + .get_one::(options::OUTPUT) + .map(|s| s.as_os_str()), )?; settings.init_precomputed(); @@ -1287,9 +1338,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .after_help(AFTER_HELP) - .override_usage(format_usage(USAGE)) + .about(get_message("sort-about")) + .after_help(get_message("sort-after-help")) + .override_usage(format_usage(&get_message("sort-usage"))) .infer_long_args(true) .disable_help_flag(true) .disable_version_flag(true) @@ -1426,8 +1477,11 @@ pub fn uu_app() -> Command { .short('o') .long(options::OUTPUT) .help("write output to FILENAME instead of stdout") + .value_parser(ValueParser::os_string()) .value_name("FILENAME") - .value_hint(clap::ValueHint::FilePath), + .value_hint(clap::ValueHint::FilePath) + // To detect multiple occurrences and raise an error + .action(ArgAction::Append), ) .arg( Arg::new(options::REVERSE) @@ -1509,9 +1563,8 @@ pub fn uu_app() -> Command { .arg( Arg::new(options::FILES0_FROM) .long(options::FILES0_FROM) - .help("read input from the files specified by NUL-terminated NUL_FILES") - .value_name("NUL_FILES") - .action(ArgAction::Append) + .help("read input from the files specified by NUL-terminated NUL_FILE") + .value_name("NUL_FILE") .value_parser(ValueParser::os_string()) .value_hint(clap::ValueHint::FilePath), ) @@ -1852,7 +1905,7 @@ fn print_sorted<'a, T: Iterator>>( ) -> UResult<()> { let output_name = output .as_output_name() - .unwrap_or("standard output") + .unwrap_or(OsStr::new("standard output")) .to_owned(); let ctx = || format!("write failed: {}", output_name.maybe_quote()); @@ -1866,13 +1919,12 @@ fn print_sorted<'a, T: Iterator>>( fn open(path: impl AsRef) -> UResult> { let path = path.as_ref(); - if path == "-" { + if path == STDIN_FILE { let stdin = stdin(); return Ok(Box::new(stdin) as Box); } let path = Path::new(path); - match File::open(path) { Ok(f) => Ok(Box::new(f) as Box), Err(error) => Err(SortError::ReadFailed { @@ -1883,6 +1935,25 @@ fn open(path: impl AsRef) -> UResult> { } } +fn open_with_open_failed_error(path: impl AsRef) -> UResult> { + // On error, returns an OpenFailed error instead of a ReadFailed error + let path = path.as_ref(); + if path == STDIN_FILE { + let stdin = stdin(); + return Ok(Box::new(stdin) as Box); + } + + let path = Path::new(path); + match File::open(path) { + Ok(f) => Ok(Box::new(f) as Box), + Err(error) => Err(SortError::OpenFailed { + path: path.to_owned(), + error, + } + .into()), + } +} + fn format_error_message(error: &ParseSizeError, s: &str, option: &str) -> String { // NOTE: // GNU's sort echos affected flag, -S or --buffer-size, depending on user's selection diff --git a/src/uu/split/locales/en-US.ftl b/src/uu/split/locales/en-US.ftl new file mode 100644 index 00000000000..0b870d63386 --- /dev/null +++ b/src/uu/split/locales/en-US.ftl @@ -0,0 +1,16 @@ +split-about = Create output files containing consecutive or interleaved sections of input +split-usage = split [OPTION]... [INPUT [PREFIX]] +split-after-help = Output fixed-size pieces of INPUT to PREFIXaa, PREFIXab, ...; default size is 1000, and default PREFIX is 'x'. With no INPUT, or when INPUT is -, read standard input. + + The SIZE argument is an integer and optional unit (example: 10K is 10*1024). + Units are K,M,G,T,P,E,Z,Y,R,Q (powers of 1024) or KB,MB,... (powers of 1000). + Binary prefixes can be used, too: KiB=K, MiB=M, and so on. + + CHUNKS may be: + + - N split into N files based on size of input + - K/N output Kth of N to stdout + - l/N split into N files without splitting lines/records + - l/K/N output Kth of N to stdout without splitting lines/records + - r/N like 'l' but use round robin distribution + - r/K/N likewise but only output Kth of N to stdout diff --git a/src/uu/split/split.md b/src/uu/split/split.md deleted file mode 100644 index 836e3a0c69a..00000000000 --- a/src/uu/split/split.md +++ /dev/null @@ -1,26 +0,0 @@ - - -# split - -``` -split [OPTION]... [INPUT [PREFIX]] -``` - -Create output files containing consecutive or interleaved sections of input - -## After Help - -Output fixed-size pieces of INPUT to PREFIXaa, PREFIXab, ...; default size is 1000, and default PREFIX is 'x'. With no INPUT, or when INPUT is -, read standard input. - -The SIZE argument is an integer and optional unit (example: 10K is 10*1024). -Units are K,M,G,T,P,E,Z,Y,R,Q (powers of 1024) or KB,MB,... (powers of 1000). -Binary prefixes can be used, too: KiB=K, MiB=M, and so on. - -CHUNKS may be: - -- N split into N files based on size of input -- K/N output Kth of N to stdout -- l/N split into N files without splitting lines/records -- l/K/N output Kth of N to stdout without splitting lines/records -- r/N like 'l' but use round robin distribution -- r/K/N likewise but only output Kth of N to stdout diff --git a/src/uu/split/src/split.rs b/src/uu/split/src/split.rs index 64548ea387d..e026aaa2e00 100644 --- a/src/uu/split/src/split.rs +++ b/src/uu/split/src/split.rs @@ -24,8 +24,9 @@ use uucore::display::Quotable; use uucore::error::{FromIo, UIoError, UResult, USimpleError, UUsageError}; use uucore::parser::parse_size::parse_size_u64; +use uucore::format_usage; +use uucore::locale::get_message; use uucore::uio_error; -use uucore::{format_usage, help_about, help_section, help_usage}; static OPT_BYTES: &str = "bytes"; static OPT_LINE_BYTES: &str = "line-bytes"; @@ -46,10 +47,6 @@ static OPT_IO_BLKSIZE: &str = "-io-blksize"; static ARG_INPUT: &str = "input"; static ARG_PREFIX: &str = "prefix"; -const ABOUT: &str = help_about!("split.md"); -const USAGE: &str = help_usage!("split.md"); -const AFTER_HELP: &str = help_section!("after help", "split.md"); - #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let (args, obs_lines) = handle_obsolete(args); @@ -229,9 +226,9 @@ fn handle_preceding_options( pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .after_help(AFTER_HELP) - .override_usage(format_usage(USAGE)) + .about(get_message("split-about")) + .after_help(get_message("split-after-help")) + .override_usage(format_usage(&get_message("split-usage"))) .infer_long_args(true) // strategy (mutually exclusive) .arg( diff --git a/src/uu/stat/locales/en-US.ftl b/src/uu/stat/locales/en-US.ftl new file mode 100644 index 00000000000..440e80e3474 --- /dev/null +++ b/src/uu/stat/locales/en-US.ftl @@ -0,0 +1,54 @@ +stat-about = Display file or file system status. +stat-usage = stat [OPTION]... FILE... +stat-after-help = Valid format sequences for files (without `--file-system`): + + -`%a`: access rights in octal (note '#' and '0' printf flags) + -`%A`: access rights in human readable form + -`%b`: number of blocks allocated (see %B) + -`%B`: the size in bytes of each block reported by %b + -`%C`: SELinux security context string + -`%d`: device number in decimal + -`%D`: device number in hex + -`%f`: raw mode in hex + -`%F`: file type + -`%g`: group ID of owner + -`%G`: group name of owner + -`%h`: number of hard links + -`%i`: inode number + -`%m`: mount point + -`%n`: file name + -`%N`: quoted file name with dereference (follow) if symbolic link + -`%o`: optimal I/O transfer size hint + -`%s`: total size, in bytes + -`%t`: major device type in hex, for character/block device special files + -`%T`: minor device type in hex, for character/block device special files + -`%u`: user ID of owner + -`%U`: user name of owner + -`%w`: time of file birth, human-readable; - if unknown + -`%W`: time of file birth, seconds since Epoch; 0 if unknown + -`%x`: time of last access, human-readable + -`%X`: time of last access, seconds since Epoch + -`%y`: time of last data modification, human-readable + + -`%Y`: time of last data modification, seconds since Epoch + -`%z`: time of last status change, human-readable + -`%Z`: time of last status change, seconds since Epoch + + Valid format sequences for file systems: + + -`%a`: free blocks available to non-superuser + -`%b`: total data blocks in file system + -`%c`: total file nodes in file system + -`%d`: free file nodes in file system + -`%f`: free blocks in file system + -`%i`: file system ID in hex + -`%l`: maximum length of filenames + -`%n`: file name + -`%s`: block size (for faster transfers) + -`%S`: fundamental block size (for block counts) + -`%t`: file system type in hex + -`%T`: file system type in human readable form + + NOTE: your shell may have its own version of stat, which usually supersedes + the version described here. Please refer to your shell's documentation + for details about the options it supports. diff --git a/src/uu/stat/src/stat.rs b/src/uu/stat/src/stat.rs index 16a96c3807d..2d8871d4044 100644 --- a/src/uu/stat/src/stat.rs +++ b/src/uu/stat/src/stat.rs @@ -13,9 +13,7 @@ use uucore::fsext::{ BirthTime, FsMeta, StatFs, pretty_filetype, pretty_fstype, read_fs_list, statfs, }; use uucore::libc::mode_t; -use uucore::{ - entries, format_usage, help_about, help_section, help_usage, show_error, show_warning, -}; +use uucore::{entries, format_usage, show_error, show_warning}; use chrono::{DateTime, Local}; use clap::{Arg, ArgAction, ArgMatches, Command}; @@ -28,9 +26,7 @@ use std::os::unix::prelude::OsStrExt; use std::path::Path; use std::{env, fs}; -const ABOUT: &str = help_about!("stat.md"); -const USAGE: &str = help_usage!("stat.md"); -const LONG_USAGE: &str = help_section!("long usage", "stat.md"); +use uucore::locale::get_message; mod options { pub const DEREFERENCE: &str = "dereference"; @@ -1135,7 +1131,9 @@ impl Stater { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().after_help(LONG_USAGE).try_get_matches_from(args)?; + let matches = uu_app() + .after_help(get_message("stat-after-help")) + .try_get_matches_from(args)?; let stater = Stater::new(&matches)?; let exit_status = stater.exec(); @@ -1149,8 +1147,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("stat-about")) + .override_usage(format_usage(&get_message("stat-usage"))) .infer_long_args(true) .arg( Arg::new(options::DEREFERENCE) diff --git a/src/uu/stat/stat.md b/src/uu/stat/stat.md deleted file mode 100644 index ac63edd18d2..00000000000 --- a/src/uu/stat/stat.md +++ /dev/null @@ -1,61 +0,0 @@ -# stat - -``` -stat [OPTION]... FILE... -``` - -Display file or file system status. - -## Long Usage - -Valid format sequences for files (without `--file-system`): - -- `%a`: access rights in octal (note '#' and '0' printf flags) -- `%A`: access rights in human readable form -- `%b`: number of blocks allocated (see %B) -- `%B`: the size in bytes of each block reported by %b -- `%C`: SELinux security context string -- `%d`: device number in decimal -- `%D`: device number in hex -- `%f`: raw mode in hex -- `%F`: file type -- `%g`: group ID of owner -- `%G`: group name of owner -- `%h`: number of hard links -- `%i`: inode number -- `%m`: mount point -- `%n`: file name -- `%N`: quoted file name with dereference (follow) if symbolic link -- `%o`: optimal I/O transfer size hint -- `%s`: total size, in bytes -- `%t`: major device type in hex, for character/block device special files -- `%T`: minor device type in hex, for character/block device special files -- `%u`: user ID of owner -- `%U`: user name of owner -- `%w`: time of file birth, human-readable; - if unknown -- `%W`: time of file birth, seconds since Epoch; 0 if unknown -- `%x`: time of last access, human-readable -- `%X`: time of last access, seconds since Epoch -- `%y`: time of last data modification, human-readable -- `%Y`: time of last data modification, seconds since Epoch -- `%z`: time of last status change, human-readable -- `%Z`: time of last status change, seconds since Epoch - -Valid format sequences for file systems: - -- `%a`: free blocks available to non-superuser -- `%b`: total data blocks in file system -- `%c`: total file nodes in file system -- `%d`: free file nodes in file system -- `%f`: free blocks in file system -- `%i`: file system ID in hex -- `%l`: maximum length of filenames -- `%n`: file name -- `%s`: block size (for faster transfers) -- `%S`: fundamental block size (for block counts) -- `%t`: file system type in hex -- `%T`: file system type in human readable form - -NOTE: your shell may have its own version of stat, which usually supersedes -the version described here. Please refer to your shell's documentation -for details about the options it supports. diff --git a/src/uu/stdbuf/Cargo.toml b/src/uu/stdbuf/Cargo.toml index 5f9e8ffd772..bcfc9fb9411 100644 --- a/src/uu/stdbuf/Cargo.toml +++ b/src/uu/stdbuf/Cargo.toml @@ -19,12 +19,10 @@ path = "src/stdbuf.rs" [dependencies] clap = { workspace = true } +libstdbuf = { package = "uu_stdbuf_libstdbuf", path = "src/libstdbuf" } tempfile = { workspace = true } uucore = { workspace = true, features = ["parser"] } -[build-dependencies] -libstdbuf = { version = "0.1.0", package = "uu_stdbuf_libstdbuf", path = "src/libstdbuf" } - [[bin]] name = "stdbuf" path = "src/main.rs" diff --git a/src/uu/stdbuf/build.rs b/src/uu/stdbuf/build.rs index b31a32235a8..52202ed3345 100644 --- a/src/uu/stdbuf/build.rs +++ b/src/uu/stdbuf/build.rs @@ -2,14 +2,20 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (ToDO) dylib libstdbuf deps liblibstdbuf +// spell-checker:ignore (ToDO) bindeps dylib libstdbuf deps liblibstdbuf use std::env; -use std::env::current_exe; use std::fs; use std::path::Path; +use std::process::Command; -#[cfg(not(any(target_vendor = "apple", target_os = "windows")))] +#[cfg(any( + target_os = "linux", + target_os = "android", + target_os = "freebsd", + target_os = "netbsd", + target_os = "dragonfly" +))] mod platform { pub const DYLIB_EXT: &str = ".so"; } @@ -19,31 +25,83 @@ mod platform { pub const DYLIB_EXT: &str = ".dylib"; } -#[cfg(target_os = "windows")] -mod platform { - pub const DYLIB_EXT: &str = ".dll"; -} - fn main() { - let current_exe = current_exe().unwrap(); + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=src/libstdbuf/src/libstdbuf.rs"); + + let out_dir = env::var("OUT_DIR").expect("OUT_DIR not set"); + let target = env::var("TARGET").unwrap_or_else(|_| "unknown".to_string()); + + // Create a separate build directory for libstdbuf to avoid conflicts + let build_dir = Path::new(&out_dir).join("libstdbuf-build"); + fs::create_dir_all(&build_dir).expect("Failed to create build directory"); + + // Get the cargo executable + let cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()); + + // This manual cargo call ensures that libstdbuf is built before stdbuf.rs is compiled, which is necessary + // for include_bytes!(..."/libstdbuf.so") to work. + // In the future, "bindeps" should be used to simplify the code and avoid the manual cargo call, + // however this is available only in cargo nightly at the moment. + // See the tracking issue: https://github.com/rust-lang/cargo/issues/9096 + let mut cmd = Command::new(&cargo); + cmd.env_clear().envs(env::vars()); + cmd.current_dir(Path::new("src/libstdbuf")).args([ + "build", + "--target-dir", + build_dir.to_str().unwrap(), + ]); + + // Get the current profile + let profile = env::var("PROFILE").unwrap_or_else(|_| "debug".to_string()); + + // Pass the release flag if we're in release mode + if profile == "release" || profile == "bench" { + cmd.arg("--release"); + } + + // Pass the target architecture if we're cross-compiling + if !target.is_empty() && target != "unknown" { + cmd.arg("--target").arg(&target); + } - let out_dir_string = env::var("OUT_DIR").unwrap(); - let out_dir = Path::new(&out_dir_string); + let status = cmd.status().expect("Failed to build libstdbuf"); + assert!(status.success(), "Failed to build libstdbuf"); - let deps_dir = current_exe.ancestors().nth(3).unwrap().join("deps"); - dbg!(&deps_dir); + // Copy the built library to OUT_DIR for include_bytes! to find + let lib_name = format!("libstdbuf{}", platform::DYLIB_EXT); + let dest_path = Path::new(&out_dir).join(format!("libstdbuf{}", platform::DYLIB_EXT)); - let libstdbuf = deps_dir - .read_dir() - .unwrap() - .flatten() - .find(|entry| { - let n = entry.file_name(); - let name = n.to_string_lossy(); + // Check multiple possible locations for the built library + let possible_paths = if !target.is_empty() && target != "unknown" { + vec![ + build_dir.join(&target).join(&profile).join(&lib_name), + build_dir + .join(&target) + .join(&profile) + .join("deps") + .join(&lib_name), + ] + } else { + vec![ + build_dir.join(&profile).join(&lib_name), + build_dir.join(&profile).join("deps").join(&lib_name), + ] + }; - name.starts_with("liblibstdbuf") && name.ends_with(platform::DYLIB_EXT) - }) - .expect("unable to find libstdbuf"); + // Try to find the library in any of the possible locations + let mut found = false; + for source_path in &possible_paths { + if source_path.exists() { + fs::copy(source_path, &dest_path).expect("Failed to copy libstdbuf library"); + found = true; + break; + } + } - fs::copy(libstdbuf.path(), out_dir.join("libstdbuf.so")).unwrap(); + assert!( + found, + "Could not find built libstdbuf library. Searched in: {:?}.", + possible_paths + ); } diff --git a/src/uu/stdbuf/locales/en-US.ftl b/src/uu/stdbuf/locales/en-US.ftl new file mode 100644 index 00000000000..fb968f98548 --- /dev/null +++ b/src/uu/stdbuf/locales/en-US.ftl @@ -0,0 +1,16 @@ +stdbuf-about = Run COMMAND, with modified buffering operations for its standard streams. + + Mandatory arguments to long options are mandatory for short options too. +stdbuf-usage = stdbuf [OPTION]... COMMAND +stdbuf-after-help = If MODE is 'L' the corresponding stream will be line buffered. + This option is invalid with standard input. + + If MODE is '0' the corresponding stream will be unbuffered. + + Otherwise, MODE is a number which may be followed by one of the following: + + KB 1000, K 1024, MB 1000*1000, M 1024*1024, and so on for G, T, P, E, Z, Y. + In this case the corresponding stream will be fully buffered with the buffer size set to MODE bytes. + + NOTE: If COMMAND adjusts the buffering of its standard streams (tee does for e.g.) then that will override corresponding settings changed by stdbuf. + Also some filters (like dd and cat etc.) don't use streams for I/O, and are thus unaffected by stdbuf settings. diff --git a/src/uu/stdbuf/src/libstdbuf/Cargo.toml b/src/uu/stdbuf/src/libstdbuf/Cargo.toml index 3f8511ffaef..6460c441eec 100644 --- a/src/uu/stdbuf/src/libstdbuf/Cargo.toml +++ b/src/uu/stdbuf/src/libstdbuf/Cargo.toml @@ -11,16 +11,10 @@ categories.workspace = true edition.workspace = true [lib] -name = "libstdbuf" +name = "stdbuf" path = "src/libstdbuf.rs" -crate-type = [ - "cdylib", - "rlib", -] # XXX: note: the rlib is just to prevent Cargo from spitting out a warning +crate-type = ["cdylib"] [dependencies] -cpp = "0.5.10" +ctor = { workspace = true } libc = { workspace = true } - -[build-dependencies] -cpp_build = "0.5.10" diff --git a/src/uu/stdbuf/src/libstdbuf/build.rs b/src/uu/stdbuf/src/libstdbuf/build.rs index 6dcd6a86912..505cdf68aec 100644 --- a/src/uu/stdbuf/src/libstdbuf/build.rs +++ b/src/uu/stdbuf/src/libstdbuf/build.rs @@ -4,8 +4,18 @@ // file that was distributed with this source code. // spell-checker:ignore (ToDO) libstdbuf -use cpp_build::Config; +use std::env; fn main() { - Config::new().pic(true).build("src/libstdbuf.rs"); + // Make sure we're building position-independent code for use with LD_PRELOAD + println!("cargo:rustc-link-arg=-fPIC"); + + let target = env::var("TARGET").unwrap_or_else(|_| "unknown".to_string()); + // Ensure the library doesn't have any undefined symbols (-z flag not supported on macOS) + if !target.contains("apple-darwin") { + println!("cargo:rustc-link-arg=-z"); + println!("cargo:rustc-link-arg=defs"); + } + + println!("cargo:rerun-if-changed=src/libstdbuf.rs"); } diff --git a/src/uu/stdbuf/src/libstdbuf/src/libstdbuf.rs b/src/uu/stdbuf/src/libstdbuf/src/libstdbuf.rs index b151ce68632..d99509880b7 100644 --- a/src/uu/stdbuf/src/libstdbuf/src/libstdbuf.rs +++ b/src/uu/stdbuf/src/libstdbuf/src/libstdbuf.rs @@ -2,34 +2,80 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (ToDO) IOFBF IOLBF IONBF cstdio setvbuf +// spell-checker:ignore (ToDO) IOFBF IOLBF IONBF setvbuf stderrp stdinp stdoutp -use cpp::cpp; +use ctor::ctor; use libc::{_IOFBF, _IOLBF, _IONBF, FILE, c_char, c_int, fileno, size_t}; use std::env; use std::ptr; -cpp! {{ - #include +// This runs automatically when the library is loaded via LD_PRELOAD +#[ctor] +fn init() { + unsafe { __stdbuf() }; +} - extern "C" { - void __stdbuf(void); +/// # Safety +/// This function is unsafe because it calls a C API +#[unsafe(no_mangle)] +pub unsafe extern "C" fn __stdbuf_get_stdin() -> *mut FILE { + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + { + unsafe extern "C" { + fn __stdinp() -> *mut FILE; + } + unsafe { __stdinp() } + } - void __attribute((constructor)) - __stdbuf_init(void) { - __stdbuf(); + #[cfg(not(any(target_os = "macos", target_os = "freebsd")))] + { + unsafe extern "C" { + static mut stdin: *mut FILE; } + unsafe { stdin } + } +} - FILE *__stdbuf_get_stdin() { return stdin; } - FILE *__stdbuf_get_stdout() { return stdout; } - FILE *__stdbuf_get_stderr() { return stderr; } +/// # Safety +/// This function is unsafe because it calls a C API +#[unsafe(no_mangle)] +pub unsafe extern "C" fn __stdbuf_get_stdout() -> *mut FILE { + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + { + unsafe extern "C" { + fn __stdoutp() -> *mut FILE; + } + unsafe { __stdoutp() } } -}} -unsafe extern "C" { - fn __stdbuf_get_stdin() -> *mut FILE; - fn __stdbuf_get_stdout() -> *mut FILE; - fn __stdbuf_get_stderr() -> *mut FILE; + #[cfg(not(any(target_os = "macos", target_os = "freebsd")))] + { + unsafe extern "C" { + static mut stdout: *mut FILE; + } + unsafe { stdout } + } +} + +/// # Safety +/// This function is unsafe because it calls a C API +#[unsafe(no_mangle)] +pub unsafe extern "C" fn __stdbuf_get_stderr() -> *mut FILE { + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + { + unsafe extern "C" { + fn __stderrp() -> *mut FILE; + } + unsafe { __stderrp() } + } + + #[cfg(not(any(target_os = "macos", target_os = "freebsd")))] + { + unsafe extern "C" { + static mut stderr: *mut FILE; + } + unsafe { stderr } + } } fn set_buffer(stream: *mut FILE, value: &str) { @@ -61,7 +107,9 @@ fn set_buffer(stream: *mut FILE, value: &str) { } /// # Safety -/// ToDO ... (safety note) +/// This function is intended to be called automatically when the library is loaded via LD_PRELOAD. +/// It assumes that the standard streams are valid and that calling setvbuf on them is safe. +/// The caller must ensure this function is only called in a compatible runtime environment. #[unsafe(no_mangle)] pub unsafe extern "C" fn __stdbuf() { if let Ok(val) = env::var("_STDBUF_E") { diff --git a/src/uu/stdbuf/src/stdbuf.rs b/src/uu/stdbuf/src/stdbuf.rs index 3b5c3fb9dbb..9c2698a97f8 100644 --- a/src/uu/stdbuf/src/stdbuf.rs +++ b/src/uu/stdbuf/src/stdbuf.rs @@ -14,12 +14,10 @@ use std::process; use tempfile::TempDir; use tempfile::tempdir; use uucore::error::{FromIo, UClapError, UResult, USimpleError, UUsageError}; +use uucore::format_usage; use uucore::parser::parse_size::parse_size_u64; -use uucore::{format_usage, help_about, help_section, help_usage}; -const ABOUT: &str = help_about!("stdbuf.md"); -const USAGE: &str = help_usage!("stdbuf.md"); -const LONG_HELP: &str = help_section!("after help", "stdbuf.md"); +use uucore::locale::get_message; mod options { pub const INPUT: &str = "input"; @@ -31,8 +29,18 @@ mod options { pub const COMMAND: &str = "command"; } +#[cfg(any( + target_os = "linux", + target_os = "android", + target_os = "freebsd", + target_os = "netbsd", + target_os = "dragonfly" +))] const STDBUF_INJECT: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/libstdbuf.so")); +#[cfg(target_vendor = "apple")] +const STDBUF_INJECT: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/libstdbuf.dylib")); + enum BufferType { Default, Line, @@ -194,9 +202,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .after_help(LONG_HELP) - .override_usage(format_usage(USAGE)) + .about(get_message("stdbuf-about")) + .after_help(get_message("stdbuf-after-help")) + .override_usage(format_usage(&get_message("stdbuf-usage"))) .trailing_var_arg(true) .infer_long_args(true) .arg( diff --git a/src/uu/stdbuf/stdbuf.md b/src/uu/stdbuf/stdbuf.md deleted file mode 100644 index e0062e6271a..00000000000 --- a/src/uu/stdbuf/stdbuf.md +++ /dev/null @@ -1,24 +0,0 @@ -# stdbuf - -``` -stdbuf [OPTION]... COMMAND -``` - -Run `COMMAND`, with modified buffering operations for its standard streams. - -Mandatory arguments to long options are mandatory for short options too. - -## After Help - -If `MODE` is 'L' the corresponding stream will be line buffered. -This option is invalid with standard input. - -If `MODE` is '0' the corresponding stream will be unbuffered. - -Otherwise, `MODE` is a number which may be followed by one of the following: - -KB 1000, K 1024, MB 1000*1000, M 1024*1024, and so on for G, T, P, E, Z, Y. -In this case the corresponding stream will be fully buffered with the buffer size set to `MODE` bytes. - -NOTE: If `COMMAND` adjusts the buffering of its standard streams (`tee` does for e.g.) then that will override corresponding settings changed by `stdbuf`. -Also some filters (like `dd` and `cat` etc.) don't use streams for I/O, and are thus unaffected by `stdbuf` settings. diff --git a/src/uu/stty/locales/en-US.ftl b/src/uu/stty/locales/en-US.ftl new file mode 100644 index 00000000000..06249f06ab4 --- /dev/null +++ b/src/uu/stty/locales/en-US.ftl @@ -0,0 +1,4 @@ +stty-about = Print or change terminal characteristics. +stty-usage = stty [-F DEVICE | --file=DEVICE] [SETTING]... + stty [-F DEVICE | --file=DEVICE] [-a|--all] + stty [-F DEVICE | --file=DEVICE] [-g|--save] diff --git a/src/uu/stty/src/flags.rs b/src/uu/stty/src/flags.rs index 79c85ceb257..d08029b5f17 100644 --- a/src/uu/stty/src/flags.rs +++ b/src/uu/stty/src/flags.rs @@ -26,6 +26,31 @@ use nix::sys::termios::{ SpecialCharacterIndices as S, }; +pub enum AllFlags<'a> { + #[cfg(any( + target_os = "freebsd", + target_os = "dragonfly", + target_os = "ios", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd" + ))] + Baud(u32), + #[cfg(not(any( + target_os = "freebsd", + target_os = "dragonfly", + target_os = "ios", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd" + )))] + Baud(BaudRate), + ControlFlags((&'a Flag, bool)), + InputFlags((&'a Flag, bool)), + LocalFlags((&'a Flag, bool)), + OutputFlags((&'a Flag, bool)), +} + pub const CONTROL_FLAGS: &[Flag] = &[ Flag::new("parenb", C::PARENB), Flag::new("parodd", C::PARODD), diff --git a/src/uu/stty/src/stty.rs b/src/uu/stty/src/stty.rs index 5cc24a5968e..6fb06acb8c7 100644 --- a/src/uu/stty/src/stty.rs +++ b/src/uu/stty/src/stty.rs @@ -3,10 +3,11 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore clocal erange tcgetattr tcsetattr tcsanow tiocgwinsz tiocswinsz cfgetospeed cfsetospeed ushort vmin vtime +// spell-checker:ignore clocal erange tcgetattr tcsetattr tcsanow tiocgwinsz tiocswinsz cfgetospeed cfsetospeed ushort vmin vtime cflag lflag mod flags; +use crate::flags::AllFlags; use clap::{Arg, ArgAction, ArgMatches, Command}; use nix::libc::{O_NONBLOCK, TIOCGWINSZ, TIOCSWINSZ, c_ushort}; use nix::sys::termios::{ @@ -16,12 +17,12 @@ use nix::sys::termios::{ use nix::{ioctl_read_bad, ioctl_write_ptr_bad}; use std::fs::File; use std::io::{self, Stdout, stdout}; -use std::ops::ControlFlow; use std::os::fd::{AsFd, BorrowedFd}; use std::os::unix::fs::OpenOptionsExt; use std::os::unix::io::{AsRawFd, RawFd}; use uucore::error::{UResult, USimpleError}; -use uucore::{format_usage, help_about, help_usage}; +use uucore::format_usage; +use uucore::locale::get_message; #[cfg(not(any( target_os = "freebsd", @@ -34,8 +35,7 @@ use uucore::{format_usage, help_about, help_usage}; use flags::BAUD_RATES; use flags::{CONTROL_CHARS, CONTROL_FLAGS, INPUT_FLAGS, LOCAL_FLAGS, OUTPUT_FLAGS}; -const USAGE: &str = help_usage!("stty.md"); -const SUMMARY: &str = help_about!("stty.md"); +const ASCII_DEL: u8 = 127; #[derive(Clone, Copy, Debug)] pub struct Flag { @@ -103,6 +103,22 @@ enum Device { Stdout(Stdout), } +enum ControlCharMappingError { + IntOutOfRange, + MultipleChars, +} + +enum ArgOptions<'a> { + Flags(AllFlags<'a>), + Mapping((SpecialCharacterIndices, u8)), +} + +impl<'a> From> for ArgOptions<'a> { + fn from(flag: AllFlags<'a>) -> Self { + ArgOptions::Flags(flag) + } +} + impl AsFd for Device { fn as_fd(&self) -> BorrowedFd<'_> { match self { @@ -142,7 +158,18 @@ impl<'a> Options<'a> { .custom_flags(O_NONBLOCK) .open(f)?, ), - None => Device::Stdout(stdout()), + // default to /dev/tty, if that does not exist then default to stdout + None => { + if let Ok(f) = std::fs::OpenOptions::new() + .read(true) + .custom_flags(O_NONBLOCK) + .open("/dev/tty") + { + Device::File(f) + } else { + Device::Stdout(stdout()) + } + } }, settings: matches .get_many::(options::SETTINGS) @@ -199,19 +226,59 @@ fn stty(opts: &Options) -> UResult<()> { )); } - // TODO: Figure out the right error message for when tcgetattr fails - let mut termios = tcgetattr(opts.file.as_fd()).expect("Could not get terminal attributes"); - - if let Some(settings) = &opts.settings { - for setting in settings { - if let ControlFlow::Break(false) = apply_setting(&mut termios, setting) { - return Err(USimpleError::new( - 1, - format!("invalid argument '{setting}'"), - )); + let mut valid_args: Vec = Vec::new(); + + if let Some(args) = &opts.settings { + let mut args_iter = args.iter(); + // iterate over args: skip to next arg if current one is a control char + while let Some(arg) = args_iter.next() { + // control char + if let Some(char_index) = cc_to_index(arg) { + if let Some(mapping) = args_iter.next() { + let cc_mapping = string_to_control_char(mapping).map_err(|e| { + let message = match e { + ControlCharMappingError::IntOutOfRange => format!( + "invalid integer argument: '{mapping}': Value too large for defined data type" + ), + ControlCharMappingError::MultipleChars => { + format!("invalid integer argument: '{mapping}'") + } + }; + USimpleError::new(1, message) + })?; + valid_args.push(ArgOptions::Mapping((char_index, cc_mapping))); + } else { + return Err(USimpleError::new(1, format!("missing argument to '{arg}'"))); + } + // non control char flag + } else if let Some(flag) = string_to_flag(arg) { + let remove_group = match flag { + AllFlags::Baud(_) => false, + AllFlags::ControlFlags((flag, remove)) => check_flag_group(flag, remove), + AllFlags::InputFlags((flag, remove)) => check_flag_group(flag, remove), + AllFlags::LocalFlags((flag, remove)) => check_flag_group(flag, remove), + AllFlags::OutputFlags((flag, remove)) => check_flag_group(flag, remove), + }; + if remove_group { + return Err(USimpleError::new(1, format!("invalid argument '{arg}'"))); + } + valid_args.push(flag.into()); + // not a valid control char or flag + } else { + return Err(USimpleError::new(1, format!("invalid argument '{arg}'"))); } } + // TODO: Figure out the right error message for when tcgetattr fails + let mut termios = tcgetattr(opts.file.as_fd()).expect("Could not get terminal attributes"); + + // iterate over valid_args, match on the arg type, do the matching apply function + for arg in &valid_args { + match arg { + ArgOptions::Mapping(mapping) => apply_char_mapping(&mut termios, mapping), + ArgOptions::Flags(flag) => apply_setting(&mut termios, flag), + } + } tcsetattr( opts.file.as_fd(), nix::sys::termios::SetArg::TCSANOW, @@ -219,11 +286,17 @@ fn stty(opts: &Options) -> UResult<()> { ) .expect("Could not write terminal attributes"); } else { + // TODO: Figure out the right error message for when tcgetattr fails + let termios = tcgetattr(opts.file.as_fd()).expect("Could not get terminal attributes"); print_settings(&termios, opts).expect("TODO: make proper error here from nix error"); } Ok(()) } +fn check_flag_group(flag: &Flag, remove: bool) -> bool { + remove && flag.group.is_some() +} + fn print_terminal_size(termios: &Termios, opts: &Options) -> nix::Result<()> { let speed = cfgetospeed(termios); @@ -274,6 +347,70 @@ fn print_terminal_size(termios: &Termios, opts: &Options) -> nix::Result<()> { Ok(()) } +fn cc_to_index(option: &str) -> Option { + for cc in CONTROL_CHARS { + if option == cc.0 { + return Some(cc.1); + } + } + None +} + +// return Some(flag) if the input is a valid flag, None if not +fn string_to_flag(option: &str) -> Option { + // BSDs use a u32 for the baud rate, so any decimal number applies. + #[cfg(any( + target_os = "freebsd", + target_os = "dragonfly", + target_os = "ios", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd" + ))] + if let Ok(n) = option.parse::() { + return Some(AllFlags::Baud(n)); + } + + #[cfg(not(any( + target_os = "freebsd", + target_os = "dragonfly", + target_os = "ios", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd" + )))] + for (text, baud_rate) in BAUD_RATES { + if *text == option { + return Some(AllFlags::Baud(*baud_rate)); + } + } + + let remove = option.starts_with('-'); + let name = option.trim_start_matches('-'); + + for cflag in CONTROL_FLAGS { + if name == cflag.name { + return Some(AllFlags::ControlFlags((cflag, remove))); + } + } + for iflag in INPUT_FLAGS { + if name == iflag.name { + return Some(AllFlags::InputFlags((iflag, remove))); + } + } + for lflag in LOCAL_FLAGS { + if name == lflag.name { + return Some(AllFlags::LocalFlags((lflag, remove))); + } + } + for oflag in OUTPUT_FLAGS { + if name == oflag.name { + return Some(AllFlags::OutputFlags((oflag, remove))); + } + } + None +} + fn control_char_to_string(cc: nix::libc::cc_t) -> nix::Result { if cc == 0 { return Ok("".to_string()); @@ -381,55 +518,25 @@ fn print_flags(termios: &Termios, opts: &Options, flags: &[Flag< } /// Apply a single setting -/// -/// The value inside the `Break` variant of the `ControlFlow` indicates whether -/// the setting has been applied. -fn apply_setting(termios: &mut Termios, s: &str) -> ControlFlow { - apply_baud_rate_flag(termios, s)?; - - let (remove, name) = match s.strip_prefix('-') { - Some(s) => (true, s), - None => (false, s), - }; - apply_flag(termios, CONTROL_FLAGS, name, remove)?; - apply_flag(termios, INPUT_FLAGS, name, remove)?; - apply_flag(termios, OUTPUT_FLAGS, name, remove)?; - apply_flag(termios, LOCAL_FLAGS, name, remove)?; - ControlFlow::Break(false) -} - -/// Apply a flag to a slice of flags -/// -/// The value inside the `Break` variant of the `ControlFlow` indicates whether -/// the setting has been applied. -fn apply_flag( - termios: &mut Termios, - flags: &[Flag], - input: &str, - remove: bool, -) -> ControlFlow { - for Flag { - name, flag, group, .. - } in flags - { - if input == *name { - // Flags with groups cannot be removed - // Since the name matches, we can short circuit and don't have to check the other flags. - if remove && group.is_some() { - return ControlFlow::Break(false); - } - // If there is a group, the bits for that group should be cleared before applying the flag - if let Some(group) = group { - group.apply(termios, false); - } - flag.apply(termios, !remove); - return ControlFlow::Break(true); +fn apply_setting(termios: &mut Termios, setting: &AllFlags) { + match setting { + AllFlags::Baud(_) => apply_baud_rate_flag(termios, setting), + AllFlags::ControlFlags((setting, disable)) => { + setting.flag.apply(termios, !disable); + } + AllFlags::InputFlags((setting, disable)) => { + setting.flag.apply(termios, !disable); + } + AllFlags::LocalFlags((setting, disable)) => { + setting.flag.apply(termios, !disable); + } + AllFlags::OutputFlags((setting, disable)) => { + setting.flag.apply(termios, !disable); } } - ControlFlow::Continue(()) } -fn apply_baud_rate_flag(termios: &mut Termios, input: &str) -> ControlFlow { +fn apply_baud_rate_flag(termios: &mut Termios, input: &AllFlags) { // BSDs use a u32 for the baud rate, so any decimal number applies. #[cfg(any( target_os = "freebsd", @@ -439,9 +546,8 @@ fn apply_baud_rate_flag(termios: &mut Termios, input: &str) -> ControlFlow target_os = "netbsd", target_os = "openbsd" ))] - if let Ok(n) = input.parse::() { - cfsetospeed(termios, n).expect("Failed to set baud rate"); - return ControlFlow::Break(true); + if let AllFlags::Baud(n) = input { + cfsetospeed(termios, *n).expect("Failed to set baud rate"); } // Other platforms use an enum. @@ -453,20 +559,67 @@ fn apply_baud_rate_flag(termios: &mut Termios, input: &str) -> ControlFlow target_os = "netbsd", target_os = "openbsd" )))] - for (text, baud_rate) in BAUD_RATES { - if *text == input { - cfsetospeed(termios, *baud_rate).expect("Failed to set baud rate"); - return ControlFlow::Break(true); + if let AllFlags::Baud(br) = input { + cfsetospeed(termios, *br).expect("Failed to set baud rate"); + } +} + +fn apply_char_mapping(termios: &mut Termios, mapping: &(SpecialCharacterIndices, u8)) { + termios.control_chars[mapping.0 as usize] = mapping.1; +} + +// GNU stty defines some valid values for the control character mappings +// 1. Standard character, can be a a single char (ie 'C') or hat notation (ie '^C') +// 2. Integer +// a. hexadecimal, prefixed by '0x' +// b. octal, prefixed by '0' +// c. decimal, no prefix +// 3. Disabling the control character: '^-' or 'undef' +// +// This function returns the ascii value of valid control chars, or ControlCharMappingError if invalid +fn string_to_control_char(s: &str) -> Result { + if s == "undef" || s == "^-" { + return Ok(0); + } + + // try to parse integer (hex, octal, or decimal) + let ascii_num = if let Some(hex) = s.strip_prefix("0x") { + u32::from_str_radix(hex, 16).ok() + } else if let Some(octal) = s.strip_prefix("0") { + u32::from_str_radix(octal, 8).ok() + } else { + s.parse::().ok() + }; + + if let Some(val) = ascii_num { + if val > 255 { + return Err(ControlCharMappingError::IntOutOfRange); + } else { + return Ok(val as u8); + } + } + // try to parse ^ or just + let mut chars = s.chars(); + match (chars.next(), chars.next()) { + (Some('^'), Some(c)) => { + // special case: ascii value of '^?' is greater than '?' + if c == '?' { + return Ok(ASCII_DEL); + } + // subtract by '@' to turn the char into the ascii value of '^' + Ok((c.to_ascii_uppercase() as u8).wrapping_sub(b'@')) } + (Some(c), None) => Ok(c as u8), + (Some(_), Some(_)) => Err(ControlCharMappingError::MultipleChars), + _ => unreachable!("No arguments provided: must have been caught earlier"), } - ControlFlow::Continue(()) } pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .override_usage(format_usage(USAGE)) - .about(SUMMARY) + .override_usage(format_usage(&get_message("stty-usage"))) + .about(get_message("stty-about")) .infer_long_args(true) .arg( Arg::new(options::ALL) diff --git a/src/uu/stty/stty.md b/src/uu/stty/stty.md deleted file mode 100644 index 6aa1decf5e7..00000000000 --- a/src/uu/stty/stty.md +++ /dev/null @@ -1,9 +0,0 @@ -# stty - -``` -stty [-F DEVICE | --file=DEVICE] [SETTING]... -stty [-F DEVICE | --file=DEVICE] [-a|--all] -stty [-F DEVICE | --file=DEVICE] [-g|--save] -``` - -Print or change terminal characteristics. diff --git a/src/uu/sum/locales/en-US.ftl b/src/uu/sum/locales/en-US.ftl new file mode 100644 index 00000000000..0d3f34b65ee --- /dev/null +++ b/src/uu/sum/locales/en-US.ftl @@ -0,0 +1,4 @@ +sum-about = Checksum and count the blocks in a file. + + With no FILE, or when FILE is -, read standard input. +sum-usage = sum [OPTION]... [FILE]... diff --git a/src/uu/sum/src/sum.rs b/src/uu/sum/src/sum.rs index 1aec0ef98c3..0f5e6c664c6 100644 --- a/src/uu/sum/src/sum.rs +++ b/src/uu/sum/src/sum.rs @@ -11,10 +11,9 @@ use std::io::{ErrorKind, Read, Write, stdin, stdout}; use std::path::Path; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError}; -use uucore::{format_usage, help_about, help_usage, show}; +use uucore::{format_usage, show}; -const USAGE: &str = help_usage!("sum.md"); -const ABOUT: &str = help_about!("sum.md"); +use uucore::locale::get_message; fn bsd_sum(mut reader: impl Read) -> std::io::Result<(usize, u16)> { let mut buf = [0; 4096]; @@ -138,8 +137,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .override_usage(format_usage(USAGE)) - .about(ABOUT) + .override_usage(format_usage(&get_message("sum-usage"))) + .about(get_message("sum-about")) .infer_long_args(true) .arg( Arg::new(options::FILE) diff --git a/src/uu/sum/sum.md b/src/uu/sum/sum.md deleted file mode 100644 index 93dbdbf62d0..00000000000 --- a/src/uu/sum/sum.md +++ /dev/null @@ -1,9 +0,0 @@ -# sum - -``` -sum [OPTION]... [FILE]... -``` - -Checksum and count the blocks in a file. - -With no FILE, or when FILE is -, read standard input. diff --git a/src/uu/sync/Cargo.toml b/src/uu/sync/Cargo.toml index c6f876e3f0f..dc6d874348e 100644 --- a/src/uu/sync/Cargo.toml +++ b/src/uu/sync/Cargo.toml @@ -22,7 +22,7 @@ clap = { workspace = true } libc = { workspace = true } uucore = { workspace = true, features = ["wide"] } -[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies] +[target.'cfg(unix)'.dependencies] nix = { workspace = true } [target.'cfg(target_os = "windows")'.dependencies] diff --git a/src/uu/sync/locales/en-US.ftl b/src/uu/sync/locales/en-US.ftl new file mode 100644 index 00000000000..bb7bfe3a556 --- /dev/null +++ b/src/uu/sync/locales/en-US.ftl @@ -0,0 +1,2 @@ +sync-about = Synchronize cached writes to persistent storage +sync-usage = sync [OPTION]... FILE... diff --git a/src/uu/sync/src/sync.rs b/src/uu/sync/src/sync.rs index abcdf40997e..47db35c324b 100644 --- a/src/uu/sync/src/sync.rs +++ b/src/uu/sync/src/sync.rs @@ -17,10 +17,9 @@ use uucore::display::Quotable; #[cfg(any(target_os = "linux", target_os = "android"))] use uucore::error::FromIo; use uucore::error::{UResult, USimpleError}; -use uucore::{format_usage, help_about, help_usage}; +use uucore::format_usage; -const ABOUT: &str = help_about!("sync.md"); -const USAGE: &str = help_usage!("sync.md"); +use uucore::locale::get_message; pub mod options { pub static FILE_SYSTEM: &str = "file-system"; @@ -31,45 +30,32 @@ static ARG_FILES: &str = "files"; #[cfg(unix)] mod platform { + use nix::unistd::sync; #[cfg(any(target_os = "linux", target_os = "android"))] - use std::fs::File; + use nix::unistd::{fdatasync, syncfs}; #[cfg(any(target_os = "linux", target_os = "android"))] - use std::os::unix::io::AsRawFd; + use std::fs::File; use uucore::error::UResult; - /// # Safety - /// This function is unsafe because it calls `libc::sync` or `libc::syscall` which are unsafe. - pub unsafe fn do_sync() -> UResult<()> { - unsafe { - // see https://github.com/rust-lang/libc/pull/2161 - #[cfg(target_os = "android")] - libc::syscall(libc::SYS_sync); - #[cfg(not(target_os = "android"))] - libc::sync(); - } + pub fn do_sync() -> UResult<()> { + sync(); Ok(()) } #[cfg(any(target_os = "linux", target_os = "android"))] - /// # Safety - /// This function is unsafe because it calls `libc::syscall` which is unsafe. - pub unsafe fn do_syncfs(files: Vec) -> UResult<()> { + pub fn do_syncfs(files: Vec) -> UResult<()> { for path in files { let f = File::open(path).unwrap(); - let fd = f.as_raw_fd(); - unsafe { libc::syscall(libc::SYS_syncfs, fd) }; + syncfs(f)?; } Ok(()) } #[cfg(any(target_os = "linux", target_os = "android"))] - /// # Safety - /// This function is unsafe because it calls `libc::syscall` which is unsafe. - pub unsafe fn do_fdatasync(files: Vec) -> UResult<()> { + pub fn do_fdatasync(files: Vec) -> UResult<()> { for path in files { let f = File::open(path).unwrap(); - let fd = f.as_raw_fd(); - unsafe { libc::syscall(libc::SYS_fdatasync, fd) }; + fdatasync(f)?; } Ok(()) } @@ -90,17 +76,22 @@ mod platform { }; use windows_sys::Win32::System::WindowsProgramming::DRIVE_FIXED; - /// # Safety - /// This function is unsafe because it calls an unsafe function. - unsafe fn flush_volume(name: &str) -> UResult<()> { + fn get_last_error() -> u32 { + // SAFETY: `GetLastError` has no safety preconditions + unsafe { GetLastError() as u32 } + } + + fn flush_volume(name: &str) -> UResult<()> { let name_wide = name.to_wide_null(); + // SAFETY: `name` is a valid `str`, so `name_wide` is valid null-terminated UTF-16 if unsafe { GetDriveTypeW(name_wide.as_ptr()) } == DRIVE_FIXED { let sliced_name = &name[..name.len() - 1]; // eliminate trailing backslash match OpenOptions::new().write(true).open(sliced_name) { Ok(file) => { + // SAFETY: `file` is a valid `File` if unsafe { FlushFileBuffers(file.as_raw_handle() as HANDLE) } == 0 { Err(USimpleError::new( - unsafe { GetLastError() } as i32, + get_last_error() as i32, "failed to flush file buffer", )) } else { @@ -117,32 +108,32 @@ mod platform { } } - /// # Safety - /// This function is unsafe because it calls an unsafe function. - unsafe fn find_first_volume() -> UResult<(String, HANDLE)> { + fn find_first_volume() -> UResult<(String, HANDLE)> { let mut name: [u16; MAX_PATH as usize] = [0; MAX_PATH as usize]; + // SAFETY: `name` was just constructed and in scope, `len()` is its length by definition let handle = unsafe { FindFirstVolumeW(name.as_mut_ptr(), name.len() as u32) }; if handle == INVALID_HANDLE_VALUE { return Err(USimpleError::new( - unsafe { GetLastError() } as i32, + get_last_error() as i32, "failed to find first volume", )); } Ok((String::from_wide_null(&name), handle)) } - /// # Safety - /// This function is unsafe because it calls an unsafe function. - unsafe fn find_all_volumes() -> UResult> { - let (first_volume, next_volume_handle) = unsafe { find_first_volume()? }; + fn find_all_volumes() -> UResult> { + let (first_volume, next_volume_handle) = find_first_volume()?; let mut volumes = vec![first_volume]; loop { let mut name: [u16; MAX_PATH as usize] = [0; MAX_PATH as usize]; + // SAFETY: `next_volume_handle` was returned by `find_first_volume`, + // `name` was just constructed and in scope, `len()` is its length by definition if unsafe { FindNextVolumeW(next_volume_handle, name.as_mut_ptr(), name.len() as u32) } == 0 { - return match unsafe { GetLastError() } { + return match get_last_error() { ERROR_NO_MORE_FILES => { + // SAFETY: `next_volume_handle` was returned by `find_first_volume` unsafe { FindVolumeClose(next_volume_handle) }; Ok(volumes) } @@ -154,31 +145,25 @@ mod platform { } } - /// # Safety - /// This function is unsafe because it calls `find_all_volumes` which is unsafe. - pub unsafe fn do_sync() -> UResult<()> { - let volumes = unsafe { find_all_volumes()? }; + pub fn do_sync() -> UResult<()> { + let volumes = find_all_volumes()?; for vol in &volumes { - unsafe { flush_volume(vol)? }; + flush_volume(vol)?; } Ok(()) } - /// # Safety - /// This function is unsafe because it calls `find_all_volumes` which is unsafe. - pub unsafe fn do_syncfs(files: Vec) -> UResult<()> { + pub fn do_syncfs(files: Vec) -> UResult<()> { for path in files { - unsafe { - flush_volume( - Path::new(&path) - .components() - .next() - .unwrap() - .as_os_str() - .to_str() - .unwrap(), - )?; - } + flush_volume( + Path::new(&path) + .components() + .next() + .unwrap() + .as_os_str() + .to_str() + .unwrap(), + )?; } Ok(()) } @@ -236,8 +221,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("sync-about")) + .override_usage(format_usage(&get_message("sync-usage"))) .infer_long_args(true) .arg( Arg::new(options::FILE_SYSTEM) @@ -263,15 +248,15 @@ pub fn uu_app() -> Command { } fn sync() -> UResult<()> { - unsafe { platform::do_sync() } + platform::do_sync() } #[cfg(any(target_os = "linux", target_os = "android", target_os = "windows"))] fn syncfs(files: Vec) -> UResult<()> { - unsafe { platform::do_syncfs(files) } + platform::do_syncfs(files) } #[cfg(any(target_os = "linux", target_os = "android"))] fn fdatasync(files: Vec) -> UResult<()> { - unsafe { platform::do_fdatasync(files) } + platform::do_fdatasync(files) } diff --git a/src/uu/sync/sync.md b/src/uu/sync/sync.md deleted file mode 100644 index 2fdd363399f..00000000000 --- a/src/uu/sync/sync.md +++ /dev/null @@ -1,7 +0,0 @@ -# sync - -``` -sync [OPTION]... FILE... -``` - -Synchronize cached writes to persistent storage diff --git a/src/uu/tac/locales/en-US.ftl b/src/uu/tac/locales/en-US.ftl new file mode 100644 index 00000000000..28a3621f278 --- /dev/null +++ b/src/uu/tac/locales/en-US.ftl @@ -0,0 +1,2 @@ +tac-about = Write each file to standard output, last line first. +tac-usage = tac [OPTION]... [FILE]... diff --git a/src/uu/tac/src/tac.rs b/src/uu/tac/src/tac.rs index 4496c2ce120..0cb2009a988 100644 --- a/src/uu/tac/src/tac.rs +++ b/src/uu/tac/src/tac.rs @@ -17,12 +17,11 @@ use std::{ use uucore::display::Quotable; use uucore::error::UError; use uucore::error::UResult; -use uucore::{format_usage, help_about, help_usage, show}; +use uucore::{format_usage, show}; use crate::error::TacError; -static USAGE: &str = help_usage!("tac.md"); -static ABOUT: &str = help_about!("tac.md"); +use uucore::locale::get_message; mod options { pub static BEFORE: &str = "before"; @@ -58,8 +57,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .override_usage(format_usage(USAGE)) - .about(ABOUT) + .override_usage(format_usage(&get_message("tac-usage"))) + .about(get_message("tac-about")) .infer_long_args(true) .arg( Arg::new(options::BEFORE) diff --git a/src/uu/tac/tac.md b/src/uu/tac/tac.md deleted file mode 100644 index 6787b3f49eb..00000000000 --- a/src/uu/tac/tac.md +++ /dev/null @@ -1,7 +0,0 @@ -# tac - -``` -tac [OPTION]... [FILE]... -``` - -Write each file to standard output, last line first. diff --git a/src/uu/tail/locales/en-US.ftl b/src/uu/tail/locales/en-US.ftl new file mode 100644 index 00000000000..8d6394838de --- /dev/null +++ b/src/uu/tail/locales/en-US.ftl @@ -0,0 +1,6 @@ +tail-about = Print the last 10 lines of each FILE to standard output. + With more than one FILE, precede each with a header giving the file name. + With no FILE, or when FILE is -, read standard input. + + Mandatory arguments to long flags are mandatory for short flags too. +tail-usage = tail [FLAG]... [FILE]... diff --git a/src/uu/tail/src/args.rs b/src/uu/tail/src/args.rs index 61448388c29..374beffd06a 100644 --- a/src/uu/tail/src/args.rs +++ b/src/uu/tail/src/args.rs @@ -16,10 +16,9 @@ use uucore::error::{UResult, USimpleError, UUsageError}; use uucore::parser::parse_size::{ParseSizeError, parse_size_u64}; use uucore::parser::parse_time; use uucore::parser::shortcut_value_parser::ShortcutValueParser; -use uucore::{format_usage, help_about, help_usage, show_warning}; +use uucore::{format_usage, show_warning}; -const ABOUT: &str = help_about!("tail.md"); -const USAGE: &str = help_usage!("tail.md"); +use uucore::locale::get_message; pub mod options { pub mod verbosity { @@ -464,8 +463,8 @@ pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("tail-about")) + .override_usage(format_usage(&get_message("tail-usage"))) .infer_long_args(true) .arg( Arg::new(options::BYTES) diff --git a/src/uu/tail/tail.md b/src/uu/tail/tail.md deleted file mode 100644 index fefe7f6ee68..00000000000 --- a/src/uu/tail/tail.md +++ /dev/null @@ -1,11 +0,0 @@ -# tail - -``` -tail [FLAG]... [FILE]... -``` - -Print the last 10 lines of each FILE to standard output. -With more than one FILE, precede each with a header giving the file name. -With no FILE, or when FILE is -, read standard input. - -Mandatory arguments to long flags are mandatory for short flags too. diff --git a/src/uu/tee/locales/en-US.ftl b/src/uu/tee/locales/en-US.ftl new file mode 100644 index 00000000000..2f0286958af --- /dev/null +++ b/src/uu/tee/locales/en-US.ftl @@ -0,0 +1,3 @@ +tee-about = Copy standard input to each FILE, and also to standard output. +tee-usage = tee [OPTION]... [FILE]... +tee-after-help = If a FILE is -, it refers to a file named - . diff --git a/src/uu/tee/src/tee.rs b/src/uu/tee/src/tee.rs index b16caeb9327..e5269caf242 100644 --- a/src/uu/tee/src/tee.rs +++ b/src/uu/tee/src/tee.rs @@ -12,16 +12,14 @@ use std::path::PathBuf; use uucore::display::Quotable; use uucore::error::UResult; use uucore::parser::shortcut_value_parser::ShortcutValueParser; -use uucore::{format_usage, help_about, help_section, help_usage, show_error}; +use uucore::{format_usage, show_error}; // spell-checker:ignore nopipe #[cfg(unix)] use uucore::signals::{enable_pipe_errors, ignore_interrupts}; -const ABOUT: &str = help_about!("tee.md"); -const USAGE: &str = help_usage!("tee.md"); -const AFTER_HELP: &str = help_section!("after help", "tee.md"); +use uucore::locale::get_message; mod options { pub const APPEND: &str = "append"; @@ -97,9 +95,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) - .after_help(AFTER_HELP) + .about(get_message("tee-about")) + .override_usage(format_usage(&get_message("tee-usage"))) + .after_help(get_message("tee-after-help")) .infer_long_args(true) // Since we use value-specific help texts for "--output-error", clap's "short help" and "long help" differ. // However, this is something that the GNU tests explicitly test for, so we *always* show the long help instead. diff --git a/src/uu/tee/tee.md b/src/uu/tee/tee.md deleted file mode 100644 index 8bf097cecd5..00000000000 --- a/src/uu/tee/tee.md +++ /dev/null @@ -1,11 +0,0 @@ -# tee - -``` -tee [OPTION]... [FILE]... -``` - -Copy standard input to each FILE, and also to standard output. - -## After Help - -If a FILE is -, it refers to a file named - . diff --git a/src/uu/test/locales/en-US.ftl b/src/uu/test/locales/en-US.ftl new file mode 100644 index 00000000000..0f7a6739417 --- /dev/null +++ b/src/uu/test/locales/en-US.ftl @@ -0,0 +1,71 @@ +test-about = Check file types and compare values. +test-usage = test EXPRESSION + test + {"[ EXPRESSION ]"} + {"[ ]"} + {"[ OPTION ]"} +test-after-help = Exit with the status determined by EXPRESSION. + + An omitted EXPRESSION defaults to false. + Otherwise, EXPRESSION is true or false and sets exit status. + + It is one of: + + - ( EXPRESSION ) EXPRESSION is true + - ! EXPRESSION EXPRESSION is false + - EXPRESSION1 -a EXPRESSION2 both EXPRESSION1 and EXPRESSION2 are true + - EXPRESSION1 -o EXPRESSION2 either EXPRESSION1 or EXPRESSION2 is true + + String operations: + - -n STRING the length of STRING is nonzero + - STRING equivalent to -n STRING + - -z STRING the length of STRING is zero + - STRING1 = STRING2 the strings are equal + - STRING1 != STRING2 the strings are not equal + + Integer comparisons: + - INTEGER1 -eq INTEGER2 INTEGER1 is equal to INTEGER2 + - INTEGER1 -ge INTEGER2 INTEGER1 is greater than or equal to INTEGER2 + - INTEGER1 -gt INTEGER2 INTEGER1 is greater than INTEGER2 + - INTEGER1 -le INTEGER2 INTEGER1 is less than or equal to INTEGER2 + - INTEGER1 -lt INTEGER2 INTEGER1 is less than INTEGER2 + - INTEGER1 -ne INTEGER2 INTEGER1 is not equal to INTEGER2 + + File operations: + - FILE1 -ef FILE2 FILE1 and FILE2 have the same device and inode numbers + - FILE1 -nt FILE2 FILE1 is newer (modification date) than FILE2 + - FILE1 -ot FILE2 FILE1 is older than FILE2 + + - -b FILE FILE exists and is block special + - -c FILE FILE exists and is character special + - -d FILE FILE exists and is a directory + - -e FILE FILE exists + - -f FILE FILE exists and is a regular file + - -g FILE FILE exists and is set-group-ID + - -G FILE FILE exists and is owned by the effective group ID + - -h FILE FILE exists and is a symbolic link (same as -L) + - -k FILE FILE exists and has its sticky bit set + - -L FILE FILE exists and is a symbolic link (same as -h) + - -N FILE FILE exists and has been modified since it was last read + - -O FILE FILE exists and is owned by the effective user ID + - -p FILE FILE exists and is a named pipe + - -r FILE FILE exists and read permission is granted + - -s FILE FILE exists and has a size greater than zero + - -S FILE FILE exists and is a socket + - -t FD file descriptor FD is opened on a terminal + - -u FILE FILE exists and its set-user-ID bit is set + - -w FILE FILE exists and write permission is granted + - -x FILE FILE exists and execute (or search) permission is granted + + Except for -h and -L, all FILE-related tests dereference (follow) symbolic links. + Beware that parentheses need to be escaped (e.g., by backslashes) for shells. + INTEGER may also be -l STRING, which evaluates to the length of STRING. + + NOTE: Binary -a and -o are inherently ambiguous. + Use test EXPR1 && test EXPR2 or test EXPR1 || test EXPR2 instead. + + NOTE: [ honors the --help and --version options, but test does not. + test treats each of those as it treats any other nonempty STRING. + + NOTE: your shell may have its own version of test and/or [, which usually supersedes the version described here. + Please refer to your shell's documentation for details about the options it supports. diff --git a/src/uu/test/src/test.rs b/src/uu/test/src/test.rs index e71e7b19166..674e70dd337 100644 --- a/src/uu/test/src/test.rs +++ b/src/uu/test/src/test.rs @@ -17,35 +17,27 @@ use std::fs; use std::os::unix::fs::MetadataExt; use uucore::display::Quotable; use uucore::error::{UResult, USimpleError}; +use uucore::format_usage; #[cfg(not(windows))] use uucore::process::{getegid, geteuid}; -use uucore::{format_usage, help_about, help_section}; -const ABOUT: &str = help_about!("test.md"); +use uucore::locale::get_message; // The help_usage method replaces util name (the first word) with {}. // And, The format_usage method replaces {} with execution_phrase ( e.g. test or [ ). // However, This test command has two util names. // So, we use test or [ instead of {} so that the usage string is correct. -const USAGE: &str = "\ -test EXPRESSION -[ -[ EXPRESSION ] -[ ] -[ OPTION -]"; // We use after_help so that this comes after the usage string (it would come before if we used about) -const AFTER_HELP: &str = help_section!("after help", "test.md"); pub fn uu_app() -> Command { // Disable printing of -h and -v as valid alternatives for --help and --version, // since we don't recognize -h and -v as help/version flags. Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) - .after_help(AFTER_HELP) + .about(get_message("test-about")) + .override_usage(format_usage(&get_message("test-usage"))) + .after_help(get_message("test-after-help")) } #[uucore::main] diff --git a/src/uu/test/test.md b/src/uu/test/test.md deleted file mode 100644 index e67eb1824ab..00000000000 --- a/src/uu/test/test.md +++ /dev/null @@ -1,79 +0,0 @@ -# test - -``` -test EXPRESSION -test -[ EXPRESSION ] -[ ] -[ OPTION -``` - -Check file types and compare values. - -## After Help - -Exit with the status determined by `EXPRESSION`. - -An omitted `EXPRESSION` defaults to false. -Otherwise, `EXPRESSION` is true or false and sets exit status. - -It is one of: - -* ( EXPRESSION ) `EXPRESSION` is true -* ! EXPRESSION `EXPRESSION` is false -* EXPRESSION1 -a EXPRESSION2 both `EXPRESSION1` and `EXPRESSION2` are true -* EXPRESSION1 -o EXPRESSION2 either `EXPRESSION1` or `EXPRESSION2` is true - -String operations: -* -n STRING the length of `STRING` is nonzero -* STRING equivalent to -n `STRING` -* -z STRING the length of `STRING` is zero -* STRING1 = STRING2 the strings are equal -* STRING1 != STRING2 the strings are not equal - -Integer comparisons: -* INTEGER1 -eq INTEGER2 `INTEGER1` is equal to `INTEGER2` -* INTEGER1 -ge INTEGER2 `INTEGER1` is greater than or equal to `INTEGER2` -* INTEGER1 -gt INTEGER2 `INTEGER1` is greater than `INTEGER2` -* INTEGER1 -le INTEGER2 `INTEGER1` is less than or equal to `INTEGER2` -* INTEGER1 -lt INTEGER2 `INTEGER1` is less than `INTEGER2` -* INTEGER1 -ne INTEGER2 `INTEGER1` is not equal to `INTEGER2` - -File operations: -* FILE1 -ef FILE2 `FILE1` and `FILE2` have the same device and inode numbers -* FILE1 -nt FILE2 `FILE1` is newer (modification date) than `FILE2` -* FILE1 -ot FILE2 `FILE1` is older than `FILE2` - -* -b FILE `FILE` exists and is block special -* -c FILE `FILE` exists and is character special -* -d FILE `FILE` exists and is a directory -* -e FILE `FILE` exists -* -f FILE `FILE` exists and is a regular file -* -g FILE `FILE` exists and is set-group-ID -* -G FILE `FILE` exists and is owned by the effective group ID -* -h FILE `FILE` exists and is a symbolic link (same as -L) -* -k FILE `FILE` exists and has its sticky bit set -* -L FILE `FILE` exists and is a symbolic link (same as -h) -* -N FILE `FILE` exists and has been modified since it was last read -* -O FILE `FILE` exists and is owned by the effective user ID -* -p FILE `FILE` exists and is a named pipe -* -r FILE `FILE` exists and read permission is granted -* -s FILE `FILE` exists and has a size greater than zero -* -S FILE `FILE` exists and is a socket -* -t FD `file` descriptor `FD` is opened on a terminal -* -u FILE `FILE` exists and its set-user-ID bit is set -* -w FILE `FILE` exists and write permission is granted -* -x FILE `FILE` exists and execute (or search) permission is granted - -Except for `-h` and `-L`, all FILE-related tests dereference (follow) symbolic links. -Beware that parentheses need to be escaped (e.g., by backslashes) for shells. -`INTEGER` may also be -l `STRING`, which evaluates to the length of `STRING`. - -NOTE: Binary `-a` and `-o` are inherently ambiguous. -Use `test EXPR1 && test EXPR2` or `test EXPR1 || test EXPR2` instead. - -NOTE: `[` honors the `--help` and `--version` options, but test does not. -test treats each of those as it treats any other nonempty `STRING`. - -NOTE: your shell may have its own version of `test` and/or `[`, which usually supersedes the version described here. -Please refer to your shell's documentation for details about the options it supports. diff --git a/src/uu/timeout/locales/en-US.ftl b/src/uu/timeout/locales/en-US.ftl new file mode 100644 index 00000000000..57126de1efc --- /dev/null +++ b/src/uu/timeout/locales/en-US.ftl @@ -0,0 +1,2 @@ +timeout-about = Start COMMAND, and kill it if still running after DURATION. +timeout-usage = timeout [OPTION] DURATION COMMAND... diff --git a/src/uu/timeout/src/timeout.rs b/src/uu/timeout/src/timeout.rs index 9de3358015c..35232d3e433 100644 --- a/src/uu/timeout/src/timeout.rs +++ b/src/uu/timeout/src/timeout.rs @@ -17,17 +17,15 @@ use uucore::error::{UClapError, UResult, USimpleError, UUsageError}; use uucore::parser::parse_time; use uucore::process::ChildExt; +use uucore::locale::get_message; #[cfg(unix)] use uucore::signals::enable_pipe_errors; use uucore::{ - format_usage, help_about, help_usage, show_error, + format_usage, show_error, signals::{signal_by_name_or_value, signal_name_by_value}, }; -const ABOUT: &str = help_about!("timeout.md"); -const USAGE: &str = help_usage!("timeout.md"); - pub mod options { pub static FOREGROUND: &str = "foreground"; pub static KILL_AFTER: &str = "kill-after"; @@ -122,8 +120,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new("timeout") .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("timeout-about")) + .override_usage(format_usage(&get_message("timeout-usage"))) .arg( Arg::new(options::FOREGROUND) .long(options::FOREGROUND) diff --git a/src/uu/timeout/timeout.md b/src/uu/timeout/timeout.md deleted file mode 100644 index f992ab32772..00000000000 --- a/src/uu/timeout/timeout.md +++ /dev/null @@ -1,7 +0,0 @@ -# timeout - -``` -timeout [OPTION] DURATION COMMAND... -``` - -Start `COMMAND`, and kill it if still running after `DURATION`. diff --git a/src/uu/touch/locales/en-US.ftl b/src/uu/touch/locales/en-US.ftl new file mode 100644 index 00000000000..a3e6d6e3e8a --- /dev/null +++ b/src/uu/touch/locales/en-US.ftl @@ -0,0 +1,2 @@ +touch-about = Update the access and modification times of each FILE to the current time. +touch-usage = touch [OPTION]... [USER] diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index 6749933f094..c12c50530de 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -23,9 +23,10 @@ use std::path::{Path, PathBuf}; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError}; use uucore::parser::shortcut_value_parser::ShortcutValueParser; -use uucore::{format_usage, help_about, help_usage, show}; +use uucore::{format_usage, show}; use crate::error::TouchError; +use uucore::locale::get_message; /// Options contains all the possible behaviors and flags for touch. /// @@ -83,9 +84,6 @@ pub enum Source { Now, } -const ABOUT: &str = help_about!("touch.md"); -const USAGE: &str = help_usage!("touch.md"); - pub mod options { // Both SOURCES and sources are needed as we need to be able to refer to the ArgGroup. pub static SOURCES: &str = "sources"; @@ -258,8 +256,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("touch-about")) + .override_usage(format_usage(&get_message("touch-usage"))) .infer_long_args(true) .disable_help_flag(true) .arg( diff --git a/src/uu/touch/touch.md b/src/uu/touch/touch.md deleted file mode 100644 index 8ce35b6868a..00000000000 --- a/src/uu/touch/touch.md +++ /dev/null @@ -1,7 +0,0 @@ -# touch - -``` -touch [OPTION]... [USER] -``` - -Update the access and modification times of each `FILE` to the current time. diff --git a/src/uu/tr/locales/en-US.ftl b/src/uu/tr/locales/en-US.ftl new file mode 100644 index 00000000000..931c6265b2b --- /dev/null +++ b/src/uu/tr/locales/en-US.ftl @@ -0,0 +1,3 @@ +tr-about = Translate or delete characters +tr-usage = tr [OPTION]... SET1 [SET2] +tr-after-help = Translate, squeeze, and/or delete characters from standard input, writing to standard output. diff --git a/src/uu/tr/src/tr.rs b/src/uu/tr/src/tr.rs index 10a636fd7b6..5259990bf69 100644 --- a/src/uu/tr/src/tr.rs +++ b/src/uu/tr/src/tr.rs @@ -18,11 +18,9 @@ use std::io::{BufWriter, Write, stdin, stdout}; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError, UUsageError}; use uucore::fs::is_stdin_directory; -use uucore::{format_usage, help_about, help_section, help_usage, os_str_as_bytes, show}; +use uucore::{format_usage, os_str_as_bytes, show}; -const ABOUT: &str = help_about!("tr.md"); -const AFTER_HELP: &str = help_section!("after help", "tr.md"); -const USAGE: &str = help_usage!("tr.md"); +use uucore::locale::get_message; mod options { pub const COMPLEMENT: &str = "complement"; @@ -34,7 +32,9 @@ mod options { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().after_help(AFTER_HELP).try_get_matches_from(args)?; + let matches = uu_app() + .after_help(get_message("tr-after-help")) + .try_get_matches_from(args)?; let delete_flag = matches.get_flag(options::DELETE); let complement_flag = matches.get_flag(options::COMPLEMENT); @@ -164,8 +164,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("tr-about")) + .override_usage(format_usage(&get_message("tr-usage"))) .infer_long_args(true) .trailing_var_arg(true) .arg( diff --git a/src/uu/tr/tr.md b/src/uu/tr/tr.md deleted file mode 100644 index 93349eeaa84..00000000000 --- a/src/uu/tr/tr.md +++ /dev/null @@ -1,11 +0,0 @@ -# tr - -``` -tr [OPTION]... SET1 [SET2] -``` - -Translate or delete characters - -## After help - -Translate, squeeze, and/or delete characters from standard input, writing to standard output. diff --git a/src/uu/true/locales/en-US.ftl b/src/uu/true/locales/en-US.ftl new file mode 100644 index 00000000000..e75e9e67088 --- /dev/null +++ b/src/uu/true/locales/en-US.ftl @@ -0,0 +1,5 @@ +true-about = Returns true, a successful exit status. + + Immediately returns with the exit status 0, except when invoked with one of the recognized + options. In those cases it will try to write the help or version text. Any IO error during this + operation causes the program to return 1 instead. diff --git a/src/uu/true/src/true.rs b/src/uu/true/src/true.rs index 29dae0ba61c..4036024a446 100644 --- a/src/uu/true/src/true.rs +++ b/src/uu/true/src/true.rs @@ -5,9 +5,8 @@ use clap::{Arg, ArgAction, Command}; use std::{ffi::OsString, io::Write}; use uucore::error::{UResult, set_exit_code}; -use uucore::help_about; -const ABOUT: &str = help_about!("true.md"); +use uucore::locale::get_message; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { @@ -43,7 +42,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) + .about(get_message("true-about")) // We provide our own help and version options, to ensure maximum compatibility with GNU. .disable_help_flag(true) .disable_version_flag(true) diff --git a/src/uu/true/true.md b/src/uu/true/true.md deleted file mode 100644 index b21c9616e14..00000000000 --- a/src/uu/true/true.md +++ /dev/null @@ -1,11 +0,0 @@ -# true - -``` -true -``` - -Returns true, a successful exit status. - -Immediately returns with the exit status `0`, except when invoked with one of the recognized -options. In those cases it will try to write the help or version text. Any IO error during this -operation causes the program to return `1` instead. diff --git a/src/uu/truncate/locales/en-US.ftl b/src/uu/truncate/locales/en-US.ftl new file mode 100644 index 00000000000..b3ae2fcc48a --- /dev/null +++ b/src/uu/truncate/locales/en-US.ftl @@ -0,0 +1,18 @@ +truncate-about = Shrink or extend the size of each file to the specified size. +truncate-usage = truncate [OPTION]... [FILE]... +truncate-after-help = SIZE is an integer with an optional prefix and optional unit. + The available units (K, M, G, T, P, E, Z, and Y) use the following format: + 'KB' => 1000 (kilobytes) + 'K' => 1024 (kibibytes) + 'MB' => 1000*1000 (megabytes) + 'M' => 1024*1024 (mebibytes) + 'GB' => 1000*1000*1000 (gigabytes) + 'G' => 1024*1024*1024 (gibibytes) + SIZE may also be prefixed by one of the following to adjust the size of each + file based on its current size: + '+' => extend by + '-' => reduce by + '<' => at most + '>' => at least + '/' => round down to multiple of + '%' => round up to multiple of diff --git a/src/uu/truncate/src/truncate.rs b/src/uu/truncate/src/truncate.rs index 056163fa3ad..461ed8057f8 100644 --- a/src/uu/truncate/src/truncate.rs +++ b/src/uu/truncate/src/truncate.rs @@ -12,8 +12,9 @@ use std::os::unix::fs::FileTypeExt; use std::path::Path; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError, UUsageError}; +use uucore::format_usage; +use uucore::locale::get_message; use uucore::parser::parse_size::{ParseSizeError, parse_size_u64}; -use uucore::{format_usage, help_about, help_section, help_usage}; #[derive(Debug, Eq, PartialEq)] enum TruncateMode { @@ -71,10 +72,6 @@ impl TruncateMode { } } -const ABOUT: &str = help_about!("truncate.md"); -const AFTER_HELP: &str = help_section!("after help", "truncate.md"); -const USAGE: &str = help_usage!("truncate.md"); - pub mod options { pub static IO_BLOCKS: &str = "io-blocks"; pub static NO_CREATE: &str = "no-create"; @@ -86,7 +83,7 @@ pub mod options { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app() - .after_help(AFTER_HELP) + .after_help(get_message("truncate-after-help")) .try_get_matches_from(args) .map_err(|e| { e.print().expect("Error writing clap::Error"); @@ -117,8 +114,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("truncate-about")) + .override_usage(format_usage(&get_message("truncate-usage"))) .infer_long_args(true) .arg( Arg::new(options::IO_BLOCKS) diff --git a/src/uu/truncate/truncate.md b/src/uu/truncate/truncate.md deleted file mode 100644 index d60a6b7b200..00000000000 --- a/src/uu/truncate/truncate.md +++ /dev/null @@ -1,26 +0,0 @@ -# truncate - -``` -truncate [OPTION]... [FILE]... -``` - -Shrink or extend the size of each file to the specified size. - -## After help - -SIZE is an integer with an optional prefix and optional unit. -The available units (K, M, G, T, P, E, Z, and Y) use the following format: - 'KB' => 1000 (kilobytes) - 'K' => 1024 (kibibytes) - 'MB' => 1000*1000 (megabytes) - 'M' => 1024*1024 (mebibytes) - 'GB' => 1000*1000*1000 (gigabytes) - 'G' => 1024*1024*1024 (gibibytes) -SIZE may also be prefixed by one of the following to adjust the size of each -file based on its current size: - '+' => extend by - '-' => reduce by - '<' => at most - '>' => at least - '/' => round down to multiple of - '%' => round up to multiple of diff --git a/src/uu/tsort/locales/en-US.ftl b/src/uu/tsort/locales/en-US.ftl new file mode 100644 index 00000000000..12cafe40c94 --- /dev/null +++ b/src/uu/tsort/locales/en-US.ftl @@ -0,0 +1,5 @@ +tsort-about = Topological sort the strings in FILE. + Strings are defined as any sequence of tokens separated by whitespace (tab, space, or newline), ordering them based on dependencies in a directed acyclic graph (DAG). + Useful for scheduling and determining execution order. + If FILE is not passed in, stdin is used instead. +tsort-usage = tsort [OPTIONS] FILE diff --git a/src/uu/tsort/src/tsort.rs b/src/uu/tsort/src/tsort.rs index 3f7e89e99ef..3a3a3c51c5a 100644 --- a/src/uu/tsort/src/tsort.rs +++ b/src/uu/tsort/src/tsort.rs @@ -9,10 +9,9 @@ use std::path::Path; use thiserror::Error; use uucore::display::Quotable; use uucore::error::{UError, UResult}; -use uucore::{format_usage, help_about, help_usage, show}; +use uucore::{format_usage, show}; -const ABOUT: &str = help_about!("tsort.md"); -const USAGE: &str = help_usage!("tsort.md"); +use uucore::locale::get_message; mod options { pub const FILE: &str = "file"; @@ -76,8 +75,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .override_usage(format_usage(USAGE)) - .about(ABOUT) + .override_usage(format_usage(&get_message("tsort-usage"))) + .about(get_message("tsort-about")) .infer_long_args(true) .arg( Arg::new(options::FILE) diff --git a/src/uu/tsort/tsort.md b/src/uu/tsort/tsort.md deleted file mode 100644 index 5effbf1e705..00000000000 --- a/src/uu/tsort/tsort.md +++ /dev/null @@ -1,10 +0,0 @@ -# tsort - -``` -tsort [OPTIONS] FILE -``` - -Topological sort the strings in FILE. -Strings are defined as any sequence of tokens separated by whitespace (tab, space, or newline), ordering them based on dependencies in a directed acyclic graph (DAG). -Useful for scheduling and determining execution order. -If FILE is not passed in, stdin is used instead. diff --git a/src/uu/tty/locales/en-US.ftl b/src/uu/tty/locales/en-US.ftl new file mode 100644 index 00000000000..8d480ec4a63 --- /dev/null +++ b/src/uu/tty/locales/en-US.ftl @@ -0,0 +1,2 @@ +tty-about = Print the file name of the terminal connected to standard input. +tty-usage = tty [OPTION]... diff --git a/src/uu/tty/src/tty.rs b/src/uu/tty/src/tty.rs index 35dc1f08678..7cad08ba8f2 100644 --- a/src/uu/tty/src/tty.rs +++ b/src/uu/tty/src/tty.rs @@ -10,10 +10,9 @@ use clap::{Arg, ArgAction, Command}; use std::io::{IsTerminal, Write}; use uucore::error::{UResult, set_exit_code}; -use uucore::{format_usage, help_about, help_usage}; +use uucore::format_usage; -const ABOUT: &str = help_about!("tty.md"); -const USAGE: &str = help_usage!("tty.md"); +use uucore::locale::get_message; mod options { pub const SILENT: &str = "silent"; @@ -58,8 +57,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("tty-about")) + .override_usage(format_usage(&get_message("tty-usage"))) .infer_long_args(true) .arg( Arg::new(options::SILENT) diff --git a/src/uu/tty/tty.md b/src/uu/tty/tty.md deleted file mode 100644 index 230399a2069..00000000000 --- a/src/uu/tty/tty.md +++ /dev/null @@ -1,7 +0,0 @@ -# tty - -``` -tty [OPTION]... -``` - -Print the file name of the terminal connected to standard input. diff --git a/src/uu/uname/locales/en-US.ftl b/src/uu/uname/locales/en-US.ftl new file mode 100644 index 00000000000..78faa1c35e4 --- /dev/null +++ b/src/uu/uname/locales/en-US.ftl @@ -0,0 +1,3 @@ +uname-about = Print certain system information. + With no OPTION, same as -s. +uname-usage = uname [OPTION]... diff --git a/src/uu/uname/src/uname.rs b/src/uu/uname/src/uname.rs index 2e2b5f42fd2..cc3a4f471eb 100644 --- a/src/uu/uname/src/uname.rs +++ b/src/uu/uname/src/uname.rs @@ -7,14 +7,12 @@ use clap::{Arg, ArgAction, Command}; use platform_info::*; +use uucore::locale::get_message; use uucore::{ error::{UResult, USimpleError}, - format_usage, help_about, help_usage, + format_usage, }; -const ABOUT: &str = help_about!("uname.md"); -const USAGE: &str = help_usage!("uname.md"); - pub mod options { pub static ALL: &str = "all"; pub static KERNEL_NAME: &str = "kernel-name"; @@ -146,8 +144,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("uname-about")) + .override_usage(format_usage(&get_message("uname-usage"))) .infer_long_args(true) .arg( Arg::new(options::ALL) diff --git a/src/uu/uname/uname.md b/src/uu/uname/uname.md deleted file mode 100644 index b90c54cfcc3..00000000000 --- a/src/uu/uname/uname.md +++ /dev/null @@ -1,8 +0,0 @@ -# uname - -``` -uname [OPTION]... -``` - -Print certain system information. -With no OPTION, same as -s. diff --git a/src/uu/unexpand/locales/en-US.ftl b/src/uu/unexpand/locales/en-US.ftl new file mode 100644 index 00000000000..b66a71494c1 --- /dev/null +++ b/src/uu/unexpand/locales/en-US.ftl @@ -0,0 +1,3 @@ +unexpand-about = Convert blanks in each FILE to tabs, writing to standard output. + With no FILE, or when FILE is -, read standard input. +unexpand-usage = unexpand [OPTION]... [FILE]... diff --git a/src/uu/unexpand/src/unexpand.rs b/src/uu/unexpand/src/unexpand.rs index fb17b971d57..1b075dd2f2c 100644 --- a/src/uu/unexpand/src/unexpand.rs +++ b/src/uu/unexpand/src/unexpand.rs @@ -15,10 +15,9 @@ use thiserror::Error; use unicode_width::UnicodeWidthChar; use uucore::display::Quotable; use uucore::error::{FromIo, UError, UResult, USimpleError}; -use uucore::{format_usage, help_about, help_usage, show}; +use uucore::{format_usage, show}; -const USAGE: &str = help_usage!("unexpand.md"); -const ABOUT: &str = help_about!("unexpand.md"); +use uucore::locale::get_message; const DEFAULT_TABSTOP: usize = 8; @@ -157,8 +156,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .override_usage(format_usage(USAGE)) - .about(ABOUT) + .override_usage(format_usage(&get_message("unexpand-usage"))) + .about(get_message("unexpand-about")) .infer_long_args(true) .arg( Arg::new(options::FILE) diff --git a/src/uu/unexpand/unexpand.md b/src/uu/unexpand/unexpand.md deleted file mode 100644 index 6fc69b93a3a..00000000000 --- a/src/uu/unexpand/unexpand.md +++ /dev/null @@ -1,8 +0,0 @@ -# unexpand - -``` -unexpand [OPTION]... [FILE]... -``` - -Convert blanks in each `FILE` to tabs, writing to standard output. -With no `FILE`, or when `FILE` is `-`, read standard input. diff --git a/src/uu/uniq/locales/en-US.ftl b/src/uu/uniq/locales/en-US.ftl new file mode 100644 index 00000000000..4ede870d18e --- /dev/null +++ b/src/uu/uniq/locales/en-US.ftl @@ -0,0 +1,7 @@ +uniq-about = Report or omit repeated lines. +uniq-usage = uniq [OPTION]... [INPUT [OUTPUT]] +uniq-after-help = Filter adjacent matching lines from INPUT (or standard input), + writing to OUTPUT (or standard output). + + Note: uniq does not detect repeated lines unless they are adjacent. + You may want to sort the input first, or use sort -u without uniq. diff --git a/src/uu/uniq/src/uniq.rs b/src/uu/uniq/src/uniq.rs index 2d54a508226..00ca6751beb 100644 --- a/src/uu/uniq/src/uniq.rs +++ b/src/uu/uniq/src/uniq.rs @@ -13,13 +13,11 @@ use std::io::{BufRead, BufReader, BufWriter, Write, stdin, stdout}; use std::num::IntErrorKind; use uucore::display::Quotable; use uucore::error::{FromIo, UError, UResult, USimpleError}; +use uucore::format_usage; use uucore::parser::shortcut_value_parser::ShortcutValueParser; use uucore::posix::{OBSOLETE, posix_version}; -use uucore::{format_usage, help_about, help_section, help_usage}; -const ABOUT: &str = help_about!("uniq.md"); -const USAGE: &str = help_usage!("uniq.md"); -const AFTER_HELP: &str = help_section!("after help", "uniq.md"); +use uucore::locale::get_message; pub mod options { pub static ALL_REPEATED: &str = "all-repeated"; @@ -593,10 +591,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("uniq-about")) + .override_usage(format_usage(&get_message("uniq-usage"))) .infer_long_args(true) - .after_help(AFTER_HELP) + .after_help(get_message("uniq-after-help")) .arg( Arg::new(options::ALL_REPEATED) .short('D') diff --git a/src/uu/uniq/uniq.md b/src/uu/uniq/uniq.md deleted file mode 100644 index 20de52395ba..00000000000 --- a/src/uu/uniq/uniq.md +++ /dev/null @@ -1,15 +0,0 @@ -# uniq - -``` -uniq [OPTION]... [INPUT [OUTPUT]] -``` - -Report or omit repeated lines. - -## After help - -Filter adjacent matching lines from `INPUT` (or standard input), -writing to `OUTPUT` (or standard output). - -Note: `uniq` does not detect repeated lines unless they are adjacent. -You may want to sort the input first, or use `sort -u` without `uniq`. diff --git a/src/uu/unlink/locales/en-US.ftl b/src/uu/unlink/locales/en-US.ftl new file mode 100644 index 00000000000..e819d905e34 --- /dev/null +++ b/src/uu/unlink/locales/en-US.ftl @@ -0,0 +1,3 @@ +unlink-about = Unlink the file at FILE. +unlink-usage = unlink FILE + unlink OPTION diff --git a/src/uu/unlink/src/unlink.rs b/src/uu/unlink/src/unlink.rs index 09a1b0f1233..418e59b6e8b 100644 --- a/src/uu/unlink/src/unlink.rs +++ b/src/uu/unlink/src/unlink.rs @@ -12,10 +12,9 @@ use clap::{Arg, Command}; use uucore::display::Quotable; use uucore::error::{FromIo, UResult}; -use uucore::{format_usage, help_about, help_usage}; +use uucore::format_usage; -const ABOUT: &str = help_about!("unlink.md"); -const USAGE: &str = help_usage!("unlink.md"); +use uucore::locale::get_message; static OPT_PATH: &str = "FILE"; #[uucore::main] @@ -30,8 +29,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("unlink-about")) + .override_usage(format_usage(&get_message("unlink-usage"))) .infer_long_args(true) .arg( Arg::new(OPT_PATH) diff --git a/src/uu/unlink/unlink.md b/src/uu/unlink/unlink.md deleted file mode 100644 index 14b023b8b66..00000000000 --- a/src/uu/unlink/unlink.md +++ /dev/null @@ -1,8 +0,0 @@ -# unlink - -``` -unlink FILE -unlink OPTION -``` - -Unlink the file at `FILE`. diff --git a/src/uu/uptime/locales/en-US.ftl b/src/uu/uptime/locales/en-US.ftl new file mode 100644 index 00000000000..6746ee712bc --- /dev/null +++ b/src/uu/uptime/locales/en-US.ftl @@ -0,0 +1,7 @@ +uptime-about = Display the current time, the length of time the system has been up, + the number of users on the system, and the average number of jobs + in the run queue over the last 1, 5 and 15 minutes. +uptime-usage = uptime [OPTION]... +uptime-about-musl-warning = Warning: When built with musl libc, the `uptime` utility may show '0 users' + due to musl's stub implementation of utmpx functions. Boot time and load averages + are still calculated using alternative mechanisms. diff --git a/src/uu/uptime/src/uptime.rs b/src/uu/uptime/src/uptime.rs index e001a64a8ef..3bf1b8d7927 100644 --- a/src/uu/uptime/src/uptime.rs +++ b/src/uu/uptime/src/uptime.rs @@ -16,25 +16,13 @@ use uucore::uptime::*; use clap::{Arg, ArgAction, Command, ValueHint, builder::ValueParser}; -use uucore::{format_usage, help_about, help_usage}; +use uucore::format_usage; +use uucore::locale::get_message; #[cfg(unix)] #[cfg(not(target_os = "openbsd"))] use uucore::utmpx::*; -#[cfg(target_env = "musl")] -const ABOUT: &str = concat!( - help_about!("uptime.md"), - "\n\nWarning: When built with musl libc, the `uptime` utility may show '0 users' \n", - "due to musl's stub implementation of utmpx functions. Boot time and load averages \n", - "are still calculated using alternative mechanisms." -); - -#[cfg(not(target_env = "musl"))] -const ABOUT: &str = help_about!("uptime.md"); - -const USAGE: &str = help_usage!("uptime.md"); - pub mod options { pub static SINCE: &str = "since"; pub static PATH: &str = "path"; @@ -76,10 +64,15 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { + #[cfg(not(target_env = "musl"))] + let about = get_message("uptime-about"); + #[cfg(target_env = "musl")] + let about = get_message("uptime-about") + &get_message("uptime-about-musl-warning"); + let cmd = Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(about) + .override_usage(format_usage(&get_message("uptime-usage"))) .infer_long_args(true) .arg( Arg::new(options::SINCE) diff --git a/src/uu/uptime/uptime.md b/src/uu/uptime/uptime.md deleted file mode 100644 index fd9c8fd2f59..00000000000 --- a/src/uu/uptime/uptime.md +++ /dev/null @@ -1,9 +0,0 @@ -# uptime - -``` -uptime [OPTION]... -``` - -Display the current time, the length of time the system has been up, -the number of users on the system, and the average number of jobs -in the run queue over the last 1, 5 and 15 minutes. diff --git a/src/uu/users/locales/en-US.ftl b/src/uu/users/locales/en-US.ftl new file mode 100644 index 00000000000..d35a99383c3 --- /dev/null +++ b/src/uu/users/locales/en-US.ftl @@ -0,0 +1,4 @@ +users-about = Print the user names of users currently logged in to the current host. +users-usage = users [FILE] +users-about-musl-warning = Warning: When built with musl libc, the `users` utility may show '0 users', + due to musl's stub implementation of utmpx functions. diff --git a/src/uu/users/src/users.rs b/src/uu/users/src/users.rs index 192ed0b579d..19b6c7c6d26 100644 --- a/src/uu/users/src/users.rs +++ b/src/uu/users/src/users.rs @@ -11,24 +11,14 @@ use std::path::Path; use clap::builder::ValueParser; use clap::{Arg, Command}; use uucore::error::UResult; -use uucore::{format_usage, help_about, help_usage}; +use uucore::format_usage; #[cfg(target_os = "openbsd")] use utmp_classic::{UtmpEntry, parse_from_path}; #[cfg(not(target_os = "openbsd"))] use uucore::utmpx::{self, Utmpx}; -#[cfg(target_env = "musl")] -const ABOUT: &str = concat!( - help_about!("users.md"), - "\n\nWarning: When built with musl libc, the `users` utility may show '0 users' \n", - "due to musl's stub implementation of utmpx functions." -); - -#[cfg(not(target_env = "musl"))] -const ABOUT: &str = help_about!("users.md"); - -const USAGE: &str = help_usage!("users.md"); +use uucore::locale::get_message; #[cfg(target_os = "openbsd")] const OPENBSD_UTMP_FILE: &str = "/var/run/utmp"; @@ -95,10 +85,15 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { + #[cfg(not(target_env = "musl"))] + let about = get_message("users-about"); + #[cfg(target_env = "musl")] + let about = get_message("users-about") + &get_message("users-about-musl-warning"); + Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(about) + .override_usage(format_usage(&get_message("users-usage"))) .infer_long_args(true) .arg( Arg::new(ARG_FILE) diff --git a/src/uu/users/users.md b/src/uu/users/users.md deleted file mode 100644 index 7b126a0f0be..00000000000 --- a/src/uu/users/users.md +++ /dev/null @@ -1,7 +0,0 @@ -# users - -``` -users [FILE] -``` - -Print the user names of users currently logged in to the current host. diff --git a/src/uu/vdir/locales/en-US.ftl b/src/uu/vdir/locales/en-US.ftl new file mode 100644 index 00000000000..f0d63274eb8 --- /dev/null +++ b/src/uu/vdir/locales/en-US.ftl @@ -0,0 +1,5 @@ +vdir-about = List directory contents. + Ignore files and directories starting with a '.' by default. + + Mandatory arguments to long options are mandatory for short options too. +vdir-usage = vdir [OPTION]... [FILE]... diff --git a/src/uu/vdir/vdir.md b/src/uu/vdir/vdir.md deleted file mode 100644 index c7a0cae3643..00000000000 --- a/src/uu/vdir/vdir.md +++ /dev/null @@ -1,10 +0,0 @@ -# vdir - -``` -vdir [OPTION]... [FILE]... -``` - -List directory contents. -Ignore files and directories starting with a '.' by default. - -Mandatory arguments to long options are mandatory for short options too. diff --git a/src/uu/wc/locales/en-US.ftl b/src/uu/wc/locales/en-US.ftl new file mode 100644 index 00000000000..7eb458ec24a --- /dev/null +++ b/src/uu/wc/locales/en-US.ftl @@ -0,0 +1,3 @@ +wc-about = Display newline, word, and byte counts for each FILE, and a total line if + more than one FILE is specified. With no FILE, or when FILE is -, read standard input. +wc-usage = wc [OPTION]... [FILE]... diff --git a/src/uu/wc/src/wc.rs b/src/uu/wc/src/wc.rs index 47abfe2102f..f3414bb43e9 100644 --- a/src/uu/wc/src/wc.rs +++ b/src/uu/wc/src/wc.rs @@ -24,10 +24,11 @@ use clap::{Arg, ArgAction, ArgMatches, Command, builder::ValueParser}; use thiserror::Error; use unicode_width::UnicodeWidthChar; use utf8::{BufReadDecoder, BufReadDecoderError}; +use uucore::locale::get_message; use uucore::{ error::{FromIo, UError, UResult}, - format_usage, help_about, help_usage, + format_usage, parser::shortcut_value_parser::ShortcutValueParser, quoting_style::{self, QuotingStyle}, show, @@ -113,9 +114,6 @@ impl<'a> Settings<'a> { } } -const ABOUT: &str = help_about!("wc.md"); -const USAGE: &str = help_usage!("wc.md"); - mod options { pub static BYTES: &str = "bytes"; pub static CHAR: &str = "chars"; @@ -397,8 +395,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("wc-about")) + .override_usage(format_usage(&get_message("wc-usage"))) .infer_long_args(true) .args_override_self(true) .arg( diff --git a/src/uu/wc/wc.md b/src/uu/wc/wc.md deleted file mode 100644 index c7aaf8545f2..00000000000 --- a/src/uu/wc/wc.md +++ /dev/null @@ -1,8 +0,0 @@ -# wc - -``` -wc [OPTION]... [FILE]... -``` - -Display newline, word, and byte counts for each FILE, and a total line if -more than one FILE is specified. With no FILE, or when FILE is -, read standard input. diff --git a/src/uu/who/locales/en-US.ftl b/src/uu/who/locales/en-US.ftl new file mode 100644 index 00000000000..7ed207f7201 --- /dev/null +++ b/src/uu/who/locales/en-US.ftl @@ -0,0 +1,5 @@ +who-about = Print information about users who are currently logged in. +who-usage = who [OPTION]... [ FILE | ARG1 ARG2 ] +who-about-musl-warning = Note: When built with musl libc, the `who` utility will not display any + information about logged-in users. This is due to musl's stub implementation + of `utmpx` functions, which prevents access to the necessary data. diff --git a/src/uu/who/src/who.rs b/src/uu/who/src/who.rs index 2203bbbd119..ea3cc987382 100644 --- a/src/uu/who/src/who.rs +++ b/src/uu/who/src/who.rs @@ -6,7 +6,8 @@ // spell-checker:ignore (ToDO) ttyname hostnames runlevel mesg wtmp statted boottime deadprocs initspawn clockchange curr runlvline pidstr exitstr hoststr use clap::{Arg, ArgAction, Command}; -use uucore::{format_usage, help_about, help_usage}; +use uucore::format_usage; +use uucore::locale::get_message; mod platform; @@ -28,19 +29,6 @@ mod options { pub const FILE: &str = "FILE"; // if length=1: FILE, if length=2: ARG1 ARG2 } -#[cfg(target_env = "musl")] -const ABOUT: &str = concat!( - help_about!("who.md"), - "\n\nNote: When built with musl libc, the `who` utility will not display any \n", - "information about logged-in users. This is due to musl's stub implementation \n", - "of `utmpx` functions, which prevents access to the necessary data." -); - -#[cfg(not(target_env = "musl"))] -const ABOUT: &str = help_about!("who.md"); - -const USAGE: &str = help_usage!("who.md"); - #[cfg(target_os = "linux")] static RUNLEVEL_HELP: &str = "print current runlevel"; #[cfg(not(target_os = "linux"))] @@ -50,10 +38,15 @@ static RUNLEVEL_HELP: &str = "print current runlevel (This is meaningless on non use platform::uumain; pub fn uu_app() -> Command { + #[cfg(not(target_env = "musl"))] + let about = get_message("who-about"); + #[cfg(target_env = "musl")] + let about = get_message("who-about") + &get_message("who-about-musl-warning"); + Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(about) + .override_usage(format_usage(&get_message("who-usage"))) .infer_long_args(true) .arg( Arg::new(options::ALL) diff --git a/src/uu/who/who.md b/src/uu/who/who.md deleted file mode 100644 index 6adc2659733..00000000000 --- a/src/uu/who/who.md +++ /dev/null @@ -1,8 +0,0 @@ -# who - -``` -who [OPTION]... [ FILE | ARG1 ARG2 ] -``` - -Print information about users who are currently logged in. - diff --git a/src/uu/whoami/locales/en-US.ftl b/src/uu/whoami/locales/en-US.ftl new file mode 100644 index 00000000000..aedac774c31 --- /dev/null +++ b/src/uu/whoami/locales/en-US.ftl @@ -0,0 +1 @@ +whoami-about = Print the current username. diff --git a/src/uu/whoami/src/whoami.rs b/src/uu/whoami/src/whoami.rs index a1fe6e62239..701d3a107ea 100644 --- a/src/uu/whoami/src/whoami.rs +++ b/src/uu/whoami/src/whoami.rs @@ -9,13 +9,10 @@ use clap::Command; use uucore::display::println_verbatim; use uucore::error::{FromIo, UResult}; -use uucore::{format_usage, help_about, help_usage}; +use uucore::locale::get_message; mod platform; -const ABOUT: &str = help_about!("whoami.md"); -const USAGE: &str = help_usage!("whoami.md"); - #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { uu_app().try_get_matches_from(args)?; @@ -32,7 +29,7 @@ pub fn whoami() -> UResult { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("whoami-about")) + .override_usage(uucore::util_name()) .infer_long_args(true) } diff --git a/src/uu/whoami/whoami.md b/src/uu/whoami/whoami.md deleted file mode 100644 index 6106d24ee7e..00000000000 --- a/src/uu/whoami/whoami.md +++ /dev/null @@ -1,7 +0,0 @@ -# whoami - -``` -whoami -``` - -Print the current username. diff --git a/src/uu/yes/locales/en-US.ftl b/src/uu/yes/locales/en-US.ftl new file mode 100644 index 00000000000..d4d0806b18b --- /dev/null +++ b/src/uu/yes/locales/en-US.ftl @@ -0,0 +1,2 @@ +yes-about = Repeatedly display a line with STRING (or 'y') +yes-usage = yes [STRING]... diff --git a/src/uu/yes/src/yes.rs b/src/uu/yes/src/yes.rs index df77be28947..add3f70276d 100644 --- a/src/uu/yes/src/yes.rs +++ b/src/uu/yes/src/yes.rs @@ -10,12 +10,11 @@ use std::error::Error; use std::ffi::OsString; use std::io::{self, Write}; use uucore::error::{UResult, USimpleError}; +use uucore::format_usage; #[cfg(unix)] use uucore::signals::enable_pipe_errors; -use uucore::{format_usage, help_about, help_usage}; -const ABOUT: &str = help_about!("yes.md"); -const USAGE: &str = help_usage!("yes.md"); +use uucore::locale::get_message; // it's possible that using a smaller or larger buffer might provide better performance on some // systems, but honestly this is good enough @@ -39,8 +38,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .about(get_message("yes-about")) + .override_usage(format_usage(&get_message("yes-usage"))) .arg( Arg::new("STRING") .value_parser(ValueParser::os_string()) diff --git a/src/uu/yes/yes.md b/src/uu/yes/yes.md deleted file mode 100644 index ad2da7dc9af..00000000000 --- a/src/uu/yes/yes.md +++ /dev/null @@ -1,7 +0,0 @@ -# yes - -``` -yes [STRING]... -``` - -Repeatedly display a line with STRING (or 'y') diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index be2db18e578..cd4f2e1c008 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -21,7 +21,6 @@ path = "src/lib/lib.rs" [dependencies] chrono = { workspace = true, optional = true } -chrono-tz = { workspace = true, optional = true } clap = { workspace = true } uucore_procs = { workspace = true } number_prefix = { workspace = true } @@ -29,7 +28,6 @@ dns-lookup = { workspace = true, optional = true } dunce = { version = "1.0.4", optional = true } wild = "2.2.1" glob = { workspace = true, optional = true } -iana-time-zone = { workspace = true, optional = true } itertools = { workspace = true, optional = true } time = { workspace = true, optional = true, features = [ "formatting", @@ -62,6 +60,7 @@ selinux = { workspace = true, optional = true } # Fluent dependencies fluent-bundle = { workspace = true } fluent = { workspace = true } +fluent-syntax = { workspace = true } unic-langid = { workspace = true } thiserror = { workspace = true } [target.'cfg(unix)'.dependencies] @@ -138,6 +137,5 @@ utf8 = [] utmpx = ["time", "time/macros", "libc", "dns-lookup"] version-cmp = [] wide = [] -custom-tz-fmt = ["chrono", "chrono-tz", "iana-time-zone"] tty = [] uptime = ["chrono", "libc", "windows-sys", "utmpx", "utmp-classic"] diff --git a/src/uucore/src/lib/features.rs b/src/uucore/src/lib/features.rs index 257043e00ba..44db5307180 100644 --- a/src/uucore/src/lib/features.rs +++ b/src/uucore/src/lib/features.rs @@ -14,8 +14,6 @@ pub mod buf_copy; pub mod checksum; #[cfg(feature = "colors")] pub mod colors; -#[cfg(feature = "custom-tz-fmt")] -pub mod custom_tz_fmt; #[cfg(feature = "encoding")] pub mod encoding; #[cfg(feature = "extendedbigdecimal")] diff --git a/src/uucore/src/lib/features/colors.rs b/src/uucore/src/lib/features/colors.rs index 885ae2fe967..2792de7c5a0 100644 --- a/src/uucore/src/lib/features/colors.rs +++ b/src/uucore/src/lib/features/colors.rs @@ -38,6 +38,7 @@ pub static TERMS: &[&str] = &[ "terminator", "tmux*", "vt100", + "wezterm*", "xterm*", ]; diff --git a/src/uucore/src/lib/features/custom_tz_fmt.rs b/src/uucore/src/lib/features/custom_tz_fmt.rs deleted file mode 100644 index 0d2b6aebe41..00000000000 --- a/src/uucore/src/lib/features/custom_tz_fmt.rs +++ /dev/null @@ -1,60 +0,0 @@ -// This file is part of the uutils coreutils package. -// -// For the full copyright and license information, please view the LICENSE -// file that was distributed with this source code. - -use chrono::{TimeZone, Utc}; -use chrono_tz::{OffsetName, Tz}; -use iana_time_zone::get_timezone; - -/// Get the alphabetic abbreviation of the current timezone. -/// -/// For example, "UTC" or "CET" or "PDT" -fn timezone_abbreviation() -> String { - let tz = match std::env::var("TZ") { - // TODO Support other time zones... - Ok(s) if s == "UTC0" || s.is_empty() => Tz::Etc__UTC, - _ => match get_timezone() { - Ok(tz_str) => tz_str.parse().unwrap(), - Err(_) => Tz::Etc__UTC, - }, - }; - - let offset = tz.offset_from_utc_date(&Utc::now().date_naive()); - offset.abbreviation().unwrap_or("UTC").to_string() -} - -/// Adapt the given string to be accepted by the chrono library crate. -/// -/// # Arguments -/// -/// fmt: the format of the string -/// -/// # Return -/// -/// A string that can be used as parameter of the chrono functions that use formats -pub fn custom_time_format(fmt: &str) -> String { - // TODO - Revisit when chrono 0.5 is released. https://github.com/chronotope/chrono/issues/970 - // chrono crashes on %#z, but it's the same as %z anyway. - // GNU `date` uses `%N` for nano seconds, however the `chrono` crate uses `%f`. - fmt.replace("%#z", "%z") - .replace("%N", "%f") - .replace("%Z", timezone_abbreviation().as_ref()) -} - -#[cfg(test)] -mod tests { - use super::{custom_time_format, timezone_abbreviation}; - - #[test] - fn test_custom_time_format() { - assert_eq!(custom_time_format("%Y-%m-%d %H-%M-%S"), "%Y-%m-%d %H-%M-%S"); - assert_eq!(custom_time_format("%d-%m-%Y %H-%M-%S"), "%d-%m-%Y %H-%M-%S"); - assert_eq!(custom_time_format("%Y-%m-%d %H-%M-%S"), "%Y-%m-%d %H-%M-%S"); - assert_eq!( - custom_time_format("%Y-%m-%d %H-%M-%S.%N"), - "%Y-%m-%d %H-%M-%S.%f" - ); - assert_eq!(custom_time_format("%Z"), timezone_abbreviation()); - } -} diff --git a/src/uucore/src/lib/features/parser/num_parser.rs b/src/uucore/src/lib/features/parser/num_parser.rs index 3ee07e41357..84aa82bdd79 100644 --- a/src/uucore/src/lib/features/parser/num_parser.rs +++ b/src/uucore/src/lib/features/parser/num_parser.rs @@ -5,7 +5,9 @@ //! Utilities for parsing numbers in various formats -// spell-checker:ignore powf copysign prec inity infinit infs bigdecimal extendedbigdecimal biguint underflowed +// spell-checker:ignore powf copysign prec ilog inity infinit infs bigdecimal extendedbigdecimal biguint underflowed muls + +use std::num::NonZeroU64; use bigdecimal::{ BigDecimal, Context, @@ -375,30 +377,69 @@ fn make_error<'a>(overflow: bool, negative: bool) -> ExtendedParserError<'a, Ext } } -/// Compute bd**exp using exponentiation by squaring algorithm, while maintaining the -/// precision specified in ctx (the number of digits would otherwise explode). -// TODO: We do lose a little bit of precision, and the last digits may not be correct. -// TODO: Upstream this to bigdecimal-rs. -fn pow_with_context(bd: BigDecimal, exp: u32, ctx: &bigdecimal::Context) -> BigDecimal { +// Compute bd**exp using exponentiation by squaring algorithm, while maintaining the +// precision specified in ctx (the number of digits would otherwise explode). +// +// Algorithm comes from https://en.wikipedia.org/wiki/Exponentiation_by_squaring +// +// TODO: Still pending discussion in https://github.com/akubera/bigdecimal-rs/issues/147, +// we do lose a little bit of precision, and the last digits may not be correct. +// Note: This has been copied from the latest revision in https://github.com/akubera/bigdecimal-rs/pull/148, +// so it's using minimum Rust version of `bigdecimal-rs`. +fn pow_with_context(bd: &BigDecimal, exp: i64, ctx: &Context) -> BigDecimal { if exp == 0 { return 1.into(); } - fn trim_precision(bd: BigDecimal, ctx: &bigdecimal::Context) -> BigDecimal { - if bd.digits() > ctx.precision().get() { - bd.with_precision_round(ctx.precision(), ctx.rounding_mode()) + // When performing a multiplication between 2 numbers, we may lose up to 2 digits + // of precision. + // "Proof": https://github.com/akubera/bigdecimal-rs/issues/147#issuecomment-2793431202 + const MARGIN_PER_MUL: u64 = 2; + // When doing many multiplication, we still introduce additional errors, add 1 more digit + // per 10 multiplications. + const MUL_PER_MARGIN_EXTRA: u64 = 10; + + fn trim_precision(bd: BigDecimal, ctx: &Context, margin: u64) -> BigDecimal { + let prec = ctx.precision().get() + margin; + if bd.digits() > prec { + bd.with_precision_round(NonZeroU64::new(prec).unwrap(), ctx.rounding_mode()) } else { bd } } - let bd = trim_precision(bd, ctx); - let ret = if exp % 2 == 0 { - pow_with_context(bd.square(), exp / 2, ctx) + // Count the number of multiplications we're going to perform, one per "1" binary digit + // in exp, and the number of times we can divide exp by 2. + let mut n = exp.unsigned_abs(); + // Note: 63 - n.leading_zeros() == n.ilog2, but that's only available in recent Rust versions. + let muls = (n.count_ones() + (63 - n.leading_zeros()) - 1) as u64; + // Note: div_ceil would be nice to use here, but only available in recent Rust versions. + // (see note above about minimum Rust version in use) + let margin_extra = (muls + MUL_PER_MARGIN_EXTRA / 2) / MUL_PER_MARGIN_EXTRA; + let mut margin = margin_extra + MARGIN_PER_MUL * muls; + + let mut bd_y: BigDecimal = 1.into(); + let mut bd_x = if exp >= 0 { + bd.clone() } else { - &bd * pow_with_context(bd.square(), (exp - 1) / 2, ctx) + bd.inverse_with_context(&ctx.with_precision( + NonZeroU64::new(ctx.precision().get() + margin + MARGIN_PER_MUL).unwrap(), + )) }; - trim_precision(ret, ctx) + + while n > 1 { + if n % 2 == 1 { + bd_y = trim_precision(&bd_x * bd_y, ctx, margin); + margin -= MARGIN_PER_MUL; + n -= 1; + } + bd_x = trim_precision(bd_x.square(), ctx, margin); + margin -= MARGIN_PER_MUL; + n /= 2; + } + debug_assert_eq!(margin, margin_extra); + + trim_precision(bd_x * bd_y, ctx, 0) } // Construct an ExtendedBigDecimal based on parsed data @@ -444,22 +485,20 @@ fn construct_extended_big_decimal<'a>( let bd = BigDecimal::from_bigint(signed_digits, 0) / BigDecimal::from_bigint(BigInt::from(16).pow(scale as u32), 0); - let abs_exponent = exponent.abs(); - // Again, pow "only" supports u32 values. Just overflow/underflow if the value provided - // is > 2**32 or < 2**-32. - if abs_exponent > u32::MAX.into() { - return Err(make_error(exponent.is_positive(), negative)); - } + // pow_with_context "only" supports i64 values. Just overflow/underflow if the value provided + // is > 2**64 or < 2**-64. + let exponent = match exponent.to_i64() { + Some(exp) => exp, + None => { + return Err(make_error(exponent.is_positive(), negative)); + } + }; // Confusingly, exponent is in base 2 for hex floating point numbers. + let base: BigDecimal = 2.into(); // Note: We cannot overflow/underflow BigDecimal here, as we will not be able to reach the // maximum/minimum scale (i64 range). - let base: BigDecimal = if !exponent.is_negative() { - 2.into() - } else { - BigDecimal::from(2).inverse() - }; - let pow2 = pow_with_context(base, abs_exponent.to_u32().unwrap(), &Context::default()); + let pow2 = pow_with_context(&base, exponent, &Context::default()); bd * pow2 } else { @@ -960,14 +999,14 @@ mod tests { assert_eq!( Ok(ExtendedBigDecimal::BigDecimal( // Wolfram Alpha says 9.8162042336235053508313854078782835648991393286913072670026492205522618203568834202759669215027003865... × 10^903089986 - BigDecimal::from_str("9.816204233623505350831385407878283564899139328691307267002649220552261820356883420275966921514831318e+903089986").unwrap() + BigDecimal::from_str("9.816204233623505350831385407878283564899139328691307267002649220552261820356883420275966921502700387e+903089986").unwrap() )), ExtendedBigDecimal::extended_parse("0x1p3000000000") ); assert_eq!( Ok(ExtendedBigDecimal::BigDecimal( // Wolfram Alpha says 1.3492131462369983551036088935544888715959511045742395978049631768570509541390540646442193112226520316... × 10^-9030900 - BigDecimal::from_str("1.349213146236998355103608893554488871595951104574239597804963176857050954139054064644219311222656999e-9030900").unwrap() + BigDecimal::from_str("1.349213146236998355103608893554488871595951104574239597804963176857050954139054064644219311222652032e-9030900").unwrap() )), // Couldn't get a answer from Wolfram Alpha for smaller negative exponents ExtendedBigDecimal::extended_parse("0x1p-30000000") @@ -975,21 +1014,21 @@ mod tests { // ExtendedBigDecimal overflow/underflow assert!(matches!( - ExtendedBigDecimal::extended_parse(&format!("0x1p{}", u32::MAX as u64 + 1)), + ExtendedBigDecimal::extended_parse(&format!("0x1p{}", u64::MAX as u128 + 1)), Err(ExtendedParserError::Overflow(ExtendedBigDecimal::Infinity)) )); assert!(matches!( - ExtendedBigDecimal::extended_parse(&format!("-0x100P{}", u32::MAX as u64 + 1)), + ExtendedBigDecimal::extended_parse(&format!("-0x100P{}", u64::MAX as u128 + 1)), Err(ExtendedParserError::Overflow( ExtendedBigDecimal::MinusInfinity )) )); assert!(matches!( - ExtendedBigDecimal::extended_parse(&format!("0x1p-{}", u32::MAX as u64 + 1)), + ExtendedBigDecimal::extended_parse(&format!("0x1p-{}", u64::MAX as u128 + 1)), Err(ExtendedParserError::Underflow(ebd)) if ebd == ExtendedBigDecimal::zero() )); assert!(matches!( - ExtendedBigDecimal::extended_parse(&format!("-0x0.100p-{}", u32::MAX as u64 + 1)), + ExtendedBigDecimal::extended_parse(&format!("-0x0.100p-{}", u64::MAX as u128 + 1)), Err(ExtendedParserError::Underflow( ExtendedBigDecimal::MinusZero )) diff --git a/src/uucore/src/lib/features/proc_info.rs b/src/uucore/src/lib/features/proc_info.rs index 7ea54a85a3e..3f5334d1007 100644 --- a/src/uucore/src/lib/features/proc_info.rs +++ b/src/uucore/src/lib/features/proc_info.rs @@ -164,7 +164,7 @@ impl ProcessInformation { let pid = { value .iter() - .last() + .next_back() .ok_or(io::ErrorKind::Other)? .to_str() .ok_or(io::ErrorKind::InvalidData)? diff --git a/src/uucore/src/lib/features/tty.rs b/src/uucore/src/lib/features/tty.rs index 6854ba16449..221ee442d63 100644 --- a/src/uucore/src/lib/features/tty.rs +++ b/src/uucore/src/lib/features/tty.rs @@ -73,7 +73,7 @@ impl TryFrom for Teletype { let f = |prefix: &str| { value .iter() - .last()? + .next_back()? .to_str()? .strip_prefix(prefix)? .parse::() diff --git a/src/uucore/src/lib/features/utmpx.rs b/src/uucore/src/lib/features/utmpx.rs index 46bc6d828d2..1dba4e16e16 100644 --- a/src/uucore/src/lib/features/utmpx.rs +++ b/src/uucore/src/lib/features/utmpx.rs @@ -40,11 +40,19 @@ use std::ptr; use std::sync::{Mutex, MutexGuard}; pub use self::ut::*; + +// See the FAQ at https://wiki.musl-libc.org/faq#Q:-Why-is-the-utmp/wtmp-functionality-only-implemented-as-stubs? +// Musl implements only stubs for the utmp functions, and the libc crate issues a deprecation warning about this. +// However, calling these stubs is the correct approach to maintain consistent behavior with GNU coreutils. +#[cfg_attr(target_env = "musl", allow(deprecated))] pub use libc::endutxent; +#[cfg_attr(target_env = "musl", allow(deprecated))] pub use libc::getutxent; +#[cfg_attr(target_env = "musl", allow(deprecated))] pub use libc::setutxent; use libc::utmpx; #[cfg(any(target_vendor = "apple", target_os = "linux", target_os = "netbsd"))] +#[cfg_attr(target_env = "musl", allow(deprecated))] pub use libc::utmpxname; /// # Safety @@ -278,6 +286,7 @@ impl Utmpx { // This can technically fail, and it would be nice to detect that, // but it doesn't return anything so we'd have to do nasty things // with errno. + #[cfg_attr(target_env = "musl", allow(deprecated))] setutxent(); } iter @@ -302,7 +311,9 @@ impl Utmpx { // is specified, no warning or anything. // So this function is pretty crazy and we don't try to detect errors. // Not much we can do besides pray. + #[cfg_attr(target_env = "musl", allow(deprecated))] utmpxname(path.as_ptr()); + #[cfg_attr(target_env = "musl", allow(deprecated))] setutxent(); } iter @@ -342,6 +353,7 @@ impl Iterator for UtmpxIter { type Item = Utmpx; fn next(&mut self) -> Option { unsafe { + #[cfg_attr(target_env = "musl", allow(deprecated))] let res = getutxent(); if res.is_null() { None @@ -361,6 +373,7 @@ impl Iterator for UtmpxIter { impl Drop for UtmpxIter { fn drop(&mut self) { unsafe { + #[cfg_attr(target_env = "musl", allow(deprecated))] endutxent(); } } diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index b1a9363f728..469c5244422 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -41,8 +41,6 @@ pub use crate::features::buf_copy; pub use crate::features::checksum; #[cfg(feature = "colors")] pub use crate::features::colors; -#[cfg(feature = "custom-tz-fmt")] -pub use crate::features::custom_tz_fmt; #[cfg(feature = "encoding")] pub use crate::features::encoding; #[cfg(feature = "extendedbigdecimal")] @@ -145,6 +143,25 @@ pub fn disable_rust_signal_handlers() -> Result<(), Errno> { Ok(()) } +pub fn get_canonical_util_name(util_name: &str) -> &str { + // remove the "uu_" prefix + let util_name = &util_name[3..]; + match util_name { + // uu_test aliases - '[' is an alias for test + "[" => "test", + + // hashsum aliases - all these hash commands are aliases for hashsum + "md5sum" | "sha1sum" | "sha224sum" | "sha256sum" | "sha384sum" | "sha512sum" + | "sha3sum" | "sha3-224sum" | "sha3-256sum" | "sha3-384sum" | "sha3-512sum" + | "shake128sum" | "shake256sum" | "b2sum" | "b3sum" => "hashsum", + + "dir" => "ls", // dir is an alias for ls + + // Default case - return the util name as is + _ => util_name, + } +} + /// Execute utility code for `util`. /// /// This macro expands to a main function that invokes the `uumain` function in `util` @@ -154,8 +171,21 @@ macro_rules! bin { ($util:ident) => { pub fn main() { use std::io::Write; + use uucore::locale; // suppress extraneous error output for SIGPIPE failures/panics uucore::panic::mute_sigpipe_panic(); + locale::setup_localization(uucore::get_canonical_util_name(stringify!($util))) + .unwrap_or_else(|err| { + match err { + uucore::locale::LocalizationError::ParseResource { + error: err_msg, + snippet, + } => eprintln!("Localization parse error at {snippet}: {err_msg}"), + other => eprintln!("Could not init the localization system: {other}"), + } + std::process::exit(99) + }); + // execute utility code let code = $util::uumain(uucore::args_os()); // (defensively) flush stdout for utility prior to exit; see diff --git a/src/uucore/src/lib/mods/locale.rs b/src/uucore/src/lib/mods/locale.rs index bcc9fb2db6a..3c69ba314ef 100644 --- a/src/uucore/src/lib/mods/locale.rs +++ b/src/uucore/src/lib/mods/locale.rs @@ -2,10 +2,11 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore unic_langid +// spell-checker:disable use crate::error::UError; use fluent::{FluentArgs, FluentBundle, FluentResource}; +use fluent_syntax::parser::ParserError; use std::collections::HashMap; use std::fs; use std::path::{Path, PathBuf}; @@ -21,10 +22,20 @@ pub enum LocalizationError { source: std::io::Error, path: PathBuf, }, - #[error("Parse error: {0}")] - Parse(String), + #[error("Parse-locale error: {0}")] + ParseLocale(String), + #[error("Resource parse error at '{snippet}': {error:?}")] + ParseResource { + #[source] + error: ParserError, + snippet: String, + }, #[error("Bundle error: {0}")] Bundle(String), + #[error("Locales directory not found: {0}")] + LocalesDirNotFound(String), + #[error("Path resolution error: {0}")] + PathResolution(String), } impl From for LocalizationError { @@ -45,26 +56,47 @@ impl UError for LocalizationError { pub const DEFAULT_LOCALE: &str = "en-US"; -// A struct to handle localization +// A struct to handle localization with optional English fallback struct Localizer { - bundle: FluentBundle, + primary_bundle: FluentBundle, + fallback_bundle: Option>, } impl Localizer { - fn new(bundle: FluentBundle) -> Self { - Self { bundle } + fn new(primary_bundle: FluentBundle) -> Self { + Self { + primary_bundle, + fallback_bundle: None, + } } - fn format(&self, id: &str, args: Option<&FluentArgs>, default: &str) -> String { - match self.bundle.get_message(id).and_then(|m| m.value()) { - Some(value) => { + fn with_fallback(mut self, fallback_bundle: FluentBundle) -> Self { + self.fallback_bundle = Some(fallback_bundle); + self + } + + fn format(&self, id: &str, args: Option<&FluentArgs>) -> String { + // Try primary bundle first + if let Some(message) = self.primary_bundle.get_message(id).and_then(|m| m.value()) { + let mut errs = Vec::new(); + return self + .primary_bundle + .format_pattern(message, args, &mut errs) + .to_string(); + } + + // Fall back to English bundle if available + if let Some(ref fallback) = self.fallback_bundle { + if let Some(message) = fallback.get_message(id).and_then(|m| m.value()) { let mut errs = Vec::new(); - self.bundle - .format_pattern(value, args, &mut errs) - .to_string() + return fallback + .format_pattern(message, args, &mut errs) + .to_string(); } - None => default.to_string(), } + + // Return the key ID if not found anywhere + id.to_string() } } @@ -76,117 +108,125 @@ thread_local! { // Initialize localization with a specific locale and config fn init_localization( locale: &LanguageIdentifier, - config: &LocalizationConfig, + locales_dir: &Path, ) -> Result<(), LocalizationError> { - let bundle = create_bundle(locale, config)?; + let en_locale = LanguageIdentifier::from_str(DEFAULT_LOCALE) + .expect("Default locale should always be valid"); + + let english_bundle = create_bundle(&en_locale, locales_dir)?; + let loc = if locale == &en_locale { + // If requesting English, just use English as primary (no fallback needed) + Localizer::new(english_bundle) + } else { + // Try to load the requested locale + if let Ok(primary_bundle) = create_bundle(locale, locales_dir) { + // Successfully loaded requested locale, load English as fallback + Localizer::new(primary_bundle).with_fallback(english_bundle) + } else { + // Failed to load requested locale, just use English as primary + Localizer::new(english_bundle) + } + }; + LOCALIZER.with(|lock| { - let loc = Localizer::new(bundle); lock.set(loc) .map_err(|_| LocalizationError::Bundle("Localizer already initialized".into())) })?; Ok(()) } -// Create a bundle for a locale with fallback chain +// Create a bundle for a specific locale fn create_bundle( locale: &LanguageIdentifier, - config: &LocalizationConfig, + locales_dir: &Path, ) -> Result, LocalizationError> { - // Create a new bundle with requested locale - let mut bundle = FluentBundle::new(vec![locale.clone()]); + let locale_path = locales_dir.join(format!("{locale}.ftl")); - // Try to load the requested locale - let mut locales_to_try = vec![locale.clone()]; - locales_to_try.extend_from_slice(&config.fallback_locales); - - // Try each locale in the chain - let mut tried_paths = Vec::new(); - - for try_locale in locales_to_try { - let locale_path = config.get_locale_path(&try_locale); - tried_paths.push(locale_path.clone()); - - if let Ok(ftl_file) = fs::read_to_string(&locale_path) { - let resource = FluentResource::try_new(ftl_file).map_err(|_| { - LocalizationError::Parse(format!( - "Failed to parse localization resource for {}", - try_locale - )) - })?; - - bundle.add_resource(resource).map_err(|_| { - LocalizationError::Bundle(format!( - "Failed to add resource to bundle for {}", - try_locale - )) - })?; - - return Ok(bundle); - } - } + let ftl_file = fs::read_to_string(&locale_path).map_err(|e| LocalizationError::Io { + source: e, + path: locale_path.clone(), + })?; - let paths_str = tried_paths - .iter() - .map(|p| p.to_string_lossy().to_string()) - .collect::>() - .join(", "); + let resource = FluentResource::try_new(ftl_file.clone()).map_err( + |(_partial_resource, mut errs): (FluentResource, Vec)| { + let first_err = errs.remove(0); + // Attempt to extract the snippet from the original ftl_file + let snippet = if let Some(range) = first_err.slice.clone() { + ftl_file.get(range).unwrap_or("").to_string() + } else { + String::new() + }; + LocalizationError::ParseResource { + error: first_err, + snippet, + } + }, + )?; - Err(LocalizationError::Io { - source: std::io::Error::new(std::io::ErrorKind::NotFound, "No localization files found"), - path: PathBuf::from(paths_str), - }) + let mut bundle = FluentBundle::new(vec![locale.clone()]); + + bundle.add_resource(resource).map_err(|errs| { + LocalizationError::Bundle(format!( + "Failed to add resource to bundle for {}: {:?}", + locale, errs + )) + })?; + + Ok(bundle) } -fn get_message_internal(id: &str, args: Option, default: &str) -> String { +fn get_message_internal(id: &str, args: Option) -> String { LOCALIZER.with(|lock| { lock.get() - .map(|loc| loc.format(id, args.as_ref(), default)) - .unwrap_or_else(|| default.to_string()) + .map(|loc| loc.format(id, args.as_ref())) + .unwrap_or_else(|| id.to_string()) // Return the key ID if localizer not initialized }) } /// Retrieves a localized message by its identifier. /// /// Looks up a message with the given ID in the current locale bundle and returns -/// the localized text. If the message ID is not found, returns the provided default text. +/// the localized text. If the message ID is not found in the current locale, +/// it will fall back to English. If the message is not found in English either, +/// returns the message ID itself. /// /// # Arguments /// /// * `id` - The message identifier in the Fluent resources -/// * `default` - Default text to use if the message ID isn't found /// /// # Returns /// -/// A `String` containing either the localized message or the default text +/// A `String` containing the localized message, or the message ID if not found /// /// # Examples /// /// ``` /// use uucore::locale::get_message; /// -/// // Get a localized greeting or fall back to English -/// let greeting = get_message("greeting", "Hello, World!"); +/// // Get a localized greeting (from .ftl files) +/// let greeting = get_message("greeting"); /// println!("{}", greeting); /// ``` -pub fn get_message(id: &str, default: &str) -> String { - get_message_internal(id, None, default) +pub fn get_message(id: &str) -> String { + get_message_internal(id, None) } /// Retrieves a localized message with variable substitution. /// /// Looks up a message with the given ID in the current locale bundle, /// substitutes variables from the provided arguments map, and returns the -/// localized text. If the message ID is not found, returns the provided default text. +/// localized text. If the message ID is not found in the current locale, +/// it will fall back to English. If the message is not found in English either, +/// returns the message ID itself. /// /// # Arguments /// /// * `id` - The message identifier in the Fluent resources /// * `ftl_args` - Key-value pairs for variable substitution in the message -/// * `default` - Default text to use if the message ID isn't found /// /// # Returns /// -/// A `String` containing either the localized message with variable substitution or the default text +/// A `String` containing the localized message with variable substitution, or the message ID if not found /// /// # Examples /// @@ -199,44 +239,25 @@ pub fn get_message(id: &str, default: &str) -> String { /// args.insert("name".to_string(), "Alice".to_string()); /// args.insert("count".to_string(), "3".to_string()); /// -/// let message = get_message_with_args( -/// "notification", -/// args, -/// "Hello! You have notifications." -/// ); +/// let message = get_message_with_args("notification", args); /// println!("{}", message); /// ``` -pub fn get_message_with_args(id: &str, ftl_args: HashMap, default: &str) -> String { - let args = ftl_args.into_iter().collect(); - get_message_internal(id, Some(args), default) -} - -// Configuration for localization -#[derive(Clone)] -struct LocalizationConfig { - locales_dir: PathBuf, - fallback_locales: Vec, -} +pub fn get_message_with_args(id: &str, ftl_args: HashMap) -> String { + let mut args = FluentArgs::new(); -impl LocalizationConfig { - // Create a new config with a specific locales directory - fn new>(locales_dir: P) -> Self { - Self { - locales_dir: locales_dir.as_ref().to_path_buf(), - fallback_locales: vec![], + for (key, value) in ftl_args { + // Try to parse as number first for proper pluralization support + if let Ok(num_val) = value.parse::() { + args.set(key, num_val); + } else if let Ok(float_val) = value.parse::() { + args.set(key, float_val); + } else { + // Keep as string if not a number + args.set(key, value); } } - // Set fallback locales - fn with_fallbacks(mut self, fallbacks: Vec) -> Self { - self.fallback_locales = fallbacks; - self - } - - // Get path for a specific locale - fn get_locale_path(&self, locale: &LanguageIdentifier) -> PathBuf { - self.locales_dir.join(format!("{}.ftl", locale)) - } + get_message_internal(id, Some(args)) } // Function to detect system locale from environment variables @@ -247,16 +268,17 @@ fn detect_system_locale() -> Result { .next() .unwrap_or(DEFAULT_LOCALE) .to_string(); - - LanguageIdentifier::from_str(&locale_str) - .map_err(|_| LocalizationError::Parse(format!("Failed to parse locale: {}", locale_str))) + LanguageIdentifier::from_str(&locale_str).map_err(|_| { + LocalizationError::ParseLocale(format!("Failed to parse locale: {}", locale_str)) + }) } -/// Sets up localization using the system locale (or default) and project paths. +/// Sets up localization using the system locale with English fallback. /// /// This function initializes the localization system based on the system's locale -/// preferences (via the LANG environment variable) or falls back to the default locale -/// if the system locale cannot be determined or is invalid. +/// preferences (via the LANG environment variable) or falls back to English +/// if the system locale cannot be determined or the locale file doesn't exist. +/// English is always loaded as a fallback. /// /// # Arguments /// @@ -270,8 +292,8 @@ fn detect_system_locale() -> Result { /// # Errors /// /// Returns a `LocalizationError` if: -/// * The localization files cannot be read -/// * The files contain invalid syntax +/// * The en-US.ftl file cannot be read (English is required) +/// * The files contain invalid Fluent syntax /// * The bundle cannot be initialized properly /// /// # Examples @@ -280,6 +302,8 @@ fn detect_system_locale() -> Result { /// use uucore::locale::setup_localization; /// /// // Initialize localization using files in the "locales" directory +/// // Make sure you have at least an "en-US.ftl" file in this directory +/// // Other locale files like "fr-FR.ftl" are optional /// match setup_localization("./locales") { /// Ok(_) => println!("Localization initialized successfully"), /// Err(e) => eprintln!("Failed to initialize localization: {}", e), @@ -290,14 +314,771 @@ pub fn setup_localization(p: &str) -> Result<(), LocalizationError> { LanguageIdentifier::from_str(DEFAULT_LOCALE).expect("Default locale should always be valid") }); - let locales_dir = PathBuf::from(p); - let fallback_locales = vec![ - LanguageIdentifier::from_str(DEFAULT_LOCALE) - .expect("Default locale should always be valid"), - ]; + let locales_dir = get_locales_dir(p)?; + init_localization(&locale, &locales_dir) +} - let config = LocalizationConfig::new(locales_dir).with_fallbacks(fallback_locales); +/// Helper function to get the locales directory based on the build configuration +fn get_locales_dir(p: &str) -> Result { + #[cfg(debug_assertions)] + { + // During development, use the project's locales directory + let manifest_dir = env!("CARGO_MANIFEST_DIR"); + // from uucore path, load the locales directory from the program directory + let dev_path = PathBuf::from(manifest_dir) + .join("../uu") + .join(p) + .join("locales"); - init_localization(&locale, &config)?; - Ok(()) + if dev_path.exists() { + return Ok(dev_path); + } + + // Fallback for development if the expected path doesn't exist + let fallback_dev_path = PathBuf::from(manifest_dir).join(p); + if fallback_dev_path.exists() { + return Ok(fallback_dev_path); + } + + Err(LocalizationError::LocalesDirNotFound(format!( + "Development locales directory not found at {} or {}", + dev_path.display(), + fallback_dev_path.display() + ))) + } + + #[cfg(not(debug_assertions))] + { + use std::env; + // In release builds, look relative to executable + let exe_path = env::current_exe().map_err(|e| { + LocalizationError::PathResolution(format!("Failed to get executable path: {}", e)) + })?; + + let exe_dir = exe_path.parent().ok_or_else(|| { + LocalizationError::PathResolution("Failed to get executable directory".to_string()) + })?; + + // Try the coreutils-style path first + let coreutils_path = exe_dir.join("locales").join(p); + if coreutils_path.exists() { + return Ok(coreutils_path); + } + + // Fallback to just the parameter as a relative path + let fallback_path = exe_dir.join(p); + if fallback_path.exists() { + return Ok(fallback_path); + } + + return Err(LocalizationError::LocalesDirNotFound(format!( + "Release locales directory not found at {} or {}", + coreutils_path.display(), + fallback_path.display() + ))); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashMap; + use std::env; + use std::fs; + use std::path::PathBuf; + use tempfile::TempDir; + + // Helper function to create a temporary directory with test locale files + fn create_test_locales_dir() -> TempDir { + let temp_dir = TempDir::new().expect("Failed to create temp directory"); + + // Create en-US.ftl + let en_content = r#" +greeting = Hello, world! +welcome = Welcome, { $name }! +count-items = You have { $count -> + [one] { $count } item + *[other] { $count } items +} +missing-in-other = This message only exists in English +"#; + + // Create fr-FR.ftl + let fr_content = r#" +greeting = Bonjour, le monde! +welcome = Bienvenue, { $name }! +count-items = Vous avez { $count -> + [one] { $count } élément + *[other] { $count } éléments +} +"#; + + // Create ja-JP.ftl (Japanese) + let ja_content = r#" +greeting = こんにちは、世界! +welcome = ようこそ、{ $name }さん! +count-items = { $count }個のアイテムがあります +"#; + + // Create ar-SA.ftl (Arabic - Right-to-Left) + let ar_content = r#" +greeting = أهلاً بالعالم! +welcome = أهلاً وسهلاً، { $name }! +count-items = لديك { $count -> + [zero] لا عناصر + [one] عنصر واحد + [two] عنصران + [few] { $count } عناصر + *[other] { $count } عنصر +} +"#; + + // Create es-ES.ftl with invalid syntax + let es_invalid_content = r#" +greeting = Hola, mundo! +invalid-syntax = This is { $missing +"#; + + fs::write(temp_dir.path().join("en-US.ftl"), en_content) + .expect("Failed to write en-US.ftl"); + fs::write(temp_dir.path().join("fr-FR.ftl"), fr_content) + .expect("Failed to write fr-FR.ftl"); + fs::write(temp_dir.path().join("ja-JP.ftl"), ja_content) + .expect("Failed to write ja-JP.ftl"); + fs::write(temp_dir.path().join("ar-SA.ftl"), ar_content) + .expect("Failed to write ar-SA.ftl"); + fs::write(temp_dir.path().join("es-ES.ftl"), es_invalid_content) + .expect("Failed to write es-ES.ftl"); + + temp_dir + } + + #[test] + fn test_localization_error_from_io_error() { + let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found"); + let loc_error = LocalizationError::from(io_error); + + match loc_error { + LocalizationError::Io { source: _, path } => { + assert_eq!(path, PathBuf::from("")); + } + _ => panic!("Expected IO error variant"), + } + } + + #[test] + fn test_localization_error_uerror_impl() { + let error = LocalizationError::Bundle("some error".to_string()); + assert_eq!(error.code(), 1); + } + + #[test] + fn test_create_bundle_success() { + let temp_dir = create_test_locales_dir(); + let locale = LanguageIdentifier::from_str("en-US").unwrap(); + + let result = create_bundle(&locale, temp_dir.path()); + assert!(result.is_ok()); + + let bundle = result.unwrap(); + assert!(bundle.get_message("greeting").is_some()); + } + + #[test] + fn test_create_bundle_file_not_found() { + let temp_dir = TempDir::new().unwrap(); + let locale = LanguageIdentifier::from_str("de-DE").unwrap(); + + let result = create_bundle(&locale, temp_dir.path()); + assert!(result.is_err()); + + if let Err(LocalizationError::Io { source: _, path }) = result { + assert!(path.to_string_lossy().contains("de-DE.ftl")); + } else { + panic!("Expected IO error"); + } + } + + #[test] + fn test_create_bundle_invalid_syntax() { + let temp_dir = create_test_locales_dir(); + let locale = LanguageIdentifier::from_str("es-ES").unwrap(); + + let result = create_bundle(&locale, temp_dir.path()); + assert!(result.is_err()); + + if let Err(LocalizationError::ParseResource { + error: _parser_err, + snippet: _, + }) = result + { + // Expected ParseResource variant + } else { + panic!("Expected ParseResource error"); + } + } + + #[test] + fn test_localizer_format_primary_bundle() { + let temp_dir = create_test_locales_dir(); + let en_bundle = create_bundle( + &LanguageIdentifier::from_str("en-US").unwrap(), + temp_dir.path(), + ) + .unwrap(); + + let localizer = Localizer::new(en_bundle); + let result = localizer.format("greeting", None); + assert_eq!(result, "Hello, world!"); + } + + #[test] + fn test_localizer_format_with_args() { + let temp_dir = create_test_locales_dir(); + let en_bundle = create_bundle( + &LanguageIdentifier::from_str("en-US").unwrap(), + temp_dir.path(), + ) + .unwrap(); + + let localizer = Localizer::new(en_bundle); + let mut args = FluentArgs::new(); + args.set("name", "Alice"); + + let result = localizer.format("welcome", Some(&args)); + assert_eq!(result, "Welcome, \u{2068}Alice\u{2069}!"); + } + + #[test] + fn test_localizer_fallback_to_english() { + let temp_dir = create_test_locales_dir(); + let fr_bundle = create_bundle( + &LanguageIdentifier::from_str("fr-FR").unwrap(), + temp_dir.path(), + ) + .unwrap(); + let en_bundle = create_bundle( + &LanguageIdentifier::from_str("en-US").unwrap(), + temp_dir.path(), + ) + .unwrap(); + + let localizer = Localizer::new(fr_bundle).with_fallback(en_bundle); + + // This message exists in French + let result1 = localizer.format("greeting", None); + assert_eq!(result1, "Bonjour, le monde!"); + + // This message only exists in English, should fallback + let result2 = localizer.format("missing-in-other", None); + assert_eq!(result2, "This message only exists in English"); + } + + #[test] + fn test_localizer_format_message_not_found() { + let temp_dir = create_test_locales_dir(); + let en_bundle = create_bundle( + &LanguageIdentifier::from_str("en-US").unwrap(), + temp_dir.path(), + ) + .unwrap(); + + let localizer = Localizer::new(en_bundle); + let result = localizer.format("nonexistent-message", None); + assert_eq!(result, "nonexistent-message"); + } + + #[test] + fn test_init_localization_english_only() { + // Run in a separate thread to avoid conflicts with other tests + std::thread::spawn(|| { + let temp_dir = create_test_locales_dir(); + let locale = LanguageIdentifier::from_str("en-US").unwrap(); + + let result = init_localization(&locale, temp_dir.path()); + assert!(result.is_ok()); + + // Test that we can get messages + let message = get_message("greeting"); + assert_eq!(message, "Hello, world!"); + }) + .join() + .unwrap(); + } + + #[test] + fn test_init_localization_with_fallback() { + std::thread::spawn(|| { + let temp_dir = create_test_locales_dir(); + let locale = LanguageIdentifier::from_str("fr-FR").unwrap(); + + let result = init_localization(&locale, temp_dir.path()); + assert!(result.is_ok()); + + // Test French message + let message1 = get_message("greeting"); + assert_eq!(message1, "Bonjour, le monde!"); + + // Test fallback to English + let message2 = get_message("missing-in-other"); + assert_eq!(message2, "This message only exists in English"); + }) + .join() + .unwrap(); + } + + #[test] + fn test_init_localization_invalid_locale_falls_back_to_english() { + std::thread::spawn(|| { + let temp_dir = create_test_locales_dir(); + let locale = LanguageIdentifier::from_str("de-DE").unwrap(); // No German file + + let result = init_localization(&locale, temp_dir.path()); + assert!(result.is_ok()); + + // Should use English as primary since German failed to load + let message = get_message("greeting"); + assert_eq!(message, "Hello, world!"); + }) + .join() + .unwrap(); + } + + #[test] + fn test_init_localization_already_initialized() { + std::thread::spawn(|| { + let temp_dir = create_test_locales_dir(); + let locale = LanguageIdentifier::from_str("en-US").unwrap(); + + // Initialize once + let result1 = init_localization(&locale, temp_dir.path()); + assert!(result1.is_ok()); + + // Try to initialize again - should fail + let result2 = init_localization(&locale, temp_dir.path()); + assert!(result2.is_err()); + + match result2 { + Err(LocalizationError::Bundle(msg)) => { + assert!(msg.contains("already initialized")); + } + _ => panic!("Expected Bundle error"), + } + }) + .join() + .unwrap(); + } + + #[test] + fn test_get_message() { + std::thread::spawn(|| { + let temp_dir = create_test_locales_dir(); + let locale = LanguageIdentifier::from_str("fr-FR").unwrap(); + + init_localization(&locale, temp_dir.path()).unwrap(); + + let message = get_message("greeting"); + assert_eq!(message, "Bonjour, le monde!"); + }) + .join() + .unwrap(); + } + + #[test] + fn test_get_message_not_initialized() { + std::thread::spawn(|| { + let message = get_message("greeting"); + assert_eq!(message, "greeting"); // Should return the ID itself + }) + .join() + .unwrap(); + } + + #[test] + fn test_get_message_with_args() { + std::thread::spawn(|| { + let temp_dir = create_test_locales_dir(); + let locale = LanguageIdentifier::from_str("en-US").unwrap(); + + init_localization(&locale, temp_dir.path()).unwrap(); + + let mut args = HashMap::new(); + args.insert("name".to_string(), "Bob".to_string()); + + let message = get_message_with_args("welcome", args); + assert_eq!(message, "Welcome, \u{2068}Bob\u{2069}!"); + }) + .join() + .unwrap(); + } + + #[test] + fn test_get_message_with_args_pluralization() { + std::thread::spawn(|| { + let temp_dir = create_test_locales_dir(); + let locale = LanguageIdentifier::from_str("en-US").unwrap(); + + init_localization(&locale, temp_dir.path()).unwrap(); + + // Test singular + let mut args1 = HashMap::new(); + args1.insert("count".to_string(), "1".to_string()); + let message1 = get_message_with_args("count-items", args1); + assert_eq!(message1, "You have \u{2068}\u{2068}1\u{2069} item\u{2069}"); + + // Test plural + let mut args2 = HashMap::new(); + args2.insert("count".to_string(), "5".to_string()); + let message2 = get_message_with_args("count-items", args2); + assert_eq!(message2, "You have \u{2068}\u{2068}5\u{2069} items\u{2069}"); + }) + .join() + .unwrap(); + } + + #[test] + fn test_detect_system_locale_from_lang_env() { + // Save current LANG value + let original_lang = env::var("LANG").ok(); + + // Test with a valid locale + unsafe { + env::set_var("LANG", "fr-FR.UTF-8"); + } + let result = detect_system_locale(); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), "fr-FR"); + + // Test with locale without encoding + unsafe { + env::set_var("LANG", "es-ES"); + } + let result = detect_system_locale(); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), "es-ES"); + + // Restore original LANG value + if let Some(val) = original_lang { + unsafe { + env::set_var("LANG", val); + } + } else { + unsafe { + env::remove_var("LANG"); + } + } + } + + #[test] + fn test_detect_system_locale_no_lang_env() { + // Save current LANG value + let original_lang = env::var("LANG").ok(); + + // Remove LANG environment variable + unsafe { + env::remove_var("LANG"); + } + + let result = detect_system_locale(); + assert!(result.is_ok()); + assert_eq!(result.unwrap().to_string(), "en-US"); + + // Restore original LANG value + if let Some(val) = original_lang { + unsafe { + env::set_var("LANG", val); + } + } else { + {} // Was already unset + } + } + + #[test] + fn test_setup_localization_success() { + std::thread::spawn(|| { + let temp_dir = create_test_locales_dir(); + + // Save current LANG value + let original_lang = env::var("LANG").ok(); + unsafe { + env::set_var("LANG", "fr-FR.UTF-8"); + } + + let result = setup_localization(temp_dir.path().to_str().unwrap()); + assert!(result.is_ok()); + + // Test that French is loaded + let message = get_message("greeting"); + assert_eq!(message, "Bonjour, le monde!"); + + // Restore original LANG value + if let Some(val) = original_lang { + unsafe { + env::set_var("LANG", val); + } + } else { + unsafe { + env::remove_var("LANG"); + } + } + }) + .join() + .unwrap(); + } + + #[test] + fn test_setup_localization_falls_back_to_english() { + std::thread::spawn(|| { + let temp_dir = create_test_locales_dir(); + + // Save current LANG value + let original_lang = env::var("LANG").ok(); + unsafe { + env::set_var("LANG", "de-DE.UTF-8"); + } // German file doesn't exist + + let result = setup_localization(temp_dir.path().to_str().unwrap()); + assert!(result.is_ok()); + + // Should fall back to English + let message = get_message("greeting"); + assert_eq!(message, "Hello, world!"); + + // Restore original LANG value + if let Some(val) = original_lang { + unsafe { + env::set_var("LANG", val); + } + } else { + unsafe { + env::remove_var("LANG"); + } + } + }) + .join() + .unwrap(); + } + + #[test] + fn test_setup_localization_missing_english_file() { + std::thread::spawn(|| { + let temp_dir = TempDir::new().unwrap(); // Empty directory + + let result = setup_localization(temp_dir.path().to_str().unwrap()); + assert!(result.is_err()); + + match result { + Err(LocalizationError::Io { source: _, path }) => { + assert!(path.to_string_lossy().contains("en-US.ftl")); + } + _ => panic!("Expected IO error for missing English file"), + } + }) + .join() + .unwrap(); + } + + #[test] + fn test_thread_local_isolation() { + use std::thread; + + let temp_dir = create_test_locales_dir(); + + // Initialize in main thread with French + let temp_path_main = temp_dir.path().to_path_buf(); + let main_handle = thread::spawn(move || { + let locale = LanguageIdentifier::from_str("fr-FR").unwrap(); + init_localization(&locale, &temp_path_main).unwrap(); + let main_message = get_message("greeting"); + assert_eq!(main_message, "Bonjour, le monde!"); + }); + main_handle.join().unwrap(); + + // Test in a different thread - should not be initialized + let temp_path = temp_dir.path().to_path_buf(); + let handle = thread::spawn(move || { + // This thread should have its own uninitialized LOCALIZER + let thread_message = get_message("greeting"); + assert_eq!(thread_message, "greeting"); // Returns ID since not initialized + + // Initialize in this thread with English + let en_locale = LanguageIdentifier::from_str("en-US").unwrap(); + init_localization(&en_locale, &temp_path).unwrap(); + let thread_message_after_init = get_message("greeting"); + assert_eq!(thread_message_after_init, "Hello, world!"); + }); + + handle.join().unwrap(); + + // Test another thread to verify French doesn't persist across threads + let final_handle = thread::spawn(move || { + // Should be uninitialized again + let final_message = get_message("greeting"); + assert_eq!(final_message, "greeting"); + }); + final_handle.join().unwrap(); + } + + #[test] + fn test_japanese_localization() { + std::thread::spawn(|| { + let temp_dir = create_test_locales_dir(); + let locale = LanguageIdentifier::from_str("ja-JP").unwrap(); + + let result = init_localization(&locale, temp_dir.path()); + assert!(result.is_ok()); + + // Test Japanese greeting + let message = get_message("greeting"); + assert_eq!(message, "こんにちは、世界!"); + + // Test Japanese with arguments + let mut args = HashMap::new(); + args.insert("name".to_string(), "田中".to_string()); + let welcome = get_message_with_args("welcome", args); + assert_eq!(welcome, "ようこそ、\u{2068}田中\u{2069}さん!"); + + // Test Japanese count (no pluralization) + let mut count_args = HashMap::new(); + count_args.insert("count".to_string(), "5".to_string()); + let count_message = get_message_with_args("count-items", count_args); + assert_eq!(count_message, "\u{2068}5\u{2069}個のアイテムがあります"); + }) + .join() + .unwrap(); + } + + #[test] + fn test_arabic_localization() { + std::thread::spawn(|| { + let temp_dir = create_test_locales_dir(); + let locale = LanguageIdentifier::from_str("ar-SA").unwrap(); + + let result = init_localization(&locale, temp_dir.path()); + assert!(result.is_ok()); + + // Test Arabic greeting (RTL text) + let message = get_message("greeting"); + assert_eq!(message, "أهلاً بالعالم!"); + + // Test Arabic with arguments + let mut args = HashMap::new(); + args.insert("name".to_string(), "أحمد".to_string()); + let welcome = get_message_with_args("welcome", args); + assert_eq!(welcome, "أهلاً وسهلاً، \u{2068}أحمد\u{2069}!"); + + // Test Arabic pluralization (zero case) + let mut args_zero = HashMap::new(); + args_zero.insert("count".to_string(), "0".to_string()); + let message_zero = get_message_with_args("count-items", args_zero); + assert_eq!(message_zero, "لديك \u{2068}لا عناصر\u{2069}"); + + // Test Arabic pluralization (one case) + let mut args_one = HashMap::new(); + args_one.insert("count".to_string(), "1".to_string()); + let message_one = get_message_with_args("count-items", args_one); + assert_eq!(message_one, "لديك \u{2068}عنصر واحد\u{2069}"); + + // Test Arabic pluralization (two case) + let mut args_two = HashMap::new(); + args_two.insert("count".to_string(), "2".to_string()); + let message_two = get_message_with_args("count-items", args_two); + assert_eq!(message_two, "لديك \u{2068}عنصران\u{2069}"); + + // Test Arabic pluralization (few case - 3-10) + let mut args_few = HashMap::new(); + args_few.insert("count".to_string(), "5".to_string()); + let message_few = get_message_with_args("count-items", args_few); + assert_eq!(message_few, "لديك \u{2068}\u{2068}5\u{2069} عناصر\u{2069}"); + + // Test Arabic pluralization (other case - 11+) + let mut args_many = HashMap::new(); + args_many.insert("count".to_string(), "15".to_string()); + let message_many = get_message_with_args("count-items", args_many); + assert_eq!(message_many, "لديك \u{2068}\u{2068}15\u{2069} عنصر\u{2069}"); + }) + .join() + .unwrap(); + } + + #[test] + fn test_mixed_script_fallback() { + std::thread::spawn(|| { + let temp_dir = create_test_locales_dir(); + let locale = LanguageIdentifier::from_str("ar-SA").unwrap(); + + let result = init_localization(&locale, temp_dir.path()); + assert!(result.is_ok()); + + // Test Arabic message exists + let arabic_message = get_message("greeting"); + assert_eq!(arabic_message, "أهلاً بالعالم!"); + + // Test fallback to English for missing message + let fallback_message = get_message("missing-in-other"); + assert_eq!(fallback_message, "This message only exists in English"); + }) + .join() + .unwrap(); + } + + #[test] + fn test_unicode_directional_isolation() { + std::thread::spawn(|| { + let temp_dir = create_test_locales_dir(); + let locale = LanguageIdentifier::from_str("ar-SA").unwrap(); + + init_localization(&locale, temp_dir.path()).unwrap(); + + // Test that Latin script names are properly isolated in RTL context + let mut args = HashMap::new(); + args.insert("name".to_string(), "John Smith".to_string()); + let message = get_message_with_args("welcome", args); + + // The Latin name should be wrapped in directional isolate characters + assert!(message.contains("\u{2068}John Smith\u{2069}")); + assert_eq!(message, "أهلاً وسهلاً، \u{2068}John Smith\u{2069}!"); + }) + .join() + .unwrap(); + } + + #[test] + fn test_error_display() { + let io_error = LocalizationError::Io { + source: std::io::Error::new(std::io::ErrorKind::NotFound, "File not found"), + path: PathBuf::from("/test/path.ftl"), + }; + let error_string = format!("{}", io_error); + assert!(error_string.contains("I/O error loading")); + assert!(error_string.contains("/test/path.ftl")); + + let bundle_error = LocalizationError::Bundle("Bundle creation failed".to_string()); + let bundle_string = format!("{}", bundle_error); + assert!(bundle_string.contains("Bundle error: Bundle creation failed")); + } + + #[test] + fn test_parse_resource_error_includes_snippet() { + let temp_dir = create_test_locales_dir(); + let locale = LanguageIdentifier::from_str("es-ES").unwrap(); + + let result = create_bundle(&locale, temp_dir.path()); + assert!(result.is_err()); + + if let Err(LocalizationError::ParseResource { + error: _err, + snippet, + }) = result + { + // The snippet should contain exactly the invalid text from es-ES.ftl + assert!( + snippet.contains("This is { $missing"), + "snippet was `{}` but did not include the invalid text", + snippet + ); + } else { + panic!("Expected LocalizationError::ParseResource with snippet"); + } + } } diff --git a/src/uucore_procs/Cargo.toml b/src/uucore_procs/Cargo.toml index 8d0fb09bb1e..bd6b48edd1d 100644 --- a/src/uucore_procs/Cargo.toml +++ b/src/uucore_procs/Cargo.toml @@ -2,11 +2,11 @@ [package] name = "uucore_procs" description = "uutils ~ 'uucore' proc-macros" -authors = ["Roy Ivy III "] repository = "https://github.com/uutils/coreutils/tree/main/src/uucore_procs" # readme = "README.md" keywords = ["cross-platform", "proc-macros", "uucore", "uutils"] # categories = ["os"] +authors.workspace = true edition.workspace = true homepage.workspace = true license.workspace = true diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index cb7eea5cdfb..fc357686136 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (flags) reflink (fs) tmpfs (linux) rlimit Rlim NOFILE clob btrfs neve ROOTDIR USERDIR procfs outfile uufs xattrs +// spell-checker:ignore (flags) reflink (fs) tmpfs (linux) rlimit Rlim NOFILE clob btrfs neve ROOTDIR USERDIR outfile uufs xattrs // spell-checker:ignore bdfl hlsl IRWXO IRWXG nconfined matchpathcon libselinux-devel use uucore::display::Quotable; use uutests::util::TestScenario; @@ -2556,22 +2556,21 @@ fn test_cp_reflink_insufficient_permission() { #[cfg(target_os = "linux")] #[test] fn test_closes_file_descriptors() { - use procfs::process::Process; use rlimit::Resource; - let me = Process::myself().unwrap(); + + let pid = std::process::id(); + let fd_path = format!("/proc/{pid}/fd"); // The test suite runs in parallel, we have pipe, sockets // opened by other tests. // So, we take in account the various fd to increase the limit - let number_file_already_opened: u64 = me.fd_count().unwrap().try_into().unwrap(); + let number_file_already_opened: u64 = std::fs::read_dir(fd_path) + .unwrap() + .count() + .try_into() + .unwrap(); let limit_fd: u64 = number_file_already_opened + 9; - // For debugging purposes: - for f in me.fd().unwrap() { - let fd = f.unwrap(); - println!("{fd:?} {:?}", fd.mode()); - } - new_ucmd!() .arg("-r") .arg("--reflink=auto") diff --git a/tests/by-util/test_csplit.rs b/tests/by-util/test_csplit.rs index a7a802b92f0..96322e44474 100644 --- a/tests/by-util/test_csplit.rs +++ b/tests/by-util/test_csplit.rs @@ -83,6 +83,15 @@ fn test_up_to_line_sequence() { assert_eq!(at.read("xx02"), generate(25, 51)); } +#[test] +fn test_up_to_line_with_non_ascii_repeat() { + // we use a different error message than GNU + new_ucmd!() + .args(&["numbers50.txt", "10", "{𝟚}"]) + .fails() + .stderr_contains("invalid pattern"); +} + #[test] fn test_up_to_match() { let (at, mut ucmd) = at_and_ucmd!(); @@ -167,6 +176,15 @@ fn test_up_to_match_offset_repeat_twice() { assert_eq!(at.read("xx03"), generate(32, 51)); } +#[test] +fn test_up_to_match_non_ascii_offset() { + // we use a different error message than GNU + new_ucmd!() + .args(&["numbers50.txt", "/9$/𝟚"]) + .fails() + .stderr_contains("invalid pattern"); +} + #[test] fn test_up_to_match_negative_offset() { let (at, mut ucmd) = at_and_ucmd!(); diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 09cf7ac790e..c3149834717 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -2,6 +2,8 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +// +// spell-checker: ignore: AEDT AEST EEST NZDT NZST use chrono::{DateTime, Datelike, Duration, NaiveTime, Utc}; // spell-checker:disable-line use regex::Regex; @@ -564,11 +566,153 @@ fn test_date_from_stdin() { ); } +const JAN2: &str = "2024-01-02 12:00:00 +0000"; +const JUL2: &str = "2024-07-02 12:00:00 +0000"; + #[test] -fn test_date_empty_tz() { +fn test_date_tz() { + fn test_tz(tz: &str, date: &str, output: &str) { + println!("Test with TZ={tz}, date=\"{date}\"."); + new_ucmd!() + .env("TZ", tz) + .arg("-d") + .arg(date) + .arg("+%Y-%m-%d %H:%M:%S %Z") + .succeeds() + .stdout_only(output); + } + + // Empty TZ, UTC0, invalid timezone. + test_tz("", JAN2, "2024-01-02 12:00:00 UTC\n"); + test_tz("UTC0", JAN2, "2024-01-02 12:00:00 UTC\n"); + // TODO: We do not handle invalid timezones the same way as GNU coreutils + //test_tz("Invalid/Timezone", JAN2, "2024-01-02 12:00:00 Invalid\n"); + + // Test various locations, some of them use daylight saving, some don't. + test_tz("America/Vancouver", JAN2, "2024-01-02 04:00:00 PST\n"); + test_tz("America/Vancouver", JUL2, "2024-07-02 05:00:00 PDT\n"); + test_tz("Europe/Berlin", JAN2, "2024-01-02 13:00:00 CET\n"); + test_tz("Europe/Berlin", JUL2, "2024-07-02 14:00:00 CEST\n"); + test_tz("Africa/Cairo", JAN2, "2024-01-02 14:00:00 EET\n"); + // Egypt restored daylight saving in 2023, so if the database is outdated, this will fail. + //test_tz("Africa/Cairo", JUL2, "2024-07-02 15:00:00 EEST\n"); + test_tz("Asia/Tokyo", JAN2, "2024-01-02 21:00:00 JST\n"); + test_tz("Asia/Tokyo", JUL2, "2024-07-02 21:00:00 JST\n"); + test_tz("Australia/Sydney", JAN2, "2024-01-02 23:00:00 AEDT\n"); + test_tz("Australia/Sydney", JUL2, "2024-07-02 22:00:00 AEST\n"); // Shifts the other way. + test_tz("Pacific/Tahiti", JAN2, "2024-01-02 02:00:00 -10\n"); // No abbreviation. + test_tz("Antarctica/South_Pole", JAN2, "2024-01-03 01:00:00 NZDT\n"); + test_tz("Antarctica/South_Pole", JUL2, "2024-07-03 00:00:00 NZST\n"); +} + +#[test] +fn test_date_tz_with_utc_flag() { new_ucmd!() - .env("TZ", "") + .env("TZ", "Europe/Berlin") + .arg("-u") + .arg("+%Z") + .succeeds() + .stdout_only("UTC\n"); +} + +#[test] +fn test_date_tz_various_formats() { + fn test_tz(tz: &str, date: &str, output: &str) { + println!("Test with TZ={tz}, date=\"{date}\"."); + new_ucmd!() + .env("TZ", tz) + .arg("-d") + .arg(date) + .arg("+%z %:z %::z %:::z %Z") + .succeeds() + .stdout_only(output); + } + + test_tz( + "America/Vancouver", + JAN2, + "-0800 -08:00 -08:00:00 -08 PST\n", + ); + // Half-hour timezone + test_tz("Asia/Calcutta", JAN2, "+0530 +05:30 +05:30:00 +05:30 IST\n"); + test_tz("Europe/Berlin", JAN2, "+0100 +01:00 +01:00:00 +01 CET\n"); + test_tz( + "Australia/Sydney", + JAN2, + "+1100 +11:00 +11:00:00 +11 AEDT\n", + ); +} + +#[test] +fn test_date_tz_with_relative_time() { + new_ucmd!() + .env("TZ", "America/Vancouver") + .arg("-d") + .arg("1 hour ago") + .arg("+%Y-%m-%d %H:%M:%S %Z") + .succeeds() + .stdout_matches(&Regex::new(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} P[DS]T\n$").unwrap()); +} + +#[test] +fn test_date_utc_time() { + // Test that -u flag shows correct UTC time + // We get 2 UTC times just in case we're really unlucky and this runs around + // an hour change. + let utc_hour_1: i32 = new_ucmd!() + .env("TZ", "Asia/Taipei") + .arg("-u") + .arg("+%-H") + .succeeds() + .stdout_str() + .trim_end() + .parse() + .unwrap(); + let tpe_hour: i32 = new_ucmd!() + .env("TZ", "Asia/Taipei") + .arg("+%-H") + .succeeds() + .stdout_str() + .trim_end() + .parse() + .unwrap(); + let utc_hour_2: i32 = new_ucmd!() + .env("TZ", "Asia/Taipei") + .arg("-u") + .arg("+%-H") + .succeeds() + .stdout_str() + .trim_end() + .parse() + .unwrap(); + // Taipei is always 8 hours ahead of UTC (no daylight savings) + assert!( + (tpe_hour - utc_hour_1 + 24) % 24 == 8 || (tpe_hour - utc_hour_2 + 24) % 24 == 8, + "TPE: {tpe_hour} UTC: {utc_hour_1}/{utc_hour_2}" + ); + + // Test that -u flag shows UTC timezone + new_ucmd!() + .arg("-u") .arg("+%Z") .succeeds() .stdout_only("UTC\n"); + + // Test that -u flag with specific timestamp shows correct UTC time + new_ucmd!() + .arg("-u") + .arg("-d") + .arg("@0") + .succeeds() + .stdout_only("Thu Jan 1 00:00:00 UTC 1970\n"); +} + +#[test] +fn test_date_empty_tz_time() { + new_ucmd!() + .env("TZ", "") + .arg("-d") + .arg("@0") + .succeeds() + .stdout_only("Thu Jan 1 00:00:00 UTC 1970\n"); } diff --git a/tests/by-util/test_expr.rs b/tests/by-util/test_expr.rs index c5fb96c3d54..0a16d4ea958 100644 --- a/tests/by-util/test_expr.rs +++ b/tests/by-util/test_expr.rs @@ -3,8 +3,8 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. // spell-checker:ignore αbcdef ; (people) kkos -// spell-checker:ignore aabcccd aabcd aabd abbbd abbcabc abbcac abbcbbbd abbcbd -// spell-checker:ignore abbccd abcac acabc andand bigcmp bignum emptysub +// spell-checker:ignore aabcccd aabcd aabd abbb abbbd abbcabc abbcac abbcbbbd abbcbd +// spell-checker:ignore abbccd abcabc abcac acabc andand bigcmp bignum emptysub // spell-checker:ignore orempty oror use uutests::new_ucmd; @@ -273,7 +273,36 @@ fn test_length_mb() { } #[test] -fn test_regex() { +fn test_regex_empty() { + new_ucmd!().args(&["", ":", ""]).fails().stdout_only("0\n"); + new_ucmd!() + .args(&["abc", ":", ""]) + .fails() + .stdout_only("0\n"); +} + +#[test] +fn test_regex_trailing_backslash() { + new_ucmd!() + .args(&["\\", ":", "\\\\"]) + .succeeds() + .stdout_only("1\n"); + new_ucmd!() + .args(&["\\", ":", "\\"]) + .fails() + .stderr_only("expr: Trailing backslash\n"); + new_ucmd!() + .args(&["abc\\", ":", "abc\\\\"]) + .succeeds() + .stdout_only("4\n"); + new_ucmd!() + .args(&["abc\\", ":", "abc\\"]) + .fails() + .stderr_only("expr: Trailing backslash\n"); +} + +#[test] +fn test_regex_caret() { new_ucmd!() .args(&["a^b", ":", "a^b"]) .succeeds() @@ -282,6 +311,14 @@ fn test_regex() { .args(&["a^b", ":", "a\\^b"]) .succeeds() .stdout_only("3\n"); + new_ucmd!() + .args(&["abc", ":", "^abc"]) + .succeeds() + .stdout_only("3\n"); + new_ucmd!() + .args(&["^abc", ":", "^^abc"]) + .succeeds() + .stdout_only("4\n"); new_ucmd!() .args(&["b", ":", "a\\|^b"]) .succeeds() @@ -290,6 +327,47 @@ fn test_regex() { .args(&["ab", ":", "\\(^a\\)b"]) .succeeds() .stdout_only("a\n"); + new_ucmd!() + .args(&["^abc", ":", "^abc"]) + .fails() + .stdout_only("0\n"); + new_ucmd!() + .args(&["^^^^^^^^^", ":", "^^^"]) + .succeeds() + .stdout_only("2\n"); + new_ucmd!() + .args(&["ab[^c]", ":", "ab[^c]"]) + .succeeds() + .stdout_only("3\n"); // Matches "ab[" + new_ucmd!() + .args(&["ab[^c]", ":", "ab\\[^c]"]) + .succeeds() + .stdout_only("6\n"); + new_ucmd!() + .args(&["[^a]", ":", "\\[^a]"]) + .succeeds() + .stdout_only("4\n"); + new_ucmd!() + .args(&["\\a", ":", "\\\\[^^]"]) + .succeeds() + .stdout_only("2\n"); + // Patterns are anchored to the beginning of the pattern "^bc" + new_ucmd!() + .args(&["abc", ":", "bc"]) + .fails() + .stdout_only("0\n"); + new_ucmd!() + .args(&["^a", ":", "^^[^^]"]) + .succeeds() + .stdout_only("2\n"); + new_ucmd!() + .args(&["abc", ":", "ab[^c]"]) + .fails() + .stdout_only("0\n"); +} + +#[test] +fn test_regex_dollar() { new_ucmd!() .args(&["a$b", ":", "a\\$b"]) .succeeds() @@ -303,66 +381,117 @@ fn test_regex() { .succeeds() .stdout_only("b\n"); new_ucmd!() - .args(&["abc", ":", "^abc"]) + .args(&["a$c", ":", "a$\\c"]) .succeeds() .stdout_only("3\n"); new_ucmd!() - .args(&["^abc", ":", "^^abc"]) + .args(&["$a", ":", "$a"]) .succeeds() - .stdout_only("4\n"); + .stdout_only("2\n"); new_ucmd!() - .args(&["b^$ic", ":", "b^\\$ic"]) + .args(&["a", ":", "a$\\|b"]) .succeeds() - .stdout_only("5\n"); + .stdout_only("1\n"); new_ucmd!() - .args(&["a$c", ":", "a$\\c"]) + .args(&["-5", ":", "-\\{0,1\\}[0-9]*$"]) + .succeeds() + .stdout_only("2\n"); + new_ucmd!() + .args(&["$", ":", "$"]) + .fails() + .stdout_only("0\n"); + new_ucmd!() + .args(&["a$", ":", "a$\\|b"]) + .fails() + .stdout_only("0\n"); +} + +#[test] +fn test_regex_range_quantifier() { + new_ucmd!() + .args(&["a", ":", "a\\{1\\}"]) + .succeeds() + .stdout_only("1\n"); + new_ucmd!() + .args(&["aaaaaaaaaa", ":", "a\\{1,\\}"]) + .succeeds() + .stdout_only("10\n"); + new_ucmd!() + .args(&["aaa", ":", "a\\{,3\\}"]) .succeeds() .stdout_only("3\n"); new_ucmd!() - .args(&["^^^^^^^^^", ":", "^^^"]) + .args(&["aa", ":", "a\\{1,3\\}"]) .succeeds() .stdout_only("2\n"); new_ucmd!() - .args(&["ab[^c]", ":", "ab[^c]"]) + .args(&["aaaa", ":", "a\\{,\\}"]) .succeeds() - .stdout_only("3\n"); // Matches "ab[" + .stdout_only("4\n"); new_ucmd!() - .args(&["ab[^c]", ":", "ab\\[^c]"]) + .args(&["a", ":", "ab\\{,3\\}"]) .succeeds() - .stdout_only("6\n"); + .stdout_only("1\n"); new_ucmd!() - .args(&["[^a]", ":", "\\[^a]"]) + .args(&["abbb", ":", "ab\\{,3\\}"]) .succeeds() .stdout_only("4\n"); new_ucmd!() - .args(&["\\a", ":", "\\\\[^^]"]) + .args(&["abcabc", ":", "\\(abc\\)\\{,\\}"]) .succeeds() - .stdout_only("2\n"); + .stdout_only("abc\n"); new_ucmd!() - .args(&["^a", ":", "^^[^^]"]) + .args(&["a", ":", "a\\{,6\\}"]) .succeeds() - .stdout_only("2\n"); + .stdout_only("1\n"); new_ucmd!() - .args(&["-5", ":", "-\\{0,1\\}[0-9]*$"]) + .args(&["{abc}", ":", "\\{abc\\}"]) .succeeds() - .stdout_only("2\n"); - new_ucmd!().args(&["", ":", ""]).fails().stdout_only("0\n"); + .stdout_only("5\n"); new_ucmd!() - .args(&["abc", ":", ""]) + .args(&["a{bc}", ":", "a\\(\\{bc\\}\\)"]) + .succeeds() + .stdout_only("{bc}\n"); + new_ucmd!() + .args(&["{b}", ":", "a\\|\\{b\\}"]) + .succeeds() + .stdout_only("3\n"); + new_ucmd!() + .args(&["{", ":", "a\\|\\{"]) + .succeeds() + .stdout_only("1\n"); + new_ucmd!() + .args(&["{}}}", ":", "\\{\\}\\}\\}"]) + .succeeds() + .stdout_only("4\n"); + new_ucmd!() + .args(&["a{}}}", ":", "a\\{\\}\\}\\}"]) .fails() - .stdout_only("0\n"); + .stderr_only("expr: Invalid content of \\{\\}\n"); new_ucmd!() - .args(&["abc", ":", "bc"]) + .args(&["ab", ":", "ab\\{\\}"]) .fails() - .stdout_only("0\n"); + .stderr_only("expr: Invalid content of \\{\\}\n"); new_ucmd!() - .args(&["^abc", ":", "^abc"]) + .args(&["_", ":", "a\\{12345678901234567890\\}"]) .fails() - .stdout_only("0\n"); + .stderr_only("expr: Regular expression too big\n"); new_ucmd!() - .args(&["abc", ":", "ab[^c]"]) + .args(&["_", ":", "a\\{12345678901234567890,\\}"]) .fails() - .stdout_only("0\n"); + .stderr_only("expr: Regular expression too big\n"); + new_ucmd!() + .args(&["_", ":", "a\\{,12345678901234567890\\}"]) + .fails() + .stderr_only("expr: Regular expression too big\n"); + new_ucmd!() + .args(&["_", ":", "a\\{1,12345678901234567890\\}"]) + .fails() + .stderr_only("expr: Regular expression too big\n"); + new_ucmd!() + .args(&["_", ":", "a\\{1,1234567890abcdef\\}"]) + .fails() + .stderr_only("expr: Invalid content of \\{\\}\n"); } #[test] @@ -834,7 +963,6 @@ mod gnu_expr { .stdout_only("\n"); } - #[ignore] #[test] fn test_bre17() { new_ucmd!() @@ -843,7 +971,6 @@ mod gnu_expr { .stdout_only("{1}a\n"); } - #[ignore] #[test] fn test_bre18() { new_ucmd!() @@ -852,7 +979,6 @@ mod gnu_expr { .stdout_only("1\n"); } - #[ignore] #[test] fn test_bre19() { new_ucmd!() @@ -1064,7 +1190,6 @@ mod gnu_expr { .stderr_contains("Invalid content of \\{\\}"); } - #[ignore] #[test] fn test_bre45() { new_ucmd!() @@ -1073,7 +1198,6 @@ mod gnu_expr { .stdout_only("1\n"); } - #[ignore] #[test] fn test_bre46() { new_ucmd!() @@ -1106,7 +1230,7 @@ mod gnu_expr { .args(&["_", ":", "a\\{32768\\}"]) .fails_with_code(2) .no_stdout() - .stderr_contains("Invalid content of \\{\\}"); + .stderr_contains("Regular expression too big\n"); } #[test] diff --git a/tests/by-util/test_ln.rs b/tests/by-util/test_ln.rs index 9ef25ef087c..99859503539 100644 --- a/tests/by-util/test_ln.rs +++ b/tests/by-util/test_ln.rs @@ -549,7 +549,11 @@ fn test_symlink_no_deref_dir() { scene.ucmd().args(&["-sn", dir1, link]).fails(); // Try with the no-deref - scene.ucmd().args(&["-sfn", dir1, link]).succeeds(); + scene + .ucmd() + .args(&["-sfn", dir1, link]) + .succeeds() + .no_stderr(); assert!(at.dir_exists(dir1)); assert!(at.dir_exists(dir2)); assert!(at.is_symlink(link)); diff --git a/tests/by-util/test_mv.rs b/tests/by-util/test_mv.rs index 577f6a75899..b20c4b2b644 100644 --- a/tests/by-util/test_mv.rs +++ b/tests/by-util/test_mv.rs @@ -443,6 +443,19 @@ fn test_mv_same_hardlink() { .stderr_is(format!("mv: '{file_a}' and '{file_b}' are the same file\n")); } +#[test] +#[cfg(all(unix, not(target_os = "android")))] +fn test_mv_dangling_symlink_to_folder() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.symlink_file("404", "abc"); + at.mkdir("x"); + + ucmd.arg("abc").arg("x").succeeds(); + + assert!(at.symlink_exists("x/abc")); +} + #[test] #[cfg(all(unix, not(target_os = "android")))] fn test_mv_same_symlink() { diff --git a/tests/by-util/test_shred.rs b/tests/by-util/test_shred.rs index 99d80c419a9..9c810ae64bb 100644 --- a/tests/by-util/test_shred.rs +++ b/tests/by-util/test_shred.rs @@ -251,3 +251,44 @@ fn test_all_patterns_present() { result.stderr_contains(pat); } } + +#[test] +fn test_random_source_regular_file() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + // Currently, our block size is 4096. If it changes, this test has to be adapted. + let mut many_bytes = Vec::with_capacity(4096 * 4); + for i in 0..4096u32 { + many_bytes.extend(i.to_le_bytes()); + } + assert_eq!(many_bytes.len(), 4096 * 4); + at.write_bytes("source_long", &many_bytes); + let file = "foo.txt"; + at.write(file, "a"); + scene + .ucmd() + .arg("-vn3") + .arg("--random-source=source_long") + .arg(file) + .succeeds() + .stderr_only("shred: foo.txt: pass 1/3 (random)...\nshred: foo.txt: pass 2/3 (random)...\nshred: foo.txt: pass 3/3 (random)...\n"); + // Should rewrite the file exactly three times + assert_eq!(at.read_bytes(file), many_bytes[(4096 * 2)..(4096 * 3)]); +} + +#[test] +#[ignore = "known issue #7947"] +fn test_random_source_dir() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.mkdir("source"); + let file = "foo.txt"; + at.write(file, "a"); + scene + .ucmd() + .arg("-v") + .arg("--random-source=source") + .arg(file) + .fails() + .stderr_only("shred: foo.txt: pass 1/3 (random)...\nshred: foo.txt: File write pass failed: Is a directory\n"); +} diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index f827eafea07..0e20f1c9ee1 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -1345,3 +1345,161 @@ fn test_failed_write_is_reported() { .fails() .stderr_is("sort: write failed: 'standard output': No space left on device\n"); } + +#[test] +// Test for GNU tests/sort/sort.pl "o2" +fn test_multiple_output_files() { + new_ucmd!() + .args(&["-o", "foo", "-o", "bar"]) + .fails_with_code(2) + .stderr_is("sort: multiple output files specified\n"); +} + +#[test] +// Test for GNU tests/sort/sort-files0-from.pl "f-extra-arg" +fn test_files0_from_extra_arg() { + new_ucmd!() + .args(&["--files0-from", "-", "foo"]) + .fails_with_code(2) + .stderr_contains( + "sort: extra operand 'foo'\nfile operands cannot be combined with --files0-from\n", + ) + .no_stdout(); +} + +#[test] +// Test for GNU tests/sort/sort-files0-from.pl "missing" +fn test_files0_from_missing() { + new_ucmd!() + .args(&["--files0-from", "missing_file"]) + .fails_with_code(2) + .stderr_only( + #[cfg(not(windows))] + "sort: open failed: missing_file: No such file or directory\n", + #[cfg(windows)] + "sort: open failed: missing_file: The system cannot find the file specified.\n", + ); +} + +#[test] +// Test for GNU tests/sort/sort-files0-from.pl "minus-in-stdin" +fn test_files0_from_minus_in_stdin() { + new_ucmd!() + .args(&["--files0-from", "-"]) + .pipe_in("-") + .fails_with_code(2) + .stderr_only("sort: when reading file names from stdin, no file name of '-' allowed\n"); +} + +#[test] +// Test for GNU tests/sort/sort-files0-from.pl "empty" +fn test_files0_from_empty() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch("file"); + scene + .ucmd() + .args(&["--files0-from", "file"]) + .fails_with_code(2) + .stderr_only("sort: no input from 'file'\n"); +} + +#[cfg(target_os = "linux")] +#[test] +// Test for GNU tests/sort/sort-files0-from.pl "empty-non-regular" +fn test_files0_from_empty_non_regular() { + new_ucmd!() + .args(&["--files0-from", "/dev/null"]) + .fails_with_code(2) + .stderr_only("sort: no input from '/dev/null'\n"); +} + +#[test] +// Test for GNU tests/sort/sort-files0-from.pl "nul-1" +fn test_files0_from_nul() { + new_ucmd!() + .args(&["--files0-from", "-"]) + .pipe_in("\0") + .fails_with_code(2) + .stderr_only("sort: -:1: invalid zero-length file name\n"); +} + +#[test] +// Test for GNU tests/sort/sort-files0-from.pl "nul-2" +fn test_files0_from_nul2() { + new_ucmd!() + .args(&["--files0-from", "-"]) + .pipe_in("\0\0") + .fails_with_code(2) + .stderr_only("sort: -:1: invalid zero-length file name\n"); +} + +#[test] +// Test for GNU tests/sort/sort-files0-from.pl "1" +fn test_files0_from_1() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch("file"); + at.append("file", "a"); + scene + .ucmd() + .args(&["--files0-from", "-"]) + .pipe_in("file") + .succeeds() + .stdout_only("a\n"); +} + +#[test] +// Test for GNU tests/sort/sort-files0-from.pl "1a" +fn test_files0_from_1a() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch("file"); + at.append("file", "a"); + scene + .ucmd() + .args(&["--files0-from", "-"]) + .pipe_in("file\0") + .succeeds() + .stdout_only("a\n"); +} + +#[test] +// Test for GNU tests/sort/sort-files0-from.pl "2" +fn test_files0_from_2() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch("file"); + at.append("file", "a"); + scene + .ucmd() + .args(&["--files0-from", "-"]) + .pipe_in("file\0file") + .succeeds() + .stdout_only("a\na\n"); +} + +#[test] +// Test for GNU tests/sort/sort-files0-from.pl "2a" +fn test_files0_from_2a() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch("file"); + at.append("file", "a"); + scene + .ucmd() + .args(&["--files0-from", "-"]) + .pipe_in("file\0file\0") + .succeeds() + .stdout_only("a\na\n"); +} + +#[test] +// Test for GNU tests/sort/sort-files0-from.pl "zero-len" +fn test_files0_from_zero_length() { + new_ucmd!() + .args(&["--files0-from", "-"]) + .pipe_in("g\0\0b\0\0") + .fails_with_code(2) + .stderr_only("sort: -:2: invalid zero-length file name\n"); +} diff --git a/tests/by-util/test_stdbuf.rs b/tests/by-util/test_stdbuf.rs index c4294c6af41..dec6bfe649c 100644 --- a/tests/by-util/test_stdbuf.rs +++ b/tests/by-util/test_stdbuf.rs @@ -2,6 +2,7 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +// spell-checker:ignore dyld dylib setvbuf use uutests::new_ucmd; #[cfg(not(target_os = "windows"))] use uutests::util::TestScenario; @@ -30,10 +31,21 @@ fn test_no_such() { .stderr_contains("No such file or directory"); } -#[cfg(all(not(target_os = "windows"), not(target_os = "openbsd")))] +// Disabled on x86_64-unknown-linux-musl because the cross-rs Docker image for this target +// does not provide musl-compiled system utilities (like head), leading to dynamic linker errors +// when preloading musl-compiled libstdbuf.so into glibc-compiled binaries. Same thing for FreeBSD. +#[cfg(all( + not(target_os = "windows"), + not(target_os = "freebsd"), + not(target_os = "openbsd"), + not(all(target_arch = "x86_64", target_env = "musl")) +))] #[test] fn test_stdbuf_unbuffered_stdout() { // This is a basic smoke test + // Note: This test only verifies that stdbuf does not crash and that output is passed through as expected + // for simple, short-lived commands. It does not guarantee that buffering is actually modified or that + // libstdbuf is loaded and functioning correctly. new_ucmd!() .args(&["-o0", "head"]) .pipe_in("The quick brown fox jumps over the lazy dog.") @@ -41,9 +53,20 @@ fn test_stdbuf_unbuffered_stdout() { .stdout_is("The quick brown fox jumps over the lazy dog."); } -#[cfg(all(not(target_os = "windows"), not(target_os = "openbsd")))] +// Disabled on x86_64-unknown-linux-musl because the cross-rs Docker image for this target +// does not provide musl-compiled system utilities (like head), leading to dynamic linker errors +// when preloading musl-compiled libstdbuf.so into glibc-compiled binaries. Same thing for FreeBSD. +#[cfg(all( + not(target_os = "windows"), + not(target_os = "freebsd"), + not(target_os = "openbsd"), + not(all(target_arch = "x86_64", target_env = "musl")) +))] #[test] fn test_stdbuf_line_buffered_stdout() { + // Note: This test only verifies that stdbuf does not crash and that output is passed through as expected + // for simple, short-lived commands. It does not guarantee that buffering is actually modified or that + // libstdbuf is loaded and functioning correctly. new_ucmd!() .args(&["-oL", "head"]) .pipe_in("The quick brown fox jumps over the lazy dog.") @@ -62,7 +85,15 @@ fn test_stdbuf_no_buffer_option_fails() { .stderr_contains("the following required arguments were not provided:"); } -#[cfg(all(not(target_os = "windows"), not(target_os = "openbsd")))] +// Disabled on x86_64-unknown-linux-musl because the cross-rs Docker image for this target +// does not provide musl-compiled system utilities (like tail), leading to dynamic linker errors +// when preloading musl-compiled libstdbuf.so into glibc-compiled binaries. Same thing for FreeBSD. +#[cfg(all( + not(target_os = "windows"), + not(target_os = "freebsd"), + not(target_os = "openbsd"), + not(all(target_arch = "x86_64", target_env = "musl")) +))] #[test] fn test_stdbuf_trailing_var_arg() { new_ucmd!() @@ -105,3 +136,83 @@ fn test_stdbuf_invalid_mode_fails() { } } } + +// macos uses DYLD_PRINT_LIBRARIES, not LD_DEBUG, so disable on macos at the moment. +// On modern Android (Bionic, API 37+), LD_DEBUG is supported and behaves similarly to glibc. +// On older Android versions (Bionic, API < 37), LD_DEBUG uses integer values instead of strings +// and is sometimes disabled. Disable test on Android for now. +// musl libc dynamic loader does not support LD_DEBUG, so disable on musl targets as well. +#[cfg(all( + not(target_os = "windows"), + not(target_os = "openbsd"), + not(target_os = "macos"), + not(target_os = "android"), + not(target_env = "musl") +))] +#[test] +fn test_libstdbuf_preload() { + use std::process::Command; + + // Run a simple program with LD_DEBUG=symbols to verify that libstdbuf is loaded correctly + // and that there are no architecture mismatches when preloading the library. + // Note: This does not check which setvbuf implementation is used, as our libstdbuf does not override setvbuf. + // for https://github.com/uutils/coreutils/issues/6591 + + let scene = TestScenario::new(util_name!()); + let coreutils_bin = &scene.bin_path; + + // Test with our own echo (should have the correct architecture even when cross-compiled using cross-rs, + // in which case the "system" echo will be the host architecture) + let uutils_echo_cmd = format!( + "LD_DEBUG=symbols {} stdbuf -oL {} echo test 2>&1", + coreutils_bin.display(), + coreutils_bin.display() + ); + let uutils_output = Command::new("sh") + .arg("-c") + .arg(&uutils_echo_cmd) + .output() + .expect("Failed to run uutils echo test"); + + let uutils_debug = String::from_utf8_lossy(&uutils_output.stdout); + + // Check if libstdbuf.so / libstdbuf.dylib is in the lookup path. + // With GLIBC, the log should contain something like: + // "symbol=setvbuf; lookup in file=/tmp/.tmp0mfmCg/libstdbuf.so [0]" + // With FreeBSD dynamic loader, the log should contain something like: + // cspell:disable-next-line + // "calling init function for /tmp/.tmpu11rhP/libstdbuf.so at ..." + let libstdbuf_in_path = if cfg!(target_os = "freebsd") { + uutils_debug + .lines() + .any(|line| line.contains("calling init function") && line.contains("libstdbuf")) + } else { + uutils_debug.contains("symbol=setvbuf") + && uutils_debug.contains("lookup in file=") + && uutils_debug.contains("libstdbuf") + }; + + // Check for lack of architecture mismatch error. The potential error message with GLIBC is: + // cspell:disable-next-line + // "ERROR: ld.so: object '/tmp/.tmpCLq8jl/libstdbuf.so' from LD_PRELOAD cannot be preloaded (cannot open shared object file): ignored." + let arch_mismatch_line = uutils_debug + .lines() + .find(|line| line.contains("cannot be preloaded")); + println!("LD_DEBUG output: {}", uutils_debug); + let no_arch_mismatch = arch_mismatch_line.is_none(); + + println!("libstdbuf in lookup path: {}", libstdbuf_in_path); + println!("No architecture mismatch: {}", no_arch_mismatch); + if let Some(error_line) = arch_mismatch_line { + println!("Architecture mismatch error: {}", error_line); + } + + assert!( + libstdbuf_in_path, + "libstdbuf should be in lookup path with uutils echo" + ); + assert!( + no_arch_mismatch, + "uutils echo should not show architecture mismatch" + ); +} diff --git a/tests/by-util/test_stty.rs b/tests/by-util/test_stty.rs index 7ccc56e5dee..e9e455e3208 100644 --- a/tests/by-util/test_stty.rs +++ b/tests/by-util/test_stty.rs @@ -64,3 +64,36 @@ fn save_and_all() { "the options for verbose and stty-readable output styles are mutually exclusive", ); } + +#[test] +fn no_mapping() { + new_ucmd!() + .args(&["intr"]) + .fails() + .stderr_contains("missing argument to 'intr'"); +} + +#[test] +fn invalid_mapping() { + new_ucmd!() + .args(&["intr", "cc"]) + .fails() + .stderr_contains("invalid integer argument: 'cc'"); + + new_ucmd!() + .args(&["intr", "256"]) + .fails() + .stderr_contains("invalid integer argument: '256': Value too large for defined data type"); + + new_ucmd!() + .args(&["intr", "0x100"]) + .fails() + .stderr_contains( + "invalid integer argument: '0x100': Value too large for defined data type", + ); + + new_ucmd!() + .args(&["intr", "0400"]) + .fails() + .stderr_contains("invalid integer argument: '0400': Value too large for defined data type"); +} diff --git a/tests/by-util/test_uptime.rs b/tests/by-util/test_uptime.rs index 7ec71cebad9..fb625bd6272 100644 --- a/tests/by-util/test_uptime.rs +++ b/tests/by-util/test_uptime.rs @@ -8,21 +8,10 @@ #[cfg(not(any(target_os = "openbsd", target_os = "freebsd")))] use uutests::at_and_ucmd; -use uutests::new_ucmd; use uutests::util::TestScenario; -use uutests::util_name; +use uutests::{new_ucmd, util_name}; -#[cfg(not(any(target_os = "macos", target_os = "openbsd", target_env = "musl")))] -use bincode::{config, serde::encode_to_vec}; use regex::Regex; -#[cfg(not(any(target_os = "macos", target_os = "openbsd", target_env = "musl")))] -use serde::Serialize; -#[cfg(not(any(target_os = "macos", target_os = "openbsd", target_env = "musl")))] -use serde_big_array::BigArray; -#[cfg(not(any(target_os = "macos", target_os = "openbsd", target_env = "musl")))] -use std::fs::File; -#[cfg(not(any(target_os = "macos", target_os = "openbsd", target_env = "musl")))] -use std::{io::Write, path::PathBuf}; #[test] fn test_invalid_arg() { @@ -110,6 +99,12 @@ fn test_uptime_with_non_existent_file() { )] #[allow(clippy::too_many_lines, clippy::items_after_statements)] fn test_uptime_with_file_containing_valid_boot_time_utmpx_record() { + use bincode::{config, serde::encode_to_vec}; + use serde::Serialize; + use serde_big_array::BigArray; + use std::fs::File; + use std::{io::Write, path::PathBuf}; + // This test will pass for freebsd but we currently don't support changing the utmpx file for // freebsd. let ts = TestScenario::new(util_name!()); diff --git a/tests/fixtures/dircolors/internal.expected b/tests/fixtures/dircolors/internal.expected index feea46455f4..38f43f19588 100644 --- a/tests/fixtures/dircolors/internal.expected +++ b/tests/fixtures/dircolors/internal.expected @@ -32,6 +32,7 @@ TERM st TERM terminator TERM tmux* TERM vt100 +TERM wezterm* TERM xterm* # Below are the color init strings for the basic file types. # One can use codes for 256 or more colors supported by modern terminals. diff --git a/tests/test_util_name.rs b/tests/test_util_name.rs index 7a8a076e893..cd0356edc8f 100644 --- a/tests/test_util_name.rs +++ b/tests/test_util_name.rs @@ -41,7 +41,7 @@ fn execution_phrase_double() { assert!( String::from_utf8(output.stderr) .unwrap() - .contains(&format!("Usage: {} ls", scenario.bin_path.display())) + .contains(&"Usage: ls".to_string()) ); } diff --git a/util/publish.sh b/util/publish.sh index 7207ba7fb91..880e82e19a2 100755 --- a/util/publish.sh +++ b/util/publish.sh @@ -51,10 +51,10 @@ TOTAL_ORDER=$(echo -e $PARTIAL_ORDER | tsort | tac) # Remove the ROOT node from the start TOTAL_ORDER=${TOTAL_ORDER#ROOT} -CRATE_VERSION=$(grep '^version' Cargo.toml | head -n1 | cut -d '"' -f2) +CRATE_VERSION=$(grep '^version =' Cargo.toml | head -n1 | cut -d '"' -f2) set -e -for dir in src/uuhelp_parser/ src/uucore_procs/ src/uucore/ src/uu/stdbuf/src/libstdbuf/; do +for dir in src/uuhelp_parser/ src/uucore_procs/ src/uucore/ src/uu/stdbuf/src/libstdbuf/ tests/uutests/; do ( cd "$dir" CRATE_NAME=$(grep '^name =' "Cargo.toml" | head -n1 | cut -d '"' -f2) diff --git a/util/why-error.md b/util/why-error.md index 978545b26fa..c4c371d070f 100644 --- a/util/why-error.md +++ b/util/why-error.md @@ -14,7 +14,6 @@ This file documents why some tests are failing: * gnu/tests/du/long-from-unreadable.sh - https://github.com/uutils/coreutils/issues/7217 * gnu/tests/du/move-dir-while-traversing.sh * gnu/tests/expr/expr-multibyte.pl -* gnu/tests/expr/expr.pl * gnu/tests/fmt/goal-option.sh * gnu/tests/fmt/non-space.sh * gnu/tests/head/head-elide-tail.pl