Cara terbaik untuk mem-parsing parameter baris perintah? [Tutup]

237

Apa cara terbaik untuk mem-parsing parameter baris perintah di Scala? Saya pribadi lebih suka sesuatu yang ringan yang tidak memerlukan botol eksternal.

Terkait:

Eugene Yokota
sumber

Jawaban:

228

Untuk sebagian besar kasus, Anda tidak memerlukan parser eksternal. Pencocokan pola Scala memungkinkan penggunaan args dalam gaya fungsional. Sebagai contoh:

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)
  }
}

akan dicetak, misalnya:

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

Versi ini hanya membutuhkan satu infile. Mudah ditingkatkan (dengan menggunakan Daftar).

Perhatikan juga bahwa pendekatan ini memungkinkan untuk menggabungkan beberapa argumen baris perintah - bahkan lebih dari dua!

pjotrp
sumber
4
isSwitch cukup memeriksa karakter pertama sebagai tanda hubung '-'
pjotrp
6
nextOptionbukan nama yang baik untuk fungsi ini. Ini adalah fungsi yang mengembalikan peta - fakta bahwa itu adalah rekursif adalah detail implementasi. Ini seperti menulis maxfungsi untuk koleksi dan menyebutnya nextMaxhanya karena Anda menulisnya dengan rekursi eksplisit. Kenapa tidak menyebutnya saja optionMap?
itsbruce
4
@itsbruce Saya hanya ingin menambahkan / memodifikasi poin Anda - itu akan menjadi yang paling "layak" dari keterbacaan / pemeliharaan untuk mendefinisikan listToOptionMap(lst:List[String])dengan fungsi yang nextOptiondidefinisikan di dalamnya, dengan pepatah baris terakhir return nextOption(Map(), lst). Yang mengatakan, saya harus mengakui bahwa saya telah membuat lebih banyak jalan pintas mengerikan dalam waktu saya daripada yang ada di jawaban ini.
tresbot
6
@theMadKing dalam kode di atas exit(1)mungkin perlusys.exit(1)
tresbot
3
Saya suka solusi Anda. Berikut modifikasi untuk menangani beberapa fileparameter: 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). Peta juga membutuhkan nilai default Nil, yaitu val options = nextOption(Map() withDefaultValue Nil, args.toList). Apa yang saya tidak suka adalah harus menggunakan asInstanceOf, karena OptionMapnilai - nilai yang bertipe Any. Apakah ada solusi yang lebih baik?
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
}

Di atas menghasilkan teks penggunaan berikut:

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

Inilah yang saat ini saya gunakan. Bersihkan penggunaan tanpa terlalu banyak bagasi. (Penafian: Saya sekarang mengelola proyek ini)

