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 ,
@@ -2048,11 +2049,14 @@ def _perform_completion(
2048
2049
2049
2050
expanded_line = statement .command_and_args
2050
2051
2051
- # We overwrote line with a properly formatted but fully stripped version
2052
- # Restore the end spaces since line is only supposed to be lstripped when
2053
- # passed to completer functions according to Python docs
2054
- rstripped_len = len (line ) - len (line .rstrip ())
2055
- expanded_line += ' ' * rstripped_len
2052
+ if not expanded_line [- 1 :].isspace ():
2053
+ # Unquoted trailing whitespace gets stripped by parse_command_only().
2054
+ # Restore it since line is only supposed to be lstripped when passed
2055
+ # to completer functions according to the Python cmd docs. Regardless
2056
+ # of what type of whitespace (' ', \n) was stripped, just append spaces
2057
+ # since shlex treats all whitespace the same when splitting.
2058
+ rstripped_len = len (line ) - len (line .rstrip ())
2059
+ expanded_line += ' ' * rstripped_len
2056
2060
2057
2061
# Fix the index values if expanded_line has a different size than line
2058
2062
if len (expanded_line ) != len (line ):
@@ -2227,7 +2231,7 @@ def complete( # type: ignore[override]
2227
2231
# Check if we are completing a multiline command
2228
2232
if self ._at_continuation_prompt :
2229
2233
# lstrip and prepend the previously typed portion of this multiline command
2230
- lstripped_previous = self ._multiline_in_progress .lstrip (). replace ( constants . LINE_FEED , ' ' )
2234
+ lstripped_previous = self ._multiline_in_progress .lstrip ()
2231
2235
line = lstripped_previous + readline .get_line_buffer ()
2232
2236
2233
2237
# Increment the indexes to account for the prepended text
@@ -2503,7 +2507,13 @@ def parseline(self, line: str) -> Tuple[str, str, str]:
2503
2507
return statement .command , statement .args , statement .command_and_args
2504
2508
2505
2509
def onecmd_plus_hooks (
2506
- self , line : str , * , add_to_history : bool = True , raise_keyboard_interrupt : bool = False , py_bridge_call : bool = False
2510
+ self ,
2511
+ line : str ,
2512
+ * ,
2513
+ add_to_history : bool = True ,
2514
+ raise_keyboard_interrupt : bool = False ,
2515
+ py_bridge_call : bool = False ,
2516
+ orig_rl_history_length : Optional [int ] = None ,
2507
2517
) -> bool :
2508
2518
"""Top-level function called by cmdloop() to handle parsing a line and running the command and all of its hooks.
2509
2519
@@ -2515,6 +2525,9 @@ def onecmd_plus_hooks(
2515
2525
:param py_bridge_call: This should only ever be set to True by PyBridge to signify the beginning
2516
2526
of an app() call from Python. It is used to enable/disable the storage of the
2517
2527
command's stdout.
2528
+ :param orig_rl_history_length: Optional length of the readline history before the current command was typed.
2529
+ This is used to assist in combining multiline readline history entries and is only
2530
+ populated by cmd2. Defaults to None.
2518
2531
:return: True if running of commands should stop
2519
2532
"""
2520
2533
import datetime
@@ -2524,7 +2537,7 @@ def onecmd_plus_hooks(
2524
2537
2525
2538
try :
2526
2539
# Convert the line into a Statement
2527
- statement = self ._input_line_to_statement (line )
2540
+ statement = self ._input_line_to_statement (line , orig_rl_history_length = orig_rl_history_length )
2528
2541
2529
2542
# call the postparsing hooks
2530
2543
postparsing_data = plugin .PostparsingData (False , statement )
@@ -2678,7 +2691,7 @@ def runcmds_plus_hooks(
2678
2691
2679
2692
return False
2680
2693
2681
- def _complete_statement (self , line : str ) -> Statement :
2694
+ def _complete_statement (self , line : str , * , orig_rl_history_length : Optional [ int ] = None ) -> Statement :
2682
2695
"""Keep accepting lines of input until the command is complete.
2683
2696
2684
2697
There is some pretty hacky code here to handle some quirks of
@@ -2687,10 +2700,29 @@ def _complete_statement(self, line: str) -> Statement:
2687
2700
backwards compatibility with the standard library version of cmd.
2688
2701
2689
2702
:param line: the line being parsed
2703
+ :param orig_rl_history_length: Optional length of the readline history before the current command was typed.
2704
+ This is used to assist in combining multiline readline history entries and is only
2705
+ populated by cmd2. Defaults to None.
2690
2706
:return: the completed Statement
2691
2707
:raises: Cmd2ShlexError if a shlex error occurs (e.g. No closing quotation)
2692
2708
:raises: EmptyStatement when the resulting Statement is blank
2693
2709
"""
2710
+
2711
+ def combine_rl_history (statement : Statement ) -> None :
2712
+ """Combine all lines of a multiline command into a single readline history entry"""
2713
+ if orig_rl_history_length is None or not statement .multiline_command :
2714
+ return
2715
+
2716
+ # Remove all previous lines added to history for this command
2717
+ while readline .get_current_history_length () > orig_rl_history_length :
2718
+ readline .remove_history_item (readline .get_current_history_length () - 1 )
2719
+
2720
+ formatted_command = single_line_format (statement )
2721
+
2722
+ # If formatted command is different than the previous history item, add it
2723
+ if orig_rl_history_length == 0 or formatted_command != readline .get_history_item (orig_rl_history_length ):
2724
+ readline .add_history (formatted_command )
2725
+
2694
2726
while True :
2695
2727
try :
2696
2728
statement = self .statement_parser .parse (line )
@@ -2702,7 +2734,7 @@ def _complete_statement(self, line: str) -> Statement:
2702
2734
# so we are done
2703
2735
break
2704
2736
except Cmd2ShlexError :
2705
- # we have unclosed quotation marks, lets parse only the command
2737
+ # we have an unclosed quotation mark, let's parse only the command
2706
2738
# and see if it's a multiline
2707
2739
statement = self .statement_parser .parse_command_only (line )
2708
2740
if not statement .multiline_command :
@@ -2718,6 +2750,7 @@ def _complete_statement(self, line: str) -> Statement:
2718
2750
# Save the command line up to this point for tab completion
2719
2751
self ._multiline_in_progress = line + '\n '
2720
2752
2753
+ # Get next line of this command
2721
2754
nextline = self ._read_command_line (self .continuation_prompt )
2722
2755
if nextline == 'eof' :
2723
2756
# they entered either a blank line, or we hit an EOF
@@ -2726,7 +2759,14 @@ def _complete_statement(self, line: str) -> Statement:
2726
2759
# terminator
2727
2760
nextline = '\n '
2728
2761
self .poutput (nextline )
2729
- line = f'{ self ._multiline_in_progress } { nextline } '
2762
+
2763
+ line += f'\n { nextline } '
2764
+
2765
+ # Combine all history lines of this multiline command as we go.
2766
+ if nextline :
2767
+ statement = self .statement_parser .parse_command_only (line )
2768
+ combine_rl_history (statement )
2769
+
2730
2770
except KeyboardInterrupt :
2731
2771
self .poutput ('^C' )
2732
2772
statement = self .statement_parser .parse ('' )
@@ -2736,13 +2776,20 @@ def _complete_statement(self, line: str) -> Statement:
2736
2776
2737
2777
if not statement .command :
2738
2778
raise EmptyStatement
2779
+ else :
2780
+ # If necessary, update history with completed multiline command.
2781
+ combine_rl_history (statement )
2782
+
2739
2783
return statement
2740
2784
2741
- def _input_line_to_statement (self , line : str ) -> Statement :
2785
+ def _input_line_to_statement (self , line : str , * , orig_rl_history_length : Optional [ int ] = None ) -> Statement :
2742
2786
"""
2743
2787
Parse the user's input line and convert it to a Statement, ensuring that all macros are also resolved
2744
2788
2745
2789
:param line: the line being parsed
2790
+ :param orig_rl_history_length: Optional length of the readline history before the current command was typed.
2791
+ This is used to assist in combining multiline readline history entries and is only
2792
+ populated by cmd2. Defaults to None.
2746
2793
:return: parsed command line as a Statement
2747
2794
:raises: Cmd2ShlexError if a shlex error occurs (e.g. No closing quotation)
2748
2795
:raises: EmptyStatement when the resulting Statement is blank
@@ -2753,11 +2800,13 @@ def _input_line_to_statement(self, line: str) -> Statement:
2753
2800
# Continue until all macros are resolved
2754
2801
while True :
2755
2802
# Make sure all input has been read and convert it to a Statement
2756
- statement = self ._complete_statement (line )
2803
+ statement = self ._complete_statement (line , orig_rl_history_length = orig_rl_history_length )
2757
2804
2758
- # Save the fully entered line if this is the first loop iteration
2805
+ # If this is the first loop iteration, save the original line and stop
2806
+ # combining multiline history entries in the remaining iterations.
2759
2807
if orig_line is None :
2760
2808
orig_line = statement .raw
2809
+ orig_rl_history_length = None
2761
2810
2762
2811
# Check if this command matches a macro and wasn't already processed to avoid an infinite loop
2763
2812
if statement .command in self .macros .keys () and statement .command not in used_macros :
@@ -3111,7 +3160,7 @@ def configure_readline() -> None:
3111
3160
nonlocal saved_history
3112
3161
nonlocal parser
3113
3162
3114
- if readline_configured : # pragma: no cover
3163
+ if readline_configured or rl_type == RlType . NONE : # pragma: no cover
3115
3164
return
3116
3165
3117
3166
# Configure tab completion
@@ -3163,7 +3212,7 @@ def complete_none(text: str, state: int) -> Optional[str]: # pragma: no cover
3163
3212
def restore_readline () -> None :
3164
3213
"""Restore readline tab completion and history"""
3165
3214
nonlocal readline_configured
3166
- if not readline_configured : # pragma: no cover
3215
+ if not readline_configured or rl_type == RlType . NONE : # pragma: no cover
3167
3216
return
3168
3217
3169
3218
if self ._completion_supported ():
@@ -3310,6 +3359,13 @@ def _cmdloop(self) -> None:
3310
3359
self ._startup_commands .clear ()
3311
3360
3312
3361
while not stop :
3362
+ # Used in building multiline readline history entries. Only applies
3363
+ # when command line is read by input() in a terminal.
3364
+ if rl_type != RlType .NONE and self .use_rawinput and sys .stdin .isatty ():
3365
+ orig_rl_history_length = readline .get_current_history_length ()
3366
+ else :
3367
+ orig_rl_history_length = None
3368
+
3313
3369
# Get commands from user
3314
3370
try :
3315
3371
line = self ._read_command_line (self .prompt )
@@ -3318,7 +3374,7 @@ def _cmdloop(self) -> None:
3318
3374
line = ''
3319
3375
3320
3376
# Run the command along with all associated pre and post hooks
3321
- stop = self .onecmd_plus_hooks (line )
3377
+ stop = self .onecmd_plus_hooks (line , orig_rl_history_length = orig_rl_history_length )
3322
3378
finally :
3323
3379
# Get sigint protection while we restore readline settings
3324
3380
with self .sigint_protection :
@@ -4871,15 +4927,13 @@ def _initialize_history(self, hist_file: str) -> None:
4871
4927
4872
4928
# Populate readline history
4873
4929
if rl_type != RlType .NONE :
4874
- last = None
4875
4930
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
4931
+ formatted_command = single_line_format (item .statement )
4932
+
4933
+ # If formatted command is different than the previous history item, add it
4934
+ cur_history_length = readline .get_current_history_length ()
4935
+ if cur_history_length == 0 or formatted_command != readline .get_history_item (cur_history_length ):
4936
+ readline .add_history (formatted_command )
4883
4937
4884
4938
def _persist_history (self ) -> None :
4885
4939
"""Write history out to the persistent history file as compressed JSON"""
0 commit comments