Home > Software engineering >  Best way to open files using std::ifstream when the current directory is not the executable director
Best way to open files using std::ifstream when the current directory is not the executable director

Time:01-04

I have two programs. Program A spawns the child process Program B. Program B uses std::ifstream("file.txt") to read in a file. However, Program A is located in /programs/a/ while Program B is in /programs/a/children/b/. Meaning that when I spawn B as a child process, Program B thinks its current directory is /programs/a and looks for file.txt there (but file.txt is actually in /programs/a/children/b).

I know that the first argument passed to Program B is always the executable path, so I can just take that and modify my calls to file I/O functions to prefix the path with that. But how do I do this in a good way? Is there a convenient way to get this to work without changing much code in Program B? I want to be able to run Program B on its own, as well as from Program A, and in both cases it should be able to read file.txt. That's why I can't just hardcode the filepath, I want it to work in both cases without changing any code between them.

For instance, I can think of making a wrapper class and then changing every call of std::ifstream so that it calls the wrapper instead, kind of like this:

std::ifstream Wrapper::ifstream(const std::string& path)
{
  return std::ifstream(prefix   path); // prefix is member variable
}

But for a project with many calls to ifstream this is inconvenient and possibly inconsistent. Any accidental use of ifstream directly could cause a crash. I would also need to do the same thing for any output filestreams too. And I would really like to minimize any external dependencies, so I'd prefer if there was a built-in way to avoid this problem. It would be nice if I could just toggle some kind of option in the ifstream class so that any call to ifstream("file.txt") becomes ifstream("path/to/file.txt"). Is this the best I can do, or is there any better option out there?

--

Edit: This is not a duplicate of this question because that person wants to know how to get the executable path. I can get the path just fine, I'm just wondering if there's a more convenient way of USING the path without modifying hundreds of calls to ifstream in my code.

CodePudding user response:

There is a standard (since C 17) way to figure this out.

You can use std::filesystem::canonical to get the current executable's location. Then get the location of the parent directory and append the filename you want.

An example is:

namespace fs = std::filesystem;
int main( int argc, char* argv[] ) {
    fs::path path( fs::canonical( argv[0] ) );
    fs::path file = path.parent_path() / "file.txt";
    std::cout << file << std::endl;
}

This should print /programs/a/children/b/file.txt

CodePudding user response:

Here is an illustration of using an environment variable as a common information that any program started from a shell in which it is defined can use. The writer writes a short text to a file, the reader reads it out. Both programs try to read the environment variable A_B_PROG_FILE which should contain the path to a file (which may not exist). This variable name is hard-coded common information; if we had the opportunity to use a common header (but not a duplicated one!) we could as well communicate the path itself that way; this is something to consider. But let's assume that the programs do not share a build environment.

First the writer.cpp:

#include <iostream>
#include <fstream>
#include <cstdlib>

/** @return -1 if file could not be opened, -2 if a write error occurred,
    or 0 on success 
*/
int main(int argc, char **argv)
{
    const char *envPath = std::getenv("A_B_PROG_FILE");
    const char *fpath = envPath ? envPath : "/tmp/defaultname.txt";
    std::ofstream f(fpath);
    if(!f) 
    { 
        std::cerr << "Couldn't open " << fpath << " for writing, exiting\n";
        return -1;
    }
    
    f << "Hello, file ->" << fpath << "<- here!\n" 
      << "This was written by ->" << (argc? argv[0] : "Unknown exe path") << "<-\n";

    return f ? 0 : -2; // if f is not good exit with error code.
    
}

The reader is very similar:

#include <iostream>
#include <fstream>
#include <cstdlib> // getenv()
/** @return -1 if file could not be opened, -2 if a read error occurred,
    or 0 on success 
*/
int main()
{
    const char *envPath = std::getenv("A_B_PROG_FILE");
    const char *fpath = envPath ? envPath : "/tmp/defaultname.txt";
    std::ifstream f(fpath);
    if(!f) 
    { 
        std::cerr << "Couldn't open " << fpath << " for reading, exiting\n";
        return -1;
    }
    std::cout << "file ->" << fpath << "<- contains: ->";
    char c;
    while(f.get(c))
    {
        std::cout << c;
    }
    std::cout << "<-";
    if(!f.eof())
    { 
        std::cerr << "Read error from ->" << fpath << "<-, exiting\n";
        return -2;  
    }
}

Here is a sample session:

$ ls /tmp
$ unset A_B_PROG_FILE
$ ./reader
Couldn't open /tmp/defaultname.txt for reading, exiting
$ ./writer
$ ./reader
file ->/tmp/defaultname.txt<- contains: ->Hello, file ->/tmp/defaultname.txt<- here!
This was written by ->./writer<-
<-
$ export A_B_PROG_FILE=/tmp/ab
$ ./reader
Couldn't open /tmp/ab for reading, exiting
$ ./writer
$ ./reader
file ->/tmp/ab<- contains: ->Hello, file ->/tmp/ab<- here!
This was written by ->./writer<-
<-
$
  •  Tags:  
  • Related