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