วิธีที่ดีที่สุดในการแยกพารามิเตอร์บรรทัดคำสั่ง? [ปิด]


237

วิธีที่ดีที่สุดในการวิเคราะห์พารามิเตอร์บรรทัดคำสั่งใน Scala คืออะไร โดยส่วนตัวแล้วฉันชอบบางสิ่งที่มีน้ำหนักเบาที่ไม่ต้องการขวดภายนอก

ที่เกี่ยวข้อง:

คำตอบ:


228

สำหรับกรณีส่วนใหญ่คุณไม่จำเป็นต้องใช้เครื่องมือแยกวิเคราะห์ภายนอก การจับคู่รูปแบบของ Scala ช่วยให้การบริโภค args ในสไตล์การทำงาน ตัวอย่างเช่น:

object MmlAlnApp {
  val usage = """
    Usage: mmlaln [--min-size num] [--max-size num] filename
  """
  def main(args: Array[String]) {
    if (args.length == 0) println(usage)
    val arglist = args.toList
    type OptionMap = Map[Symbol, Any]

    def nextOption(map : OptionMap, list: List[String]) : OptionMap = {
      def isSwitch(s : String) = (s(0) == '-')
      list match {
        case Nil => map
        case "--max-size" :: value :: tail =>
                               nextOption(map ++ Map('maxsize -> value.toInt), tail)
        case "--min-size" :: value :: tail =>
                               nextOption(map ++ Map('minsize -> value.toInt), tail)
        case string :: opt2 :: tail if isSwitch(opt2) => 
                               nextOption(map ++ Map('infile -> string), list.tail)
        case string :: Nil =>  nextOption(map ++ Map('infile -> string), list.tail)
        case option :: tail => println("Unknown option "+option) 
                               exit(1) 
      }
    }
    val options = nextOption(Map(),arglist)
    println(options)
  }
}

จะพิมพ์เช่น:

Map('infile -> test/data/paml-aln1.phy, 'maxsize -> 4, 'minsize -> 2)

รุ่นนี้ใช้เวลาเพียงหนึ่ง infile ง่ายต่อการปรับปรุง (โดยใช้รายการ)

โปรดทราบด้วยว่าวิธีการนี้ช่วยให้สามารถทำการเชื่อมโยงอาร์กิวเมนต์บรรทัดคำสั่งหลายชุดเข้าด้วยกันได้มากกว่าสองข้อ!


4
isSwitch เพียงตรวจสอบอักขระตัวแรกว่าเป็น '-'
pjotrp

6
nextOptionไม่ใช่ชื่อที่ดีสำหรับฟังก์ชั่น มันเป็นฟังก์ชั่นที่ส่งคืนแผนที่ - ความจริงที่ว่ามันวนซ้ำเป็นรายละเอียดการนำไปปฏิบัติ มันเหมือนกับการเขียนmaxฟังก์ชั่นสำหรับคอลเลกชันและเรียกมันnextMaxง่ายๆเพราะคุณเขียนมันด้วยการเรียกซ้ำแบบชัดแจ้ง ทำไมไม่เรียกมันว่าoptionMapอะไร?
มันบรูซ

4
@itsbruce ผมแค่อยากจะเพิ่ม / แก้ไขจุดของคุณ - มันจะเป็นมากที่สุด "เหมาะสม" จากการอ่าน / การบำรุงรักษาเพื่อกำหนดlistToOptionMap(lst:List[String])ด้วยฟังก์ชั่นที่กำหนดไว้ในที่ที่มีบรรทัดสุดท้ายบอกว่าnextOption return nextOption(Map(), lst)ที่กล่าวว่าฉันต้องสารภาพว่าฉันได้ทำทางลัดอย่างยิ่งใหญ่ในเวลาของฉันมากกว่าหนึ่งในคำตอบนี้
tresbot

6
@theMadKing ในโค้ดด้านบนexit(1)อาจต้องsys.exit(1)
tresbot

3
ฉันชอบทางออกของคุณ นี่คือการปรับเปลี่ยนในการจัดการหลายพารามิเตอร์:file case string :: tail => { if (isSwitch(string)) { println("Unknown option: " + string) sys.exit(1) } else nextOption(map ++ Map('files -> (string :: map('files).asInstanceOf[List[String]])), tail)แผนที่ยังต้องการค่าเริ่มต้นคือNil val options = nextOption(Map() withDefaultValue Nil, args.toList)สิ่งที่ฉันไม่ชอบที่มีการหันไปasInstanceOfเนื่องจากค่าความเป็นอยู่ของชนิดOptionMap Anyมีวิธีแก้ปัญหาที่ดีกว่านี้ไหม?
Mauro Lacy

196

scopt / scopt

val parser = new scopt.OptionParser[Config]("scopt") {
  head("scopt", "3.x")

  opt[Int]('f', "foo") action { (x, c) =>
    c.copy(foo = x) } text("foo is an integer property")

  opt[File]('o', "out") required() valueName("<file>") action { (x, c) =>
    c.copy(out = x) } text("out is a required file property")

  opt[(String, Int)]("max") action { case ((k, v), c) =>
    c.copy(libName = k, maxCount = v) } validate { x =>
    if (x._2 > 0) success
    else failure("Value <max> must be >0") 
  } keyValueName("<libname>", "<max>") text("maximum count for <libname>")

  opt[Unit]("verbose") action { (_, c) =>
    c.copy(verbose = true) } text("verbose is a flag")

  note("some notes.\n")

  help("help") text("prints this usage text")

  arg[File]("<file>...") unbounded() optional() action { (x, c) =>
    c.copy(files = c.files :+ x) } text("optional unbounded args")

  cmd("update") action { (_, c) =>
    c.copy(mode = "update") } text("update is a command.") children(
    opt[Unit]("not-keepalive") abbr("nk") action { (_, c) =>
      c.copy(keepalive = false) } text("disable keepalive"),
    opt[Boolean]("xyz") action { (x, c) =>
      c.copy(xyz = x) } text("xyz is a boolean property")
  )
}
// parser.parse returns Option[C]
parser.parse(args, Config()) map { config =>
  // do stuff
} getOrElse {
  // arguments are bad, usage message will have been displayed
}

ด้านบนสร้างข้อความการใช้งานต่อไปนี้:

scopt 3.x
Usage: scopt [update] [options] [<file>...]

  -f <value> | --foo <value>
        foo is an integer property
  -o <file> | --out <file>
        out is a required file property
  --max:<libname>=<max>
        maximum count for <libname>
  --verbose
        verbose is a flag
some notes.

  --help
        prints this usage text
  <file>...
        optional unbounded args

Command: update
update is a command.

  -nk | --not-keepalive
        disable keepalive    
  --xyz <value>
        xyz is a boolean property

นี่คือสิ่งที่ฉันใช้อยู่ในปัจจุบัน ทำความสะอาดการใช้งานโดยไม่ต้องบรรทุกสัมภาระมากเกินไป (ข้อจำกัดความรับผิดชอบ: ตอนนี้ฉันรักษาโครงการนี้ไว้)


6
ฉันชอบตัวสร้างรูปแบบ DSL ที่ดีกว่ามากเพราะมันช่วยให้สามารถมอบหมายพารามิเตอร์การสร้างโมดูลได้
Daniel C. Sobral

3
หมายเหตุ: ไม่เหมือนกับที่แสดง scopt ไม่จำเป็นต้องมีคำอธิบายประกอบหลายประเภท
Blaisorblade

9
หากคุณใช้วิธีนี้ในการแยกวิเคราะห์เพื่อหางาน Spark โปรดเตือนว่าพวกเขาไม่ได้เล่นด้วยกันอย่างดี ไม่มีอะไรที่ฉันพยายามจะได้รับ spark-submit เพื่อทำงานกับ scopt :-(
jbrown

4
@BirdJaguarIV หากจุดประกายใช้ scopt ซึ่งอาจเป็นปัญหา - เวอร์ชันที่ขัดแย้งกันใน jar หรือบางอย่าง ฉันใช้หอยเชลล์กับงาน spark แทนและไม่มีปัญหาใด ๆ
jbrown

12
แม้ว่าห้องสมุดนี้จะสร้างเอกสาร CLI ที่ดีโดยอัตโนมัติ แต่โค้ดนั้นดูดีกว่า brainf * ck เล็กน้อย
Jonathan Neufeld

58

ฉันรู้ว่าคำถามถูกถามเมื่อไม่นานมานี้ แต่ฉันคิดว่ามันอาจช่วยให้บางคนที่กำลัง googling (เช่นฉัน) และกดหน้านี้

หอยเชลล์ดูค่อนข้างมีแนวโน้มเช่นกัน

คุณสมบัติ (อ้างจากหน้า GitHub ที่เชื่อมโยง):

  • ตัวเลือกการตั้งค่าสถานะค่าเดียวและหลายค่า
  • ชื่อตัวเลือกแบบสั้นสไตล์ POSIX (-a) พร้อมการจัดกลุ่ม (-abc)
  • ชื่อตัวเลือกแบบยาวของ GNU (--opt)
  • อาร์กิวเมนต์ของคุณสมบัติ (-Dkey = value, -D key1 = value key2 = value)
  • ประเภทของตัวเลือกและค่าคุณสมบัติที่ไม่ใช่สตริง (พร้อมตัวแปลงที่ขยายได้)
  • การจับคู่ที่มีประสิทธิภาพในส่วนต่อท้าย
  • subcommands

และโค้ดตัวอย่าง (จากหน้า Github นั้น):

import org.rogach.scallop._;

object Conf extends ScallopConf(List("-c","3","-E","fruit=apple","7.2")) {
  // all options that are applicable to builder (like description, default, etc) 
  // are applicable here as well
  val count:ScallopOption[Int] = opt[Int]("count", descr = "count the trees", required = true)
                .map(1+) // also here work all standard Option methods -
                         // evaluation is deferred to after option construction
  val properties = props[String]('E')
  // types (:ScallopOption[Double]) can be omitted, here just for clarity
  val size:ScallopOption[Double] = trailArg[Double](required = false)
}


// that's it. Completely type-safe and convenient.
Conf.count() should equal (4)
Conf.properties("fruit") should equal (Some("apple"))
Conf.size.get should equal (Some(7.2))
// passing into other functions
def someInternalFunc(conf:Conf.type) {
  conf.count() should equal (4)
}
someInternalFunc(Conf)

4
Scallop pwns ส่วนที่เหลือลงมือในแง่ของคุณสมบัติ
อัปเดต

ฉันเห็นด้วย. ทิ้งความคิดเห็นไว้ที่นี่เพียงใส่ @Eugene Yokota ที่ไม่ได้บันทึกไว้ ตรวจสอบบล็อกนี้จากหอยเชลล์
ปราโมทย์

1
ปัญหามัน menthions กับ scopt คือ "มันดูดี แต่ไม่สามารถแยกตัวเลือกซึ่งใช้รายการของการขัดแย้ง (เช่น -a 1 2 3) และคุณไม่มีวิธีที่จะขยายมันเพื่อรับรายการเหล่านั้น lib)." แต่นี้ไม่จริงดูgithub.com/scopt/scopt#options
Alexey Romanov

2
มันใช้งานง่ายกว่าและสร้างสำเร็จรูปน้อยกว่า scopt ไม่รวม(x, c) => c.copy(xyz = x) อยู่ใน scopt
WeiChing 林煒清

43

ฉันชอบการเลื่อนผ่านข้อโต้แย้งสำหรับการกำหนดค่าที่ค่อนข้างง่าย

var name = ""
var port = 0
var ip = ""
args.sliding(2, 2).toList.collect {
  case Array("--ip", argIP: String) => ip = argIP
  case Array("--port", argPort: String) => port = argPort.toInt
  case Array("--name", argName: String) => name = argName
}

2
ฉลาด. ใช้ได้เฉพาะในกรณีที่ ARG ทุกตัวระบุค่าใช่มั้ย
Brent Faust

2
ไม่ควรargs.sliding(2, 2)หรือ
m01

1
ไม่ควรvar port = 0หรือ
swdev

17

ชุดเครื่องมือสำหรับส่วนต่อประสานบรรทัดคำสั่ง (CLIST)

นี่เป็นของฉันด้วย! (แม้ว่าจะค่อนข้างช้าในเกม)

https://github.com/backuity/clist

ตรงข้ามกับscoptมันคือการเปลี่ยนแปลงอย่างสิ้นเชิง ... แต่รอ! นั่นทำให้เรามีไวยากรณ์ที่ดีงาม:

class Cat extends Command(description = "concatenate files and print on the standard output") {

  // type-safety: members are typed! so showAll is a Boolean
  var showAll        = opt[Boolean](abbrev = "A", description = "equivalent to -vET")
  var numberNonblank = opt[Boolean](abbrev = "b", description = "number nonempty output lines, overrides -n")

  // files is a Seq[File]
  var files          = args[Seq[File]](description = "files to concat")
}

และวิธีง่ายๆในการรัน:

Cli.parse(args).withCommand(new Cat) { case cat =>
    println(cat.files)
}

คุณสามารถทำสิ่งต่างๆได้มากขึ้น (คำสั่งหลายตัวเลือกการกำหนดค่ามากมาย ... ) และไม่มีการพึ่งพา

ฉันจะจบด้วยคุณสมบัติที่โดดเด่นชนิดการใช้งานเริ่มต้น (มักจะถูกละเลยสำหรับคำสั่งหลายคำสั่ง): Clist


มันมีการตรวจสอบ?
KF

ใช่แล้ว (ดูgithub.com/backuity/clist/blob/master/demo/src/main/scala/…สำหรับตัวอย่าง) มันไม่ได้บันทึกไว้ว่า ... PR? :)
Bruno Bieth

พยายามมันค่อนข้างสะดวก ฉันใช้ scopt ก่อนหน้านี้ฉันยังไม่คุ้นเคยกับการเพิ่มการตรวจสอบความถูกต้อง แต่ไม่เพียง แต่ในนิยามของพารามิเตอร์แต่ละตัว แต่มันใช้ได้ดีกับฉัน และการกำหนดพารามิเตอร์และการตรวจสอบที่แตกต่างกันในลักษณะที่แตกต่างกันจากนั้นรวมเข้าด้วยกันในหลาย ๆ กรณีนั่นเป็นประโยชน์จริง ๆ ฉันได้รับความเดือดร้อนเป็นจำนวนมากเมื่อไม่สะดวกที่จะใช้พารามิเตอร์ซ้ำ ขอบคุณสำหรับการตอบ!
KF

การตรวจสอบส่วนใหญ่จะดำเนินการในช่วง deserialization ของพารามิเตอร์บรรทัดคำสั่ง (ดูอ่าน ) ดังนั้นถ้าคุณสามารถกำหนดข้อ จำกัด ของการตรวจสอบของคุณผ่านประเภท (เช่นPassword, Hex, ... ) แล้วคุณสามารถใช้ประโยชน์จากนี้
Bruno Bieth

13

นี้เป็นส่วนใหญ่เป็นโคลนไร้ยางอายของคำตอบของฉันไปที่คำถามของ Java หัวข้อเดียวกัน ปรากฎว่า JewelCLI เป็นมิตรกับผู้ใช้โดยไม่ต้องใช้วิธีการสไตล์ JavaBean เพื่อรับการตั้งชื่ออาร์กิวเมนต์อัตโนมัติ

JewelCLI เป็นห้องสมุด Scala ง่าย Java สำหรับบรรทัดคำสั่งแยกที่อัตราผลตอบแทนรหัสสะอาด มันใช้อินเทอร์เฟซ Proxied ที่กำหนดค่าด้วยคำอธิบายประกอบเพื่อสร้าง API แบบปลอดภัยสำหรับพารามิเตอร์บรรทัดคำสั่งของคุณแบบไดนามิก

ตัวอย่างอินเตอร์เฟสพารามิเตอร์Person.scala:

import uk.co.flamingpenguin.jewel.cli.Option

trait Person {
  @Option def name: String
  @Option def times: Int
}

ตัวอย่างการใช้อินเตอร์เฟสพารามิเตอร์Hello.scala:

import uk.co.flamingpenguin.jewel.cli.CliFactory.parseArguments
import uk.co.flamingpenguin.jewel.cli.ArgumentValidationException

object Hello {
  def main(args: Array[String]) {
    try {
      val person = parseArguments(classOf[Person], args:_*)
      for (i <- 1 to (person times))
        println("Hello " + (person name))
    } catch {
      case e: ArgumentValidationException => println(e getMessage)
    }
  }
}

บันทึกสำเนาของไฟล์ด้านบนลงในไดเรกทอรีเดียวและดาวน์โหลดJewelCLI 0.6 JARไปยังไดเรกทอรีนั้นด้วย

คอมไพล์และรันตัวอย่างใน Bash บน Linux / Mac OS X / etc:

scalac -cp jewelcli-0.6.jar:. Person.scala Hello.scala
scala -cp jewelcli-0.6.jar:. Hello --name="John Doe" --times=3

รวบรวมและเรียกใช้ตัวอย่างในพรอมต์คำสั่งของ Windows:

scalac -cp jewelcli-0.6.jar;. Person.scala Hello.scala
scala -cp jewelcli-0.6.jar;. Hello --name="John Doe" --times=3

การรันตัวอย่างควรให้ผลลัพธ์ต่อไปนี้:

Hello John Doe
Hello John Doe
Hello John Doe

ความสนุกอย่างหนึ่งที่คุณอาจสังเกตเห็นคือ (args: _ *) การเรียกวิธีการ Java varargs จาก Scala ต้องใช้สิ่งนี้ นี่เป็นวิธีการแก้ปัญหาที่ฉันเรียนรู้จากdaily-scala.blogspot.com/2009/11/varargs.htmlในบล็อก Daily Scala ที่ยอดเยี่ยมของ Jesse Eichar ฉันขอแนะนำ Daily Scala :)
Alain O'Dea

12

วิธีแยกพารามิเตอร์โดยไม่ต้องพึ่งพาภายนอก เป็นคำถามที่ดีมาก! คุณอาจจะสนใจในpicocli

Picocli ถูกออกแบบมาเพื่อแก้ปัญหาที่ถามในคำถาม: มันเป็นบรรทัดคำสั่งแยกกรอบในไฟล์เดียวเพื่อให้คุณสามารถรวมไว้ในรูปแบบแหล่งที่มา นี้จะช่วยให้ผู้ใช้เรียกใช้โปรแกรม picocli ตามโดยไม่ต้อง picocli เป็นพึ่งพาภายนอก

มันทำงานได้โดยการเพิ่มความคิดเห็นฟิลด์เพื่อให้คุณเขียนรหัสน้อยมาก สรุปด่วน:

  • พิมพ์ทุกอย่างมาก - ตัวเลือกบรรทัดคำสั่งรวมถึงพารามิเตอร์ตำแหน่ง
  • รองรับตัวเลือกสั้น ๆ แบบคลัสเตอร์ POSIX (เพื่อให้จัดการได้<command> -xvfInputFileเช่นกัน<command> -x -v -f InputFile)
  • แบบจำลอง arity ที่อนุญาตให้มีพารามิเตอร์ต่ำสุด, สูงสุดและตัวแปรเช่น"1..*",,"3..5"
  • API ที่คล่องแคล่วและกะทัดรัดเพื่อลดรหัสลูกค้าสำเร็จรูป
  • subcommands
  • ความช่วยเหลือในการใช้งานด้วยสี ANSI

ข้อความช่วยเหลือการใช้งานนั้นง่ายต่อการปรับแต่งด้วยหมายเหตุประกอบ (ไม่มีการตั้งโปรแกรม) ตัวอย่างเช่น:

ข้อความช่วยเหลือการใช้งานเพิ่มเติม(ที่มา )

ฉันไม่สามารถต้านทานการเพิ่มภาพหน้าจออีกหนึ่งภาพเพื่อแสดงว่ามีข้อความช่วยเหลือการใช้งานประเภทใดที่เป็นไปได้ ความช่วยเหลือในการใช้คือหน้าใบสมัครของคุณดังนั้นจงสร้างสรรค์และสนุก!

การสาธิต picocli

คำเตือน: ฉันสร้าง picocli ข้อเสนอแนะหรือคำถามยินดีต้อนรับมาก มันเขียนด้วยภาษาจาวา แต่แจ้งให้เราทราบหากมีปัญหาในการใช้งานใน scala และฉันจะพยายามแก้ไข


1
ทำไมต้องลงคะแนน? นี้เป็นห้องสมุดเท่านั้นฉันรู้ที่เฉพาะที่ออกแบบมาเพื่อแก้ไขปัญหาที่กล่าวถึงใน OP: วิธีการหลีกเลี่ยงการเพิ่มการพึ่งพา
Remko Popma

"สนับสนุนให้ผู้เขียนแอปพลิเคชันรวมไว้" การทำงานที่ดี.
keos

คุณมีตัวอย่างเรื่องสกาล่าหรือไม่?
CruncherBigData

1
ฉันเริ่มสร้างตัวอย่างสำหรับภาษา JVM อื่น ๆ : github.com/remkop/picocli/issues/183ข้อเสนอแนะและยินดีต้อนรับยินดีต้อนรับ!
Remko Popma

11

ฉันมาจากโลกของ Java ฉันชอบargs4jเพราะง่ายข้อมูลจำเพาะสามารถอ่านได้มากขึ้น (ขอบคุณคำอธิบายประกอบ) และสร้างผลลัพธ์ที่จัดรูปแบบได้ดี

นี่คือตัวอย่างข้อมูลของฉัน:

สเปค

import org.kohsuke.args4j.{CmdLineException, CmdLineParser, Option}

object CliArgs {

  @Option(name = "-list", required = true,
    usage = "List of Nutch Segment(s) Part(s)")
  var pathsList: String = null

  @Option(name = "-workdir", required = true,
    usage = "Work directory.")
  var workDir: String = null

  @Option(name = "-master",
    usage = "Spark master url")
  var masterUrl: String = "local[2]"

}

การแยกวิเคราะห์

//var args = "-listt in.txt -workdir out-2".split(" ")
val parser = new CmdLineParser(CliArgs)
try {
  parser.parseArgument(args.toList.asJava)
} catch {
  case e: CmdLineException =>
    print(s"Error:${e.getMessage}\n Usage:\n")
    parser.printUsage(System.out)
    System.exit(1)
}
println("workDir  :" + CliArgs.workDir)
println("listFile :" + CliArgs.pathsList)
println("master   :" + CliArgs.masterUrl)

บนอาร์กิวเมนต์ที่ไม่ถูกต้อง

Error:Option "-list" is required
 Usage:
 -list VAL    : List of Nutch Segment(s) Part(s)
 -master VAL  : Spark master url (default: local[2])
 -workdir VAL : Work directory.

10

สกาล่า-optparse-applicative

ฉันคิดว่า scala-optparse-applicative เป็นตัวแยกวิเคราะห์บรรทัดคำสั่งที่ใช้งานได้ดีที่สุดใน Scala

https://github.com/bmjames/scala-optparse-applicative


มันมีตัวอย่าง / เอกสารนอกเหนือจากสิ่งที่อยู่ใน README หรือไม่
Erik Kaplun

1
ตรวจสอบexamplesในรหัสทดสอบ
gpampara

8

นอกจากนี้ยังมีJCommander (ข้อจำกัดความรับผิดชอบ: ฉันสร้างมันขึ้นมา):

object Main {
  object Args {
    @Parameter(
      names = Array("-f", "--file"),
      description = "File to load. Can be specified multiple times.")
    var file: java.util.List[String] = null
  }

  def main(args: Array[String]): Unit = {
    new JCommander(Args, args.toArray: _*)
    for (filename <- Args.file) {
      val f = new File(filename)
      printf("file: %s\n", f.getName)
    }
  }
}

2
ฉันชอบอันนี้ parsers 'บริสุทธิ์สกาล่า' เหล่านั้นไม่มีไวยากรณ์ที่สะอาด
tactoth

@tothoth ตรวจสอบสิ่งนี้มันมีรูปแบบที่ชัดเจน: stackoverflow.com/questions/2315912/…
Bruno Bieth

6

ฉันชอบวิธีสไลด์ () ของ joslinm ไม่ใช่ vars ที่เปลี่ยนแปลงไม่ได้;) ดังนั้นนี่เป็นวิธีที่ไม่เปลี่ยนรูปแบบของวิธีการนั้น:

case class AppArgs(
              seed1: String,
              seed2: String,
              ip: String,
              port: Int
              )
object AppArgs {
  def empty = new AppArgs("", "", "", 0)
}

val args = Array[String](
  "--seed1", "akka.tcp://seed1",
  "--seed2", "akka.tcp://seed2",
  "--nodeip", "192.167.1.1",
  "--nodeport", "2551"
)

val argsInstance = args.sliding(2, 1).toList.foldLeft(AppArgs.empty) { case (accumArgs, currArgs) => currArgs match {
    case Array("--seed1", seed1) => accumArgs.copy(seed1 = seed1)
    case Array("--seed2", seed2) => accumArgs.copy(seed2 = seed2)
    case Array("--nodeip", ip) => accumArgs.copy(ip = ip)
    case Array("--nodeport", port) => accumArgs.copy(port = port.toInt)
    case unknownArg => accumArgs // Do whatever you want for this case
  }
}


3

ฉันได้ลองวิธีการแก้ปัญหาของ @pjotrp โดยใช้รายการสัญลักษณ์คีย์ตำแหน่งที่ต้องการแผนที่ของธง -> สัญลักษณ์คีย์และตัวเลือกเริ่มต้น:

def parseOptions(args: List[String], required: List[Symbol], optional: Map[String, Symbol], options: Map[Symbol, String]): Map[Symbol, String] = {
  args match {
    // Empty list
    case Nil => 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))

    // Exit if an unknown argument is received
    case _ =>
      printf("unknown argument(s): %s\n", args.mkString(", "))
      sys.exit(1)
  }
}

def main(sysargs Array[String]) {
  // Required positional arguments by key in options
  val required = List('arg1, 'arg2)

  // Optional arguments by flag which map to a key in options
  val optional = Map("--flag1" -> 'flag1, "--flag2" -> 'flag2)

  // Default options that are passed in
  var defaultOptions = Map()

  // Parse options based on the command line args
  val options = parseOptions(sysargs.toList, required, optional, defaultOptions)
}

ฉันอัปเดตโค้ดชิ้นนี้เพื่อจัดการกับค่าสถานะ (ไม่ใช่แค่ตัวเลือกที่มีค่า) และยังเพื่อกำหนดตัวเลือก / ตั้งค่าสถานะด้วยแบบสั้นและยาว -f|--flagsเช่น ลองดูที่gist.github.com/DavidGamba/b3287d40b019e498982cและอย่าลังเลที่จะอัพเดทคำตอบหากคุณชอบ ฉันอาจจะทำให้ทุกแผนที่และตัวเลือกเพื่อให้คุณสามารถผ่านสิ่งที่คุณต้องการด้วยอาร์กิวเมนต์ที่มีชื่อเท่านั้น
DavidG

3

ฉันใช้วิธีตอบคำถามยอดนิยม (จาก dave4420) และพยายามปรับปรุงโดยทำให้เป็นวัตถุประสงค์ทั่วไปมากขึ้น

มันคืนMap[String,String]ค่าพารามิเตอร์บรรทัดคำสั่งทั้งหมดคุณสามารถสอบถามพารามิเตอร์ที่คุณต้องการ (เช่นใช้.contains) หรือแปลงค่าเป็นประเภทที่คุณต้องการ (เช่นใช้toInt)

def argsToOptionMap(args:Array[String]):Map[String,String]= {
  def nextOption(
      argList:List[String], 
      map:Map[String, String]
    ) : Map[String, String] = {
    val pattern       = "--(\\w+)".r // Selects Arg from --Arg
    val patternSwitch = "-(\\w+)".r  // Selects Arg from -Arg
    argList match {
      case Nil => map
      case pattern(opt)       :: value  :: tail => nextOption( tail, map ++ Map(opt->value) )
      case patternSwitch(opt) :: tail => nextOption( tail, map ++ Map(opt->null) )
      case string             :: Nil  => map ++ Map(string->null)
      case option             :: tail => {
        println("Unknown option:"+option) 
        sys.exit(1)
      }
    }
  }
  nextOption(args.toList,Map())
}

ตัวอย่าง:

val args=Array("--testing1","testing1","-a","-b","--c","d","test2")
argsToOptionMap( args  )

ให้:

res0: Map[String,String] = Map(testing1 -> testing1, a -> null, b -> null, c -> d, test2 -> null)


2

นี่คือตัวแยกวิเคราะห์บรรทัดคำสั่ง scalaที่ใช้งานง่าย มันจะจัดรูปแบบข้อความช่วยเหลือโดยอัตโนมัติและแปลงอาร์กิวเมนต์เป็นประเภทที่คุณต้องการ รองรับ POSIX สั้นและสวิตช์แบบยาวของ GNU สนับสนุนสวิตช์ที่มีอาร์กิวเมนต์ที่ต้องการอาร์กิวเมนต์ที่เลือกได้และอาร์กิวเมนต์ค่าหลายค่า คุณสามารถระบุรายการที่แน่นอนของค่าที่ยอมรับได้สำหรับสวิตช์เฉพาะ ชื่อสวิตช์แบบยาวสามารถย่อบนบรรทัดคำสั่งเพื่อความสะดวก คล้ายกับตัวแยกวิเคราะห์ตัวเลือกในไลบรารีมาตรฐานทับทิม


2

ฉันไม่เคยชอบทับทิมเช่นตัวแยกวิเคราะห์ตัวเลือก นักพัฒนาส่วนใหญ่ที่ใช้พวกเขาไม่เคยเขียนman pageที่เหมาะสมสำหรับสคริปต์ของพวกเขาและจบลงด้วยตัวเลือกยาว ๆ ของเพจที่ไม่ได้จัดระเบียบอย่างถูกต้องเนื่องจาก parser ของพวกเขา

ฉันชอบวิธีของ Perl ในการทำสิ่งต่าง ๆ กับ Perl's Getopt :: Longเสมอ

ฉันกำลังทำงานเกี่ยวกับการใช้งานแบบสกาล่าของมัน API เริ่มต้นมีลักษณะดังนี้:

def print_version() = () => println("version is 0.2")

def main(args: Array[String]) {
  val (options, remaining) = OptionParser.getOptions(args,
    Map(
      "-f|--flag"       -> 'flag,
      "-s|--string=s"   -> 'string,
      "-i|--int=i"      -> 'int,
      "-f|--float=f"    -> 'double,
      "-p|-procedure=p" -> { () => println("higher order function" }
      "-h=p"            -> { () => print_synopsis() }
      "--help|--man=p"  -> { () => launch_manpage() },
      "--version=p"     -> print_version,
    ))

ดังนั้นการโทรscriptแบบนี้:

$ script hello -f --string=mystring -i 7 --float 3.14 --p --version world -- --nothing

จะพิมพ์:

higher order function
version is 0.2

และการกลับมา:

remaining = Array("hello", "world", "--nothing")

options = Map('flag   -> true,
              'string -> "mystring",
              'int    -> 7,
              'double -> 3.14)

โครงการนี้จะเป็นเจ้าภาพใน GitHub สกาล่า-getoptions


2

ฉันเพิ่งสร้างการแจงนับอย่างง่ายของฉัน

val args: Array[String] = "-silent -samples 100 -silent".split(" +").toArray
                                              //> args  : Array[String] = Array(-silent, -samples, 100, -silent)
object Opts extends Enumeration {

    class OptVal extends Val {
        override def toString = "-" + super.toString
    }

    val nopar, silent = new OptVal() { // boolean options
        def apply(): Boolean = args.contains(toString)
    }

    val samples, maxgen = new OptVal() { // integer options
        def apply(default: Int) = { val i = args.indexOf(toString) ;  if (i == -1) default else args(i+1).toInt}
        def apply(): Int = apply(-1)
    }
}

Opts.nopar()                              //> res0: Boolean = false
Opts.silent()                             //> res1: Boolean = true
Opts.samples()                            //> res2: Int = 100
Opts.maxgen()                             //> res3: Int = -1

ฉันเข้าใจว่าวิธีแก้ปัญหามีข้อบกพร่องสำคัญสองประการที่อาจทำให้คุณเสียสมาธิ: ช่วยลดเสรีภาพ (เช่นการพึ่งพาห้องสมุดอื่น ๆ ที่ให้คุณค่ากับคุณมาก) และความซ้ำซ้อน (หลักการ DRY คุณพิมพ์ชื่อตัวเลือกเพียงครั้งเดียวตามโปรแกรม Scala ตัวแปรและกำจัดมันเป็นครั้งที่สองพิมพ์เป็นข้อความบรรทัดคำสั่ง)


2

ผมขอแนะนำให้ใช้http://docopt.org/ มี scala-port แต่การใช้งานจาวาhttps://github.com/docopt/docopt.javaทำงานได้ดีและดูเหมือนว่าจะได้รับการบำรุงรักษาที่ดีขึ้น นี่คือตัวอย่าง:

import org.docopt.Docopt

import scala.collection.JavaConversions._
import scala.collection.JavaConverters._

val doc =
"""
Usage: my_program [options] <input>

Options:
 --sorted   fancy sorting
""".stripMargin.trim

//def args = "--sorted test.dat".split(" ").toList
var results = new Docopt(doc).
  parse(args()).
  map {case(key, value)=>key ->value.toString}

val inputFile = new File(results("<input>"))
val sorted = results("--sorted").toBoolean

2

นี่คือสิ่งที่ฉันทำ มันส่งคืน tuple ของแผนที่และรายการ รายการใช้สำหรับอินพุตเช่นชื่อไฟล์อินพุต แผนที่ใช้สำหรับสวิตช์ / ตัวเลือก

val args = "--sw1 1 input_1 --sw2 --sw3 2 input_2 --sw4".split(" ")
val (options, inputs) = OptParser.parse(args)

จะกลับมา

options: Map[Symbol,Any] = Map('sw1 -> 1, 'sw2 -> true, 'sw3 -> 2, 'sw4 -> true)
inputs: List[Symbol] = List('input_1, 'input_2)

สวิตช์สามารถเป็น "--t" ซึ่ง x จะถูกตั้งค่าเป็นจริงหรือ "--x 10" ซึ่ง x จะถูกตั้งค่าเป็น "10" ทุกอย่างอื่นจะจบลงในรายการ

object OptParser {
  val map: Map[Symbol, Any] = Map()
  val list: List[Symbol] = List()

  def parse(args: Array[String]): (Map[Symbol, Any], List[Symbol]) = _parse(map, list, args.toList)

  private [this] def _parse(map: Map[Symbol, Any], list: List[Symbol], args: List[String]): (Map[Symbol, Any], List[Symbol]) = {
    args match {
      case Nil => (map, list)
      case arg :: value :: tail if (arg.startsWith("--") && !value.startsWith("--")) => _parse(map ++ Map(Symbol(arg.substring(2)) -> value), list, tail)
      case arg :: tail if (arg.startsWith("--")) => _parse(map ++ Map(Symbol(arg.substring(2)) -> true), list, tail)
      case opt :: tail => _parse(map, list :+ Symbol(opt), tail)
    }
  }
}

1

ฉันชอบรูปลักษณ์ที่สะอาดของรหัสนี้ ... รวบรวมจากการสนทนาที่นี่: http://www.scala-lang.org/old/node/4380

object ArgParser {
  val usage = """
Usage: parser [-v] [-f file] [-s sopt] ...
Where: -v   Run verbosely
       -f F Set input file to F
       -s S Set Show option to S
"""

  var filename: String = ""
  var showme: String = ""
  var debug: Boolean = false
  val unknown = "(^-[^\\s])".r

  val pf: PartialFunction[List[String], List[String]] = {
    case "-v" :: tail => debug = true; tail
    case "-f" :: (arg: String) :: tail => filename = arg; tail
    case "-s" :: (arg: String) :: tail => showme = arg; tail
    case unknown(bad) :: tail => die("unknown argument " + bad + "\n" + usage)
  }

  def main(args: Array[String]) {
    // if there are required args:
    if (args.length == 0) die()
    val arglist = args.toList
    val remainingopts = parseArgs(arglist,pf)

    println("debug=" + debug)
    println("showme=" + showme)
    println("filename=" + filename)
    println("remainingopts=" + remainingopts)
  }

  def parseArgs(args: List[String], pf: PartialFunction[List[String], List[String]]): List[String] = args match {
    case Nil => Nil
    case _ => if (pf isDefinedAt args) parseArgs(pf(args),pf) else args.head :: parseArgs(args.tail,pf)
  }

  def die(msg: String = usage) = {
    println(msg)
    sys.exit(1)
  }

}

1

ในขณะที่ทุกคนโพสต์เป็นทางออกของตัวเองที่นี่เป็นของฉันเพราะฉันต้องการเขียนสิ่งที่ง่ายขึ้นสำหรับผู้ใช้: https://gist.github.com/gwenzek/78355526e476e08bb34d

เค้าร่างมีไฟล์รหัสรวมทั้งไฟล์ทดสอบและตัวอย่างสั้น ๆ ที่คัดลอกมาที่นี่:

import ***.ArgsOps._


object Example {
    val parser = ArgsOpsParser("--someInt|-i" -> 4, "--someFlag|-f", "--someWord" -> "hello")

    def main(args: Array[String]){
        val argsOps = parser <<| args
        val someInt : Int = argsOps("--someInt")
        val someFlag : Boolean = argsOps("--someFlag")
        val someWord : String = argsOps("--someWord")
        val otherArgs = argsOps.args

        foo(someWord, someInt, someFlag)
    }
}

ไม่มีตัวเลือกแฟนซีที่จะบังคับให้ตัวแปรอยู่ในขอบเขตเพราะฉันไม่รู้สึกว่า parser เป็นสถานที่ที่ดีที่สุดในการทำเช่นนั้น

หมายเหตุ: คุณสามารถมีนามแฝงได้มากเท่าที่คุณต้องการสำหรับตัวแปรที่กำหนด


1

ฉันจะไปกอง ฉันแก้ไขมันด้วยโค้ดบรรทัดธรรมดา อาร์กิวเมนต์บรรทัดคำสั่งของฉันมีลักษณะเช่นนี้:

input--hdfs:/path/to/myData/part-00199.avro output--hdfs:/path/toWrite/Data fileFormat--avro option1--5

สิ่งนี้จะสร้างอาร์เรย์ผ่านฟังก์ชันบรรทัดคำสั่งดั้งเดิมของ Scala (จากแอพหรือวิธีหลัก):

Array("input--hdfs:/path/to/myData/part-00199.avro", "output--hdfs:/path/toWrite/Data","fileFormat--avro","option1--5")

ฉันสามารถใช้บรรทัดนี้เพื่อแยกอาร์เรย์ args เริ่มต้น:

val nArgs = args.map(x=>x.split("--")).map(y=>(y(0),y(1))).toMap

ซึ่งสร้างแผนที่ที่มีชื่อเกี่ยวข้องกับค่าบรรทัดคำสั่ง:

Map(input -> hdfs:/path/to/myData/part-00199.avro, output -> hdfs:/path/toWrite/Data, fileFormat -> avro, option1 -> 5)

จากนั้นฉันสามารถเข้าถึงค่าของพารามิเตอร์ที่มีชื่อในรหัสของฉันและลำดับที่ปรากฏบนบรรทัดคำสั่งไม่เกี่ยวข้องอีกต่อไป ฉันรู้ว่านี่ค่อนข้างง่ายและไม่มีฟังก์ชั่นขั้นสูงดังกล่าวข้างต้น แต่ดูเหมือนจะเพียงพอในกรณีส่วนใหญ่ต้องการเพียงหนึ่งบรรทัดของโค้ดและไม่เกี่ยวข้องกับการพึ่งพาจากภายนอก


1

นี่คือซับ 1 ของฉัน

    def optArg(prefix: String) = args.drop(3).find { _.startsWith(prefix) }.map{_.replaceFirst(prefix, "")}
    def optSpecified(prefix: String) = optArg(prefix) != None
    def optInt(prefix: String, default: Int) = optArg(prefix).map(_.toInt).getOrElse(default)

มันลดลง 3 ข้อโต้แย้งบังคับและให้ตัวเลือก มีการระบุจำนวนเต็มเช่น-Xmx<size>ตัวเลือก java ที่มีชื่อเสียงร่วมกับส่วนนำหน้า คุณสามารถแยกวิเคราะห์ไบนารีและจำนวนเต็มได้ง่าย

val cacheEnabled = optSpecified("cacheOff")
val memSize = optInt("-Xmx", 1000)

ไม่จำเป็นต้องนำเข้าอะไร


0

ชาย - หญิงหนึ่งซับในที่รวดเร็วและสกปรกสำหรับการแยกคู่คีย์ = ค่าของการแยก:

def main(args: Array[String]) {
    val cli = args.map(_.split("=") match { case Array(k, v) => k->v } ).toMap
    val saveAs = cli("saveAs")
    println(saveAs)
}

0

freecli

package freecli
package examples
package command

import java.io.File

import freecli.core.all._
import freecli.config.all._
import freecli.command.all._

object Git extends App {

  case class CommitConfig(all: Boolean, message: String)
  val commitCommand =
    cmd("commit") {
      takesG[CommitConfig] {
        O.help --"help" ::
        flag --"all" -'a' -~ des("Add changes from all known files") ::
        O.string -'m' -~ req -~ des("Commit message")
      } ::
      runs[CommitConfig] { config =>
        if (config.all) {
          println(s"Commited all ${config.message}!")
        } else {
          println(s"Commited ${config.message}!")
        }
      }
    }

  val rmCommand =
    cmd("rm") {
      takesG[File] {
        O.help --"help" ::
        file -~ des("File to remove from git")
      } ::
      runs[File] { f =>
        println(s"Removed file ${f.getAbsolutePath} from git")
      }
    }

  val remoteCommand =
   cmd("remote") {
     takes(O.help --"help") ::
     cmd("add") {
       takesT {
         O.help --"help" ::
         string -~ des("Remote name") ::
         string -~ des("Remote url")
       } ::
       runs[(String, String)] {
         case (s, u) => println(s"Remote $s $u added")
       }
     } ::
     cmd("rm") {
       takesG[String] {
         O.help --"help" ::
         string -~ des("Remote name")
       } ::
       runs[String] { s =>
         println(s"Remote $s removed")
       }
     }
   }

  val git =
    cmd("git", des("Version control system")) {
      takes(help --"help" :: version --"version" -~ value("v1.0")) ::
      commitCommand ::
      rmCommand ::
      remoteCommand
    }

  val res = runCommandOrFail(git)(args).run
}

สิ่งนี้จะสร้างการใช้งานต่อไปนี้:

การใช้

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.