118118from .history import (
119119 History ,
120120 HistoryItem ,
121+ single_line_format ,
121122)
122123from .parsing import (
123124 Macro ,
@@ -2048,11 +2049,14 @@ def _perform_completion(
20482049
20492050 expanded_line = statement .command_and_args
20502051
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 whitespace characters the same when splitting.
2058+ rstripped_len = len (line ) - len (line .rstrip ())
2059+ expanded_line += ' ' * rstripped_len
20562060
20572061 # Fix the index values if expanded_line has a different size than line
20582062 if len (expanded_line ) != len (line ):
@@ -2227,7 +2231,7 @@ def complete( # type: ignore[override]
22272231 # Check if we are completing a multiline command
22282232 if self ._at_continuation_prompt :
22292233 # 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 ()
22312235 line = lstripped_previous + readline .get_line_buffer ()
22322236
22332237 # Increment the indexes to account for the prepended text
@@ -2503,7 +2507,13 @@ def parseline(self, line: str) -> Tuple[str, str, str]:
25032507 return statement .command , statement .args , statement .command_and_args
25042508
25052509 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 ,
25072517 ) -> bool :
25082518 """Top-level function called by cmdloop() to handle parsing a line and running the command and all of its hooks.
25092519
@@ -2515,6 +2525,9 @@ def onecmd_plus_hooks(
25152525 :param py_bridge_call: This should only ever be set to True by PyBridge to signify the beginning
25162526 of an app() call from Python. It is used to enable/disable the storage of the
25172527 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.
25182531 :return: True if running of commands should stop
25192532 """
25202533 import datetime
@@ -2524,7 +2537,7 @@ def onecmd_plus_hooks(
25242537
25252538 try :
25262539 # 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 )
25282541
25292542 # call the postparsing hooks
25302543 postparsing_data = plugin .PostparsingData (False , statement )
@@ -2678,7 +2691,7 @@ def runcmds_plus_hooks(
26782691
26792692 return False
26802693
2681- def _complete_statement (self , line : str ) -> Statement :
2694+ def _complete_statement (self , line : str , * , orig_rl_history_length : Optional [ int ] = None ) -> Statement :
26822695 """Keep accepting lines of input until the command is complete.
26832696
26842697 There is some pretty hacky code here to handle some quirks of
@@ -2687,10 +2700,29 @@ def _complete_statement(self, line: str) -> Statement:
26872700 backwards compatibility with the standard library version of cmd.
26882701
26892702 :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.
26902706 :return: the completed Statement
26912707 :raises: Cmd2ShlexError if a shlex error occurs (e.g. No closing quotation)
26922708 :raises: EmptyStatement when the resulting Statement is blank
26932709 """
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+
26942726 while True :
26952727 try :
26962728 statement = self .statement_parser .parse (line )
@@ -2702,7 +2734,7 @@ def _complete_statement(self, line: str) -> Statement:
27022734 # so we are done
27032735 break
27042736 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
27062738 # and see if it's a multiline
27072739 statement = self .statement_parser .parse_command_only (line )
27082740 if not statement .multiline_command :
@@ -2718,6 +2750,7 @@ def _complete_statement(self, line: str) -> Statement:
27182750 # Save the command line up to this point for tab completion
27192751 self ._multiline_in_progress = line + '\n '
27202752
2753+ # Get next line of this command
27212754 nextline = self ._read_command_line (self .continuation_prompt )
27222755 if nextline == 'eof' :
27232756 # they entered either a blank line, or we hit an EOF
@@ -2726,7 +2759,14 @@ def _complete_statement(self, line: str) -> Statement:
27262759 # terminator
27272760 nextline = '\n '
27282761 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+
27302770 except KeyboardInterrupt :
27312771 self .poutput ('^C' )
27322772 statement = self .statement_parser .parse ('' )
@@ -2736,13 +2776,20 @@ def _complete_statement(self, line: str) -> Statement:
27362776
27372777 if not statement .command :
27382778 raise EmptyStatement
2779+ else :
2780+ # If necessary, update history with completed multiline command.
2781+ combine_rl_history (statement )
2782+
27392783 return statement
27402784
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 :
27422786 """
27432787 Parse the user's input line and convert it to a Statement, ensuring that all macros are also resolved
27442788
27452789 :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.
27462793 :return: parsed command line as a Statement
27472794 :raises: Cmd2ShlexError if a shlex error occurs (e.g. No closing quotation)
27482795 :raises: EmptyStatement when the resulting Statement is blank
@@ -2753,11 +2800,13 @@ def _input_line_to_statement(self, line: str) -> Statement:
27532800 # Continue until all macros are resolved
27542801 while True :
27552802 # 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 )
27572804
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.
27592807 if orig_line is None :
27602808 orig_line = statement .raw
2809+ orig_rl_history_length = None
27612810
27622811 # Check if this command matches a macro and wasn't already processed to avoid an infinite loop
27632812 if statement .command in self .macros .keys () and statement .command not in used_macros :
@@ -3111,7 +3160,7 @@ def configure_readline() -> None:
31113160 nonlocal saved_history
31123161 nonlocal parser
31133162
3114- if readline_configured : # pragma: no cover
3163+ if readline_configured or rl_type == RlType . NONE : # pragma: no cover
31153164 return
31163165
31173166 # Configure tab completion
@@ -3163,7 +3212,7 @@ def complete_none(text: str, state: int) -> Optional[str]: # pragma: no cover
31633212 def restore_readline () -> None :
31643213 """Restore readline tab completion and history"""
31653214 nonlocal readline_configured
3166- if not readline_configured : # pragma: no cover
3215+ if not readline_configured or rl_type == RlType . NONE : # pragma: no cover
31673216 return
31683217
31693218 if self ._completion_supported ():
@@ -3310,6 +3359,13 @@ def _cmdloop(self) -> None:
33103359 self ._startup_commands .clear ()
33113360
33123361 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+
33133369 # Get commands from user
33143370 try :
33153371 line = self ._read_command_line (self .prompt )
@@ -3318,7 +3374,7 @@ def _cmdloop(self) -> None:
33183374 line = ''
33193375
33203376 # 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 )
33223378 finally :
33233379 # Get sigint protection while we restore readline settings
33243380 with self .sigint_protection :
@@ -4871,15 +4927,13 @@ def _initialize_history(self, hist_file: str) -> None:
48714927
48724928 # Populate readline history
48734929 if rl_type != RlType .NONE :
4874- last = None
48754930 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 )
48834937
48844938 def _persist_history (self ) -> None :
48854939 """Write history out to the persistent history file as compressed JSON"""
0 commit comments