Implement --select-1 and --exit-0 (#27, #36)

This commit is contained in:
Junegunn Choi 2014-04-02 21:41:57 +09:00
parent ab9fbf1967
commit 22d3929ae3
3 changed files with 183 additions and 77 deletions

View File

@ -50,23 +50,31 @@ Usage
``` ```
usage: fzf [options] usage: fzf [options]
Options Search
-m, --multi Enable multi-select
-x, --extended Extended-search mode -x, --extended Extended-search mode
-e, --extended-exact Extended-search mode (exact match) -e, --extended-exact Extended-search mode (exact match)
-q, --query=STR Initial query -i Case-insensitive match (default: smart-case match)
-f, --filter=STR Filter mode. Do not start interactive finder. +i Case-sensitive match
-n, --nth=[-]N[,..] Comma-separated list of field indexes for limiting -n, --nth=[-]N[,..] Comma-separated list of field indexes for limiting
search scope (positive or negative integers) search scope (positive or negative integers)
-d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style) -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style)
Search result
-s, --sort=MAX Maximum number of matched items to sort (default: 1000) -s, --sort=MAX Maximum number of matched items to sort (default: 1000)
+s, --no-sort Do not sort the result. Keep the sequence unchanged. +s, --no-sort Do not sort the result. Keep the sequence unchanged.
-i Case-insensitive match (default: smart-case match)
+i Case-sensitive match Interface
-m, --multi Enable multi-select with tab/shift-tab
--no-mouse Disable mouse
+c, --no-color Disable colors +c, --no-color Disable colors
+2, --no-256 Disable 256-color +2, --no-256 Disable 256-color
--black Use black background --black Use black background
--no-mouse Disable mouse
Scripting
-q, --query=STR Start the finder with the given query
-1, --select-1 (with --query) Automatically select the only match
-0, --exit-0 (with --query) Exit when there's no match
-f, --filter=STR Filter mode. Do not start interactive finder.
Environment variables Environment variables
FZF_DEFAULT_COMMAND Default command to use when input is tty FZF_DEFAULT_COMMAND Default command to use when input is tty
@ -137,10 +145,12 @@ Useful examples
--------------- ---------------
```sh ```sh
# vimf - Open selected file in Vim # fe [FUZZY PATTERN] - Open the selected file with the default editor
vimf() { # - Bypass fuzzy finder if there's only one match (--select-1)
# - Exit if there's no match (--exit-0)
fe() {
local file local file
file=$(fzf --query="$1") && vim "$file" file=$(fzf --query="$1" --select-1 --exit-0) && ${EDITOR:-vim} "$file"
} }
# fd - cd to selected directory # fd - cd to selected directory
@ -193,29 +203,6 @@ ftags() {
) && $EDITOR $(cut -f3 <<< "$line") -c "set nocst" \ ) && $EDITOR $(cut -f3 <<< "$line") -c "set nocst" \
-c "silent tag $(cut -f2 <<< "$line")" -c "silent tag $(cut -f2 <<< "$line")"
} }
# fq1 [QUERY]
# - Immediately select the file when there's only one match.
# If not, start the fuzzy finder as usual.
fq1() {
local lines
lines=$(fzf --filter="$1" --no-sort)
if [ -z "$lines" ]; then
return 1
elif [ $(wc -l <<< "$lines") -eq 1 ]; then
echo "$lines"
else
echo "$lines" | fzf --query="$1"
fi
}
# fe [QUERY]
# - Open the selected file with the default editor
# (Bypass fuzzy finder when there's only one match)
fe() {
local file
file=$(fq1 "$1") && ${EDITOR:-vim} "$file"
}
``` ```
Key bindings for command line Key bindings for command line

106
fzf
View File

