Skip to content

Commit ce5092f

Browse files
committed
Remove cmd2.Cmd.redirector for #381
1 parent 9d4d929 commit ce5092f

File tree

8 files changed

+58
-57
lines changed

8 files changed

+58
-57
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: 12 additions & 7 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 = [';']
@@ -1820,7 +1819,7 @@ def _redirect_output(self, statement):
18201819

18211820
# We want Popen to raise an exception if it fails to open the process. Thus we don't set shell to True.
18221821
try:
1823-
self.pipe_proc = subprocess.Popen(shlex.split(statement.pipe_to), stdin=subproc_stdin)
1822+
self.pipe_proc = subprocess.Popen(statement.pipe_to, stdin=subproc_stdin)
18241823
except Exception as ex:
18251824
# Restore stdout to what it was and close the pipe
18261825
self.stdout.close()
@@ -1834,24 +1833,30 @@ def _redirect_output(self, statement):
18341833
raise ex
18351834
elif statement.output:
18361835
if (not statement.output_to) and (not can_clip):
1837-
raise EnvironmentError('Cannot redirect to paste buffer; install ``xclip`` and re-run to enable')
1836+
raise EnvironmentError("Cannot redirect to paste buffer; install 'pyperclip' and re-run to enable")
18381837
self.kept_state = Statekeeper(self, ('stdout',))
18391838
self.kept_sys = Statekeeper(sys, ('stdout',))
18401839
self.redirecting = True
18411840
if statement.output_to:
1841+
# going to a file
18421842
mode = 'w'
1843-
if statement.output == 2 * self.redirector:
1843+
# statement.output can only contain
1844+
# REDIRECTION_APPEND or REDIRECTION_OUTPUT
1845+
if statement.output == constants.REDIRECTION_APPEND:
18441846
mode = 'a'
18451847
sys.stdout = self.stdout = open(os.path.expanduser(statement.output_to), mode)
18461848
else:
1849+
# going to a paste buffer
18471850
sys.stdout = self.stdout = tempfile.TemporaryFile(mode="w+")
1848-
if statement.output == '>>':
1851+
if statement.output == constants.REDIRECTION_APPEND:
18491852
self.poutput(get_paste_buffer())
18501853

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

cmd2/constants.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@
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]
1014

1115
# Regular expression to match ANSI escape codes
1216
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)