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

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