Skip to content

Commit

Permalink
[interactive] Initial implementation of $PS1.
Browse files Browse the repository at this point in the history
Also implement ${x@P} so we can test the PS1 evaluation.

Co-authored-by: okay <okayzed@users.noreply.github.com>
  • Loading branch information
Andy Chu and okayzed committed Oct 13, 2018
1 parent ffe3e73 commit 46dd613
Show file tree
Hide file tree
Showing 13 changed files with 343 additions and 24 deletions.
15 changes: 9 additions & 6 deletions bin/oil.py
Expand Up @@ -86,10 +86,6 @@ def _tlog(msg):
_tlog('after imports')


# bash --noprofile --norc uses 'bash-4.3$ '
OSH_PS1 = 'osh$ '


def _ShowVersion():
util.ShowAppVersion('Oil')

Expand All @@ -115,6 +111,9 @@ def _ShowVersion():
OSH_SPEC.LongFlag('--parser-mem-dump', args.Str)
OSH_SPEC.LongFlag('--runtime-mem-dump', args.Str)

# For bash compatibility
OSH_SPEC.LongFlag('--norc')

builtin.AddOptionsToArgSpec(OSH_SPEC)


Expand Down Expand Up @@ -197,22 +196,26 @@ def OshMain(argv0, argv, login_shell):
if e.errno != errno.ENOENT:
raise

# Needed in non-interactive shells for @P
prompt = ui.Prompt(arena, parse_ctx, ex)
ui.PROMPT = prompt

if opts.c is not None:
arena.PushSource('<command string>')
line_reader = reader.StringLineReader(opts.c, arena)
if opts.i: # -c and -i can be combined
exec_opts.interactive = True
elif opts.i: # force interactive
arena.PushSource('<stdin -i>')
line_reader = reader.InteractiveLineReader(OSH_PS1, arena)
line_reader = reader.InteractiveLineReader(arena, prompt)
exec_opts.interactive = True
else:
try:
script_name = arg_r.Peek()
except IndexError:
if sys.stdin.isatty():
arena.PushSource('<interactive>')
line_reader = reader.InteractiveLineReader(OSH_PS1, arena)
line_reader = reader.InteractiveLineReader(arena, prompt)
exec_opts.interactive = True
else:
arena.PushSource('<stdin>')
Expand Down
14 changes: 14 additions & 0 deletions core/id_kind.py
Expand Up @@ -258,6 +258,15 @@ def AddKinds(spec):
('Plus', '+' ),
])

# Statically parse @P, so @x etc. is an error.
spec.AddKindPairs('VOp0', [
('Q', '@Q'), # ${x@Q} for quoting
('E', '@E'),
('P', '@P'), # ${PS1@P} for prompt eval
('A', '@A'),
('a', '@a'),
])

# String removal ops
spec.AddKindPairs('VOp1', [
('Percent', '%' ),
Expand Down Expand Up @@ -377,6 +386,11 @@ def AddKinds(spec):
'Eof',
])

# For parsing prompt strings like PS1.
spec.AddKind('PS', [
'Subst', 'Octal3', 'LBrace', 'RBrace', 'Literals', 'BadBackslash',
])


# Shared between [[ and test/[.
_UNARY_STR_CHARS = 'zn' # -z -n
Expand Down
1 change: 1 addition & 0 deletions core/lexer_gen.py
Expand Up @@ -356,6 +356,7 @@ def main(argv):
TranslateOshLexer(lex.LEXER_DEF)
TranslateSimpleLexer('MatchEchoToken', lex.ECHO_E_DEF)
TranslateSimpleLexer('MatchGlobToken', lex.GLOB_DEF)
TranslateSimpleLexer('MatchPS1Token', lex.PS1_DEF)
TranslateRegexToPredicate(lex.VAR_NAME_RE, 'IsValidVarName')
TranslateRegexToPredicate(pretty.PLAIN_WORD_RE, 'IsPlainWord')

