SkillAgentSearch skills...

Argyle

Scala command-line arguments parser

Install / Use

/learn @jfkelley/Argyle
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Argyle

Command-line argument parsing for Scala

Example:

import com.joefkelley.argyle._
import scala.util.{Try, Success, Failure}

object Example extends App {

  val a = (
    required[String]("--name", "-n") and
    optional[String]("--occupation") and
    required[Int]("--age") and
    repeated[String]("--hobby") and
    requiredOneOf("--single" -> Single, "--married" -> Married) and
    optional[Int]("--fingers").default(10) and
    flag("--debug") and
    optionalBranch[Pet](
      "--pet-rock" -> required[String]("--rock-name").to[Rock],
      "--pet-dog"  -> (required[String]("--dog-breed") and required[String]("--dog-name")).to[Dog]
    ) and
    (requiredOneOf("--male" -> Male, "--female" -> Female) xor required[String]("--gender").to[OtherGender])
  ).to[PersonConfig]

  a.parse(args) match {
    case Success(person) => println(person)
    case Failure(e) => throw e
  }

  case class PersonConfig(
      name: String,
      occupation: Option[String],
      age: Int,
      hobbies: List[String],
      maritalStatus: MaritalStatus,
      nFingers: Int,
      likesToDebug: Boolean,
      pet: Option[Pet],
      gender: Gender)

  sealed trait MaritalStatus
  object Single extends MaritalStatus
  object Married extends MaritalStatus

  trait Pet
  case class Dog(breed: String, name: String) extends Pet
  case class Rock(name: String) extends Pet

  sealed trait Gender
  object Male extends Gender
  object Female extends Gender
  case class OtherGender(str: String) extends Gender

}

SBT Dependency

libraryDependencies += "com.joefkelley" %% "argyle" % "1.0.0"

Background

Argyle is a simple tool for parsing command-line arguments. Its goals are:

  • Absolute minimum boilerplate
  • Type-safe
  • Flexible / Extensible
  • Minimal dependencies
  • Pure functional

It does not currently have the ability to print help/usage information. The hope is that the syntax is so concise and readable that the code is documentation enough. Including this feature would be a crutch that would allow compromising on the first goal ;)

Usage

Three steps:

  1. Define a case class containing all of the options you want to configure
  2. Construct a com.joefkelley.argyle.Arg[<config class>] by "and"-ing together more granular Args representing individual fields
  3. Call .parse(args) on that, returning a Try[<config class>]

Here's a simple example that requires two command-line arguments: "--name" and "--age":

case class Person(name: String, age: Int)
val nameArg = required[String]("--name")
val ageArg = required[Int]("--age")
val personArg = (nameArg and ageArg).to[Person]
val result: Try[Person] = personArg.parse(args)

Breaking this down line-by-line:

case class Person(name: String, age: Int)

This class will eventually contain all of the configuration options we want from the command line

val nameArg = required[String]("--name")

Creates an Arg[String] that will match arguments passed in the form "--name Joe" or similar. Note that because the arg is required, parsing will fail if it is not present.

val ageArg = required[Int]("--age")

The specified argument type will be the type of the resulting object that is eventually returned by the parser. In this case it is Int, so this creates an Arg[Int]. The signature of the required method is:

def required[A : Reader](keys: String*): Arg[A]

Note that the type requires an implicit typeclass com.joefkelley.argyle.Reader[A]. There are built-in values for all primitive types, Files, ISO-8601 Dates and Times, Lists (comma-separated), and Eithers. If you wish to use some different parsing logic, the Reader trait has a very simple interface that you can implement.

val personArg = (nameArg and ageArg).to[Person]

The and method here combines an Arg[String] and an Arg[Int] into an Arg[String :: Int :: HNil], an arg parser for a shapeless hlist. But there is no need to deal with shapeless directly. The .to[Person] call converts this to an Arg[Person]. Note that this is still type-safe; the types must match the fields of the Person class or it would not compile.

val result: Try[Person] = personArg.parse(args)

Finally, we call .parse(args), which will return a Success[Person] iff both "--name" and "--age" are supplied, the argument for "--age" is an integer, and there are no other unused arguments. Otherwise, it will return a Failure containing an appropriate error message. By default, the args are expected in the form "--name Joe --age 100", but equals-separated format can also be used by calling .parse(args, com.joefkelley.argyle.EqualsSeparated), for example "--name=Joe --age=100".

Full API Details

The com.joefkelley.argyle package object contains several methods for creating Args. They are:

def required[A : Reader](keys: String*): Arg[A]

Requires that one of the given keys must be present exactly once, and will fail otherwise.

