From 1274d1f7e36ed5de2cd7d4ea6924bc97dc020022 Mon Sep 17 00:00:00 2001 From: Wind Date: Sat, 2 Aug 2025 10:48:07 +0800 Subject: [PATCH] Add `-h/--help` flag to testbin (#16196) # Description As title, this pr introduce `-h` flag to testbin, so if I want to see which testbin I should use, I don't need to look into source code. ### About the change I don't know if there is any way to get docstring of a function inside rust code. So I created a trait, and put docstring into it's `help` method: ```rust pub trait TestBin { // the docstring of original functions are moved here. fn help(&self) -> &'static str; fn run(&self); } ``` Take `cococo` testbin as example, the changes are: ``` original cococo function --> Cococo struct, then 1. put the body of `cococo` function into `run` method 2. put the docstring of `cococo` function into `help` method ``` # User-Facing Changes `-h/--help` flag in testbin is enabled. ``` > nu --testbin -h Usage: nu --testbin : chop -> With no parameters, will chop a character off the end of each line cococo -> Cross platform echo using println!()(e.g: nu --testbin cococo a b c) echo_env -> Echo's value of env keys from args(e.g: nu --testbin echo_env FOO BAR) echo_env_mixed -> Mix echo of env keys from input(e.g: nu --testbin echo_env_mixed out-err FOO BAR; nu --testbin echo_env_mixed err-out FOO BAR) echo_env_stderr -> Echo's value of env keys from args to stderr(e.g: nu --testbin echo_env_stderr FOO BAR) echo_env_stderr_fail -> Echo's value of env keys from args to stderr, and exit with failure(e.g: nu --testbin echo_env_stderr_fail FOO BAR) fail -> Exits with failure code 1(e.g: nu --testbin fail) iecho -> Another type of echo that outputs a parameter per line, looping infinitely(e.g: nu --testbin iecho 3) input_bytes_length -> Prints the number of bytes received on stdin(e.g: 0x[deadbeef] | nu --testbin input_bytes_length) meow -> Cross platform cat (open a file, print the contents) using read_to_string and println!()(e.g: nu --testbin meow file.txt) meowb -> Cross platform cat (open a file, print the contents) using read() and write_all() / binary(e.g: nu --testbin meowb sample.db) nonu -> Cross platform echo but concats arguments without space and NO newline(e.g: nu --testbin nonu a b c) nu_repl -> Run a REPL with the given source lines relay -> Relays anything received on stdin to stdout(e.g: 0x[beef] | nu --testbin relay) repeat_bytes -> A version of repeater that can output binary data, even null bytes(e.g: nu --testbin repeat_bytes 003d9fbf 10) repeater -> Repeat a string or char N times(e.g: nu --testbin repeater a 5) ``` # Tests + Formatting None, all existed tests can guarantee the behavior of testbins doesn't change. # After Submitting NaN --- src/main.rs | 34 ++-- src/test_bins.rs | 505 ++++++++++++++++++++++++++++++----------------- 2 files changed, 338 insertions(+), 201 deletions(-) diff --git a/src/main.rs b/src/main.rs index 0e9785c5c9..5c1eb6e7e0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -367,28 +367,18 @@ fn main() -> Result<()> { start_time = std::time::Instant::now(); if let Some(testbin) = &parsed_nu_cli_args.testbin { - // Call out to the correct testbin - match testbin.item.as_str() { - "echo_env" => test_bins::echo_env(true), - "echo_env_stderr" => test_bins::echo_env(false), - "echo_env_stderr_fail" => test_bins::echo_env_and_fail(false), - "echo_env_mixed" => test_bins::echo_env_mixed(), - "cococo" => test_bins::cococo(), - "meow" => test_bins::meow(), - "meowb" => test_bins::meowb(), - "relay" => test_bins::relay(), - "iecho" => test_bins::iecho(), - "fail" => test_bins::fail(), - "nonu" => test_bins::nonu(), - "chop" => test_bins::chop(), - "repeater" => test_bins::repeater(), - "repeat_bytes" => test_bins::repeat_bytes(), - // Important: nu_repl must be called with `--testbin=nu_repl` - // `--testbin nu_repl` will not work due to argument count logic - // in test_bins.rs - "nu_repl" => test_bins::nu_repl(), - "input_bytes_length" => test_bins::input_bytes_length(), - _ => std::process::exit(1), + let dispatcher = test_bins::new_testbin_dispatcher(); + let test_bin = testbin.item.as_str(); + match dispatcher.get(test_bin) { + Some(test_bin) => test_bin.run(), + None => { + if ["-h", "--help"].contains(&test_bin) { + test_bins::show_help(&dispatcher); + } else { + eprintln!("ERROR: Unknown testbin '{test_bin}'"); + std::process::exit(1); + } + } } std::process::exit(0) } else { diff --git a/src/test_bins.rs b/src/test_bins.rs index e0587859f5..e2b7791781 100644 --- a/src/test_bins.rs +++ b/src/test_bins.rs @@ -9,10 +9,301 @@ use nu_protocol::{ }; use nu_std::load_standard_library; use std::{ + collections::HashMap, io::{self, BufRead, Read, Write}, sync::Arc, }; +pub trait TestBin { + fn help(&self) -> &'static str; + fn run(&self); +} + +pub struct EchoEnv; +pub struct EchoEnvStderr; +pub struct EchoEnvStderrFail; +pub struct EchoEnvMixed; +pub struct Cococo; +pub struct Meow; +pub struct Meowb; +pub struct Relay; +pub struct Iecho; +pub struct Fail; +pub struct Nonu; +pub struct Chop; +pub struct Repeater; +pub struct RepeatBytes; +pub struct NuRepl; +pub struct InputBytesLength; + +impl TestBin for EchoEnv { + fn help(&self) -> &'static str { + "Echo's value of env keys from args(e.g: nu --testbin echo_env FOO BAR)" + } + + fn run(&self) { + echo_env(true) + } +} + +impl TestBin for EchoEnvStderr { + fn help(&self) -> &'static str { + "Echo's value of env keys from args to stderr(e.g: nu --testbin echo_env_stderr FOO BAR)" + } + + fn run(&self) { + echo_env(false) + } +} + +impl TestBin for EchoEnvStderrFail { + fn help(&self) -> &'static str { + "Echo's value of env keys from args to stderr, and exit with failure(e.g: nu --testbin echo_env_stderr_fail FOO BAR)" + } + + fn run(&self) { + echo_env(false); + fail(); + } +} + +impl TestBin for EchoEnvMixed { + fn help(&self) -> &'static str { + "Mix echo of env keys from input(e.g: nu --testbin echo_env_mixed out-err FOO BAR; nu --testbin echo_env_mixed err-out FOO BAR)" + } + + fn run(&self) { + let args = args(); + let args = &args[1..]; + + if args.len() != 3 { + panic!( + r#"Usage examples: +* nu --testbin echo_env_mixed out-err FOO BAR +* nu --testbin echo_env_mixed err-out FOO BAR"# + ) + } + match args[0].as_str() { + "out-err" => { + let (out_arg, err_arg) = (&args[1], &args[2]); + echo_one_env(out_arg, true); + echo_one_env(err_arg, false); + } + "err-out" => { + let (err_arg, out_arg) = (&args[1], &args[2]); + echo_one_env(err_arg, false); + echo_one_env(out_arg, true); + } + _ => panic!("The mixed type must be `out_err`, `err_out`"), + } + } +} + +impl TestBin for Cococo { + fn help(&self) -> &'static str { + "Cross platform echo using println!()(e.g: nu --testbin cococo a b c)" + } + + fn run(&self) { + let args: Vec = args(); + + if args.len() > 1 { + // Write back out all the arguments passed + // if given at least 1 instead of chickens + // speaking co co co. + println!("{}", &args[1..].join(" ")); + } else { + println!("cococo"); + } + } +} + +impl TestBin for Meow { + fn help(&self) -> &'static str { + "Cross platform cat (open a file, print the contents) using read_to_string and println!()(e.g: nu --testbin meow file.txt)" + } + + fn run(&self) { + let args: Vec = args(); + + for arg in args.iter().skip(1) { + let contents = std::fs::read_to_string(arg).expect("Expected a filepath"); + println!("{contents}"); + } + } +} + +impl TestBin for Meowb { + fn help(&self) -> &'static str { + "Cross platform cat (open a file, print the contents) using read() and write_all() / binary(e.g: nu --testbin meowb sample.db)" + } + + fn run(&self) { + let args: Vec = args(); + + let stdout = io::stdout(); + let mut handle = stdout.lock(); + + for arg in args.iter().skip(1) { + let buf = std::fs::read(arg).expect("Expected a filepath"); + handle.write_all(&buf).expect("failed to write to stdout"); + } + } +} + +impl TestBin for Relay { + fn help(&self) -> &'static str { + "Relays anything received on stdin to stdout(e.g: 0x[beef] | nu --testbin relay)" + } + + fn run(&self) { + io::copy(&mut io::stdin().lock(), &mut io::stdout().lock()) + .expect("failed to copy stdin to stdout"); + } +} + +impl TestBin for Iecho { + fn help(&self) -> &'static str { + "Another type of echo that outputs a parameter per line, looping infinitely(e.g: nu --testbin iecho 3)" + } + + fn run(&self) { + // println! panics if stdout gets closed, whereas writeln gives us an error + let mut stdout = io::stdout(); + let _ = args() + .iter() + .skip(1) + .cycle() + .try_for_each(|v| writeln!(stdout, "{v}")); + } +} + +impl TestBin for Fail { + fn help(&self) -> &'static str { + "Exits with failure code 1(e.g: nu --testbin fail)" + } + + fn run(&self) { + fail(); + } +} + +impl TestBin for Nonu { + fn help(&self) -> &'static str { + "Cross platform echo but concats arguments without space and NO newline(e.g: nu --testbin nonu a b c)" + } + + fn run(&self) { + args().iter().skip(1).for_each(|arg| print!("{arg}")); + } +} + +impl TestBin for Chop { + fn help(&self) -> &'static str { + "With no parameters, will chop a character off the end of each line" + } + + fn run(&self) { + if did_chop_arguments() { + // we are done and don't care about standard input. + std::process::exit(0); + } + + // if no arguments given, chop from standard input and exit. + let stdin = io::stdin(); + let mut stdout = io::stdout(); + + for given in stdin.lock().lines().map_while(Result::ok) { + let chopped = if given.is_empty() { + &given + } else { + let to = given.len() - 1; + &given[..to] + }; + + if let Err(_e) = writeln!(stdout, "{chopped}") { + break; + } + } + + std::process::exit(0); + } +} +impl TestBin for Repeater { + fn help(&self) -> &'static str { + "Repeat a string or char N times(e.g: nu --testbin repeater a 5)" + } + + fn run(&self) { + let mut stdout = io::stdout(); + let args = args(); + let mut args = args.iter().skip(1); + let letter = args.next().expect("needs a character to iterate"); + let count = args.next().expect("need the number of times to iterate"); + + let count: u64 = count.parse().expect("can't convert count to number"); + + for _ in 0..count { + let _ = write!(stdout, "{letter}"); + } + let _ = stdout.flush(); + } +} + +impl TestBin for RepeatBytes { + fn help(&self) -> &'static str { + "A version of repeater that can output binary data, even null bytes(e.g: nu --testbin repeat_bytes 003d9fbf 10)" + } + + fn run(&self) { + let mut stdout = io::stdout(); + let args = args(); + let mut args = args.iter().skip(1); + + while let (Some(binary), Some(count)) = (args.next(), args.next()) { + let bytes: Vec = (0..binary.len()) + .step_by(2) + .map(|i| { + u8::from_str_radix(&binary[i..i + 2], 16) + .expect("binary string is valid hexadecimal") + }) + .collect(); + let count: u64 = count.parse().expect("repeat count must be a number"); + + for _ in 0..count { + stdout + .write_all(&bytes) + .expect("writing to stdout must not fail"); + } + } + + let _ = stdout.flush(); + } +} + +impl TestBin for NuRepl { + fn help(&self) -> &'static str { + "Run a REPL with the given source lines, it must be called with `--testbin=nu_repl`, `--testbin nu_repl` will not work due to argument count logic" + } + + fn run(&self) { + nu_repl(); + } +} + +impl TestBin for InputBytesLength { + fn help(&self) -> &'static str { + "Prints the number of bytes received on stdin(e.g: 0x[deadbeef] | nu --testbin input_bytes_length)" + } + + fn run(&self) { + let stdin = io::stdin(); + let count = stdin.lock().bytes().count(); + + println!("{count}"); + } +} + /// Echo's value of env keys from args /// Example: nu --testbin env_echo FOO BAR /// If it it's not present echo's nothing @@ -23,11 +314,6 @@ pub fn echo_env(to_stdout: bool) { } } -pub fn echo_env_and_fail(to_stdout: bool) { - echo_env(to_stdout); - fail(); -} - fn echo_one_env(arg: &str, to_stdout: bool) { if let Ok(v) = std::env::var(arg) { if to_stdout { @@ -38,177 +324,10 @@ fn echo_one_env(arg: &str, to_stdout: bool) { } } -/// Mix echo of env keys from input -/// Example: -/// * nu --testbin echo_env_mixed out-err FOO BAR -/// * nu --testbin echo_env_mixed err-out FOO BAR -/// If it's not present, panic instead -pub fn echo_env_mixed() { - let args = args(); - let args = &args[1..]; - - if args.len() != 3 { - panic!( - r#"Usage examples: -* nu --testbin echo_env_mixed out-err FOO BAR -* nu --testbin echo_env_mixed err-out FOO BAR"# - ) - } - match args[0].as_str() { - "out-err" => { - let (out_arg, err_arg) = (&args[1], &args[2]); - echo_one_env(out_arg, true); - echo_one_env(err_arg, false); - } - "err-out" => { - let (err_arg, out_arg) = (&args[1], &args[2]); - echo_one_env(err_arg, false); - echo_one_env(out_arg, true); - } - _ => panic!("The mixed type must be `out_err`, `err_out`"), - } -} - -/// Cross platform echo using println!() -/// Example: nu --testbin cococo a b c -/// a b c -pub fn cococo() { - let args: Vec = args(); - - if args.len() > 1 { - // Write back out all the arguments passed - // if given at least 1 instead of chickens - // speaking co co co. - println!("{}", &args[1..].join(" ")); - } else { - println!("cococo"); - } -} - -/// Cross platform cat (open a file, print the contents) using read_to_string and println!() -pub fn meow() { - let args: Vec = args(); - - for arg in args.iter().skip(1) { - let contents = std::fs::read_to_string(arg).expect("Expected a filepath"); - println!("{contents}"); - } -} - -/// Cross platform cat (open a file, print the contents) using read() and write_all() / binary -pub fn meowb() { - let args: Vec = args(); - - let stdout = io::stdout(); - let mut handle = stdout.lock(); - - for arg in args.iter().skip(1) { - let buf = std::fs::read(arg).expect("Expected a filepath"); - handle.write_all(&buf).expect("failed to write to stdout"); - } -} - -// Relays anything received on stdin to stdout -pub fn relay() { - io::copy(&mut io::stdin().lock(), &mut io::stdout().lock()) - .expect("failed to copy stdin to stdout"); -} - -/// Cross platform echo but concats arguments without space and NO newline -/// nu --testbin nonu a b c -/// abc -pub fn nonu() { - args().iter().skip(1).for_each(|arg| print!("{arg}")); -} - -/// Repeat a string or char N times -/// nu --testbin repeater a 5 -/// aaaaa -/// nu --testbin repeater test 5 -/// testtesttesttesttest -pub fn repeater() { - let mut stdout = io::stdout(); - let args = args(); - let mut args = args.iter().skip(1); - let letter = args.next().expect("needs a character to iterate"); - let count = args.next().expect("need the number of times to iterate"); - - let count: u64 = count.parse().expect("can't convert count to number"); - - for _ in 0..count { - let _ = write!(stdout, "{letter}"); - } - let _ = stdout.flush(); -} - -/// A version of repeater that can output binary data, even null bytes -pub fn repeat_bytes() { - let mut stdout = io::stdout(); - let args = args(); - let mut args = args.iter().skip(1); - - while let (Some(binary), Some(count)) = (args.next(), args.next()) { - let bytes: Vec = (0..binary.len()) - .step_by(2) - .map(|i| { - u8::from_str_radix(&binary[i..i + 2], 16) - .expect("binary string is valid hexadecimal") - }) - .collect(); - let count: u64 = count.parse().expect("repeat count must be a number"); - - for _ in 0..count { - stdout - .write_all(&bytes) - .expect("writing to stdout must not fail"); - } - } - - let _ = stdout.flush(); -} - -/// Another type of echo that outputs a parameter per line, looping infinitely -pub fn iecho() { - // println! panics if stdout gets closed, whereas writeln gives us an error - let mut stdout = io::stdout(); - let _ = args() - .iter() - .skip(1) - .cycle() - .try_for_each(|v| writeln!(stdout, "{v}")); -} - pub fn fail() { std::process::exit(1); } -/// With no parameters, will chop a character off the end of each line -pub fn chop() { - if did_chop_arguments() { - // we are done and don't care about standard input. - std::process::exit(0); - } - - // if no arguments given, chop from standard input and exit. - let stdin = io::stdin(); - let mut stdout = io::stdout(); - - for given in stdin.lock().lines().map_while(Result::ok) { - let chopped = if given.is_empty() { - &given - } else { - let to = given.len() - 1; - &given[..to] - }; - - if let Err(_e) = writeln!(stdout, "{chopped}") { - break; - } - } - - std::process::exit(0); -} - fn outcome_err(engine_state: &EngineState, error: &ShellError) -> ! { report_shell_error(engine_state, error); std::process::exit(1); @@ -356,14 +475,42 @@ fn did_chop_arguments() -> bool { false } -pub fn input_bytes_length() { - let stdin = io::stdin(); - let count = stdin.lock().bytes().count(); - - println!("{count}"); -} - fn args() -> Vec { // skip (--testbin bin_name args) std::env::args().skip(2).collect() } + +pub fn show_help(dispatcher: &std::collections::HashMap>) { + println!("Usage: nu --testbin \n:"); + let mut names = dispatcher.keys().collect::>(); + names.sort(); + for n in names { + let test_bin = dispatcher.get(n).expect("Test bin should exist"); + println!("{n} -> {}", test_bin.help()) + } +} + +/// Create a new testbin dispatcher, which is useful to guide the testbin to run. +pub fn new_testbin_dispatcher() -> HashMap> { + let mut dispatcher: HashMap> = HashMap::new(); + dispatcher.insert("echo_env".to_string(), Box::new(EchoEnv)); + dispatcher.insert("echo_env_stderr".to_string(), Box::new(EchoEnvStderr)); + dispatcher.insert( + "echo_env_stderr_fail".to_string(), + Box::new(EchoEnvStderrFail), + ); + dispatcher.insert("echo_env_mixed".to_string(), Box::new(EchoEnvMixed)); + dispatcher.insert("cococo".to_string(), Box::new(Cococo)); + dispatcher.insert("meow".to_string(), Box::new(Meow)); + dispatcher.insert("meowb".to_string(), Box::new(Meowb)); + dispatcher.insert("relay".to_string(), Box::new(Relay)); + dispatcher.insert("iecho".to_string(), Box::new(Iecho)); + dispatcher.insert("fail".to_string(), Box::new(Fail)); + dispatcher.insert("nonu".to_string(), Box::new(Nonu)); + dispatcher.insert("chop".to_string(), Box::new(Chop)); + dispatcher.insert("repeater".to_string(), Box::new(Repeater)); + dispatcher.insert("repeat_bytes".to_string(), Box::new(RepeatBytes)); + dispatcher.insert("nu_repl".to_string(), Box::new(NuRepl)); + dispatcher.insert("input_bytes_length".to_string(), Box::new(InputBytesLength)); + dispatcher +}