13

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

einpoklum
  • 118,144
  • 57
  • 340
  • 684
  • By "command", it looks like you mean "another program on the user's machine"? Can you confirm this is the usage you mean? – Stobor May 21 '09 at 00:30

6 Answers6

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.

Ben Collins
  • 159
  • 1
  • 2
  • The redirection was what other answers were missing. +1 for avoiding which output – Zach Mar 02 '19 at 20:25
  • This is both slow and relies on the existence of `which` - that is not necessary. In fact... IIANM, `which` is [not every required](https://pubs.opengroup.org/onlinepubs/9699919799/idx/utilities.html) by POSIX to exist - correct me if I'm wrong. – einpoklum May 04 '23 at 15:55
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.

pts
  • 80,836
  • 20
  • 110
  • 183
  • 1
    +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(). – j_random_hacker May 21 '09 at 05:02
  • 1
    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. – pts May 21 '09 at 09:06
  • Get the active list of executable extension from the *PATHEXT* environment variable on Win32: https://renenyffenegger.ch/notes/Windows/development/environment-variables/PATHEXT – pts Mar 30 '23 at 12:58
  • 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. – pts Mar 30 '23 at 13:01
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;
}
perror
  • 7,071
  • 16
  • 58
  • 85
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.

Naaff
  • 9,213
  • 3
  • 38
  • 43
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).

einpoklum
  • 118,144
  • 57
  • 340
  • 684
-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);
}
Amine Beihaqi
  • 177
  • 2
  • 8