| 1 | # Copyright 2016 Andy Chu. All rights reserved.
 | 
| 2 | # Licensed under the Apache License, Version 2.0 (the "License");
 | 
| 3 | # you may not use this file except in compliance with the License.
 | 
| 4 | # You may obtain a copy of the License at
 | 
| 5 | #
 | 
| 6 | #   http://www.apache.org/licenses/LICENSE-2.0
 | 
| 7 | """
 | 
| 8 | reader.py - Read lines of input.
 | 
| 9 | """
 | 
| 10 | from __future__ import print_function
 | 
| 11 | 
 | 
| 12 | from _devbuild.gen.id_kind_asdl import Id
 | 
| 13 | from core.error import p_die
 | 
| 14 | from mycpp import mylib
 | 
| 15 | from mycpp.mylib import log
 | 
| 16 | 
 | 
| 17 | from typing import Optional, Tuple, List, TYPE_CHECKING
 | 
| 18 | if TYPE_CHECKING:
 | 
| 19 |     from _devbuild.gen.syntax_asdl import Token, SourceLine
 | 
| 20 |     from core.alloc import Arena
 | 
| 21 |     from core.comp_ui import PromptState
 | 
| 22 |     from osh import history
 | 
| 23 |     from osh import prompt
 | 
| 24 |     from frontend.py_readline import Readline
 | 
| 25 | 
 | 
| 26 | _ = log
 | 
| 27 | 
 | 
| 28 | _PS2 = '> '
 | 
| 29 | 
 | 
| 30 | 
 | 
| 31 | class _Reader(object):
 | 
| 32 | 
 | 
| 33 |     def __init__(self, arena):
 | 
| 34 |         # type: (Arena) -> None
 | 
| 35 |         self.arena = arena
 | 
| 36 |         self.line_num = 1  # physical line numbers start from 1
 | 
| 37 | 
 | 
| 38 |     def SetLineOffset(self, n):
 | 
| 39 |         # type: (int) -> None
 | 
| 40 |         """For --location-line-offset."""
 | 
| 41 |         self.line_num = n
 | 
| 42 | 
 | 
| 43 |     def _GetLine(self):
 | 
| 44 |         # type: () -> Optional[str]
 | 
| 45 |         raise NotImplementedError()
 | 
| 46 | 
 | 
| 47 |     def GetLine(self):
 | 
| 48 |         # type: () -> Tuple[SourceLine, int]
 | 
| 49 |         line_str = self._GetLine()
 | 
| 50 |         if line_str is None:
 | 
| 51 |             eof_line = None  # type: Optional[SourceLine]
 | 
| 52 |             return eof_line, 0
 | 
| 53 | 
 | 
| 54 |         src_line = self.arena.AddLine(line_str, self.line_num)
 | 
| 55 |         self.line_num += 1
 | 
| 56 |         return src_line, 0
 | 
| 57 | 
 | 
| 58 |     def Reset(self):
 | 
| 59 |         # type: () -> None
 | 
| 60 |         """Called after command execution in main_loop.py."""
 | 
| 61 |         pass
 | 
| 62 | 
 | 
| 63 |     def LastLineHint(self):
 | 
| 64 |         # type: () -> bool
 | 
| 65 |         """A hint if we're on the last line, for optimization.
 | 
| 66 | 
 | 
| 67 |         This is only for performance, not correctness.
 | 
| 68 |         """
 | 
| 69 |         return False
 | 
| 70 | 
 | 
| 71 | 
 | 
| 72 | class DisallowedLineReader(_Reader):
 | 
| 73 |     """For CommandParser in YSH expressions."""
 | 
| 74 | 
 | 
| 75 |     def __init__(self, arena, blame_token):
 | 
| 76 |         # type: (Arena, Token) -> None
 | 
| 77 |         _Reader.__init__(self, arena)  # TODO: This arena is useless
 | 
| 78 |         self.blame_token = blame_token
 | 
| 79 | 
 | 
| 80 |     def _GetLine(self):
 | 
| 81 |         # type: () -> Optional[str]
 | 
| 82 |         p_die("Here docs aren't allowed in expressions", self.blame_token)
 | 
| 83 | 
 | 
| 84 | 
 | 
| 85 | class FileLineReader(_Reader):
 | 
| 86 |     """For -c and stdin?"""
 | 
| 87 | 
 | 
| 88 |     def __init__(self, f, arena):
 | 
| 89 |         # type: (mylib.LineReader, Arena) -> None
 | 
