Skip to content

Commit 22ea84e

Browse files
author
Andy Chu
committed
[history] Avoid conflict with ${!indirect} and [!charclass].
Add unit tests for conflicts with $!x and !(foo|bar). This is a simpler hack than the one bash has. Fixes issue #264. Also: - Fix unit tests broken by the PWD change.
1 parent dce8a09 commit 22ea84e

File tree

6 files changed

+49
-7
lines changed

6 files changed

+49
-7
lines changed

frontend/lex.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -610,7 +610,11 @@ def IsKeyword(name):
610610
# Search by prefix of substring (optional '?').
611611
# NOTE: there are no numbers allowed here! Bash doesn't seem to support it.
612612
# No hyphen since it conflits with $-1 too.
613-
R(r'!\??[a-zA-Z_/.][0-9a-zA-Z_/.]+', Id.History_Search),
613+
#
614+
# Required trailing whitespace is there to avoid conflict with [!charclass]
615+
# and ${!indirect}. This is a simpler hack than the one bash has. See
616+
# frontend/lex_test.py.
617+
R(r'!\??[a-zA-Z_/.][0-9a-zA-Z_/.]+[ \t\r\n]', Id.History_Search),
614618

615619
# Single quoted, e.g. 'a' or $'\n'. Terminated by another single quote or
616620
# end of string.

frontend/lex_test.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,39 @@ def testHistoryLexer(self):
342342
# YES operator
343343
self.assert_(Id.History_Op in [tok_type for tok_type, _ in tokens])
344344

345+
def testHistoryDoesNotConflict(self):
346+
lex = match.HISTORY_LEXER
347+
348+
# https://github.com/oilshell/oil/issues/264
349+
#
350+
# Bash has a bunch of hacks to suppress the conflict between ! for history
351+
# and:
352+
#
353+
# 1. [!abc] globbing
354+
# 2. ${!foo} indirect expansion
355+
# 3. $!x -- the PID
356+
# 4. !(foo|bar) -- extended glob
357+
#
358+
# I guess [[ a != b ]] doesn't match the pattern in bash.
359+
360+
three_other = [Id.History_Other, Id.History_Other, Id.History_Other]
361+
two_other = [Id.History_Other, Id.History_Other]
362+
CASES = [
363+
(r'[!abc]', three_other),
364+
(r'${!indirect}', three_other),
365+
(r'$!x', three_other), # didn't need a special case
366+
(r'!(foo|bar)', two_other), # didn't need a special case
367+
]
368+
369+
for s, expected_types in CASES:
370+
tokens = list(lex.Tokens(s))
371+
print(tokens)
372+
actual_types = [id_ for id_, val in tokens]
373+
374+
self.assert_(Id.History_Search not in actual_types, tokens)
375+
376+
self.assertEqual(expected_types, actual_types)
377+
345378

346379
if __name__ == '__main__':
347380
unittest.main()

osh/builtin.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -666,8 +666,6 @@ def _PrintDirStack(dir_stack, style, home_dir):
666666
print(ui.PrettyDir(entry, home_dir))
667667

668668
elif style == SINGLE_LINE:
669-
# NOTE: Changed to list comprehension to avoid LOAD_CLOSURE/MAKE_CLOSURE.
670-
# TODO: Restore later?
671669
s = ' '.join(ui.PrettyDir(entry, home_dir) for entry in dir_stack.Iter())
672670
print(s)
673671

osh/history.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,11 @@ def Eval(self, line):
133133
raise util.HistoryError('%s: not found', val)
134134

135135
elif id_ == Id.History_Search:
136+
# Remove the required space at the end and save it. A simple hack than
137+
# the one bash has.
138+
last_char = val[-1]
139+
val = val[:-1]
140+
136141
# Search backward
137142
prefix = None
138143
substring = None
@@ -149,6 +154,7 @@ def Eval(self, line):
149154
if substring and substring in cmd:
150155
out = cmd
151156
if out is not None:
157+
out += last_char # restore required space
152158
break
153159

154160
if out is None:

osh/history_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,9 @@ def testReplacements(self):
5656
self.assertEqual('echo hi', hist_ev.Eval('echo hi'))
5757

5858
# Search for prefix
59-
self.assertEqual('echo ${two:-}', hist_ev.Eval('!echo'))
59+
self.assertEqual('echo ${two:-}\n', hist_ev.Eval('!echo\n'))
6060
# Search for substring
61-
self.assertEqual('echo ${two:-}', hist_ev.Eval('!?two'))
61+
self.assertEqual('echo ${two:-} ', hist_ev.Eval('!?two '))
6262

6363
# Indexes and negative indexes
6464
self.assertEqual('echo 1', hist_ev.Eval('!1'))

osh/state_test.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,8 @@ def testSetVarClearFlag(self):
9999
self.assertEqual('/', mem.var_stack[0].vars['PYTHONPATH'].val.s)
100100
self.assertEqual(True, mem.var_stack[0].vars['PYTHONPATH'].exported)
101101

102-
self.assertEqual({'PYTHONPATH': '/'}, mem.GetExported())
102+
ex = mem.GetExported()
103+
self.assertEqual('/', ex['PYTHONPATH'])
103104

104105
mem.SetVar(
105106
lvalue.LhsName('PYTHONPATH'), None, (var_flags_e.Exported,),
@@ -242,7 +243,7 @@ def testExportThenAssign(self):
242243
lvalue.LhsName('U'), value.Str('u'), (), scope_e.Dynamic)
243244
print(mem)
244245
e = mem.GetExported()
245-
self.assertEqual({'U': 'u'}, e)
246+
self.assertEqual('u', e['U'])
246247

247248
def testUnset(self):
248249
mem = _InitMem()

0 commit comments

Comments
 (0)