diff --git a/cmd/golist/golist.go b/cmd/golist/golist.go index ee028ae..e9c038d 100644 --- a/cmd/golist/golist.go +++ b/cmd/golist/golist.go @@ -11,9 +11,8 @@ import ( "strings" "text/template" - "github.com/urfave/cli" - "pagure.io/golist/pkg/util" + "pagure.io/golist/pkg/cli" ) var ( @@ -100,7 +99,6 @@ func main() { } app.Action = func(c *cli.Context) error { - if len(c.StringSlice("package-path")) == 0 { return fmt.Errorf("--package-path is not set") } diff --git a/pkg/cli/cli.go b/pkg/cli/cli.go new file mode 100644 index 0000000..ec91056 --- /dev/null +++ b/pkg/cli/cli.go @@ -0,0 +1,293 @@ +package cli + +/* golist uses a very small portion of functionality + from github.com/urfave/cli. This module provides + minimal substitute API implementations for only the + core functionality used by golist, for the purpose of + bootstrapping golist without additional dependencies. +*/ + +import ( + "strings" + "fmt" + "os" +) + +type String string +type StringSlice []string +type Bool bool + +type Flag interface { + name() string + usage() string +} + +type BoolFlag struct { + Name string + Usage string + Value bool +} + +type StringFlag struct { + Name string + Usage string + Value string +} + +type StringSliceFlag struct { + Name string + Usage string + Value *StringSlice +} + +type App struct { + Name string + Usage string + Version string + Flags []Flag + Action func(*Context) error + +} + +func NewApp() App { + var a App + return a +} + +func (a *App) Run(osArgs []string) error { + c, err := newContext(a.Flags, osArgs) + if err != nil { + return err + } + if c.Bool("help") { + a.PrintHelp() + os.Exit(0) + } + return a.Action(c) +} + +func (a *App) PrintHelp() { + maxNameLen := 0 + for _, flag := range a.Flags { + length := len(flag.name()) + if length > maxNameLen { + maxNameLen = length + } + } + fmtSpec := "%-" + fmt.Sprintf("%v", maxNameLen + 6) + "s\t%s\n" + fmt.Printf("%s - %s\n", a.Name, a.Usage) + fmt.Printf("Options:\n") + for _, flag := range a.Flags { + flagNameSlice := append([]string{canonicalName(flag)}, alternateNames(flag)...) + for i, _ := range flagNameSlice { + if len(flagNameSlice[i]) > 1 { + flagNameSlice[i] = fmt.Sprintf("--%s", flagNameSlice[i]) + } else { + flagNameSlice[i] = fmt.Sprintf("-%s", flagNameSlice[i]) + } + } + flagNameStr := strings.Join(flagNameSlice, ", ") + switch flag.(type) { + case StringFlag: + flagNameStr = fmt.Sprintf(" %s value", flagNameStr) + case StringSliceFlag: + flagNameStr = fmt.Sprintf(" %s value", flagNameStr) + case BoolFlag: + flagNameStr = fmt.Sprintf(" %s", flagNameStr) + } + fmt.Printf(fmtSpec, flagNameStr, flag.usage()) + } +} + + +type Context struct { + flagValues map[string]interface{} + flagDecls map[string]Flag + altFlags map[string]string + positionalArgs []string +} + +func (c *Context) Bool(flag string) bool { + iface, ok := c.flagDecls[flag] + if !ok { + panic("undefined flag" + flag) + } + switch iface.(type) { + case BoolFlag: + break + default: + panic("flag type mismatch - expected BoolFlag, got: " + flag) + } + val, ok := c.flagValues[flag] + if !ok { + return iface.(BoolFlag).Value + } + return val.(bool) +} + +func (c *Context) String(flag string) string { + iface, ok := c.flagDecls[flag] + if !ok { + panic("undefined flag" + flag) + } + switch iface.(type) { + case StringFlag: + break + default: + panic("flag type mismatch - expected StringFlag, got: " + flag) + } + val, ok:= c.flagValues[flag] + if !ok { + return iface.(StringFlag).Value + } + return val.(string) +} + +func (c *Context) StringSlice(flag string) []string { + iface, ok := c.flagDecls[flag]; + if !ok { + panic("undefined flag" + flag) + } + switch iface.(type) { + case StringSliceFlag: + break + default: + panic("flag type mismatch - expected StringSliceFlag, got: " + flag) + } + val, ok := c.flagValues[flag] + if !ok { + val = iface.(StringSliceFlag).Value + if val != nil { + return []string{} + } + } + return val.([]string) +} + +// Create a hash mapping from flag names to declarations +// and alt names to flag names. +func (c *Context) setupFlags(flagDecls []Flag) error { + helpFlag := BoolFlag { + Name: "help, h", + Usage: "Show help message", + } + flagDecls = append(flagDecls, helpFlag) + for _, flag := range flagDecls { + flagName := canonicalName(flag) + if _, ok:= c.flagDecls[flagName]; ok { + return fmt.Errorf("cannot redeclare flag: %s", flagName) + } + c.flagDecls[flagName] = flag + altFlagNames := alternateNames(flag) + for _, altName := range altFlagNames { + c.altFlags[altName] = flagName + } + } + return nil +} + +func (c *Context) parseArgs(osArgs []string) error { + // process command line arguments as a stream of tokens. + // operations consume the first token in the stream until + // the stream is empty. + argStream := osArgs + for len(argStream) > 0 { + arg := argStream[0] + if ! isFlag(arg) { + argStream = argStream[1:] + c.positionalArgs = append(c.positionalArgs, arg) + + } else { + arg = trimFlag(arg) + if _, ok:= c.altFlags[arg]; ok { + arg = c.altFlags[arg] + } + iface, ok := c.flagDecls[arg] + if !ok { + return fmt.Errorf("unexpected argument: %v", arg) + } + switch flag := iface.(type) { + case StringFlag: + argStream = argStream[1:] + if len(argStream) == 0 { + return fmt.Errorf("expected value for argument: %v", arg) + } + if isFlag(argStream[0]) { + return fmt.Errorf("unexpected flag: %v", arg) + } + c.flagValues[arg] = argStream[0] + case StringSliceFlag: + argStream = argStream[1:] + if len (argStream) == 0 { + return fmt.Errorf("expected value for argument: %v", arg) + } + if isFlag(argStream[0]) { + return fmt.Errorf("unexpected flag: %v", arg) + } + c.flagValues[arg] = make([]string, 0) + c.flagValues[arg] = append(c.flagValues[arg].([]string), argStream[0]) + argStream = argStream[1:] + for len(argStream) > 0 && ! isFlag(argStream[0]) { + c.flagValues[arg] = append(c.flagValues[arg].([]string), argStream[0]) + argStream = argStream[1:] + } + case BoolFlag: + argStream = argStream[1:] + c.flagValues[canonicalName(flag)] = true + default: + return fmt.Errorf("unexpected flag: %v", arg) + } + } + } + return nil +} + +func newContext(flags []Flag, osArgs []string) (*Context, error) { + var c Context + c.flagValues = make(map[string]interface{}) + c.flagDecls = make(map[string]Flag) + c.altFlags = make(map[string]string) + c.altFlags = make(map[string]string) + if err := c.setupFlags(flags); err != nil { + return nil, err + } + if err := c.parseArgs(osArgs); err != nil { + return nil, err + } + return &c, nil +} + +func (f StringFlag) name() string {return f.Name} +func (f StringFlag) usage() string {return f.Usage} +func (f BoolFlag) name() string {return f.Name} +func (f BoolFlag) usage() string {return f.Usage} +func (f StringSliceFlag) name() string {return f.Name} +func (f StringSliceFlag) usage() string {return f.Usage} + +// takes a Flag with a comma delimited string +// of flag names and returns the first one +func canonicalName(flag Flag) string { + flagNames := strings.Split(flag.name(), ",") + return strings.TrimSpace(flagNames[0]) +} + +// takes a Flag with a comma delimited string +// of flag names and returns them as a string slice +// with the canonical (first) flag ommitted +func alternateNames(flag Flag) []string { + flagNames := strings.Split(flag.name(), ",") + altNames := flagNames[1:] + for i, _ := range altNames { + altNames[i] = strings.TrimSpace(altNames[i]) + } + return altNames +} + +func isFlag(arg string) bool { + return strings.HasPrefix(arg, "-") +} + +func trimFlag(arg string) string { + return strings.Trim(strings.Trim(arg, "-"), "-") +} +