/* Edit translations using a subprocess. Copyright (C) 2001-2010, 2012, 2015 Free Software Foundation, Inc. Written by Bruno Haible , 2001. 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 3 of the License, 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, see . */ #ifdef HAVE_CONFIG_H # include "config.h" #endif #include #include #include #include #include #include #include #include #include #include "closeout.h" #include "dir-list.h" #include "error.h" #include "xvasprintf.h" #include "error-progname.h" #include "progname.h" #include "relocatable.h" #include "basename.h" #include "message.h" #include "read-catalog.h" #include "read-po.h" #include "read-properties.h" #include "read-stringtable.h" #include "write-catalog.h" #include "write-po.h" #include "write-properties.h" #include "write-stringtable.h" #include "color.h" #include "msgl-charset.h" #include "xalloc.h" #include "findprog.h" #include "pipe-filter.h" #include "xsetenv.h" #include "filters.h" #include "msgl-iconv.h" #include "po-charset.h" #include "propername.h" #include "gettext.h" #define _(str) gettext (str) /* We use a child process, and communicate through a bidirectional pipe. */ /* Force output of PO file even if empty. */ static int force_po; /* Keep the header entry unmodified. */ static int keep_header; /* Name of the subprogram. */ static const char *sub_name; /* Pathname of the subprogram. */ static const char *sub_path; /* Argument list for the subprogram. */ static const char **sub_argv; static int sub_argc; static bool newline; /* Filter function. */ static void (*filter) (const char *str, size_t len, char **resultp, size_t *lengthp); /* Long options. */ static const struct option long_options[] = { { "add-location", optional_argument, NULL, 'n' }, { "color", optional_argument, NULL, CHAR_MAX + 6 }, { "directory", required_argument, NULL, 'D' }, { "escape", no_argument, NULL, 'E' }, { "force-po", no_argument, &force_po, 1 }, { "help", no_argument, NULL, 'h' }, { "indent", no_argument, NULL, CHAR_MAX + 1 }, { "input", required_argument, NULL, 'i' }, { "keep-header", no_argument, &keep_header, 1 }, { "newline", no_argument, NULL, CHAR_MAX + 9 }, { "no-escape", no_argument, NULL, CHAR_MAX + 2 }, { "no-location", no_argument, NULL, CHAR_MAX + 8 }, { "no-wrap", no_argument, NULL, CHAR_MAX + 3 }, { "output-file", required_argument, NULL, 'o' }, { "properties-input", no_argument, NULL, 'P' }, { "properties-output", no_argument, NULL, 'p' }, { "sort-by-file", no_argument, NULL, 'F' }, { "sort-output", no_argument, NULL, 's' }, { "strict", no_argument, NULL, 'S' }, { "stringtable-input", no_argument, NULL, CHAR_MAX + 4 }, { "stringtable-output", no_argument, NULL, CHAR_MAX + 5 }, { "style", required_argument, NULL, CHAR_MAX + 7 }, { "version", no_argument, NULL, 'V' }, { "width", required_argument, NULL, 'w', }, { NULL, 0, NULL, 0 } }; /* Forward declaration of local functions. */ static void usage (int status) #if defined __GNUC__ && ((__GNUC__ == 2 && __GNUC_MINOR__ >= 5) || __GNUC__ > 2) __attribute__ ((noreturn)) #endif ; static void generic_filter (const char *str, size_t len, char **resultp, size_t *lengthp); static msgdomain_list_ty *process_msgdomain_list (msgdomain_list_ty *mdlp); int main (int argc, char **argv) { int opt; bool do_help; bool do_version; char *output_file; const char *input_file; msgdomain_list_ty *result; catalog_input_format_ty input_syntax = &input_format_po; catalog_output_format_ty output_syntax = &output_format_po; bool sort_by_filepos = false; bool sort_by_msgid = false; int i; /* Set program name for messages. */ set_program_name (argv[0]); error_print_progname = maybe_print_progname; #ifdef HAVE_SETLOCALE /* Set locale via LC_ALL. */ setlocale (LC_ALL, ""); #endif /* Set the text message domain. */ bindtextdomain (PACKAGE, relocate (LOCALEDIR)); bindtextdomain ("bison-runtime", relocate (BISON_LOCALEDIR)); textdomain (PACKAGE); /* Ensure that write errors on stdout are detected. */ atexit (close_stdout); /* Set default values for variables. */ do_help = false; do_version = false; output_file = NULL; input_file = NULL; /* The '+' in the options string causes option parsing to terminate when the first non-option, i.e. the subprogram name, is encountered. */ while ((opt = getopt_long (argc, argv, "+D:EFhi:n:o:pPsVw:", long_options, NULL)) != EOF) switch (opt) { case '\0': /* Long option. */ break; case 'D': dir_list_append (optarg); break; case 'E': message_print_style_escape (true); break; case 'F': sort_by_filepos = true; break; case 'h': do_help = true; break; case 'i': if (input_file != NULL) { error (EXIT_SUCCESS, 0, _("at most one input file allowed")); usage (EXIT_FAILURE); } input_file = optarg; break; case 'n': if (handle_filepos_comment_option (optarg)) usage (EXIT_FAILURE); break; case 'o': output_file = optarg; break; case 'p': output_syntax = &output_format_properties; break; case 'P': input_syntax = &input_format_properties; break; case 's': sort_by_msgid = true; break; case 'S': message_print_style_uniforum (); break; case 'V': do_version = true; break; case 'w': { int value; char *endp; value = strtol (optarg, &endp, 10); if (endp != optarg) message_page_width_set (value); } break; case CHAR_MAX + 1: message_print_style_indent (); break; case CHAR_MAX + 2: message_print_style_escape (false); break; case CHAR_MAX + 3: /* --no-wrap */ message_page_width_ignore (); break; case CHAR_MAX + 4: /* --stringtable-input */ input_syntax = &input_format_stringtable; break; case CHAR_MAX + 5: /* --stringtable-output */ output_syntax = &output_format_stringtable; break; case CHAR_MAX + 6: /* --color */ if (handle_color_option (optarg) || color_test_mode) usage (EXIT_FAILURE); break; case CHAR_MAX + 7: /* --style */ handle_style_option (optarg); break; case CHAR_MAX + 8: /* --no-location */ message_print_style_filepos (filepos_comment_none); break; case CHAR_MAX + 9: /* --newline */ newline = true; break; default: usage (EXIT_FAILURE); break; } /* Version information is requested. */ if (do_version) { printf ("%s (GNU %s) %s\n", basename (program_name), PACKAGE, VERSION); /* xgettext: no-wrap */ printf (_("Copyright (C) %s Free Software Foundation, Inc.\n\ License GPLv3+: GNU GPL version 3 or later \n\ This is free software: you are free to change and redistribute it.\n\ There is NO WARRANTY, to the extent permitted by law.\n\ "), "2001-2010"); printf (_("Written by %s.\n"), proper_name ("Bruno Haible")); exit (EXIT_SUCCESS); } /* Help is requested. */ if (do_help) usage (EXIT_SUCCESS); /* Test for the subprogram name. */ if (optind == argc) error (EXIT_FAILURE, 0, _("missing filter name")); sub_name = argv[optind]; /* Verify selected options. */ if (sort_by_msgid && sort_by_filepos) error (EXIT_FAILURE, 0, _("%s and %s are mutually exclusive"), "--sort-output", "--sort-by-file"); /* Build argument list for the program. */ sub_argc = argc - optind; sub_argv = XNMALLOC (sub_argc + 1, const char *); for (i = 0; i < sub_argc; i++) sub_argv[i] = argv[optind + i]; sub_argv[i] = NULL; /* Extra checks for sed scripts. */ if (strcmp (sub_name, "sed") == 0) { if (sub_argc == 1) error (EXIT_FAILURE, 0, _("at least one sed script must be specified")); /* Replace GNU sed specific options with portable sed options. */ for (i = 1; i < sub_argc; i++) { if (strcmp (sub_argv[i], "--expression") == 0) sub_argv[i] = "-e"; else if (strcmp (sub_argv[i], "--file") == 0) sub_argv[i] = "-f"; else if (strcmp (sub_argv[i], "--quiet") == 0 || strcmp (sub_argv[i], "--silent") == 0) sub_argv[i] = "-n"; if (strcmp (sub_argv[i], "-e") == 0 || strcmp (sub_argv[i], "-f") == 0) i++; } } /* By default, input comes from standard input. */ if (input_file == NULL) input_file = "-"; /* Read input file. */ result = read_catalog_file (input_file, input_syntax); /* Recognize special programs as built-ins. */ if (strcmp (sub_name, "recode-sr-latin") == 0 && sub_argc == 1) { filter = serbian_to_latin; /* Convert the input to UTF-8 first. */ result = iconv_msgdomain_list (result, po_charset_utf8, true, input_file); } else if (strcmp (sub_name, "quot") == 0 && sub_argc == 1) { filter = ascii_quote_to_unicode; /* Convert the input to UTF-8 first. */ result = iconv_msgdomain_list (result, po_charset_utf8, true, input_file); } else if (strcmp (sub_name, "boldquot") == 0 && sub_argc == 1) { filter = ascii_quote_to_unicode_bold; /* Convert the input to UTF-8 first. */ result = iconv_msgdomain_list (result, po_charset_utf8, true, input_file); } else { filter = generic_filter; /* Warn if the current locale is not suitable for this PO file. */ compare_po_locale_charsets (result); /* Attempt to locate the program. This is an optimization, to avoid that spawn/exec searches the PATH on every call. */ sub_path = find_in_path (sub_name); /* Finish argument list for the program. */ sub_argv[0] = sub_path; } /* Apply the subprogram. */ result = process_msgdomain_list (result); /* Sort the results. */ if (sort_by_filepos) msgdomain_list_sort_by_filepos (result); else if (sort_by_msgid) msgdomain_list_sort_by_msgid (result); /* Write the merged message list out. */ msgdomain_list_print (result, output_file, output_syntax, force_po, false); exit (EXIT_SUCCESS); } /* Display usage information and exit. */ static void usage (int status) { if (status != EXIT_SUCCESS) fprintf (stderr, _("Try '%s --help' for more information.\n"), program_name); else { printf (_("\ Usage: %s [OPTION] FILTER [FILTER-OPTION]\n\ "), program_name); printf ("\n"); printf (_("\ Applies a filter to all translations of a translation catalog.\n\ ")); printf ("\n"); printf (_("\ Mandatory arguments to long options are mandatory for short options too.\n")); printf ("\n"); printf (_("\ Input file location:\n")); printf (_("\ -i, --input=INPUTFILE input PO file\n")); printf (_("\ -D, --directory=DIRECTORY add DIRECTORY to list for input files search\n")); printf (_("\ If no input file is given or if it is -, standard input is read.\n")); printf ("\n"); printf (_("\ Output file location:\n")); printf (_("\ -o, --output-file=FILE write output to specified file\n")); printf (_("\ The results are written to standard output if no output file is specified\n\ or if it is -.\n")); printf ("\n"); printf (_("\ The FILTER can be any program that reads a translation from standard input\n\ and writes a modified translation to standard output.\n\ ")); printf ("\n"); printf (_("\ Filter input and output:\n")); printf (_("\ --newline add a newline at the end of input and\n\ remove a newline from the end of output")); printf ("\n"); printf (_("\ Useful FILTER-OPTIONs when the FILTER is 'sed':\n")); printf (_("\ -e, --expression=SCRIPT add SCRIPT to the commands to be executed\n")); printf (_("\ -f, --file=SCRIPTFILE add the contents of SCRIPTFILE to the commands\n\ to be executed\n")); printf (_("\ -n, --quiet, --silent suppress automatic printing of pattern space\n")); printf ("\n"); printf (_("\ Input file syntax:\n")); printf (_("\ -P, --properties-input input file is in Java .properties syntax\n")); printf (_("\ --stringtable-input input file is in NeXTstep/GNUstep .strings syntax\n")); printf ("\n"); printf (_("\ Output details:\n")); printf (_("\ --color use colors and other text attributes always\n\ --color=WHEN use colors and other text attributes if WHEN.\n\ WHEN may be 'always', 'never', 'auto', or 'html'.\n")); printf (_("\ --style=STYLEFILE specify CSS style rule file for --color\n")); printf (_("\ --no-escape do not use C escapes in output (default)\n")); printf (_("\ -E, --escape use C escapes in output, no extended chars\n")); printf (_("\ --force-po write PO file even if empty\n")); printf (_("\ --indent indented output style\n")); printf (_("\ --keep-header keep header entry unmodified, don't filter it\n")); printf (_("\ --no-location suppress '#: filename:line' lines\n")); printf (_("\ -n, --add-location preserve '#: filename:line' lines (default)\n")); printf (_("\ --strict strict Uniforum output style\n")); printf (_("\ -p, --properties-output write out a Java .properties file\n")); printf (_("\ --stringtable-output write out a NeXTstep/GNUstep .strings file\n")); printf (_("\ -w, --width=NUMBER set output page width\n")); printf (_("\ --no-wrap do not break long message lines, longer than\n\ the output page width, into several lines\n")); printf (_("\ -s, --sort-output generate sorted output\n")); printf (_("\ -F, --sort-by-file sort output by file location\n")); printf ("\n"); printf (_("\ Informative output:\n")); printf (_("\ -h, --help display this help and exit\n")); printf (_("\ -V, --version output version information and exit\n")); printf ("\n"); /* TRANSLATORS: The placeholder indicates the bug-reporting address for this package. Please add _another line_ saying "Report translation bugs to <...>\n" with the address for translation bugs (typically your translation team's web or email address). */ fputs (_("Report bugs to .\n"), stdout); } exit (status); } /* Callbacks called from pipe_filter_ii_execute. */ struct locals { /* String being written. */ const char *str; size_t len; /* String being read and accumulated. */ char *result; size_t allocated; size_t length; }; static const void * prepare_write (size_t *num_bytes_p, void *private_data) { struct locals *l = (struct locals *) private_data; if (l->len > 0) { *num_bytes_p = l->len; return l->str; } else return NULL; } static void done_write (void *data_written, size_t num_bytes_written, void *private_data) { struct locals *l = (struct locals *) private_data; l->str += num_bytes_written; l->len -= num_bytes_written; } static void * prepare_read (size_t *num_bytes_p, void *private_data) { struct locals *l = (struct locals *) private_data; if (l->length == l->allocated) { l->allocated = l->allocated + (l->allocated >> 1) + 1; l->result = (char *) xrealloc (l->result, l->allocated); } *num_bytes_p = l->allocated - l->length; return l->result + l->length; } static void done_read (void *data_read, size_t num_bytes_read, void *private_data) { struct locals *l = (struct locals *) private_data; l->length += num_bytes_read; } /* Process a string STR of size LEN bytes through the subprogram. Store the freshly allocated result at *RESULTP and its length at *LENGTHP. */ static void generic_filter (const char *str, size_t len, char **resultp, size_t *lengthp) { struct locals l; l.str = str; l.len = len; l.allocated = len + (len >> 2) + 1; l.result = XNMALLOC (l.allocated, char); l.length = 0; pipe_filter_ii_execute (sub_name, sub_path, sub_argv, false, true, prepare_write, done_write, prepare_read, done_read, &l); *resultp = l.result; *lengthp = l.length; } /* Process a string STR of size LEN bytes, then remove NUL bytes. Store the freshly allocated result at *RESULTP and its length at *LENGTHP. */ static void process_string (const char *str, size_t len, char **resultp, size_t *lengthp) { char *result; size_t length; filter (str, len, &result, &length); /* Remove NUL bytes from result. */ { char *p = result; char *pend = result + length; for (; p < pend; p++) if (*p == '\0') { char *q; q = p; for (; p < pend; p++) if (*p != '\0') *q++ = *p; length = q - result; break; } } *resultp = result; *lengthp = length; } /* Do the same thing as process_string but append a newline to STR before processing, and remove a newline from the result. */ static void process_string_with_newline (const char *str, size_t len, char **resultp, size_t *lengthp) { char *newstr; char *result; size_t length; newstr = XNMALLOC (len + 1, char); memcpy (newstr, str, len); newstr[len] = '\n'; process_string (newstr, len + 1, &result, &length); free (newstr); if (length > 0 && result[length - 1] == '\n') result[--length] = '\0'; else error (0, 0, _("filter output is not terminated with a newline")); *resultp = result; *lengthp = length; } static void process_message (message_ty *mp) { const char *msgstr = mp->msgstr; size_t msgstr_len = mp->msgstr_len; char *location; size_t nsubstrings; char **substrings; size_t total_len; char *total_str; const char *p; char *q; size_t k; /* Keep the header entry unmodified, if --keep-header was given. */ if (is_header (mp) && keep_header) return; /* Set environment variables for the subprocess. Note: These environment variables, especially MSGFILTER_MSGCTXT and MSGFILTER_MSGID, may contain non-ASCII characters. The subprocess may not interpret these values correctly if the locale encoding is different from the PO file's encoding. We want about this situation, above. On Unix, this problem is often harmless. On Windows, however, - both native Windows and Cygwin - the values of environment variables *must* be in the encoding that is the value of GetACP(), because the system may convert the environment from char** to wchar_t** before spawning the subprocess and back from wchar_t** to char** in the subprocess, and it does so using the GetACP() codepage. */ if (mp->msgctxt != NULL) xsetenv ("MSGFILTER_MSGCTXT", mp->msgctxt, 1); else unsetenv ("MSGFILTER_MSGCTXT"); xsetenv ("MSGFILTER_MSGID", mp->msgid, 1); if (mp->msgid_plural != NULL) xsetenv ("MSGFILTER_MSGID_PLURAL", mp->msgid_plural, 1); else unsetenv ("MSGFILTER_MSGID_PLURAL"); location = xasprintf ("%s:%ld", mp->pos.file_name, (long) mp->pos.line_number); xsetenv ("MSGFILTER_LOCATION", location, 1); free (location); if (mp->prev_msgctxt != NULL) xsetenv ("MSGFILTER_PREV_MSGCTXT", mp->prev_msgctxt, 1); else unsetenv ("MSGFILTER_PREV_MSGCTXT"); if (mp->prev_msgid != NULL) xsetenv ("MSGFILTER_PREV_MSGID", mp->prev_msgid, 1); else unsetenv ("MSGFILTER_PREV_MSGID"); if (mp->prev_msgid_plural != NULL) xsetenv ("MSGFILTER_PREV_MSGID_PLURAL", mp->prev_msgid_plural, 1); else unsetenv ("MSGFILTER_PREV_MSGID_PLURAL"); /* Count NUL delimited substrings. */ for (p = msgstr, nsubstrings = 0; p < msgstr + msgstr_len; p += strlen (p) + 1, nsubstrings++); /* Process each NUL delimited substring separately. */ substrings = XNMALLOC (nsubstrings, char *); for (p = msgstr, k = 0, total_len = 0; k < nsubstrings; k++) { char *result; size_t length; if (mp->msgid_plural != NULL) { char *plural_form_string = xasprintf ("%zu", k); xsetenv ("MSGFILTER_PLURAL_FORM", plural_form_string, 1); free (plural_form_string); } else unsetenv ("MSGFILTER_PLURAL_FORM"); if (newline) process_string_with_newline (p, strlen (p), &result, &length); else process_string (p, strlen (p), &result, &length); result = (char *) xrealloc (result, length + 1); result[length] = '\0'; substrings[k] = result; total_len += length + 1; p += strlen (p) + 1; } /* Concatenate the results, including the NUL after each. */ total_str = XNMALLOC (total_len, char); for (k = 0, q = total_str; k < nsubstrings; k++) { size_t length = strlen (substrings[k]); memcpy (q, substrings[k], length + 1); free (substrings[k]); q += length + 1; } free (substrings); mp->msgstr = total_str; mp->msgstr_len = total_len; } static void process_message_list (message_list_ty *mlp) { size_t j; for (j = 0; j < mlp->nitems; j++) process_message (mlp->item[j]); } static msgdomain_list_ty * process_msgdomain_list (msgdomain_list_ty *mdlp) { size_t k; for (k = 0; k < mdlp->nitems; k++) process_message_list (mdlp->item[k]->messages); return mdlp; }