tool_context: just print the bash_completion, let the caller do what it want
[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 bash_completion.write_to(sys.stdout)
259 exit 0
260 end
261
262 var errors = option_context.get_errors
263 if not errors.is_empty then
264 for e in errors do print "Error: {e}"
265 print tooldescription
266 print "Use --help for help"
267 exit 1
268 end
269
270 if option_context.rest.is_empty and not accept_no_arguments then
271 print tooldescription
272 print "Use --help for help"
273 exit 1
274 end
275
276 # Set verbose level
277 verbose_level = opt_verbose.value
278
279 if self.opt_quiet.value then self.opt_warn.value = 0
280
281 if opt_log_dir.value != null then log_directory = opt_log_dir.value.as(not null)
282 if opt_log.value then
283 # Make sure the output directory exists
284 log_directory.mkdir
285 end
286
287 nit_dir = compute_nit_dir
288 end
289
290 # Get the current `nit_version` or "DUMMY_VERSION" if `--set-dummy-tool` is set.
291 fun version: String do
292 if opt_set_dummy_tool.value then
293 return "DUMMY_VERSION"
294 end
295 return nit_version
296 end
297
298 # Get the name of the tool or "DUMMY_TOOL" id `--set-dummy-tool` is set.
299 fun toolname: String do
300 if opt_set_dummy_tool.value then
301 return "DUMMY_TOOL"
302 end
303 return sys.program_name.basename("")
304 end
305
306 # The identified root directory of the Nit project
307 var nit_dir: nullable String
308
309 private fun compute_nit_dir: nullable String
310 do
311 # a environ variable has precedence
312 var res = "NIT_DIR".environ
313 if not res.is_empty then return res
314
315 # find the runpath of the program from argv[0]
316 res = "{sys.program_name.dirname}/.."
317 if res.file_exists and "{res}/src/nit.nit".file_exists then return res.simplify_path
318
319 # find the runpath of the process from /proc
320 var exe = "/proc/self/exe"
321 if exe.file_exists then
322 res = exe.realpath
323 res = res.dirname.join_path("..")
324 if res.file_exists and "{res}/src/nit.nit".file_exists then return res.simplify_path
325 end
326
327 return null
328 end
329 end
330
331 # This class generates a compatible `bash_completion` script file.
332 #
333 # On some Linux systems `bash_completion` allow the program to control command line behaviour.
334 #
335 # $ nitls [TAB][TAB]
336 # file1.nit file2.nit file3.nit
337 #
338 # $ nitls --[TAB][TAB]
339 # --bash-toolname --keep --path --tree
340 # --depends --log --project --verbose
341 # --disable-phase --log-dir --quiet --version
342 # --gen-bash-completion --no-color --recursive --warn
343 # --help --only-metamodel --source
344 # --ignore-visibility --only-parse --stop-on-first-error
345 #
346 # Generated file must be placed in system bash_completion directory `/etc/bash_completion.d/`
347 # or in the user directory `~/.bash_completion`.
348 class BashCompletion
349 super Template
350
351 var toolcontext: ToolContext
352
353 init(toolcontext: ToolContext) do
354 self.toolcontext = toolcontext
355 end
356
357 private fun extract_options_names: Array[String] do
358 var names = new Array[String]
359 for option in toolcontext.option_context.options do
360 for name in option.names do
361 if name.has_prefix("--") then names.add name
362 end
363 end
364 return names
365 end
366
367 redef fun rendering do
368 var name = toolcontext.toolname
369 var option_names = extract_options_names
370 addn "# generated bash completion file for {name} {toolcontext.version}"
371 addn "_{name}()"
372 addn "\{"
373 addn " local cur prev opts"
374 addn " COMPREPLY=()"
375 addn " cur=\"$\{COMP_WORDS[COMP_CWORD]\}\""
376 addn " prev=\"$\{COMP_WORDS[COMP_CWORD-1]\}\""
377 if option_names != null then
378 addn " opts=\"{option_names.join(" ")}\""
379 addn " if [[ $\{cur\} == -* ]] ; then"
380 addn " COMPREPLY=( $(compgen -W \"$\{opts\}\" -- $\{cur\}) )"
381 addn " return 0"
382 addn " fi"
383 end
384 addn "\} &&"
385 addn "complete -o default -F _{name} {name}"
386 end
387 end