Extract command-line parsing into a testable function in lib.rs #24
Approved by g1t
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 #19: Extract command-line parsing into a testable function in lib.rs
Currently
src/main.rsreads the name straight fromenv::args().nth(1). Add tosrc/lib.rsapub struct Options { pub name: Option<String> }andpub fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Options(args excludes the program name). Flag-like arguments (starting with--) must be skipped when choosing the name, so the name is the first non-flag argument; later issues will add flags to this struct. Updatemainto use it, keeping the existing fallbacks (USER env var, thenworld) and behaviour unchanged. Add unit tests insrc/lib.rsfor: no args, a name, and a leading unknown--flagfollowed by a name.These commands must pass when you are done. Run them if the tools are installed:
- cargo build
- cargo test
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/main.rs; cat src/lib.rs
cat src/main.rs; cat src/lib.rsresult→ use std::env;
use std::env; use hello::greeting; fn main() { // The name given, or whoever is signed in. let name = env::args() .nth(1) .or_else(|| env::var("USER").ok()) .unwrap_or_else(|| "world".to_owned()); println!("{}", greeting(&name)); } //! Greetings. /// 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::*; #[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!"); } }bash{"command":"python3 - <<'E'\np='src/lib.rs'\ns=open(p).read()\ns=s.replace('''/// The greeting for''','''/// Parsed command-line options.\n#[derive(Debug, Default, PartialEq, Eq)]\npub struct Options {\n /// The name to greet, if one was given.\n pub name: Option<String>,\n}\n\n/// Parses command-line arguments (excluding the program name).\n///\n/// Arguments starting with `--` are treated as flags and skipped; the name is\n/// the first remaining argument.\npub fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Options {\n let name = args.into_iter().find(|arg| !arg.starts_with(\"--\"));\n Options { name }\n}\n\n/// The greeting for''',1)\ns=s.replace(''' use super::*;\n''',''' use super::*;\n\n fn args(list: &[&str]) -> Vec<String> {\n list.iter().map(|s| s.to_string()).collect()\n }\n\n #[test]\n fn parse_args_no_args() {\n assert_eq!(parse_args(args(&[])).name, None);\n }\n\n #[test]\n fn parse_args_name() {\n assert_eq!(parse_args(args(&[\"Ada\"])).name.as_deref(), Some(\"Ada\"));\n }\n\n #[test]\n fn parse_args_skips_leading_flag() {\n assert_eq!(\n parse_args(args(&[\"--unknown\", \"Ada\"])).name.as_deref(),\n Some(\"Ada\")\n );\n }\n''',1)\nopen(p,'w').write(s)\np='src/main.rs'\ns=open(p).read()\ns=s.replace('use hello::greeting;','use hello::{greeting, parse_args};')\ns=s.replace('''env::args()\n .nth(1)\n''','''parse_args(env::args().skip(1))\n .name\n''')\nopen(p,'w').write(s)\nE\ncargo build 2>&1|tail -3; cargo test 2>&1|grep -E \"test result|FAIL|error\""}
{"command":"python3 - <<'E'\np='src/lib.rs'\ns=open(p).read()\ns=s.replace('''/// The greeting for''','''/// Parsed command-line options.\n#[derive(Debug, Default, PartialEq, Eq)]\npub struct Options {\n /// The name to greet, if one was given.\n pub name: Option<String>,\n}\n\n/// Parses command-line arguments (excluding the program name).\n///\n/// Arguments starting with `--` are treated as flags and skipped; the name is\n/// the first remaining argument.\npub fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Options {\n let name = args.into_iter().find(|arg| !arg.starts_with(\"--\"));\n Options { name }\n}\n\n/// The greeting for''',1)\ns=s.replace(''' use super::*;\n''',''' use super::*;\n\n fn args(list: &[&str]) -> Vec<String> {\n list.iter().map(|s| s.to_string()).collect()\n }\n\n #[test]\n fn parse_args_no_args() {\n assert_eq!(parse_args(args(&[])).name, None);\n }\n\n #[test]\n fn parse_args_name() {\n assert_eq!(parse_args(args(&[\"Ada\"])).name.as_deref(), Some(\"Ada\"));\n }\n\n #[test]\n fn parse_args_skips_leading_flag() {\n assert_eq!(\n parse_args(args(&[\"--unknown\", \"Ada\"])).name.as_deref(),\n Some(\"Ada\")\n );\n }\n''',1)\nopen(p,'w').write(s)\np='src/main.rs'\ns=open(p).read()\ns=s.replace('use hello::greeting;','use hello::{greeting, parse_args};')\ns=s.replace('''env::args()\n .nth(1)\n''','''parse_args(env::args().skip(1))\n .name\n''')\nopen(p,'w').write(s)\nE\ncargo build 2>&1|tail -3; cargo test 2>&1|grep -E \"test result|FAIL|error\""}result→ <tool_use_error>Error: No such tool available: bash. Tool names are case-sensitive: call Bash instead.</tool_use_error>
<tool_use_error>Error: No such tool available: bash. Tool names are case-sensitive: call Bash instead.</tool_use_error>Bashpython3 - <<'E'
python3 - <<'E' p='src/lib.rs' s=open(p).read() s=s.replace('''/// The greeting for''','''/// Parsed command-line options. #[derive(Debug, Default, PartialEq, Eq)] pub struct Options { /// The name to greet, if one was given. pub name: Option<String>, } /// Parses command-line arguments (excluding the program name). /// /// 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 name = args.into_iter().find(|arg| !arg.starts_with("--")); Options { name } } /// The greeting for''',1) s=s.replace(''' use super::*; ''',''' 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") ); } ''',1) open(p,'w').write(s) p='src/main.rs' s=open(p).read() s=s.replace('use hello::greeting;','use hello::{greeting, parse_args};') s=s.replace('''env::args() .nth(1) ''','''parse_args(env::args().skip(1)) .name ''') open(p,'w').write(s) E cargo build 2>&1|tail -3; cargo test 2>&1|grep -E "test result|FAIL|error"result→ Compiling hello v0.1.0 (/work/repo)
Compiling hello v0.1.0 (/work/repo) Finished `dev` profile [unoptimized + debuginfo] target(s) in 5.12s test result: ok. 10 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. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sBashgit commit -qam "Extract argument parsing into parse_args in lib.rs" && git log --oneline | head -1
git commit -qam "Extract argument parsing into parse_args in lib.rs" && git log --oneline | head -1result→ 76b3709 Extract argument parsing into parse_args in lib.rs
76b3709 Extract argument parsing into parse_args in lib.rsg1t
I moved argument parsing out of
mainintosrc/lib.rs. It addsOptions { name: Option<String> }andparse_args, which takes the arguments without the program name. Arguments starting with--are skipped, and the name is the first argument left over.mainnow callsparse_args(env::args().skip(1))and falls back to theUSERenv var and thenworld, as before. One small change in behaviour: a leading--flagis no longer taken as the name. Previously it would have been.I added three unit tests for no args, a plain name, and a leading
--unknownflag followed by a name.cargo buildandcargo testboth pass. I didn't run the binary by hand.Note
This run cost $0.0670 over 5 turns.
Note
Pushed 76b37094ade6.