子进程中的bash通配符匹配

0 投票
2 回答
711 浏览
提问于 2025-04-17 22:13

如果我有一个文件夹,想要删除里面除了一个文件以外的所有文件,我可能会在bash中这样做:

cd /tmp/a
rm -rf !(specialfile)
cd -

把这个操作用最简单的Python代码来写却失败了:

>>> subprocess.Popen( 'cd /tmp/a; rm -rf !(specialfile); cd -', stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).communicate()

并且出现了这个提示:

('', "/bin/sh: -c: line 0: syntax error near unexpected token `('\n/bin/sh: -c: line 0: `cd /tmp/a; rm -rf !(specialfile); cd -'\n")

在Python中,接下来最好的办法似乎是:

p = '/tmp/a'
    for i in os.listdir( p ):
    if i != 'specialfile':
        os.remove( os.path.join( p, i ) )

但当然,这样做对文件和子文件夹的处理效果并不好。有没有更好的方法呢?

2 个回答

2

更新:正如 @isedev 和 OP @JohnSchmitt 在评论中指出的,subprocess.Popen 调用的是 sh,而不是 bash(而且 sh 可能是也可能不是 bash,这取决于你使用的平台),但是使用扩展模式匹配操作符 !(...) 需要 (a) 使用 bash 并且 (b) 开启 extglob 选项(下面会有更多背景信息)。

因此,解决方法是:

  • 明确调用 bash,并通过 -c 命令行选项传递命令字符串。
  • 通过 -O 命令行选项开启 extglob shell 选项(如果不这样做,模式 !(specialfile) 会导致 OP 遇到的语法错误)。

借用 @JohnSchmitt 自己的评论,我们可以得到:

subprocess.Popen("bash -O extglob -c 'cd /tmp/a; rm -rf !(file2); cd -'",
  stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).communicate()

(不太优雅的替代方案是在 rm 命令之前,在 bash 命令字符串中添加 shopt -s extglob;。)

背景

!(specialfile) 是一个 扩展 模式匹配操作符的例子(可以查看 man bashPattern Matching 部分);这些扩展操作符默认是没有启用的;使用 shopt -s extglob 可以启用它们(使用 shopt -u extglob 可以禁用它们)。

1

你可以使用os.walk,正如@Bakuriu提到的那样。很重要的一点是,要从底部向上遍历文件夹树,这样才能确保在删除文件夹时,里面是空的,除了那个包含“specialfile”的文件夹。这就是为什么在os.rmdir命令中需要使用try语句的原因。

import os
for root, dirs, files in os.walk(top, topdown=False):
    for name in files:
        if name != 'specialfile':
            os.remove(os.path.join(root, name))
    for name in dirs:
        try:
            os.rmdir(os.path.join(root, name))
        except:
            pass

撰写回答