@ -51,7 +51,7 @@ end
class FZF class FZF
C = Curses C = Curses
attr_reader :rxflag, :sort, :nth, :color, :black, :ansi256, attr_reader :rxflag, :sort, :nth, :color, :black, :ansi256,
:mouse, :multi, :query, :filter, :extended :mouse, :multi, :query, :select1, :exit0, :filter, :extended
class AtomicVar class AtomicVar
def initialize value def initialize value
@ -83,6 +83,8 @@ class FZF
@multi = false @multi = false
@mouse = true @mouse = true
@extended = nil @extended = nil
@select1 = false
@exit0 = false
@filter = nil @filter = nil
@nth = nil @nth = nil
@delim = nil @delim = nil
@ -113,6 +115,10 @@ class FZF
when '--mouse' then @mouse = true when '--mouse' then @mouse = true
when '--no-mouse' then @mouse = false when '--no-mouse' then @mouse = false
when '+s', '--no-sort' then @sort = nil when '+s', '--no-sort' then @sort = nil
when '-1', '--select-1' then @select1 = true
when '+1', '--no-select-1' then @select1 = false
when '-0', '--exit-0' then @exit0 = true
when '+0', '--no-exit-0' then @exit0 = false
when '-q', '--query' when '-q', '--query'
usage 1, 'query string required' unless query = argv.shift usage 1, 'query string required' unless query = argv.shift
@query = AtomicVar.new query.dup @query = AtomicVar.new query.dup
@ -184,41 +190,60 @@ class FZF
def start def start
if @filter if @filter
start_reader(false).join start_reader.join
filter_list @new filter_list @new
else else
@stdout = $stdout.clone start_reader
$stdout.reopen($stderr) emit(:key) { q = @query.get; [q, q.length] } unless @query.empty?
if !@query.empty? && (@select1 || @exit0)
start_search do |loaded, matches|
len = matches.length
if loaded
if @select1 && len == 1
puts matches.first.first
exit 0
elsif @exit0 && len == 0
exit 1
end
end
start_reader true if loaded || len > 1
init_screen start_renderer
start_renderer Thread.new { start_loop }
start_search end
start_loop end
sleep
else
start_search
start_renderer
start_loop
end
end end
end end
def filter_list list def filter_list list
matches = get_matcher.match(list, @filter, '', '') matches = matcher.match(list, @filter, '', '')
if @sort && matches.length <= @sort if @sort && matches.length <= @sort
matches = sort_by_rank(matches) matches = sort_by_rank(matches)
end end
matches.each { |m| puts m.first } matches.each { |m| puts m.first }
end end
def get_matcher def matcher
if @extended @matcher ||=
ExtendedFuzzyMatcher.new @rxflag, @extended, @nth, @delim if @extended
else ExtendedFuzzyMatcher.new @rxflag, @extended, @nth, @delim
FuzzyMatcher.new @rxflag, @nth, @delim else
end FuzzyMatcher.new @rxflag, @nth, @delim
end
end end
def version def version
File.open(__FILE__, 'r') do |f| File.open(__FILE__, 'r') do |f|
f.each_line do |line| f.each_line do |line|
if line =~ /Version: (.*)/ if line =~ /Version: (.*)/
$stdout.puts "fzf " << $1 $stdout.puts 'fzf ' << $1
exit exit
end end
end end
@ -229,23 +254,31 @@ class FZF
$stderr.puts message if message $stderr.puts message if message
$stderr.puts %[usage: fzf [options] $stderr.puts %[usage: fzf [options]
Options Search
-m, --multi Enable multi-select
-x, --extended Extended-search mode -x, --extended Extended-search mode
-e, --extended-exact Extended-search mode (exact match) -e, --extended-exact Extended-search mode (exact match)
-q, --query=STR Initial query -i Case-insensitive match (default: smart-case match)
-f, --filter=STR Filter mode. Do not start interactive finder. +i Case-sensitive match
-n, --nth=[-]N[,..] Comma-separated list of field indexes for limiting -n, --nth=[-]N[,..] Comma-separated list of field indexes for limiting
search scope (positive or negative integers) search scope (positive or negative integers)
-d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style) -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style)
Search result
-s, --sort=MAX Maximum number of matched items to sort (default: 1000) -s, --sort=MAX Maximum number of matched items to sort (default: 1000)
+s, --no-sort Do not sort the result. Keep the sequence unchanged. +s, --no-sort Do not sort the result. Keep the sequence unchanged.
-i Case-insensitive match (default: smart-case match)
+i Case-sensitive match Interface
-m, --multi Enable multi-select with tab/shift-tab
--no-mouse Disable mouse
+c, --no-color Disable colors +c, --no-color Disable colors
+2, --no-256 Disable 256-color +2, --no-256 Disable 256-color
--black Use black background --black Use black background
--no-mouse Disable mouse
Scripting
-q, --query=STR Start the finder with the given query
-1, --select-1 (with --query) Automatically select the only match
-0, --exit-0 (with --query) Exit when there's no match
-f, --filter=STR Filter mode. Do not start interactive finder.
Environment variables Environment variables
FZF_DEFAULT_COMMAND Default command to use when input is tty FZF_DEFAULT_COMMAND Default command to use when input is tty
@ -547,6 +580,9 @@ class FZF
end end
def init_screen def init_screen
@stdout = $stdout.clone
$stdout.reopen($stderr)
C.init_screen C.init_screen
C.mousemask C::ALL_MOUSE_EVENTS if @mouse C.mousemask C::ALL_MOUSE_EVENTS if @mouse
C.start_color C.start_color
@ -604,7 +640,7 @@ class FZF
C.refresh C.refresh
end end
def start_reader curses def start_reader
stream = stream =
if @source.tty? if @source.tty?
if default_command = ENV['FZF_DEFAULT_COMMAND'] if default_command = ENV['FZF_DEFAULT_COMMAND']
@ -623,13 +659,12 @@ class FZF
emit(:new) { @new << line.chomp } emit(:new) { @new << line.chomp }
end end
emit(:loaded) { true } emit(:loaded) { true }
@spinner.clear if curses @spinner.clear if @spinner
end end
end end
def start_search def start_search &callback
matcher = get_matcher Thread.new do
searcher = Thread.new {
lists = [] lists = []
events = {} events = {}
fcache = {} fcache = {}
@ -668,7 +703,7 @@ class FZF
progress = 0 progress = 0
started_at = Time.now started_at = Time.now
if new_search && !lists.empty? if updated = new_search && !lists.empty?
q, cx = events.delete(:key) || [q, 0] q, cx = events.delete(:key) || [q, 0]
empty = matcher.empty?(q) empty = matcher.empty?(q)
unless matches = fcache[q] unless matches = fcache[q]
@ -699,6 +734,10 @@ class FZF
@matches.set matches @matches.set matches
end#new_search end#new_search
callback = nil if callback &&
(updated || events[:loaded]) &&
callback.call(events[:loaded], matches)
# This small delay reduces the number of partial lists # This small delay reduces the number of partial lists
sleep((delay = [20, delay + 5].min) * 0.01) unless user_input sleep((delay = [20, delay + 5].min) * 0.01) unless user_input
@ -707,7 +746,7 @@ class FZF
rescue Exception => e rescue Exception => e
@main.raise e @main.raise e
end end
} end
end end
def pick def pick
@ -751,6 +790,8 @@ class FZF
end end
def start_renderer def start_renderer
init_screen
Thread.new do Thread.new do
begin begin
while blk = @queue.shift while blk = @queue.shift
@ -1030,7 +1071,6 @@ class FZF
actions[127] = actions[ctrl(:h)] actions[127] = actions[ctrl(:h)]
actions[ctrl(:q)] = actions[ctrl(:g)] = actions[ctrl(:c)] = actions[:esc] actions[ctrl(:q)] = actions[ctrl(:g)] = actions[ctrl(:c)] = actions[:esc]
emit(:key) { [@query.get, cursor] } unless @query.empty?
while true while true
@cursor_x.set cursor @cursor_x.set cursor
render { print_input } render { print_input }
@ -1093,7 +1133,7 @@ class FZF
if (token = tokens[n]) && (md = token.match(pat) rescue nil) if (token = tokens[n]) && (md = token.match(pat) rescue nil)
prefix_length += (tokens[0...n] || []).join.length prefix_length += (tokens[0...n] || []).join.length
offset = md.offset(0).map { |o| o + prefix_length } offset = md.offset(0).map { |o| o + prefix_length }
return MatchData.new offset return MatchData.new(offset)
end end
end end
nil nil

