darnir
darnir

Reputation: 5180

Search for a file in $PATH on Linux in C

I would like to test whether GNUPlot is installed on the system on which my program is running.
For that, I figured I'll test for the existence of the gnuplot executable in the user's install locations through stat() call.

However, I don't know how to read the $PATH environment variable in C so I can test for the existence of the file in those locations.

Upvotes: 2

Views: 7197

Answers (7)

dakka
dakka

Reputation: 42

Using C++17 to get a vector of path elements.

% a.out ls
/bin/ls

#include <iostream>
#include <vector>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
using namespace std;

vector<string> get_paths (string str)
{
   vector<string> result;
   while(!str.empty())
   {
      if (auto pos { str.find_first_of (':') }; pos == string::npos)
      {
         result.push_back(str);
         break;
      }
      else
      {
         result.emplace_back(str.substr(0, pos));
         str.erase(0, pos + 1);
      }
   }
   return move(result);
}

bool exist(const string& fname, int perm=F_OK) { return access(fname.c_str(), perm) == 0; }

int main (int argc, char *argv[])
{
   auto result { get_paths(getenv("PATH")) };
   for (auto pp : result)
   {
      string npath { pp };
      if (*npath.rbegin() != '/')
         npath += '/';
      npath += argv[1];
      if (exist(npath))
         cout << npath << endl;
   }
   return 0;
}

Upvotes: -1

scrutari
scrutari

Reputation: 1618

To build on one of the previous answers, you can use getenv to get the contents of PATH and then iterate over its components. Instead of using strchr you can use strsep:

#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <stdbool.h>

bool exists(const char fname[])
{
    return access(fname, F_OK | X_OK) != -1;
}

bool find_in_path(const char name[], char *fullpath, size_t sz) {
    char *paths = strdup(getenv("PATH"));
    char *tmp = paths; // to use in free
    const char *item;
    bool found = false;
    while ((item = strsep(&paths, ":")) != NULL) {
        snprintf(fullpath, sz, "%s/%s", item, name);
        if (exists(fullpath)) {
            found = true;
            break;
        }
    }
    free(tmp);
    return found;
}

int main() {
    char fullpath[512];
    bool found = find_in_path("uname", fullpath, sizeof(fullpath));
    if (found) {
        printf("found: %s\n", fullpath);
    }
    return 0;
}

Upvotes: 0

ninjaconcombre
ninjaconcombre

Reputation: 534

I had a similar need and resolved it by copying libc execvp code source. I did in the most cross platform I could think of(I have no guatanty and tested just on linux). If it's not such a matter to you and you care about performances, you should use acess or _acess. Note that there is no error check whatsoever and it will just return NULL or a founded openable file in path.

The accepted answer is sometime not acceptable, when you are willing to run the same small binary over and over, redoing the path search every time by calling execvp can be non negligable overhead.

So here is the code and associated tests, you will be mainely interested in the search_in_path_openable_file function.

.h file:

bool is_openable_file(char* path);
/*Return true if path is a readable file. You can call perror if return false to check what happened*/

char* search_in_path_openable_file(char* file_name);
/*Search into PATH env variable a file_name and return the full path of the first that is openable, NULL if not in path*/

char* search_executable(char* file_name);
/*Search file, if not openable and not absolute path(contain /), look for opennable file in the path. If nothing is openable, return NULL. If something is openable, return it as it is (not guaratented to have a full path, but garatanted to be openable)*/

.c file:

#include "file_info.h"
#include <stdio.h>
#include <string.h> //strcpy

/*I wanted to do a really cross platform way. access or _acess may be better*/
bool is_openable_file(char *path) {
    FILE *fp = fopen(path, "r");
    if (fp) {
        // exists
        fclose(fp);
        return true;
    }

    return false;
}

bool is_openable_file_until(char *path_begin, size_t until) {
    char old = path_begin[until];
    path_begin[until] = 0;
    bool res = is_openable_file(path_begin);
    path_begin[until] = old;
    return res;
}

/*You may thinks that libc would have done this function and use it to implement execp function family, but you would be wrong. They just hardcoded the search in every execp function. Unbelievable.
 *
 * So this function is a modification of their execvp function. 
 *
 * */

