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

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