Skip to content

Commit 93a7eb7

Browse files
authored
Merge pull request #220 from python-cmd2/piped_input_improvements
Piped input now properly honors the echo setting
2 parents ba1319f + 9ab2614 commit 93a7eb7

File tree

2 files changed

+136
-14
lines changed

2 files changed

+136
-14
lines changed

cmd2.py

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -717,8 +717,9 @@ def postparsing_postcmd(self, stop):
717717
"""
718718
if not sys.platform.startswith('win'):
719719
# Fix those annoying problems that occur with terminal programs like "less" when you pipe to them
720-
proc = subprocess.Popen(shlex.split('stty sane'))
721-
proc.communicate()
720+
if self.stdin.isatty():
721+
proc = subprocess.Popen(shlex.split('stty sane'))
722+
proc.communicate()
722723
return stop
723724

724725
def parseline(self, line):
@@ -969,25 +970,46 @@ def _surround_ansi_escapes(prompt, start="\x01", end="\x02"):
969970
return result
970971

971972
def pseudo_raw_input(self, prompt):
972-
"""copied from cmd's cmdloop; like raw_input, but accounts for changed stdin, stdout"""
973+
"""
974+
began life as a copy of cmd's cmdloop; like raw_input but
975+
976+
- accounts for changed stdin, stdout
977+
- if input is a pipe (instead of a tty), look at self.echo
978+
to decide whether to print the prompt and the input
979+
"""
973980

974981
# Deal with the vagaries of readline and ANSI escape codes
975982
safe_prompt = self._surround_ansi_escapes(prompt)
976983

977984
if self.use_rawinput:
978985
try:
979-
line = sm.input(safe_prompt)
986+
if sys.stdin.isatty():
987+
line = sm.input(safe_prompt)
988+
else:
989+
line = sm.input()
990+
if self.echo:
991+
sys.stdout.write('{}{}\n'.format(safe_prompt,line))
980992
except EOFError:
981993
line = 'eof'
982994
else:
983-
self.poutput(safe_prompt, end='')
984-
self.stdout.flush()
985-
line = self.stdin.readline()
986-
if not len(line):
987-
line = 'eof'
995+
if self.stdin.isatty():
996+
# on a tty, print the prompt first, then read the line
997+
self.poutput(safe_prompt, end='')
998+
self.stdout.flush()
999+
line = self.stdin.readline()
1000+
if len(line) == 0:
1001+
line = 'eof'
9881002
else:
989-
line = line.rstrip('\r\n')
990-
1003+
# we are reading from a pipe, read the line to see if there is
1004+
# anything there, if so, then decide whether to print the
1005+
# prompt or not
1006+
line = self.stdin.readline()
1007+
if len(line):
1008+
# we read something, output the prompt and the something
1009+
if self.echo:
1010+
self.poutput('{}{}'.format(safe_prompt, line))
1011+
else:
1012+
line = 'eof'
9911013
return line.strip()
9921014

9931015
def _cmdloop(self):

tests/test_cmd2.py

