Add a --times N flag that prints the greeting N times #27
Approved by g1t-agent
Note
Running on Claude Sonnet 5.5.
Prompt
You are a coding agent working in the git repository checked out in the current directory.
Issue #22: Add a --times N flag that prints the greeting N times
Add
pub times: usize(default 1) toOptionsinsrc/lib.rs, set by--times N(the value is the next argument, which must not be mistaken for the name). On a missing or non-numeric value, print an error to stderr and exit with a non-zero status (parse_args may return a Result). Insrc/main.rs, print the greeting that many times, one per line, and combine correctly with--shout. Add unit tests for parsing (valid, missing value, non-number) and integration tests intests/cli.rsrunning the binary with--times 3 Adaand with--times 2 --shout Ada.These commands must pass when you are done. Run them if the tools are installed:
- cargo test
- cargo run -- --times 3 Ada
Make the change and keep it focused on the issue. Commit your work with a clear message. Do not push; that is done for you. Finish with a short summary of what you changed and why. It becomes the description of your pull request, so write it for a reviewer: plain sentences, no headings, no emoji, no checklists, and nothing about whether anything was committed or pushed. Say what you did not verify.
Bashcat src/lib.rs src/main.rs; ls tests; cat tests/cli.rs
cat src/lib.rs src/main.rs; ls tests; cat tests/cli.rsresult→ //! Greetings.
//! Greetings. /// Parsed command-line options. #[derive(Debug, Default, PartialEq, Eq)] pub struct Options { /// The name to greet, if one was given. pub name: Option<String>, /// Whether to upper-case the greeting (`--shout`). pub shout: bool, /// Whether to print the crate version and exit (`--version`). pub version: bool, } /// Parses command-line arguments (excluding the program name). /// /// `--shout` sets [`Options::shout`] and `--version` sets /// [`Options::version`]; other arguments starting with `--` are /// treated as flags and skipped. The name is the first remaining argument. pub fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Options { let mut options = Options::default(); for arg in args { if arg == "--shout" { options.shout = true; } else if arg == "--version" { options.version = true; } else if !arg.starts_with("--") && options.name.is_none() { options.name = Some(arg); } } options } /// `text` upper-cased. pub fn shout(text: &str) -> String { text.to_uppercase() } /// The greeting for `name`. pub fn greeting(name: &str) -> String { let trimmed = name.trim(); let display_name = if trimmed.is_empty() { "world" } else { trimmed }; format!("Hello, {display_name}!") } /// The farewell for `name`. pub fn farewell(name: &str) -> String { let trimmed = name.trim(); let display_name = if trimmed.is_empty() { "world" } else { trimmed }; format!("Goodbye, {display_name}!") } #[cfg(test)] mod tests { use super::*; fn args(list: &[&str]) -> Vec<String> { list.iter().map(|s| s.to_string()).collect() } #[test] fn parse_args_no_args() { assert_eq!(parse_args(args(&[])).name, None); } #[test] fn parse_args_name() { assert_eq!(parse_args(args(&["Ada"])).name.as_deref(), Some("Ada")); } #[test] fn parse_args_skips_leading_flag() { assert_eq!( parse_args(args(&["--unknown", "Ada"])).name.as_deref(), Some("Ada") ); } #[test] fn parse_args_shout_defaults_off() { assert!(!parse_args(args(&["Ada"])).shout); } #[test] fn parse_args_shout_before_name() { let options = parse_args(args(&["--shout", "Ada"])); assert!(options.shout); assert_eq!(options.name.as_deref(), Some("Ada")); } #[test] fn parse_args_shout_after_name() { let options = parse_args(args(&["Ada", "--shout"])); assert!(options.shout); assert_eq!(options.name.as_deref(), Some("Ada")); } #[test] fn parse_args_shout_alone() { let options = parse_args(args(&["--shout"])); assert!(options.shout); assert_eq!(options.name, None); } #[test] fn parse_args_version_defaults_off() { assert!(!parse_args(args(&["Ada"])).version); } #[test] fn parse_args_version() { let options = parse_args(args(&["--version"])); assert!(options.version); assert_eq!(options.name, None); } #[test] fn shout_upper_cases_greeting() { assert_eq!(shout(&greeting("Ada")), "HELLO, ADA!"); } #[test] fn greets_by_name() { assert_eq!(greeting("Ada"), "Hello, Ada!"); } #[test] fn greets_world_with_empty_string() { assert_eq!(greeting(""), "Hello, world!"); } #[test] fn greets_world_with_whitespace_only() { assert_eq!(greeting(" "), "Hello, world!"); } #[test] fn trims_whitespace_from_name() { assert_eq!(greeting(" Ada "), "Hello, Ada!"); } #[test] fn says_goodbye_by_name() { assert_eq!(farewell("Ada"), "Goodbye, Ada!"); } #[test] fn says_goodbye_to_world_when_blank() { assert_eq!(farewell(" "), "Goodbye, world!"); } #[test] fn farewell_trims_whitespace() { assert_eq!(farewell(" Ada "), "Goodbye, Ada!"); } } use std::env; use hello::{greeting, parse_args, shout}; fn main() { let options = parse_args(env::args().skip(1)); if options.version { println!("{}", env!("CARGO_PKG_VERSION")); return; } // The name given, or whoever is signed in. let name = options .name .or_else(|| env::var("USER").ok()) .unwrap_or_else(|| "world".to_owned()); let message = greeting(&name); println!("{}", if options.shout { shout(&message) } else { message }); } cli.rs use std::process::Command; #[test] fn version_flag_prints_crate_version() { let output = Command::new(env!("CARGO_BIN_EXE_hello")) .arg("--version") .output() .unwrap(); assert!(output.status.success()); assert_eq!( String::from_utf8(output.stdout).unwrap(), format!("{}\n", env!("CARGO_PKG_VERSION")) ); }Bashpython3 - <<'E'
python3 - <<'E' import re s=open('src/lib.rs').read() s=re.sub(r'parse_args\(args\((&\[[^\]]*\])\)\)',r'parse_args(args(\1)).unwrap()',s) s=s.replace('''#[derive(Debug, Default, PartialEq, Eq)] pub struct Options {''','''#[derive(Debug, PartialEq, Eq)] pub struct Options {''') s=s.replace(''' pub version: bool, } ''',''' pub version: bool, /// How many times to print the greeting (`--times N`). pub times: usize, } impl Default for Options { fn default() -> Self { Options { name: None, shout: false, version: false, times: 1, } } } ''',1) a=s.index('/// Parses command-line') b=s.index('/// `text` upper-cased') s=s[:a]+'''/// Parses command-line arguments (excluding the program name). /// /// `--shout` sets [`Options::shout`], `--version` sets /// [`Options::version`], and `--times N` sets [`Options::times`] (the /// next argument is consumed as the value); other arguments starting /// with `--` are treated as flags and skipped. The name is the first /// remaining argument. /// /// Returns an error message if `--times` has a missing or non-numeric value. pub fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<Options, String> { let mut options = Options::default(); let mut args = args.into_iter(); while let Some(arg) = args.next() { if arg == "--shout" { options.shout = true; } else if arg == "--version" { options.version = true; } else if arg == "--times" { let value = args .next() .ok_or_else(|| "--times requires a value".to_owned())?; options.times = value .parse() .map_err(|_| format!("--times value must be a number, got {value:?}"))?; } else if !arg.starts_with("--") && options.name.is_none() { options.name = Some(arg); } } Ok(options) } '''+s[b:] s=s.replace(''' #[test] fn shout_upper_cases_greeting''',''' #[test] fn parse_args_times_defaults_to_one() { assert_eq!(parse_args(args(&["Ada"])).unwrap().times, 1); } #[test] fn parse_args_times_valid() { let options = parse_args(args(&["--times", "3", "Ada"])).unwrap(); assert_eq!(options.times, 3); assert_eq!(options.name.as_deref(), Some("Ada")); } #[test] fn parse_args_times_missing_value() { assert!(parse_args(args(&["--times"])).is_err()); } #[test] fn parse_args_times_non_number() { assert!(parse_args(args(&["--times", "abc", "Ada"])).is_err()); } #[test] fn shout_upper_cases_greeting''',1) open('src/lib.rs','w').write(s) m=open('src/main.rs').read() m=m.replace(''' let options = parse_args(env::args().skip(1)); ''',''' let options = match parse_args(env::args().skip(1)) { Ok(options) => options, Err(message) => { eprintln!("error: {message}"); process::exit(2); } }; ''').replace('use std::env;','use std::{env, process};') m=m.replace(''' println!("{}", if options.shout { shout(&message) } else { message });''',''' let line = if options.shout { shout(&message) } else { message }; for _ in 0..options.times { println!("{line}"); }''') open('src/main.rs','w').write(m) E cat >> tests/cli.rs <<'E' fn run(args: &[&str]) -> std::process::Output { Command::new(env!("CARGO_BIN_EXE_hello")) .args(args) .output() .unwrap() } #[test] fn times_prints_greeting_repeatedly() { let output = run(&["--times", "3", "Ada"]); assert!(output.status.success()); assert_eq!( String::from_utf8(output.stdout).unwrap(), "Hello, Ada!\n".repeat(3) ); } #[test] fn times_combines_with_shout() { let output = run(&["--times", "2", "--shout", "Ada"]); assert!(output.status.success()); assert_eq!( String::from_utf8(output.stdout).unwrap(), "HELLO, ADA!\n".repeat(2) ); } #[test] fn times_with_bad_value_fails() { let output = run(&["--times", "abc"]); assert!(!output.status.success()); assert!(!output.stderr.is_empty()); } E cargo test 2>&1 | tail -15; cargo run -q -- --times 3 Adaresult→ running 4 tests
running 4 tests test times_combines_with_shout ... ok test times_prints_greeting_repeatedly ... ok test times_with_bad_value_fails ... ok test version_flag_prints_crate_version ... ok test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s Doc-tests hello running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Hello, Ada! Hello, Ada! Hello, Ada!Bashcargo test 2>&1 | grep -E "test result|FAILED"; git commit -qam "Add --times N flag that prints the greeting N times" && echo ok
cargo test 2>&1 | grep -E "test result|FAILED"; git commit -qam "Add --times N flag that prints the greeting N times" && echo okresult→ test result: ok. 21 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 21 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s okg1t-agent
I added a
--times Nflag that prints the greeting N times, one per line. It works together with--shout.cargo testpasses, andcargo run -- --times 3 AdaprintsHello, Ada!three times.- Parsing:
Optionshas a newtimes: usizefield, defaulting to 1.--timestakes the next argument as its value, so that value is never read as the name. - Errors: A missing or non-numeric value makes
parse_argsreturn anErr.mainprintserror: ...to stderr and exits with status 2. - API change:
parse_argsnow returnsResult<Options, String>, so I added.unwrap()to the existing parsing tests. - Default impl:
Optionsnow has a hand-writtenDefault, because the derived one would have settimesto 0. - Tests: The new unit tests cover the default, a valid value, a missing value and a non-number. The integration tests run
--times 3 Adaand--times 2 --shout Ada. I also added one for a bad value, which checks for a non-zero exit and non-empty stderr.
I did not check how
--times 0behaves beyond the code: it prints nothing and exits successfully. I also did not check negative numbers, which fail to parse asusizeand so are treated as errors.- Parsing:
Note
This run cost $0.0712 over 4 turns.
Note
Pushed b1c42490059b.