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

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