t/t0201-gettext-fallbacks.sh: test for broken eval_gettext Add a test for the broken eval_gettext() variable interpolation behavior. Reported-by: Johannes Sixt <j.sixt@xxxxxxxxxxxxx> Signed-off-by: Ãvar ArnfjÃrà Bjarmason <avarab@xxxxxxxxx> git-sh-i18n.sh: use envsubst Signed-off-by: Ãvar ArnfjÃrà Bjarmason <avarab@xxxxxxxxx> WIP: add a git-sh-i18n--envsubst I can't figure out how to make the Makefile read from git-sh-i18n--envsubst.c and produce git-sh-i18n--envsubst, not git-git-sh-i18n--envsubst. I also can't see how to make it available to git tests. If I name it sh-i18n--envsubst.c I'll get linking errors (about xrealloc). --- Heading out so I thought I'd dump a WIP patch here and ask if anyone knows how to solve this Makefile issue I'm having. I wanted to call the two programs /{git-sh-i18n.sh,git-sh-i18n--envsubst.c}, but maybe going that route is the wrong thing to do? .gitignore | 1 + Makefile | 3 + git-sh-i18n--envsubst.c | 392 ++++++++++++++++++++++++++++++++++++++++++ git-sh-i18n.sh | 3 +- t/t0201-gettext-fallbacks.sh | 23 +++ 5 files changed, 420 insertions(+), 2 deletions(-) create mode 100644 git-sh-i18n--envsubst.c diff --git a/.gitignore b/.gitignore index 80ca718..d2d9ec2 100644 --- a/.gitignore +++ b/.gitignore @@ -126,6 +126,7 @@ /git-send-email /git-send-pack /git-sh-i18n +/git-sh-i18n--envsubst /git-sh-setup /git-shell /git-shortlog diff --git a/Makefile b/Makefile index e1650ba..cdfe6bc 100644 --- a/Makefile +++ b/Makefile @@ -435,6 +435,7 @@ PROGRAM_OBJS += shell.o PROGRAM_OBJS += show-index.o PROGRAM_OBJS += upload-pack.o PROGRAM_OBJS += http-backend.o +PROGRAM_OBJS += git-sh-i18n--envsubst.o PROGRAMS += $(patsubst %.o,git-%$X,$(PROGRAM_OBJS)) @@ -2016,6 +2017,8 @@ git-http-fetch$X: revision.o http.o http-walker.o http-fetch.o $(GITLIBS) git-http-push$X: revision.o http.o http-push.o $(GITLIBS) $(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) \ $(LIBS) $(CURL_LIBCURL) $(EXPAT_LIBEXPAT) +git-sh-i18n--envsubst$X: git-sh-i18n--envsubst.o $(GITLIBS) + $(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) $(REMOTE_CURL_ALIASES): $(REMOTE_CURL_PRIMARY) $(QUIET_LNCP)$(RM) $@ && \ diff --git a/git-sh-i18n--envsubst.c b/git-sh-i18n--envsubst.c new file mode 100644 index 0000000..3be7e8e --- /dev/null +++ b/git-sh-i18n--envsubst.c @@ -0,0 +1,392 @@ +/* This is a modified version of + 67d0871a8c:gettext-runtime/src/envsubst.c from the gettext.git + repository that does only the envsubst that we need in the + git-sh-i18n fallbacks. +*/ + +#include "git-compat-util.h" + +/* Substitution of environment variables in shell format strings. + Copyright (C) 2003-2007 Free Software Foundation, Inc. + Written by Bruno Haible <bruno@xxxxxxxxx>, 2003. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + +#include <errno.h> +#include <getopt.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +/* If true, substitution shall be performed on all variables. */ +static bool all_variables; + +static void print_variables (const char *string); +static void note_variables (const char *string); +static void subst_from_stdin (void); + +int +main (int argc, char *argv[]) +{ + all_variables = true; + subst_from_stdin (); + + exit (EXIT_SUCCESS); +} + +/* Parse the string and invoke the callback each time a $VARIABLE or + ${VARIABLE} construct is seen, where VARIABLE is a nonempty sequence + of ASCII alphanumeric/underscore characters, starting with an ASCII + alphabetic/underscore character. + We allow only ASCII characters, to avoid dependencies w.r.t. the current + encoding: While "${\xe0}" looks like a variable access in ISO-8859-1 + encoding, it doesn't look like one in the BIG5, BIG5-HKSCS, GBK, GB18030, + SHIFT_JIS, JOHAB encodings, because \xe0\x7d is a single character in these + encodings. */ +static void +find_variables (const char *string, + void (*callback) (const char *var_ptr, size_t var_len)) +{ + for (; *string != '\0';) + if (*string++ == '$') + { + const char *variable_start; + const char *variable_end; + bool valid; + char c; + + if (*string == '{') + string++; + + variable_start = string; + c = *string; + if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_') + { + do + c = *++string; + while ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') + || (c >= '0' && c <= '9') || c == '_'); + variable_end = string; + + if (variable_start[-1] == '{') + { + if (*string == '}') + { + string++; + valid = true; + } + else + valid = false; + } + else + valid = true; + + if (valid) + callback (variable_start, variable_end - variable_start); + } + } +} + + +/* Print a variable to stdout, followed by a newline. */ +static void +print_variable (const char *var_ptr, size_t var_len) +{ + fwrite (var_ptr, var_len, 1, stdout); + putchar ('\n'); +} + +/* Print the variables contained in STRING to stdout, each one followed by a + newline. */ +static void +print_variables (const char *string) +{ + find_variables (string, &print_variable); +} + + +/* Type describing list of immutable strings, + implemented using a dynamic array. */ +typedef struct string_list_ty string_list_ty; +struct string_list_ty +{ + const char **item; + size_t nitems; + size_t nitems_max; +}; + +/* Initialize an empty list of strings. */ +static inline void +string_list_init (string_list_ty *slp) +{ + slp->item = NULL; + slp->nitems = 0; + slp->nitems_max = 0; +} + +/* Append a single string to the end of a list of strings. */ +static inline void +string_list_append (string_list_ty *slp, const char *s) +{ + /* Grow the list. */ + if (slp->nitems >= slp->nitems_max) + { + size_t nbytes; + + slp->nitems_max = slp->nitems_max * 2 + 4; + nbytes = slp->nitems_max * sizeof (slp->item[0]); + slp->item = (const char **) xrealloc (slp->item, nbytes); + } + + /* Add the string to the end of the list. */ + slp->item[slp->nitems++] = s; +} + +/* Compare two strings given by reference. */ +static int +cmp_string (const void *pstr1, const void *pstr2) +{ + const char *str1 = *(const char **)pstr1; + const char *str2 = *(const char **)pstr2; + + return strcmp (str1, str2); +} + +/* Sort a list of strings. */ +static inline void +string_list_sort (string_list_ty *slp) +{ + if (slp->nitems > 0) + qsort (slp->item, slp->nitems, sizeof (slp->item[0]), cmp_string); +} + +/* Test whether a string list contains a given string. */ +static inline int +string_list_member (const string_list_ty *slp, const char *s) +{ + size_t j; + + for (j = 0; j < slp->nitems; ++j) + if (strcmp (slp->item[j], s) == 0) + return 1; + return 0; +} + +/* Test whether a sorted string list contains a given string. */ +static int +sorted_string_list_member (const string_list_ty *slp, const char *s) +{ + size_t j1, j2; + + j1 = 0; + j2 = slp->nitems; + if (j2 > 0) + { + /* Binary search. */ + while (j2 - j1 > 1) + { + /* Here we know that if s is in the list, it is at an index j + with j1 <= j < j2. */ + size_t j = (j1 + j2) >> 1; + int result = strcmp (slp->item[j], s); + + if (result > 0) + j2 = j; + else if (result == 0) + return 1; + else + j1 = j + 1; + } + if (j2 > j1) + if (strcmp (slp->item[j1], s) == 0) + return 1; + } + return 0; +} + +/* Destroy a list of strings. */ +static inline void +string_list_destroy (string_list_ty *slp) +{ + size_t j; + + for (j = 0; j < slp->nitems; ++j) + free ((char *) slp->item[j]); + if (slp->item != NULL) + free (slp->item); +} + + +/* Set of variables on which to perform substitution. + Used only if !all_variables. */ +static string_list_ty variables_set; + +/* Adds a variable to variables_set. */ +static void +note_variable (const char *var_ptr, size_t var_len) +{ + char *string = xmalloc (var_len + 1); + memcpy (string, var_ptr, var_len); + string[var_len] = '\0'; + + string_list_append (&variables_set, string); +} + +/* Stores the variables occurring in the string in variables_set. */ +static void +note_variables (const char *string) +{ + string_list_init (&variables_set); + find_variables (string, ¬e_variable); + string_list_sort (&variables_set); +} + + +static int +do_getc () +{ + int c = getc (stdin); + + if (c == EOF) + { + if (ferror (stdin)) + error ("error while reading standard input"); + } + + return c; +} + +static inline void +do_ungetc (int c) +{ + if (c != EOF) + ungetc (c, stdin); +} + +/* Copies stdin to stdout, performing substitutions. */ +static void +subst_from_stdin () +{ + static char *buffer; + static size_t bufmax; + static size_t buflen; + int c; + + for (;;) + { + c = do_getc (); + if (c == EOF) + break; + /* Look for $VARIABLE or ${VARIABLE}. */ + if (c == '$') + { + bool opening_brace = false; + bool closing_brace = false; + + c = do_getc (); + if (c == '{') + { + opening_brace = true; + c = do_getc (); + } + if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_') + { + bool valid; + + /* Accumulate the VARIABLE in buffer. */ + buflen = 0; + do + { + if (buflen >= bufmax) + { + bufmax = 2 * bufmax + 10; + buffer = xrealloc (buffer, bufmax); + } + buffer[buflen++] = c; + + c = do_getc (); + } + while ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') + || (c >= '0' && c <= '9') || c == '_'); + + if (opening_brace) + { + if (c == '}') + { + closing_brace = true; + valid = true; + } + else + { + valid = false; + do_ungetc (c); + } + } + else + { + valid = true; + do_ungetc (c); + } + + if (valid) + { + /* Terminate the variable in the buffer. */ + if (buflen >= bufmax) + { + bufmax = 2 * bufmax + 10; + buffer = xrealloc (buffer, bufmax); + } + buffer[buflen] = '\0'; + + /* Test whether the variable shall be substituted. */ + if (!all_variables + && !sorted_string_list_member (&variables_set, buffer)) + valid = false; + } + + if (valid) + { + /* Substitute the variable's value from the environment. */ + const char *env_value = getenv (buffer); + + if (env_value != NULL) + fputs (env_value, stdout); + } + else + { + /* Perform no substitution at all. Since the buffered input + contains no other '$' than at the start, we can just + output all the buffered contents. */ + putchar ('$'); + if (opening_brace) + putchar ('{'); + fwrite (buffer, buflen, 1, stdout); + if (closing_brace) + putchar ('}'); + } + } + else + { + do_ungetc (c); + putchar ('$'); + if (opening_brace) + putchar ('{'); + } + } + else + putchar (c); + } +} diff --git a/git-sh-i18n.sh b/git-sh-i18n.sh index f8dd43a..b3b599a 100644 --- a/git-sh-i18n.sh +++ b/git-sh-i18n.sh @@ -55,8 +55,7 @@ then } eval_gettext () { - gettext_eval="printf '%s' \"$1\"" - printf "%s" "`eval \"$gettext_eval\"`" + printf "%s" "$1" | /home/avar/g/git/git-git-sh-i18n--envsubst } fi else diff --git a/t/t0201-gettext-fallbacks.sh b/t/t0201-gettext-fallbacks.sh index 7a85d9b..682c602 100755 --- a/t/t0201-gettext-fallbacks.sh +++ b/t/t0201-gettext-fallbacks.sh @@ -46,4 +46,27 @@ test_expect_success NO_GETTEXT_POISON 'eval_gettext: our eval_gettext() fallback test_cmp expect actual ' +test_expect_success NO_GETTEXT_POISON 'eval_gettext: our eval_gettext() fallback can interpolate whitespace variables' ' + git_am_cmdline="git am" && + export git_am_cmdline && + printf "test git am" >expect && + eval_gettext "test \$git_am_cmdline" >actual && + test_cmp expect actual +' + +test_expect_success NO_GETTEXT_POISON 'eval_gettext: git am $cmdline bug' ' + cmdline="git am -3" && + export cmdline && + cat >expect <<EOF && +When you have resolved this problem run "git am -3 --resolved". +If you would prefer to skip this patch, instead run "git am -3 --skip". +To restore the original branch and stop patching run "git am -3 --abort". +EOF + eval_gettext "When you have resolved this problem run \"\$cmdline --resolved\". +If you would prefer to skip this patch, instead run \"\$cmdline --skip\". +To restore the original branch and stop patching run \"\$cmdline --abort\"." >actual && + echo >>actual && + test_cmp expect actual +' + test_done -- 1.7.3.2.309.g4cea8.dirty -- To unsubscribe from this list: send the line "unsubscribe git" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html