tool_context: do not need an attribute bash_completion
[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 init
206 do
207 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)
208 end
209
210 # Name, usage and synopsis of the tool.
211 # It is mainly used in `usage`.
212 # Should be correctly set by the client before calling `process_options`
213 # A multi-line string is recommmended.
214 #
215 # eg. `"Usage: tool [OPTION]... [FILE]...\nDo some things."`
216 var tooldescription: String writable = "Usage: [OPTION]... [ARG]..."
217
218 # Does `process_options` should accept an empty sequence of arguments.
219 # ie. nothing except options.
220 # Is `false` by default.
221 #
222 # If required, if should be set by the client before calling `process_options`
223 var accept_no_arguments writable = false
224
225 # print the full usage of the tool.
226 # Is called by `process_option` on `--help`.
227 # It also could be called by the client.
228 fun usage
229 do
230 print tooldescription
231 option_context.usage
232 end
233
234 # Parse and process the options given on the command line
235 fun process_options(args: Sequence[String])
236 do
237 self.opt_warn.value = 1
238
239 # init options
240 option_context.parse(args)
241
242 if opt_help.value then
243 usage
244 exit 0
245 end
246
247 if opt_version.value then
248 print version
249 exit 0
250 end
251
252 if opt_bash_completion.value then
253 var bash_completion = new BashCompletion(self)
254 bash_completion.write_to(sys.stdout)
255 exit 0
256 end
257
258 var errors = option_context.get_errors
259 if not errors.is_empty then
260 for e in errors do print "Error: {e}"
261 print tooldescription
262 print "Use --help for help"
263 exit 1
264 end
265
266 if option_context.rest.is_empty and not accept_no_arguments then
267 print tooldescription
268 print "Use --help for help"
269 exit 1
270 end
271
272 # Set verbose level
273 verbose_level = opt_verbose.value
274
275 if self.opt_quiet.value then self.opt_warn.value = 0
276
277 if opt_log_dir.value != null then log_directory = opt_log_dir.value.as(not null)
278 if opt_log.value then
279 # Make sure the output directory exists
280 log_directory.mkdir
281 end
282
283 nit_dir = compute_nit_dir
284 end
285
286 # Get the current `nit_version` or "DUMMY_VERSION" if `--set-dummy-tool` is set.
287 fun version: String do
288 if opt_set_dummy_tool.value then
289 return "DUMMY_VERSION"
290 end
291 return nit_version
292 end
293
294 # Get the name of the tool or "DUMMY_TOOL" id `--set-dummy-tool` is set.
295 fun toolname: String do
296 if opt_set_dummy_tool.value then
297 return "DUMMY_TOOL"
298 end
299 return sys.program_name.basename("")
300 end
301
302 # The identified root directory of the Nit project
303 var nit_dir: nullable String
304
305 private fun compute_nit_dir: nullable String
306 do
307 # a environ variable has precedence
308 var res = "NIT_DIR".environ
309 if not res.is_empty then return res
310
311 # find the runpath of the program from argv[0]
312 res = "{sys.program_name.dirname}/.."
313 if res.file_exists and "{res}/src/nit.nit".file_exists then return res.simplify_path
314
315 # find the runpath of the process from /proc
316 var exe = "/proc/self/exe"
317 if exe.file_exists then
318 res = exe.realpath
319 res = res.dirname.join_path("..")
320 if res.file_exists and "{res}/src/nit.nit".file_exists then return res.simplify_path
321 end
322
323 return null
324 end
325 end
326
327 # This class generates a compatible `bash_completion` script file.
328 #
329 # On some Linux systems `bash_completion` allow the program to control command line behaviour.
330 #
331 # $ nitls [TAB][TAB]
332 # file1.nit file2.nit file3.nit
333 #
334 # $ nitls --[TAB][TAB]
335 # --bash-toolname --keep --path --tree
336 # --depends --log --project --verbose
337 # --disable-phase --log-dir --quiet --version
338 # --gen-bash-completion --no-color --recursive --warn
339 # --help --only-metamodel --source
340 # --ignore-visibility --only-parse --stop-on-first-error
341 #
342 # Generated file must be placed in system bash_completion directory `/etc/bash_completion.d/`
343 # or in the user directory `~/.bash_completion`.
344 class BashCompletion
345 super Template
346
347 var toolcontext: ToolContext
348
349 init(toolcontext: ToolContext) do
350 self.toolcontext = toolcontext
351 end
352
353 private fun extract_options_names: Array[String] do
354 var names = new Array[String]
355 for option in toolcontext.option_context.options do
356 for name in option.names do
357 if name.has_prefix("--") then names.add name
358 end
359 end
360 return names
361 end
362
363 redef fun rendering do
364 var name = toolcontext.toolname
365 var option_names = extract_options_names
366 addn "# generated bash completion file for {name} {toolcontext.version}"
367 addn "_{name}()"
368 addn "\{"
369 addn " local cur prev opts"
370 addn " COMPREPLY=()"
371 addn " cur=\"$\{COMP_WORDS[COMP_CWORD]\}\""
372 addn " prev=\"$\{COMP_WORDS[COMP_CWORD-1]\}\""
373 if option_names != null then
374 addn " opts=\"{option_names.join(" ")}\""
375 addn " if [[ $\{cur\} == -* ]] ; then"
376 addn " COMPREPLY=( $(compgen -W \"$\{opts\}\" -- $\{cur\}) )"
377 addn " return 0"
378 addn " fi"
379 end
380 addn "\} &&"
381 addn "complete -o default -F _{name} {name}"
382 end
383 end