tool_context: correct default for bashcompletion
[nit.git] / src / toolcontext.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Copyright 2006-2008 Floréal Morandat <morandat@lirmm.fr>
4 # Copyright 2008-2012 Jean Privat <jean@pryen.org>
5 # Copyright 2009 Jean-Sebastien Gelinas <calestar@gmail.com>
6 # Copyright 2014 Alexandre Terrasa <alexandre@moz-code.org>
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20 # Common command-line tool infractructure than handle options and error messages
21 module toolcontext
22
23 import opts
24 import location
25 import version
26 import template
27
28 class Message
29 super Comparable
30 redef type OTHER: Message
31
32 var location: nullable Location
33 var text: String
34
35 # Comparisons are made on message locations.
36 redef fun <(other: OTHER): Bool do
37 if location == null then return true
38 if other.location == null then return false
39
40 return location.as(not null) < other.location.as(not null)
41 end
42
43 redef fun to_s: String
44 do
45 var l = location
46 if l == null then
47 return text
48 else
49 return "{l}: {text}"
50 end
51 end
52
53 fun to_color_string: String
54 do
55 var esc = 27.ascii
56 var red = "{esc}[0;31m"
57 var bred = "{esc}[1;31m"
58 var green = "{esc}[0;32m"
59 var yellow = "{esc}[0;33m"
60 var def = "{esc}[0m"
61
62 var l = location
63 if l == null then
64 return text
65 else if l.file == null then
66 return "{yellow}{l}{def}: {text}"
67 else
68 return "{yellow}{l}{def}: {text}\n{l.colored_line("1;31")}"
69 end
70 end
71 end
72
73 # Global context for tools
74 class ToolContext
75 # Number of errors
76 var error_count: Int = 0
77
78 # Number of warnings
79 var warning_count: Int = 0
80
81 # Directory where to generate log files
82 var log_directory: String = "logs"
83
84 # Messages
85 private var messages: Array[Message] = new Array[Message]
86 private var message_sorter: ComparableSorter[Message] = new ComparableSorter[Message]
87
88 fun check_errors
89 do
90 if messages.length > 0 then
91 message_sorter.sort(messages)
92
93 for m in messages do
94 if opt_no_color.value then
95 sys.stderr.write("{m}\n")
96 else
97 sys.stderr.write("{m.to_color_string}\n")
98 end
99 end
100
101 messages.clear
102 end
103
104 if error_count > 0 then exit(1)
105 end
106
107 # Display an error
108 fun error(l: nullable Location, s: String)
109 do
110 messages.add(new Message(l,s))
111 error_count = error_count + 1
112 if opt_stop_on_first_error.value then check_errors
113 end
114
115 # Add an error, show errors and quit
116 fun fatal_error(l: nullable Location, s: String)
117 do
118 error(l,s)
119 check_errors
120 end
121
122 # Display a warning
123 fun warning(l: nullable Location, s: String)
124 do
125 if opt_warn.value == 0 then return
126 messages.add(new Message(l,s))
127 warning_count = warning_count + 1
128 if opt_stop_on_first_error.value then check_errors
129 end
130
131 # Display an info
132 fun info(s: String, level: Int)
133 do
134 if level <= verbose_level then
135 print "{s}"
136 end
137 end
138
139 # Executes a program while checking if it's available and if the execution ended correctly
140 #
141 # Stops execution and prints errors if the program isn't available or didn't end correctly
142 fun exec_and_check(args: Array[String], error: String)
143 do
144 var prog = args.first
145 args.remove_at 0
146
147 # Is the wanted program available?
148 var proc_which = new IProcess.from_a("which", [prog])
149 proc_which.wait
150 var res = proc_which.status
151 if res != 0 then
152 print "{error}: executable \"{prog}\" not found"
153 exit 1
154 end
155
156 # Execute the wanted program
157 var proc = new Process.from_a(prog, args)
158 proc.wait
159 res = proc.status
160 if res != 0 then
161 print "{error}: execution of \"{prog} {args.join(" ")}\" failed"
162 exit 1
163 end
164 end
165
166 # Global OptionContext
167 var option_context: OptionContext = new OptionContext
168
169 # Option --warn
170 var opt_warn: OptionCount = new OptionCount("Show warnings", "-W", "--warn")
171
172 # Option --quiet
173 var opt_quiet: OptionBool = new OptionBool("Do not show warnings", "-q", "--quiet")
174
175 # Option --log
176 var opt_log: OptionBool = new OptionBool("Generate various log files", "--log")
177
178 # Option --log-dir
179 var opt_log_dir: OptionString = new OptionString("Directory where to generate log files", "--log-dir")
180
181 # Option --help
182 var opt_help: OptionBool = new OptionBool("Show Help (This screen)", "-h", "-?", "--help")
183
184 # Option --version
185 var opt_version: OptionBool = new OptionBool("Show version and exit", "--version")
186
187 # Option --set-dummy-tool
188 var opt_set_dummy_tool: OptionBool = new OptionBool("Set toolname and version to DUMMY. Useful for testing", "--set-dummy-tool")
189
190 # Option --verbose
191 var opt_verbose: OptionCount = new OptionCount("Verbose", "-v", "--verbose")
192
193 # Option --stop-on-first-error
194 var opt_stop_on_first_error: OptionBool = new OptionBool("Stop on first error", "--stop-on-first-error")
195
196 # Option --no-color
197 var opt_no_color: OptionBool = new OptionBool("Do not use color to display errors and warnings", "--no-color")
198
199 # Option --bash-completion
200 var opt_bash_completion: OptionBool = new OptionBool("Generate bash_completion file for this program", "--bash-completion")
201
202 # Verbose level
203 var verbose_level: Int = 0
204
205 # Bash completion behavior in command line
206 # see `BashCompletion`
207 var bash_completion: BashCompletion
208
209 init
210 do
211 bash_completion = new BashCompletion(self)
212 option_context.add_option(opt_warn, opt_quiet, opt_stop_on_first_error, opt_no_color, opt_log, opt_log_dir, opt_help, opt_version, opt_set_dummy_tool, opt_verbose, opt_bash_completion)
213 end
214
215 # Name, usage and synopsis of the tool.
216 # It is mainly used in `usage`.
217 # Should be correctly set by the client before calling `process_options`
218 # A multi-line string is recommmended.
219 #
220 # eg. `"Usage: tool [OPTION]... [FILE]...\nDo some things."`
221 var tooldescription: String writable = "Usage: [OPTION]... [ARG]..."
222
223 # Does `process_options` should accept an empty sequence of arguments.
224 # ie. nothing except options.
225 # Is `false` by default.
226 #
227 # If required, if should be set by the client before calling `process_options`
228 var accept_no_arguments writable = false
229
230 # print the full usage of the tool.
231 # Is called by `process_option` on `--help`.
232 # It also could be called by the client.
233 fun usage
234 do
235 print tooldescription
236 option_context.usage
237 end
238
239 # Parse and process the options given on the command line
240 fun process_options(args: Sequence[String])
241 do
242 self.opt_warn.value = 1
243
244 # init options
245 option_context.parse(args)
246
247 if opt_help.value then
248 usage
249 exit 0
250 end
251
252 if opt_version.value then
253 print version
254 exit 0
255 end
256
257 if opt_bash_completion.value then
258 print bash_completion.write_to_string
259 bash_completion.write_to_file("{sys.program_name}.bash")
260 exit 0
261 end
262
263 var errors = option_context.get_errors
264 if not errors.is_empty then
265 for e in errors do print "Error: {e}"
266 print tooldescription
267 print "Use --help for help"
268 exit 1
269 end
270
271 if option_context.rest.is_empty and not accept_no_arguments then
272 print tooldescription
273 print "Use --help for help"
274 exit 1
275 end
276
277 # Set verbose level
278 verbose_level = opt_verbose.value
279
280 if self.opt_quiet.value then self.opt_warn.value = 0
281
282 if opt_log_dir.value != null then log_directory = opt_log_dir.value.as(not null)
283 if opt_log.value then
284 # Make sure the output directory exists
285 log_directory.mkdir
286 end
287
288 nit_dir = compute_nit_dir
289 end
290
291 # Get the current `nit_version` or "DUMMY_VERSION" if `--set-dummy-tool` is set.
292 fun version: String do
293 if opt_set_dummy_tool.value then
294 return "DUMMY_VERSION"
295 end
296 return nit_version
297 end
298
299 # Get the name of the tool or "DUMMY_TOOL" id `--set-dummy-tool` is set.
300 fun toolname: String do
301 if opt_set_dummy_tool.value then
302 return "DUMMY_TOOL"
303 end
304 return sys.program_name.basename("")
305 end
306
307 # The identified root directory of the Nit project
308 var nit_dir: nullable String
309
310 private fun compute_nit_dir: nullable String
311 do
312 # a environ variable has precedence
313 var res = "NIT_DIR".environ
314 if not res.is_empty then return res
315
316 # find the runpath of the program from argv[0]
317 res = "{sys.program_name.dirname}/.."
318 if res.file_exists and "{res}/src/nit.nit".file_exists then return res.simplify_path
319
320 # find the runpath of the process from /proc
321 var exe = "/proc/self/exe"
322 if exe.file_exists then
323 res = exe.realpath
324 res = res.dirname.join_path("..")
325 if res.file_exists and "{res}/src/nit.nit".file_exists then return res.simplify_path
326 end
327
328 return null
329 end
330 end
331
332 # This class generates a compatible `bash_completion` script file.
333 #
334 # On some Linux systems `bash_completion` allow the program to control command line behaviour.
335 #
336 # $ nitls [TAB][TAB]
337 # file1.nit file2.nit file3.nit
338 #
339 # $ nitls --[TAB][TAB]
340 # --bash-toolname --keep --path --tree
341 # --depends --log --project --verbose
342 # --disable-phase --log-dir --quiet --version
343 # --gen-bash-completion --no-color --recursive --warn
344 # --help --only-metamodel --source
345 # --ignore-visibility --only-parse --stop-on-first-error
346 #
347 # Generated file must be placed in system bash_completion directory `/etc/bash_completion.d/`
348 # or in the user directory `~/.bash_completion`.
349 class BashCompletion
350 super Template
351
352 var toolcontext: ToolContext
353
354 init(toolcontext: ToolContext) do
355 self.toolcontext = toolcontext
356 end
357
358 private fun extract_options_names: Array[String] do
359 var names = new Array[String]
360 for option in toolcontext.option_context.options do
361 for name in option.names do
362 if name.has_prefix("--") then names.add name
363 end
364 end
365 return names
366 end
367
368 redef fun rendering do
369 var name = toolcontext.toolname
370 var option_names = extract_options_names
371 addn "# generated bash completion file for {name} {toolcontext.version}"
372 addn "_{name}()"
373 addn "\{"
374 addn " local cur prev opts"
375 addn " COMPREPLY=()"
376 addn " cur=\"$\{COMP_WORDS[COMP_CWORD]\}\""
377 addn " prev=\"$\{COMP_WORDS[COMP_CWORD-1]\}\""
378 if option_names != null then
379 addn " opts=\"{option_names.join(" ")}\""
380 addn " if [[ $\{cur\} == -* ]] ; then"
381 addn " COMPREPLY=( $(compgen -W \"$\{opts\}\" -- $\{cur\}) )"
382 addn " return 0"
383 addn " fi"
384 end
385 addn "\} &&"
386 addn "complete -o default -F _{name} {name}"
387 end
388 end