diff --git a/CMakeLists.txt b/CMakeLists.txt index ec2908a4b..27a9a5229 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -107,7 +107,7 @@ set(FISH_BUILTIN_SRCS src/builtins/eval.cpp src/builtins/fg.cpp src/builtins/function.cpp src/builtins/functions.cpp src/builtins/history.cpp src/builtins/jobs.cpp src/builtins/math.cpp src/builtins/printf.cpp src/builtins/path.cpp - src/builtins/pwd.cpp src/builtins/random.cpp src/builtins/read.cpp + src/builtins/pwd.cpp src/builtins/read.cpp src/builtins/realpath.cpp src/builtins/set.cpp src/builtins/set_color.cpp src/builtins/source.cpp src/builtins/status.cpp src/builtins/string.cpp src/builtins/test.cpp src/builtins/type.cpp src/builtins/ulimit.cpp diff --git a/fish-rust/Cargo.lock b/fish-rust/Cargo.lock index 87084289d..d7e097480 100644 --- a/fish-rust/Cargo.lock +++ b/fish-rust/Cargo.lock @@ -358,6 +358,7 @@ dependencies = [ "nix", "num-traits", "once_cell", + "rand", "unixstring", "widestring", "widestring-suffix", @@ -658,6 +659,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "prettyplease" version = "0.1.23" @@ -710,6 +717,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "redox_syscall" version = "0.2.16" diff --git a/fish-rust/Cargo.toml b/fish-rust/Cargo.toml index df8206419..900f33610 100644 --- a/fish-rust/Cargo.toml +++ b/fish-rust/Cargo.toml @@ -15,6 +15,7 @@ libc = "0.2.137" nix = { version = "0.25.0", default-features = false, features = [] } num-traits = "0.2.15" once_cell = "1.17.0" +rand = { version = "0.8.5", features = ["small_rng"] } unixstring = "0.2.7" widestring = "1.0.2" diff --git a/fish-rust/src/builtins/mod.rs b/fish-rust/src/builtins/mod.rs index 6634804b7..dd530c8ae 100644 --- a/fish-rust/src/builtins/mod.rs +++ b/fish-rust/src/builtins/mod.rs @@ -2,6 +2,7 @@ pub mod shared; pub mod echo; pub mod emit; +mod exit; +pub mod random; pub mod r#return; pub mod wait; -mod exit; diff --git a/fish-rust/src/builtins/random.rs b/fish-rust/src/builtins/random.rs new file mode 100644 index 000000000..68ce61577 --- /dev/null +++ b/fish-rust/src/builtins/random.rs @@ -0,0 +1,188 @@ +use libc::c_int; + +use crate::builtins::shared::{ + builtin_missing_argument, builtin_print_help, builtin_unknown_option, io_streams_t, + STATUS_CMD_OK, STATUS_INVALID_ARGS, +}; +use crate::ffi::parser_t; +use crate::wchar::{widestrs, wstr}; +use crate::wgetopt::{wgetopter_t, wopt, woption, woption_argument_t}; +use crate::wutil::{self, fish_wcstoi_radix_all, format::printf::sprintf, wgettext_fmt}; +use num_traits::PrimInt; +use once_cell::sync::Lazy; +use rand::rngs::SmallRng; +use rand::{Rng, SeedableRng}; +use std::sync::Mutex; + +static seeded_engine: Lazy> = Lazy::new(|| Mutex::new(SmallRng::from_entropy())); + +#[widestrs] +pub fn random( + parser: &mut parser_t, + streams: &mut io_streams_t, + argv: &mut [&wstr], +) -> Option { + let cmd = argv[0]; + let argc = argv.len(); + let print_hints = false; + + const shortopts: &wstr = "+:h"L; + const longopts: &[woption] = &[wopt("help"L, woption_argument_t::no_argument, 'h')]; + + let mut w = wgetopter_t::new(shortopts, longopts, argv); + while let Some(c) = w.wgetopt_long() { + match c { + 'h' => { + builtin_print_help(parser, streams, cmd); + return STATUS_CMD_OK; + } + ':' => { + builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1], print_hints); + return STATUS_INVALID_ARGS; + } + '?' => { + builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1], print_hints); + return STATUS_INVALID_ARGS; + } + _ => { + panic!("unexpected retval from wgeopter.next()"); + } + } + } + + let mut engine = seeded_engine.lock().unwrap(); + let mut start = 0; + let mut end = 32767; + let mut step = 1; + let arg_count = argc - w.woptind; + let i = w.woptind; + if arg_count >= 1 && argv[i] == "choice" { + if arg_count == 1 { + streams + .err + .append(wgettext_fmt!("%ls: nothing to choose from\n", cmd,)); + return STATUS_INVALID_ARGS; + } + + let rand = engine.gen_range(0..arg_count - 1); + streams.out.append(sprintf!("%ls\n"L, argv[i + 1 + rand])); + return STATUS_CMD_OK; + } + fn parse( + streams: &mut io_streams_t, + cmd: &wstr, + num: &wstr, + ) -> Result { + let res = fish_wcstoi_radix_all(num.chars(), None, true); + if res.is_err() { + streams + .err + .append(wgettext_fmt!("%ls: %ls: invalid integer\n", cmd, num,)); + } + return res; + } + + match arg_count { + 0 => { + // Keep the defaults + } + 1 => { + // Seed the engine persistently + let num = parse::(streams, cmd, argv[i]); + match num { + Err(_) => return STATUS_INVALID_ARGS, + Ok(x) => *engine = SmallRng::seed_from_u64(x as u64), + } + return STATUS_CMD_OK; + } + 2 => { + // start is first, end is second + match parse::(streams, cmd, argv[i]) { + Err(_) => return STATUS_INVALID_ARGS, + Ok(x) => start = x, + } + + match parse::(streams, cmd, argv[i + 1]) { + Err(_) => return STATUS_INVALID_ARGS, + Ok(x) => end = x, + } + } + 3 => { + // start, step, end + match parse::(streams, cmd, argv[i]) { + Err(_) => return STATUS_INVALID_ARGS, + Ok(x) => start = x, + } + + // start, step, end + match parse::(streams, cmd, argv[i + 1]) { + Err(_) => return STATUS_INVALID_ARGS, + Ok(0) => { + streams + .err + .append(wgettext_fmt!("%ls: STEP must be a positive integer\n", cmd,)); + return STATUS_INVALID_ARGS; + } + Ok(x) => step = x, + } + + match parse::(streams, cmd, argv[i + 2]) { + Err(_) => return STATUS_INVALID_ARGS, + Ok(x) => end = x, + } + } + _ => { + streams + .err + .append(wgettext_fmt!("%ls: too many arguments\n", cmd,)); + return Some(1); + } + } + + if end <= start { + streams + .err + .append(wgettext_fmt!("%ls: END must be greater than START\n", cmd,)); + return STATUS_INVALID_ARGS; + } + + // Possibilities can be abs(i64::MIN) + i64::MAX, + // so we do this as i128 + let possibilities = (end as i128 - start as i128) / (step as i128); + + if possibilities == 0 { + streams.err.append(wgettext_fmt!( + "%ls: range contains only one possible value\n", + cmd, + )); + return STATUS_INVALID_ARGS; + } + + let rand = engine.gen_range(0..=possibilities); + + let result = start as i128 + rand as i128 * step as i128; + + // We do our math as i128, + // and then we check if it fits in 64 bit - signed or unsigned! + match i64::try_from(result) { + Ok(x) => { + streams.out.append(sprintf!("%d\n"L, x)); + return STATUS_CMD_OK; + }, + Err(_) => { + match u64::try_from(result) { + Ok(x) => { + streams.out.append(sprintf!("%d\n"L, x)); + return STATUS_CMD_OK; + }, + Err(_) => { + streams.err.append(wgettext_fmt!( + "%ls: range contains only one possible value\n", + cmd, + )); + return STATUS_INVALID_ARGS; + }, + } + }, + } +} diff --git a/fish-rust/src/builtins/shared.rs b/fish-rust/src/builtins/shared.rs index c9d5152aa..b9dc55d14 100644 --- a/fish-rust/src/builtins/shared.rs +++ b/fish-rust/src/builtins/shared.rs @@ -118,6 +118,7 @@ pub fn run_builtin( RustBuiltin::Echo => super::echo::echo(parser, streams, args), RustBuiltin::Emit => super::emit::emit(parser, streams, args), RustBuiltin::Exit => super::exit::exit(parser, streams, args), + RustBuiltin::Random => super::random::random(parser, streams, args), RustBuiltin::Return => super::r#return::r#return(parser, streams, args), RustBuiltin::Wait => wait::wait(parser, streams, args), } diff --git a/fish-rust/src/wutil/wcstoi.rs b/fish-rust/src/wutil/wcstoi.rs index 44cde6cc4..150d3381f 100644 --- a/fish-rust/src/wutil/wcstoi.rs +++ b/fish-rust/src/wutil/wcstoi.rs @@ -106,11 +106,19 @@ where negative = false; } let consumed_all = chars.peek() == None; - Ok(ParseResult { result, negative, consumed_all }) + Ok(ParseResult { + result, + negative, + consumed_all, + }) } /// Parse some iterator over Chars into some Integer type, optionally with a radix. -fn fish_wcstoi_impl(src: Chars, mradix: Option, consume_all: bool) -> Result +fn fish_wcstoi_impl( + src: Chars, + mradix: Option, + consume_all: bool, +) -> Result where Chars: Iterator, Int: PrimInt, @@ -120,7 +128,10 @@ where let signed = Int::min_value() < Int::zero(); let ParseResult { - result, negative, consumed_all, .. + result, + negative, + consumed_all, + .. } = fish_parse_radix(src, mradix)?; if !signed && negative { @@ -169,7 +180,11 @@ where fish_wcstoi_impl(src, Some(radix), false) } -pub fn fish_wcstoi_radix_all(src: Chars, radix: Option, consume_all: bool) -> Result +pub fn fish_wcstoi_radix_all( + src: Chars, + radix: Option, + consume_all: bool, +) -> Result where Chars: Iterator, Int: PrimInt, diff --git a/src/builtin.cpp b/src/builtin.cpp index 5468b5f9f..4b73d1ef8 100644 --- a/src/builtin.cpp +++ b/src/builtin.cpp @@ -50,7 +50,6 @@ #include "builtins/path.h" #include "builtins/printf.h" #include "builtins/pwd.h" -#include "builtins/random.h" #include "builtins/read.h" #include "builtins/realpath.h" #include "builtins/set.h" @@ -401,7 +400,7 @@ static constexpr builtin_data_t builtin_datas[] = { {L"path", &builtin_path, N_(L"Handle paths")}, {L"printf", &builtin_printf, N_(L"Prints formatted text")}, {L"pwd", &builtin_pwd, N_(L"Print the working directory")}, - {L"random", &builtin_random, N_(L"Generate random number")}, + {L"random", &implemented_in_rust, N_(L"Generate random number")}, {L"read", &builtin_read, N_(L"Read a line of input into variables")}, {L"realpath", &builtin_realpath, N_(L"Show absolute path sans symlinks")}, {L"return", &implemented_in_rust, N_(L"Stop the currently evaluated function")}, @@ -534,6 +533,9 @@ static maybe_t try_get_rust_builtin(const wcstring &cmd) { if (cmd == L"exit") { return RustBuiltin::Exit; } + if (cmd == L"random") { + return RustBuiltin::Random; + } if (cmd == L"wait") { return RustBuiltin::Wait; } diff --git a/src/builtin.h b/src/builtin.h index e1a61452b..dace4789e 100644 --- a/src/builtin.h +++ b/src/builtin.h @@ -112,7 +112,8 @@ enum RustBuiltin : int32_t { Echo, Emit, Exit, - Wait, + Random, Return, + Wait, }; #endif diff --git a/src/builtins/random.cpp b/src/builtins/random.cpp deleted file mode 100644 index 5f79a2271..000000000 --- a/src/builtins/random.cpp +++ /dev/null @@ -1,160 +0,0 @@ -// Implementation of the random builtin. -#include "config.h" // IWYU pragma: keep - -#include "random.h" - -#include -#include -#include -#include - -#include "../builtin.h" -#include "../common.h" -#include "../fallback.h" // IWYU pragma: keep -#include "../io.h" -#include "../maybe.h" -#include "../wutil.h" // IWYU pragma: keep - -/// \return a random-seeded engine. -static std::minstd_rand get_seeded_engine() { - std::minstd_rand engine; - // seed engine with 2*32 bits of random data - // for the 64 bits of internal state of minstd_rand - std::random_device rd; - std::seed_seq seed{rd(), rd()}; - engine.seed(seed); - return engine; -} - -/// The random builtin generates random numbers. -maybe_t builtin_random(parser_t &parser, io_streams_t &streams, const wchar_t **argv) { - const wchar_t *cmd = argv[0]; - int argc = builtin_count_args(argv); - help_only_cmd_opts_t opts; - - int optind; - int retval = parse_help_only_cmd_opts(opts, &optind, argc, argv, parser, streams); - if (retval != STATUS_CMD_OK) return retval; - - if (opts.print_help) { - builtin_print_help(parser, streams, cmd); - return STATUS_CMD_OK; - } - - // We have a single engine which we lazily seed. Lock it here. - static owning_lock s_engine{get_seeded_engine()}; - auto engine_lock = s_engine.acquire(); - std::minstd_rand &engine = *engine_lock; - - int arg_count = argc - optind; - long long start, end; - unsigned long long step; - bool choice = false; - if (arg_count >= 1 && !std::wcscmp(argv[optind], L"choice")) { - if (arg_count == 1) { - streams.err.append_format(L"%ls: nothing to choose from\n", cmd); - return STATUS_INVALID_ARGS; - } - choice = true; - start = 1; - step = 1; - end = arg_count - 1; - } else { - bool parse_error = false; - auto parse_ll = [&](const wchar_t *str) { - long long ll = fish_wcstoll(str); - if (errno) { - streams.err.append_format(BUILTIN_ERR_NOT_NUMBER, cmd, str); - parse_error = true; - } - return ll; - }; - auto parse_ull = [&](const wchar_t *str) { - unsigned long long ull = fish_wcstoull(str); - if (errno) { - streams.err.append_format(BUILTIN_ERR_NOT_NUMBER, cmd, str); - parse_error = true; - } - return ull; - }; - if (arg_count == 0) { - start = 0; - end = 32767; - step = 1; - } else if (arg_count == 1) { - long long seed = parse_ll(argv[optind]); - if (parse_error) return STATUS_INVALID_ARGS; - engine.seed(static_cast(seed)); - return STATUS_CMD_OK; - } else if (arg_count == 2) { - start = parse_ll(argv[optind]); - step = 1; - end = parse_ll(argv[optind + 1]); - } else if (arg_count == 3) { - start = parse_ll(argv[optind]); - step = parse_ull(argv[optind + 1]); - end = parse_ll(argv[optind + 2]); - } else { - streams.err.append_format(BUILTIN_ERR_TOO_MANY_ARGUMENTS, cmd); - return STATUS_INVALID_ARGS; - } - - if (parse_error) { - return STATUS_INVALID_ARGS; - } else if (start >= end) { - streams.err.append_format(L"%ls: END must be greater than START\n", cmd); - return STATUS_INVALID_ARGS; - } else if (step == 0) { - streams.err.append_format(L"%ls: STEP must be a positive integer\n", cmd); - return STATUS_INVALID_ARGS; - } - } - - // only for negative argument - auto safe_abs = [](long long ll) -> unsigned long long { - return -static_cast(ll); - }; - long long real_end; - if (start >= 0 || end < 0) { - // 0 <= start <= end - long long diff = end - start; - // 0 <= diff <= LL_MAX - real_end = start + static_cast(diff / step); - } else { - // start < 0 <= end - unsigned long long abs_start = safe_abs(start); - unsigned long long diff = (end + abs_start); - real_end = diff / step - abs_start; - } - - if (!choice && start == real_end) { - streams.err.append_format(L"%ls: range contains only one possible value\n", cmd); - return STATUS_INVALID_ARGS; - } - - std::uniform_int_distribution dist(start, real_end); - long long random = dist(engine); - long long result; - if (start >= 0) { - // 0 <= start <= random <= end - long long diff = random - start; - // 0 < step * diff <= end - start <= LL_MAX - result = start + static_cast(diff * step); - } else if (random < 0) { - // start <= random < 0 - long long diff = random - start; - result = diff * step - safe_abs(start); - } else { - // start < 0 <= random - unsigned long long abs_start = safe_abs(start); - unsigned long long diff = (random + abs_start); - result = diff * step - abs_start; - } - - if (choice) { - streams.out.append_format(L"%ls\n", argv[optind + result]); - } else { - streams.out.append_format(L"%lld\n", result); - } - return STATUS_CMD_OK; -} diff --git a/src/builtins/random.h b/src/builtins/random.h deleted file mode 100644 index 1dc2603c7..000000000 --- a/src/builtins/random.h +++ /dev/null @@ -1,11 +0,0 @@ -// Prototypes for executing builtin_random function. -#ifndef FISH_BUILTIN_RANDOM_H -#define FISH_BUILTIN_RANDOM_H - -#include "../maybe.h" - -class parser_t; -struct io_streams_t; - -maybe_t builtin_random(parser_t &parser, io_streams_t &streams, const wchar_t **argv); -#endif diff --git a/tests/checks/random.fish b/tests/checks/random.fish index 2e32c6b30..712313686 100644 --- a/tests/checks/random.fish +++ b/tests/checks/random.fish @@ -40,7 +40,7 @@ random choic a b c #CHECKERR: random: too many arguments function check_boundaries - if not test $argv[1] -ge $argv[2] -a $argv[1] -le $argv[3] + if not test "$argv[1]" -ge "$argv[2]" -a "$argv[1]" -le "$argv[3]" printf "Unexpected: %s <= %s <= %s not verified\n" $argv[2] $argv[1] $argv[3] >&2 return 1 end