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

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