Lines changed: 103 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"""
88
import os
99
import sys
10+
import io
1011
import tempfile
1112

1213
import mock
@@ -849,6 +850,7 @@ def test_cmdloop_without_rawinput():
849850
# Create a cmd2.Cmd() instance and make sure basic settings are like we want for test
850851
app = cmd2.Cmd()
851852
app.use_rawinput = False
853+
app.echo = False
852854
app.intro = 'Hello World, this is an intro ...'
853855
app.stdout = StdOut()
854856

@@ -858,7 +860,7 @@ def test_cmdloop_without_rawinput():
858860

859861
# Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args
860862
testargs = ["prog"]
861-
expected = app.intro + '\n{}'.format(app.prompt)
863+
expected = app.intro + '\n'
862864
with mock.patch.object(sys, 'argv', testargs):
863865
# Run the command loop
864866
app.cmdloop()
@@ -1367,7 +1369,6 @@ def test_eos(base_app):
13671369
# And make sure it reduced the length of the script dir list
13681370
assert len(base_app._script_dir) == 0
13691371

1370-
13711372
def test_echo(capsys):
13721373
app = cmd2.Cmd()
13731374
# Turn echo on and pre-stage some commands in the queue, simulating like we are in the middle of a script
@@ -1388,7 +1389,106 @@ def test_echo(capsys):
13881389
assert app._current_script_dir is None
13891390
assert out.startswith('{}{}\n'.format(app.prompt, command) + 'history [arg]: lists past commands issued')
13901391

1391-
1392+
def test_pseudo_raw_input_tty_rawinput_true():
1393+
# use context managers so original functions get put back when we are done
1394+
# we dont use decorators because we need m_input for the assertion
1395+
with mock.patch('sys.stdin.isatty',
1396+
mock.MagicMock(name='isatty', return_value=True)):
1397+
with mock.patch('six.moves.input',
1398+
mock.MagicMock(name='input', side_effect=['set', EOFError])) as m_input:
1399+
# run the cmdloop, which should pull input from our mocks
1400+
app = cmd2.Cmd()
1401+
app.use_rawinput = True
1402+
app._cmdloop()
1403+
# because we mocked the input() call, we won't get the prompt
1404+
# or the name of the command in the output, so we can't check
1405+
# if its there. We assume that if input got called twice, once
1406+
# for the 'set' command, and once for the 'quit' command,
1407+
# that the rest of it worked
1408+
assert m_input.call_count == 2
1409+
1410+
def test_pseudo_raw_input_tty_rawinput_false():
1411+
# gin up some input like it's coming from a tty
1412+
fakein = io.StringIO(u'{}'.format('set\n'))
1413+
mtty = mock.MagicMock(name='isatty', return_value=True)
1414+
fakein.isatty = mtty
1415+
mreadline = mock.MagicMock(name='readline', wraps=fakein.readline)
1416+
fakein.readline = mreadline
1417+
1418+
# run the cmdloop, telling it where to get input from
1419+
app = cmd2.Cmd(stdin=fakein)
1420+
app.use_rawinput = False
1421+
app._cmdloop()
1422+
1423+
# because we mocked the readline() call, we won't get the prompt
1424+
# or the name of the command in the output, so we can't check
1425+
# if its there. We assume that if readline() got called twice, once
1426+
# for the 'set' command, and once for the 'quit' command,
1427+
# that the rest of it worked
1428+
assert mreadline.call_count == 2
1429+
1430+
# the next helper function and two tests check for piped
1431+
# input when use_rawinput is True.
1432+
def piped_rawinput_true(capsys, echo, command):
1433+
app = cmd2.Cmd()
1434+
app.use_rawinput = True
1435+
app.echo = echo
1436+
# run the cmdloop, which should pull input from our mock
1437+
app._cmdloop()
1438+
out, err = capsys.readouterr()
1439+
return (app, out)
1440+
1441+
# using the decorator puts the original function at six.moves.input
1442+
# back when this method returns
1443+
@mock.patch('six.moves.input',
1444+
mock.MagicMock(name='input', side_effect=['set', EOFError]))
1445+
def test_pseudo_raw_input_piped_rawinput_true_echo_true(capsys):
1446+
command = 'set'
1447+
app, out = piped_rawinput_true(capsys, True, command)
1448+
out = out.splitlines()
1449+
assert out[0] == '{}{}'.format(app.prompt, command)
1450+
assert out[1] == 'abbrev: False'
1451+
1452+
# using the decorator puts the original function at six.moves.input
1453+
# back when this method returns
1454+
@mock.patch('six.moves.input',
1455+
mock.MagicMock(name='input', side_effect=['set', EOFError]))
1456+
def test_pseudo_raw_input_piped_rawinput_true_echo_false(capsys):
1457+
command = 'set'
1458+
app, out = piped_rawinput_true(capsys, False, command)
1459+
firstline = out.splitlines()[0]
1460+
assert firstline == 'abbrev: False'
1461+
assert not '{}{}'.format(app.prompt, command) in out
1462+
1463+
# the next helper function and two tests check for piped
1464+
# input when use_rawinput=False
1465+
def piped_rawinput_false(capsys, echo, command):
1466+
fakein = io.StringIO(u'{}'.format(command))
1467+
# run the cmdloop, telling it where to get input from
1468+
app = cmd2.Cmd(stdin=fakein)
1469+
app.use_rawinput = False
1470+
app.echo = echo
1471+
app.abbrev = False
1472+
app._cmdloop()
1473+
out, err = capsys.readouterr()
1474+
return (app, out)
1475+
1476+
def test_pseudo_raw_input_piped_rawinput_false_echo_true(capsys):
1477+
command = 'set'
1478+
app, out = piped_rawinput_false(capsys, True, command)
1479+
out = out.splitlines()
1480+
assert out[0] == '{}{}'.format(app.prompt, command)
1481+
assert out[1] == 'abbrev: False'
1482+
1483+
def test_pseudo_raw_input_piped_rawinput_false_echo_false(capsys):
1484+
command = 'set'
1485+
app, out = piped_rawinput_false(capsys, False, command)
1486+
firstline = out.splitlines()[0]
1487+
assert firstline == 'abbrev: False'
1488+
assert not '{}{}'.format(app.prompt, command) in out
1489+
1490+
#
1491+
# other input tests
13921492
def test_raw_input(base_app):
13931493
base_app.use_raw_input = True
13941494
fake_input = 'quit'

0 commit comments

Comments
 (0)