Eugene Yokota
sumber
6
Saya suka pola pembangun DSL jauh lebih baik, karena memungkinkan pendelegasian konstruksi parameter ke modul.
Daniel C. Sobral
3
Catatan: tidak seperti yang diperlihatkan, scopt tidak membutuhkan banyak anotasi jenis.
Blaisorblade
9
Jika Anda menggunakan ini untuk mengurai args untuk pekerjaan percikan, berhati-hatilah karena mereka tidak bermain bersama dengan baik. Secara harfiah tidak ada yang saya coba dapat memicu-kirim untuk bekerja dengan scopt :-(
jbrown
4
@BirdJaguarIV Jika percikan menggunakan scopt itu mungkin masalah - versi yang bertentangan dalam toples atau sesuatu. Saya menggunakan scallop dengan pekerjaan percikan sebagai gantinya dan tidak memiliki masalah.
jbrown
12
Ironisnya meskipun perpustakaan ini secara otomatis menghasilkan dokumentasi CLI yang baik, kode ini terlihat sedikit lebih baik daripada brainf * ck.
Jonathan Neufeld
58

Saya menyadari bahwa pertanyaan itu diajukan beberapa waktu yang lalu, tetapi saya pikir itu mungkin membantu beberapa orang, yang mencari-cari di sekitar (seperti saya), dan membuka halaman ini.

Scallop terlihat cukup menjanjikan juga.

Fitur (kutipan dari halaman github tertaut):

  • pilihan bendera, nilai tunggal dan beberapa nilai
  • Nama opsi pendek gaya POSIX (-a) dengan pengelompokan (-abc)
  • Nama opsi panjang gaya GNU (--opt)
  • Argumen properti (-Dkey = value, -D key1 = value key2 = value)
  • Jenis opsi dan nilai properti non-string (dengan konverter yang dapat diperpanjang)
  • Pencocokan kuat pada trailing args
  • Sub-perintah

Dan beberapa kode contoh (juga dari halaman Github itu):

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)
rintcius
sumber
4
Scallop menurunkan sisanya ke tangan dalam hal fitur. Malu tren SO biasa "jawaban pertama menang" telah mendorong ini ke bawah daftar :(
samthebest
Saya setuju. Meninggalkan komentar di sini hanya memetikan @Eugene Yokota tidak terjawab untuk mencatat. Lihat blog ini scallop
Pramit
1
Masalah yang disebutkannya dengan scopt adalah "Kelihatannya bagus, tetapi tidak dapat mengurai opsi, yang mengambil daftar argumen (yaitu -a 1 2 3). Dan Anda tidak memiliki cara untuk memperpanjangnya untuk mendapatkan daftar tersebut (kecuali forking the lib). " tetapi ini tidak lagi benar, lihat github.com/scopt/scopt#options .
Alexey Romanov
2
ini lebih intuitif dan lebih sedikit boilerplate daripada scopt. tidak ada lagi yang (x, c) => c.copy(xyz = x) di pilih
WeiChing 林 煒 清
43

Saya suka menggeser argumen untuk konfigurasi yang relatif sederhana.

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
}
joslinm
sumber
2
Pintar. Hanya berfungsi jika setiap arg juga menentukan nilai, kan?
Brent Faust
2
Bukankah seharusnya demikian args.sliding(2, 2)?
m01
1
Bukankah seharusnya demikian var port = 0?
swdev
17

Toolkit Scala Antarmuka Baris Perintah (CLIST)

ini milikku juga! (meskipun agak terlambat dalam permainan)

https://github.com/backuity/clist

Berbeda dengan scoptitu sepenuhnya bisa berubah ... tapi tunggu! Itu memberi kita sintaks yang cukup bagus:

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")
}

Dan cara sederhana untuk menjalankannya:

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

Anda dapat melakukan lebih banyak tentu saja (multi-perintah, banyak opsi konfigurasi, ...) dan tidak memiliki ketergantungan.

Saya akan selesai dengan semacam fitur khas, penggunaan default (cukup sering diabaikan untuk banyak perintah): clist

Bruno Bieth
sumber
Apakah ada validasi?
KF
Ya itu (lihat github.com/backuity/clist/blob/master/demo/src/main/scala/… untuk contoh). Itu tidak didokumentasikan ... PR? :)
Bruno Bieth
Mencobanya, cukup nyaman. Saya menggunakan scopt sebelumnya, saya masih belum terbiasa untuk menambahkan validasi bersama, tetapi tidak hanya dalam definisi setiap parameter. Tapi itu berhasil dengan saya. Dan mendefinisikan parameter dan validasi yang berbeda dalam sifat yang berbeda, kemudian menggabungkannya dalam kasus yang berbeda, itu sangat membantu. Saya menderita banyak dalam scopt ketika tidak nyaman untuk menggunakan kembali parameter. Terima kasih atas balasan!
KF
Kebanyakan validasi dilakukan selama deserialization parameter baris perintah (lihat Baca ), jadi jika Anda dapat menentukan kendala validasi Anda melalui jenis (yaitu Password, Hex, ...), maka Anda dapat memanfaatkan ini.
Bruno Bieth
13

