summaryrefslogtreecommitdiff
path: root/cmd2
diff options
context:
space:
mode:
authorKevin Van Brunt <kmvanbrunt@gmail.com>2019-05-07 23:39:08 -0400
committerKevin Van Brunt <kmvanbrunt@gmail.com>2019-05-07 23:39:08 -0400
commitcedc154489a654dc229eed3b4494f5eae43a4290 (patch)
tree5a5300c54a47326f2f293b4486e913fdad7c6cab /cmd2
parentf9ea58edbbe27ba5bcea2534263c992e8a2c7ab8 (diff)
downloadcmd2-git-cedc154489a654dc229eed3b4494f5eae43a4290.tar.gz
Added capability to redirect pipe commands and chain them together
Diffstat (limited to 'cmd2')
-rw-r--r--cmd2/cmd2.py74
-rw-r--r--cmd2/parsing.py22
-rw-r--r--cmd2/utils.py21
3 files changed, 67 insertions, 50 deletions
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 431c51ae..aa4c7a49 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -2027,35 +2027,44 @@ class Cmd(cmd.Cmd):
subproc_stdin = io.open(read_fd, 'r')
new_stdout = io.open(write_fd, 'w')
- # We want Popen to raise an exception if it fails to open the process. Thus we don't set shell to True.
+ # Set options to not forward signals to the pipe process. If a Ctrl-C event occurs,
+ # our sigint handler will forward it only to the most recent pipe process. This makes
+ # sure pipe processes close in the right order (most recent first).
+ if sys.platform == 'win32':
+ creationflags = subprocess.CREATE_NEW_PROCESS_GROUP
+ start_new_session = False
+ else:
+ creationflags = 0
+ start_new_session = True
+
+ # For any stream that is a StdSim, we will use a pipe so we can capture its output
+ proc = subprocess.Popen(statement.pipe_to,
+ stdin=subproc_stdin,
+ stdout=subprocess.PIPE if isinstance(self.stdout, utils.StdSim) else self.stdout,
+ stderr=subprocess.PIPE if isinstance(sys.stderr, utils.StdSim) else sys.stderr,
+ creationflags=creationflags,
+ start_new_session=start_new_session,
+ shell=True)
+
+ # Popen was called with shell=True so the user can do stuff like redirect the output of the pipe
+ # process (ex: !ls | grep foo > out.txt). But this makes it difficult to know if the pipe process
+ # started OK, since the shell itself always starts. Therefore, we will wait a short time and check
+ # if the pipe process is still running.
try:
- # Set options to not forward signals to the pipe process. If a Ctrl-C event occurs,
- # our sigint handler will forward it only to the most recent pipe process. This makes
- # sure pipe processes close in the right order (most recent first).
- if sys.platform == 'win32':
- creationflags = subprocess.CREATE_NEW_PROCESS_GROUP
- start_new_session = False
- else:
- creationflags = 0
- start_new_session = True
-
- # For any stream that is a StdSim, we will use a pipe so we can capture its output
- proc = \
- subprocess.Popen(statement.pipe_to,
- stdin=subproc_stdin,
- stdout=subprocess.PIPE if isinstance(self.stdout, utils.StdSim) else self.stdout,
- stderr=subprocess.PIPE if isinstance(sys.stderr, utils.StdSim) else sys.stderr,
- creationflags=creationflags,
- start_new_session=start_new_session)
+ proc.wait(0.2)
+ except subprocess.TimeoutExpired:
+ pass
- saved_state.redirecting = True
- saved_state.pipe_proc_reader = utils.ProcReader(proc, self.stdout, sys.stderr)
- sys.stdout = self.stdout = new_stdout
- except Exception as ex:
- self.perror('Failed to open pipe because - {}'.format(ex), traceback_war=False)
+ # Check if the pipe process already exited
+ if proc.returncode is not None:
+ self.perror('Pipe process exited with code {} before command could run'.format(proc.returncode))
subproc_stdin.close()
new_stdout.close()
redir_error = True
+ else:
+ saved_state.redirecting = True
+ saved_state.pipe_proc_reader = utils.ProcReader(proc, self.stdout, sys.stderr)
+ sys.stdout = self.stdout = new_stdout
elif statement.output:
import tempfile
@@ -3021,21 +3030,8 @@ class Cmd(cmd.Cmd):
# Create a list of arguments to shell
tokens = [args.command] + args.command_args
- # Support expanding ~ in quoted paths
- for index, _ in enumerate(tokens):
- if tokens[index]:
- # Check if the token is quoted. Since parsing already passed, there isn't
- # an unclosed quote. So we only need to check the first character.
- first_char = tokens[index][0]
- if first_char in constants.QUOTES:
- tokens[index] = utils.strip_quotes(tokens[index])
-
- tokens[index] = os.path.expanduser(tokens[index])
-
- # Restore the quotes
- if first_char in constants.QUOTES:
- tokens[index] = first_char + tokens[index] + first_char
-
+ # Expand ~ where needed
+ utils.expand_user_in_tokens(tokens)
expanded_command = ' '.join(tokens)
# Prevent KeyboardInterrupts while in the shell process. The shell process will
diff --git a/cmd2/parsing.py b/cmd2/parsing.py
index 934f1d26..306d3bdd 100644
--- a/cmd2/parsing.py
+++ b/cmd2/parsing.py
@@ -160,8 +160,8 @@ class Statement(str):
# characters appearing after the terminator but before output redirection, if any
suffix = attr.ib(default='', validator=attr.validators.instance_of(str))
- # if output was piped to a shell command, the shell command as a list of tokens
- pipe_to = attr.ib(default=attr.Factory(list), validator=attr.validators.instance_of(list))
+ # if output was piped to a shell command, the shell command as a string
+ pipe_to = attr.ib(default='', validator=attr.validators.instance_of(str))
# if output was redirected, the redirection token, i.e. '>>'
output = attr.ib(default='', validator=attr.validators.instance_of(str))
@@ -208,12 +208,12 @@ class Statement(str):
rtn += ' ' + self.suffix
if self.pipe_to:
- rtn += ' | ' + ' '.join(self.pipe_to)
+ rtn += ' | ' + self.pipe_to
if self.output:
rtn += ' ' + self.output
if self.output_to:
- rtn += ' ' + self.output_to
+ rtn += ' ' + utils.quote_string_if_needed(self.output_to)
return rtn
@@ -460,18 +460,18 @@ class StatementParser:
try:
# find the first pipe if it exists
pipe_pos = tokens.index(constants.REDIRECTION_PIPE)
- # save everything after the first pipe as tokens
- pipe_to = tokens[pipe_pos + 1:]
- for pos, cur_token in enumerate(pipe_to):
- unquoted_token = utils.strip_quotes(cur_token)
- pipe_to[pos] = os.path.expanduser(unquoted_token)
+ # Get the tokens for the pipe command and expand ~ where needed
+ pipe_to_tokens = tokens[pipe_pos + 1:]
+ utils.expand_user_in_tokens(pipe_to_tokens)
+
+ # Build the pipe command line string
+ pipe_to = ' '.join(pipe_to_tokens)
# remove all the tokens after the pipe
tokens = tokens[:pipe_pos]
except ValueError:
- # no pipe in the tokens
- pipe_to = []
+ pipe_to = ''
# check for output redirect
output = ''
diff --git a/cmd2/utils.py b/cmd2/utils.py
index e8e8a611..45e55c2b 100644
--- a/cmd2/utils.py
+++ b/cmd2/utils.py
@@ -275,6 +275,27 @@ def unquote_specific_tokens(args: List[str], tokens_to_unquote: List[str]) -> No
args[i] = unquoted_arg
+def expand_user_in_tokens(tokens: List[str]) -> None:
+ """
+ Call os.path.expanduser() on all tokens in an already parsed list of command-line arguments.
+ This also supports expanding user in quoted tokens.
+ :param tokens: tokens to expand
+ """
+ for index, _ in enumerate(tokens):
+ if tokens[index]:
+ # Check if the token is quoted. Since parsing already passed, there isn't
+ # an unclosed quote. So we only need to check the first character.
+ first_char = tokens[index][0]
+ if first_char in constants.QUOTES:
+ tokens[index] = strip_quotes(tokens[index])
+
+ tokens[index] = os.path.expanduser(tokens[index])
+
+ # Restore the quotes
+ if first_char in constants.QUOTES:
+ tokens[index] = first_char + tokens[index] + first_char
+
+
def find_editor() -> str:
"""Find a reasonable editor to use by default for the system that the cmd2 application is running on."""
editor = os.environ.get('EDITOR')