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

857 lines, 513 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 # TODO: Add debug_frame here, with ctx_Eval or ctx_EvalDebugFrame
94 src = source.Dynamic('eval arg', eval_loc)
95 with dev.ctx_Tracer(self.tracer, 'eval', None):
96 with state.ctx_EvalDebugFrame(self.mem, self.mem.token_for_line):
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):
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, props, 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, resolved = row
493
494 if kind == 'file':
495 what = resolved
496 elif kind == 'alias':
497 what = ('an alias for %s' %
498 j8_lite.EncodeString(resolved, unquoted_ok=True))
499 elif kind in ('proc', 'invokable'):
500 # Note: haynode should be an invokable
501 what = 'a YSH %s' % kind
502 else: # builtin, function, keyword
503 what = 'a shell %s' % kind
504
505 print('%s is %s' % (name, what))
506
507 # if kind == 'function':
508 # bash is the only shell that prints the function
509
510
511def _PrintEntry(arg, row):
512 # type: (arg_types.type, Tuple[str, str, Optional[str]]) -> None
513
514 _, kind, resolved = row
515 assert kind is not None
516
517 if arg.t: # short string
518 print(kind)
519
520 elif arg.p:
521 #log('%s %s %s', name, kind, resolved)
522 if kind == 'file':
523 print(resolved)
524
525 else: # free-form text
526 _PrintFreeForm(row)
527
528
529class Command(vm._Builtin):
530 """'command ls' suppresses function lookup."""
531
532 def __init__(
533 self,
534 shell_ex, # type: vm._Executor
535 funcs, # type: state.Procs
536 aliases, # type: Dict[str, str]
537 search_path, # type: executor.SearchPath
538 ):
539 # type: (...) -> None
540 self.shell_ex = shell_ex
541 self.funcs = funcs
542 self.aliases = aliases
543 self.search_path = search_path
544
545 def Run(self, cmd_val):
546 # type: (cmd_value.Argv) -> int
547
548 # accept_typed_args=True because we invoke other builtins
549 attrs, arg_r = flag_util.ParseCmdVal('command',
550 cmd_val,
551 accept_typed_args=True)
552 arg = arg_types.command(attrs.attrs)
553
554 argv, locs = arg_r.Rest2()
555
556 if arg.v or arg.V:
557 status = 0
558 for argument in argv:
559 r = _ResolveName(argument, self.funcs, self.aliases,
560 self.search_path, False)
561 if len(r):
562 # Print only the first occurrence
563 row = r[0]
564 if arg.v:
565 name, _, path = row
566 if path is not None:
567 print(path) # /usr/bin/awk
568 else:
569 print(name) # myfunc
570 else:
571 _PrintFreeForm(row)
572 else:
573 # match bash behavior by printing to stderr
574 print_stderr('%s: not found' % argument)
575 status = 1 # nothing printed, but we fail
576
577 return status
578
579 cmd_val2 = cmd_value.Argv(argv, locs, cmd_val.is_last_cmd,
580 cmd_val.self_obj, cmd_val.proc_args)
581
582 cmd_st = CommandStatus.CreateNull(alloc_lists=True)
583
584 # If we respected do_fork here instead of passing DO_FORK
585 # unconditionally, the case 'command date | wc -l' would take 2
586 # processes instead of 3. See test/syscall
587 run_flags = executor.NO_CALL_PROCS
588 if cmd_val.is_last_cmd:
589 run_flags |= executor.IS_LAST_CMD
590 if arg.p:
591 run_flags |= executor.USE_DEFAULT_PATH
592
593 return self.shell_ex.RunSimpleCommand(cmd_val2, cmd_st, run_flags)
594
595
596def _ShiftArgv(cmd_val):
597 # type: (cmd_value.Argv) -> cmd_value.Argv
598 return cmd_value.Argv(cmd_val.argv[1:], cmd_val.arg_locs[1:],
599 cmd_val.is_last_cmd, cmd_val.self_obj,
600 cmd_val.proc_args)
601
602
603class Builtin(vm._Builtin):
604
605 def __init__(self, shell_ex, errfmt):
606 # type: (vm._Executor, ui.ErrorFormatter) -> None
607 self.shell_ex = shell_ex
608 self.errfmt = errfmt
609
610 def Run(self, cmd_val):
611 # type: (cmd_value.Argv) -> int
612
613 if len(cmd_val.argv) == 1:
614 return 0 # this could be an error in strict mode?
615
616 name = cmd_val.argv[1]
617
618 # Run regular builtin or special builtin
619 to_run = consts.LookupNormalBuiltin(name)
620 if to_run == consts.NO_INDEX:
621 to_run = consts.LookupSpecialBuiltin(name)
622 if to_run == consts.NO_INDEX:
623 location = cmd_val.arg_locs[1]
624 if consts.LookupAssignBuiltin(name) != consts.NO_INDEX:
625 # NOTE: There's a similar restriction for 'command'
626 self.errfmt.Print_("Can't run assignment builtin recursively",
627 blame_loc=location)
628 else:
629 self.errfmt.Print_("%r isn't a shell builtin" % name,
630 blame_loc=location)
631 return 1
632
633 cmd_val2 = _ShiftArgv(cmd_val)
634 return self.shell_ex.RunBuiltin(to_run, cmd_val2)
635
636
637class RunProc(vm._Builtin):
638
639 def __init__(self, shell_ex, procs, errfmt):
640 # type: (vm._Executor, state.Procs, ui.ErrorFormatter) -> None
641 self.shell_ex = shell_ex
642 self.procs = procs
643 self.errfmt = errfmt
644
645 def Run(self, cmd_val):
646 # type: (cmd_value.Argv) -> int
647 _, arg_r = flag_util.ParseCmdVal('runproc',
648 cmd_val,
649 accept_typed_args=True)
650 argv, locs = arg_r.Rest2()
651
652 if len(argv) == 0:
653 raise error.Usage('requires arguments', loc.Missing)
654
655 name = argv[0]
656 proc, _ = self.procs.GetInvokable(name)
657 if not proc:
658 # note: should runproc be invoke?
659 self.errfmt.PrintMessage('runproc: no invokable named %r' % name)
660 return 1
661
662 cmd_val2 = cmd_value.Argv(argv, locs, cmd_val.is_last_cmd,
663 cmd_val.self_obj, cmd_val.proc_args)
664
665 cmd_st = CommandStatus.CreateNull(alloc_lists=True)
666 run_flags = executor.IS_LAST_CMD if cmd_val.is_last_cmd else 0
667 return self.shell_ex.RunSimpleCommand(cmd_val2, cmd_st, run_flags)
668
669
670class Invoke(vm._Builtin):
671 """
672 Introspection:
673
674 invoke - YSH introspection on first word
675 type --all - introspection on variables too?
676 - different than = type(x)
677
678 3 Coarsed-grained categories
679 - invoke --builtin aka builtin
680 - including special builtins
681 - invoke --proc-like aka runproc
682 - myproc (42)
683 - sh-func
684 - invokable-obj
685 - invoke --extern aka extern
686
687 Note: If you don't distinguish between proc, sh-func, and invokable-obj,
688 then 'runproc' suffices.
689
690 invoke --proc-like reads more nicely though, and it also combines.
691
692 invoke --builtin --extern # this is like 'command'
693
694 You can also negate:
695
696 invoke --no-proc-like --no-builtin --no-extern
697
698 - type -t also has 'keyword' and 'assign builtin'
699
700 With no args, print a table of what's available
701
702 invoke --builtin
703 invoke --builtin true
704 """
705
706 def __init__(self, shell_ex, procs, errfmt):
707 # type: (vm._Executor, state.Procs, ui.ErrorFormatter) -> None
708 self.shell_ex = shell_ex
709 self.procs = procs
710 self.errfmt = errfmt
711
712 def Run(self, cmd_val):
713 # type: (cmd_value.Argv) -> int
714 _, arg_r = flag_util.ParseCmdVal('invoke',
715 cmd_val,
716 accept_typed_args=True)
717 #argv, locs = arg_r.Rest2()
718
719 print('TODO: invoke')
720 # TODO
721 return 0
722
723
724class Extern(vm._Builtin):
725
726 def __init__(self, shell_ex, procs, errfmt):
727 # type: (vm._Executor, state.Procs, ui.ErrorFormatter) -> None
728 self.shell_ex = shell_ex
729 self.procs = procs
730 self.errfmt = errfmt
731
732 def Run(self, cmd_val):
733 # type: (cmd_value.Argv) -> int
734 _, arg_r = flag_util.ParseCmdVal('extern',
735 cmd_val,
736 accept_typed_args=True)
737 #argv, locs = arg_r.Rest2()
738
739 print('TODO: extern')
740
741 return 0
742
743
744def _ResolveName(
745 name, # type: str
746 procs, # type: state.Procs
747 aliases, # type: Dict[str, str]
748 search_path, # type: executor.SearchPath
749 do_all, # type: bool
750):
751 # type: (...) -> List[Tuple[str, str, Optional[str]]]
752 """
753 Returns:
754 A list of (name, type, optional file system path)
755
756 TODO: All of these could be in YSH:
757
758 type, type -t, type -a
759 pp proc
760
761 We could builtin functions like isShellFunc() and isInvokableObj()
762 """
763 # MyPy tuple type
764 no_str = None # type: Optional[str]
765
766 results = [] # type: List[Tuple[str, str, Optional[str]]]
767
768 if procs:
769 if procs.IsShellFunc(name):
770 results.append((name, 'function', no_str))
771
772 if procs.IsProc(name):
773 results.append((name, 'proc', no_str))
774 elif procs.IsInvokableObj(name): # can't be both proc and obj
775 results.append((name, 'invokable', no_str))
776
777 if name in aliases:
778 results.append((name, 'alias', aliases[name]))
779
780 # See if it's a builtin
781 if consts.LookupNormalBuiltin(name) != 0:
782 results.append((name, 'builtin', no_str))
783 elif consts.LookupSpecialBuiltin(name) != 0:
784 results.append((name, 'builtin', no_str))
785 elif consts.LookupAssignBuiltin(name) != 0:
786 results.append((name, 'builtin', no_str))
787
788 # See if it's a keyword
789 if consts.IsControlFlow(name): # continue, etc.
790 results.append((name, 'keyword', no_str))
791 elif consts.IsKeyword(name):
792 results.append((name, 'keyword', no_str))
793
794 # See if it's external
795 for path in search_path.LookupReflect(name, do_all):
796 if posix.access(path, X_OK):
797 results.append((name, 'file', path))
798
799 return results
800
801
802class Type(vm._Builtin):
803
804 def __init__(
805 self,
806 funcs, # type: state.Procs
807 aliases, # type: Dict[str, str]
808 search_path, # type: executor.SearchPath
809 errfmt, # type: ui.ErrorFormatter
810 ):
811 # type: (...) -> None
812 self.funcs = funcs
813 self.aliases = aliases
814 self.search_path = search_path
815 self.errfmt = errfmt
816
817 def Run(self, cmd_val):
818 # type: (cmd_value.Argv) -> int
819 attrs, arg_r = flag_util.ParseCmdVal('type', cmd_val)
820 arg = arg_types.type(attrs.attrs)
821
822 if arg.f: # suppress function lookup
823 funcs = None # type: state.Procs
824 else:
825 funcs = self.funcs
826
827 status = 0
828 names = arg_r.Rest()
829
830 if arg.P: # -P should forces PATH search, regardless of builtin/alias/function/etc.
831 for name in names:
832 paths = self.search_path.LookupReflect(name, arg.a)
833 if len(paths):
834 for path in paths:
835 print(path)
836 else:
837 status = 1
838 return status
839
840 for argument in names:
841 r = _ResolveName(argument, funcs, self.aliases, self.search_path,
842 arg.a)
843 if arg.a:
844 for row in r:
845 _PrintEntry(arg, row)
846 else:
847 if len(r): # Just print the first one
848 _PrintEntry(arg, r[0])
849
850 # Error case
851 if len(r) == 0:
852 if not arg.t: # 'type -t' is silent in this case
853 # match bash behavior by printing to stderr
854 print_stderr('%s: not found' % argument)
855 status = 1 # nothing printed, but we fail
856
857 return status