Expand Down
19 changes: 10 additions & 9 deletions core/reader.py
Expand Up @@ -10,6 +10,7 @@
"""

import cStringIO
import sys

from core import util
log = util.log
Expand Down Expand Up @@ -39,27 +40,27 @@ def Reset(self):

_PS2 = '> '


class InteractiveLineReader(_Reader):
def __init__(self, ps1, arena):
def __init__(self, arena, prompt):
_Reader.__init__(self, arena)
self.ps1 = ps1
self.prompt_str = ps1
self.prompt = prompt
self.prompt_str = ''
self.Reset() # initialize self.prompt_str

def _GetLine(self):
sys.stderr.write(self.prompt_str)
try:
ret = raw_input(self.prompt_str) + '\n' # newline required
ret = raw_input('') + '\n' # newline required
except EOFError:
ret = None
self.prompt_str = _PS2
self.prompt_str = _PS2 # TODO: Do we need $PS2? Would be easy.
return ret

def Reset(self):
"""Call this after command execution, to free memory taken up by the lines,
and reset prompt string back to PS1.
"""
self.prompt_str = self.ps1
# free vector...
self.prompt_str = self.prompt.PS1()


class FileLineReader(_Reader):
Expand Down Expand Up @@ -97,7 +98,7 @@ def StringLineReader(s, arena):

class VirtualLineReader(_Reader):
"""Read from lines we already read from the OS.
Used for here docs and aliases.
"""

Expand Down
123 changes: 121 additions & 2 deletions core/ui.py
Expand Up @@ -10,14 +10,24 @@
"""
from __future__ import print_function

import os
import pwd
import socket # socket.gethostname()
import sys

from asdl import const
from asdl import encode
from asdl import format as fmt
from core import dev
from osh import ast_lib
from osh.meta import ast
from osh import match
from osh.meta import ast, runtime, Id

value_e = runtime.value_e


# bash --noprofile --norc uses 'bash-4.3$ '
DEFAULT_PS1 = 'osh$ '


def Clear():
Expand Down Expand Up @@ -70,7 +80,6 @@ def Write(self, msg, *args):


class TestStatusLine(object):

def __init__(self):
pass

Expand All @@ -81,6 +90,116 @@ def Write(self, msg, *args):
print('\t' + msg)


#
# Prompt handling
#

# Global instance set by main(). TODO: Use dependency injection.
PROMPT = None

# NOTE: word_compile._ONE_CHAR has some of the same stuff.
_ONE_CHAR = {
'a' : '\a',
'e' : '\x1b',
'\\' : '\\',
}


def _GetCurrentUserName():
uid = os.getuid() # Does it make sense to cache this somewhere?
try:
e = pwd.getpwuid(uid)
except KeyError:
return "<ERROR: Couldn't determine user name for uid %d>" % uid
else:
return e.pw_name


class Prompt(object):
def __init__(self, arena, parse_ctx, ex):
self.arena = arena
self.parse_ctx = parse_ctx
self.ex = ex

self.parse_cache = {} # PS1 value -> CompoundWord.

def _ReplaceBackslashCodes(self, s):
ret = []
non_printing = 0
for id_, value in match.PS1_LEXER.Tokens(s):
# BadBacklash means they should have escaped with \\, but we can't
# make this an error.
if id_ in (Id.PS_Literals, Id.PS_BadBackslash):
ret.append(value)

elif id_ == Id.PS_Octal3:
i = int(value[1:], 8)
ret.append(chr(i % 256))

elif id_ == Id.PS_LBrace:
non_printing += 1

elif id_ == Id.PS_RBrace:
non_printing -= 1

elif id_ == Id.PS_Subst: # \u \h \w etc.
char = value[1:]
if char == 'u':
r = _GetCurrentUserName()

elif char == 'h':
r = socket.gethostname()

elif char == 'w':
val = self.ex.mem.GetVar('PWD')
if val.tag == value_e.Str:
r = val.s
else:
r = '<Error: PWD is not a string>'

elif char in _ONE_CHAR:
r = _ONE_CHAR[char]

else:
raise NotImplementedError(char)

ret.append(r)

else:
raise AssertionError('Invalid token %r' % id_)

return ''.join(ret)

def PS1(self):
val = self.ex.mem.GetVar('PS1')
return self.EvalPS1(val)

def EvalPS1(self, val):
if val.tag != value_e.Str:
return DEFAULT_PS1

ps1_str = val.s

