Skip to content

Commit 84e2262

Browse files
committed
Make --accept-nth and --with-nth support templates
1 parent 378137d commit 84e2262

File tree

9 files changed

+158
-48
lines changed

9 files changed

+158
-48
lines changed

CHANGELOG.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,40 @@
11
CHANGELOG
22
=========
33

4+
0.60.0
5+
------
6+
7+
- Added `--accept-nth` for choosing output fields
8+
```sh
9+
ps -ef | fzf --multi --header-lines 1 | awk '{print $2}'
10+
# Becomes
11+
ps -ef | fzf --multi --header-lines 1 --accept-nth 2
12+
13+
git branch | fzf | cut -c3-
14+
# Can be rewritten as
15+
git branch | fzf --accept-nth -1
16+
```
17+
- `--accept-nth` and `--with-nth` now support a template that includes multiple field index expressions in curly braces
18+
```sh
19+
echo foo,bar,baz | fzf --delimiter , --accept-nth '{1}, {3}, {2}'
20+
# foo, baz, bar
21+
22+
echo foo,bar,baz | fzf --delimiter , --with-nth '{1},{3},{2},{1..2}'
23+
# foo,baz,bar,foo,bar
24+
```
25+
- Added `exclude` and `exclude-multi` actions for dynamically excluding items
26+
```sh
27+
seq 100 | fzf --bind 'ctrl-x:exclude'
28+
29+
# 'exclude-multi' will exclude the selected items or the current item
30+
seq 100 | fzf --multi --bind 'ctrl-x:exclude-multi'
31+
```
32+
- Preview window now prints wrap indicator when wrapping is enabled
33+
```sh
34+
seq 100 | xargs | fzf --wrap --preview 'echo {}' --preview-window wrap
35+
```
36+
- Bug fixes and improvements
37+
438
0.59.0
539
------
640
_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/_
365399
- 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.
366400
```sh
367401
# Now this will work as expected. Previously, this would print an invalid header line.
368-
# `fzf < /dev/null` or `: | fzf` would fix the problem, but then an extraneous
402+
# `fzf < /dev/null` or `: | fzf` would fix the problem, but then an extraneous
369403
# `load` event would fire and the header would be prematurely updated.
370404
fzf --header 'Loading ...' --header-lines 1 \
371405
--bind 'start:reload:sleep 1; ps -ef' \

man/man1/fzf.1

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,12 +117,33 @@ transformed lines (unlike in \fB\-\-preview\fR where fields are extracted from
117117
the original lines) because fzf doesn't allow searching against the hidden
118118
fields.
119119
.TP
120-
.BI "\-\-with\-nth=" "N[,..]"
121-
Transform the presentation of each line using field index expressions
120+
.BI "\-\-with\-nth=" "N[,..] or TEMPLATE"
121+
Transform the presentation of each line using the field index expressions.
122+
For advanced transformation, you can provide a template containing field index
123+
expressions in curly braces.
124+
125+
.RS
126+
e.g.
127+
# Single expression: drop the first field
128+
echo foo bar baz | fzf --with-nth 2..
129+
130+
# Use template to rearrange fields
131+
echo foo,bar,baz | fzf --delimiter , --with-nth '{1},{3},{2},{1..2}'
132+
.RE
122133
.TP
123-
.BI "\-\-accept\-nth=" "N[,..]"
134+
.BI "\-\-accept\-nth=" "N[,..] or TEMPLATE"
124135
Define which fields to print on accept. The last delimiter is stripped from the
125-
output.
136+
output. For advanced transformation, you can provide a template containing
137+
field index expressions in curly braces.
138+
139+
.RS
140+
e.g.
141+
# Single expression
142+
echo foo bar baz | fzf --accept-nth 2
143+
144+
# Template
145+
echo foo bar baz | fzf --accept-nth '1st: {1}, 2nd: {2}, 3rd: {3}'
146+
.RE
126147
.TP
127148
.B "+s, \-\-no\-sort"
128149
Do not sort the result

src/core.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ func Run(opts *Options) (int, error) {
9696
var chunkList *ChunkList
9797
var itemIndex int32
9898
header := make([]string, 0, opts.HeaderLines)
99-
if len(opts.WithNth) == 0 {
99+
if opts.WithNth == nil {
100100
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
101101
if len(header) < opts.HeaderLines {
102102
header = append(header, byteString(data))
@@ -109,6 +109,7 @@ func Run(opts *Options) (int, error) {
109109
return true
110110
})
111111
} else {
112+
nthTransformer := opts.WithNth(opts.Delimiter)
112113
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
113114
tokens := Tokenize(byteString(data), opts.Delimiter)
114115
if opts.Ansi && opts.Theme.Colored && len(tokens) > 1 {
@@ -127,15 +128,13 @@ func Run(opts *Options) (int, error) {
127128
}
128129
}
129130
}
130-
trans := Transform(tokens, opts.WithNth)
131-
transformed := JoinTokens(trans)
131+
transformed := nthTransformer(tokens)
132132
if len(header) < opts.HeaderLines {
133133
header = append(header, transformed)
134134
eventBox.Set(EvtHeader, header)
135135
return false
136136
}
137137
item.text, item.colors = ansiProcessor(stringBytes(transformed))
138-
item.text.TrimTrailingWhitespaces()
139138
item.text.Index = itemIndex
140139
item.origText = &data
141140
itemIndex++

src/options.go

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -544,8 +544,8 @@ type Options struct {
544544
Case Case
545545
Normalize bool
546546
Nth []Range
547-
WithNth []Range
548-
AcceptNth []Range
547+
WithNth func(Delimiter) func([]Token) string
548+
AcceptNth func(Delimiter) func([]Token) string
549549
Delimiter Delimiter
550550
Sort int
551551
Track trackOption
@@ -667,8 +667,6 @@ func defaultOptions() *Options {
667667
Case: CaseSmart,
668668
Normalize: true,
669669
Nth: make([]Range, 0),
670-
WithNth: make([]Range, 0),
671-
AcceptNth: make([]Range, 0),
672670
Delimiter: Delimiter{},
673671
Sort: 1000,
674672
Track: trackDisabled,
@@ -771,6 +769,62 @@ func splitNth(str string) ([]Range, error) {
771769
return ranges, nil
772770
}
773771

772+
func nthTransformer(str string) (func(Delimiter) func([]Token) string, error) {
773+
// ^[0-9,-.]+$"
774+
if match, _ := regexp.MatchString("^[0-9,-.]+$", str); match {
775+
nth, err := splitNth(str)
776+
if err != nil {
777+
return nil, err
778+
}
779+
return func(Delimiter) func([]Token) string {
780+
return func(tokens []Token) string {
781+
return JoinTokens(Transform(tokens, nth))
782+
}
783+
}, nil
784+
}
785+
786+
// {...} {...} ...
787+
placeholder := regexp.MustCompile("{[0-9,-.]+}")
788+
indexes := placeholder.FindAllStringIndex(str, -1)
789+
if indexes == nil {
790+
return nil, errors.New("template should include at least 1 placeholder: " + str)
791+
}
792+
793+
type NthParts struct {
794+
str string
795+
nth []Range
796+
}
797+
798+
parts := make([]NthParts, len(indexes))
799+
idx := 0
800+
for _, index := range indexes {
801+
if idx < index[0] {
802+
parts = append(parts, NthParts{str: str[idx:index[0]]})
803+
}
804+
if nth, err := splitNth(str[index[0]+1 : index[1]-1]); err == nil {
805+
parts = append(parts, NthParts{nth: nth})
806+
}
807+
idx = index[1]
808+
}
809+
if idx < len(str) {
810+
parts = append(parts, NthParts{str: str[idx:]})
811+
}
812+
813+
return func(delimiter Delimiter) func([]Token) string {
814+
return func(tokens []Token) string {
815+
str := ""
816+
for _, holder := range parts {
817+
if holder.nth != nil {
818+
str += StripLastDelimiter(JoinTokens(Transform(tokens, holder.nth)), delimiter)
819+
} else {
820+
str += holder.str
821+
}
822+
}
823+
return str
824+
}
825+
}, nil
826+
}
827+
774828
func delimiterRegexp(str string) Delimiter {
775829
// Special handling of \t
776830
str = strings.ReplaceAll(str, "\\t", "\t")
@@ -2387,15 +2441,15 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
23872441
if err != nil {
23882442
return err
23892443
}
2390-
if opts.WithNth, err = splitNth(str); err != nil {
2444+
if opts.WithNth, err = nthTransformer(str); err != nil {
23912445
return err
23922446
}
23932447
case "--accept-nth":
23942448
str, err := nextString("nth expression required")
23952449
if err != nil {
23962450
return err
23972451
}
2398-
if opts.AcceptNth, err = splitNth(str); err != nil {
2452+
if opts.AcceptNth, err = nthTransformer(str); err != nil {
23992453
return err
24002454
}
24012455
case "-s", "--sort":

src/terminal.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ type Terminal struct {
305305
nthAttr tui.Attr
306306
nth []Range
307307
nthCurrent []Range
308-
acceptNth []Range
308+
acceptNth func([]Token) string
309309
tabstop int
310310
margin [4]sizeSpec
311311
padding [4]sizeSpec
@@ -919,7 +919,6 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
919919
nthAttr: opts.Theme.Nth.Attr,
920920
nth: opts.Nth,
921921
nthCurrent: opts.Nth,
922-
acceptNth: opts.AcceptNth,
923922
tabstop: opts.Tabstop,
924923
hasStartActions: false,
925924
hasResultActions: false,
@@ -961,6 +960,9 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
961960
lastAction: actStart,
962961
lastFocus: minItem.Index(),
963962
numLinesCache: make(map[int32]numLinesCacheValue)}
963+
if opts.AcceptNth != nil {
964+
t.acceptNth = opts.AcceptNth(t.delimiter)
965+
}
964966

965967
// This should be called before accessing tui.Color*
966968
tui.InitTheme(opts.Theme, renderer.DefaultTheme(), opts.Black, opts.InputBorderShape.Visible(), opts.HeaderBorderShape.Visible())
@@ -1570,9 +1572,11 @@ func (t *Terminal) output() bool {
15701572
transform := func(item *Item) string {
15711573
return item.AsString(t.ansi)
15721574
}
1573-
if len(t.acceptNth) > 0 {
1575+
if t.acceptNth != nil {
15741576
transform = func(item *Item) string {
1575-
return JoinTokens(StripLastDelimiter(Transform(Tokenize(item.AsString(t.ansi), t.delimiter), t.acceptNth), t.delimiter))
1577+
tokens := Tokenize(item.AsString(t.ansi), t.delimiter)
1578+
transformed := t.acceptNth(tokens)
1579+
return StripLastDelimiter(transformed, t.delimiter)
15761580
}
15771581
}
15781582
found := len(t.selected) > 0

src/tokenizer.go

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"regexp"
77
"strconv"
88
"strings"
9+
"unicode"
910

1011
"github.com/junegunn/fzf/src/util"
1112
)
@@ -211,32 +212,18 @@ func Tokenize(text string, delimiter Delimiter) []Token {
211212
return withPrefixLengths(tokens, 0)
212213
}
213214

214-
// StripLastDelimiter removes the trailing delimiter and whitespaces from the
215-
// last token.
216-
func StripLastDelimiter(tokens []Token, delimiter Delimiter) []Token {
217-
if len(tokens) == 0 {
218-
return tokens
219-
}
220-
221-
lastToken := tokens[len(tokens)-1]
222-
223-
if delimiter.str == nil && delimiter.regex == nil {
224-
lastToken.text.TrimTrailingWhitespaces()
225-
} else {
226-
if delimiter.str != nil {
227-
lastToken.text.TrimSuffix([]rune(*delimiter.str))
228-
} else if delimiter.regex != nil {
229-
str := lastToken.text.ToString()
230-
locs := delimiter.regex.FindAllStringIndex(str, -1)
231-
if len(locs) > 0 {
232-
lastLoc := locs[len(locs)-1]
233-
lastToken.text.SliceRight(lastLoc[0])
234-
}
215+
// StripLastDelimiter removes the trailing delimiter and whitespaces
216+
func StripLastDelimiter(str string, delimiter Delimiter) string {
217+
if delimiter.str != nil {
218+
str = strings.TrimSuffix(str, *delimiter.str)
219+
} else if delimiter.regex != nil {
220+
locs := delimiter.regex.FindAllStringIndex(str, -1)
221+
if len(locs) > 0 {
222+
lastLoc := locs[len(locs)-1]
223+
str = str[:lastLoc[0]]
235224
}
236-
lastToken.text.TrimTrailingWhitespaces()
237225
}
238-
239-
return tokens
226+
return strings.TrimRightFunc(str, unicode.IsSpace)
240227
}
241228

242229
// JoinTokens concatenates the tokens into a single string

src/util/chars.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -184,11 +184,6 @@ func (chars *Chars) TrailingWhitespaces() int {
184184
return whitespaces
185185
}
186186

187-
func (chars *Chars) TrimTrailingWhitespaces() {
188-
whitespaces := chars.TrailingWhitespaces()
189-
chars.slice = chars.slice[0 : len(chars.slice)-whitespaces]
190-
}
191-
192187
func (chars *Chars) TrimSuffix(runes []rune) {
193188
lastIdx := len(chars.slice)
194189
firstIdx := lastIdx - len(runes)

test/test_core.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1772,4 +1772,13 @@ def test_accept_nth_regex_delimiter
17721772
assert_equal ['bar,bar,foo :,:bazfoo'], File.readlines(tempname, chomp: true)
17731773
end
17741774
end
1775+
1776+
def test_accept_nth_template
1777+
tmux.send_keys %(echo "foo ,bar,baz" | #{FZF} -d, --accept-nth '1st: {1}, 3rd: {3}, 2nd: {2}' --sync --bind start:accept > #{tempname}), :Enter
1778+
wait do
1779+
assert_path_exists tempname
1780+
# Last delimiter and the whitespaces are removed
1781+
assert_equal ['1st: foo, 3rd: baz, 2nd: bar'], File.readlines(tempname, chomp: true)
1782+
end
1783+
end
17751784
end

test/test_filter.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,13 @@ def test_with_nth_basic
5959
`#{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 < #{tempname}`.chomp
6060
end
6161

62+
def test_with_nth_template
63+
writelines(['hello world ', 'byebye'])
64+
assert_equal \
65+
'hello world ',
66+
`#{FZF} -f"^he he.he." -x -n 2.. --with-nth '{2} {1}. {1}.' < #{tempname}`.chomp
67+
end
68+
6269
def test_with_nth_ansi
6370
writelines(["\x1b[33mhello \x1b[34;1mworld\x1b[m ", 'byebye'])
6471
assert_equal \

0 commit comments

Comments
 (0)