From 1d2d32c847e39818bedae5f86ca75e6b70b60444 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 13 Jan 2016 03:07:42 +0900 Subject: [PATCH] Accept comma-separated list of sort criteria --- CHANGELOG.md | 9 +++ man/man1/fzf.1 | 15 ++++- src/chunklist_test.go | 12 +++- src/core.go | 22 +++++-- src/item.go | 110 ++++++++++++++++++-------------- src/item_test.go | 29 +++++---- src/merger.go | 2 +- src/merger_test.go | 2 +- src/options.go | 65 +++++++++++++------ src/pattern.go | 2 +- src/terminal.go | 6 +- test/test_go.rb | 144 +++++++++++++++++++++++++++++++++++------- 12 files changed, 298 insertions(+), 120 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1f4b70a..87f78273 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ CHANGELOG ========= +0.11.2 +------ + +- `--tiebreak` now accepts comma-separated list of sort criteria. + - Each criterion should appear only once in the list + - `index` is only allowed at the end of the list + - `index` is implicitly appended to the list when not specified + - Default is `length` (or equivalently `length,index`) + 0.11.1 ------ diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 416482a4..feca9728 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. .. -.TH fzf 1 "Dec 2015" "fzf 0.11.1" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Jan 2016" "fzf 0.11.2" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder @@ -68,8 +68,8 @@ Reverse the order of the input e.g. \fBhistory | fzf --tac --no-sort\fR .RE .TP -.BI "--tiebreak=" "CRI" -Sort criterion to use when the scores are tied +.BI "--tiebreak=" "CRI[,..]" +Comma-separated list of sort criteria to apply when the scores are tied. .br .R "" .br @@ -81,6 +81,15 @@ Sort criterion to use when the scores are tied .br .BR index " Prefers item that appeared earlier in the input stream" .br +.R "" +.br +- Each criterion should appear only once in the list +.br +- \fBindex\fR is only allowed at the end of the list +.br +- \fBindex\fR is implicitly appended to the list when not specified +.br +- Default is \fBlength\fR (or equivalently \fBlength\fR,index) .SS Interface .TP .B "-m, --multi" diff --git a/src/chunklist_test.go b/src/chunklist_test.go index 26795ef2..6ddd336f 100644 --- a/src/chunklist_test.go +++ b/src/chunklist_test.go @@ -6,8 +6,11 @@ import ( ) func TestChunkList(t *testing.T) { + // FIXME global + sortCriteria = []criterion{byMatchLen, byLength, byIndex} + cl := NewChunkList(func(s []byte, i int) *Item { - return &Item{text: []rune(string(s)), rank: Rank{0, 0, uint32(i * 2)}} + return &Item{text: []rune(string(s)), rank: buildEmptyRank(int32(i * 2))} }) // Snapshot @@ -36,8 +39,11 @@ func TestChunkList(t *testing.T) { if len(*chunk1) != 2 { t.Error("Snapshot should contain only two items") } - if string((*chunk1)[0].text) != "hello" || (*chunk1)[0].rank.index != 0 || - string((*chunk1)[1].text) != "world" || (*chunk1)[1].rank.index != 2 { + last := func(arr []int32) int32 { + return arr[len(arr)-1] + } + if string((*chunk1)[0].text) != "hello" || last((*chunk1)[0].rank) != 0 || + string((*chunk1)[1].text) != "world" || last((*chunk1)[1].rank) != 2 { t.Error("Invalid data") } if chunk1.IsFull() { diff --git a/src/core.go b/src/core.go index dcba7ecf..1906c508 100644 --- a/src/core.go +++ b/src/core.go @@ -52,7 +52,7 @@ func Run(opts *Options) { initProcs() sort := opts.Sort > 0 - rankTiebreak = opts.Tiebreak + sortCriteria = opts.Criteria if opts.Version { fmt.Println(version) @@ -103,9 +103,9 @@ func Run(opts *Options) { runes, colors := ansiProcessor(data) return &Item{ text: runes, - index: uint32(index), + index: int32(index), colors: colors, - rank: Rank{0, 0, uint32(index)}} + rank: buildEmptyRank(int32(index))} }) } else { chunkList = NewChunkList(func(data []byte, index int) *Item { @@ -120,9 +120,9 @@ func Run(opts *Options) { item := Item{ text: joinTokens(trans), origText: &runes, - index: uint32(index), + index: int32(index), colors: nil, - rank: Rank{0, 0, uint32(index)}} + rank: buildEmptyRank(int32(index))} trimmed, colors := ansiProcessorRunes(item.text) item.text = trimmed @@ -141,9 +141,19 @@ func Run(opts *Options) { } // Matcher + forward := true + for _, cri := range opts.Criteria[1:] { + if cri == byEnd { + forward = false + break + } + if cri == byBegin { + break + } + } patternBuilder := func(runes []rune) *Pattern { return BuildPattern( - opts.Fuzzy, opts.Extended, opts.Case, opts.Tiebreak != byEnd, + opts.Fuzzy, opts.Extended, opts.Case, forward, opts.Nth, opts.Delimiter, runes) } matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox) diff --git a/src/item.go b/src/item.go index 5ce25c71..a4fa609b 100644 --- a/src/item.go +++ b/src/item.go @@ -20,25 +20,35 @@ type Item struct { text []rune origText *[]rune transformed []Token - index uint32 + index int32 offsets []Offset colors []ansiOffset - rank Rank + rank []int32 } -// Rank is used to sort the search result -type Rank struct { - matchlen uint16 - tiebreak uint16 - index uint32 +// Sort criteria to use. Never changes once fzf is started. +var sortCriteria []criterion + +func isRankValid(rank []int32) bool { + // Exclude ordinal index + for i := 0; i < len(rank)-1; i++ { + if rank[i] > 0 { + return true + } + } + return false } -// Tiebreak criterion to use. Never changes once fzf is started. -var rankTiebreak tiebreak +func buildEmptyRank(index int32) []int32 { + len := len(sortCriteria) + arr := make([]int32, len) + arr[len-1] = index + return arr +} // Rank calculates rank of the Item -func (item *Item) Rank(cache bool) Rank { - if cache && (item.rank.matchlen > 0 || item.rank.tiebreak > 0) { +func (item *Item) Rank(cache bool) []int32 { + if cache && isRankValid(item.rank) { return item.rank } matchlen := 0 @@ -64,32 +74,37 @@ func (item *Item) Rank(cache bool) Rank { } } if matchlen == 0 { - matchlen = math.MaxUint16 + matchlen = math.MaxInt32 } - var tiebreak uint16 - switch rankTiebreak { - case byLength: - // It is guaranteed that .transformed in not null in normal execution - if item.transformed != nil { - // If offsets is empty, lenSum will be 0, but we don't care - tiebreak = uint16(lenSum) - } else { - tiebreak = uint16(len(item.text)) - } - case byBegin: - // We can't just look at item.offsets[0][0] because it can be an inverse term - tiebreak = uint16(minBegin) - case byEnd: - if prevEnd > 0 { - tiebreak = uint16(1 + len(item.text) - prevEnd) - } else { - // Empty offsets due to inverse terms. - tiebreak = 1 + rank := make([]int32, len(sortCriteria)) + for idx, criterion := range sortCriteria { + var val int32 + switch criterion { + case byMatchLen: + val = int32(matchlen) + case byLength: + // It is guaranteed that .transformed in not null in normal execution + if item.transformed != nil { + // If offsets is empty, lenSum will be 0, but we don't care + val = int32(lenSum) + } else { + val = int32(len(item.text)) + } + case byBegin: + // We can't just look at item.offsets[0][0] because it can be an inverse term + val = int32(minBegin) + case byEnd: + if prevEnd > 0 { + val = int32(1 + len(item.text) - prevEnd) + } else { + // Empty offsets due to inverse terms. + val = 1 + } + case byIndex: + val = item.index } - case byIndex: - tiebreak = 1 + rank[idx] = val } - rank := Rank{uint16(matchlen), tiebreak, item.index} if cache { item.rank = rank } @@ -254,18 +269,19 @@ func (a ByRelevanceTac) Less(i, j int) bool { return compareRanks(irank, jrank, true) } -func compareRanks(irank Rank, jrank Rank, tac bool) bool { - if irank.matchlen < jrank.matchlen { - return true - } else if irank.matchlen > jrank.matchlen { - return false - } - - if irank.tiebreak < jrank.tiebreak { - return true - } else if irank.tiebreak > jrank.tiebreak { - return false +func compareRanks(irank []int32, jrank []int32, tac bool) bool { + lastIdx := len(irank) - 1 + for idx, left := range irank { + right := jrank[idx] + if tac && idx == lastIdx { + left = left * -1 + right = right * -1 + } + if left < right { + return true + } else if left > right { + return false + } } - - return (irank.index <= jrank.index) != tac + return true } diff --git a/src/item_test.go b/src/item_test.go index 50d6851e..f26f8370 100644 --- a/src/item_test.go +++ b/src/item_test.go @@ -23,27 +23,30 @@ func TestOffsetSort(t *testing.T) { } func TestRankComparison(t *testing.T) { - if compareRanks(Rank{3, 0, 5}, Rank{2, 0, 7}, false) || - !compareRanks(Rank{3, 0, 5}, Rank{3, 0, 6}, false) || - !compareRanks(Rank{1, 2, 3}, Rank{1, 3, 2}, false) || - !compareRanks(Rank{0, 0, 0}, Rank{0, 0, 0}, false) { + if compareRanks([]int32{3, 0, 5}, []int32{2, 0, 7}, false) || + !compareRanks([]int32{3, 0, 5}, []int32{3, 0, 6}, false) || + !compareRanks([]int32{1, 2, 3}, []int32{1, 3, 2}, false) || + !compareRanks([]int32{0, 0, 0}, []int32{0, 0, 0}, false) { t.Error("Invalid order") } - if compareRanks(Rank{3, 0, 5}, Rank{2, 0, 7}, true) || - !compareRanks(Rank{3, 0, 5}, Rank{3, 0, 6}, false) || - !compareRanks(Rank{1, 2, 3}, Rank{1, 3, 2}, true) || - !compareRanks(Rank{0, 0, 0}, Rank{0, 0, 0}, false) { + if compareRanks([]int32{3, 0, 5}, []int32{2, 0, 7}, true) || + !compareRanks([]int32{3, 0, 5}, []int32{3, 0, 6}, false) || + !compareRanks([]int32{1, 2, 3}, []int32{1, 3, 2}, true) || + !compareRanks([]int32{0, 0, 0}, []int32{0, 0, 0}, false) { t.Error("Invalid order (tac)") } } // Match length, string length, index func TestItemRank(t *testing.T) { + // FIXME global + sortCriteria = []criterion{byMatchLen, byLength, byIndex} + strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")} item1 := Item{text: strs[0], index: 1, offsets: []Offset{}} rank1 := item1.Rank(true) - if rank1.matchlen != math.MaxUint16 || rank1.tiebreak != 3 || rank1.index != 1 { + if rank1[0] != math.MaxInt32 || rank1[1] != 3 || rank1[2] != 1 { t.Error(item1.Rank(true)) } // Only differ in index @@ -63,10 +66,10 @@ func TestItemRank(t *testing.T) { } // Sort by relevance - item3 := Item{text: strs[1], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} - item4 := Item{text: strs[1], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} - item5 := Item{text: strs[2], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} - item6 := Item{text: strs[2], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} + item3 := Item{text: strs[1], rank: []int32{0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} + item4 := Item{text: strs[1], rank: []int32{0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} + item5 := Item{text: strs[2], rank: []int32{0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} + item6 := Item{text: strs[2], rank: []int32{0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} items = []*Item{&item1, &item2, &item3, &item4, &item5, &item6} sort.Sort(ByRelevance(items)) if items[0] != &item6 || items[1] != &item4 || diff --git a/src/merger.go b/src/merger.go index cce8a947..26ed17b5 100644 --- a/src/merger.go +++ b/src/merger.go @@ -88,7 +88,7 @@ func (mg *Merger) cacheable() bool { func (mg *Merger) mergedGet(idx int) *Item { for i := len(mg.merged); i <= idx; i++ { - minRank := Rank{0, 0, 0} + minRank := buildEmptyRank(0) minIdx := -1 for listIdx, list := range mg.lists { cursor := mg.cursors[listIdx] diff --git a/src/merger_test.go b/src/merger_test.go index b7a2993a..34efc84d 100644 --- a/src/merger_test.go +++ b/src/merger_test.go @@ -23,7 +23,7 @@ func randItem() *Item { } return &Item{ text: []rune(str), - index: rand.Uint32(), + index: rand.Int31(), offsets: offsets} } diff --git a/src/options.go b/src/options.go index ad05213c..30e00160 100644 --- a/src/options.go +++ b/src/options.go @@ -27,7 +27,8 @@ const usage = `usage: fzf [options] -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style) +s, --no-sort Do not sort the result --tac Reverse the order of the input - --tiebreak=CRITERION Sort criterion when the scores are tied; + --tiebreak=CRI[,..] Comma-separated list of sort criteria to apply + when the scores are tied; [length|begin|end|index] (default: length) Interface @@ -75,10 +76,11 @@ const ( ) // Sort criteria -type tiebreak int +type criterion int const ( - byLength tiebreak = iota + byMatchLen criterion = iota + byLength byBegin byEnd byIndex @@ -98,7 +100,7 @@ type Options struct { Delimiter Delimiter Sort int Tac bool - Tiebreak tiebreak + Criteria []criterion Multi bool Ansi bool Mouse bool @@ -145,7 +147,7 @@ func defaultOptions() *Options { Delimiter: Delimiter{}, Sort: 1000, Tac: false, - Tiebreak: byLength, + Criteria: []criterion{byMatchLen, byLength, byIndex}, Multi: false, Ansi: false, Mouse: true, @@ -361,20 +363,43 @@ func parseKeyChords(str string, message string) map[int]string { return chords } -func parseTiebreak(str string) tiebreak { - switch strings.ToLower(str) { - case "length": - return byLength - case "index": - return byIndex - case "begin": - return byBegin - case "end": - return byEnd - default: - errorExit("invalid sort criterion: " + str) +func parseTiebreak(str string) []criterion { + criteria := []criterion{byMatchLen} + hasIndex := false + hasLength := false + hasBegin := false + hasEnd := false + check := func(notExpected *bool, name string) { + if *notExpected { + errorExit("duplicate sort criteria: " + name) + } + if hasIndex { + errorExit("index should be the last criterion") + } + *notExpected = true + } + for _, str := range strings.Split(strings.ToLower(str), ",") { + switch str { + case "index": + check(&hasIndex, "index") + criteria = append(criteria, byIndex) + case "length": + check(&hasLength, "length") + criteria = append(criteria, byLength) + case "begin": + check(&hasBegin, "begin") + criteria = append(criteria, byBegin) + case "end": + check(&hasEnd, "end") + criteria = append(criteria, byEnd) + default: + errorExit("invalid sort criterion: " + str) + } + } + if !hasIndex { + criteria = append(criteria, byIndex) } - return byLength + return criteria } func dupeTheme(theme *curses.ColorTheme) *curses.ColorTheme { @@ -715,7 +740,7 @@ func parseOptions(opts *Options, allArgs []string) { case "--expect": opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required") case "--tiebreak": - opts.Tiebreak = parseTiebreak(nextString(allArgs, &i, "sort criterion required")) + opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required")) case "--bind": keymap, opts.Execmap, opts.ToggleSort = parseKeymap(keymap, opts.Execmap, opts.ToggleSort, nextString(allArgs, &i, "bind expression required")) @@ -850,7 +875,7 @@ func parseOptions(opts *Options, allArgs []string) { } else if match, value := optString(arg, "--expect="); match { opts.Expect = parseKeyChords(value, "key names required") } else if match, value := optString(arg, "--tiebreak="); match { - opts.Tiebreak = parseTiebreak(value) + opts.Criteria = parseTiebreak(value) } else if match, value := optString(arg, "--color="); match { opts.Theme = parseTheme(opts.Theme, value) } else if match, value := optString(arg, "--bind="); match { diff --git a/src/pattern.go b/src/pattern.go index 2abcf439..4c61b87d 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -309,7 +309,7 @@ func dupItem(item *Item, offsets []Offset) *Item { index: item.index, offsets: offsets, colors: item.colors, - rank: Rank{0, 0, item.index}} + rank: buildEmptyRank(item.index)} } func (p *Pattern) basicMatch(item *Item) (int, int, int) { diff --git a/src/terminal.go b/src/terminal.go index a19f41d8..c9b80565 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -50,7 +50,7 @@ type Terminal struct { progress int reading bool merger *Merger - selected map[uint32]selectedItem + selected map[int32]selectedItem reqBox *util.EventBox eventBox *util.EventBox mutex sync.Mutex @@ -223,7 +223,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { ansi: opts.Ansi, reading: true, merger: EmptyMerger, - selected: make(map[uint32]selectedItem), + selected: make(map[int32]selectedItem), reqBox: util.NewEventBox(), eventBox: eventBox, mutex: sync.Mutex{}, @@ -466,7 +466,7 @@ func (t *Terminal) printHeader() { text: []rune(trimmed), index: 0, colors: colors, - rank: Rank{0, 0, 0}} + rank: buildEmptyRank(0)} t.move(line, 2, true) t.printHighlighted(item, false, C.ColHeader, 0, false) diff --git a/test/test_go.rb b/test/test_go.rb index fcf43401..85e3d838 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -459,8 +459,8 @@ class TestGoFZF < TestBase def test_unicode_case writelines tempname, %w[строКА1 СТРОКА2 строка3 Строка4] - assert_equal %w[СТРОКА2 Строка4], `cat #{tempname} | #{FZF} -fС`.split($/) - assert_equal %w[строКА1 СТРОКА2 строка3 Строка4], `cat #{tempname} | #{FZF} -fс`.split($/) + assert_equal %w[СТРОКА2 Строка4], `#{FZF} -fС < #{tempname}`.split($/) + assert_equal %w[строКА1 СТРОКА2 строка3 Строка4], `#{FZF} -fс < #{tempname}`.split($/) end def test_tiebreak @@ -472,7 +472,7 @@ class TestGoFZF < TestBase ] writelines tempname, input - assert_equal input, `cat #{tempname} | #{FZF} -ffoobar --tiebreak=index`.split($/) + assert_equal input, `#{FZF} -ffoobar --tiebreak=index < #{tempname}`.split($/) by_length = %w[ ----foobar-- @@ -480,8 +480,8 @@ class TestGoFZF < TestBase -------foobar- --foobar-------- ] - assert_equal by_length, `cat #{tempname} | #{FZF} -ffoobar`.split($/) - assert_equal by_length, `cat #{tempname} | #{FZF} -ffoobar --tiebreak=length`.split($/) + assert_equal by_length, `#{FZF} -ffoobar < #{tempname}`.split($/) + assert_equal by_length, `#{FZF} -ffoobar --tiebreak=length < #{tempname}`.split($/) by_begin = %w[ --foobar-------- @@ -489,17 +489,117 @@ class TestGoFZF < TestBase -----foobar--- -------foobar- ] - assert_equal by_begin, `cat #{tempname} | #{FZF} -ffoobar --tiebreak=begin`.split($/) - assert_equal by_begin, `cat #{tempname} | #{FZF} -f"!z foobar" -x --tiebreak begin`.split($/) + assert_equal by_begin, `#{FZF} -ffoobar --tiebreak=begin < #{tempname}`.split($/) + assert_equal by_begin, `#{FZF} -f"!z foobar" -x --tiebreak begin < #{tempname}`.split($/) assert_equal %w[ -------foobar- ----foobar-- -----foobar--- --foobar-------- - ], `cat #{tempname} | #{FZF} -ffoobar --tiebreak end`.split($/) + ], `#{FZF} -ffoobar --tiebreak end < #{tempname}`.split($/) - assert_equal input, `cat #{tempname} | #{FZF} -f"!z" -x --tiebreak end`.split($/) + assert_equal input, `#{FZF} -f"!z" -x --tiebreak end < #{tempname}`.split($/) + end + + # Since 0.11.2 + def test_tiebreak_list + input = %w[ + f-o-o-b-a-r + foobar---- + --foobar + ----foobar + foobar-- + --foobar-- + foobar + ] + writelines tempname, input + + assert_equal %w[ + foobar---- + --foobar + ----foobar + foobar-- + --foobar-- + foobar + f-o-o-b-a-r + ], `#{FZF} -ffb --tiebreak=index < #{tempname}`.split($/) + + by_length = %w[ + foobar + --foobar + foobar-- + foobar---- + ----foobar + --foobar-- + f-o-o-b-a-r + ] + assert_equal by_length, `#{FZF} -ffb < #{tempname}`.split($/) + assert_equal by_length, `#{FZF} -ffb --tiebreak=length < #{tempname}`.split($/) + + assert_equal %w[ + foobar + foobar-- + --foobar + foobar---- + --foobar-- + ----foobar + f-o-o-b-a-r + ], `#{FZF} -ffb --tiebreak=length,begin < #{tempname}`.split($/) + + assert_equal %w[ + foobar + --foobar + foobar-- + ----foobar + --foobar-- + foobar---- + f-o-o-b-a-r + ], `#{FZF} -ffb --tiebreak=length,end < #{tempname}`.split($/) + + assert_equal %w[ + foobar---- + foobar-- + foobar + --foobar + --foobar-- + ----foobar + f-o-o-b-a-r + ], `#{FZF} -ffb --tiebreak=begin < #{tempname}`.split($/) + + by_begin_end = %w[ + foobar + foobar-- + foobar---- + --foobar + --foobar-- + ----foobar + f-o-o-b-a-r + ] + assert_equal by_begin_end, `#{FZF} -ffb --tiebreak=begin,length < #{tempname}`.split($/) + assert_equal by_begin_end, `#{FZF} -ffb --tiebreak=begin,end < #{tempname}`.split($/) + + assert_equal %w[ + --foobar + ----foobar + foobar + foobar-- + --foobar-- + foobar---- + f-o-o-b-a-r + ], `#{FZF} -ffb --tiebreak=end < #{tempname}`.split($/) + + by_begin_end = %w[ + foobar + --foobar + ----foobar + foobar-- + --foobar-- + foobar---- + f-o-o-b-a-r + ] + assert_equal by_begin_end, `#{FZF} -ffb --tiebreak=end,begin < #{tempname}`.split($/) + assert_equal by_begin_end, `#{FZF} -ffb --tiebreak=end,length < #{tempname}`.split($/) end def test_tiebreak_length_with_nth @@ -517,7 +617,7 @@ class TestGoFZF < TestBase 123:hello 1234567:h ] - assert_equal output, `cat #{tempname} | #{FZF} -fh`.split($/) + assert_equal output, `#{FZF} -fh < #{tempname}`.split($/) output = %w[ 1234567:h @@ -525,7 +625,7 @@ class TestGoFZF < TestBase 1:hell 123:hello ] - assert_equal output, `cat #{tempname} | #{FZF} -fh -n2 -d:`.split($/) + assert_equal output, `#{FZF} -fh -n2 -d: < #{tempname}`.split($/) end def test_tiebreak_length_with_nth_trim_length @@ -544,7 +644,7 @@ class TestGoFZF < TestBase "apple juice bottle 1", "apple ui bottle 2", ] - assert_equal output, `cat #{tempname} | #{FZF} -fa -n1`.split($/) + assert_equal output, `#{FZF} -fa -n1 < #{tempname}`.split($/) # len(1 ~ 2) output = [ @@ -553,7 +653,7 @@ class TestGoFZF < TestBase "apple juice bottle 1", "app ice bottle 3", ] - assert_equal output, `cat #{tempname} | #{FZF} -fai -n1..2`.split($/) + assert_equal output, `#{FZF} -fai -n1..2 < #{tempname}`.split($/) # len(1) + len(2) output = [ @@ -562,7 +662,7 @@ class TestGoFZF < TestBase "apple ui bottle 2", "apple juice bottle 1", ] - assert_equal output, `cat #{tempname} | #{FZF} -x -f"a i" -n1,2`.split($/) + assert_equal output, `#{FZF} -x -f"a i" -n1,2 < #{tempname}`.split($/) # len(2) output = [ @@ -571,8 +671,8 @@ class TestGoFZF < TestBase "app ice bottle 3", "apple juice bottle 1", ] - assert_equal output, `cat #{tempname} | #{FZF} -fi -n2`.split($/) - assert_equal output, `cat #{tempname} | #{FZF} -fi -n2,1..2`.split($/) + assert_equal output, `#{FZF} -fi -n2 < #{tempname}`.split($/) + assert_equal output, `#{FZF} -fi -n2,1..2 < #{tempname}`.split($/) end def test_tiebreak_end_backward_scan @@ -582,8 +682,8 @@ class TestGoFZF < TestBase ] writelines tempname, input - assert_equal input.reverse, `cat #{tempname} | #{FZF} -f fb`.split($/) - assert_equal input, `cat #{tempname} | #{FZF} -f fb --tiebreak=end`.split($/) + assert_equal input.reverse, `#{FZF} -f fb < #{tempname}`.split($/) + assert_equal input, `#{FZF} -f fb --tiebreak=end < #{tempname}`.split($/) end def test_invalid_cache @@ -613,7 +713,7 @@ class TestGoFZF < TestBase File.open(tempname, 'w') do |f| f << data end - assert_equal data, `cat #{tempname} | #{FZF} -f .`.chomp + assert_equal data, `#{FZF} -f . < #{tempname}`.chomp end def test_read0 @@ -888,18 +988,18 @@ class TestGoFZF < TestBase def test_with_nth writelines tempname, ['hello world ', 'byebye'] - assert_equal 'hello world ', `cat #{tempname} | #{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1`.chomp + assert_equal 'hello world ', `#{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 < #{tempname}`.chomp end def test_with_nth_ansi writelines tempname, ["\x1b[33mhello \x1b[34;1mworld\x1b[m ", 'byebye'] - assert_equal 'hello world ', `cat #{tempname} | #{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 --ansi`.chomp + assert_equal 'hello world ', `#{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 --ansi < #{tempname}`.chomp end def test_with_nth_no_ansi src = "\x1b[33mhello \x1b[34;1mworld\x1b[m " writelines tempname, [src, 'byebye'] - assert_equal src, `cat #{tempname} | #{FZF} -fhehe -x -n 2.. --with-nth 2,1,1 --no-ansi`.chomp + assert_equal src, `#{FZF} -fhehe -x -n 2.. --with-nth 2,1,1 --no-ansi < #{tempname}`.chomp end def test_exit_0_exit_code