OILS / frontend / reader.py View on Github | oils.pub

273 lines, 134 significant
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"""
8reader.py - Read lines of input.
9"""
10from __future__ import print_function
11
12from _devbuild.gen.id_kind_asdl import Id
13from core.error import p_die
14from mycpp import iolib
15from mycpp import mylib
16from mycpp.mylib import log
17
18from typing import Optional, Tuple, List, TYPE_CHECKING
19if 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 prompt
24 from frontend.py_readline import Readline
25
26_ = log
27
28_PS2 = '> '
29
30
31class _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
72class 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
85class 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
114def 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
123class 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
161def _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 Raises
170 EOFError: on Ctrl-D
171 KeyboardInterrupt: on Ctrl-C
172 """
173 w = mylib.Stderr()
174 w.write(prompt)
175 w.flush()
176
177 line = mylib.Stdin().readline()
178 assert line is not None
179 if len(line) == 0:
180 # empty string == EOF
181 raise EOFError()
182
183 return line
184
185
186class InteractiveLineReader(_Reader):
187
188 def __init__(
189 self,
190 arena, # type: Arena
191 prompt_ev, # type: prompt.Evaluator
192 line_input, # type: Optional[Readline]
193 prompt_state, # type:PromptState
194 ):
195 # type: (...) -> None
196 """
197 Args:
198 prompt_state: Current prompt is PUBLISHED here.
199 """
200 _Reader.__init__(self, arena)
201 self.prompt_ev = prompt_ev
202 self.line_input = line_input
203 self.prompt_state = prompt_state
204
205 self.prev_line = None # type: Optional[str]
206 self.prompt_str = ''
207
208 self.Reset()
209
210 def Reset(self):
211 # type: () -> None
212 """Called after command execution."""
213 self.render_ps1 = True
214
215 def _ReadlinePromptInput(self):
216 # type: () -> str
217 if mylib.CPP:
218 line = self.line_input.prompt_input(self.prompt_str)
219 else:
220 # Hack to restore CPython's signal handling behavior while
221 # raw_input() is called.
222 #
223 # A cleaner way to do this would be to fork CPython's raw_input()
224 # so it handles EINTR. It's called in frontend/pyreadline.py
225 import signal
226
227 tmp = signal.signal(signal.SIGINT, iolib.gOrigSigIntHandler)
228 try:
229 line = self.line_input.prompt_input(self.prompt_str)
230 finally:
231 signal.signal(signal.SIGINT, tmp)
232 return line
233
234 def _GetLine(self):
235 # type: () -> Optional[str]
236
237 # NOTE: In bash, the prompt goes to stderr, but this seems to cause drawing
238 # problems with readline? It needs to know about the prompt.
239 #sys.stderr.write(self.prompt_str)
240
241 if self.render_ps1:
242 self.prompt_str = self.prompt_ev.EvalFirstPrompt()
243 self.prompt_state.SetLastPrompt(self.prompt_str)
244
245 line = None # type: Optional[str]
246 try:
247 # Note: Python/bltinmodule.c builtin_raw_input() has the isatty()
248 # logic, but doing it in Python reduces our C++ code
249 if (not self.line_input or not mylib.Stdout().isatty() or
250 not mylib.Stdin().isatty()):
251 line = _PlainPromptInput(self.prompt_str)
252 else:
253 line = self._ReadlinePromptInput()
254 except EOFError:
255 print('^D') # bash prints 'exit'; mksh prints ^D.
256
257 if line is not None:
258 # NOTE: Like bash, OSH does this on EVERY line in a multi-line command,
259 # which is confusing.
260
261 # Add the line if it's not EOL, not whitespace-only, not the same as the
262 # previous line, and we have line_input.
263 if (len(line.strip()) and line != self.prev_line and
264 self.line_input is not None):
265 # no trailing newlines
266 # TODO: need to rewrite this history entry with the expanded line at the parsing stage
267 self.line_input.add_history(line.rstrip())
268 self.prev_line = line
269
270 self.prompt_str = _PS2 # TODO: Do we need $PS2? Would be easy.
271 self.prompt_state.SetLastPrompt(self.prompt_str)
272 self.render_ps1 = False
273 return line