1

This has been driving me crazy for about three years. I don't know how to fully describe the problem, but I think I can finally describe a way to recreate it. Your milage may vary. I have a mixture of ubuntu server and desktop machines of various versions and a few gentoo machines with various states of disrepair. They all seem to kindof do their own thing, although with similarities.

Try this and let me know if you see the same thing.

  1. pop open two xterms (TERM=xterm)
  2. resize one so they're not the same
  3. issue screen -R test1 in one (TERM=screen)
  4. and screen -x test1 in the other
  5. hooray, typing in one shows up in the other; although notice that their different size produces artifacts and things
  6. issue a couple commands in your shell
  7. hit ^AF in the one that doesn't fit quite right, now it fits!!
  8. scroll back over the history a little
  9. goto 6

Eventually you'll notice a couple history lines combine. If you don't, then it's something unique to my setup, which spans various distributions and computers; so that's a confusing concept to me.

If you see the thing I'm seeing then this:

bash$ ls -al
bash$ ps auxfw

becomes this:

bash$ ls -al; ps auxfw

It doesn't happen every time. I have to really play with it — unless I don't want it to happen, then it always does. On some systems (or combinations), I get a line separator like the example above. On some systems, I do not. That I get the line separator on some systems seems to indicate to me that bash supports this behavior. Its history is entirely handled by libreadline and after perusing (ie, carefully reading) the man pages, I couldn't find a single readline setting for combining two history lines. Nor can I find anything in the bash manpage.

So, how can I invoke this on purpose? Or, if I can't do that, how can I disable it completely? I would take either answer as a solution. Currently, I only see it when I don't want it.

3
  • I'm not able to reproduce the behavior you're seeing. The only thing I'm aware of that controls putting semicolons in history lines are the shopt options cmdhist and lithist, but they shouldn't affect commands like you've shown, only multi-line commands like while loops entered at the command line. If you do Ctrl-a F in the smaller terminal, it sets the larger one up the same way and draws a dashed line to designate the bottom of the screen, but that's not what you're describing. Doing Ctrl-a F in both will set them back like they originally were. Commented Jan 29, 2011 at 2:16
  • Having missing \\[ and \\] in your PS1 can do weird things with scrolling through history, but if you don't type anything in the wrong place because of it, it's just a screen artifact and doesn't affect the contents of history. Commented Jan 29, 2011 at 2:18
  • It's definitely not a screen artifact. When I scroll through history, the combined lines are part of the history after the resize event, sometimes with an extra line separator added in the correct place. And yes, when I do ^aF it does exactly what you say, but in addition it also sometimes combines two history lines, neatly and correctly — but why. I'm not surprised you've never seen it before. I've long suspected it's something in my environment, or others would comment on it in forums and mailing lists.
    – jettero
    Commented Jan 30, 2011 at 2:33

1 Answer 1

0

From the CHANGELOG for bash-4.2-rc2:

This document details the changes between this version, bash-4.2-alpha, and the previous version, bash-4.1-release.

q. Fixed a bug that caused spurious semicolons to be added into the command history in certain cases.

The tarball of the release candidate is available here.

Edit:

Here are some partial diffs between Bash 3.2 and Bash 4.2 RC2:

y.tab.c:

#if defined (HISTORY)                                                                                                                   #if defined (HISTORY)
/* A list of tokens which can be followed by newlines, but not by                                                                       /* A list of tokens which can be followed by newlines, but not by
   semi-colons.  When concatenating multiple lines of history, the                                                                         semi-colons.  When concatenating multiple lines of history, the
   newline separator for such tokens is replaced with a space. */                                                                          newline separator for such tokens is replaced with a space. */
static int no_semi_successors[] = {                                                                                                  |  static const int no_semi_successors[] = {
  '\n', '{', '(', ')', ';', '&', '|',                                                                                                     '\n', '{', '(', ')', ';', '&', '|',
  CASE, DO, ELSE, IF, SEMI_SEMI, THEN, UNTIL, WHILE, AND_AND, OR_OR, IN,                                                             |    CASE, DO, ELSE, IF, SEMI_SEMI, SEMI_AND, SEMI_SEMI_AND, THEN, UNTIL,
                                                                                                                                     >    WHILE, AND_AND, OR_OR, IN,
  0                                                                                                                                       0
};                                                                                                                                      };

