diff --git a/cmd/gitmirror/main.go b/cmd/gitmirror/main.go index 8133ff9..87ad164 100644 --- a/cmd/gitmirror/main.go +++ b/cmd/gitmirror/main.go @@ -8,5 +8,5 @@ import ( ) func main() { - os.Exit(cli.Gitmirror(os.Args)) + os.Exit(cli.Gitmirror(os.Args[1:])) } diff --git a/go.mod b/go.mod index b3b3c52..0c982cf 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module github.com/telemachus/gitmirror go 1.23.4 -require github.com/google/go-cmp v0.6.0 +require ( + github.com/MakeNowJust/heredoc v1.0.0 + github.com/google/go-cmp v0.6.0 +) diff --git a/go.sum b/go.sum index 5a8d551..2559825 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,4 @@ +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= diff --git a/internal/cli/app.go b/internal/cli/app.go index a0605ce..380c241 100644 --- a/internal/cli/app.go +++ b/internal/cli/app.go @@ -8,7 +8,7 @@ import ( "path/filepath" "strings" - "github.com/telemachus/gitmirror/internal/optionparser" + "github.com/telemachus/gitmirror/internal/flag" ) type appEnv struct { @@ -18,29 +18,41 @@ type appEnv struct { home string storage string exitVal int + help bool quiet bool + version bool } func appFrom(args []string) (*appEnv, error) { app := &appEnv{cmd: cmd, exitVal: exitSuccess} - op := optionparser.NewOptionParser() - op.On("-c", "--config FILE", "Use FILE as config file (default ~/.gitmirror.json)", &app.config) - op.On("-q", "--quiet", "Print only error messages", &app.quiet) - op.On("--version", "Print version and exit", version) - op.Command("clone", "Clone git repositories using `git clone --mirror`") - op.Command("update|up", "Update git repositories using `git remote update`") - op.Command("sync", "Run both update and clone (in that order)") - op.Start = 24 - op.Banner = "Usage: gitmirror [options] " - op.Coda = "\nFor more information or to file a bug report visit https://github.com/telemachus/gitmirror" + fs := flag.NewFlagSet("gitmirror") + fs.StringVar(&app.config, "config", "", "") + fs.StringVar(&app.config, "c", "", "") + fs.BoolVar(&app.help, "help", false, "") + fs.BoolVar(&app.help, "h", false, "") + fs.BoolVar(&app.quiet, "quiet", false, "") + fs.BoolVar(&app.quiet, "q", false, "") + fs.BoolVar(&app.version, "version", false, "") - // Do not continue if we cannot parse and validate arguments or get the - // user's home directory. - if err := op.ParseFrom(args); err != nil { + if err := fs.Parse(args); err != nil { return nil, err } - if err := validate(op.Extra); err != nil { + + // Quick and dirty, but why be fancy in these cases? + if app.help { + fmt.Print(_usage) + os.Exit(exitSuccess) + } + if app.version { + fmt.Printf("%s %s\n", cmd, cmdVersion) + os.Exit(exitSuccess) + } + + // Do not continue if we cannot parse and validate arguments or get the + // user's home directory. + extraArgs := fs.Args() + if err := validate(extraArgs); err != nil { return nil, err } home, err := os.UserHomeDir() @@ -52,7 +64,7 @@ func appFrom(args []string) (*appEnv, error) { app.config = filepath.Join(home, config) } app.storage = filepath.Join(home, storage) - app.subCmd = op.Extra[0] + app.subCmd = extraArgs[0] return app, nil } @@ -83,9 +95,3 @@ func validate(extra []string) error { return nil } - -// Quick and dirty, but why be fancy in this case? -func version() { - fmt.Printf("%s %s\n", cmd, cmdVersion) - os.Exit(exitSuccess) -} diff --git a/internal/cli/usage.go b/internal/cli/usage.go new file mode 100644 index 0000000..63ecfe9 --- /dev/null +++ b/internal/cli/usage.go @@ -0,0 +1,21 @@ +package cli + +import "github.com/MakeNowJust/heredoc" + +var _usage = heredoc.Docf(` + usage: gitmirror [options] [options] + + Options + -c, --config=FILE Use FILE as config file (default ~/.gitmirror.json) + -q, --quiet Print only error messages + + -h, --help Print this help and exit + --version Print version and exit + + Subcommands + clone Clone git repositories using %[1]sgit clone --mirror%[1]s + update|up Update git repositories using %[1]sgit remote update%[1]s + sync Run both update and clone (in that order) + + For more information or to file a bug report visit https://github.com/telemachus/gitmirror + `, "`") diff --git a/internal/flag/flag.go b/internal/flag/flag.go new file mode 100644 index 0000000..0cdf969 --- /dev/null +++ b/internal/flag/flag.go @@ -0,0 +1,732 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Copyright 2025 Peter Aronoff. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package flag implements command-line flag parsing. + +# Usage + +Define flags using [flag.String], [Bool], [Int], etc. + +This declares an integer flag, -n, stored in the pointer nFlag, with type *int: + + import "flag" + var nFlag = flag.Int("n", 1234, "help message for flag n") + +If you like, you can bind the flag to a variable using the Var() functions. + + var flagvar int + func init() { + flag.IntVar(&flagvar, "flagname", 1234, "help message for flagname") + } + +Or you can create custom flags that satisfy the Value interface (with +pointer receivers) and couple them to flag parsing by + + flag.Var(&flagVal, "name", "help message for flagname") + +For such flags, the default value is just the initial value of the variable. + +After all flags are defined, call + + flag.Parse() + +to parse the command line into the defined flags. + +Flags may then be used directly. If you're using the flags themselves, +they are all pointers; if you bind to variables, they're values. + + fmt.Println("ip has value ", *ip) + fmt.Println("flagvar has value ", flagvar) + +After parsing, the arguments following the flags are available as the +slice [flag.Args] or individually as [flag.Arg](i). +The arguments are indexed from 0 through [flag.NArg]-1. + +# Command line flag syntax + +The following forms are permitted: + + -flag + --flag // double dashes are also permitted + -flag=x + -flag x // non-boolean flags only + +One or two dashes may be used; they are equivalent. +The last form is not permitted for boolean flags because the +meaning of the command + + cmd -x * + +where * is a Unix shell wildcard, will change if there is a file +called 0, false, etc. You must use the -flag=false form to turn +off a boolean flag. + +Flag parsing stops just before the first non-flag argument +("-" is a non-flag argument) or after the terminator "--". + +Integer flags accept 1234, 0664, 0x1234 and may be negative. +Boolean flags may be: + + 1, 0, t, f, T, F, true, false, TRUE, FALSE, True, False + +Duration flags accept any input valid for time.ParseDuration. + +The default set of command-line flags is controlled by +top-level functions. The [FlagSet] type allows one to define +independent sets of flags, such as to implement subcommands +in a command-line interface. The methods of [FlagSet] are +analogous to the top-level functions for the command-line +flag set. +*/ +package flag + +import ( + "errors" + "fmt" + "runtime" + "slices" + "strconv" + "strings" + "time" +) + +// errParse is returned by Set if a flag's value fails to parse, such as with an invalid integer for Int. +// It then gets wrapped through failf to provide more information. +var errParse = errors.New("parse error") + +// errRange is returned by Set if a flag's value is out of range. +// It then gets wrapped through failf to provide more information. +var errRange = errors.New("value out of range") + +func numError(err error) error { + ne, ok := err.(*strconv.NumError) + if !ok { + return err + } + if ne.Err == strconv.ErrSyntax { + return errParse + } + if ne.Err == strconv.ErrRange { + return errRange + } + return err +} + +// -- bool Value +type boolValue bool + +func newBoolValue(val bool, p *bool) *boolValue { + *p = val + return (*boolValue)(p) +} + +func (b *boolValue) Set(s string) error { + v, err := strconv.ParseBool(s) + if err != nil { + err = errParse + } + *b = boolValue(v) + return err +} + +func (b *boolValue) Get() any { return bool(*b) } + +func (b *boolValue) String() string { return strconv.FormatBool(bool(*b)) } + +func (b *boolValue) IsBoolFlag() bool { return true } + +// optional interface to indicate boolean flags that can be +// supplied without "=value" text +type boolFlag interface { + Value + IsBoolFlag() bool +} + +// -- int Value +type intValue int + +func newIntValue(val int, p *int) *intValue { + *p = val + return (*intValue)(p) +} + +func (i *intValue) Set(s string) error { + v, err := strconv.ParseInt(s, 0, strconv.IntSize) + if err != nil { + err = numError(err) + } + *i = intValue(v) + return err +} + +func (i *intValue) Get() any { return int(*i) } + +func (i *intValue) String() string { return strconv.Itoa(int(*i)) } + +// -- int64 Value +type int64Value int64 + +func newInt64Value(val int64, p *int64) *int64Value { + *p = val + return (*int64Value)(p) +} + +func (i *int64Value) Set(s string) error { + v, err := strconv.ParseInt(s, 0, 64) + if err != nil { + err = numError(err) + } + *i = int64Value(v) + return err +} + +func (i *int64Value) Get() any { return int64(*i) } + +func (i *int64Value) String() string { return strconv.FormatInt(int64(*i), 10) } + +// -- uint Value +type uintValue uint + +func newUintValue(val uint, p *uint) *uintValue { + *p = val + return (*uintValue)(p) +} + +func (i *uintValue) Set(s string) error { + v, err := strconv.ParseUint(s, 0, strconv.IntSize) + if err != nil { + err = numError(err) + } + *i = uintValue(v) + return err +} + +func (i *uintValue) Get() any { return uint(*i) } + +func (i *uintValue) String() string { return strconv.FormatUint(uint64(*i), 10) } + +// -- uint64 Value +type uint64Value uint64 + +func newUint64Value(val uint64, p *uint64) *uint64Value { + *p = val + return (*uint64Value)(p) +} + +func (i *uint64Value) Set(s string) error { + v, err := strconv.ParseUint(s, 0, 64) + if err != nil { + err = numError(err) + } + *i = uint64Value(v) + return err +} + +func (i *uint64Value) Get() any { return uint64(*i) } + +func (i *uint64Value) String() string { return strconv.FormatUint(uint64(*i), 10) } + +// -- string Value +type stringValue string + +func newStringValue(val string, p *string) *stringValue { + *p = val + return (*stringValue)(p) +} + +func (s *stringValue) Set(val string) error { + *s = stringValue(val) + return nil +} + +func (s *stringValue) Get() any { return string(*s) } + +func (s *stringValue) String() string { return string(*s) } + +// -- float64 Value +type float64Value float64 + +func newFloat64Value(val float64, p *float64) *float64Value { + *p = val + return (*float64Value)(p) +} + +func (f *float64Value) Set(s string) error { + v, err := strconv.ParseFloat(s, 64) + if err != nil { + err = numError(err) + } + *f = float64Value(v) + return err +} + +func (f *float64Value) Get() any { return float64(*f) } + +func (f *float64Value) String() string { return strconv.FormatFloat(float64(*f), 'g', -1, 64) } + +// -- time.Duration Value +type durationValue time.Duration + +func newDurationValue(val time.Duration, p *time.Duration) *durationValue { + *p = val + return (*durationValue)(p) +} + +func (d *durationValue) Set(s string) error { + v, err := time.ParseDuration(s) + if err != nil { + err = errParse + } + *d = durationValue(v) + return err +} + +func (d *durationValue) Get() any { return time.Duration(*d) } + +func (d *durationValue) String() string { return (*time.Duration)(d).String() } + +// -- func Value +type funcValue func(string) error + +func (f funcValue) Set(s string) error { return f(s) } + +func (f funcValue) String() string { return "" } + +// -- boolFunc Value +type boolFuncValue func(string) error + +func (f boolFuncValue) Set(s string) error { return f(s) } + +func (f boolFuncValue) String() string { return "" } + +func (f boolFuncValue) IsBoolFlag() bool { return true } + +// Value is the interface to the dynamic value stored in a flag. +// (The default value is represented as a string.) +// +// If a Value has an IsBoolFlag() bool method returning true, +// the command-line parser makes -name equivalent to -name=true +// rather than using the next command-line argument. +// +// Set is called once, in command line order, for each flag present. +// The flag package may call the [String] method with a zero-valued receiver, +// such as a nil pointer. +type Value interface { + String() string + Set(string) error +} + +// Getter is an interface that allows the contents of a [Value] to be retrieved. +// It wraps the [Value] interface, rather than being part of it, because it +// appeared after Go 1 and its compatibility rules. All [Value] types provided +// by this package satisfy the [Getter] interface, except the type used by [Func]. +type Getter interface { + Value + Get() any +} + +// A FlagSet represents a set of defined flags. The zero value of a FlagSet +// has no name and has [ContinueOnError] error handling. +// +// [Flag] names must be unique within a FlagSet. An attempt to define a flag whose +// name is already in use will cause a panic. +type FlagSet struct { + name string + parsed bool + actual map[string]*Flag + formal map[string]*Flag + args []string // arguments after flags + undef map[string]string // flags which didn't exist at the time of Set +} + +// A Flag represents the state of a flag. +type Flag struct { + Name string // name as it appears on command line + Usage string // help message + Value Value // value as set +} + +// sortFlags returns the flags as a slice in lexicographical sorted order. +func sortFlags(flags map[string]*Flag) []*Flag { + result := make([]*Flag, len(flags)) + i := 0 + for _, f := range flags { + result[i] = f + i++ + } + slices.SortFunc(result, func(a, b *Flag) int { + return strings.Compare(a.Name, b.Name) + }) + return result +} + +// Name returns the name of the flag set. +func (f *FlagSet) Name() string { + return f.name +} + +// VisitAll visits the flags in lexicographical order, calling fn for each. +// It visits all flags, even those not set. +func (f *FlagSet) VisitAll(fn func(*Flag)) { + for _, flag := range sortFlags(f.formal) { + fn(flag) + } +} + +// Visit visits the flags in lexicographical order, calling fn for each. +// It visits only those flags that have been set. +func (f *FlagSet) Visit(fn func(*Flag)) { + for _, flag := range sortFlags(f.actual) { + fn(flag) + } +} + +// Lookup returns the [Flag] structure of the named flag, returning nil if none exists. +func (f *FlagSet) Lookup(name string) *Flag { + return f.formal[name] +} + +// Set sets the value of the named flag. +func (f *FlagSet) Set(name, value string) error { + return f.set(name, value) +} + +func (f *FlagSet) set(name, value string) error { + flag, ok := f.formal[name] + if !ok { + // Remember that a flag that isn't defined is being set. + // We return an error in this case, but in addition if + // subsequently that flag is defined, we want to panic + // at the definition point. + // This is a problem which occurs if both the definition + // and the Set call are in init code and for whatever + // reason the init code changes evaluation order. + // See issue 57411. + _, file, line, ok := runtime.Caller(2) + if !ok { + file = "?" + line = 0 + } + if f.undef == nil { + f.undef = map[string]string{} + } + f.undef[name] = fmt.Sprintf("%s:%d", file, line) + + return fmt.Errorf("no such flag -%v", name) + } + err := flag.Value.Set(value) + if err != nil { + return err + } + if f.actual == nil { + f.actual = make(map[string]*Flag) + } + f.actual[name] = flag + return nil +} + +// NFlag returns the number of flags that have been set. +func (f *FlagSet) NFlag() int { return len(f.actual) } + +// Arg returns the i'th argument. Arg(0) is the first remaining argument +// after flags have been processed. Arg returns an empty string if the +// requested element does not exist. +func (f *FlagSet) Arg(i int) string { + if i < 0 || i >= len(f.args) { + return "" + } + return f.args[i] +} + +// NArg is the number of arguments remaining after flags have been processed. +func (f *FlagSet) NArg() int { return len(f.args) } + +// Args returns the non-flag arguments. +func (f *FlagSet) Args() []string { return f.args } + +// BoolVar defines a bool flag with specified name, default value, and usage string. +// The argument p points to a bool variable in which to store the value of the flag. +func (f *FlagSet) BoolVar(p *bool, name string, value bool, usage string) { + f.Var(newBoolValue(value, p), name, usage) +} + +// Bool defines a bool flag with specified name, default value, and usage string. +// The return value is the address of a bool variable that stores the value of the flag. +func (f *FlagSet) Bool(name string, value bool, usage string) *bool { + p := new(bool) + f.BoolVar(p, name, value, usage) + return p +} + +// IntVar defines an int flag with specified name, default value, and usage string. +// The argument p points to an int variable in which to store the value of the flag. +func (f *FlagSet) IntVar(p *int, name string, value int, usage string) { + f.Var(newIntValue(value, p), name, usage) +} + +// Int defines an int flag with specified name, default value, and usage string. +// The return value is the address of an int variable that stores the value of the flag. +func (f *FlagSet) Int(name string, value int, usage string) *int { + p := new(int) + f.IntVar(p, name, value, usage) + return p +} + +// Int64Var defines an int64 flag with specified name, default value, and usage string. +// The argument p points to an int64 variable in which to store the value of the flag. +func (f *FlagSet) Int64Var(p *int64, name string, value int64, usage string) { + f.Var(newInt64Value(value, p), name, usage) +} + +// Int64 defines an int64 flag with specified name, default value, and usage string. +// The return value is the address of an int64 variable that stores the value of the flag. +func (f *FlagSet) Int64(name string, value int64, usage string) *int64 { + p := new(int64) + f.Int64Var(p, name, value, usage) + return p +} + +// UintVar defines a uint flag with specified name, default value, and usage string. +// The argument p points to a uint variable in which to store the value of the flag. +func (f *FlagSet) UintVar(p *uint, name string, value uint, usage string) { + f.Var(newUintValue(value, p), name, usage) +} + +// Uint defines a uint flag with specified name, default value, and usage string. +// The return value is the address of a uint variable that stores the value of the flag. +func (f *FlagSet) Uint(name string, value uint, usage string) *uint { + p := new(uint) + f.UintVar(p, name, value, usage) + return p +} + +// Uint64Var defines a uint64 flag with specified name, default value, and usage string. +// The argument p points to a uint64 variable in which to store the value of the flag. +func (f *FlagSet) Uint64Var(p *uint64, name string, value uint64, usage string) { + f.Var(newUint64Value(value, p), name, usage) +} + +// Uint64 defines a uint64 flag with specified name, default value, and usage string. +// The return value is the address of a uint64 variable that stores the value of the flag. +func (f *FlagSet) Uint64(name string, value uint64, usage string) *uint64 { + p := new(uint64) + f.Uint64Var(p, name, value, usage) + return p +} + +// StringVar defines a string flag with specified name, default value, and usage string. +// The argument p points to a string variable in which to store the value of the flag. +func (f *FlagSet) StringVar(p *string, name string, value string, usage string) { + f.Var(newStringValue(value, p), name, usage) +} + +// String defines a string flag with specified name, default value, and usage string. +// The return value is the address of a string variable that stores the value of the flag. +func (f *FlagSet) String(name string, value string, usage string) *string { + p := new(string) + f.StringVar(p, name, value, usage) + return p +} + +// Float64Var defines a float64 flag with specified name, default value, and usage string. +// The argument p points to a float64 variable in which to store the value of the flag. +func (f *FlagSet) Float64Var(p *float64, name string, value float64, usage string) { + f.Var(newFloat64Value(value, p), name, usage) +} + +// Float64 defines a float64 flag with specified name, default value, and usage string. +// The return value is the address of a float64 variable that stores the value of the flag. +func (f *FlagSet) Float64(name string, value float64, usage string) *float64 { + p := new(float64) + f.Float64Var(p, name, value, usage) + return p +} + +// DurationVar defines a time.Duration flag with specified name, default value, and usage string. +// The argument p points to a time.Duration variable in which to store the value of the flag. +// The flag accepts a value acceptable to time.ParseDuration. +func (f *FlagSet) DurationVar(p *time.Duration, name string, value time.Duration, usage string) { + f.Var(newDurationValue(value, p), name, usage) +} + +// Duration defines a time.Duration flag with specified name, default value, and usage string. +// The return value is the address of a time.Duration variable that stores the value of the flag. +// The flag accepts a value acceptable to time.ParseDuration. +func (f *FlagSet) Duration(name string, value time.Duration, usage string) *time.Duration { + p := new(time.Duration) + f.DurationVar(p, name, value, usage) + return p +} + +// Func defines a flag with the specified name and usage string. +// Each time the flag is seen, fn is called with the value of the flag. +// If fn returns a non-nil error, it will be treated as a flag value parsing error. +func (f *FlagSet) Func(name, usage string, fn func(string) error) { + f.Var(funcValue(fn), name, usage) +} + +// BoolFunc defines a flag with the specified name and usage string without requiring values. +// Each time the flag is seen, fn is called with the value of the flag. +// If fn returns a non-nil error, it will be treated as a flag value parsing error. +func (f *FlagSet) BoolFunc(name, usage string, fn func(string) error) { + f.Var(boolFuncValue(fn), name, usage) +} + +// Var defines a flag with the specified name and usage string. The type and +// value of the flag are represented by the first argument, of type [Value], which +// typically holds a user-defined implementation of [Value]. For instance, the +// caller could create a flag that turns a comma-separated string into a slice +// of strings by giving the slice the methods of [Value]; in particular, [Set] would +// decompose the comma-separated string into the slice. +func (f *FlagSet) Var(value Value, name string, usage string) { + // Flag must not begin "-" or contain "=". + if strings.HasPrefix(name, "-") { + panic(f.sprintf("flag %q begins with -", name)) + } else if strings.Contains(name, "=") { + panic(f.sprintf("flag %q contains =", name)) + } + + // Remember the default value as a string; it won't change. + flag := &Flag{name, usage, value} + _, alreadythere := f.formal[name] + if alreadythere { + var msg string + if f.name == "" { + msg = f.sprintf("flag redefined: %s", name) + } else { + msg = f.sprintf("%s flag redefined: %s", f.name, name) + } + panic(msg) // Happens only if flags are declared with identical names + } + if pos := f.undef[name]; pos != "" { + panic(fmt.Sprintf("flag %s set at %s before being defined", name, pos)) + } + if f.formal == nil { + f.formal = make(map[string]*Flag) + } + f.formal[name] = flag +} + +// sprintf formats the message and returns it. +func (f *FlagSet) sprintf(format string, a ...any) string { + msg := fmt.Sprintf(format, a...) + return msg +} + +// failf prints to standard error a formatted error and usage message and +// returns the error. +func (f *FlagSet) failf(format string, a ...any) error { + msg := f.sprintf(format, a...) + return errors.New(msg) +} + +// parseOne parses one flag. It reports whether a flag was seen. +func (f *FlagSet) parseOne() (bool, error) { + if len(f.args) == 0 { + return false, nil + } + s := f.args[0] + if len(s) < 2 || s[0] != '-' { + return false, nil + } + numMinuses := 1 + if s[1] == '-' { + numMinuses++ + if len(s) == 2 { // "--" terminates the flags + f.args = f.args[1:] + return false, nil + } + } + name := s[numMinuses:] + if len(name) == 0 || name[0] == '-' || name[0] == '=' { + return false, f.failf("bad flag syntax: %s", s) + } + + // it's a flag. does it have an argument? + f.args = f.args[1:] + hasValue := false + value := "" + for i := 1; i < len(name); i++ { // equals cannot be first + if name[i] == '=' { + value = name[i+1:] + hasValue = true + name = name[0:i] + break + } + } + + flag, ok := f.formal[name] + if !ok { + return false, f.failf("flag provided but not defined: -%s", name) + } + + if fv, ok := flag.Value.(boolFlag); ok && fv.IsBoolFlag() { // special case: doesn't need an arg + if hasValue { + if err := fv.Set(value); err != nil { + return false, f.failf("invalid boolean value %q for -%s: %v", value, name, err) + } + } else { + if err := fv.Set("true"); err != nil { + return false, f.failf("invalid boolean flag %s: %v", name, err) + } + } + } else { + // It must have a value, which might be the next argument. + if !hasValue && len(f.args) > 0 { + // value is the next arg + hasValue = true + value, f.args = f.args[0], f.args[1:] + } + if !hasValue { + return false, f.failf("flag needs an argument: -%s", name) + } + if err := flag.Value.Set(value); err != nil { + return false, f.failf("invalid value %q for flag -%s: %v", value, name, err) + } + } + if f.actual == nil { + f.actual = make(map[string]*Flag) + } + f.actual[name] = flag + return true, nil +} + +// Parse parses flag definitions from the argument list, which should not +// include the command name. Must be called after all flags in the [FlagSet] +// are defined and before flags are accessed by the program. +// The return value will be [ErrHelp] if -help or -h were set but not defined. +func (f *FlagSet) Parse(arguments []string) error { + f.parsed = true + f.args = arguments + for { + seen, err := f.parseOne() + if seen { + continue + } + if err == nil { + break + } + return err + } + return nil +} + +// Parsed reports whether f.Parse has been called. +func (f *FlagSet) Parsed() bool { + return f.parsed +} + +// NewFlagSet returns a new, empty flag set with the specified name. +func NewFlagSet(name string) *FlagSet { + f := &FlagSet{name: name} + return f +} + +// Init sets the name for a flag set. +// By default, the zero [FlagSet] uses an empty name. +func (f *FlagSet) Init(name string) { + f.name = name +} diff --git a/internal/optionparser/.gitignore b/internal/optionparser/.gitignore deleted file mode 100644 index 8365624..0000000 --- a/internal/optionparser/.gitignore +++ /dev/null @@ -1,23 +0,0 @@ -# Compiled Object files, Static and Dynamic libs (Shared Objects) -*.o -*.a -*.so - -# Folders -_obj -_test - -# Architecture specific extensions/prefixes -*.[568vq] -[568vq].out - -*.cgo1.go -*.cgo2.c -_cgo_defun.c -_cgo_gotypes.go -_cgo_export.* - -_testmain.go - -*.exe -*.test diff --git a/internal/optionparser/LICENSE b/internal/optionparser/LICENSE deleted file mode 100644 index a4b6d90..0000000 --- a/internal/optionparser/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2024 Peter Aronoff -Copyright (c) 2014–2023 speedata - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/internal/optionparser/README.md b/internal/optionparser/README.md deleted file mode 100644 index a621f37..0000000 --- a/internal/optionparser/README.md +++ /dev/null @@ -1,132 +0,0 @@ -[![GoDoc](https://pkg.go.dev/badge/github.com/speedata/optionparser)](https://pkg.go.dev/github.com/speedata/optionparser) - - -optionparser -============ - -Mature command line arguments processor for Go. - -Inspired by Ruby's (OptionParser) command line arguments processor. - -Installation ------------- - - go get github.com/speedata/optionparser - -Usage ------ - - op := optionparser.NewOptionParser() - op.On(arguments ...interface{}) - ... - err := op.Parse() - -where `arguments` is one of: - - * `"-a"`: a short argument - * `"--argument"`: a long argument - * `"--argument [FOO]"` a long argument with an optional parameter - * `"--argument FOO"` a long argument with a mandatory parameter - * `"Some text"`: The description text for the command line parameter - * `&aboolean`: Set the given boolean to `true` if the argument is given, set to `false` if parameter is prefixed with `no-`, such as `--no-foo`. - * `&astring`: Set the string to the value of the given parameter - * `function`: Call the function. The function must have the signature `func()`. - * `map[string]string`: Set an entry of the map to the value of the given parameter and the key of the argument. - * `[]string` Set the slice values to a comma separated list. - -Help usage ----------- - -The options `-h` and `--help` are included by default. The example below output this on `cmd -h`: - - Usage: [parameter] command - -h, --help Show this help - -a, --func call myfunc - --bstring=FOO set string to FOO - -c set boolean option (try -no-c) - -d, --dlong=VAL set option - -e, --elong[=VAL] set option with optional parameter - -f boolean option - - Commands - y Run command y - z Run command z - -Settings --------- - -After calling `op := optionparser.NewOptionParser()` you can set `op.Banner` to set the first line of the help output. The default value is `Usage: [parameter] command`. - -To control the first and last column of the help output, set `op.Start` and `op.Stop`. The default values are the integer values of 30 and 79. - - -Example usage -------------- - -```go -package main - -import ( - "fmt" - "log" - - "github.com/speedata/optionparser" -) - -func myfunc() { - fmt.Println("myfunc called") -} - -func main() { - var somestring string - var truefalse bool - options := make(map[string]string) - stringslice := []string{} - - op := optionparser.NewOptionParser() - op.On("-a", "--func", "call myfunc", myfunc) - op.On("--bstring FOO", "set string to FOO", &somestring) - op.On("-c", "set boolean option (try -no-c)", options) - op.On("-d", "--dlong VAL", "set option", options) - op.On("-e", "--elong [VAL]", "set option with optional parameter", options) - op.On("-f", "boolean option", &truefalse) - op.On("-g VALUES", "give multiple values", &stringslice) - - op.Command("y", "Run command y") - op.Command("z", "Run command z") - - err := op.Parse() - if err != nil { - log.Fatal(err) - } - fmt.Printf("string `somestring' is now %q\n", somestring) - fmt.Printf("options %v\n", options) - fmt.Printf("-f %v\n", truefalse) - fmt.Printf("-g %v\n", stringslice) - fmt.Printf("Extra: %#v\n", op.Extra) -} -``` - -and the output of `go run main.go -a --bstring foo -c -d somevalue -e x -f -g a,b,c y z` - -is: - - myfunc called - string `somestring' is now "foo" - options map[c:true dlong:somevalue elong:x] - -f true - -g [a b c] - Extra: []string{"y", "z"} - - - -**State**: Actively maintained, and used in production. Without warranty, of course.
-**Maturity level**: 5/5 (works well in all tested repositories, there will be no API change)
-**License**: Free software (MIT License)
-**Installation**: Just run `go get github.com/speedata/optionparser`
-**API documentation**: https://pkg.go.dev/github.com/speedata/optionparser
-**Contact**: , [@speedata@typo.social](https://typo.social/@speedata)
-**Repository**: https://github.com/speedata/optionparser
-**Dependencies**: None
-**Contribution**: We like to get any kind of feedback (success stories, bug reports, merge requests, ...) - diff --git a/internal/optionparser/optionparser.go b/internal/optionparser/optionparser.go deleted file mode 100644 index dd80fb1..0000000 --- a/internal/optionparser/optionparser.go +++ /dev/null @@ -1,454 +0,0 @@ -// Package optionparser is a library for defining and parsing command line -// options. It aims to provide a natural language interface for defining short -// and long parameters and mandatory and optional arguments. It provides the -// user for nice output formatting on the built in method '--help'. -package optionparser - -import ( - "fmt" - "os" - "regexp" - "strings" -) - -// A command is a non-dash option (with a helptext) -type command struct { - name string - helptext string -} - -// OptionParser contains the methods to parse options and the settings to -// influence the output of --help. Set the Banner and Coda for usage info, set -// Start and Stop for output of the long description text. -type OptionParser struct { - Extra []string - Banner string - Coda string - Start int - Stop int - options []*allowedOptions - short map[string]*allowedOptions - long map[string]*allowedOptions - commands []command -} - -type argumentDescription struct { - argument string - param string - optional bool - short bool - negate bool -} - -type allowedOptions struct { - optional bool - param string - short string - long string - boolParameter bool - function func(string) - functionNoArgs func() - boolvalue *bool - stringvalue *string - stringmap map[string]string - stringslice *[]string - helptext string -} - -// Return true if s starts with a dash ('-s' for example) -func isOption(s string) bool { - io := regexp.MustCompile("^-") - return io.MatchString(s) -} - -func wordwrap(s string, wd int) []string { - // if the string is shorter than the width, we can just return it - if len(s) <= wd { - return []string{s} - } - - // Otherwise, we return the first part - // split at the last occurrence of space before wd - stop := strings.LastIndex(s[0:wd], " ") - - // no space found in the next wd characters, impossible to split - if stop < 0 { - stop = strings.Index(s, " ") - if stop < 0 { // no space found in the remaining characters - return []string{s} - } - } - a := []string{s[0:stop]} - j := wordwrap(s[stop+1:], wd) - return append(a, j...) -} - -// Analyze the given argument such as '-s' or 'foo=bar' and -// return an argumentDescription -func splitOn(arg string) *argumentDescription { - var ( - argument string - param string - optional bool - short bool - negate bool - ) - - doubleDash := regexp.MustCompile("^--") - singleDash := regexp.MustCompile("^-[^-]") - - if doubleDash.MatchString(arg) { - short = false - } else if singleDash.MatchString(arg) { - short = true - } else { - panic("can't happen") - } - - var init int - if short { - init = 1 - } else { - init = 2 - } - if len(arg) > init+2 { - if arg[init:init+3] == "no-" { - negate = true - init = init + 3 - } - } - - re := regexp.MustCompile("[ =]") - loc := re.FindStringIndex(arg) - if len(loc) == 0 { - // no optional parameter, we know everything we need to know - return &argumentDescription{ - argument: arg[init:], - optional: false, - short: short, - negate: negate, - } - } - - // Now we know that the option requires an argument, it could be optional - argument = arg[init:loc[0]] - pos := loc[1] - length := len(arg) - - if arg[loc[1]:loc[1]+1] == "[" { - pos++ - length-- - optional = true - } else { - optional = false - } - param = arg[pos:length] - - a := argumentDescription{ - argument, - param, - optional, - short, - negate, - } - return &a -} - -// prints the nice help output -func formatAndOutput(start int, stop int, dashShort string, short string, comma string, dashLong string, long string, lines []string) { - if long == "" && len(short) > 2 { - formatString := fmt.Sprintf("%%-1s%%-%d.%ds %%s\n", start-3, stop-3) - // the formatString now looks like this: "%-1s%-2s%1s %-2s%-22.71s %s" - fmt.Printf(formatString, dashShort, short, lines[0]) - } else { - formatString := fmt.Sprintf("%%-1s%%-1s%%1s %%-2s%%-%d.%ds %%s\n", start-8, stop-8) - // the formatString now looks like this: "%-1s%-2s%1s %-2s%-22.71s %s" - fmt.Printf(formatString, dashShort, short, comma, dashLong, long, lines[0]) - } - if len(lines) > 0 { - formatString := fmt.Sprintf("%%%ds%%s\n", start-1) - for i := 1; i < len(lines); i++ { - fmt.Printf(formatString, " ", lines[i]) - } - } -} - -func set(obj *allowedOptions, hasNoPrefix bool, param string) { - if obj.function != nil { - obj.function(param) - } - if obj.stringvalue != nil { - *obj.stringvalue = param - } - if obj.stringmap != nil { - var name string - var value string - switch { - case obj.long != "": - name = obj.long - case obj.short != "": - name = obj.short - } - // return error if no name given - - if param != "" { - value = param - } else { - if hasNoPrefix { - value = "false" - } else { - value = "true" - } - } - obj.stringmap[name] = value - } - if obj.stringslice != nil { - eachParam := strings.Split(param, ",") - *obj.stringslice = append(*obj.stringslice, eachParam...) - } - if obj.functionNoArgs != nil { - obj.functionNoArgs() - } - if obj.boolvalue != nil { - if hasNoPrefix { - *obj.boolvalue = false - } else { - *obj.boolvalue = true - } - } -} - -// Command defines optional arguments to the command line. These are written in -// a separate section called 'Commands' on --help. -func (op *OptionParser) Command(cmd string, helptext string) { - cmds := command{cmd, helptext} - op.commands = append(op.commands, cmds) -} - -// On defines arguments and parameters. Each argument is one of: -// - a short option, such as "-x", -// - a long option, such as "--extra", -// - a long option with an argument such as "--extra FOO" (or "--extra=FOO") for a mandatory argument, -// - a long option with an argument in brackets, e.g. "--extra [FOO]" for a parameter with optional argument, -// - a string (not starting with "-") used for the parameter description, e.g. "This parameter does this and that", -// - a string variable in the form of &str that is used for saving the result of the argument, -// - a variable of type map[string]string which is used to store the result (the parameter name is the key, the value is either the string true or the argument given on the command line) -// - a variable of type *[]string which gets a comma separated list of values, -// - a bool variable (in the form &bool) to hold a boolean value, or -// - a function in the form of func() or in the form of func(string) which gets called if the command line parameter is found. -// -// On panics if the user supplies is an type in its argument other the ones -// given above. -// -// op := optionparser.NewOptionParser() -// op.On("-a", "--func", "call myfunc", myfunc) -// op.On("--bstring FOO", "set string to FOO", &somestring) -// op.On("-c", "set boolean option (try -no-c)", options) -// op.On("-d", "--dlong VAL", "set option", options) -// op.On("-e", "--elong [VAL]", "set option with optional parameter", options) -// op.On("-f", "boolean option", &truefalse) -// op.On("-g VALUES", "give multiple values", &stringslice) -// -// and running the program with --help gives the following output: -// -// go run main.go --help -// Usage: [parameter] command -// -h, --help Print this help and exit -// -a, --func call myfunc -// --bstring=FOO set string to FOO -// -c set boolean option (try -no-c) -// -d, --dlong=VAL set option -// -e, --elong[=VAL] set option with optional parameter -// -f boolean option -// -g=VALUES give multiple values -func (op *OptionParser) On(a ...interface{}) { - option := new(allowedOptions) - op.options = append(op.options, option) - for _, i := range a { - switch x := i.(type) { - case string: - // a short option, a long option or a help text - if isOption(x) { - ret := splitOn(x) - if ret.short { - // short argument ('-s') - op.short[ret.argument] = option - option.short = ret.argument - } else { - // long argument ('--something') - op.long[ret.argument] = option - option.long = ret.argument - } - if ret.optional { - option.optional = true - } - if ret.param != "" { - option.param = ret.param - } - if ret.negate { - option.boolParameter = true - } - } else { - // a string, probably the help text - option.helptext = x - } - case func(string): - option.function = x - case func(): - option.functionNoArgs = x - case *bool: - option.boolvalue = x - case *string: - option.stringvalue = x - case map[string]string: - option.stringmap = x - case *[]string: - option.stringslice = x - default: - panic(fmt.Sprintf("Unknown parameter type: %#T\n", x)) - } - } -} - -// ParseFrom takes a slice of string arguments and interprets them. If it finds -// an unknown option or a missing mandatory argument, it returns an error. -func (op *OptionParser) ParseFrom(args []string) error { - i := 1 - for i < len(args) { - switch { - // Users can pass -- to mark the end of flag parsing. This - // check must come first since isOption will treat -- as - // a malformed option. - case args[i] == "--": - op.Extra = append(op.Extra, args[i+1:]...) - return nil - case isOption(args[i]): - ret := splitOn(args[i]) - - var option *allowedOptions - if ret.short { - option = op.short[ret.argument] - } else { - option = op.long[ret.argument] - } - - if option == nil { - return fmt.Errorf("unknown option %s", ret.argument) - } - - // the parameter in ret.param is only set by `splitOn()` when used with - // the equal sign: "--foo=bar". If the user gives a parameter with "--foo bar" - // it is not in ret.param. So we look at the next thing in our args array - // and if its not a parameter (starting with `-`), we take this as the perhaps - // optional parameter - if ret.param == "" && i < len(args)-1 && !isOption(args[i+1]) { - // next could be a parameter - ret.param = args[i+1] - // delete this possible parameter from the args list - args = append(args[:i+1], args[i+2:]...) - } - - if ret.param != "" { - if option.param != "" { - // OK, we've got a parameter and we expect one - set(option, ret.negate, ret.param) - } else { - // we've got a parameter but didn't expect one, - // so let's push it onto the stack - op.Extra = append(op.Extra, ret.param) - set(option, ret.negate, "") - } - } else { - // no parameter found - if option.param != "" { - // parameter expected - if !option.optional { - // No parameter found but expected - return fmt.Errorf("parameter expected but none given %s", ret.argument) - } - } - set(option, ret.negate, "") - } - default: - // not an option, we push it onto the extra array - op.Extra = append(op.Extra, args[i]) - } - i++ - } - return nil -} - -// Parse takes the command line arguments as found in os.Args and interprets -// them. If it finds an unknown option or a missing mandatory argument, it -// returns an error. -func (op *OptionParser) Parse() error { - return op.ParseFrom(os.Args) -} - -// Help prints help text generated from the "On" commands -func (op *OptionParser) Help() { - fmt.Println(op.Banner) - wd := op.Stop - op.Start - for _, o := range op.options { - short := o.short - long := o.long - if o.boolParameter { - long = "[no-]" + o.long - } - if o.long != "" { - if o.param != "" { - if o.optional { - long = fmt.Sprintf("%s[=%s]", o.long, o.param) - } else { - long = fmt.Sprintf("%s=%s", o.long, o.param) - } - } - } else { - // short - if o.param != "" { - if o.optional { - short = fmt.Sprintf("%s[=%s]", o.short, o.param) - } else { - short = fmt.Sprintf("%s=%s", o.short, o.param) - } - } - } - dashShort := "-" - dashLong := "--" - comma := "," - if short == "" { - dashShort = "" - comma = "" - } - if long == "" { - dashLong = "" - comma = "" - } - lines := wordwrap(o.helptext, wd) - formatAndOutput(op.Start, op.Stop, dashShort, short, comma, dashLong, long, lines) - } - if len(op.commands) > 0 { - fmt.Println("\nSubcommands") - for _, cmd := range op.commands { - lines := wordwrap(cmd.helptext, wd) - formatAndOutput(op.Start, op.Stop, "", "", "", "", cmd.name, lines) - } - } - if op.Coda != "" { - fmt.Println(op.Coda) - } -} - -// NewOptionParser initializes the OptionParser struct with sane settings for -// Banner, Start and Stop and adds a "-h", "--help" option for convenience. -func NewOptionParser() *OptionParser { - a := &OptionParser{} - a.Extra = []string{} - a.Banner = "Usage: [parameter] command" - a.Start = 30 - a.Stop = 79 - a.short = map[string]*allowedOptions{} - a.long = map[string]*allowedOptions{} - a.On("-h", "--help", "Print this help and exit", func() { a.Help(); os.Exit(0) }) - return a -} diff --git a/revive.toml b/revive.toml index c2d37f8..373b56c 100644 --- a/revive.toml +++ b/revive.toml @@ -19,5 +19,5 @@ enableAllRules = true Arguments = [110] [rule.unhandled-error] - Arguments = ["fmt.Printf", "fmt.Fprintf", "fmt.Println", "fmt.Fprintln"] + Arguments = ["fmt.*"]