Ini sebagian besar merupakan tiruan tak tahu malu dari jawaban saya untuk pertanyaan Java tentang topik yang sama . Ternyata JewelCLI ramah-Scala karena tidak memerlukan metode gaya JavaBean untuk mendapatkan penamaan argumen otomatis.

JewelCLI adalah perpustakaan Java Scala-friendly untuk parsing baris perintah yang menghasilkan kode bersih . Menggunakan Proxied Interfaces Configured with Annotations untuk secara dinamis membangun API tipe-aman untuk parameter baris perintah Anda.

Contoh antarmuka parameter Person.scala:

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

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

Contoh penggunaan antarmuka parameter 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)
    }
  }
}

Simpan salinan file-file di atas ke satu direktori dan unduh JewelCLI 0,6 JAR ke direktori itu juga.

Kompilasi dan jalankan contoh di Bash di 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

Kompilasi dan jalankan contoh di Prompt Perintah Windows:

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

Menjalankan contoh harus menghasilkan output berikut:

Hello John Doe
Hello John Doe
Hello John Doe
Alain O'Dea
sumber
Satu hal yang menyenangkan dalam hal ini yang mungkin Anda perhatikan adalah (args: _ *). Memanggil metode Java varargs dari Scala membutuhkan ini. Ini adalah solusi yang saya pelajari dari daily-scala.blogspot.com/2009/11/varargs.html di blog Daily Scala Jesse Eichar yang sangat bagus. Saya sangat merekomendasikan Daily Scala :)
Alain O'Dea
12

Cara mengurai parameter tanpa ketergantungan eksternal. Pertanyaan bagus! Anda mungkin tertarik pada picocli .

Picocli dirancang khusus untuk menyelesaikan masalah yang diajukan dalam pertanyaan: itu adalah kerangka penguraian baris perintah dalam satu file, sehingga Anda dapat memasukkannya dalam bentuk sumber . Ini memungkinkan pengguna menjalankan aplikasi berbasis picocli tanpa memerlukan picocli sebagai ketergantungan eksternal .

Ini bekerja dengan bidang anotasi sehingga Anda menulis kode yang sangat sedikit. Ringkasan cepat:

  • Sangat diketik semuanya - opsi baris perintah serta parameter posisi
  • Dukungan untuk opsi pendek berkerumun POSIX (sehingga dapat menangani <command> -xvfInputFileserta <command> -x -v -f InputFile)
  • Model arity yang memungkinkan jumlah parameter minimum, maksimum dan variabel, misalnya "1..*","3..5"
  • API yang lancar dan kompak untuk meminimalkan kode klien boilerplate
  • Sub-perintah
  • Bantuan penggunaan dengan warna ANSI

Pesan bantuan penggunaan mudah disesuaikan dengan anotasi (tanpa pemrograman). Sebagai contoh:

Pesan bantuan penggunaan diperpanjang( sumber )

Saya tidak bisa menolak menambahkan satu tangkapan layar lagi untuk menunjukkan pesan bantuan penggunaan seperti apa yang mungkin. Bantuan penggunaan adalah wajah aplikasi Anda, jadi jadilah kreatif dan bersenang-senang!

demo picocli

Penafian: Saya membuat picocli. Umpan balik atau pertanyaan sangat disambut. Itu ditulis dalam java, tetapi beri tahu saya jika ada masalah menggunakannya dalam scala dan saya akan mencoba mengatasinya.

Remko Popma
sumber
1
Mengapa downvote? Ini adalah satu-satunya perpustakaan yang saya tahu yang secara khusus dirancang untuk mengatasi masalah yang disebutkan dalam OP: bagaimana cara menghindari menambahkan ketergantungan.
Remko Popma
"dorong penulis aplikasi untuk memasukkannya". Kerja bagus.
keos
apakah Anda memiliki contoh scala?
CruncherBigData
1
Saya sudah mulai membuat contoh untuk bahasa JVM lainnya: github.com/remkop/picocli/issues/183 Umpan balik dan kontribusi diterima!
Remko Popma
11

