Skip to content

Commit b48d6b5

Browse files
authored
Merge pull request #398 from python-cmd2/remove_dynamic_redirectors
Remove cmd2.Cmd.redirector for #381
2 parents 9d4d929 + ceb3ef5 commit b48d6b5

File tree

8 files changed

+63
-64
lines changed

8 files changed

+63
-64
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
* Deleted ``cmd_with_subs_completer``, ``get_subcommands``, and ``get_subcommand_completer``
3030
* Replaced by default AutoCompleter implementation for all commands using argparse
3131
* Deleted support for old method of calling application commands with ``cmd()`` and ``self``
32+
* ``cmd2.redirector`` is no longer supported. Output redirection can only be done with '>' or '>>'
3233
* Python 2 no longer supported
3334
* ``cmd2`` now supports Python 3.4+
3435

cmd2/cmd2.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,6 @@ class Cmd(cmd.Cmd):
338338
# Attributes used to configure the StatementParser, best not to change these at runtime
339339
blankLinesAllowed = False
340340
multiline_commands = []
341-
redirector = '>' # for sending output to file
342341
shortcuts = {'?': 'help', '!': 'shell', '@': 'load', '@@': '_relative_load'}
343342
aliases = dict()
344343
terminators = [';']
@@ -1149,29 +1148,26 @@ def _redirect_complete(self, text, line, begidx, endidx, compfunc):
11491148

11501149
if len(raw_tokens) > 1:
11511150

1152-
# Build a list of all redirection tokens
1153-
all_redirects = constants.REDIRECTION_CHARS + ['>>']
1154-
11551151
# Check if there are redirection strings prior to the token being completed
11561152
seen_pipe = False
11571153
has_redirection = False
11581154

11591155
for cur_token in raw_tokens[:-1]:
1160-
if cur_token in all_redirects:
1156+
if cur_token in constants.REDIRECTION_TOKENS:
11611157
has_redirection = True
11621158

1163-
if cur_token == '|':
1159+
if cur_token == constants.REDIRECTION_PIPE:
11641160
seen_pipe = True
11651161

11661162
# Get token prior to the one being completed
11671163
prior_token = raw_tokens[-2]
11681164

11691165
# If a pipe is right before the token being completed, complete a shell command as the piped process
1170-
if prior_token == '|':
1166+
if prior_token == constants.REDIRECTION_PIPE:
11711167
return self.shell_cmd_complete(text, line, begidx, endidx)
11721168

11731169
# Otherwise do path completion either as files to redirectors or arguments to the piped process
1174-
elif prior_token in all_redirects or seen_pipe:
1170+
elif prior_token in constants.REDIRECTION_TOKENS or seen_pipe:
11751171
return self.path_complete(text, line, begidx, endidx)
11761172

11771173
# If there were redirection strings anywhere on the command line, then we
@@ -1820,7 +1816,7 @@ def _redirect_output(self, statement):
18201816

18211817
# We want Popen to raise an exception if it fails to open the process. Thus we don't set shell to True.
18221818
try:
1823-
self.pipe_proc = subprocess.Popen(shlex.split(statement.pipe_to), stdin=subproc_stdin)
1819+
self.pipe_proc = subprocess.Popen(statement.pipe_to, stdin=subproc_stdin)
18241820
except Exception as ex:
18251821
# Restore stdout to what it was and close the pipe
18261822
self.stdout.close()
@@ -1834,24 +1830,30 @@ def _redirect_output(self, statement):
18341830
raise ex
18351831
elif statement.output:
18361832
if (not statement.output_to) and (not can_clip):
1837-
raise EnvironmentError('Cannot redirect to paste buffer; install ``xclip`` and re-run to enable')
1833+
raise EnvironmentError("Cannot redirect to paste buffer; install 'pyperclip' and re-run to enable")
18381834
self.kept_state = Statekeeper(self, ('stdout',))
18391835
self.kept_sys = Statekeeper(sys, ('stdout',))
18401836
self.redirecting = True
18411837
if statement.output_to:
1838+
# going to a file
18421839
mode = 'w'
1843-
if statement.output == 2 * self.redirector:
1840+
# statement.output can only contain
1841+
# REDIRECTION_APPEND or REDIRECTION_OUTPUT
1842+
if statement.output == constants.REDIRECTION_APPEND:
18441843
mode = 'a'
18451844
sys.stdout = self.stdout = open(os.path.expanduser(statement.output_to), mode)
18461845
else:
1846+
# going to a paste buffer
18471847
sys.stdout = self.stdout = tempfile.TemporaryFile(mode="w+")
1848-
if statement.output == '>>':
1848+
if statement.output == constants.REDIRECTION_APPEND:
18491849
self.poutput(get_paste_buffer())
18501850

