1 /* poke.c - Interactive editor for binary files.  */
2 
3 /* Copyright (C) 2019, 2020, 2021 Jose E. Marchesi */
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 <http://www.gnu.org/licenses/>.
17  */
18 
19 #include <config.h>
20 #include <progname.h>
21 #include <getopt.h>
22 #include <stdlib.h>
23 #include <stdio.h>
24 #include <unistd.h>
25 #include <string.h>
26 #include <locale.h>
27 #include <textstyle.h>
28 #include "xalloc.h"
29 #include <assert.h>
30 
31 #ifdef HAVE_HSERVER
32 #  include "pk-hserver.h"
33 #endif
34 
35 #include "poke.h"
36 #include "pk-cmd.h"
37 #include "pk-repl.h"
38 #include "pk-utils.h"
39 #include "pk-mi.h"
40 #include "pk-map.h"
41 #include "pk-ios.h"
42 
43 /* poke can be run either interactively (from a tty) or in batch mode.
44    The following predicate records this.  */
45 
46 int poke_interactive_p;
47 
48 #if HAVE_HSERVER
49 /* The following global indicates whether the hyperserver is activated
50    or not.  */
51 int poke_hserver_p;
52 #endif
53 
54 /* The following global indicates whether the MI shall be used.  */
55 int poke_mi_p;
56 
57 #ifdef POKE_MI
58 /* This build of poke supports mi if the user wants it.  */
59 static const int mi_supported_p = 1;
60 #else
61 /* This build of poke does not support mi.  */
62 static const int mi_supported_p = 0;
pk_mi(void)63 int pk_mi (void) {assert (0); return 0;}
64 #endif
65 
66 /* The following global indicates whether poke should be as terse as
67    possible in its output.  This is useful when running poke from
68    other programs.  */
69 
70 int poke_quiet_p;
71 
72 /* The following global contains the directory holding the program's
73    architecture independent files, such as scripts.  */
74 
75 char *poke_datadir;
76 
77 /* The following global contains the directory holding the program's
78    info files.  */
79 
80 char *poke_infodir;
81 
82 /* The following global contains the directory holding pickles shipped
83    with poke.  In an installed program, this is the same than
84    poke_datadir/pickles, but the POKE_PICKLESDIR environment variable
85    can be set to a different value, which is mainly to run an
86    uninstalled poke.  */
87 
88 char *poke_picklesdir;
89 
90 /* The following global contains the directory holding the standard
91    map files.  In an installed poke, this is the same than
92    poke_datadir/maps, but the POKE_MAPSDIR environment variable can be
93    set to a different value, which is mainly to run an uinstalled
94    poke.  */
95 
96 char *poke_mapsdir;
97 
98 /* The following global contains the directory holding the help
99    support files.  In an installed poke, this is the same than
100    poke_datadir, but the POKE_DOCDIR environment variable can be set
101    to a different value, which is mainly to run an uninstalled
102    poke.  */
103 
104 char *poke_docdir;
105 
106 /* The following global contains the name of the program to use to
107    display documentation.  Valid values are `info' and `less'.  It
108    defaults to `info'.  */
109 
110 char *poke_doc_viewer = NULL;
111 
112 /* The following global determines whether auto-maps shall be
113    acknowleged when loading files or not.  Defaults to `yes'.  */
114 
115 int poke_auto_map_p = 1;
116 
117 /* The following global determines whether the user specified that the
118    hyperlinks server should not be activated, in the command line.  */
119 
120 int poke_no_hserver_arg = 0;
121 
122 /* The following global determines whether map information shall be
123    included in the REPL prompt.  Defaults to `yes'.  */
124 
125 int poke_prompt_maps_p = 1;
126 
127 /* This is used by commands to indicate to the REPL that it must
128    exit.  */
129 
130 int poke_exit_p;
131 int poke_exit_code;
132 
133 /* The following global is the poke compiler.  */
134 pk_compiler poke_compiler;
135 
136 /* The following global indicates whether to load a user
137    initialization file.  It defaults to 1.  */
138 
139 int poke_load_init_file = 1;
140 
141 /* Command line options management.  */
142 
143 enum
144 {
145   HELP_ARG,
146   VERSION_ARG,
147   QUIET_ARG,
148   LOAD_ARG,
149   LOAD_AND_EXIT_ARG,
150   CMD_ARG,
151   NO_INIT_FILE_ARG,
152   SOURCE_ARG,
153   COLOR_ARG,
154   STYLE_ARG,
155   MI_ARG,
156   NO_AUTO_MAP_ARG,
157   NO_HSERVER_ARG
158 };
159 
160 static const struct option long_options[] =
161 {
162   {"help", no_argument, NULL, HELP_ARG},
163   {"version", no_argument, NULL, VERSION_ARG},
164   {"quiet", no_argument, NULL, QUIET_ARG},
165   {"load", required_argument, NULL, LOAD_ARG},
166   {"command", required_argument, NULL, CMD_ARG},
167   {"source", required_argument, NULL, SOURCE_ARG},
168   {"no-init-file", no_argument, NULL, NO_INIT_FILE_ARG},
169   {"color", required_argument, NULL, COLOR_ARG},
170   {"style", required_argument, NULL, STYLE_ARG},
171   {"mi", no_argument, NULL, MI_ARG},
172   {"no-auto-map", no_argument, NULL, NO_AUTO_MAP_ARG},
173   {"no-hserver", no_argument, NULL, NO_HSERVER_ARG},
174   {NULL, 0, NULL, 0},
175 };
176 
177 static void
print_help(void)178 print_help (void)
179 {
180   /* TRANSLATORS: --help output, GNU poke synopsis.
181      no-wrap */
182   pk_puts (_("Usage: poke [OPTION]... [FILE]\n"));
183 
184   /* TRANSLATORS: --help output, GNU poke summary.
185      no-wrap */
186   pk_puts (_("Interactive editor for binary files.\n"));
187 
188   pk_puts ("\n");
189   /* TRANSLATORS: --help output, GNU poke arguments.
190      no-wrap */
191   pk_puts (_("  -l, --load=FILE                     load the given pickle at startup\n"));
192   pk_puts (_("  -L FILE                             load the given pickle and exit\n"));
193 
194   pk_puts ("\n");
195 
196   /* TRANSLATORS: --help output, GNU poke arguments.
197      no-wrap */
198   pk_puts (_("Commanding poke from the command line:\n"));
199   pk_puts (_("  -c, --command=CMD                   execute the given command\n"));
200   pk_puts (_("  -s, --source=FILE                   execute commands from FILE\n"));
201 
202   pk_puts ("\n");
203   pk_puts (_("Styling text output:\n"));
204   pk_puts (_("      --color=(yes|no|auto|html|test) emit styled output\n"));
205   pk_puts (_("      --style=STYLE_FILE              style file to use when styling\n"));
206 
207   pk_puts ("\n");
208   pk_puts (_("Machine interface:\n"));
209   pk_puts (_("      --mi                            use the MI in stdin/stdout\n"));
210 
211   pk_puts ("\n");
212   /* TRANSLATORS: --help output, less used GNU poke arguments.
213      no-wrap */
214   pk_puts (_("  -q, --no-init-file                  do not load an init file\n"));
215   pk_puts (_("      --no-auto-map                   disable auto-map\n"));
216 #if HAVE_HSERVER
217   pk_puts (_("      --no-hserver                    do not run the hyperlinks server\n"));
218 #endif
219   pk_puts (_("      --quiet                         be as terse as possible\n"));
220   pk_puts (_("      --help                          print a help message and exit\n"));
221   pk_puts (_("      --version                       show version and exit\n"));
222 
223   pk_puts ("\n");
224   /* TRANSLATORS: --help output 5+ (reports)
225      TRANSLATORS: the placeholder indicates the bug-reporting address
226      for this application.  Please add _another line_ with the
227      address for translation bugs.
228      no-wrap */
229   pk_printf (_("\
230 Report bugs in the bug tracker at\n\
231   <%s>\n\
232   or by email to <%s>.\n"), PACKAGE_BUGZILLA, PACKAGE_BUGREPORT);
233 #ifdef PACKAGE_PACKAGER_BUG_REPORTS
234   printf (_("Report %s bugs to: %s\n"), PACKAGE_PACKAGER,
235           PACKAGE_PACKAGER_BUG_REPORTS);
236 #endif
237   pk_printf (_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
238   pk_printf (_("General help using GNU software: %s\n"), "<http://www.gnu.org/gethelp/>");
239 }
240 
241 void
pk_print_version(int hand_p)242 pk_print_version (int hand_p)
243 {
244   if (hand_p)
245     {
246       pk_term_class ("logo");
247       pk_puts ("     _____\n");
248       pk_puts (" ---'   __\\_______\n");
249       pk_printf ("            ______)  GNU poke %s\n", VERSION);
250       pk_puts ("            __)\n");
251       pk_puts ("           __)\n");
252       pk_puts (" ---._______)\n");
253       pk_term_end_class ("logo");
254       /* xgettext: no-wrap */
255       pk_puts ("\n");
256     }
257   else
258     pk_printf ("GNU poke %s\n\n", VERSION);
259 
260   /* It is important to separate the year from the rest of the message,
261      as done here, to avoid having to retranslate the message when a new
262      year comes around.  */
263   pk_term_class ("copyright");
264   /* TRANSLATORS:
265      If your target locale supports it, you can translate (C) to the
266      copyright symbol (U+00A9 in Unicode), but there is no obligation
267      to do this.  In other cases it's probably best to leave it untranslated.  */
268   pk_printf (_("\
269 %s (C) %s The poke authors.\n\
270 License GPLv3+: GNU GPL version 3 or later"), "Copyright", "2019-2021");
271   pk_term_hyperlink ("http://gnu.org/licenses/gpl.html", NULL);
272   pk_puts (" <http://gnu.org/licenses/gpl.html>");
273   pk_term_end_hyperlink ();
274   pk_puts (".\n\
275 This is free software: you are free to change and redistribute it.\n\
276 There is NO WARRANTY, to the extent permitted by law.\n");
277   pk_term_end_class ("copyright");
278 
279     pk_printf (_("\
280 \nPowered by Jitter %s."), JITTER_VERSION);
281 
282     pk_puts (_("\
283 \n\
284 Perpetrated by Jose E. Marchesi.\n"));
285 
286 }
287 
288 static void
finalize(void)289 finalize (void)
290 {
291 #ifdef HAVE_HSERVER
292   if (poke_hserver_p)
293     pk_hserver_shutdown ();
294 #endif
295   pk_cmd_shutdown ();
296   pk_map_shutdown ();
297   pk_compiler_free (poke_compiler);
298   pk_term_shutdown ();
299 }
300 
301 /* Callbacks for the terminal output in libpoke.  */
302 
303 static struct pk_term_if poke_term_if =
304   {
305     .flush_fn = pk_term_flush,
306     .puts_fn = pk_puts,
307     .printf_fn = pk_printf,
308     .indent_fn = pk_term_indent,
309     .class_fn = pk_term_class,
310     .end_class_fn = pk_term_end_class,
311     .hyperlink_fn = pk_term_hyperlink,
312     .end_hyperlink_fn = pk_term_end_hyperlink,
313     .get_color_fn = pk_term_get_color,
314     .set_color_fn = pk_term_set_color,
315     .get_bgcolor_fn = pk_term_get_bgcolor,
316     .set_bgcolor_fn = pk_term_set_bgcolor,
317   };
318 
319 static void
set_script_args(int argc,char * argv[])320 set_script_args (int argc, char *argv[])
321 {
322   int i, nargs;
323   uint64_t index;
324   pk_val argv_array;
325 
326   /* Look for -L SCRIPT */
327   for (i = 0; i < argc; ++i)
328     if (STREQ (argv[i], "-L"))
329       break;
330 
331   /* Any argument after SCRIPT is an argument for the script.  */
332   i = i + 2;
333   nargs = argc - i;
334   argv_array = pk_make_array (pk_make_uint (nargs, 64),
335                               pk_make_array_type (pk_make_string_type (),
336                                                   PK_NULL /* bound */));
337 
338   for (index = 0; i < argc; ++i, ++index)
339     pk_array_insert_elem (argv_array, index,
340                           pk_make_string (argv[i]));
341 
342   pk_defvar (poke_compiler, "argv", argv_array);
343 }
344 
345 static void
parse_args_1(int argc,char * argv[])346 parse_args_1 (int argc, char *argv[])
347 {
348   char c;
349   int ret;
350 
351   while ((ret = getopt_long (argc,
352                              argv,
353                              "ql:c:s:L:",
354                              long_options,
355                              NULL)) != -1)
356     {
357       c = ret;
358       switch (c)
359         {
360         case MI_ARG:
361           if (!mi_supported_p)
362             {
363               fputs (_("MI is not built into this instance of poke\n"),
364                      stderr);
365               exit (EXIT_FAILURE);
366             }
367           else
368             {
369               poke_mi_p = 1;
370             }
371           break;
372         case 'L':
373           poke_interactive_p = 0;
374           return;
375         case NO_AUTO_MAP_ARG:
376           poke_auto_map_p = 0;
377           break;
378         case NO_HSERVER_ARG:
379           poke_no_hserver_arg = 1;
380           break;
381         case 'q':
382         case NO_INIT_FILE_ARG:
383           poke_load_init_file = 0;
384           break;
385         default:
386           break;
387         }
388     }
389 }
390 
391 static void
parse_args_2(int argc,char * argv[])392 parse_args_2 (int argc, char *argv[])
393 {
394   char c;
395   int ret;
396 
397   optind = 1;
398   while ((ret = getopt_long (argc,
399                              argv,
400                              "ql:c:s:L:",
401                              long_options,
402                              NULL)) != -1)
403     {
404       c = ret;
405       switch (c)
406         {
407         case HELP_ARG:
408           print_help ();
409           goto exit_success;
410           break;
411         case VERSION_ARG:
412           pk_print_version (0 /* hand_p */);
413           goto exit_success;
414           break;
415         case QUIET_ARG:
416           poke_quiet_p = 1;
417           pk_set_quiet_p (poke_compiler, 1);
418           break;
419         case 'l':
420         case LOAD_ARG:
421           if (pk_compile_file (poke_compiler, optarg,
422                                NULL /* exit_status */) != PK_OK)
423             goto exit_success;
424           break;
425         case 'c':
426         case CMD_ARG:
427           {
428             poke_interactive_p = 0;
429             if (!pk_cmd_exec (optarg))
430               goto exit_failure;
431             break;
432           }
433         case 's':
434         case SOURCE_ARG:
435           {
436             if (!pk_cmd_exec_script (optarg))
437               goto exit_failure;
438             break;
439           }
440         case 'L':
441           {
442             int exit_status;
443 
444             /* Build argv in the compiler, with the rest of the
445                command-line arguments.  Then execute the script and
446                return.  */
447             set_script_args (argc, argv);
448             if (pk_compile_file (poke_compiler, optarg, &exit_status) != PK_OK)
449               goto exit_failure;
450 
451             finalize ();
452             exit (exit_status);
453             break;
454           }
455         case MI_ARG:
456         case NO_AUTO_MAP_ARG:
457         case NO_HSERVER_ARG:
458         case 'q':
459         case NO_INIT_FILE_ARG:
460           /* These are handled in parse_args_1.  */
461           break;
462           /* libtextstyle arguments are handled in pk-term.c, not
463              here.   */
464         case COLOR_ARG:
465         case STYLE_ARG:
466           break;
467         default:
468           goto exit_failure;
469         }
470     }
471 
472   if (optind < argc)
473     {
474       char *filename = argv[optind++];
475       int xxx = poke_auto_map_p;
476 
477       poke_auto_map_p = 0; /* XXX */
478       if (pk_open_ios (filename, 1 /* set_cur_p */) == PK_IOS_NOID)
479         {
480           if (!poke_quiet_p)
481             pk_printf (_("cannot open file %s\n"), filename);
482           goto exit_failure;
483         }
484       poke_auto_map_p = xxx; /* XXX */
485 
486       optind++;
487     }
488 
489   if (optind < argc)
490     {
491       print_help ();
492       goto exit_failure;
493     }
494 
495   return;
496 
497  exit_success:
498   finalize ();
499   exit (EXIT_SUCCESS);
500 
501  exit_failure:
502   finalize ();
503   exit (EXIT_FAILURE);
504 }
505 
506 static void
initialize(int argc,char * argv[])507 initialize (int argc, char *argv[])
508 {
509   /* This is used by the `progname' gnulib module.  */
510   set_program_name ("poke");
511 
512   /* i18n */
513   setlocale (LC_ALL, "");
514   bindtextdomain (PACKAGE, LOCALEDIR);
515   textdomain (PACKAGE);
516 
517   /* Determine the directory containing poke's scripts and other
518      architecture-independent data.  */
519   poke_datadir = getenv ("POKEDATADIR");
520   if (poke_datadir == NULL)
521     poke_datadir = PKGDATADIR;
522 
523   poke_picklesdir = getenv ("POKEPICKLESDIR");
524   if (poke_picklesdir == NULL)
525     {
526       poke_picklesdir = pk_str_concat (poke_datadir, "/pickles", NULL);
527       pk_assert_alloc (poke_picklesdir);
528     }
529 
530   poke_mapsdir = getenv ("POKEMAPSDIR");
531   if (poke_mapsdir == NULL)
532     {
533       poke_mapsdir = pk_str_concat (poke_datadir, "/maps", NULL);
534       pk_assert_alloc (poke_mapsdir);
535     }
536 
537   poke_docdir = getenv ("POKEDOCDIR");
538   if (poke_docdir == NULL)
539     poke_docdir = poke_datadir;
540 
541   poke_infodir = getenv ("POKEINFODIR");
542   if (poke_infodir == NULL)
543     poke_infodir = PKGINFODIR;
544 
545   /* Initialize the terminal output.  */
546   pk_term_init (argc, argv);
547 
548   /* Initialize the poke incremental compiler.  */
549   poke_compiler = pk_compiler_new (&poke_term_if);
550   if (poke_compiler == NULL)
551     pk_fatal ("creating the incremental compiler");
552 
553   /* Make poke_interactive_p available to poke programs.  */
554   if (pk_defvar (poke_compiler, "poke_interactive_p",
555                  pk_make_int (poke_interactive_p, 32)) == PK_ERROR)
556     pk_fatal ("defining poke_interactive_p");
557 
558   /* Load poke.pk  */
559   if (!pk_load (poke_compiler, "poke"))
560     pk_fatal ("unable to load the poke module");
561 
562 
563   /* Initialize the global map.  */
564   pk_map_init ();
565 
566   /* Initialize the command subsystem.  This should be done even if
567      called non-interactively.  */
568   pk_cmd_init ();
569 
570 #ifdef HAVE_HSERVER
571   poke_hserver_p = (poke_interactive_p
572                     && pk_term_color_p ()
573                     && !poke_mi_p
574                     && !poke_no_hserver_arg);
575 
576   /* Initialize and start the terminal hyperlinks server.  */
577   if (poke_hserver_p)
578     {
579       pk_hserver_init ();
580       pk_hserver_start ();
581     }
582 #endif
583 
584   /* Initialize the documentation viewer.  */
585   poke_doc_viewer = xstrdup ("info");
586 }
587 
588 static void
initialize_user(void)589 initialize_user (void)
590 {
591   /* Load the user's initialization file ~/.pokerc, if it exists in
592      the HOME directory.  */
593   char *homedir = getenv ("HOME");
594 
595   if (homedir != NULL)
596     {
597       char *pokerc = pk_str_concat (homedir, "/.pokerc", NULL);
598       pk_assert_alloc (pokerc);
599 
600       if (pk_file_readable (pokerc) == NULL)
601         {
602           if (!pk_cmd_exec_script (pokerc))
603             exit (EXIT_FAILURE);
604           else
605             return;
606         }
607 
608       free (pokerc);
609     }
610 
611   /* If no ~/.pokerc file was found, acknowledge the XDG Base
612      Directory Specification, as documented in
613      https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
614 
615      If the environment variable XDG_CONFIG_HOME if defined, try to
616      load the file $XDG_CONFIG_HOME/pokerc.conf.
617 
618      If the environment variable $XDG_CONFIG_DIRS is defined, try to
619      use all the : separated paths in that variable to find
620      pokerc.conf.  Else, try to load /etc/xdg/poke/pokerc.conf.  */
621   {
622     const char *xdg_config_home = getenv ("XDG_CONFIG_HOME");
623     const char *xdg_config_dirs = getenv ("XDG_CONFIG_DIRS");
624 
625     if (xdg_config_home == NULL)
626       xdg_config_home = "";
627 
628     if (xdg_config_dirs == NULL)
629       xdg_config_dirs = "/etc/xdg";
630 
631     char *config_path = pk_str_concat (xdg_config_dirs, ":", xdg_config_home, NULL);
632     pk_assert_alloc (config_path);
633 
634     char *dir = strtok (config_path, ":");
635     do
636       {
637         /* Ignore empty entries.  */
638         if (*dir != '\0')
639           {
640             /* Mount the full path and determine whether the resulting
641                file is readable. */
642             char *config_filename = pk_str_concat (dir, "/poke/pokerc.conf", NULL);
643             pk_assert_alloc (config_filename);
644 
645             if (pk_file_readable (config_filename) == NULL)
646               {
647                 /* Load the configuration file.  */
648                 int ret = pk_cmd_exec_script (config_filename);
649                 if (!ret)
650                   exit (EXIT_FAILURE);
651                 break;
652               }
653 
654             free (config_filename);
655           }
656       }
657     while ((dir = strtok (NULL, ":")) != NULL);
658 
659     free (config_path);
660   }
661 }
662 
663 void
pk_fatal(const char * errmsg)664 pk_fatal (const char *errmsg)
665 {
666   if (errmsg)
667     pk_printf ("fatal error: %s\n", errmsg);
668   pk_printf ("This is a bug. Please report it to %s\n",
669              PACKAGE_BUGREPORT);
670   abort ();
671 }
672 
673 int
main(int argc,char * argv[])674 main (int argc, char *argv[])
675 {
676   /* Determine whether the tool has been invoked interactively.  */
677   poke_interactive_p = isatty (fileno (stdin));
678 
679   /* First round of argument parsing: everything that can impact the
680      initialization.  */
681   parse_args_1 (argc, argv);
682 
683   /* Initialization.  */
684   initialize (argc, argv);
685 
686   /* User's initialization.  */
687   if (poke_load_init_file)
688     initialize_user ();
689 
690   /* Second round of argument parsing: loading files, opening files
691      for IO, etc etc */
692   parse_args_2 (argc, argv);
693 
694   /* Enter the REPL or MI.  */
695   if (poke_mi_p)
696     {
697       if (!pk_mi ())
698         poke_exit_code = EXIT_FAILURE;
699     }
700   else if (poke_interactive_p)
701     pk_repl ();
702 
703   /* Cleanup.  */
704   finalize ();
705 
706   return poke_exit_code;
707 }
708