Fix Savannah bug # 1332: handle backslash-newline pairs in command scripts
authorPaul Smith <psmith@gnu.org>
Sun, 26 Jun 2005 03:31:29 +0000 (03:31 +0000)
committerPaul Smith <psmith@gnu.org>
Sun, 26 Jun 2005 03:31:29 +0000 (03:31 +0000)
according to POSIX rules.

ChangeLog
NEWS
doc/make.texi
job.c
tests/ChangeLog
tests/scripts/misc/general3

index d462182..dc13423 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,17 @@
 2005-06-25  Paul D. Smith  <psmith@gnu.org>
 
+       * job.c (construct_command_argv_internal): Sanitize handling of
+       backslash/newline pairs according to POSIX: that is, keep the
+       backslash-newline in the command script, but remove a following
+       TAB character, if present.  In the fast path, make sure that the
+       behavior matches what the shell would do both inside and outside
+       of quotes.  In the slow path, quote the backslash and put a
+       literal newline in the string.
+       Fixes Savannah bug #1332.
+       * doc/make.texi (Execution): Document the new behavior and give
+       some examples.
+       * NEWS: Make a note of the new behavior.
+
        * make.h [WINDOWS32]: #include <direct.h>.
        Fixes Savannah bug #13478.
 
        the symlink we can get as the mtime of the file and don't fail.
        Fixes Savannah bug #13280.
 
-       Fix Savannah bug #1454.
-
        * read.c (find_char_unquote): Accept a new argument IGNOREVARS.
        If it's set, then don't stop on STOPCHARs or BLANKs if they're
        inside a variable reference.  Make this function static as it's
        only used here.
        (eval): Call find_char_unquote() with IGNOREVARS set when we're
        parsing an unexpanded line looking for semicolons.
+       Fixes Savannah bug #1454.
        * misc.c (remove_comments): Move this to read.c and make it static
        as it's only used there.  Call find_char_unquote() with new arg.
        * make.h: Remove prototypes for find_char_unquote() and
        remove_comments() since they're static now.
 
-       Implement the MAKE_RESTARTS variable, and disable -B if it's >0.
-       Fixes Savannah bug #7566.
-
-       * doc/make.texi (Special Variables): Document MAKE_RESTARTS.
-       * NEWS: Mention MAKE_RESTARTS.
        * main.c (main): If we see MAKE_RESTARTS in the environment, unset
        its export flag and obtain its value.  When we need to re-exec,
        increment the value and add it into the environment.
-       (always_make_set): New variable.  Change the -B option to set this
-       one instead.
+       * doc/make.texi (Special Variables): Document MAKE_RESTARTS.
+       * NEWS: Mention MAKE_RESTARTS.
+       * main.c (always_make_set): New variable.  Change the -B option to
+       set this one instead.
        (main): When checking makefiles, only set always_make_flag if
        always_make_set is set AND the restarts flag is 0.  When building
        normal targets, set it IFF always_make_set is set.
@@ -38,6 +46,7 @@
        files to NEW before we check makefiles if we've never restarted
        before.  If we have restarted, set what-if files to NEW _after_ we
        check makefiles.
+       Fixes Savannah bug #7566:
 
 2005-06-17  Paul D. Smith  <psmith@gnu.org>
 
diff --git a/NEWS b/NEWS
index b39114d..48b8dec 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -28,6 +28,11 @@ Version 3.81beta3
   double-quote any "$" in your filenames; instead of "foo: boo$$bar" you
   now must write "foo: foo$$$$bar".
 
+* WARNING: Backward-incompatibility!
+  In order to comply with POSIX, the way in which GNU make processes
+  backslash-newline sequences in command strings has changed.  See the
+  GNU make manual section "Shell Execution" for details.
+
 * New command-line option: -L (--check-symlink-times).  On systems that
   support symbolic links, if this option is given then GNU make will
   use the most recent modification time of any symbolic links that are
