| /* Pass translations to a subprocess. |
| Copyright (C) 2001-2020 Free Software Foundation, Inc. |
| Written by Bruno Haible <haible@clisp.cons.org>, 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 <https://www.gnu.org/licenses/>. */ |
| |
| |
| #ifdef HAVE_CONFIG_H |
| # include "config.h" |
| #endif |
| |
| #include <errno.h> |
| #include <getopt.h> |
| #include <limits.h> |
| #include <locale.h> |
| #include <signal.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <sys/types.h> |
| #include <unistd.h> |
| |
| #include "noreturn.h" |
| #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-lgpl.h" |
| #include "message.h" |
| #include "read-catalog.h" |
| #include "read-po.h" |
| #include "read-properties.h" |
| #include "read-stringtable.h" |
| #include "msgl-charset.h" |
| #include "xalloc.h" |
| #include "full-write.h" |
| #include "findprog.h" |
| #include "spawn-pipe.h" |
| #include "wait-process.h" |
| #include "xsetenv.h" |
| #include "propername.h" |
| #include "gettext.h" |
| |
| #define _(str) gettext (str) |
| |
| #ifndef STDOUT_FILENO |
| # define STDOUT_FILENO 1 |
| #endif |
| |
| |
| /* 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; |
| |
| /* Maximum exit code encountered. */ |
| static int exitcode; |
| |
| /* Long options. */ |
| static const struct option long_options[] = |
| { |
| { "directory", required_argument, NULL, 'D' }, |
| { "help", no_argument, NULL, 'h' }, |
| { "input", required_argument, NULL, 'i' }, |
| { "newline", no_argument, NULL, CHAR_MAX + 2 }, |
| { "properties-input", no_argument, NULL, 'P' }, |
| { "stringtable-input", no_argument, NULL, CHAR_MAX + 1 }, |
| { "version", no_argument, NULL, 'V' }, |
| { NULL, 0, NULL, 0 } |
| }; |
| |
| |
| /* Forward declaration of local functions. */ |
| _GL_NORETURN_FUNC static void usage (int status); |
| static void process_msgdomain_list (const msgdomain_list_ty *mdlp); |
| |
| |
| int |
| main (int argc, char **argv) |
| { |
| int opt; |
| bool do_help; |
| bool do_version; |
| const char *input_file; |
| msgdomain_list_ty *result; |
| catalog_input_format_ty input_syntax = &input_format_po; |
| size_t i; |
| |
| /* Set program name for messages. */ |
| set_program_name (argv[0]); |
| error_print_progname = maybe_print_progname; |
| |
| /* Set locale via LC_ALL. */ |
| setlocale (LC_ALL, ""); |
| |
| /* 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; |
| 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:hi:PV", long_options, NULL)) |
| != EOF) |
| switch (opt) |
| { |
| case '\0': /* Long option. */ |
| break; |
| |
| case 'D': |
| dir_list_append (optarg); |
| 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 'P': |
| input_syntax = &input_format_properties; |
| break; |
| |
| case 'V': |
| do_version = true; |
| break; |
| |
| case CHAR_MAX + 1: /* --stringtable-input */ |
| input_syntax = &input_format_stringtable; |
| break; |
| |
| case CHAR_MAX + 2: /* --newline */ |
| newline = true; |
| break; |
| |
| default: |
| usage (EXIT_FAILURE); |
| break; |
| } |
| |
| /* Version information is requested. */ |
| if (do_version) |
| { |
| printf ("%s (GNU %s) %s\n", last_component (program_name), |
| PACKAGE, VERSION); |
| /* xgettext: no-wrap */ |
| printf (_("Copyright (C) %s Free Software Foundation, Inc.\n\ |
| License GPLv3+: GNU GPL version 3 or later <%s>\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-2020", "https://gnu.org/licenses/gpl.html"); |
| 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 command name")); |
| sub_name = argv[optind]; |
| |
| /* 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; |
| |
| /* By default, input comes from standard input. */ |
| if (input_file == NULL) |
| input_file = "-"; |
| |
| /* Read input file. */ |
| result = read_catalog_file (input_file, input_syntax); |
| |
| if (strcmp (sub_name, "0") != 0) |
| { |
| /* Warn if the current locale is not suitable for this PO file. */ |
| compare_po_locale_charsets (result); |
| |
| /* Block SIGPIPE for this process and for the subprocesses. |
| The subprogram may have side effects (additionally to producing some |
| output), therefore if there are no readers on stdout, processing of the |
| strings must continue nevertheless. */ |
| { |
| sigset_t sigpipe_set; |
| |
| sigemptyset (&sigpipe_set); |
| sigaddset (&sigpipe_set, SIGPIPE); |
| sigprocmask (SIG_UNBLOCK, &sigpipe_set, NULL); |
| } |
| |
| /* 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; |
| } |
| |
| exitcode = 0; /* = EXIT_SUCCESS */ |
| |
| /* Apply the subprogram. */ |
| process_msgdomain_list (result); |
| |
| exit (exitcode); |
| } |
| |
| |
| /* 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] COMMAND [COMMAND-OPTION]\n\ |
| "), program_name); |
| printf ("\n"); |
| /* xgettext: no-wrap */ |
| printf (_("\ |
| Applies a command to all translations of a translation catalog.\n\ |
| The COMMAND can be any program that reads a translation from standard\n\ |
| input. It is invoked once for each translation. Its output becomes\n\ |
| msgexec's output. msgexec's return code is the maximum return code\n\ |
| across all invocations.\n\ |
| ")); |
| printf ("\n"); |
| /* xgettext: no-wrap */ |
| printf (_("\ |
| A special builtin command called '0' outputs the translation, followed by a\n\ |
| null byte. The output of \"msgexec 0\" is suitable as input for \"xargs -0\".\n\ |
| ")); |
| printf ("\n"); |
| printf (_("\ |
| Command input:\n")); |
| printf (_("\ |
| --newline add newline at the end of input\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 (_("\ |
| 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 (_("\ |
| 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 first placeholder is the web address of the Savannah |
| project of this package. The second placeholder is the bug-reporting |
| email 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). */ |
| printf(_("\ |
| Report bugs in the bug tracker at <%s>\n\ |
| or by email to <%s>.\n"), |
| "https://savannah.gnu.org/projects/gettext", |
| "bug-gettext@gnu.org"); |
| } |
| |
| exit (status); |
| } |
| |
| |
| #ifdef EINTR |
| |
| /* EINTR handling for close(). |
| These functions can return -1/EINTR even though we don't have any |
| signal handlers set up, namely when we get interrupted via SIGSTOP. */ |
| |
| static inline int |
| nonintr_close (int fd) |
| { |
| int retval; |
| |
| do |
| retval = close (fd); |
| while (retval < 0 && errno == EINTR); |
| |
| return retval; |
| } |
| #undef close |
| #define close nonintr_close |
| |
| #endif |
| |
| |
| /* Pipe a string STR of size LEN bytes to the subprogram. |
| The byte after STR is known to be a '\0' byte. */ |
| static void |
| process_string (const message_ty *mp, const char *str, size_t len) |
| { |
| if (strcmp (sub_name, "0") == 0) |
| { |
| /* Built-in command "0". */ |
| if (full_write (STDOUT_FILENO, str, len + 1) < len + 1) |
| error (EXIT_FAILURE, errno, _("write to stdout failed")); |
| } |
| else |
| { |
| /* General command. */ |
| char *location; |
| pid_t child; |
| int fd[1]; |
| void (*orig_sigpipe_handler)(int); |
| int exitstatus; |
| char *newstr; |
| |
| /* Set environment variables for the subprocess. |
| Note: These environment variables, especially MSGEXEC_MSGCTXT and |
| MSGEXEC_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 warned 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 ("MSGEXEC_MSGCTXT", mp->msgctxt, 1); |
| else |
| unsetenv ("MSGEXEC_MSGCTXT"); |
| xsetenv ("MSGEXEC_MSGID", mp->msgid, 1); |
| if (mp->msgid_plural != NULL) |
| xsetenv ("MSGEXEC_MSGID_PLURAL", mp->msgid_plural, 1); |
| else |
| unsetenv ("MSGEXEC_MSGID_PLURAL"); |
| location = xasprintf ("%s:%ld", mp->pos.file_name, |
| (long) mp->pos.line_number); |
| xsetenv ("MSGEXEC_LOCATION", location, 1); |
| free (location); |
| if (mp->prev_msgctxt != NULL) |
| xsetenv ("MSGEXEC_PREV_MSGCTXT", mp->prev_msgctxt, 1); |
| else |
| unsetenv ("MSGEXEC_PREV_MSGCTXT"); |
| if (mp->prev_msgid != NULL) |
| xsetenv ("MSGEXEC_PREV_MSGID", mp->prev_msgid, 1); |
| else |
| unsetenv ("MSGEXEC_PREV_MSGID"); |
| if (mp->prev_msgid_plural != NULL) |
| xsetenv ("MSGEXEC_PREV_MSGID_PLURAL", mp->prev_msgid_plural, 1); |
| else |
| unsetenv ("MSGEXEC_PREV_MSGID_PLURAL"); |
| |
| /* Open a pipe to a subprocess. */ |
| child = create_pipe_out (sub_name, sub_path, sub_argv, NULL, |
| NULL, false, true, true, fd); |
| |
| /* Ignore SIGPIPE here. We don't care if the subprocesses terminates |
| successfully without having read all of the input that we feed it. */ |
| orig_sigpipe_handler = signal (SIGPIPE, SIG_IGN); |
| |
| if (newline) |
| { |
| newstr = XNMALLOC (len + 1, char); |
| memcpy (newstr, str, len); |
| newstr[len++] = '\n'; |
| } |
| else |
| newstr = (char *) str; |
| |
| if (full_write (fd[0], newstr, len) < len) |
| if (errno != EPIPE) |
| error (EXIT_FAILURE, errno, |
| _("write to %s subprocess failed"), sub_name); |
| |
| if (newstr != str) |
| free (newstr); |
| |
| close (fd[0]); |
| |
| signal (SIGPIPE, orig_sigpipe_handler); |
| |
| /* Remove zombie process from process list, and retrieve exit status. */ |
| /* FIXME: Should ignore_sigpipe be set to true here? It depends on the |
| semantics of the subprogram... */ |
| exitstatus = |
| wait_subprocess (child, sub_name, false, false, true, true, NULL); |
| if (exitcode < exitstatus) |
| exitcode = exitstatus; |
| } |
| } |
| |
| |
| static void |
| process_message (const message_ty *mp) |
| { |
| const char *msgstr = mp->msgstr; |
| size_t msgstr_len = mp->msgstr_len; |
| const char *p; |
| size_t k; |
| |
| /* Process each NUL delimited substring separately. */ |
| for (p = msgstr, k = 0; p < msgstr + msgstr_len; k++) |
| { |
| size_t length = strlen (p); |
| |
| if (mp->msgid_plural != NULL) |
| { |
| char *plural_form_string = xasprintf ("%lu", (unsigned long) k); |
| |
| xsetenv ("MSGEXEC_PLURAL_FORM", plural_form_string, 1); |
| free (plural_form_string); |
| } |
| else |
| unsetenv ("MSGEXEC_PLURAL_FORM"); |
| process_string (mp, p, length); |
| |
| p += length + 1; |
| } |
| } |
| |
| |
| static void |
| process_message_list (const message_list_ty *mlp) |
| { |
| size_t j; |
| |
| for (j = 0; j < mlp->nitems; j++) |
| process_message (mlp->item[j]); |
| } |
| |
| |
| static void |
| process_msgdomain_list (const msgdomain_list_ty *mdlp) |
| { |
| size_t k; |
| |
| for (k = 0; k < mdlp->nitems; k++) |
| process_message_list (mdlp->item[k]->messages); |
| } |