• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /* Edit translations using a subprocess.
2    Copyright (C) 2001-2010, 2012, 2014-2016, 2018-2020 Free Software Foundation, Inc.
3    Written by Bruno Haible <haible@clisp.cons.org>, 2001.
4 
5    This program is free software: you can redistribute it and/or modify
6    it under the terms of the GNU General Public License as published by
7    the Free Software Foundation; either version 3 of the License, or
8    (at your option) any later version.
9 
10    This program is distributed in the hope that it will be useful,
11    but WITHOUT ANY WARRANTY; without even the implied warranty of
12    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13    GNU General Public License for more details.
14 
15    You should have received a copy of the GNU General Public License
16    along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
17 
18 
19 #ifdef HAVE_CONFIG_H
20 # include "config.h"
21 #endif
22 
23 #include <getopt.h>
24 #include <limits.h>
25 #include <locale.h>
26 #include <stdio.h>
27 #include <stdlib.h>
28 #include <string.h>
29 #include <sys/types.h>
30 #include <sys/time.h>
31 #include <unistd.h>
32 
33 #include <textstyle.h>
34 
35 #include "noreturn.h"
36 #include "closeout.h"
37 #include "dir-list.h"
38 #include "error.h"
39 #include "xvasprintf.h"
40 #include "error-progname.h"
41 #include "progname.h"
42 #include "relocatable.h"
43 #include "basename-lgpl.h"
44 #include "message.h"
45 #include "read-catalog.h"
46 #include "read-po.h"
47 #include "read-properties.h"
48 #include "read-stringtable.h"
49 #include "write-catalog.h"
50 #include "write-po.h"
51 #include "write-properties.h"
52 #include "write-stringtable.h"
53 #include "msgl-charset.h"
54 #include "xalloc.h"
55 #include "findprog.h"
56 #include "pipe-filter.h"
57 #include "xsetenv.h"
58 #include "filters.h"
59 #include "msgl-iconv.h"
60 #include "po-charset.h"
61 #include "propername.h"
62 #include "gettext.h"
63 
64 #define _(str) gettext (str)
65 
66 
67 /* We use a child process, and communicate through a bidirectional pipe.  */
68 
69 
70 /* Force output of PO file even if empty.  */
71 static int force_po;
72 
73 /* Keep the header entry unmodified.  */
74 static int keep_header;
75 
76 /* Name of the subprogram.  */
77 static const char *sub_name;
78 
79 /* Pathname of the subprogram.  */
80 static const char *sub_path;
81 
82 /* Argument list for the subprogram.  */
83 static const char **sub_argv;
84 static int sub_argc;
85 
86 static bool newline;
87 
88 /* Filter function.  */
89 static void (*filter) (const char *str, size_t len, char **resultp, size_t *lengthp);
90 
91 /* Long options.  */
92 static const struct option long_options[] =
93 {
94   { "add-location", optional_argument, NULL, 'n' },
95   { "color", optional_argument, NULL, CHAR_MAX + 6 },
96   { "directory", required_argument, NULL, 'D' },
97   { "escape", no_argument, NULL, 'E' },
98   { "force-po", no_argument, &force_po, 1 },
99   { "help", no_argument, NULL, 'h' },
100   { "indent", no_argument, NULL, CHAR_MAX + 1 },
101   { "input", required_argument, NULL, 'i' },
102   { "keep-header", no_argument, &keep_header, 1 },
103   { "newline", no_argument, NULL, CHAR_MAX + 9 },
104   { "no-escape", no_argument, NULL, CHAR_MAX + 2 },
105   { "no-location", no_argument, NULL, CHAR_MAX + 8 },
106   { "no-wrap", no_argument, NULL, CHAR_MAX + 3 },
107   { "output-file", required_argument, NULL, 'o' },
108   { "properties-input", no_argument, NULL, 'P' },
109   { "properties-output", no_argument, NULL, 'p' },
110   { "sort-by-file", no_argument, NULL, 'F' },
111   { "sort-output", no_argument, NULL, 's' },
112   { "strict", no_argument, NULL, 'S' },
113   { "stringtable-input", no_argument, NULL, CHAR_MAX + 4 },
114   { "stringtable-output", no_argument, NULL, CHAR_MAX + 5 },
115   { "style", required_argument, NULL, CHAR_MAX + 7 },
116   { "version", no_argument, NULL, 'V' },
117   { "width", required_argument, NULL, 'w' },
118   { NULL, 0, NULL, 0 }
119 };
120 
121 
122 /* Forward declaration of local functions.  */
123 _GL_NORETURN_FUNC static void usage (int status);
124 static void generic_filter (const char *str, size_t len, char **resultp, size_t *lengthp);
125 static msgdomain_list_ty *process_msgdomain_list (msgdomain_list_ty *mdlp);
126 
127 
128 int
main(int argc,char ** argv)129 main (int argc, char **argv)
130 {
131   int opt;
132   bool do_help;
133   bool do_version;
134   char *output_file;
135   const char *input_file;
136   msgdomain_list_ty *result;
137   catalog_input_format_ty input_syntax = &input_format_po;
138   catalog_output_format_ty output_syntax = &output_format_po;
139   bool sort_by_filepos = false;
140   bool sort_by_msgid = false;
141   int i;
142 
143   /* Set program name for messages.  */
144   set_program_name (argv[0]);
145   error_print_progname = maybe_print_progname;
146 
147   /* Set locale via LC_ALL.  */
148   setlocale (LC_ALL, "");
149 
150   /* Set the text message domain.  */
151   bindtextdomain (PACKAGE, relocate (LOCALEDIR));
152   bindtextdomain ("bison-runtime", relocate (BISON_LOCALEDIR));
153   textdomain (PACKAGE);
154 
155   /* Ensure that write errors on stdout are detected.  */
156   atexit (close_stdout);
157 
158   /* Set default values for variables.  */
159   do_help = false;
160   do_version = false;
161   output_file = NULL;
162   input_file = NULL;
163 
164   /* The '+' in the options string causes option parsing to terminate when
165      the first non-option, i.e. the subprogram name, is encountered.  */
166   while ((opt = getopt_long (argc, argv, "+D:EFhi:n:o:pPsVw:", long_options,
167                              NULL))
168          != EOF)
169     switch (opt)
170       {
171       case '\0':                /* Long option.  */
172         break;
173 
174       case 'D':
175         dir_list_append (optarg);
176         break;
177 
178       case 'E':
179         message_print_style_escape (true);
180         break;
181 
182       case 'F':
183         sort_by_filepos = true;
184         break;
185 
186       case 'h':
187         do_help = true;
188         break;
189 
190       case 'i':
191         if (input_file != NULL)
192           {
193             error (EXIT_SUCCESS, 0, _("at most one input file allowed"));
194             usage (EXIT_FAILURE);
195           }
196         input_file = optarg;
197         break;
198 
199       case 'n':
200         if (handle_filepos_comment_option (optarg))
201           usage (EXIT_FAILURE);
202         break;
203 
204       case 'o':
205         output_file = optarg;
206         break;
207 
208       case 'p':
209         output_syntax = &output_format_properties;
210         break;
211 
212       case 'P':
213         input_syntax = &input_format_properties;
214         break;
215 
216       case 's':
217         sort_by_msgid = true;
218         break;
219 
220       case 'S':
221         message_print_style_uniforum ();
222         break;
223 
224       case 'V':
225         do_version = true;
226         break;
227 
228       case 'w':
229         {
230           int value;
231           char *endp;
232           value = strtol (optarg, &endp, 10);
233           if (endp != optarg)
234             message_page_width_set (value);
235         }
236         break;
237 
238       case CHAR_MAX + 1:
239         message_print_style_indent ();
240         break;
241 
242       case CHAR_MAX + 2:
243         message_print_style_escape (false);
244         break;
245 
246       case CHAR_MAX + 3: /* --no-wrap */
247         message_page_width_ignore ();
248         break;
249 
250       case CHAR_MAX + 4: /* --stringtable-input */
251         input_syntax = &input_format_stringtable;
252         break;
253 
254       case CHAR_MAX + 5: /* --stringtable-output */
255         output_syntax = &output_format_stringtable;
256         break;
257 
258       case CHAR_MAX + 6: /* --color */
259         if (handle_color_option (optarg) || color_test_mode)
260           usage (EXIT_FAILURE);
261         break;
262 
263       case CHAR_MAX + 7: /* --style */
264         handle_style_option (optarg);
265         break;
266 
267       case CHAR_MAX + 8: /* --no-location */
268         message_print_style_filepos (filepos_comment_none);
269         break;
270 
271       case CHAR_MAX + 9: /* --newline */
272         newline = true;
273         break;
274 
275       default:
276         usage (EXIT_FAILURE);
277         break;
278       }
279 
280   /* Version information is requested.  */
281   if (do_version)
282     {
283       printf ("%s (GNU %s) %s\n", last_component (program_name),
284               PACKAGE, VERSION);
285       /* xgettext: no-wrap */
286       printf (_("Copyright (C) %s Free Software Foundation, Inc.\n\
287 License GPLv3+: GNU GPL version 3 or later <%s>\n\
288 This is free software: you are free to change and redistribute it.\n\
289 There is NO WARRANTY, to the extent permitted by law.\n\
290 "),
291               "2001-2020", "https://gnu.org/licenses/gpl.html");
292       printf (_("Written by %s.\n"), proper_name ("Bruno Haible"));
293       exit (EXIT_SUCCESS);
294     }
295 
296   /* Help is requested.  */
297   if (do_help)
298     usage (EXIT_SUCCESS);
299 
300   /* Test for the subprogram name.  */
301   if (optind == argc)
302     error (EXIT_FAILURE, 0, _("missing filter name"));
303   sub_name = argv[optind];
304 
305   /* Verify selected options.  */
306   if (sort_by_msgid && sort_by_filepos)
307     error (EXIT_FAILURE, 0, _("%s and %s are mutually exclusive"),
308            "--sort-output", "--sort-by-file");
309 
310   /* Build argument list for the program.  */
311   sub_argc = argc - optind;
312   sub_argv = XNMALLOC (sub_argc + 1, const char *);
313   for (i = 0; i < sub_argc; i++)
314     sub_argv[i] = argv[optind + i];
315   sub_argv[i] = NULL;
316 
317   /* Extra checks for sed scripts.  */
318   if (strcmp (sub_name, "sed") == 0)
319     {
320       if (sub_argc == 1)
321         error (EXIT_FAILURE, 0,
322                _("at least one sed script must be specified"));
323 
324       /* Replace GNU sed specific options with portable sed options.  */
325       for (i = 1; i < sub_argc; i++)
326         {
327           if (strcmp (sub_argv[i], "--expression") == 0)
328             sub_argv[i] = "-e";
329           else if (strcmp (sub_argv[i], "--file") == 0)
330             sub_argv[i] = "-f";
331           else if (strcmp (sub_argv[i], "--quiet") == 0
332                    || strcmp (sub_argv[i], "--silent") == 0)
333             sub_argv[i] = "-n";
334 
335           if (strcmp (sub_argv[i], "-e") == 0
336               || strcmp (sub_argv[i], "-f") == 0)
337             i++;
338         }
339     }
340 
341   /* By default, input comes from standard input.  */
342   if (input_file == NULL)
343     input_file = "-";
344 
345   /* Read input file.  */
346   result = read_catalog_file (input_file, input_syntax);
347 
348   /* Recognize special programs as built-ins.  */
349   if (strcmp (sub_name, "recode-sr-latin") == 0 && sub_argc == 1)
350     {
351       filter = serbian_to_latin;
352 
353       /* Convert the input to UTF-8 first.  */
354       result = iconv_msgdomain_list (result, po_charset_utf8, true, input_file);
355     }
356   else if (strcmp (sub_name, "quot") == 0 && sub_argc == 1)
357     {
358       filter = ascii_quote_to_unicode;
359 
360       /* Convert the input to UTF-8 first.  */
361       result = iconv_msgdomain_list (result, po_charset_utf8, true, input_file);
362     }
363   else if (strcmp (sub_name, "boldquot") == 0 && sub_argc == 1)
364     {
365       filter = ascii_quote_to_unicode_bold;
366 
367       /* Convert the input to UTF-8 first.  */
368       result = iconv_msgdomain_list (result, po_charset_utf8, true, input_file);
369     }
370   else
371     {
372       filter = generic_filter;
373 
374       /* Warn if the current locale is not suitable for this PO file.  */
375       compare_po_locale_charsets (result);
376 
377       /* Attempt to locate the program.
378          This is an optimization, to avoid that spawn/exec searches the PATH
379          on every call.  */
380       sub_path = find_in_path (sub_name);
381 
382       /* Finish argument list for the program.  */
383       sub_argv[0] = sub_path;
384     }
385 
386   /* Apply the subprogram.  */
387   result = process_msgdomain_list (result);
388 
389   /* Sort the results.  */
390   if (sort_by_filepos)
391     msgdomain_list_sort_by_filepos (result);
392   else if (sort_by_msgid)
393     msgdomain_list_sort_by_msgid (result);
394 
395   /* Write the merged message list out.  */
396   msgdomain_list_print (result, output_file, output_syntax, force_po, false);
397 
398   exit (EXIT_SUCCESS);
399 }
400 
401 
402 /* Display usage information and exit.  */
403 static void
usage(int status)404 usage (int status)
405 {
406   if (status != EXIT_SUCCESS)
407     fprintf (stderr, _("Try '%s --help' for more information.\n"),
408              program_name);
409   else
410     {
411       printf (_("\
412 Usage: %s [OPTION] FILTER [FILTER-OPTION]\n\
413 "), program_name);
414       printf ("\n");
415       printf (_("\
416 Applies a filter to all translations of a translation catalog.\n\
417 "));
418       printf ("\n");
419       printf (_("\
420 Mandatory arguments to long options are mandatory for short options too.\n"));
421       printf ("\n");
422       printf (_("\
423 Input file location:\n"));
424       printf (_("\
425   -i, --input=INPUTFILE       input PO file\n"));
426       printf (_("\
427   -D, --directory=DIRECTORY   add DIRECTORY to list for input files search\n"));
428       printf (_("\
429 If no input file is given or if it is -, standard input is read.\n"));
430       printf ("\n");
431       printf (_("\
432 Output file location:\n"));
433       printf (_("\
434   -o, --output-file=FILE      write output to specified file\n"));
435       printf (_("\
436 The results are written to standard output if no output file is specified\n\
437 or if it is -.\n"));
438       printf ("\n");
439       printf (_("\
440 The FILTER can be any program that reads a translation from standard input\n\
441 and writes a modified translation to standard output.\n\
442 "));
443       printf ("\n");
444       printf (_("\
445 Filter input and output:\n"));
446       printf (_("\
447   --newline                   add a newline at the end of input and\n\
448                                 remove a newline from the end of output"));
449       printf ("\n");
450       printf (_("\
451 Useful FILTER-OPTIONs when the FILTER is 'sed':\n"));
452       printf (_("\
453   -e, --expression=SCRIPT     add SCRIPT to the commands to be executed\n"));
454       printf (_("\
455   -f, --file=SCRIPTFILE       add the contents of SCRIPTFILE to the commands\n\
456                                 to be executed\n"));
457       printf (_("\
458   -n, --quiet, --silent       suppress automatic printing of pattern space\n"));
459       printf ("\n");
460       printf (_("\
461 Input file syntax:\n"));
462       printf (_("\
463   -P, --properties-input      input file is in Java .properties syntax\n"));
464       printf (_("\
465       --stringtable-input     input file is in NeXTstep/GNUstep .strings syntax\n"));
466       printf ("\n");
467       printf (_("\
468 Output details:\n"));
469       printf (_("\
470       --color                 use colors and other text attributes always\n\
471       --color=WHEN            use colors and other text attributes if WHEN.\n\
472                               WHEN may be 'always', 'never', 'auto', or 'html'.\n"));
473       printf (_("\
474       --style=STYLEFILE       specify CSS style rule file for --color\n"));
475       printf (_("\
476       --no-escape             do not use C escapes in output (default)\n"));
477       printf (_("\
478   -E, --escape                use C escapes in output, no extended chars\n"));
479       printf (_("\
480       --force-po              write PO file even if empty\n"));
481       printf (_("\
482       --indent                indented output style\n"));
483       printf (_("\
484       --keep-header           keep header entry unmodified, don't filter it\n"));
485       printf (_("\
486       --no-location           suppress '#: filename:line' lines\n"));
487       printf (_("\
488   -n, --add-location          preserve '#: filename:line' lines (default)\n"));
489       printf (_("\
490       --strict                strict Uniforum output style\n"));
491       printf (_("\
492   -p, --properties-output     write out a Java .properties file\n"));
493       printf (_("\
494       --stringtable-output    write out a NeXTstep/GNUstep .strings file\n"));
495       printf (_("\
496   -w, --width=NUMBER          set output page width\n"));
497       printf (_("\
498       --no-wrap               do not break long message lines, longer than\n\
499                               the output page width, into several lines\n"));
500       printf (_("\
501   -s, --sort-output           generate sorted output\n"));
502       printf (_("\
503   -F, --sort-by-file          sort output by file location\n"));
504       printf ("\n");
505       printf (_("\
506 Informative output:\n"));
507       printf (_("\
508   -h, --help                  display this help and exit\n"));
509       printf (_("\
510   -V, --version               output version information and exit\n"));
511       printf ("\n");
512       /* TRANSLATORS: The first placeholder is the web address of the Savannah
513          project of this package.  The second placeholder is the bug-reporting
514          email address for this package.  Please add _another line_ saying
515          "Report translation bugs to <...>\n" with the address for translation
516          bugs (typically your translation team's web or email address).  */
517       printf(_("\
518 Report bugs in the bug tracker at <%s>\n\
519 or by email to <%s>.\n"),
520              "https://savannah.gnu.org/projects/gettext",
521              "bug-gettext@gnu.org");
522     }
523 
524   exit (status);
525 }
526 
527 
528 /* Callbacks called from pipe_filter_ii_execute.  */
529 
530 struct locals
531 {
532   /* String being written.  */
533   const char *str;
534   size_t len;
535   /* String being read and accumulated.  */
536   char *result;
537   size_t allocated;
538   size_t length;
539 };
540 
541 static const void *
prepare_write(size_t * num_bytes_p,void * private_data)542 prepare_write (size_t *num_bytes_p, void *private_data)
543 {
544   struct locals *l = (struct locals *) private_data;
545 
546   if (l->len > 0)
547     {
548       *num_bytes_p = l->len;
549       return l->str;
550     }
551   else
552     return NULL;
553 }
554 
555 static void
done_write(void * data_written,size_t num_bytes_written,void * private_data)556 done_write (void *data_written, size_t num_bytes_written, void *private_data)
557 {
558   struct locals *l = (struct locals *) private_data;
559 
560   l->str += num_bytes_written;
561   l->len -= num_bytes_written;
562 }
563 
564 static void *
prepare_read(size_t * num_bytes_p,void * private_data)565 prepare_read (size_t *num_bytes_p, void *private_data)
566 {
567   struct locals *l = (struct locals *) private_data;
568 
569   if (l->length == l->allocated)
570     {
571       l->allocated = l->allocated + (l->allocated >> 1) + 1;
572       l->result = (char *) xrealloc (l->result, l->allocated);
573     }
574   *num_bytes_p = l->allocated - l->length;
575   return l->result + l->length;
576 }
577 
578 static void
done_read(void * data_read,size_t num_bytes_read,void * private_data)579 done_read (void *data_read, size_t num_bytes_read, void *private_data)
580 {
581   struct locals *l = (struct locals *) private_data;
582 
583   l->length += num_bytes_read;
584 }
585 
586 
587 /* Process a string STR of size LEN bytes through the subprogram.
588    Store the freshly allocated result at *RESULTP and its length at *LENGTHP.
589  */
590 static void
generic_filter(const char * str,size_t len,char ** resultp,size_t * lengthp)591 generic_filter (const char *str, size_t len, char **resultp, size_t *lengthp)
592 {
593   struct locals l;
594 
595   l.str = str;
596   l.len = len;
597   l.allocated = len + (len >> 2) + 1;
598   l.result = XNMALLOC (l.allocated, char);
599   l.length = 0;
600 
601   pipe_filter_ii_execute (sub_name, sub_path, sub_argv, false, true,
602                           prepare_write, done_write, prepare_read, done_read,
603                           &l);
604 
605   *resultp = l.result;
606   *lengthp = l.length;
607 }
608 
609 
610 /* Process a string STR of size LEN bytes, then remove NUL bytes.
611    Store the freshly allocated result at *RESULTP and its length at *LENGTHP.
612  */
613 static void
process_string(const char * str,size_t len,char ** resultp,size_t * lengthp)614 process_string (const char *str, size_t len, char **resultp, size_t *lengthp)
615 {
616   char *result;
617   size_t length;
618 
619   filter (str, len, &result, &length);
620 
621   /* Remove NUL bytes from result.  */
622   {
623     char *p = result;
624     char *pend = result + length;
625 
626     for (; p < pend; p++)
627       if (*p == '\0')
628         {
629           char *q;
630 
631           q = p;
632           for (; p < pend; p++)
633             if (*p != '\0')
634               *q++ = *p;
635           length = q - result;
636           break;
637         }
638   }
639 
640   *resultp = result;
641   *lengthp = length;
642 }
643 
644 
645 /* Do the same thing as process_string but append a newline to STR
646    before processing, and remove a newline from the result.
647  */
648 static void
process_string_with_newline(const char * str,size_t len,char ** resultp,size_t * lengthp)649 process_string_with_newline (const char *str, size_t len, char **resultp,
650                              size_t *lengthp)
651 {
652   char *newstr;
653   char *result;
654   size_t length;
655 
656   newstr = XNMALLOC (len + 1, char);
657   memcpy (newstr, str, len);
658   newstr[len] = '\n';
659 
660   process_string (newstr, len + 1, &result, &length);
661 
662   free (newstr);
663 
664   if (length > 0 && result[length - 1] == '\n')
665     result[--length] = '\0';
666   else
667     error (0, 0, _("filter output is not terminated with a newline"));
668 
669   *resultp = result;
670   *lengthp = length;
671 }
672 
673 
674 static void
process_message(message_ty * mp)675 process_message (message_ty *mp)
676 {
677   const char *msgstr = mp->msgstr;
678   size_t msgstr_len = mp->msgstr_len;
679   char *location;
680   size_t nsubstrings;
681   char **substrings;
682   size_t total_len;
683   char *total_str;
684   const char *p;
685   char *q;
686   size_t k;
687 
688   /* Keep the header entry unmodified, if --keep-header was given.  */
689   if (is_header (mp) && keep_header)
690     return;
691 
692   /* Set environment variables for the subprocess.
693      Note: These environment variables, especially MSGFILTER_MSGCTXT and
694      MSGFILTER_MSGID, may contain non-ASCII characters.  The subprocess
695      may not interpret these values correctly if the locale encoding is
696      different from the PO file's encoding.  We want about this situation,
697      above.
698      On Unix, this problem is often harmless.  On Windows, however, - both
699      native Windows and Cygwin - the values of environment variables *must*
700      be in the encoding that is the value of GetACP(), because the system
701      may convert the environment from char** to wchar_t** before spawning
702      the subprocess and back from wchar_t** to char** in the subprocess,
703      and it does so using the GetACP() codepage.  */
704   if (mp->msgctxt != NULL)
705     xsetenv ("MSGFILTER_MSGCTXT", mp->msgctxt, 1);
706   else
707     unsetenv ("MSGFILTER_MSGCTXT");
708   xsetenv ("MSGFILTER_MSGID", mp->msgid, 1);
709   if (mp->msgid_plural != NULL)
710     xsetenv ("MSGFILTER_MSGID_PLURAL", mp->msgid_plural, 1);
711   else
712     unsetenv ("MSGFILTER_MSGID_PLURAL");
713   location = xasprintf ("%s:%ld", mp->pos.file_name,
714                         (long) mp->pos.line_number);
715   xsetenv ("MSGFILTER_LOCATION", location, 1);
716   free (location);
717   if (mp->prev_msgctxt != NULL)
718     xsetenv ("MSGFILTER_PREV_MSGCTXT", mp->prev_msgctxt, 1);
719   else
720     unsetenv ("MSGFILTER_PREV_MSGCTXT");
721   if (mp->prev_msgid != NULL)
722     xsetenv ("MSGFILTER_PREV_MSGID", mp->prev_msgid, 1);
723   else
724     unsetenv ("MSGFILTER_PREV_MSGID");
725   if (mp->prev_msgid_plural != NULL)
726     xsetenv ("MSGFILTER_PREV_MSGID_PLURAL", mp->prev_msgid_plural, 1);
727   else
728     unsetenv ("MSGFILTER_PREV_MSGID_PLURAL");
729 
730   /* Count NUL delimited substrings.  */
731   for (p = msgstr, nsubstrings = 0;
732        p < msgstr + msgstr_len;
733        p += strlen (p) + 1, nsubstrings++);
734 
735   /* Process each NUL delimited substring separately.  */
736   substrings = XNMALLOC (nsubstrings, char *);
737   for (p = msgstr, k = 0, total_len = 0; k < nsubstrings; k++)
738     {
739       char *result;
740       size_t length;
741 
742       if (mp->msgid_plural != NULL)
743         {
744           char *plural_form_string = xasprintf ("%lu", (unsigned long) k);
745 
746           xsetenv ("MSGFILTER_PLURAL_FORM", plural_form_string, 1);
747           free (plural_form_string);
748         }
749       else
750         unsetenv ("MSGFILTER_PLURAL_FORM");
751 
752       if (newline)
753         process_string_with_newline (p, strlen (p), &result, &length);
754       else
755         process_string (p, strlen (p), &result, &length);
756 
757       result = (char *) xrealloc (result, length + 1);
758       result[length] = '\0';
759       substrings[k] = result;
760       total_len += length + 1;
761 
762       p += strlen (p) + 1;
763     }
764 
765   /* Concatenate the results, including the NUL after each.  */
766   total_str = XNMALLOC (total_len, char);
767   for (k = 0, q = total_str; k < nsubstrings; k++)
768     {
769       size_t length = strlen (substrings[k]);
770 
771       memcpy (q, substrings[k], length + 1);
772       free (substrings[k]);
773       q += length + 1;
774     }
775   free (substrings);
776 
777   mp->msgstr = total_str;
778   mp->msgstr_len = total_len;
779 }
780 
781 
782 static void
process_message_list(message_list_ty * mlp)783 process_message_list (message_list_ty *mlp)
784 {
785   size_t j;
786 
787   for (j = 0; j < mlp->nitems; j++)
788     process_message (mlp->item[j]);
789 }
790 
791 
792 static msgdomain_list_ty *
process_msgdomain_list(msgdomain_list_ty * mdlp)793 process_msgdomain_list (msgdomain_list_ty *mdlp)
794 {
795   size_t k;
796 
797   for (k = 0; k < mdlp->nitems; k++)
798     process_message_list (mdlp->item[k]->messages);
799 
800   return mdlp;
801 }
802