Saya dari dunia Java, saya suka args4j karena sederhana, spesifikasinya lebih mudah dibaca (terima kasih pada anotasi) dan menghasilkan keluaran yang diformat dengan baik.

Berikut ini cuplikan contoh saya:

Spesifikasi

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]"

}

Parse

//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)

Pada argumen yang tidak valid

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.
Thamme Gowda
sumber
10

scala-optparse-aplikatif

Saya pikir scala-optparse-aplikatif adalah parser baris perintah perpustakaan paling fungsional di Scala.

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

Kenji Yoshida
sumber
apakah ada contoh / dokumen selain apa yang ada dalam README?
Erik Kaplun
1
Ya, periksa examplesdi kode tes
gpampara
8

Ada juga JCommander (penafian: Saya membuatnya):

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)
    }
  }
}
Cedric Beust
sumber
2
Saya suka yang ini. parser 'scala murni' itu tidak memiliki sintaksis yang bersih
tactoth
@tothoth memeriksa yang ini, ia memiliki sintaks yang jelas: stackoverflow.com/questions/2315912/…
Bruno Bieth
6

Saya menyukai pendekatan slide () dari joslinm, bukan vars yang bisa berubah;) Jadi, inilah cara yang tidak dapat diubah untuk pendekatan itu:

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
  }
}
haggy
sumber
3

Saya telah mencoba menggeneralisasi solusi @ pjotrp dengan memasukkan daftar simbol kunci posisi yang diperlukan, peta bendera -> simbol kunci dan opsi default:

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)
}
Byron Ruth
sumber
Saya memperbarui potongan kode ini untuk menangani flag (tidak hanya opsi dengan nilai) dan juga untuk menentukan opsi / flag dengan bentuk pendek dan panjang. mis -f|--flags. Lihatlah gist.github.com/DavidGamba/b3287d40b019e498982c dan jangan ragu untuk memperbarui jawabannya jika Anda menyukainya. Saya mungkin akan membuat setiap Peta dan opsi sehingga Anda hanya bisa memberikan apa yang Anda perlukan dengan argumen bernama.
DavidG
3

Saya mendasarkan pendekatan saya pada jawaban teratas (dari dave4420), dan mencoba memperbaikinya dengan menjadikannya lebih umum.

Ini mengembalikan a Map[String,String]dari semua parameter baris perintah Anda dapat menanyakan ini untuk parameter spesifik yang Anda inginkan (misalnya menggunakan .contains) atau mengubah nilai menjadi tipe yang Anda inginkan (misalnya menggunakan 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())
}

Contoh:

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

Memberi:

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

Berikut adalah pengurai baris perintah scala yang mudah digunakan. Secara otomatis memformat teks bantuan, dan itu mengonversi argumen beralih ke jenis yang Anda inginkan. Baik POSIX pendek, dan sakelar gaya GNU panjang didukung. Mendukung sakelar dengan argumen yang diperlukan, argumen opsional, dan argumen multi nilai. Anda bahkan dapat menentukan daftar nilai terbatas yang dapat diterima untuk sakelar tertentu. Nama saklar panjang dapat disingkat pada baris perintah untuk kenyamanan. Mirip dengan parser opsi di pustaka standar Ruby.

sellmerfud
sumber
2

Saya tidak pernah suka ruby ​​like parser opsi. Sebagian besar pengembang yang menggunakannya tidak pernah menulis halaman manual yang tepat untuk skrip mereka dan berakhir dengan pilihan halaman panjang tidak terorganisir dengan cara yang tepat karena parser mereka.

Saya selalu lebih suka cara Perl melakukan sesuatu dengan Perl's Getopt :: Long .

Saya sedang mengerjakan implementasi scala itu. API awal terlihat seperti ini:

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,
    ))

Jadi panggilan scriptseperti ini:

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

