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