OILS / frontend / reader.py View on Github | oilshell.org

280 lines, 138 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 history
24 from osh import prompt
25 from frontend.py_readline import Readline
26
27_ = log
28
29_PS2 = '> '
30
31
32class _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
73class 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
86class 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
115def 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
124class 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
162def _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
187class 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