Akan mencetak:

higher order function
version is 0.2

Dan kembali:

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

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

Proyek ini diselenggarakan di github scala-getoptions .

DavidG
sumber
2

Saya baru saja membuat enumerasi sederhana saya

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

Saya mengerti bahwa solusi memiliki dua kelemahan utama yang dapat mengalihkan perhatian Anda: Solusi ini menghilangkan kebebasan (yaitu ketergantungan pada perpustakaan lain, yang sangat Anda hargai) dan redundansi (prinsip KERING, Anda mengetikkan nama opsi hanya satu kali, seperti program Scala variabel dan menghilangkannya kedua kalinya diketik sebagai teks baris perintah).

Val
sumber
2

Saya sarankan untuk menggunakan http://docopt.org/ . Ada scala-port tetapi implementasi Java https://github.com/docopt/docopt.java berfungsi dengan baik dan tampaknya lebih baik dipertahankan. Ini sebuah contoh:

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
Holger Brandl
sumber
2

Ini yang saya masak. Ini mengembalikan tuple peta dan daftar. Daftar adalah untuk input, seperti nama file input. Peta untuk sakelar / opsi.

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

akan kembali

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

Switch dapat berupa "--t" yang x akan disetel ke true, atau "--x 10" yang x akan diatur ke "10". Segala sesuatu yang lain akan berakhir dalam daftar.

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)
    }
  }
}
auselen
sumber
1

Saya suka tampilan bersih dari kode ini ... diperoleh dari diskusi di sini: 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)
  }

}
Alan Jurgensen
sumber
1

Karena semua orang memposting solusi sendiri di sini adalah milik saya, karena saya ingin sesuatu yang lebih mudah untuk ditulis untuk pengguna: https://gist.github.com/gwenzek/78355526e476e08bb34d

Intinya berisi file kode, ditambah file uji dan contoh singkat yang disalin di sini:

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)
    }
}

Tidak ada opsi mewah untuk memaksa variabel berada dalam beberapa batasan, karena saya tidak merasa bahwa parser adalah tempat terbaik untuk melakukannya.

Catatan: Anda dapat memiliki alias sebanyak yang Anda inginkan untuk variabel yang diberikan.

gwenzek
sumber
1

Saya akan menumpuk. Saya memecahkan ini dengan garis kode sederhana. Argumen baris perintah saya terlihat seperti ini:

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

Ini menciptakan sebuah array melalui fungsi baris perintah asli Scala (dari salah satu Aplikasi atau metode utama):

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

Saya kemudian dapat menggunakan baris ini untuk mem-parsing array default args:

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

Yang membuat peta dengan nama yang terkait dengan nilai-nilai baris perintah:

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

Saya kemudian dapat mengakses nilai-nilai parameter bernama dalam kode saya dan urutan mereka muncul di baris perintah tidak lagi relevan. Saya menyadari ini cukup sederhana dan tidak memiliki semua fungsi canggih yang disebutkan di atas tetapi tampaknya cukup dalam banyak kasus, hanya membutuhkan satu baris kode, dan tidak melibatkan dependensi eksternal.

J Calbreath
sumber
1

Ini milikku 1-liner

    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)

Itu menjatuhkan 3 argumen wajib dan memberikan opsi. Integer ditentukan seperti -Xmx<size>opsi java terkenal , bersama dengan awalan. Anda dapat mengurai binari dan bilangan bulat sesederhana

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

Tidak perlu mengimpor apa pun.

Valentin Tihomirov
sumber
0

One-liner cepat dan kotor orang miskin untuk kunci parsing = pasangan nilai:

def main(args: Array[String]) {
    val cli = args.map(_.split("=") match { case Array(k, v) => k->v } ).toMap
    val saveAs = cli("saveAs")
    println(saveAs)
}
botkop
sumber
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
}

Ini akan menghasilkan penggunaan berikut:

Pemakaian

pavlosgi
sumber