diff options
Diffstat (limited to 'src/options.go')
-rw-r--r-- | src/options.go | 1015 |
1 files changed, 666 insertions, 349 deletions
diff --git a/src/options.go b/src/options.go index 38a5ba7c..83b0e97f 100644 --- a/src/options.go +++ b/src/options.go @@ -1,6 +1,7 @@ package fzf import ( + "errors" "fmt" "os" "regexp" @@ -10,20 +11,19 @@ import ( "github.com/junegunn/fzf/src/algo" "github.com/junegunn/fzf/src/tui" - "github.com/junegunn/fzf/src/util" "github.com/mattn/go-shellwords" "github.com/rivo/uniseg" ) -const usage = `usage: fzf [options] +const Usage = `usage: fzf [options] Search -x, --extended Extended-search mode (enabled by default; +x or --no-extended to disable) -e, --exact Enable Exact-match - -i Case-insensitive match (default: smart-case match) - +i Case-sensitive match + -i, --ignore-case Case-insensitive match (default: smart-case match) + +i, --no-ignore-case Case-sensitive match --scheme=SCHEME Scoring scheme [default|path|history] --literal Do not normalize latin script letters before matching -n, --nth=N[,..] Comma-separated list of field index expressions @@ -57,7 +57,7 @@ const usage = `usage: fzf [options] Layout --height=[~]HEIGHT[%] Display fzf window below the cursor with the given height instead of using fullscreen. - A negative value is calcalated as the terminal height + A negative value is calculated as the terminal height minus the given value. If prefixed with '~', fzf will determine the height according to the input size. @@ -92,6 +92,7 @@ const usage = `usage: fzf [options] --ansi Enable processing of ANSI color codes --tabstop=SPACES Number of spaces for a tab character (default: 8) --color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors + --highlight-line Highlight the whole current line --no-bold Do not use bold text History @@ -120,6 +121,7 @@ const usage = `usage: fzf [options] --read0 Read input delimited by ASCII NUL characters --print0 Print output delimited by ASCII NUL characters --sync Synchronous search for multi-staged filtering + --with-shell=STR Shell command and flags to start child processes with --listen[=[ADDR:]PORT] Start HTTP server to receive actions (POST /) (To allow remote process execution, use --listen-unsafe) --version Display version information and exit @@ -246,9 +248,10 @@ func (o *previewOpts) Toggle() { o.hidden = !o.hidden } -func parseLabelPosition(opts *labelOpts, arg string) { +func parseLabelPosition(opts *labelOpts, arg string) error { opts.column = 0 opts.bottom = false + var err error for _, token := range splitRegexp.Split(strings.ToLower(arg), -1) { switch token { case "center": @@ -258,9 +261,10 @@ func parseLabelPosition(opts *labelOpts, arg string) { case "top": opts.bottom = false default: - opts.column = atoi(token) + opts.column, err = atoi(token) } } + return err } func (a previewOpts) aboveOrBelow() bool { @@ -290,6 +294,8 @@ type walkerOpts struct { // Options stores the values of command-line options type Options struct { + Input chan string + Output chan string Bash bool Zsh bool Fish bool @@ -317,6 +323,7 @@ type Options struct { MinHeight int Layout layoutType Cycle bool + CursorLine bool KeepRight bool Hscroll bool HscrollOff int @@ -356,6 +363,7 @@ type Options struct { Unicode bool Ambidouble bool Tabstop int + WithShell string ListenAddr *listenAddress Unsafe bool ClearOnExit bool @@ -363,6 +371,7 @@ type Options struct { WalkerRoot string WalkerSkip []string Version bool + Help bool CPUProfile string MEMProfile string BlockProfile string @@ -453,21 +462,10 @@ func defaultOptions() *Options { WalkerOpts: walkerOpts{file: true, hidden: true, follow: true}, WalkerRoot: ".", WalkerSkip: []string{".git", "node_modules"}, + Help: false, Version: false} } -func help(code int) { - os.Stdout.WriteString(usage) - util.Exit(code) -} - -var errorContext = "" - -func errorExit(msg string) { - os.Stderr.WriteString(errorContext + msg + "\n") - util.Exit(exitError) -} - func optString(arg string, prefixes ...string) (bool, string) { for _, prefix := range prefixes { if strings.HasPrefix(arg, prefix) { @@ -477,13 +475,13 @@ func optString(arg string, prefixes ...string) (bool, string) { return false, "" } -func nextString(args []string, i *int, message string) string { +func nextString(args []string, i *int, message string) (string, error) { if len(args) > *i+1 { *i++ } else { - errorExit(message) + return "", errors.New(message) } - return args[*i] + return args[*i], nil } func optionalNextString(args []string, i *int) (bool, string) { @@ -494,44 +492,52 @@ func optionalNextString(args []string, i *int) (bool, string) { return false, "" } -func atoi(str string) int { +func atoi(str string) (int, error) { num, err := strconv.Atoi(str) if err != nil { - errorExit("not a valid integer: " + str) + return 0, errors.New("not a valid integer: " + str) } - return num + return num, nil } -func atof(str string) float64 { +func atof(str string) (float64, error) { num, err := strconv.ParseFloat(str, 64) if err != nil { - errorExit("not a valid number: " + str) + return 0, errors.New("not a valid number: " + str) } - return num + return num, nil } -func nextInt(args []string, i *int, message string) int { +func nextInt(args []string, i *int, message string) (int, error) { if len(args) > *i+1 { *i++ } else { - errorExit(message) + return 0, errors.New(message) + } + n, err := atoi(args[*i]) + if err != nil { + return 0, errors.New(message) } - return atoi(args[*i]) + return n, nil } -func optionalNumeric(args []string, i *int, defaultValue int) int { +func optionalNumeric(args []string, i *int, defaultValue int) (int, error) { if len(args) > *i+1 { if strings.IndexAny(args[*i+1], "0123456789") == 0 { *i++ - return atoi(args[*i]) + n, err := atoi(args[*i]) + if err != nil { + return 0, err + } + return n, nil } } - return defaultValue + return defaultValue, nil } -func splitNth(str string) []Range { +func splitNth(str string) ([]Range, error) { if match, _ := regexp.MatchString("^[0-9,-.]+$", str); !match { - errorExit("invalid format: " + str) + return nil, errors.New("invalid format: " + str) } tokens := strings.Split(str, ",") @@ -539,11 +545,11 @@ func splitNth(str string) []Range { for idx, s := range tokens { r, ok := ParseRange(&s) if !ok { - errorExit("invalid format: " + str) + return nil, errors.New("invalid format: " + str) } ranges[idx] = r } - return ranges + return ranges, nil } func delimiterRegexp(str string) Delimiter { @@ -573,72 +579,68 @@ func isNumeric(char uint8) bool { return char >= '0' && char <= '9' } -func parseAlgo(str string) algo.Algo { +func parseAlgo(str string) (algo.Algo, error) { switch str { case "v1": - return algo.FuzzyMatchV1 + return algo.FuzzyMatchV1, nil case "v2": - return algo.FuzzyMatchV2 - default: - errorExit("invalid algorithm (expected: v1 or v2)") + return algo.FuzzyMatchV2, nil } - return algo.FuzzyMatchV2 + return nil, errors.New("invalid algorithm (expected: v1 or v2)") } -func processScheme(opts *Options) { +func processScheme(opts *Options) error { if !algo.Init(opts.Scheme) { - errorExit("invalid scoring scheme (expected: default|path|history)") + return errors.New("invalid scoring scheme (expected: default|path|history)") } if opts.Scheme == "history" { opts.Criteria = []criterion{byScore} } + return nil } -func parseBorder(str string, optional bool) tui.BorderShape { +func parseBorder(str string, optional bool) (tui.BorderShape, error) { switch str { case "rounded": - return tui.BorderRounded + return tui.BorderRounded, nil case "sharp": - return tui.BorderSharp + return tui.BorderSharp, nil case "bold": - return tui.BorderBold + return tui.BorderBold, nil case "block": - return tui.BorderBlock + return tui.BorderBlock, nil case "thinblock": - return tui.BorderThinBlock + return tui.BorderThinBlock, nil case "double": - return tui.BorderDouble + return tui.BorderDouble, nil case "horizontal": - return tui.BorderHorizontal + return tui.BorderHorizontal, nil case "vertical": - return tui.BorderVertical + return tui.BorderVertical, nil case "top": - return tui.BorderTop + return tui.BorderTop, nil case "bottom": - return tui.BorderBottom + return tui.BorderBottom, nil case "left": - return tui.BorderLeft + return tui.BorderLeft, nil case "right": - return tui.BorderRight + return tui.BorderRight, nil case "none": - return tui.BorderNone - default: - if optional && str == "" { - return tui.DefaultBorderShape - } - errorExit("invalid border style (expected: rounded|sharp|bold|block|thinblock|double|horizontal|vertical|top|bottom|left|right|none)") + return tui.BorderNone, nil + } + if optional && str == "" { + return tui.DefaultBorderShape, nil } - return tui.BorderNone + return tui.BorderNone, errors.New("invalid border style (expected: rounded|sharp|bold|block|thinblock|double|horizontal|vertical|top|bottom|left|right|none)") } -func parseKeyChords(str string, message string) map[tui.Event]string { - return parseKeyChordsImpl(str, message, errorExit) +func parseKeyChords(str string, message string) (map[tui.Event]string, error) { + return parseKeyChordsImpl(str, message) } -func parseKeyChordsImpl(str string, message string, exit func(string)) map[tui.Event]string { +func parseKeyChordsImpl(str string, message string) (map[tui.Event]string, error) { if len(str) == 0 { - exit(message) - return nil + return nil, errors.New(message) } str = regexp.MustCompile("(?i)(alt-),").ReplaceAllString(str, "$1"+string([]rune{escapedComma})) @@ -706,6 +708,8 @@ func parseKeyChordsImpl(str string, message string, exit func(string)) map[tui.E add(tui.Jump) case "jump-cancel": add(tui.JumpCancel) + case "click-header": + add(tui.ClickHeader) case "alt-enter", "alt-return": chords[tui.CtrlAltKey('m')] = key case "alt-space": @@ -806,54 +810,64 @@ func parseKeyChordsImpl(str string, message string, exit func(string)) map[tui.E } else if len(runes) == 1 { chords[tui.Key(runes[0])] = key } else { - exit("unsupported key: " + key) - return nil + return nil, errors.New("unsupported key: " + key) } } } - return chords + return chords, nil } -func parseTiebreak(str string) []criterion { +func parseTiebreak(str string) ([]criterion, error) { criteria := []criterion{byScore} hasIndex := false hasChunk := false hasLength := false hasBegin := false hasEnd := false - check := func(notExpected *bool, name string) { + check := func(notExpected *bool, name string) error { if *notExpected { - errorExit("duplicate sort criteria: " + name) + return errors.New("duplicate sort criteria: " + name) } if hasIndex { - errorExit("index should be the last criterion") + return errors.New("index should be the last criterion") } *notExpected = true + return nil } for _, str := range strings.Split(strings.ToLower(str), ",") { switch str { case "index": - check(&hasIndex, "index") + if err := check(&hasIndex, "index"); err != nil { + return nil, err + } case "chunk": - check(&hasChunk, "chunk") + if err := check(&hasChunk, "chunk"); err != nil { + return nil, err + } criteria = append(criteria, byChunk) case "length": - check(&hasLength, "length") + if err := check(&hasLength, "length"); err != nil { + return nil, err + } criteria = append(criteria, byLength) case "begin": - check(&hasBegin, "begin") + if err := check(&hasBegin, "begin"); err != nil { + return nil, err + } criteria = append(criteria, byBegin) case "end": - check(&hasEnd, "end") + if err := check(&hasEnd, "end"); err != nil { + return nil, err + } criteria = append(criteria, byEnd) default: - errorExit("invalid sort criterion: " + str) + return nil, errors.New("invalid sort criterion: " + str) } } if len(criteria) > 4 { - errorExit("at most 3 tiebreaks are allowed: " + str) + return nil, errors.New("at most 3 tiebreaks are allowed: " + str) } - return criteria + return criteria, nil } func dupeTheme(theme *tui.ColorTheme) *tui.ColorTheme { @@ -861,7 +875,8 @@ func dupeTheme(theme *tui.ColorTheme) *tui.ColorTheme { return &dupe } -func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme { +func parseTheme(defaultTheme *tui.ColorTheme, str string) (*tui.ColorTheme, error) { + var err error theme := dupeTheme(defaultTheme) rrggbb := regexp.MustCompile("^#[0-9a-fA-F]{6}$") for _, str := range strings.Split(strings.ToLower(str), ",") { @@ -876,7 +891,8 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme { theme = tui.NoColorTheme() default: fail := func() { - errorExit("invalid color specification: " + str) + // Let the code proceed to simplify the error handling + err = errors.New("invalid color specification: " + str) } // Color is disabled if theme == nil { @@ -966,16 +982,22 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme { mergeAttr(&theme.PreviewFg) case "preview-bg": mergeAttr(&theme.PreviewBg) - case "fg+": + case "current-fg", "fg+": mergeAttr(&theme.Current) - case "bg+": + case "current-bg", "bg+": mergeAttr(&theme.DarkBg) + case "selected-fg": + mergeAttr(&theme.SelectedFg) + case "selected-bg": + mergeAttr(&theme.SelectedBg) case "gutter": mergeAttr(&theme.Gutter) case "hl": mergeAttr(&theme.Match) - case "hl+": + case "current-hl", "hl+": mergeAttr(&theme.CurrentMatch) + case "selected-hl": + mergeAttr(&theme.SelectedMatch) case "border": mergeAttr(&theme.Border) case "preview-border": @@ -999,7 +1021,7 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme { case "pointer": mergeAttr(&theme.Cursor) case "marker": - mergeAttr(&theme.Selected) + mergeAttr(&theme.Marker) case "header": mergeAttr(&theme.Header) default: @@ -1007,10 +1029,10 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme { } } } - return theme + return theme, err } -func parseWalkerOpts(str string) walkerOpts { +func parseWalkerOpts(str string) (walkerOpts, error) { opts := walkerOpts{} for _, str := range strings.Split(strings.ToLower(str), ",") { switch str { @@ -1025,13 +1047,13 @@ func parseWalkerOpts(str string) walkerOpts { case "": // Ignored default: - errorExit("invalid walker option: " + str) + return opts, errors.New("invalid walker option: " + str) } } if !opts.file && !opts.dir { - errorExit("at least one of 'file' or 'dir' should be specified") + return opts, errors.New("at least one of 'file' or 'dir' should be specified") } - return opts + return opts, nil } var ( @@ -1055,7 +1077,7 @@ const ( func init() { executeRegexp = regexp.MustCompile( - `(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:header|query|prompt|border-label|preview-label)|transform|change-preview-window|change-preview|(?:re|un)bind|pos|put)`) + `(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:header|query|prompt|border-label|preview-label)|transform|change-(?:preview-window|preview|multi)|(?:re|un)bind|pos|put)`) splitRegexp = regexp.MustCompile("[,:]+") actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+") } @@ -1075,7 +1097,7 @@ Loop: break } cs := string(action[0]) - ce := ")" + var ce string switch action[0] { case ':': masked += strings.Repeat(" ", len(action)) @@ -1116,13 +1138,13 @@ Loop: return masked } -func parseSingleActionList(str string, exit func(string)) []*action { +func parseSingleActionList(str string) ([]*action, error) { // We prepend a colon to satisfy executeRegexp and remove it later masked := maskActionContents(":" + str)[1:] - return parseActionList(masked, str, []*action{}, false, exit) + return parseActionList(masked, str, []*action{}, false) } -func parseActionList(masked string, original string, prevActions []*action, putAllowed bool, exit func(string)) []*action { +func parseActionList(masked string, original string, prevActions []*action, putAllowed bool) ([]*action, error) { maskedStrings := strings.Split(masked, "+") originalStrings := make([]string, len(maskedStrings)) idx := 0 @@ -1299,15 +1321,17 @@ func parseActionList(masked string, original string, prevActions []*action, putA if putAllowed { appendAction(actChar) } else { - exit("unable to put non-printable character") + return nil, errors.New("unable to put non-printable character") } default: t := isExecuteAction(specLower) if t == actIgnore { if specIndex == 0 && specLower == "" { actions = append(prevActions, actions...) + } else if specLower == "change-multi" { + appendAction(actChangeMulti) } else { - exit("unknown action: " + spec) + return nil, errors.New("unknown action: " + spec) } } else { offset := len(actionNameRegexp.FindString(spec)) @@ -1325,27 +1349,28 @@ func parseActionList(masked string, original string, prevActions []*action, putA actions = append(actions, &action{t: t, a: actionArg}) } switch t { - case actBecome: - if util.IsWindows() { - exit("become action is not supported on Windows") - } case actUnbind, actRebind: - parseKeyChordsImpl(actionArg, spec[0:offset]+" target required", exit) + if _, err := parseKeyChordsImpl(actionArg, spec[0:offset]+" target required"); err != nil { + return nil, err + } case actChangePreviewWindow: opts := previewOpts{} for _, arg := range strings.Split(actionArg, "|") { // Make sure that each expression is valid - parsePreviewWindowImpl(&opts, arg, exit) + if err := parsePreviewWindowImpl(&opts, arg); err != nil { + return nil, err + } } } } } prevSpec = "" } - return actions + return actions, nil } -func parseKeymap(keymap map[tui.Event][]*action, str string, exit func(string)) { +func parseKeymap(keymap map[tui.Event][]*action, str string) error { + var err error masked := maskActionContents(str) idx := 0 for _, pairStr := range strings.Split(masked, ",") { @@ -1354,7 +1379,7 @@ func parseKeymap(keymap map[tui.Event][]*action, str string, exit func(string)) pair := strings.SplitN(pairStr, ":", 2) if len(pair) < 2 { - exit("bind action not specified: " + origPairStr) + return errors.New("bind action not specified: " + origPairStr) } var key tui.Event if len(pair[0]) == 1 && pair[0][0] == escapedColon { @@ -1364,12 +1389,19 @@ func parseKeymap(keymap map[tui.Event][]*action, str string, exit func(string)) } else if len(pair[0]) == 1 && pair[0][0] == escapedPlus { key = tui.Key('+') } else { - keys := parseKeyChordsImpl(pair[0], "key name required", exit) + keys, err := parseKeyChordsImpl(pair[0], "key name required") + if err != nil { + return err + } key = firstKey(keys) } putAllowed := key.Type == tui.Rune && unicode.IsGraphic(key.Char) - keymap[key] = parseActionList(pair[1], origPairStr[len(pair[0])+1:], keymap[key], putAllowed, exit) + keymap[key], err = parseActionList(pair[1], origPairStr[len(pair[0])+1:], keymap[key], putAllowed) + if err != nil { + return err + } } + return nil } func isExecuteAction(str string) actionType { @@ -1407,6 +1439,8 @@ func isExecuteAction(str string) actionType { return actChangePrompt case "change-query": return actChangeQuery + case "change-multi": + return actChangeMulti case "pos": return actPosition case "execute": @@ -1433,43 +1467,56 @@ func isExecuteAction(str string) actionType { return actIgnore } -func parseToggleSort(keymap map[tui.Event][]*action, str string) { - keys := parseKeyChords(str, "key name required") +func parseToggleSort(keymap map[tui.Event][]*action, str string) error { + keys, err := parseKeyChords(str, "key name required") + if err != nil { + return err + } if len(keys) != 1 { - errorExit("multiple keys specified") + return errors.New("multiple keys specified") } keymap[firstKey(keys)] = toActions(actToggleSort) + return nil } func strLines(str string) []string { return strings.Split(strings.TrimSuffix(str, "\n"), "\n") } -func parseSize(str string, maxPercent float64, label string) sizeSpec { +func parseSize(str string, maxPercent float64, label string) (sizeSpec, error) { + var spec = sizeSpec{} var val float64 + var err error percent := strings.HasSuffix(str, "%") if percent { - val = atof(str[:len(str)-1]) + if val, err = atof(str[:len(str)-1]); err != nil { + return spec, err + } + if val < 0 { - errorExit(label + " must be non-negative") + return spec, errors.New(label + " must be non-negative") } if val > maxPercent { - errorExit(fmt.Sprintf("%s too large (max: %d%%)", label, int(maxPercent))) + return spec, fmt.Errorf("%s too large (max: %d%%)", label, int(maxPercent)) } } else { if strings.Contains(str, ".") { - errorExit(label + " (without %) must be a non-negative integer") + return spec, errors.New(label + " (without %) must be a non-negative integer") } - val = float64(atoi(str)) + i, err := atoi(str) + if err != nil { + return spec, err + } + val = float64(i) if val < 0 { - errorExit(label + " must be non-negative") + return spec, errors.New(label + " must be non-negative") } } - return sizeSpec{val, percent} + return sizeSpec{val, percent}, nil } -func parseHeight(str string) heightSpec { +func parseHeight(str string) (heightSpec, error) { heightSpec := heightSpec{} if strings.HasPrefix(str, "~") { heightSpec.auto = true @@ -1477,66 +1524,66 @@ func parseHeight(str string) heightSpec { } if strings.HasPrefix(str, "-") { if heightSpec.auto { - errorExit("negative(-) height is not compatible with adaptive(~) height") + return heightSpec, errors.New("negative(-) height is not compatible with adaptive(~) height") } heightSpec.inverse = true str = str[1:] } - size := parseSize(str, 100, "height") + size, err := parseSize(str, 100, "height") + if err != nil { + return heightSpec, err + } heightSpec.size = size.size heightSpec.percent = size.percent - return heightSpec + return heightSpec, nil } -func parseLayout(str string) layoutType { +func parseLayout(str string) (layoutType, error) { switch str { case "default": - return layoutDefault + return layoutDefault, nil case "reverse": - return layoutReverse + return layoutReverse, nil case "reverse-list": - return layoutReverseList - default: - errorExit("invalid layout (expected: default / reverse / reverse-list)") + return layoutReverseList, nil } - return layoutDefault + return layoutDefault, errors.New("invalid layout (expected: default / reverse / reverse-list)") } -func parseInfoStyle(str string) (infoStyle, string) { +func parseInfoStyle(str string) (infoStyle, string, error) { switch str { case "default": - return infoDefault, "" + return infoDefault, "", nil case "right": - return infoRight, "" + return infoRight, "", nil case "inline": - return infoInline, defaultInfoPrefix + return infoInline, defaultInfoPrefix, nil case "inline-right": - return infoInlineRight, "" + return infoInlineRight, "", nil case "hidden": - return infoHidden, "" - default: - type infoSpec struct { - name string - style infoStyle - } - for _, spec := range []infoSpec{ - {"inline", infoInline}, - {"inline-right", infoInlineRight}} { - if strings.HasPrefix(str, spec.name+":") { - return spec.style, strings.ReplaceAll(str[len(spec.name)+1:], "\n", " ") - } + return infoHidden, "", nil + } + type infoSpec struct { + name string + style infoStyle + } + for _, spec := range []infoSpec{ + {"inline", infoInline}, + {"inline-right", infoInlineRight}} { + if strings.HasPrefix(str, spec.name+":") { + return spec.style, strings.ReplaceAll(str[len(spec.name)+1:], "\n", " "), nil } - errorExit("invalid info style (expected: default|right|hidden|inline[-right][:PREFIX])") } - return infoDefault, "" + return infoDefault, "", errors.New("invalid info style (expected: default|right|hidden|inline[-right][:PREFIX])") } -func parsePreviewWindow(opts *previewOpts, input string) { - parsePreviewWindowImpl(opts, input, errorExit) +func parsePreviewWindow(opts *previewOpts, input string) error { + return parsePreviewWindowImpl(opts, input) } -func parsePreviewWindowImpl(opts *previewOpts, input string, exit func(string)) { +func parsePreviewWindowImpl(opts *previewOpts, input string) error { + var err error tokenRegex := regexp.MustCompile(`[:,]*(<([1-9][0-9]*)\(([^)<]+)\)|[^,:]+)`) sizeRegex := regexp.MustCompile("^[0-9]+%?$") offsetRegex := regexp.MustCompile(`^(\+{-?[0-9]+})?([+-][0-9]+)*(-?/[1-9][0-9]*)?$`) @@ -1545,7 +1592,9 @@ func parsePreviewWindowImpl(opts *previewOpts, input string, exit func(string)) var alternative string for _, match := range tokens { if len(match[2]) > 0 { - opts.threshold = atoi(match[2]) + if opts.threshold, err = atoi(match[2]); err != nil { + return err + } alternative = match[3] continue } @@ -1606,14 +1655,17 @@ func parsePreviewWindowImpl(opts *previewOpts, input string, exit func(string)) opts.follow = false default: if headerRegex.MatchString(token) { - opts.headerLines = atoi(token[1:]) + if opts.headerLines, err = atoi(token[1:]); err != nil { + return err + } } else if sizeRegex.MatchString(token) { - opts.size = parseSize(token, 99, "window size") + if opts.size, err = parseSize(token, 99, "window size"); err != nil { + return err + } } else if offsetRegex.MatchString(token) { opts.scroll = token } else { - exit("invalid preview window option: " + token) - return + return errors.New("invalid preview window option: " + token) } } } @@ -1622,82 +1674,119 @@ func parsePreviewWindowImpl(opts *previewOpts, input string, exit func(string)) opts.alternative = &alternativeOpts opts.alternative.hidden = false opts.alternative.alternative = nil - parsePreviewWindowImpl(opts.alternative, alternative, exit) + err = parsePreviewWindowImpl(opts.alternative, alternative) } + return err } -func parseMargin(opt string, margin string) [4]sizeSpec { +func parseMargin(opt string, margin string) ([4]sizeSpec, error) { margins := strings.Split(margin, ",") - checked := func(str string) sizeSpec { + checked := func(str string) (sizeSpec, error) { return parseSize(str, 49, opt) } switch len(margins) { case 1: - m := checked(margins[0]) - return [4]sizeSpec{m, m, m, m} + m, e := checked(margins[0]) + return [4]sizeSpec{m, m, m, m}, e case 2: - tb := checked(margins[0]) - rl := checked(margins[1]) - return [4]sizeSpec{tb, rl, tb, rl} + tb, e := checked(margins[0]) + if e != nil { + return defaultMargin(), e + } + rl, e := checked(margins[1]) + if e != nil { + return defaultMargin(), e + } + return [4]sizeSpec{tb, rl, tb, rl}, nil case 3: - t := checked(margins[0]) - rl := checked(margins[1]) - b := checked(margins[2]) - return [4]sizeSpec{t, rl, b, rl} + t, e := checked(margins[0]) + if e != nil { + return defaultMargin(), e + } + rl, e := checked(margins[1]) + if e != nil { + return defaultMargin(), e + } + b, e := checked(margins[2]) + if e != nil { + return defaultMargin(), e + } + return [4]sizeSpec{t, rl, b, rl}, nil case 4: - return [4]sizeSpec{ - checked(margins[0]), checked(margins[1]), - checked(margins[2]), checked(margins[3])} - default: - errorExit("invalid " + opt + ": " + margin) + t, e := checked(margins[0]) + if e != nil { + return defaultMargin(), e + } + r, e := checked(margins[1]) + if e != nil { + return defaultMargin(), e + } + b, e := checked(margins[2]) + if e != nil { + return defaultMargin(), e + } + l, e := checked(margins[3]) + if e != nil { + return defaultMargin(), e + } + return [4]sizeSpec{t, r, b, l}, nil } - return defaultMargin() + return [4]sizeSpec{}, errors.New("invalid " + opt + ": " + margin) } -func parseOptions(opts *Options, allArgs []string) { +func parseOptions(opts *Options, allArgs []string) error { + var err error var historyMax int if opts.History == nil { historyMax = defaultHistoryMax } else { historyMax = opts.History.maxSize } - setHistory := func(path string) { + setHistory := func(path string) error { h, e := NewHistory(path, historyMax) if e != nil { - errorExit(e.Error()) + return e } opts.History = h + return nil } - setHistoryMax := func(max int) { + setHistoryMax := func(max int) error { historyMax = max if historyMax < 1 { - errorExit("history max must be a positive integer") + return errors.New("history max must be a positive integer") } if opts.History != nil { opts.History.maxSize = historyMax } + return nil } validateJumpLabels := false + clearExitingOpts := func() { + // Last-one-wins strategy + opts.Bash = false + opts.Zsh = false + opts.Fish = false + opts.Help = false + opts.Version = false + } for i := 0; i < len(allArgs); i++ { arg := allArgs[i] switch arg { case "--bash": + clearExitingOpts() opts.Bash = true - if opts.Zsh || opts.Fish { - errorExit("cannot specify --bash with --zsh or --fish") - } case "--zsh": + clearExitingOpts() opts.Zsh = true - if opts.Bash || opts.Fish { - errorExit("cannot specify --zsh with --bash or --fish") - } case "--fish": + clearExitingOpts() opts.Fish = true - if opts.Bash || opts.Zsh { - errorExit("cannot specify --fish with --bash or --zsh") - } case "-h", "--help": - help(exitOk) + clearExitingOpts() + opts.Help = true + case "--version": + clearExitingOpts() + opts.Version = true case "-x", "--extended": opts.Extended = true case "-e", "--exact": @@ -1711,20 +1800,43 @@ func parseOptions(opts *Options, allArgs []string) { case "+e", "--no-exact": opts.Fuzzy = true case "-q", "--query": - opts.Query = nextString(allArgs, &i, "query string required") + if opts.Query, err = nextString(allArgs, &i, "query string required"); err != nil { + return err + } case "-f", "--filter": - filter := nextString(allArgs, &i, "query string required") + filter, err := nextString(allArgs, &i, "query string required") + if err != nil { + return err + } opts.Filter = &filter case "--literal": opts.Normalize = false case "--no-literal": opts.Normalize = true case "--algo": - opts.FuzzyAlgo = parseAlgo(nextString(allArgs, &i, "algorithm required (v1|v2)")) + str, err := nextString(allArgs, &i, "algorithm required (v1|v2)") + if err != nil { + return err + } + if opts.FuzzyAlgo, err = parseAlgo(str); err != nil { + return err + } case "--scheme": - opts.Scheme = strings.ToLower(nextString(allArgs, &i, "scoring scheme required (default|path|history)")) + str, err := nextString(allArgs, &i, "scoring scheme required (default|path|history)") + if err != nil { + return err + } + opts.Scheme = strings.ToLower(str) case "--expect": - for k, v := range parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required") { + str, err := nextString(allArgs, &i, "key names required") + if err != nil { + return err + } + chords, err := parseKeyChords(str, "key names required") + if err != nil { + return err + } + for k, v := range chords { opts.Expect[k] = v } case "--no-expect": @@ -1734,26 +1846,64 @@ func parseOptions(opts *Options, allArgs []string) { case "--disabled", "--phony": opts.Phony = true case "--tiebreak": - opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required")) + str, err := nextString(allArgs, &i, "sort criterion required") + if err != nil { + return err + } + if opts.Criteria, err = parseTiebreak(str); err != nil { + return err + } case "--bind": - parseKeymap(opts.Keymap, nextString(allArgs, &i, "bind expression required"), errorExit) + str, err := nextString(allArgs, &i, "bind expression required") + if err != nil { + return err + } + if err := parseKeymap(opts.Keymap, str); err != nil { + return err + } case "--color": _, spec := optionalNextString(allArgs, &i) if len(spec) == 0 { opts.Theme = tui.EmptyTheme() } else { - opts.Theme = parseTheme(opts.Theme, spec) + if opts.Theme, err = parseTheme(opts.Theme, spec); err != nil { + return err + } } case "--toggle-sort": - parseToggleSort(opts.Keymap, nextString(allArgs, &i, "key name required")) + str, err := nextString(allArgs, &i, "key name required") + if err != nil { + return err + } + if err := parseToggleSort(opts.Keymap, str); err != nil { + return err + } case "-d", "--delimiter": - opts.Delimiter = delimiterRegexp(nextString(allArgs, &i, "delimiter required")) + str, err := nextString(allArgs, &i, "delimiter required") + if err != nil { + return err + } + opts.Delimiter = delimiterRegexp(str) case "-n", "--nth": - opts.Nth = splitNth(nextString(allArgs, &i, "nth expression required")) + str, err := nextString(allArgs, &i, "nth expression required") + if err != nil { + return err + } + if opts.Nth, err = splitNth(str); err != nil { + return err + } case "--with-nth": - opts.WithNth = splitNth(nextString(allArgs, &i, "nth expression required")) + str, err := nextString(allArgs, &i, "nth expression required") + if err != nil { + return err + } + if opts.WithNth, err = splitNth(str); err != nil { + return err + } case "-s", "--sort": - opts.Sort = optionalNumeric(allArgs, &i, 1) + if opts.Sort, err = optionalNumeric(allArgs, &i, 1); err != nil { + return err + } case "+s", "--no-sort": opts.Sort = 0 case "--track": @@ -1764,12 +1914,14 @@ func parseOptions(opts *Options, allArgs []string) { opts.Tac = true case "--no-tac": opts.Tac = false - case "-i": + case "-i", "--ignore-case": opts.Case = CaseIgnore - case "+i": + case "+i", "--no-ignore-case": opts.Case = CaseRespec |