18511851
def _restore_output(self, statement):
1852-
"""Handles restoring state after output redirection as well as the actual pipe operation if present.
1852+
"""Handles restoring state after output redirection as well as
1853+
the actual pipe operation if present.
18531854
1854-
:param statement: Statement object which contains the parsed input from the user
1855+
:param statement: Statement object which contains the parsed
1856+
input from the user
18551857
"""
18561858
# If we have redirected output to a file or the clipboard or piped it to a shell command, then restore state
18571859
if self.kept_state is not None:

cmd2/constants.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,14 @@
44

55
import re
66

7-
# Used for command parsing, tab completion and word breaks. Do not change.
7+
# Used for command parsing, output redirection, tab completion and word
8+
# breaks. Do not change.
89
QUOTES = ['"', "'"]
9-
REDIRECTION_CHARS = ['|', '>']
10+
REDIRECTION_PIPE = '|'
11+
REDIRECTION_OUTPUT = '>'
12+
REDIRECTION_APPEND = '>>'
13+
REDIRECTION_CHARS = [REDIRECTION_PIPE, REDIRECTION_OUTPUT]
14+
REDIRECTION_TOKENS = [REDIRECTION_PIPE, REDIRECTION_OUTPUT, REDIRECTION_APPEND]
1015

1116
# Regular expression to match ANSI escape codes
1217
ANSI_ESCAPE_RE = re.compile(r'\x1b[^m]*m')

cmd2/parsing.py

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ class Statement(str):
4545
redirection, if any
4646
:type suffix: str or None
4747
:var pipe_to: if output was piped to a shell command, the shell command
48-
:type pipe_to: str or None
48+
as a list of tokens
49+
:type pipe_to: list
4950
:var output: if output was redirected, the redirection token, i.e. '>>'
5051
:type output: str or None
5152
:var output_to: if output was redirected, the destination, usually a filename
@@ -283,39 +284,42 @@ def parse(self, rawinput: str) -> Statement:
283284
argv = tokens
284285
tokens = []
285286

287+
# check for a pipe to a shell process
288+
# if there is a pipe, everything after the pipe needs to be passed
289+
# to the shell, even redirected output
290+
# this allows '(Cmd) say hello | wc > countit.txt'
291+
try:
292+
# find the first pipe if it exists
293+
pipe_pos = tokens.index(constants.REDIRECTION_PIPE)
294+
# save everything after the first pipe as tokens
295+
pipe_to = tokens[pipe_pos+1:]
296+
# remove all the tokens after the pipe
297+
tokens = tokens[:pipe_pos]
298+
except ValueError:
299+
# no pipe in the tokens
300+
pipe_to = None
301+
286302
# check for output redirect
287303
output = None
288304
output_to = None
289305
try:
290-
output_pos = tokens.index('>')
291-
output = '>'
306+
output_pos = tokens.index(constants.REDIRECTION_OUTPUT)
307+
output = constants.REDIRECTION_OUTPUT
292308
output_to = ' '.join(tokens[output_pos+1:])
293309
# remove all the tokens after the output redirect
294310
tokens = tokens[:output_pos]
295311
except ValueError:
296312
pass
297313

298314
try:
299-
output_pos = tokens.index('>>')
300-
output = '>>'
315+
output_pos = tokens.index(constants.REDIRECTION_APPEND)
316+
output = constants.REDIRECTION_APPEND
301317
output_to = ' '.join(tokens[output_pos+1:])
302318
# remove all tokens after the output redirect
303319
tokens = tokens[:output_pos]
304320
except ValueError:
305321
pass
306322

307-
# check for pipes
308-
try:
309-
# find the first pipe if it exists
310-
pipe_pos = tokens.index('|')
311-
# save everything after the first pipe
312-
pipe_to = ' '.join(tokens[pipe_pos+1:])
313-
# remove all the tokens after the pipe
314-
tokens = tokens[:pipe_pos]
315-
except ValueError:
316-
# no pipe in the tokens
317-
pipe_to = None
318-
319323
if terminator:
320324
# whatever is left is the suffix
321325
suffix = ' '.join(tokens)

docs/freefeatures.rst

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -100,26 +100,8 @@ As in a Unix shell, output of a command can be redirected:
100100
- appended to a file with ``>>``, as in ``mycommand args >> filename.txt``
101101
- piped (``|``) as input to operating-system commands, as in
102102
``mycommand args | wc``
103-
- sent to the paste buffer, ready for the next Copy operation, by
104-
ending with a bare ``>``, as in ``mycommand args >``.. Redirecting
105-
to paste buffer requires software to be installed on the operating
106-
system, pywin32_ on Windows or xclip_ on \*nix.
103+
- sent to the operating system paste buffer, by ending with a bare ``>``, as in ``mycommand args >``. You can even append output to the current contents of the paste buffer by ending your command with ``>>``.
107104

108-
If your application depends on mathematical syntax, ``>`` may be a bad
109-
choice for redirecting output - it will prevent you from using the
110-
greater-than sign in your actual user commands. You can override your
111-
app's value of ``self.redirector`` to use a different string for output redirection::
112-
113-
class MyApp(cmd2.Cmd):
114-
redirector = '->'
115-
116-
::
117-
118-
(Cmd) say line1 -> out.txt
119-
(Cmd) say line2 ->-> out.txt
120-
(Cmd) !cat out.txt
121-
line1
122-
line2
123105

124106
.. note::
125107

@@ -136,8 +118,8 @@ app's value of ``self.redirector`` to use a different string for output redirect
136118
arguments after them from the command line arguments accordingly. But output from a command will not be redirected
137119
to a file or piped to a shell command.
138120

139-
.. _pywin32: http://sourceforge.net/projects/pywin32/
140-
.. _xclip: http://www.cyberciti.biz/faq/xclip-linux-insert-files-command-output-intoclipboard/
121+
If you need to include any of these redirection characters in your command,
122+
you can enclose them in quotation marks, ``mycommand 'with > in the argument'``.
141123

142124
Python
143125
======

docs/unfreefeatures.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,17 @@ commands whose names are listed in the
1010
parameter ``app.multiline_commands``. These
1111
commands will be executed only
1212
after the user has entered a *terminator*.
13-
By default, the command terminators is
13+
By default, the command terminator is
1414
``;``; replacing or appending to the list
1515
``app.terminators`` allows different
1616
terminators. A blank line
1717
is *always* considered a command terminator
1818
(cannot be overridden).
1919

20+
In multiline commands, output redirection characters
21+
like ``>`` and ``|`` are part of the command
22+
arguments unless they appear after the terminator.
23+
2024

2125
Parsed statements
2226
=================

tests/test_cmd2.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1430,7 +1430,7 @@ def test_clipboard_failure(capsys):
14301430
# Make sure we got the error output
14311431
out, err = capsys.readouterr()
14321432
assert out == ''
1433-
assert 'Cannot redirect to paste buffer; install ``xclip`` and re-run to enable' in err
1433+
assert "Cannot redirect to paste buffer; install 'pyperclip' and re-run to enable" in err
14341434

14351435

14361436
class CmdResultApp(cmd2.Cmd):

tests/test_parsing.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ def test_parse_simple_pipe(parser, line):
159159
assert statement.command == 'simple'
160160
assert not statement.args
161161
assert statement.argv == ['simple']
162-
assert statement.pipe_to == 'piped'
162+
assert statement.pipe_to == ['piped']
163163

164164
def test_parse_double_pipe_is_not_a_pipe(parser):
165165
line = 'double-pipe || is not a pipe'
@@ -177,7 +177,7 @@ def test_parse_complex_pipe(parser):
177177
assert statement.argv == ['command', 'with', 'args,', 'terminator']
178178
assert statement.terminator == '&'
179179
assert statement.suffix == 'sufx'
180-
assert statement.pipe_to == 'piped'
180+
assert statement.pipe_to == ['piped']
181181

182182
@pytest.mark.parametrize('line,output', [
183183
('help > out.txt', '>'),
@@ -227,9 +227,9 @@ def test_parse_pipe_and_redirect(parser):
227227
assert statement.argv == ['output', 'into']
228228
assert statement.terminator == ';'
229229
assert statement.suffix == 'sufx'
230-
assert statement.pipe_to == 'pipethrume plz'
231-
assert statement.output == '>'
232-
assert statement.output_to == 'afile.txt'
230+
assert statement.pipe_to == ['pipethrume', 'plz', '>', 'afile.txt']
231+
assert not statement.output
232+
assert not statement.output_to
233233

234234
def test_parse_output_to_paste_buffer(parser):
235235
line = 'output to paste buffer >> '
@@ -240,8 +240,9 @@ def test_parse_output_to_paste_buffer(parser):
240240
assert statement.output == '>>'
241241

242242
def test_parse_redirect_inside_terminator(parser):
243-
"""The terminator designates the end of the commmand/arguments portion. If a redirector
244-
occurs before a terminator, then it will be treated as part of the arguments and not as a redirector."""
243+
"""The terminator designates the end of the commmand/arguments portion.
244+
If a redirector occurs before a terminator, then it will be treated as
245+
part of the arguments and not as a redirector."""
245246
line = 'has > inside;'
246247
statement = parser.parse(line)
247248
assert statement.command == 'has'
@@ -385,7 +386,7 @@ def test_parse_alias_pipe(parser, line):
385386
statement = parser.parse(line)
386387
assert statement.command == 'help'
387388
assert not statement.args
388-
assert statement.pipe_to == 'less'
389+
assert statement.pipe_to == ['less']
389390

390391
def test_parse_alias_terminator_no_whitespace(parser):
391392
line = 'helpalias;'

0 commit comments

Comments
 (0)