Skip to content

Instantly share code, notes, and snippets.

@bruth
Last active December 24, 2015 01:59
Show Gist options
  • Save bruth/6727690 to your computer and use it in GitHub Desktop.
Save bruth/6727690 to your computer and use it in GitHub Desktop.
Simple command line parser
object cli {
case class ArgumentsRequired(args: List[Any]) extends Exception {
override def getMessage: String = {
"required arguments: %s".format(this.args.mkString(", "))
}
}
case class UnknownArguments(args: List[Any]) extends Exception {
override def getMessage: String = {
"unknown arguments: %s".format(this.args.mkString(", "))
}
}
/**
* parseOptions parses command line arguments based on the provided required
* and optional arguments. If not all the required arguments have been
* fulfilled or the unknown arguments are provided an exception will be
* thrown.
*
* @param args A list of strings that contain the options being parsed.
* @param required A list of symbols that will be mapped to positional
* arguments (strings that are not flag-based).
* @param optional A map of string -> symbol representing the argument flags
* and corresponding symbol to be used in the options map.
* @param options A map of symbol -> string which contains the parsed (or
* predefined) options.
* @return A map of symbol -> string containing the parsed options.
*/
def parseOptions(args: List[String], required: List[Symbol], optional: Map[String, Symbol], options: Map[Symbol, String]): Map[Symbol, String] = {
args match {
// Empty list. Ensure the required arguments have been satisfied.
case Nil => {
if (required != Nil) {
throw new ArgumentsRequired(required)
}
options
}
// Keyword arguments
case key :: value :: tail if optional.get(key) != None =>
parseOptions(tail, required, optional, options ++ Map(optional(key) -> value))
// Positional arguments
case value :: tail if required != Nil =>
parseOptions(tail, required.tail, optional, options ++ Map(required.head -> value))
// Unknown argument is received
case _ =>
throw new UnknownArguments(args)
}
}
}
@masinoa
Copy link

masinoa commented Sep 28, 2013

The way I would write this is


@tailrec
def parseOptions(args: Option[List[String]] = None, 
    required: Option[List[Symbol]] = None, 
    optional: Option[Map[String, Symbol]] = None, 
    current: Map[String, Symbol]=None) = Option[Map[Symbol,String]] = {
    
    val cur = current.getOrElse(Map[String, Symbol]())
    val opt = optional.getOrElse(Map[String,Symbol]())
    val rq = required.getOrElse(List[Symbol]())
    
    args match{
        case None => current
        
        case Some(key :: value :: tail) if opt.contains(key) => 
            parseOptions(Some(tail), Some(rq), Some(opt), Some(cur ++ Map(opt(key) -> value)))
        
        case Some(value :: tail) if !rq.isEmpty => 
            parseOptions(Some(tail), Some(rq.tail), Some(opt), Some(cur ++ Map(rq.head -> value)))
            
        case _ =>
            printf("unknown argument(s): %s\n", args.mkString(", "))
            sys.exit(1)
    }
        
}

The @tailrec tag ensures that your recursive method is tail recursive which means you won't blow the stack. Probably not important here, but in general its a good idea to use whenever doing recursion. I, like Mike suggested, would also use the Option constructs with default args. That is idiomatic Scala and the default arg makes it easier for the user of the method since they don't have to supply those arguments.

FWIW, I like your use of recursion in this case, it's probably the most elegant solution. Additionally, your use of pattern guards is cool (I often forget they exist) though I did change the condition checks to use builtins and I didn't know you could do case Some(value :: tail) matches - just when I think I know Scala I'm reminded that I don't.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment