扩展具有环境变量路径的文件名

32

什么是扩展的最佳方式?

${MyPath}/filename.txt to /home/user/filename.txt
或者
%MyPath%/filename.txt to c:\Documents and settings\user\filename.txt

是否有不需要遍历路径字符串而直接查找环境变量的方法? 我看到 wxWidgets 有一个 wxExpandEnvVars 函数。但在这种情况下,我不能使用 wxWidgets,所以我希望能找到 boost::filesystem 的等效函数或类似函数。 我只举了 home 目录作为例子,实际上我正在寻找通用的路径展开方法。

9个回答

33

如果针对UNIX(或至少是POSIX)系统,请查看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;
}

看起来它甚至会执行类glob的扩展(这可能对您的特定情况有用,也可能没有用)。


对我而言,这是另一半的路程。上述解决方案可在Posix和Window上运行。 - xryl669
2
如果可以的话,我会给这个答案点赞三次。使用系统提供的工具确实比正则表达式更不容易出错,这是一个很好的回答。 - John Zwinck
1
注意:这在幕后使用/bin/sh,并分叉进程以实现结果。 - teknopaul
1
请注意,wordexp会删除引号、空格等内容。通常情况下它是有效的,但是你的字符串将会变得和原来大不相同。例如,在文件名中有未匹配的单引号是可以的,但是wordexp会将其删除。 - norekhov
它不仅会在幕后分叉进程,还会执行cmdsubstitution。因此,这容易受到注入攻击:“wordexp("$(sudo reboot)")”。有一个“WRDE_NOCMD”标志,但是对于我来说,只要我传递一个命令进行替换,它就会使wordexp崩溃(从而使其容易受到DoS攻击)。 - umläute

24

如果您有使用C++11的奢侈条件,那么正则表达式非常方便。我写了一个用于原地更新和一个声明式版本。

#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;
}

这种方法的优点是可以轻松地适应句法变化并处理较长字符串。(在OS X上使用Clang编译和测试,使用标志-std=c++0x)


4
g++ 4.9.3 (Ubuntu) 无法编译,与迭代器和const_iterator的转换有关。我必须将text.replace(match[0].first, match[0].second, var); 改为text.replace(match.position(0), match.length(0), var);。 - fchen
尝试使用“gcc版本5.4.0(Ubuntu)”和“g++ -std=c ++ 11 -c -Wall expand.cpp”进行了测试,但没有发现任何错误。 - sfkleach
如果您能够包含输出(或将其发送给我),那就太好了。很抱歉,我无法在Ubuntu 18.04上复制任何错误。 - sfkleach
无法在gcc 4.8及以下版本上工作,因为正则表达式是在gcc 4.9上实现的。https://dev59.com/MWcs5IYBdhLWcg3w1nhq - Lucas Coelho
请看我的回答,其中包含修改后的版本,支持使其他解析器或进程处理缺失的变量而不是删除它们,类似于Python中的os.path.expandvars - Sherwin F

24

在Windows上,您可以使用ExpandEnvironmentStrings函数来实现。目前还不确定是否有Unix等效的函数。


谢谢Rob。这让我完成了一半的工作。我想我会研究一下非Windows情况下的解析方法。 - Dan

7
简单便携:
#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" ); 可以得到 /home/joe/.myconfigfile


该函数可以将 "${HOME}/.myconfigfile" 中的环境变量(如 HOME)展开为实际值(如 /home/joe),从而得到完整的配置文件路径。

请注意,这仅扩展单个环境变量。对于示例来说是可以的。如果您需要在一个字符串中扩展多个环境变量,请在后面递归调用expand_environment_variables。 - tbleher
1
注意:为了获取同一个变量,调用 getenv(variable.c_str()) 两次并不是最好的做法,最好将结果存储起来。 - vladon
2
你在 const* v 后面缺少了 "char"。 - user997112

3
由于该问题标记为“wxWidgets”,因此您可以使用wxConfig用于其环境变量扩展的wxExpandEnvVars()函数。不幸的是,该函数本身没有记录,但基本上它会在所有平台上扩展任何出现的$VAR$(VAR)${VAR},以及仅在Windows下的%VAR%

2

在C/C++语言中,我会这样解决Unix下的环境变量。fs_parm指针应包含可能要扩展的环境变量的文件规范(或文本)。wrkSpc指向的空间必须长MAX_PATH+60个字符。echo字符串中的双引号是为了防止通配符被处理。大多数默认shell都应该能够处理这个问题。


   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            */

对于MS Windows, 使用ExpandEnvironmentStrings()函数。


2
就像“wordexp”解决方案一样,这种方法容易受到注入攻击的影响:fs_parm = "$(sudo reboot)";。它不仅开放了命令替换的可能性,还是一个典型的“引号注入”漏洞:fs_parm ="foo\"; sudo reboot\"" - umläute

1
这是我使用的内容:
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;
}

0
我需要能够解析嵌套的环境变量,同时保留那些在环境中未找到的变量以供另一个解析器处理,因此我基于@sfkleach的优秀答案想出了以下方法:
#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();
        }
    }
}

与我的回答相比,这个答案有一些非常好的特性。我喜欢处理嵌套环境变量的单独方式和更好的正则表达式用于查找匹配项。使用matchIter非常有吸引力,但我稍微担心在更新"${foo}${bar}"且$foo扩展为空字符串(非null)时它是否能够健壮地取得进展。迭代器基于索引找到匹配项,因此它可能会提高索引以确保不找到相同的匹配项,这可能会导致跳过"${bar}"。 - sfkleach
字符串(非空)。 更新:使用 g++ 11.3.0(Cygwin)进行测试,不幸的是我发现了相当多的问题。 特别是即使替换非空短值,似乎也会混淆迭代器,导致它跳过。 - sfkleach
@sfkleach 感谢您的反馈,我已经为更长的变量创建了测试用例,但没有考虑短或空的变量。在迭代时改变字符串是一个坏主意,所以我已经更新它来匹配静态副本。如果您发现任何其他问题,请告诉我。 - Sherwin F

0

使用Qt,这对我有效:

#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"))函数,可以得到/home/martin/.myconfigfile的结果。(它也支持嵌套扩展)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接