Skip to content

Commit

Permalink
introduce sub commands (#209)
Browse files Browse the repository at this point in the history
  • Loading branch information
ktr0731 authored Dec 22, 2019
1 parent 97d1de8 commit 33d0d09
Show file tree
Hide file tree
Showing 42 changed files with 1,561 additions and 361 deletions.
140 changes: 28 additions & 112 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,149 +2,66 @@
package app

import (
"context"
"fmt"
"os"
"io"

"github.com/ktr0731/evans/cache"
"github.com/ktr0731/evans/config"
"github.com/ktr0731/evans/cui"
"github.com/ktr0731/evans/logger"
"github.com/ktr0731/evans/meta"
"github.com/ktr0731/evans/mode"
"github.com/ktr0731/evans/prompt"
"github.com/pkg/errors"
"github.com/spf13/pflag"
"golang.org/x/sync/errgroup"
)

// App is the root component for running the application.
type App struct {
cui cui.UI

flagSet *pflag.FlagSet

cfg *mergedConfig
cmd *command
}

// New instantiates a new App instance. ui must not be a nil.
// Note that cui is also used for the REPL UI if the mode is REPL mode.
func New(ui cui.UI) *App {
var flags flags
cmd := newOldCommand(&flags, ui)
return &App{
cui: ui,
cmd: cmd,
}
}

// Run starts the application. The return value means the exit code.
func (a *App) Run(args []string) int {
err := a.run(args)
// Currently, Evans is migrating to new-style command-line interface.
// So, there are both of old-style and new-style command-line interfaces in this version.

a.cmd.SetArgs(args)
for _, r := range args {
// Hack.
switch r {
case "cli", "repl": // Sub commands for new-style interface.
// If an arg named "cli" or "repl" is passed, it is regarded as a sub-command of new-style.
a.cmd.registerNewCommands()
case "-h", "--help":
// If the help flags is passed, call registerNewCommands for display sub-command helps.
a.cmd.registerNewCommands()
}
}
err := a.cmd.Execute()
if err == nil {
return 0
}

switch err := err.(type) {
case *config.ValidationError:
a.printUsage()
a.cui.Error(fmt.Sprintf("evans: %s", err.Err))
default:
a.cui.Error(fmt.Sprintf("evans: %s", err))
}
a.cui.Error(fmt.Sprintf("evans: %s", err))
return 1
}

func (a *App) run(args []string) error {
flags, err := a.parseFlags(args)
if err != nil {
return err
}
if err := flags.validate(); err != nil {
return errors.Wrap(err, "invalid flag condition")
}

if flags.meta.verbose {
logger.SetOutput(os.Stderr)
}

switch {
case flags.meta.edit:
if err := config.Edit(); err != nil {
return errors.Wrap(err, "failed to edit the project local config file")
}
return nil
case flags.meta.editGlobal:
if err := config.EditGlobal(); err != nil {
return errors.Wrap(err, "failed to edit the global config file")
}
return nil
case flags.meta.version:
a.printVersion()
return nil
case flags.meta.help:
a.printUsage()
return nil
}

cfg, err := mergeConfig(a.flagSet, flags)
if err != nil {
if err, ok := err.(*config.ValidationError); ok {
return err
}
return errors.Wrap(err, "failed to merge command line flags and config files")
}
a.cfg = cfg

if cfg.REPL.ColoredOutput {
a.cui = cui.NewColored(a.cui)
}

isCLIMode := (cfg.cli || mode.IsCLIMode(cfg.file))
if cfg.repl || !isCLIMode {
cache, err := cache.Get()
if err != nil {
return errors.Wrap(err, "failed to get the cache content")
}

baseCtx, cancel := context.WithCancel(context.Background())
defer cancel()
eg, ctx := errgroup.WithContext(baseCtx)
// Run update checker asynchronously.
eg.Go(func() error {
return checkUpdate(ctx, a.cfg.Config, cache)
})

if a.cfg.Config.Meta.AutoUpdate {
eg.Go(func() error {
return processUpdate(ctx, a.cfg.Config, a.cui.Writer(), cache, prompt.New())
})
} else if err := processUpdate(ctx, a.cfg.Config, a.cui.Writer(), cache, prompt.New()); err != nil {
return errors.Wrap(err, "failed to update Evans")
}

if err := mode.RunAsREPLMode(a.cfg.Config, a.cui, cache); err != nil {
return errors.Wrap(err, "failed to run REPL mode")
}

// Always call cancel func because it is hope to abort update checking if REPL mode is finished
// before update checking. If update checking is finished before REPL mode, cancel do nothing.
cancel()
if err := eg.Wait(); err != nil {
return errors.Wrap(err, "failed to check application update")
}
} else if err := mode.RunAsCLIMode(a.cfg.Config, a.cfg.call, a.cfg.file, a.cui); err != nil {
return errors.Wrap(err, "failed to run CLI mode")
}

return nil
}

// printUsage shows the command usage text to cui.Writer and exit. Do not call it before calling parseFlags.
func (a *App) printUsage() {
a.flagSet.Usage()
func printUsage(cmd interface{ Help() error }) {
_ = cmd.Help() // Help never return errors.
}

// printVersion shows the version of the command and exit.
func (a *App) printVersion() {
a.cui.Output(fmt.Sprintf("%s %s", meta.AppName, meta.Version.String()))
func printVersion(w io.Writer) {
fmt.Fprintf(w, "%s %s\n", meta.AppName, meta.Version.String())
}

// mergedConfig represents the conclusive config. Common config items are stored to *config.Config.
Expand All @@ -163,13 +80,12 @@ type mergedConfig struct {
repl bool
}

func mergeConfig(fs *pflag.FlagSet, flags *flags) (*mergedConfig, error) {
func mergeConfig(fs *pflag.FlagSet, flags *flags, args []string) (*mergedConfig, error) {
cfg, err := config.Get(fs)
if err != nil {
return nil, errors.Wrap(err, "failed to get config")
}
protos := fs.Args()
cfg.Default.ProtoFile = append(cfg.Default.ProtoFile, protos...)
cfg.Default.ProtoFile = append(cfg.Default.ProtoFile, args...)

if err := cfg.Validate(); err != nil {
return nil, err
Expand Down
Loading

0 comments on commit 33d0d09

Please sign in to comment.