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
|