| 90 |         """
 | 
| 91 |     Args:
 | 
| 92 |       lines: List of (line_id, line) pairs
 | 
| 93 |     """
 | 
| 94 |         _Reader.__init__(self, arena)
 | 
| 95 |         self.f = f
 | 
| 96 |         self.last_line_hint = False
 | 
| 97 | 
 | 
| 98 |     def _GetLine(self):
 | 
| 99 |         # type: () -> Optional[str]
 | 
| 100 |         line = self.f.readline()
 | 
| 101 |         if len(line) == 0:
 | 
| 102 |             return None
 | 
| 103 | 
 | 
| 104 |         if not line.endswith('\n'):
 | 
| 105 |             self.last_line_hint = True
 | 
| 106 | 
 | 
| 107 |         return line
 | 
| 108 | 
 | 
| 109 |     def LastLineHint(self):
 | 
| 110 |         # type: () -> bool
 | 
| 111 |         return self.last_line_hint
 | 
| 112 | 
 | 
| 113 | 
 | 
| 114 | def StringLineReader(s, arena):
 | 
| 115 |     # type: (str, Arena) -> FileLineReader
 | 
| 116 |     return FileLineReader(mylib.BufLineReader(s), arena)
 | 
| 117 | 
 | 
| 118 | 
 | 
| 119 | # TODO: Should be BufLineReader(Str)?
 | 
| 120 | # This doesn't have to copy.  It just has a pointer.
 | 
| 121 | 
 | 
| 122 | 
 | 
| 123 | class VirtualLineReader(_Reader):
 | 
| 124 |     """Allows re-reading from lines we already read from the OS.
 | 
| 125 | 
 | 
| 126 |     Used by here docs.
 | 
| 127 |     """
 | 
| 128 | 
 | 
| 129 |     def __init__(self, arena, lines, do_lossless):
 | 
| 130 |         # type: (Arena, List[Tuple[SourceLine, int]], bool) -> None
 | 
| 131 |         _Reader.__init__(self, arena)
 | 
| 132 |         self.lines = lines
 | 
| 133 |         self.do_lossless = do_lossless
 | 
| 134 | 
 | 
| 135 |         self.num_lines = len(lines)
 | 
| 136 |         self.pos = 0
 | 
| 137 | 
 | 
| 138 |     def GetLine(self):
 | 
| 139 |         # type: () -> Tuple[SourceLine, int]
 | 
| 140 |         if self.pos == self.num_lines:
 | 
| 141 |             eof_line = None  # type: Optional[SourceLine]
 | 
| 142 |             return eof_line, 0
 | 
| 143 | 
 | 
| 144 |         src_line, start_offset = self.lines[self.pos]
 | 
| 145 | 
 | 
| 146 |         self.pos += 1
 | 
| 147 | 
 | 
| 148 |         # Maintain lossless invariant for STRIPPED tabs: add a Token to the
 | 
| 149 |         # arena invariant, but don't refer to it.
 | 
| 150 |         if self.do_lossless:  # avoid garbage, doesn't affect correctness
 | 
| 151 |             if start_offset != 0:
 | 
| 152 |                 self.arena.NewToken(Id.Lit_CharsWithoutPrefix, start_offset, 0,
 | 
| 153 |                                     src_line)
 | 
| 154 | 
 | 
| 155 |         # NOTE: we return a partial line, but we also want the lexer to create
 | 
| 156 |         # tokens with the correct line_spans.  So we have to tell it 'start_offset'
 | 
| 157 |         # as well.
 | 
| 158 |         return src_line, start_offset
 | 
| 159 | 
 | 
| 160 | 
 | 
| 161 | def _PlainPromptInput(prompt):
 | 
| 162 |     # type: (str) -> str
 | 
| 163 |     """
 | 
| 164 |     Returns line WITH trailing newline, like Python's f.readline(), and unlike
 | 
| 165 |     raw_input() / GNU readline
 | 
| 166 | 
 | 
| 167 |     Same interface as readline.prompt_input().
 | 
| 168 |     """
 | 
| 169 |     w = mylib.Stderr()
 | 
| 170 |     w.write(prompt)
 | 
| 171 |     w.flush()
 | 
| 172 | 
 | 
| 173 |     line = mylib.Stdin().readline()
 | 
| 174 |     assert line is not None
 | 
| 175 |     if len(line) == 0:
 | 
| 176 |         # empty string == EOF
 | 
| 177 |         raise EOFError()
 | 
