Module shell (2)
This commit is contained in:
parent
898822d78f
commit
8e5ad41b11
|
@ -0,0 +1,127 @@
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitea.zaclys.com/bvaudour/gob/format"
|
||||||
|
. "gitea.zaclys.com/bvaudour/gob/option"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Parser est une interface pour parser des arguments en ligne de commande.
|
||||||
|
type Parser interface {
|
||||||
|
Parse([]string) Result[[]string]
|
||||||
|
IsParsed() bool
|
||||||
|
Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper est une interface permettant d’afficher l’aide d’une ligne de commande.
|
||||||
|
type Helper interface {
|
||||||
|
Name() string
|
||||||
|
Usage() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command représente une commande à saisir.
|
||||||
|
type Command interface {
|
||||||
|
Parser
|
||||||
|
Helper
|
||||||
|
Help() string
|
||||||
|
Flags() FlagSet
|
||||||
|
Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommandSet est une commande regroupant plusieurs sous-commandes.
|
||||||
|
type CommandSet interface {
|
||||||
|
Parser
|
||||||
|
Helper
|
||||||
|
Help() string
|
||||||
|
Commands() []Command
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommandHelp retourne l’aide sur une commande
|
||||||
|
func CommandHelp(c Command) string {
|
||||||
|
h := c.Help()
|
||||||
|
if h != "" {
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString(c.Usage())
|
||||||
|
sb.WriteByte('\n')
|
||||||
|
sb.WriteString(format.Apply("Usage:", "bold", "underline"))
|
||||||
|
sb.WriteByte(' ')
|
||||||
|
sb.WriteString(format.Apply(c.Name(), "l_yellow"))
|
||||||
|
sb.WriteString(format.Apply(" <options> <args>", "yellow"))
|
||||||
|
sb.WriteByte('\n')
|
||||||
|
if fs := c.Flags(); fs != nil {
|
||||||
|
sb.WriteByte('\n')
|
||||||
|
sb.WriteString(fs.Help())
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommandSetHelp retourne l’aide sur un ensemble de commandes
|
||||||
|
func CommandSetHelp(cs CommandSet) string {
|
||||||
|
h := cs.Help()
|
||||||
|
if h != "" {
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString(cs.Usage())
|
||||||
|
sb.WriteByte('\n')
|
||||||
|
sb.WriteString(format.Apply("Usage:", "bold", "underline"))
|
||||||
|
sb.WriteByte(' ')
|
||||||
|
sb.WriteString(format.Apply(cs.Name(), "l_yellow"))
|
||||||
|
sb.WriteString(format.Apply(" <cmd> <args>", "yellow"))
|
||||||
|
sb.WriteString("\n\n")
|
||||||
|
sb.WriteString(format.Apply("Available commands:", "bold", "l_red"))
|
||||||
|
sb.WriteByte('\n')
|
||||||
|
|
||||||
|
var (
|
||||||
|
cmds = cs.Commands()
|
||||||
|
lines [][]string
|
||||||
|
n int
|
||||||
|
hasHelp bool
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, c := range cmds {
|
||||||
|
hasHelp = hasHelp || c.Name() == "help"
|
||||||
|
cmd, usage := c.Name(), strings.Split(c.Usage(), "\n")
|
||||||
|
if len(cmd) > n {
|
||||||
|
n = len(cmd)
|
||||||
|
}
|
||||||
|
for i, l := range usage {
|
||||||
|
columns := make([]string, 2)
|
||||||
|
if i == 0 {
|
||||||
|
columns[0] = cmd
|
||||||
|
}
|
||||||
|
columns[1] = l
|
||||||
|
lines = append(lines, columns)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasHelp {
|
||||||
|
cmd, usage := "help", "Print this help"
|
||||||
|
if len(cmd) > n {
|
||||||
|
n = len(cmd)
|
||||||
|
}
|
||||||
|
lines = append(lines, []string{cmd, usage})
|
||||||
|
}
|
||||||
|
|
||||||
|
f, c := format.NewAlignment(format.LEFT, n), format.ColorOf("l_yellow")
|
||||||
|
for _, columns := range lines {
|
||||||
|
cmd, usage := columns[0], columns[1]
|
||||||
|
sb.WriteString(format.Tab(2))
|
||||||
|
if cmd == "" {
|
||||||
|
sb.WriteString(f.Format(cmd))
|
||||||
|
} else {
|
||||||
|
sb.WriteString(c.Format(f.Format(cmd)))
|
||||||
|
}
|
||||||
|
sb.WriteString(format.Tab(1))
|
||||||
|
sb.WriteString(usage)
|
||||||
|
sb.WriteByte('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
package flag
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
. "gitea.zaclys.com/bvaudour/gob/option"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Arg représente un argument de commande
|
||||||
|
type Arg struct {
|
||||||
|
name string
|
||||||
|
usage string
|
||||||
|
value *[]string
|
||||||
|
nb int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse parse l’argument à partir de l’entrée fournie
|
||||||
|
// et retourne les éléments restant à parser.
|
||||||
|
// Si le parsage échoue, une erreur est retournée.
|
||||||
|
func (a *Arg) Parse(args []string) (next Result[[]string]) {
|
||||||
|
l := len(args)
|
||||||
|
switch a.nb {
|
||||||
|
case -1:
|
||||||
|
*a.value = make([]string, l)
|
||||||
|
copy(*a.value, args)
|
||||||
|
case 0:
|
||||||
|
if l > 0 {
|
||||||
|
next, *a.value = Ok(args[1:]), []string{args[0]}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if l < a.nb {
|
||||||
|
next = Err[[]string](fmt.Errorf("Not enough args for %s", a.name))
|
||||||
|
} else {
|
||||||
|
*a.value = make([]string, a.nb)
|
||||||
|
copy(*a.value, args[:a.nb])
|
||||||
|
next = Ok(args[a.nb:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsParsed retourne vrai si l’argument a déjà été parsé.
|
||||||
|
func (a *Arg) IsParsed() bool {
|
||||||
|
return len(*a.value) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset réinitialise le parsage.
|
||||||
|
func (a *Arg) Reset() {
|
||||||
|
*a.value = []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name retourne le nom de l’argument.
|
||||||
|
func (a *Arg) Name() string {
|
||||||
|
return a.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage retourne l’aide sur l’utilisation de l’argument.
|
||||||
|
func (a *Arg) Usage() string {
|
||||||
|
return a.usage
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewArg retourne un nouvel argument avec son nom, sa description
|
||||||
|
// le nombre d’éléments attendu et un pointeur vers la valeur parsée.
|
||||||
|
//
|
||||||
|
// Si nbargs < 0, l’argument peut recevoir un nombre d’éléments indéterminé.
|
||||||
|
// Si nbargs = 0, l’argument est facultatif.
|
||||||
|
func NewArg(name, usage string, nbargs int, value *[]string) *Arg {
|
||||||
|
if nbargs < 0 {
|
||||||
|
nbargs = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Arg{
|
||||||
|
name: name,
|
||||||
|
usage: usage,
|
||||||
|
value: value,
|
||||||
|
nb: nbargs,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,321 @@
|
||||||
|
package flag
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitea.zaclys.com/bvaudour/gob/convert"
|
||||||
|
. "gitea.zaclys.com/bvaudour/gob/option"
|
||||||
|
"gitea.zaclys.com/bvaudour/gob/shell/command"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConversionFunc est une fonction de conversion.
|
||||||
|
//
|
||||||
|
// Les éléments retournés sont :
|
||||||
|
// - next : liste des éléments restants à parser
|
||||||
|
// - value : la valeur parsée
|
||||||
|
// - parsed : le nombre d’éléments parsés
|
||||||
|
type ConversionFunc[T any] func([]string, *flag[T]) (Result[[]string], T, int)
|
||||||
|
|
||||||
|
type callbackFunc[T any] func([]string, string) (Result[[]string], T, int)
|
||||||
|
|
||||||
|
// Flag représente un argument nommé de commande.
|
||||||
|
type Flag interface {
|
||||||
|
command.Flag
|
||||||
|
capacity() int
|
||||||
|
nbParsed() int
|
||||||
|
def() any
|
||||||
|
}
|
||||||
|
|
||||||
|
type flag[T any] struct {
|
||||||
|
name string
|
||||||
|
flags []string
|
||||||
|
fval string
|
||||||
|
usage string
|
||||||
|
multiple int
|
||||||
|
value *T
|
||||||
|
defaultval T
|
||||||
|
parsed int
|
||||||
|
conv ConversionFunc[T]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse parse l’argument à partir de l’entrée fournie
|
||||||
|
// et retourne les éléments restant à parser.
|
||||||
|
// Si le parsage échoue, une erreur est retournée.
|
||||||
|
func (f *flag[T]) Parse(args []string) (next Result[[]string]) {
|
||||||
|
var value T
|
||||||
|
var parsed int
|
||||||
|
if next, value, parsed = f.conv(args, f); next.IsOk() {
|
||||||
|
f.value = &value
|
||||||
|
f.parsed += parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsParsed retourne vrai si l’argument a déjà été parsé.
|
||||||
|
func (f *flag[T]) IsParsed() bool {
|
||||||
|
return f.parsed > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset réinitialise le parsage.
|
||||||
|
func (f *flag[T]) Reset() {
|
||||||
|
f.parsed = 0
|
||||||
|
f.value = &f.defaultval
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name retourne le nom principal de l’argument nommé.
|
||||||
|
func (f *flag[T]) Name() string {
|
||||||
|
return f.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage retourne l’aide sur l’usage du flag
|
||||||
|
func (f *flag[T]) Usage() string {
|
||||||
|
return f.usage
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArgName retorune le nom donné à l’argument suivant le flag.
|
||||||
|
func (f *flag[T]) ArgName() string {
|
||||||
|
s := f.fval
|
||||||
|
switch f.multiple {
|
||||||
|
case -1:
|
||||||
|
s += " (...)"
|
||||||
|
case 0:
|
||||||
|
s += " (optional)"
|
||||||
|
case 1:
|
||||||
|
default:
|
||||||
|
s = fmt.Sprintf("%s (%d)", s, f.multiple)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.TrimSpace(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flags retourne tous les flags disponibles identifiant le même flag.
|
||||||
|
func (f *flag[T]) Flags() []string {
|
||||||
|
return f.flags
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value retourne la valeur du flag.
|
||||||
|
func (f *flag[T]) Value() any {
|
||||||
|
return *f.value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *flag[T]) capacity() int {
|
||||||
|
return f.multiple
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *flag[T]) nbParsed() int {
|
||||||
|
return f.parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *flag[T]) def() any {
|
||||||
|
return f.defaultval
|
||||||
|
}
|
||||||
|
|
||||||
|
func optional(fac bool) int {
|
||||||
|
if fac {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func nbArgs(n int) int {
|
||||||
|
if n < 1 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkArgLen(args []string, name string) (err Option[error]) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
err = Some(fmt.Errorf("argument needed for flag %s", name))
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkCapacity(l, nb int, name string) (err Option[error]) {
|
||||||
|
if nb > 0 && l >= nb {
|
||||||
|
err = Some(fmt.Errorf("overflow value for flag %s", name))
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertArg[T any](args []string, name string, v *T) (next Result[[]string], parsed int) {
|
||||||
|
if convert.Convert(args[0], v) {
|
||||||
|
next, parsed = Ok(args[1:]), 1
|
||||||
|
} else {
|
||||||
|
next = Err[[]string](fmt.Errorf("%s: invalid value for flag %s", args[0], name))
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func genericArg[T any](args []string, name string) (next Result[[]string], value T, parsed int) {
|
||||||
|
chck := checkArgLen(args, name)
|
||||||
|
if err, ok := chck.Get(); ok {
|
||||||
|
next = Err[[]string](err)
|
||||||
|
} else {
|
||||||
|
next, parsed = convertArg(args, name, &value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringArg(args []string, name string) (next Result[[]string], value string, parsed int) {
|
||||||
|
chck := checkArgLen(args, name)
|
||||||
|
if err, ok := chck.Get(); ok {
|
||||||
|
next = Err[[]string](err)
|
||||||
|
} else {
|
||||||
|
next, value, parsed = Ok(args[1:]), args[0], 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func boolArg(args []string, name string) (next Result[[]string], val bool, parsed int) {
|
||||||
|
chck := checkArgLen(args, name)
|
||||||
|
if err, ok := chck.Get(); ok {
|
||||||
|
next = Err[[]string](err)
|
||||||
|
} else {
|
||||||
|
next, val, parsed = Ok(args), true, 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func singleParse[T any](cb callbackFunc[T]) ConversionFunc[T] {
|
||||||
|
return func(args []string, f *flag[T]) (next Result[[]string], value T, parsed int) {
|
||||||
|
return cb(args, f.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func multipleParse[T any](cb callbackFunc[T]) ConversionFunc[[]T] {
|
||||||
|
return func(args []string, f *flag[[]T]) (next Result[[]string], value []T, parsed int) {
|
||||||
|
l, n := len(*f.value), f.multiple
|
||||||
|
chck := checkCapacity(l, n, f.name)
|
||||||
|
if err, ok := chck.Get(); ok {
|
||||||
|
next = Err[[]string](err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]T, l+1)
|
||||||
|
next, result[l], parsed = cb(args, f.name)
|
||||||
|
args, ok := next.Ok()
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(result[:l], *f.value)
|
||||||
|
if n > 0 {
|
||||||
|
n--
|
||||||
|
}
|
||||||
|
|
||||||
|
for len(args) > 0 && n != 0 {
|
||||||
|
var v T
|
||||||
|
var p int
|
||||||
|
next, v, p = cb(args, f.name)
|
||||||
|
if args, ok = next.Ok(); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, v)
|
||||||
|
parsed += p
|
||||||
|
|
||||||
|
if n > 0 {
|
||||||
|
n--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value = result
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New retourne un nouvel argument nommé avec :
|
||||||
|
// - son nom,
|
||||||
|
// - son usage,
|
||||||
|
// - le nom donné à l’argument qui suit
|
||||||
|
// - un pointeur vers sa valeur
|
||||||
|
// - sa valeur par défaut si non parsé
|
||||||
|
// - son nombre d’arguments (indéfini si -1, optionnel si 0)
|
||||||
|
// - sa fonction de conversion
|
||||||
|
// - des alias supplémentaires
|
||||||
|
func New[T any](
|
||||||
|
name, usage, argname string,
|
||||||
|
value *T,
|
||||||
|
defaultvalue T,
|
||||||
|
nb int,
|
||||||
|
conv ConversionFunc[T],
|
||||||
|
others ...string,
|
||||||
|
) Flag {
|
||||||
|
t := convert.TypeOf(value).Elem()
|
||||||
|
if nb < 0 || nb > 1 && !t.Is(convert.Slice) {
|
||||||
|
panic("value must be a pointer of slice")
|
||||||
|
}
|
||||||
|
|
||||||
|
f := &flag[T]{
|
||||||
|
name: name,
|
||||||
|
flags: append([]string{name}, others...),
|
||||||
|
fval: argname,
|
||||||
|
usage: usage,
|
||||||
|
multiple: nb,
|
||||||
|
value: value,
|
||||||
|
defaultval: defaultvalue,
|
||||||
|
conv: conv,
|
||||||
|
}
|
||||||
|
f.Reset()
|
||||||
|
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// Number retourne un argument nommé de type nombre.
|
||||||
|
func Number[T convert.NumberType](name, usage string, value *T, defaultvalue T, fac bool, others ...string) Flag {
|
||||||
|
argname, nb := "<number>", optional(fac)
|
||||||
|
conv := singleParse(genericArg[T])
|
||||||
|
return New(name, usage, argname, value, defaultvalue, nb, conv, others...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String retourne un argument nommé de type chaîne de caractères.
|
||||||
|
func String(name, usage string, value *string, defaultvalue string, fac bool, others ...string) Flag {
|
||||||
|
argname, nb := "<string>", optional(fac)
|
||||||
|
conv := singleParse(stringArg)
|
||||||
|
return New(name, usage, argname, value, defaultvalue, nb, conv, others...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bool retourne un argument nommé sans argument supplémentaire.
|
||||||
|
func Bool(name, usage string, value *bool, defaultvalue bool, fac bool, others ...string) Flag {
|
||||||
|
argname, nb := "", optional(fac)
|
||||||
|
conv := singleParse(boolArg)
|
||||||
|
return New(name, usage, argname, value, defaultvalue, nb, conv, others...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NumberSlice retourne un argument nommé de type tableau de nombres.
|
||||||
|
func NumberSlice[T convert.NumberType](name, usage string, nb int, value *[]T, defaultvalue []T, others ...string) Flag {
|
||||||
|
argname, nb := "<int>", nbArgs(nb)
|
||||||
|
conv := multipleParse(genericArg[T])
|
||||||
|
return New(name, usage, argname, value, defaultvalue, nb, conv, others...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringSlice retourne un argument nommé de type tableau de strings.
|
||||||
|
func StringSlice(name, usage string, nb int, value *[]string, defaultvalue []string, others ...string) Flag {
|
||||||
|
argname, nb := "<string>", nbArgs(nb)
|
||||||
|
conv := multipleParse(stringArg)
|
||||||
|
return New(name, usage, argname, value, defaultvalue, nb, conv, others...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BoolSlice retourne un argument nommé de type tableau de booléens
|
||||||
|
func BoolSlice(name, usage string, nb int, value *[]bool, defaultvalue []bool, others ...string) Flag {
|
||||||
|
argname, nb := "", nbArgs(nb)
|
||||||
|
conv := func(args []string, f *flag[[]bool]) (next Result[[]string], val []bool, parsed int) {
|
||||||
|
l := len(*f.value)
|
||||||
|
val = make([]bool, l+1)
|
||||||
|
copy(val[:l], *f.value)
|
||||||
|
next, val[l], parsed = Ok(args), true, 1
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return New(name, usage, argname, value, defaultvalue, nb, conv, others...)
|
||||||
|
}
|
|
@ -0,0 +1,432 @@
|
||||||
|
package flag
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitea.zaclys.com/bvaudour/gob/collection"
|
||||||
|
"gitea.zaclys.com/bvaudour/gob/format"
|
||||||
|
. "gitea.zaclys.com/bvaudour/gob/option"
|
||||||
|
"gitea.zaclys.com/bvaudour/gob/shell/command"
|
||||||
|
)
|
||||||
|
|
||||||
|
func formatLines(sb *strings.Builder, lines [][]string) {
|
||||||
|
n := 0
|
||||||
|
for _, columns := range lines {
|
||||||
|
l := len([]rune(columns[0]))
|
||||||
|
if l > n {
|
||||||
|
n = l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f, c := format.NewAlignment(format.LEFT, n), format.ColorOf("l_yellow")
|
||||||
|
for _, columns := range lines {
|
||||||
|
left, right := columns[0], columns[1]
|
||||||
|
sb.WriteString(format.Tab(2))
|
||||||
|
if left == "" {
|
||||||
|
sb.WriteString(f.Format(left))
|
||||||
|
} else {
|
||||||
|
sb.WriteString(c.Format(f.Format(left)))
|
||||||
|
}
|
||||||
|
sb.WriteString(format.Tab(1))
|
||||||
|
sb.WriteString(right)
|
||||||
|
sb.WriteByte('\n')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlagSet et un ensemble de flags
|
||||||
|
type FlagSet struct {
|
||||||
|
flags []Flag
|
||||||
|
fmap map[string]Flag
|
||||||
|
args []*Arg
|
||||||
|
unix bool
|
||||||
|
parsed bool
|
||||||
|
helpNeeded bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FlagSet) checkFlagsCapacity(args []string) Result[[]string] {
|
||||||
|
for _, flg := range fs.flags {
|
||||||
|
c, p := flg.capacity(), flg.nbParsed()
|
||||||
|
if c > 0 && p != c {
|
||||||
|
return Err[[]string](fmt.Errorf("Mismatch number of args for flag %s: required (%d), has (%d)", flg.Name(), c, p))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FlagSet) checkArgsCapacity(args []string) Result[[]string] {
|
||||||
|
for _, arg := range fs.args {
|
||||||
|
if arg.nb > 0 && len(*arg.value) < arg.nb {
|
||||||
|
return Err[[]string](fmt.Errorf("Not enough args for %s", arg.name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FlagSet) parseUnix(args []string) (next Result[[]string]) {
|
||||||
|
var (
|
||||||
|
notFlags, unconsumedArgs []string
|
||||||
|
f string
|
||||||
|
flg Flag
|
||||||
|
ok bool
|
||||||
|
)
|
||||||
|
|
||||||
|
// Décomposition des arguments raccourcis.
|
||||||
|
args = formatArgs(args)
|
||||||
|
|
||||||
|
// On sépare ce qui fait parti des options nommées des options non nommées.
|
||||||
|
for i, e := range args {
|
||||||
|
if e == "--" {
|
||||||
|
args, notFlags = args[:i], args[i+1:]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := collection.NewSet[string]()
|
||||||
|
for f := range fs.fmap {
|
||||||
|
flags.Add(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// On vérifie que l’aide est demandée.
|
||||||
|
for _, e := range args {
|
||||||
|
switch e {
|
||||||
|
case "-h":
|
||||||
|
if !flags.Contains("-h") {
|
||||||
|
fs.parsed, fs.helpNeeded = true, true
|
||||||
|
return Ok(args[:0])
|
||||||
|
}
|
||||||
|
case "--help":
|
||||||
|
if !flags.Contains("--help") {
|
||||||
|
fs.parsed, fs.helpNeeded = true, true
|
||||||
|
return Ok(args[:0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parsage des flags
|
||||||
|
for len(args) > 0 {
|
||||||
|
f = args[0]
|
||||||
|
if flg, ok = fs.fmap[f]; !ok {
|
||||||
|
if isUnixFlag(f) {
|
||||||
|
return Err[[]string](fmt.Errorf("Invalid flag: %s", f))
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
nextFlagIdx := len(args)
|
||||||
|
for i, e := range args {
|
||||||
|
if flags.Contains(e) {
|
||||||
|
nextFlagIdx = i + 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next = flg.Parse(args[:nextFlagIdx])
|
||||||
|
if unconsumedArgs, ok = next.Ok(); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(unconsumedArgs, args[nextFlagIdx:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if containsUnixFlag(args) {
|
||||||
|
return Err[[]string](fmt.Errorf("Not a flag: %s", args[0]))
|
||||||
|
}
|
||||||
|
return Ok(append(args, notFlags...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FlagSet) parseNotUnix(args []string) (next Result[[]string]) {
|
||||||
|
var (
|
||||||
|
unconsumedArgs []string
|
||||||
|
flg Flag
|
||||||
|
ok bool
|
||||||
|
)
|
||||||
|
|
||||||
|
flags := collection.NewSet[string]()
|
||||||
|
for f := range fs.fmap {
|
||||||
|
flags.Add(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parsage des flags
|
||||||
|
for len(args) > 0 {
|
||||||
|
f := args[0]
|
||||||
|
if flg, ok = fs.fmap[f]; !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
nextFlagIdx := len(args)
|
||||||
|
for i, e := range args {
|
||||||
|
if flags.Contains(e) {
|
||||||
|
nextFlagIdx = i + 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next = flg.Parse(args[:nextFlagIdx])
|
||||||
|
if unconsumedArgs, ok = next.Ok(); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(unconsumedArgs, args[nextFlagIdx:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// On vérifie que les arguments restants ne sont pas des flags.
|
||||||
|
if flags.ContainsOneOf(args...) {
|
||||||
|
return Err[[]string](fmt.Errorf("Not a flag: %s", args[0]))
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FlagSet) parseArgs(args []string) (next Result[[]string]) {
|
||||||
|
var (
|
||||||
|
i int
|
||||||
|
ok bool
|
||||||
|
)
|
||||||
|
|
||||||
|
for len(args) > 0 && i < len(fs.args) {
|
||||||
|
next = fs.args[i].Parse(args)
|
||||||
|
if args, ok = next.Ok(); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
// S’il reste des arguments à parser, on lève une erreur
|
||||||
|
if len(args) > 0 {
|
||||||
|
return Err[[]string](fmt.Errorf("Too much args given"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse parse les arguments pour alimenter le flagset.
|
||||||
|
// Il retourne la liste des arguments restant à parser et une erreur éventuelle
|
||||||
|
// si le parsage a échoué.
|
||||||
|
func (fs *FlagSet) Parse(args []string) (next Result[[]string]) {
|
||||||
|
var ok bool
|
||||||
|
|
||||||
|
// Parsage des flags
|
||||||
|
if fs.unix {
|
||||||
|
next = fs.parseUnix(args)
|
||||||
|
} else {
|
||||||
|
next = fs.parseNotUnix(args)
|
||||||
|
}
|
||||||
|
if args, ok = next.Ok(); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// On vérifie que les flags ont la bonne capacité.
|
||||||
|
next = fs.checkFlagsCapacity(args)
|
||||||
|
if args, ok = next.Ok(); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parsage des arguments
|
||||||
|
next = fs.parseArgs(args)
|
||||||
|
if args, ok = next.Ok(); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// On vérifie que les arguments ont la bonne capacité.
|
||||||
|
next = fs.checkArgsCapacity(args)
|
||||||
|
|
||||||
|
// Si tout est bon, on marque le set comme parsé.
|
||||||
|
if args, ok = next.Ok(); ok {
|
||||||
|
fs.parsed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsParsed retourne vrai si le flagset a été parsé.
|
||||||
|
func (fs *FlagSet) IsParsed() bool {
|
||||||
|
return fs.parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset réinitialise l’état du flagset, comme s’il n’avait
|
||||||
|
// jamais été parsé.
|
||||||
|
func (fs *FlagSet) Reset() {
|
||||||
|
fs.helpNeeded = false
|
||||||
|
fs.parsed = false
|
||||||
|
|
||||||
|
for _, f := range fs.flags {
|
||||||
|
f.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, a := range fs.args {
|
||||||
|
a.Reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HelpRequired retourne vrai si le parsage contient un flag de type aide.
|
||||||
|
func (fs *FlagSet) HelpRequired() bool {
|
||||||
|
return fs.helpNeeded
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flags retourne la liste des flags disponibles dans le flagset.
|
||||||
|
func (fs *FlagSet) Flags() []command.Flag {
|
||||||
|
out := make([]command.Flag, len(fs.flags))
|
||||||
|
for i, f := range fs.flags {
|
||||||
|
out[i] = f
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Args retourne la valeur des arguments non nommés.
|
||||||
|
func (fs *FlagSet) Args() []string {
|
||||||
|
var out []string
|
||||||
|
for _, a := range fs.args {
|
||||||
|
out = append(out, *a.value...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FlagSet) helpUnix(sb *strings.Builder) {
|
||||||
|
var (
|
||||||
|
lines [][]string
|
||||||
|
hl, hs bool
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, f := range fs.flags {
|
||||||
|
var shorts, longs []string
|
||||||
|
for _, f := range f.Flags() {
|
||||||
|
if isShort(f) {
|
||||||
|
hs = hs || f != "-h"
|
||||||
|
shorts = append(shorts, f)
|
||||||
|
} else {
|
||||||
|
hl = hl || f != "--help"
|
||||||
|
longs = append(longs, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
flags = strings.Join(append(shorts, longs...), ",")
|
||||||
|
name, def = f.ArgName(), f.def()
|
||||||
|
arg string
|
||||||
|
)
|
||||||
|
if name == "" {
|
||||||
|
arg = fmt.Sprintf("Arg: (%s) [default: %v]", name, def)
|
||||||
|
} else {
|
||||||
|
arg = fmt.Sprintf("[default: %v]", def)
|
||||||
|
}
|
||||||
|
lines = append(lines, []string{flags, format.Apply(arg, "italic")})
|
||||||
|
for _, u := range strings.Split(f.Usage(), "\n") {
|
||||||
|
lines = append(lines, []string{"", u})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hs || hl {
|
||||||
|
lines = append(lines, []string{"-h,--help", "Print this help"})
|
||||||
|
}
|
||||||
|
|
||||||
|
formatLines(sb, lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FlagSet) helpFlags(sb *strings.Builder) {
|
||||||
|
var lines [][]string
|
||||||
|
for _, f := range fs.flags {
|
||||||
|
name := fmt.Sprintf("%s %s", f.Name(), f.ArgName())
|
||||||
|
def := fmt.Sprintf("default: %v", f.def())
|
||||||
|
lines = append(lines, []string{name, format.Apply(def, "italic")})
|
||||||
|
for _, u := range strings.Split(f.Usage(), "\n") {
|
||||||
|
lines = append(lines, []string{"", u})
|
||||||
|
}
|
||||||
|
aliases := f.Flags()
|
||||||
|
if l := len(aliases); l > 1 {
|
||||||
|
lines = append(lines, []string{"", format.Apply(fmt.Sprintf("other flags: %s", strings.Join(aliases[:l-1], ",")), "italic", "yellow")})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
formatLines(sb, lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FlagSet) helpArgs(sb *strings.Builder) {
|
||||||
|
var lines [][]string
|
||||||
|
for _, arg := range fs.args {
|
||||||
|
name, usage := arg.name, strings.Split(arg.usage, "\n")
|
||||||
|
for i, line := range usage {
|
||||||
|
columns := make([]string, 2)
|
||||||
|
if i == 0 {
|
||||||
|
columns[0] = name
|
||||||
|
}
|
||||||
|
columns[1] = line
|
||||||
|
lines = append(lines, columns)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
formatLines(sb, lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Help retourne l’aide complète sur le flagset.
|
||||||
|
func (fs *FlagSet) Help() string {
|
||||||
|
var sb strings.Builder
|
||||||
|
|
||||||
|
if len(fs.flags) > 0 {
|
||||||
|
sb.WriteString(format.Apply("Options:\n", "bold", "l_red"))
|
||||||
|
if fs.unix {
|
||||||
|
(fs.helpUnix(&sb))
|
||||||
|
} else {
|
||||||
|
fs.helpFlags(&sb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fs.args) > 0 {
|
||||||
|
if len(fs.flags) > 0 {
|
||||||
|
sb.WriteByte('\n')
|
||||||
|
}
|
||||||
|
sb.WriteString(format.Apply("Arguments:\n", "bold", "l_red"))
|
||||||
|
fs.helpArgs(&sb)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSet retourne un nouveau set de flags.
|
||||||
|
// Si unix, les arguments nommés doivent être de type
|
||||||
|
// unix : -n pour les noms courts et --name pour les noms longs.
|
||||||
|
func NewSet(unix bool) *FlagSet {
|
||||||
|
return &FlagSet{
|
||||||
|
unix: unix,
|
||||||
|
fmap: make(map[string]Flag),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add ajoute des flags au flagset.
|
||||||
|
func (fs *FlagSet) Add(flags ...Flag) *FlagSet {
|
||||||
|
for _, f := range flags {
|
||||||
|
for _, e := range f.Flags() {
|
||||||
|
if fs.unix && !isUnixFlag(e) {
|
||||||
|
panic(fmt.Sprintf("Flag %s is not an unix flag", e))
|
||||||
|
}
|
||||||
|
if _, exists := fs.fmap[e]; exists {
|
||||||
|
panic(fmt.Sprintf("Flag %s is already defined", e))
|
||||||
|
}
|
||||||
|
fs.fmap[e] = f
|
||||||
|
}
|
||||||
|
fs.flags = append(fs.flags, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddArgs ajoute des arguments au flagset.
|
||||||
|
func (fs *FlagSet) AddArgs(args ...*Arg) *FlagSet {
|
||||||
|
var lastArg *Arg
|
||||||
|
if l := len(fs.args); l > 0 {
|
||||||
|
lastArg = fs.args[l-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, a := range args {
|
||||||
|
if lastArg != nil && lastArg.nb < 1 {
|
||||||
|
panic("facultative argument must be unique and last located")
|
||||||
|
}
|
||||||
|
lastArg = a
|
||||||
|
}
|
||||||
|
fs.args = append(fs.args, args...)
|
||||||
|
|
||||||
|
return fs
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
package flag
|
||||||
|
|
||||||
|
func isShort(e string) bool {
|
||||||
|
re := []rune(e)
|
||||||
|
|
||||||
|
return e != "--" && len(re) == 2 && re[0] == '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
func isLong(e string) bool {
|
||||||
|
return len(e) > 2 && e[:2] == "--"
|
||||||
|
}
|
||||||
|
|
||||||
|
func isCombined(e string) bool {
|
||||||
|
re := []rune(e)
|
||||||
|
|
||||||
|
return len(re) > 2 && re[0] != '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
func isUnixFlag(e string) bool {
|
||||||
|
return isShort(e) || isLong(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatArg(e string) []string {
|
||||||
|
if isCombined(e) {
|
||||||
|
var out []string
|
||||||
|
for _, r := range e[1:] {
|
||||||
|
out = append(out, string([]rune{'-', r}))
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
return []string{e}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatArgs(args []string) []string {
|
||||||
|
var (
|
||||||
|
out []string
|
||||||
|
ignore bool
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, e := range args {
|
||||||
|
if ignore {
|
||||||
|
out = append(out, e)
|
||||||
|
} else {
|
||||||
|
ignore = e == "--"
|
||||||
|
out = append(out, formatArg(e)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsUnixFlag(args []string) bool {
|
||||||
|
for _, arg := range args {
|
||||||
|
if isUnixFlag(arg) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package command
|
||||||
|
|
||||||
|
// Flag est une interface représentant des arguments nommés d’une ligne de commande.
|
||||||
|
type Flag interface {
|
||||||
|
Parser
|
||||||
|
Helper
|
||||||
|
ArgName() string
|
||||||
|
Flags() []string
|
||||||
|
Value() any
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlagSet est un ensemble de flags.
|
||||||
|
type FlagSet interface {
|
||||||
|
Parser
|
||||||
|
Help() string
|
||||||
|
HelpRequired() bool
|
||||||
|
Flags() []Flag
|
||||||
|
Args() []string
|
||||||
|
}
|
Loading…
Reference in New Issue