1 #!/usr/bin/env python2
2 # Copyright 2016 Andy Chu. All rights reserved.
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 """
9 builtin_misc.py - Misc builtins.
10 """
11 from __future__ import print_function
12
13 from errno import EINTR
14
15 from _devbuild.gen import arg_types
16 from _devbuild.gen.runtime_asdl import (span_e, cmd_value, value, scope_e)
17 from _devbuild.gen.syntax_asdl import source, loc, loc_t
18 from core import alloc
19 from core import error
20 from core.error import e_usage, e_die, e_die_status
21 from core import pyos
22 from core import pyutil
23 from core import state
24 from core import util
25 from core import ui
26 from core import vm
27 from data_lang import qsn_native
28 from frontend import flag_spec
29 from frontend import location
30 from frontend import reader
31 from frontend import typed_args
32 from mycpp import mylib
33 from mycpp.mylib import log, STDIN_FILENO
34 from osh import word_compile
35 from pylib import os_path
36
37 import libc
38 import posix_ as posix
39
40 from typing import Tuple, List, Dict, Optional, Any, TYPE_CHECKING
41 if TYPE_CHECKING:
42 from _devbuild.gen.runtime_asdl import span_t
43 from core.pyutil import _ResourceLoader
44 from core.state import Mem, DirStack
45 from core.ui import ErrorFormatter
46 from frontend.parse_lib import ParseContext
47 from osh.cmd_eval import CommandEvaluator
48 from osh.split import SplitContext
49
50 _ = log
51
52 #
53 # Implementation of builtins.
54 #
55
56
57 class Times(vm._Builtin):
58 def __init__(self):
59 # type: () -> None
60 vm._Builtin.__init__(self)
61
62 def Run(self, cmd_val):
63 # type: (cmd_value.Argv) -> int
64 pyos.PrintTimes()
65 return 0
66
67
68 # The Read builtin splits using IFS.
69 #
70 # Summary:
71 # - Split with IFS, except \ can escape them! This is different than the
72 # algorithm for splitting words (at least the way I've represented it.)
73
74 # Bash manual:
75 # - If there are more words than names, the remaining words and their
76 # intervening delimiters are assigned to the last name.
77 # - If there are fewer words read from the input stream than names, the
78 # remaining names are assigned empty values.
79 # - The characters in the value of the IFS variable are used to split the line
80 # into words using the same rules the shell uses for expansion (described
81 # above in Word Splitting).
82 # - The backslash character '\' may be used to remove any special meaning for
83 # the next character read and for line continuation.
84
85
86 def _AppendParts(s, spans, max_results, join_next, parts):
87 # type: (str, List[Tuple[span_t, int]], int, bool, List[mylib.BufWriter]) -> Tuple[bool, bool]
88 """Append to 'parts', for the 'read' builtin.
89
90 Similar to _SpansToParts in osh/split.py
91
92 Args:
93 s: The original string
94 spans: List of (span, end_index)
95 max_results: the maximum number of parts we want
96 join_next: Whether to join the next span to the previous part. This
97 happens in two cases:
98 - when we have '\ '
99 - and when we have more spans # than max_results.
100 """
101 start_index = 0
102 # If the last span was black, and we get a backslash, set join_next to merge
103 # two black spans.
104 last_span_was_black = False
105
106 for span_type, end_index in spans:
107 if span_type == span_e.Black:
108 if join_next and len(parts):
109 parts[-1].write(s[start_index:end_index])
110 join_next = False
111 else:
112 buf = mylib.BufWriter()
113 buf.write(s[start_index:end_index])
114 parts.append(buf)
115 last_span_was_black = True
116
117 elif span_type == span_e.Delim:
118 if join_next:
119 parts[-1].write(s[start_index:end_index])
120 join_next = False
121 last_span_was_black = False
122
123 elif span_type == span_e.Backslash:
124 if last_span_was_black:
125 join_next = True
126 last_span_was_black = False
127
128 if max_results and len(parts) >= max_results:
129 join_next = True
130
131 start_index = end_index
132
133 done = True
134 if len(spans):
135 #log('%s %s', s, spans)
136 #log('%s', spans[-1])
137 last_span_type, _ = spans[-1]
138 if last_span_type == span_e.Backslash:
139 done = False
140
141 #log('PARTS %s', parts)
142 return done, join_next
143
144
145 #
146 # Three read() wrappers for 'read' builtin that RunPendingTraps: _ReadN,
147 # _ReadUntilDelim, and _ReadLineSlowly
148 #
149
150
151 def _ReadN(num_bytes, cmd_ev):
152 # type: (int, CommandEvaluator) -> str
153 chunks = [] # type: List[str]
154 bytes_left = num_bytes
155 while bytes_left > 0:
156 n, err_num = pyos.Read(STDIN_FILENO, bytes_left,
157 chunks) # read up to n bytes
158
159 if n < 0:
160 if err_num == EINTR:
161 cmd_ev.RunPendingTraps()
162 # retry after running traps
163 else:
164 raise pyos.ReadError(err_num)
165
166 elif n == 0: # EOF
167 break
168
169 else:
170 bytes_left -= n
171
172 return ''.join(chunks)
173
174
175 def _ReadUntilDelim(delim_byte, cmd_ev):
176 # type: (int, CommandEvaluator) -> Tuple[str, bool]
177 """Read a portion of stdin.
178
179 Read until that delimiter, but don't include it.
180 """
181 eof = False
182 ch_array = [] # type: List[int]
183 while True:
184 ch, err_num = pyos.ReadByte(0)
185 if ch < 0:
186 if err_num == EINTR:
187 cmd_ev.RunPendingTraps()
188 # retry after running traps
189 else:
190 raise pyos.ReadError(err_num)
191
192 elif ch == pyos.EOF_SENTINEL:
193 eof = True
194 break
195
196 elif ch == delim_byte:
197 break
198
199 else:
200 ch_array.append(ch)
201
202 return pyutil.ChArrayToString(ch_array), eof
203
204
205 # sys.stdin.readline() in Python has its own buffering which is incompatible
206 # with shell semantics. dash, mksh, and zsh all read a single byte at a
207 # time with read(0, 1).
208
209 # TODO:
210 # - _ReadLineSlowly should have keep_newline (mapfile -t)
211 # - this halves memory usage!
212
213
214 def _ReadLineSlowly(cmd_ev):
215 # type: (CommandEvaluator) -> str
216 """Read a line from stdin."""
217 ch_array = [] # type: List[int]
218 while True:
219 ch, err_num = pyos.ReadByte(0)
220
221 if ch < 0:
222 if err_num == EINTR:
223 cmd_ev.RunPendingTraps()
224 # retry after running traps
225 else:
226 raise pyos.ReadError(err_num)
227
228 elif ch == pyos.EOF_SENTINEL:
229 break
230
231 else:
232 ch_array.append(ch)
233
234 # TODO: Add option to omit newline
235 if ch == pyos.NEWLINE_CH:
236 break
237
238 return pyutil.ChArrayToString(ch_array)
239
240
241 def ReadAll():
242 # type: () -> str
243 """Read all of stdin.
244
245 Similar to command sub in core/executor.py.
246 """
247 chunks = [] # type: List[str]
248 while True:
249 n, err_num = pyos.Read(0, 4096, chunks)
250
251 if n < 0:
252 if err_num == EINTR:
253 # Retry only. Like read --line (and command sub), read --all doesn't
254 # run traps. It would be a bit weird to run every 4096 bytes.
255 pass
256 else:
257 raise pyos.ReadError(err_num)
258
259 elif n == 0: # EOF
260 break
261
262 return ''.join(chunks)
263
264
265 class ctx_TermAttrs(object):
266 def __init__(self, fd, local_modes):
267 # type: (int, int) -> None
268 self.fd = fd
269
270 # We change term_attrs[3] in Python, which is lflag "local modes"
271 orig_local_modes, term_attrs = pyos.PushTermAttrs(fd, local_modes)
272
273 # Workaround: destructured assignment into members doesn't work
274 self.orig_local_modes = orig_local_modes
275 self.term_attrs = term_attrs
276
277 def __enter__(self):
278 # type: () -> None
279 pass
280
281 def __exit__(self, type, value, traceback):
282 # type: (Any, Any, Any) -> None
283 pyos.PopTermAttrs(self.fd, self.orig_local_modes, self.term_attrs)
284
285
286 class Read(vm._Builtin):
287 def __init__(self, splitter, mem, parse_ctx, cmd_ev, errfmt):
288 # type: (SplitContext, Mem, ParseContext, CommandEvaluator, ErrorFormatter) -> None
289 self.splitter = splitter
290 self.mem = mem
291 self.parse_ctx = parse_ctx
292 self.cmd_ev = cmd_ev
293 self.errfmt = errfmt
294 self.stdin_ = mylib.Stdin()
295
296 def _Line(self, arg, var_name):
297 # type: (arg_types.read, str) -> int
298 """For read --line."""
299
300 # Use an optimized C implementation rather than _ReadLineSlowly, which
301 # calls ReadByte() over and over.
302 line = pyos.ReadLine()
303 if len(line) == 0: # EOF
304 return 1
305
306 if not arg.with_eol:
307 if line.endswith('\r\n'):
308 line = line[:-2]
309 elif line.endswith('\n'):
310 line = line[:-1]
311
312 # Lines that don't start with a single quote aren't QSN. They may contain
313 # a single quote internally, like:
314 #
315 # Fool's Gold
316 if arg.q and line.startswith("'"):
317 arena = self.parse_ctx.arena
318 line_reader = reader.StringLineReader(line, arena)
319 lexer = self.parse_ctx.MakeLexer(line_reader)
320
321 # The parser only yields valid tokens:
322 # Char_Literals, Char_OneChar, Char_Hex, Char_UBraced
323 # So we can use word_compile.EvalCStringToken, which is also used for
324 # $''.
325 # Important: we don't generate Id.Unknown_Backslash because that is valid
326 # in echo -e. We just make it Id.Unknown_Tok?
327 try:
328 # TODO: read should know about stdin, and redirects, and pipelines?
329 with alloc.ctx_SourceCode(arena, source.Stdin('')):
330 tokens = qsn_native.Parse(lexer)
331 except error.Parse as e:
332 self.errfmt.PrettyPrintError(e)
333 return 1
334 tmp = [word_compile.EvalCStringToken(t) for t in tokens]
335 line = ''.join(tmp)
336
337 lhs = location.LName(var_name)
338 self.mem.SetValue(lhs, value.Str(line), scope_e.LocalOnly)
339 return 0
340
341 def _All(self, var_name):
342 # type: (str) -> int
343 contents = ReadAll()
344
345 # No error conditions?
346
347 lhs = location.LName(var_name)
348 self.mem.SetValue(lhs, value.Str(contents), scope_e.LocalOnly)
349 return 0
350
351 def Run(self, cmd_val):
352 # type: (cmd_value.Argv) -> int
353 try:
354 status = self._Run(cmd_val)
355 except pyos.ReadError as e: # different paths for read -d, etc.
356 # don't quote code since YSH errexit will likely quote
357 self.errfmt.PrintMessage("read error: %s" %
358 posix.strerror(e.err_num))
359 status = 1
360 return status
361
362 def _Run(self, cmd_val):
363 # type: (cmd_value.Argv) -> int
364 attrs, arg_r = flag_spec.ParseCmdVal('read', cmd_val)
365 arg = arg_types.read(attrs.attrs)
366 names = arg_r.Rest()
367
368 # Don't respect any of the other options here? This is buffered I/O.
369 if arg.line: # read --line
370 var_name, var_loc = arg_r.Peek2()
371 if var_name is None:
372 var_name = '_line'
373 else:
374 if var_name.startswith(':'): # optional : sigil
375 var_name = var_name[1:]
376 arg_r.Next()
377
378 next_arg, next_loc = arg_r.Peek2()
379 if next_arg is not None:
380 raise error.Usage('got extra argument', next_loc)
381
382 return self._Line(arg, var_name)
383
384 if arg.q:
385 e_usage('--qsn can only be used with --line', loc.Missing)
386
387 if arg.all: # read --all
388 var_name, var_loc = arg_r.Peek2()
389 if var_name is None:
390 var_name = '_all'
391 else:
392 if var_name.startswith(':'): # optional : sigil
393 var_name = var_name[1:]
394 arg_r.Next()
395
396 next_arg, next_loc = arg_r.Peek2()
397 if next_arg is not None:
398 raise error.Usage('got extra argument', next_loc)
399
400 return self._All(var_name)
401
402 if arg.q:
403 e_usage('--qsn not implemented yet', loc.Missing)
404
405 if arg.t >= 0.0:
406 if arg.t != 0.0:
407 e_die("read -t isn't implemented (except t=0)")
408 else:
409 return 0 if pyos.InputAvailable(STDIN_FILENO) else 1
410
411 bits = 0
412 if self.stdin_.isatty():
413 # -d and -n should be unbuffered
414 if arg.d is not None or arg.n >= 0:
415 bits |= pyos.TERM_ICANON
416 if arg.s: # silent
417 bits |= pyos.TERM_ECHO
418
419 if arg.p is not None: # only if tty
420 mylib.Stderr().write(arg.p)
421
422 if bits == 0:
423 status = self._Read(arg, names)
424 else:
425 with ctx_TermAttrs(STDIN_FILENO, ~bits):
426 status = self._Read(arg, names)
427 return status
428
429 def _Read(self, arg, names):
430 # type: (arg_types.read, List[str]) -> int
431
432 if arg.n >= 0: # read a certain number of bytes (-1 means unset)
433 if len(names):
434 name = names[0]
435 else:
436 name = 'REPLY' # default variable name
437
438 s = _ReadN(arg.n, self.cmd_ev)
439
440 state.BuiltinSetString(self.mem, name, s)
441
442 # Did we read all the bytes we wanted?
443 return 0 if len(s) == arg.n else 1
444
445 if len(names) == 0:
446 names.append('REPLY')
447
448 # leftover words assigned to the last name
449 if arg.a is not None:
450 max_results = 0 # no max
451 else:
452 max_results = len(names)
453
454 if arg.Z: # -0 is synonym for -r -d ''
455 raw = True
456 delim_byte = 0
457 else:
458 raw = arg.r
459 if arg.d is not None:
460 if len(arg.d):
461 delim_byte = ord(arg.d[0])
462 else:
463 delim_byte = 0 # -d '' delimits by NUL
464 else:
465 delim_byte = pyos.NEWLINE_CH # read a line
466
467 # We have to read more than one line if there is a line continuation (and
468 # it's not -r).
469 parts = [] # type: List[mylib.BufWriter]
470 join_next = False
471 status = 0
472 while True:
473 line, eof = _ReadUntilDelim(delim_byte, self.cmd_ev)
474
475 if eof:
476 # status 1 to terminate loop. (This is true even though we set
477 # variables).
478 status = 1
479
480 #log('LINE %r', line)
481 if len(line) == 0:
482 break
483
484 spans = self.splitter.SplitForRead(line, not raw)
485 done, join_next = _AppendParts(line, spans, max_results, join_next,
486 parts)
487
488 #log('PARTS %s continued %s', parts, continued)
489 if done:
490 break
491
492 entries = [buf.getvalue() for buf in parts]
493 num_parts = len(entries)
494 if arg.a is not None:
495 state.BuiltinSetArray(self.mem, arg.a, entries)
496 else:
497 for i in xrange(max_results):
498 if i < num_parts:
499 s = entries[i]
500 else:
501 s = '' # if there are too many variables
502 var_name = names[i]
503 if var_name.startswith(':'):
504 var_name = var_name[1:]
505 #log('read: %s = %s', var_name, s)
506 state.BuiltinSetString(self.mem, var_name, s)
507
508 return status
509
510
511 class MapFile(vm._Builtin):
512 """Mapfile / readarray."""
513
514 def __init__(self, mem, errfmt, cmd_ev):
515 # type: (Mem, ErrorFormatter, CommandEvaluator) -> None
516 self.mem = mem
517 self.errfmt = errfmt
518 self.cmd_ev = cmd_ev
519
520 def Run(self, cmd_val):
521 # type: (cmd_value.Argv) -> int
522 attrs, arg_r = flag_spec.ParseCmdVal('mapfile', cmd_val)
523 arg = arg_types.mapfile(attrs.attrs)
524
525 var_name, _ = arg_r.Peek2()
526 if var_name is None:
527 var_name = 'MAPFILE'
528 else:
529 if var_name.startswith(':'):
530 var_name = var_name[1:]
531
532 lines = [] # type: List[str]
533 while True:
534 # bash uses this slow algorithm; YSH could provide read --all-lines
535 try:
536 line = _ReadLineSlowly(self.cmd_ev)
537 except pyos.ReadError as e:
538 self.errfmt.PrintMessage("mapfile: read() error: %s" %
539 posix.strerror(e.err_num))
540 return 1
541 if len(line) == 0:
542 break
543 # note: at least on Linux, bash doesn't strip \r\n
544 if arg.t and line.endswith('\n'):
545 line = line[:-1]
546 lines.append(line)
547
548 state.BuiltinSetArray(self.mem, var_name, lines)
549 return 0
550
551
552 class ctx_CdBlock(object):
553 def __init__(self, dir_stack, dest_dir, mem, errfmt, out_errs):
554 # type: (DirStack, str, Mem, ErrorFormatter, List[bool]) -> None
555 dir_stack.Push(dest_dir)
556
557 self.dir_stack = dir_stack
558 self.mem = mem
559 self.errfmt = errfmt
560 self.out_errs = out_errs
561
562 def __enter__(self):
563 # type: () -> None
564 pass
565
566 def __exit__(self, type, value, traceback):
567 # type: (Any, Any, Any) -> None
568 _PopDirStack('cd', self.mem, self.dir_stack, self.errfmt, self.out_errs)
569
570
571 class Cd(vm._Builtin):
572 def __init__(self, mem, dir_stack, cmd_ev, errfmt):
573 # type: (Mem, DirStack, CommandEvaluator, ErrorFormatter) -> None
574 self.mem = mem
575 self.dir_stack = dir_stack
576 self.cmd_ev = cmd_ev # To run blocks
577 self.errfmt = errfmt
578
579 def Run(self, cmd_val):
580 # type: (cmd_value.Argv) -> int
581 attrs, arg_r = flag_spec.ParseCmdVal('cd',
582 cmd_val,
583 accept_typed_args=True)
584 arg = arg_types.cd(attrs.attrs)
585
586 dest_dir, arg_loc = arg_r.Peek2()
587 if dest_dir is None:
588 try:
589 dest_dir = state.GetString(self.mem, 'HOME')
590 except error.Runtime as e:
591 self.errfmt.Print_(e.UserErrorString())
592 return 1
593
594 if dest_dir == '-':
595 try:
596 dest_dir = state.GetString(self.mem, 'OLDPWD')
597 print(dest_dir) # Shells print the directory
598 except error.Runtime as e:
599 self.errfmt.Print_(e.UserErrorString())
600 return 1
601
602 try:
603 pwd = state.GetString(self.mem, 'PWD')
604 except error.Runtime as e:
605 self.errfmt.Print_(e.UserErrorString())
606 return 1
607
608 # Calculate new directory, chdir() to it, then set PWD to it. NOTE: We can't
609 # call posix.getcwd() because it can raise OSError if the directory was
610 # removed (ENOENT.)
611 abspath = os_path.join(pwd, dest_dir) # make it absolute, for cd ..
612 if arg.P:
613 # -P means resolve symbolic links, then process '..'
614 real_dest_dir = libc.realpath(abspath)
615 else:
616 # -L means process '..' first. This just does string manipulation. (But
617 # realpath afterward isn't correct?)
618 real_dest_dir = os_path.normpath(abspath)
619
620 err_num = pyos.Chdir(real_dest_dir)
621 if err_num != 0:
622 self.errfmt.Print_("cd %r: %s" %
623 (real_dest_dir, posix.strerror(err_num)),
624 blame_loc=arg_loc)
625 return 1
626
627 state.ExportGlobalString(self.mem, 'PWD', real_dest_dir)
628
629 # WEIRD: We need a copy that is NOT PWD, because the user could mutate PWD.
630 # Other shells use global variables.
631 self.mem.SetPwd(real_dest_dir)
632
633 block = typed_args.GetOneBlock(cmd_val.typed_args)
634 if block:
635 out_errs = [] # type: List[bool]
636 with ctx_CdBlock(self.dir_stack, real_dest_dir, self.mem,
637 self.errfmt, out_errs):
638 unused = self.cmd_ev.EvalBlock(block)
639 if len(out_errs):
640 return 1
641
642 else: # No block
643 state.ExportGlobalString(self.mem, 'OLDPWD', pwd)
644 self.dir_stack.Replace(real_dest_dir) # for pushd/popd/dirs
645
646 return 0
647
648
649 WITH_LINE_NUMBERS = 1
650 WITHOUT_LINE_NUMBERS = 2
651 SINGLE_LINE = 3
652
653
654 def _PrintDirStack(dir_stack, style, home_dir):
655 # type: (DirStack, int, Optional[str]) -> None
656 """ Helper for 'dirs' builtin """
657
658 if style == WITH_LINE_NUMBERS:
659 for i, entry in enumerate(dir_stack.Iter()):
660 print('%2d %s' % (i, ui.PrettyDir(entry, home_dir)))
661
662 elif style == WITHOUT_LINE_NUMBERS:
663 for entry in dir_stack.Iter():
664 print(ui.PrettyDir(entry, home_dir))
665
666 elif style == SINGLE_LINE:
667 parts = [ui.PrettyDir(entry, home_dir) for entry in dir_stack.Iter()]
668 s = ' '.join(parts)
669 print(s)
670
671
672 class Pushd(vm._Builtin):
673 def __init__(self, mem, dir_stack, errfmt):
674 # type: (Mem, DirStack, ErrorFormatter) -> None
675 self.mem = mem
676 self.dir_stack = dir_stack
677 self.errfmt = errfmt
678
679 def Run(self, cmd_val):
680 # type: (cmd_value.Argv) -> int
681 _, arg_r = flag_spec.ParseCmdVal('pushd', cmd_val)
682
683 dir_arg, dir_arg_loc = arg_r.Peek2()
684 if dir_arg is None:
685 # TODO: It's suppose to try another dir before doing this?
686 self.errfmt.Print_('pushd: no other directory')
687 # bash oddly returns 1, not 2
688 return 1
689
690 arg_r.Next()
691 extra, extra_loc = arg_r.Peek2()
692 if extra is not None:
693 e_usage('got too many arguments', extra_loc)
694
695 # TODO: 'cd' uses normpath? Is that inconsistent?
696 dest_dir = os_path.abspath(dir_arg)
697 err_num = pyos.Chdir(dest_dir)
698 if err_num != 0:
699 self.errfmt.Print_("pushd: %r: %s" %
700 (dest_dir, posix.strerror(err_num)),
701 blame_loc=dir_arg_loc)
702 return 1
703
704 self.dir_stack.Push(dest_dir)
705 _PrintDirStack(self.dir_stack, SINGLE_LINE,
706 state.MaybeString(self.mem, 'HOME'))
707 state.ExportGlobalString(self.mem, 'PWD', dest_dir)
708 self.mem.SetPwd(dest_dir)
709 return 0
710
711
712 def _PopDirStack(label, mem, dir_stack, errfmt, out_errs):
713 # type: (str, Mem, DirStack, ErrorFormatter, List[bool]) -> bool
714 """ Helper for popd and cd { ... } """
715 dest_dir = dir_stack.Pop()
716 if dest_dir is None:
717 errfmt.Print_('%s: directory stack is empty' % label)
718 out_errs.append(True) # "return" to caller
719 return False
720
721 err_num = pyos.Chdir(dest_dir)
722 if err_num != 0:
723 # Happens if a directory is deleted in pushing and popping
724 errfmt.Print_('%s: %r: %s' % (label, dest_dir, posix.strerror(err_num)))
725 out_errs.append(True) # "return" to caller
726 return False
727
728 state.SetGlobalString(mem, 'PWD', dest_dir)
729 mem.SetPwd(dest_dir)
730 return True
731
732
733 class Popd(vm._Builtin):
734 def __init__(self, mem, dir_stack, errfmt):
735 # type: (Mem, DirStack, ErrorFormatter) -> None
736 self.mem = mem
737 self.dir_stack = dir_stack
738 self.errfmt = errfmt
739
740 def Run(self, cmd_val):
741 # type: (cmd_value.Argv) -> int
742 _, arg_r = flag_spec.ParseCmdVal('pushd', cmd_val)
743
744 extra, extra_loc = arg_r.Peek2()
745 if extra is not None:
746 e_usage('got extra argument', extra_loc)
747
748 out_errs = [] # type: List[bool]
749 _PopDirStack('popd', self.mem, self.dir_stack, self.errfmt, out_errs)
750 if len(out_errs):
751 return 1 # error
752
753 _PrintDirStack(self.dir_stack, SINGLE_LINE,
754 state.MaybeString(self.mem, ('HOME')))
755 return 0
756
757
758 class Dirs(vm._Builtin):
759 def __init__(self, mem, dir_stack, errfmt):
760 # type: (Mem, DirStack, ErrorFormatter) -> None
761 self.mem = mem
762 self.dir_stack = dir_stack
763 self.errfmt = errfmt
764
765 def Run(self, cmd_val):
766 # type: (cmd_value.Argv) -> int
767 attrs, arg_r = flag_spec.ParseCmdVal('dirs', cmd_val)
768 arg = arg_types.dirs(attrs.attrs)
769
770 home_dir = state.MaybeString(self.mem, 'HOME')
771 style = SINGLE_LINE
772
773 # Following bash order of flag priority
774 if arg.l:
775 home_dir = None # disable pretty ~
776 if arg.c:
777 self.dir_stack.Reset()
778 return 0
779 elif arg.v:
780 style = WITH_LINE_NUMBERS
781 elif arg.p:
782 style = WITHOUT_LINE_NUMBERS
783
784 _PrintDirStack(self.dir_stack, style, home_dir)
785 return 0
786
787
788 class Pwd(vm._Builtin):
789 """
790 NOTE: pwd doesn't just call getcwd(), which returns a "physical" dir (not a
791 symlink).
792 """
793
794 def __init__(self, mem, errfmt):
795 # type: (Mem, ErrorFormatter) -> None
796 self.mem = mem
797 self.errfmt = errfmt
798
799 def Run(self, cmd_val):
800 # type: (cmd_value.Argv) -> int
801 attrs, arg_r = flag_spec.ParseCmdVal('pwd', cmd_val)
802 arg = arg_types.pwd(attrs.attrs)
803
804 # NOTE: 'pwd' will succeed even if the directory has disappeared. Other
805 # shells behave that way too.
806 pwd = self.mem.pwd
807
808 # '-L' is the default behavior; no need to check it
809 # TODO: ensure that if multiple flags are provided, the *last* one overrides
810 # the others
811 if arg.P:
812 pwd = libc.realpath(pwd)
813 print(pwd)
814 return 0
815
816
817 # Needs a different _ResourceLoader to translate
818 class Help(vm._Builtin):
819 def __init__(self, lang, loader, help_data, errfmt):
820 # type: (str, _ResourceLoader, Dict[str, str], ErrorFormatter) -> None
821 self.lang = lang
822 self.loader = loader
823 self.help_data = help_data
824 self.errfmt = errfmt
825 self.version_str = pyutil.GetVersion(self.loader)
826 self.f = mylib.Stdout()
827
828 def _ShowTopic(self, topic_id, blame_loc):
829 # type: (str, loc_t) -> int
830
831 prefix = 'https://www.oilshell.org/release'
832
833 # For local preview
834 if 0:
835 prefix = 'file:///home/andy/git/oilshell/oil/_release'
836 self.version_str = 'VERSION'
837
838 chapter_name = self.help_data.get(topic_id)
839
840 # If we have a chapter name, it's not embedded in the binary. So just
841 # print the URL.
842 if chapter_name is not None:
843 util.PrintTopicHeader(topic_id, self.f)
844 print(' %s/%s/doc/ref/chap-%s.html#%s' % (prefix,
845 self.version_str,
846 chapter_name,
847 topic_id))
848 return 0
849
850 found = util.PrintEmbeddedHelp(self.loader, topic_id, self.f)
851 if not found:
852 # Notes:
853 # 1. bash suggests:
854 # man -k zzz
855 # info zzz
856 # help help
857 # We should do something smarter.
858
859 # 2. This also happens on 'build/dev.sh minimal', which isn't quite
860 # accurate. We don't have an exact list of help topics!
861
862 # 3. This is mostly an interactive command. Is it obnoxious to
863 # quote the line of code?
864 self.errfmt.Print_('no help topics match %r' % topic_id, blame_loc)
865 return 1
866
867 return 0
868
869 def Run(self, cmd_val):
870 # type: (cmd_value.Argv) -> int
871
872 attrs, arg_r = flag_spec.ParseCmdVal('help', cmd_val)
873 #arg = arg_types.help(attrs.attrs)
874
875 topic_id, blame_loc = arg_r.Peek2()
876 if topic_id is None:
877 found = self._ShowTopic('help', blame_loc) == 0
878 assert found
879
880 # e.g. ysh-chapters
881 found = self._ShowTopic('%s-chapters' % self.lang, blame_loc) == 0
882 assert found
883
884 print('All docs: https://www.oilshell.org/release/%s/doc/' %
885 self.version_str)
886 print('')
887
888 return 0
889 else:
890 arg_r.Next()
891
892 return self._ShowTopic(topic_id, blame_loc)
893
894
895 class Cat(vm._Builtin):
896 """Internal implementation detail for $(< file).
897
898 Maybe expose this as 'builtin cat' ?
899 """
900
901 def __init__(self):
902 # type: () -> None
903 """Empty constructor for mycpp."""
904 vm._Builtin.__init__(self)
905
906 def Run(self, cmd_val):
907 # type: (cmd_value.Argv) -> int
908 chunks = [] # type: List[str]
909 while True:
910 n, err_num = pyos.Read(0, 4096, chunks)
911
912 if n < 0:
913 if err_num == EINTR:
914 pass # retry
915 else:
916 # Like the top level IOError handler
917 e_die_status(2,
918 'osh I/O error: %s' % posix.strerror(err_num))
919 # TODO: Maybe just return 1?
920
921 elif n == 0: # EOF
922 break
923
924 else:
925 # Stream it to stdout
926 assert len(chunks) == 1
927 mylib.Stdout().write(chunks[0])
928 chunks.pop()
929
930 return 0