1 /* xgettext Ruby backend.
2    Copyright (C) 2020 Free Software Foundation, Inc.
3    Written by Bruno Haible <bruno@clisp.org>, 2020.
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 #ifdef HAVE_CONFIG_H
19 # include "config.h"
20 #endif
21 
22 /* Specification.  */
23 #include "x-ruby.h"
24 
25 #include <errno.h>
26 #include <stdbool.h>
27 #include <stdio.h>
28 #include <stdlib.h>
29 #include <string.h>
30 
31 #include "message.h"
32 #include "sh-quote.h"
33 #include "spawn-pipe.h"
34 #include "wait-process.h"
35 #include "xvasprintf.h"
36 #include "x-po.h"
37 #include "xgettext.h"
38 #include "xg-message.h"
39 #include "c-strstr.h"
40 #include "read-catalog-abstract.h"
41 #include "error.h"
42 #include "gettext.h"
43 
44 /* A convenience macro.  I don't like writing gettext() every time.  */
45 #define _(str) gettext (str)
46 
47 /* The Ruby syntax is defined in
48    https://ruby-doc.org/core-2.7.1/doc/syntax_rdoc.html
49    https://ruby-doc.org/core-2.7.1/doc/syntax/comments_rdoc.html
50    https://ruby-doc.org/core-2.7.1/doc/syntax/literals_rdoc.html
51    We don't parse Ruby directly, but instead rely on the 'rxgettext' program
52    from https://github.com/ruby-gettext/gettext .  */
53 
54 
55 /* ====================== Keyword set customization.  ====================== */
56 
57 /* This function currently has no effect.  */
58 void
x_ruby_extract_all(void)59 x_ruby_extract_all (void)
60 {
61 }
62 
63 /* This function currently has no effect.  */
64 void
x_ruby_keyword(const char * keyword)65 x_ruby_keyword (const char *keyword)
66 {
67 }
68 
69 /* This function currently has no effect.  */
70 void
init_flag_table_ruby(void)71 init_flag_table_ruby (void)
72 {
73 }
74 
75 
76 /* ========================= Extracting strings.  ========================== */
77 
78 void
extract_ruby(const char * real_filename,const char * logical_filename,flag_context_list_table_ty * flag_table,msgdomain_list_ty * mdlp)79 extract_ruby (const char *real_filename, const char *logical_filename,
80               flag_context_list_table_ty *flag_table,
81               msgdomain_list_ty *mdlp)
82 {
83   const char *progname = "rxgettext";
84   char *dummy_filename;
85   msgdomain_list_ty *mdlp2;
86   int pass;
87 
88   dummy_filename = xasprintf (_("(output from '%s')"), progname);
89 
90   /* Invoke rgettext twice:
91      1. to get the messages, without ruby-format flags.
92      2. to get the 'xgettext:' comments that guide us while adding
93         [no-]ruby-format flags.  */
94   mdlp2 = msgdomain_list_alloc (true);
95   for (pass = 0; pass < 2; pass++)
96     {
97       char *argv[4];
98       unsigned int i;
99       pid_t child;
100       int fd[1];
101       FILE *fp;
102       int exitstatus;
103 
104       /* Prepare arguments.  */
105       argv[0] = (char *) progname;
106       i = 1;
107 
108       if (pass > 0)
109         argv[i++] = (char *) "--add-comments=xgettext:";
110       else
111         {
112           if (add_all_comments)
113             argv[i++] = (char *) "--add-comments";
114           else if (comment_tag != NULL)
115             argv[i++] = xasprintf ("--add-comments=%s", comment_tag);
116         }
117 
118       argv[i++] = (char *) real_filename;
119 
120       argv[i] = NULL;
121 
122       if (verbose)
123         {
124           char *command = shell_quote_argv (argv);
125           error (0, 0, "%s", command);
126           free (command);
127         }
128 
129       child = create_pipe_in (progname, progname, argv,
130                               DEV_NULL, false, true, true, fd);
131 
132       fp = fdopen (fd[0], "r");
133       if (fp == NULL)
134         error (EXIT_FAILURE, errno, _("fdopen() failed"));
135 
136       /* Read the resulting PO file.  */
137       extract_po (fp, dummy_filename, dummy_filename, flag_table,
138                   pass == 0 ? mdlp : mdlp2);
139 
140       fclose (fp);
141 
142       /* Remove zombie process from process list, and retrieve exit status.  */
143       exitstatus =
144         wait_subprocess (child, progname, false, false, true, true, NULL);
145       if (exitstatus != 0)
146         error (EXIT_FAILURE, 0, _("%s subprocess failed with exit code %d"),
147                progname, exitstatus);
148     }
149 
150   /* Add [no-]ruby-format flags and process 'xgettext:' comments.
151      This processing is similar to the one done in remember_a_message().  */
152   if (mdlp->nitems == 1 && mdlp2->nitems == 1)
153     {
154       message_list_ty *mlp = mdlp->item[0]->messages;
155       message_list_ty *mlp2 = mdlp2->item[0]->messages;
156       size_t j;
157 
158       for (j = 0; j < mlp->nitems; j++)
159         {
160           message_ty *mp = mlp->item[j];
161 
162           if (!is_header (mp))
163             {
164               /* Find 'xgettext:' comments and apply them to mp.  */
165               message_ty *mp2 =
166                 message_list_search (mlp2, mp->msgctxt, mp->msgid);
167 
168               if (mp2 != NULL && mp2->comment_dot != NULL)
169                 {
170                   string_list_ty *mp2_comment_dot = mp2->comment_dot;
171                   size_t k;
172 
173                   for (k = 0; k < mp2_comment_dot->nitems; k++)
174                     {
175                       const char *s = mp2_comment_dot->item[k];
176 
177                       /* To reduce the possibility of unwanted matches we do a
178                          two step match: the line must contain 'xgettext:' and
179                          one of the possible format description strings.  */
180                       const char *t = c_strstr (s, "xgettext:");
181                       if (t != NULL)
182                         {
183                           bool tmp_fuzzy;
184                           enum is_format tmp_format[NFORMATS];
185                           struct argument_range tmp_range;
186                           enum is_wrap tmp_wrap;
187                           enum is_syntax_check tmp_syntax_check[NSYNTAXCHECKS];
188                           bool interesting;
189                           size_t i;
190 
191                           t += strlen ("xgettext:");
192 
193                           po_parse_comment_special (t, &tmp_fuzzy, tmp_format,
194                                                     &tmp_range, &tmp_wrap,
195                                                     tmp_syntax_check);
196 
197                           interesting = false;
198                           for (i = 0; i < NFORMATS; i++)
199                             if (tmp_format[i] != undecided)
200                               {
201                                 mp->is_format[i] = tmp_format[i];
202                                 interesting = true;
203                               }
204                           if (has_range_p (tmp_range))
205                             {
206                               intersect_range (mp, &tmp_range);
207                               interesting = true;
208                             }
209                           if (tmp_wrap != undecided)
210                             {
211                               mp->do_wrap = tmp_wrap;
212                               interesting = true;
213                             }
214                           for (i = 0; i < NSYNTAXCHECKS; i++)
215                             if (tmp_syntax_check[i] != undecided)
216                               {
217                                 mp->do_syntax_check[i] = tmp_syntax_check[i];
218                                 interesting = true;
219                               }
220 
221                           /* If the "xgettext:" marker was followed by an
222                              interesting keyword, and we updated our
223                              is_format/do_wrap variables, eliminate the comment
224                              from the #. comments.  */
225                           if (interesting)
226                             if (mp->comment_dot != NULL)
227                               {
228                                 const char *removed =
229                                   string_list_remove (mp->comment_dot, s);
230 
231                                 if (removed != NULL)
232                                   free ((char *) removed);
233                               }
234                         }
235                     }
236                 }
237 
238               /* Now evaluate the consequences of the 'xgettext:' comments,  */
239               decide_is_format (mp);
240               decide_do_wrap (mp);
241               decide_syntax_check (mp);
242             }
243         }
244     }
245 
246   msgdomain_list_free (mdlp2);
247 
248   free (dummy_filename);
249 }
250