Options refactoring (#395)

* options.Opt now has a separate options.Flag field.
This commit is contained in:
Neil O'Toole 2024-02-09 17:06:07 -07:00 committed by GitHub
parent fabc46c758
commit 07cbe46bde
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 224 additions and 250 deletions

View File

@ -10,15 +10,15 @@ Breaking changes are annotated with ☢️, and alpha/beta features with 🐥.
## [v0.47.4] - UPCOMING
Minor release with changes to flags.
Patch release with changes to flags.
See the earlier [`v0.47.0`](https://github.com/neilotoole/sq/releases/tag/v0.47.0)
release for recent headline features.
### Added
- By default, `sq` prints source locations with the password redacted. This is a sensible default, but
there are legitimate reasons to access the unredacted connection string. There's a new
global flag `--no-redact` for that (and a corresponding [`redact`](https://sq.io/docs/config#redact) config option).
there are legitimate reasons to access the unredacted connection string. Thus a new
global flag `--no-redact` (and a corresponding [`redact`](https://sq.io/docs/config#redact) config option).
```shell
# Default behavior: password is redacted
@ -30,6 +30,11 @@ release for recent headline features.
@sakila/pg12 postgres postgres://sakila:p_ssW0rd@192.168.50.132/sakila
```
- Previously, if an error occurred when [`verbose`](https://sq.io/docs/config#verbose) was true,
and [`error.format`](https://sq.io/docs/config#errorformat) was `text`, `sq` would print a stack trace
to `stderr`. This was poor default behavior, flooding the user terminal, so the default is now no stack trace.
To restore the previous behavior, use the new `-E` (`--error.stack`) flag, or set the [`error.stack`](https://sq.io/docs/config#errorstack) config option.
### Changed

View File

@ -26,7 +26,7 @@ import (
"github.com/neilotoole/sq/libsq/source/location"
)
func newSrcAddCmd() *cobra.Command { //nolint:funlen
func newSrcAddCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "add [--handle @HANDLE] LOCATION",
RunE: execSrcAdd,
@ -172,12 +172,9 @@ More examples:
cmd.Flags().BoolP(flag.AddActive, flag.AddActiveShort, false, flag.AddActiveUsage)
addOptionFlag(cmd.Flags(), driver.OptIngestHeader)
// cmd.Flags().Bool(flag.IngestHeader, false, flag.IngestHeaderUsage)
addOptionFlag(cmd.Flags(), csv.OptEmptyAsNull)
addOptionFlag(cmd.Flags(), csv.OptDelim)
// cmd.Flags().Bool(flag.CSVEmptyAsNull, true, flag.CSVEmptyAsNullUsage)
// cmd.Flags().String(flag.CSVDelim, flag.CSVDelimDefault, flag.CSVDelimUsage)
panicOn(cmd.RegisterFlagCompletionFunc(csv.OptDelim.Flag(), completeStrings(-1, csv.NamedDelims()...)))
panicOn(cmd.RegisterFlagCompletionFunc(csv.OptDelim.Flag().Name, completeStrings(-1, csv.NamedDelims()...)))
return cmd
}

View File

@ -18,8 +18,7 @@ import (
var OptDiffNumLines = options.NewInt(
"diff.lines",
"unified",
'U',
&options.Flag{Name: "unified", Short: 'U'},
3,
"Generate diffs with <n> lines of context",
`Generate diffs with <n> lines of context, where n >= 0.`,
@ -28,8 +27,7 @@ var OptDiffNumLines = options.NewInt(
var OptDiffDataFormat = format.NewOpt(
"diff.data.format",
"format",
'f',
&options.Flag{Name: "format", Short: 'f'},
format.Text,
func(f format.Format) error {
switch f { //nolint:exhaustive
@ -187,7 +185,7 @@ The default (3) can be changed via:
}
panicOn(cmd.RegisterFlagCompletionFunc(
OptDiffDataFormat.Flag(),
OptDiffDataFormat.Flag().Name,
completeStrings(-1, stringz.Strings(diffFormats)...),
))

View File

@ -26,8 +26,7 @@ import (
// operations.
var OptPingCmdTimeout = options.NewDuration(
"ping.timeout",
"",
0,
nil,
time.Second*10,
"ping command timeout duration",
"How long the ping command waits before timeout occurs. Example: 500ms or 2m10s.",
@ -38,8 +37,7 @@ func newPingCmd() *cobra.Command {
Use: "ping [@HANDLE|GROUP]*",
RunE: execPing,
ValidArgsFunction: completeHandleOrGroup,
Short: "Ping data sources",
Short: "Ping data sources",
Long: `Ping data sources (or groups of sources) to check connection health.
If no arguments provided, the active data source is pinged. Otherwise, ping
the specified sources or groups.

View File

@ -111,12 +111,12 @@ See docs and more: https://sq.io`,
cmd.PersistentFlags().String(flag.Config, "", flag.ConfigUsage)
addOptionFlag(cmd.PersistentFlags(), OptLogEnabled)
panicOn(cmd.RegisterFlagCompletionFunc(OptLogEnabled.Flag(), completeBool))
panicOn(cmd.RegisterFlagCompletionFunc(OptLogEnabled.Flag().Name, completeBool))
addOptionFlag(cmd.PersistentFlags(), OptLogFile)
addOptionFlag(cmd.PersistentFlags(), OptLogLevel)
panicOn(cmd.RegisterFlagCompletionFunc(OptLogLevel.Flag(), completeStrings(
panicOn(cmd.RegisterFlagCompletionFunc(OptLogLevel.Flag().Name, completeStrings(
1,
slog.LevelDebug.String(),
slog.LevelInfo.String(),
@ -125,11 +125,14 @@ See docs and more: https://sq.io`,
)))
addOptionFlag(cmd.PersistentFlags(), OptLogFormat)
panicOn(cmd.RegisterFlagCompletionFunc(OptLogFormat.Flag(), completeStrings(
panicOn(cmd.RegisterFlagCompletionFunc(OptLogFormat.Flag().Name, completeStrings(
1,
string(format.Text),
string(format.JSON),
)))
addOptionFlag(cmd.PersistentFlags(), OptErrorFormat)
addOptionFlag(cmd.PersistentFlags(), OptErrorStack)
return cmd
}

View File

@ -325,12 +325,12 @@ func addTextFormatFlags(cmd *cobra.Command) {
func addQueryCmdFlags(cmd *cobra.Command) {
addOptionFlag(cmd.Flags(), OptFormat)
panicOn(cmd.RegisterFlagCompletionFunc(
OptFormat.Flag(),
OptFormat.Flag().Name,
completeStrings(-1, stringz.Strings(format.All())...),
))
addResultFormatFlags(cmd)
cmd.MarkFlagsMutuallyExclusive(append(
[]string{OptFormat.Flag()},
[]string{OptFormat.Flag().Name},
flag.OutputFormatFlags...,
)...)

View File

@ -122,7 +122,7 @@ func TestCmdSQL_SelectFromUserDriver(t *testing.T) {
func TestCmdSQL_StdinQuery(t *testing.T) {
t.Parallel()
flagIngestHeader := driver.OptIngestHeader.Flag()
flagIngestHeader := driver.OptIngestHeader.Flag().Name
testCases := []struct {
fpath string

View File

@ -24,8 +24,7 @@ import (
var OptShellCompletionTimeout = options.NewDuration(
"shell-completion.timeout",
"",
0,
nil,
time.Millisecond*500,
"Shell completion timeout",
`How long shell completion should wait before giving up. This can become relevant
@ -35,9 +34,7 @@ tables in a source.`,
var OptShellCompletionLog = options.NewBool(
"shell-completion.log",
"",
false,
0,
nil,
false,
"Enable logging of shell completion activity",
`Enable logging of shell completion activity. This is really only useful for
@ -50,9 +47,7 @@ timeout triggers logging of errors.`,
var OptShellCompletionGroupFilter = options.NewBool(
"shell-completion.group-filter",
"",
false,
0,
nil,
true,
"Shell completion initial source suggestions from active group only",
`When true, shell completion initially suggests only sources within the active

View File

@ -75,8 +75,7 @@ func (DiscardStore) Location() string {
// OptConfigLockTimeout is the time allowed to acquire the config lock.
var OptConfigLockTimeout = options.NewDuration(
"config.lock.timeout",
"",
0,
nil,
time.Second*5,
"Wait timeout to acquire config lock",
`Wait timeout to acquire the config lock (which prevents multiple sq instances

View File

@ -28,9 +28,7 @@ import (
var (
OptLogEnabled = options.NewBool(
"log",
"",
false,
0,
nil,
false,
"Enable logging",
"Enable logging.",
@ -38,8 +36,7 @@ var (
OptLogFile = options.NewString(
"log.file",
"",
0,
nil,
getDefaultLogFilePath(),
nil,
"Log file path",
@ -55,8 +52,7 @@ var (
OptLogFormat = format.NewOpt(
"log.format",
"",
0,
nil,
format.Text,
func(f format.Format) error {
if f == format.Text || f == format.JSON {
@ -65,7 +61,7 @@ var (
return errz.Errorf("option {log.format} allows only %q or %q", format.Text, format.JSON)
},
"Log output format",
"Log output format (text or json)",
fmt.Sprintf(
`Log output format. Allowed formats are %q (human-friendly) or %q.`, format.Text, format.JSON),
)
@ -216,7 +212,7 @@ func getLogEnabled(ctx context.Context, osArgs []string, cfg *config.Config) boo
bootLog := lg.FromContext(ctx)
var enabled bool
flg := OptLogEnabled.Flag()
flg := OptLogEnabled.Flag().Name
val, ok, err := getBootstrapFlagValue(flg, "", OptLogEnabled.Usage(), osArgs)
if err != nil {
bootLog.Warn("Reading log 'enabled' from flag", lga.Flag, flg, lga.Err, err)
@ -275,7 +271,7 @@ func getLogEnabled(ctx context.Context, osArgs []string, cfg *config.Config) boo
func getLogLevel(ctx context.Context, osArgs []string, cfg *config.Config) slog.Level {
bootLog := lg.FromContext(ctx)
flg := OptLogLevel.Flag()
flg := OptLogLevel.Flag().Name
val, ok, err := getBootstrapFlagValue(flg, "", OptLogLevel.Usage(), osArgs)
if err != nil {
bootLog.Warn("Reading log level from flag", lga.Flag, flg, lga.Err, err)
@ -320,7 +316,7 @@ func getLogLevel(ctx context.Context, osArgs []string, cfg *config.Config) slog.
func getLogFormat(ctx context.Context, osArgs []string, cfg *config.Config) format.Format {
bootLog := lg.FromContext(ctx)
flg := OptLogFormat.Flag()
flg := OptLogFormat.Flag().Name
val, ok, err := getBootstrapFlagValue(flg, "", OptLogFormat.Usage(), osArgs)
if err != nil {
bootLog.Warn("Error reading log format from flag", lga.Flag, flg, lga.Err, err)
@ -373,7 +369,7 @@ func getLogFormat(ctx context.Context, osArgs []string, cfg *config.Config) form
func getLogFilePath(ctx context.Context, osArgs []string, cfg *config.Config) string {
bootLog := lg.FromContext(ctx)
flg := OptLogFile.Flag()
flg := OptLogFile.Flag().Name
fp, ok, err := getBootstrapFlagValue(flg, "", OptLogFile.Usage(), osArgs)
if err != nil {
bootLog.Warn("Reading log file from flag", lga.Flag, flg, lga.Err, err)
@ -426,7 +422,7 @@ var _ options.Opt = LogLevelOpt{}
// NewLogLevelOpt returns a new LogLevelOpt instance.
func NewLogLevelOpt(key string, defaultVal slog.Level, usage, help string) LogLevelOpt {
opt := options.NewBaseOpt(key, "", 0, usage, help)
opt := options.NewBaseOpt(key, nil, usage, help)
return LogLevelOpt{BaseOpt: opt, defaultVal: defaultVal}
}

View File

@ -30,7 +30,7 @@ import (
func getOptionsFromFlags(flags *pflag.FlagSet, reg *options.Registry) (options.Options, error) {
o := options.Options{}
err := reg.Visit(func(opt options.Opt) error {
f := flags.Lookup(opt.Flag())
f := flags.Lookup(opt.Flag().Name)
if f == nil {
return nil
}
@ -47,12 +47,12 @@ func getOptionsFromFlags(flags *pflag.FlagSet, reg *options.Registry) (options.O
if bOpt, ok := opt.(options.Bool); ok {
// Special handling for bool, because
// the flag value could be inverted.
val, err := flags.GetBool(bOpt.Flag())
val, err := flags.GetBool(bOpt.Flag().Name)
if err != nil {
return errz.Err(err)
}
if bOpt.FlagInverted() {
if bOpt.Flag().Invert {
val = !val
}
o[bOpt.Key()] = val
@ -154,6 +154,7 @@ func RegisterDefaultOpts(reg *options.Registry) {
reg.Add(
OptFormat,
OptErrorFormat,
OptErrorStack,
OptDatetimeFormat,
OptDatetimeFormatAsNumber,
OptDateFormat,
@ -242,36 +243,37 @@ func filterOptionsForSrc(typ drivertype.Type, opts ...options.Opt) []options.Opt
// addOptionFlag adds a flag derived from opt to flags, returning the
// flag name used.
func addOptionFlag(flags *pflag.FlagSet, opt options.Opt) (key string) {
key = opt.Flag()
flg := opt.Flag()
key = flg.Name
switch opt := opt.(type) {
case options.Int:
if opt.Short() == 0 {
flags.Int(key, opt.Default(), opt.Usage())
if flg.Short == 0 {
flags.Int(key, opt.Default(), flg.Usage)
return key
}
flags.IntP(key, string(opt.Short()), opt.Default(), opt.Usage())
flags.IntP(key, string(flg.Short), opt.Default(), flg.Usage)
return key
case options.Bool:
defVal := opt.Default()
if opt.FlagInverted() {
if flg.Invert {
defVal = !defVal
}
if opt.Short() == 0 {
flags.Bool(key, defVal, opt.Usage())
if flg.Short == 0 {
flags.Bool(key, defVal, flg.Usage)
return key
}
flags.BoolP(key, string(opt.Short()), defVal, opt.Usage())
flags.BoolP(key, string(flg.Short), defVal, flg.Usage)
return key
case options.Duration:
if opt.Short() == 0 {
flags.Duration(key, opt.Default(), opt.Usage())
if flg.Short == 0 {
flags.Duration(key, opt.Default(), flg.Usage)
return key
}
flags.DurationP(key, string(opt.Short()), opt.Get(nil), opt.Usage())
flags.DurationP(key, string(flg.Short), opt.Get(nil), flg.Usage)
default:
// Treat as string
}
@ -281,12 +283,12 @@ func addOptionFlag(flags *pflag.FlagSet, opt options.Opt) (key string) {
defVal = fmt.Sprintf("%v", v)
}
if opt.Short() == 0 {
flags.String(key, defVal, opt.Usage())
if flg.Short == 0 {
flags.String(key, defVal, flg.Usage)
return key
}
flags.StringP(key, string(opt.Short()), defVal, opt.Usage())
flags.StringP(key, string(flg.Short), defVal, flg.Usage)
return key
}

View File

@ -11,15 +11,12 @@ import (
)
func TestRegisterDefaultOpts(t *testing.T) {
log := lgt.New(t)
reg := &options.Registry{}
log.Debug("options.Registry (before)", "reg", reg)
cli.RegisterDefaultOpts(reg)
log.Debug("options.Registry (after)", "reg", reg)
lgt.New(t).Debug("options.Registry (after)", "reg", reg)
keys := reg.Keys()
require.Len(t, keys, 53)
require.Len(t, keys, 54)
for _, opt := range reg.Opts() {
opt := opt
@ -30,7 +27,8 @@ func TestRegisterDefaultOpts(t *testing.T) {
require.NotNil(t, opt.DefaultAny())
require.Equal(t, opt.GetAny(nil), opt.DefaultAny())
require.NotEmpty(t, opt.Usage())
require.True(t, opt.Short() >= 0)
require.NotEmpty(t, opt.Flag().Usage)
require.True(t, opt.Flag().Short >= 0)
require.Equal(t, opt.Key(), opt.String())
require.NotEmpty(t, opt.Help())
})

View File

@ -38,9 +38,7 @@ import (
var (
OptPrintHeader = options.NewBool(
"header",
"",
false,
0,
nil,
true,
"Print header row",
`Controls whether a header row is printed. This applies only to certain formats,
@ -50,8 +48,7 @@ such as "text" or "csv".`,
OptFormat = format.NewOpt(
"format",
"format",
'f',
&options.Flag{Short: 'f'},
format.Text,
nil,
"Specify output format",
@ -66,8 +63,7 @@ command, sq falls back to "text". Available formats:
OptErrorFormat = format.NewOpt(
"error.format",
"",
0,
nil,
format.Text,
func(f format.Format) error {
if f == format.Text || f == format.JSON {
@ -80,11 +76,19 @@ command, sq falls back to "text". Available formats:
fmt.Sprintf(`The format to output errors in. Allowed formats are %q or %q.`, format.Text, format.JSON),
)
OptErrorStack = options.NewBool(
"error.stack",
&options.Flag{Short: 'E'},
false,
"Print error stack trace to stderr",
`Print error stack trace to stderr. This only applies when error.format is
"text"; when error.format is "json", the stack trace is always printed.`,
options.TagOutput,
)
OptVerbose = options.NewBool(
"verbose",
"",
false,
'v',
&options.Flag{Short: 'v'},
false,
"Print verbose output",
`Print verbose output.`,
@ -93,9 +97,7 @@ command, sq falls back to "text". Available formats:
OptMonochrome = options.NewBool(
"monochrome",
"",
false,
'M',
&options.Flag{Short: 'M'},
false,
"Don't print color output",
`Don't print color output.`,
@ -104,9 +106,11 @@ command, sq falls back to "text". Available formats:
OptRedact = options.NewBool(
"redact",
"no-redact",
true,
0,
&options.Flag{
Name: "no-redact",
Invert: true,
Usage: "Don't redact passwords in output",
},
true,
"Redact passwords in output",
`Redact passwords in output.`,
@ -115,19 +119,20 @@ command, sq falls back to "text". Available formats:
OptProgress = options.NewBool(
"progress",
"no-progress",
&options.Flag{
Name: "no-progress",
Invert: true,
Usage: "Don't show progress bar",
},
true,
0,
true,
"Progress bar for long-running operations",
`Progress bar for long-running operations.`,
"Show progress bar for long-running operations",
`Show progress bar for long-running operations.`,
options.TagOutput,
)
OptProgressDelay = options.NewDuration(
"progress.delay",
"",
0,
nil,
time.Second*2,
"Progress bar render delay",
`Delay before showing a progress bar.`,
@ -135,8 +140,7 @@ command, sq falls back to "text". Available formats:
OptDebugTrackMemory = options.NewDuration(
"debug.stats.frequency",
"",
0,
nil,
0,
"Memory usage sampling interval.",
`Memory usage sampling interval. If non-zero, peak memory usage is periodically
@ -145,9 +149,7 @@ sampled, and reported on exit. If zero, memory usage sampling is disabled.`,
OptCompact = options.NewBool(
"compact",
"",
false,
'c',
&options.Flag{Short: 'c'},
false,
"Compact instead of pretty-printed output",
`Compact instead of pretty-printed output.`,
@ -156,8 +158,7 @@ sampled, and reported on exit. If zero, memory usage sampling is disabled.`,
OptTuningFlushThreshold = options.NewInt(
"tuning.flush-threshold",
"",
0,
nil,
1000,
"Output writer buffer flush threshold in bytes",
`Size in bytes after which output writers should flush any internal buffer.
@ -171,8 +172,7 @@ Generally, it is not necessary to fiddle this knob.`,
OptDatetimeFormat = options.NewString(
"format.datetime",
"",
0,
nil,
"RFC3339",
nil,
"Timestamp format: constant such as RFC3339 or a strftime format",
@ -185,9 +185,7 @@ Generally, it is not necessary to fiddle this knob.`,
OptDatetimeFormatAsNumber = options.NewBool(
"format.datetime.number",
"",
false,
0,
nil,
true,
"Render numeric datetime value as number instead of string",
`Render numeric datetime value as number instead of string, if possible. If
@ -207,8 +205,7 @@ is not an integer.
OptDateFormat = options.NewString(
"format.date",
"",
0,
nil,
"DateOnly",
nil,
"Date format: constant such as DateOnly or a strftime format",
@ -223,9 +220,7 @@ situation, use format.datetime instead.
OptDateFormatAsNumber = options.NewBool(
"format.date.number",
"",
false,
0,
nil,
true,
"Render numeric date value as number instead of string",
`Render numeric date value as number instead of string, if possible. If
@ -244,8 +239,7 @@ that this option is no-op if the rendered value is not an integer.
OptTimeFormat = options.NewString(
"format.time",
"",
0,
nil,
"TimeOnly",
nil,
"Time format: constant such as TimeOnly or a strftime format",
@ -260,13 +254,10 @@ situation, use format.datetime instead.
OptTimeFormatAsNumber = options.NewBool(
"format.time.number",
"",
false,
0,
nil,
true,
"Render numeric time value as number instead of string",
`
Render numeric time value as number instead of string, if possible. If format.time
`Render numeric time value as number instead of string, if possible. If format.time
renders a numeric value (e.g. "59"), that value is typically rendered as a string.
For some output formats, such as JSON, it can be useful to instead render the
value as a naked number instead of a string. Note that this option is no-op if
@ -304,7 +295,7 @@ func newWriters(cmd *cobra.Command, clnup *cleanup.Cleanup, o options.Options,
Metadata: tablew.NewMetadataWriter(outCfg.out, outCfg.outPr),
Source: tablew.NewSourceWriter(outCfg.out, outCfg.outPr),
Ping: tablew.NewPingWriter(outCfg.out, outCfg.outPr),
Error: tablew.NewErrorWriter(outCfg.errOut, outCfg.errOutPr),
Error: tablew.NewErrorWriter(outCfg.errOut, outCfg.errOutPr, OptErrorStack.Get(o)),
Version: tablew.NewVersionWriter(outCfg.out, outCfg.outPr),
Config: tablew.NewConfigWriter(outCfg.out, outCfg.outPr),
}

View File

@ -9,10 +9,10 @@ var _ options.Opt = Opt{}
// NewOpt returns a new format.Opt instance. If validFn is non-nil, it
// is executed against possible values.
func NewOpt(key, flag string, short rune, defaultVal Format,
func NewOpt(key string, flag *options.Flag, defaultVal Format,
validFn func(Format) error, usage, help string,
) Opt {
opt := options.NewBaseOpt(key, flag, short, usage, help, options.TagOutput)
opt := options.NewBaseOpt(key, flag, usage, help, options.TagOutput)
return Opt{BaseOpt: opt, defaultVal: defaultVal, validFn: validFn}
}

View File

@ -20,7 +20,8 @@ type VerboseOpt struct { //nolint:govet // field alignment
IsSet bool `json:"is_set"`
DefaultValue any `json:"default_value"`
Value any `json:"value"`
Help string `json:"help"`
// FIXME: Add Flag?
Help string `json:"help"`
}
// NewVerboseOpt returns a VerboseOpt built from opt and o.

View File

@ -12,20 +12,21 @@ import (
// errorWriter implements output.ErrorWriter.
type errorWriter struct {
w io.Writer
pr *output.Printing
w io.Writer
pr *output.Printing
stacktrace bool
}
// NewErrorWriter returns an output.ErrorWriter that
// outputs in text format.
func NewErrorWriter(w io.Writer, pr *output.Printing) output.ErrorWriter {
return &errorWriter{w: w, pr: pr}
func NewErrorWriter(w io.Writer, pr *output.Printing, stacktrace bool) output.ErrorWriter {
return &errorWriter{w: w, pr: pr, stacktrace: stacktrace}
}
// Error implements output.ErrorWriter.
func (w *errorWriter) Error(systemErr, humanErr error) {
fmt.Fprintln(w.w, w.pr.Error.Sprintf("sq: %v", humanErr))
if !w.pr.Verbose {
if !w.stacktrace {
return
}

View File

@ -14,8 +14,7 @@ var (
// OptDatetimeFormat is Excel's custom datetime format string.
OptDatetimeFormat = options.NewString(
"format.excel.datetime",
"",
0,
nil,
"yyyy-mm-dd hh:mm",
func(s string) error {
err := validateDatetimeFormatString(s)
@ -38,8 +37,7 @@ Examples:
// OptDateFormat is Excel's custom date-only format string.
OptDateFormat = options.NewString(
"format.excel.date",
"",
0,
nil,
"yyyy-mm-dd",
func(s string) error {
err := validateDatetimeFormatString(s)
@ -60,8 +58,7 @@ Examples:
// OptTimeFormat is Excel's custom time format string.
OptTimeFormat = options.NewString(
"format.excel.time",
"",
0,
nil,
"hh:mm:ss",
func(s string) error {
err := validateDatetimeFormatString(s)

View File

@ -26,9 +26,7 @@ import (
// or as the zero value for the kind of that field.
var OptEmptyAsNull = options.NewBool(
"driver.csv.empty-as-null",
"",
false,
0,
nil,
true,
"Treat ingest empty CSV fields as NULL",
`When true, empty CSV fields are treated as NULL. When false,
@ -41,8 +39,7 @@ the zero value for that type is used, e.g. empty string or 0.`,
// OptDelim specifies the CSV delimiter to use.
var OptDelim = options.NewString(
"driver.csv.delim",
"",
0,
nil,
delimCommaKey,
nil,
"Delimiter for ingest CSV data",

View File

@ -46,18 +46,13 @@ type Opt interface {
// Key returns the Opt key, such as "ping.timeout".
Key() string
// Flag is the long flag name to use, which is typically the same value
// as returned by Opt.Key. However, a distinct value can be supplied, such
// that flag usage and config usage have different keys. For example,
// an Opt might have a key "diff.num-lines", but a flag "lines".
Flag() string
// Short is the short key. The zero value indicates no short key.
// For example, if the key is "json", the short key could be 'j'.
Short() rune
// Flag is the computed flag config for the Opt.
Flag() Flag
// Usage is a one-line description of the Opt. Additional detail can be
// found in Help.
// found in Help. It is typically the case that [Flag.Usage] is the same value
// as Usage, but it can be overridden if the flag usage text should differ
// from the Opt usage text.
Usage() string
// Help returns the Opt's help text, which typically provides more detail
@ -93,34 +88,63 @@ type Opt interface {
Process(o Options) (Options, error)
}
// Flag describe an Opt's behavior as a command-line flag. It can be passed to
// the "NewX" Opt constructor functions, e.g. [NewBool], to override the Opt's
// flag configuration. The computed Flag value is available via Opt.Flag.
// It is common to pass a nil *Flag to the Opt constructors; the value returned
// by Opt.Flag will be appropriately populated with default values.
type Flag struct {
// Name is the flag name to use. Defaults to [Opt.Key].
Name string
// Usage is the flag's usage text. Defaults to [Opt.Usage], but can be
// overridden if the flag usage text should differ from the [Opt] usage text.
// This is typically only the case when [Flag.Invert] is true.
Usage string
// Short is the short flag name, e.g. 'v' for "verbose". The zero value
// indicates no short name.
Short rune
// Invert indicates that the flag's boolean value is inverted vs the flag
// name. For example, if [Opt.Key] is "progress", but [Flag.Name] is
// "no-progress", then [Flag.Invert] should be true. This field is ignored for
// non-boolean [Opt] types.
Invert bool
}
// BaseOpt is a partial implementation of options.Opt that concrete
// types can build on.
type BaseOpt struct {
flag Flag
key string
flag string
usage string
help string
tags []string
short rune
}
// NewBaseOpt returns a new BaseOpt. If flag is empty string, key is
// used as the flag value.
func NewBaseOpt(key, flag string, short rune, usage, help string, tags ...string) BaseOpt {
if flag == "" {
flag = key
}
// NewBaseOpt returns a new BaseOpt.
func NewBaseOpt(key string, flag *Flag, usage, help string, tags ...string) BaseOpt {
slices.Sort(tags)
return BaseOpt{
opt := BaseOpt{
key: key,
flag: flag,
short: short,
usage: usage,
help: help,
tags: tags,
}
if flag != nil {
opt.flag = *flag
}
if opt.flag.Name == "" {
opt.flag.Name = key
}
if opt.flag.Usage == "" {
opt.flag.Usage = usage
}
return opt
}
// Key implements options.Opt.
@ -129,15 +153,10 @@ func (op BaseOpt) Key() string {
}
// Flag implements options.Opt.
func (op BaseOpt) Flag() string {
func (op BaseOpt) Flag() Flag {
return op.flag
}
// Short implements options.Opt.
func (op BaseOpt) Short() rune {
return op.short
}
// Usage implements options.Opt.
func (op BaseOpt) Usage() string {
return op.usage
@ -190,16 +209,16 @@ func (op BaseOpt) Process(o Options) (Options, error) {
var _ Opt = String{}
// NewString returns an options.String instance. If flag is empty, the
// value of key is used. If valid Fn is non-nil, it is called from
// the process function.
//
//nolint:revive
func NewString(key, flag string, short rune, defaultVal string,
validFn func(string) error, usage, help string, tags ...string,
// NewString returns an options.String instance. If validFn is non-nil, it is
// called by [String.Process].
func NewString(key string, flag *Flag, defaultVal string, validFn func(string) error,
usage, help string, tags ...string,
) String {
if flag == nil {
flag = &Flag{}
}
return String{
BaseOpt: NewBaseOpt(key, flag, short, usage, help, tags...),
BaseOpt: NewBaseOpt(key, flag, usage, help, tags...),
defaultVal: defaultVal,
validFn: validFn,
}
@ -275,11 +294,14 @@ func (op String) Process(o Options) (Options, error) {
var _ Opt = Int{}
// NewInt returns an options.Int instance. If flag is empty, the
// value of key is used.
func NewInt(key, flag string, short rune, defaultVal int, usage, help string, tags ...string) Int {
// NewInt returns an options.Int instance.
func NewInt(key string, flag *Flag, defaultVal int, usage, help string, tags ...string) Int {
if flag == nil {
flag = &Flag{}
}
return Int{
BaseOpt: NewBaseOpt(key, flag, short, usage, help, tags...),
BaseOpt: NewBaseOpt(key, flag, usage, help, tags...),
defaultVal: defaultVal,
}
}
@ -403,32 +425,25 @@ func (op Int) Process(o Options) (Options, error) {
var _ Opt = Bool{}
// NewBool returns an options.Bool instance. If flag is empty, the value
// of key is used. If invertFlag is true, the flag's boolean value
// is inverted to set the option. For example, if the Opt is "progress",
// and the flag is "--no-progress", then invertFlag should be true.
func NewBool(key, flag string, invertFlag bool, short rune, //nolint:revive
defaultVal bool, usage, help string, tags ...string,
) Bool {
// NewBool returns an options.Bool instance. If arg flag is non-nil and
// [Flag.Invert] is true, the flag's boolean value is inverted to set the option.
// For example, if [Opt.Key] is progress, and [Flag.Name] is "--no-progress",
// then [Flag.Invert] should be true.
func NewBool(key string, flag *Flag, defaultVal bool, usage, help string, tags ...string) Bool {
if flag == nil {
flag = &Flag{}
}
return Bool{
BaseOpt: NewBaseOpt(key, flag, short, usage, help, tags...),
defaultVal: defaultVal,
flagInverted: invertFlag,
BaseOpt: NewBaseOpt(key, flag, usage, help, tags...),
defaultVal: defaultVal,
}
}
// Bool is an options.Opt for type bool.
type Bool struct {
BaseOpt
defaultVal bool
flagInverted bool
}
// FlagInverted returns true Opt value is the inverse of the flag value.
// For example, if the Opt is "progress", and the flag is "--no-progress",
// then FlagInverted will return true.
func (op Bool) FlagInverted() bool {
return op.flagInverted
defaultVal bool
}
// GetAny implements options.Opt.
@ -519,13 +534,16 @@ func (op Bool) Process(o Options) (Options, error) {
var _ Opt = Duration{}
// NewDuration returns an options.Duration instance. If flag is empty, the
// value of key is used.
func NewDuration(key, flag string, short rune, defaultVal time.Duration,
// NewDuration returns an options.Duration instance.
func NewDuration(key string, flag *Flag, defaultVal time.Duration,
usage, help string, tags ...string,
) Duration {
if flag == nil {
flag = &Flag{}
}
return Duration{
BaseOpt: NewBaseOpt(key, flag, short, usage, help, tags...),
BaseOpt: NewBaseOpt(key, flag, usage, help, tags...),
defaultVal: defaultVal,
}
}

View File

@ -69,7 +69,7 @@ func TestInt(t *testing.T) {
t.Run(tu.Name(i, tc.key), func(t *testing.T) {
reg := &options.Registry{}
opt := options.NewInt(tc.key, "", 0, tc.defaultVal, "", "")
opt := options.NewInt(tc.key, nil, tc.defaultVal, "", "")
reg.Add(opt)
o := options.Options{tc.key: tc.input}
@ -115,7 +115,7 @@ func TestBool(t *testing.T) {
t.Run(tu.Name(i, tc.key), func(t *testing.T) {
reg := &options.Registry{}
opt := options.NewBool(tc.key, "", false, 0, tc.defaultVal, "", "")
opt := options.NewBool(tc.key, nil, tc.defaultVal, "", "")
reg.Add(opt)
o := options.Options{tc.key: tc.input}
@ -156,8 +156,8 @@ func TestOptions_LogValue(t *testing.T) {
}
func TestEffective(t *testing.T) {
optHello := options.NewString("hello", "", 0, "world", nil, "", "")
optCount := options.NewInt("count", "", 0, 1, "", "")
optHello := options.NewString("hello", nil, "world", nil, "", "")
optCount := options.NewInt("count", nil, 1, "", "")
in := options.Options{"count": 7}
want := options.Options{"count": 7, "hello": "world"}

View File

@ -464,8 +464,7 @@ func barRenderDelay(b *Bar, d time.Duration) <-chan struct{} {
// progress impl is stable.
var OptDebugSleep = options.NewDuration(
"debug.progress.sleep",
"",
0,
nil,
0,
"DEBUG: Sleep during operations to facilitate testing progress bars",
`DEBUG: Sleep during operations to facilitate testing progress bars.`,

View File

@ -12,9 +12,7 @@ import (
// If not set, the ingester *may* try to detect if the input has a header.
var OptIngestHeader = options.NewBool(
"ingest.header",
"",
false,
0,
nil,
false,
"Ingest data has a header row",
`Specifies whether ingested data has a header row or not.
@ -28,9 +26,11 @@ to detect the header.`,
// OptIngestCache specifies whether ingested data is cached or not.
var OptIngestCache = options.NewBool(
"ingest.cache",
"no-cache",
true,
0,
&options.Flag{
Name: "no-cache",
Invert: true,
Usage: "Don't cache ingest data",
},
true,
"Cache ingest data",
`Specifies whether ingested data is cached or not, on a default or per-source
@ -43,8 +43,7 @@ ingested each time.
$ sq config set ingest.cache false
# Set ingest caching behavior for a specific source
$ sq config set --src @sakila ingest.cache false
`,
$ sq config set --src @sakila ingest.cache false`,
options.TagSource,
)
@ -52,8 +51,7 @@ ingested each time.
// should take to determine ingest data type.
var OptIngestSampleSize = options.NewInt(
"ingest.sample-size",
"",
0,
nil,
256,
"Ingest data sample size for type detection",
`Specify the number of samples that a detector should take to determine type.`,
@ -64,8 +62,7 @@ var OptIngestSampleSize = options.NewInt(
// OptIngestColRename transforms a column name in ingested data.
var OptIngestColRename = options.NewString(
"ingest.column.rename",
"",
0,
nil,
"{{.Name}}{{with .Recurrence}}_{{.}}{{end}}",
func(s string) error {
return stringz.ValidTemplate("ingest.column.rename", s)

View File

@ -25,8 +25,7 @@ var (
// OptConnMaxOpen controls sql.DB.SetMaxOpenConn.
OptConnMaxOpen = options.NewInt(
"conn.max-open",
"",
0,
nil,
0,
"Max open connections to DB",
`Maximum number of open connections to the database.
@ -39,8 +38,7 @@ A value of zero indicates no limit.`,
// OptConnMaxIdle controls sql.DB.SetMaxIdleConns.
OptConnMaxIdle = options.NewInt(
"conn.max-idle",
"",
0,
nil,
2,
"Max connections in idle connection pool",
`Set the maximum number of connections in the idle connection pool. If
@ -55,8 +53,7 @@ If n <= 0, no idle connections are retained.`,
// OptConnMaxIdleTime controls sql.DB.SetConnMaxIdleTime.
OptConnMaxIdleTime = options.NewDuration(
"conn.max-idle-time",
"",
0,
nil,
time.Second*2,
"Max connection idle time",
`Sets the maximum amount of time a connection may be idle. Expired connections
@ -70,8 +67,7 @@ If n <= 0, connections are not closed due to a connection's idle time.`,
// OptConnMaxLifetime controls sql.DB.SetConnMaxLifetime.
OptConnMaxLifetime = options.NewDuration(
"conn.max-lifetime",
"",
0,
nil,
time.Minute*10,
"Max connection lifetime",
`
@ -86,8 +82,7 @@ If n <= 0, connections are not closed due to a connection's age.`,
// OptConnOpenTimeout controls connection open timeout.
OptConnOpenTimeout = options.NewDuration(
"conn.open-timeout",
"",
0,
nil,
time.Second*10,
"Connection open timeout",
"Max time to wait before a connection open timeout occurs.",
@ -99,8 +94,7 @@ If n <= 0, connections are not closed due to a connection's age.`,
// between retries.
OptMaxRetryInterval = options.NewDuration(
"retry.max-interval",
"",
0,
nil,
time.Second*3,
"Max interval between retries",
`The maximum interval to wait between retries. If an operation is
@ -113,8 +107,7 @@ operations back off, typically using a Fibonacci backoff.`,
// by an errgroup.
OptTuningErrgroupLimit = options.NewInt(
"tuning.errgroup-limit",
"",
0,
nil,
16,
"Max goroutines in any one errgroup",
`Controls the maximum number of goroutines that can be spawned by an errgroup.
@ -132,8 +125,7 @@ etc.`,
// insertion/writing.
OptTuningRecChanSize = options.NewInt(
"tuning.record-buffer",
"",
0,
nil,
1024,
"Size of record buffer",
`Controls the size of the buffer channel for record insertion/writing.`,

View File

@ -602,8 +602,7 @@ func mungeSetZeroValue(i int, rec []any, destMeta record.Meta) {
// OptResultColRename transforms a column name returned from the DB.
var OptResultColRename = options.NewString(
"result.column.rename",
"",
0,
nil,
"{{.Name}}{{with .Recurrence}}_{{.}}{{end}}",
func(s string) error {
return stringz.ValidTemplate("result.column.rename", s)

View File

@ -30,8 +30,7 @@ import (
// See also: driver.OptIngestCache.
var OptCacheLockTimeout = options.NewDuration(
"cache.lock.timeout",
"",
0,
nil,
time.Second*5,
"Wait timeout to acquire cache lock",
`Wait timeout to acquire cache lock. During this period, retry will occur

View File

@ -19,8 +19,7 @@ import (
var (
OptHTTPRequestTimeout = options.NewDuration(
"http.request.timeout",
"",
0,
nil,
time.Second*10,
"HTTP/S request initial response timeout duration",
`How long to wait for initial response from a HTTP/S endpoint before timeout
@ -32,8 +31,7 @@ Contrast with http.response.timeout.`,
)
OptHTTPResponseTimeout = options.NewDuration(
"http.response.timeout",
"",
0,
nil,
0,
"HTTP/S request completion timeout duration",
`How long to wait for the entire HTTP transaction to complete. This includes
@ -45,9 +43,7 @@ Contrast with http.request.timeout.`,
)
OptHTTPSInsecureSkipVerify = options.NewBool(
"https.insecure-skip-verify",
"",
false,
0,
nil,
false,
"Skip HTTPS TLS verification",
"Skip HTTPS TLS verification. Useful when downloading against self-signed certs.",

View File

@ -33,9 +33,7 @@ import (
var OptContinueOnError = options.NewBool(
"download.refresh.ok-on-err",
"",
false,
0,
nil,
true,
"Continue with stale download if refresh fails",
`Continue with stale download if refresh fails. This option applies if a download
@ -48,9 +46,7 @@ download when the network is unavailable. If false, an error is returned instead
var OptCache = options.NewBool(
"download.cache",
"",
false,
0,
nil,
true,
"Cache downloads",
`Cache downloaded remote files. When false, the download cache is not used and