1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-30 12:37:49 +00:00

Merge branch 'master' of https://github.com/uutils/coreutils into hbina-tr-reimplement-expansion

This commit is contained in:
Hanif Bin Ariffin 2021-11-18 18:25:29 +08:00
commit 9a09b8a06c
31 changed files with 1840 additions and 1187 deletions

View file

@ -532,7 +532,7 @@ jobs:
if [ $n_fails -gt 0 ] ; then echo "::warning ::${n_fails}+ test failures" ; fi
test_freebsd:
runs-on: macos-latest
runs-on: macos-10.15
name: Tests/FreeBSD test suite
env:
mem: 2048

253
Cargo.lock generated
View file

@ -20,7 +20,7 @@ version = "0.7.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
dependencies = [
"memchr 2.4.0",
"memchr 2.4.1",
]
[[package]]
@ -76,6 +76,17 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "bigdecimal"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6aaf33151a6429fe9211d1b276eafdf70cdff28b071e76c0b0e1503221ea3744"
dependencies = [
"num-bigint",
"num-integer",
"num-traits",
]
[[package]]
name = "binary-heap-plus"
version = "0.4.1"
@ -101,7 +112,7 @@ dependencies = [
"log",
"peeking_take_while",
"proc-macro2",
"quote 1.0.9",
"quote 1.0.10",
"regex",
"rustc-hash",
"shlex",
@ -149,12 +160,12 @@ dependencies = [
[[package]]
name = "bstr"
version = "0.2.16"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90682c8d613ad3373e66de8c6411e0ae2ab2571e879d2efbf73558cc66f21279"
checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
dependencies = [
"lazy_static",
"memchr 2.4.0",
"memchr 2.4.1",
"regex-automata",
]
@ -166,9 +177,9 @@ checksum = "560c32574a12a89ecd91f5e742165893f86e3ab98d21f8ea548658eb9eef5f40"
[[package]]
name = "byte-unit"
version = "4.0.12"
version = "4.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "063197e6eb4b775b64160dedde7a0986bb2836cce140e9492e9e96f28e18bcd8"
checksum = "956ffc5b0ec7d7a6949e3f21fd63ba5af4cffdc2ba1e0b7bf62b481458c4ae7f"
dependencies = [
"utf8-width",
]
@ -187,9 +198,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "cc"
version = "1.0.69"
version = "1.0.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2"
checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd"
[[package]]
name = "cexpr"
@ -227,9 +238,9 @@ dependencies = [
[[package]]
name = "clang-sys"
version = "1.2.0"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "853eda514c284c2287f4bf20ae614f8781f40a81d32ecda6e91449304dfe077c"
checksum = "fa66045b9cb23c2e9c1520732030608b02ee07e5cfaa5a521ec15ded7fa24c90"
dependencies = [
"glob",
"libc",
@ -479,7 +490,7 @@ dependencies = [
"if_rust_version",
"lazy_static",
"proc-macro2",
"quote 1.0.9",
"quote 1.0.10",
"syn",
]
@ -560,9 +571,9 @@ dependencies = [
[[package]]
name = "crossterm"
version = "0.20.0"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ebde6a9dd5e331cd6c6f48253254d117642c31653baa475e394657c59c1f7d"
checksum = "c85525306c4291d1b73ce93c8acf9c339f9b213aef6c1d85c3830cbf1c16325c"
dependencies = [
"bitflags",
"crossterm_winapi",
@ -576,30 +587,30 @@ dependencies = [
[[package]]
name = "crossterm_winapi"
version = "0.8.0"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a6966607622438301997d3dac0d2f6e9a90c68bb6bc1785ea98456ab93c0507"
checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c"
dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "ctor"
version = "0.1.20"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e98e2ad1a782e33928b96fc3948e7c355e5af34ba4de7670fe8bac2a3b2006d"
checksum = "ccc0a48a9b826acdf4028595adc9db92caea352f7af011a3034acd172a52a0aa"
dependencies = [
"quote 1.0.9",
"quote 1.0.10",
"syn",
]
[[package]]
name = "ctrlc"
version = "3.1.9"
version = "3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "232295399409a8b7ae41276757b5a1cc21032848d42bff2352261f958b3ca29a"
checksum = "a19c6cedffdc8c03a3346d723eb20bd85a13362bb96dc2ac000842c6381ec7bf"
dependencies = [
"nix 0.20.0",
"nix 0.23.0",
"winapi 0.3.9",
]
@ -642,7 +653,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
dependencies = [
"proc-macro2",
"quote 1.0.9",
"quote 1.0.10",
"syn",
]
@ -672,9 +683,9 @@ dependencies = [
[[package]]
name = "dns-lookup"
version = "1.0.5"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "093d88961fd18c4ecacb8c80cd0b356463ba941ba11e0e01f9cf5271380b79dc"
checksum = "53ecafc952c4528d9b51a458d1a8904b81783feff9fde08ab6ed2545ff396872"
dependencies = [
"cfg-if 1.0.0",
"libc",
@ -792,9 +803,9 @@ checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"
[[package]]
name = "gcd"
version = "2.0.1"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c7cd301bf2ab11ae4e5bdfd79c221d97a25e46c089144a62ee9d09cb32d2b92"
checksum = "6c8763772808ee8fe3128f0fc424bed6d9942293fddbcfd595ecfa58a81fe00b"
[[package]]
name = "generic-array"
@ -858,9 +869,9 @@ dependencies = [
[[package]]
name = "half"
version = "1.7.1"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62aca2aba2d62b4a7f5b33f3712cb1b0692779a56fb510499d5c0aa594daeaf3"
checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7"
[[package]]
name = "hashbrown"
@ -920,9 +931,9 @@ checksum = "46dbcb333e86939721589d25a3557e180b52778cb33c7fdfe9e0158ff790d5ec"
[[package]]
name = "instant"
version = "0.1.10"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bee0328b1209d157ef001c94dd85b4f8f64139adb0eac2659f4b08382b2f474d"
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
dependencies = [
"cfg-if 1.0.0",
]
@ -975,15 +986,15 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "libc"
version = "0.2.101"
version = "0.2.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cb00336871be5ed2c8ed44b60ae9959dc5b9f08539422ed43f09e34ecaeba21"
checksum = "fbe5e23404da5b4f555ef85ebed98fb4083e55a00c317800bc2a50ede9f3d219"
[[package]]
name = "libloading"
version = "0.7.0"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f84d96438c15fcd6c3f244c8fce01d1e2b9c6b5623e9c711dc9286d8fc92d6a"
checksum = "c0cf036d15402bea3c5d4de17b3fce76b3e4a56ebc1f577be0e7a72f7c607cf0"
dependencies = [
"cfg-if 1.0.0",
"winapi 0.3.9",
@ -991,9 +1002,9 @@ dependencies = [
[[package]]
name = "lock_api"
version = "0.4.4"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0382880606dff6d15c9476c416d18690b72742aa7b605bb6dd6ec9030fbf07eb"
checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109"
dependencies = [
"scopeguard",
]
@ -1022,12 +1033,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
[[package]]
name = "maybe-uninit"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
[[package]]
name = "md5"
version = "0.3.8"
@ -1045,9 +1050,9 @@ dependencies = [
[[package]]
name = "memchr"
version = "2.4.0"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc"
checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
[[package]]
name = "memmap2"
@ -1075,9 +1080,9 @@ checksum = "9c64630dcdd71f1a64c435f54885086a0de5d6a12d104d69b165fb7d5286d677"
[[package]]
name = "mio"
version = "0.7.7"
version = "0.7.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e50ae3f04d169fcc9bde0b547d1c205219b7157e07ded9c5aff03e0637cb3ed7"
checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc"
dependencies = [
"libc",
"log",
@ -1132,6 +1137,19 @@ dependencies = [
"memoffset",
]
[[package]]
name = "nix"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f305c2c2e4c39a82f7bf0bf65fb557f9070ce06781d4f2454295cc34b1c43188"
dependencies = [
"bitflags",
"cc",
"cfg-if 1.0.0",
"libc",
"memoffset",
]
[[package]]
name = "nodrop"
version = "0.1.14"
@ -1146,7 +1164,7 @@ checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2"
dependencies = [
"bitvec",
"funty",
"memchr 2.4.0",
"memchr 2.4.1",
"version_check",
]
@ -1172,9 +1190,9 @@ dependencies = [
[[package]]
name = "num-bigint"
version = "0.4.2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74e768dff5fb39a41b3bcd30bb25cf989706c90d028d1ad71971987aa309d535"
checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f"
dependencies = [
"autocfg",
"num-integer",
@ -1228,7 +1246,7 @@ checksum = "486ea01961c4a818096de679a8b740b26d9033146ac5291b1c98557658f8cdd9"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote 1.0.9",
"quote 1.0.10",
"syn",
]
@ -1282,6 +1300,15 @@ dependencies = [
"hashbrown",
]
[[package]]
name = "os_display"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "748cc1d0dc55247316a5bedd8dc8c5478c8a0c2e2001176b38ce7c0ed732c7a5"
dependencies = [
"unicode-width",
]
[[package]]
name = "ouroboros"
version = "0.10.1"
@ -1302,7 +1329,7 @@ dependencies = [
"Inflector",
"proc-macro-error",
"proc-macro2",
"quote 1.0.9",
"quote 1.0.10",
"syn",
]
@ -1317,9 +1344,9 @@ dependencies = [
[[package]]
name = "parking_lot"
version = "0.11.1"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb"
checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
dependencies = [
"instant",
"lock_api",
@ -1328,15 +1355,15 @@ dependencies = [
[[package]]
name = "parking_lot_core"
version = "0.8.3"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018"
checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216"
dependencies = [
"cfg-if 1.0.0",
"instant",
"libc",
"redox_syscall",
"smallvec 1.6.1",
"smallvec",
"winapi 0.3.9",
]
@ -1367,9 +1394,9 @@ checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
[[package]]
name = "pkg-config"
version = "0.3.19"
version = "0.3.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c"
checksum = "12295df4f294471248581bc09bef3c38a5e46f1e36d6a37353621a0c6c357e1f"
[[package]]
name = "platform-info"
@ -1383,9 +1410,9 @@ dependencies = [
[[package]]
name = "ppv-lite86"
version = "0.2.10"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba"
[[package]]
name = "pretty_assertions"
@ -1401,9 +1428,9 @@ dependencies = [
[[package]]
name = "proc-macro-crate"
version = "1.0.0"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fdbd1df62156fbc5945f4762632564d7d038153091c3fcf1067f6aef7cff92"
checksum = "1ebace6889caf889b4d3f76becee12e90353f2b8c7d875534a71e5742f8f6f83"
dependencies = [
"thiserror",
"toml",
@ -1417,7 +1444,7 @@ checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote 1.0.9",
"quote 1.0.10",
"syn",
"version_check",
]
@ -1429,7 +1456,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote 1.0.9",
"quote 1.0.10",
"version_check",
]
@ -1441,9 +1468,9 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
[[package]]
name = "proc-macro2"
version = "1.0.28"
version = "1.0.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612"
checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43"
dependencies = [
"unicode-xid 0.2.2",
]
@ -1480,9 +1507,9 @@ checksum = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a"
[[package]]
name = "quote"
version = "1.0.9"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
dependencies = [
"proc-macro2",
]
@ -1668,7 +1695,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
dependencies = [
"aho-corasick",
"memchr 2.4.0",
"memchr 2.4.1",
"regex-syntax",
]
@ -1695,9 +1722,9 @@ dependencies = [
[[package]]
name = "retain_mut"
version = "0.1.3"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9c17925a9027d298a4603d286befe3f9dc0e8ed02523141914eb628798d6e5b"
checksum = "448296241d034b96c11173591deaa1302f2c17b56092106c1f92c1bc0183a8c9"
[[package]]
name = "rlimit"
@ -1742,9 +1769,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "selinux"
version = "0.2.3"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cf704a543fe60d898f3253f1cc37655d0f0e9cdb68ef6230557e0e031b80608"
checksum = "09715d6b4356e916047e61e4dce40a67ac93036851957b91713d3d9c282d1548"
dependencies = [
"bitflags",
"libc",
@ -1782,7 +1809,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
dependencies = [
"proc-macro2",
"quote 1.0.9",
"quote 1.0.10",
"syn",
]
@ -1819,15 +1846,15 @@ dependencies = [
[[package]]
name = "shlex"
version = "1.0.0"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42a568c8f2cd051a4d283bd6eb0343ac214c1b0f1ac19f93e1175b2dee38c73d"
checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3"
[[package]]
name = "signal-hook"
version = "0.3.9"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "470c5a6397076fae0094aaf06a08e6ba6f37acb77d3b1b91ea92b4d6c8650c39"
checksum = "9c98891d737e271a2954825ef19e46bd16bdb98e2746f2eec4f7a4ef7946efd1"
dependencies = [
"libc",
"signal-hook-registry",
@ -1855,18 +1882,9 @@ dependencies = [
[[package]]
name = "smallvec"
version = "0.6.14"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0"
dependencies = [
"maybe-uninit",
]
[[package]]
name = "smallvec"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e"
checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309"
[[package]]
name = "smawk"
@ -1876,11 +1894,10 @@ checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"
[[package]]
name = "socket2"
version = "0.3.19"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e"
checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516"
dependencies = [
"cfg-if 1.0.0",
"libc",
"winapi 0.3.9",
]
@ -1911,18 +1928,18 @@ checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec"
dependencies = [
"heck",
"proc-macro2",
"quote 1.0.9",
"quote 1.0.10",
"syn",
]
[[package]]
name = "syn"
version = "1.0.74"
version = "1.0.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c"
checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966"
dependencies = [
"proc-macro2",
"quote 1.0.9",
"quote 1.0.10",
"unicode-xid 0.2.2",
]
@ -2033,21 +2050,21 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.26"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2"
checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.26"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745"
checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
dependencies = [
"proc-macro2",
"quote 1.0.9",
"quote 1.0.10",
"syn",
]
@ -2072,9 +2089,9 @@ dependencies = [
[[package]]
name = "typenum"
version = "1.13.0"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06"
checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec"
[[package]]
name = "unicode-linebreak"
@ -2093,9 +2110,9 @@ checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
[[package]]
name = "unicode-width"
version = "0.1.8"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
[[package]]
name = "unicode-xid"
@ -2316,7 +2333,7 @@ dependencies = [
"atty",
"bstr",
"clap",
"memchr 2.4.0",
"memchr 2.4.1",
"uucore",
"uucore_procs",
]
@ -2441,7 +2458,7 @@ dependencies = [
"paste",
"quickcheck",
"rand 0.7.3",
"smallvec 0.6.14",
"smallvec",
"uucore",
"uucore_procs",
]
@ -2494,7 +2511,7 @@ dependencies = [
"hex",
"libc",
"md5",
"memchr 2.4.0",
"memchr 2.4.1",
"regex",
"regex-syntax",
"sha1",
@ -2509,7 +2526,7 @@ name = "uu_head"
version = "0.0.8"
dependencies = [
"clap",
"memchr 2.4.0",
"memchr 2.4.1",
"uucore",
"uucore_procs",
]
@ -2713,7 +2730,7 @@ dependencies = [
"aho-corasick",
"clap",
"libc",
"memchr 2.4.0",
"memchr 2.4.1",
"regex",
"regex-syntax",
"uucore",
@ -2831,7 +2848,7 @@ dependencies = [
"aho-corasick",
"clap",
"libc",
"memchr 2.4.0",
"memchr 2.4.1",
"regex",
"regex-syntax",
"uucore",
@ -2914,6 +2931,7 @@ dependencies = [
name = "uu_seq"
version = "0.0.8"
dependencies = [
"bigdecimal",
"clap",
"num-bigint",
"num-traits",
@ -2961,7 +2979,7 @@ dependencies = [
"ctrlc",
"fnv",
"itertools 0.10.1",
"memchr 2.4.0",
"memchr 2.4.1",
"ouroboros",
"rand 0.7.3",
"rayon",
@ -3036,7 +3054,7 @@ name = "uu_tac"
version = "0.0.8"
dependencies = [
"clap",
"memchr 2.4.0",
"memchr 2.4.1",
"memmap2",
"regex",
"uucore",
@ -3265,6 +3283,7 @@ dependencies = [
"libc",
"nix 0.20.0",
"once_cell",
"os_display",
"termion",
"thiserror",
"time",
@ -3279,7 +3298,7 @@ name = "uucore_procs"
version = "0.0.7"
dependencies = [
"proc-macro2",
"quote 1.0.9",
"quote 1.0.10",
"syn",
]
@ -3402,6 +3421,6 @@ dependencies = [
[[package]]
name = "z85"
version = "3.0.3"
version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ac8b56e4f9906a4ef5412875e9ce448364023335cec645fd457ecf51d4f2781"
checksum = "af896e93db81340b74b65f74276a99b210c086f3d34ed0abf433182a462af856"

View file

@ -131,21 +131,15 @@ fn basename(fullname: &str, suffix: &str) -> String {
// Convert to path buffer and get last path component
let pb = PathBuf::from(path);
match pb.components().last() {
Some(c) => strip_suffix(c.as_os_str().to_str().unwrap(), suffix),
Some(c) => {
let name = c.as_os_str().to_str().unwrap();
if name == suffix {
name.to_string()
} else {
name.strip_suffix(suffix).unwrap_or(name).to_string()
}
}
None => "".to_owned(),
}
}
// can be replaced with strip_suffix once MSRV is 1.45
#[allow(clippy::manual_strip)]
fn strip_suffix(name: &str, suffix: &str) -> String {
if name == suffix {
return name.to_owned();
}
if name.ends_with(suffix) {
return name[..name.len() - suffix.len()].to_owned();
}
name.to_owned()
}

36
src/uu/env/src/env.rs vendored
View file

@ -7,7 +7,7 @@
/* last synced with: env (GNU coreutils) 8.13 */
// spell-checker:ignore (ToDO) chdir execvp progname subcommand subcommands unsets
// spell-checker:ignore (ToDO) chdir execvp progname subcommand subcommands unsets setenv putenv posix_spawnp
#[macro_use]
extern crate clap;
@ -256,7 +256,32 @@ fn run_env(args: impl uucore::Args) -> UResult<()> {
// set specified env vars
for &(name, val) in &opts.sets {
// FIXME: set_var() panics if name is an empty string
/*
* set_var panics if name is an empty string
* set_var internally calls setenv (on unix at least), while GNU env calls putenv instead.
*
* putenv returns successfully if provided with something like "=a" and modifies the environ
* variable to contain "=a" inside it, effectively modifying the process' current environment
* to contain a malformed string in it. Using GNU's implementation, the command `env =a`
* prints out the malformed string and even invokes the child process with that environment.
* This can be seen by using `env -i =a env` or `env -i =a cat /proc/self/environ`
*
* POSIX.1-2017 doesn't seem to mention what to do if the string is malformed (at least
* not in "Chapter 8, Environment Variables" or in the definition for environ and various
* exec*'s or in the description of env in the "Shell & Utilities" volume).
*
* It also doesn't specify any checks for putenv before modifying the environ variable, which
* is likely why glibc doesn't do so. However, setenv's first argument cannot point to
* an empty string or a string containing '='.
*
* There is no benefit in replicating GNU's env behavior, since it will only modify the
* environment in weird ways
*/
if name.is_empty() {
show_warning!("no name specified for value {}", val.quote());
continue;
}
env::set_var(name, val);
}
@ -264,7 +289,12 @@ fn run_env(args: impl uucore::Args) -> UResult<()> {
// we need to execute a command
let (prog, args) = build_command(&mut opts.program);
// FIXME: this should just use execvp() (no fork()) on Unix-like systems
/*
* On Unix-like systems Command::status either ends up calling either fork or posix_spawnp
* (which ends up calling clone). Keep using the current process would be ideal, but the
* standard library contains many checks and fail-safes to ensure the process ends up being
* created. This is much simpler than dealing with the hassles of calling execvp directly.
*/
match Command::new(&*prog).args(args).status() {
Ok(exit) if !exit.success() => return Err(exit.code().unwrap().into()),
Err(ref err) if err.kind() == io::ErrorKind::NotFound => return Err(127.into()),

View file

@ -44,7 +44,8 @@ on Daniel Lemire's [*Microbenchmarking calls for idealized conditions*][lemire],
which I recommend reading if you want to add benchmarks to `factor`.
1. Select a small, self-contained, deterministic component
`gcd` and `table::factor` are good example of such:
(`gcd` and `table::factor` are good examples):
- no I/O or access to external data structures ;
- no call into other components ;
- behavior is deterministic: no RNG, no concurrency, ... ;
@ -53,16 +54,19 @@ which I recommend reading if you want to add benchmarks to `factor`.
maximizing the numbers of samples we can take in a given time.
2. Benchmarks are immutable (once merged in `uutils`)
Modifying a benchmark means previously-collected values cannot meaningfully
be compared, silently giving nonsensical results. If you must modify an
existing benchmark, rename it.
3. Test common cases
We are interested in overall performance, rather than specific edge-cases;
use **reproducibly-randomized inputs**, sampling from either all possible
input values or some subset of interest.
4. Use [`criterion`], `criterion::black_box`, ...
`criterion` isn't perfect, but it is also much better than ad-hoc
solutions in each benchmark.

View file

@ -15,13 +15,13 @@ edition = "2018"
num-traits = "0.2.13" # used in src/numerics.rs, which is included by build.rs
[dependencies]
clap = { version = "2.33", features = ["wrap_help"] }
coz = { version = "0.1.3", optional = true }
num-traits = "0.2.13" # Needs at least version 0.2.13 for "OverflowingAdd"
rand = { version = "0.7", features = ["small_rng"] }
smallvec = { version = "0.6.14, < 1.0" }
smallvec = "1.7" # TODO(nicoo): Use `union` feature, requires Rust 1.49 or later.
uucore = { version = ">=0.0.8", package = "uucore", path = "../../uucore" }
uucore_procs = { version=">=0.0.7", package = "uucore_procs", path = "../../uucore_procs" }
clap = { version = "2.33", features = ["wrap_help"] }
[dev-dependencies]
paste = "0.1.18"

View file

@ -52,7 +52,7 @@ pub fn gcd(mut u: u64, mut v: u64) -> u64 {
#[cfg(test)]
mod tests {
use super::*;
use quickcheck::quickcheck;
use quickcheck::{quickcheck, TestResult};
quickcheck! {
fn euclidean(a: u64, b: u64) -> bool {
@ -76,13 +76,12 @@ mod tests {
gcd(0, a) == a
}
fn divisor(a: u64, b: u64) -> () {
fn divisor(a: u64, b: u64) -> TestResult {
// Test that gcd(a, b) divides a and b, unless a == b == 0
if a == 0 && b == 0 { return; }
if a == 0 && b == 0 { return TestResult::discard(); } // restrict test domain to !(a == b == 0)
let g = gcd(a, b);
assert_eq!(a % g, 0);
assert_eq!(b % g, 0);
TestResult::from_bool( g != 0 && a % g == 0 && b % g == 0 )
}
fn commutative(a: u64, b: u64) -> bool {

View file

@ -1111,6 +1111,9 @@ only ignore '.' and '..'.",
.long(options::COLOR)
.help("Color output based on file type.")
.takes_value(true)
.possible_values(&[
"always", "yes", "force", "auto", "tty", "if-tty", "never", "no", "none",
])
.require_equals(true)
.min_values(0),
)
@ -2103,11 +2106,9 @@ fn get_security_context(config: &Config, p_buf: &Path, must_dereference: bool) -
}
Ok(None) => substitute_string,
Ok(Some(context)) => {
let mut context = context.as_bytes();
if context.ends_with(&[0]) {
// TODO: replace with `strip_prefix()` when MSRV >= 1.51
context = &context[..context.len() - 1]
};
let context = context.as_bytes();
let context = context.strip_suffix(&[0]).unwrap_or(context);
String::from_utf8(context.to_vec()).unwrap_or_else(|e| {
show_warning!(
"getting security context of: {}: {}",

View file

@ -0,0 +1,19 @@
# Benchmarking to measure performance
To compare the performance of the `uutils` version of `seq` with the
GNU version of `seq`, you can use a benchmarking tool like
[hyperfine][0]. On Ubuntu 18.04 or later, you can install `hyperfine` by
running
sudo apt-get install hyperfine
Next, build the `seq` binary under the release profile:
cargo build --release -p uu_seq
Finally, you can compare the performance of the two versions of `head`
by running, for example,
hyperfine "seq 1000000" "target/release/seq 1000000"
[0]: https://github.com/sharkdp/hyperfine

View file

@ -1,3 +1,4 @@
# spell-checker:ignore bigdecimal
[package]
name = "uu_seq"
version = "0.0.8"
@ -15,6 +16,7 @@ edition = "2018"
path = "src/seq.rs"
[dependencies]
bigdecimal = "0.3"
clap = { version = "2.33", features = ["wrap_help"] }
num-bigint = "0.4.0"
num-traits = "0.2.14"

View file

@ -1,190 +0,0 @@
//! Counting number of digits needed to represent a number.
//!
//! The [`num_integral_digits`] and [`num_fractional_digits`] functions
//! count the number of digits needed to represent a number in decimal
//! notation (like "123.456").
use std::convert::TryInto;
use std::num::ParseIntError;
use uucore::display::Quotable;
/// The number of digits after the decimal point in a given number.
///
/// The input `s` is a string representing a number, either an integer
/// or a floating point number in either decimal notation or scientific
/// notation. This function returns the number of digits after the
/// decimal point needed to print the number in decimal notation.
///
/// # Examples
///
/// ```rust,ignore
/// assert_eq!(num_fractional_digits("123.45e-1").unwrap(), 3);
/// ```
pub fn num_fractional_digits(s: &str) -> Result<usize, ParseIntError> {
match (s.find('.'), s.find('e')) {
// For example, "123456".
(None, None) => Ok(0),
// For example, "123e456".
(None, Some(j)) => {
let exponent: i64 = s[j + 1..].parse()?;
if exponent < 0 {
Ok(-exponent as usize)
} else {
Ok(0)
}
}
// For example, "123.456".
(Some(i), None) => Ok(s.len() - (i + 1)),
// For example, "123.456e789".
(Some(i), Some(j)) if i < j => {
// Because of the match guard, this subtraction will not underflow.
let num_digits_between_decimal_point_and_e = (j - (i + 1)) as i64;
let exponent: i64 = s[j + 1..].parse()?;
if num_digits_between_decimal_point_and_e < exponent {
Ok(0)
} else {
Ok((num_digits_between_decimal_point_and_e - exponent)
.try_into()
.unwrap())
}
}
_ => crash!(
1,
"invalid floating point argument: {}\n Try '{} --help' for more information.",
s.quote(),
uucore::execution_phrase()
),
}
}
/// The number of digits before the decimal point in a given number.
///
/// The input `s` is a string representing a number, either an integer
/// or a floating point number in either decimal notation or scientific
/// notation. This function returns the number of digits before the
/// decimal point needed to print the number in decimal notation.
///
/// # Examples
///
/// ```rust,ignore
/// assert_eq!(num_fractional_digits("123.45e-1").unwrap(), 2);
/// ```
pub fn num_integral_digits(s: &str) -> Result<usize, ParseIntError> {
match (s.find('.'), s.find('e')) {
// For example, "123456".
(None, None) => Ok(s.len()),
// For example, "123e456".
(None, Some(j)) => {
let exponent: i64 = s[j + 1..].parse()?;
let total = j as i64 + exponent;
if total < 1 {
Ok(1)
} else {
Ok(total.try_into().unwrap())
}
}
// For example, "123.456".
(Some(i), None) => Ok(i),
// For example, "123.456e789".
(Some(i), Some(j)) => {
let exponent: i64 = s[j + 1..].parse()?;
let minimum: usize = {
let integral_part: f64 = crash_if_err!(1, s[..j].parse());
if integral_part == -0.0 && integral_part.is_sign_negative() {
2
} else {
1
}
};
let total = i as i64 + exponent;
if total < minimum as i64 {
Ok(minimum)
} else {
Ok(total.try_into().unwrap())
}
}
}
}
#[cfg(test)]
mod tests {
mod test_num_integral_digits {
use crate::num_integral_digits;
#[test]
fn test_integer() {
assert_eq!(num_integral_digits("123").unwrap(), 3);
}
#[test]
fn test_decimal() {
assert_eq!(num_integral_digits("123.45").unwrap(), 3);
}
#[test]
fn test_scientific_no_decimal_positive_exponent() {
assert_eq!(num_integral_digits("123e4").unwrap(), 3 + 4);
}
#[test]
fn test_scientific_with_decimal_positive_exponent() {
assert_eq!(num_integral_digits("123.45e6").unwrap(), 3 + 6);
}
#[test]
fn test_scientific_no_decimal_negative_exponent() {
assert_eq!(num_integral_digits("123e-4").unwrap(), 1);
}
#[test]
fn test_scientific_with_decimal_negative_exponent() {
assert_eq!(num_integral_digits("123.45e-6").unwrap(), 1);
assert_eq!(num_integral_digits("123.45e-1").unwrap(), 2);
}
}
mod test_num_fractional_digits {
use crate::num_fractional_digits;
#[test]
fn test_integer() {
assert_eq!(num_fractional_digits("123").unwrap(), 0);
}
#[test]
fn test_decimal() {
assert_eq!(num_fractional_digits("123.45").unwrap(), 2);
}
#[test]
fn test_scientific_no_decimal_positive_exponent() {
assert_eq!(num_fractional_digits("123e4").unwrap(), 0);
}
#[test]
fn test_scientific_with_decimal_positive_exponent() {
assert_eq!(num_fractional_digits("123.45e6").unwrap(), 0);
assert_eq!(num_fractional_digits("123.45e1").unwrap(), 1);
}
#[test]
fn test_scientific_no_decimal_negative_exponent() {
assert_eq!(num_fractional_digits("123e-4").unwrap(), 4);
assert_eq!(num_fractional_digits("123e-1").unwrap(), 1);
}
#[test]
fn test_scientific_with_decimal_negative_exponent() {
assert_eq!(num_fractional_digits("123.45e-6").unwrap(), 8);
assert_eq!(num_fractional_digits("123.45e-1").unwrap(), 3);
}
}
}

View file

@ -0,0 +1,290 @@
// spell-checker:ignore bigdecimal extendedbigdecimal extendedbigint
//! An arbitrary precision float that can also represent infinity, NaN, etc.
//!
//! The finite values are stored as [`BigDecimal`] instances. Because
//! the `bigdecimal` library does not represent infinity, NaN, etc., we
//! need to represent them explicitly ourselves. The
//! [`ExtendedBigDecimal`] enumeration does that.
//!
//! # Examples
//!
//! Addition works for [`ExtendedBigDecimal`] as it does for floats. For
//! example, adding infinity to any finite value results in infinity:
//!
//! ```rust,ignore
//! let summand1 = ExtendedBigDecimal::BigDecimal(BigDecimal::zero());
//! let summand2 = ExtendedBigDecimal::Infinity;
//! assert_eq!(summand1 + summand2, ExtendedBigDecimal::Infinity);
//! ```
use std::cmp::Ordering;
use std::fmt::Display;
use std::ops::Add;
use bigdecimal::BigDecimal;
use num_bigint::BigInt;
use num_bigint::ToBigInt;
use num_traits::One;
use num_traits::Zero;
use crate::extendedbigint::ExtendedBigInt;
#[derive(Debug, Clone)]
pub enum ExtendedBigDecimal {
/// Arbitrary precision floating point number.
BigDecimal(BigDecimal),
/// Floating point positive infinity.
///
/// This is represented as its own enumeration member instead of as
/// a [`BigDecimal`] because the `bigdecimal` library does not
/// support infinity, see [here][0].
///
/// [0]: https://github.com/akubera/bigdecimal-rs/issues/67
Infinity,
/// Floating point negative infinity.
///
/// This is represented as its own enumeration member instead of as
/// a [`BigDecimal`] because the `bigdecimal` library does not
/// support infinity, see [here][0].
///
/// [0]: https://github.com/akubera/bigdecimal-rs/issues/67
MinusInfinity,
/// Floating point negative zero.
///
/// This is represented as its own enumeration member instead of as
/// a [`BigDecimal`] because the `bigdecimal` library does not
/// support negative zero.
MinusZero,
/// Floating point NaN.
///
/// This is represented as its own enumeration member instead of as
/// a [`BigDecimal`] because the `bigdecimal` library does not
/// support NaN, see [here][0].
///
/// [0]: https://github.com/akubera/bigdecimal-rs/issues/67
Nan,
}
/// The smallest integer greater than or equal to this number.
fn ceil(x: BigDecimal) -> BigInt {
if x.is_integer() {
// Unwrapping the Option because it always returns Some
x.to_bigint().unwrap()
} else {
(x + BigDecimal::one().half()).round(0).to_bigint().unwrap()
}
}
/// The largest integer less than or equal to this number.
fn floor(x: BigDecimal) -> BigInt {
if x.is_integer() {
// Unwrapping the Option because it always returns Some
x.to_bigint().unwrap()
} else {
(x - BigDecimal::one().half()).round(0).to_bigint().unwrap()
}
}
impl ExtendedBigDecimal {
/// The smallest integer greater than or equal to this number.
pub fn ceil(self) -> ExtendedBigInt {
match self {
ExtendedBigDecimal::BigDecimal(x) => ExtendedBigInt::BigInt(ceil(x)),
other => From::from(other),
}
}
/// The largest integer less than or equal to this number.
pub fn floor(self) -> ExtendedBigInt {
match self {
ExtendedBigDecimal::BigDecimal(x) => ExtendedBigInt::BigInt(floor(x)),
other => From::from(other),
}
}
}
impl From<ExtendedBigInt> for ExtendedBigDecimal {
fn from(big_int: ExtendedBigInt) -> Self {
match big_int {
ExtendedBigInt::BigInt(n) => Self::BigDecimal(BigDecimal::from(n)),
ExtendedBigInt::Infinity => ExtendedBigDecimal::Infinity,
ExtendedBigInt::MinusInfinity => ExtendedBigDecimal::MinusInfinity,
ExtendedBigInt::MinusZero => ExtendedBigDecimal::MinusZero,
ExtendedBigInt::Nan => ExtendedBigDecimal::Nan,
}
}
}
impl Display for ExtendedBigDecimal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ExtendedBigDecimal::BigDecimal(x) => {
let (n, p) = x.as_bigint_and_exponent();
match p {
0 => ExtendedBigDecimal::BigDecimal(BigDecimal::new(n * 10, 1)).fmt(f),
_ => x.fmt(f),
}
}
ExtendedBigDecimal::Infinity => f32::INFINITY.fmt(f),
ExtendedBigDecimal::MinusInfinity => f32::NEG_INFINITY.fmt(f),
ExtendedBigDecimal::MinusZero => {
// FIXME In Rust version 1.53.0 and later, the display
// of floats was updated to allow displaying negative
// zero. See
// https://github.com/rust-lang/rust/pull/78618. Currently,
// this just formats "0.0".
(0.0f32).fmt(f)
}
ExtendedBigDecimal::Nan => "nan".fmt(f),
}
}
}
impl Zero for ExtendedBigDecimal {
fn zero() -> Self {
ExtendedBigDecimal::BigDecimal(BigDecimal::zero())
}
fn is_zero(&self) -> bool {
match self {
Self::BigDecimal(n) => n.is_zero(),
Self::MinusZero => true,
_ => false,
}
}
}
impl Add for ExtendedBigDecimal {
type Output = Self;
fn add(self, other: Self) -> Self {
match (self, other) {
(Self::BigDecimal(m), Self::BigDecimal(n)) => Self::BigDecimal(m.add(n)),
(Self::BigDecimal(_), Self::MinusInfinity) => Self::MinusInfinity,
(Self::BigDecimal(_), Self::Infinity) => Self::Infinity,
(Self::BigDecimal(_), Self::Nan) => Self::Nan,
(Self::BigDecimal(m), Self::MinusZero) => Self::BigDecimal(m),
(Self::Infinity, Self::BigDecimal(_)) => Self::Infinity,
(Self::Infinity, Self::Infinity) => Self::Infinity,
(Self::Infinity, Self::MinusZero) => Self::Infinity,
(Self::Infinity, Self::MinusInfinity) => Self::Nan,
(Self::Infinity, Self::Nan) => Self::Nan,
(Self::MinusInfinity, Self::BigDecimal(_)) => Self::MinusInfinity,
(Self::MinusInfinity, Self::MinusInfinity) => Self::MinusInfinity,
(Self::MinusInfinity, Self::MinusZero) => Self::MinusInfinity,
(Self::MinusInfinity, Self::Infinity) => Self::Nan,
(Self::MinusInfinity, Self::Nan) => Self::Nan,
(Self::Nan, _) => Self::Nan,
(Self::MinusZero, other) => other,
}
}
}
impl PartialEq for ExtendedBigDecimal {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::BigDecimal(m), Self::BigDecimal(n)) => m.eq(n),
(Self::BigDecimal(_), Self::MinusInfinity) => false,
(Self::BigDecimal(_), Self::Infinity) => false,
(Self::BigDecimal(_), Self::Nan) => false,
(Self::BigDecimal(_), Self::MinusZero) => false,
(Self::Infinity, Self::BigDecimal(_)) => false,
(Self::Infinity, Self::Infinity) => true,
(Self::Infinity, Self::MinusZero) => false,
(Self::Infinity, Self::MinusInfinity) => false,
(Self::Infinity, Self::Nan) => false,
(Self::MinusInfinity, Self::BigDecimal(_)) => false,
(Self::MinusInfinity, Self::Infinity) => false,
(Self::MinusInfinity, Self::MinusZero) => false,
(Self::MinusInfinity, Self::MinusInfinity) => true,
(Self::MinusInfinity, Self::Nan) => false,
(Self::Nan, _) => false,
(Self::MinusZero, Self::BigDecimal(_)) => false,
(Self::MinusZero, Self::Infinity) => false,
(Self::MinusZero, Self::MinusZero) => true,
(Self::MinusZero, Self::MinusInfinity) => false,
(Self::MinusZero, Self::Nan) => false,
}
}
}
impl PartialOrd for ExtendedBigDecimal {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
match (self, other) {
(Self::BigDecimal(m), Self::BigDecimal(n)) => m.partial_cmp(n),
(Self::BigDecimal(_), Self::MinusInfinity) => Some(Ordering::Greater),
(Self::BigDecimal(_), Self::Infinity) => Some(Ordering::Less),
(Self::BigDecimal(_), Self::Nan) => None,
(Self::BigDecimal(m), Self::MinusZero) => m.partial_cmp(&BigDecimal::zero()),
(Self::Infinity, Self::BigDecimal(_)) => Some(Ordering::Greater),
(Self::Infinity, Self::Infinity) => Some(Ordering::Equal),
(Self::Infinity, Self::MinusZero) => Some(Ordering::Greater),
(Self::Infinity, Self::MinusInfinity) => Some(Ordering::Greater),
(Self::Infinity, Self::Nan) => None,
(Self::MinusInfinity, Self::BigDecimal(_)) => Some(Ordering::Less),
(Self::MinusInfinity, Self::Infinity) => Some(Ordering::Less),
(Self::MinusInfinity, Self::MinusZero) => Some(Ordering::Less),
(Self::MinusInfinity, Self::MinusInfinity) => Some(Ordering::Equal),
(Self::MinusInfinity, Self::Nan) => None,
(Self::Nan, _) => None,
(Self::MinusZero, Self::BigDecimal(n)) => BigDecimal::zero().partial_cmp(n),
(Self::MinusZero, Self::Infinity) => Some(Ordering::Less),
(Self::MinusZero, Self::MinusZero) => Some(Ordering::Equal),
(Self::MinusZero, Self::MinusInfinity) => Some(Ordering::Greater),
(Self::MinusZero, Self::Nan) => None,
}
}
}
#[cfg(test)]
mod tests {
use bigdecimal::BigDecimal;
use num_traits::Zero;
use crate::extendedbigdecimal::ExtendedBigDecimal;
#[test]
fn test_addition_infinity() {
let summand1 = ExtendedBigDecimal::BigDecimal(BigDecimal::zero());
let summand2 = ExtendedBigDecimal::Infinity;
assert_eq!(summand1 + summand2, ExtendedBigDecimal::Infinity);
}
#[test]
fn test_addition_minus_infinity() {
let summand1 = ExtendedBigDecimal::BigDecimal(BigDecimal::zero());
let summand2 = ExtendedBigDecimal::MinusInfinity;
assert_eq!(summand1 + summand2, ExtendedBigDecimal::MinusInfinity);
}
#[test]
fn test_addition_nan() {
let summand1 = ExtendedBigDecimal::BigDecimal(BigDecimal::zero());
let summand2 = ExtendedBigDecimal::Nan;
let sum = summand1 + summand2;
match sum {
ExtendedBigDecimal::Nan => (),
_ => unreachable!(),
}
}
#[test]
fn test_display() {
assert_eq!(
format!("{}", ExtendedBigDecimal::BigDecimal(BigDecimal::zero())),
"0.0"
);
assert_eq!(format!("{}", ExtendedBigDecimal::Infinity), "inf");
assert_eq!(format!("{}", ExtendedBigDecimal::MinusInfinity), "-inf");
assert_eq!(format!("{}", ExtendedBigDecimal::Nan), "nan");
// FIXME In Rust version 1.53.0 and later, the display of floats
// was updated to allow displaying negative zero. Until then, we
// just display `MinusZero` as "0.0".
//
// assert_eq!(format!("{}", ExtendedBigDecimal::MinusZero), "-0.0");
//
}
}

View file

@ -0,0 +1,218 @@
// spell-checker:ignore bigint extendedbigint extendedbigdecimal
//! An arbitrary precision integer that can also represent infinity, NaN, etc.
//!
//! Usually infinity, NaN, and negative zero are only represented for
//! floating point numbers. The [`ExtendedBigInt`] enumeration provides
//! a representation of those things with the set of integers. The
//! finite values are stored as [`BigInt`] instances.
//!
//! # Examples
//!
//! Addition works for [`ExtendedBigInt`] as it does for floats. For
//! example, adding infinity to any finite value results in infinity:
//!
//! ```rust,ignore
//! let summand1 = ExtendedBigInt::BigInt(BigInt::zero());
//! let summand2 = ExtendedBigInt::Infinity;
//! assert_eq!(summand1 + summand2, ExtendedBigInt::Infinity);
//! ```
use std::cmp::Ordering;
use std::fmt::Display;
use std::ops::Add;
use num_bigint::BigInt;
use num_bigint::ToBigInt;
use num_traits::One;
use num_traits::Zero;
use crate::extendedbigdecimal::ExtendedBigDecimal;
#[derive(Debug, Clone)]
pub enum ExtendedBigInt {
BigInt(BigInt),
Infinity,
MinusInfinity,
MinusZero,
Nan,
}
impl ExtendedBigInt {
/// The integer number one.
pub fn one() -> Self {
// We would like to implement `num_traits::One`, but it requires
// a multiplication implementation, and we don't want to
// implement that here.
ExtendedBigInt::BigInt(BigInt::one())
}
}
impl From<ExtendedBigDecimal> for ExtendedBigInt {
fn from(big_decimal: ExtendedBigDecimal) -> Self {
match big_decimal {
// TODO When can this fail?
ExtendedBigDecimal::BigDecimal(x) => Self::BigInt(x.to_bigint().unwrap()),
ExtendedBigDecimal::Infinity => ExtendedBigInt::Infinity,
ExtendedBigDecimal::MinusInfinity => ExtendedBigInt::MinusInfinity,
ExtendedBigDecimal::MinusZero => ExtendedBigInt::MinusZero,
ExtendedBigDecimal::Nan => ExtendedBigInt::Nan,
}
}
}
impl Display for ExtendedBigInt {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ExtendedBigInt::BigInt(n) => n.fmt(f),
ExtendedBigInt::Infinity => f32::INFINITY.fmt(f),
ExtendedBigInt::MinusInfinity => f32::NEG_INFINITY.fmt(f),
ExtendedBigInt::MinusZero => {
// FIXME Come up with a way of formatting this with a
// "-" prefix.
0.fmt(f)
}
ExtendedBigInt::Nan => "nan".fmt(f),
}
}
}
impl Zero for ExtendedBigInt {
fn zero() -> Self {
ExtendedBigInt::BigInt(BigInt::zero())
}
fn is_zero(&self) -> bool {
match self {
Self::BigInt(n) => n.is_zero(),
Self::MinusZero => true,
_ => false,
}
}
}
impl Add for ExtendedBigInt {
type Output = Self;
fn add(self, other: Self) -> Self {
match (self, other) {
(Self::BigInt(m), Self::BigInt(n)) => Self::BigInt(m.add(n)),
(Self::BigInt(_), Self::MinusInfinity) => Self::MinusInfinity,
(Self::BigInt(_), Self::Infinity) => Self::Infinity,
(Self::BigInt(_), Self::Nan) => Self::Nan,
(Self::BigInt(m), Self::MinusZero) => Self::BigInt(m),
(Self::Infinity, Self::BigInt(_)) => Self::Infinity,
(Self::Infinity, Self::Infinity) => Self::Infinity,
(Self::Infinity, Self::MinusZero) => Self::Infinity,
(Self::Infinity, Self::MinusInfinity) => Self::Nan,
(Self::Infinity, Self::Nan) => Self::Nan,
(Self::MinusInfinity, Self::BigInt(_)) => Self::MinusInfinity,
(Self::MinusInfinity, Self::MinusInfinity) => Self::MinusInfinity,
(Self::MinusInfinity, Self::MinusZero) => Self::MinusInfinity,
(Self::MinusInfinity, Self::Infinity) => Self::Nan,
(Self::MinusInfinity, Self::Nan) => Self::Nan,
(Self::Nan, _) => Self::Nan,
(Self::MinusZero, other) => other,
}
}
}
impl PartialEq for ExtendedBigInt {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::BigInt(m), Self::BigInt(n)) => m.eq(n),
(Self::BigInt(_), Self::MinusInfinity) => false,
(Self::BigInt(_), Self::Infinity) => false,
(Self::BigInt(_), Self::Nan) => false,
(Self::BigInt(_), Self::MinusZero) => false,
(Self::Infinity, Self::BigInt(_)) => false,
(Self::Infinity, Self::Infinity) => true,
(Self::Infinity, Self::MinusZero) => false,
(Self::Infinity, Self::MinusInfinity) => false,
(Self::Infinity, Self::Nan) => false,
(Self::MinusInfinity, Self::BigInt(_)) => false,
(Self::MinusInfinity, Self::Infinity) => false,
(Self::MinusInfinity, Self::MinusZero) => false,
(Self::MinusInfinity, Self::MinusInfinity) => true,
(Self::MinusInfinity, Self::Nan) => false,
(Self::Nan, _) => false,
(Self::MinusZero, Self::BigInt(_)) => false,
(Self::MinusZero, Self::Infinity) => false,
(Self::MinusZero, Self::MinusZero) => true,
(Self::MinusZero, Self::MinusInfinity) => false,
(Self::MinusZero, Self::Nan) => false,
}
}
}
impl PartialOrd for ExtendedBigInt {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
match (self, other) {
(Self::BigInt(m), Self::BigInt(n)) => m.partial_cmp(n),
(Self::BigInt(_), Self::MinusInfinity) => Some(Ordering::Greater),
(Self::BigInt(_), Self::Infinity) => Some(Ordering::Less),
(Self::BigInt(_), Self::Nan) => None,
(Self::BigInt(m), Self::MinusZero) => m.partial_cmp(&BigInt::zero()),
(Self::Infinity, Self::BigInt(_)) => Some(Ordering::Greater),
(Self::Infinity, Self::Infinity) => Some(Ordering::Equal),
(Self::Infinity, Self::MinusZero) => Some(Ordering::Greater),
(Self::Infinity, Self::MinusInfinity) => Some(Ordering::Greater),
(Self::Infinity, Self::Nan) => None,
(Self::MinusInfinity, Self::BigInt(_)) => Some(Ordering::Less),
(Self::MinusInfinity, Self::Infinity) => Some(Ordering::Less),
(Self::MinusInfinity, Self::MinusZero) => Some(Ordering::Less),
(Self::MinusInfinity, Self::MinusInfinity) => Some(Ordering::Equal),
(Self::MinusInfinity, Self::Nan) => None,
(Self::Nan, _) => None,
(Self::MinusZero, Self::BigInt(n)) => BigInt::zero().partial_cmp(n),
(Self::MinusZero, Self::Infinity) => Some(Ordering::Less),
(Self::MinusZero, Self::MinusZero) => Some(Ordering::Equal),
(Self::MinusZero, Self::MinusInfinity) => Some(Ordering::Greater),
(Self::MinusZero, Self::Nan) => None,
}
}
}
#[cfg(test)]
mod tests {
use num_bigint::BigInt;
use num_traits::Zero;
use crate::extendedbigint::ExtendedBigInt;
#[test]
fn test_addition_infinity() {
let summand1 = ExtendedBigInt::BigInt(BigInt::zero());
let summand2 = ExtendedBigInt::Infinity;
assert_eq!(summand1 + summand2, ExtendedBigInt::Infinity);
}
#[test]
fn test_addition_minus_infinity() {
let summand1 = ExtendedBigInt::BigInt(BigInt::zero());
let summand2 = ExtendedBigInt::MinusInfinity;
assert_eq!(summand1 + summand2, ExtendedBigInt::MinusInfinity);
}
#[test]
fn test_addition_nan() {
let summand1 = ExtendedBigInt::BigInt(BigInt::zero());
let summand2 = ExtendedBigInt::Nan;
let sum = summand1 + summand2;
match sum {
ExtendedBigInt::Nan => (),
_ => unreachable!(),
}
}
#[test]
fn test_display() {
assert_eq!(format!("{}", ExtendedBigInt::BigInt(BigInt::zero())), "0");
assert_eq!(format!("{}", ExtendedBigInt::Infinity), "inf");
assert_eq!(format!("{}", ExtendedBigInt::MinusInfinity), "-inf");
assert_eq!(format!("{}", ExtendedBigInt::Nan), "nan");
// FIXME Come up with a way of displaying negative zero as
// "-0". Currently it displays as just "0".
//
// assert_eq!(format!("{}", ExtendedBigInt::MinusZero), "-0");
//
}
}

119
src/uu/seq/src/number.rs Normal file
View file

@ -0,0 +1,119 @@
// spell-checker:ignore extendedbigdecimal extendedbigint
//! A type to represent the possible start, increment, and end values for seq.
//!
//! The [`Number`] enumeration represents the possible values for the
//! start, increment, and end values for `seq`. These may be integers,
//! floating point numbers, negative zero, etc. A [`Number`] can be
//! parsed from a string by calling [`parse`].
use num_traits::Zero;
use crate::extendedbigdecimal::ExtendedBigDecimal;
use crate::extendedbigint::ExtendedBigInt;
/// An integral or floating point number.
#[derive(Debug, PartialEq)]
pub enum Number {
Int(ExtendedBigInt),
Float(ExtendedBigDecimal),
}
impl Number {
/// Decide whether this number is zero (either positive or negative).
pub fn is_zero(&self) -> bool {
// We would like to implement `num_traits::Zero`, but it
// requires an addition implementation, and we don't want to
// implement that here.
match self {
Number::Int(n) => n.is_zero(),
Number::Float(x) => x.is_zero(),
}
}
/// Convert this number into an `ExtendedBigDecimal`.
pub fn into_extended_big_decimal(self) -> ExtendedBigDecimal {
match self {
Number::Int(n) => ExtendedBigDecimal::from(n),
Number::Float(x) => x,
}
}
/// The integer number one.
pub fn one() -> Self {
// We would like to implement `num_traits::One`, but it requires
// a multiplication implementation, and we don't want to
// implement that here.
Number::Int(ExtendedBigInt::one())
}
/// Round this number towards the given other number.
///
/// If `other` is greater, then round up. If `other` is smaller,
/// then round down.
pub fn round_towards(self, other: &ExtendedBigInt) -> ExtendedBigInt {
match self {
// If this number is already an integer, it is already
// rounded to the nearest integer in the direction of
// `other`.
Number::Int(num) => num,
// Otherwise, if this number is a float, we need to decide
// whether `other` is larger or smaller than it, and thus
// whether to round up or round down, respectively.
Number::Float(num) => {
let other: ExtendedBigDecimal = From::from(other.clone());
if other > num {
num.ceil()
} else {
// If they are equal, then `self` is already an
// integer, so calling `floor()` does no harm and
// will just return that integer anyway.
num.floor()
}
}
}
}
}
/// A number with a specified number of integer and fractional digits.
///
/// This struct can be used to represent a number along with information
/// on how many significant digits to use when displaying the number.
/// The [`num_integral_digits`] field also includes the width needed to
/// display the "-" character for a negative number.
///
/// You can get an instance of this struct by calling [`str::parse`].
#[derive(Debug)]
pub struct PreciseNumber {
pub number: Number,
pub num_integral_digits: usize,
pub num_fractional_digits: usize,
}
impl PreciseNumber {
pub fn new(
number: Number,
num_integral_digits: usize,
num_fractional_digits: usize,
) -> PreciseNumber {
PreciseNumber {
number,
num_integral_digits,
num_fractional_digits,
}
}
/// The integer number one.
pub fn one() -> Self {
// We would like to implement `num_traits::One`, but it requires
// a multiplication implementation, and we don't want to
// implement that here.
PreciseNumber::new(Number::one(), 1, 0)
}
/// Decide whether this number is zero (either positive or negative).
pub fn is_zero(&self) -> bool {
// We would like to implement `num_traits::Zero`, but it
// requires an addition implementation, and we don't want to
// implement that here.
self.number.is_zero()
}
}

View file

@ -0,0 +1,589 @@
// spell-checker:ignore extendedbigdecimal extendedbigint bigdecimal numberparse
//! Parsing numbers for use in `seq`.
//!
//! This module provides an implementation of [`FromStr`] for the
//! [`PreciseNumber`] struct.
use std::convert::TryInto;
use std::str::FromStr;
use bigdecimal::BigDecimal;
use num_bigint::BigInt;
use num_bigint::Sign;
use num_traits::Num;
use num_traits::Zero;
use crate::extendedbigdecimal::ExtendedBigDecimal;
use crate::extendedbigint::ExtendedBigInt;
use crate::number::Number;
use crate::number::PreciseNumber;
/// An error returned when parsing a number fails.
#[derive(Debug, PartialEq)]
pub enum ParseNumberError {
Float,
Nan,
Hex,
}
/// Decide whether a given string and its parsed `BigInt` is negative zero.
fn is_minus_zero_int(s: &str, n: &BigInt) -> bool {
s.starts_with('-') && n == &BigInt::zero()
}
/// Decide whether a given string and its parsed `BigDecimal` is negative zero.
fn is_minus_zero_float(s: &str, x: &BigDecimal) -> bool {
s.starts_with('-') && x == &BigDecimal::zero()
}
/// Parse a number with neither a decimal point nor an exponent.
///
/// # Errors
///
/// This function returns an error if the input string is a variant of
/// "NaN" or if no [`BigInt`] could be parsed from the string.
///
/// # Examples
///
/// ```rust,ignore
/// let actual = "0".parse::<Number>().unwrap().number;
/// let expected = Number::BigInt(BigInt::zero());
/// assert_eq!(actual, expected);
/// ```
fn parse_no_decimal_no_exponent(s: &str) -> Result<PreciseNumber, ParseNumberError> {
match s.parse::<BigInt>() {
Ok(n) => {
// If `s` is '-0', then `parse()` returns `BigInt::zero()`,
// but we need to return `Number::MinusZeroInt` instead.
if is_minus_zero_int(s, &n) {
Ok(PreciseNumber::new(
Number::Int(ExtendedBigInt::MinusZero),
s.len(),
0,
))
} else {
Ok(PreciseNumber::new(
Number::Int(ExtendedBigInt::BigInt(n)),
s.len(),
0,
))
}
}
Err(_) => {
// Possibly "NaN" or "inf".
//
// TODO In Rust v1.53.0, this change
// https://github.com/rust-lang/rust/pull/78618 improves the
// parsing of floats to include being able to parse "NaN"
// and "inf". So when the minimum version of this crate is
// increased to 1.53.0, we should just use the built-in
// `f32` parsing instead.
if s.eq_ignore_ascii_case("inf") {
Ok(PreciseNumber::new(
Number::Float(ExtendedBigDecimal::Infinity),
0,
0,
))
} else if s.eq_ignore_ascii_case("-inf") {
Ok(PreciseNumber::new(
Number::Float(ExtendedBigDecimal::MinusInfinity),
0,
0,
))
} else if s.eq_ignore_ascii_case("nan") || s.eq_ignore_ascii_case("-nan") {
Err(ParseNumberError::Nan)
} else {
Err(ParseNumberError::Float)
}
}
}
}
/// Parse a number with an exponent but no decimal point.
///
/// # Errors
///
/// This function returns an error if `s` is not a valid number.
///
/// # Examples
///
/// ```rust,ignore
/// let actual = "1e2".parse::<Number>().unwrap().number;
/// let expected = "100".parse::<BigInt>().unwrap();
/// assert_eq!(actual, expected);
/// ```
fn parse_exponent_no_decimal(s: &str, j: usize) -> Result<PreciseNumber, ParseNumberError> {
let exponent: i64 = s[j + 1..].parse().map_err(|_| ParseNumberError::Float)?;
// If the exponent is strictly less than zero, then the number
// should be treated as a floating point number that will be
// displayed in decimal notation. For example, "1e-2" will be
// displayed as "0.01", but "1e2" will be displayed as "100",
// without a decimal point.
let x: BigDecimal = s.parse().map_err(|_| ParseNumberError::Float)?;
let num_integral_digits = if is_minus_zero_float(s, &x) {
2
} else {
let total = j as i64 + exponent;
let result = if total < 1 {
1
} else {
total.try_into().unwrap()
};
if x.sign() == Sign::Minus {
result + 1
} else {
result
}
};
let num_fractional_digits = if exponent < 0 { -exponent as usize } else { 0 };
if exponent < 0 {
if is_minus_zero_float(s, &x) {
Ok(PreciseNumber::new(
Number::Float(ExtendedBigDecimal::MinusZero),
num_integral_digits,
num_fractional_digits,
))
} else {
Ok(PreciseNumber::new(
Number::Float(ExtendedBigDecimal::BigDecimal(x)),
num_integral_digits,
num_fractional_digits,
))
}
} else {
let zeros = "0".repeat(exponent.try_into().unwrap());
let expanded = [&s[0..j], &zeros].concat();
parse_no_decimal_no_exponent(&expanded)
}
}
/// Parse a number with a decimal point but no exponent.
///
/// # Errors
///
/// This function returns an error if `s` is not a valid number.
///
/// # Examples
///
/// ```rust,ignore
/// let actual = "1.2".parse::<Number>().unwrap().number;
/// let expected = "1.2".parse::<BigDecimal>().unwrap();
/// assert_eq!(actual, expected);
/// ```
fn parse_decimal_no_exponent(s: &str, i: usize) -> Result<PreciseNumber, ParseNumberError> {
let x: BigDecimal = s.parse().map_err(|_| ParseNumberError::Float)?;
let num_integral_digits = i;
let num_fractional_digits = s.len() - (i + 1);
if is_minus_zero_float(s, &x) {
Ok(PreciseNumber::new(
Number::Float(ExtendedBigDecimal::MinusZero),
num_integral_digits,
num_fractional_digits,
))
} else {
Ok(PreciseNumber::new(
Number::Float(ExtendedBigDecimal::BigDecimal(x)),
num_integral_digits,
num_fractional_digits,
))
}
}
/// Parse a number with both a decimal point and an exponent.
///
/// # Errors
///
/// This function returns an error if `s` is not a valid number.
///
/// # Examples
///
/// ```rust,ignore
/// let actual = "1.2e3".parse::<Number>().unwrap().number;
/// let expected = "1200".parse::<BigInt>().unwrap();
/// assert_eq!(actual, expected);
/// ```
fn parse_decimal_and_exponent(
s: &str,
i: usize,
j: usize,
) -> Result<PreciseNumber, ParseNumberError> {
// Because of the match guard, this subtraction will not underflow.
let num_digits_between_decimal_point_and_e = (j - (i + 1)) as i64;
let exponent: i64 = s[j + 1..].parse().map_err(|_| ParseNumberError::Float)?;
let val: BigDecimal = s.parse().map_err(|_| ParseNumberError::Float)?;
let num_integral_digits = {
let minimum: usize = {
let integral_part: f64 = s[..j].parse().map_err(|_| ParseNumberError::Float)?;
if integral_part == -0.0 && integral_part.is_sign_negative() {
2
} else {
1
}
};
let total = i as i64 + exponent;
if total < minimum as i64 {
minimum
} else {
total.try_into().unwrap()
}
};
let num_fractional_digits = if num_digits_between_decimal_point_and_e < exponent {
0
} else {
(num_digits_between_decimal_point_and_e - exponent)
.try_into()
.unwrap()
};
if num_digits_between_decimal_point_and_e <= exponent {
if is_minus_zero_float(s, &val) {
Ok(PreciseNumber::new(
Number::Int(ExtendedBigInt::MinusZero),
num_integral_digits,
num_fractional_digits,
))
} else {
let zeros: String = "0".repeat(
(exponent - num_digits_between_decimal_point_and_e)
.try_into()
.unwrap(),
);
let expanded = [&s[0..i], &s[i + 1..j], &zeros].concat();
let n = expanded
.parse::<BigInt>()
.map_err(|_| ParseNumberError::Float)?;
Ok(PreciseNumber::new(
Number::Int(ExtendedBigInt::BigInt(n)),
num_integral_digits,
num_fractional_digits,
))
}
} else if is_minus_zero_float(s, &val) {
Ok(PreciseNumber::new(
Number::Float(ExtendedBigDecimal::MinusZero),
num_integral_digits,
num_fractional_digits,
))
} else {
Ok(PreciseNumber::new(
Number::Float(ExtendedBigDecimal::BigDecimal(val)),
num_integral_digits,
num_fractional_digits,
))
}
}
/// Parse a hexadecimal integer from a string.
///
/// # Errors
///
/// This function returns an error if no [`BigInt`] could be parsed from
/// the string.
///
/// # Examples
///
/// ```rust,ignore
/// let actual = "0x0".parse::<Number>().unwrap().number;
/// let expected = Number::BigInt(BigInt::zero());
/// assert_eq!(actual, expected);
/// ```
fn parse_hexadecimal(s: &str) -> Result<PreciseNumber, ParseNumberError> {
let (is_neg, s) = if s.starts_with('-') {
(true, &s[3..])
} else {
(false, &s[2..])
};
if s.starts_with('-') || s.starts_with('+') {
// Even though this is more like an invalid hexadecimal number,
// GNU reports this as an invalid floating point number, so we
// use `ParseNumberError::Float` to match that behavior.
return Err(ParseNumberError::Float);
}
let num = BigInt::from_str_radix(s, 16).map_err(|_| ParseNumberError::Hex)?;
match (is_neg, num == BigInt::zero()) {
(true, true) => Ok(PreciseNumber::new(
Number::Int(ExtendedBigInt::MinusZero),
2,
0,
)),
(true, false) => Ok(PreciseNumber::new(
Number::Int(ExtendedBigInt::BigInt(-num)),
0,
0,
)),
(false, _) => Ok(PreciseNumber::new(
Number::Int(ExtendedBigInt::BigInt(num)),
0,
0,
)),
}
}
impl FromStr for PreciseNumber {
type Err = ParseNumberError;
fn from_str(mut s: &str) -> Result<Self, Self::Err> {
// Trim leading whitespace.
s = s.trim_start();
// Trim a single leading "+" character.
if s.starts_with('+') {
s = &s[1..];
}
// Check if the string seems to be in hexadecimal format.
//
// May be 0x123 or -0x123, so the index `i` may be either 0 or 1.
if let Some(i) = s.to_lowercase().find("0x") {
if i <= 1 {
return parse_hexadecimal(s);
}
}
// Find the decimal point and the exponent symbol. Parse the
// number differently depending on its form. This is important
// because the form of the input dictates how the output will be
// presented.
match (s.find('.'), s.find('e')) {
// For example, "123456" or "inf".
(None, None) => parse_no_decimal_no_exponent(s),
// For example, "123e456" or "1e-2".
(None, Some(j)) => parse_exponent_no_decimal(s, j),
// For example, "123.456".
(Some(i), None) => parse_decimal_no_exponent(s, i),
// For example, "123.456e789".
(Some(i), Some(j)) if i < j => parse_decimal_and_exponent(s, i, j),
// For example, "1e2.3" or "1.2.3".
_ => Err(ParseNumberError::Float),
}
}
}
#[cfg(test)]
mod tests {
use bigdecimal::BigDecimal;
use num_bigint::BigInt;
use num_traits::Zero;
use crate::extendedbigdecimal::ExtendedBigDecimal;
use crate::extendedbigint::ExtendedBigInt;
use crate::number::Number;
use crate::number::PreciseNumber;
use crate::numberparse::ParseNumberError;
/// Convenience function for parsing a [`Number`] and unwrapping.
fn parse(s: &str) -> Number {
s.parse::<PreciseNumber>().unwrap().number
}
/// Convenience function for getting the number of integral digits.
fn num_integral_digits(s: &str) -> usize {
s.parse::<PreciseNumber>().unwrap().num_integral_digits
}
/// Convenience function for getting the number of fractional digits.
fn num_fractional_digits(s: &str) -> usize {
s.parse::<PreciseNumber>().unwrap().num_fractional_digits
}
#[test]
fn test_parse_minus_zero_int() {
assert_eq!(parse("-0e0"), Number::Int(ExtendedBigInt::MinusZero));
assert_eq!(parse("-0e-0"), Number::Int(ExtendedBigInt::MinusZero));
assert_eq!(parse("-0e1"), Number::Int(ExtendedBigInt::MinusZero));
assert_eq!(parse("-0e+1"), Number::Int(ExtendedBigInt::MinusZero));
assert_eq!(parse("-0.0e1"), Number::Int(ExtendedBigInt::MinusZero));
assert_eq!(parse("-0x0"), Number::Int(ExtendedBigInt::MinusZero));
}
#[test]
fn test_parse_minus_zero_float() {
assert_eq!(parse("-0.0"), Number::Float(ExtendedBigDecimal::MinusZero));
assert_eq!(parse("-0e-1"), Number::Float(ExtendedBigDecimal::MinusZero));
assert_eq!(
parse("-0.0e-1"),
Number::Float(ExtendedBigDecimal::MinusZero)
);
}
#[test]
fn test_parse_big_int() {
assert_eq!(parse("0"), Number::Int(ExtendedBigInt::zero()));
assert_eq!(parse("0.1e1"), Number::Int(ExtendedBigInt::one()));
assert_eq!(
parse("1.0e1"),
Number::Int(ExtendedBigInt::BigInt("10".parse::<BigInt>().unwrap()))
);
}
#[test]
fn test_parse_hexadecimal_big_int() {
assert_eq!(parse("0x0"), Number::Int(ExtendedBigInt::zero()));
assert_eq!(
parse("0x10"),
Number::Int(ExtendedBigInt::BigInt("16".parse::<BigInt>().unwrap()))
);
}
#[test]
fn test_parse_big_decimal() {
assert_eq!(
parse("0.0"),
Number::Float(ExtendedBigDecimal::BigDecimal(
"0.0".parse::<BigDecimal>().unwrap()
))
);
assert_eq!(
parse(".0"),
Number::Float(ExtendedBigDecimal::BigDecimal(
"0.0".parse::<BigDecimal>().unwrap()
))
);
assert_eq!(
parse("1.0"),
Number::Float(ExtendedBigDecimal::BigDecimal(
"1.0".parse::<BigDecimal>().unwrap()
))
);
assert_eq!(
parse("10e-1"),
Number::Float(ExtendedBigDecimal::BigDecimal(
"1.0".parse::<BigDecimal>().unwrap()
))
);
assert_eq!(
parse("-1e-3"),
Number::Float(ExtendedBigDecimal::BigDecimal(
"-0.001".parse::<BigDecimal>().unwrap()
))
);
}
#[test]
fn test_parse_inf() {
assert_eq!(parse("inf"), Number::Float(ExtendedBigDecimal::Infinity));
assert_eq!(parse("+inf"), Number::Float(ExtendedBigDecimal::Infinity));
assert_eq!(
parse("-inf"),
Number::Float(ExtendedBigDecimal::MinusInfinity)
);
}
#[test]
fn test_parse_invalid_float() {
assert_eq!(
"1.2.3".parse::<PreciseNumber>().unwrap_err(),
ParseNumberError::Float
);
assert_eq!(
"1e2e3".parse::<PreciseNumber>().unwrap_err(),
ParseNumberError::Float
);
assert_eq!(
"1e2.3".parse::<PreciseNumber>().unwrap_err(),
ParseNumberError::Float
);
assert_eq!(
"-+-1".parse::<PreciseNumber>().unwrap_err(),
ParseNumberError::Float
);
}
#[test]
fn test_parse_invalid_hex() {
assert_eq!(
"0xg".parse::<PreciseNumber>().unwrap_err(),
ParseNumberError::Hex
);
}
#[test]
fn test_parse_invalid_nan() {
assert_eq!(
"nan".parse::<PreciseNumber>().unwrap_err(),
ParseNumberError::Nan
);
assert_eq!(
"NAN".parse::<PreciseNumber>().unwrap_err(),
ParseNumberError::Nan
);
assert_eq!(
"NaN".parse::<PreciseNumber>().unwrap_err(),
ParseNumberError::Nan
);
assert_eq!(
"nAn".parse::<PreciseNumber>().unwrap_err(),
ParseNumberError::Nan
);
assert_eq!(
"-nan".parse::<PreciseNumber>().unwrap_err(),
ParseNumberError::Nan
);
}
#[test]
fn test_num_integral_digits() {
// no decimal, no exponent
assert_eq!(num_integral_digits("123"), 3);
// decimal, no exponent
assert_eq!(num_integral_digits("123.45"), 3);
// exponent, no decimal
assert_eq!(num_integral_digits("123e4"), 3 + 4);
assert_eq!(num_integral_digits("123e-4"), 1);
assert_eq!(num_integral_digits("-1e-3"), 2);
// decimal and exponent
assert_eq!(num_integral_digits("123.45e6"), 3 + 6);
assert_eq!(num_integral_digits("123.45e-6"), 1);
assert_eq!(num_integral_digits("123.45e-1"), 2);
// minus zero int
assert_eq!(num_integral_digits("-0e0"), 2);
assert_eq!(num_integral_digits("-0e-0"), 2);
assert_eq!(num_integral_digits("-0e1"), 3);
assert_eq!(num_integral_digits("-0e+1"), 3);
assert_eq!(num_integral_digits("-0.0e1"), 3);
// minus zero float
assert_eq!(num_integral_digits("-0.0"), 2);
assert_eq!(num_integral_digits("-0e-1"), 2);
assert_eq!(num_integral_digits("-0.0e-1"), 2);
// TODO In GNU `seq`, the `-w` option does not seem to work with
// hexadecimal arguments. In order to match that behavior, we
// report the number of integral digits as zero for hexadecimal
// inputs.
assert_eq!(num_integral_digits("0xff"), 0);
}
#[test]
fn test_num_fractional_digits() {
// no decimal, no exponent
assert_eq!(num_fractional_digits("123"), 0);
assert_eq!(num_fractional_digits("0xff"), 0);
// decimal, no exponent
assert_eq!(num_fractional_digits("123.45"), 2);
// exponent, no decimal
assert_eq!(num_fractional_digits("123e4"), 0);
assert_eq!(num_fractional_digits("123e-4"), 4);
assert_eq!(num_fractional_digits("123e-1"), 1);
assert_eq!(num_fractional_digits("-1e-3"), 3);
// decimal and exponent
assert_eq!(num_fractional_digits("123.45e6"), 0);
assert_eq!(num_fractional_digits("123.45e1"), 1);
assert_eq!(num_fractional_digits("123.45e-6"), 8);
assert_eq!(num_fractional_digits("123.45e-1"), 3);
// minus zero int
assert_eq!(num_fractional_digits("-0e0"), 0);
assert_eq!(num_fractional_digits("-0e-0"), 0);
assert_eq!(num_fractional_digits("-0e1"), 0);
assert_eq!(num_fractional_digits("-0e+1"), 0);
assert_eq!(num_fractional_digits("-0.0e1"), 0);
// minus zero float
assert_eq!(num_fractional_digits("-0.0"), 1);
assert_eq!(num_fractional_digits("-0e-1"), 1);
assert_eq!(num_fractional_digits("-0.0e-1"), 2);
}
}

View file

@ -1,23 +1,24 @@
// TODO: Make -w flag work with decimals
// TODO: Support -f flag
// spell-checker:ignore (ToDO) istr chiter argptr ilen
// spell-checker:ignore (ToDO) istr chiter argptr ilen extendedbigdecimal extendedbigint numberparse
#[macro_use]
extern crate uucore;
use clap::{crate_version, App, AppSettings, Arg};
use num_bigint::BigInt;
use num_traits::One;
use num_traits::Zero;
use num_traits::{Num, ToPrimitive};
use std::cmp;
use std::io::{stdout, ErrorKind, Write};
use std::str::FromStr;
mod digits;
use crate::digits::num_fractional_digits;
use crate::digits::num_integral_digits;
mod extendedbigdecimal;
mod extendedbigint;
mod number;
mod numberparse;
use crate::extendedbigdecimal::ExtendedBigDecimal;
use crate::extendedbigint::ExtendedBigInt;
use crate::number::Number;
use crate::number::PreciseNumber;
use crate::numberparse::ParseNumberError;
use uucore::display::Quotable;
@ -43,124 +44,55 @@ struct SeqOptions {
widths: bool,
}
enum Number {
/// Negative zero, as if it were an integer.
MinusZero,
BigInt(BigInt),
F64(f64),
}
impl Number {
fn is_zero(&self) -> bool {
match self {
Number::MinusZero => true,
Number::BigInt(n) => n.is_zero(),
Number::F64(n) => n.is_zero(),
}
}
fn into_f64(self) -> f64 {
match self {
Number::MinusZero => -0.,
// BigInt::to_f64() can not return None.
Number::BigInt(n) => n.to_f64().unwrap(),
Number::F64(n) => n,
}
}
/// Convert this number into a bigint, consuming it.
///
/// For floats, this returns the [`BigInt`] corresponding to the
/// floor of the number.
fn into_bigint(self) -> BigInt {
match self {
Number::MinusZero => BigInt::zero(),
Number::F64(x) => BigInt::from(x.floor() as i64),
Number::BigInt(n) => n,
}
}
}
impl FromStr for Number {
type Err = String;
fn from_str(mut s: &str) -> Result<Self, Self::Err> {
s = s.trim_start();
if s.starts_with('+') {
s = &s[1..];
}
let is_neg = s.starts_with('-');
match s.to_lowercase().find("0x") {
Some(i) if i <= 1 => match &s.as_bytes()[i + 2] {
b'-' | b'+' => Err(format!(
"invalid hexadecimal argument: {}\nTry '{} --help' for more information.",
s.quote(),
uucore::execution_phrase(),
)),
// TODO: hexadecimal floating point parsing (see #2660)
b'.' => Err(format!(
"NotImplemented: hexadecimal floating point numbers: {}\nTry '{} --help' for more information.",
s.quote(),
uucore::execution_phrase(),
)),
_ => {
let num = BigInt::from_str_radix(&s[i + 2..], 16)
.map_err(|_| format!(
"invalid hexadecimal argument: {}\nTry '{} --help' for more information.",
s.quote(),
uucore::execution_phrase(),
))?;
match (is_neg, num == BigInt::zero()) {
(true, true) => Ok(Number::MinusZero),
(true, false) => Ok(Number::BigInt(-num)),
(false, _) => Ok(Number::BigInt(num)),
}
}
},
Some(_) => Err(format!(
"invalid hexadecimal argument: {}\nTry '{} --help' for more information.",
s.quote(),
uucore::execution_phrase(),
)),
None => match s.parse::<BigInt>() {
Ok(n) => {
// If `s` is '-0', then `parse()` returns
// `BigInt::zero()`, but we need to return
// `Number::MinusZero` instead.
if n == BigInt::zero() && is_neg {
Ok(Number::MinusZero)
} else {
Ok(Number::BigInt(n))
}
}
Err(_) => match s.parse::<f64>() {
Ok(value) if value.is_nan() => Err(format!(
"invalid 'not-a-number' argument: {}\nTry '{} --help' for more information.",
s.quote(),
uucore::execution_phrase(),
)),
Ok(value) => Ok(Number::F64(value)),
Err(_) => Err(format!(
"invalid floating point argument: {}\nTry '{} --help' for more information.",
s.quote(),
uucore::execution_phrase(),
)),
},
},
}
}
}
/// A range of integers.
///
/// The elements are (first, increment, last).
type RangeInt = (BigInt, BigInt, BigInt);
type RangeInt = (ExtendedBigInt, ExtendedBigInt, ExtendedBigInt);
/// A range of f64.
/// A range of floats.
///
/// The elements are (first, increment, last).
type RangeF64 = (f64, f64, f64);
type RangeFloat = (ExtendedBigDecimal, ExtendedBigDecimal, ExtendedBigDecimal);
/// Terminate the process with error code 1.
///
/// Before terminating the process, this function prints an error
/// message that depends on `arg` and `e`.
///
/// Although the signature of this function states that it returns a
/// [`PreciseNumber`], it never reaches the return statement. It is just
/// there to make it easier to use this function when unwrapping the
/// result of calling [`str::parse`] when attempting to parse a
/// [`PreciseNumber`].
///
/// # Examples
///
/// ```rust,ignore
/// let s = "1.2e-3";
/// s.parse::<PreciseNumber>.unwrap_or_else(|e| exit_with_error(s, e))
/// ```
fn exit_with_error(arg: &str, e: ParseNumberError) -> ! {
match e {
ParseNumberError::Float => crash!(
1,
"invalid floating point argument: {}\nTry '{} --help' for more information.",
arg.quote(),
uucore::execution_phrase()
),
ParseNumberError::Nan => crash!(
1,
"invalid 'not-a-number' argument: {}\nTry '{} --help' for more information.",
arg.quote(),
uucore::execution_phrase()
),
ParseNumberError::Hex => crash!(
1,
"invalid hexadecimal argument: {}\nTry '{} --help' for more information.",
arg.quote(),
uucore::execution_phrase()
),
}
}
pub fn uumain(args: impl uucore::Args) -> i32 {
let usage = usage();
@ -174,53 +106,17 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
widths: matches.is_present(OPT_WIDTHS),
};
let mut largest_dec = 0;
let mut padding = 0;
let first = if numbers.len() > 1 {
let slice = numbers[0];
largest_dec = num_fractional_digits(slice).unwrap_or_else(|_| {
crash!(
1,
"invalid floating point argument: {}\n Try '{} --help' for more information.",
slice.quote(),
uucore::execution_phrase()
)
});
padding = num_integral_digits(slice).unwrap_or_else(|_| {
crash!(
1,
"invalid floating point argument: {}\n Try '{} --help' for more information.",
slice.quote(),
uucore::execution_phrase()
)
});
crash_if_err!(1, slice.parse())
slice.parse().unwrap_or_else(|e| exit_with_error(slice, e))
} else {
Number::BigInt(BigInt::one())
PreciseNumber::one()
};
let increment = if numbers.len() > 2 {
let slice = numbers[1];
let dec = num_fractional_digits(slice).unwrap_or_else(|_| {
crash!(
1,
"invalid floating point argument: {}\n Try '{} --help' for more information.",
slice.quote(),
uucore::execution_phrase()
)
});
let int_digits = num_integral_digits(slice).unwrap_or_else(|_| {
crash!(
1,
"invalid floating point argument: {}\n Try '{} --help' for more information.",
slice.quote(),
uucore::execution_phrase()
)
});
largest_dec = cmp::max(largest_dec, dec);
padding = cmp::max(padding, int_digits);
crash_if_err!(1, slice.parse())
slice.parse().unwrap_or_else(|e| exit_with_error(slice, e))
} else {
Number::BigInt(BigInt::one())
PreciseNumber::one()
};
if increment.is_zero() {
show_error!(
@ -230,54 +126,36 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
);
return 1;
}
let last: Number = {
let last: PreciseNumber = {
let slice = numbers[numbers.len() - 1];
let int_digits = num_integral_digits(slice).unwrap_or_else(|_| {
crash!(
1,
"invalid floating point argument: {}\n Try '{} --help' for more information.",
slice.quote(),
uucore::execution_phrase()
)
});
padding = cmp::max(padding, int_digits);
crash_if_err!(1, slice.parse())
slice.parse().unwrap_or_else(|e| exit_with_error(slice, e))
};
let is_negative_zero_f64 = |x: f64| x == -0.0 && x.is_sign_negative() && largest_dec == 0;
let result = match (first, last, increment) {
// For example, `seq -0 1 2` or `seq -0 1 2.0`.
(Number::MinusZero, last, Number::BigInt(increment)) => print_seq_integers(
(BigInt::zero(), increment, last.into_bigint()),
options.separator,
options.terminator,
options.widths,
padding,
true,
),
// For example, `seq -0e0 1 2` or `seq -0e0 1 2.0`.
(Number::F64(x), last, Number::BigInt(increment)) if is_negative_zero_f64(x) => {
let padding = first
.num_integral_digits
.max(increment.num_integral_digits)
.max(last.num_integral_digits);
let largest_dec = first
.num_fractional_digits
.max(increment.num_fractional_digits);
let result = match (first.number, increment.number, last.number) {
(Number::Int(first), Number::Int(increment), last) => {
let last = last.round_towards(&first);
print_seq_integers(
(BigInt::zero(), increment, last.into_bigint()),
(first, increment, last),
options.separator,
options.terminator,
options.widths,
padding,
true,
)
}
// For example, `seq 0 1 2` or `seq 0 1 2.0`.
(Number::BigInt(first), last, Number::BigInt(increment)) => print_seq_integers(
(first, increment, last.into_bigint()),
options.separator,
options.terminator,
options.widths,
padding,
false,
),
// For example, `seq 0 0.5 1` or `seq 0.0 0.5 1` or `seq 0.0 0.5 1.0`.
(first, last, increment) => print_seq(
(first.into_f64(), increment.into_f64(), last.into_f64()),
(first, increment, last) => print_seq(
(
first.into_extended_big_decimal(),
increment.into_extended_big_decimal(),
last.into_extended_big_decimal(),
),
largest_dec,
options.separator,
options.terminator,
@ -329,7 +207,7 @@ pub fn uu_app() -> App<'static, 'static> {
)
}
fn done_printing<T: Num + PartialOrd>(next: &T, increment: &T, last: &T) -> bool {
fn done_printing<T: Zero + PartialOrd>(next: &T, increment: &T, last: &T) -> bool {
if increment >= &T::zero() {
next > last
} else {
@ -337,9 +215,73 @@ fn done_printing<T: Num + PartialOrd>(next: &T, increment: &T, last: &T) -> bool
}
}
/// Write a big decimal formatted according to the given parameters.
///
/// This method is an adapter to support displaying negative zero on
/// Rust versions earlier than 1.53.0. After that version, we should be
/// able to display negative zero using the default formatting provided
/// by `-0.0f32`, for example.
fn write_value_float(
writer: &mut impl Write,
value: &ExtendedBigDecimal,
width: usize,
precision: usize,
is_first_iteration: bool,
) -> std::io::Result<()> {
let value_as_str = if *value == ExtendedBigDecimal::MinusZero && is_first_iteration {
format!(
"-{value:>0width$.precision$}",
value = value,
width = if width > 0 { width - 1 } else { width },
precision = precision,
)
} else if *value == ExtendedBigDecimal::Infinity || *value == ExtendedBigDecimal::MinusInfinity
{
format!(
"{value:>width$.precision$}",
value = value,
width = width,
precision = precision,
)
} else {
format!(
"{value:>0width$.precision$}",
value = value,
width = width,
precision = precision,
)
};
write!(writer, "{}", value_as_str)
}
/// Write a big int formatted according to the given parameters.
fn write_value_int(
writer: &mut impl Write,
value: &ExtendedBigInt,
width: usize,
pad: bool,
is_first_iteration: bool,
) -> std::io::Result<()> {
let value_as_str = if pad {
if *value == ExtendedBigInt::MinusZero && is_first_iteration {
format!("-{value:>0width$}", value = value, width = width - 1,)
} else {
format!("{value:>0width$}", value = value, width = width,)
}
} else if *value == ExtendedBigInt::MinusZero && is_first_iteration {
format!("-{}", value)
} else {
format!("{}", value)
};
write!(writer, "{}", value_as_str)
}
// TODO `print_seq()` and `print_seq_integers()` are nearly identical,
// they could be refactored into a single more general function.
/// Floating point based code path
fn print_seq(
range: RangeF64,
range: RangeFloat,
largest_dec: usize,
separator: String,
terminator: String,
@ -349,30 +291,23 @@ fn print_seq(
let stdout = stdout();
let mut stdout = stdout.lock();
let (first, increment, last) = range;
let mut i = 0isize;
let is_first_minus_zero = first == -0.0 && first.is_sign_negative();
let mut value = first + i as f64 * increment;
let mut value = first;
let padding = if pad { padding + 1 + largest_dec } else { 0 };
let mut is_first_iteration = true;
while !done_printing(&value, &increment, &last) {
if !is_first_iteration {
write!(stdout, "{}", separator)?;
}
let mut width = padding;
if is_first_iteration && is_first_minus_zero {
write!(stdout, "-")?;
width -= 1;
}
is_first_iteration = false;
write!(
stdout,
"{value:>0width$.precision$}",
value = value,
width = width,
precision = largest_dec,
write_value_float(
&mut stdout,
&value,
padding,
largest_dec,
is_first_iteration,
)?;
i += 1;
value = first + i as f64 * increment;
// TODO Implement augmenting addition.
value = value + increment.clone();
is_first_iteration = false;
}
if !is_first_iteration {
write!(stdout, "{}", terminator)?;
@ -401,7 +336,6 @@ fn print_seq_integers(
terminator: String,
pad: bool,
padding: usize,
is_first_minus_zero: bool,
) -> std::io::Result<()> {
let stdout = stdout();
let mut stdout = stdout.lock();
@ -412,18 +346,10 @@ fn print_seq_integers(
if !is_first_iteration {
write!(stdout, "{}", separator)?;
}
let mut width = padding;
if is_first_iteration && is_first_minus_zero {
write!(stdout, "-")?;
width -= 1;
}
write_value_int(&mut stdout, &value, padding, pad, is_first_iteration)?;
// TODO Implement augmenting addition.
value = value + increment.clone();
is_first_iteration = false;
if pad {
write!(stdout, "{number:>0width$}", number = value, width = width)?;
} else {
write!(stdout, "{}", value)?;
}
value += &increment;
}
if !is_first_iteration {

View file

@ -193,16 +193,13 @@ pub fn read<T: Read>(
/// Split `read` into `Line`s, and add them to `lines`.
fn parse_lines<'a>(
mut read: &'a str,
read: &'a str,
lines: &mut Vec<Line<'a>>,
line_data: &mut LineData<'a>,
separator: u8,
settings: &GlobalSettings,
) {
// Strip a trailing separator. TODO: Once our MinRustV is 1.45 or above, use strip_suffix() instead.
if read.ends_with(separator as char) {
read = &read[..read.len() - 1];
}
let read = read.strip_suffix(separator as char).unwrap_or(read);
assert!(lines.is_empty());
assert!(line_data.selections.is_empty());

View file

@ -30,6 +30,7 @@ data-encoding-macro = { version="0.1.12", optional=true }
z85 = { version="3.0.3", optional=true }
libc = { version="0.2.15", optional=true }
once_cell = "1.8.0"
os_display = "0.1.0"
[target.'cfg(unix)'.dependencies]
walkdir = { version="2.3.2", optional=true }

View file

@ -163,12 +163,11 @@ pub fn strip_minus_from_mode(args: &mut Vec<String>) -> bool {
if arg == "--" {
break;
}
if arg.starts_with('-') {
if let Some(arg_stripped) = arg.strip_prefix('-') {
if let Some(second) = arg.chars().nth(1) {
match second {
'r' | 'w' | 'x' | 'X' | 's' | 't' | 'u' | 'g' | 'o' | '0'..='7' => {
// TODO: use strip_prefix() once minimum rust version reaches 1.45.0
*arg = arg[1..arg.len()].to_string();
*arg = arg_stripped.to_string();
return true;
}
_ => {}

View file

@ -19,378 +19,16 @@
/// println_verbatim(path)?; // Prints "foo/bar.baz"
/// # Ok::<(), std::io::Error>(())
/// ```
// spell-checker:ignore Fbar
use std::borrow::Cow;
use std::ffi::OsStr;
#[cfg(any(unix, target_os = "wasi", windows))]
use std::fmt::Write as FmtWrite;
use std::fmt::{self, Display, Formatter};
use std::io::{self, Write as IoWrite};
#[cfg(unix)]
use std::os::unix::ffi::OsStrExt;
#[cfg(target_os = "wasi")]
use std::os::wasi::ffi::OsStrExt;
#[cfg(any(unix, target_os = "wasi"))]
use std::str::from_utf8;
/// An extension trait for displaying filenames to users.
pub trait Quotable {
/// Returns an object that implements [`Display`] for printing filenames with
/// proper quoting and escaping for the platform.
///
/// On Unix this corresponds to sh/bash syntax, on Windows Powershell syntax
/// is used.
///
/// # Examples
///
/// ```
/// use std::path::Path;
/// use uucore::display::Quotable;
///
/// let path = Path::new("foo/bar.baz");
///
/// println!("Found file {}", path.quote()); // Prints "Found file 'foo/bar.baz'"
/// ```
fn quote(&self) -> Quoted<'_>;
/// Like `quote()`, but don't actually add quotes unless necessary because of
/// whitespace or special characters.
///
/// # Examples
///
/// ```
/// use std::path::Path;
/// use uucore::display::Quotable;
/// use uucore::show_error;
///
/// let foo = Path::new("foo/bar.baz");
/// let bar = Path::new("foo bar");
///
/// show_error!("{}: Not found", foo.maybe_quote()); // Prints "util: foo/bar.baz: Not found"
/// show_error!("{}: Not found", bar.maybe_quote()); // Prints "util: 'foo bar': Not found"
/// ```
fn maybe_quote(&self) -> Quoted<'_> {
let mut quoted = self.quote();
quoted.force_quote = false;
quoted
}
}
macro_rules! impl_as_ref {
($type: ty) => {
impl Quotable for $type {
fn quote(&self) -> Quoted<'_> {
Quoted::new(self.as_ref())
}
}
};
}
impl_as_ref!(str);
impl_as_ref!(&'_ str);
impl_as_ref!(String);
impl_as_ref!(std::path::Path);
impl_as_ref!(&'_ std::path::Path);
impl_as_ref!(std::path::PathBuf);
impl_as_ref!(std::path::Component<'_>);
impl_as_ref!(std::path::Components<'_>);
impl_as_ref!(std::path::Iter<'_>);
impl_as_ref!(std::ffi::OsStr);
impl_as_ref!(&'_ std::ffi::OsStr);
impl_as_ref!(std::ffi::OsString);
// Cow<'_, str> does not implement AsRef<OsStr> and this is unlikely to be fixed
// for backward compatibility reasons. Otherwise we'd use a blanket impl.
impl Quotable for Cow<'_, str> {
fn quote(&self) -> Quoted<'_> {
let text: &str = self.as_ref();
Quoted::new(text.as_ref())
}
}
impl Quotable for Cow<'_, std::path::Path> {
fn quote(&self) -> Quoted<'_> {
let text: &std::path::Path = self.as_ref();
Quoted::new(text.as_ref())
}
}
/// A wrapper around [`OsStr`] for printing paths with quoting and escaping applied.
#[derive(Debug, Copy, Clone)]
pub struct Quoted<'a> {
text: &'a OsStr,
force_quote: bool,
}
impl<'a> Quoted<'a> {
fn new(text: &'a OsStr) -> Self {
Quoted {
text,
force_quote: true,
}
}
}
impl Display for Quoted<'_> {
#[cfg(any(windows, unix, target_os = "wasi"))]
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
// On Unix we emulate sh syntax. On Windows Powershell.
// They're just similar enough to share some code.
/// Characters with special meaning outside quotes.
// https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_02
// I don't know why % is in there, and GNU doesn't quote it either.
// {} were used in a version elsewhere but seem unnecessary, GNU doesn't
// quote them. They're used in function definitions but not in a way we
// have to worry about.
#[cfg(any(unix, target_os = "wasi"))]
const SPECIAL_SHELL_CHARS: &[u8] = b"|&;<>()$`\\\"'*?[]=";
// FIXME: I'm not a PowerShell wizard and don't know if this is correct.
// I just copied the Unix version, removed \, and added ,{} based on
// experimentation.
// I have noticed that ~?*[] only get expanded in some contexts, so watch
// out for that if doing your own tests.
// Get-ChildItem seems unwilling to quote anything so it doesn't help.
// There's the additional wrinkle that Windows has stricter requirements
// for filenames: I've been testing using a Linux build of PowerShell, but
// this code doesn't even compile on Linux.
#[cfg(windows)]
const SPECIAL_SHELL_CHARS: &[u8] = b"|&;<>()$`\"'*?[]=,{}";
/// Characters with a special meaning at the beginning of a name.
// ~ expands a home directory.
// # starts a comment.
// ! is a common extension for expanding the shell history.
#[cfg(any(unix, target_os = "wasi"))]
const SPECIAL_SHELL_CHARS_START: &[char] = &['~', '#', '!'];
// Same deal as before, this is possibly incomplete.
// A single stand-alone exclamation mark seems to have some special meaning.
#[cfg(windows)]
const SPECIAL_SHELL_CHARS_START: &[char] = &['~', '#', '@', '!'];
/// Characters that are interpreted specially in a double-quoted string.
#[cfg(any(unix, target_os = "wasi"))]
const DOUBLE_UNSAFE: &[u8] = &[b'"', b'`', b'$', b'\\'];
#[cfg(windows)]
const DOUBLE_UNSAFE: &[u8] = &[b'"', b'`', b'$'];
let text = match self.text.to_str() {
None => return write_escaped(f, self.text),
Some(text) => text,
};
let mut is_single_safe = true;
let mut is_double_safe = true;
let mut requires_quote = self.force_quote;
if let Some(first) = text.chars().next() {
if SPECIAL_SHELL_CHARS_START.contains(&first) {
requires_quote = true;
}
// Unlike in Unix, quoting an argument may stop it
// from being recognized as an option. I like that very much.
// But we don't want to quote "-" because that's a common
// special argument and PowerShell doesn't mind it.
#[cfg(windows)]
if first == '-' && text.len() > 1 {
requires_quote = true;
}
} else {
// Empty string
requires_quote = true;
}
for ch in text.chars() {
if ch.is_ascii() {
let ch = ch as u8;
if ch == b'\'' {
is_single_safe = false;
}
if DOUBLE_UNSAFE.contains(&ch) {
is_double_safe = false;
}
if !requires_quote && SPECIAL_SHELL_CHARS.contains(&ch) {
requires_quote = true;
}
if ch.is_ascii_control() {
return write_escaped(f, self.text);
}
}
if !requires_quote && ch.is_whitespace() {
// This includes unicode whitespace.
// We maybe don't have to escape it, we don't escape other lookalike
// characters either, but it's confusing if it goes unquoted.
requires_quote = true;
}
}
if !requires_quote {
return f.write_str(text);
} else if is_single_safe {
return write_simple(f, text, '\'');
} else if is_double_safe {
return write_simple(f, text, '\"');
} else {
return write_single_escaped(f, text);
}
fn write_simple(f: &mut Formatter<'_>, text: &str, quote: char) -> fmt::Result {
f.write_char(quote)?;
f.write_str(text)?;
f.write_char(quote)?;
Ok(())
}
#[cfg(any(unix, target_os = "wasi"))]
fn write_single_escaped(f: &mut Formatter<'_>, text: &str) -> fmt::Result {
let mut iter = text.split('\'');
if let Some(chunk) = iter.next() {
if !chunk.is_empty() {
write_simple(f, chunk, '\'')?;
}
}
for chunk in iter {
f.write_str("\\'")?;
if !chunk.is_empty() {
write_simple(f, chunk, '\'')?;
}
}
Ok(())
}
/// Write using the syntax described here:
/// https://www.gnu.org/software/bash/manual/html_node/ANSI_002dC-Quoting.html
///
/// Supported by these shells:
/// - bash
/// - zsh
/// - busybox sh
/// - mksh
///
/// Not supported by these:
/// - fish
/// - dash
/// - tcsh
#[cfg(any(unix, target_os = "wasi"))]
fn write_escaped(f: &mut Formatter<'_>, text: &OsStr) -> fmt::Result {
f.write_str("$'")?;
for chunk in from_utf8_iter(text.as_bytes()) {
match chunk {
Ok(chunk) => {
for ch in chunk.chars() {
match ch {
'\n' => f.write_str("\\n")?,
'\t' => f.write_str("\\t")?,
'\r' => f.write_str("\\r")?,
// We could do \b, \f, \v, etc., but those are
// rare enough to be confusing.
// \0 doesn't work consistently because of the
// octal \nnn syntax, and null bytes can't appear
// in filenames anyway.
ch if ch.is_ascii_control() => write!(f, "\\x{:02X}", ch as u8)?,
'\\' | '\'' => {
// '?' and '"' can also be escaped this way
// but AFAICT there's no reason to do so
f.write_char('\\')?;
f.write_char(ch)?;
}
ch => {
f.write_char(ch)?;
}
}
}
}
Err(unit) => write!(f, "\\x{:02X}", unit)?,
}
}
f.write_char('\'')?;
Ok(())
}
#[cfg(windows)]
fn write_single_escaped(f: &mut Formatter<'_>, text: &str) -> fmt::Result {
// Quotes in Powershell can be escaped by doubling them
f.write_char('\'')?;
let mut iter = text.split('\'');
if let Some(chunk) = iter.next() {
f.write_str(chunk)?;
}
for chunk in iter {
f.write_str("''")?;
f.write_str(chunk)?;
}
f.write_char('\'')?;
Ok(())
}
#[cfg(windows)]
fn write_escaped(f: &mut Formatter<'_>, text: &OsStr) -> fmt::Result {
// ` takes the role of \ since \ is already used as the path separator.
// Things are UTF-16-oriented, so we escape code units as "`u{1234}".
use std::char::decode_utf16;
use std::os::windows::ffi::OsStrExt;
f.write_char('"')?;
for ch in decode_utf16(text.encode_wide()) {
match ch {
Ok(ch) => match ch {
'\0' => f.write_str("`0")?,
'\r' => f.write_str("`r")?,
'\n' => f.write_str("`n")?,
'\t' => f.write_str("`t")?,
ch if ch.is_ascii_control() => write!(f, "`u{{{:04X}}}", ch as u8)?,
'`' => f.write_str("``")?,
'$' => f.write_str("`$")?,
'"' => f.write_str("\"\"")?,
ch => f.write_char(ch)?,
},
Err(err) => write!(f, "`u{{{:04X}}}", err.unpaired_surrogate())?,
}
}
f.write_char('"')?;
Ok(())
}
}
#[cfg(not(any(unix, target_os = "wasi", windows)))]
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
// As a fallback, we use Rust's own escaping rules.
// This is reasonably sane and very easy to implement.
// We use single quotes because that's hardcoded in a lot of tests.
let text = self.text.to_string_lossy();
if self.force_quote || !text.chars().all(|ch| ch.is_alphanumeric() || ch == '.') {
write!(f, "'{}'", text.escape_debug())
} else {
f.write_str(&text)
}
}
}
#[cfg(any(unix, target_os = "wasi"))]
fn from_utf8_iter(mut bytes: &[u8]) -> impl Iterator<Item = Result<&str, u8>> {
std::iter::from_fn(move || {
if bytes.is_empty() {
return None;
}
match from_utf8(bytes) {
Ok(text) => {
bytes = &[];
Some(Ok(text))
}
Err(err) if err.valid_up_to() == 0 => {
let res = bytes[0];
bytes = &bytes[1..];
Some(Err(res))
}
Err(err) => {
let (valid, rest) = bytes.split_at(err.valid_up_to());
bytes = rest;
Some(Ok(from_utf8(valid).unwrap()))
}
}
})
}
// These used to be defined here, but they live in their own crate now.
pub use os_display::{Quotable, Quoted};
/// Print a path (or `OsStr`-like object) directly to stdout, with a trailing newline,
/// without losing any information if its encoding is invalid.
@ -429,129 +67,3 @@ pub fn print_verbatim<S: AsRef<OsStr>>(text: S) -> io::Result<()> {
write!(stdout, "{}", std::path::Path::new(text.as_ref()).display())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn verify_quote(cases: &[(impl Quotable, &str)]) {
for (case, expected) in cases {
assert_eq!(case.quote().to_string(), *expected);
}
}
fn verify_maybe(cases: &[(impl Quotable, &str)]) {
for (case, expected) in cases {
assert_eq!(case.maybe_quote().to_string(), *expected);
}
}
/// This should hold on any platform, or else other tests fail.
#[test]
fn test_basic() {
verify_quote(&[
("foo", "'foo'"),
("", "''"),
("foo/bar.baz", "'foo/bar.baz'"),
]);
verify_maybe(&[
("foo", "foo"),
("", "''"),
("foo bar", "'foo bar'"),
("$foo", "'$foo'"),
("-", "-"),
]);
}
#[cfg(any(unix, target_os = "wasi", windows))]
#[test]
fn test_common() {
verify_maybe(&[
("a#b", "a#b"),
("#ab", "'#ab'"),
("a~b", "a~b"),
("!", "'!'"),
]);
}
#[cfg(any(unix, target_os = "wasi"))]
#[test]
fn test_unix() {
verify_quote(&[
("can't", r#""can't""#),
(r#"can'"t"#, r#"'can'\''"t'"#),
(r#"can'$t"#, r#"'can'\''$t'"#),
("foo\nb\ta\r\\\0`r", r#"$'foo\nb\ta\r\\\x00`r'"#),
("foo\x02", r#"$'foo\x02'"#),
(r#"'$''"#, r#"\''$'\'\'"#),
]);
verify_quote(&[(OsStr::from_bytes(b"foo\xFF"), r#"$'foo\xFF'"#)]);
verify_maybe(&[
("-x", "-x"),
("a,b", "a,b"),
("a\\b", "'a\\b'"),
("}", ("}")),
]);
}
#[cfg(windows)]
#[test]
fn test_windows() {
use std::ffi::OsString;
use std::os::windows::ffi::OsStringExt;
verify_quote(&[
(r#"foo\bar"#, r#"'foo\bar'"#),
("can't", r#""can't""#),
(r#"can'"t"#, r#"'can''"t'"#),
(r#"can'$t"#, r#"'can''$t'"#),
("foo\nb\ta\r\\\0`r", r#""foo`nb`ta`r\`0``r""#),
("foo\x02", r#""foo`u{0002}""#),
(r#"'$''"#, r#"'''$'''''"#),
]);
verify_quote(&[(
OsString::from_wide(&[b'x' as u16, 0xD800]),
r#""x`u{D800}""#,
)]);
verify_maybe(&[
("-x", "'-x'"),
("a,b", "'a,b'"),
("a\\b", "a\\b"),
("}", "'}'"),
]);
}
#[cfg(any(unix, target_os = "wasi"))]
#[test]
fn test_utf8_iter() {
type ByteStr = &'static [u8];
type Chunk = Result<&'static str, u8>;
const CASES: &[(ByteStr, &[Chunk])] = &[
(b"", &[]),
(b"hello", &[Ok("hello")]),
// Immediately invalid
(b"\xFF", &[Err(b'\xFF')]),
// Incomplete UTF-8
(b"\xC2", &[Err(b'\xC2')]),
(b"\xF4\x8F", &[Err(b'\xF4'), Err(b'\x8F')]),
(b"\xFF\xFF", &[Err(b'\xFF'), Err(b'\xFF')]),
(b"hello\xC2", &[Ok("hello"), Err(b'\xC2')]),
(b"\xFFhello", &[Err(b'\xFF'), Ok("hello")]),
(b"\xFF\xC2hello", &[Err(b'\xFF'), Err(b'\xC2'), Ok("hello")]),
(b"foo\xFFbar", &[Ok("foo"), Err(b'\xFF'), Ok("bar")]),
(
b"foo\xF4\x8Fbar",
&[Ok("foo"), Err(b'\xF4'), Err(b'\x8F'), Ok("bar")],
),
(
b"foo\xFF\xC2bar",
&[Ok("foo"), Err(b'\xFF'), Err(b'\xC2'), Ok("bar")],
),
];
for &(case, expected) in CASES {
assert_eq!(
from_utf8_iter(case).collect::<Vec<_>>().as_slice(),
expected
);
}
}
}

View file

@ -113,18 +113,12 @@ fn test_wrap_bad_arg() {
#[test]
fn test_base32_extra_operand() {
let ts = TestScenario::new(util_name!());
// Expect a failure when multiple files are specified.
ts.ucmd()
new_ucmd!()
.arg("a.txt")
.arg("b.txt")
.fails()
.stderr_only(format!(
"{0}: extra operand 'b.txt'\nTry '{1} {0} --help' for more information.",
ts.util_name,
ts.bin_path.to_string_lossy()
));
.usage_error("extra operand 'b.txt'");
}
#[test]

View file

@ -95,18 +95,12 @@ fn test_wrap_bad_arg() {
#[test]
fn test_base64_extra_operand() {
let ts = TestScenario::new(util_name!());
// Expect a failure when multiple files are specified.
ts.ucmd()
new_ucmd!()
.arg("a.txt")
.arg("b.txt")
.fails()
.stderr_only(format!(
"{0}: extra operand 'b.txt'\nTry '{1} {0} --help' for more information.",
ts.util_name,
ts.bin_path.to_string_lossy()
));
.usage_error("extra operand 'b.txt'");
}
#[test]

View file

@ -114,12 +114,7 @@ fn test_no_args() {
#[test]
fn test_no_args_output() {
let ts = TestScenario::new(util_name!());
ts.ucmd().fails().stderr_is(&format!(
"{0}: missing operand\nTry '{1} {0} --help' for more information.",
ts.util_name,
ts.bin_path.to_string_lossy()
));
new_ucmd!().fails().usage_error("missing operand");
}
#[test]
@ -129,12 +124,10 @@ fn test_too_many_args() {
#[test]
fn test_too_many_args_output() {
let ts = TestScenario::new(util_name!());
ts.ucmd().args(&["a", "b", "c"]).fails().stderr_is(format!(
"{0}: extra operand 'c'\nTry '{1} {0} --help' for more information.",
ts.util_name,
ts.bin_path.to_string_lossy()
));
new_ucmd!()
.args(&["a", "b", "c"])
.fails()
.usage_error("extra operand 'c'");
}
#[cfg(any(unix, target_os = "redox"))]

View file

@ -563,17 +563,13 @@ fn test_cp_backup_off() {
#[test]
fn test_cp_backup_no_clobber_conflicting_options() {
let ts = TestScenario::new(util_name!());
ts.ucmd()
new_ucmd!()
.arg("--backup")
.arg("--no-clobber")
.arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_HOW_ARE_YOU_SOURCE)
.fails().stderr_is(&format!(
"{0}: options --backup and --no-clobber are mutually exclusive\nTry '{1} {0} --help' for more information.",
ts.util_name,
ts.bin_path.to_string_lossy()
));
.fails()
.usage_error("options --backup and --no-clobber are mutually exclusive");
}
#[test]

View file

@ -108,6 +108,15 @@ fn test_ignore_environment() {
scene.ucmd().arg("-").run().no_stdout();
}
#[test]
fn test_empty_name() {
new_ucmd!()
.arg("-i")
.arg("=xyz")
.run()
.stderr_only("env: warning: no name specified for value 'xyz'");
}
#[test]
fn test_null_delimiter() {
let out = new_ucmd!()

View file

@ -15,15 +15,10 @@ fn test_more_dir_arg() {
// Maybe we could capture the error, i.e. "Device not found" in that case
// but I am leaving this for later
if atty::is(atty::Stream::Stdout) {
let ts = TestScenario::new(util_name!());
let result = ts.ucmd().arg(".").run();
result.failure();
let expected_error_message = &format!(
"{0}: '.' is a directory.\nTry '{1} {0} --help' for more information.",
ts.util_name,
ts.bin_path.to_string_lossy()
);
assert_eq!(result.stderr_str().trim(), expected_error_message);
new_ucmd!()
.arg(".")
.fails()
.usage_error("'.' is a directory.");
} else {
}
}

View file

@ -522,17 +522,13 @@ fn test_mv_backup_off() {
#[test]
fn test_mv_backup_no_clobber_conflicting_options() {
let ts = TestScenario::new(util_name!());
ts.ucmd().arg("--backup")
new_ucmd!()
.arg("--backup")
.arg("--no-clobber")
.arg("file1")
.arg("file2")
.fails()
.stderr_is(&format!("{0}: options --backup and --no-clobber are mutually exclusive\nTry '{1} {0} --help' for more information.",
ts.util_name,
ts.bin_path.to_string_lossy()
));
.usage_error("options --backup and --no-clobber are mutually exclusive");
}
#[test]

View file

@ -22,15 +22,10 @@ fn test_negative_adjustment() {
#[test]
fn test_adjustment_with_no_command_should_error() {
let ts = TestScenario::new(util_name!());
ts.ucmd()
.args(&["-n", "19"])
.run()
.stderr_is(&format!("{0}: A command must be given with an adjustment.\nTry '{1} {0} --help' for more information.\n",
ts.util_name,
ts.bin_path.to_string_lossy()
));
new_ucmd!()
.args(&["-n", "19"])
.fails()
.usage_error("A command must be given with an adjustment.");
}
#[test]

View file

@ -7,25 +7,25 @@ fn test_hex_rejects_sign_after_identifier() {
.args(&["0x-123ABC"])
.fails()
.no_stdout()
.stderr_contains("invalid hexadecimal argument: '0x-123ABC'")
.stderr_contains("invalid floating point argument: '0x-123ABC'")
.stderr_contains("for more information.");
new_ucmd!()
.args(&["0x+123ABC"])
.fails()
.no_stdout()
.stderr_contains("invalid hexadecimal argument: '0x+123ABC'")
.stderr_contains("invalid floating point argument: '0x+123ABC'")
.stderr_contains("for more information.");
new_ucmd!()
.args(&["-0x-123ABC"])
.fails()
.no_stdout()
.stderr_contains("invalid hexadecimal argument: '-0x-123ABC'")
.stderr_contains("invalid floating point argument: '-0x-123ABC'")
.stderr_contains("for more information.");
new_ucmd!()
.args(&["-0x+123ABC"])
.fails()
.no_stdout()
.stderr_contains("invalid hexadecimal argument: '-0x+123ABC'")
.stderr_contains("invalid floating point argument: '-0x+123ABC'")
.stderr_contains("for more information.");
}
@ -60,30 +60,24 @@ fn test_hex_identifier_in_wrong_place() {
.args(&["1234ABCD0x"])
.fails()
.no_stdout()
.stderr_contains("invalid hexadecimal argument: '1234ABCD0x'")
.stderr_contains("invalid floating point argument: '1234ABCD0x'")
.stderr_contains("for more information.");
}
#[test]
fn test_rejects_nan() {
let ts = TestScenario::new(util_name!());
ts.ucmd().args(&["NaN"]).fails().stderr_only(format!(
"{0}: invalid 'not-a-number' argument: 'NaN'\nTry '{1} {0} --help' for more information.",
ts.util_name,
ts.bin_path.to_string_lossy()
));
new_ucmd!()
.arg("NaN")
.fails()
.usage_error("invalid 'not-a-number' argument: 'NaN'");
}
#[test]
fn test_rejects_non_floats() {
let ts = TestScenario::new(util_name!());
ts.ucmd().args(&["foo"]).fails().stderr_only(&format!(
"{0}: invalid floating point argument: 'foo'\nTry '{1} {0} --help' for more information.",
ts.util_name,
ts.bin_path.to_string_lossy()
));
new_ucmd!()
.arg("foo")
.fails()
.usage_error("invalid floating point argument: 'foo'");
}
#[test]
@ -479,6 +473,15 @@ fn test_width_decimal_scientific_notation_trailing_zeros_increment() {
.no_stderr();
}
#[test]
fn test_width_negative_scientific_notation() {
new_ucmd!()
.args(&["-w", "-1e-3", "1"])
.succeeds()
.stdout_is("-0.001\n00.999\n")
.no_stderr();
}
/// Test that trailing zeros in the end argument do not contribute to width.
#[test]
fn test_width_decimal_scientific_notation_trailing_zeros_end() {
@ -521,6 +524,22 @@ fn test_inf() {
run(&["inf"], b"1\n2\n3\n");
}
#[test]
fn test_inf_width() {
run(
&["-w", "1.000", "inf", "inf"],
b"1.000\n inf\n inf\n inf\n",
);
}
#[test]
fn test_neg_inf_width() {
run(
&["-w", "1.000", "-inf", "-inf"],
b"1.000\n -inf\n -inf\n -inf\n",
);
}
#[test]
fn test_ignore_leading_whitespace() {
new_ucmd!()
@ -538,9 +557,65 @@ fn test_trailing_whitespace_error() {
new_ucmd!()
.arg("1 ")
.fails()
.no_stdout()
.stderr_contains("seq: invalid floating point argument: '1 '")
// FIXME The second line of the error message is "Try 'seq
// --help' for more information."
.stderr_contains("for more information.");
.usage_error("invalid floating point argument: '1 '");
}
#[test]
fn test_negative_zero_int_start_float_increment() {
new_ucmd!()
.args(&["-0", "0.1", "0.1"])
.succeeds()
.stdout_is("-0.0\n0.1\n")
.no_stderr();
}
#[test]
fn test_float_precision_increment() {
new_ucmd!()
.args(&["999", "0.1", "1000.1"])
.succeeds()
.stdout_is(
"999.0
999.1
999.2
999.3
999.4
999.5
999.6
999.7
999.8
999.9
1000.0
1000.1
",
)
.no_stderr();
}
/// Test for floating point precision issues.
#[test]
fn test_negative_increment_decimal() {
new_ucmd!()
.args(&["0.1", "-0.1", "-0.2"])
.succeeds()
.stdout_is("0.1\n0.0\n-0.1\n-0.2\n")
.no_stderr();
}
#[test]
fn test_zero_not_first() {
new_ucmd!()
.args(&["-w", "-0.1", "0.1", "0.1"])
.succeeds()
.stdout_is("-0.1\n00.0\n00.1\n")
.no_stderr();
}
#[test]
fn test_rounding_end() {
new_ucmd!()
.args(&["1", "-1", "0.1"])
.succeeds()
.stdout_is("1\n")
.no_stderr();
}

View file

@ -53,16 +53,10 @@ fn test_stdbuf_trailing_var_arg() {
#[cfg(not(target_os = "windows"))]
#[test]
fn test_stdbuf_line_buffering_stdin_fails() {
let ts = TestScenario::new(util_name!());
ts.ucmd()
new_ucmd!()
.args(&["-i", "L", "head"])
.fails()
.stderr_is(&format!(
"{0}: line buffering stdin is meaningless\nTry '{1} {0} --help' for more information.",
ts.util_name,
ts.bin_path.to_string_lossy()
));
.usage_error("line buffering stdin is meaningless");
}
#[cfg(not(target_os = "windows"))]

View file

@ -62,6 +62,10 @@ fn read_scenario_fixture<S: AsRef<OsStr>>(tmpd: &Option<Rc<TempDir>>, file_rel_p
/// within a struct which has convenience assertion functions about those outputs
#[derive(Debug, Clone)]
pub struct CmdResult {
/// bin_path provided by `TestScenario` or `UCommand`
bin_path: String,
/// util_name provided by `TestScenario` or `UCommand`
util_name: Option<String>,
//tmpd is used for convenience functions for asserts against fixtures
tmpd: Option<Rc<TempDir>>,
/// exit status for command (if there is one)
@ -77,6 +81,8 @@ pub struct CmdResult {
impl CmdResult {
pub fn new(
bin_path: String,
util_name: Option<String>,
tmpd: Option<Rc<TempDir>>,
code: Option<i32>,
success: bool,
@ -84,6 +90,8 @@ impl CmdResult {
stderr: &[u8],
) -> CmdResult {
CmdResult {
bin_path,
util_name,
tmpd,
code,
success,
@ -357,6 +365,23 @@ impl CmdResult {
self
}
/// asserts that
/// 1. the command resulted in stderr stream output that equals the
/// the following format when both are trimmed of trailing whitespace
/// `"{util_name}: {msg}\nTry '{bin_path} {util_name} --help' for more information."`
/// This the expected format when a UUsageError is returned or when show_error! is called
/// `msg` should be the same as the one provided to UUsageError::new or show_error!
///
/// 2. the command resulted in empty (zero-length) stdout stream output
pub fn usage_error<T: AsRef<str>>(&self, msg: T) -> &CmdResult {
self.stderr_only(format!(
"{0}: {2}\nTry '{1} {0} --help' for more information.",
self.util_name.as_ref().unwrap(), // This shouldn't be called using a normal command
self.bin_path,
msg.as_ref()
))
}
pub fn stdout_contains<T: AsRef<str>>(&self, cmp: T) -> &CmdResult {
assert!(
self.stdout_str().contains(cmp.as_ref()),
@ -728,20 +753,12 @@ impl AtPath {
// Source:
// http://stackoverflow.com/questions/31439011/getfinalpathnamebyhandle-without-prepended
let prefix = "\\\\?\\";
// FixME: replace ...
#[allow(clippy::manual_strip)]
if s.starts_with(prefix) {
String::from(&s[prefix.len()..])
if let Some(stripped) = s.strip_prefix(prefix) {
String::from(stripped)
} else {
s
}
// ... with ...
// if let Some(stripped) = s.strip_prefix(prefix) {
// String::from(stripped)
// } else {
// s
// }
// ... when using MSRV with stabilized `strip_prefix()`
}
}
@ -788,31 +805,36 @@ impl TestScenario {
/// Returns builder for invoking the target uutils binary. Paths given are
/// treated relative to the environment's unique temporary test directory.
pub fn ucmd(&self) -> UCommand {
let mut cmd = self.cmd(&self.bin_path);
cmd.arg(&self.util_name);
cmd
self.composite_cmd(&self.bin_path, &self.util_name, true)
}
/// Returns builder for invoking the target uutils binary. Paths given are
/// treated relative to the environment's unique temporary test directory.
pub fn composite_cmd<S: AsRef<OsStr>, T: AsRef<OsStr>>(
&self,
bin: S,
util_name: T,
env_clear: bool,
) -> UCommand {
UCommand::new_from_tmp(bin, Some(util_name), self.tmpd.clone(), env_clear)
}
/// Returns builder for invoking any system command. Paths given are treated
/// relative to the environment's unique temporary test directory.
pub fn cmd<S: AsRef<OsStr>>(&self, bin: S) -> UCommand {
UCommand::new_from_tmp(bin, self.tmpd.clone(), true)
UCommand::new_from_tmp::<S, S>(bin, None, self.tmpd.clone(), true)
}
/// Returns builder for invoking any uutils command. Paths given are treated
/// relative to the environment's unique temporary test directory.
pub fn ccmd<S: AsRef<OsStr>>(&self, bin: S) -> UCommand {
let mut cmd = self.cmd(&self.bin_path);
cmd.arg(bin);
cmd
self.composite_cmd(&self.bin_path, bin, true)
}
// different names are used rather than an argument
// because the need to keep the environment is exceedingly rare.
pub fn ucmd_keepenv(&self) -> UCommand {
let mut cmd = self.cmd_keepenv(&self.bin_path);
cmd.arg(&self.util_name);
cmd
self.composite_cmd(&self.bin_path, &self.util_name, false)
}
/// Returns builder for invoking any system command. Paths given are treated
@ -820,7 +842,7 @@ impl TestScenario {
/// Differs from the builder returned by `cmd` in that `cmd_keepenv` does not call
/// `Command::env_clear` (Clears the entire environment map for the child process.)
pub fn cmd_keepenv<S: AsRef<OsStr>>(&self, bin: S) -> UCommand {
UCommand::new_from_tmp(bin, self.tmpd.clone(), false)
UCommand::new_from_tmp::<S, S>(bin, None, self.tmpd.clone(), false)
}
}
@ -834,6 +856,8 @@ impl TestScenario {
pub struct UCommand {
pub raw: Command,
comm_string: String,
bin_path: String,
util_name: Option<String>,
tmpd: Option<Rc<TempDir>>,
has_run: bool,
ignore_stdin_write_error: bool,
@ -846,12 +870,20 @@ pub struct UCommand {
}
impl UCommand {
pub fn new<T: AsRef<OsStr>, U: AsRef<OsStr>>(arg: T, curdir: U, env_clear: bool) -> UCommand {
UCommand {
pub fn new<T: AsRef<OsStr>, S: AsRef<OsStr>, U: AsRef<OsStr>>(
bin_path: T,
util_name: Option<S>,
curdir: U,
env_clear: bool,
) -> UCommand {
let bin_path = bin_path.as_ref();
let util_name = util_name.as_ref().map(|un| un.as_ref());
let mut ucmd = UCommand {
tmpd: None,
has_run: false,
raw: {
let mut cmd = Command::new(arg.as_ref());
let mut cmd = Command::new(bin_path);
cmd.current_dir(curdir.as_ref());
if env_clear {
if cfg!(windows) {
@ -871,7 +903,9 @@ impl UCommand {
}
cmd
},
comm_string: String::from(arg.as_ref().to_str().unwrap()),
comm_string: String::from(bin_path.to_str().unwrap()),
bin_path: bin_path.to_str().unwrap().to_string(),
util_name: util_name.map(|un| un.to_str().unwrap().to_string()),
ignore_stdin_write_error: false,
bytes_into_stdin: None,
stdin: None,
@ -879,12 +913,23 @@ impl UCommand {
stderr: None,
#[cfg(target_os = "linux")]
limits: vec![],
};
if let Some(un) = util_name {
ucmd.arg(un);
}
ucmd
}
pub fn new_from_tmp<T: AsRef<OsStr>>(arg: T, tmpd: Rc<TempDir>, env_clear: bool) -> UCommand {
pub fn new_from_tmp<T: AsRef<OsStr>, S: AsRef<OsStr>>(
bin_path: T,
util_name: Option<S>,
tmpd: Rc<TempDir>,
env_clear: bool,
) -> UCommand {
let tmpd_path_buf = String::from(&(*tmpd.as_ref().path().to_str().unwrap()));
let mut ucmd: UCommand = UCommand::new(arg.as_ref(), tmpd_path_buf, env_clear);
let mut ucmd: UCommand = UCommand::new(bin_path, util_name, tmpd_path_buf, env_clear);
ucmd.tmpd = Some(tmpd);
ucmd
}
@ -1029,6 +1074,8 @@ impl UCommand {
let prog = self.run_no_wait().wait_with_output().unwrap();
CmdResult {
bin_path: self.bin_path.clone(),
util_name: self.util_name.clone(),
tmpd: self.tmpd.clone(),
code: prog.status.code(),
success: prog.status.success(),
@ -1276,6 +1323,8 @@ pub fn expected_result(ts: &TestScenario, args: &[&str]) -> std::result::Result<
};
Ok(CmdResult::new(
ts.bin_path.as_os_str().to_str().unwrap().to_string(),
Some(ts.util_name.clone()),
Some(result.tmpd()),
Some(result.code()),
result.succeeded(),
@ -1293,6 +1342,8 @@ mod tests {
#[test]
fn test_code_is() {
let res = CmdResult {
bin_path: "".into(),
util_name: None,
tmpd: None,
code: Some(32),
success: false,
@ -1306,6 +1357,8 @@ mod tests {
#[should_panic]
fn test_code_is_fail() {
let res = CmdResult {
bin_path: "".into(),
util_name: None,
tmpd: None,
code: Some(32),
success: false,
@ -1318,6 +1371,8 @@ mod tests {
#[test]
fn test_failure() {
let res = CmdResult {
bin_path: "".into(),
util_name: None,
tmpd: None,
code: None,
success: false,
@ -1331,6 +1386,8 @@ mod tests {
#[should_panic]
fn test_failure_fail() {
let res = CmdResult {
bin_path: "".into(),
util_name: None,
tmpd: None,
code: None,
success: true,
@ -1343,6 +1400,8 @@ mod tests {
#[test]
fn test_success() {
let res = CmdResult {
bin_path: "".into(),
util_name: None,
tmpd: None,
code: None,
success: true,
@ -1356,6 +1415,8 @@ mod tests {
#[should_panic]
fn test_success_fail() {
let res = CmdResult {
bin_path: "".into(),
util_name: None,
tmpd: None,
code: None,
success: false,
@ -1368,6 +1429,8 @@ mod tests {
#[test]
fn test_no_stderr_output() {
let res = CmdResult {
bin_path: "".into(),
util_name: None,
tmpd: None,
code: None,
success: true,
@ -1382,6 +1445,8 @@ mod tests {
#[should_panic]
fn test_no_stderr_fail() {
let res = CmdResult {
bin_path: "".into(),
util_name: None,
tmpd: None,
code: None,
success: true,
@ -1396,6 +1461,8 @@ mod tests {
#[should_panic]
fn test_no_stdout_fail() {
let res = CmdResult {
bin_path: "".into(),
util_name: None,
tmpd: None,
code: None,
success: true,
@ -1409,6 +1476,8 @@ mod tests {
#[test]
fn test_std_does_not_contain() {
let res = CmdResult {
bin_path: "".into(),
util_name: None,
tmpd: None,
code: None,
success: true,
@ -1423,6 +1492,8 @@ mod tests {
#[should_panic]
fn test_stdout_does_not_contain_fail() {
let res = CmdResult {
bin_path: "".into(),
util_name: None,
tmpd: None,
code: None,
success: true,
@ -1437,6 +1508,8 @@ mod tests {
#[should_panic]
fn test_stderr_does_not_contain_fail() {
let res = CmdResult {
bin_path: "".into(),
util_name: None,
tmpd: None,
code: None,
success: true,
@ -1450,6 +1523,8 @@ mod tests {
#[test]
fn test_stdout_matches() {
let res = CmdResult {
bin_path: "".into(),
util_name: None,
tmpd: None,
code: None,
success: true,
@ -1466,6 +1541,8 @@ mod tests {
#[should_panic]
fn test_stdout_matches_fail() {
let res = CmdResult {
bin_path: "".into(),
util_name: None,
tmpd: None,
code: None,
success: true,
@ -1481,6 +1558,8 @@ mod tests {
#[should_panic]
fn test_stdout_not_matches_fail() {
let res = CmdResult {
bin_path: "".into(),
util_name: None,
tmpd: None,
code: None,
success: true,
@ -1495,6 +1574,8 @@ mod tests {
#[test]
fn test_normalized_newlines_stdout_is() {
let res = CmdResult {
bin_path: "".into(),
util_name: None,
tmpd: None,
code: None,
success: true,
@ -1511,6 +1592,8 @@ mod tests {
#[should_panic]
fn test_normalized_newlines_stdout_is_fail() {
let res = CmdResult {
bin_path: "".into(),
util_name: None,
tmpd: None,
code: None,
success: true,