34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
|
# File 'lib/okf/cli/search.rb', line 34
def call(argv)
options = { json: false, regexp: false, fuzzy: false, engine: nil }
parser = OptionParser.new do |o|
o.banner = "Usage: okf search <dir|@slug…|@all> <term…> [--engine NAME] [--regexp|--fuzzy] [--in FIELDS] [--type T] [--area A] [--tag T] [--json]"
search_engine_note(o)
json_flags(o, options, "emit the matches as JSON")
projection_flags(o, options)
o.on("-e", "--regexp", "read each term as a Ruby regular expression rather",
"than literal text — case-insensitive (scan engine)") { options[:regexp] = true }
o.on("--fuzzy",
"tolerate typos, edit distance #{OKF::Bundle::Search::FUZZY_DISTANCE} × term length (index engine)") { options[:fuzzy] = true }
o.on("--engine NAME", "match with this engine instead of the default",
"(#{engine_names}) — index is BM25+ ranked, token-based") { |v| options[:engine] = v }
o.on("--in LIST", Array, "search only these fields (#{OKF::Bundle::Search::FIELDS.join(", ")})") { |v| options[:in] = v.map(&:downcase) }
filter_flags(o, options, :type, :area, :tag)
help_flag(o)
end
begin
parser.parse!(argv)
rescue OptionParser::ParseError => e
@err.puts e.message
return 2
end
if argv.first&.start_with?("@")
pairs = ref_targets(argv) or return 2
dir = nil
else
dir = argv.shift
if dir.nil?
@err.puts parser.banner
return 2
end
dir = resolve_ref(dir) or return 2
end
terms = argv
if terms.empty?
@err.puts parser.banner
return 2
end
stray = terms.find { |term| term.start_with?("@") }
@err.puts "note: '#{stray}' searches as a literal term — an @slug or @all must lead" if stray
unknown = Array(options[:in]) - OKF::Bundle::Search::FIELDS
return usage_error("unknown field(s): #{unknown.join(", ")} (searchable: #{OKF::Bundle::Search::FIELDS.join(", ")})") unless unknown.empty?
if options[:regexp] && options[:fuzzy]
return usage_error("--regexp and --fuzzy are mutually exclusive (a pattern is matched literally, not by edit distance)")
end
return multi_search(pairs, terms, options) if pairs
folder = OKF::Bundle::Folder.load(dir)
report_skipped(folder)
rows = OKF::Bundle::Search.call(folder.bundle, terms, fields: options[:in], regexp: options[:regexp],
fuzzy: options[:fuzzy], engine: options[:engine])
keep = filter_ids(folder, options)
rows = rows.select { |row| keep.include?(row[:id]) } unless keep.nil?
return print_search_json(dir, terms, rows, options) if options[:json]
print_search(dir, terms, rows, folder.bundle.concepts.size)
0
rescue RegexpError => e
usage_error("invalid pattern: #{e.message}")
rescue OKF::Bundle::Search::UnknownEngine => e
usage_error(e.message)
rescue OKF::Bundle::Search::UnsupportedQuery => e
usage_error(unsupported_query_message(e))
end
|