index f51fe32..4f07338 100644 (file)
@@ -3565,17 +3565,59 @@ foo : bar/lose
 @cindex @code{\} (backslash), in commands
 @cindex quoting newline, in commands
 @cindex newline, quoting, in commands
-If you would like to split a single shell command into multiple lines of
-text, you must use a backslash at the end of all but the last subline.
-Such a sequence of lines is combined into a single line, by deleting the
-backslash-newline sequences, before passing it to the shell.  Thus, the
-following is equivalent to the preceding example:
+A shell command can be split into multiple lines of text by placing a
+backslash before each newline.  Such a sequence of lines is provided
+to the shell as a single command script.  The backslash and newline
+are preserved in the shell command.  If the first character on the
+line after a backslash-newline is a tab, the tab will @emph{not} be
+included in the shell command.  So, this makefile:
 
 @example
 @group
-foo : bar/lose
-        cd bar;  \
-        gobble lose > ../foo
+all :
+        @@echo no\
+space
+        @@echo no\
+        space
+@end group
+@end example
+
+consists of two separate shell commands where the output is:
+
+@example
+@group
+nospace
+nospace
+@end group
+@end example
+
+As a more complex example, this makefile:
+
+@example
+@group
+all : ; @@echo 'hello \
+        world' ; echo "hello \
+    world"
+@end group
+@end example
+
+will run one shell with a command script of:
+
+@example
+@group
+echo 'hello \
+world' ; echo "hello \
+    world"
+@end group
+@end example
+
+which, according to shell quoting rules, will yield the following output:
+
+@example
+@group
+hello \
+world
+hello     world
 @end group
 @end example
 