View File

@ -1,6 +1,9 @@
#!/usr/bin/env ruby #!/usr/bin/env ruby
# encoding: utf-8 # encoding: utf-8
require 'curses'
require 'timeout'
require 'stringio'
require 'minitest/autorun' require 'minitest/autorun'
$LOAD_PATH.unshift File.expand_path('../..', __FILE__) $LOAD_PATH.unshift File.expand_path('../..', __FILE__)
ENV['FZF_EXECUTABLE'] = '0' ENV['FZF_EXECUTABLE'] = '0'
@ -20,6 +23,15 @@ class TestFZF < MiniTest::Unit::TestCase
assert_equal true, fzf.color assert_equal true, fzf.color
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 true, fzf.color
assert_equal false, fzf.black
assert_equal true, fzf.ansi256
assert_equal '', fzf.query.get
assert_equal false, fzf.select1
assert_equal false, fzf.exit0
assert_equal nil, fzf.filter
assert_equal nil, fzf.extended
end end
def test_environment_variables def test_environment_variables
@ -30,7 +42,8 @@ class TestFZF < MiniTest::Unit::TestCase
assert_equal nil, fzf.nth assert_equal nil, fzf.nth
ENV['FZF_DEFAULT_OPTS'] = ENV['FZF_DEFAULT_OPTS'] =
'-x -m -s 10000 -q " hello world " +c +2 --no-mouse -f "goodbye world" --black --nth=3,-1,2' '-x -m -s 10000 -q " hello world " +c +2 --select-1 -0 ' +
'--no-mouse -f "goodbye world" --black --nth=3,-1,2'
fzf = FZF.new [] fzf = FZF.new []
assert_equal 10000, fzf.sort assert_equal 10000, fzf.sort
assert_equal ' hello world ', assert_equal ' hello world ',
@ -43,13 +56,16 @@ class TestFZF < MiniTest::Unit::TestCase
assert_equal false, fzf.ansi256 assert_equal false, fzf.ansi256
assert_equal true, fzf.black assert_equal true, fzf.black
assert_equal false, fzf.mouse assert_equal false, fzf.mouse
assert_equal true, fzf.select1
assert_equal true, fzf.exit0
assert_equal [3, -1, 2], fzf.nth assert_equal [3, -1, 2], fzf.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 fzf = FZF.new %w[--sort=2000 --no-color --multi +i --query hello --select-1
--filter=howdy --extended-exact --no-mouse --no-256 --nth=1] --exit-0 --filter=howdy --extended-exact
--no-mouse --no-256 --nth=1]
assert_equal 2000, fzf.sort assert_equal 2000, fzf.sort
assert_equal true, fzf.multi assert_equal true, fzf.multi
assert_equal false, fzf.color assert_equal false, fzf.color
@ -58,12 +74,16 @@ class TestFZF < MiniTest::Unit::TestCase
assert_equal false, fzf.mouse assert_equal false, fzf.mouse
assert_equal 0, fzf.rxflag assert_equal 0, fzf.rxflag
assert_equal 'hello', fzf.query.get assert_equal 'hello', fzf.query.get
assert_equal true, fzf.select1
assert_equal true, fzf.exit0
assert_equal 'howdy', fzf.filter assert_equal 'howdy', fzf.filter
assert_equal :exact, fzf.extended assert_equal :exact, fzf.extended
assert_equal [1], fzf.nth assert_equal [1], fzf.nth
fzf = FZF.new %w[--sort=2000 --no-color --multi +i --query hello # Long opts (left-to-right)
--filter a --filter b --no-256 --black --nth -2 fzf = FZF.new %w[--sort=2000 --no-color --multi +i --query=hello
--filter a --filter b --no-256 --black --nth -1 --nth -2
--select-1 --exit-0 --no-select-1 --no-exit-0
--no-sort -i --color --no-multi --256] --no-sort -i --color --no-multi --256]
assert_equal nil, fzf.sort assert_equal nil, fzf.sort
assert_equal false, fzf.multi assert_equal false, fzf.multi
@ -74,11 +94,13 @@ class TestFZF < MiniTest::Unit::TestCase
assert_equal 1, fzf.rxflag assert_equal 1, fzf.rxflag
assert_equal 'b', fzf.filter assert_equal 'b', fzf.filter
assert_equal 'hello', fzf.query.get assert_equal 'hello', fzf.query.get
assert_equal false, fzf.select1
assert_equal false, fzf.exit0
assert_equal nil, fzf.extended assert_equal nil, fzf.extended
assert_equal [-2], fzf.nth assert_equal [-2], fzf.nth
# Short opts # Short opts
fzf = FZF.new %w[-s 2000 +c -m +i -qhello -x -fhowdy +2 -n3] fzf = FZF.new %w[-s2000 +c -m +i -qhello -x -fhowdy +2 -n3 -1 -0]
assert_equal 2000, fzf.sort assert_equal 2000, fzf.sort
assert_equal true, fzf.multi assert_equal true, fzf.multi
assert_equal false, fzf.color assert_equal false, fzf.color
@ -88,10 +110,14 @@ class TestFZF < MiniTest::Unit::TestCase
assert_equal 'howdy', fzf.filter assert_equal 'howdy', fzf.filter
assert_equal :fuzzy, fzf.extended assert_equal :fuzzy, fzf.extended
assert_equal [3], fzf.nth assert_equal [3], fzf.nth
assert_equal true, fzf.select1
assert_equal true, fzf.exit0
# Left-to-right # Left-to-right
fzf = FZF.new %w[-s 2000 +c -m +i -qhello -x -fgoodbye +2 -n3 -n4,5 fzf = FZF.new %w[-s 2000 +c -m +i -qhello -x -fgoodbye +2 -n3 -n4,5
-s 3000 -c +m -i -q world +x -fworld -2 --black --no-black] -s 3000 -c +m -i -q world +x -fworld -2 --black --no-black
-1 -0 +1 +0
]
assert_equal 3000, fzf.sort assert_equal 3000, fzf.sort
assert_equal false, fzf.multi assert_equal false, fzf.multi
assert_equal true, fzf.color assert_equal true, fzf.color
@ -99,13 +125,11 @@ class TestFZF < MiniTest::Unit::TestCase
assert_equal false, fzf.black assert_equal false, fzf.black
assert_equal 1, fzf.rxflag assert_equal 1, fzf.rxflag
assert_equal 'world', fzf.query.get assert_equal 'world', fzf.query.get
assert_equal false, fzf.select1
assert_equal false, fzf.exit0
assert_equal 'world', fzf.filter assert_equal 'world', fzf.filter
assert_equal nil, fzf.extended assert_equal nil, fzf.extended
assert_equal [4, 5], fzf.nth assert_equal [4, 5], fzf.nth
fzf = FZF.new %w[--query hello +s -s 2000 --query=world]
assert_equal 2000, fzf.sort
assert_equal 'world', fzf.query.get
rescue SystemExit => e rescue SystemExit => e
assert false, "Exited" assert false, "Exited"
end end
@ -538,5 +562,60 @@ class TestFZF < MiniTest::Unit::TestCase
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [2], regex matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [2], regex
assert_equal [[list[0], [[1, 2]]], [list[1], [[8, 9]]]], matcher.match(list, 'f', '', '') assert_equal [[list[0], [[1, 2]]], [list[1], [[8, 9]]]], matcher.match(list, 'f', '', '')
end end
def stream_for str
StringIO.new(str).tap do |sio|
sio.instance_eval do
alias org_gets gets
def gets
org_gets.tap { |e| sleep 0.5 unless e.nil? }
end
end
end
end
def test_select_1
stream = stream_for "Hello\nWorld"
output = StringIO.new
begin
$stdout = output
FZF.new(%w[--query=ol --select-1], stream).start
rescue SystemExit => e
assert_equal 0, e.status
assert_equal 'World', output.string.chomp
ensure
$stdout = STDOUT
end
end
def test_select_1_ambiguity
stream = stream_for "Hello\nWorld"
begin
Timeout::timeout(3) do
FZF.new(%w[--query=o --select-1], stream).start
end
flunk 'Should not reach here'
rescue Exception => e
Curses.close_screen
assert_instance_of Timeout::Error, e
end
end
def test_exit_0
stream = stream_for "Hello\nWorld"
output = StringIO.new
begin
$stdout = output
FZF.new(%w[--query=zz --exit-0], stream).start
rescue SystemExit => e
assert_equal 1, e.status
assert_equal '', output.string
ensure
$stdout = STDOUT
end
end
end end