OILS / core / ui.py View on Github | oilshell.org

537 lines, 287 significant
1# Copyright 2016 Andy Chu. All rights reserved.
2# Licensed under the Apache License, Version 2.0 (the "License");
3# you may not use this file except in compliance with the License.
4# You may obtain a copy of the License at
5#
6# http://www.apache.org/licenses/LICENSE-2.0
7"""
8ui.py - User interface constructs.
9"""
10from __future__ import print_function
11
12from _devbuild.gen.id_kind_asdl import Id, Id_t, Id_str
13from _devbuild.gen.syntax_asdl import (
14 Token,
15 SourceLine,
16 loc,
17 loc_e,
18 loc_t,
19 command_t,
20 command_str,
21 source,
22 source_e,
23)
24from _devbuild.gen.value_asdl import (value_t, value_str)
25from asdl import format as fmt
26from data_lang import pretty
27from frontend import lexer
28from frontend import location
29from mycpp import mylib
30from mycpp.mylib import print_stderr, tagswitch, log
31from data_lang import j8_lite
32import libc
33
34from typing import List, Optional, Any, cast, TYPE_CHECKING
35if TYPE_CHECKING:
36 from _devbuild.gen import arg_types
37 from core import error
38 from core.error import _ErrorWithLocation
39
40_ = log
41
42
43def ValType(val):
44 # type: (value_t) -> str
45 """For displaying type errors in the UI."""
46
47 return value_str(val.tag(), dot=False)
48
49
50def CommandType(cmd):
51 # type: (command_t) -> str
52 """For displaying commands in the UI."""
53
54 # Displays 'command.Simple' for now, maybe change it.
55 return command_str(cmd.tag())
56
57
58def PrettyId(id_):
59 # type: (Id_t) -> str
60 """For displaying type errors in the UI."""
61
62 # Displays 'Id.BoolUnary_v' for now
63 return Id_str(id_)
64
65
66def PrettyToken(tok):
67 # type: (Token) -> str
68 """Returns a readable token value for the user.
69
70 For syntax errors.
71 """
72 if tok.id == Id.Eof_Real:
73 return 'EOF'
74
75 val = tok.line.content[tok.col:tok.col + tok.length]
76 # TODO: Print length 0 as 'EOF'?
77 return repr(val)
78
79
80def PrettyDir(dir_name, home_dir):
81 # type: (str, Optional[str]) -> str
82 """Maybe replace the home dir with ~.
83
84 Used by the 'dirs' builtin and the prompt evaluator.
85 """
86 if home_dir is not None:
87 if dir_name == home_dir or dir_name.startswith(home_dir + '/'):
88 return '~' + dir_name[len(home_dir):]
89
90 return dir_name
91
92
93def _PrintCodeExcerpt(line, col, length, f):
94 # type: (str, int, int, mylib.Writer) -> None
95
96 buf = mylib.BufWriter()
97
98 buf.write(' ')
99
100 # TODO: Be smart about horizontal space when printing code snippet
101 # - Accept max_width param, which is terminal width or perhaps 100
102 # when there's no terminal
103 # - If 'length' of token is greater than max_width, then perhaps print 10
104 # chars on each side
105 # - If len(line) is less than max_width, then print everything normally
106 # - If len(line) is greater than max_width, then print up to max_width
107 # but make sure to include the entire token, with some context
108 # Print > < or ... to show truncation
109 #
110 # ^col 80 ^~~~~ error
111
112 buf.write(line.rstrip())
113
114 buf.write('\n ')
115 # preserve tabs
116 for c in line[:col]:
117 buf.write('\t' if c == '\t' else ' ')
118 buf.write('^')
119 buf.write('~' * (length - 1))
120 buf.write('\n')
121
122 # Do this all in a single write() call so it's less likely to be
123 # interleaved. See test/runtime-errors.sh errexit_multiple_processes
124 f.write(buf.getvalue())
125
126
127def _PrintTokenTooLong(loc_tok, f):
128 # type: (loc.TokenTooLong, mylib.Writer) -> None
129 line = loc_tok.line
130 col = loc_tok.col
131
132 buf = mylib.BufWriter()
133
134 buf.write(' ')
135 # Only print 10 characters, since it's probably very long
136 buf.write(line.content[:col + 10].rstrip())
137 buf.write('\n ')
138
139 # preserve tabs, like _PrintCodeExcerpt
140 for c in line.content[:col]:
141 buf.write('\t' if c == '\t' else ' ')
142
143 buf.write('^\n')
144
145 source_str = GetLineSourceString(loc_tok.line, quote_filename=True)
146 buf.write(
147 '%s:%d: Token starting at column %d is too long: %d bytes (%s)\n' %
148 (source_str, line.line_num, loc_tok.col, loc_tok.length,
149 Id_str(loc_tok.id)))
150
151 # single write() call
152 f.write(buf.getvalue())
153
154
155def GetLineSourceString(line, quote_filename=False):
156 # type: (SourceLine, bool) -> str
157 """Returns a human-readable string for dev tools.
158
159 This function is RECURSIVE because there may be dynamic parsing.
160 """
161 src = line.src
162 UP_src = src
163
164 with tagswitch(src) as case:
165 if case(source_e.Interactive):
166 s = '[ interactive ]' # This might need some changes
167 elif case(source_e.Headless):
168 s = '[ headless ]'
169 elif case(source_e.CFlag):
170 s = '[ -c flag ]'
171 elif case(source_e.Stdin):
172 src = cast(source.Stdin, UP_src)
173 s = '[ stdin%s ]' % src.comment
174
175 elif case(source_e.MainFile):
176 src = cast(source.MainFile, UP_src)
177 # This will quote a file called '[ -c flag ]' to disambiguate it!
178 # also handles characters that are unprintable in a terminal.
179 s = src.path
180 if quote_filename:
181 s = j8_lite.EncodeString(s, unquoted_ok=True)
182 elif case(source_e.SourcedFile):
183 src = cast(source.SourcedFile, UP_src)
184 # ditto
185 s = src.path
186 if quote_filename:
187 s = j8_lite.EncodeString(s, unquoted_ok=True)
188
189 elif case(source_e.ArgvWord):
190 src = cast(source.ArgvWord, UP_src)
191
192 # Note: _PrintWithLocation() uses this more specifically
193
194 # TODO: check loc.Missing; otherwise get Token from loc_t, then line
195 blame_tok = location.TokenFor(src.location)
196 if blame_tok is None:
197 s = '[ %s word at ? ]' % src.what
198 else:
199 line = blame_tok.line
200 line_num = line.line_num
201 outer_source = GetLineSourceString(
202 line, quote_filename=quote_filename)
203 s = '[ %s word at line %d of %s ]' % (src.what, line_num,
204 outer_source)
205
206 elif case(source_e.Variable):
207 src = cast(source.Variable, UP_src)
208
209 if src.var_name is None:
210 var_name = '?'
211 else:
212 var_name = repr(src.var_name)
213
214 if src.location.tag() == loc_e.Missing:
215 where = '?'
216 else:
217 blame_tok = location.TokenFor(src.location)
218 assert blame_tok is not None
219 line_num = blame_tok.line.line_num
220 outer_source = GetLineSourceString(
221 blame_tok.line, quote_filename=quote_filename)
222 where = 'line %d of %s' % (line_num, outer_source)
223
224 s = '[ var %s at %s ]' % (var_name, where)
225
226 elif case(source_e.VarRef):
227 src = cast(source.VarRef, UP_src)
228
229 orig_tok = src.orig_tok
230 line_num = orig_tok.line.line_num
231 outer_source = GetLineSourceString(orig_tok.line,
232 quote_filename=quote_filename)
233 where = 'line %d of %s' % (line_num, outer_source)
234
235 var_name = lexer.TokenVal(orig_tok)
236 s = '[ contents of var %r at %s ]' % (var_name, where)
237
238 elif case(source_e.Alias):
239 src = cast(source.Alias, UP_src)
240 s = '[ expansion of alias %r ]' % src.argv0
241
242 elif case(source_e.Reparsed):
243 src = cast(source.Reparsed, UP_src)
244 span2 = src.left_token
245 outer_source = GetLineSourceString(span2.line,
246 quote_filename=quote_filename)
247 s = '[ %s in %s ]' % (src.what, outer_source)
248
249 elif case(source_e.Synthetic):
250 src = cast(source.Synthetic, UP_src)
251 s = '-- %s' % src.s # use -- to say it came from a flag
252
253 else:
254 raise AssertionError(src)
255
256 return s
257
258
259def _PrintWithLocation(prefix, msg, blame_loc, show_code):
260 # type: (str, str, loc_t, bool) -> None
261 """Should we have multiple error formats:
262
263 - single line and verbose?
264 - and turn on "stack" tracing? For 'source' and more?
265 """
266 f = mylib.Stderr()
267 if blame_loc.tag() == loc_e.TokenTooLong:
268 _PrintTokenTooLong(cast(loc.TokenTooLong, blame_loc), f)
269 return
270
271 blame_tok = location.TokenFor(blame_loc)
272 if blame_tok is None: # When does this happen?
273 f.write('[??? no location ???] %s%s\n' % (prefix, msg))
274 return
275
276 orig_col = blame_tok.col
277 src = blame_tok.line.src
278 line = blame_tok.line.content
279 line_num = blame_tok.line.line_num # overwritten by source__LValue case
280
281 if show_code:
282 UP_src = src
283
284 with tagswitch(src) as case:
285 if case(source_e.Reparsed):
286 # Special case for LValue/backticks
287
288 # We want the excerpt to look like this:
289 # a[x+]=1
290 # ^
291 # Rather than quoting the internal buffer:
292 # x+
293 # ^
294
295 # Show errors:
296 # test/parse-errors.sh text-arith-context
297
298 src = cast(source.Reparsed, UP_src)
299 tok2 = src.left_token
300 line_num = tok2.line.line_num
301
302 line2 = tok2.line.content
303 lbracket_col = tok2.col + tok2.length
304 # NOTE: The inner line number is always 1 because of reparsing.
305 # We overwrite it with the original token.
306 _PrintCodeExcerpt(line2, orig_col + lbracket_col, 1, f)
307
308 elif case(source_e.ArgvWord):
309 src = cast(source.ArgvWord, UP_src)
310 # Special case for eval, unset, printf -v, etc.
311
312 # Show errors:
313 # test/runtime-errors.sh test-assoc-array
314
315 #print('OUTER blame_loc', blame_loc)
316 #print('OUTER tok', blame_tok)
317 #print('INNER src.location', src.location)
318
319 # Print code and location for MOST SPECIFIC location
320 _PrintCodeExcerpt(line, blame_tok.col, blame_tok.length, f)
321 source_str = GetLineSourceString(blame_tok.line,
322 quote_filename=True)
323 f.write('%s:%d\n' % (source_str, line_num))
324 f.write('\n')
325
326 # Now print OUTER location, with error message
327 _PrintWithLocation(prefix, msg, src.location, show_code)
328 return
329
330 else:
331 _PrintCodeExcerpt(line, blame_tok.col, blame_tok.length, f)
332
333 source_str = GetLineSourceString(blame_tok.line, quote_filename=True)
334
335 # TODO: If the line is blank, it would be nice to print the last non-blank
336 # line too?
337 f.write('%s:%d: %s%s\n' % (source_str, line_num, prefix, msg))
338
339
340class ctx_Location(object):
341
342 def __init__(self, errfmt, location):
343 # type: (ErrorFormatter, loc_t) -> None
344 errfmt.loc_stack.append(location)
345 self.errfmt = errfmt
346
347 def __enter__(self):
348 # type: () -> None
349 pass
350
351 def __exit__(self, type, value, traceback):
352 # type: (Any, Any, Any) -> None
353 self.errfmt.loc_stack.pop()
354
355
356# TODO:
357# - ColorErrorFormatter
358# - BareErrorFormatter? Could just display the foo.sh:37:8: and not quotation.
359#
360# Are these controlled by a flag? It's sort of like --comp-ui. Maybe
361# --error-ui.
362
363
364class ErrorFormatter(object):
365 """Print errors with code excerpts.
366
367 Philosophy:
368 - There should be zero or one code quotation when a shell exits non-zero.
369 Showing the same line twice is noisy.
370 - When running parallel processes, avoid interleaving multi-line code
371 quotations. (TODO: turn off in child processes?)
372 """
373
374 def __init__(self):
375 # type: () -> None
376 self.loc_stack = [] # type: List[loc_t]
377 self.one_line_errexit = False # root process
378
379 def OneLineErrExit(self):
380 # type: () -> None
381 """Unused now.
382
383 For SubprogramThunk.
384 """
385 self.one_line_errexit = True
386
387 # A stack used for the current builtin. A fallback for UsageError.
388 # TODO: Should we have PushBuiltinName? Then we can have a consistent style
389 # like foo.sh:1: (compopt) Not currently executing.
390 def _FallbackLocation(self, blame_loc):
391 # type: (Optional[loc_t]) -> loc_t
392 if blame_loc is None or blame_loc.tag() == loc_e.Missing:
393 if len(self.loc_stack):
394 return self.loc_stack[-1]
395 return loc.Missing
396
397 return blame_loc
398
399 def PrefixPrint(self, msg, prefix, blame_loc):
400 # type: (str, str, loc_t) -> None
401 """Print a hard-coded message with a prefix, and quote code."""
402 _PrintWithLocation(prefix,
403 msg,
404 self._FallbackLocation(blame_loc),
405 show_code=True)
406
407 def Print_(self, msg, blame_loc=None):
408 # type: (str, loc_t) -> None
409 """Print message and quote code."""
410 _PrintWithLocation('',
411 msg,
412 self._FallbackLocation(blame_loc),
413 show_code=True)
414
415 def PrintMessage(self, msg, blame_loc=None):
416 # type: (str, loc_t) -> None
417 """Print a message WITHOUT quoting code."""
418 _PrintWithLocation('',
419 msg,
420 self._FallbackLocation(blame_loc),
421 show_code=False)
422
423 def StderrLine(self, msg):
424 # type: (str) -> None
425 """Just print to stderr."""
426 print_stderr(msg)
427
428 def PrettyPrintError(self, err, prefix=''):
429 # type: (_ErrorWithLocation, str) -> None
430 """Print an exception that was caught, with a code quotation.
431
432 Unlike other methods, this doesn't use the GetLocationForLine()
433 fallback. That only applies to builtins; instead we check
434 e.HasLocation() at a higher level, in CommandEvaluator.
435 """
436 # TODO: Should there be a special span_id of 0 for EOF? runtime.NO_SPID
437 # means there is no location info, but 0 could mean that the location is EOF.
438 # So then you query the arena for the last line in that case?
439 # Eof_Real is the ONLY token with 0 span, because it's invisible!
440 # Well Eol_Tok is a sentinel with span_id == runtime.NO_SPID. I think that
441 # is OK.
442 # Problem: the column for Eof could be useful.
443
444 _PrintWithLocation(prefix, err.UserErrorString(), err.location, True)
445
446 def PrintErrExit(self, err, pid):
447 # type: (error.ErrExit, int) -> None
448
449 # TODO:
450 # - Don't quote code if you already quoted something on the same line?
451 # - _PrintWithLocation calculates the line_id. So you need to remember that?
452 # - return it here?
453 prefix = 'errexit PID %d: ' % pid
454 _PrintWithLocation(prefix, err.UserErrorString(), err.location,
455 err.show_code)
456
457
458def PrintAst(node, flag):
459 # type: (command_t, arg_types.main) -> None
460
461 if flag.ast_format == 'none':
462 print_stderr('AST not printed.')
463 if 0:
464 from _devbuild.gen.id_kind_asdl import Id_str
465 from frontend.lexer import ID_HIST, LAZY_ID_HIST
466
467 print(LAZY_ID_HIST)
468 print(len(LAZY_ID_HIST))
469
470 for id_, count in ID_HIST.most_common(10):
471 print('%8d %s' % (count, Id_str(id_)))
472 print()
473 total = sum(ID_HIST.values())
474 uniq = len(ID_HIST)
475 print('%8d total tokens' % total)
476 print('%8d unique tokens IDs' % uniq)
477 print()
478
479 for id_, count in LAZY_ID_HIST.most_common(10):
480 print('%8d %s' % (count, Id_str(id_)))
481 print()
482 total = sum(LAZY_ID_HIST.values())
483 uniq = len(LAZY_ID_HIST)
484 print('%8d total tokens' % total)
485 print('%8d tokens with LazyVal()' % total)
486 print('%8d unique tokens IDs' % uniq)
487 print()
488
489 if 0:
490 from osh.word_parse import WORD_HIST
491 #print(WORD_HIST)
492 for desc, count in WORD_HIST.most_common(20):
493 print('%8d %s' % (count, desc))
494
495 else: # text output
496 f = mylib.Stdout()
497
498 afmt = flag.ast_format # note: mycpp rewrite to avoid 'in'
499 if afmt in ('text', 'abbrev-text'):
500 ast_f = fmt.DetectConsoleOutput(f)
501 elif afmt in ('html', 'abbrev-html'):
502 ast_f = fmt.HtmlOutput(f)
503 else:
504 raise AssertionError()
505
506 if 'abbrev-' in afmt:
507 # ASDL "abbreviations" are only supported by asdl/gen_python.py
508 if mylib.PYTHON:
509 tree = node.AbbreviatedTree()
510 else:
511 tree = node.PrettyTree()
512 else:
513 tree = node.PrettyTree()
514
515 ast_f.FileHeader()
516 fmt.PrintTree(tree, ast_f)
517 ast_f.FileFooter()
518 ast_f.write('\n')
519
520
521def PrettyPrintValue(val, f):
522 # type: (value_t, mylib.Writer) -> None
523 """For the = keyword"""
524
525 printer = pretty.PrettyPrinter()
526 printer.SetUseStyles(f.isatty())
527 try:
528 width = libc.get_terminal_width()
529 if width > 0:
530 printer.SetMaxWidth(width)
531 except (IOError, OSError):
532 pass
533
534 buf = mylib.BufWriter()
535 printer.PrintValue(val, buf)
536 f.write(buf.getvalue())
537 f.write('\n')