OILS / builtin / meta_osh.py View on Github | oilshell.org

501 lines, 349 significant
1#!/usr/bin/env python2
2"""
3meta_osh.py - Builtins that call back into the interpreter.
4"""
5from __future__ import print_function
6
7from _devbuild.gen import arg_types
8from _devbuild.gen.runtime_asdl import cmd_value, CommandStatus
9from _devbuild.gen.value_asdl import value, value_e
10from _devbuild.gen.syntax_asdl import source, loc
11from core import alloc
12from core import dev
13from core import error
14from core import executor
15from core import main_loop
16from core import process
17from core.error import e_usage
18from core import pyutil # strerror
19from core import state
20from core import vm
21from data_lang import j8_lite
22from frontend import flag_util
23from frontend import consts
24from frontend import reader
25from frontend import typed_args
26from mycpp.mylib import log, print_stderr
27from pylib import os_path
28from osh import cmd_eval
29
30import posix_ as posix
31from posix_ import X_OK # translated directly to C macro
32
33_ = log
34
35from typing import Dict, List, Tuple, Optional, cast, TYPE_CHECKING
36if TYPE_CHECKING:
37 from frontend import args
38 from frontend.parse_lib import ParseContext
39 from core import optview
40 from display import ui
41 from osh.cmd_eval import CommandEvaluator
42 from osh.cmd_parse import CommandParser
43
44
45class Eval(vm._Builtin):
46
47 def __init__(
48 self,
49 parse_ctx, # type: ParseContext
50 exec_opts, # type: optview.Exec
51 cmd_ev, # type: CommandEvaluator
52 tracer, # type: dev.Tracer
53 errfmt, # type: ui.ErrorFormatter
54 mem, # type: state.Mem
55 ):
56 # type: (...) -> None
57 self.parse_ctx = parse_ctx
58 self.arena = parse_ctx.arena
59 self.exec_opts = exec_opts
60 self.cmd_ev = cmd_ev
61 self.tracer = tracer
62 self.errfmt = errfmt
63 self.mem = mem
64
65 def RunTyped(self, cmd_val):
66 # type: (cmd_value.Argv) -> int
67 """For eval (mycmd)"""
68 rd = typed_args.ReaderForProc(cmd_val)
69 cmd = rd.PosCommand()
70 dollar0 = rd.NamedStr("dollar0", None)
71 pos_args_raw = rd.NamedList("pos_args", None)
72 vars = rd.NamedDict("vars", None)
73 rd.Done()
74
75 pos_args = None # type: List[str]
76 if pos_args_raw is not None:
77 pos_args = []
78 for arg in pos_args_raw:
79 if arg.tag() != value_e.Str:
80 raise error.TypeErr(
81 arg, "Expected pos_args to be a list of Strs",
82 rd.LeftParenToken())
83
84 pos_args.append(cast(value.Str, arg).s)
85
86 with state.ctx_Eval(self.mem, dollar0, pos_args, vars):
87 return self.cmd_ev.EvalCommand(cmd)
88
89 def Run(self, cmd_val):
90 # type: (cmd_value.Argv) -> int
91 if cmd_val.proc_args:
92 return self.RunTyped(cmd_val)
93
94 # There are no flags, but we need it to respect --
95 _, arg_r = flag_util.ParseCmdVal('eval', cmd_val)
96
97 if self.exec_opts.simple_eval_builtin():
98 code_str, eval_loc = arg_r.ReadRequired2('requires code string')
99 if not arg_r.AtEnd():
100 e_usage('requires exactly 1 argument', loc.Missing)
101 else:
102 code_str = ' '.join(arg_r.Rest())
103 # code_str could be EMPTY, so just use the first one
104 eval_loc = cmd_val.arg_locs[0]
105
106 line_reader = reader.StringLineReader(code_str, self.arena)
107 c_parser = self.parse_ctx.MakeOshParser(line_reader)
108
109 src = source.ArgvWord('eval', eval_loc)
110 with dev.ctx_Tracer(self.tracer, 'eval', None):
111 with alloc.ctx_SourceCode(self.arena, src):
112 return main_loop.Batch(self.cmd_ev,
113 c_parser,
114 self.errfmt,
115 cmd_flags=cmd_eval.RaiseControlFlow)
116
117
118class Source(vm._Builtin):
119
120 def __init__(
121 self,
122 parse_ctx, # type: ParseContext
123 search_path, # type: state.SearchPath
124 cmd_ev, # type: CommandEvaluator
125 fd_state, # type: process.FdState
126 tracer, # type: dev.Tracer
127 errfmt, # type: ui.ErrorFormatter
128 loader, # type: pyutil._ResourceLoader
129 ):
130 # type: (...) -> None
131 self.parse_ctx = parse_ctx
132 self.arena = parse_ctx.arena
133 self.search_path = search_path
134 self.cmd_ev = cmd_ev
135 self.fd_state = fd_state
136 self.tracer = tracer
137 self.errfmt = errfmt
138 self.loader = loader
139
140 self.mem = cmd_ev.mem
141
142 def Run(self, cmd_val):
143 # type: (cmd_value.Argv) -> int
144 attrs, arg_r = flag_util.ParseCmdVal('source', cmd_val)
145 arg = arg_types.source(attrs.attrs)
146
147 path_arg = arg_r.Peek()
148 if path_arg is None:
149 e_usage('missing required argument', loc.Missing)
150 arg_r.Next()
151
152 # Old:
153 # source --builtin two.sh # looks up stdlib/two.sh
154 # New:
155 # source $LIB_OSH/two.sh # looks up stdlib/osh/two.sh
156 # source ///osh/two.sh # looks up stdlib/osh/two.sh
157 builtin_path = None # type: Optional[str]
158 if arg.builtin:
159 builtin_path = path_arg
160 elif path_arg.startswith('///'):
161 builtin_path = path_arg[3:]
162
163 if builtin_path is not None:
164 try:
165 load_path = os_path.join("stdlib", builtin_path)
166 contents = self.loader.Get(load_path)
167 except (IOError, OSError):
168 self.errfmt.Print_('source failed: No builtin file %r' %
169 load_path,
170 blame_loc=cmd_val.arg_locs[2])
171 return 2
172
173 line_reader = reader.StringLineReader(contents, self.arena)
174 c_parser = self.parse_ctx.MakeOshParser(line_reader)
175 return self._Exec(cmd_val, arg_r, load_path, c_parser)
176
177 else:
178 # 'source' respects $PATH
179 resolved = self.search_path.LookupOne(path_arg,
180 exec_required=False)
181 if resolved is None:
182 resolved = path_arg
183
184 try:
185 # Shell can't use descriptors 3-9
186 f = self.fd_state.Open(resolved)
187 except (IOError, OSError) as e:
188 self.errfmt.Print_('source %r failed: %s' %
189 (path_arg, pyutil.strerror(e)),
190 blame_loc=cmd_val.arg_locs[1])
191 return 1
192
193 line_reader = reader.FileLineReader(f, self.arena)
194 c_parser = self.parse_ctx.MakeOshParser(line_reader)
195
196 with process.ctx_FileCloser(f):
197 return self._Exec(cmd_val, arg_r, path_arg, c_parser)
198
199 def _Exec(self, cmd_val, arg_r, path, c_parser):
200 # type: (cmd_value.Argv, args.Reader, str, CommandParser) -> int
201 call_loc = cmd_val.arg_locs[0]
202
203 # A sourced module CAN have a new arguments array, but it always shares
204 # the same variable scope as the caller. The caller could be at either a
205 # global or a local scope.
206
207 # TODO: I wonder if we compose the enter/exit methods more easily.
208
209 with dev.ctx_Tracer(self.tracer, 'source', cmd_val.argv):
210 source_argv = arg_r.Rest()
211 with state.ctx_Source(self.mem, path, source_argv):
212 with state.ctx_ThisDir(self.mem, path):
213 src = source.SourcedFile(path, call_loc)
214 with alloc.ctx_SourceCode(self.arena, src):
215 try:
216 status = main_loop.Batch(
217 self.cmd_ev,
218 c_parser,
219 self.errfmt,
220 cmd_flags=cmd_eval.RaiseControlFlow)
221 except vm.IntControlFlow as e:
222 if e.IsReturn():
223 status = e.StatusCode()
224 else:
225 raise
226
227 return status
228
229
230def _PrintFreeForm(row):
231 # type: (Tuple[str, str, Optional[str]]) -> None
232 name, kind, resolved = row
233
234 if kind == 'file':
235 what = resolved
236 elif kind == 'alias':
237 what = ('an alias for %s' %
238 j8_lite.EncodeString(resolved, unquoted_ok=True))
239 else: # builtin, function, keyword
240 what = 'a shell %s' % kind
241
242 # TODO: Should also print haynode
243
244 print('%s is %s' % (name, what))
245
246 # if kind == 'function':
247 # bash is the only shell that prints the function
248
249
250def _PrintEntry(arg, row):
251 # type: (arg_types.type, Tuple[str, str, Optional[str]]) -> None
252
253 _, kind, resolved = row
254 assert kind is not None
255
256 if arg.t: # short string
257 print(kind)
258
259 elif arg.p:
260 #log('%s %s %s', name, kind, resolved)
261 if kind == 'file':
262 print(resolved)
263
264 else: # free-form text
265 _PrintFreeForm(row)
266
267
268class Command(vm._Builtin):
269 """'command ls' suppresses function lookup."""
270
271 def __init__(
272 self,
273 shell_ex, # type: vm._Executor
274 funcs, # type: state.Procs
275 aliases, # type: Dict[str, str]
276 search_path, # type: state.SearchPath
277 ):
278 # type: (...) -> None
279 self.shell_ex = shell_ex
280 self.funcs = funcs
281 self.aliases = aliases
282 self.search_path = search_path
283
284 def Run(self, cmd_val):
285 # type: (cmd_value.Argv) -> int
286
287 # accept_typed_args=True because we invoke other builtins
288 attrs, arg_r = flag_util.ParseCmdVal('command',
289 cmd_val,
290 accept_typed_args=True)
291 arg = arg_types.command(attrs.attrs)
292
293 argv, locs = arg_r.Rest2()
294
295 if arg.v or arg.V:
296 status = 0
297 for argument in argv:
298 r = _ResolveName(argument, self.funcs, self.aliases,
299 self.search_path, False)
300 if len(r):
301 # command -v prints the name (-V is more detailed)
302 # Print it only once.
303 row = r[0]
304 name, _, _ = row
305 if arg.v:
306 print(name)
307 else:
308 _PrintFreeForm(row)
309 else:
310 # match bash behavior by printing to stderr
311 print_stderr('%s: not found' % argument)
312 status = 1 # nothing printed, but we fail
313
314 return status
315
316 cmd_val2 = cmd_value.Argv(argv, locs, cmd_val.is_last_cmd,
317 cmd_val.proc_args)
318
319 cmd_st = CommandStatus.CreateNull(alloc_lists=True)
320
321 # If we respected do_fork here instead of passing DO_FORK
322 # unconditionally, the case 'command date | wc -l' would take 2
323 # processes instead of 3. See test/syscall
324 run_flags = executor.NO_CALL_PROCS
325 if cmd_val.is_last_cmd:
326 run_flags |= executor.IS_LAST_CMD
327 if arg.p:
328 run_flags |= executor.USE_DEFAULT_PATH
329
330 return self.shell_ex.RunSimpleCommand(cmd_val2, cmd_st, run_flags)
331
332
333def _ShiftArgv(cmd_val):
334 # type: (cmd_value.Argv) -> cmd_value.Argv
335 return cmd_value.Argv(cmd_val.argv[1:], cmd_val.arg_locs[1:],
336 cmd_val.is_last_cmd, cmd_val.proc_args)
337
338
339class Builtin(vm._Builtin):
340
341 def __init__(self, shell_ex, errfmt):
342 # type: (vm._Executor, ui.ErrorFormatter) -> None
343 self.shell_ex = shell_ex
344 self.errfmt = errfmt
345
346 def Run(self, cmd_val):
347 # type: (cmd_value.Argv) -> int
348
349 if len(cmd_val.argv) == 1:
350 return 0 # this could be an error in strict mode?
351
352 name = cmd_val.argv[1]
353
354 # Run regular builtin or special builtin
355 to_run = consts.LookupNormalBuiltin(name)
356 if to_run == consts.NO_INDEX:
357 to_run = consts.LookupSpecialBuiltin(name)
358 if to_run == consts.NO_INDEX:
359 location = cmd_val.arg_locs[1]
360 if consts.LookupAssignBuiltin(name) != consts.NO_INDEX:
361 # NOTE: There's a similar restriction for 'command'
362 self.errfmt.Print_("Can't run assignment builtin recursively",
363 blame_loc=location)
364 else:
365 self.errfmt.Print_("%r isn't a shell builtin" % name,
366 blame_loc=location)
367 return 1
368
369 cmd_val2 = _ShiftArgv(cmd_val)
370 return self.shell_ex.RunBuiltin(to_run, cmd_val2)
371
372
373class RunProc(vm._Builtin):
374
375 def __init__(self, shell_ex, procs, errfmt):
376 # type: (vm._Executor, state.Procs, ui.ErrorFormatter) -> None
377 self.shell_ex = shell_ex
378 self.procs = procs
379 self.errfmt = errfmt
380
381 def Run(self, cmd_val):
382 # type: (cmd_value.Argv) -> int
383 _, arg_r = flag_util.ParseCmdVal('runproc',
384 cmd_val,
385 accept_typed_args=True)
386 argv, locs = arg_r.Rest2()
387
388 if len(argv) == 0:
389 raise error.Usage('requires arguments', loc.Missing)
390
391 name = argv[0]
392 if not self.procs.Get(name):
393 self.errfmt.PrintMessage('runproc: no proc named %r' % name)
394 return 1
395
396 cmd_val2 = cmd_value.Argv(argv, locs, cmd_val.is_last_cmd,
397 cmd_val.proc_args)
398
399 cmd_st = CommandStatus.CreateNull(alloc_lists=True)
400 run_flags = executor.IS_LAST_CMD if cmd_val.is_last_cmd else 0
401 return self.shell_ex.RunSimpleCommand(cmd_val2, cmd_st, run_flags)
402
403
404def _ResolveName(
405 name, # type: str
406 funcs, # type: state.Procs
407 aliases, # type: Dict[str, str]
408 search_path, # type: state.SearchPath
409 do_all, # type: bool
410):
411 # type: (...) -> List[Tuple[str, str, Optional[str]]]
412
413 # MyPy tuple type
414 no_str = None # type: Optional[str]
415
416 results = [] # type: List[Tuple[str, str, Optional[str]]]
417
418 if funcs and funcs.Get(name):
419 results.append((name, 'function', no_str))
420
421 if name in aliases:
422 results.append((name, 'alias', aliases[name]))
423
424 # See if it's a builtin
425 if consts.LookupNormalBuiltin(name) != 0:
426 results.append((name, 'builtin', no_str))
427 elif consts.LookupSpecialBuiltin(name) != 0:
428 results.append((name, 'builtin', no_str))
429 elif consts.LookupAssignBuiltin(name) != 0:
430 results.append((name, 'builtin', no_str))
431
432 # See if it's a keyword
433 if consts.IsControlFlow(name): # continue, etc.
434 results.append((name, 'keyword', no_str))
435 elif consts.IsKeyword(name):
436 results.append((name, 'keyword', no_str))
437
438 # See if it's external
439 for path in search_path.LookupReflect(name, do_all):
440 if posix.access(path, X_OK):
441 results.append((name, 'file', path))
442
443 return results
444
445
446class Type(vm._Builtin):
447
448 def __init__(
449 self,
450 funcs, # type: state.Procs
451 aliases, # type: Dict[str, str]
452 search_path, # type: state.SearchPath
453 errfmt, # type: ui.ErrorFormatter
454 ):
455 # type: (...) -> None
456 self.funcs = funcs
457 self.aliases = aliases
458 self.search_path = search_path
459 self.errfmt = errfmt
460
461 def Run(self, cmd_val):
462 # type: (cmd_value.Argv) -> int
463 attrs, arg_r = flag_util.ParseCmdVal('type', cmd_val)
464 arg = arg_types.type(attrs.attrs)
465
466 if arg.f: # suppress function lookup
467 funcs = None # type: state.Procs
468 else:
469 funcs = self.funcs
470
471 status = 0
472 names = arg_r.Rest()
473
474 if arg.P: # -P should forces PATH search, regardless of builtin/alias/function/etc.
475 for name in names:
476 paths = self.search_path.LookupReflect(name, arg.a)
477 if len(paths):
478 for path in paths:
479 print(path)
480 else:
481 status = 1
482 return status
483
484 for argument in names:
485 r = _ResolveName(argument, funcs, self.aliases, self.search_path,
486 arg.a)
487 if arg.a:
488 for row in r:
489 _PrintEntry(arg, row)
490 else:
491 if len(r): # Just print the first one
492 _PrintEntry(arg, r[0])
493
494 # Error case
495 if len(r) == 0:
496 if not arg.t: # 'type -t' is silent in this case
497 # match bash behavior by printing to stderr
498 print_stderr('%s: not found' % argument)
499 status = 1 # nothing printed, but we fail
500
501 return status