Search code examples
scalacommand-line-interfacescopt

How to do a case insensitive match for command line arguments in scala?


I'm working on a command line tool written in Scala which is executed as:

sbt "run --customerAccount 1234567"

Now, I wish to make this flexible to accept "--CUSTOMERACCOUNT" or --cUsToMerAccount or --customerACCOUNT ...you get the drift

Here's what the code looks like:

lazy val OptionsParser: OptionParser[Args] = new scopt.OptionParser[Args]("scopt") {
    head(
      "XML Generator",
      "Creates XML for testing"
    )
    help("help").text(s"Prints this usage message. $envUsage")

    opt[String]('c', "customerAccount")
      .text("Required: Please provide customer account number as -c 12334 or --customerAccount 12334")
      .required()
      .action { (cust, args) =>
        assert(cust.nonEmpty, "cust is REQUIRED!!")
        args.copy(cust = cust)
      }
}

I assume the opt[String]('c', "customerAccount") does the pattern matching from the command line and will match with "customerAccount" - how do I get this to match with "--CUSTOMERACCOUNT" or --cUsToMerAccount or --customerACCOUNT? What exactly does the args.copy (cust = cust) do?

I apologize if the questions seem too basic. I'm incredibly new to Scala, have worked in Java and Python earlier so sometimes I find the syntax a little hard to understand as well.


Solution

  • You'd normally be parsing the args with code like:

    OptionsParser.parse(args, Args())
    

    So if you want case-insensitivity, probably the easiest way is to canonicalize the case of args with something like

    val canonicalized = args.map(_.toLowerCase)
    
    OptionsParser.parse(canonicalized, Args())
    

    Or, if you for instance wanted to only canonicalize args starting with -- and before a bare --:

    val canonicalized =
      args.foldLeft(false -> List.empty[String]) { (state, arg) =>
        val (afterDashes, result) = state
        if (afterDashes) true -> (arg :: result)  // pass through unchanged
        else {
          if (arg == "==") true -> (arg :: result) // move to afterDash state & pass through
          else {
            if (arg.startsWith("--")) false -> (arg.toLowerCase :: result)
            else false -> (arg :: result) // pass through unchanged
          }
        }
      }
      ._2    // Extract the result
      .reverse // Reverse it back into the original order (if building up a sequence, your first choice should be to build a list in reversed order and reverse at the end)
    
      OptionsParser.parse(canonicalized, Args())
    

    Re the second question, since Args is (almost certainly) a case class, it has a copy method which constructs a new object with (most likely, depending on usage) different values for its fields. So

     args.copy(cust = cust)
    

    creates a new Args object, where:

    • the value of the cust field in that object is the value of the cust variable in that block (this is basically a somewhat clever hack that works with named method arguments)
    • every other field's value is taken from args