Search code examples
rustclap

How to enable this Clap script to pass arguments as -i and -l?


With this script, I'm able to pass arguments as:

cargo run -- i

// or

trs -- i
use anyhow::{Context, Result};
use clap::{Parser, ValueEnum};
use regex::Regex;
use std::io::{BufRead, BufReader};

#[derive(Parser)]
#[clap(author, version, about)]
struct Cli {
    /// Choose a command: 'i' for incrementing numbers, 'l' for incrementing letters
    #[clap(value_enum)]
    command: Command,
}

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum Command {
    I,
    L,
}

struct TextTransformer {
    text: String,
}

impl TextTransformer {
    fn new<R: BufRead>(mut reader: R) -> Result<Self> {
        let mut text = String::new();
        let mut line = String::new();

        while reader
            .read_line(&mut line)
            .with_context(|| "Failed to read line from stdin")?
            > 0
        {
            text.push_str(&line);
            line.clear();
        }

        Ok(Self { text })
    }

    fn increment_numbers(mut self) -> Self {
        // Update regex to match numbers within alphanumeric strings
        let number_regex = Regex::new(r"(\d+(\.\d+)?)").unwrap();
        self.text = number_regex
            .replace_all(&self.text, |caps: &regex::Captures| {
                let num = caps[1].parse::<f64>().unwrap();
                (num + 1.0).to_string()
            })
            .to_string();

        self
    }

    fn increment_letters(mut self) -> Self {
        // Regex to match alphabetic characters
        let letter_regex = Regex::new(r"[A-Za-z]").unwrap();
        self.text = letter_regex
            .replace_all(&self.text, |caps: &regex::Captures| {
                let c = caps[0].chars().next().unwrap();
                let incremented = match c {
                    'a'..='y' | 'A'..='Y' => (c as u8 + 1) as char,
                    'z' => 'a',
                    'Z' => 'A',
                    _ => c,
                };
                incremented.to_string()
            })
            .to_string();

        self
    }

    fn finalize(self) -> String {
        self.text
    }
}

fn main() -> Result<()> {
    let cli = Cli::parse();

    let stdin = std::io::stdin();
    let reader = BufReader::new(stdin.lock());

    let output = match cli.command {
        Command::I => TextTransformer::new(reader)?.increment_numbers().finalize(),
        Command::L => TextTransformer::new(reader)?.increment_letters().finalize(),
    };

    print!("{}", output);

    Ok(())
}

But if I try to do something like trs -i, I get this error:

Shell error: error: unexpected argument '-i' found  tip: to pass '-i' as a value, use '-- -i'

Why is this?

I want the trs -i to work, not the trs i.


Solution

  • A command field of the form

        #[clap(value_enum)]
        command: Command,
    

    Describes a positional argument, the value of which needs to match one of the enum variants. With variants I and L, Cli will expect either i or l at the first argument position, and not both. Multi-letter values are supported in this position.

    Arguments with a preceding - or -- are conventionally for flags and options. Since this argument was not specified as a flag, it does not expect -i or -l.

    As this is an attempt at implementing subcommands, the most idiomatic way to achieve what you want is:

    1. Declare command as a subcommand instead of an argument with a fixed list of choices;
    2. Then to support flags, add short_flag and/or long_flag to the subcommands available.
    #[derive(Parser)]
    struct Cli {
        #[clap(subcommand)]
        command: Command,
    }
    
    #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Parser)]
    enum Command {
        /// increment numbers
        #[clap(short_flag('i'))]
        I,
        /// increment letters
        #[clap(short_flag('l'))]
        L,
    }
    

    The resulting help output:

    Usage: trs <COMMAND>
    
    Commands:
      i, -i  increment numbers
      l, -l  increment letters
      help   Print this message or the help of the given subcommand(s)
    
    Options:
      -h, --help  Print help
    

    Playground