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 :
@@ -2718,6 +2747,7 @@ def _complete_statement(self, line: str) -> Statement:
2718
2747
# Save the command line up to this point for tab completion
2719
2748
self ._multiline_in_progress = line + '\n '
2720
2749
2750
+ # Get next line of this command
2721
2751
nextline = self ._read_command_line (self .continuation_prompt )
2722
2752
if nextline == 'eof' :
2723
2753
# they entered either a blank line, or we hit an EOF
@@ -2726,7 +2756,14 @@ def _complete_statement(self, line: str) -> Statement:
2726
2756
# terminator
2727
2757
nextline = '\n '
2728
2758
self .poutput (nextline )
2729
- line = f'{ self ._multiline_in_progress } { nextline } '
2759
+
2760
+ line += f'\n { nextline } '
2761
+
2762
+ # Combine all history lines of this multiline command as we go.
2763
+ if nextline :
2764
+ statement = self .statement_parser .parse_command_only (line )
2765
+ combine_rl_history (statement )
2766
+
2730
2767
except KeyboardInterrupt :
2731
2768
self .poutput ('^C' )
2732
2769
statement = self .statement_parser .parse ('' )
@@ -2736,13 +2773,20 @@ def _complete_statement(self, line: str) -> Statement:
2736
2773
2737
2774
if not statement .command :
2738
2775
raise EmptyStatement
2776
+ else :
2777
+ # If necessary, update history with completed multiline command.
2778
+ combine_rl_history (statement )
2779
+
2739
2780
return statement
2740
2781
2741
- def _input_line_to_statement (self , line : str ) -> Statement :
2782
+ def _input_line_to_statement (self , line : str , * , orig_rl_history_length : Optional [ int ] = None ) -> Statement :
2742
2783
"""
2743
2784
Parse the user's input line and convert it to a Statement, ensuring that all macros are also resolved
2744
2785
2745
2786
:param line: the line being parsed
2787
+ :param orig_rl_history_length: Optional length of the readline history before the current command was typed.
2788
+ This is used to assist in combining multiline readline history entries and is only
2789
+ populated by cmd2. Defaults to None.
2746
2790
:return: parsed command line as a Statement
2747
2791
:raises: Cmd2ShlexError if a shlex error occurs (e.g. No closing quotation)
2748
2792
:raises: EmptyStatement when the resulting Statement is blank
@@ -2753,11 +2797,13 @@ def _input_line_to_statement(self, line: str) -> Statement:
2753
2797
# Continue until all macros are resolved
2754
2798
while True :
2755
2799
# Make sure all input has been read and convert it to a Statement
2756
- statement = self ._complete_statement (line )
2800
+ statement = self ._complete_statement (line , orig_rl_history_length = orig_rl_history_length )
2757
2801
2758
- # Save the fully entered line if this is the first loop iteration
2802
+ # If this is the first loop iteration, save the original line and stop
2803
+ # combining multiline history entries in the remaining iterations.
2759
2804
if orig_line is None :
2760
2805
orig_line = statement .raw
2806
+ orig_rl_history_length = None
2761
2807
2762
2808
# Check if this command matches a macro and wasn't already processed to avoid an infinite loop
2763
2809
if statement .command in self .macros .keys () and statement .command not in used_macros :
@@ -3111,7 +3157,7 @@ def configure_readline() -> None:
3111
3157
nonlocal saved_history
3112
3158
nonlocal parser
3113
3159
3114
- if readline_configured : # pragma: no cover
3160
+ if readline_configured or rl_type == RlType . NONE : # pragma: no cover
3115
3161
return
3116
3162
3117
3163
# Configure tab completion
@@ -3163,7 +3209,7 @@ def complete_none(text: str, state: int) -> Optional[str]: # pragma: no cover
3163
3209
def restore_readline () -> None :
3164
3210
"""Restore readline tab completion and history"""
3165
3211
nonlocal readline_configured
3166
- if not readline_configured : # pragma: no cover
3212
+ if not readline_configured or rl_type == RlType . NONE : # pragma: no cover
3167
3213
return
3168
3214
3169
3215
if self ._completion_supported ():
@@ -3310,6 +3356,13 @@ def _cmdloop(self) -> None:
3310
3356
self ._startup_commands .clear ()
3311
3357
3312
3358
while not stop :
3359
+ # Used in building multiline readline history entries. Only applies
3360
+ # when command line is read by input() in a terminal.
3361
+ if rl_type != RlType .NONE and self .use_rawinput and sys .stdin .isatty ():
3362
+ orig_rl_history_length = readline .get_current_history_length ()
3363
+ else :
3364
+ orig_rl_history_length = None
3365
+
3313
3366
# Get commands from user
3314
3367
try :
3315
3368
line = self ._read_command_line (self .prompt )
@@ -3318,7 +3371,7 @@ def _cmdloop(self) -> None:
3318
3371
line = ''
3319
3372
3320
3373
# Run the command along with all associated pre and post hooks
3321
- stop = self .onecmd_plus_hooks (line )
3374
+ stop = self .onecmd_plus_hooks (line , orig_rl_history_length = orig_rl_history_length )
3322
3375
finally :
3323
3376
# Get sigint protection while we restore readline settings
3324
3377
with self .sigint_protection :
@@ -4871,15 +4924,13 @@ def _initialize_history(self, hist_file: str) -> None:
4871
4924
4872
4925
# Populate readline history
4873
4926
if rl_type != RlType .NONE :
4874
- last = None
4875
4927
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
4928
+ formatted_command = single_line_format (item .statement )
4929
+
4930
+ # If formatted command is different than the previous history item, add it
4931
+ cur_history_length = readline .get_current_history_length ()
4932
+ if cur_history_length == 0 or formatted_command != readline .get_history_item (cur_history_length ):
4933
+ readline .add_history (formatted_command )
4883
4934
4884
4935
def _persist_history (self ) -> None :
4885
4936
"""Write history out to the persistent history file as compressed JSON"""
0 commit comments