Skip to content

Commit

Permalink
[osh] Implement ${a[@]@p} and make ${a[@]@A} and ${a[@]@q} cons…
Browse files Browse the repository at this point in the history
…istent with Bash (#2210)

* [osh refactor] make prompt.EvalPrompt receive str
* [osh] Fix `${a[@]@q}` connecting words
* [osh] Fix `${a[@]@A}` to generate an attribute for each element
* [osh] Implement `${a[@]@p}`
* [spec/var-op-bash] Turn osh's var-op-bash#15 result to FAIL
  * type query on $@ is a known difference for now
  • Loading branch information
akinomyoga authored Jan 2, 2025
1 parent eadacc6 commit 1485501
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 46 deletions.
21 changes: 11 additions & 10 deletions osh/prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,22 +242,18 @@ def _ReplaceBackslashCodes(self, tokens):

return ''.join(ret)

def EvalPrompt(self, UP_val):
# type: (value_t) -> str
def EvalPrompt(self, s):
# type: (str) -> str
"""Perform the two evaluations that bash does.
Used by $PS1 and ${x@P}.
"""
if UP_val.tag() != value_e.Str:
return '' # e.g. if the user does 'unset PS1'

val = cast(value.Str, UP_val)

# Parse backslash escapes (cached)
tokens = self.tokens_cache.get(val.s)
tokens = self.tokens_cache.get(s)
if tokens is None:
tokens = match.Ps1Tokens(val.s)
self.tokens_cache[val.s] = tokens
tokens = match.Ps1Tokens(s)
self.tokens_cache[s] = tokens

# Replace values.
ps1_str = self._ReplaceBackslashCodes(tokens)
Expand Down Expand Up @@ -304,7 +300,12 @@ def EvalFirstPrompt(self):
# Now try evaluating $PS1
ps1_val = self.mem.env_config.GetVal('PS1')
#log('ps1_val %s', ps1_val)
return self.EvalPrompt(ps1_val)
UP_ps1_val = ps1_val
if UP_ps1_val.tag() == value_e.Str:
ps1_val = cast(value.Str, UP_ps1_val)
return self.EvalPrompt(ps1_val.s)
else:
return '' # e.g. if the user does 'unset PS1'


PROMPT_COMMAND = 'PROMPT_COMMAND'
Expand Down
10 changes: 4 additions & 6 deletions osh/prompt_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

import unittest

from _devbuild.gen.value_asdl import value
from core import state
from core import test_lib
from frontend import match
Expand All @@ -25,21 +24,20 @@ def setUp(self):

def testEvaluator(self):
# Regression for caching bug!
self.assertEqual('foo', self.p.EvalPrompt(value.Str('foo')))
self.assertEqual('foo', self.p.EvalPrompt(value.Str('foo')))
self.assertEqual('foo', self.p.EvalPrompt('foo'))
self.assertEqual('foo', self.p.EvalPrompt('foo'))

def testNoEscapes(self):
for prompt_str in ["> ", "osh>", "[[]][[]][][]]][["]:
self.assertEqual(self.p.EvalPrompt(value.Str(prompt_str)),
prompt_str)
self.assertEqual(self.p.EvalPrompt(prompt_str), prompt_str)

def testValidEscapes(self):
for prompt_str in [
"\[\033[01;34m\]user\[\033[00m\] >", r"\[\]\[\]\[\]",
r"\[\] hi \[hi\] \[\] hello"
]:
self.assertEqual(
self.p.EvalPrompt(value.Str(prompt_str)),
self.p.EvalPrompt(prompt_str),
prompt_str.replace(r"\[", "\x01").replace(r"\]", "\x02"))

def testInvalidEscapes(self):
Expand Down
44 changes: 38 additions & 6 deletions osh/word_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -1048,7 +1048,7 @@ def _Slice(self, val, op, var_name, part):
return val

def _Nullary(self, val, op, var_name, vsub_token, vsub_state):
# type: (value_t, Token, Optional[str], Token, VarSubState) -> Tuple[value.Str, bool]
# type: (value_t, Token, Optional[str], Token, VarSubState) -> Tuple[value_t, bool]

quoted2 = False
op_id = op.id
Expand All @@ -1057,13 +1057,31 @@ def _Nullary(self, val, op, var_name, vsub_token, vsub_state):
UP_val = val
with tagswitch(val) as case:
if case(value_e.Undef):
result = value.Str('')
result = value.Str('') # type: value_t
elif case(value_e.Str):
str_val = cast(value.Str, UP_val)
prompt = self.prompt_ev.EvalPrompt(str_val)
prompt = self.prompt_ev.EvalPrompt(str_val.s)
# readline gets rid of these, so we should too.
p = prompt.replace('\x01', '').replace('\x02', '')
result = value.Str(p)
elif case(value_e.BashArray, value_e.BashAssoc):
if val.tag() == value_e.BashArray:
val = cast(value.BashArray, UP_val)
values = [
s for s in bash_impl.BashArray_GetValues(val)
if s is not None
]
elif val.tag() == value_e.BashAssoc:
val = cast(value.BashAssoc, UP_val)
values = bash_impl.BashAssoc_GetValues(val)
else:
raise AssertionError()

tmp = [
self.prompt_ev.EvalPrompt(s).replace(
'\x01', '').replace('\x02', '') for s in values
]
result = value.BashArray(tmp)
else:
e_die("Can't use @P on %s" % ui.ValType(val), op)

Expand All @@ -1078,7 +1096,10 @@ def _Nullary(self, val, op, var_name, vsub_token, vsub_state):
self._ProcessUndef(val, vsub_token, vsub_state)

# For unset variables, we do not generate any quoted words.
result = value.Str('')
if vsub_state.array_ref is not None:
result = value.BashArray([])
else:
result = value.Str('')

elif case(value_e.Str):
str_val = cast(value.Str, UP_val)
Expand All @@ -1100,7 +1121,7 @@ def _Nullary(self, val, op, var_name, vsub_token, vsub_state):
# TODO: should use fastfunc.ShellEncode
j8_lite.MaybeShellEncode(s) for s in values
]
result = value.Str(' '.join(tmp))
result = value.BashArray(tmp)
else:
e_die("Can't use @Q on %s" % ui.ValType(val), op)

