Portable way to find out if a command exists (C/C++)
Asked Answered
S

7

14

C standard library provides functions system and popen to run a command. But is there a portable way to detect if a command exists?

Summerwood answered 21/5, 2009 at 0:27 Comment(1)
By "command", it looks like you mean "another program on the user's machine"? Can you confirm this is the usage you mean?Alehouse
M
15

For POSIX systems I found this to work very well (I'm checking for avconv in this example):

if (system("which avconv > /dev/null 2>&1")) {
    // Command doesn't exist...
} else {
    // Command does exist, do something with it...
}

The Redirect to /dev/null is simply to avoid anything being printed to stdout. It relies on the exit value from the which command alone.

Mozellamozelle answered 21/10, 2016 at 17:43 Comment(2)
The redirection was what other answers were missing. +1 for avoiding which outputKenwee
This is both slow and relies on the existence of which - that is not necessary. In fact... IIANM, which is not every required by POSIX to exist - correct me if I'm wrong.Cockle
A
6

No, there isn't any standard C function for that.

The Unix-only solution is to split getenv("PATH") on : (colon), and try to find the command executable (with the stat function) in the directories.

Some more details on what happens on Unix: execvp(3) in musl (a libc for Linux) getenv("PATH") on : (colon), and tries to run the program with the execve(2) system call (rather than the stat(2) system call). It's not feasible to emulate this 100% with stat(2), because for example execve(2) may fail with ENOEXEC (Exec format error) if it doesn't recognize the file format, which depends on kernel settings (including binfmt-misc module on Linux).

Some more details on what a Unix shell does. All the logic in the Dash shell is in exec.c. The shellexec function (called by the exec builtin) is similar to execvp(3) in musl (split PATH on : + execve(2)). The type builtin calls the describe_command function, which calls find_command, which does: split PATH on : + stat(2) + regular-file-check + execute-permission-check. The regular-file-check (near S_ISREG) checks that the filename is a regular file (i.e. rather than a directory, pipe or device etc.). The execute-permission-check (near getegid) checks approximately that the current process has execute permission on the file, by checking the relevant executable bit in st_mode returned by stat(2). Please note that these checks are qlwo only approximations, the ENOEXEC error above also applies here.

Archean answered 21/5, 2009 at 0:30 Comment(4)
+1. On Windows, it's a bit different: break on ";" not ":", and you need to check files ending with (at least) ".exe", ".com" and ".bat". In fact it's trickier than that -- as I recall, cmd.exe searches for .com files first in ALL directories, then for .exe files in ALL directories, etc. I don't recall the details unfortunately, but it is different than the order used by CreateProcess().Icao
On Windows, there are many more executable extensions, such as .pif, .lnk, .vbs, .scr etc. It would be better to find an API call to list all of them instead of hardcoding them.Archean
Get the active list of executable extension from the PATHEXT environment variable on Win32: renenyffenegger.ch/notes/Windows/development/…Archean
In Python, os.pathsep is ':' on Unix and ';' on Windows. Standard C and C++ don't predefine this constant. std::filesystem::path::preferred_separator is something else ('/' or '\\') in C++17.Archean
S
5

While I don't think there is a completely portable way to do this (some systems don't even support command interpreters), system() does return 0 if there were no errors while running your command. I suppose you could just try running your command and then check the return value of system.

To check if a command interpreter is available, call system( NULL ) and check for a non-zero value.

Stine answered 21/5, 2009 at 1:4 Comment(0)
P
5

Here is a way to scan all the paths stored in the PATH variable, scanning for the mathsat executable:

#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#include <string>
#include <iostream>

using namespace std;

int main ()
{
  struct stat sb;
  string delimiter = ":";
  string path = string(getenv("PATH"));
  size_t start_pos = 0, end_pos = 0;

  while ((end_pos = path.find(':', start_pos)) != string::npos)
    {
      string current_path =
        path.substr(start_pos, end_pos - start_pos) + "/mathsat";

      if ((stat(mathsat_path.c_str(), &sb) == 0) && (sb.st_mode & S_IXOTH))
        {
          cout << "Okay" << endl;
          return EXIT_SUCCESS;
         }

      start_pos = end_pos + 1;
     }

  return EXIT_SUCCESS;
}
Pigeonwing answered 1/4, 2014 at 13:10 Comment(1)
This is great. It excludes the last item in the path though.Williemaewillies
C
0

There is no fully-portable way to do this in C, as @pts' answer explains. But if you assume a POSIX environment, you can do this.

First, let's recall how to check whether a path designates an executable file (with C and POSIX):

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

int is_executable_file(char const * file_path) 
{
    struct stat sb;
    return 
        (stat(file_path, &sb) == 0) &&  
        S_ISREG(sb.st_mode) && 
        (access(file_path, X_OK) == 0);
}

Now let us use this function to check the paths obtained by concatenating the command name to all elements of the PATH environment variable:

#include <memory.h>
#include <stdlib.h>
#include <string.h>

int is_executable_file(char const * file_path);

