git-sh-i18n--envsubst: our own envsubst(1) for eval_gettext()

Add a git-sh-i18n--envsubst program which is a stripped-down version of the GNU envsubst(1) program that comes with GNU gettext for use in the eval_gettext() fallback. We need a C helper program because implementing eval_gettext() purely in shell turned out to be unworkable. Digging through the Git mailing list archives will reveal two shell implementations of eval_gettext that are almost good enough, but fail on an edge case which is tested for in the tests which are part of this patch. These are the modifications I made to envsubst.c as I turned it into sh-i18n--envsubst.c: * Added our git-compat-util.h header for xrealloc() and friends. * Removed inclusion of gettext-specific headers. * Removed most of main() and replaced it with my own. The modified version only does option parsing for --variables. That's all it needs. * Modified error() invocations to use our error() instead of error(3). * Replaced the gettext XNMALLOC(n, size) macro with just xmalloc(n). Since XNMALLOC() only allocated char's. * Removed the string_list_destroy function. It's redundant (also in the upstream code). * Replaced the use of stdbool.h (a C99 header) by doing the following replacements on the code: * s/bool/unsigned short int/g * s/true/1/g * s/false/0/g Reported-by: Johannes Sixt <j.sixt@viscovery.net> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Ævar Arnfjörð Bjarmason committed May 14, 2011 at 13:47 UTC ba67aaf2d05d1aa3def2b6eb1dc1d39bdf25bb41
5 files changed +524
.gitignore
+2
@@ -127,6 +127,8 @@
127 /git-rm
128 /git-send-email
129 /git-send-pack
130 +/git-sh-i18n
131 +/git-sh-i18n--envsubst
132 /git-sh-setup
133 /git-shell
134 /git-shortlog
Documentation/git-sh-i18n--envsubst.txt new
+26
@@ -0,0 +1,26 @@
1 +git-sh-i18n--envsubst(1)
2 +========================
3 +
4 +NAME
5 +----
6 +git-sh-i18n--envsubst - Git's own envsubst(1) for i18n fallbacks
7 +
8 +DESCRIPTION
9 +-----------
10 +
11 +This is not a command the end user would want to run. Ever.
12 +This documentation is meant for people who are studying the
13 +plumbing scripts and/or are writing new ones.
14 +
15 +git-sh-i18n--envsubst is Git's stripped-down copy of the GNU
16 +`envsubst(1)` program that comes with the GNU gettext package. It's
17 +used internally by linkgit:git-sh-i18n[1] to interpolate the variables
18 +passed to the the `eval_gettext` function.
19 +
20 +No promises are made about the interface, or that this
21 +program won't disappear without warning in the next version
22 +of Git. Don't use it.
23 +
24 +GIT
25 +---
26 +Part of the linkgit:git[1] suite
Makefile
+1
@@ -414,6 +414,7 @@ PROGRAM_OBJS += shell.o
414 PROGRAM_OBJS += show-index.o
415 PROGRAM_OBJS += upload-pack.o
416 PROGRAM_OBJS += http-backend.o
417 +PROGRAM_OBJS += sh-i18n--envsubst.o
418
419 PROGRAMS += $(patsubst %.o,git-%$X,$(PROGRAM_OBJS))
420
sh-i18n--envsubst.c new
+444
@@ -0,0 +1,444 @@
1 +/*
2 + * sh-i18n--envsubst.c - a stripped-down version of gettext's envsubst(1)
3 + *
4 + * Copyright (C) 2010 Ævar Arnfjörð Bjarmason
5 + *
6 + * This is a modified version of
7 + * 67d0871a8c:gettext-runtime/src/envsubst.c from the gettext.git
8 + * repository. It has been stripped down to only implement the
9 + * envsubst(1) features that we need in the git-sh-i18n fallbacks.
10 + *
11 + * The "Close standard error" part in main() is from
12 + * 8dac033df0:gnulib-local/lib/closeout.c. The copyright notices for
13 + * both files are reproduced immediately below.
14 + */
15 +
16 +#include "git-compat-util.h"
17 +
18 +/* Substitution of environment variables in shell format strings.
19 + Copyright (C) 2003-2007 Free Software Foundation, Inc.
20 + Written by Bruno Haible <bruno@clisp.org>, 2003.
21 +
22 + This program is free software; you can redistribute it and/or modify
23 + it under the terms of the GNU General Public License as published by
24 + the Free Software Foundation; either version 2, or (at your option)
25 + any later version.
26 +
27 + This program is distributed in the hope that it will be useful,
28 + but WITHOUT ANY WARRANTY; without even the implied warranty of
29 + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
30 + GNU General Public License for more details.
31 +
32 + You should have received a copy of the GNU General Public License
33 + along with this program; if not, write to the Free Software Foundation,
34 + Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */
35 +
36 +/* closeout.c - close standard output and standard error
37 + Copyright (C) 1998-2007 Free Software Foundation, Inc.
38 +
39 + This program is free software; you can redistribute it and/or modify
40 + it under the terms of the GNU General Public License as published by
41 + the Free Software Foundation; either version 2, or (at your option)
42 + any later version.
43 +
44 + This program is distributed in the hope that it will be useful,
45 + but WITHOUT ANY WARRANTY; without even the implied warranty of
46 + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
47 + GNU General Public License for more details.
48 +
49 + You should have received a copy of the GNU General Public License
50 + along with this program; if not, write to the Free Software Foundation,
51 + Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */
52 +
53 +#include <errno.h>
54 +#include <getopt.h>
55 +#include <stdio.h>
56 +#include <stdlib.h>
57 +#include <string.h>
58 +
59 +/* If true, substitution shall be performed on all variables. */
60 +static unsigned short int all_variables;
61 +
62 +/* Forward declaration of local functions. */
63 +static void print_variables (const char *string);
64 +static void note_variables (const char *string);
65 +static void subst_from_stdin (void);
66 +
67 +int
68 +main (int argc, char *argv[])
69 +{
70 + /* Default values for command line options. */
71 + unsigned short int show_variables = 0;
72 +
73 + switch (argc)
74 + {
75 + case 1:
76 + error ("we won't substitute all variables on stdin for you");
77 + /*
78 + all_variables = 1;
79 + subst_from_stdin ();
80 + */
81 + case 2:
82 + /* echo '$foo and $bar' | git sh-i18n--envsubst --variables '$foo and $bar' */
83 + all_variables = 0;
84 + note_variables (argv[1]);
85 + subst_from_stdin ();
86 + break;
87 + case 3:
88 + /* git sh-i18n--envsubst --variables '$foo and $bar' */
89 + if (strcmp(argv[1], "--variables"))
90 + error ("first argument must be --variables when two are given");
91 + show_variables = 1;
92 + print_variables (argv[2]);
93 + break;
94 + default:
95 + error ("too many arguments");
96 + break;
97 + }
98 +
99 + /* Close standard error. This is simpler than fwriteerror_no_ebadf, because
100 + upon failure we don't need an errno - all we can do at this point is to
101 + set an exit status. */
102 + errno = 0;
103 + if (ferror (stderr) || fflush (stderr))
104 + {
105 + fclose (stderr);
106 + exit (EXIT_FAILURE);
107 + }
108 + if (fclose (stderr) && errno != EBADF)
109 + exit (EXIT_FAILURE);
110 +
111 + exit (EXIT_SUCCESS);
112 +}
113 +
114 +/* Parse the string and invoke the callback each time a $VARIABLE or
115 + ${VARIABLE} construct is seen, where VARIABLE is a nonempty sequence
116 + of ASCII alphanumeric/underscore characters, starting with an ASCII
117 + alphabetic/underscore character.
118 + We allow only ASCII characters, to avoid dependencies w.r.t. the current
119 + encoding: While "${\xe0}" looks like a variable access in ISO-8859-1
120 + encoding, it doesn't look like one in the BIG5, BIG5-HKSCS, GBK, GB18030,
121 + SHIFT_JIS, JOHAB encodings, because \xe0\x7d is a single character in these
122 + encodings. */
123 +static void
124 +find_variables (const char *string,
125 + void (*callback) (const char *var_ptr, size_t var_len))
126 +{
127 + for (; *string != '\0';)
128 + if (*string++ == '$')
129 + {
130 + const char *variable_start;
131 + const char *variable_end;
132 + unsigned short int valid;
133 + char c;
134 +
135 + if (*string == '{')
136 + string++;
137 +
138 + variable_start = string;
139 + c = *string;
140 + if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_')
141 + {
142 + do
143 + c = *++string;
144 + while ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
145 + || (c >= '0' && c <= '9') || c == '_');
146 + variable_end = string;
147 +
148 + if (variable_start[-1] == '{')
149 + {
150 + if (*string == '}')
151 + {
152 + string++;
153 + valid = 1;
154 + }
155 + else
156 + valid = 0;
157 + }
158 + else
159 + valid = 1;
160 +
161 + if (valid)
162 + callback (variable_start, variable_end - variable_start);
163 + }
164 + }
165 +}
166 +
167 +
168 +/* Print a variable to stdout, followed by a newline. */
169 +static void
170 +print_variable (const char *var_ptr, size_t var_len)
171 +{
172 + fwrite (var_ptr, var_len, 1, stdout);
173 + putchar ('\n');
174 +}
175 +
176 +/* Print the variables contained in STRING to stdout, each one followed by a
177 + newline. */
178 +static void
179 +print_variables (const char *string)
180 +{
181 + find_variables (string, &print_variable);
182 +}
183 +
184 +
185 +/* Type describing list of immutable strings,
186 + implemented using a dynamic array. */
187 +typedef struct string_list_ty string_list_ty;
188 +struct string_list_ty
189 +{
190 + const char **item;
191 + size_t nitems;
192 + size_t nitems_max;
193 +};
194 +
195 +/* Initialize an empty list of strings. */
196 +static inline void
197 +string_list_init (string_list_ty *slp)
198 +{
199 + slp->item = NULL;
200 + slp->nitems = 0;
201 + slp->nitems_max = 0;
202 +}
203 +
204 +/* Append a single string to the end of a list of strings. */
205 +static inline void
206 +string_list_append (string_list_ty *slp, const char *s)
207 +{
208 + /* Grow the list. */
209 + if (slp->nitems >= slp->nitems_max)
210 + {
211 + size_t nbytes;
212 +
213 + slp->nitems_max = slp->nitems_max * 2 + 4;
214 + nbytes = slp->nitems_max * sizeof (slp->item[0]);
215 + slp->item = (const char **) xrealloc (slp->item, nbytes);
216 + }
217 +
218 + /* Add the string to the end of the list. */
219 + slp->item[slp->nitems++] = s;
220 +}
221 +
222 +/* Compare two strings given by reference. */
223 +static int
224 +cmp_string (const void *pstr1, const void *pstr2)
225 +{
226 + const char *str1 = *(const char **)pstr1;
227 + const char *str2 = *(const char **)pstr2;
228 +
229 + return strcmp (str1, str2);
230 +}
231 +
232 +/* Sort a list of strings. */
233 +static inline void
234 +string_list_sort (string_list_ty *slp)
235 +{
236 + if (slp->nitems > 0)
237 + qsort (slp->item, slp->nitems, sizeof (slp->item[0]), cmp_string);
238 +}
239 +
240 +/* Test whether a string list contains a given string. */
241 +static inline int
242 +string_list_member (const string_list_ty *slp, const char *s)
243 +{
244 + size_t j;
245 +
246 + for (j = 0; j < slp->nitems; ++j)
247 + if (strcmp (slp->item[j], s) == 0)
248 + return 1;
249 + return 0;
250 +}
251 +
252 +/* Test whether a sorted string list contains a given string. */
253 +static int
254 +sorted_string_list_member (const string_list_ty *slp, const char *s)
255 +{
256 + size_t j1, j2;
257 +
258 + j1 = 0;
259 + j2 = slp->nitems;
260 + if (j2 > 0)
261 + {
262 + /* Binary search. */
263 + while (j2 - j1 > 1)
264 + {
265 + /* Here we know that if s is in the list, it is at an index j
266 + with j1 <= j < j2. */
267 + size_t j = (j1 + j2) >> 1;
268 + int result = strcmp (slp->item[j], s);
269 +
270 + if (result > 0)
271 + j2 = j;
272 + else if (result == 0)
273 + return 1;
274 + else
275 + j1 = j + 1;
276 + }
277 + if (j2 > j1)
278 + if (strcmp (slp->item[j1], s) == 0)
279 + return 1;
280 + }
281 + return 0;
282 +}
283 +
284 +
285 +/* Set of variables on which to perform substitution.
286 + Used only if !all_variables. */
287 +static string_list_ty variables_set;
288 +
289 +/* Adds a variable to variables_set. */
290 +static void
291 +note_variable (const char *var_ptr, size_t var_len)
292 +{
293 + char *string = xmalloc (var_len + 1);
294 + memcpy (string, var_ptr, var_len);
295 + string[var_len] = '\0';
296 +
297 + string_list_append (&variables_set, string);
298 +}
299 +
300 +/* Stores the variables occurring in the string in variables_set. */
301 +static void
302 +note_variables (const char *string)
303 +{
304 + string_list_init (&variables_set);
305 + find_variables (string, &note_variable);
306 + string_list_sort (&variables_set);
307 +}
308 +
309 +
310 +static int
311 +do_getc (void)
312 +{
313 + int c = getc (stdin);
314 +
315 + if (c == EOF)
316 + {
317 + if (ferror (stdin))
318 + error ("error while reading standard input");
319 + }
320 +
321 + return c;
322 +}
323 +
324 +static inline void
325 +do_ungetc (int c)
326 +{
327 + if (c != EOF)
328 + ungetc (c, stdin);
329 +}
330 +
331 +/* Copies stdin to stdout, performing substitutions. */
332 +static void
333 +subst_from_stdin (void)
334 +{
335 + static char *buffer;
336 + static size_t bufmax;
337 + static size_t buflen;
338 + int c;
339 +
340 + for (;;)
341 + {
342 + c = do_getc ();
343 + if (c == EOF)
344 + break;
345 + /* Look for $VARIABLE or ${VARIABLE}. */
346 + if (c == '$')
347 + {
348 + unsigned short int opening_brace = 0;
349 + unsigned short int closing_brace = 0;
350 +
351 + c = do_getc ();
352 + if (c == '{')
353 + {
354 + opening_brace = 1;
355 + c = do_getc ();
356 + }
357 + if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_')
358 + {
359 + unsigned short int valid;
360 +
361 + /* Accumulate the VARIABLE in buffer. */
362 + buflen = 0;
363 + do
364 + {
365 + if (buflen >= bufmax)
366 + {
367 + bufmax = 2 * bufmax + 10;
368 + buffer = xrealloc (buffer, bufmax);
369 + }
370 + buffer[buflen++] = c;
371 +
372 + c = do_getc ();
373 + }
374 + while ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
375 + || (c >= '0' && c <= '9') || c == '_');
376 +
377 + if (opening_brace)
378 + {
379 + if (c == '}')
380 + {
381 + closing_brace = 1;
382 + valid = 1;
383 + }
384 + else
385 + {
386 + valid = 0;
387 + do_ungetc (c);
388 + }
389 + }
390 + else
391 + {
392 + valid = 1;
393 + do_ungetc (c);
394 + }
395 +
396 + if (valid)
397 + {
398 + /* Terminate the variable in the buffer. */
399 + if (buflen >= bufmax)
400 + {
401 + bufmax = 2 * bufmax + 10;
402 + buffer = xrealloc (buffer, bufmax);
403 + }
404 + buffer[buflen] = '\0';
405 +
406 + /* Test whether the variable shall be substituted. */
407 + if (!all_variables
408 + && !sorted_string_list_member (&variables_set, buffer))
409 + valid = 0;
410 + }
411 +
412 + if (valid)
413 + {
414 + /* Substitute the variable's value from the environment. */
415 + const char *env_value = getenv (buffer);
416 +
417 + if (env_value != NULL)
418 + fputs (env_value, stdout);
419 + }
420 + else
421 + {
422 + /* Perform no substitution at all. Since the buffered input
423 + contains no other '$' than at the start, we can just
424 + output all the buffered contents. */
425 + putchar ('$');
426 + if (opening_brace)
427 + putchar ('{');
428 + fwrite (buffer, buflen, 1, stdout);
429 + if (closing_brace)
430 + putchar ('}');
431 + }
432 + }
433 + else
434 + {
435 + do_ungetc (c);
436 + putchar ('$');
437 + if (opening_brace)
438 + putchar ('{');
439 + }
440 + }
441 + else
442 + putchar (c);
443 + }
444 +}
t/t0201-gettext-fallbacks.sh new
+51
@@ -0,0 +1,51 @@
1 +#!/bin/sh
2 +#
3 +# Copyright (c) 2010 Ævar Arnfjörð Bjarmason
4 +#
5 +
6 +test_description='Gettext Shell fallbacks'
7 +
8 +. ./test-lib.sh
9 +. "$GIT_BUILD_DIR"/git-sh-i18n
10 +
11 +test_expect_success 'gettext: our gettext() fallback has pass-through semantics' '
12 + printf "test" >expect &&
13 + gettext "test" >actual &&
14 + test_i18ncmp expect actual &&
15 + printf "test more words" >expect &&
16 + gettext "test more words" >actual &&
17 + test_i18ncmp expect actual
18 +'
19 +
20 +test_expect_success 'eval_gettext: our eval_gettext() fallback has pass-through semantics' '
21 + printf "test" >expect &&
22 + eval_gettext "test" >actual &&
23 + test_i18ncmp expect actual &&
24 + printf "test more words" >expect &&
25 + eval_gettext "test more words" >actual &&
26 + test_i18ncmp expect actual
27 +'
28 +
29 +test_expect_success 'eval_gettext: our eval_gettext() fallback can interpolate variables' '
30 + printf "test YesPlease" >expect &&
31 + GIT_INTERNAL_GETTEXT_TEST_FALLBACKS=YesPlease eval_gettext "test \$GIT_INTERNAL_GETTEXT_TEST_FALLBACKS" >actual &&
32 + test_i18ncmp expect actual
33 +'
34 +
35 +test_expect_success 'eval_gettext: our eval_gettext() fallback can interpolate variables with spaces' '
36 + cmdline="git am" &&
37 + export cmdline;
38 + printf "When you have resolved this problem run git am --resolved." >expect &&
39 + eval_gettext "When you have resolved this problem run \$cmdline --resolved." >actual
40 + test_i18ncmp expect actual
41 +'
42 +
43 +test_expect_success 'eval_gettext: our eval_gettext() fallback can interpolate variables with spaces and quotes' '
44 + cmdline="git am" &&
45 + export cmdline;
46 + printf "When you have resolved this problem run \"git am --resolved\"." >expect &&
47 + eval_gettext "When you have resolved this problem run \"\$cmdline --resolved\"." >actual
48 + test_i18ncmp expect actual
49 +'
50 +
51 +test_done