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

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