def testHistoryLexer(self): print(list(match.HistoryTokens(r'echo hi'))) print(list(match.HistoryTokens(r'echo !! !* !^ !$'))) # No history operator with \ escape tokens = list(match.HistoryTokens(r'echo \!!')) print(tokens) self.assert_(Id.History_Op not in [tok_type for tok_type, _ in tokens]) print(list(match.HistoryTokens(r'echo !3...'))) print(list(match.HistoryTokens(r'echo !-5...'))) print(list(match.HistoryTokens(r'echo !x/foo.py bar'))) print('---') # No history operator in single quotes tokens = list(match.HistoryTokens(r"echo '!!' $'!!' ")) print(tokens) self.assert_(Id.History_Op not in [tok_type for tok_type, _ in tokens]) # No history operator in incomplete single quotes tokens = list(match.HistoryTokens(r"echo '!! ")) print(tokens) self.assert_(Id.History_Op not in [tok_type for tok_type, _ in tokens]) # Quoted single quote, and then a History operator tokens = list(match.HistoryTokens(r"echo \' !! ")) print(tokens) # YES operator self.assert_(Id.History_Op in [tok_type for tok_type, _ in tokens])
def testHistoryDoesNotConflict(self): # https://github.com/oilshell/oil/issues/264 # # Bash has a bunch of hacks to suppress the conflict between ! for history # and: # # 1. [!abc] globbing # 2. ${!foo} indirect expansion # 3. $!x -- the PID # 4. !(foo|bar) -- extended glob # # I guess [[ a != b ]] doesn't match the pattern in bash. three_other = [Id.History_Other, Id.History_Other, Id.History_Other] two_other = [Id.History_Other, Id.History_Other] CASES = [ (r'[!abc]', three_other), (r'${!indirect}', three_other), (r'$!x', three_other), # didn't need a special case (r'!(foo|bar)', two_other), # didn't need a special case ] for s, expected_types in CASES: tokens = list(match.HistoryTokens(s)) print(tokens) actual_types = [id_ for id_, val in tokens] self.assert_(Id.History_Search not in actual_types, tokens) self.assertEqual(expected_types, actual_types)
def Eval(self, line): # type: (str) -> str """Returns an expanded line.""" if not self.readline_mod: return line tokens = match.HistoryTokens(line) # Common case: no history expansion. if all(id_ == Id.History_Other for (id_, _) in tokens): return line history_len = self.readline_mod.get_current_history_length() if history_len <= 0: # no commands to expand return line self.debug_f.log('history length = %d', history_len) parts = [] for id_, val in tokens: if id_ == Id.History_Other: out = val elif id_ == Id.History_Op: prev = self.readline_mod.get_history_item(history_len) ch = val[1] if ch == '!': out = prev else: self.parse_ctx.trail.Clear() # not strictly necessary? line_reader = reader.StringLineReader( prev, self.parse_ctx.arena) c_parser = self.parse_ctx.MakeOshParser(line_reader) try: c_parser.ParseLogicalLine() except error.Parse as e: #from core import ui #ui.PrettyPrintError(e, self.parse_ctx.arena) # Invalid command in history. TODO: We should never enter these. self.debug_f.log( "Couldn't parse historical command %r: %s", prev, e) # NOTE: We're using the trail rather than the return value of # ParseLogicalLine because it handles cases like # $ for i in 1 2 3; do sleep ${i}; done # $ echo !$ # which should expand to 'echo ${i}' words = self.parse_ctx.trail.words #self.debug_f.log('TRAIL WORDS: %s', words) if ch == '^': try: w = words[1] except IndexError: raise util.HistoryError("No first word in %r", prev) spid1 = word_.LeftMostSpanForWord(w) spid2 = word_.RightMostSpanForWord(w) elif ch == '$': try: w = words[-1] except IndexError: raise util.HistoryError("No last word in %r", prev) spid1 = word_.LeftMostSpanForWord(w) spid2 = word_.RightMostSpanForWord(w) elif ch == '*': try: w1 = words[1] w2 = words[-1] except IndexError: raise util.HistoryError( "Couldn't find words in %r", prev) spid1 = word_.LeftMostSpanForWord(w1) spid2 = word_.RightMostSpanForWord(w2) else: raise AssertionError(ch) arena = self.parse_ctx.arena span1 = arena.GetLineSpan(spid1) span2 = arena.GetLineSpan(spid2) begin = span1.col end = span2.col + span2.length out = prev[begin:end] elif id_ == Id.History_Num: index = int( val[1:]) # regex ensures this. Maybe have - on the front. if index < 0: num = history_len + 1 + index else: num = index out = self.readline_mod.get_history_item(num) if out is None: # out of range raise util.HistoryError('%s: not found', val) elif id_ == Id.History_Search: # Remove the required space at the end and save it. A simple hack than # the one bash has. last_char = val[-1] val = val[:-1] # Search backward prefix = None substring = '' if val[1] == '?': substring = val[2:] else: prefix = val[1:] out = None for i in xrange(history_len, 1, -1): cmd = self.readline_mod.get_history_item(i) if prefix and cmd.startswith(prefix): out = cmd if len(substring) and substring in cmd: out = cmd if out is not None: out += last_char # restore required space break if out is None: raise util.HistoryError('%r found no results', val) else: raise AssertionError(id_) parts.append(out) line = ''.join(parts) # show what we expanded to sys.stdout.write('! %s' % line) return line