为什么命令中的星号会导致for循环扩展?

为什么命令中的星号会导致for循环扩展?

我想写一个可以运行的脚本

git diff --name-status master..<BRANCH>

但是当我运行这个时:

for i in $(git branch | grep -v master); do
   echo $i;
done

我得到回显一个目录,因为git branch回显星号(我在当前目录中有一个目录)

* <SELECTED BRANCH>

为什么*会扩展以及如何防止扩展?

更新:我可以通过使用以下方法来防止这种情况:

for i in $(git branch | tr -d '*' | grep -v master);
done;

但为什么会出现这种情况呢?为什么我需要删除星号?

答案1

不带引号的变量和命令替换,例如$ior$(git …)应用分割+全局运算符到字符串结果。那是:

  1. 构建一个包含变量值或命令输出的字符串(在后一种情况下减去最后的换行符)。
  2. 根据值将字符串拆分为单独的字段IFS
  3. 将每个字段解释为通配符模式,并将其替换为匹配文件名列表;如果模式与任何文件都不匹配,则保持原样。

git branch | grep -v master(步骤 1)的输出包含* master被分割(步骤 2)为两个字段*master*被当前目录中的文件名列表替换(步骤 3)。

您可以运行set -f暂时禁用通配符。另一种方法是避免命令替换,而是解析输入read。但两者都没有做正确的事情——你会得到虚假的分支名称,因为 git 输出包含的内容超出了你的需要(*指示当前分支的标志,还有remotes/somewhere/foo -> bar远程跟踪分支之类的东西)。我认为以下内容如果不优雅的话是安全的:

for i in $(git branch | sed -e 's/^..//' -e 's/ .*//'); do
   echo $i
done

我认为最可靠的方法是使用git-for-each-ref

for i in $(git for-each-ref --format='%(refname:short)' refs/heads refs/remotes); do
  echo $i
done

答案2

像这样的文本流应该使用循环来读取while,而不是for,这可能是导致问题的原因(尽管我无法重现它)。一个简单的方法可以做到这一点:

git branch | while IFS= read -r line
do
    echo "${line:2}"
done

比较:

$ cd -- "$(mktemp -d)"
$ git init
Initialized empty Git repository in /tmp/tmp.MJFmu7q7EH/.git/
$ touch README
$ git commit --allow-empty -m "Initial commit"
[master (root-commit) da46b03] Initial commit
$ git branch foo
$ git branch
  foo
* master
$ for i in $(git branch)
> do
>    echo $i
> done
foo
*
master
$ git branch | while IFS= read -r line
> do
>     echo "${line:2}"
> done
foo
master

相关内容