summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJunegunn Choi <junegunn.c@gmail.com>2025-02-12 20:15:04 +0900
committerJunegunn Choi <junegunn.c@gmail.com>2025-02-12 20:15:04 +0900
commit84e2262ad63df2112f16b2a80fc661294c3da45e (patch)
tree803f4bf41de9d0011efcc2e29f788ac990fc7c73
parent378137d34a2a11b16c66dff2bf4309c7ce232a94 (diff)
downloadfzf-84e2262ad63df2112f16b2a80fc661294c3da45e.tar.gz
Make --accept-nth and --with-nth support templates
-rw-r--r--CHANGELOG.md36
-rw-r--r--man/man1/fzf.129
-rw-r--r--src/core.go7
-rw-r--r--src/options.go66
-rw-r--r--src/terminal.go12
-rw-r--r--src/tokenizer.go35
-rw-r--r--src/util/chars.go5
-rw-r--r--test/test_core.rb9
-rw-r--r--test/test_filter.rb7
9 files changed, 158 insertions, 48 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7202d1d5..657df7bf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,40 @@
CHANGELOG
=========
+0.60.0
+------
+
+- Added `--accept-nth` for choosing output fields
+ ```sh
+ ps -ef | fzf --multi --header-lines 1 | awk '{print $2}'
+ # Becomes
+ ps -ef | fzf --multi --header-lines 1 --accept-nth 2
+
+ git branch | fzf | cut -c3-
+ # Can be rewritten as
+ git branch | fzf --accept-nth -1
+ ```
+- `--accept-nth` and `--with-nth` now support a template that includes multiple field index expressions in curly braces
+ ```sh
+ echo foo,bar,baz | fzf --delimiter , --accept-nth '{1}, {3}, {2}'
+ # foo, baz, bar
+
+ echo foo,bar,baz | fzf --delimiter , --with-nth '{1},{3},{2},{1..2}'
+ # foo,baz,bar,foo,bar
+ ```
+- Added `exclude` and `exclude-multi` actions for dynamically excluding items
+ ```sh
+ seq 100 | fzf --bind 'ctrl-x:exclude'
+
+ # 'exclude-multi' will exclude the selected items or the current item
+ seq 100 | fzf --multi --bind 'ctrl-x:exclude-multi'
+ ```
+- Preview window now prints wrap indicator when wrapping is enabled
+ ```sh
+ seq 100 | xargs | fzf --wrap --preview 'echo {}' --preview-window wrap
+ ```
+- Bug fixes and improvements
+
0.59.0
------
_Release highlights: https://junegunn.github.io/fzf/releases/0.59.0/_
@@ -365,7 +399,7 @@ _Release highlights: https://junegunn.github.io/fzf/releases/0.54.0/_
- fzf will not start the initial reader when `reload` or `reload-sync` is bound to `start` event. `fzf < /dev/null` or `: | fzf` are no longer required and extraneous `load` event will not fire due to the empty list.
```sh
# Now this will work as expected. Previously, this would print an invalid header line.
- # `fzf < /dev/null` or `: | fzf` would fix the problem, but then an extraneous
+ # `fzf < /dev/null` or `: | fzf` would fix the problem, but then an extraneous
# `load` event would fire and the header would be prematurely updated.
fzf --header 'Loading ...' --header-lines 1 \
--bind 'start:reload:sleep 1; ps -ef' \
diff --git a/man/man1/fzf.1 b/man/man1/fzf.1
index 14ebb84e..1cea2d13 100644
--- a/man/man1/fzf.1
+++ b/man/man1/fzf.1
@@ -117,12 +117,33 @@ transformed lines (unlike in \fB\-\-preview\fR where fields are extracted from
the original lines) because fzf doesn't allow searching against the hidden
fields.
.TP
-.BI "\-\-with\-nth=" "N[,..]"
-Transform the presentation of each line using field index expressions
+.BI "\-\-with\-nth=" "N[,..] or TEMPLATE"
+Transform the presentation of each line using the field index expressions.
+For advanced transformation, you can provide a template containing field index
+expressions in curly braces.
+
+.RS
+e.g.
+ # Single expression: drop the first field
+ echo foo bar baz | fzf --with-nth 2..
+
+ # Use template to rearrange fields
+ echo foo,bar,baz | fzf --delimiter , --with-nth '{1},{3},{2},{1..2}'
+.RE
.TP
-.BI "\-\-accept\-nth=" "N[,..]"
+.BI "\-\-accept\-nth=" "N[,..] or TEMPLATE"
Define which fields to print on accept. The last delimiter is stripped from the
-output.
+output. For advanced transformation, you can provide a template containing
+field index expressions in curly braces.
+
+.RS
+e.g.
+ # Single expression
+ echo foo bar baz | fzf --accept-nth 2
+
+ # Template
+ echo foo bar baz | fzf --accept-nth '1st: {1}, 2nd: {2}, 3rd: {3}'
+.RE
.TP
.B "+s, \-\-no\-sort"
Do not sort the result
diff --git a/src/core.go b/src/core.go
index 08d9e868..939910b3 100644
--- a/src/core.go
+++ b/src/core.go
@@ -96,7 +96,7 @@ func Run(opts *Options) (int, error) {
var chunkList *ChunkList
var itemIndex int32
header := make([]string, 0, opts.HeaderLines)
- if len(opts.WithNth) == 0 {
+ if opts.WithNth == nil {
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
if len(header) < opts.HeaderLines {
header = append(header, byteString(data))
@@ -109,6 +109,7 @@ func Run(opts *Options) (int, error) {
return true
})
} else {
+ nthTransformer := opts.WithNth(opts.Delimiter)
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
tokens := Tokenize(byteString(data), opts.Delimiter)
if opts.Ansi && opts.Theme.Colored && len(tokens) > 1 {
@@ -127,15 +128,13 @@ func Run(opts *Options) (int, error) {
}
}
}
- trans := Transform(tokens, opts.WithNth)
- transformed := JoinTokens(trans)
+ transformed := nthTransformer(tokens)
if len(header) < opts.HeaderLines {
header = append(header, transformed)
eventBox.Set(EvtHeader, header)
return false
}
item.text, item.colors = ansiProcessor(stringBytes(transformed))
- item.text.TrimTrailingWhitespaces()
item.text.Index = itemIndex
item.origText = &data
itemIndex++
diff --git a/src/options.go b/src/options.go
index cad2936e..80079a4c 100644
--- a/src/options.go
+++ b/src/options.go
@@ -544,8 +544,8 @@ type Options struct {
Case Case
Normalize bool
Nth []Range
- WithNth []Range
- AcceptNth []Range
+ WithNth func(Delimiter) func([]Token) string
+ AcceptNth func(Delimiter) func([]Token) string
Delimiter Delimiter
Sort int
Track trackOption
@@ -667,8 +667,6 @@ func defaultOptions() *Options {
Case: CaseSmart,
Normalize: true,
Nth: make([]Range, 0),
- WithNth: make([]Range, 0),
- AcceptNth: make([]Range, 0),
Delimiter: Delimiter{},
Sort: 1000,
Track: trackDisabled,
@@ -771,6 +769,62 @@ func splitNth(str string) ([]Range, error) {
return ranges, nil
}
+func nthTransformer(str string) (func(Delimiter) func([]Token) string, error) {
+ // ^[0-9,-.]+$"
+ if match, _ := regexp.MatchString("^[0-9,-.]+$", str); match {
+ nth, err := splitNth(str)
+ if err != nil {
+ return nil, err
+ }
+ return func(Delimiter) func([]Token) string {
+ return func(tokens []Token) string {
+ return JoinTokens(Transform(tokens, nth))
+ }
+ }, nil
+ }
+
+ // {...} {...} ...
+ placeholder := regexp.MustCompile("{[0-9,-.]+}")
+ indexes := placeholder.FindAllStringIndex(str, -1)
+ if indexes == nil {
+ return nil, errors.New("template should include at least 1 placeholder: " + str)
+ }
+
+ type NthParts struct {
+ str string
+ nth []Range
+ }
+
+ parts := make([]NthParts, len(indexes))
+ idx := 0
+ for _, index := range indexes {
+ if idx < index[0] {
+ parts = append(parts, NthParts{str: str[idx:index[0]]})
+ }
+ if nth, err := splitNth(str[index[0]+1 : index[1]-1]); err == nil {
+ parts = append(parts, NthParts{nth: nth})
+ }
+ idx = index[1]
+ }
+ if idx < len(str) {
+ parts = append(parts, NthParts{str: str[idx:]})
+ }
+
+ return func(delimiter Delimiter) func([]Token) string {
+ return func(tokens []Token) string {
+ str := ""
+ for _, holder := range parts {
+ if holder.nth != nil {
+ str += StripLastDelimiter(JoinTokens(Transform(tokens, holder.nth)), delimiter)
+ } else {
+ str += holder.str
+ }
+ }
+ return str
+ }
+ }, nil
+}
+
func delimiterRegexp(str string) Delimiter {
// Special handling of \t
str = strings.ReplaceAll(str, "\\t", "\t")
@@ -2387,7 +2441,7 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
if err != nil {
return err
}
- if opts.WithNth, err = splitNth(str); err != nil {
+ if opts.WithNth, err = nthTransformer(str); err != nil {
return err
}
case "--accept-nth":
@@ -2395,7 +2449,7 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
if err != nil {
return err
}
- if opts.AcceptNth, err = splitNth(str); err != nil {
+ if opts.AcceptNth, err = nthTransformer(str); err != nil {
return err
}
case "-s", "--sort":
diff --git a/src/terminal.go b/src/terminal.go
index 273f2650..9a4abf86 100644
--- a/src/terminal.go
+++ b/src/terminal.go
@@ -305,7 +305,7 @@ type Terminal struct {
nthAttr tui.Attr
nth []Range
nthCurrent []Range
- acceptNth []Range
+ acceptNth func([]Token) string
tabstop int
margin [4]sizeSpec
padding [4]sizeSpec
@@ -919,7 +919,6 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
nthAttr: opts.Theme.Nth.Attr,
nth: opts.Nth,
nthCurrent: opts.Nth,
- acceptNth: opts.AcceptNth,
tabstop: opts.Tabstop,
hasStartActions: false,
hasResultActions: false,
@@ -961,6 +960,9 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
lastAction: actStart,
lastFocus: minItem.Index(),
numLinesCache: make(map[int32]numLinesCacheValue)}
+ if opts.AcceptNth != nil {
+ t.acceptNth = opts.AcceptNth(t.delimiter)
+ }
// This should be called before accessing tui.Color*
tui.InitTheme(opts.Theme, renderer.DefaultTheme(), opts.Black, opts.InputBorderShape.Visible(), opts.HeaderBorderShape.Visible())
@@ -1570,9 +1572,11 @@ func (t *Terminal) output() bool {
transform := func(item *Item) string {
return item.AsString(t.ansi)
}
- if len(t.acceptNth) > 0 {
+ if t.acceptNth != nil {
transform = func(item *Item) string {
- return JoinTokens(StripLastDelimiter(Transform(Tokenize(item.AsString(t.ansi), t.delimiter), t.acceptNth), t.delimiter))
+ tokens := Tokenize(item.AsString(t.ansi), t.delimiter)
+ transformed := t.acceptNth(tokens)
+ return StripLastDelimiter(transformed, t.delimiter)
}
}
found := len(t.selected) > 0
diff --git a/src/tokenizer.go b/src/tokenizer.go
index 057d7405..aaddd17d 100644
--- a/src/tokenizer.go
+++ b/src/tokenizer.go
@@ -6,6 +6,7 @@ import (
"regexp"
"strconv"
"strings"
+ "unicode"
"github.com/junegunn/fzf/src/util"
)
@@ -211,32 +212,18 @@ func Tokenize(text string, delimiter Delimiter) []Token {
return withPrefixLengths(tokens, 0)
}
-// StripLastDelimiter removes the trailing delimiter and whitespaces from the
-// last token.
-func StripLastDelimiter(tokens []Token, delimiter Delimiter) []Token {
- if len(tokens) == 0 {
- return tokens
- }
-
- lastToken := tokens[len(tokens)-1]
-
- if delimiter.str == nil && delimiter.regex == nil {
- lastToken.text.TrimTrailingWhitespaces()
- } else {
- if delimiter.str != nil {
- lastToken.text.TrimSuffix([]rune(*delimiter.str))
- } else if delimiter.regex != nil {
- str := lastToken.text.ToString()
- locs := delimiter.regex.FindAllStringIndex(str, -1)
- if len(locs) > 0 {
- lastLoc := locs[len(locs)-1]
- lastToken.text.SliceRight(lastLoc[0])
- }
+// StripLastDelimiter removes the trailing delimiter and whitespaces
+func StripLastDelimiter(str string, delimiter Delimiter) string {
+ if delimiter.str != nil {
+ str = strings.TrimSuffix(str, *delimiter.str)
+ } else if delimiter.regex != nil {
+ locs := delimiter.regex.FindAllStringIndex(str, -1)
+ if len(locs) > 0 {
+ lastLoc := locs[len(locs)-1]
+ str = str[:lastLoc[0]]
}
- lastToken.text.TrimTrailingWhitespaces()
}
-
- return tokens
+ return strings.TrimRightFunc(str, unicode.IsSpace)
}
// JoinTokens concatenates the tokens into a single string
diff --git a/src/util/chars.go b/src/util/chars.go
index dd037caa..adde02a6 100644
--- a/src/util/chars.go
+++ b/src/util/chars.go
@@ -184,11 +184,6 @@ func (chars *Chars) TrailingWhitespaces() int {
return whitespaces
}
-func (chars *Chars) TrimTrailingWhitespaces() {
- whitespaces := chars.TrailingWhitespaces()
- chars.slice = chars.slice[0 : len(chars.slice)-whitespaces]
-}
-
func (chars *Chars) TrimSuffix(runes []rune) {
lastIdx := len(chars.slice)
firstIdx := lastIdx - len(runes)
diff --git a/test/test_core.rb b/test/test_core.rb
index 0d7f68f5..0e60b57e 100644
--- a/test/test_core.rb
+++ b/test/test_core.rb
@@ -1772,4 +1772,13 @@ class TestCore < TestInteractive
assert_equal ['bar,bar,foo :,:bazfoo'], File.readlines(tempname, chomp: true)
end
end
+
+ def test_accept_nth_template
+ tmux.send_keys %(echo "foo ,bar,baz" | #{FZF} -d, --accept-nth '1st: {1}, 3rd: {3}, 2nd: {2}' --sync --bind start:accept > #{tempname}), :Enter
+ wait do
+ assert_path_exists tempname
+ # Last delimiter and the whitespaces are removed
+ assert_equal ['1st: foo, 3rd: baz, 2nd: bar'], File.readlines(tempname, chomp: true)
+ end
+ end
end
diff --git a/test/test_filter.rb b/test/test_filter.rb
index dc66ec00..718c6e57 100644
--- a/test/test_filter.rb
+++ b/test/test_filter.rb
@@ -59,6 +59,13 @@ class TestFilter < TestBase
`#{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 < #{tempname}`.chomp
end
+ def test_with_nth_template
+ writelines(['hello world ', 'byebye'])
+ assert_equal \
+ 'hello world ',
+ `#{FZF} -f"^he he.he." -x -n 2.. --with-nth '{2} {1}. {1}.' < #{tempname}`.chomp
+ end
+
def test_with_nth_ansi
writelines(["\x1b[33mhello \x1b[34;1mworld\x1b[m ", 'byebye'])
assert_equal \