char const * strchrnul_(char const * haystack, size_t haystack_length, char needle)
{
    char const * strchr_result = strchr(haystack, needle);
    return (strchr_result != NULL) ? strchr_result : haystack + haystack_length;
}

int executable_is_on_path(char const* executable_basename)
{
    char const * const path_env_var = getenv("PATH");
    // You may want to decide on your own policy regarding behavior when there is no PATH
    if (path_env_var == NULL) { return is_executable(executable_basename); }
    const size_t path_env_var_len = strlen(path_env_var);
    if (path_env_var_len == 0) { return is_executable(executable_basename); }
    const size_t basename_len = strlen(executable_basename);

    // Allocate the maximum possible length of an executable path using a directory in the PATH string,
    // so that we don't need to re-allocate later
    char * executable_path = (char*) malloc(path_env_var_len + basename_len + 2);
    if (executable_path == NULL) { return false; } // or you can throw an exception, or exit etc.

    for (char const * token_start = path_env_var; token_start < path_env_var + path_env_var_len; ) {
        static char const delimiter = ':';
        char const *token_end = strchrnul_(token_start, path_env_var_len, delimiter);
        // composing the full path of the prospective executable
        {
            off_t token_len = token_end - token_start;
            strncpy(executable_path, token_start, token_len);
            executable_path[token_len] = '/';
            strcpy(executable_path + token_len + 1, executable_basename);
            executable_path[basename_len + token_len + 1] = '\0';
        }
        if (is_executable(executable_path)) { 
            free(executable_path);
            return true;
        }
        token_start = token_end + 1;
    }
    free(executable_path);
    return false;
}

If you were using C++, you would have a somewhat easier time with string, filesystem and path operations; but this is pure C (in fact, it's C89).

Cockle answered 4/5, 2023 at 20:23 Comment(0)
W
0

A variation of what Ben Collins said, but using POSIX command -v instead of which which is not part of POSIX.

You can do this by running the command:

command -v CMD_TO_CHECK > /dev/null 2>&1
  • The -v flags tells command to not execute the file and would normally also print the file path to stdout.
  • The > /dev/null redirects stdout output to a null stream, meaning it is just discarded. This way we won't have clutter in our program's stdout.
  • The 2>&1 merges stderr into stdout by redirecting the file descriptor 2's output into 1's, meaning stderr won't be printed either. On my system command -v doesn't ever seem to print to stderr but I don't know if this would be true for every implementation.

If the program is found, system returns 0, otherwise it will return something greater than 0 if the program doesn't exist. This is why when checking, you should use system(...) == 0, or alternatively !system(...).

Example to check if Clang exists:

#include <stdlib.h>
// ...

if( !system("command -v clang > /dev/null 2>&1") )
{
    // The command exists!
}
else
{
    // Doesn't exist
}

Using this you can make a function to check if an arbitrary program exists:

#include <stdio.h>
#include <stdlib.h>

int programExists(const char *name)
{
    char buffer[512];

    snprintf(
        buffer,
        sizeof buffer,
        "command -v %s > /dev/null 2>&1",
        name
    );

    return !system( buffer );
}

int main()
{
    printf("GCC ");
    if( programExists("gcc") ) printf("exists!\n");
    else printf("not found.\n");

    printf("obscureprogram ");
    if( programExists("obscureprogram") ) printf("exists!\n");
    else printf("not found.\n");

    return 0;
}

or use the first example to avoid the buffering and using snprintf.

Worshipful answered 26/10, 2023 at 3:10 Comment(0)
M
-1

here is a snippet from my code that i use to look for existing commands in the system using PATH from environment variables.

first i get the PATH variable then i split it using : then i store it in 2d array and return in get_path(...).

after that i join the string cmd with each path in PATH variable and check if i can access it using access(2) function if the file exist then i found the path to the executable and i return it else i print error with perror() and return NULL.

char    **get_paths(char **envp)
{
    while (*envp)
    {
        if (ft_strncmp(*envp, "PATH=", 5) == 0)
            return (ft_split(*envp + 5, ':'));
        envp++;
    }
    return (NULL);
}

char    *cmd_file(char **envp, char *cmd)
{
    char    **paths;
    char    *path_tmp;
    char    *file;

    paths = envp;
    if (access(cmd, X_OK) == 0)
        return (ft_strdup(cmd));
    while (*paths && cmd)
    {
        path_tmp = ft_strjoin(*paths, "/");
        file = ft_strjoin(path_tmp, cmd);
        free(path_tmp);
        if (access(file, X_OK) == 0)
            return (file);
        free(file);
        paths++;
    }
    perror(cmd);
    return (NULL);
}

int main(int argc, char **argv, char **envp)
{
    char **paths;
    char *cmd;
    paths = get_paths(envp);
    cmd = cmd_file(paths, argv[1]);
    printf("%s\n", cmd);
}
Mariquilla answered 19/1, 2023 at 10:58 Comment(2)
Please add your includes. I don't know of functions named ft_whatever.Cockle
ft_ functions are custom made functions most of them are from the standard c library just remove the ft and include the standard c library. you can find ft_ functions definitions here: github.com/Milimas/libftMariquilla

© 2022 - 2024 — McMap. All rights reserved.