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 )
@@ -2678,7 +2688,7 @@ def runcmds_plus_hooks(
26782688
26792689 return False
26802690
2681- def _complete_statement (self , line : str ) -> Statement :
2691+ def _complete_statement (self , line : str , * , orig_rl_history_length : Optional [ int ] = None ) -> Statement :
26822692 """Keep accepting lines of input until the command is complete.
26832693
26842694 There is some pretty hacky code here to handle some quirks of
@@ -2687,10 +2697,29 @@ def _complete_statement(self, line: str) -> Statement:
26872697 backwards compatibility with the standard library version of cmd.
26882698
26892699 :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.
26902703 :return: the completed Statement
26912704 :raises: Cmd2ShlexError if a shlex error occurs (e.g. No closing quotation)
26922705 :raises: EmptyStatement when the resulting Statement is blank
26932706 """
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+
26942723 while True :
26952724 try :
26962725 statement = self .statement_parser .parse (line )
@@ -2702,7 +2731,7 @@ def _complete_statement(self, line: str) -> Statement:
27022731 # so we are done
27032732 break
27042733 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
27062735 # and see if it's a multiline
27072736 statement = self .statement_parser .parse_command_only (line )
27082737 if not statement .multiline_command :
@@ -2718,6 +2747,7 @@ def _complete_statement(self, line: str) -> Statement:
27182747 # Save the command line up to this point for tab completion
27192748 self ._multiline_in_progress = line + '\n '
27202749
2750+ # Get next line of this command
27212751 nextline = self ._read_command_line (self .continuation_prompt )
27222752 if nextline == 'eof' :
27232753 # they entered either a blank line, or we hit an EOF
@@ -2726,7 +2756,14 @@ def _complete_statement(self, line: str) -> Statement:
27262756 # terminator
27272757 nextline = '\n '
27282758 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+
27302767 except KeyboardInterrupt :
27312768 self .poutput ('^C' )
27322769 statement = self .statement_parser .parse ('' )
@@ -2736,13 +2773,20 @@ def _complete_statement(self, line: str) -> Statement:
27362773
27372774 if not statement .command :
27382775 raise EmptyStatement
2776+ else :
2777+ # If necessary, update history with completed multiline command.
2778+ combine_rl_history (statement )
2779+
27392780 return statement
27402781
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 :
27422783 """
27432784 Parse the user's input line and convert it to a Statement, ensuring that all macros are also resolved
27442785
27452786 :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.
27462790 :return: parsed command line as a Statement
27472791 :raises: Cmd2ShlexError if a shlex error occurs (e.g. No closing quotation)
27482792 :raises: EmptyStatement when the resulting Statement is blank
@@ -2753,11 +2797,13 @@ def _input_line_to_statement(self, line: str) -> Statement:
27532797 # Continue until all macros are resolved
27542798 while True :
27552799 # 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 )
27572801
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.
27592804 if orig_line is None :
27602805 orig_line = statement .raw
2806+ orig_rl_history_length = None
27612807
27622808 # Check if this command matches a macro and wasn't already processed to avoid an infinite loop
27632809 if statement .command in self .macros .keys () and statement .command not in used_macros :
@@ -3111,7 +3157,7 @@ def configure_readline() -> None:
31113157 nonlocal saved_history
31123158 nonlocal parser
31133159
3114- if readline_configured : # pragma: no cover
3160+ if readline_configured or rl_type == RlType . NONE : # pragma: no cover
31153161 return
31163162
31173163 # Configure tab completion
@@ -3163,7 +3209,7 @@ def complete_none(text: str, state: int) -> Optional[str]: # pragma: no cover
31633209 def restore_readline () -> None :
31643210 """Restore readline tab completion and history"""
31653211 nonlocal readline_configured
3166- if not readline_configured : # pragma: no cover
3212+ if not readline_configured or rl_type == RlType . NONE : # pragma: no cover
31673213 return
31683214
31693215 if self ._completion_supported ():
@@ -3310,6 +3356,13 @@ def _cmdloop(self) -> None:
33103356 self ._startup_commands .clear ()
33113357
33123358 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+
33133366 # Get commands from user
33143367 try :
33153368 line = self ._read_command_line (self .prompt )
@@ -3318,7 +3371,7 @@ def _cmdloop(self) -> None:
33183371 line = ''
33193372
33203373 # 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 )
33223375 finally :
33233376 # Get sigint protection while we restore readline settings
33243377 with self .sigint_protection :
@@ -4871,15 +4924,13 @@ def _initialize_history(self, hist_file: str) -> None:
48714924
48724925 # Populate readline history
48734926 if rl_type != RlType .NONE :
4874- last = None
48754927 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 )
48834934
48844935 def _persist_history (self ) -> None :
48854936 """Write history out to the persistent history file as compressed JSON"""
0 commit comments