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<-
<-
$
