Skip to content

Commit 202cc07

Browse files
committed
Simplifying adding opening quotes
1 parent 9d9e843 commit 202cc07

2 files changed

Lines changed: 73 additions & 170 deletions

File tree

cmd2.py

Lines changed: 72 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -1321,7 +1321,7 @@ def tokens_for_completion(self, line, begidx, endidx):
13211321
On Success
13221322
tokens: list of unquoted tokens
13231323
this is generally the list needed for tab completion functions
1324-
raw_tokens: list of tokens as they appear on the command line, meaning their quotes are preserved
1324+
raw_tokens: list of tokens with any quotes preserved
13251325
this can be used to know if a token was quoted or is missing a closing quote
13261326
13271327
Both lists are guaranteed to have at least 1 item
@@ -1349,7 +1349,7 @@ def tokens_for_completion(self, line, begidx, endidx):
13491349
break
13501350
except ValueError:
13511351
# ValueError can be caused by missing closing quote
1352-
if len(quotes_to_try) == 0:
1352+
if not quotes_to_try:
13531353
# Since we have no more quotes to try, something else
13541354
# is causing the parsing error. Return None since
13551355
# this means the line is malformed.
@@ -1481,23 +1481,23 @@ def delimiter_complete(self, text, line, begidx, endidx, match_against, delimite
14811481
matches = self.basic_complete(text, line, begidx, endidx, match_against)
14821482

14831483
# Display only the portion of the match that's being completed based on delimiter
1484-
if len(matches) > 0:
1484+
if matches:
14851485

14861486
# Get the common beginning for the matches
14871487
common_prefix = os.path.commonprefix(matches)
14881488
prefix_tokens = common_prefix.split(delimiter)
14891489

14901490
# Calculate what portion of the match we are completing
14911491
display_token_index = 0
1492-
if len(prefix_tokens) > 0:
1492+
if prefix_tokens:
14931493
display_token_index = len(prefix_tokens) - 1
14941494

14951495
# Get this portion for each match and store them in self.display_matches
14961496
for cur_match in matches:
14971497
match_tokens = cur_match.split(delimiter)
14981498
display_token = match_tokens[display_token_index]
14991499

1500-
if len(display_token) == 0:
1500+
if not display_token:
15011501
display_token = delimiter
15021502
self.display_matches.append(display_token)
15031503

@@ -1759,7 +1759,7 @@ def shell_cmd_complete(self, text, line, begidx, endidx, complete_blank=False):
17591759
:return: List[str] - a list of possible tab completions
17601760
"""
17611761
# Don't tab complete anything if no shell command has been started
1762-
if not complete_blank and len(text) == 0:
1762+
if not complete_blank and not text:
17631763
return []
17641764

17651765
# If there are no path characters in the search text, then do shell command completion in the user's path
@@ -1862,7 +1862,7 @@ def _display_matches_gnu_readline(self, substitution, matches, longest_match_len
18621862
if rl_type == RlType.GNU:
18631863

18641864
# Check if we should show display_matches
1865-
if len(self.display_matches) > 0:
1865+
if self.display_matches:
18661866
matches_to_display = self.display_matches
18671867

18681868
# Recalculate longest_match_length for display_matches
@@ -1920,7 +1920,7 @@ def _display_matches_pyreadline(self, matches):
19201920
if rl_type == RlType.PYREADLINE:
19211921

19221922
# Check if we should show display_matches
1923-
if len(self.display_matches) > 0:
1923+
if self.display_matches:
19241924
matches_to_display = self.display_matches
19251925
else:
19261926
matches_to_display = matches
@@ -1931,117 +1931,6 @@ def _display_matches_pyreadline(self, matches):
19311931
# Display the matches
19321932
orig_pyreadline_display(matches_to_display)
19331933

1934-
def _handle_completion_token_quote(self, raw_completion_token):
1935-
"""
1936-
This is called by complete() to add an opening quote to the token being completed if it is needed
1937-
The readline input buffer is then updated with the new string
1938-
:param raw_completion_token: str - the token being completed as it appears on the command line
1939-
:return: True if a quote was added, False otherwise
1940-
"""
1941-
if len(self.completion_matches) == 0:
1942-
return False
1943-
1944-
quote_added = False
1945-
1946-
# Check if token on screen is already quoted
1947-
if len(raw_completion_token) == 0 or raw_completion_token[0] not in QUOTES:
1948-
1949-
# Get the common prefix of all matches. This is what be written to the screen.
1950-
common_prefix = os.path.commonprefix(self.completion_matches)
1951-
1952-
# If common_prefix contains a space, then we must add an opening quote to it
1953-
if ' ' in common_prefix:
1954-
1955-
# Figure out what kind of quote to add
1956-
if '"' in common_prefix:
1957-
quote = "'"
1958-
else:
1959-
quote = '"'
1960-
1961-
new_completion_token = quote + common_prefix
1962-
1963-
# Handle a single result
1964-
if len(self.completion_matches) == 1:
1965-
str_to_append = ''
1966-
1967-
# Add a closing quote if allowed
1968-
if self.allow_closing_quote:
1969-
str_to_append += quote
1970-
1971-
orig_line = readline.get_line_buffer()
1972-
endidx = readline.get_endidx()
1973-
1974-
# If we are at the end of the line, then add a space if allowed
1975-
if self.allow_appended_space and endidx == len(orig_line):
1976-
str_to_append += ' '
1977-
1978-
new_completion_token += str_to_append
1979-
1980-
# Update the line
1981-
quote_added = True
1982-
self._replace_completion_token(raw_completion_token, new_completion_token)
1983-
1984-
return quote_added
1985-
1986-
def _replace_completion_token(self, raw_completion_token, new_completion_token):
1987-
"""
1988-
Replaces the token being completed in the readline line buffer which updates the screen
1989-
This is used for things like adding an opening quote for completions with spaces
1990-
:param raw_completion_token: str - the original token being completed as it appears on the command line
1991-
:param new_completion_token: str- the replacement token
1992-
:return: None
1993-
"""
1994-
orig_line = readline.get_line_buffer()
1995-
endidx = readline.get_endidx()
1996-
1997-
starting_index = orig_line[:endidx].rfind(raw_completion_token)
1998-
1999-
if starting_index != -1:
2000-
# Build the new line
2001-
new_line = orig_line[:starting_index]
2002-
new_line += new_completion_token
2003-
new_line += orig_line[endidx:]
2004-
2005-
# Calculate the new cursor offset
2006-
len_diff = len(new_completion_token) - len(raw_completion_token)
2007-
new_point = endidx + len_diff
2008-
2009-
# Replace the line and update the cursor offset
2010-
self._set_readline_line(new_line)
2011-
self._set_readline_point(new_point)
2012-
2013-
@staticmethod
2014-
def _set_readline_line(new_line):
2015-
"""
2016-
Sets the readline line buffer
2017-
:param new_line: str - the new line contents
2018-
"""
2019-
if rl_type == RlType.GNU:
2020-
# Byte encode the new line
2021-
if six.PY3:
2022-
encoded_line = bytes(new_line, encoding='utf-8')
2023-
else:
2024-
encoded_line = bytes(new_line)
2025-
2026-
# Replace the line
2027-
readline_lib.rl_replace_line(encoded_line, 0)
2028-
2029-
elif rl_type == RlType.PYREADLINE:
2030-
readline.rl.mode.l_buffer.set_line(new_line)
2031-
2032-
@staticmethod
2033-
def _set_readline_point(new_point):
2034-
"""
2035-
Sets the cursor offset in the readline line buffer
2036-
:param new_point: int - the new cursor offset
2037-
"""
2038-
if rl_type == RlType.GNU:
2039-
rl_point = ctypes.c_int.in_dll(readline_lib, "rl_point")
2040-
rl_point.value = new_point
2041-
2042-
elif rl_type == RlType.PYREADLINE:
2043-
readline.rl.mode.l_buffer.point = new_point
2044-
20451934
# ----- Methods which override stuff in cmd -----
20461935

20471936
def complete(self, text, state):
@@ -2119,21 +2008,30 @@ def complete(self, text, state):
21192008
self.completion_matches = []
21202009
return None
21212010

2122-
# readline still performs word breaks after a quote. Therefore something like quoted search
2123-
# text with a space would have resulted in begidx pointing to the middle of the token we
2124-
# we want to complete. Figure out where that token actually begins and save the beginning
2125-
# portion of it that was not part of the text readline gave us. We will remove it from the
2126-
# completions later since readline expects them to start with the original text.
2127-
actual_begidx = line[:endidx].rfind(tokens[-1])
2011+
# Text we need to remove from completions later
21282012
text_to_remove = ''
21292013

2130-
if actual_begidx != begidx:
2131-
text_to_remove = line[actual_begidx:begidx]
2014+
# Get the token being completed with any opening quote preserved
2015+
raw_completion_token = raw_tokens[-1]
21322016

2133-
# Adjust text and where it begins so the completer routines
2134-
# get unbroken search text to complete on.
2135-
text = text_to_remove + text
2136-
begidx = actual_begidx
2017+
# Check if the token being completed has an opening quote
2018+
if raw_completion_token and raw_completion_token[0] in QUOTES:
2019+
unclosed_quote = raw_completion_token[0]
2020+
2021+
# readline still performs word breaks after a quote. Therefore something like quoted search
2022+
# text with a space would have resulted in begidx pointing to the middle of the token we
2023+
# we want to complete. Figure out where that token actually begins and save the beginning
2024+
# portion of it that was not part of the text readline gave us. We will remove it from the
2025+
# completions later since readline expects them to start with the original text.
2026+
actual_begidx = line[:endidx].rfind(tokens[-1])
2027+
2028+
if actual_begidx != begidx:
2029+
text_to_remove = line[actual_begidx:begidx]
2030+
2031+
# Adjust text and where it begins so the completer routines
2032+
# get unbroken search text to complete on.
2033+
text = text_to_remove + text
2034+
begidx = actual_begidx
21372035

21382036
# Check if a valid command was entered
21392037
if command in self.get_all_commands():
@@ -2164,7 +2062,7 @@ def complete(self, text, state):
21642062
# call the completer function for the current command
21652063
self.completion_matches = self._redirect_complete(text, line, begidx, endidx, compfunc)
21662064

2167-
if len(self.completion_matches) > 0:
2065+
if self.completion_matches:
21682066

21692067
# Eliminate duplicates
21702068
matches_set = set(self.completion_matches)
@@ -2173,36 +2071,44 @@ def complete(self, text, state):
21732071
display_matches_set = set(self.display_matches)
21742072
self.display_matches = list(display_matches_set)
21752073

2176-
# Get the token being completed as it appears on the command line
2177-
raw_completion_token = raw_tokens[-1]
2178-
2179-
# Add an opening quote if needed
2180-
if self._handle_completion_token_quote(raw_completion_token):
2181-
# An opening quote was added and the screen was updated. Return no results.
2182-
self.completion_matches = []
2183-
return None
2184-
2185-
if text_to_remove or shortcut_to_restore:
2186-
# If self.display_matches is empty, then set it to self.completion_matches
2187-
# before we alter them. That way the suggestions will reflect how we parsed
2188-
# the token being completed and not how readline did.
2189-
if len(self.display_matches) == 0:
2190-
self.display_matches = copy.copy(self.completion_matches)
2191-
2192-
# Check if we need to remove text from the beginning of tab completions
2193-
if text_to_remove:
2194-
self.completion_matches = \
2195-
[m.replace(text_to_remove, '', 1) for m in self.completion_matches]
2196-
2197-
# Check if we need to restore a shortcut in the tab completions
2198-
# so it doesn't get erased from the command line
2199-
if shortcut_to_restore:
2200-
self.completion_matches = \
2201-
[shortcut_to_restore + match for match in self.completion_matches]
2202-
2203-
# If the token being completed starts with a quote then we know it has an unclosed quote
2204-
if len(raw_completion_token) > 0 and raw_completion_token[0] in QUOTES:
2205-
unclosed_quote = raw_completion_token[0]
2074+
# If self.display_matches is empty, then set it to self.completion_matches
2075+
# before we alter them. That way the suggestions will reflect how we parsed
2076+
# the token being completed and not how readline did.
2077+
if not self.display_matches:
2078+
self.display_matches = copy.copy(self.completion_matches)
2079+
2080+
# Check if we need to add an opening quote
2081+
if not unclosed_quote:
2082+
2083+
# Get the common prefix of all matches. This is the actual tab completion.
2084+
common_prefix = os.path.commonprefix(self.completion_matches)
2085+
2086+
# Join all matches into 1 string for ease of searching
2087+
all_matches_str = ''.join(self.completion_matches)
2088+
2089+
# If there is a common_prefix and any of the matches have a space,
2090+
# then we must add an opening quote to the matches.
2091+
if common_prefix and ' ' in all_matches_str:
2092+
2093+
# Figure out what kind of quote to add
2094+
if '"' in all_matches_str:
2095+
quote = "'"
2096+
else:
2097+
quote = '"'
2098+
2099+
unclosed_quote = quote
2100+
self.completion_matches = [quote + match for match in self.completion_matches]
2101+
2102+
# Check if we need to remove text from the beginning of tab completions
2103+
elif text_to_remove:
2104+
self.completion_matches = \
2105+
[m.replace(text_to_remove, '', 1) for m in self.completion_matches]
2106+
2107+
# Check if we need to restore a shortcut in the tab completions
2108+
# so it doesn't get erased from the command line
2109+
if shortcut_to_restore:
2110+
self.completion_matches = \
2111+
[shortcut_to_restore + match for match in self.completion_matches]
22062112

22072113
else:
22082114
# Complete token against aliases and command names
@@ -2226,7 +2132,7 @@ def complete(self, text, state):
22262132
self.completion_matches[0] += str_to_append
22272133

22282134
# Otherwise sort matches
2229-
elif len(self.completion_matches) > 0:
2135+
elif self.completion_matches:
22302136
self.completion_matches.sort()
22312137
self.display_matches.sort()
22322138

@@ -2871,7 +2777,7 @@ def do_alias(self, arglist):
28712777
alias save_results "print_results > out.txt"
28722778
"""
28732779
# If no args were given, then print a list of current aliases
2874-
if len(arglist) == 0:
2780+
if not arglist:
28752781
for cur_alias in self.aliases:
28762782
self.poutput("alias {} {}".format(cur_alias, self.aliases[cur_alias]))
28772783

@@ -2919,7 +2825,7 @@ def do_unalias(self, arglist):
29192825
Options:
29202826
-a remove all alias definitions
29212827
"""
2922-
if len(arglist) == 0:
2828+
if not arglist:
29232829
self.do_help('unalias')
29242830

29252831
if '-a' in arglist:
@@ -3233,7 +3139,7 @@ def do_shell(self, command):
32333139

32343140
# Support expanding ~ in quoted paths
32353141
for index, _ in enumerate(tokens):
3236-
if len(tokens[index]) > 0:
3142+
if tokens[index]:
32373143
# Check if the token is quoted. Since shlex.split() passed, there isn't
32383144
# an unclosed quote, so we only need to check the first character.
32393145
first_char = tokens[index][0]

tests/test_completion.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -795,10 +795,7 @@ def test_subcommand_tab_completion_add_quote(sc_app):
795795
begidx = endidx - len(text)
796796

797797
first_match = complete_tester(text, line, begidx, endidx, sc_app)
798-
799-
# No matches are returned when an opening quote is added to the screen
800-
assert first_match is None
801-
assert readline.get_line_buffer() == 'base sport "Space Ball" '
798+
assert first_match is not None and sc_app.completion_matches == ['"Space Ball" ']
802799

803800
def test_subcommand_tab_completion_space_in_text(sc_app):
804801
text = 'B'

0 commit comments

Comments
 (0)