修改xargs中的替换字符串

80

有时候我使用 xargs 命令时,并不需要显式地使用替换字符串:

find . -name "*.txt" | xargs rm -rf
在其他情况下,我想要指定替换字符串以执行像这样的操作:
find . -name "*.txt" | xargs -I '{}' mv '{}' /foo/'{}'.bar

前一个命令将当前目录下所有的文本文件移动到 /foo 并将扩展名改为 bar

如果我想修改替换字符串而不是在其末尾添加一些文本,那该怎么做呢?比如说,我想要实现与上一个示例中相同的功能,但文件应该被重命名/移动到 /foo/<name>.bar.txt(而不是 /foo/<name>.txt.bar)。

更新:我成功找到了解决方案:

find . -name "*.txt" | xargs -I{} \
    sh -c 'base=$(basename $1) ; name=${base%.*} ; ext=${base##*.} ; \
           mv "$1" "foo/${name}.bar.${ext}"' -- {}

但我想知道是否有更短/更好的解决方案。


1
不,除非我会更多地引用 mv "$1" "foo/${name}.bar.${ext}" 并且你可以这样使用 basename: base=${1##*/}。你应该将你的解决方案发布为答案并接受它。 - Dennis Williamson
@DennisWilliamson 谢谢您的评论!我会再等一段时间,看看是否有人想出了什么花哨的东西,否则我会自己回答这个问题。 - betabandido
1
我认为如果你的文件名是行末的最后一件事,那么在命令行中就不需要-I{}或{}。 (请注意,xargs的目的是分组,因此如果您不想在1个xargs参数调用的末尾有多个内容,则需要“-l 1”(对于某些版本的xargs为-L 1)。 -I{}意味着-l 1,所以这也适用于这里。) - Michael Campbell
“sh -c” 对我来说很关键!谢谢!否则我的“echo {}|sed '...'”中的cut/sed命令将无法执行。 - Julian
8个回答

58
下面这个命令使用xargs构建移动命令,将第二个'.'替换为'.bar.',然后在mac OSX上使用bash执行命令。
ls *.txt | xargs -I {} echo mv {} foo/{} | sed 's/\./.bar./2' | bash

我会在这里使用 echo 而不是 ls - Tripp Kinetics
@TrippKinetics,您能否详细说明在这里如何使用echo?以及为什么要使用它? - Asrail
ls 输出不应直接由文本处理工具进行处理。它专门设计用于用户输出。当输出到文本处理工具时,其行为并不总是一致的。与其使用 ls *.txt | pipecommand,最好使用 echo *.txt | pipecommand,有时甚至可以使用 find . -type f -name '*.txt' -print | pipecommand - Tripp Kinetics

36

在一次遍历中可以实现这个(在GNU中测试过),避免使用临时变量赋值。

find . -name "*.txt" | xargs -I{} sh -c 'mv "$1" "foo/$(basename ${1%.*}).new.${1##*.}"' -- {}

5
我喜欢你使用替换字符串作为shell的位置参数进行替换和扩展的方式,非常酷。 - GL2014
好奇为什么你在结尾使用了-- {}而不是简单的{}。我的意思是为什么不这样做? find . -name ".txt" | xargs -I{} sh -c 'mv "$0" "foo/$(basename ${0%.}).new.${1##*.}"' {} - mac
1
@mac:(回复有点晚了,抱歉!)因为在处理符合“*.txt”模式的文件时,你永远无法预先知道会遇到什么。你可能会得到以“-”开头的文件,并且你想避免将它们与选项混淆。 - Cbhihe
有人能解释一下最后的 -- {} 是如何应用的吗?它是作用于 xargs 还是 sh 上? - Collin
@CollinGonzalez 中的 -- 告诉 sh 命令不会再有其他选项,以防其中一个文件以 - 开头。最终的 {} 将替换为 xargs 通过 -I{} 选项接收到的参数。这反过来又传递给子进程,该子进程将 mv 命令作为 $1 运行。 - Santrix
1
@Santrix 我认为在这种情况下你的回答中有一个重要的事情。当使用-c命令调用sh时,第一个参数将被赋给$0。因此,如果你没有使用--,那么你实际的文件路径参数将被放置在$0而不是$1中。 - masonCherry

21
在这种情况下,使用while循环更易读:
find . -name "*.txt" | while IFS= read -r pathname; do
    base=$(basename "$pathname"); name=${base%.*}; ext=${base##*.}
    mv "$pathname" "foo/${name}.bar.${ext}"
done

请注意,您可能会在不同的子目录中找到相同名称的文件。您是否接受使用mv将重复文件覆盖?


是的,我知道它有那个“问题”。实际上,我发布的示例相当愚蠢。我只是为了在我的问题中展示一个简单的例子,所以没问题 :) - betabandido
2
处理包含换行符的文件名,使用以下命令:find . -name "*.txt" -print0 | while read -r -d '' pathname; do ... - Cem
4
请注意,如果您尝试使用“--max-procs”并行化xargs,则此(串行)方法将无效。 - user67416

12

2
我还建议使用GNU Parallel而不是xargs,它更强大和灵活。至于安装,我强烈建议使用您的发行版软件包管理器(例如sudo apt-get install parallel)或按照Parallel的推荐安装过程进行安装(如http://git.savannah.gnu.org/cgit/parallel.git/tree/README所述):`(wget -O - pi.dk/3 || curl pi.dk/3/ || fetch -o - http://pi.dk/3) | bash` - MestreLion
6
@MestreLion请不要推荐使用curl管道到shell的反模式。什么是pi.dk?为什么我应该相信这个域名?gnu.org至少更可信。但是,这两个URL都没有使用TLS,更重要的是,它们都没有验证下载内容的加密签名。唯一可以建议的解决方案是使用apt-get - user712624
正如您所看到的,pi.dk/3 对加密签名进行验证,如果签名不匹配,则停止运行。如果您不想直接将其传输到 shell 中,可以将其保存到文件中,检查代码,然后再运行它。 - Ole Tange
2
@blujay:这不是我的建议,而是来自官方文档:http://www.gnu.org/software/parallel/parallel_tutorial.html#Prerequisites。虽然我同意这不如使用你的发行版的软件包管理器好,但它比简单的`wget ... && chmod +x`要好得多。 - MestreLion
3
很遗憾这个内容出现在Parallel网站上,你不应该再传播它。它并不比wget... 命令更好,因为它存在许多危险性(详见https://www.seancassidy.me/dont-pipe-to-your-shell.html)。这相当于告诉Windows用户从一个随意的网站运行一个exe文件而不扫描病毒。坚决拒绝。 - user712624
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - MestreLion

2
文件应该从<name>.txt 重命名/移动到/foo/<name>.bar.txt 您可以使用 rename 实用程序,例如:
rename s/\.txt$/\.txt\.bar/g *.txt

提示:替换语法类似于sedvim

然后使用mv将文件移动到某个目标目录:

mkdir /some/path
mv *.bar /some/path

要将文件根据其名称的某部分重命名到子目录中,请检查以下内容:

-p/--mkpath/--make-dirs 在目标路径中创建任何不存在的目录。


测试:

$ touch {1..5}.txt
$ rename --dry-run "s/.txt$/.txt.bar/g" *.txt
'1.txt' would be renamed to '1.txt.bar'
'2.txt' would be renamed to '2.txt.bar'
'3.txt' would be renamed to '3.txt.bar'
'4.txt' would be renamed to '4.txt.bar'
'5.txt' would be renamed to '5.txt.bar'

2
如果你可以使用除了bash/sh之外的其他工具,并且这只是为了一个花哨的"mv"... 你可以尝试神圣的"rename.pl"脚本。我一直在Linux和Windows下的cygwin上使用它。 http://people.sc.fsu.edu/~jburkardt/pl_src/rename/rename.html
rename.pl 's/^(.*?)\.(.*)$/\1-new_stuff_here.\2/' list_of_files_or_glob

您还可以使用"-p"参数来运行rename.pl,以便它告诉您它将要执行的操作,而不会实际执行。

我在我的c:/bin(cygwin/windows环境)中尝试了以下操作。我使用了"-p"参数,以便它输出它将要执行的操作。此示例仅拆分基本名称和扩展名,并在它们之间添加一个字符串。

perl c:/bin/rename.pl -p 's/^(.*?)\.(.*)$/\1-new_stuff_here.\2/' *.bat

rename "here.bat" => "here-new_stuff_here.bat"
rename "htmldecode.bat" => "htmldecode-new_stuff_here.bat"
rename "htmlencode.bat" => "htmlencode-new_stuff_here.bat"
rename "sdiff.bat" => "sdiff-new_stuff_here.bat"
rename "widvars.bat" => "widvars-new_stuff_here.bat"

谢谢你的回答!mv示例只是一个例子。然而,我在许多其他情况下都需要做类似的事情,所以我猜rename.pl对于那些情况不起作用。 - betabandido
好的,是和不是;你可以轻松地修改rename.pl,实际重命名文件,而是对其进行其他操作。正如你所看到的,使用“-p”参数时,它根本不会重命名,而只是打印转换结果。你可以将该脚本用作基础来做许多其他事情,它接受的参数是任意的Perl代码,这非常强大。但无论如何,你肯定是受欢迎的。 - Michael Campbell

2

补充一下,维基百科文章提供了出人意料的信息。

例如:

Shell trick

实现类似效果的另一种方法是使用shell作为启动的命令,并在该shell中处理复杂性,例如:

$ mkdir ~/backups
$ find /path -type f -name '*~' -print0 | xargs -0 bash -c 'for filename; do cp -a "$filename" ~/backups; done' bash

0
受@justaname上面的答案启发,这个命令将包含Perl一行代码: find ./ -name \*.txt | perl -p -e 's/^(.*\/(.*)\.txt)$/mv $1 .\/foo\/$2.bar.txt/' | bash

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