Comparison Story: RustCmdLine

2.  CmdLineParse — argv into options and patterns

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)));
            }
        }
    }
}