-
-
Save bruth/6727690 to your computer and use it in GitHub Desktop.
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) | |
} | |
} | |
} |
One idiomatic scala thing if this is refactored to be a generic method, is to use an Option
type with default parameters. For example:
def parseOptions(map: Map[Symbol, String], args: List[String], pos: List[Symbol]): Map[Symbol, String]
becomes:
def parseOptions(map: Map[Symbol, String], args: Option[List[String]]=None, pos: Option[List[Symbol]]=None): Map[Symbol, String]
This allows you to use the following pattern:
args match {
case None => bail_out
case Some[List[String]] => process_the_list()
...
You might also consider having parseOptions method return an Option[Map[Symbol, String]] so that you can easily test return values to see if they exist in your main function when you go to actually use options. For example, somewhere in your code you might have a test:
val myLocalVal = options.getOrElse(useDefaultOptions())
Another style thing here is that it looks like you're passing a muatbel Map
as "map". Whenever I use something like that, I look to see if it can be mutable. I think rather than passing the empty map, it's more functional to just generate the final map one time within the body of your method.
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.
@mitalia @masinoa have any suggestions or improvements for this code? anything non-idiomatic?