diff --git a/doc/news/_preparation_next_release.md b/doc/news/_preparation_next_release.md index 95c1dd57802..0ad0311fcb5 100644 --- a/doc/news/_preparation_next_release.md +++ b/doc/news/_preparation_next_release.md @@ -300,6 +300,10 @@ This section keeps you up-to-date with the multi-language support provided by El - _NOTE_: The `path` argument for `kdb complete` is now required, so instead of `kdb complete` `kdb complete ""` has to be used. The reason for this is described in issue #4952. - <> +- add support for external commands, with and without spec _(@hannes99)_ + - It is still possible to execute external binaries that are placed in a specific director, same as it worked in the old implementation. + - It is now possible to provide a specification to external commands to `kdb`, so `kdb` check/parses arguments and then calls the + external program with them. ### <> diff --git a/doc/tutorials/README.md b/doc/tutorials/README.md index 1a703af953b..b5f089d8253 100644 --- a/doc/tutorials/README.md +++ b/doc/tutorials/README.md @@ -12,6 +12,7 @@ Read this first to get the basic concepts of Elektra. - [Cascading](cascading.md) - [Arrays](arrays.md) - [Mount Configuration Files](mount.md) +- [External KDB commands](external-commands.md) ## Developers diff --git a/doc/tutorials/external-commands.md b/doc/tutorials/external-commands.md new file mode 100644 index 00000000000..30617a3a889 --- /dev/null +++ b/doc/tutorials/external-commands.md @@ -0,0 +1,57 @@ +# How-to: Add external commands + +This tutorial will describe how to provide `kdb` with the specification of external programs. +So `kdb` can parse and check the provided options and arguments according to the provided specification. +This allows you to have, for example, a shell script but its args are checked by `kdb` before running it. +It is possible to either mount(`kdb mount`) the specification, or set the keys manually using `kdb set` and `kdb meta-set`. +Both options will be described in the following. + +For a reference of how the specification can look like [Command Line Options](command-line-options.md). + +## With `kdb mount` + +```ni +[file] + meta:/description = the file that shall be deleted + meta:/args = indexed + meta:/args/index = 0 + +[] + meta:/command = somecommand + meta:/description = Simple script + meta:/external = 1 + meta:/bin = /path/to/test.sh +``` + +The file then has to be mounted with + +```sh +kdb mount /path/to/spec.ni spec:/sw/elektra/kdb/#0/current/somecommand mini +``` + +## Alternative to `kdb mount` + +This is the same as mounting the spec file. + +```bash +kdb set spec:/sw/elektra/kdb/#0/current/somecommand "" +kdb meta-set spec:/sw/elektra/kdb/#0/current/somecommand external 1 +kdb meta-set spec:/sw/elektra/kdb/#0/current/somecommand bin "/path/to/test.sh" +kdb meta-set spec:/sw/elektra/kdb/#0/current/somecommand command "somecommand" +kdb meta-set spec:/sw/elektra/kdb/#0/current/somecommand description "Simple script" +kdb set spec:/sw/elektra/kdb/#0/current/somecommand "" +kdb meta-set spec:/sw/elektra/kdb/#0/current/somecommand/file description "the file that shall be deleted" +kdb meta-set spec:/sw/elektra/kdb/#0/current/somecommand/file args indexed +kdb meta-set spec:/sw/elektra/kdb/#0/current/somecommand/file args/index 0 +``` + +> **_NOTE:_** Extra arguments are directly passed on to the external command. So it is possible to provide the external program with more +> args than specified in the spec. Those are not check by `KDB`. + +So basically keys in `spec:/sw/elektra/kdb/#0/current/..` are considered external commands as long as the metakey `external` is set to 1 +and a metakey `bin`, that has the path to the binary, is set. Instead of mounting the spec file it is also possible to the set spec +manually using `kdb`. + +`bin` should be an absolut path. If it is not, the binary will be search relative to where `kdb` is executed. + +External commands specified like this will appear in `kdb --help` and can be used with `kdb somecommand`. diff --git a/src/tools/kdb/external.c b/src/tools/kdb/external.c new file mode 100644 index 00000000000..8b32689c7fe --- /dev/null +++ b/src/tools/kdb/external.c @@ -0,0 +1,196 @@ +/** + * @file + * + * @brief Code to support external programs + * + * @copyright BSD License (see LICENSE.md or https://www.libelektra.org) + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#else +#include +#include +#endif + +extern char ** environ; + +const char * getExternalBin (KeySet * binaries, const char * key) +{ + Key * tmp = keyNew ("/", KEY_END); + keySetBaseName (tmp, key); + Key * resultKey = ksLookup (binaries, tmp, KDB_O_NONE); + keyDel (tmp); + if (resultKey == NULL) + { + return NULL; + } + return keyString (resultKey); +} + + +int tryLoadExternal (char * commandName, KeySet * binaries) +{ + char * execPathPtr = getenv ("KDB_EXEC_PATH"); + bool found = false; + char path[PATH_MAX] = { 0 }; + char * saveptr; + struct stat buf; + + if (execPathPtr) + { + char * execPath = strdup (execPathPtr); + char * token = strtok_r (execPath, ":", &saveptr); + while (token != NULL && !found) + { + snprintf (path, sizeof (path), "%s/%s", token, commandName); + found = stat (path, &buf) != -1; + token = strtok_r (NULL, ":", &saveptr); + } + elektraFree (execPath); + } + + if (!found) + { + snprintf (path, sizeof (path), "%s/%s", BUILTIN_EXEC_FOLDER, commandName); + found = stat (path, &buf) != -1; + } + + if (found) + { + Key * tmp = keyNew ("/", KEY_END); + keySetBaseName (tmp, commandName); + keySetString (tmp, path); + ksAppendKey (binaries, tmp); + return 0; + } + return 1; +} + +int loadExternalSpec (KeySet * spec, KeySet * binaries, Key * errorKey) +{ + KDB * handle = kdbOpen (NULL, errorKey); + Key * baseKey = keyNew ("spec:" CLI_BASE_KEY, KEY_END); + KeySet * config = ksNew (0, KS_END); + if (kdbGet (handle, config, errorKey) == -1) + { + ELEKTRA_SET_VALIDATION_SEMANTIC_ERRORF (errorKey, "could not load '%s': %s", CLI_BASE_KEY, GET_ERR (baseKey)); + keyDel (baseKey); + ksDel (config); + kdbClose (handle, errorKey); + return 1; + } + Key * cur = NULL; + KeySet * part = ksCut (config, baseKey); + + for (elektraCursor it = 0; it < ksGetSize (part); ++it) + { + cur = ksAtCursor (part, it); + const Key * externalMeta = keyGetMeta (cur, "external"); + const Key * externalBinary = keyGetMeta (cur, "bin"); + bool isExternal = false; + if (externalBinary != NULL && externalMeta != NULL && elektraKeyToBoolean (externalMeta, &isExternal) && isExternal) + { // add external spec and save path to binary + KeySet * externalCommandSpec = ksCut (part, cur); + + Key * tmp = keyNew ("/", KEY_END); + keySetBaseName (tmp, keyBaseName (cur)); + keySetString (tmp, keyString (externalBinary)); + ksAppendKey (binaries, tmp); + + ksAppend (spec, externalCommandSpec); + ksDel (externalCommandSpec); + it--; + } + } + ksDel (part); + ksDel (config); + kdbClose (handle, errorKey); + keyDel (baseKey); + return 0; +} + +int runExternal (const char * bin, char ** argv, Key * errorKey) +{ + // the external program should think it was called directly + argv[1] = (char *) bin; + + int status = 0; + +#ifdef _WIN32 + STARTUPINFO si; + PROCESS_INFORMATION pi; + + ZeroMemory (&si, sizeof (si)); + si.cb = sizeof (si); + ZeroMemory (&pi, sizeof (pi)); + + // Construct command line string + char cmdline[MAX_PATH] = ""; + for (int i = 1; argv[i]; ++i) + { + strcat (cmdline, "\""); + strcat (cmdline, argv[i]); + strcat (cmdline, "\" "); + } + + // Start the child process. + if (!CreateProcess (NULL, // Module name + cmdline, // Command line + NULL, // Process handle not inheritable + NULL, // Thread handle not inheritable + FALSE, // Set handle inheritance to FALSE + 0, // No creation flags + NULL, // Use parent's environment block + NULL, // Use parent's starting directory + &si, // Pointer to STARTUPINFO structure + &pi) // Pointer to PROCESS_INFORMATION structure + ) + { + ELEKTRA_SET_RESOURCE_ERRORF (errorKey, "CreateProcess failed: %lu", GetLastError ()); + return 1; + } + + // Wait until child process exits. + WaitForSingleObject (pi.hProcess, INFINITE); + + // Get exit code + DWORD exitCode; + GetExitCodeProcess (pi.hProcess, &exitCode); + status = (int) exitCode; + + // Close process and thread handles. + CloseHandle (pi.hProcess); + CloseHandle (pi.hThread); +#else + pid_t pid; + + if (posix_spawn (&pid, bin, NULL, NULL, &(argv[1]), environ) != 0) + { + ELEKTRA_SET_RESOURCE_ERRORF (errorKey, "posix_spawn failed: %s", strerror (errno)); + return 1; + } + + if (waitpid (pid, &status, 0) < 0) + { + ELEKTRA_SET_RESOURCE_ERRORF (errorKey, "waitpid failed: %s", strerror (errno)); + return 1; + } +#endif + + fflush (stdout); + return status; +} diff --git a/src/tools/kdb/external.h b/src/tools/kdb/external.h new file mode 100644 index 00000000000..43d74bfaf7b --- /dev/null +++ b/src/tools/kdb/external.h @@ -0,0 +1,20 @@ +/** + * @file + * + * @brief Header for things needed for external programs + * + * @copyright BSD License (see LICENSE.md or https://www.libelektra.org) + */ + +#ifndef ELEKTRA_KDB_EXTERNAL_H +#define ELEKTRA_KDB_EXTERNAL_H + +#include + +const char * getExternalBin (KeySet * binaries, const char * key); + +int runExternal (const char * bin, char ** argv, Key * errorKey); +int loadExternalSpec (KeySet * spec, KeySet * binaries, Key * errorKey); +int tryLoadExternal (char * commandName, KeySet * binaries); + +#endif // ELEKTRA_KDB_EXTERNAL_H diff --git a/src/tools/kdb/main.c b/src/tools/kdb/main.c index 1903d65f287..7194b225312 100644 --- a/src/tools/kdb/main.c +++ b/src/tools/kdb/main.c @@ -44,6 +44,7 @@ #include #include +#include #include #include #include @@ -205,6 +206,10 @@ int main (int argc, char ** argv) "kdb is a program to manage Elektra's key database.", KEY_END), KS_END); + // external programs with spec in spec:/sw/elektra/kdb/#0/current/ + KeySet * externalBinaries = ksNew (10, KS_END); + loadExternalSpec (options, externalBinaries, parentKey); + // C spec for (unsigned long i = 0; i < sizeof (subcommands) / sizeof (subcommands[0]); ++i) { @@ -225,6 +230,7 @@ int main (int argc, char ** argv) printWarnings (parentKey); keyDel (parentKey); ksDel (options); + ksDel (externalBinaries); return 0; } if (result == -1) @@ -232,7 +238,14 @@ int main (int argc, char ** argv) const char * errorMessage = GET_ERR (parentKey); if (elektraStrNCmp (errorMessage, "Unknown sub-command:", 20) == 0) { - result = cpp_main (argc, argv); + if (tryLoadExternal (argv[1], externalBinaries) == 0) + { + result = 0; + } + else + { + result = 4; + } } else if (elektraStrNCmp (errorMessage, "Unknown short option:", 21) == 0 || elektraStrNCmp (errorMessage, "Unknown long option:", 20) == 0) @@ -252,6 +265,17 @@ int main (int argc, char ** argv) const char * subcommand = keyString (ksLookupByName (options, CLI_BASE_KEY, 0)); + // external + const char * externalBin = getExternalBin (externalBinaries, argv[1]); + if (externalBin != NULL) + { + Key * errorKey = keyNew (CLI_SPEC_KEY, KEY_END); + result = runExternal (externalBin, argv, errorKey); + printError (errorKey); + keyDel (errorKey); + goto cleanup; + } + // C for (unsigned long i = 0; i < sizeof (subcommands) / sizeof (command); ++i) { @@ -279,5 +303,6 @@ int main (int argc, char ** argv) cleanup: keyDel (parentKey); ksDel (options); + ksDel (externalBinaries); return result; }