Merge pull request #104 from junegunn/add-with-nth

Add --with-nth option
This commit is contained in:
Junegunn Choi 2014-11-04 23:30:11 +09:00
commit 80819f3c44
5 changed files with 250 additions and 97 deletions

9
.travis.yml Normal file
View File

@ -0,0 +1,9 @@
language: ruby
rvm:
- "1.8.7"
- "1.9.3"
- "2.0.0"
- "2.1.1"
install: gem install curses minitest

View File

@ -72,6 +72,7 @@ usage: fzf [options]
-n, --nth=N[,..] Comma-separated list of field index expressions -n, --nth=N[,..] Comma-separated list of field index expressions
for limiting search scope. Each can be a non-zero for limiting search scope. Each can be a non-zero
integer or a range expression ([BEGIN]..[END]) 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) -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style)
Search result Search result

View File

@ -6,3 +6,4 @@ Rake::TestTask.new(:test) do |test|
test.verbose = true test.verbose = true
end end
task :default => :test

90
fzf
View File

@ -7,7 +7,7 @@
# / __/ / /_/ __/ # / __/ / /_/ __/
# /_/ /___/_/ Fuzzy finder for your shell # /_/ /___/_/ Fuzzy finder for your shell
# #
# Version: 0.8.7 (Aug 17, 2014) # Version: 0.8.8 (Nov 4, 2014)
# #
# Author: Junegunn Choi # Author: Junegunn Choi
# URL: https://github.com/junegunn/fzf # URL: https://github.com/junegunn/fzf
@ -53,11 +53,34 @@ unless String.method_defined? :force_encoding
end end
end end
class String
attr_accessor :orig
def tokenize delim, nth
unless delim
# AWK default
prefix_length = (index(/\S/) || 0) rescue 0
tokens = scan(/\S+\s*/) rescue []
else
prefix_length = 0
tokens = scan(delim) rescue []
end
nth.map { |n|
if n.begin == 0 && n.end == -1
[prefix_length, tokens.join]
elsif part = tokens[n]
[prefix_length + (tokens[0...(n.begin)] || []).join.length,
part.join]
end
}.compact
end
end
class FZF class FZF
C = Curses C = Curses
attr_reader :rxflag, :sort, :nth, :color, :black, :ansi256, :reverse, :prompt, attr_reader :rxflag, :sort, :nth, :color, :black, :ansi256, :reverse, :prompt,
:mouse, :multi, :query, :select1, :exit0, :filter, :extended, :mouse, :multi, :query, :select1, :exit0, :filter, :extended,
:print_query :print_query, :with_nth
def sync def sync
@shr_mtx.synchronize { yield } @shr_mtx.synchronize { yield }
@ -95,6 +118,7 @@ class FZF
@exit0 = false @exit0 = false
@filter = nil @filter = nil
@nth = nil @nth = nil
@with_nth = nil
@delim = nil @delim = nil
@reverse = false @reverse = false
@prompt = '> ' @prompt = '> '
@ -148,6 +172,11 @@ class FZF
@nth = parse_nth nth @nth = parse_nth nth
when /^-n([0-9,-\.]+)$/, /^--nth=([0-9,-\.]+)$/ when /^-n([0-9,-\.]+)$/, /^--nth=([0-9,-\.]+)$/
@nth = parse_nth $1 @nth = parse_nth $1
when '--with-nth'
usage 1, 'field expression required' unless nth = argv.shift
@with_nth = parse_nth nth
when /^--with-nth=([0-9,-\.]+)$/
@with_nth = parse_nth $1
when '-d', '--delimiter' when '-d', '--delimiter'
usage 1, 'delimiter required' unless delim = argv.shift usage 1, 'delimiter required' unless delim = argv.shift
@delim = FZF.build_delim_regex delim @delim = FZF.build_delim_regex delim
@ -181,6 +210,7 @@ class FZF
@queue = Queue.new @queue = Queue.new
@pending = nil @pending = nil
@rev_dir = @reverse ? -1 : 1 @rev_dir = @reverse ? -1 : 1
@stdout = $stdout.clone
unless @filter unless @filter
# Shared variables: needs protection # Shared variables: needs protection
@ -200,7 +230,7 @@ class FZF
end end
def parse_nth nth def parse_nth nth
nth.split(',').map { |expr| ranges = nth.split(',').map { |expr|
x = proc { usage 1, "invalid field expression: #{expr}" } x = proc { usage 1, "invalid field expression: #{expr}" }
first, second = expr.split('..', 2) first, second = expr.split('..', 2)
x.call if !first.empty? && first.to_i == 0 || x.call if !first.empty? && first.to_i == 0 ||
@ -215,6 +245,7 @@ class FZF
Range.new(*[first, second].map { |e| e > 0 ? e - 1 : e }) Range.new(*[first, second].map { |e| e > 0 ? e - 1 : e })
} }
ranges == [0..-1] ? nil : ranges
end end
def FZF.build_delim_regex delim def FZF.build_delim_regex delim
@ -222,6 +253,10 @@ class FZF
Regexp.compile "(?:.*?#{delim})|(?:.+?$)" Regexp.compile "(?:.*?#{delim})|(?:.+?$)"
end end
def burp string, orig = nil
@stdout.puts(orig || string.orig || string)
end
def start def start
if @filter if @filter
start_reader.join start_reader.join
@ -236,7 +271,7 @@ class FZF
if loaded if loaded
if @select1 && len == 1 if @select1 && len == 1
puts @query if @print_query puts @query if @print_query
puts empty ? matches.first : matches.first.first burp(empty ? matches.first : matches.first.first)
exit 0 exit 0
elsif @exit0 && len == 0 elsif @exit0 && len == 0
puts @query if @print_query puts @query if @print_query
@ -319,6 +354,7 @@ class FZF
-n, --nth=N[,..] Comma-separated list of field index expressions -n, --nth=N[,..] Comma-separated list of field index expressions
for limiting search scope. Each can be a non-zero for limiting search scope. Each can be a non-zero
integer or a range expression ([BEGIN]..[END]) 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) -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style)
Search result Search result
@ -520,7 +556,6 @@ class FZF
end end
def init_screen def init_screen
@stdout = $stdout.clone
$stdout.reopen($stderr) $stdout.reopen($stderr)
C.init_screen C.init_screen
@ -595,14 +630,28 @@ class FZF
end end
Thread.new do Thread.new do
if @with_nth
while line = stream.gets
emit(:new) { @new << transform(line) }
end
else
while line = stream.gets while line = stream.gets
emit(:new) { @new << line.chomp } emit(:new) { @new << line.chomp }
end end
end
emit(:loaded) { true } emit(:loaded) { true }
@spinner.clear if @spinner @spinner.clear if @spinner
end end
end end
def transform line
line = line.chomp
mut = (line =~ / $/ ? line : line + ' ').
tokenize(@delim, @with_nth).map { |e| e.last }.join('').sub(/ *$/, '')
mut.orig = line
mut
end
def start_search &callback def start_search &callback
Thread.new do Thread.new do
lists = [] lists = []
@ -694,7 +743,8 @@ class FZF
def pick def pick
sync do sync do
[*@matches.fetch(@ycur, [])][0] item = @matches[@ycur]
item.is_a?(Array) ? item[0] : item
end end
end end
@ -1000,7 +1050,7 @@ class FZF
if @selects.has_key? sel if @selects.has_key? sel
@selects.delete sel @selects.delete sel
else else
@selects[sel] = 1 @selects[sel] = sel.orig
end end
end end
vselect { |v| v + case o vselect { |v| v + case o
@ -1080,10 +1130,10 @@ class FZF
@stdout.puts q if @print_query @stdout.puts q if @print_query
if got if got
if selects.empty? if selects.empty?
@stdout.puts got burp got
else else
selects.each do |sel, _| selects.each do |sel, orig|
@stdout.puts sel burp sel, orig
end end
end end
end end
@ -1108,25 +1158,7 @@ class FZF
end end
def tokenize str def tokenize str
@tokens_cache[str] ||= @tokens_cache[str] ||= str.tokenize(@delim, @nth)
begin
unless @delim
# AWK default
prefix_length = (str.index(/\S/) || 0) rescue 0
tokens = str.scan(/\S+\s*/) rescue []
else
prefix_length = 0
tokens = str.scan(@delim) rescue []
end
@nth.map { |n|
if n.begin == 0 && n.end == -1
[prefix_length, tokens.join]
elsif part = tokens[n]
[prefix_length + (tokens[0...(n.begin)] || []).join.length,
part.join]
end
}.compact
end
end end
def do_match str, pat def do_match str, pat

View File

@ -1,6 +1,7 @@
#!/usr/bin/env ruby #!/usr/bin/env ruby
# encoding: utf-8 # encoding: utf-8
require 'rubygems'
require 'curses' require 'curses'
require 'timeout' require 'timeout'
require 'stringio' require 'stringio'
@ -10,6 +11,48 @@ $LOAD_PATH.unshift File.expand_path('../..', __FILE__)
ENV['FZF_EXECUTABLE'] = '0' ENV['FZF_EXECUTABLE'] = '0'
load 'fzf' load 'fzf'
class MockTTY
def initialize
@buffer = ''
@mutex = Mutex.new
@condv = ConditionVariable.new
end
def read_nonblock sz
@mutex.synchronize do
take sz
end
end
def take sz
if @buffer.length >= sz
ret = @buffer[0, sz]
@buffer = @buffer[sz..-1]
ret
end
end
def getc
sleep 0.1
while true
@mutex.synchronize do
if char = take(1)
return char
else
@condv.wait(@mutex)
end
end
end
end
def << str
@mutex.synchronize do
@buffer << str
@condv.broadcast
end
end
end
class TestFZF < MiniTest::Unit::TestCase class TestFZF < MiniTest::Unit::TestCase
def setup def setup
ENV.delete 'FZF_DEFAULT_SORT' ENV.delete 'FZF_DEFAULT_SORT'
@ -25,6 +68,7 @@ class TestFZF < MiniTest::Unit::TestCase
assert_equal nil, fzf.rxflag assert_equal nil, fzf.rxflag
assert_equal true, fzf.mouse assert_equal true, fzf.mouse
assert_equal nil, fzf.nth assert_equal nil, fzf.nth
assert_equal nil, fzf.with_nth
assert_equal true, fzf.color assert_equal true, fzf.color
assert_equal false, fzf.black assert_equal false, fzf.black
assert_equal true, fzf.ansi256 assert_equal true, fzf.ansi256
@ -47,7 +91,7 @@ class TestFZF < MiniTest::Unit::TestCase
ENV['FZF_DEFAULT_OPTS'] = ENV['FZF_DEFAULT_OPTS'] =
'-x -m -s 10000 -q " hello world " +c +2 --select-1 -0 ' << '-x -m -s 10000 -q " hello world " +c +2 --select-1 -0 ' <<
'--no-mouse -f "goodbye world" --black --nth=3,-1,2 --reverse --print-query' '--no-mouse -f "goodbye world" --black --with-nth=3,-3..,2 --nth=3,-1,2 --reverse --print-query'
fzf = FZF.new [] fzf = FZF.new []
assert_equal 10000, fzf.sort assert_equal 10000, fzf.sort
assert_equal ' hello world ', assert_equal ' hello world ',
@ -65,13 +109,14 @@ class TestFZF < MiniTest::Unit::TestCase
assert_equal true, fzf.reverse assert_equal true, fzf.reverse
assert_equal true, fzf.print_query assert_equal true, fzf.print_query
assert_equal [2..2, -1..-1, 1..1], fzf.nth assert_equal [2..2, -1..-1, 1..1], fzf.nth
assert_equal [2..2, -3..-1, 1..1], fzf.with_nth
end end
def test_option_parser def test_option_parser
# Long opts # Long opts
fzf = FZF.new %w[--sort=2000 --no-color --multi +i --query hello --select-1 fzf = FZF.new %w[--sort=2000 --no-color --multi +i --query hello --select-1
--exit-0 --filter=howdy --extended-exact --exit-0 --filter=howdy --extended-exact
--no-mouse --no-256 --nth=1 --reverse --prompt (hi) --no-mouse --no-256 --nth=1 --with-nth=.. --reverse --prompt (hi)
--print-query] --print-query]
assert_equal 2000, fzf.sort assert_equal 2000, fzf.sort
assert_equal true, fzf.multi assert_equal true, fzf.multi
@ -86,6 +131,7 @@ class TestFZF < MiniTest::Unit::TestCase
assert_equal 'howdy', fzf.filter assert_equal 'howdy', fzf.filter
assert_equal :exact, fzf.extended assert_equal :exact, fzf.extended
assert_equal [0..0], fzf.nth assert_equal [0..0], fzf.nth
assert_equal nil, fzf.with_nth
assert_equal true, fzf.reverse assert_equal true, fzf.reverse
assert_equal '(hi)', fzf.prompt assert_equal '(hi)', fzf.prompt
assert_equal true, fzf.print_query assert_equal true, fzf.print_query
@ -168,13 +214,12 @@ class TestFZF < MiniTest::Unit::TestCase
end end
end end
# FIXME Only on 1.9 or above
def test_width def test_width
fzf = FZF.new [] fzf = FZF.new []
assert_equal 5, fzf.width('abcde') assert_equal 5, fzf.width('abcde')
assert_equal 4, fzf.width('한글') assert_equal 4, fzf.width('한글')
assert_equal 5, fzf.width('한글.') assert_equal 5, fzf.width('한글.')
end end if RUBY_VERSION >= '1.9'
def test_trim def test_trim
fzf = FZF.new [] fzf = FZF.new []
@ -187,7 +232,7 @@ class TestFZF < MiniTest::Unit::TestCase
assert_equal ['가나a', 6], fzf.trim('가나ab라마바사.', 5, false) assert_equal ['가나a', 6], fzf.trim('가나ab라마바사.', 5, false)
assert_equal ['가나ab', 5], fzf.trim('가나ab라마바사.', 6, false) assert_equal ['가나ab', 5], fzf.trim('가나ab라마바사.', 6, false)
assert_equal ['가나ab', 5], fzf.trim('가나ab라마바사.', 7, false) assert_equal ['가나ab', 5], fzf.trim('가나ab라마바사.', 7, false)
end end if RUBY_VERSION >= '1.9'
def test_format def test_format
fzf = FZF.new [] fzf = FZF.new []
@ -563,28 +608,41 @@ class TestFZF < MiniTest::Unit::TestCase
assert_equal [[list[0], [[8, 9]]]], matcher.match(list, '^s', '', '') assert_equal [[list[0], [[8, 9]]]], matcher.match(list, '^s', '', '')
end end
def stream_for str def stream_for str, delay = 0
StringIO.new(str).tap do |sio| StringIO.new(str).tap do |sio|
sio.instance_eval do sio.instance_eval do
alias org_gets gets alias org_gets gets
def gets def gets
org_gets.tap { |e| sleep 0.5 unless e.nil? } org_gets.tap { |e| sleep(@delay) unless e.nil? }
end
def reopen _
end end
end end
sio.instance_variable_set :@delay, delay
end end
end end
def assert_fzf_output opts, given, expected def assert_fzf_output opts, given, expected
stream = stream_for given stream = stream_for given
output = StringIO.new output = stream_for ''
def sorted_lines line
line.split($/).sort
end
begin begin
tty = MockTTY.new
$stdout = output $stdout = output
FZF.new(opts, stream).start fzf = FZF.new(opts, stream)
fzf.instance_variable_set :@tty, tty
thr = block_given? && Thread.new { yield tty }
fzf.start
thr && thr.join
rescue SystemExit => e rescue SystemExit => e
assert_equal 0, e.status assert_equal 0, e.status
assert_equal expected, output.string.chomp assert_equal sorted_lines(expected), sorted_lines(output.string)
ensure ensure
$stdout = STDOUT $stdout = STDOUT
end end
@ -613,15 +671,12 @@ class TestFZF < MiniTest::Unit::TestCase
end end
def test_select_1_ambiguity def test_select_1_ambiguity
stream = stream_for "Hello\nWorld"
begin begin
Timeout::timeout(2) do Timeout::timeout(0.5) do
FZF.new(%w[--query=o --select-1], stream).start assert_fzf_output %w[--query=o --select-1], "hello\nworld", "should not match"
end end
flunk 'Should not reach here' rescue Timeout::Error
rescue Exception => e
Curses.close_screen Curses.close_screen
assert_instance_of Timeout::Error, e
end end
end end
@ -638,6 +693,32 @@ class TestFZF < MiniTest::Unit::TestCase
assert_fzf_output %w[--exit-0], '', '' assert_fzf_output %w[--exit-0], '', ''
end end
def test_with_nth
source = "hello world\nbatman"
assert_fzf_output %w[-0 -1 --with-nth=2,1 -x -q ^worl],
source, 'hello world'
assert_fzf_output %w[-0 -1 --with-nth=2,1 -x -q llo$],
source, 'hello world'
assert_fzf_output %w[-0 -1 --with-nth=.. -x -q llo$],
source, ''
assert_fzf_output %w[-0 -1 --with-nth=2,2,2,..,1 -x -q worlworlworlhellworlhell],
source, 'hello world'
assert_fzf_output %w[-0 -1 --with-nth=1,1,-1,1 -x -q batbatbatbat],
source, 'batman'
end
def test_with_nth_transform
fzf = FZF.new %w[--with-nth 2..,1]
assert_equal 'my world hello', fzf.transform('hello my world')
assert_equal 'my world hello', fzf.transform('hello my world')
assert_equal 'my world hello', fzf.transform('hello my world ')
fzf = FZF.new %w[--with-nth 2,-1,2]
assert_equal 'my world my', fzf.transform('hello my world')
assert_equal 'world world world', fzf.transform('hello world')
assert_equal 'world world world', fzf.transform('hello world ')
end
def test_ranking_overlap_match_regions def test_ranking_overlap_match_regions
list = [ list = [
'1 3 4 2', '1 3 4 2',
@ -688,13 +769,42 @@ class TestFZF < MiniTest::Unit::TestCase
tmp << 'hello ' << [0xff].pack('C*') << ' world' << $/ << [0xff].pack('C*') tmp << 'hello ' << [0xff].pack('C*') << ' world' << $/ << [0xff].pack('C*')
tmp.close tmp.close
begin begin
Timeout::timeout(1) do Timeout::timeout(0.5) do
FZF.new(%w[-n..,1,2.. -q^ -x], File.open(tmp.path)).start FZF.new(%w[-n..,1,2.. -q^ -x], File.open(tmp.path)).start
end end
rescue Timeout::Error rescue Timeout::Error
Curses.close_screen
end end
ensure ensure
tmp.unlink tmp.unlink
end end
def test_with_nth_mock_tty
# Manual selection with input
assert_fzf_output ["--with-nth=2,1"], "hello world", "hello world" do |tty|
tty << "world"
tty << "hell"
tty << "\r"
end
# Manual selection without input
assert_fzf_output ["--with-nth=2,1"], "hello world", "hello world" do |tty|
tty << "\r"
end
# Manual selection with input and --multi
lines = "hello world\ngoodbye world"
assert_fzf_output %w[-m --with-nth=2,1], lines, lines do |tty|
tty << "o"
tty << "\e[Z\e[Z"
tty << "\r"
end
# Manual selection without input and --multi
assert_fzf_output %w[-m --with-nth=2,1], lines, lines do |tty|
tty << "\e[Z\e[Z"
tty << "\r"
end
end
end end