# NOTE: This is copied from the PS4 logic in Tracer.
try:
ps1_word = self.parse_cache[ps1_str]
except KeyError:
w_parser = self.parse_ctx.MakeWordParserForPlugin(ps1_str, self.arena)

try:
ps1_word = w_parser.ReadPS()
except Exception as e:
error_str = '<ERROR: cannot parse PS1>'
t = ast.token(Id.Lit_Chars, error_str, const.NO_INTEGER)
ps1_word = ast.CompoundWord([ast.LiteralPart(t)])

self.parse_cache[ps1_str] = ps1_word

# e.g. "${debian_chroot}\u" -> '\u'
val2 = self.ex.word_ev.EvalWordToString(ps1_word)
return self._ReplaceBackslashCodes(val2.s)


def PrintFilenameAndLine(span_id, arena, f=sys.stderr):
line_span = arena.GetLineSpan(span_id)
line_id = line_span.line_id
Expand Down
17 changes: 14 additions & 3 deletions core/word_eval.py
Expand Up @@ -7,13 +7,14 @@

from core import braces
from core import expr_eval
from core import libstr
from core import glob_
from core import libstr
from core import state
from core import word_compile
from core import ui
from core import util

from osh.meta import Id, Kind, LookupKind, ast, runtime
from osh.meta import ast, runtime, Id, Kind, LookupKind
from osh import match

word_e = ast.word_e
Expand Down Expand Up @@ -137,6 +138,7 @@ def __init__(self, mem, exec_opts, splitter, arena):
self.mem = mem # for $HOME, $1, etc.
self.exec_opts = exec_opts # for nounset
self.splitter = splitter

self.globber = glob_.Globber(exec_opts)
# NOTE: Executor also instantiates one.
self.arith_ev = expr_eval.ArithEvaluator(mem, exec_opts, self, arena)
Expand Down Expand Up @@ -557,7 +559,16 @@ def _EvalBracedVarSub(self, part, part_vals, quoted):

elif part.suffix_op:
op = part.suffix_op
if op.tag == suffix_op_e.StringUnary:
if op.tag == suffix_op_e.StringNullary:
if op.op_id == Id.VOp0_P:
# TODO: Use dependency injection
#val = self.prompt._EvalPS1(val)
prompt = ui.PROMPT.EvalPS1(val)
val = runtime.Str(prompt)
else:
raise NotImplementedError(op.op_id)

elif op.tag == suffix_op_e.StringUnary:
if LookupKind(part.suffix_op.op_id) == Kind.VTest:
# TODO: Change style to:
# if self._ApplyTestOp(...)
Expand Down
27 changes: 27 additions & 0 deletions native/fastlex.c
Expand Up @@ -100,6 +100,31 @@ fastlex_MatchGlobToken(PyObject *self, PyObject *args) {
return Py_BuildValue("(ii)", id, end_pos);
}

static PyObject *
fastlex_MatchPS1Token(PyObject *self, PyObject *args) {
unsigned char* line;
int line_len;

int start_pos;
if (!PyArg_ParseTuple(args, "s#i", &line, &line_len, &start_pos)) {
return NULL;
}

// Bounds checking.
if (start_pos > line_len) {
PyErr_Format(PyExc_ValueError,
"Invalid MatchPS1Token call (start_pos = %d, line_len = %d)",
start_pos, line_len);
return NULL;
}

int id;
int end_pos;
MatchPS1Token(line, line_len, start_pos, &id, &end_pos);
return Py_BuildValue("(ii)", id, end_pos);
}


static PyObject *
fastlex_IsValidVarName(PyObject *self, PyObject *args) {
const char *name;
Expand Down Expand Up @@ -130,6 +155,8 @@ static PyMethodDef methods[] = {
"(line, start_pos) -> (id, end_pos)."},
{"MatchGlobToken", fastlex_MatchGlobToken, METH_VARARGS,
"(line, start_pos) -> (id, end_pos)."},
{"MatchPS1Token", fastlex_MatchPS1Token, METH_VARARGS,
"(line, start_pos) -> (id, end_pos)."},
{"IsValidVarName", fastlex_IsValidVarName, METH_VARARGS,
"Is it a valid var name?"},
{"IsPlainWord", fastlex_IsPlainWord, METH_VARARGS,
Expand Down

0 comments on commit 46dd613

Please sign in to comment.