/* If we are not within a delimited expression, try to be smart                                                                         /* If we are not within a delimited expression, try to be smart
   about which separators can be semi-colons and which must be                                                                             about which separators can be semi-colons and which must be
   newlines.  Returns the string that should be added into the                                                                             newlines.  Returns the string that should be added into the
   history entry. */                                                                                                                 |     history entry.  LINE is the line we're about to add; it helps
                                                                                                                                     >     make some more intelligent decisions in certain cases. */
char *                                                                                                                                  char *
history_delimiting_chars ()                                                                                                          |  history_delimiting_chars (line)
                                                                                                                                     >       const char *line;
{                                                                                                                                       {
                                                                                                                                     >    static int last_was_heredoc = 0;      /* was the last entry the start of a here document? */
  register int i;                                                                                                                         register int i;

                                                                                                                                     >    if ((parser_state & PST_HEREDOC) == 0)
                                                                                                                                     >      last_was_heredoc = 0;
                                                                                                                                     >
  if (dstack.delimiter_depth != 0)                                                                                                        if (dstack.delimiter_depth != 0)
    return ("\n");                                                                                                                          return ("\n");
                                                                                                                                     |
                                                                                                                                     >    /* We look for current_command_line_count == 2 because we are looking to
                                                                                                                                     >       add the first line of the body of the here document (the second line
                                                                                                                                     >       of the command).  We also keep LAST_WAS_HEREDOC as a private sentinel
                                                                                                                                     >       variable to note when we think we added the first line of a here doc
                                                                                                                                     >       (the one with a "<<" somewhere in it) */
                                                                                                                                     >    if (parser_state & PST_HEREDOC)
                                                                                                                                     >      {
                                                                                                                                     >        if (last_was_heredoc)
                                                                                                                                     >          {
                                                                                                                                     >            last_was_heredoc = 0;
                                                                                                                                     >            return "\n";
                                                                                                                                     >          }
                                                                                                                                     >        return (current_command_line_count == 2 ? "\n" : "");
                                                                                                                                     >      }
                                                                                                                                     >
  /* First, handle some special cases. */                                                                                                 /* First, handle some special cases. */
  /*(*/                                                                                                                                   /*(*/
  /* If we just read `()', assume it's a function definition, and don't                                                                   /* If we just read `()', assume it's a function definition, and don't
     add a semicolon.  If the token before the `)' was not `(', and we're                                                                    add a semicolon.  If the token before the `)' was not `(', and we're
     not in the midst of parsing a case statement, assume it's a                                                                             not in the midst of parsing a case statement, assume it's a
     parenthesized command and add the semicolon. */                                                                                         parenthesized command and add the semicolon. */
  /*)(*/                                                                                                                                  /*)(*/
  if (token_before_that == ')')                                                                                                           if (token_before_that == ')')
    {                                                                                                                                       {
      if (two_tokens_ago == '(')        /*)*/   /* function def */                                                                            if (two_tokens_ago == '(')        /*)*/   /* function def */
        return " ";                                                                                                                             return " ";
      /* This does not work for subshells inside case statement                                                                               /* This does not work for subshells inside case statement
         command lists.  It's a suboptimal solution. */                                                                                          command lists.  It's a suboptimal solution. */
      else if (parser_state & PST_CASESTMT)     /* case statement pattern */                                                                  else if (parser_state & PST_CASESTMT)     /* case statement pattern */
        return " ";                                                                                                                             return " ";
      else                                                                                                                                    else
        return "; ";                            /* (...) subshell */                                                                            return "; ";                            /* (...) subshell */
    }                                                                                                                                       }
  else if (token_before_that == WORD && two_tokens_ago == FUNCTION)                                                                       else if (token_before_that == WORD && two_tokens_ago == FUNCTION)
    return " ";         /* function def using `function name' without `()' */                                                               return " ";         /* function def using `function name' without `()' */

                                                                                                                                     >    /* If we're not in a here document, but we think we're about to parse one,
                                                                                                                                     >       and we would otherwise return a `;', return a newline to delimit the
                                                                                                                                     >       line with the here-doc delimiter */
                                                                                                                                     >    else if ((parser_state & PST_HEREDOC) == 0 && current_command_line_count > 1 && last_read_token == '\n' && strstr (line, "<<"))
                                                                                                                                     >      {
                                                                                                                                     >        last_was_heredoc = 1;
                                                                                                                                     >        return "\n";
                                                                                                                                     >      }
                                                                                                                                     >
  else if (token_before_that == WORD && two_tokens_ago == FOR)                                                                            else if (token_before_that == WORD && two_tokens_ago == FOR)
    {                                                                                                                                       {
      /* Tricky.  `for i\nin ...' should not have a semicolon, but                                                                            /* Tricky.  `for i\nin ...' should not have a semicolon, but
         `for i\ndo ...' should.  We do what we can. */                                                                                          `for i\ndo ...' should.  We do what we can. */
      for (i = shell_input_line_index; whitespace(shell_input_line[i]); i++)                                                         |        for (i = shell_input_line_index; whitespace (shell_input_line[i]); i++)
        ;                                                                                                                                       ;
      if (shell_input_line[i] && shell_input_line[i] == 'i' && shell_input_line[i+1] == 'n')                                                  if (shell_input_line[i] && shell_input_line[i] == 'i' && shell_input_line[i+1] == 'n')
        return " ";                                                                                                                             return " ";
      return ";";                                                                                                                             return ";";
    }                                                                                                                                       }
  else if (two_tokens_ago == CASE && token_before_that == WORD && (parser_state & PST_CASESTMT))                                          else if (two_tokens_ago == CASE && token_before_that == WORD && (parser_state & PST_CASESTMT))
    return " ";                                                                                                                             return " ";

  for (i = 0; no_semi_successors[i]; i++)                                                                                                 for (i = 0; no_semi_successors[i]; i++)
    {                                                                                                                                       {
      if (token_before_that == no_semi_successors[i])                                                                                         if (token_before_that == no_semi_successors[i])
        return (" ");                                                                                                                           return (" ");
    }                                                                                                                                       }

  return ("; ");                                                                                                                          return ("; ");
}                                                                                                                                       }
#endif /* HISTORY */                                                                                                                    #endif /* HISTORY */

bashhist.c:

/* Add a line to the history list.                                                                                                      /* Add a line to the history list.
   The variable COMMAND_ORIENTED_HISTORY controls the style of history                                                                     The variable COMMAND_ORIENTED_HISTORY controls the style of history
   remembering;  when non-zero, and LINE is not the first line of a                                                                        remembering;  when non-zero, and LINE is not the first line of a
   complete parser construct, append LINE to the last history line instead                                                                 complete parser construct, append LINE to the last history line instead
   of adding it as a new line. */                                                                                                          of adding it as a new line. */
void                                                                                                                                    void
bash_add_history (line)                                                                                                                 bash_add_history (line)
     char *line;                                                                                                                             char *line;
{                                                                                                                                       {
  int add_it, offset, curlen;                                                                                                             int add_it, offset, curlen;
  HIST_ENTRY *current, *old;                                                                                                              HIST_ENTRY *current, *old;
  char *chars_to_add, *new_line;                                                                                                          char *chars_to_add, *new_line;

  add_it = 1;                                                                                                                             add_it = 1;
  if (command_oriented_history && current_command_line_count > 1)                                                                         if (command_oriented_history && current_command_line_count > 1)
    {                                                                                                                                       {
      chars_to_add = literal_history ? "\n" : history_delimiting_chars ();                                                           |        chars_to_add = literal_history ? "\n" : history_delimiting_chars (line);

      using_history ();                                                                                                                       using_history ();
      current = previous_history ();                                                                                                          current = previous_history ();

      if (current)                                                                                                                            if (current)
        {                                                                                                                                       {
          /* If the previous line ended with an escaped newline (escaped                                                                          /* If the previous line ended with an escaped newline (escaped
             with backslash, but otherwise unquoted), then remove the quoted                                                                         with backslash, but otherwise unquoted), then remove the quoted
             newline, since that is what happens when the line is parsed. */                                                                         newline, since that is what happens when the line is parsed. */
          curlen = strlen (current->line);                                                                                                        curlen = strlen (current->line);

          if (dstack.delimiter_depth == 0 && current->line[curlen - 1] == '\\' &&                                                                 if (dstack.delimiter_depth == 0 && current->line[curlen - 1] == '\\' &&
              current->line[curlen - 2] != '\\')                                                                                                      current->line[curlen - 2] != '\\')
            {                                                                                                                                       {
              current->line[curlen - 1] = '\0';                                                                                                       current->line[curlen - 1] = '\0';
              curlen--;                                                                                                                               curlen--;
              chars_to_add = "";                                                                                                                      chars_to_add = "";
            }                                                                                                                                       }

                                                                                                                                     >            /* If we're not in some kind of quoted construct, the current history
                                                                                                                                     >               entry ends with a newline, and we're going to add a semicolon,
                                                                                                                                     >               don't.  In some cases, it results in a syntax error (e.g., before
                                                                                                                                     >               a close brace), and it should not be needed. */
                                                                                                                                     >            if (dstack.delimiter_depth == 0 && current->line[curlen - 1] == '\n' && *chars_to_add == ';')
                                                                                                                                     >              chars_to_add++;
                                                                                                                                     >
          new_line = (char *)xmalloc (1                                                                                                           new_line = (char *)xmalloc (1
                                      + curlen                                                                                                                                + curlen
                                      + strlen (line)                                                                                                                         + strlen (line)
                                      + strlen (chars_to_add));                                                                                                               + strlen (chars_to_add));
          sprintf (new_line, "%s%s%s", current->line, chars_to_add, line);                                                                        sprintf (new_line, "%s%s%s", current->line, chars_to_add, line);
          offset = where_history ();                                                                                                              offset = where_history ();
          old = replace_history_entry (offset, new_line, current->data);                                                                          old = replace_history_entry (offset, new_line, current->data);
          free (new_line);                                                                                                                        free (new_line);

          if (old)                                                                                                                                if (old)
            free_history_entry (old);                                                                                                               free_history_entry (old);

          add_it = 0;                                                                                                                             add_it = 0;
        }                                                                                                                                       }
    }                                                                                                                                       }

  if (add_it)                                                                                                                             if (add_it)
    really_add_history (line);                                                                                                              really_add_history (line);

                                                                                                                                     >  #if defined (SYSLOG_HISTORY)
                                                                                                                                     >    bash_syslog_history (line);
                                                                                                                                     >  #endif
                                                                                                                                     >
  using_history ();                                                                                                                       using_history ();
}                                                                                                                                       }
7
  • That's interesting and probably partially applicable since the process that adds the semicolon is probably related. But this also happens on my old gentoo machine that's running GNU bash, version 3.2.33 — and that machine does not add the semicolon; it just combines the lines. I'm going to grab source and find that patch and see what's going on in that part of the code.
    – jettero
    Commented Feb 3, 2011 at 10:47
  • gross: cvs -z3 -d:pserver:[email protected]:/sources/bash co
    – jettero
    Commented Feb 3, 2011 at 10:54
  • Oh, that's not used anymore, whew: git://git.sv.gnu.org/bash.git
    – jettero
    Commented Feb 3, 2011 at 11:31
  • nope, that doesn't have the revision history. If the bash sources are in a public repository somewhere, I can't find it.
    – jettero
    Commented Feb 3, 2011 at 11:49
  • @jettero: They're not, but the tarball I linked to has the source, of course. I have added some partial diffs to my answer. Commented Feb 3, 2011 at 15:26

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .