PageRenderTime 62ms CodeModel.GetById 33ms RepoModel.GetById 0ms app.codeStats 0ms

/src/orc/util/CmdLineParser.scala

https://github.com/laurenyew/cOrcS
Scala | 342 lines | 243 code | 42 blank | 57 comment | 91 complexity | 16208cc0be5e6c89f5bbdd76f8da0fe6 MD5 | raw file
Possible License(s): BSD-3-Clause
  1. //
  2. // CmdLineParser.scala -- Scala trait CmdLineParser
  3. // Project OrcScala
  4. //
  5. // $Id: CmdLineParser.scala 2933 2011-12-15 16:26:02Z jthywissen $
  6. //
  7. // Created by jthywiss on Jul 19, 2010.
  8. //
  9. // Copyright (c) 2011 The University of Texas at Austin. All rights reserved.
  10. //
  11. // Use and redistribution of this file is governed by the license terms in
  12. // the LICENSE file found in the project's top-level directory and also found at
  13. // URL: http://orc.csres.utexas.edu/license.shtml .
  14. //
  15. package orc.util
  16. import java.io.File
  17. import java.util.NoSuchElementException
  18. /** Parses command line arguments per POSIX and GNU command line syntax guidelines.
  19. * Mix this trait in and add XxxOprd and XxxOpt statements to your class holding
  20. * the resulting parsed values. Call parseCmdLine after all the XxxOprd and XxxOpt
  21. * statements.
  22. *
  23. * This CmdLineParser understands POSIX single-letter option names (-a -b -c) and GNU
  24. * long option names (--option-name=value). It also handles the various option argument
  25. * syntax: -avalue -a value --long-name=value and -long-name value, it tries to
  26. * "do the right thing" in the ambiguous situation -abc (could mean -a -b -c or
  27. * -a bc or --abc), and it understands the -- option list terminator.
  28. * See POSIX XBD §12.1 and §12.2 and GNU libc §25.1.1.
  29. *
  30. * @author jthywiss
  31. */
  32. trait CmdLineParser {
  33. var recognizedOprds = scala.collection.mutable.Map.empty[Int, CmdLineOprd]
  34. var recognizedOpts = scala.collection.mutable.Set.empty[CmdLineOpt]
  35. var recognizedShortOpts = scala.collection.mutable.Map.empty[Char, CmdLineOpt]
  36. var recognizedLongOpts = scala.collection.mutable.Map.empty[String, CmdLineOpt]
  37. ////////
  38. // Constructor
  39. ////////
  40. UnitOpt(() => false, () => printHelp, '?', "help", usage = "Give this help list")
  41. protected def printHelp {
  42. def shortOptHelp(opt: CmdLineOpt) =
  43. if (opt.shortName != 0 && opt.shortName != ' ')
  44. "-" + opt.shortName + (if (opt.argName != null && opt.argName.length > 0) (" " + opt.argName) else "")
  45. else
  46. ""
  47. def longOptHelp(opt: CmdLineOpt) =
  48. if (opt.longName != null && opt.longName.length > 0)
  49. "--" + opt.longName + (if (opt.argName != null && opt.argName.length > 0) ("=" + opt.argName) else "")
  50. else
  51. ""
  52. val helpString = usageString +
  53. (if (recognizedOpts.size == 0) ""
  54. else "\nOptions:\n" +
  55. (recognizedOpts.toList.sortBy(o => (o.longName, o.shortName)).map({ opt: CmdLineOpt =>
  56. //TODO: Aligned columns?
  57. " " + shortOptHelp(opt) +
  58. " " + longOptHelp(opt) +
  59. " " + opt.usage
  60. })).mkString("\n"))
  61. throw new PrintVersionAndMessageException(helpString)
  62. }
  63. UnitOpt(() => false, () => printUsage, ' ', "usage", usage = "Give a short usage message")
  64. protected def printUsage { throw new PrintVersionAndMessageException(usageString) }
  65. UnitOpt(() => false, () => printVersion, 'V', "version", usage = "Print program version")
  66. protected def printVersion { throw new PrintVersionAndMessageException("") }
  67. def usageString =
  68. "usage: " +
  69. (if (recognizedOpts.size > 0) "[options...]" else "") +
  70. (for { i <- 0 until recognizedOprds.size } yield " " + recognizedOprds(i).argName).mkString(" ")
  71. //"Try the --help option for more information." //TODO: In simple cases, show the usage here
  72. ////////
  73. // Command line operand and option definitions
  74. ////////
  75. abstract class CmdLineOprdOpt(val argName: String, val usage: String, val required: Boolean, val hidden: Boolean) {
  76. def getValue: String
  77. def setValue(s: String): Unit
  78. }
  79. abstract class CmdLineOprd(val position: Int, override val argName: String, override val usage: String, override val required: Boolean, override val hidden: Boolean) extends CmdLineOprdOpt(argName, usage, required, hidden) {
  80. if (recognizedOprds.contains(position)) throw new MultiplyDefinedCmdLineOprndError(position)
  81. recognizedOprds += ((position, this))
  82. }
  83. abstract class CmdLineOpt(val shortName: Char, val longName: String, override val argName: String, override val usage: String, override val required: Boolean, override val hidden: Boolean) extends CmdLineOprdOpt(argName, usage, required, hidden) {
  84. if (shortName != ' ') {
  85. if (recognizedShortOpts.contains(shortName)) throw new MultiplyDefinedCmdLineOptError(shortName.toString)
  86. recognizedShortOpts += ((shortName, this))
  87. }
  88. if (longName != null) {
  89. if (recognizedLongOpts.contains(longName)) throw new MultiplyDefinedCmdLineOptError(longName)
  90. recognizedLongOpts += ((longName, this))
  91. }
  92. recognizedOpts += this
  93. }
  94. case class BooleanOprd(val getter: Function0[Boolean], val setter: (Boolean => Unit), override val position: Int, override val argName: String = "BOOL", override val usage: String = "", override val required: Boolean = true, override val hidden: Boolean = false)
  95. extends CmdLineOprd(position, argName, usage, required, hidden) {
  96. def getValue: String = { getter().toString }
  97. def setValue(value: String) { setter(value.toBoolean) }
  98. }
  99. case class DoubleOprd(val getter: Function0[Double], val setter: (Double => Unit), override val position: Int, override val argName: String = "DOUBLE", override val usage: String = "", override val required: Boolean = true, override val hidden: Boolean = false)
  100. extends CmdLineOprd(position, argName, usage, required, hidden) {
  101. def getValue: String = { getter().toString }
  102. def setValue(value: String) { setter(value.toDouble) }
  103. }
  104. case class IntOprd(val getter: Function0[Int], val setter: (Int => Unit), override val position: Int, override val argName: String = "INT", override val usage: String = "", override val required: Boolean = true, override val hidden: Boolean = false)
  105. extends CmdLineOprd(position, argName, usage, required, hidden) {
  106. def getValue: String = { getter().toString }
  107. def setValue(value: String) { setter(value.toInt) }
  108. }
  109. case class CharOprd(val getter: Function0[Char], val setter: (Char => Unit), override val position: Int, override val argName: String = "CHAR", override val usage: String = "", override val required: Boolean = true, override val hidden: Boolean = false)
  110. extends CmdLineOprd(position, argName, usage, required, hidden) {
  111. def getValue: String = { getter().toString }
  112. def setValue(value: String) { setter(value(0)) }
  113. }
  114. case class StringOprd(val getter: Function0[String], val setter: (String => Unit), override val position: Int, override val argName: String = "STRING", override val usage: String = "", override val required: Boolean = true, override val hidden: Boolean = false)
  115. extends CmdLineOprd(position, argName, usage, required, hidden) {
  116. def getValue: String = { getter() }
  117. def setValue(value: String) { setter(value) }
  118. }
  119. case class StringListOprd(val getter: Function0[Seq[String]], val setter: (Seq[String] => Unit), override val position: Int, override val argName: String = "STRING", override val usage: String = "", override val required: Boolean = true, override val hidden: Boolean = false)
  120. extends CmdLineOprd(position, argName, usage, required, hidden) {
  121. def getValue: String = { getter().mkString(File.pathSeparator) }
  122. def setValue(value: String) { setter(value.split(File.pathSeparator)) }
  123. }
  124. case class FileOprd(val getter: Function0[File], val setter: (File => Unit), override val position: Int, override val argName: String = "FILE", override val usage: String = "", override val required: Boolean = true, override val hidden: Boolean = false)
  125. extends CmdLineOprd(position, argName, usage, required, hidden) {
  126. def getValue: String = {
  127. getter() match {
  128. case null => ""
  129. case f => f.toString
  130. }
  131. }
  132. def setValue(value: String) { if (value != null && !value.isEmpty) setter(new File(value)) }
  133. }
  134. case class PathListOprd(val getter: Function0[Seq[File]], val setter: (Seq[File] => Unit), override val position: Int, override val argName: String = "PATH", override val usage: String = "", override val required: Boolean = true, override val hidden: Boolean = false)
  135. extends CmdLineOprd(position, argName, usage, required, hidden) {
  136. def getValue: String = { getter().map(_.toString).mkString(File.pathSeparator) }
  137. def setValue(value: String) { setter(value.split(File.pathSeparator).map(new File(_))) }
  138. }
  139. case class UnitOpt(val getter: Function0[Boolean], val setter: (() => Unit), override val shortName: Char, override val longName: String, override val argName: String = "", override val usage: String = "", override val required: Boolean = false, override val hidden: Boolean = false)
  140. extends CmdLineOpt(shortName, longName, argName, usage, required, hidden) {
  141. def getValue: String = ""
  142. def setValue(value: String) { setter() }
  143. }
  144. case class BooleanOpt(val getter: Function0[Boolean], val setter: (Boolean => Unit), override val shortName: Char, override val longName: String, override val argName: String = "BOOL", override val usage: String = "", override val required: Boolean = false, override val hidden: Boolean = false)
  145. extends CmdLineOpt(shortName, longName, argName, usage, required, hidden) {
  146. def getValue: String = { getter().toString }
  147. def setValue(value: String) { setter(value.toBoolean) }
  148. }
  149. case class DoubleOpt(val getter: Function0[Double], val setter: (Double => Unit), override val shortName: Char, override val longName: String, override val argName: String = "DOUBLE", override val usage: String = "", override val required: Boolean = false, override val hidden: Boolean = false)
  150. extends CmdLineOpt(shortName, longName, argName, usage, required, hidden) {
  151. def getValue: String = { getter().toString }
  152. def setValue(value: String) { setter(value.toDouble) }
  153. }
  154. case class IntOpt(val getter: Function0[Int], val setter: (Int => Unit), override val shortName: Char, override val longName: String, override val argName: String = "INT", override val usage: String = "", override val required: Boolean = false, override val hidden: Boolean = false)
  155. extends CmdLineOpt(shortName, longName, argName, usage, required, hidden) {
  156. def getValue: String = { getter().toString }
  157. def setValue(value: String) { setter(value.toInt) }
  158. }
  159. case class CharOpt(val getter: Function0[Char], val setter: (Char => Unit), override val shortName: Char, override val longName: String, override val argName: String = "CHAR", override val usage: String = "", override val required: Boolean = false, override val hidden: Boolean = false)
  160. extends CmdLineOpt(shortName, longName, argName, usage, required, hidden) {
  161. def getValue: String = { getter().toString }
  162. def setValue(value: String) { setter(value(0)) }
  163. }
  164. case class StringOpt(val getter: Function0[String], val setter: (String => Unit), override val shortName: Char, override val longName: String, override val argName: String = "STRING", override val usage: String = "", override val required: Boolean = false, override val hidden: Boolean = false)
  165. extends CmdLineOpt(shortName, longName, argName, usage, required, hidden) {
  166. def getValue: String = { getter() }
  167. def setValue(value: String) { setter(value) }
  168. }
  169. case class StringListOpt(val getter: Function0[Seq[String]], val setter: (Seq[String] => Unit), override val shortName: Char, override val longName: String, override val argName: String = "STRING", override val usage: String = "", override val required: Boolean = false, override val hidden: Boolean = false)
  170. extends CmdLineOpt(shortName, longName, argName, usage, required, hidden) {
  171. def getValue: String = { getter().mkString(File.pathSeparator) }
  172. def setValue(value: String) { setter(value.split(File.pathSeparator)) }
  173. }
  174. case class FileOpt(val getter: Function0[File], val setter: (File => Unit), override val shortName: Char, override val longName: String, override val argName: String = "FILE", override val usage: String = "", override val required: Boolean = false, override val hidden: Boolean = false)
  175. extends CmdLineOpt(shortName, longName, argName, usage, required, hidden) {
  176. def getValue: String = {
  177. getter() match {
  178. case null => ""
  179. case f => f.toString
  180. }
  181. }
  182. def setValue(value: String) { if (value != null && !value.isEmpty) setter(new File(value)) }
  183. }
  184. case class PathListOpt(val getter: Function0[List[File]], val setter: (Seq[File] => Unit), override val shortName: Char, override val longName: String, override val argName: String = "PATH", override val usage: String = "", override val required: Boolean = false, override val hidden: Boolean = false)
  185. extends CmdLineOpt(shortName, longName, argName, usage, required, hidden) {
  186. def getValue: String = { getter().map(_.toString).mkString(File.pathSeparator) }
  187. def setValue(value: String) { setter(value.split(File.pathSeparator).map(new File(_))) }
  188. }
  189. ////////
  190. // Parse method
  191. ////////
  192. @throws(classOf[PrintVersionAndMessageException])
  193. @throws(classOf[CmdLineUsageException])
  194. def parseCmdLine(args: Seq[String]) {
  195. // Parse command line arguments per POSIX XBD §12.1 and §12.2 and GNU libc §25.1.1
  196. // Note: We don't attempt the GNU libc "abbreviate options to unique prefix" nonsense
  197. var maxReqOprdIndex = -1
  198. for ((_, oprd) <- recognizedOprds) {
  199. val position = oprd.position
  200. if (position > 0 && !recognizedOprds.contains(position - 1)) throw new MissingCmdLineOprdError(position)
  201. if (position > 0 && oprd.required && !recognizedOprds(position - 1).required) throw new InvalidRequiredCmdLineOprdError(position)
  202. if (oprd.required) maxReqOprdIndex = maxReqOprdIndex max position
  203. }
  204. def before(str: String, sep: String) = { val i = str.indexOf(sep); if (i >= 0) str.take(i) else str }
  205. def after(str: String, sep: String) = { val i = str.indexOf(sep); if (i >= 0) str.drop(i + 1) else null }
  206. var currOprdIndex = 0
  207. var endOptions = false
  208. var gobbleNext: CmdLineOpt = null
  209. for (arg <- args) {
  210. if (gobbleNext != null) { gobbleNext.setValue(arg); gobbleNext = null }
  211. else if (endOptions) setCurrOprd(arg)
  212. else if (arg.equals("--")) endOptions = true
  213. else if (arg.startsWith("--")) setLongOpt(arg.drop(2)) //GNU-style long name (lower case, hyphenated) option
  214. else if (arg.startsWith("-") && arg.length == 2) setShortOpt(arg(1), null) //POSIX-style short name (one alphanum char) option
  215. else if (!arg.startsWith("-")) setCurrOprd(arg)
  216. else { // Ambiguous: "-abc" could be POSIX "-a -b -c" or "-a bc" or GNU "--abc"
  217. if (recognizedShortOpts.contains(arg(1))) {
  218. if (recognizedShortOpts(arg(1)).isInstanceOf[UnitOpt]) {
  219. // "-abc" treated as "-a -b -c"
  220. arg.drop(1).map(setShortOpt(_, null))
  221. } else {
  222. // "-abc" treated as "-a bc"
  223. setShortOpt(arg(1), arg.drop(2))
  224. }
  225. } else if (recognizedLongOpts.contains(before(arg, "=").drop(1))) {
  226. // "-abc" treated as "--abc"
  227. setLongOpt(arg.drop(1))
  228. } else throw new UnrecognizedCmdLineOptException(arg.drop(1), this)
  229. }
  230. }
  231. if (currOprdIndex <= maxReqOprdIndex) throw new MissingCmdLineOprdsException(recognizedOprds(currOprdIndex).argName, this)
  232. //TODO: Check all req'd opts are present
  233. def setCurrOprd(arg: String) = {
  234. try {
  235. recognizedOprds(currOprdIndex).setValue(arg)
  236. currOprdIndex += 1
  237. } catch {
  238. case _: NoSuchElementException => throw new ExtraneousCmdLineOprdsException(arg, this)
  239. }
  240. }
  241. def setLongOpt(arg: String) = {
  242. try {
  243. val currOpt = recognizedLongOpts(before(arg, "="))
  244. if (!currOpt.isInstanceOf[UnitOpt] && !arg.contains("=")) gobbleNext = currOpt
  245. else if (currOpt.isInstanceOf[UnitOpt] && arg.contains("=")) throw new ExtraneousCmdLineOptArgException(currOpt.longName, after(arg, "="), this)
  246. else currOpt.setValue(after(arg, "="))
  247. } catch {
  248. case _: NoSuchElementException => throw new UnrecognizedCmdLineOptException(before(arg, "="), this)
  249. }
  250. }
  251. def setShortOpt(optName: Char, value: String) = {
  252. try {
  253. val currOpt = recognizedShortOpts(optName)
  254. if (!currOpt.isInstanceOf[UnitOpt] && value == null) gobbleNext = currOpt
  255. else if (currOpt.isInstanceOf[UnitOpt] && value != null) throw new ExtraneousCmdLineOptArgException(optName.toString, value, this)
  256. else currOpt.setValue(value)
  257. } catch {
  258. case _: NoSuchElementException => throw new UnrecognizedCmdLineOptException(optName.toString, this)
  259. }
  260. }
  261. }
  262. ////////
  263. // Parse method
  264. ////////
  265. def composeCmdLine(): Array[String] = {
  266. ((recognizedOpts.toList.sortBy(o => (o.longName, o.shortName)).flatMap({ opt: CmdLineOpt =>
  267. if (!opt.isInstanceOf[UnitOpt]) {
  268. if (opt.shortName != 0 && opt.shortName != ' ') List("-" + opt.shortName, opt.getValue) else List("--" + opt.longName + "=" + opt.getValue)
  269. } else {
  270. if (opt.asInstanceOf[UnitOpt].getter()) {
  271. if (opt.shortName != 0 && opt.shortName != ' ') List("-" + opt.shortName) else List("--" + opt.longName)
  272. } else {
  273. Nil
  274. }
  275. }
  276. })) ++
  277. (for { i <- 0 until recognizedOprds.size } yield recognizedOprds(i).getValue).toList).toArray
  278. }
  279. }
  280. ////////
  281. // Error for mal-formed operand and option declarations
  282. ////////
  283. class MissingCmdLineOprdError(operandIndex: Int) extends Error("Command line operand number " + operandIndex + " not defined, but operand " + (operandIndex + 1) + " is")
  284. class InvalidRequiredCmdLineOprdError(operandIndex: Int) extends Error("Command line operand number " + operandIndex + " marked required, but operand " + (operandIndex - 1) + " is not")
  285. class MultiplyDefinedCmdLineOprndError(operandIndex: Int) extends Error("Command line operand number " + operandIndex + " multiply defined")
  286. class MultiplyDefinedCmdLineOptError(optName: String) extends Error("Command line option \"" + optName + "\" multiply defined")
  287. ////////
  288. // Exceptions for command args that didn't parse
  289. ////////
  290. abstract class CmdLineUsageException(msg: String, p: CmdLineParser) extends IllegalArgumentException(msg + "\n" + p.usageString + "\nTry the --help option for more information.")
  291. class ExtraneousCmdLineOprdsException(val operand: String, p: CmdLineParser) extends CmdLineUsageException("extra operand -- '" + operand + "'", p)
  292. class MissingCmdLineOprdsException(val argName: String, p: CmdLineParser) extends CmdLineUsageException("missing " + argName + " operand", p)
  293. class UnrecognizedCmdLineOptException(val optName: String, p: CmdLineParser) extends CmdLineUsageException("invalid option -- " + optName, p)
  294. class MissingCmdLineOptException(val optName: String, p: CmdLineParser) extends CmdLineUsageException("missing option -- " + optName, p)
  295. class ExtraneousCmdLineOptArgException(val optName: String, optArg: String, p: CmdLineParser) extends CmdLineUsageException("option " + optName + " doesn't allow an argument", p)
  296. class MissingCmdLineOptArgException(val optName: String, p: CmdLineParser) extends CmdLineUsageException("option requires an argument -- " + optName, p)
  297. ////////
  298. // Exception for command args that requests immediate termination of program with usage/help/version info.
  299. ////////
  300. class PrintVersionAndMessageException(msg: String) extends Exception(msg)