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