| 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 _devbuild.gen.id_kind_asdl import Id
|
| 13 | from core.error import p_die
|
| 14 | from mycpp import mylib
|
| 15 | from mycpp.mylib import log
|
| 16 |
|
| 17 | from typing import Optional, Tuple, List, TYPE_CHECKING
|
| 18 | if 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 |
|
| 31 | class _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 |
|
| 72 | class 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 |
|
| 85 | class 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 |
|
| 114 | def 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 |
|
| 123 | class 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 |
|
| 161 | def _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 |
|
| 182 | class 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
|