diff --git a/Cargo.lock b/Cargo.lock
index a34c6a5..91c4be6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -537,6 +537,7 @@ dependencies = [
"log",
"num_cpus",
"pretty_assertions",
+ "rand",
"regex",
"semver",
"serde",
@@ -637,6 +638,12 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+[[package]]
+name = "ppv-lite86"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+
[[package]]
name = "pretty_assertions"
version = "1.4.0"
@@ -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 = "rayon"
version = "1.10.0"
diff --git a/Cargo.toml b/Cargo.toml
index cf03201..9c200b1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -36,6 +36,7 @@ lexiclean = "0.0.1"
libc = "0.2.0"
log = "0.4.4"
num_cpus = "1.15.0"
+rand = "0.8.5"
regex = "1.10.4"
semver = "1.0.20"
serde = { version = "1.0.130", features = ["derive", "rc"] }
diff --git a/README.md b/README.md
index 06a51c5..371ab19 100644
--- a/README.md
+++ b/README.md
@@ -1517,6 +1517,13 @@ which will halt execution.
[BLAKE3]: https://github.com/BLAKE3-team/BLAKE3/
+#### Random
+
+- `choose(n, alphabet)`master - Generate a string of `n` randomly
+ selected characters from `alphabet`, which may not contain repeated
+ characters. For example, `choose('64', HEX)` will generate a random
+ 64-character lowercase hex string.
+
#### Semantic Versions
- `semver_matches(version, requirement)`1.16.0 - Check whether a
diff --git a/src/function.rs b/src/function.rs
index 2c7dc0c..f8fc996 100644
--- a/src/function.rs
+++ b/src/function.rs
@@ -4,7 +4,9 @@ use {
ToKebabCase, ToLowerCamelCase, ToShoutyKebabCase, ToShoutySnakeCase, ToSnakeCase, ToTitleCase,
ToUpperCamelCase,
},
+ rand::{seq::SliceRandom, thread_rng},
semver::{Version, VersionReq},
+ std::collections::HashSet,
Function::*,
};
@@ -27,6 +29,7 @@ pub(crate) fn get(name: &str) -> Option {
"cache_directory" => Nullary(|_| dir("cache", dirs::cache_dir)),
"canonicalize" => Unary(canonicalize),
"capitalize" => Unary(capitalize),
+ "choose" => Binary(choose),
"clean" => Unary(clean),
"config_directory" => Nullary(|_| dir("config", dirs::config_dir)),
"config_local_directory" => Nullary(|_| dir("local config", dirs::config_local_dir)),
@@ -157,6 +160,30 @@ fn capitalize(_evaluator: &Evaluator, s: &str) -> Result {
Ok(capitalized)
}
+fn choose(_evaluator: &Evaluator, n: &str, alphabet: &str) -> Result {
+ if alphabet.is_empty() {
+ return Err("empty alphabet".into());
+ }
+
+ let mut chars = HashSet::::with_capacity(alphabet.len());
+
+ for c in alphabet.chars() {
+ if !chars.insert(c) {
+ return Err(format!("alphabet contains repeated character `{c}`"));
+ }
+ }
+
+ let alphabet = alphabet.chars().collect::>();
+
+ let n = n
+ .parse::()
+ .map_err(|err| format!("failed to parse `{n}` as positive integer: {err}"))?;
+
+ let mut rng = thread_rng();
+
+ Ok((0..n).map(|_| alphabet.choose(&mut rng).unwrap()).collect())
+}
+
fn clean(_evaluator: &Evaluator, path: &str) -> Result {
Ok(Path::new(path).lexiclean().to_str().unwrap().to_owned())
}
diff --git a/tests/functions.rs b/tests/functions.rs
index 95bddfd..e134110 100644
--- a/tests/functions.rs
+++ b/tests/functions.rs
@@ -661,6 +661,69 @@ fn uuid() {
.run();
}
+#[test]
+fn choose() {
+ Test::new()
+ .justfile(r#"x := choose('10', 'xXyYzZ')"#)
+ .args(["--evaluate", "x"])
+ .stdout_regex("^[X-Zx-z]{10}$")
+ .run();
+}
+
+#[test]
+fn choose_bad_alphabet_empty() {
+ Test::new()
+ .justfile("x := choose('10', '')")
+ .args(["--evaluate"])
+ .status(1)
+ .stderr(
+ "
+ error: Call to function `choose` failed: empty alphabet
+ ——▶ justfile:1:6
+ │
+ 1 │ x := choose('10', '')
+ │ ^^^^^^
+ ",
+ )
+ .run();
+}
+
+#[test]
+fn choose_bad_alphabet_repeated() {
+ Test::new()
+ .justfile("x := choose('10', 'aa')")
+ .args(["--evaluate"])
+ .status(1)
+ .stderr(
+ "
+ error: Call to function `choose` failed: alphabet contains repeated character `a`
+ ——▶ justfile:1:6
+ │
+ 1 │ x := choose('10', 'aa')
+ │ ^^^^^^
+ ",
+ )
+ .run();
+}
+
+#[test]
+fn choose_bad_length() {
+ Test::new()
+ .justfile("x := choose('foo', HEX)")
+ .args(["--evaluate"])
+ .status(1)
+ .stderr(
+ "
+ error: Call to function `choose` failed: failed to parse `foo` as positive integer: invalid digit found in string
+ ——▶ justfile:1:6
+ │
+ 1 │ x := choose('foo', HEX)
+ │ ^^^^^^
+ ",
+ )
+ .run();
+}
+
#[test]
fn sha256() {
Test::new()