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