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

256 lines, 126 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 mylib
15from mycpp.mylib import log
16
17from typing import Optional, Tuple, List, TYPE_CHECKING
18if 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
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 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
182class 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