diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index d096ff43c..0372336a7 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -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 diff --git a/Cargo.lock b/Cargo.lock index 649a93d9a..005a3c125 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/src/uu/basename/src/basename.rs b/src/uu/basename/src/basename.rs index 464c0e4f1..f7f4a3d08 100644 --- a/src/uu/basename/src/basename.rs +++ b/src/uu/basename/src/basename.rs @@ -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() -} diff --git a/src/uu/env/src/env.rs b/src/uu/env/src/env.rs index aec97fc24..8967a798a 100644 --- a/src/uu/env/src/env.rs +++ b/src/uu/env/src/env.rs @@ -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()), diff --git a/src/uu/factor/BENCHMARKING.md b/src/uu/factor/BENCHMARKING.md index 6bf9cbf90..0f9afaff0 100644 --- a/src/uu/factor/BENCHMARKING.md +++ b/src/uu/factor/BENCHMARKING.md @@ -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. diff --git a/src/uu/factor/Cargo.toml b/src/uu/factor/Cargo.toml index 2d2fa236b..f79afeecf 100644 --- a/src/uu/factor/Cargo.toml +++ b/src/uu/factor/Cargo.toml @@ -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" diff --git a/src/uu/factor/src/numeric/gcd.rs b/src/uu/factor/src/numeric/gcd.rs index 004ef1515..78197c722 100644 --- a/src/uu/factor/src/numeric/gcd.rs +++ b/src/uu/factor/src/numeric/gcd.rs @@ -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 { diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 3fa3b4f8e..32707da36 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -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: {}: {}", diff --git a/src/uu/seq/BENCHMARKING.md b/src/uu/seq/BENCHMARKING.md new file mode 100644 index 000000000..4d2f82afe --- /dev/null +++ b/src/uu/seq/BENCHMARKING.md @@ -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 diff --git a/src/uu/seq/Cargo.toml b/src/uu/seq/Cargo.toml index f5a23310f..5aeffd3b9 100644 --- a/src/uu/seq/Cargo.toml +++ b/src/uu/seq/Cargo.toml @@ -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" diff --git a/src/uu/seq/src/digits.rs b/src/uu/seq/src/digits.rs deleted file mode 100644 index bde933978..000000000 --- a/src/uu/seq/src/digits.rs +++ /dev/null @@ -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 { - 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 { - 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); - } - } -} diff --git a/src/uu/seq/src/extendedbigdecimal.rs b/src/uu/seq/src/extendedbigdecimal.rs new file mode 100644 index 000000000..6cad83dad --- /dev/null +++ b/src/uu/seq/src/extendedbigdecimal.rs @@ -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 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 { + 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"); + // + } +} diff --git a/src/uu/seq/src/extendedbigint.rs b/src/uu/seq/src/extendedbigint.rs new file mode 100644 index 000000000..4a33fa617 --- /dev/null +++ b/src/uu/seq/src/extendedbigint.rs @@ -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 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 { + 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"); + // + } +} diff --git a/src/uu/seq/src/number.rs b/src/uu/seq/src/number.rs new file mode 100644 index 000000000..9062fa1a1 --- /dev/null +++ b/src/uu/seq/src/number.rs @@ -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() + } +} diff --git a/src/uu/seq/src/numberparse.rs b/src/uu/seq/src/numberparse.rs new file mode 100644 index 000000000..da235790d --- /dev/null +++ b/src/uu/seq/src/numberparse.rs @@ -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::().unwrap().number; +/// let expected = Number::BigInt(BigInt::zero()); +/// assert_eq!(actual, expected); +/// ``` +fn parse_no_decimal_no_exponent(s: &str) -> Result { + match s.parse::() { + 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::().unwrap().number; +/// let expected = "100".parse::().unwrap(); +/// assert_eq!(actual, expected); +/// ``` +fn parse_exponent_no_decimal(s: &str, j: usize) -> Result { + 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::().unwrap().number; +/// let expected = "1.2".parse::().unwrap(); +/// assert_eq!(actual, expected); +/// ``` +fn parse_decimal_no_exponent(s: &str, i: usize) -> Result { + 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::().unwrap().number; +/// let expected = "1200".parse::().unwrap(); +/// assert_eq!(actual, expected); +/// ``` +fn parse_decimal_and_exponent( + s: &str, + i: usize, + j: usize, +) -> Result { + // 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::() + .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::().unwrap().number; +/// let expected = Number::BigInt(BigInt::zero()); +/// assert_eq!(actual, expected); +/// ``` +fn parse_hexadecimal(s: &str) -> Result { + 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 { + // 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::().unwrap().number + } + + /// Convenience function for getting the number of integral digits. + fn num_integral_digits(s: &str) -> usize { + s.parse::().unwrap().num_integral_digits + } + + /// Convenience function for getting the number of fractional digits. + fn num_fractional_digits(s: &str) -> usize { + s.parse::().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::().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::().unwrap())) + ); + } + + #[test] + fn test_parse_big_decimal() { + assert_eq!( + parse("0.0"), + Number::Float(ExtendedBigDecimal::BigDecimal( + "0.0".parse::().unwrap() + )) + ); + assert_eq!( + parse(".0"), + Number::Float(ExtendedBigDecimal::BigDecimal( + "0.0".parse::().unwrap() + )) + ); + assert_eq!( + parse("1.0"), + Number::Float(ExtendedBigDecimal::BigDecimal( + "1.0".parse::().unwrap() + )) + ); + assert_eq!( + parse("10e-1"), + Number::Float(ExtendedBigDecimal::BigDecimal( + "1.0".parse::().unwrap() + )) + ); + assert_eq!( + parse("-1e-3"), + Number::Float(ExtendedBigDecimal::BigDecimal( + "-0.001".parse::().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::().unwrap_err(), + ParseNumberError::Float + ); + assert_eq!( + "1e2e3".parse::().unwrap_err(), + ParseNumberError::Float + ); + assert_eq!( + "1e2.3".parse::().unwrap_err(), + ParseNumberError::Float + ); + assert_eq!( + "-+-1".parse::().unwrap_err(), + ParseNumberError::Float + ); + } + + #[test] + fn test_parse_invalid_hex() { + assert_eq!( + "0xg".parse::().unwrap_err(), + ParseNumberError::Hex + ); + } + + #[test] + fn test_parse_invalid_nan() { + assert_eq!( + "nan".parse::().unwrap_err(), + ParseNumberError::Nan + ); + assert_eq!( + "NAN".parse::().unwrap_err(), + ParseNumberError::Nan + ); + assert_eq!( + "NaN".parse::().unwrap_err(), + ParseNumberError::Nan + ); + assert_eq!( + "nAn".parse::().unwrap_err(), + ParseNumberError::Nan + ); + assert_eq!( + "-nan".parse::().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); + } +} diff --git a/src/uu/seq/src/seq.rs b/src/uu/seq/src/seq.rs index a76a23c4e..75e9b1598 100644 --- a/src/uu/seq/src/seq.rs +++ b/src/uu/seq/src/seq.rs @@ -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 { - 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::() { - 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::() { - 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::.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(next: &T, increment: &T, last: &T) -> bool { +fn done_printing(next: &T, increment: &T, last: &T) -> bool { if increment >= &T::zero() { next > last } else { @@ -337,9 +215,73 @@ fn done_printing(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 { diff --git a/src/uu/sort/src/chunks.rs b/src/uu/sort/src/chunks.rs index 80d6060d4..bfe6aa73b 100644 --- a/src/uu/sort/src/chunks.rs +++ b/src/uu/sort/src/chunks.rs @@ -193,16 +193,13 @@ pub fn 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_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()); diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index 952eecc28..153e3cd57 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -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 } diff --git a/src/uucore/src/lib/features/mode.rs b/src/uucore/src/lib/features/mode.rs index 5e299f988..2206177a3 100644 --- a/src/uucore/src/lib/features/mode.rs +++ b/src/uucore/src/lib/features/mode.rs @@ -163,12 +163,11 @@ pub fn strip_minus_from_mode(args: &mut Vec) -> 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; } _ => {} diff --git a/src/uucore/src/lib/mods/display.rs b/src/uucore/src/lib/mods/display.rs index dfe64184f..95288973a 100644 --- a/src/uucore/src/lib/mods/display.rs +++ b/src/uucore/src/lib/mods/display.rs @@ -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 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> { - 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>(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::>().as_slice(), - expected - ); - } - } -} diff --git a/tests/by-util/test_base32.rs b/tests/by-util/test_base32.rs index ffe2cf74c..4d244704d 100644 --- a/tests/by-util/test_base32.rs +++ b/tests/by-util/test_base32.rs @@ -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] diff --git a/tests/by-util/test_base64.rs b/tests/by-util/test_base64.rs index 87aa0db44..9a7d525bb 100644 --- a/tests/by-util/test_base64.rs +++ b/tests/by-util/test_base64.rs @@ -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] diff --git a/tests/by-util/test_basename.rs b/tests/by-util/test_basename.rs index 141745ac3..962d7373d 100644 --- a/tests/by-util/test_basename.rs +++ b/tests/by-util/test_basename.rs @@ -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"))] diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index e86f35833..50abfe967 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -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] diff --git a/tests/by-util/test_env.rs b/tests/by-util/test_env.rs index 1d76c433d..ecb215f8c 100644 --- a/tests/by-util/test_env.rs +++ b/tests/by-util/test_env.rs @@ -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!() diff --git a/tests/by-util/test_more.rs b/tests/by-util/test_more.rs index 4b2719d8f..5c2b3c0f6 100644 --- a/tests/by-util/test_more.rs +++ b/tests/by-util/test_more.rs @@ -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 { } } diff --git a/tests/by-util/test_mv.rs b/tests/by-util/test_mv.rs index 8d9b00664..f6650cdba 100644 --- a/tests/by-util/test_mv.rs +++ b/tests/by-util/test_mv.rs @@ -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] diff --git a/tests/by-util/test_nice.rs b/tests/by-util/test_nice.rs index 7a99a333d..4a77ae24e 100644 --- a/tests/by-util/test_nice.rs +++ b/tests/by-util/test_nice.rs @@ -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] diff --git a/tests/by-util/test_seq.rs b/tests/by-util/test_seq.rs index 6ed3cb67d..490fcf60c 100644 --- a/tests/by-util/test_seq.rs +++ b/tests/by-util/test_seq.rs @@ -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(); } diff --git a/tests/by-util/test_stdbuf.rs b/tests/by-util/test_stdbuf.rs index c05b65d70..3b03a1d4c 100644 --- a/tests/by-util/test_stdbuf.rs +++ b/tests/by-util/test_stdbuf.rs @@ -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"))] diff --git a/tests/common/util.rs b/tests/common/util.rs index c7ac816e1..cfde5f229 100644 --- a/tests/common/util.rs +++ b/tests/common/util.rs @@ -62,6 +62,10 @@ fn read_scenario_fixture>(tmpd: &Option>, 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, //tmpd is used for convenience functions for asserts against fixtures tmpd: Option>, /// 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, tmpd: Option>, code: Option, 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>(&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>(&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, T: AsRef>( + &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>(&self, bin: S) -> UCommand { - UCommand::new_from_tmp(bin, self.tmpd.clone(), true) + UCommand::new_from_tmp::(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>(&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>(&self, bin: S) -> UCommand { - UCommand::new_from_tmp(bin, self.tmpd.clone(), false) + UCommand::new_from_tmp::(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, tmpd: Option>, has_run: bool, ignore_stdin_write_error: bool, @@ -846,12 +870,20 @@ pub struct UCommand { } impl UCommand { - pub fn new, U: AsRef>(arg: T, curdir: U, env_clear: bool) -> UCommand { - UCommand { + pub fn new, S: AsRef, U: AsRef>( + bin_path: T, + util_name: Option, + 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>(arg: T, tmpd: Rc, env_clear: bool) -> UCommand { + pub fn new_from_tmp, S: AsRef>( + bin_path: T, + util_name: Option, + tmpd: Rc, + 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,