Skip to content

Commit

Permalink
More refactoring of cli and options
Browse files Browse the repository at this point in the history
  • Loading branch information
neilotoole committed Apr 25, 2023
1 parent 633c4f2 commit edc7ec4
Show file tree
Hide file tree
Showing 23 changed files with 730 additions and 288 deletions.
219 changes: 0 additions & 219 deletions cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,9 @@ import (
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"strings"
"sync"

"github.com/neilotoole/sq/libsq/core/options"

"github.com/neilotoole/sq/cli/output/format"

"github.com/neilotoole/sq/cli/flag"

"github.com/neilotoole/sq/libsq/core/lg"
Expand All @@ -36,15 +30,7 @@ import (

"github.com/neilotoole/sq/cli/buildinfo"

"golang.org/x/exp/slog"

"github.com/spf13/cobra"
"github.com/spf13/pflag"

"github.com/neilotoole/sq/cli/config"
"github.com/neilotoole/sq/cli/output/jsonw"
"github.com/neilotoole/sq/libsq/core/cleanup"
"github.com/neilotoole/sq/libsq/core/errz"
)

func init() { //nolint:gochecknoinits
Expand Down Expand Up @@ -283,208 +269,3 @@ func addCmd(rc *RunContext, parentCmd, cmd *cobra.Command) *cobra.Command {

return cmd
}

// defaultLogging returns a log (and its associated closer) if
// logging has been enabled via envars.
func defaultLogging() (*slog.Logger, slog.Handler, *cleanup.Cleanup, error) {
truncate, _ := strconv.ParseBool(os.Getenv(config.EnvarLogTruncate))

logFilePath, ok := os.LookupEnv(config.EnvarLogPath)
if !ok || logFilePath == "" || strings.TrimSpace(logFilePath) == "" {
return lg.Discard(), nil, nil, nil
}

// Let's try to create the dir holding the logfile... if it already exists,
// then os.MkdirAll will just no-op
parent := filepath.Dir(logFilePath)
err := os.MkdirAll(parent, 0o750)
if err != nil {
return lg.Discard(), nil, nil, errz.Wrapf(err, "failed to create parent dir of log file %s", logFilePath)
}

fileFlag := os.O_APPEND
if truncate {
fileFlag = os.O_TRUNC
}

logFile, err := os.OpenFile(logFilePath, os.O_RDWR|os.O_CREATE|fileFlag, 0o600)
if err != nil {
return lg.Discard(), nil, nil, errz.Wrapf(err, "unable to open log file: %s", logFilePath)
}
clnup := cleanup.New().AddE(logFile.Close)

replace := func(groups []string, a slog.Attr) slog.Attr {
// We want source to be "pkg/file.go".
if a.Key == slog.SourceKey {
fp := a.Value.String()
a.Value = slog.StringValue(filepath.Join(filepath.Base(filepath.Dir(fp)), filepath.Base(fp)))
}
return a
}

h := slog.HandlerOptions{
AddSource: true,
Level: slog.LevelDebug,
ReplaceAttr: replace,
}.NewJSONHandler(logFile)

return slog.New(h), h, clnup, nil
}

// printError is the centralized function for printing
// and logging errors. This func has a lot of (possibly needless)
// redundancy; ultimately err will print if non-nil (even if
// rc or any of its fields are nil).
func printError(rc *RunContext, err error) {
log := lg.Discard()
if rc != nil && rc.Log != nil {
log = rc.Log
}

if err == nil {
log.Warn("printError called with nil error")
return
}

if errors.Is(err, errNoMsg) {
// errNoMsg is a sentinel err that sq doesn't want to print
return
}

switch {
default:
case errors.Is(err, context.Canceled):
err = errz.New("canceled")
case errors.Is(err, context.DeadlineExceeded):
err = errz.New("timeout")
}

var cmd *cobra.Command
if rc != nil {
cmd = rc.Cmd

cmdName := "unknown"
if cmd != nil {
cmdName = cmd.Name()
}

lg.Error(log, "nil command", err, lga.Cmd, cmdName)

wrtrs := rc.writers
if wrtrs != nil && wrtrs.errw != nil {
// If we have an errorWriter, we print to it
// and return.
wrtrs.errw.Error(err)
return
}

// Else we don't have an errorWriter, so we fall through
}

// If we get this far, something went badly wrong in bootstrap
// (probably the config is corrupt).
// At this point, we could just print err to os.Stderr and be done.
// However, our philosophy is to always provide the ability
// to output errors in json if possible. So, even though cobra
// may not have initialized and our own config may be borked, we
// will still try to determine if the user wants the error
// in json, specified via flags (by directly using the pflag
// package) or via sq config's default output format.

opts := options.Options{}
if rc != nil && rc.Config != nil && rc.Config.Options != nil {
opts = rc.Config.Options
} else {
opts, _ = options.DefaultRegistry.Process(opts)
}

// getPrinting works even if cmd is nil
pr, _, errOut := getPrinting(cmd, opts, os.Stdout, os.Stderr)

if bootstrapIsFormatJSON(rc) {
// The user wants JSON, either via defaults or flags.
jw := jsonw.NewErrorWriter(log, errOut, pr)
jw.Error(err)
return
}

// The user didn't want JSON, so we just print to stderr.
if isColorTerminal(os.Stderr) {
pr.Error.Fprintln(os.Stderr, "sq: "+err.Error())
} else {
fmt.Fprintln(os.Stderr, "sq: "+err.Error())
}
}

// cmdFlagChanged returns true if cmd is non-nil and
// has the named flag and that flag been changed.
func cmdFlagChanged(cmd *cobra.Command, name string) bool {
if cmd == nil {
return false
}

f := cmd.Flag(name)
if f == nil {
return false
}

return f.Changed
}

// cmdFlagTrue returns true if flag name has been changed
// and the flag value is true.
func cmdFlagTrue(cmd *cobra.Command, name string) bool {
if !cmdFlagChanged(cmd, name) {
return false
}

b, err := cmd.Flags().GetBool(name)
if err != nil {
panic(err) // Should never happen
}

return b
}

// bootstrapIsFormatJSON is a last-gasp attempt to check if the user
// supplied --json=true on the command line, to determine if a
// bootstrap error (hopefully rare) should be output in JSON.
func bootstrapIsFormatJSON(rc *RunContext) bool {
// If no RunContext, assume false
if rc == nil {
return false
}

defaultFormat := format.Table
if rc.Config != nil {
defaultFormat = OptOutputFormat.Get(rc.Config.Options)
}

// If args were provided, create a new flag set and check
// for the --json flag.
if len(rc.Args) > 0 {
flagSet := pflag.NewFlagSet("bootstrap", pflag.ContinueOnError)

jsonFlag := flagSet.BoolP(flag.JSON, flag.JSONShort, false, flag.JSONUsage)
err := flagSet.Parse(rc.Args)
if err != nil {
return false
}

// No --json flag, return true if the config file default is JSON
if jsonFlag == nil {
return defaultFormat == format.JSON
}

return *jsonFlag
}

// No args, return true if the config file default is JSON
return defaultFormat == format.JSON
}

func panicOn(err error) {
if err != nil {
panic(err)
}
}
2 changes: 2 additions & 0 deletions cli/cmd_driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ func newDriverListCmd() *cobra.Command {
cmd.Flags().BoolP(flag.YAML, flag.YAMLShort, false, flag.YAMLUsage)
cmd.Flags().BoolP(flag.Table, flag.TableShort, false, flag.TableUsage)
cmd.Flags().BoolP(flag.Header, flag.HeaderShort, false, flag.HeaderUsage)
cmd.Flags().BoolP(flag.NoHeader, flag.NoHeaderShort, false, flag.NoHeaderUsage)
cmd.MarkFlagsMutuallyExclusive(flag.Header, flag.NoHeader)

return cmd
}
Expand Down
10 changes: 7 additions & 3 deletions cli/cmd_inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,9 @@ func execInspect(cmd *cobra.Command, args []string) error {
}
} else {
// We received an argument, which can be one of these forms:
// @my1 -- inspect the named source
// @my1.tbluser -- inspect a table of the named source
// .tbluser -- inspect a table from the active source
// @sakila -- inspect the named source
// @sakila.actor -- inspect a table of the named source
// .actor -- inspect a table from the active source
var handle string
handle, table, err = source.ParseTableHandle(args[0])
if err != nil {
Expand All @@ -120,6 +120,10 @@ func execInspect(cmd *cobra.Command, args []string) error {
}
}

if err = applySourceOptions(cmd, src); err != nil {
return err
}

dbase, err := rc.databases.Open(ctx, src)
if err != nil {
return errz.Wrapf(err, "failed to inspect %s", src.Handle)
Expand Down
4 changes: 3 additions & 1 deletion cli/cmd_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ any further descendants.
$ sq ls -g prod`,
}

cmd.Flags().BoolP(flag.Header, flag.HeaderShort, false, flag.HeaderUsage)
cmd.Flags().BoolP(flag.Header, flag.HeaderShort, true, flag.HeaderUsage)
cmd.Flags().BoolP(flag.NoHeader, flag.NoHeaderShort, false, flag.NoHeaderUsage)
cmd.MarkFlagsMutuallyExclusive(flag.Header, flag.NoHeader)
cmd.Flags().BoolP(flag.JSON, flag.JSONShort, false, flag.JSONUsage)
cmd.Flags().BoolP(flag.ListGroup, flag.ListGroupShort, false, flag.ListGroupUsage)

Expand Down
9 changes: 5 additions & 4 deletions cli/cmd_ping.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,15 @@ func execPing(cmd *cobra.Command, args []string) error {

srcs = lo.Uniq(srcs)

timeout := OptPingTimeout.Get(rc.Config.Options)
if cmdFlagChanged(cmd, flag.PingTimeout) {
timeout, _ = cmd.Flags().GetDuration(flag.PingTimeout)
cmdOpts, err := getCmdOptions(cmd)
if err != nil {
return err
}
timeout := OptPingTimeout.Get(cmdOpts)

rc.Log.Debug("Using ping timeout", lga.Val, timeout)

err := pingSources(cmd.Context(), rc.registry, srcs, rc.writers.pingw, timeout)
err = pingSources(cmd.Context(), rc.registry, srcs, rc.writers.pingw, timeout)
if errors.Is(err, context.Canceled) {
// It's common to cancel "sq ping". We don't want to print the cancel message.
return errNoMsg
Expand Down
25 changes: 21 additions & 4 deletions cli/cmd_slq.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"strings"

"github.com/neilotoole/sq/cli/flag"
"github.com/neilotoole/sq/drivers/csv"

"golang.org/x/exp/slices"

Expand Down Expand Up @@ -93,6 +94,10 @@ func execSLQ(cmd *cobra.Command, args []string) error {
return err
}

if err = applyCollectionOptions(cmd, rc.Config.Collection); err != nil {
return err
}

if !cmdFlagChanged(cmd, flag.Insert) {
// The user didn't specify the --insert=@src.tbl flag,
// so we just want to print the records.
Expand Down Expand Up @@ -129,6 +134,7 @@ func execSLQInsert(ctx context.Context, rc *RunContext, mArgs map[string]string,
destSrc *source.Source, destTbl string,
) error {
args, coll, dbases := rc.Args, rc.Config.Collection, rc.databases

slq, err := preprocessUserSLQ(ctx, rc, args)
if err != nil {
return err
Expand Down Expand Up @@ -346,20 +352,31 @@ func addQueryCmdFlags(cmd *cobra.Command) {
cmd.Flags().BoolP(flag.Raw, flag.RawShort, false, flag.RawUsage)
cmd.Flags().Bool(flag.HTML, false, flag.HTMLUsage)
cmd.Flags().Bool(flag.Markdown, false, flag.MarkdownUsage)

cmd.Flags().BoolP(flag.Header, flag.HeaderShort, false, flag.HeaderUsage)
cmd.Flags().BoolP(flag.NoHeader, flag.NoHeaderShort, false, flag.NoHeaderUsage)
cmd.MarkFlagsMutuallyExclusive(flag.Header, flag.NoHeader)

cmd.Flags().BoolP(flag.Pretty, "", true, flag.PrettyUsage)

cmd.Flags().StringP(flag.Insert, "", "", flag.InsertUsage)
_ = cmd.RegisterFlagCompletionFunc(flag.Insert, (&handleTableCompleter{onlySQL: true, handleRequired: true}).complete)

panicOn(cmd.RegisterFlagCompletionFunc(flag.Insert,
(&handleTableCompleter{onlySQL: true, handleRequired: true}).complete))

cmd.Flags().StringP(flag.ActiveSrc, "", "", flag.ActiveSrcUsage)
_ = cmd.RegisterFlagCompletionFunc(flag.ActiveSrc, completeHandle(0))
panicOn(cmd.RegisterFlagCompletionFunc(flag.ActiveSrc, completeHandle(0)))

// The driver flag can be used if data is piped to sq over stdin
cmd.Flags().StringP(flag.Driver, "", "", flag.QueryDriverUsage)
_ = cmd.RegisterFlagCompletionFunc(flag.Driver, completeDriverType)
panicOn(cmd.RegisterFlagCompletionFunc(flag.Driver, completeDriverType))

cmd.Flags().BoolP(flag.XLSXImportHeader, "", false, flag.XLSXImportHeaderUsage)

cmd.Flags().StringP(flag.SrcOptions, "", "", flag.QuerySrcOptionsUsage)
cmd.Flags().BoolP(flag.CSVImportHeader, "", false, flag.CSVImportHeaderUsage)
cmd.Flags().BoolP(flag.CSVEmptyAsNull, "", true, flag.CSVEmptyAsNullUsage)
cmd.Flags().StringP(flag.CSVDelim, "", flag.CSVDelimDefault, flag.CSVDelimUsage)
panicOn(cmd.RegisterFlagCompletionFunc(flag.CSVDelim, completeStrings(1, csv.NamedDelims()...)))
}

// extractFlagArgsValues returns a map {key:value} of predefined variables
Expand Down
7 changes: 5 additions & 2 deletions cli/cmd_sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,9 @@ func execSQL(cmd *cobra.Command, args []string) error {
rc := RunContextFrom(cmd.Context())
switch len(args) {
default:
// FIXME: we should allow multiple args and concat them
return errz.New("a single query string is required")
case 0:
return errz.New("empty SQL query string")
return errz.New("no SQL query string")
case 1:
if strings.TrimSpace(args[0]) == "" {
return errz.New("empty SQL query string")
Expand All @@ -82,6 +81,10 @@ func execSQL(cmd *cobra.Command, args []string) error {
// determineSources successfully returns.
activeSrc := coll.Active()

if err = applySourceOptions(cmd, activeSrc); err != nil {
return err
}

if !cmdFlagChanged(cmd, flag.Insert) {
// The user didn't specify the --insert=@src.tbl flag,
// so we just want to print the records.
Expand Down
Loading

0 comments on commit edc7ec4

Please sign in to comment.