118118from .history import (
119119 History ,
120120 HistoryItem ,
121+ single_line_format ,
121122)
122123from .parsing import (
123124 Macro ,
@@ -2227,7 +2228,7 @@ def complete( # type: ignore[override]
22272228 # Check if we are completing a multiline command
22282229 if self ._at_continuation_prompt :
22292230 # 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 ()
22312232 line = lstripped_previous + readline .get_line_buffer ()
22322233
22332234 # Increment the indexes to account for the prepended text
@@ -2503,7 +2504,13 @@ def parseline(self, line: str) -> Tuple[str, str, str]:
25032504 return statement .command , statement .args , statement .command_and_args
25042505
25052506 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 ,
25072514 ) -> bool :
25082515 """Top-level function called by cmdloop() to handle parsing a line and running the command and all of its hooks.
25092516
@@ -2515,6 +2522,9 @@ def onecmd_plus_hooks(
25152522 :param py_bridge_call: This should only ever be set to True by PyBridge to signify the beginning
25162523 of an app() call from Python. It is used to enable/disable the storage of the
25172524 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.
25182528 :return: True if running of commands should stop
25192529 """
25202530 import datetime
@@ -2524,7 +2534,7 @@ def onecmd_plus_hooks(
25242534
25252535 try :
25262536 # 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 )
25282538
25292539 # call the postparsing hooks
25302540 postparsing_data = plugin .PostparsingData (False , statement )
@@ -2738,11 +2748,14 @@ def _complete_statement(self, line: str) -> Statement:
27382748 raise EmptyStatement
27392749 return statement
27402750
2741- def _input_line_to_statement (self , line : str ) -> Statement :
2751+ def _input_line_to_statement (self , line : str , * , orig_rl_history_length : Optional [ int ] = None ) -> Statement :
27422752 """
27432753 Parse the user's input line and convert it to a Statement, ensuring that all macros are also resolved
27442754
27452755 :param line: the line being parsed
2756+ :param orig_rl_history_length: Optional length of the readline history before the current command was typed.
2757+ This is used to assist in combining multiline readline history entries and is only
2758+ populated by cmd2. Defaults to None.
27462759 :return: parsed command line as a Statement
27472760 :raises: Cmd2ShlexError if a shlex error occurs (e.g. No closing quotation)
27482761 :raises: EmptyStatement when the resulting Statement is blank
@@ -2755,10 +2768,23 @@ def _input_line_to_statement(self, line: str) -> Statement:
27552768 # Make sure all input has been read and convert it to a Statement
27562769 statement = self ._complete_statement (line )
27572770
2758- # Save the fully entered line if this is the first loop iteration
2771+ # If this is the first loop iteration, save the original line and if necessary,
2772+ # combine multiline history entries into single readline history item.
27592773 if orig_line is None :
27602774 orig_line = statement .raw
27612775
2776+ if orig_rl_history_length is not None and statement .multiline_command :
2777+ # Remove all lines added to history for this command
2778+ while readline .get_current_history_length () > orig_rl_history_length :
2779+ readline .remove_history_item (readline .get_current_history_length () - 1 )
2780+
2781+ # Combine the lines of this command
2782+ combined_command = single_line_format (statement )
2783+
2784+ # If combined_command is different than the previous history item, then add it
2785+ if orig_rl_history_length == 0 or combined_command != readline .get_history_item (orig_rl_history_length ):
2786+ readline .add_history (combined_command )
2787+
27622788 # Check if this command matches a macro and wasn't already processed to avoid an infinite loop
27632789 if statement .command in self .macros .keys () and statement .command not in used_macros :
27642790 used_macros .append (statement .command )
@@ -3111,7 +3137,7 @@ def configure_readline() -> None:
31113137 nonlocal saved_history
31123138 nonlocal parser
31133139
3114- if readline_configured : # pragma: no cover
3140+ if readline_configured or rl_type == RlType . NONE : # pragma: no cover
31153141 return
31163142
31173143 # Configure tab completion
@@ -3163,7 +3189,7 @@ def complete_none(text: str, state: int) -> Optional[str]: # pragma: no cover
31633189 def restore_readline () -> None :
31643190 """Restore readline tab completion and history"""
31653191 nonlocal readline_configured
3166- if not readline_configured : # pragma: no cover
3192+ if not readline_configured or rl_type == RlType . NONE : # pragma: no cover
31673193 return
31683194
31693195 if self ._completion_supported ():
@@ -3310,6 +3336,13 @@ def _cmdloop(self) -> None:
33103336 self ._startup_commands .clear ()
33113337
33123338 while not stop :
3339+ # Used in building multiline readline history entries. Only applies
3340+ # when command line is read using input() in a terminal.
3341+ if rl_type != RlType .NONE and self .use_rawinput and sys .stdin .isatty ():
3342+ orig_rl_history_length = readline .get_current_history_length ()
3343+ else :
3344+ orig_rl_history_length = None
3345+
33133346 # Get commands from user
33143347 try :
33153348 line = self ._read_command_line (self .prompt )
@@ -3318,7 +3351,7 @@ def _cmdloop(self) -> None:
33183351 line = ''
33193352
33203353 # Run the command along with all associated pre and post hooks
3321- stop = self .onecmd_plus_hooks (line )
3354+ stop = self .onecmd_plus_hooks (line , orig_rl_history_length = orig_rl_history_length )
33223355 finally :
33233356 # Get sigint protection while we restore readline settings
33243357 with self .sigint_protection :
@@ -4873,13 +4906,13 @@ def _initialize_history(self, hist_file: str) -> None:
48734906 if rl_type != RlType .NONE :
48744907 last = None
48754908 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
4909+ formatted_command = single_line_format ( item . statement )
4910+
4911+ # readline only adds a single entry for multiple sequential identical lines
4912+ # so we emulate that behavior here
4913+ if formatted_command != last :
4914+ readline .add_history (formatted_command )
4915+ last = formatted_command
48834916
48844917 def _persist_history (self ) -> None :
48854918 """Write history out to the persistent history file as compressed JSON"""
0 commit comments