Skip to content

Commit 5d2133a

Browse files
authored
Merge pull request #467 from python-cmd2/history
History clear
2 parents ea7a4bb + 4a233b8 commit 5d2133a

File tree

5 files changed

+100
-37
lines changed

5 files changed

+100
-37
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
* Bug Fixes
33
* Fixed bug when StatementParser ``__init__()`` was called with ``terminators`` equal to ``None``
44
* Fixed bug when ``Cmd.onecmd()`` was called with a raw ``str``
5+
* Enhancements
6+
* Added ``--clear`` flag to ``history`` command that clears both the command and readline history.
57
* Deletions
68
* The ``CmdResult`` helper class which was *deprecated* in the previous release has now been deleted
79
* It has been replaced by the improved ``CommandResult`` class

cmd2/cmd2.py

Lines changed: 59 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -369,18 +369,6 @@ def __init__(self, completekey: str='tab', stdin=None, stdout=None, persistent_h
369369
except AttributeError:
370370
pass
371371

372-
# If persistent readline history is enabled, then read history from file and register to write to file at exit
373-
if persistent_history_file and rl_type != RlType.NONE:
374-
persistent_history_file = os.path.expanduser(persistent_history_file)
375-
try:
376-
readline.read_history_file(persistent_history_file)
377-
# default history len is -1 (infinite), which may grow unruly
378-
readline.set_history_length(persistent_history_length)
379-
except FileNotFoundError:
380-
pass
381-
import atexit
382-
atexit.register(readline.write_history_file, persistent_history_file)
383-
384372
# Call super class constructor
385373
super().__init__(completekey=completekey, stdin=stdin, stdout=stdout)
386374

@@ -448,6 +436,37 @@ def __init__(self, completekey: str='tab', stdin=None, stdout=None, persistent_h
448436
# If this string is non-empty, then this warning message will print if a broken pipe error occurs while printing
449437
self.broken_pipe_warning = ''
450438

439+
# Check if history should persist
440+
if persistent_history_file and rl_type != RlType.NONE:
441+
persistent_history_file = os.path.expanduser(persistent_history_file)
442+
read_err = False
443+
444+
try:
445+
# First try to read any existing history file
446+
readline.read_history_file(persistent_history_file)
447+
except FileNotFoundError:
448+
pass
449+
except OSError as ex:
450+
self.perror("readline cannot read persistent history file '{}': {}".format(persistent_history_file, ex),
451+
traceback_war=False)
452+
read_err = True
453+
454+
if not read_err:
455+
try:
456+
# Make sure readline is able to write the history file. Doing it this way is a more thorough check
457+
# than trying to open the file with write access since readline's underlying function needs to
458+
# create a temporary file in the same directory and may not have permission.
459+
readline.set_history_length(persistent_history_length)
460+
readline.write_history_file(persistent_history_file)
461+
except OSError as ex:
462+
self.perror("readline cannot write persistent history file '{}': {}".
463+
format(persistent_history_file, ex), traceback_war=False)
464+
else:
465+
# Set history file and register to save our history at exit
466+
import atexit
467+
self.persistent_history_file = persistent_history_file
468+
atexit.register(readline.write_history_file, self.persistent_history_file)
469+
451470
# If a startup script is provided, then add it in the queue to load
452471
if startup_script is not None:
453472
startup_script = os.path.expanduser(startup_script)
@@ -610,7 +629,7 @@ def ppaged(self, msg: str, end: str='\n', chop: bool=False) -> None:
610629
try:
611630
self.pipe_proc.stdin.write(msg_str.encode('utf-8', 'replace'))
612631
self.pipe_proc.stdin.close()
613-
except (IOError, KeyboardInterrupt):
632+
except (OSError, KeyboardInterrupt):
614633
pass
615634

616635
# Less doesn't respect ^C, but catches it for its own UI purposes (aborting search etc. inside less)
@@ -2574,8 +2593,9 @@ def run(filename):
25742593
try:
25752594
with open(filename) as f:
25762595
interp.runcode(f.read())
2577-
except IOError as e:
2578-
self.perror(e)
2596+
except OSError as ex:
2597+
error_msg = "Error opening script file '{}': {}".format(filename, ex)
2598+
self.perror(error_msg, traceback_war=False)
25792599

25802600
bridge = PyscriptBridge(self)
25812601
self.pystate['run'] = run
@@ -2769,6 +2789,7 @@ def load_ipy(app):
27692789
history_parser_group.add_argument('-s', '--script', action='store_true', help='script format; no separation lines')
27702790
history_parser_group.add_argument('-o', '--output-file', metavar='FILE', help='output commands to a script file')
27712791
history_parser_group.add_argument('-t', '--transcript', help='output commands and results to a transcript file')
2792+
history_parser_group.add_argument('-c', '--clear', action="store_true", help='clears all history')
27722793
_history_arg_help = """empty all history items
27732794
a one history item by number
27742795
a..b, a:b, a:, ..b items by indices (inclusive)
@@ -2778,7 +2799,18 @@ def load_ipy(app):
27782799

27792800
@with_argparser(history_parser)
27802801
def do_history(self, args: argparse.Namespace) -> None:
2781-
"""View, run, edit, and save previously entered commands."""
2802+
"""View, run, edit, save, or clear previously entered commands."""
2803+
2804+
if args.clear:
2805+
# Clear command and readline history
2806+
self.history.clear()
2807+
2808+
if rl_type != RlType.NONE:
2809+
readline.clear_history()
2810+
if self.persistent_history_file:
2811+
os.remove(self.persistent_history_file)
2812+
return
2813+
27822814
# If an argument was supplied, then retrieve partial contents of the history
27832815
cowardly_refuse_to_run = False
27842816
if args.arg:
@@ -2984,25 +3016,30 @@ def do_load(self, arglist: List[str]) -> None:
29843016
"""
29853017
# If arg is None or arg is an empty string this is an error
29863018
if not arglist:
2987-
self.perror('load command requires a file path:', traceback_war=False)
3019+
self.perror('load command requires a file path', traceback_war=False)
29883020
return
29893021

29903022
file_path = arglist[0].strip()
29913023
expanded_path = os.path.abspath(os.path.expanduser(file_path))
29923024

3025+
# Make sure the path exists and we can access it
3026+
if not os.path.exists(expanded_path):
3027+
self.perror("'{}' does not exist or cannot be accessed".format(expanded_path), traceback_war=False)
3028+
return
3029+
29933030
# Make sure expanded_path points to a file
29943031
if not os.path.isfile(expanded_path):
2995-
self.perror('{} does not exist or is not a file'.format(expanded_path), traceback_war=False)
3032+
self.perror("'{}' is not a file".format(expanded_path), traceback_war=False)
29963033
return
29973034

29983035
# Make sure the file is not empty
29993036
if os.path.getsize(expanded_path) == 0:
3000-
self.perror('{} is empty'.format(expanded_path), traceback_war=False)
3037+
self.perror("'{}' is empty".format(expanded_path), traceback_war=False)
30013038
return
30023039

30033040
# Make sure the file is ASCII or UTF-8 encoded text
30043041
if not utils.is_text_file(expanded_path):
3005-
self.perror('{} is not an ASCII or UTF-8 encoded text file'.format(expanded_path), traceback_war=False)
3042+
self.perror("'{}' is not an ASCII or UTF-8 encoded text file".format(expanded_path), traceback_war=False)
30063043
return
30073044

30083045
try:
@@ -3011,8 +3048,8 @@ def do_load(self, arglist: List[str]) -> None:
30113048
# self._script_dir list when done.
30123049
with open(expanded_path, encoding='utf-8') as target:
30133050
self.cmdqueue = target.read().splitlines() + ['eos'] + self.cmdqueue
3014-
except IOError as e: # pragma: no cover
3015-
self.perror('Problem accessing script from {}:\n{}'.format(expanded_path, e))
3051+
except OSError as ex: # pragma: no cover
3052+
self.perror("Problem accessing script from '{}': {}".format(expanded_path, ex))
30163053
return
30173054

30183055
self._script_dir.append(os.path.dirname(expanded_path))

cmd2/utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def is_text_file(file_path: str) -> bool:
127127
# noinspection PyUnusedLocal
128128
if sum(1 for line in f) > 0:
129129
valid_text_file = True
130-
except IOError: # pragma: no cover
130+
except OSError: # pragma: no cover
131131
pass
132132
except UnicodeDecodeError:
133133
# The file is not ASCII. Check if it is UTF-8.
@@ -137,7 +137,7 @@ def is_text_file(file_path: str) -> bool:
137137
# noinspection PyUnusedLocal
138138
if sum(1 for line in f) > 0:
139139
valid_text_file = True
140-
except IOError: # pragma: no cover
140+
except OSError: # pragma: no cover
141141
pass
142142
except UnicodeDecodeError:
143143
# Not UTF-8

tests/conftest.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
alias Define or display aliases
3838
edit Edit a file in a text editor.
3939
help List available commands with "help" or detailed help with "help cmd".
40-
history View, run, edit, and save previously entered commands.
40+
history View, run, edit, save, or clear previously entered commands.
4141
load Runs commands in script file that is encoded as either ASCII or UTF-8 text.
4242
py Invoke python command, shell, or script
4343
pyscript Runs a python script file inside the console
@@ -49,9 +49,9 @@
4949
"""
5050

5151
# Help text for the history command
52-
HELP_HISTORY = """usage: history [-h] [-r | -e | -s | -o FILE | -t TRANSCRIPT] [arg]
52+
HELP_HISTORY = """usage: history [-h] [-r | -e | -s | -o FILE | -t TRANSCRIPT | -c] [arg]
5353
54-
View, run, edit, and save previously entered commands.
54+
View, run, edit, save, or clear previously entered commands.
5555
5656
positional arguments:
5757
arg empty all history items
@@ -69,6 +69,7 @@
6969
output commands to a script file
7070
-t TRANSCRIPT, --transcript TRANSCRIPT
7171
output commands and results to a transcript file
72+
-c, --clear clears all history
7273
"""
7374

7475
# Output from the shortcuts command with default built-in shortcuts

tests/test_cmd2.py

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ def test_pyscript_with_nonexist_file(base_app, capsys):
231231
python_script = 'does_not_exist.py'
232232
run_cmd(base_app, "pyscript {}".format(python_script))
233233
out, err = capsys.readouterr()
234-
assert err.startswith("EXCEPTION of type 'FileNotFoundError' occurred with message:")
234+
assert "Error opening script file" in err
235235

236236
def test_pyscript_with_exception(base_app, capsys, request):
237237
test_dir = os.path.dirname(request.module.__file__)
@@ -426,7 +426,7 @@ def test_history_run_all_commands(base_app):
426426
out = run_cmd(base_app, 'history -r')
427427
# this should generate an error, but we don't currently have a way to
428428
# capture stderr in these tests. So we assume that if we got nothing on
429-
# standard out, that the error occured because if the commaned executed
429+
# standard out, that the error occurred because if the command executed
430430
# then we should have a list of shortcuts in our output
431431
assert out == []
432432

@@ -435,6 +435,23 @@ def test_history_run_one_command(base_app):
435435
output = run_cmd(base_app, 'history -r 1')
436436
assert output == expected
437437

438+
def test_history_clear(base_app):
439+
# Add commands to history
440+
run_cmd(base_app, 'help')
441+
run_cmd(base_app, 'alias')
442+
443+
# Make sure history has items
444+
out = run_cmd(base_app, 'history')
445+
assert out
446+
447+
# Clear the history
448+
run_cmd(base_app, 'history --clear')
449+
450+
# Make sure history is empty
451+
out = run_cmd(base_app, 'history')
452+
assert out == []
453+
454+
438455
def test_base_load(base_app, request):
439456
test_dir = os.path.dirname(request.module.__file__)
440457
filename = os.path.join(test_dir, 'script.txt')
@@ -457,8 +474,7 @@ def test_load_with_empty_args(base_app, capsys):
457474
out, err = capsys.readouterr()
458475

459476
# The load command requires a file path argument, so we should get an error message
460-
expected = normalize("""ERROR: load command requires a file path:\n""")
461-
assert normalize(str(err)) == expected
477+
assert "load command requires a file path" in str(err)
462478
assert base_app.cmdqueue == []
463479

464480

@@ -468,10 +484,18 @@ def test_load_with_nonexistent_file(base_app, capsys):
468484
out, err = capsys.readouterr()
469485

470486
# The load command requires a path to an existing file
471-
assert str(err).startswith("ERROR")
472-
assert "does not exist or is not a file" in str(err)
487+
assert "does not exist" in str(err)
473488
assert base_app.cmdqueue == []
474489

490+
def test_load_with_directory(base_app, capsys, request):
491+
test_dir = os.path.dirname(request.module.__file__)
492+
493+
# The way the load command works, we can't directly capture its stdout or stderr
494+
run_cmd(base_app, 'load {}'.format(test_dir))
495+
out, err = capsys.readouterr()
496+
497+
assert "is not a file" in str(err)
498+
assert base_app.cmdqueue == []
475499

476500
def test_load_with_empty_file(base_app, capsys, request):
477501
test_dir = os.path.dirname(request.module.__file__)
@@ -481,8 +505,7 @@ def test_load_with_empty_file(base_app, capsys, request):
481505
run_cmd(base_app, 'load {}'.format(filename))
482506
out, err = capsys.readouterr()
483507

484-
# The load command requires non-empty scripts files
485-
assert str(err).startswith("ERROR")
508+
# The load command requires non-empty script files
486509
assert "is empty" in str(err)
487510
assert base_app.cmdqueue == []
488511

@@ -724,7 +747,7 @@ def test_pipe_to_shell(base_app, capsys):
724747
out, err = capsys.readouterr()
725748

726749
# Unfortunately with the improved way of piping output to a subprocess, there isn't any good way of getting
727-
# access to the output produced by that subprocess within a unit test, but we can verify that no error occured
750+
# access to the output produced by that subprocess within a unit test, but we can verify that no error occurred
728751
assert not err
729752

730753
def test_pipe_to_shell_error(base_app, capsys):
@@ -1225,7 +1248,7 @@ def test_help_cat_verbose(helpcat_app):
12251248
================================================================================
12261249
alias Define or display aliases
12271250
help List available commands with "help" or detailed help with "help cmd".
1228-
history View, run, edit, and save previously entered commands.
1251+
history View, run, edit, save, or clear previously entered commands.
12291252
load Runs commands in script file that is encoded as either ASCII or UTF-8 text.
12301253
py Invoke python command, shell, or script
12311254
pyscript Runs a python script file inside the console

0 commit comments

Comments
 (0)