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

546 lines, 293 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, value_e, 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
32from data_lang import j8_lite
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 buf.write(line.rstrip())
100 buf.write('\n ')
101 # preserve tabs
102 for c in line[:col]:
103 buf.write('\t' if c == '\t' else ' ')
104 buf.write('^')
105 buf.write('~' * (length - 1))
106 buf.write('\n')
107
108 # Do this all in a single write() call so it's less likely to be
109 # interleaved. See test/runtime-errors.sh errexit_multiple_processes
110 f.write(buf.getvalue())
111
112
113def _PrintTokenTooLong(loc_tok, f):
114 # type: (loc.TokenTooLong, mylib.Writer) -> None
115 line = loc_tok.line
116 col = loc_tok.col
117
118 buf = mylib.BufWriter()
119
120 buf.write(' ')
121 # Only print 10 characters, since it's probably very long
122 buf.write(line.content[:col + 10].rstrip())
123 buf.write('\n ')
124
125 # preserve tabs, like _PrintCodeExcerpt
126 for c in line.content[:col]:
127 buf.write('\t' if c == '\t' else ' ')
128
129 buf.write('^\n')
130
131 source_str = GetLineSourceString(loc_tok.line, quote_filename=True)
132 buf.write(
133 '%s:%d: Token starting at column %d is too long: %d bytes (%s)\n' %
134 (source_str, line.line_num, loc_tok.col, loc_tok.length,
135 Id_str(loc_tok.id)))
136
137 # single write() call
138 f.write(buf.getvalue())
139
140
141def GetLineSourceString(line, quote_filename=False):
142 # type: (SourceLine, bool) -> str
143 """Returns a human-readable string for dev tools.
144
145 This function is RECURSIVE because there may be dynamic parsing.
146 """
147 src = line.src
148 UP_src = src
149
150 with tagswitch(src) as case:
151 if case(source_e.Interactive):
152 s = '[ interactive ]' # This might need some changes
153 elif case(source_e.Headless):
154 s = '[ headless ]'
155 elif case(source_e.CFlag):
156 s = '[ -c flag ]'
157 elif case(source_e.Stdin):
158 src = cast(source.Stdin, UP_src)
159 s = '[ stdin%s ]' % src.comment
160
161 elif case(source_e.MainFile):
162 src = cast(source.MainFile, UP_src)
163 # This will quote a file called '[ -c flag ]' to disambiguate it!
164 # also handles characters that are unprintable in a terminal.
165 s = src.path
166 if quote_filename:
167 s = j8_lite.EncodeString(s, unquoted_ok=True)
168 elif case(source_e.SourcedFile):
169 src = cast(source.SourcedFile, UP_src)
170 # ditto
171 s = src.path
172 if quote_filename:
173 s = j8_lite.EncodeString(s, unquoted_ok=True)
174
175 elif case(source_e.ArgvWord):
176 src = cast(source.ArgvWord, UP_src)
177
178 # Note: _PrintWithLocation() uses this more specifically
179
180 # TODO: check loc.Missing; otherwise get Token from loc_t, then line
181 blame_tok = location.TokenFor(src.location)
182 if blame_tok is None:
183 s = '[ %s word at ? ]' % src.what
184 else:
185 line = blame_tok.line
186 line_num = line.line_num
187 outer_source = GetLineSourceString(
188 line, quote_filename=quote_filename)
189 s = '[ %s word at line %d of %s ]' % (src.what, line_num,
190 outer_source)
191
192 elif case(source_e.Variable):
193 src = cast(source.Variable, UP_src)
194
195 if src.var_name is None:
196 var_name = '?'
197 else:
198 var_name = repr(src.var_name)
199
200 if src.location.tag() == loc_e.Missing:
201 where = '?'
202 else:
203 blame_tok = location.TokenFor(src.location)
204 assert blame_tok is not None
205 line_num = blame_tok.line.line_num
206 outer_source = GetLineSourceString(
207 blame_tok.line, quote_filename=quote_filename)
208 where = 'line %d of %s' % (line_num, outer_source)
209
210 s = '[ var %s at %s ]' % (var_name, where)
211
212 elif case(source_e.VarRef):
213 src = cast(source.VarRef, UP_src)
214
215 orig_tok = src.orig_tok
216 line_num = orig_tok.line.line_num
217 outer_source = GetLineSourceString(orig_tok.line,
218 quote_filename=quote_filename)
219 where = 'line %d of %s' % (line_num, outer_source)
220
221 var_name = lexer.TokenVal(orig_tok)
222 s = '[ contents of var %r at %s ]' % (var_name, where)
223
224 elif case(source_e.Alias):
225 src = cast(source.Alias, UP_src)
226 s = '[ expansion of alias %r ]' % src.argv0
227
228 elif case(source_e.Reparsed):
229 src = cast(source.Reparsed, UP_src)
230 span2 = src.left_token
231 outer_source = GetLineSourceString(span2.line,
232 quote_filename=quote_filename)
233 s = '[ %s in %s ]' % (src.what, outer_source)
234
235 elif case(source_e.Synthetic):
236 src = cast(source.Synthetic, UP_src)
237 s = '-- %s' % src.s # use -- to say it came from a flag
238
239 else:
240 raise AssertionError(src)
241
242 return s
243
244
245def _PrintWithLocation(prefix, msg, blame_loc, show_code):
246 # type: (str, str, loc_t, bool) -> None
247 """Should we have multiple error formats:
248
249 - single line and verbose?
250 - and turn on "stack" tracing? For 'source' and more?
251 """
252 f = mylib.Stderr()
253 if blame_loc.tag() == loc_e.TokenTooLong:
254 _PrintTokenTooLong(cast(loc.TokenTooLong, blame_loc), f)
255 return
256
257 blame_tok = location.TokenFor(blame_loc)
258 if blame_tok is None: # When does this happen?
259 f.write('[??? no location ???] %s%s\n' % (prefix, msg))
260 return
261
262 orig_col = blame_tok.col
263 src = blame_tok.line.src
264 line = blame_tok.line.content
265 line_num = blame_tok.line.line_num # overwritten by source__LValue case
266
267 if show_code:
268 UP_src = src
269
270 with tagswitch(src) as case:
271 if case(source_e.Reparsed):
272 # Special case for LValue/backticks
273
274 # We want the excerpt to look like this:
275 # a[x+]=1
276 # ^
277 # Rather than quoting the internal buffer:
278 # x+
279 # ^
280
281 # Show errors:
282 # test/parse-errors.sh text-arith-context
283
284 src = cast(source.Reparsed, UP_src)
285 tok2 = src.left_token
286 line_num = tok2.line.line_num
287
288 line2 = tok2.line.content
289 lbracket_col = tok2.col + tok2.length
290 # NOTE: The inner line number is always 1 because of reparsing.
291 # We overwrite it with the original token.
292 _PrintCodeExcerpt(line2, orig_col + lbracket_col, 1, f)
293
294 elif case(source_e.ArgvWord):
295 src = cast(source.ArgvWord, UP_src)
296 # Special case for eval, unset, printf -v, etc.
297
298 # Show errors:
299 # test/runtime-errors.sh test-assoc-array
300
301 #print('OUTER blame_loc', blame_loc)
302 #print('OUTER tok', blame_tok)
303 #print('INNER src.location', src.location)
304
305 # Print code and location for MOST SPECIFIC location
306 _PrintCodeExcerpt(line, blame_tok.col, blame_tok.length, f)
307 source_str = GetLineSourceString(blame_tok.line,
308 quote_filename=True)
309 f.write('%s:%d\n' % (source_str, line_num))
310 f.write('\n')
311
312 # Now print OUTER location, with error message
313 _PrintWithLocation(prefix, msg, src.location, show_code)
314 return
315
316 else:
317 _PrintCodeExcerpt(line, blame_tok.col, blame_tok.length, f)
318
319 source_str = GetLineSourceString(blame_tok.line, quote_filename=True)
320
321 # TODO: If the line is blank, it would be nice to print the last non-blank
322 # line too?
323 f.write('%s:%d: %s%s\n' % (source_str, line_num, prefix, msg))
324
325
326class ctx_Location(object):
327
328 def __init__(self, errfmt, location):
329 # type: (ErrorFormatter, loc_t) -> None
330 errfmt.loc_stack.append(location)
331 self.errfmt = errfmt
332
333 def __enter__(self):
334 # type: () -> None
335 pass
336
337 def __exit__(self, type, value, traceback):
338 # type: (Any, Any, Any) -> None
339 self.errfmt.loc_stack.pop()
340
341
342# TODO:
343# - ColorErrorFormatter
344# - BareErrorFormatter? Could just display the foo.sh:37:8: and not quotation.
345#
346# Are these controlled by a flag? It's sort of like --comp-ui. Maybe
347# --error-ui.
348
349
350class ErrorFormatter(object):
351 """Print errors with code excerpts.
352
353 Philosophy:
354 - There should be zero or one code quotation when a shell exits non-zero.
355 Showing the same line twice is noisy.
356 - When running parallel processes, avoid interleaving multi-line code
357 quotations. (TODO: turn off in child processes?)
358 """
359
360 def __init__(self):
361 # type: () -> None
362 self.loc_stack = [] # type: List[loc_t]
363 self.one_line_errexit = False # root process
364
365 def OneLineErrExit(self):
366 # type: () -> None
367 """Unused now.
368
369 For SubprogramThunk.
370 """
371 self.one_line_errexit = True
372
373 # A stack used for the current builtin. A fallback for UsageError.
374 # TODO: Should we have PushBuiltinName? Then we can have a consistent style
375 # like foo.sh:1: (compopt) Not currently executing.
376 def _FallbackLocation(self, blame_loc):
377 # type: (Optional[loc_t]) -> loc_t
378 if blame_loc is None or blame_loc.tag() == loc_e.Missing:
379 if len(self.loc_stack):
380 return self.loc_stack[-1]
381 return loc.Missing
382
383 return blame_loc
384
385 def PrefixPrint(self, msg, prefix, blame_loc):
386 # type: (str, str, loc_t) -> None
387 """Print a hard-coded message with a prefix, and quote code."""
388 _PrintWithLocation(prefix,
389 msg,
390 self._FallbackLocation(blame_loc),
391 show_code=True)
392
393 def Print_(self, msg, blame_loc=None):
394 # type: (str, loc_t) -> None
395 """Print message and quote code."""
396 _PrintWithLocation('',
397 msg,
398 self._FallbackLocation(blame_loc),
399 show_code=True)
400
401 def PrintMessage(self, msg, blame_loc=None):
402 # type: (str, loc_t) -> None
403 """Print a message WITHOUT quoting code."""
404 _PrintWithLocation('',
405 msg,
406 self._FallbackLocation(blame_loc),
407 show_code=False)
408
409 def StderrLine(self, msg):
410 # type: (str) -> None
411 """Just print to stderr."""
412 print_stderr(msg)
413
414 def PrettyPrintError(self, err, prefix=''):
415 # type: (_ErrorWithLocation, str) -> None
416 """Print an exception that was caught, with a code quotation.
417
418 Unlike other methods, this doesn't use the GetLocationForLine()
419 fallback. That only applies to builtins; instead we check
420 e.HasLocation() at a higher level, in CommandEvaluator.
421 """
422 # TODO: Should there be a special span_id of 0 for EOF? runtime.NO_SPID
423 # means there is no location info, but 0 could mean that the location is EOF.
424 # So then you query the arena for the last line in that case?
425 # Eof_Real is the ONLY token with 0 span, because it's invisible!
426 # Well Eol_Tok is a sentinel with span_id == runtime.NO_SPID. I think that
427 # is OK.
428 # Problem: the column for Eof could be useful.
429
430 _PrintWithLocation(prefix, err.UserErrorString(), err.location, True)
431
432 def PrintErrExit(self, err, pid):
433 # type: (error.ErrExit, int) -> None
434
435 # TODO:
436 # - Don't quote code if you already quoted something on the same line?
437 # - _PrintWithLocation calculates the line_id. So you need to remember that?
438 # - return it here?
439 prefix = 'errexit PID %d: ' % pid
440 _PrintWithLocation(prefix, err.UserErrorString(), err.location,
441 err.show_code)
442
443
444def PrintAst(node, flag):
445 # type: (command_t, arg_types.main) -> None
446
447 if flag.ast_format == 'none':
448 print_stderr('AST not printed.')
449 if 0:
450 from _devbuild.gen.id_kind_asdl import Id_str
451 from frontend.lexer import ID_HIST, LAZY_ID_HIST
452
453 print(LAZY_ID_HIST)
454 print(len(LAZY_ID_HIST))
455
456 for id_, count in ID_HIST.most_common(10):
457 print('%8d %s' % (count, Id_str(id_)))
458 print()
459 total = sum(ID_HIST.values())
460 uniq = len(ID_HIST)
461 print('%8d total tokens' % total)
462 print('%8d unique tokens IDs' % uniq)
463 print()
464
465 for id_, count in LAZY_ID_HIST.most_common(10):
466 print('%8d %s' % (count, Id_str(id_)))
467 print()
468 total = sum(LAZY_ID_HIST.values())
469 uniq = len(LAZY_ID_HIST)
470 print('%8d total tokens' % total)
471 print('%8d tokens with LazyVal()' % total)
472 print('%8d unique tokens IDs' % uniq)
473 print()
474
475 if 0:
476 from osh.word_parse import WORD_HIST
477 #print(WORD_HIST)
478 for desc, count in WORD_HIST.most_common(20):
479 print('%8d %s' % (count, desc))
480
481 else: # text output
482 f = mylib.Stdout()
483
484 afmt = flag.ast_format # note: mycpp rewrite to avoid 'in'
485 if afmt in ('text', 'abbrev-text'):
486 ast_f = fmt.DetectConsoleOutput(f)
487 elif afmt in ('html', 'abbrev-html'):
488 ast_f = fmt.HtmlOutput(f)
489 else:
490 raise AssertionError()
491
492 if 'abbrev-' in afmt:
493 # ASDL "abbreviations" are only supported by asdl/gen_python.py
494 if mylib.PYTHON:
495 tree = node.AbbreviatedTree()
496 else:
497 tree = node.PrettyTree()
498 else:
499 tree = node.PrettyTree()
500
501 ast_f.FileHeader()
502 fmt.PrintTree(tree, ast_f)
503 ast_f.FileFooter()
504 ast_f.write('\n')
505
506
507def PrettyPrintValue(val, f):
508 # type: (value_t, mylib.Writer) -> None
509 """For the = keyword"""
510
511 ysh_type = ValType(val)
512 id_str = j8.ValueIdString(val)
513
514 UP_val = val
515 with tagswitch(val) as case:
516 # "JSON" data types will use J8 serialization
517 if case(value_e.Null, value_e.Bool, value_e.Int, value_e.Float,
518 value_e.Str, value_e.List, value_e.Dict):
519 # Use () instead of <> as a hint that it's a "JSON value"
520 f.write('(%s%s) ' % (ysh_type, id_str))
521
522 # Unused STUB
523 doc = pretty.FromValue(val)
524
525 buf = mylib.BufWriter()
526
527 # TODO: Wrap lines, and show color. Use core/ansi.py
528 p = j8.InstancePrinter(buf, -1, j8.SHOW_CYCLES | j8.SHOW_NON_DATA)
529
530 # error.Encode should be impossible - we show cycles and non-data
531 p.Print(val)
532
533 f.write(buf.getvalue())
534 f.write('\n')
535
536 elif case(value_e.Range):
537 val = cast(value.Range, UP_val)
538
539 # Printing Range values more nicely. Note that pp line (x) doesn't
540 # have this.
541 f.write('(%s) %d .. %d\n' % (ysh_type, val.lower, val.upper))
542
543 else:
544 # Just print object and ID. Use <> to show that it's more like
545 # a reference type.
546 f.write('<%s%s>\n' % (ysh_type, id_str))