/* poke.c - Interactive editor for binary files. */ /* Copyright (C) 2019, 2020, 2021 Jose E. Marchesi */ /* 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 . */ #include #include #include #include #include #include #include #include #include #include "xalloc.h" #include #ifdef HAVE_HSERVER # include "pk-hserver.h" #endif #include "poke.h" #include "pk-cmd.h" #include "pk-repl.h" #include "pk-utils.h" #include "pk-mi.h" #include "pk-map.h" #include "pk-ios.h" /* poke can be run either interactively (from a tty) or in batch mode. The following predicate records this. */ int poke_interactive_p; #if HAVE_HSERVER /* The following global indicates whether the hyperserver is activated or not. */ int poke_hserver_p; #endif /* The following global indicates whether the MI shall be used. */ int poke_mi_p; #ifdef POKE_MI /* This build of poke supports mi if the user wants it. */ static const int mi_supported_p = 1; #else /* This build of poke does not support mi. */ static const int mi_supported_p = 0; int pk_mi (void) {assert (0); return 0;} #endif /* The following global indicates whether poke should be as terse as possible in its output. This is useful when running poke from other programs. */ int poke_quiet_p; /* The following global contains the directory holding the program's architecture independent files, such as scripts. */ char *poke_datadir; /* The following global contains the directory holding the program's info files. */ char *poke_infodir; /* The following global contains the directory holding pickles shipped with poke. In an installed program, this is the same than poke_datadir/pickles, but the POKE_PICKLESDIR environment variable can be set to a different value, which is mainly to run an uninstalled poke. */ char *poke_picklesdir; /* The following global contains the directory holding the standard map files. In an installed poke, this is the same than poke_datadir/maps, but the POKE_MAPSDIR environment variable can be set to a different value, which is mainly to run an uinstalled poke. */ char *poke_mapsdir; /* The following global contains the directory holding the help support files. In an installed poke, this is the same than poke_datadir, but the POKE_DOCDIR environment variable can be set to a different value, which is mainly to run an uninstalled poke. */ char *poke_docdir; /* The following global contains the name of the program to use to display documentation. Valid values are `info' and `less'. It defaults to `info'. */ char *poke_doc_viewer = NULL; /* The following global determines whether auto-maps shall be acknowleged when loading files or not. Defaults to `yes'. */ int poke_auto_map_p = 1; /* The following global determines whether the user specified that the hyperlinks server should not be activated, in the command line. */ int poke_no_hserver_arg = 0; /* The following global determines whether map information shall be included in the REPL prompt. Defaults to `yes'. */ int poke_prompt_maps_p = 1; /* This is used by commands to indicate to the REPL that it must exit. */ int poke_exit_p; int poke_exit_code; /* The following global is the poke compiler. */ pk_compiler poke_compiler; /* The following global indicates whether to load a user initialization file. It defaults to 1. */ int poke_load_init_file = 1; /* Command line options management. */ enum { HELP_ARG, VERSION_ARG, QUIET_ARG, LOAD_ARG, LOAD_AND_EXIT_ARG, CMD_ARG, NO_INIT_FILE_ARG, SOURCE_ARG, COLOR_ARG, STYLE_ARG, MI_ARG, NO_AUTO_MAP_ARG, NO_HSERVER_ARG }; static const struct option long_options[] = { {"help", no_argument, NULL, HELP_ARG}, {"version", no_argument, NULL, VERSION_ARG}, {"quiet", no_argument, NULL, QUIET_ARG}, {"load", required_argument, NULL, LOAD_ARG}, {"command", required_argument, NULL, CMD_ARG}, {"source", required_argument, NULL, SOURCE_ARG}, {"no-init-file", no_argument, NULL, NO_INIT_FILE_ARG}, {"color", required_argument, NULL, COLOR_ARG}, {"style", required_argument, NULL, STYLE_ARG}, {"mi", no_argument, NULL, MI_ARG}, {"no-auto-map", no_argument, NULL, NO_AUTO_MAP_ARG}, {"no-hserver", no_argument, NULL, NO_HSERVER_ARG}, {NULL, 0, NULL, 0}, }; static void print_help (void) { /* TRANSLATORS: --help output, GNU poke synopsis. no-wrap */ pk_puts (_("Usage: poke [OPTION]... [FILE]\n")); /* TRANSLATORS: --help output, GNU poke summary. no-wrap */ pk_puts (_("Interactive editor for binary files.\n")); pk_puts ("\n"); /* TRANSLATORS: --help output, GNU poke arguments. no-wrap */ pk_puts (_(" -l, --load=FILE load the given pickle at startup\n")); pk_puts (_(" -L FILE load the given pickle and exit\n")); pk_puts ("\n"); /* TRANSLATORS: --help output, GNU poke arguments. no-wrap */ pk_puts (_("Commanding poke from the command line:\n")); pk_puts (_(" -c, --command=CMD execute the given command\n")); pk_puts (_(" -s, --source=FILE execute commands from FILE\n")); pk_puts ("\n"); pk_puts (_("Styling text output:\n")); pk_puts (_(" --color=(yes|no|auto|html|test) emit styled output\n")); pk_puts (_(" --style=STYLE_FILE style file to use when styling\n")); pk_puts ("\n"); pk_puts (_("Machine interface:\n")); pk_puts (_(" --mi use the MI in stdin/stdout\n")); pk_puts ("\n"); /* TRANSLATORS: --help output, less used GNU poke arguments. no-wrap */ pk_puts (_(" -q, --no-init-file do not load an init file\n")); pk_puts (_(" --no-auto-map disable auto-map\n")); #if HAVE_HSERVER pk_puts (_(" --no-hserver do not run the hyperlinks server\n")); #endif pk_puts (_(" --quiet be as terse as possible\n")); pk_puts (_(" --help print a help message and exit\n")); pk_puts (_(" --version show version and exit\n")); pk_puts ("\n"); /* TRANSLATORS: --help output 5+ (reports) TRANSLATORS: the placeholder indicates the bug-reporting address for this application. Please add _another line_ with the address for translation bugs. no-wrap */ pk_printf (_("\ Report bugs in the bug tracker at\n\ <%s>\n\ or by email to <%s>.\n"), PACKAGE_BUGZILLA, PACKAGE_BUGREPORT); #ifdef PACKAGE_PACKAGER_BUG_REPORTS printf (_("Report %s bugs to: %s\n"), PACKAGE_PACKAGER, PACKAGE_PACKAGER_BUG_REPORTS); #endif pk_printf (_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL); pk_printf (_("General help using GNU software: %s\n"), ""); } void pk_print_version (int hand_p) { if (hand_p) { pk_term_class ("logo"); pk_puts (" _____\n"); pk_puts (" ---' __\\_______\n"); pk_printf (" ______) GNU poke %s\n", VERSION); pk_puts (" __)\n"); pk_puts (" __)\n"); pk_puts (" ---._______)\n"); pk_term_end_class ("logo"); /* xgettext: no-wrap */ pk_puts ("\n"); } else pk_printf ("GNU poke %s\n\n", VERSION); /* It is important to separate the year from the rest of the message, as done here, to avoid having to retranslate the message when a new year comes around. */ pk_term_class ("copyright"); /* TRANSLATORS: If your target locale supports it, you can translate (C) to the copyright symbol (U+00A9 in Unicode), but there is no obligation to do this. In other cases it's probably best to leave it untranslated. */ pk_printf (_("\ %s (C) %s The poke authors.\n\ License GPLv3+: GNU GPL version 3 or later"), "Copyright", "2019-2021"); pk_term_hyperlink ("http://gnu.org/licenses/gpl.html", NULL); pk_puts (" "); pk_term_end_hyperlink (); pk_puts (".\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"); pk_term_end_class ("copyright"); pk_printf (_("\ \nPowered by Jitter %s."), JITTER_VERSION); pk_puts (_("\ \n\ Perpetrated by Jose E. Marchesi.\n")); } static void finalize (void) { #ifdef HAVE_HSERVER if (poke_hserver_p) pk_hserver_shutdown (); #endif pk_cmd_shutdown (); pk_map_shutdown (); pk_compiler_free (poke_compiler); pk_term_shutdown (); } /* Callbacks for the terminal output in libpoke. */ static struct pk_term_if poke_term_if = { .flush_fn = pk_term_flush, .puts_fn = pk_puts, .printf_fn = pk_printf, .indent_fn = pk_term_indent, .class_fn = pk_term_class, .end_class_fn = pk_term_end_class, .hyperlink_fn = pk_term_hyperlink, .end_hyperlink_fn = pk_term_end_hyperlink, .get_color_fn = pk_term_get_color, .set_color_fn = pk_term_set_color, .get_bgcolor_fn = pk_term_get_bgcolor, .set_bgcolor_fn = pk_term_set_bgcolor, }; static void set_script_args (int argc, char *argv[]) { int i, nargs; uint64_t index; pk_val argv_array; /* Look for -L SCRIPT */ for (i = 0; i < argc; ++i) if (STREQ (argv[i], "-L")) break; /* Any argument after SCRIPT is an argument for the script. */ i = i + 2; nargs = argc - i; argv_array = pk_make_array (pk_make_uint (nargs, 64), pk_make_array_type (pk_make_string_type (), PK_NULL /* bound */)); for (index = 0; i < argc; ++i, ++index) pk_array_insert_elem (argv_array, index, pk_make_string (argv[i])); pk_defvar (poke_compiler, "argv", argv_array); } static void parse_args_1 (int argc, char *argv[]) { char c; int ret; while ((ret = getopt_long (argc, argv, "ql:c:s:L:", long_options, NULL)) != -1) { c = ret; switch (c) { case MI_ARG: if (!mi_supported_p) { fputs (_("MI is not built into this instance of poke\n"), stderr); exit (EXIT_FAILURE); } else { poke_mi_p = 1; } break; case 'L': poke_interactive_p = 0; return; case NO_AUTO_MAP_ARG: poke_auto_map_p = 0; break; case NO_HSERVER_ARG: poke_no_hserver_arg = 1; break; case 'q': case NO_INIT_FILE_ARG: poke_load_init_file = 0; break; default: break; } } } static void parse_args_2 (int argc, char *argv[]) { char c; int ret; optind = 1; while ((ret = getopt_long (argc, argv, "ql:c:s:L:", long_options, NULL)) != -1) { c = ret; switch (c) { case HELP_ARG: print_help (); goto exit_success; break; case VERSION_ARG: pk_print_version (0 /* hand_p */); goto exit_success; break; case QUIET_ARG: poke_quiet_p = 1; pk_set_quiet_p (poke_compiler, 1); break; case 'l': case LOAD_ARG: if (pk_compile_file (poke_compiler, optarg, NULL /* exit_status */) != PK_OK) goto exit_success; break; case 'c': case CMD_ARG: { poke_interactive_p = 0; if (!pk_cmd_exec (optarg)) goto exit_failure; break; } case 's': case SOURCE_ARG: { if (!pk_cmd_exec_script (optarg)) goto exit_failure; break; } case 'L': { int exit_status; /* Build argv in the compiler, with the rest of the command-line arguments. Then execute the script and return. */ set_script_args (argc, argv); if (pk_compile_file (poke_compiler, optarg, &exit_status) != PK_OK) goto exit_failure; finalize (); exit (exit_status); break; } case MI_ARG: case NO_AUTO_MAP_ARG: case NO_HSERVER_ARG: case 'q': case NO_INIT_FILE_ARG: /* These are handled in parse_args_1. */ break; /* libtextstyle arguments are handled in pk-term.c, not here. */ case COLOR_ARG: case STYLE_ARG: break; default: goto exit_failure; } } if (optind < argc) { char *filename = argv[optind++]; int xxx = poke_auto_map_p; poke_auto_map_p = 0; /* XXX */ if (pk_open_ios (filename, 1 /* set_cur_p */) == PK_IOS_NOID) { if (!poke_quiet_p) pk_printf (_("cannot open file %s\n"), filename); goto exit_failure; } poke_auto_map_p = xxx; /* XXX */ optind++; } if (optind < argc) { print_help (); goto exit_failure; } return; exit_success: finalize (); exit (EXIT_SUCCESS); exit_failure: finalize (); exit (EXIT_FAILURE); } static void initialize (int argc, char *argv[]) { /* This is used by the `progname' gnulib module. */ set_program_name ("poke"); /* i18n */ setlocale (LC_ALL, ""); bindtextdomain (PACKAGE, LOCALEDIR); textdomain (PACKAGE); /* Determine the directory containing poke's scripts and other architecture-independent data. */ poke_datadir = getenv ("POKEDATADIR"); if (poke_datadir == NULL) poke_datadir = PKGDATADIR; poke_picklesdir = getenv ("POKEPICKLESDIR"); if (poke_picklesdir == NULL) { poke_picklesdir = pk_str_concat (poke_datadir, "/pickles", NULL); pk_assert_alloc (poke_picklesdir); } poke_mapsdir = getenv ("POKEMAPSDIR"); if (poke_mapsdir == NULL) { poke_mapsdir = pk_str_concat (poke_datadir, "/maps", NULL); pk_assert_alloc (poke_mapsdir); } poke_docdir = getenv ("POKEDOCDIR"); if (poke_docdir == NULL) poke_docdir = poke_datadir; poke_infodir = getenv ("POKEINFODIR"); if (poke_infodir == NULL) poke_infodir = PKGINFODIR; /* Initialize the terminal output. */ pk_term_init (argc, argv); /* Initialize the poke incremental compiler. */ poke_compiler = pk_compiler_new (&poke_term_if); if (poke_compiler == NULL) pk_fatal ("creating the incremental compiler"); /* Make poke_interactive_p available to poke programs. */ if (pk_defvar (poke_compiler, "poke_interactive_p", pk_make_int (poke_interactive_p, 32)) == PK_ERROR) pk_fatal ("defining poke_interactive_p"); /* Load poke.pk */ if (!pk_load (poke_compiler, "poke")) pk_fatal ("unable to load the poke module"); /* Initialize the global map. */ pk_map_init (); /* Initialize the command subsystem. This should be done even if called non-interactively. */ pk_cmd_init (); #ifdef HAVE_HSERVER poke_hserver_p = (poke_interactive_p && pk_term_color_p () && !poke_mi_p && !poke_no_hserver_arg); /* Initialize and start the terminal hyperlinks server. */ if (poke_hserver_p) { pk_hserver_init (); pk_hserver_start (); } #endif /* Initialize the documentation viewer. */ poke_doc_viewer = xstrdup ("info"); } static void initialize_user (void) { /* Load the user's initialization file ~/.pokerc, if it exists in the HOME directory. */ char *homedir = getenv ("HOME"); if (homedir != NULL) { char *pokerc = pk_str_concat (homedir, "/.pokerc", NULL); pk_assert_alloc (pokerc); if (pk_file_readable (pokerc) == NULL) { if (!pk_cmd_exec_script (pokerc)) exit (EXIT_FAILURE); else return; } free (pokerc); } /* If no ~/.pokerc file was found, acknowledge the XDG Base Directory Specification, as documented in https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html If the environment variable XDG_CONFIG_HOME if defined, try to load the file $XDG_CONFIG_HOME/pokerc.conf. If the environment variable $XDG_CONFIG_DIRS is defined, try to use all the : separated paths in that variable to find pokerc.conf. Else, try to load /etc/xdg/poke/pokerc.conf. */ { const char *xdg_config_home = getenv ("XDG_CONFIG_HOME"); const char *xdg_config_dirs = getenv ("XDG_CONFIG_DIRS"); if (xdg_config_home == NULL) xdg_config_home = ""; if (xdg_config_dirs == NULL) xdg_config_dirs = "/etc/xdg"; char *config_path = pk_str_concat (xdg_config_dirs, ":", xdg_config_home, NULL); pk_assert_alloc (config_path); char *dir = strtok (config_path, ":"); do { /* Ignore empty entries. */ if (*dir != '\0') { /* Mount the full path and determine whether the resulting file is readable. */ char *config_filename = pk_str_concat (dir, "/poke/pokerc.conf", NULL); pk_assert_alloc (config_filename); if (pk_file_readable (config_filename) == NULL) { /* Load the configuration file. */ int ret = pk_cmd_exec_script (config_filename); if (!ret) exit (EXIT_FAILURE); break; } free (config_filename); } } while ((dir = strtok (NULL, ":")) != NULL); free (config_path); } } void pk_fatal (const char *errmsg) { if (errmsg) pk_printf ("fatal error: %s\n", errmsg); pk_printf ("This is a bug. Please report it to %s\n", PACKAGE_BUGREPORT); abort (); } int main (int argc, char *argv[]) { /* Determine whether the tool has been invoked interactively. */ poke_interactive_p = isatty (fileno (stdin)); /* First round of argument parsing: everything that can impact the initialization. */ parse_args_1 (argc, argv); /* Initialization. */ initialize (argc, argv); /* User's initialization. */ if (poke_load_init_file) initialize_user (); /* Second round of argument parsing: loading files, opening files for IO, etc etc */ parse_args_2 (argc, argv); /* Enter the REPL or MI. */ if (poke_mi_p) { if (!pk_mi ()) poke_exit_code = EXIT_FAILURE; } else if (poke_interactive_p) pk_repl (); /* Cleanup. */ finalize (); return poke_exit_code; }