diff --git a/job.c b/job.c
index b805eda..1776aca 100644 (file)
--- a/job.c
+++ b/job.c
@@ -2363,12 +2363,10 @@ construct_command_argv_internal (char *line, char **restp, char *shell,
   instring = word_has_equals = seen_nonequals = last_argument_was_empty = 0;
   for (p = line; *p != '\0'; ++p)
     {
-      if (ap > end)
-       abort ();
+      assert (ap <= end);
 
       if (instring)
        {
-       string_char:
          /* Inside a string, just copy any char except a closing quote
             or a backslash-newline combination.  */
          if (*p == instring)
@@ -2378,7 +2376,21 @@ construct_command_argv_internal (char *line, char **restp, char *shell,
                last_argument_was_empty = 1;
            }
          else if (*p == '\\' && p[1] == '\n')
-           goto swallow_escaped_newline;
+            {
+              /* Backslash-newline is handled differently depending on what
+                 kind of string we're in: inside single-quoted strings you
+                 keep them; in double-quoted strings they disappear.  */
+              if (instring == '"')
+                ++p;
+              else
+                {
+                  *(ap++) = *(p++);
+                  *(ap++) = *p;
+                }
+              /* If there's a TAB here, skip it.  */
+              if (p[1] == '\t')
+                ++p;
+            }
          else if (*p == '\n' && restp != NULL)
            {
              /* End of the command line.  */
@@ -2418,37 +2430,21 @@ construct_command_argv_internal (char *line, char **restp, char *shell,
            break;
 
          case '\\':
-           /* Backslash-newline combinations are eaten.  */
+           /* Backslash-newline has special case handling, ref POSIX.
+               We're in the fastpath, so emulate what the shell would do.  */
            if (p[1] == '\n')
              {
-             swallow_escaped_newline:
-
-               /* Eat the backslash, the newline, and following whitespace,
-                  replacing it all with a single space.  */
-               p += 2;
-
-               /* If there is a tab after a backslash-newline,
-                  remove it from the source line which will be echoed,
-                  since it was most likely used to line
-                  up the continued line with the previous one.  */
-               if (*p == '\t')
-                  /* Note these overlap and strcpy() is undefined for
-                     overlapping objects in ANSI C.  The strlen() _IS_ right,
-                     since we need to copy the nul byte too.  */
-                 bcopy (p + 1, p, strlen (p));
-
-               if (instring)
-                 goto string_char;
-               else
-                 {
-                   if (ap != new_argv[i])
-                     /* Treat this as a space, ending the arg.
-                        But if it's at the beginning of the arg, it should
-                        just get eaten, rather than becoming an empty arg. */
-                     goto end_of_arg;
-                   else
-                     p = next_token (p) - 1;
-                 }
+               /* Throw out the backslash and newline.  */
+                ++p;
+
+               /* If there is a tab after a backslash-newline, remove it.  */
+               if (p[1] == '\t')
+                  ++p;
+
+                /* If there's nothing in this argument yet, skip any
+                   whitespace before the start of the next word.  */
+                if (ap == new_argv[i])
+                  p = next_token (p + 1) - 1;
              }
            else if (p[1] != '\0')
               {
@@ -2502,7 +2498,6 @@ construct_command_argv_internal (char *line, char **restp, char *shell,
 
          case ' ':
          case '\t':
-         end_of_arg:
            /* We have the end of an argument.
               Terminate the text of the argument.  */
            *ap++ = '\0';
@@ -2538,9 +2533,7 @@ construct_command_argv_internal (char *line, char **restp, char *shell,
              }
 
            /* Ignore multiple whitespace chars.  */
-           p = next_token (p);
-           /* Next iteration should examine the first nonwhite char.  */
-           --p;
+           p = next_token (p) - 1;
            break;
 
          default:
@@ -2672,23 +2665,15 @@ construct_command_argv_internal (char *line, char **restp, char *shell,
          }
        else if (*p == '\\' && p[1] == '\n')
          {
-           /* Eat the backslash, the newline, and following whitespace,
-              replacing it all with a single space (which is escaped
-              from the shell).  */
-           p += 2;
-
-           /* If there is a tab after a backslash-newline,
-              remove it from the source line which will be echoed,
-              since it was most likely used to line
-              up the continued line with the previous one.  */
-           if (*p == '\t')
-             bcopy (p + 1, p, strlen (p));
-
-           p = next_token (p);
-           --p;
-            if (unixy_shell && !batch_mode_shell)
-              *ap++ = '\\';
-           *ap++ = ' ';
+           /* POSIX says we keep the backslash-newline, but throw out the
+               next char if it's a TAB.  */
+            *(ap++) = '\\';
+            *(ap++) = *(p++);
+            *(ap++) = *p;
+
+           if (p[1] == '\t')
+             ++p;
+
            continue;
          }
 
index 143bc2f..c07d7c1 100644 (file)
@@ -1,5 +1,10 @@
 2005-06-25  Paul D. Smith  <psmith@gnu.org>
 
+       * scripts/misc/general3: Implement comprehensive testing of
+       backslash-newline behavior in command scripts: various types of
+       quoting, fast path / slow path, etc.
+       Tests fix for Savannah bug #1332.
+
        * scripts/options/symlinks: Test symlinks to non-existent files.
        Tests fix for Savannah bug #13280.
 
index 40b7ed9..b3142c2 100644 (file)
@@ -53,4 +53,261 @@ all: ; @:
 ',
               '', 'true; true');
 
+# TEST 4
+
+# Check that backslashes in command scripts are handled according to POSIX.
+# Checks Savannah bug # 1332.
+
+# Test the fastpath / no quotes
+run_make_test('
+all:
+       @echo foo\
+bar
+       @echo foo\
+       bar
+       @echo foo\
+    bar
+       @echo foo\
+           bar
+       @echo foo \
+bar
+       @echo foo \
+       bar
+       @echo foo \
+    bar
+       @echo foo \
+           bar
+',
+              '', 'foobar
+foobar
+foo bar
+foo bar
+foo bar
+foo bar
+foo bar
+foo bar');
+
+# Test the fastpath / single quotes
+run_make_test("
+all:
+       \@echo 'foo\\
+bar'
+       \@echo 'foo\\
+       bar'
+       \@echo 'foo\\
+    bar'
+       \@echo 'foo\\
+           bar'
+       \@echo 'foo \\
+bar'
+       \@echo 'foo \\
+       bar'
+       \@echo 'foo \\
+    bar'
+       \@echo 'foo \\
+           bar'
+",
+              '', 'foo\
+bar
+foo\
+bar
+foo\
+    bar
+foo\
+    bar
+foo \
+bar
+foo \
+bar
+foo \
+    bar
+foo \
+    bar');
+
+# Test the fastpath / double quotes
+run_make_test('
+all:
+       @echo "foo\
+bar"
+       @echo "foo\
+       bar"
+       @echo "foo\
+    bar"
+       @echo "foo\
+           bar"
+       @echo "foo \
+bar"
+       @echo "foo \
+       bar"
+       @echo "foo \
+    bar"
+       @echo "foo \
+           bar"
+',
+              '', 'foobar
+foobar
+foo    bar
+foo    bar
+foo bar
+foo bar
+foo     bar
+foo     bar');
+
+# Test the slow path / no quotes
+run_make_test('
+all:
+       @echo hi; echo foo\
+bar
+       @echo hi; echo foo\
+       bar
+       @echo hi; echo foo\
+ bar
+       @echo hi; echo foo\
+        bar
+       @echo hi; echo foo \
+bar
+       @echo hi; echo foo \
+       bar
+       @echo hi; echo foo \
+ bar
+       @echo hi; echo foo \
+        bar
+',
+              '', 'hi
+foobar
+hi
+foobar
+hi
+foo bar
+hi
+foo bar
+hi
+foo bar
+hi
+foo bar
+hi
+foo bar
+hi
+foo bar');
+
+# Test the slow path / no quotes.  This time we put the slow path
+# determination _after_ the backslash-newline handling.
+run_make_test('
+all:
+       @echo foo\
+bar; echo hi
+       @echo foo\
+       bar; echo hi
+       @echo foo\
+ bar; echo hi
+       @echo foo\
+        bar; echo hi
+       @echo foo \
+bar; echo hi
+       @echo foo \
+       bar; echo hi
+       @echo foo \
+ bar; echo hi
+       @echo foo \
+        bar; echo hi
+',
+              '', 'foobar
+hi
+foobar
+hi
+foo bar
+hi
+foo bar
+hi
+foo bar
+hi
+foo bar
+hi
+foo bar
+hi
+foo bar
+hi');
+
+# Test the slow path / single quotes
+run_make_test("
+all:
+       \@echo hi; echo 'foo\\
+bar'
+       \@echo hi; echo 'foo\\
+       bar'
+       \@echo hi; echo 'foo\\
+    bar'
+       \@echo hi; echo 'foo\\
+           bar'
+       \@echo hi; echo 'foo \\
+bar'
+       \@echo hi; echo 'foo \\
+       bar'
+       \@echo hi; echo 'foo \\
+    bar'
+       \@echo hi; echo 'foo \\
+           bar'
+",
+              '', 'hi
+foo\
+bar
+hi
+foo\
+bar
+hi
+foo\
+    bar
+hi
+foo\
+    bar
+hi
+foo \
+bar
+hi
+foo \
+bar
+hi
+foo \
+    bar
+hi
+foo \
+    bar');
+
+# Test the slow path / double quotes
+run_make_test('
+all:
+       @echo hi; echo "foo\
+bar"
+       @echo hi; echo "foo\
+       bar"
+       @echo hi; echo "foo\
+    bar"
+       @echo hi; echo "foo\
+           bar"
+       @echo hi; echo "foo \
+bar"
+       @echo hi; echo "foo \
+       bar"
+       @echo hi; echo "foo \
+    bar"
+       @echo hi; echo "foo \
+           bar"
+',
+              '', 'hi
+foobar
+hi
+foobar
+hi
+foo    bar
+hi
+foo    bar
+hi
+foo bar
+hi
+foo bar
+hi
+foo     bar
+hi
+foo     bar');
+
 1;