2. RustCmdLine — CmdLineParse
CmdLineParse converts raw env::args() tokens into
a HashMap<char, String> of options and a
Vec<String> of file-extension patterns. A token
starting with / or - is treated as an option
key; the next token (if it does not itself start with / or
-) becomes its value, otherwise the value defaults to
"true". Accepting both prefixes lets the same CLI work in
PowerShell (/P .) and in bash (-P .) without
path conversion.
2.1 Design Points
-
Two storage layers. Options are keyed on a single
char; the comma-separated /p value is
expanded into a separate Vec<String> of patterns
for direct consumption by DirNav.
-
Absolute-path normalization.
abs_path()
canonicalizes the path and strips the Windows \\?\ UNC
prefix, then replaces backslashes with forward slashes so output is
uniform across platforms.
-
Ownership discipline. The struct owns all data (no
borrowed
&str fields), so it can be constructed in
one scope and consumed in another with no lifetime plumbing.
-
Defaults applied first, then overridden.
main calls default_options() before
parse(), so any argv value simply overwrites the default
entry in the map. A missing argument leaves the default in place.
2.2 Public API Summary
| Method |
Purpose |
new() |
Construct with empty maps and canned help string. |
default_options() |
Seed P=., s=true, r=., H=true. |
parse() |
Read args() into the option map; expand /p into patterns. |
path() / abs_path() |
Return raw or canonicalized root path. |
get_regex() / set_regex() |
Read or overwrite the /r value. |
contains_option(c) / value(c) |
Query the option map by single-char key. |
patterns() / add_pattern(p) |
Read the pattern vector; append without duplicates. |
help() / replace_help(s) |
Read or replace the built-in help text. |
2.3 Source — RustCmdLine/src/cmd_line_lib.rs
/////////////////////////////////////////////////////////////
// cmd_line_lib.rs //
// //
// Jim Fawcett, https://JimFawcett.github.io, 19 Apr 2020 //
// Revised: 05 Mar 2026 //
/////////////////////////////////////////////////////////////
use std::env::{args};
use std::collections::HashMap;
use std::fs::*;
/////////////////////////////////////////////////////////////
// sample command line with options
//-----------------------------------------------------------
// /P "." /p "rs,txt" /s [true] /r "abc" /h [true] /H [true]
//
// P - path in either absolute or relative form
// p - pattern, a file extension indicating file to process
// s - recurse directory tree rooted at P
// r - regular expression
// H - hide directories that don't contain any target files
// h - help: display this message
// custom option:
// /x [v] - x is application specific option which may
// have value, v
// Note:
// Any attribute that has no value on command line will
// have value "true" in option map
/////////////////////////////////////////////////////////////
/// display command line arguments
pub fn show_cmd_line() {
print!("\n {:?}\n ", args().next().unwrap_or_default());
let mut iter = args().skip(1).peekable();
while let Some(arg) = iter.next() {
print!("{:?}", arg);
if iter.peek().is_some() {
print!(" ");
}
}
}
pub type Options = HashMap<char, String>;
pub type CmdLinePatterns = Vec<String>;
/// Parses command line into options and patterns
#[derive(Debug, Default)]
pub struct CmdLineParse {
opt_map : Options,
patterns : CmdLinePatterns,
help_str : String,
}
impl CmdLineParse {
/// create new instance of parser
pub fn new() -> Self {
let help = CmdLineParse::help_txt();
Self {
opt_map: Options::default(),
patterns: CmdLinePatterns::new(),
help_str: help,
}
}
/// returns string with command line arguments example
fn help_txt() -> String {
let mut str =
"\n Help:\n Options: /P . /p \"rs,txt\"".to_string();
str.push_str(" /s /r \"abc\" /H /h");
str
}
/// does the command line argument start with '/'
fn is_opt(&self, s:&str) -> bool {
let bytes = s.as_bytes();
let first = bytes[0] as char;
first == '/' || first == '-'
}
/// returns path string with default value "."
pub fn path(&self) -> String {
if self.contains_option('P') {
self.opt_map[&'P'].clone()
}
else {
".".to_string()
}
}
/// replace Win path separator "\\" with Linux "/"
/// - use only with absolute paths for Windows
fn replace_sep(path: &str) -> String {
let mut rtn = path.to_string();
if rtn.contains("\\") {
rtn = rtn.replace("\\", "/");
rtn = rtn.chars().skip(4).collect();
}
rtn
}
/// convert relative to absolute path
pub fn abs_path(&self) -> String {
let abs = std::path::PathBuf::from(self.path());
let rslt = canonicalize(&abs);
match rslt {
Ok(path_buf) => {
let ap = path_buf.to_string_lossy().to_string();
let ap = CmdLineParse::replace_sep(&ap);
ap
}
Err(error) => error.to_string()
}
}
/// set new root path
pub fn set_path(&mut self, p:&str) {
self.opt_map.insert('P', p.to_string());
}
/// set new regex string for matching
pub fn set_regex(&mut self, re:&str) {
self.opt_map.insert('r', re.to_string());
}
/// return current regex string
pub fn get_regex(&self) -> &str {
let re_opt = self.opt_map.get(&'r');
match re_opt {
Some(value) => value,
None => ".",
}
}
/// commonly used default options
pub fn default_options(&mut self) {
self.opt_map.insert('P', ".".to_string()); // root is curr dir
self.opt_map.insert('s', "true".to_string()); // recurse
self.opt_map.insert('r', ".".to_string()); // regex always matches
self.opt_map.insert('H', "true".to_string()); // hide unused dirs
}
/// does options contain opt char?
pub fn contains_option(&self, opt:char) -> bool {
self.opt_map.contains_key(&opt)
}
/// insert {o,v} if o key doesn't exist, else overwrite v
pub fn add_option(&mut self, o:char, v:&str) {
self.opt_map.insert(o, v.to_string());
}
/// return option value
pub fn value(&self, opt:char) -> &str {
&self.opt_map[&opt]
}
/// add file ext (with no "*.")
pub fn add_pattern(&mut self, p:&str) -> &mut Self {
let s = String::from(p);
if !self.patterns.contains(&s) {
self.patterns.push(s);
}
self
}
/// returns non-mutable reference to patterns
pub fn patterns(&self) -> &CmdLinePatterns {
&self.patterns
}
/// returns non-mutable reference to options
pub fn options(& self) -> &Options {
&self.opt_map
}
/// return help string
pub fn help(&self) -> &str {
&self.help_str
}
/// replace help string
pub fn replace_help(&mut self, s:&str) {
self.help_str = s.to_string();
}
/// parse command line arguments, provided by env()
pub fn parse(&mut self) {
let cl_args:Vec<String> = args().collect();
let end = cl_args.len();
for i in 1..end {
if self.is_opt(&cl_args[i]) {
let bytes = cl_args[i].as_bytes();
let key = bytes[1] as char;
if i < end - 1 {
if !self.is_opt(&cl_args[i+1]) {
self.opt_map.insert(key,cl_args[i+1].to_string());
}
else {
self.opt_map.insert(key, "true".to_string());
}
}
else {
self.opt_map.insert(key, "true".to_string());
}
}
}
/*-- build patterns --*/
if self.contains_option('p') {
let pat_str = self.value('p').to_string();
let split_iter = pat_str.split(',');
for patt in split_iter {
self.add_pattern(patt);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cl_args() {
let _mock_args = vec!["/P", ".", "/p", "rs,txt", "/s"];
print!("\n cl args: ");
for arg in args() {
print!("{:?} ", arg);
}
let mut parser = CmdLineParse::new();
parser.parse();
for arg in args() {
let bytes = arg.as_bytes();
if '/' == (bytes[0] as char) {
assert!(parser.opt_map.contains_key(&(bytes[1] as char)));
}
}
}
}