Git重命名多个文件和文件夹

45

我正试图在我的应用程序中重命名许多文件,并且需要能够通过git从应用程序根目录中的所有子目录中执行重命名 (即 git mv %filenamematch% %replacement%),仅替换匹配的文本。但我不擅长bash脚本。

更新:如果还可以重命名与之匹配的目录,那就太好了!


你有例子吗? '%filenamematch%'是什么样的?它总是在开头、结尾、中间还是其他位置?'%replacement%'是什么样子的? - GoZoner
它可以是person.rb或lookitisa_person_here.html。在这两种情况下,需要匹配和更改这个人,使其变成其他的东西。它还应该区分大小写。 - Jonathan
9个回答

47
这应该就可以解决问题了:
for file in $(git ls-files | grep %filenamematch% | sed -e 's/\(%filenamematch%[^/]*\).*/\1/' | uniq); do git mv $file $(echo $file | sed -e 's/%filenamematch%/%replacement%/'); done

要理解这段代码的作用,您需要了解管道符“|”和命令替换符“$(...)”。这些强大的shell构造允许我们组合多个命令以获得所需的结果。请参见PipelinesCommand Substitution
以下是这个一行代码的具体过程:
  1. git ls-files: 这将生成Git存储库中的文件列表。 它类似于您可以从ls获得的内容,但它仅输出Git项目文件。 从此列表开始确保不会触及您的.git /目录中的任何内容。

  2. | grep %filenamematch%: 我们通过grep将来自git ls-files的列表进行筛选,以将其缩小为仅包含我们要查找的单词或模式的文件名。

  3. | sed -e 's/\(%filenamematch%[^/]*\).*/\1/': 我们将这些匹配项通过sed(流编辑器)执行(-e)sed的s(替换)命令来筛选出匹配目录后的任何/和随后的字符(如果有)。

  4. | uniq: 在匹配是目录的情况下,现在我们已经削减了包含的目录和文件,可能会有许多匹配行。 我们使用uniq将它们全部变成一行。

  5. for file in ...: shell的“for”命令将遍历列表中的所有项目(文件名)。 每个文件名依次分配给变量“$file”,然后执行分号(;)后面的命令。

  6. sed -e 's/%filenamematch%/%replacement%/': 我们使用echo将每个文件名通过sed进行传递,再次使用其替换命令-这次对文件名执行模式替换。

  7. git mv: 我们使用此git命令将现有文件($file)移动到新文件名(由sed更改)。

更好地理解这一点的方法是观察每个步骤的独立性。为此,请在您的shell中运行以下命令,并观察输出。所有这些都是非破坏性的,只产生列表供您观察:

1.

git ls-files

2.

git ls-files | grep %filenamematch%

3.

git ls-files | grep %filenamematch% | sed -e 's/\(%filenamematch%[^/]*\).*/\1/'

4.

git ls-files | grep %filenamematch% | sed -e 's/\(%filenamematch%[^/]*\).*/\1/' | uniq

5.

for file in $(git ls-files | grep %filenamematch% | sed -e 's/\(%filenamematch%[^/]*\).*/\1/' | uniq); do echo $file; done

6.

for file in $(git ls-files | grep %filenamematch% | sed -e 's/\(%filenamematch%[^/]*\).*/\1/' | uniq); do echo $file | sed -e 's/%filenamematch%/%replacement%/'; done


