-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathauto_argmatcher.lua
287 lines (255 loc) · 9.01 KB
/
auto_argmatcher.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
--------------------------------------------------------------------------------
-- Usage:
--
-- This script automatically creates argmatchers for commands by parsing the
-- help text output from the commands. You must provide a config file that
-- lists the commands for which to auto-create argmatchers.
--
-- Because there is no universal convention for how to request help text from
-- programs, this cannot assume how to request help text. So, you must provide
-- a config file tell it which commands, and how to safely request their help
-- text.
--
-- This loads auto_argmatcher.config files from all of the following locations:
--
-- - The Clink profile directory.
-- - The directory where this script is located.
--
--------------------------------------------------------------------------------
-- Config file format:
--
-- Each line in auto_argmatcher.config has the following format:
--
-- command help_flag parser modes
--
-- The parser runs "command help_flag" and parses the output text.
-- For example:
--
-- dir /? basic
-- where /? basic
--
-- Parsers:
--
-- basic (default) Basic parser that extracts up to one flag and
-- description per line.
-- curl A simple parser that extracts multiple flags and a
-- description per line; based on the format of the Curl
-- tool for Windows.
-- gnu Parser based on help text format of GNU tools like grep
-- and sed.
--
-- Modes:
--
-- --smartcase (default) Flags are case insensitive if all are listed
-- in upper case.
-- --caseless Treat flags as case insensitive.
-- --slashes Some programs recognize both - and / flags, but only
-- list the - flags in the help text. This mode adds a /
-- flag for each - flag (e.g. for -x, also adds /x).
-- --concat Allow one letter flags to be concatenated. This mode
-- recognizes /hk if /h and /k are one letter flags.
-- --override_command The 'help_flag' specifies the whole command to run.
--
-- Example for --override_command:
--
-- Suppose there is a doskey alias "ax" for "abc xyz $*". The command to
-- run to get help text would not be "ax /?", so use --override_command:
--
-- ax "abc xyz /?" basic --override_command
--
-- Notes:
--
-- Blank lines and lines beginning with # or ; or / are ignored.
--
-- Fields may be surrounded by double quotes if they contain spaces. The
-- double quotes are not included as part of the string. Double quotes may
-- be embedded by escaping with a backslash (\"). Note that backslash is
-- only an escape character when followed by a double quote (\"); any other
-- backslashes are kept as-is (so in "c:\dir\subdir" the backslashes are
-- not escapes).
--
-- The command can be a base name (e.g. "foo"), or a name (e.g. "foo.exe"),
-- or a fully qualified path (e.g. "c:\tools\foo.exe").
--------------------------------------------------------------------------------
if not clink.oncommand or not clink.getargmatcher then
print('auto_argmatcher.lua requires a newer version of Clink; please upgrade.')
return
end
local help_parser = require('help_parser')
--------------------------------------------------------------------------------
local _config = {}
--------------------------------------------------------------------------------
local _modes = {
['--smartcase'] = { 'case', nil },
['--caseless'] = { 'case', 1 },
['--case'] = { 'case', 2 },
['--slashes'] = { 'slashes', 1 },
['--concat'] = { 'concat', true },
['--override-command'] = { 'override_command', true },
}
--------------------------------------------------------------------------------
local function explode(line)
if not line:find('"') then
return string.explode(line, ' \t')
end
local words = {}
local word = ""
local quote = false
local i = 1
while i <= #line do
local c = line:sub(i, i)
if c == '"' then
quote = not quote
elseif not quote and (c == ' ' or c == '\t') then
if #word > 0 then
table.insert(words, word)
word = ""
end
else
if quote and c == '\\' and line:sub(i + 1, i + 1) == '"' then
word = word .. '"'
i = i + 1
else
word = word .. c
end
end
i = i + 1
end
if #word > 0 then
table.insert(words, word)
end
return words
end
--------------------------------------------------------------------------------
local function load_config_file(name)
local file = io.open(name)
if not file then
return
end
local any
for line in file:lines() do
if #line == 0 or line:find('^[#;/]') then -- luacheck: ignore 542
-- Ignore the line.
else
-- First word is command, second word is flags (can be quoted).
local words = explode(line)
if words and words[1] then
words[1] = path.normalise(words[1])
local c = { command=words[1], flags=words[2], parser=words[3] }
for i = 3, #words do
local m = _modes[words[i]]
if m and m[1] then
c[m[1]] = m[2]
end
end
_config[words[1]] = c
any = true
end
end
end
file:close()
return any
end
--------------------------------------------------------------------------------
local function get_config_file(dir)
if not dir or dir == '' then
local info = debug.getinfo(1, 'S')
local src = info.source
if not src then
error('unable to detect config file location.')
return
end
src = src:gsub('^@', '')
dir = path.getdirectory(src)
end
return path.join(dir, 'auto_argmatcher.config')
end
--------------------------------------------------------------------------------
local function load_config()
_config = {}
local any
any = load_config_file(get_config_file(os.getenv('=clink.profile'))) or any -- luacheck: ignore 321
any = load_config_file(get_config_file()) or any
return any
end
--------------------------------------------------------------------------------
local function read_lines(command)
local lines = {}
if command and command ~= '' then
local r = io.popen('2>nul ' .. command)
if r then
for line in r:lines() do
if unicode.fromcodepage then
line = unicode.fromcodepage(line)
end
table.insert(lines, line)
end
r:close()
end
end
return lines
end
--------------------------------------------------------------------------------
local function build_argmatcher(entry)
-- Putting redirection first also works around the CMD problem when the
-- command line starts with a quote.
local command
if entry.override_command then
command = entry.flags
else
command = entry.command .. ' ' .. (entry.flags or '')
end
-- Choose which parser to use.
local need_init = true
-- Create a delayinit function.
local function delayinit(argmatcher)
-- Only init once.
if not need_init then
return
end
-- Capture the help text from the command.
local lines = read_lines(command)
-- Auto-detect GNU parser.
local parser = entry.parser
if not parser then
local num = #lines
for i = (num - 25 > 1) and (num - 25) or 1, num do
if lines[i]:match('^GNU ') then
parser = 'gnu'
break
end
end
end
-- Parse the help text.
help_parser.run(argmatcher, parser, lines, entry)
need_init = false
end
-- Create an argmatcher with delayinit.
clink.argmatcher(entry.command):setdelayinit(delayinit)
end
--------------------------------------------------------------------------------
local function oncommand(line_state, info)
if clink.getargmatcher(line_state) then
return
end
local file = clink.lower(info.file)
if file and file ~= "" then
local entry = _config[file] or _config[path.getname(file)] or _config[path.getbasename(file)]
if entry then
build_argmatcher(entry)
return
end
end
local command = clink.lower(path.getname(info.command))
if command and command ~= "" then
local entry = _config[command] or _config[path.getbasename(command)]
if entry then
build_argmatcher(entry)
return
end
end
end
--------------------------------------------------------------------------------
if load_config() then
clink.oncommand(oncommand)
end