OILS / builtin / meta_oils.py View on Github | oils.pub

1020 lines, 608 significant
1#!/usr/bin/env python2
2"""
3meta_oils.py - Builtins that call back into the interpreter, or reflect on it.
4
5OSH builtins:
6 builtin command type
7 source eval
8
9YSH builtins:
10 invoke extern
11 use
12"""
13from __future__ import print_function
14
15from _devbuild.gen import arg_types
16from _devbuild.gen.runtime_asdl import cmd_value, CommandStatus
17from _devbuild.gen.syntax_asdl import source, loc, loc_t, CompoundWord
18from _devbuild.gen.value_asdl import Obj, value, value_t
19from core import alloc
20from core import dev
21from core import error
22from core.error import e_usage
23from core import executor
24from core import main_loop
25from core import process
26from core import pyutil # strerror
27from core import state
28from core import vm
29from data_lang import j8_lite
30from display import ui
31from frontend import consts
32from frontend import flag_util
33from frontend import reader
34from mycpp.mylib import log, print_stderr, NewDict
35from pylib import os_path
36from osh import cmd_eval
37
38import libc
39
40_ = log
41
42from typing import Dict, List, Tuple, Optional, TYPE_CHECKING
43if TYPE_CHECKING:
44 from frontend import args
45 from frontend.parse_lib import ParseContext
46 from core import optview
47 from display import ui
48 from mycpp import mylib
49 from osh.cmd_eval import CommandEvaluator
50 from osh import cmd_parse
51
52
53class Eval(vm._Builtin):
54
55 def __init__(
56 self,
57 parse_ctx, # type: ParseContext
58 exec_opts, # type: optview.Exec
59 cmd_ev, # type: CommandEvaluator
60 tracer, # type: dev.Tracer
61 errfmt, # type: ui.ErrorFormatter
62 mem, # type: state.Mem
63 ):
64 # type: (...) -> None
65 self.parse_ctx = parse_ctx
66 self.arena = parse_ctx.arena
67 self.exec_opts = exec_opts
68 self.cmd_ev = cmd_ev
69 self.tracer = tracer
70 self.errfmt = errfmt
71 self.mem = mem
72
73 def Run(self, cmd_val):
74 # type: (cmd_value.Argv) -> int
75
76 # There are no flags, but we need it to respect --
77 _, arg_r = flag_util.ParseCmdVal('eval', cmd_val)
78
79 if self.exec_opts.simple_eval_builtin():
80 code_str, eval_loc = arg_r.ReadRequired2('requires code string')
81 if not arg_r.AtEnd():
82 e_usage('requires exactly 1 argument', loc.Missing)
83 else:
84 code_str = ' '.join(arg_r.Rest())
85 # code_str could be EMPTY, so just use the first one
86 eval_loc = cmd_val.arg_locs[0]
87
88 line_reader = reader.StringLineReader(code_str, self.arena)
89 c_parser = self.parse_ctx.MakeOshParser(line_reader)
90
91 src = source.Dynamic('eval arg', eval_loc)
92 with dev.ctx_Tracer(self.tracer, 'eval', None):
93 with state.ctx_CompoundWordDebugFrame(self.mem, eval_loc):
94 with alloc.ctx_SourceCode(self.arena, src):
95 return main_loop.Batch(self.cmd_ev,
96 c_parser,
97 self.errfmt,
98 cmd_flags=cmd_eval.RaiseControlFlow)
99
100
101def _VarName(module_path):
102 # type: (str) -> str
103 """Convert ///path/foo-bar.ysh -> foo_bar
104
105 Design issue: proc vs. func naming conventinos imply treating hyphens
106 differently.
107
108 foo-bar myproc
109 var x = `foo-bar`.myproc
110
111 I guess use this for now:
112
113 foo_bar myproc
114 var x = foo_bar.myproc
115
116 The user can also choose this:
117
118 fooBar myproc
119 var x = fooBar.myproc
120 """
121 basename = os_path.basename(module_path)
122 i = basename.rfind('.')
123 if i != -1:
124 basename = basename[:i]
125 #return basename.replace('-', '_')
126 return basename
127
128
129class ShellFile(vm._Builtin):
130 """
131 These share code:
132 - 'source' builtin for OSH
133 - 'use' builtin for YSH
134 """
135
136 def __init__(
137 self,
138 parse_ctx, # type: ParseContext
139 search_path, # type: executor.SearchPath
140 cmd_ev, # type: CommandEvaluator
141 fd_state, # type: process.FdState
142 tracer, # type: dev.Tracer
143 errfmt, # type: ui.ErrorFormatter
144 loader, # type: pyutil._ResourceLoader
145 module_invoke=None, # type: vm._Builtin
146 ):
147 # type: (...) -> None
148 """
149 If module_invoke is passed, this class behaves like 'use'. Otherwise
150 it behaves like 'source'.
151 """
152 self.parse_ctx = parse_ctx
153 self.arena = parse_ctx.arena
154 self.search_path = search_path
155 self.cmd_ev = cmd_ev
156 self.fd_state = fd_state
157 self.tracer = tracer
158 self.errfmt = errfmt
159 self.loader = loader
160 self.module_invoke = module_invoke
161
162 self.builtin_name = 'use' if module_invoke else 'source'
163 self.mem = cmd_ev.mem
164
165 # Don't load modules more than once
166 # keyed by libc.realpath(arg)
167 self._disk_cache = {} # type: Dict[str, Obj]
168
169 # keyed by ///
170 self._embed_cache = {} # type: Dict[str, Obj]
171
172 def Run(self, cmd_val):
173 # type: (cmd_value.Argv) -> int
174 if self.module_invoke:
175 return self._Use(cmd_val)
176 else:
177 return self._Source(cmd_val)
178
179 def LoadEmbeddedFile(self, embed_path, blame_loc):
180 # type: (str, loc_t) -> Tuple[str, cmd_parse.CommandParser]
181 try:
182 load_path = os_path.join("stdlib", embed_path)
183 contents = self.loader.Get(load_path)
184 except (IOError, OSError):
185 self.errfmt.Print_('%r failed: No builtin file %r' %
186 (self.builtin_name, load_path),
187 blame_loc=blame_loc)
188 return None, None # error
189
190 line_reader = reader.StringLineReader(contents, self.arena)
191 c_parser = self.parse_ctx.MakeOshParser(line_reader)
192 return load_path, c_parser
193
194 def _LoadDiskFile(self, fs_path, blame_loc):
195 # type: (str, loc_t) -> Tuple[mylib.LineReader, cmd_parse.CommandParser]
196 try:
197 # Shell can't use descriptors 3-9
198 f = self.fd_state.Open(fs_path)
199 except (IOError, OSError) as e:
200 self.errfmt.Print_(
201 '%s %r failed: %s' %
202 (self.builtin_name, fs_path, pyutil.strerror(e)),
203 blame_loc=blame_loc)
204 return None, None
205
206 line_reader = reader.FileLineReader(f, self.arena)
207 c_parser = self.parse_ctx.MakeOshParser(line_reader)
208 return f, c_parser
209
210 def _SourceExec(self, cmd_val, arg_r, path, c_parser):
211 # type: (cmd_value.Argv, args.Reader, str, cmd_parse.CommandParser) -> int
212 call_loc = cmd_val.arg_locs[0]
213
214 # A sourced module CAN have a new arguments array, but it always shares
215 # the same variable scope as the caller. The caller could be at either a
216 # global or a local scope.
217
218 # TODO: I wonder if we compose the enter/exit methods more easily.
219
220 with dev.ctx_Tracer(self.tracer, 'source', cmd_val.argv):
221 source_argv = arg_r.Rest()
222 with state.ctx_Source(self.mem, path, source_argv, call_loc):
223 with state.ctx_ThisDir(self.mem, path):
224 src = source.OtherFile(path, call_loc)
225 with alloc.ctx_SourceCode(self.arena, src):
226 try:
227 status = main_loop.Batch(
228 self.cmd_ev,
229 c_parser,
230 self.errfmt,
231 cmd_flags=cmd_eval.RaiseControlFlow)
232 except vm.IntControlFlow as e:
233 if e.IsReturn():
234 status = e.StatusCode()
235 else:
236 raise
237
238 return status
239
240 def _NewModule(self):
241 # type: () -> Obj
242 # Builtin proc that serves as __invoke__ - it looks up procs in 'self'
243 methods = Obj(None,
244 {'__invoke__': value.BuiltinProc(self.module_invoke)})
245 props = NewDict() # type: Dict[str, value_t]
246 module_obj = Obj(methods, props)
247 return module_obj
248
249 def _UseExec(
250 self,
251 cmd_val, # type: cmd_value.Argv
252 path, # type: str
253 path_loc, # type: loc_t
254 c_parser, # type: cmd_parse.CommandParser
255 props, # type: Dict[str, value_t]
256 ):
257 # type: (...) -> int
258 """
259 Args:
260 props: is mutated, and will contain module properties
261 """
262 error_strs = [] # type: List[str]
263
264 with dev.ctx_Tracer(self.tracer, 'use', cmd_val.argv):
265 with state.ctx_ModuleEval(self.mem, cmd_val.arg_locs[0], props,
266 error_strs):
267 with state.ctx_ThisDir(self.mem, path):
268 src = source.OtherFile(path, path_loc)
269 with alloc.ctx_SourceCode(self.arena, src):
270 try:
271 status = main_loop.Batch(
272 self.cmd_ev,
273 c_parser,
274 self.errfmt,
275 cmd_flags=cmd_eval.RaiseControlFlow)
276 except vm.IntControlFlow as e:
277 if e.IsReturn():
278 status = e.StatusCode()
279 else:
280 raise
281 if status != 0:
282 return status
283 #e_die("'use' failed 2", path_loc)
284
285 if len(error_strs):
286 for s in error_strs:
287 self.errfmt.PrintMessage('Error: %s' % s, path_loc)
288 return 1
289
290 return 0
291
292 def _Source(self, cmd_val):
293 # type: (cmd_value.Argv) -> int
294 attrs, arg_r = flag_util.ParseCmdVal('source', cmd_val)
295 arg = arg_types.source(attrs.attrs)
296
297 path_arg, path_loc = arg_r.ReadRequired2('requires a file path')
298
299 # Old:
300 # source --builtin two.sh # looks up stdlib/two.sh
301 # New:
302 # source $LIB_OSH/two.sh # looks up stdlib/osh/two.sh
303 # source ///osh/two.sh # looks up stdlib/osh/two.sh
304 embed_path = None # type: Optional[str]
305 if arg.builtin:
306 embed_path = path_arg
307 elif path_arg.startswith('///'):
308 embed_path = path_arg[3:]
309
310 if embed_path is not None:
311 load_path, c_parser = self.LoadEmbeddedFile(embed_path, path_loc)
312 if c_parser is None:
313 return 1 # error was already shown
314
315 return self._SourceExec(cmd_val, arg_r, load_path, c_parser)
316
317 else:
318 # 'source' respects $PATH
319 resolved = self.search_path.LookupOne(path_arg,
320 exec_required=False)
321 if resolved is None:
322 resolved = path_arg
323
324 f, c_parser = self._LoadDiskFile(resolved, path_loc)
325 if c_parser is None:
326 return 1 # error was already shown
327
328 with process.ctx_FileCloser(f):
329 return self._SourceExec(cmd_val, arg_r, path_arg, c_parser)
330
331 raise AssertionError()
332
333 def _BindNames(self, module_obj, module_name, pick_names, pick_locs):
334 # type: (Obj, str, Optional[List[str]], Optional[List[CompoundWord]]) -> int
335 state.SetGlobalValue(self.mem, module_name, module_obj)
336
337 if pick_names is None:
338 return 0
339
340 for i, name in enumerate(pick_names):
341 val = module_obj.d.get(name)
342 # ctx_ModuleEval ensures this
343 if val is None:
344 # note: could be more precise
345 self.errfmt.Print_("use: module doesn't provide name %r" %
346 name,
347 blame_loc=pick_locs[i])
348 return 1
349 state.SetGlobalValue(self.mem, name, val)
350 return 0
351
352 def _Use(self, cmd_val):
353 # type: (cmd_value.Argv) -> int
354 """
355 Module system with all the power of Python, but still a proc
356
357 use util.ysh # util is a value.Obj
358
359 # Importing a bunch of words
360 use dialect-ninja.ysh --all-provided
361 use dialect-github.ysh --all-provided
362
363 # This declares some names
364 use --extern grep sed
365
366 # Renaming
367 use util.ysh (&myutil)
368
369 # Ignore
370 use util.ysh (&_)
371
372 # Picking specifics
373 use util.ysh --names log die
374
375 # Rename
376 var mylog = log
377 """
378 attrs, arg_r = flag_util.ParseCmdVal('use', cmd_val)
379 arg = arg_types.use(attrs.attrs)
380
381 # Accepts any args
382 if arg.extern_: # use --extern grep # no-op for static analysis
383 return 0
384
385 path_arg, path_loc = arg_r.ReadRequired2('requires a module path')
386
387 pick_names = None # type: Optional[List[str]]
388 pick_locs = None # type: Optional[List[CompoundWord]]
389
390 # There is only one flag
391 flag, flag_loc = arg_r.Peek2()
392 if flag is not None:
393 if flag == '--pick':
394 arg_r.Next()
395 p = arg_r.Peek()
396 if p is None:
397 raise error.Usage('with --pick expects one or more names',
398 flag_loc)
399 pick_names, pick_locs = arg_r.Rest2()
400
401 elif flag == '--all-provided':
402 arg_r.Next()
403 arg_r.Done()
404 print('TODO: --all-provided not implemented')
405
406 elif flag == '--all-for-testing':
407 arg_r.Next()
408 arg_r.Done()
409 print('TODO: --all-for testing not implemented')
410
411 else:
412 raise error.Usage(
413 'expected flag like --pick after module path', flag_loc)
414
415 # Similar logic as 'source'
416 if path_arg.startswith('///'):
417 embed_path = path_arg[3:]
418 else:
419 embed_path = None
420
421 if self.mem.InsideFunction():
422 raise error.Usage("may only be used at the top level", path_loc)
423
424 # Important, consider:
425 # use symlink.ysh # where symlink.ysh -> realfile.ysh
426 #
427 # Then the cache key would be '/some/path/realfile.ysh'
428 # But the variable name bound is 'symlink'
429 var_name = _VarName(path_arg)
430 #log('var %s', var_name)
431
432 if embed_path is not None:
433 # Embedded modules are cached using /// path as cache key
434 cached_obj = self._embed_cache.get(embed_path)
435 if cached_obj:
436 return self._BindNames(cached_obj, var_name, pick_names,
437 pick_locs)
438
439 load_path, c_parser = self.LoadEmbeddedFile(embed_path, path_loc)
440 if c_parser is None:
441 return 1 # error was already shown
442
443 module_obj = self._NewModule()
444
445 # Cache BEFORE executing, to prevent circular import
446 self._embed_cache[embed_path] = module_obj
447
448 status = self._UseExec(cmd_val, load_path, path_loc, c_parser,
449 module_obj.d)
450 if status != 0:
451 return status
452
453 return self._BindNames(module_obj, var_name, pick_names, pick_locs)
454
455 else:
456 normalized = libc.realpath(path_arg)
457 if normalized is None:
458 self.errfmt.Print_("use: couldn't find %r" % path_arg,
459 blame_loc=path_loc)
460 return 1
461
462 # Disk modules are cached using normalized path as cache key
463 cached_obj = self._disk_cache.get(normalized)
464 if cached_obj:
465 return self._BindNames(cached_obj, var_name, pick_names,
466 pick_locs)
467
468 f, c_parser = self._LoadDiskFile(normalized, path_loc)
469 if c_parser is None:
470 return 1 # error was already shown
471
472 module_obj = self._NewModule()
473
474 # Cache BEFORE executing, to prevent circular import
475 self._disk_cache[normalized] = module_obj
476
477 with process.ctx_FileCloser(f):
478 status = self._UseExec(cmd_val, path_arg, path_loc, c_parser,
479 module_obj.d)
480 if status != 0:
481 return status
482
483 return self._BindNames(module_obj, var_name, pick_names, pick_locs)
484
485 return 0
486
487
488def _PrintFreeForm(row):
489 # type: (Tuple[str, str, Optional[str]]) -> None
490 """For type builtin and command builtin"""
491
492 name, kind, detail = row
493
494 if kind == 'file':
495 what = detail # file path
496 elif kind == 'alias':
497 what = ('an alias for %s' %
498 j8_lite.EncodeString(detail, unquoted_ok=True))
499 elif kind == 'proc':
500 # "a YSH proc" - for regular procs
501 # "a YSH invokable" (object)
502 what = 'a YSH %s' % (detail if detail is not None else kind)
503 elif kind == 'builtin':
504 if detail is None:
505 prefix = ''
506 else:
507 prefix = detail + ' '
508 what = 'a %sshell %s' % (prefix, kind)
509 elif kind in ('keyword', 'function'):
510 # OSH prints 'shell function' instead of 'function', to distinguish
511 # from YSH func
512 what = 'a shell %s' % kind
513 else:
514 raise AssertionError()
515
516 print('%s is %s' % (name, what))
517
518
519# 8 space gutter column
520# 12 spaces for name
521# 12 for 'kind' - builtin proc extern
522_TABLE_ROW_FMT = '%-8s%-12s%-12s%s'
523
524
525def _PrintTableRow(row):
526 # type: (Tuple[str, str, Optional[str]]) -> None
527 name, kind, detail = row
528
529 # Ideas for printing
530 #
531 # I guess the path always s
532 #
533 # time alias
534 # time 'special builtin'
535 # time /usr/bin/time
536 # time '/home/dir with spaces/time'
537 #
538 # TODO: Might want to create an enum:
539 # first_word =
540 # Alias(str body)
541 # | Keyword
542 # | Extern(str path)
543 # | Builtin(kind k)
544 # | ShellFunction
545 # | Proc
546 # | Invokable
547
548 name_row = j8_lite.EncodeString(name, unquoted_ok=True)
549
550 kind_row = kind
551 if kind == 'function':
552 # Consistent with --sh-func
553 kind_row = 'sh-func'
554 elif kind == 'file':
555 # Consistent with --extern
556 kind_row = 'extern'
557
558 detail_row = (j8_lite.EncodeString(detail, unquoted_ok=True)
559 if detail is not None else '-')
560
561 # 20 cols for "private builtin"
562 print(_TABLE_ROW_FMT % ('', name_row, kind_row, detail_row))
563
564
565class Command(vm._Builtin):
566 """'command ls' suppresses function lookup."""
567
568 def __init__(
569 self,
570 shell_ex, # type: vm._Executor
571 procs, # type: state.Procs
572 aliases, # type: Dict[str, str]
573 search_path, # type: executor.SearchPath
574 ):
575 # type: (...) -> None
576 self.shell_ex = shell_ex
577 self.procs = procs
578 self.aliases = aliases
579 self.search_path = search_path
580
581 def Run(self, cmd_val):
582 # type: (cmd_value.Argv) -> int
583
584 # accept_typed_args=True because we invoke other builtins
585 attrs, arg_r = flag_util.ParseCmdVal('command',
586 cmd_val,
587 accept_typed_args=True)
588 arg = arg_types.command(attrs.attrs)
589
590 argv, locs = arg_r.Rest2()
591
592 if arg.v or arg.V:
593 status = 0
594 for argument in argv:
595 r = _ResolveName(argument, self.procs, self.aliases,
596 self.search_path, False)
597 if len(r):
598 # Print only the first occurrence.
599 # TODO: it would be nice to short-circuit the lookups
600 row = r[0]
601 if arg.v:
602 name, kind, detail = row
603 if kind == 'file':
604 print(detail) # /usr/bin/awk
605 else:
606 print(name) # myfunc
607 else:
608 _PrintFreeForm(row)
609 else:
610 # match bash behavior by printing to stderr
611 print_stderr('%s: not found' % argument)
612 status = 1 # nothing printed, but we fail
613
614 return status
615
616 cmd_val2 = cmd_value.Argv(argv, locs, cmd_val.is_last_cmd,
617 cmd_val.self_obj, cmd_val.proc_args)
618
619 cmd_st = CommandStatus.CreateNull(alloc_lists=True)
620
621 # If we respected do_fork here instead of passing DO_FORK
622 # unconditionally, the case 'command date | wc -l' would take 2
623 # processes instead of 3. See test/syscall
624 run_flags = executor.NO_CALL_PROCS
625 if cmd_val.is_last_cmd:
626 run_flags |= executor.IS_LAST_CMD
627 if arg.p:
628 run_flags |= executor.USE_DEFAULT_PATH
629
630 return self.shell_ex.RunSimpleCommand(cmd_val2, cmd_st, run_flags)
631
632
633def _ShiftArgv(cmd_val):
634 # type: (cmd_value.Argv) -> cmd_value.Argv
635 return cmd_value.Argv(cmd_val.argv[1:], cmd_val.arg_locs[1:],
636 cmd_val.is_last_cmd, cmd_val.self_obj,
637 cmd_val.proc_args)
638
639
640class Builtin(vm._Builtin):
641
642 def __init__(self, shell_ex, errfmt):
643 # type: (vm._Executor, ui.ErrorFormatter) -> None
644 self.shell_ex = shell_ex
645 self.errfmt = errfmt
646
647 def Run(self, cmd_val):
648 # type: (cmd_value.Argv) -> int
649 attrs, arg_r = flag_util.ParseCmdVal('builtin',
650 cmd_val,
651 accept_typed_args=True)
652 argv, locs = arg_r.Rest2()
653
654 if len(argv) == 0:
655 return 0 # this could be an error in strict mode?
656
657 name = argv[0]
658
659 to_run = _LookupAnyBuiltin(name)
660
661 if to_run == consts.NO_INDEX: # error
662 location = locs[0]
663 if consts.LookupAssignBuiltin(name) != consts.NO_INDEX:
664 # NOTE: core/executor.py has a similar restriction for 'command'
665 self.errfmt.Print_("'builtin' can't run assignment builtin",
666 blame_loc=location)
667 else:
668 self.errfmt.Print_("%r isn't a shell builtin" % name,
669 blame_loc=location)
670 return 1
671
672 cmd_val2 = _ShiftArgv(cmd_val)
673 return self.shell_ex.RunBuiltin(to_run, cmd_val2)
674
675
676class RunProc(vm._Builtin):
677
678 def __init__(self, shell_ex, procs, errfmt):
679 # type: (vm._Executor, state.Procs, ui.ErrorFormatter) -> None
680 self.shell_ex = shell_ex
681 self.procs = procs
682 self.errfmt = errfmt
683
684 def Run(self, cmd_val):
685 # type: (cmd_value.Argv) -> int
686 _, arg_r = flag_util.ParseCmdVal('runproc',
687 cmd_val,
688 accept_typed_args=True)
689 argv, locs = arg_r.Rest2()
690
691 if len(argv) == 0:
692 raise error.Usage('requires arguments', cmd_val.arg_locs[0])
693
694 name = argv[0]
695 proc, _ = self.procs.GetInvokable(name)
696 if not proc:
697 self.errfmt.Print_('runproc: no invokable named %r' % name,
698 locs[0])
699 return 1
700
701 cmd_val2 = cmd_value.Argv(argv, locs, cmd_val.is_last_cmd,
702 cmd_val.self_obj, cmd_val.proc_args)
703
704 # NOTE: Don't need cmd_st?
705 cmd_st = CommandStatus.CreateNull(alloc_lists=True)
706 run_flags = executor.IS_LAST_CMD if cmd_val.is_last_cmd else 0
707 return self.shell_ex.RunSimpleCommand(cmd_val2, cmd_st, run_flags)
708
709
710def _LookupAnyBuiltin(name):
711 # type: (str) -> int
712 to_run = consts.LookupNormalBuiltin(name) # builtin true
713
714 if to_run == consts.NO_INDEX: # builtin eval
715 to_run = consts.LookupSpecialBuiltin(name)
716
717 if to_run == consts.NO_INDEX: # builtin sleep
718 # Note that plain 'sleep' doesn't work
719 to_run = consts.LookupPrivateBuiltin(name)
720
721 return to_run
722
723
724class Invoke(vm._Builtin):
725 """Invoke a command, controlling the resolution of the first word."""
726
727 def __init__(
728 self,
729 shell_ex, # type: vm._Executor
730 procs, # type: state.Procs
731 aliases, # type: Dict[str, str]
732 search_path, # type: executor.SearchPath
733 errfmt, # type: ui.ErrorFormatter
734 ):
735 # type: (...) -> None
736 self.shell_ex = shell_ex
737 self.procs = procs
738 self.aliases = aliases
739 self.search_path = search_path
740 self.errfmt = errfmt
741
742 def Run(self, cmd_val):
743 # type: (cmd_value.Argv) -> int
744 attrs, arg_r = flag_util.ParseCmdVal('invoke',
745 cmd_val,
746 accept_typed_args=True)
747 arg = arg_types.invoke(attrs.attrs)
748 argv, locs = arg_r.Rest2()
749
750 if len(argv) == 0:
751 raise error.Usage('expected arguments', cmd_val.arg_locs[0])
752
753 if arg.show:
754 # she-dot of .qtt8
755 print(_TABLE_ROW_FMT % ('#.qtt8', 'name', 'kind', 'detail'))
756 for name in argv:
757 r = _ResolveName(name,
758 self.procs,
759 self.aliases,
760 self.search_path,
761 True,
762 do_private=True)
763 if len(r):
764 for row in r:
765 _PrintTableRow(row)
766 else:
767 # - means not found
768 _PrintTableRow((name, '-', '-'))
769 return 0
770
771 cmd_val2 = cmd_value.Argv(argv, locs, cmd_val.is_last_cmd,
772 cmd_val.self_obj, cmd_val.proc_args)
773
774 name = argv[0]
775 location = locs[0]
776
777 tried = [] # type: List[str]
778
779 # Look it up
780 if arg.proc:
781 tried.append('--proc')
782 proc_val, self_obj = self.procs.GetProc(name)
783 if proc_val is not None:
784 return self.shell_ex._RunInvokable(proc_val, self_obj,
785 location, cmd_val2)
786
787 if arg.sh_func:
788 tried.append('--sh-func')
789 sh_func = self.procs.GetShellFunc(name)
790 if sh_func:
791 return self.shell_ex._RunInvokable(sh_func, None, location,
792 cmd_val2)
793
794 if arg.builtin:
795 tried.append('--builtin')
796 # Look up any builtin
797
798 to_run = _LookupAnyBuiltin(name)
799 if to_run != consts.NO_INDEX:
800 # early return
801 return self.shell_ex.RunBuiltin(to_run, cmd_val2)
802
803 # Special considerations:
804 # - set PATH for lookup
805 # - set ENV for external process with a DICT
806 # - I think these are better for the 'extern' builtin
807 if arg.extern_:
808 tried.append('--extern')
809 # cmd_st is None - should we get rid of it? See 'runproc' too
810 return self.shell_ex.RunExternal(name, location, cmd_val2, None, 0)
811
812 if len(tried) == 0:
813 raise error.Usage(
814 'expected one or more flags like --proc --sh-func --builtin --extern',
815 cmd_val.arg_locs[0])
816
817 # Command not found
818 self.errfmt.Print_("'invoke' couldn't find command %r (tried %s)" %
819 (name, ' '.join(tried)),
820 blame_loc=location)
821 return 127
822
823
824class Extern(vm._Builtin):
825 """
826 Why does this exist?
827 - Run with ENV in a dict, similar to env -i, but possibly easier
828 - Run with $PATH
829 """
830
831 def __init__(self, shell_ex, procs, errfmt):
832 # type: (vm._Executor, state.Procs, ui.ErrorFormatter) -> None
833 self.shell_ex = shell_ex
834 self.procs = procs
835 self.errfmt = errfmt
836
837 def Run(self, cmd_val):
838 # type: (cmd_value.Argv) -> int
839 _, arg_r = flag_util.ParseCmdVal('extern',
840 cmd_val,
841 accept_typed_args=True)
842 #argv, locs = arg_r.Rest2()
843
844 print('TODO: extern')
845
846 return 0
847
848
849def _ResolveName(
850 name, # type: str
851 procs, # type: state.Procs
852 aliases, # type: Dict[str, str]
853 search_path, # type: executor.SearchPath
854 do_all, # type: bool
855 do_private=False, # type: bool
856):
857 # type: (...) -> List[Tuple[str, str, Optional[str]]]
858 """
859 Returns:
860 A list of (name, type, optional arg)
861
862 When type == 'alias', arg is the expansion text
863 When type == 'file', arg is the path
864 When type == 'builtin', arg is 'special', 'private', or None
865 When type == 'proc', arg is 'invokable' or None
866
867 POSIX has these rules:
868 https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_09_01_01
869
870 a. special builtins (eval :)
871 b. undefined builtins (complete whence)
872 c. shell function
873 d. list of builtins (alias command) - could include th undefined ones
874 e. external
875
876 Our ShellExecutor uses this order, wich is compatible:
877 1. special builtins
878 2. shell functions
879 3. normal builtins
880 4. external
881 """
882 # MyPy tuple type
883 no_str = None # type: Optional[str]
884
885 results = [] # type: List[Tuple[str, str, Optional[str]]]
886
887 # Aliases can redefine keywords
888 if name in aliases:
889 results.append((name, 'alias', aliases[name]))
890
891 # Keywords come before simple commands
892 if consts.IsControlFlow(name): # continue, etc.
893 results.append((name, 'keyword', no_str))
894 elif consts.IsKeyword(name):
895 results.append((name, 'keyword', no_str))
896
897 # Simple commands are looked up in this order by ShellExecutor:
898 #
899 # 1. special builtins
900 # 2. procs
901 # 3. shell functions - see state.Procs, shell functions come second
902 # 4. normal builtins
903 # 5. external commands
904 #
905 # These require the 'builtin' prefix:
906 # 6. private builtins
907
908 # Special builtins are looked up FIRST
909 if consts.LookupSpecialBuiltin(name) != 0:
910 results.append((name, 'builtin', 'special'))
911
912 if procs:
913 if procs.IsProc(name):
914 results.append((name, 'proc', no_str))
915 elif procs.IsInvokableObj(name): # can't be both proc and obj
916 results.append((name, 'proc', 'invokable'))
917
918 if procs.IsShellFunc(name): # shell functions AFTER procs
919 results.append((name, 'function', no_str))
920
921 # See if it's a builtin
922 if consts.LookupNormalBuiltin(name) != 0:
923 results.append((name, 'builtin', no_str))
924 elif consts.LookupAssignBuiltin(name) != 0:
925 results.append((name, 'builtin', 'special'))
926
927 # See if it's external
928 for path in search_path.LookupReflect(name, do_all):
929 results.append((name, 'file', path))
930
931 # Private builtins after externals
932 if do_private and consts.LookupPrivateBuiltin(name) != 0:
933 results.append((name, 'builtin', 'private'))
934
935 return results
936
937
938class Type(vm._Builtin):
939
940 def __init__(
941 self,
942 procs, # type: state.Procs
943 aliases, # type: Dict[str, str]
944 search_path, # type: executor.SearchPath
945 errfmt, # type: ui.ErrorFormatter
946 ):
947 # type: (...) -> None
948 self.procs = procs
949 self.aliases = aliases
950 self.search_path = search_path
951 self.errfmt = errfmt
952
953 def _PrintEntry(self, arg, row):
954 # type: (arg_types.type, Tuple[str, str, Optional[str]]) -> None
955 """For type builtin"""
956
957 name, kind, detail = row
958 assert kind is not None
959
960 if arg.t: # short string
961 print(kind)
962
963 elif arg.p:
964 #log('%s %s %s', name, kind, resolved)
965 if kind == 'file':
966 print(detail) # print the file path
967
968 else: # free-form text
969 _PrintFreeForm(row)
970 if kind == 'function':
971 #self._PrintShellFuncSource(name)
972 sh_func = self.procs.GetShellFunc(name)
973 assert sh_func is not None # we already looked it up
974 ui.PrintShFunction(sh_func)
975
976 def Run(self, cmd_val):
977 # type: (cmd_value.Argv) -> int
978 attrs, arg_r = flag_util.ParseCmdVal('type', cmd_val)
979 arg = arg_types.type(attrs.attrs)
980
981 if arg.f: # suppress function lookup
982 procs = None # type: state.Procs
983 else:
984 procs = self.procs
985
986 status = 0
987 names = arg_r.Rest()
988
989 if arg.P: # -P should forces PATH search, regardless of builtin/alias/function/etc.
990 for name in names:
991 paths = self.search_path.LookupReflect(name, arg.a)
992 if len(paths):
993 for path in paths:
994 print(path)
995 else:
996 status = 1
997 return status
998
999 for argument in names:
1000 r = _ResolveName(argument, procs, self.aliases, self.search_path,
1001 arg.a)
1002
1003 if arg.a:
1004 for row in r:
1005 self._PrintEntry(arg, row)
1006 else:
1007 # Just print the first one.
1008 # TODO: it would be nice to short-circuit the lookups.
1009 # It would be nice if 'yield' worked.
1010 if len(r):
1011 self._PrintEntry(arg, r[0])
1012
1013 # Error case
1014 if len(r) == 0:
1015 if not arg.t: # 'type -t' is silent in this case
1016 # match bash behavior by printing to stderr
1017 print_stderr('%s: not found' % argument)
1018 status = 1 # nothing printed, but we fail
1019
1020 return status