1
当然。这是那种可能看起来有些令人生畏的bash一行代码。也许其他人会有关于使其更加简洁或易读的建议。我会添加一些解释性的注释。 - Jonathan Camenisch
1
注意:这个命令有一个缺陷:如果你有任何与替换模式匹配的目录,它们将会导致错误。换句话说,它将尝试使用 git mv path/person/file.rb path/alien/file.rb 进行移动,如果目标目录不存在,则可能会失败。为了解决这个问题,我们可以在搜索模式中添加一些逻辑,以确保匹配的字符串后面没有斜杠。 - Jonathan Camenisch
1
更健壮的模式大致如下:for file in $(git ls-files | grep '%filenamematch%[^/]*$); git mv $file $(echo $file | sed -e 's/%filenamematch%/%replacement%[^/]*$/')但是,我需要测试以确保我正确使用了grep和sed的正则表达式语法。它们并不总是完全与我习惯的相同。 - Jonathan Camenisch
1
原来你可能根本不需要使用 sed:只需运行 do git mv $file ${file//old/new}。参见:http://mywiki.wooledge.org/BashFAQ/100 - Gregg Lind
7
我必须使用“do %git mv ....%; done”将git移动语句包围起来,以使它按照 http://www.cyberciti.biz/faq/linux-unix-bash-for-loop-one-line-command/ 中所述正常工作。 - dmeu
显示剩余11条评论

20
使用命令rename和正则表达式重命名文件:
rename 's/old/new/' *

然后通过添加新文件和删除旧文件将更改在 Git 中进行注册:

git add .
git ls-files -z --deleted | xargs -0 git rm

在较新的 Git 版本中,您可以使用 --all 标志:

git add --all .

这和使用git mv命令移动一个文件是一样的吗?历史记录会被保留吗? - R.D.
是的,它们是一样的。git mv 命令始终等同于 git rmgit add 命令的组合,Git 不会像其他版本控制系统那样为移动操作存储额外的元数据。 - Flimm
不错的方法,易于理解。对我来说,rename old new * 是一个更简单的方式,其中每个文件名(*)中第一次出现的new被替换为old - Chris K
1
今天我学到了--all标志。我认为这是最清晰的方法来完成这个任务。 - Kyle Heironimus
这正是我一直在寻找的。rename 已经提供了一种干净的方式来重命名多个文件。我正在寻找像 git rename 而不是 git mv 这样的东西,而这实际上通过使用 rename 然后跟着一个简单的 git add --all 来完成相同的工作。这是最好的解决方案。 - Praveen

16

虽然来晚了,但这应该可以在BASH中运行(适用于文件和目录,但对于目录我会小心谨慎):

find . -name '*foo*' -exec bash -c 'file={}; git mv $file ${file/foo/bar}' \;

如果您能解释脚本中 before 和 after 的位置会很棒。在 *foo*/foo/bar 之间,我认为 foo 表示 before,bar 表示 after。 - Jonathan
2
是的。在你之前的评论中,是“foo”(指一个人)。之后是“bar”。BASH shell 语法 ${variable/pattern-to-match/replace-with} 被用来生成新名称。 - GoZoner
1
除非将 sh 替换为 bash,否则在 Ubuntu 14.04 上我会得到 sh: 1: Bad substitution 错误,请参见此处 - davetapley
@GoZoner 噢,${file/foo/bar}"shell 参数扩展" 技术 吗? - Nate Anderson

9

在shell循环中使用git mv

git-mv的目的是什么?

(假设你在一个合理的平台上!)

@jonathan-camenish的回答的基础上构建:

# things between backticks are 'subshell' commands.  I like the $() spelling over ``
# git ls-files     -> lists the files tracked by git, one per line
# | grep somestring -> pipes (i.e., "|") that list through a filter
#     '|' connects the output of one command to the input of the next
# leading to:  for file in some_filtered_list
# git mv  f1 f2  ->  renames the file, and informs git of the move.
# here 'f2' is constructed as the result of a subshell command
#     based on the sed command you listed earlier.

for file in `git ls-files | grep filenamematch`; do git mv $file `echo $file | sed -e 's/%filenamematch%/%replacement%/'`; done

这里是一个更长的例子(在bash或类似的语言中)

mkdir blah; cd blah; 
touch old_{f1,f2,f3,f4} same_{f1,f2,f3}
git init && git add old_* same_* && git commit -m "first commit"
for file in $(git ls-files | grep old); do git mv $file $(echo $file | sed -e 's/old/new/'); done
git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#   renamed:    old_f1 -> new_f1
#   renamed:    old_f2 -> new_f2
#   renamed:    old_f3 -> new_f3
#   renamed:    old_f4 -> new_f4
#

参见: 从Unix命令行进行Ad Hoc数据分析


基本上,无论何处使用“mv”,都应该使用“git mv”。如果需要更多的明确性,请在问题中更新一些重命名示例。 - Gregg Lind
更新了帖子,现在可能更清楚了,有什么想法吗?我不擅长Bash脚本编写。 - Jonathan
1
希望这个扩展示例有所帮助! - Gregg Lind
我真的认为这是问题的正确答案。GIT 能够跟踪文件移动,没有专有的复制脚本。谢谢! - anhoppe

4

这对我来说很有效:

ls folder*/*.css | while read line; do git mv -- $line ${line%.css}.scss; done;

解释:

  • ls folder*/*.css - 使用ls获取所有符合glob模式的CSS文件的目录列表(以folder开头并包含.css扩展名的目录)。
  • while read line - 逐行读取ls命令的输出结果。
  • do git mv -- $line ${line%.css}.css - 在每一行的输出结果上执行git mv命令($line变量包含每一行),匹配每个文件名的开头并排除.css扩展名(使用${line%),然后添加一个新的.scss扩展名(使用--来防止文件名和标志之间的歧义)。

下面的代码可以用于“干运行”(不会实际执行git mv):

ls variant*/*.css | while read line; do echo git mv $line to ${line%.css}.scss; done;

1
我使用https://github.com/75lb/renamer成功解决了这个问题。它没有明确执行git mv,但是Git似乎仍然可以完美地处理结果。
当我按照顶部答案中的所有步骤操作时,在最后一步卡住了,当控制台响应为for pipe quote>
如果有人能解释一下这是什么意思,我会很感激!

那看起来像是在提示需要输入一个闭合的 ) 或 ``` 字符。 - Jonathan Camenisch

1

以下是使用PowerShell进行操作的一般方法,使用foreach()

  1. 使用foreach($varname in Get-ChildItem …) {…}来迭代文件,使用变量$varname。这里我使用Get-ChildItem,因为这是该命令的官方名称,但您可以使用dirls或任何您已分配给Get-ChildItem的自定义别名。
  2. 使用[io.path]::ChangeExtension($varname, "new-ext"))更改扩展名。(感谢https://dev59.com/zGct5IYBdhLWcg3wZcmL#12120352。)
  3. 使用$(…)在新命令中评估子表达式,例如作为git mv的参数。

因此,如果您想使用git将所有*.bat文件重命名为*.cmd,则应使用以下命令。我已经很好地格式化了它,但您也可以将其全部输入到一行中。

foreach ($bat in Get-ChildItem *.bat) {
  git mv $bat $([io.path]::ChangeExtension($bat, "cmd"))
}

无法将以 .pre.py 结尾的文件更改为以 .py 结尾的文件。可能是因为它只计算扩展名中的第二个点。 - timwaagh

0

我通常使用NetBeans来做这种类型的工作,因为当有更简单的方法时,我会避免使用命令行。NetBeans对git有一些支持,并且您可以通过“收藏夹”选项卡在任意目录/文件上使用它。


0

这里是我如何将 ./src 文件夹中的所有 .less 文件重命名为 .scss

find ./src -type f |grep "\.less$" | while read line; do git mv -- $line ${line%.less}.scss; done;

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