Current section

Files

Jump to
outil src outil opt.gleam
Raw

src/outil/opt.gleam

import gleam/float
import gleam/int
import gleam/list
import gleam/option.{None, Option, Some}
import gleam/result
import gleam/string
import outil.{
BoolOpt, Command, Configure, FloatOpt, IntOpt, Opt, StringOpt, parse_bool,
}
import outil/error.{MalformedArgument, MissingArgument, Reason}
import outil/help
/// Add a named bool option to the command before continuing.
pub fn bool(
cmd: Command,
long: String,
description: String,
continue: Configure(Bool, a, _),
) -> a {
bool_(cmd, long, None, description, continue)
}
/// Add a bool option with a short name to the command before continuing.
pub fn bool_(
cmd: Command,
long: String,
short: Option(String),
description: String,
continue: Configure(Bool, a, _),
) -> a {
let opt = Opt(long, short, description, BoolOpt)
let opt_parser = bool_opt_parser(long, short)
let opt_parser = fn(run_cmd: Command) { help.wrap_usage(run_cmd, opt_parser) }
continue(opt_parser, add_option(cmd, opt))
}
/// Add a named float option to the command before continuing.
pub fn float(
cmd: Command,
long: String,
description: String,
default: Float,
continue: Configure(Float, a, _),
) -> a {
float_(cmd, long, None, description, default, continue)
}
/// Add a float option with a short name to the command before continuing.
pub fn float_(
cmd: Command,
long: String,
short: Option(String),
description: String,
default: Float,
continue: Configure(Float, a, _),
) -> a {
let opt = Opt(long, short, description, FloatOpt(default))
with_named_option(cmd, opt, default, float.parse, continue)
}
/// Add a named int option to the command before continuing.
pub fn int(
cmd: Command,
long: String,
description: String,
default: Int,
continue: Configure(Int, a, _),
) -> a {
int_(cmd, long, None, description, default, continue)
}
/// Add an int option with a short name to the command before continuing.
pub fn int_(
cmd: Command,
long: String,
short: Option(String),
description: String,
default: Int,
continue: Configure(Int, a, _),
) -> a {
let opt = Opt(long, short, description, IntOpt(default))
with_named_option(cmd, opt, default, int.parse, continue)
}
/// Add a named string option to the command before continuing.
pub fn string(
cmd: Command,
long: String,
description: String,
default: String,
continue: Configure(String, a, _),
) -> a {
string_(cmd, long, None, description, default, continue)
}
/// Add a string option with a short name to the command before continuing.
pub fn string_(
cmd: Command,
long: String,
short: Option(String),
description: String,
default: String,
continue: Configure(String, a, _),
) -> a {
let opt = Opt(long, short, description, StringOpt(default))
with_named_option(cmd, opt, default, Ok, continue)
}
/// Add a named option to the command. Bring your own parser.
///
/// The continuation function gets a parser for the option and the command
/// with the option added, for further configuration.
pub fn with_named_option(
cmd: Command,
opt: Opt,
default: b,
parse: fn(String) -> Result(b, Nil),
continue: Configure(b, a, _),
) -> a {
let opt_parser = named_opt_parser(opt.long, opt.short, parse, default)
let opt_parser = fn(run_cmd: Command) { help.wrap_usage(run_cmd, opt_parser) }
continue(opt_parser, add_option(cmd, opt))
}
fn add_option(cmd: Command, opt: Opt) -> Command {
Command(
cmd.name,
cmd.description,
cmd.arguments,
list.append(cmd.options, [opt]),
cmd.argv,
)
}
pub fn named_opt_parser(
long: String,
short: Option(String),
parser: fn(String) -> Result(a, Nil),
default: a,
) -> fn(List(String)) -> Result(a, Reason) {
fn(args: List(String)) {
case find(long, short, args) {
#(None, _) -> Ok(default)
#(Some(_), Some(arg)) ->
parser(arg)
|> result.map_error(fn(_) { MalformedArgument(long, arg) })
#(Some(_), None) -> Error(MissingArgument(long))
}
}
}
// Bool opts are special because they can be specified without an argument.
fn bool_opt_parser(
long: String,
short: Option(String),
) -> fn(List(String)) -> Result(Bool, Reason) {
fn(args: List(String)) {
let #(opt, arg) = find(long, short, args)
case opt {
None -> Ok(False)
Some(_) ->
case arg {
None -> Ok(True)
Some(arg) ->
parse_bool(arg)
|> result.map_error(fn(_) { MalformedArgument(long, arg) })
}
}
}
}
// Find an option named "--long" or "-short" in the list of arguments.
// If the string contains an equals sign, the option has an argument.
fn find(
long: String,
short: Option(String),
args: List(String),
) -> #(Option(String), Option(String)) {
let long_opt = "--" <> long
let short_opt =
short
|> option.map(fn(s) { "-" <> s })
let opt =
list.find(
args,
fn(arg) {
let is_long = string.starts_with(arg, long_opt)
let is_short =
short_opt
|> option.map(fn(s) { string.starts_with(arg, s) })
|> option.unwrap(False)
is_long || is_short
},
)
case opt {
Error(_) -> #(None, None)
Ok(opt) -> {
let arg =
string.split(opt, "=")
|> list.at(1)
#(Some(opt), option.from_result(arg))
}
}
}