def optional[A : Reader](keys: String*): Arg[Option[A]]

Requires that one of the given keys will be present at most once. Will result in a None if not present, a Some[A] if present, and fail if present more than once.

def repeated[A : Reader](keys: String*): Arg[List[A]]

Matches any one of the keys any number of times, resulting in a List[A]. For example, repeated[Int]("-n") would successfully parse "-n 1 -n 5 -n 10", resulting in List(1, 5, 10).

def repeatedAtLeastOnce[A : Reader](keys: String*): Arg[List[A]]

Same as above, but fails if not present at least once. Always results in a list with at least one element.

def requiredOneOf[A](kvs: (String, A)*): Arg[A]

A parser that matches exactly one of the keys in the key-value pairs, resulting in its corresponding value. Note that no value should be passed in the command-line arguments. For example, requiredOneOf("-a" -> 1, "-b" -> 2) would match just "-a", resulting in a value of 1. Fails if none of the keys are present.

def optionalOneOf[A](kvs: (String, A)*): Arg[Option[A]]

Same as above, except returns a None instead of failing if none are present, and returns a Some[A] if one is.

def flag(keys: String*): Arg[Boolean]

If any of the provided keys are present, results in true, otherwise false.

def requiredFree[A : Reader]: Arg[A]

Matches any argument. For example, requiredFree[String] and required[Int]("--n") would match "--n 5 foo". Fails if no extra args are present.

def optionalFree[A : Reader]: Arg[Option[A]]

Same as above, except returns a None instead of failing if none are present, and returns a Some[A] if one is.

def repeatedFree[A : Reader]: Arg[List[A]]

Matches any number of arguments not matched by "keyed" arguments.

def repeatedAtLeastOnceFree[A : Reader]: Arg[List[A]]

Same as above, except fails if no arguments are present.

Note that the order of free arguments is important; they are matched greedily in order, but with back-tracking if neccessary. For example, repeatedFree[String] and requiredFree[String] will match all arguments except the last for the repeatedFree, and just the last for the requiredFree. If the order were reversed, the requiredFree would match the first argument, and the repeatedFree would match the rest.

Something like repeatedFree[String] and requiredFree[Int] should also successfully match "foo 5 bar", since back-tracking will eventually find that the requiredFree[Int] must match the "5", and the repeatedFree[String] will match everything else.

def requiredBranch[A](kvs: (String, Arg[A])*): Arg[A]

Allows branching behavior based on the presence of keys in the key-value pairs. "Activates" one of the passed args based on which key is present, and parses using that arg. For example: requiredBranch("-a" -> required[String]("--foo"), "-b" -> requiredFree[String]) would parse either "-a --foo hello" or "-b hello". Arbitrarily-complex args can be used, including nested branches or anything else. Note that the "branching" argument must be present in the command line arguments before the arguments parsed within that branch (i.e. "--foo hello -a" would not work).

def optionalBranch[A](kvs: (String, Arg[A])*): Arg[Option[A]]

Same as above, but does not fail if none of the branching keys are present.

def constant[A](a: A): Arg[A]

Does not consume or require any command-line arguments, just returns the value back. Useful for configuration options that can't be changed, or for simple cases of branching arguments.

The com.joefkelley.argyle.Arg[A] class itself contains methods for combining and modifying args. They are:

def flatMap[B](f: A => Try[B]): Arg[B]

If parsing succeeds, applies f to the result, and succeeds if the result is a success, otherwise fails.

def map[B](f: A => B): Arg[B] = flatMap(a => Success(f(a)))

If parsing succeeds, applies f to the result and returns the output.

def as[B](implicit f: A => B): Arg[B] = map(f)

Syntactic sugar for map for cases when an implicit conversion A => B is possible.

def xor[B >: A](arg2: Arg[B]): Arg[B]

Returns a new Arg that succeeds if either this, or the passed in arg is present and succeeds. Notably fails if they are both present. For example, required[Int]("-n").xor(required[Int]("-m")) would return 1 for both "-n 1" and "-m 1", but would fail for "-n 1 -m 1".

def or[B >: A](arg2: Arg[B], f: (B, B) => B): Arg[B]

Same as xor, except does not fail if both are present. Instead, calls f with the output from both. (In the order f(thisOutput, arg2Output)).

def default[B](b: B)(implicit ev: A <:< Option[B]): Arg[B]

For arguments that result in an Option[A], such as those from optional[A](...), promotes it

View on GitHub
GitHub Stars64
CategoryDevelopment
Updated2h ago
Forks4

Languages

Scala

Security Score

80/100

Audited on Apr 5, 2026

No findings