Module shell (2)

This commit is contained in:
Benjamin VAUDOUR 2023-10-05 10:42:40 +02:00
parent 898822d78f
commit 8e5ad41b11
6 changed files with 1039 additions and 0 deletions

127
shell/command/command.go Normal file
View File

@ -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 dafficher laide dune 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 laide 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 laide 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()
}

79
shell/command/flag/arg.go Normal file
View File

@ -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 largument à partir de lentré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 largument 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 largument.
func (a *Arg) Name() string {
return a.name
}
// Usage retourne laide sur lutilisation de largument.
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, largument peut recevoir un nombre déléments indéterminé.
// Si nbargs = 0, largument 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,
}
}

321
shell/command/flag/flag.go Normal file
View File

@ -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 largument à partir de lentré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 largument 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 largument nommé.
func (f *flag[T]) Name() string {
return f.name
}
// Usage retourne laide sur lusage du flag
func (f *flag[T]) Usage() string {
return f.usage
}
// ArgName retorune le nom donné à largument 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é à largument qui suit
// - un pointeur vers sa valeur
// - sa valeur par défaut si non parsé
// - son nombre darguments (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...)
}

View File

@ -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 laide 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++
}
// Sil 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 sil navait
// 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 laide 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
}

View File

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

19
shell/command/option.go Normal file
View File

@ -0,0 +1,19 @@
package command
// Flag est une interface représentant des arguments nommés dune 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
}