如何处理在使用git diff --name-only迭代结果时文件名中包含空格的情况

12

我正在处理的一个脚本需要遍历git diff中的每个文件。然而,我不知道如何处理文件名中的空格。任何有空格的文件都会被拆分成"2个文件"。我知道它们需要用" "包装起来,但是我不知道在传递到@参数之前如何实现。

当文件名中存在空格时,我应该如何迭代文件?

git diff --name-only  $1

这是一个简单的测试,可以重现该错误:

copyfiles()
{
    echo "Copying added files"
    for file in $@; do

        new_file=$(echo ${file##*/})

        directory=$(echo ${file%/*})
        echo "Full Path is is  $file"
        echo "File is  $new_file"
        echo "Directory is  $directory"
        cp $file $COPY_TO
    done    
}

COPY_TO="testDir"
DIFF_FILES=$( git diff --name-only  $1) 
copyfiles $DIFF_FILES 

目前脚本的运行方式如下:

test.sh <git commit id>
5个回答

7
git diff -z --name-only |
while read -d $'\0' file
do
    echo ${file}
done

1
你必须小心:read -d 只在 bash 中有效,而不是 sh。 - moi

6
--name-only命令的输出需要进行一定程度的转义,但是这样的输出方式并不方便使用。通过-z选项,git diff解释了这种转义(以及替代方法):

-z

当给出--raw、--numstat、--name-only或--name-status参数时,不混淆路径名并使用NUL作为输出字段终止符。

如果不使用此选项,则每个路径名输出将用制表符、换行符、双引号和反斜杠字符替换为\t、\n、\"和\\,如果其中任何一个替换发生,则路径名将用双引号括起来。

以下是一个示例:

$ git init ugh
$ cd ugh
$ touch 'spa ce' $'new\nline' $'t\tab'
$ ls # Unhelpful really
new?line  spa ce  t?ab
$ ls --quote # Minorly helpful but wrong (for shell usage)
"new\nline"  "spa ce"  "t\tab"
$ git add -A
$ git diff --cached --name-only
"new\nline"
spa ce
"t\tab"
$ git diff --cached --name-only -z # Doesn't copy and paste well and is a bit confusing to read this way
new
line^@spa ce^@t ab^@
$ printf %q\\n "$(git diff --cached --name-only -z )"
$'new\nlinespa cet\tab'

无论如何,这里的重点是最好使用-z输出并使用read读取文件列表。
while IFS= read -r -d '' file; do
    printf 'file = %q\n' "$file"
done < <(git diff --cached --name-only -z)

你也可以将git diff的输出重定向到while循环中,但如果你需要从循环内部获取变量,则需要使用此进程替换方法来避免管道方法的子shell问题。

1
这个答案非常有帮助。谢谢你。它让我能够将 git diff --name-only -z 的输出作为输入传递给 git diff/git difftool。我在这里演示了一下:https://dev59.com/hF4c5IYBdhLWcg3wSIgs#62853776 - Gabriel Staples

4

使用-z使git-diff使用空终止符。 例如:

export COPY_TO
git diff -z --name-only | xargs -0 sh -c 'for file; do
    new_file=$(echo ${file##*/})
    directory=$(echo ${file%/*})
    echo "Full Path is is  $file"
    echo "File is  $new_file"
    echo "Directory is  $directory"
    cp "$file" "$COPY_TO"
done' sh

请注意,更合理的解决方案是拒绝来自文件名中含有空格的人的拉取请求。

1
喜欢合理的解决方案,我不明白为什么源文件会有空格! - chrispepper1989
我可以问一下为什么只是在我的当前脚本中添加“-z”没有起作用吗? - chrispepper1989
我无法弄清如何以一种方式使用它,使我能够在每个文件上运行一个函数。 - chrispepper1989
1
如果您正在使用bash,则可以使用export -f导出函数,然后执行xargs -0 -I {} bash -c 'function_name {}'。这将为每个文件调用一次该函数,而不是传递多个文件名。但我强烈建议不要这样做,因为导出函数很麻烦。最好将其放在一个shell脚本中。 - William Pursell
哦,多奇怪啊,那么总是最好使用“do”吗? - chrispepper1989
显示剩余4条评论

0

我认为你的代码需要这个命令 IFS=$'\n'

echo "this command is important"

IFS=$'\n'
for file_change in `git diff --name-only $1`
do
    echo "Put $file_change ..."

    # File Name
    fileName=$(basename "$file_change")
    echo "$fileName"

    # Directory
    dir=$(dirname "$file_change")
    echo "$dir"
    

    # copy file
    cp $file_change $REMOTE_DIR$file_change
done

0

感谢@Etan Resiner的回答。以下是一个示例,展示如何使用git diff --name-only -z "$merge_base" $BACKUP_BRANCH的输出作为输入,将转义后的文件名发送到git diffgit difftool中。它需要额外的--,请参见下面的代码。

我使用它修复了我的git changes程序,现在它可以处理git仓库中具有空格或特殊字符(例如')的文件名。现在,该程序看起来像这样:

用法:

Usage: git changes <common_base> <backup_branch> [any other args to pass to git difftool]

git-changes.sh:

请特别注意填充files_changed_escaped变量,这是直接从@Etan Reisner的答案中学到的。
COMMON_BASE_BRANCH="$1"
BACKUP_BRANCH="$2"
# Obtain all but the first args; see:
# https://dev59.com/t2ox5IYBdhLWcg3wql9t#9057392
ARGS_3_AND_LATER="${@:3}"

merge_base="$(git merge-base $BACKUP_BRANCH $COMMON_BASE_BRANCH)"
files_changed="$(git diff --name-only "$merge_base" $BACKUP_BRANCH)"

echo "Checking for changes against backup branch \"$BACKUP_BRANCH\""
echo "only in these files which were previously-modified by that backup branch:"
echo "--- files originally changed by the backup branch: ---"
echo "$files_changed"
echo "------------------------------------------------------"
echo "Checking only these files for differences between your backup branch and your current branch."

# Now, escape the filenames so that they can be used even if they have spaces or special characters,
# such as single quotes (') in their filenames!
# See: https://dev59.com/hF4c5IYBdhLWcg3wSIgs#28109890
files_changed_escaped=""
while IFS= read -r -d '' file; do
    escaped_filename="$(printf "%q" "$file")"
    files_changed_escaped="${files_changed_escaped}    ${escaped_filename}"
done < <(git diff --name-only -z "$merge_base" $BACKUP_BRANCH)

# DEBUG PRINTS. COMMENT OUT WHEN DONE DEBUGGING.
echo "$files_changed_escaped"
echo "----------"
# print withOUT quotes to see if that changes things; ans: indeed, it does: this removes extra 
# spaces and I think will replace each true newline char (\n) with a single space as well 
echo $files_changed_escaped 
echo "=========="

# NB: the `--` is REQUIRED before listing all of the files to search in, or else escaped files
# that have a dash (-) in their filename confuse the `git diff` parser and the parser thinks they
# are options! It will output this error:
#       fatal: option '-\' must come before non-option arguments
# Putting the list of all escaped filenames to check AFTER the `--` forces the parser to know
# they cannot be options, because the `--` with nothing after it signifies the end of all optional
# args.
git difftool $ARGS_3_AND_LATER $BACKUP_BRANCH -- $files_changed_escaped
echo "Done."

您可以在我的dotfiles项目中下载git changes程序,链接在这里:https://github.com/ElectricRCAircraftGuy/eRCaGuy_dotfiles

它还包含一些其他的东西,例如git diffn,它是带有行号的git diff


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