32

What's the best way to expand

${MyPath}/filename.txt to /home/user/filename.txt

or

%MyPath%/filename.txt to c:\Documents and settings\user\filename.txt

with out traversing the path string looking for environement variables directly? I see that wxWidgets has a wxExpandEnvVars function. I can't use wxWidgets in this case, so I was hoping to find a boost::filesystem equivalent or similar. I am only using the home directory as an example, I am looking for general purpose path expansion.

Dan
  • 1,339
  • 3
  • 13
  • 19

9 Answers9

33

For UNIX (or at least POSIX) systems, have a look at wordexp:

#include <iostream>
#include <wordexp.h>
using namespace std;
int main() {
  wordexp_t p;
  char** w;
  wordexp( "$HOME/bin", &p, 0 );
  w = p.we_wordv;
  for (size_t i=0; i<p.we_wordc;i++ ) cout << w[i] << endl;
  wordfree( &p );
  return 0;
}

It seems it will even do glob-like expansions (which may or may not be useful for your particular situation).

MikeGM
  • 1,061
  • 9
  • 14
  • For me, this is the other half way. This solution with the one above work on both Posix and Window. – xryl669 May 27 '13 at 17:00
  • 2
    I would upvote this three times if I could. Great answer using the system-provided facilities which are certainly less prone to errors than regex stuff. – John Zwinck Sep 19 '14 at 06:19
  • 1
    N.B. this uses /bin/sh under the hood tho and forks processes to achieve the results. – teknopaul Jan 09 '20 at 22:36
  • 1
    Note that wordexp removed quotes, removes spaces e.t.c. Generally it works but your string will be seriously different after. For example having unmatched single quote is ok in filename but wordexp will remove it. – norekhov Apr 15 '20 at 11:54
  • not only will it fork processes under the hood, it will also do cmdsubstitution. therefore this is open for injection attacks: `wordexp("$(sudo reboot)")`. there's a `WRDE_NOCMD` flag, but at least for me this will simply make `wordexp` segfault whenever i pass it a command for substitution (making this open for DoS attacks) – umläute Jan 21 '22 at 10:30
24

On Windows, you can use ExpandEnvironmentStrings. Not sure about a Unix equivalent yet.

Rob Kennedy
  • 161,384
  • 21
  • 275
  • 467
  • Thanks Rob. This gets me half way there. I guess I'll look into parsing method for non windows cases. – Dan Dec 15 '09 at 13:08
24

If you have the luxury of using C++11, then regular expressions are quite handy. I wrote a version for updating in place and a declarative version.

#include <string>
#include <regex>

// Update the input string.
void autoExpandEnvironmentVariables( std::string & text ) {
    static std::regex env( "\\$\\{([^}]+)\\}" );
    std::smatch match;
    while ( std::regex_search( text, match, env ) ) {
        const char * s = getenv( match[1].str().c_str() );
        const std::string var( s == NULL ? "" : s );
        text.replace( match[0].first, match[0].second, var );
    }
}

// Leave input alone and return new string.
std::string expandEnvironmentVariables( const std::string & input ) {
    std::string text = input;
    autoExpandEnvironmentVariables( text );
    return text;
}

An advantage of this approach is that it can be adapted easily to cope with syntactic variations and deal with wide strings too. (Compiled and tested using Clang on OS X with the flag -std=c++0x)

