mirror of
https://github.com/junegunn/fzf.git
synced 2025-05-19 12:50:22 -07:00
Make --accept-nth and --with-nth support templates
This commit is contained in:
parent
378137d34a
commit
84e2262ad6
36
CHANGELOG.md
36
CHANGELOG.md
@ -1,6 +1,40 @@
|
|||||||
CHANGELOG
|
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
|
0.59.0
|
||||||
------
|
------
|
||||||
_Release highlights: https://junegunn.github.io/fzf/releases/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.
|
- 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
|
```sh
|
||||||
# Now this will work as expected. Previously, this would print an invalid header line.
|
# 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.
|
# `load` event would fire and the header would be prematurely updated.
|
||||||
fzf --header 'Loading ...' --header-lines 1 \
|
fzf --header 'Loading ...' --header-lines 1 \
|
||||||
--bind 'start:reload:sleep 1; ps -ef' \
|
--bind 'start:reload:sleep 1; ps -ef' \
|
||||||
|
@ -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
|
the original lines) because fzf doesn't allow searching against the hidden
|
||||||
fields.
|
fields.
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-with\-nth=" "N[,..]"
|
.BI "\-\-with\-nth=" "N[,..] or TEMPLATE"
|
||||||
Transform the presentation of each line using field index expressions
|
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
|
.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
|
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
|
.TP
|
||||||
.B "+s, \-\-no\-sort"
|
.B "+s, \-\-no\-sort"
|
||||||
Do not sort the result
|
Do not sort the result
|
||||||
|
@ -96,7 +96,7 @@ func Run(opts *Options) (int, error) {
|
|||||||
var chunkList *ChunkList
|
var chunkList *ChunkList
|
||||||
var itemIndex int32
|
var itemIndex int32
|
||||||
header := make([]string, 0, opts.HeaderLines)
|
header := make([]string, 0, opts.HeaderLines)
|
||||||
if len(opts.WithNth) == 0 {
|
if opts.WithNth == nil {
|
||||||
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
|
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
|
||||||
if len(header) < opts.HeaderLines {
|
if len(header) < opts.HeaderLines {
|
||||||
header = append(header, byteString(data))
|
header = append(header, byteString(data))
|
||||||
@ -109,6 +109,7 @@ func Run(opts *Options) (int, error) {
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
nthTransformer := opts.WithNth(opts.Delimiter)
|
||||||
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
|
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
|
||||||
tokens := Tokenize(byteString(data), opts.Delimiter)
|
tokens := Tokenize(byteString(data), opts.Delimiter)
|
||||||
if opts.Ansi && opts.Theme.Colored && len(tokens) > 1 {
|
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 := nthTransformer(tokens)
|
||||||
transformed := JoinTokens(trans)
|
|
||||||
if len(header) < opts.HeaderLines {
|
if len(header) < opts.HeaderLines {
|
||||||
header = append(header, transformed)
|
header = append(header, transformed)
|
||||||
eventBox.Set(EvtHeader, header)
|
eventBox.Set(EvtHeader, header)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
item.text, item.colors = ansiProcessor(stringBytes(transformed))
|
item.text, item.colors = ansiProcessor(stringBytes(transformed))
|
||||||
item.text.TrimTrailingWhitespaces()
|
|
||||||
item.text.Index = itemIndex
|
item.text.Index = itemIndex
|
||||||
item.origText = &data
|
item.origText = &data
|
||||||
itemIndex++
|
itemIndex++
|
||||||
|
@ -544,8 +544,8 @@ type Options struct {
|
|||||||
Case Case
|
Case Case
|
||||||
Normalize bool
|
Normalize bool
|
||||||
Nth []Range
|
Nth []Range
|
||||||
WithNth []Range
|
WithNth func(Delimiter) func([]Token) string
|
||||||
AcceptNth []Range
|
AcceptNth func(Delimiter) func([]Token) string
|
||||||
Delimiter Delimiter
|
Delimiter Delimiter
|
||||||
Sort int
|
Sort int
|
||||||
Track trackOption
|
Track trackOption
|
||||||
@ -667,8 +667,6 @@ func defaultOptions() *Options {
|
|||||||
Case: CaseSmart,
|
Case: CaseSmart,
|
||||||
Normalize: true,
|
Normalize: true,
|
||||||
Nth: make([]Range, 0),
|
Nth: make([]Range, 0),
|
||||||
WithNth: make([]Range, 0),
|
|
||||||
AcceptNth: make([]Range, 0),
|
|
||||||
Delimiter: Delimiter{},
|
Delimiter: Delimiter{},
|
||||||
Sort: 1000,
|
Sort: 1000,
|
||||||
Track: trackDisabled,
|
Track: trackDisabled,
|
||||||
@ -771,6 +769,62 @@ func splitNth(str string) ([]Range, error) {
|
|||||||
return ranges, nil
|
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 {
|
func delimiterRegexp(str string) Delimiter {
|
||||||
// Special handling of \t
|
// Special handling of \t
|
||||||
str = strings.ReplaceAll(str, "\\t", "\t")
|
str = strings.ReplaceAll(str, "\\t", "\t")
|
||||||
@ -2387,7 +2441,7 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if opts.WithNth, err = splitNth(str); err != nil {
|
if opts.WithNth, err = nthTransformer(str); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case "--accept-nth":
|
case "--accept-nth":
|
||||||
@ -2395,7 +2449,7 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if opts.AcceptNth, err = splitNth(str); err != nil {
|
if opts.AcceptNth, err = nthTransformer(str); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case "-s", "--sort":
|
case "-s", "--sort":
|
||||||
|
@ -305,7 +305,7 @@ type Terminal struct {
|
|||||||
nthAttr tui.Attr
|
nthAttr tui.Attr
|
||||||
nth []Range
|
nth []Range
|
||||||
nthCurrent []Range
|
nthCurrent []Range
|
||||||
acceptNth []Range
|
acceptNth func([]Token) string
|
||||||
tabstop int
|
tabstop int
|
||||||
margin [4]sizeSpec
|
margin [4]sizeSpec
|
||||||
padding [4]sizeSpec
|
padding [4]sizeSpec
|
||||||
@ -919,7 +919,6 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
|
|||||||
nthAttr: opts.Theme.Nth.Attr,
|
nthAttr: opts.Theme.Nth.Attr,
|
||||||
nth: opts.Nth,
|
nth: opts.Nth,
|
||||||
nthCurrent: opts.Nth,
|
nthCurrent: opts.Nth,
|
||||||
acceptNth: opts.AcceptNth,
|
|
||||||
tabstop: opts.Tabstop,
|
tabstop: opts.Tabstop,
|
||||||
hasStartActions: false,
|
hasStartActions: false,
|
||||||
hasResultActions: false,
|
hasResultActions: false,
|
||||||
@ -961,6 +960,9 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
|
|||||||
lastAction: actStart,
|
lastAction: actStart,
|
||||||
lastFocus: minItem.Index(),
|
lastFocus: minItem.Index(),
|
||||||
numLinesCache: make(map[int32]numLinesCacheValue)}
|
numLinesCache: make(map[int32]numLinesCacheValue)}
|
||||||
|
if opts.AcceptNth != nil {
|
||||||
|
t.acceptNth = opts.AcceptNth(t.delimiter)
|
||||||
|
}
|
||||||
|
|
||||||
// This should be called before accessing tui.Color*
|
// This should be called before accessing tui.Color*
|
||||||
tui.InitTheme(opts.Theme, renderer.DefaultTheme(), opts.Black, opts.InputBorderShape.Visible(), opts.HeaderBorderShape.Visible())
|
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 {
|
transform := func(item *Item) string {
|
||||||
return item.AsString(t.ansi)
|
return item.AsString(t.ansi)
|
||||||
}
|
}
|
||||||
if len(t.acceptNth) > 0 {
|
if t.acceptNth != nil {
|
||||||
transform = func(item *Item) string {
|
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
|
found := len(t.selected) > 0
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
"github.com/junegunn/fzf/src/util"
|
"github.com/junegunn/fzf/src/util"
|
||||||
)
|
)
|
||||||
@ -211,32 +212,18 @@ func Tokenize(text string, delimiter Delimiter) []Token {
|
|||||||
return withPrefixLengths(tokens, 0)
|
return withPrefixLengths(tokens, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// StripLastDelimiter removes the trailing delimiter and whitespaces from the
|
// StripLastDelimiter removes the trailing delimiter and whitespaces
|
||||||
// last token.
|
func StripLastDelimiter(str string, delimiter Delimiter) string {
|
||||||
func StripLastDelimiter(tokens []Token, delimiter Delimiter) []Token {
|
if delimiter.str != nil {
|
||||||
if len(tokens) == 0 {
|
str = strings.TrimSuffix(str, *delimiter.str)
|
||||||
return tokens
|
} else if delimiter.regex != nil {
|
||||||
}
|
locs := delimiter.regex.FindAllStringIndex(str, -1)
|
||||||
|
if len(locs) > 0 {
|
||||||
lastToken := tokens[len(tokens)-1]
|
lastLoc := locs[len(locs)-1]
|
||||||
|
str = str[:lastLoc[0]]
|
||||||
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])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
lastToken.text.TrimTrailingWhitespaces()
|
|
||||||
}
|
}
|
||||||
|
return strings.TrimRightFunc(str, unicode.IsSpace)
|
||||||
return tokens
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// JoinTokens concatenates the tokens into a single string
|
// JoinTokens concatenates the tokens into a single string
|
||||||
|
@ -184,11 +184,6 @@ func (chars *Chars) TrailingWhitespaces() int {
|
|||||||
return whitespaces
|
return whitespaces
|
||||||
}
|
}
|
||||||
|
|
||||||
func (chars *Chars) TrimTrailingWhitespaces() {
|
|
||||||
whitespaces := chars.TrailingWhitespaces()
|
|
||||||
chars.slice = chars.slice[0 : len(chars.slice)-whitespaces]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (chars *Chars) TrimSuffix(runes []rune) {
|
func (chars *Chars) TrimSuffix(runes []rune) {
|
||||||
lastIdx := len(chars.slice)
|
lastIdx := len(chars.slice)
|
||||||
firstIdx := lastIdx - len(runes)
|
firstIdx := lastIdx - len(runes)
|
||||||
|
@ -1772,4 +1772,13 @@ class TestCore < TestInteractive
|
|||||||
assert_equal ['bar,bar,foo :,:bazfoo'], File.readlines(tempname, chomp: true)
|
assert_equal ['bar,bar,foo :,:bazfoo'], File.readlines(tempname, chomp: true)
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
@ -59,6 +59,13 @@ class TestFilter < TestBase
|
|||||||
`#{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 < #{tempname}`.chomp
|
`#{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 < #{tempname}`.chomp
|
||||||
end
|
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
|
def test_with_nth_ansi
|
||||||
writelines(["\x1b[33mhello \x1b[34;1mworld\x1b[m ", 'byebye'])
|
writelines(["\x1b[33mhello \x1b[34;1mworld\x1b[m ", 'byebye'])
|
||||||
assert_equal \
|
assert_equal \
|
||||||
|
Loading…
x
Reference in New Issue
Block a user