使用
os.path.expandvars
我可以在字符串中扩展环境变量,但需要注意:“格式错误的变量名称和对不存在变量的引用保持不变”(强调我的)。此外,os.path.expandvars
也扩展了转义的 \$
。
我想以类似 bash 的方式扩展变量,至少在这两点上。比较:
import os.environ
import os.path
os.environ['MyVar'] = 'my_var'
if 'unknown' in os.environ:
del os.environ['unknown']
print(os.path.expandvars("$MyVar$unknown\$MyVar"))
给出
my_var$unknown\my_var
与:
unset unknown
MyVar=my_var
echo $MyVar$unknown\$MyVar
这给出了
my_var$MyVar
,这就是我想要的。
以下实现与
os.path.expandvars
保持完全兼容,但通过可选参数提供了更大的灵活性:
import os
import re
def expandvars(path, default=None, skip_escaped=False):
"""Expand environment variables of form $var and ${var}.
If parameter 'skip_escaped' is True, all escaped variable references
(i.e. preceded by backslashes) are skipped.
Unknown variables are set to 'default'. If 'default' is None,
they are left unchanged.
"""
def replace_var(m):
return os.environ.get(m.group(2) or m.group(1), m.group(0) if default is None else default)
reVar = (r'(?<!\\)' if skip_escaped else '') + r'\$(\w+|\{([^}]*)\})'
return re.sub(reVar, replace_var, path)
以下是一些调用示例:
>>> expandvars("$SHELL$unknown\$SHELL")
'/bin/bash$unknown\\/bin/bash'
>>> expandvars("$SHELL$unknown\$SHELL", '')
'/bin/bash\\/bin/bash'
>>> expandvars("$SHELL$unknown\$SHELL", '', True)
'/bin/bash\\$SHELL'
试试这个:
re.sub('\$[A-Za-z_][A-Za-z0-9_]*', '', os.path.expandvars(path))
正则表达式应该匹配任何有效的变量名称,按照这个答案,并且每个匹配都将替换为空字符串。
编辑:如果您不想替换转义的变量(即
\$VAR
),请在正则表达式中使用否定的lookbehind断言:
re.sub(r'(?<!\\)\$[A-Za-z_][A-Za-z0-9_]*', '', os.path.expandvars(path))
(表示匹配项前面不应带有
\
)。
编辑 2:让我们将其设为一个函数:
def expandvars2(path):
return re.sub(r'(?<!\\)\$[A-Za-z_][A-Za-z0-9_]*', '', os.path.expandvars(path))
检查结果:
>>> print(expandvars2('$TERM$FOO\$BAR'))
xterm-256color\$BAR
变量
$TERM
扩展为其值,不存在的变量 $FOO
扩展为空字符串,并且 \$BAR
未被触及。
expandvars
的 pip 包正是这样做的。
pip3 install expandvars
from expandvars import expandvars
print(expandvars("$PATH:${HOME:?}/bin:${SOME_UNDEFINED_PATH:-/default/path}"))
# /bin:/sbin:/usr/bin:/usr/sbin:/home/you/bin:/default/path
它的优点是实现默认值语法(即
${VARNAME:-default}
)。
另一种解决方案 - 正如 @HuStmpHrrr 所指出的 - 是让
bash
评估你的字符串,这样你就不必在 python 中复制所有想要的 bash 功能。
不如我给出的其他解决方案那么高效,但它非常简单,这也是一个很好的功能:)
>>> from subprocess import check_output
>>> s = '$TERM$FOO\$TERM'
>>> check_output(["bash","-c","echo \"{}\"".format(s)])
b'xterm-256color$TERM\n'
附注谨防
"
和 \
的转义:在调用 \
之前,您可能需要将
\\
替换为
"
,将
\"
替换为
s
中的
check_output
这里有一个使用原始
expandvars
逻辑的解决方案:暂时将 os.environ
替换为代理对象,该代理对象使未知变量为空字符串。请注意,defaultdict
不起作用,因为 os.environ
对于转义问题,您可以将
r'\$'
替换为某个保证不在字符串中并且不会被扩展的值,然后将其替换回去。
class EnvironProxy(object):
__slots__ = ('_original_environ',)
def __init__(self):
self._original_environ = os.environ
def __enter__(self):
self._original_environ = os.environ
os.environ = self
return self
def __exit__(self, exc_type, exc_val, exc_tb):
os.environ = self._original_environ
def __getitem__(self, item):
try:
return self._original_environ[item]
except KeyError:
return ''
def expandvars(path):
replacer = '\0' # NUL shouldn't be in a file path anyways.
while replacer in path:
replacer *= 2
path = path.replace('\\$', replacer)
with EnvironProxy():
return os.path.expandvars(path).replace(replacer, '$')
我对各种答案不满意,需要更复杂一点来处理更多边缘情况,例如任意数量的反斜杠和 ${} 样式变量,但不想支付 bash eval 的成本。 这是我基于正则表达式的解决方案:
#!/bin/python
import re
import os
def expandvars(data,environ=os.environ):
out = ""
regex = r'''
( (?:.*?(?<!\\)) # Match non-variable ending in non-slash
(?:\\\\)* ) # Match 0 or even number of backslash
(?:$|\$ (?: (\w+)|\{(\w+)\} ) ) # Match variable or END
'''
for m in re.finditer(regex, data, re.VERBOSE|re.DOTALL):
this = re.sub(r'\\(.)',lambda x: x.group(1),m.group(1))
v = m.group(2) if m.group(2) else m.group(3)
if v and v in environ:
this += environ[v]
out += this
return out
# Replace with os.environ as desired
envars = { "foo":"bar", "baz":"$Baz" }
tests = { r"foo": r"foo",
r"$foo": r"bar",
r"$$": r"$$", # This could be considered a bug
r"$$foo": r"$bar", # This could be considered a bug
r"\n$foo\r": r"nbarr", # This could be considered a bug
r"$bar": r"",
r"$baz": r"$Baz",
r"bar$foo": r"barbar",
r"$foo$foo": r"barbar",
r"$foobar": r"",
r"$foo bar": r"bar bar",
r"$foo-Bar": r"bar-Bar",
r"$foo_Bar": r"",
r"${foo}bar": r"barbar",
r"baz${foo}bar": r"bazbarbar",
r"foo\$baz": r"foo$baz",
r"foo\\$baz": r"foo\$Baz",
r"\$baz": r"$baz",
r"\\$foo": r"\bar",
r"\\\$foo": r"\$foo",
r"\\\\$foo": r"\\bar",
r"\\\\\$foo": r"\\$foo" }
for t,v in tests.iteritems():
g = expandvars(t,envars)
if v != g:
print "%s -> '%s' != '%s'"%(t,g,v)
print "\n\n"
我遇到了同样的问题,但我会提出一种不同且非常简单的方法。
如果我们看看“转义字符”的基本含义(因为它们在打印机设备中开始),其目的是告诉设备“对接下来发生的事情做一些不同的事情”。这是一种离合器。在我们的特定情况下,我们遇到的唯一问题是当序列中有两个字符“\”和“$”时。
不幸的是,我们无法控制标准的os.path.expandvars,因此字符串会被传递lock、stock和barrel。然而,我们可以做的是欺骗该函数,使其在这种情况下无法识别“$”!最好的方法是将 $ 替换为任意“实体”,然后将其转换回来。
def expandvars(value):
"""
Expand the env variables in a string, respecting the escape sequence \$
"""
DOLLAR = r"\$"
escaped = value.replace(r"\$", r"\%s" % DOLLAR)
return os.path.expandvars(escaped).replace(DOLLAR, "$")
我使用了 HTML 实体,但任何合理的不太可能的序列都可以(随机序列可能更好)。我们可能会想象这种方法会产生不必要的副作用的情况,但它们应该是不太可能的,可以忽略不计。
你可以试试这个。受到 Seth Robertson 的回答的启发
def expand_vars(input_str: str, env_dict: Optional[dict[str, str]] = None) -> str:
"""
Method to expand variables in given string data.
Args:
input_str (str): String data in which variables need to be expanded.
env_dict (dict, optional): Environment variables dict. Defaults to os.environ.
Returns:
str: Expanded string data.
"""
if env_dict is None:
env_dict = dict(os.environ)
def replacer(match: re.Match) -> str:
prefix = match.group(1)
var_name = match.group(2) or match.group(3)
if var_name in env_dict:
return prefix + env_dict[var_name]
return prefix + (f"${{{var_name}}}" if match.group(3) else f"${var_name}")
pattern = r"(.*?)(?:\$(\w+)|\$\{(\w+)\})"
# Expand variables until no more changes occur
previous_str = None
while previous_str != input_str:
previous_str = input_str
input_str = re.sub(pattern, replacer, input_str)
return input_str