sfkleach
  • 707
  • 6
  • 9
  • 4
    g++ 4.9.3 (Ubuntu) cannot compile, it's something related to conversion between iterator and const_iterator. I have to change text.replace( match[0].first, match[0].second, var ); to text.replace( match.position(0), match.length(0), var ); – fchen Mar 24 '16 at 15:38
  • Tried it with `gcc version 5.4.0 (Ubuntu)` with `g++ -std=c++11 -c -Wall expand.cpp` but didn't get any error. – sfkleach Jun 25 '18 at 22:55
  • If you could include the output (or send it to me) that would be great. I can't duplicate any errors on Ubuntu 18.04, sorry. – sfkleach Oct 06 '18 at 13:18
  • Does not work on gcc 4.8 and below because regex expressions are implemented on gcc 4.9 https://stackoverflow.com/questions/12530406/is-gcc-4-8-or-earlier-buggy-about-regular-expressions – Lucas Coelho Dec 03 '19 at 18:44
  • See my answer for a modified version of this to support leaving missing vars alone for other parsers or processes to handle instead of removing, similar to `os.path.expandvars` in Python – Sherwin F Mar 20 '23 at 14:51
7

Simple and portable:

#include <cstdlib>
#include <string>

static std::string expand_environment_variables( const std::string &s ) {
    if( s.find( "${" ) == std::string::npos ) return s;

    std::string pre  = s.substr( 0, s.find( "${" ) );
    std::string post = s.substr( s.find( "${" ) + 2 );

    if( post.find( '}' ) == std::string::npos ) return s;

    std::string variable = post.substr( 0, post.find( '}' ) );
    std::string value    = "";

    post = post.substr( post.find( '}' ) + 1 );

    const char *v = getenv( variable.c_str() );
    if( v != NULL ) value = std::string( v );

    return expand_environment_variables( pre + value + post );
}

expand_environment_variables( "${HOME}/.myconfigfile" ); yields /home/joe/.myconfigfile

mouviciel
  • 66,855
  • 13
  • 106
  • 140
user3116736
  • 111
  • 1
  • 5
  • Note that this only expands a single environment variable. Which is OK for the example. If you need to expand multiple environment variables in one string, calls expane_environment_variables recursively on post. – tbleher Jul 07 '14 at 13:09
  • 1
    Note: you call `getenv(variable.c_str())` twice for getting the same var, better to store result. – vladon Sep 06 '15 at 19:10
  • 2
    You're missing "char" after const* v – user997112 Sep 10 '18 at 10:58
3

As the question is tagged "wxWidgets", you can use wxExpandEnvVars() function used by wxConfig for its environment variable expansion. The function itself is unfortunately not documented but it basically does what you think it should and expands any occurrences of $VAR, $(VAR) or ${VAR} on all platforms and also of %VAR% under Windows only.

VZ.
  • 21,740
  • 3
  • 39
  • 42
2

Within the C/C++ language, here is what I do to resolve environmental variables under Unix. The fs_parm pointer would contain the filespec (or text) of possible environmental variables to be expanded. The space that wrkSpc points to must be MAX_PATH+60 chars long. The double quotes in the echo string are to prevent the wild cards from being processed. Most default shells should be able to handle this.


   FILE *fp1;

   sprintf(wrkSpc, "echo \"%s\" 2>/dev/null", fs_parm);
   if ((fp1 = popen(wrkSpc, "r")) == NULL || /* do echo cmd     */
       fgets(wrkSpc, MAX_NAME, fp1) == NULL)/* Get echo results */
   {                        /* open/get pipe failed             */
     pclose(fp1);           /* close pipe                       */
     return (P_ERROR);      /* pipe function failed             */
   }
   pclose(fp1);             /* close pipe                       */
   wrkSpc[strlen(wrkSpc)-1] = '\0';/* remove newline            */

For MS Windows, use the ExpandEnvironmentStrings() function.

Dennis
  • 37
  • 1
  • 2
    like the `wordexp` solution, this is wide open for injection attacks: `fs_parm = "$(sudo reboot)";`. not only is it open to command substitution, it's also a text-book "quotation injection": `fs_parm ="foo\"; sudo reboot\"". – umläute Jan 21 '22 at 10:37
1

This is what I use:

