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

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