summaryrefslogtreecommitdiff
path: root/cmd2/utils.py
diff options
context:
space:
mode:
Diffstat (limited to 'cmd2/utils.py')
-rw-r--r--cmd2/utils.py158
1 files changed, 149 insertions, 9 deletions
diff --git a/cmd2/utils.py b/cmd2/utils.py
index 02956f6b..ddd43507 100644
--- a/cmd2/utils.py
+++ b/cmd2/utils.py
@@ -4,8 +4,9 @@
import collections
import os
-from typing import Any, List, Optional, Union
+import re
import unicodedata
+from typing import Any, Iterable, List, Optional, Union
from . import constants
@@ -19,6 +20,28 @@ def strip_ansi(text: str) -> str:
return constants.ANSI_ESCAPE_RE.sub('', text)
+def is_quoted(arg: str) -> bool:
+ """
+ Checks if a string is quoted
+ :param arg: the string being checked for quotes
+ :return: True if a string is quoted
+ """
+ return len(arg) > 1 and arg[0] == arg[-1] and arg[0] in constants.QUOTES
+
+
+def quote_string_if_needed(arg: str) -> str:
+ """ Quotes a string if it contains spaces and isn't already quoted """
+ if is_quoted(arg) or ' ' not in arg:
+ return arg
+
+ if '"' in arg:
+ quote = "'"
+ else:
+ quote = '"'
+
+ return quote + arg + quote
+
+
def strip_quotes(arg: str) -> str:
""" Strip outer quotes from a string.
@@ -27,7 +50,7 @@ def strip_quotes(arg: str) -> str:
:param arg: string to strip outer quotes from
:return: same string with potentially outer quotes stripped
"""
- if len(arg) > 1 and arg[0] == arg[-1] and arg[0] in constants.QUOTES:
+ if is_quoted(arg):
arg = arg[1:-1]
return arg
@@ -66,10 +89,12 @@ def cast(current: Any, new: str) -> Any:
"""Tries to force a new value into the same type as the current when trying to set the value for a parameter.
:param current: current value for the parameter, type varies
- :param new: str - new value
+ :param new: new value
:return: new value with same type as current, or the current value if there was an error casting
"""
typ = type(current)
+ orig_new = new
+
if typ == bool:
try:
return bool(int(new))
@@ -77,18 +102,18 @@ def cast(current: Any, new: str) -> Any:
pass
try:
new = new.lower()
+ if (new == 'on') or (new[0] in ('y', 't')):
+ return True
+ if (new == 'off') or (new[0] in ('n', 'f')):
+ return False
except AttributeError:
pass
- if (new == 'on') or (new[0] in ('y', 't')):
- return True
- if (new == 'off') or (new[0] in ('n', 'f')):
- return False
else:
try:
return typ(new)
except (ValueError, TypeError):
pass
- print("Problem setting parameter (now %s) to %s; incorrect type?" % (current, new))
+ print("Problem setting parameter (now {}) to {}; incorrect type?".format(current, orig_new))
return current
@@ -169,10 +194,125 @@ def norm_fold(astr: str) -> str:
return unicodedata.normalize('NFC', astr).casefold()
-def alphabetical_sort(list_to_sort: List[str]) -> List[str]:
+def alphabetical_sort(list_to_sort: Iterable[str]) -> List[str]:
"""Sorts a list of strings alphabetically.
+ For example: ['a1', 'A11', 'A2', 'a22', 'a3']
+
+ To sort a list in place, don't call this method, which makes a copy. Instead, do this:
+
+ my_list.sort(key=norm_fold)
+
:param list_to_sort: the list being sorted
:return: the sorted list
"""
return sorted(list_to_sort, key=norm_fold)
+
+
+def try_int_or_force_to_lower_case(input_str: str) -> Union[int, str]:
+ """
+ Tries to convert the passed-in string to an integer. If that fails, it converts it to lower case using norm_fold.
+ :param input_str: string to convert
+ :return: the string as an integer or a lower case version of the string
+ """
+ try:
+ return int(input_str)
+ except ValueError:
+ return norm_fold(input_str)
+
+
+def natural_keys(input_str: str) -> List[Union[int, str]]:
+ """
+ Converts a string into a list of integers and strings to support natural sorting (see natural_sort).
+
+ For example: natural_keys('abc123def') -> ['abc', '123', 'def']
+ :param input_str: string to convert
+ :return: list of strings and integers
+ """
+ return [try_int_or_force_to_lower_case(substr) for substr in re.split(r'(\d+)', input_str)]
+
+
+def natural_sort(list_to_sort: Iterable[str]) -> List[str]:
+ """
+ Sorts a list of strings case insensitively as well as numerically.
+
+ For example: ['a1', 'A2', 'a3', 'A11', 'a22']
+
+ To sort a list in place, don't call this method, which makes a copy. Instead, do this:
+
+ my_list.sort(key=natural_keys)
+
+ :param list_to_sort: the list being sorted
+ :return: the list sorted naturally
+ """
+ return sorted(list_to_sort, key=natural_keys)
+
+
+class StdSim(object):
+ """Class to simulate behavior of sys.stdout or sys.stderr.
+
+ Stores contents in internal buffer and optionally echos to the inner stream it is simulating.
+ """
+ class ByteBuf(object):
+ """Inner class which stores an actual bytes buffer and does the actual output if echo is enabled."""
+ def __init__(self, inner_stream, echo: bool = False) -> None:
+ self.byte_buf = b''
+ self.inner_stream = inner_stream
+ self.echo = echo
+
+ def write(self, b: bytes) -> None:
+ """Add bytes to internal bytes buffer and if echo is True, echo contents to inner stream."""
+ if not isinstance(b, bytes):
+ raise TypeError('a bytes-like object is required, not {}'.format(type(b)))
+ self.byte_buf += b
+ if self.echo:
+ self.inner_stream.buffer.write(b)
+
+ def __init__(self, inner_stream, echo: bool = False) -> None:
+ self.buffer = self.ByteBuf(inner_stream, echo)
+ self.inner_stream = inner_stream
+
+ def write(self, s: str) -> None:
+ """Add str to internal bytes buffer and if echo is True, echo contents to inner stream."""
+ if not isinstance(s, str):
+ raise TypeError('write() argument must be str, not {}'.format(type(s)))
+ b = s.encode()
+ self.buffer.write(b)
+
+ def getvalue(self) -> str:
+ """Get the internal contents as a str.
+
+ :return string from the internal contents
+ """
+ return self.buffer.byte_buf.decode()
+
+ def read(self) -> str:
+ """Read from the internal contents as a str and then clear them out.
+
+ :return: string from the internal contents
+ """
+ result = self.getvalue()
+ self.clear()
+ return result
+
+ def clear(self) -> None:
+ """Clear the internal contents."""
+ self.buffer.byte_buf = b''
+
+ def __getattr__(self, item: str):
+ if item in self.__dict__:
+ return self.__dict__[item]
+ else:
+ return getattr(self.inner_stream, item)
+
+
+def unquote_redirection_tokens(args: List[str]) -> None:
+ """
+ Unquote redirection tokens in a list of command-line arguments
+ This is used when redirection tokens have to be passed to another command
+ :param args: the command line args
+ """
+ for i, arg in enumerate(args):
+ unquoted_arg = strip_quotes(arg)
+ if unquoted_arg in constants.REDIRECTION_TOKENS:
+ args[i] = unquoted_arg