From 513b3c09a54af31ffd1b0eb9b3c47849816483be Mon Sep 17 00:00:00 2001 From: Luca BRUNO Date: Mon, 20 Dec 2021 10:00:02 +0000 Subject: main: add support for CLI extensions via external binaries This adds some logic to detect and dispatch unknown subcommands to extensions available in `$PATH`. Additional commands can be implemented by adding relevant `ostree-$verb` binaries to the system. As an example, if a `/usr/bin/ostree-extcommand` extension is provided, the execution of `ostree extcommand --help` will be dispatched to that as `ostree-extcommand extcommand --help`. --- Makefile-tests.am | 1 + src/ostree/main.c | 25 ++++-------- src/ostree/ot-main.c | 97 ++++++++++++++++++++++++++++++++++++++++++++ src/ostree/ot-main.h | 6 +++ tests/test-cli-extensions.sh | 27 ++++++++++++ 5 files changed, 140 insertions(+), 16 deletions(-) create mode 100755 tests/test-cli-extensions.sh diff --git a/Makefile-tests.am b/Makefile-tests.am index 69d3035d..6bae65cf 100644 --- a/Makefile-tests.am +++ b/Makefile-tests.am @@ -62,6 +62,7 @@ _installed_or_uninstalled_test_scripts = \ tests/test-basic-user.sh \ tests/test-basic-user-only.sh \ tests/test-basic-root.sh \ + tests/test-cli-extensions.sh \ tests/test-pull-subpath.sh \ tests/test-archivez.sh \ tests/test-remote-add.sh \ diff --git a/src/ostree/main.c b/src/ostree/main.c index 0e47ede3..7d17080c 100644 --- a/src/ostree/main.c +++ b/src/ostree/main.c @@ -26,7 +26,6 @@ #include #include #include -#include #include "ot-main.h" #include "ot-builtins.h" @@ -131,22 +130,16 @@ int main (int argc, char **argv) { - g_autoptr(GError) error = NULL; - int ret; + g_assert (argc > 0); - setlocale (LC_ALL, ""); - - g_set_prgname (argv[0]); - - ret = ostree_run (argc, argv, commands, &error); - - if (error != NULL) + g_autofree gchar *ext_command = ostree_command_lookup_external (argc, argv, commands); + if (ext_command != NULL) { - g_printerr ("%s%serror:%s%s %s\n", - ot_get_red_start (), ot_get_bold_start (), - ot_get_bold_end (), ot_get_red_end (), - error->message); + argv[0] = ext_command; + return ostree_command_exec_external (argv); + } + else + { + return ostree_main (argc, argv, commands); } - - return ret; } diff --git a/src/ostree/ot-main.c b/src/ostree/ot-main.c index 017a65a1..8ee73038 100644 --- a/src/ostree/ot-main.c +++ b/src/ostree/ot-main.c @@ -23,6 +23,7 @@ #include +#include #include #include #include @@ -140,6 +141,102 @@ message_handler (const gchar *log_domain, g_printerr ("%s: %s\n", g_get_prgname (), message); } +int +ostree_main (int argc, + char **argv, + OstreeCommand *commands) +{ + g_autoptr(GError) error = NULL; + + setlocale (LC_ALL, ""); + + g_set_prgname (argv[0]); + + int ret = ostree_run (argc, argv, commands, &error); + + if (error != NULL) + { + g_printerr ("%s%serror:%s%s %s\n", + ot_get_red_start (), ot_get_bold_start (), + ot_get_bold_end (), ot_get_red_end (), + error->message); + } + + return ret; +} + + +/** + * ostree_command_lookup_external: + * @argc: number of entries in @argv + * @argv: array of command-line arguments + * @commands: array of hardcoded internal commands + * + * Search for a relevant ostree extension binary in $PATH. Given a verb + * from argv, if it is not an internal command, it tries to locate a + * corresponding 'ostree-$verb' executable on the system. + * + * Returns: (transfer full) (nullable): callname (i.e. argv[0]) for the + * external command if found, or %NULL otherwise. + */ +gchar * +ostree_command_lookup_external (int argc, + char **argv, + OstreeCommand *commands) +{ + g_assert (commands != NULL); + + // Find the first verb (ignoring all earlier flags), then + // check if it is a known native command. Otherwise, try to look it + // up in $PATH. + // We ignore argv[0] here, the ostree binary itself is not multicall. + for (guint arg_index = 1; arg_index < argc; arg_index++) + { + char *current_arg = argv[arg_index]; + if (current_arg == NULL || + g_str_has_prefix (current_arg, "-") || + g_strcmp0 (current_arg, "") == 0) + continue; + + for (guint cmd_index = 0; commands[cmd_index].name != NULL; cmd_index++) + { + if (g_strcmp0 (current_arg, (commands[cmd_index]).name) == 0) + return NULL; + } + + g_autofree gchar *ext_command = g_strdup_printf ("ostree-%s", current_arg); + if (g_find_program_in_path (ext_command) == NULL) + return NULL; + + return g_steal_pointer (&ext_command); + } + + return NULL; +} + +/** + * ostree_command_exec_external: + * @argv: array of command-line arguments + * + * Execute an ostree extension binary. + * + * Returns: diverge on proper execution, otherwise return 1. + */ +int +ostree_command_exec_external (char **argv) +{ + int r = execvp(argv[0], argv); + g_assert (r == -1); + + setlocale (LC_ALL, ""); + g_printerr ("%s%serror:%s%s: Executing %s: %s\n", + ot_get_red_start (), ot_get_bold_start (), + ot_get_bold_end (), ot_get_red_end (), + argv[0], + g_strerror (errno)); + return 1; +} + int ostree_run (int argc, char **argv, diff --git a/src/ostree/ot-main.h b/src/ostree/ot-main.h index ed06e621..b369deb8 100644 --- a/src/ostree/ot-main.h +++ b/src/ostree/ot-main.h @@ -58,10 +58,16 @@ struct OstreeCommandInvocation { OstreeCommand *command; }; +int ostree_main (int argc, char **argv, OstreeCommand *commands); + int ostree_run (int argc, char **argv, OstreeCommand *commands, GError **error); int ostree_usage (OstreeCommand *commands, gboolean is_error); +char* ostree_command_lookup_external (int argc, char **argv, OstreeCommand *commands); + +int ostree_command_exec_external (char **argv); + gboolean ostree_parse_sysroot_or_repo_option (GOptionContext *context, const char *sysroot_path, const char *repo_path, diff --git a/tests/test-cli-extensions.sh b/tests/test-cli-extensions.sh new file mode 100755 index 00000000..6e483c5e --- /dev/null +++ b/tests/test-cli-extensions.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# +# Copyright (C) 2021 Red Hat Inc. +# SPDX-License-Identifier: LGPL-2.0+ + +set -euo pipefail + +. $(dirname $0)/libtest.sh + +echo '1..2' + +mkdir -p ./localbin +export PATH="./localbin/:${PATH}" +ln -s /usr/bin/env ./localbin/ostree-env +${CMD_PREFIX} ostree env --help >out.txt +assert_file_has_content out.txt "with an empty environment" +rm -rf -- localbin + +echo 'ok CLI extension localbin ostree-env' + +if ${CMD_PREFIX} ostree nosuchcommand 2>err.txt; then + assert_not_reached "missing CLI extension ostree-nosuchcommand succeeded" +fi +assert_file_has_content err.txt "Unknown command 'nosuchcommand'" +rm -f -- err.txt + +echo 'ok CLI extension unknown ostree-nosuchcommand' -- cgit v1.2.1