| 178 | 
 | 
| 179 |     return line
 | 
| 180 | 
 | 
| 181 | 
 | 
| 182 | class InteractiveLineReader(_Reader):
 | 
| 183 | 
 | 
| 184 |     def __init__(
 | 
| 185 |             self,
 | 
| 186 |             arena,  # type: Arena
 | 
| 187 |             prompt_ev,  # type: prompt.Evaluator
 | 
| 188 |             hist_ev,  # type: history.Evaluator
 | 
| 189 |             line_input,  # type: Optional[Readline]
 | 
| 190 |             prompt_state,  # type:PromptState
 | 
| 191 |     ):
 | 
| 192 |         # type: (...) -> None
 | 
| 193 |         """
 | 
| 194 |         Args:
 | 
| 195 |           prompt_state: Current prompt is PUBLISHED here.
 | 
| 196 |         """
 | 
| 197 |         _Reader.__init__(self, arena)
 | 
| 198 |         self.prompt_ev = prompt_ev
 | 
| 199 |         self.hist_ev = hist_ev
 | 
| 200 |         self.line_input = line_input
 | 
| 201 |         self.prompt_state = prompt_state
 | 
| 202 | 
 | 
| 203 |         self.prev_line = None  # type: str
 | 
| 204 |         self.prompt_str = ''
 | 
| 205 | 
 | 
| 206 |         self.Reset()
 | 
| 207 | 
 | 
| 208 |     def Reset(self):
 | 
| 209 |         # type: () -> None
 | 
| 210 |         """Called after command execution."""
 | 
| 211 |         self.render_ps1 = True
 | 
| 212 | 
 | 
| 213 |     def _GetLine(self):
 | 
| 214 |         # type: () -> Optional[str]
 | 
| 215 | 
 | 
| 216 |         # NOTE: In bash, the prompt goes to stderr, but this seems to cause drawing
 | 
| 217 |         # problems with readline?  It needs to know about the prompt.
 | 
| 218 |         #sys.stderr.write(self.prompt_str)
 | 
| 219 | 
 | 
| 220 |         if self.render_ps1:
 | 
| 221 |             self.prompt_str = self.prompt_ev.EvalFirstPrompt()
 | 
| 222 |             self.prompt_state.SetLastPrompt(self.prompt_str)
 | 
| 223 | 
 | 
| 224 |         line = None  # type: Optional[str]
 | 
| 225 |         try:
 | 
| 226 |             # Note: Python/bltinmodule.c builtin_raw_input() has the isatty()
 | 
| 227 |             # logic, but doing it in Python reduces our C++ code
 | 
| 228 |             if (not self.line_input or not mylib.Stdout().isatty() or
 | 
| 229 |                     not mylib.Stdin().isatty()):
 | 
| 230 |                 line = _PlainPromptInput(self.prompt_str)
 | 
| 231 |             else:
 | 
| 232 |                 line = self.line_input.prompt_input(self.prompt_str)
 | 
| 233 |         except EOFError:
 | 
| 234 |             print('^D')  # bash prints 'exit'; mksh prints ^D.
 | 
| 235 | 
 | 
| 236 |         if line is not None:
 | 
| 237 |             # NOTE: Like bash, OSH does this on EVERY line in a multi-line command,
 | 
| 238 |             # which is confusing.
 | 
| 239 | 
 | 
| 240 |             # Also, in bash this is affected by HISTCONTROL=erasedups.  But I
 | 
| 241 |             # realized I don't like that behavior because it changes the numbers!  I
 | 
| 242 |             # can't just remember a number -- I have to type 'hi' again.
 | 
| 243 |             line = self.hist_ev.Eval(line)
 | 
| 244 | 
 | 
| 245 |             # Add the line if it's not EOL, not whitespace-only, not the same as the
 | 
| 246 |             # previous line, and we have line_input.
 | 
| 247 |             if (len(line.strip()) and line != self.prev_line and
 | 
| 248 |                     self.line_input is not None):
 | 
| 249 |                 # no trailing newlines
 | 
| 250 |                 self.line_input.add_history(line.rstrip())
 | 
| 251 |                 self.prev_line = line
 | 
| 252 | 
 | 
| 253 |         self.prompt_str = _PS2  # TODO: Do we need $PS2?  Would be easy.
 | 
| 254 |         self.prompt_state.SetLastPrompt(self.prompt_str)
 | 
| 255 |         self.render_ps1 = False
 | 
| 256 |         return line
 |