Expand All @@ -1126,7 +1147,18 @@ def _Nullary(self, val, op, var_name, vsub_token, vsub_state):
if cell.nameref:
chars.append('n')

result = value.Str(''.join(chars))
count = 1
with tagswitch(val) as case:
if case(value_e.Undef):
count = 0
elif case(value_e.BashArray):
val = cast(value.BashArray, UP_val)
count = bash_impl.BashArray_Count(val)
elif case(value_e.BashAssoc):
val = cast(value.BashAssoc, UP_val)
count = bash_impl.BashAssoc_Count(val)

result = value.BashArray([''.join(chars)] * count)

else:
e_die('Var op %r not implemented' % lexer.TokenVal(op), op)
Expand Down
6 changes: 0 additions & 6 deletions spec/prompt.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -234,12 +234,6 @@ status=0
x
status=0
## END
## OK osh STDOUT:
status=1
status=1
x
status=0
## END

#### default PS1
#flags='--norc --noprofile'
Expand Down
96 changes: 80 additions & 16 deletions spec/var-op-bash.test.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
## compare_shells: bash
## oils_failures_allowed: 7
## oils_failures_allowed: 8

#### Lower Case with , and ,,
x='ABC DEF'
Expand Down Expand Up @@ -297,13 +297,6 @@ status=0
'a' 'b\nc'
status=0

status=0
## END
## OK osh STDOUT:
status=1
a $'b\\nc'
status=0
a
status=0
## END

Expand All @@ -314,7 +307,7 @@ $SH -c 'declare -A A=(["x"]="y"); echo ${A@P} - ${A[@]@P}'
echo status=$?

# note: "y z" causes a bug!
$SH -c 'declare -A A=(["x"]="y"); echo ${A@Q} - ${A[@]@Q}'
$SH -c 'declare -A A=(["x"]="y"); echo ${A@Q} - ${A[@]@Q}' | sed 's/^- y$/- '\''y'\''/'
echo status=$?

$SH -c 'declare -A A=(["x"]=y); echo ${A@a} - ${A[@]@a}'
Expand All @@ -327,13 +320,6 @@ status=0
A - A
status=0
## END
## OK osh STDOUT:
status=1
- y
status=0
A - A
status=0
## END

#### ${!var[@]@X}
# note: "y z" causes a bug!
Expand Down Expand Up @@ -472,3 +458,81 @@ a
A
A
## END


#### Array expansion with nullary var ops

declare -a a=({1..9})
declare -A A=(['a']=hello ['b']=world ['c']=osh ['d']=ysh)

echo "@Q"
argv.py "${a[@]@Q}"
argv.py "${a[*]@Q}"
argv.py "${A[@]@Q}"
argv.py "${A[*]@Q}"
argv.py "${u[@]@Q}"
argv.py "${u[*]@Q}"

echo "@P"
argv.py "${a[@]@P}"
argv.py "${a[*]@P}"
argv.py "${A[@]@P}"
argv.py "${A[*]@P}"
argv.py "${u[@]@P}"
argv.py "${u[*]@P}"

echo "@a"
argv.py "${a[@]@a}"
argv.py "${a[*]@a}"
argv.py "${A[@]@a}"
argv.py "${A[*]@a}"
argv.py "${u[@]@a}"
argv.py "${u[*]@a}"

## STDOUT:
@Q
['1', '2', '3', '4', '5', '6', '7', '8', '9']
['1 2 3 4 5 6 7 8 9']
['hello', 'world', 'osh', 'ysh']
['hello world osh ysh']
[]
['']
@P
['1', '2', '3', '4', '5', '6', '7', '8', '9']
['1 2 3 4 5 6 7 8 9']
['hello', 'world', 'osh', 'ysh']
['hello world osh ysh']
[]
['']
@a
['a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a']
['a a a a a a a a a']
['A', 'A', 'A', 'A']
['A A A A']
[]
['']
## END

## OK bash STDOUT:
@Q
["'1'", "'2'", "'3'", "'4'", "'5'", "'6'", "'7'", "'8'", "'9'"]
["'1' '2' '3' '4' '5' '6' '7' '8' '9'"]
["'ysh'", "'osh'", "'world'", "'hello'"]
["'ysh' 'osh' 'world' 'hello'"]
[]
['']
@P
['1', '2', '3', '4', '5', '6', '7', '8', '9']
['1 2 3 4 5 6 7 8 9']
['ysh', 'osh', 'world', 'hello']
['ysh osh world hello']
[]
['']
@a
['a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a']
['a a a a a a a a a']
['A', 'A', 'A', 'A']
['A A A A']
[]
['']
## END
3 changes: 1 addition & 2 deletions spec/var-ref.test.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
## compare_shells: bash
## oils_failures_allowed: 1

# Var refs are done with ${!a}
#
Expand Down Expand Up @@ -744,7 +743,7 @@ test-op0 'a3[@]'
# []
# []

## BUG bash STDOUT:
## OK bash STDOUT:
==== v1 ====
["'value'"]
['value']
Expand Down

0 comments on commit 1485501

Please sign in to comment.