const unsigned short expandEnvVars(std::string& original)
{
    const boost::regex envscan("%([0-9A-Za-z\\/]*)%");
    const boost::sregex_iterator end;
    typedef std::list<std::tuple<const std::string,const std::string>> t2StrLst;
    t2StrLst replacements;
    for (boost::sregex_iterator rit(original.begin(), original.end(), envscan); rit != end; ++rit)
        replacements.push_back(std::make_pair((*rit)[0],(*rit)[1]));
    unsigned short cnt = 0;
    for (t2StrLst::const_iterator lit = replacements.begin(); lit != replacements.end(); ++lit)
    {
        const char* expanded = std::getenv(std::get<1>(*lit).c_str());
        if (expanded == NULL)
            continue;
        boost::replace_all(original, std::get<0>(*lit), expanded);
        cnt++;
    }
    return cnt;
}
Andreas
  • 11
  • 1
0

Using Qt, this works for me:

#include <QString>
#include <QRegExp>

QString expand_environment_variables( QString s )
{
    QString r(s);
    QRegExp env_var("\\$([A-Za-z0-9_]+)");
    int i;

    while((i = env_var.indexIn(r)) != -1) {
        QByteArray value(qgetenv(env_var.cap(1).toLatin1().data()));
        if(value.size() > 0) {
            r.remove(i, env_var.matchedLength());
            r.insert(i, value);
        } else
            break;
    }
    return r;
}

expand_environment_variables(QString("$HOME/.myconfigfile")); yields /home/martin/.myconfigfile (It also works with nested expansions)

Martin A
  • 9
  • 4
0

I needed the ability to parse nested env variables while leaving untouched those not found in the environment for another parser to handle so I came up with this based on @sfkleach's excellent answer:

#include <string>
#include <regex>

// Update the input string.
void autoExpandEnvironmentVariables(std::string& text) {
    using namespace std;
    static regex envRegex("\\$(\\w+|\\{\\w+\\})", regex::ECMAScript);

    // 0,1 indicates to get the full match + first subgroup
    size_t offset = 0;
    const string matchText = text;
    sregex_token_iterator matchIter(matchText.begin(), matchText.end(), envRegex, {0, 1});
    for (sregex_token_iterator end; matchIter != end; ++matchIter) {
        const string match = matchIter->str();
        string envVarName = (++matchIter)->str();
        
        // Remove matching braces
        if (envVarName.front() == '{' && envVarName.back() == '}') {
            envVarName.erase(envVarName.begin());
            envVarName.erase(envVarName.end()-1);
        }
        
        // Search for env var and replace if found
        const char * s = getenv(envVarName.c_str());
        if (s != nullptr) {
            string value(s);

            // Handle nested env vars
            autoExpandEnvironmentVariables(value);
            
            // Since we're manipulating the string, do a new find
            // instead of using original match info
            size_t pos = text.find(match, offset);
            if (pos != string::npos) {
                text.replace(pos, match.length(), value);
                offset = pos + value.length();
            }
        } else {
            offset += match.length();
        }
    }
}
Sherwin F
  • 658
  • 7
  • 13
  • Some very nice features in this answer compared with my answer. I like the separate handling of the nested environment variables and the better regex used for finding matches. Using matchIter is very attractive but I am slightly concerned about whether or not it robustly makes progress when updating "${foo}${bar}" and $foo expands to an empty string (non-null). The iterator finds matches based on indexes so it presumably bumps the index in order to ensure it doesn't find the same match, which might cause it to skip `${bar}`. – sfkleach Mar 21 '23 at 10:28
  • string (non-null). UPDATE: Tested using g++ 11.3.0 (Cygwin) and unfortunately I found quite a few issues. In particular even substituting non-empty short values seemed to confuse the iterator causing it to skip. – sfkleach Mar 21 '23 at 10:42
  • @sfkleach Thanks for the feedback, I created test cases for longer vars, but didn't consider short or empty ones. Mutating the string while iterating was a bad idea so I've updated it to match on a static copy. Let me know if you find any other issues. – Sherwin F Mar 21 '23 at 18:27