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 :
@@ -2727,6 +2756,12 @@ def _complete_statement(self, line: str) -> Statement:
27272756 nextline = '\n '
27282757 self .poutput (nextline )
27292758 line = f'{ self ._multiline_in_progress } { nextline } '
2759+
2760+ # Combine all lines of this multiline command as we go.
2761+ if nextline :
2762+ statement = self .statement_parser .parse_command_only (line )
2763+ combine_rl_history (statement )
2764+
27302765 except KeyboardInterrupt :
27312766 self .poutput ('^C' )
27322767 statement = self .statement_parser .parse ('' )
@@ -2736,13 +2771,20 @@ def _complete_statement(self, line: str) -> Statement:
27362771
27372772 if not statement .command :
27382773 raise EmptyStatement
2774+ else :
2775+ # If necessary, update history with completed multiline command.
2776+ combine_rl_history (statement )
2777+
27392778 return statement
27402779
2741- def _input_line_to_statement (self , line : str ) -> Statement :
2780+ def _input_line_to_statement (self , line : str , * , orig_rl_history_length : Optional [ int ] = None ) -> Statement :
27422781 """
27432782 Parse the user's input line and convert it to a Statement, ensuring that all macros are also resolved
27442783
27452784 :param line: the line being parsed
2785+ :param orig_rl_history_length: Optional length of the readline history before the current command was typed.
2786+ This is used to assist in combining multiline readline history entries and is only
2787+ populated by cmd2. Defaults to None.
27462788 :return: parsed command line as a Statement
27472789 :raises: Cmd2ShlexError if a shlex error occurs (e.g. No closing quotation)
27482790 :raises: EmptyStatement when the resulting Statement is blank
@@ -2753,11 +2795,13 @@ def _input_line_to_statement(self, line: str) -> Statement:
27532795 # Continue until all macros are resolved
27542796 while True :
27552797 # Make sure all input has been read and convert it to a Statement
2756- statement = self ._complete_statement (line )
2798+ statement = self ._complete_statement (line , orig_rl_history_length = orig_rl_history_length )
27572799
2758- # Save the fully entered line if this is the first loop iteration
2800+ # If this is the first loop iteration, save the original line and stop
2801+ # combining multiline history entries in the remaining iterations.
27592802 if orig_line is None :
27602803 orig_line = statement .raw
2804+ orig_rl_history_length = None
27612805
27622806 # Check if this command matches a macro and wasn't already processed to avoid an infinite loop
27632807 if statement .command in self .macros .keys () and statement .command not in used_macros :
@@ -3111,7 +3155,7 @@ def configure_readline() -> None:
31113155 nonlocal saved_history
31123156 nonlocal parser
31133157
3114- if readline_configured : # pragma: no cover
3158+ if readline_configured or rl_type == RlType . NONE : # pragma: no cover
31153159 return
31163160
31173161 # Configure tab completion
@@ -3163,7 +3207,7 @@ def complete_none(text: str, state: int) -> Optional[str]: # pragma: no cover
31633207 def restore_readline () -> None :
31643208 """Restore readline tab completion and history"""
31653209 nonlocal readline_configured
3166- if not readline_configured : # pragma: no cover
3210+ if not readline_configured or rl_type == RlType . NONE : # pragma: no cover
31673211 return
31683212
31693213 if self ._completion_supported ():
@@ -3310,6 +3354,13 @@ def _cmdloop(self) -> None:
33103354 self ._startup_commands .clear ()
33113355
33123356 while not stop :
3357+ # Used in building multiline readline history entries. Only applies
3358+ # when command line is read by input() in a terminal.
3359+ if rl_type != RlType .NONE and self .use_rawinput and sys .stdin .isatty ():
3360+ orig_rl_history_length = readline .get_current_history_length ()
3361+ else :
3362+ orig_rl_history_length = None
3363+
33133364 # Get commands from user
33143365 try :
33153366 line = self ._read_command_line (self .prompt )
@@ -3318,7 +3369,7 @@ def _cmdloop(self) -> None:
33183369 line = ''
33193370
33203371 # Run the command along with all associated pre and post hooks
3321- stop = self .onecmd_plus_hooks (line )
3372+ stop = self .onecmd_plus_hooks (line , orig_rl_history_length = orig_rl_history_length )
33223373 finally :
33233374 # Get sigint protection while we restore readline settings
33243375 with self .sigint_protection :
@@ -4871,15 +4922,13 @@ def _initialize_history(self, hist_file: str) -> None:
48714922
48724923 # Populate readline history
48734924 if rl_type != RlType .NONE :
4874- last = None
48754925 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
4926+ formatted_command = single_line_format (item .statement )
4927+
4928+ # If formatted command is different than the previous history item, add it
4929+ cur_history_length = readline .get_current_history_length ()
4930+ if cur_history_length == 0 or formatted_command != readline .get_history_item (cur_history_length ):
4931+ readline .add_history (formatted_command )
48834932
48844933 def _persist_history (self ) -> None :
48854934 """Write history out to the persistent history file as compressed JSON"""
0 commit comments