Skip to content
Snippets Groups Projects
options.go 9.16 KiB
Newer Older
Junegunn Choi's avatar
Junegunn Choi committed
package fzf

import (
	"fmt"
	"os"
	"regexp"
	"strings"
	"unicode/utf8"

	"github.com/junegunn/fzf/src/curses"
Junegunn Choi's avatar
Junegunn Choi committed

	"github.com/junegunn/go-shellwords"
Junegunn Choi's avatar
Junegunn Choi committed
)

Junegunn Choi's avatar
Junegunn Choi committed
const usage = `usage: fzf [options]
Junegunn Choi's avatar
Junegunn Choi committed

Junegunn Choi's avatar
Junegunn Choi committed
    -x, --extended        Extended-search mode
    -e, --extended-exact  Extended-search mode (exact match)
    -i                    Case-insensitive match (default: smart-case match)
    +i                    Case-sensitive match
    -n, --nth=N[,..]      Comma-separated list of field index expressions
                          for limiting search scope. Each can be a non-zero
                          integer or a range expression ([BEGIN]..[END])
        --with-nth=N[,..] Transform the item using index expressions for search
    -d, --delimiter=STR   Field delimiter regex for --nth (default: AWK-style)

  Search result
    +s, --no-sort         Do not sort the result
        --tac             Reverse the order of the input
                          (e.g. 'history | fzf --tac --no-sort')
Junegunn Choi's avatar
Junegunn Choi committed

  Interface
    -m, --multi           Enable multi-select with tab/shift-tab
Junegunn Choi's avatar
Junegunn Choi committed
        --ansi            Enable processing of ANSI color codes
Junegunn Choi's avatar
Junegunn Choi committed
        --no-mouse        Disable mouse
    +c, --no-color        Disable colors
    +2, --no-256          Disable 256-color
        --black           Use black background
        --reverse         Reverse orientation
        --prompt=STR      Input prompt (default: '> ')

  Scripting
    -q, --query=STR       Start the finder with the given query
    -1, --select-1        Automatically select the only match
    -0, --exit-0          Exit immediately when there's no match
    -f, --filter=STR      Filter mode. Do not start interactive finder.
        --print-query     Print query as the first line
        --expect=KEYS     Comma-separated list of keys to complete fzf
Junegunn Choi's avatar
Junegunn Choi committed
        --sync            Synchronous search for multi-staged filtering
                          (e.g. 'fzf --multi | fzf --sync')
Junegunn Choi's avatar
Junegunn Choi committed

  Environment variables
    FZF_DEFAULT_COMMAND   Default command to use when input is tty
Junegunn Choi's avatar
Junegunn Choi committed
    FZF_DEFAULT_OPTS      Defaults options. (e.g. '-x -m')
Junegunn Choi's avatar
Junegunn Choi committed
// Mode denotes the current search mode
Junegunn Choi's avatar
Junegunn Choi committed
type Mode int

Junegunn Choi's avatar
Junegunn Choi committed
// Search modes
Junegunn Choi's avatar
Junegunn Choi committed
const (
Junegunn Choi's avatar
Junegunn Choi committed
	ModeFuzzy Mode = iota
	ModeExtended
	ModeExtendedExact
Junegunn Choi's avatar
Junegunn Choi committed
)

Junegunn Choi's avatar
Junegunn Choi committed
// Case denotes case-sensitivity of search
Junegunn Choi's avatar
Junegunn Choi committed
type Case int

Junegunn Choi's avatar
Junegunn Choi committed
// Case-sensitivities
Junegunn Choi's avatar
Junegunn Choi committed
const (
Junegunn Choi's avatar
Junegunn Choi committed
	CaseSmart Case = iota
	CaseIgnore
	CaseRespect
Junegunn Choi's avatar
Junegunn Choi committed
)

Junegunn Choi's avatar
Junegunn Choi committed
// Options stores the values of command-line options
Junegunn Choi's avatar
Junegunn Choi committed
type Options struct {
	Mode       Mode
	Case       Case
	Nth        []Range
	WithNth    []Range
	Delimiter  *regexp.Regexp
	Sort       int
Junegunn Choi's avatar
Junegunn Choi committed
	Multi      bool
	Ansi       bool
Junegunn Choi's avatar
Junegunn Choi committed
	Mouse      bool
	Color      bool
	Color256   bool
	Black      bool
	Reverse    bool
	Prompt     string
	Query      string
	Select1    bool
	Exit0      bool
	Filter     *string
Junegunn Choi's avatar
Junegunn Choi committed
	PrintQuery bool
Junegunn Choi's avatar
Junegunn Choi committed
	Sync       bool
Junegunn Choi's avatar
Junegunn Choi committed
	Version    bool
}

Junegunn Choi's avatar
Junegunn Choi committed
func defaultOptions() *Options {
Junegunn Choi's avatar
Junegunn Choi committed
	return &Options{
Junegunn Choi's avatar
Junegunn Choi committed
		Mode:       ModeFuzzy,
		Case:       CaseSmart,
Junegunn Choi's avatar
Junegunn Choi committed
		Nth:        make([]Range, 0),
		WithNth:    make([]Range, 0),
		Delimiter:  nil,
		Sort:       1000,
Junegunn Choi's avatar
Junegunn Choi committed
		Multi:      false,
		Ansi:       false,
Junegunn Choi's avatar
Junegunn Choi committed
		Mouse:      true,
		Color:      true,
		Color256:   strings.Contains(os.Getenv("TERM"), "256"),
		Black:      false,
		Reverse:    false,
		Prompt:     "> ",
		Query:      "",
		Select1:    false,
		Exit0:      false,
		Filter:     nil,
Junegunn Choi's avatar
Junegunn Choi committed
		PrintQuery: false,
Junegunn Choi's avatar
Junegunn Choi committed
		Sync:       false,
Junegunn Choi's avatar
Junegunn Choi committed
		Version:    false}
}

func help(ok int) {
Junegunn Choi's avatar
Junegunn Choi committed
	os.Stderr.WriteString(usage)
Junegunn Choi's avatar
Junegunn Choi committed
	os.Exit(ok)
}

func errorExit(msg string) {
	os.Stderr.WriteString(msg + "\n")
	help(1)
}

func optString(arg string, prefix string) (bool, string) {
	rx, _ := regexp.Compile(fmt.Sprintf("^(?:%s)(.*)$", prefix))
	matches := rx.FindStringSubmatch(arg)
	if len(matches) > 1 {
		return true, matches[1]
	}
Junegunn Choi's avatar
Junegunn Choi committed
	return false, ""
Junegunn Choi's avatar
Junegunn Choi committed
}

func nextString(args []string, i *int, message string) string {
	if len(args) > *i+1 {
		*i++
	} else {
		errorExit(message)
	}
	return args[*i]
}

func optionalNumeric(args []string, i *int) int {
	if len(args) > *i+1 {
		if strings.IndexAny(args[*i+1], "0123456789") == 0 {
			*i++
		}
	}
	return 1 // Don't care
}

func splitNth(str string) []Range {
	if match, _ := regexp.MatchString("^[0-9,-.]+$", str); !match {
		errorExit("invalid format: " + str)
	}

	tokens := strings.Split(str, ",")
	ranges := make([]Range, len(tokens))
	for idx, s := range tokens {
		r, ok := ParseRange(&s)
		if !ok {
			errorExit("invalid format: " + str)
		}
		ranges[idx] = r
	}
	return ranges
}

func delimiterRegexp(str string) *regexp.Regexp {
	rx, e := regexp.Compile(str)
	if e != nil {
		str = regexp.QuoteMeta(str)
	}

	rx, e = regexp.Compile(fmt.Sprintf("(?:.*?%s)|(?:.+?$)", str))
	if e != nil {
		errorExit("invalid regular expression: " + e.Error())
	}
	return rx
}

func isAlphabet(char uint8) bool {
	return char >= 'a' && char <= 'z'
}

func parseKeyChords(str string) []int {
	var chords []int
	for _, key := range strings.Split(str, ",") {
		lkey := strings.ToLower(key)
		if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) {
			chords = append(chords, curses.CtrlA+int(lkey[5])-'a')
		} else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) {
			chords = append(chords, curses.AltA+int(lkey[4])-'a')
		} else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '4' {
			chords = append(chords, curses.F1+int(key[1])-'1')
		} else if utf8.RuneCountInString(key) == 1 {
			chords = append(chords, curses.AltZ+int([]rune(key)[0]))
		} else {
			errorExit("unsupported key: " + key)
		}
	}
	return chords
}

Junegunn Choi's avatar
Junegunn Choi committed
func parseOptions(opts *Options, allArgs []string) {
	for i := 0; i < len(allArgs); i++ {
		arg := allArgs[i]
		switch arg {
		case "-h", "--help":
			help(0)
		case "-x", "--extended":
Junegunn Choi's avatar
Junegunn Choi committed
			opts.Mode = ModeExtended
Junegunn Choi's avatar
Junegunn Choi committed
		case "-e", "--extended-exact":
Junegunn Choi's avatar
Junegunn Choi committed
			opts.Mode = ModeExtendedExact
Junegunn Choi's avatar
Junegunn Choi committed
		case "+x", "--no-extended", "+e", "--no-extended-exact":
Junegunn Choi's avatar
Junegunn Choi committed
			opts.Mode = ModeFuzzy
Junegunn Choi's avatar
Junegunn Choi committed
		case "-q", "--query":
			opts.Query = nextString(allArgs, &i, "query string required")
		case "-f", "--filter":
			filter := nextString(allArgs, &i, "query string required")
			opts.Filter = &filter
		case "--expect":
			opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required"))
Junegunn Choi's avatar
Junegunn Choi committed
		case "-d", "--delimiter":
			opts.Delimiter = delimiterRegexp(nextString(allArgs, &i, "delimiter required"))
		case "-n", "--nth":
			opts.Nth = splitNth(nextString(allArgs, &i, "nth expression required"))
		case "--with-nth":
			opts.WithNth = splitNth(nextString(allArgs, &i, "nth expression required"))
		case "-s", "--sort":
			opts.Sort = optionalNumeric(allArgs, &i)
		case "+s", "--no-sort":
			opts.Sort = 0
		case "--tac":
			opts.Tac = true
		case "--no-tac":
			opts.Tac = false
Junegunn Choi's avatar
Junegunn Choi committed
		case "-i":
Junegunn Choi's avatar
Junegunn Choi committed
			opts.Case = CaseIgnore
Junegunn Choi's avatar
Junegunn Choi committed
		case "+i":
Junegunn Choi's avatar
Junegunn Choi committed
			opts.Case = CaseRespect
Junegunn Choi's avatar
Junegunn Choi committed
		case "-m", "--multi":
			opts.Multi = true
		case "+m", "--no-multi":
			opts.Multi = false
		case "--ansi":
			opts.Ansi = true
		case "--no-ansi":
			opts.Ansi = false
Junegunn Choi's avatar
Junegunn Choi committed
		case "--no-mouse":
			opts.Mouse = false
		case "+c", "--no-color":
			opts.Color = false
		case "+2", "--no-256":
			opts.Color256 = false
		case "--black":
			opts.Black = true
		case "--no-black":
			opts.Black = false
		case "--reverse":
			opts.Reverse = true
		case "--no-reverse":
			opts.Reverse = false
		case "-1", "--select-1":
			opts.Select1 = true
		case "+1", "--no-select-1":
			opts.Select1 = false
		case "-0", "--exit-0":
			opts.Exit0 = true
		case "+0", "--no-exit-0":
			opts.Exit0 = false
		case "--print-query":
			opts.PrintQuery = true
		case "--no-print-query":
			opts.PrintQuery = false
		case "--prompt":
			opts.Prompt = nextString(allArgs, &i, "prompt string required")
Junegunn Choi's avatar
Junegunn Choi committed
		case "--sync":
			opts.Sync = true
		case "--no-sync":
			opts.Sync = false
		case "--async":
			opts.Sync = false
Junegunn Choi's avatar
Junegunn Choi committed
		case "--version":
			opts.Version = true
		default:
			if match, value := optString(arg, "-q|--query="); match {
				opts.Query = value
			} else if match, value := optString(arg, "-f|--filter="); match {
				opts.Filter = &value
			} else if match, value := optString(arg, "-d|--delimiter="); match {
				opts.Delimiter = delimiterRegexp(value)
			} else if match, value := optString(arg, "--prompt="); match {
				opts.Prompt = value
			} else if match, value := optString(arg, "-n|--nth="); match {
				opts.Nth = splitNth(value)
			} else if match, value := optString(arg, "--with-nth="); match {
				opts.WithNth = splitNth(value)
			} else if match, _ := optString(arg, "-s|--sort="); match {
				opts.Sort = 1 // Don't care
			} else if match, value := optString(arg, "--expect="); match {
				opts.Expect = parseKeyChords(value)
Junegunn Choi's avatar
Junegunn Choi committed
			} else {
				errorExit("unknown option: " + arg)
			}
		}
	}

	// If we're not using extended search mode, --nth option becomes irrelevant
	// if it contains the whole range
	if opts.Mode == ModeFuzzy || len(opts.Nth) == 1 {
		for _, r := range opts.Nth {
			if r.begin == rangeEllipsis && r.end == rangeEllipsis {
				opts.Nth = make([]Range, 0)
				return
			}
		}
	}
Junegunn Choi's avatar
Junegunn Choi committed
// ParseOptions parses command-line options
Junegunn Choi's avatar
Junegunn Choi committed
func ParseOptions() *Options {
Junegunn Choi's avatar
Junegunn Choi committed
	opts := defaultOptions()
Junegunn Choi's avatar
Junegunn Choi committed

	// Options from Env var
	words, _ := shellwords.Parse(os.Getenv("FZF_DEFAULT_OPTS"))
	parseOptions(opts, words)

	// Options from command-line arguments
	parseOptions(opts, os.Args[1:])
	return opts
}