char* search_in_path_openable_file(char* file){
    char *path = getenv("PATH");
    if (path == NULL)
        return NULL;
    size_t pathlen = strlen(path);
    size_t len = strlen(file) + 1;
    int total_max_size=pathlen + len;
    char* buf=malloc(sizeof(char)*total_max_size);
    if (*file == '\0') {
        return NULL;
    }
    char *name, *p;
    /* Copy the file name at the top.  */
    name = memcpy(buf + pathlen + 1, file, len);
    /* And add the slash.  */
    *--name = '/';
    p = path;
    do {
        char *startp;
        path = p;
        //Let's avoid this GNU extension.
        //p = strchrnul (path, ':');
        p = strchr(path, ':');
        if (!p)
            p = strchr(path, '\0');
        if (p == path)
            /* Two adjacent colons, or a colon at the beginning or the end
               of `PATH' means to search the current directory.  */
            startp = name + 1;
        else
            startp = memcpy(name - (p - path), path, p - path);
        /* Try to execute this name.  If it works, execv will not return.  */
        if (is_openable_file(startp))
            return startp;
    } while (*p++ != '\0');
    /* We tried every element and none of them worked.  */
    return NULL;
}

char* search_executable(char* file_name){

    if (is_openable_file(file_name)){//See realpath manual bug. Watch out
        return file_name;
    }
    if (strchr (file_name, '/') != NULL) //Don't search when it contains a slash. 
        return NULL;
    return search_in_path_openable_file(file_name);
}

tests (As you see I did not test a lot this function, there may exist some problem, use at your risk):

 #include "file_info.h"
#include "munit.h"
#include <stdbool.h>
#include <unistd.h>
static void generate_search_executable(char* test_str, char* expected){
    char* res= search_executable(test_str);
     if (res==NULL)
        munit_assert_ptr(expected,==,NULL );
     else
    munit_assert_string_equal(expected,res);
}

static void generate_openable(char* test_str, bool expected){
    bool res= is_openable_file(test_str);
    munit_assert_true(expected==res);
}

static void generate_path_search(char* test_str, char* expected_res){
     char* res= search_in_path_openable_file(test_str);
     if (res==NULL)
        munit_assert_ptr(expected_res,==,NULL );
     else
    munit_assert_string_equal(expected_res,res);
}

//TODO do for other platform, better test would also set path to a custom folder that we control
#define EXISTING_FILE_NOT_IN_PATH "/usr/include/stdlib.h" 
#define EXISTING_FILE_IN_PATH "ls" 
#define EXISTING_FILE_IN_PATH_FULL "/bin/ls" 
#define NOT_EXISTING_FILE "/usrarfzsvdvwxv/ixvxwvnxcvcelgude/ssdvtdbool.h" 
int main() {

    generate_openable(EXISTING_FILE_IN_PATH, false);
    generate_openable(EXISTING_FILE_NOT_IN_PATH, true);
    generate_openable(NOT_EXISTING_FILE, false);

    generate_path_search(EXISTING_FILE_IN_PATH, EXISTING_FILE_IN_PATH_FULL);
    generate_path_search(NOT_EXISTING_FILE, NULL);
    generate_path_search(EXISTING_FILE_NOT_IN_PATH, NULL);

    generate_search_executable(EXISTING_FILE_IN_PATH, EXISTING_FILE_IN_PATH_FULL);
    generate_search_executable(NOT_EXISTING_FILE, NULL);
    generate_search_executable(EXISTING_FILE_NOT_IN_PATH, EXISTING_FILE_NOT_IN_PATH);

    generate_search_executable("", NULL );

    //test current folder existence(maybe it just depend on path containing .,I am not sure, in that case we should remove thoses tests
    generate_search_executable("file_info_test", "file_info_test" );


}

Upvotes: 0

user529758
user529758

Reputation:

Use the getenv() function.

char *paths = getenv("PATH");

To loop through the parts of the column-separated list of paths, use strchr():

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

char *dup = strdup(getenv("PATH"));
char *s = dup;
char *p = NULL;
do {
    p = strchr(s, ':');
    if (p != NULL) {
        p[0] = 0;
    }
    printf("Path in $PATH: %s\n", s);
    s = p + 1;
} while (p != NULL);

free(dup);

Upvotes: 7

bahaa
bahaa

Reputation: 195

Let which do the work for you

if (system("which gnuplot"))
    /* not installed or not in path or not executable or some other error */

If you need the full path for some reason, run which with popen.
Or run gnuplot with some flag which makes it return immediately with 0 */

if (system("gnuplot --version"))
    /* not installed ... */

Upvotes: -2

R.. GitHub STOP HELPING ICE
R.. GitHub STOP HELPING ICE

Reputation: 215287

To read the PATH environment variable, use getenv("PATH").

However, if you just want to run gnuplot if it's available, and perform some fallback action if it's not, then you should just try to run it (e.g. with fork and execvp or posix_spawnp) and handle the failure case.

Upvotes: 1

Oliver Charlesworth
Oliver Charlesworth

Reputation: 272567

Use getenv() to inspect the value of a particular environment variable.

Upvotes: 2

Related Questions