From 8e5ad41b1112f586ac0aefea7f9ef8dc1751ecec Mon Sep 17 00:00:00 2001 From: Benjamin VAUDOUR Date: Thu, 5 Oct 2023 10:42:40 +0200 Subject: [PATCH] Module shell (2) --- shell/command/command.go | 127 ++++++++++ shell/command/flag/arg.go | 79 +++++++ shell/command/flag/flag.go | 321 +++++++++++++++++++++++++ shell/command/flag/flagset.go | 432 ++++++++++++++++++++++++++++++++++ shell/command/flag/format.go | 61 +++++ shell/command/option.go | 19 ++ 6 files changed, 1039 insertions(+) create mode 100644 shell/command/command.go create mode 100644 shell/command/flag/arg.go create mode 100644 shell/command/flag/flag.go create mode 100644 shell/command/flag/flagset.go create mode 100644 shell/command/flag/format.go create mode 100644 shell/command/option.go diff --git a/shell/command/command.go b/shell/command/command.go new file mode 100644 index 0000000..1bbb8b6 --- /dev/null +++ b/shell/command/command.go @@ -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(" ", "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(" ", "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() +} diff --git a/shell/command/flag/arg.go b/shell/command/flag/arg.go new file mode 100644 index 0000000..229bd08 --- /dev/null +++ b/shell/command/flag/arg.go @@ -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, + } +} diff --git a/shell/command/flag/flag.go b/shell/command/flag/flag.go new file mode 100644 index 0000000..a2fe85f --- /dev/null +++ b/shell/command/flag/flag.go @@ -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 := "", 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 := "", 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 := "", 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 := "", 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...) +} diff --git a/shell/command/flag/flagset.go b/shell/command/flag/flagset.go new file mode 100644 index 0000000..73230d9 --- /dev/null +++ b/shell/command/flag/flagset.go @@ -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 +} diff --git a/shell/command/flag/format.go b/shell/command/flag/format.go new file mode 100644 index 0000000..994864e --- /dev/null +++ b/shell/command/flag/format.go @@ -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 +} diff --git a/shell/command/option.go b/shell/command/option.go new file mode 100644 index 0000000..4813c27 --- /dev/null +++ b/shell/command/option.go @@ -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 +}