OILS
/
frontend
/
reader.py
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 mycpp import mylib
|
13 |
|
14 |
from core.error import p_die
|
15 |
|
16 |
from typing import Optional, Tuple, List, TYPE_CHECKING
|
17 |
if TYPE_CHECKING:
|
18 |
from _devbuild.gen.syntax_asdl import Token, SourceLine
|
19 |
from core.alloc import Arena
|
20 |
from core.comp_ui import PromptState
|
21 |
from osh import history
|
22 |
from osh import prompt
|
23 |
from frontend.py_readline import Readline
|
24 |
|
25 |
_PS2 = '> '
|
26 |
|
27 |
|
28 |
class _Reader(object):
|
29 |
def __init__(self, arena):
|
30 |
# type: (Arena) -> None
|
31 |
self.arena = arena
|
32 |
self.line_num = 1 # physical line numbers start from 1
|
33 |
|
34 |
def SetLineOffset(self, n):
|
35 |
# type: (int) -> None
|
36 |
"""For --location-line-offset."""
|
37 |
self.line_num = n
|
38 |
|
39 |
def _GetLine(self):
|
40 |
# type: () -> Optional[str]
|
41 |
raise NotImplementedError()
|
42 |
|
43 |
def GetLine(self):
|
44 |
# type: () -> Tuple[SourceLine, int]
|
45 |
line_str = self._GetLine()
|
46 |
if line_str is None:
|
47 |
eof_line = None # type: Optional[SourceLine]
|
48 |
return eof_line, 0
|
49 |
|
50 |
src_line = self.arena.AddLine(line_str, self.line_num)
|
51 |
self.line_num += 1
|
52 |
return src_line, 0
|
53 |
|
54 |
def Reset(self):
|
55 |
# type: () -> None
|
56 |
"""Called after command execution in main_loop.py."""
|
57 |
pass
|
58 |
|
59 |
def LastLineHint(self):
|
60 |
# type: () -> bool
|
61 |
"""A hint if we're on the last line, for optimization.
|
62 |
|
63 |
This is only for performance, not correctness.
|
64 |
"""
|
65 |
return False
|
66 |
|
67 |
|
68 |
class DisallowedLineReader(_Reader):
|
69 |
"""For CommandParser in YSH expressions."""
|
70 |
|
71 |
def __init__(self, arena, blame_token):
|
72 |
# type: (Arena, Token) -> None
|
73 |
_Reader.__init__(self, arena) # TODO: This arena is useless
|
74 |
self.blame_token = blame_token
|
75 |
|
76 |
def _GetLine(self):
|
77 |
# type: () -> Optional[str]
|
78 |
p_die("Here docs aren't allowed in expressions", self.blame_token)
|
79 |
|
80 |
|
81 |
class FileLineReader(_Reader):
|
82 |
"""For -c and stdin?"""
|
83 |
|
84 |
def __init__(self, f, arena):
|
85 |
# type: (mylib.LineReader, Arena) -> None
|
86 |
"""
|
87 |
Args:
|
88 |
lines: List of (line_id, line) pairs
|
89 |
"""
|
90 |
_Reader.__init__(self, arena)
|
91 |
self.f = f
|
92 |
self.last_line_hint = False
|
93 |
|
94 |
def _GetLine(self):
|
95 |
# type: () -> Optional[str]
|
96 |
line = self.f.readline()
|
97 |
if len(line) == 0:
|
98 |
return None
|
99 |
|
100 |
if not line.endswith('\n'):
|
101 |
self.last_line_hint = True
|
102 |
|
103 |
#from mycpp.mylib import log
|
104 |
#log('LINE %r', line)
|
105 |
return line
|
106 |
|
107 |
def LastLineHint(self):
|
108 |
# type: () -> bool
|
109 |
return self.last_line_hint
|
110 |
|
111 |
|
112 |
def StringLineReader(s, arena):
|
113 |
# type: (str, Arena) -> FileLineReader
|
114 |
return FileLineReader(mylib.BufLineReader(s), arena)
|
115 |
|
116 |
|
117 |
# TODO: Should be BufLineReader(Str)?
|
118 |
# This doesn't have to copy. It just has a pointer.
|
119 |
|
120 |
|
121 |
class VirtualLineReader(_Reader):
|
122 |
"""Read from lines we already read from the OS.
|
123 |
|
124 |
Used for here docs and aliases.
|
125 |
"""
|
126 |
|
127 |
def __init__(self, lines, arena):
|
128 |
# type: (List[Tuple[SourceLine, int]], Arena) -> None
|
129 |
"""
|
130 |
Args:
|
131 |
lines: List of (line_id, line) pairs
|
132 |
"""
|
133 |
_Reader.__init__(self, arena)
|
134 |
self.lines = lines
|
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 |
# NOTE: we return a partial line, but we also want the lexer to create
|
149 |
# tokens with the correct line_spans. So we have to tell it 'start_offset'
|
150 |
# as well.
|
151 |
return src_line, start_offset
|
152 |
|
153 |
|
154 |
def _readline_no_tty(prompt):
|
155 |
# type: (str) -> str
|
156 |
w = mylib.Stderr()
|
157 |
w.write(prompt)
|
158 |
w.flush()
|
159 |
|
160 |
line = mylib.Stdin().readline()
|
161 |
if line is None or len(line) == 0:
|
162 |
# empty string == EOF
|
163 |
raise EOFError()
|
164 |
|
165 |
return line
|
166 |
|
167 |
|
168 |
class InteractiveLineReader(_Reader):
|
169 |
def __init__(self, arena, prompt_ev, hist_ev, line_input, prompt_state):
|
170 |
# type: (Arena, prompt.Evaluator, history.Evaluator, Readline, PromptState) -> None
|
171 |
# TODO: Hook up PromptEvaluator and history.Evaluator when they have types.
|
172 |
"""
|
173 |
Args:
|
174 |
prompt_state: Current prompt is PUBLISHED here.
|
175 |
"""
|
176 |
_Reader.__init__(self, arena)
|
177 |
self.prompt_ev = prompt_ev
|
178 |
self.hist_ev = hist_ev
|
179 |
self.line_input = line_input # may be None!
|
180 |
self.prompt_state = prompt_state
|
181 |
|
182 |
self.prev_line = None # type: str
|
183 |
self.prompt_str = ''
|
184 |
|
185 |
self.Reset()
|
186 |
|
187 |
def Reset(self):
|
188 |
# type: () -> None
|
189 |
"""Called after command execution."""
|
190 |
self.render_ps1 = True
|
191 |
|
192 |
def _GetLine(self):
|
193 |
# type: () -> Optional[str]
|
194 |
|
195 |
# NOTE: In bash, the prompt goes to stderr, but this seems to cause drawing
|
196 |
# problems with readline? It needs to know about the prompt.
|
197 |
#sys.stderr.write(self.prompt_str)
|
198 |
|
199 |
if self.render_ps1:
|
200 |
self.prompt_str = self.prompt_ev.EvalFirstPrompt()
|
201 |
self.prompt_state.SetLastPrompt(self.prompt_str)
|
202 |
|
203 |
line = None # type: Optional[str]
|
204 |
try:
|
205 |
if not mylib.Stdout().isatty() or not mylib.Stdin().isatty():
|
206 |
line = _readline_no_tty(
|
207 |
self.prompt_str) + '\n' # newline required
|
208 |
else:
|
209 |
line = raw_input(self.prompt_str) + '\n' # newline required
|
210 |
except EOFError:
|
211 |
print('^D') # bash prints 'exit'; mksh prints ^D.
|
212 |
|
213 |
if line is not None:
|
214 |
# NOTE: Like bash, OSH does this on EVERY line in a multi-line command,
|
215 |
# which is confusing.
|
216 |
|
217 |
# Also, in bash this is affected by HISTCONTROL=erasedups. But I
|
218 |
# realized I don't like that behavior because it changes the numbers! I
|
219 |
# can't just remember a number -- I have to type 'hi' again.
|
220 |
line = self.hist_ev.Eval(line)
|
221 |
|
222 |
# Add the line if it's not EOL, not whitespace-only, not the same as the
|
223 |
# previous line, and we have line_input.
|
224 |
if (len(line.strip()) and line != self.prev_line and
|
225 |
self.line_input is not None):
|
226 |
self.line_input.add_history(
|
227 |
line.rstrip()) # no trailing newlines
|
228 |
self.prev_line = line
|
229 |
|
230 |
self.prompt_str = _PS2 # TODO: Do we need $PS2? Would be easy.
|
231 |
self.prompt_state.SetLastPrompt(self.prompt_str)
|
232 |
self.render_ps1 = False
|
233 |
return line
|