118
118
from .history import (
119
119
History ,
120
120
HistoryItem ,
121
+ single_line_format ,
121
122
)
122
123
from .parsing import (
123
124
Macro ,
@@ -2227,7 +2228,7 @@ def complete( # type: ignore[override]
2227
2228
# Check if we are completing a multiline command
2228
2229
if self ._at_continuation_prompt :
2229
2230
# lstrip and prepend the previously typed portion of this multiline command
2230
- lstripped_previous = self ._multiline_in_progress .lstrip (). replace ( constants . LINE_FEED , ' ' )
2231
+ lstripped_previous = self ._multiline_in_progress .lstrip ()
2231
2232
line = lstripped_previous + readline .get_line_buffer ()
2232
2233
2233
2234
# Increment the indexes to account for the prepended text
@@ -2503,7 +2504,13 @@ def parseline(self, line: str) -> Tuple[str, str, str]:
2503
2504
return statement .command , statement .args , statement .command_and_args
2504
2505
2505
2506
def onecmd_plus_hooks (
2506
- self , line : str , * , add_to_history : bool = True , raise_keyboard_interrupt : bool = False , py_bridge_call : bool = False
2507
+ self ,
2508
+ line : str ,
2509
+ * ,
2510
+ add_to_history : bool = True ,
2511
+ raise_keyboard_interrupt : bool = False ,
2512
+ py_bridge_call : bool = False ,
2513
+ orig_rl_history_length : Optional [int ] = None ,
2507
2514
) -> bool :
2508
2515
"""Top-level function called by cmdloop() to handle parsing a line and running the command and all of its hooks.
2509
2516
@@ -2515,6 +2522,9 @@ def onecmd_plus_hooks(
2515
2522
:param py_bridge_call: This should only ever be set to True by PyBridge to signify the beginning
2516
2523
of an app() call from Python. It is used to enable/disable the storage of the
2517
2524
command's stdout.
2525
+ :param orig_rl_history_length: Optional length of the readline history before the current command was typed.
2526
+ This is used to assist in combining multiline readline history entries and is only
2527
+ populated by cmd2. Defaults to None.
2518
2528
:return: True if running of commands should stop
2519
2529
"""
2520
2530
import datetime
@@ -2524,7 +2534,7 @@ def onecmd_plus_hooks(
2524
2534
2525
2535
try :
2526
2536
# Convert the line into a Statement
2527
- statement = self ._input_line_to_statement (line )
2537
+ statement = self ._input_line_to_statement (line , orig_rl_history_length = orig_rl_history_length )
2528
2538
2529
2539
# call the postparsing hooks
2530
2540
postparsing_data = plugin .PostparsingData (False , statement )
@@ -2678,7 +2688,7 @@ def runcmds_plus_hooks(
2678
2688
2679
2689
return False
2680
2690
2681
- def _complete_statement (self , line : str ) -> Statement :
2691
+ def _complete_statement (self , line : str , * , orig_rl_history_length : Optional [ int ] = None ) -> Statement :
2682
2692
"""Keep accepting lines of input until the command is complete.
2683
2693
2684
2694
There is some pretty hacky code here to handle some quirks of
@@ -2687,10 +2697,29 @@ def _complete_statement(self, line: str) -> Statement:
2687
2697
backwards compatibility with the standard library version of cmd.
2688
2698
2689
2699
:param line: the line being parsed
2700
+ :param orig_rl_history_length: Optional length of the readline history before the current command was typed.
2701
+ This is used to assist in combining multiline readline history entries and is only
2702
+ populated by cmd2. Defaults to None.
2690
2703
:return: the completed Statement
2691
2704
:raises: Cmd2ShlexError if a shlex error occurs (e.g. No closing quotation)
2692
2705
:raises: EmptyStatement when the resulting Statement is blank
2693
2706
"""
2707
+
2708
+ def combine_rl_history (statement : Statement ) -> None :
2709
+ """Combine all lines of a multiline command into a single readline history entry"""
2710
+ if orig_rl_history_length is None or not statement .multiline_command :
2711
+ return
2712
+
2713
+ # Remove all previous lines added to history for this command
2714
+ while readline .get_current_history_length () > orig_rl_history_length :
2715
+ readline .remove_history_item (readline .get_current_history_length () - 1 )
2716
+
2717
+ formatted_command = single_line_format (statement )
2718
+
2719
+ # If formatted command is different than the previous history item, add it
2720
+ if orig_rl_history_length == 0 or formatted_command != readline .get_history_item (orig_rl_history_length ):
2721
+ readline .add_history (formatted_command )
2722
+
2694
2723
while True :
2695
2724
try :
2696
2725
statement = self .statement_parser .parse (line )
@@ -2702,7 +2731,7 @@ def _complete_statement(self, line: str) -> Statement:
2702
2731
# so we are done
2703
2732
break
2704
2733
except Cmd2ShlexError :
2705
- # we have unclosed quotation marks, lets parse only the command
2734
+ # we have an unclosed quotation mark, let's parse only the command
2706
2735
# and see if it's a multiline
2707
2736
statement = self .statement_parser .parse_command_only (line )
2708
2737
if not statement .multiline_command :
@@ -2727,6 +2756,12 @@ def _complete_statement(self, line: str) -> Statement:
2727
2756
nextline = '\n '
2728
2757
self .poutput (nextline )
2729
2758
line = f'{ self ._multiline_in_progress } { nextline } '
2759
+
2760
+ # Combine all lines of this multiline command as we go.
2761
+ if nextline :
2762
+ statement = self .statement_parser .parse_command_only (line )
2763
+ combine_rl_history (statement )
2764
+
2730
2765
except KeyboardInterrupt :
2731
2766
self .poutput ('^C' )
2732
2767
statement = self .statement_parser .parse ('' )
@@ -2736,13 +2771,20 @@ def _complete_statement(self, line: str) -> Statement:
2736
2771
2737
2772
if not statement .command :
2738
2773
raise EmptyStatement
2774
+ else :
2775
+ # If necessary, update history with completed multiline command.
2776
+ combine_rl_history (statement )
2777
+
2739
2778
return statement
2740
2779
2741
- def _input_line_to_statement (self , line : str ) -> Statement :
2780
+ def _input_line_to_statement (self , line : str , * , orig_rl_history_length : Optional [ int ] = None ) -> Statement :
2742
2781
"""
2743
2782
Parse the user's input line and convert it to a Statement, ensuring that all macros are also resolved
2744
2783
2745
2784
:param line: the line being parsed
2785
+ :param orig_rl_history_length: Optional length of the readline history before the current command was typed.
2786
+ This is used to assist in combining multiline readline history entries and is only
2787
+ populated by cmd2. Defaults to None.
2746
2788
:return: parsed command line as a Statement
2747
2789
:raises: Cmd2ShlexError if a shlex error occurs (e.g. No closing quotation)
2748
2790
:raises: EmptyStatement when the resulting Statement is blank
@@ -2753,11 +2795,13 @@ def _input_line_to_statement(self, line: str) -> Statement:
2753
2795
# Continue until all macros are resolved
2754
2796
while True :
2755
2797
# Make sure all input has been read and convert it to a Statement
2756
- statement = self ._complete_statement (line )
2798
+ statement = self ._complete_statement (line , orig_rl_history_length = orig_rl_history_length )
2757
2799
2758
- # Save the fully entered line if this is the first loop iteration
2800
+ # If this is the first loop iteration, save the original line and stop
2801
+ # combining multiline history entries in the remaining iterations.
2759
2802
if orig_line is None :
2760
2803
orig_line = statement .raw
2804
+ orig_rl_history_length = None
2761
2805
2762
2806
# Check if this command matches a macro and wasn't already processed to avoid an infinite loop
2763
2807
if statement .command in self .macros .keys () and statement .command not in used_macros :
@@ -3111,7 +3155,7 @@ def configure_readline() -> None:
3111
3155
nonlocal saved_history
3112
3156
nonlocal parser
3113
3157
3114
- if readline_configured : # pragma: no cover
3158
+ if readline_configured or rl_type == RlType . NONE : # pragma: no cover
3115
3159
return
3116
3160
3117
3161
# Configure tab completion
@@ -3163,7 +3207,7 @@ def complete_none(text: str, state: int) -> Optional[str]: # pragma: no cover
3163
3207
def restore_readline () -> None :
3164
3208
"""Restore readline tab completion and history"""
3165
3209
nonlocal readline_configured
3166
- if not readline_configured : # pragma: no cover
3210
+ if not readline_configured or rl_type == RlType . NONE : # pragma: no cover
3167
3211
return
3168
3212
3169
3213
if self ._completion_supported ():
@@ -3310,6 +3354,13 @@ def _cmdloop(self) -> None:
3310
3354
self ._startup_commands .clear ()
3311
3355
3312
3356
while not stop :
3357
+ # Used in building multiline readline history entries. Only applies
3358
+ # when command line is read by input() in a terminal.
3359
+ if rl_type != RlType .NONE and self .use_rawinput and sys .stdin .isatty ():
3360
+ orig_rl_history_length = readline .get_current_history_length ()
3361
+ else :
3362
+ orig_rl_history_length = None
3363
+
3313
3364
# Get commands from user
3314
3365
try :
3315
3366
line = self ._read_command_line (self .prompt )
@@ -3318,7 +3369,7 @@ def _cmdloop(self) -> None:
3318
3369
line = ''
3319
3370
3320
3371
# Run the command along with all associated pre and post hooks
3321
- stop = self .onecmd_plus_hooks (line )
3372
+ stop = self .onecmd_plus_hooks (line , orig_rl_history_length = orig_rl_history_length )
3322
3373
finally :
3323
3374
# Get sigint protection while we restore readline settings
3324
3375
with self .sigint_protection :
@@ -4871,15 +4922,13 @@ def _initialize_history(self, hist_file: str) -> None:
4871
4922
4872
4923
# Populate readline history
4873
4924
if rl_type != RlType .NONE :
4874
- last = None
4875
4925
for item in self .history :
4876
- # Break the command into its individual lines
4877
- for line in item .raw .splitlines ():
4878
- # readline only adds a single entry for multiple sequential identical lines
4879
- # so we emulate that behavior here
4880
- if line != last :
4881
- readline .add_history (line )
4882
- last = line
4926
+ formatted_command = single_line_format (item .statement )
4927
+
4928
+ # If formatted command is different than the previous history item, add it
4929
+ cur_history_length = readline .get_current_history_length ()
4930
+ if cur_history_length == 0 or formatted_command != readline .get_history_item (cur_history_length ):
4931
+ readline .add_history (formatted_command )
4883
4932
4884
4933
def _persist_history (self ) -> None :
4885
4934
"""Write history out to the persistent history file as compressed JSON"""
0 commit comments