这个问题是由我在一本 Linux 杂志上看到的一个简短脚本引发的。为了证明这不是我编造的,下面是它的一张图片:
我想写一封信给这份出版物的编辑,告诉他们这篇文章有什么问题以及如何写得更好。
该脚本尝试将 jpeg 文件捕获到变量中,以便lepton
可以对它们进行某些操作(使用压缩)。
for jpeg in `echo "$(file $(find ./ ) |
grep JPEG | cut -f 1 -d ':')"`
do
/path/to/command "$jpeg"
...
显然,在这种情况下,我们不能相信文件以.jpg
扩展名命名,所以我们不能用类似这样的方法捕获它们
for f in *.JPG *.jpg *.JPEG *.jpeg ; do ...
因为作者曾经file
检查过它们的类型,但是如果文件名不可信并且没有合理的扩展名,那么我就不明白我们如何能相信它们不存在-rf *
换行符(; \ $!|
或者其他什么东西。
我怎样才能明智地将文件捕获到变量中按类型使用for
或,或者也许通过使用或其他方法while
来避免这样做?find
-exec
对图片中代码错误的洞察和演示可获得奖励。
我用 [bash] 标记了这个问题,因为它是关于一个 bash 脚本,但是如果你想用一种不使用 bash 的方式来回答,那么请随意这样做。
答案1
0.脚本想要做这样的事情。
问题中显示的脚本尝试枚举文件并检查它们是否为 JPEG,但无法可靠地完成。它尝试传递所有路径在file
一次运行中,从 的输出中提取文件名和类型file
,其中是合理的因为这可能比file
对每个文件一次又一次地运行更快。但要正确执行此操作,您需要注意如何将路径传递给file
,如何file
分隔其输出以及如何使用该输出。您可以使用这个:
#!/bin/bash
find . -exec file --mime-type -r0F '' {} + | while read -rd ''; do
read -r mimetype
case "$mimetype" in image/jpeg)
# Bash placed the filename in "$REPLY" -- put commands that use it here.
# You can have as many commands as you want before the closing ";;" token.
;;
esac
done
这是几种正确的方法之一。(确实如此不是需要设置IFS=
;见下文。)find
将+
多个路径参数传递给file
并只运行它处理所有参数所需的次数,通常只运行一次。归功于藝術本身为了传递--mime-type
给file
获取 MIME 类型,它包含您真正想要的信息并且易于解析。
以下是详细解释。我以 JPEG 压缩这一特定任务为例。这就是您展示的脚本的用途,lepton
在决定如何改进该脚本时,应该考虑一些奇怪之处。如果您只想查看lepton
在每个 JPEG 文件上运行的脚本,您可以跳至第节7. 综合起来。
1. 安装lepton
您展示的脚本旨在遍历目录层次结构,查找 JPEG 图像,并使用无损 JPEG 压缩器对其进行处理lepton
。对于您问题的主要动机,命令可能并不重要,但不同的命令具有不同的语法。有些命令在一次运行中接受多个输入文件名。大多数接受--
以指示选项的结束。我将使用它lepton
作为我的例子。该lepton
命令不接受多个输入文件名并且不识别--
。
要使用lepton
,请先安装它。它的针对 Ubuntu 17.04 及更高版本进行了官方打包(sudo apt install lepton
)。对于较早的 Ubuntu 版本,或要使用比您的版本打包的版本更新的版本,请克隆它的git
存储库(git clone https://github.com/dropbox/lepton.git
)并构建源代码按照 README 中的说明进行操作.或者你也可以查找 PPA。
取决于您的安装方式,lepton
可能位于/usr/bin
、/usr/local/bin
或其他地方。您可能希望将其放置在$PATH
; 然后你可以将其作为 运行lepton
。你展示的脚本使用绝对路径和lepton
标准实用程序mv
和rm
, 但不是其他标准实用程序file
,find
,grep
和cut
。(这是 Bash,因此echo
--无论如何,在那个脚本中毫无意义--是 shell 内置的。exit
是始终是内置的)虽然这不是剧本的严肃的缺陷,没有明显的原因导致这种不一致。除非你正在编写一个脚本来容忍没有$PATH
合理设置——在这种情况下你必须对所有外部命令使用绝对路径——我建议对标准命令和你安装的命令使用相对路径。
2.跑步lepton
注意事项和一般信息
我使用 lepton v1.0-1.2.1-104-g209463a(来自 Git)进行了测试lepton
。2016 年 7 月发布所以我猜当前的语法将继续有效。但未来版本可能会添加功能。如果你几年后才读到这篇文章,你可能会检查它是否lepton
增加了对曾经需要脚本的任务的支持。
请注意传递的命令行参数。例如,我尝试将lepton
用作-verbose
第一个参数和art.jpg
第二个参数。它将其解释-verbose
为输入文件名并退出并显示错误,但在截断之前art.jpg
(它将其解释为输出文件名)将其截断为零字节。幸运的是,我有备份!
您可以将零个、一个或两个路径传递给lepton
。在所有情况下,它都会检查其输入文件或流以查看其是否包含 JPEG 或 Lepton 数据。JPEG 被压缩为 Lepton;Lepton 被解压缩为 JPEG。lepton
将删除和添加文件扩展名,但不会使用它们来决定要做什么。
零文件名——lepton -
读取自标准输入并写信给标准输出。
因此,有一种读取方法lepton - < infile > outfile
infile
并写信给outfile
,即使它们的名称以 开头-
(就像选项一样)。但我将使用的方法传递以 开头的路径.
,因此我不必担心这一点。
一个文件名 —读取lepton infile
infile
并命名其自己的输出文件。
这就是您展示的脚本的使用方式lepton
。
如果内容infile
看起来像 JPEG,lepton
则输出 Lepton 文件;如果其内容看起来像 Lepton 文件,lepton
则输出 JPEG。lepton
通过从中剥离扩展名来决定如何命名其输出文件infile
,如果有的话,并根据所创建的文件类型添加.jpg
或扩展名。但它确实.lep
不是使用正在删除的扩展名(如果有)来推断其正在操作的文件类型。
它认为最后的 .
以及其后的任何内容作为扩展。如果infile
是a.b.c
,则得到a.b.lep
或a.b.jpg
。如果文件名以 a 开头,.
没有其他.
s,则lepton
仍然将其视为扩展:从名为 的 JPEG中.abc
,您可以获得.lep
。只有.
在文件名(而不是目录名)中才会触发此操作,因此从 Lepton 文件中,x/fo.o/abc
您可以获得x/fo.o/abc.jpg
(您想要的),而不是x/fo.jpg
(这很糟糕)。
如果通过这种方式获得的输出文件名命名了一个现有文件,_
则会在扩展名后末尾添加 s,直到没有 s,并使用添加下划线的名称:,,,,abc.lep
等等,,,,等等。abc.lep_
abc.lep__
xyz.jpg
xyz.jpg_
xyz.jpg__
当您的文件以合理的方式命名时,这种方法最有效。
自动删除和添加扩展名以及添加下划线可以避免您自己处理的问题——在输出文件已经存在的情况下防止数据丢失。但它也暴露了什么可能您展示的脚本存在一个很深的设计缺陷。如果您的文件命名合理,则所有 JPEG 文件都以.jpg
或结尾.jpeg
(可能大写),并且没有非 JPEG 文件以 或 结尾。但是您不必检查文件以file
找出哪些是 JPEG!
因此,您展示的脚本的前提是文件也许不会合理命名。脚本在包含空格、*
和其他特殊字符的文件名上出现错误或意外行为总是很糟糕的。因此,它在空格上拆分和扩展 glob 的行为(外部未加引号的命令替换,旨在拆分单独的文件名,就是这样)尤其糟糕。请参阅Byte Commander 的精彩回答了解详情。这可能是你展示的剧本中最严重的缺陷。
但也值得考虑的是,如果文件名的最后一个字符.
没有概念上开始文件扩展名。假设Pictures
有四个文件,都是 JPEG:01. Milan wide-angle sunset
、、和。然后01. Milan wide-angle sunset highres
创建、、和——可能不是您想要的。02. Kyle birthday party prep - blooper cakes
03. The subtle found art of unopened expired paint cans with peeling labels
for f in ~/Pictures/0*; do lepton "$f"; done
01.lep
01.lep_
02.lep
03.lep
如果您有未命名的 JPEG.jpg
或可能.jpeg
,最好的一般方法是按此方式重命名它们,并调查这样做时出现的任何命名冲突。但这超出了本答案的范围。
这些重命名问题发生在不像 JPEG 那样命名的 JPEG 上,不是像 JPEG 一样命名的非 JPEG 文件。但即便如此,也可能有更好的解决方案。如果问题出._
在 macOS 中的文件,而您不想删除它们,只需排除以 开头的文件._
(甚至是以 开头的文件.
)。不过,只传递一个路径可以避免lepton
数据丢失(由于其_
附加规则);如果主要的目标是排除非 JPEG,虽然实现上需要修复,但基本思想是合理的。
所以我将使用单路径语法lepton infile
。但是任何考虑lepton
对奇怪命名的文件进行此类自动化处理的人都应该记住,生成的.lep
文件的命名方式可能不会透露输入文件名。
两个文件名 —完全符合您的预期。lepton infile outfile
但仅仅因为你期望它,并不意味着它就是正确的事。
与其他运行方式一样lepton
,lepton
确定infile
是要压缩的 JPEG 文件还是要解压缩的 Lepton 文件,方法是检查其内容。如果infile
是 JPEG,lepton
写入名为 Lepton 的文件outfile
; 如果infile
是一个 Lepton 文件,lepton
写入一个名为outfile
。使用此双路径语法,lepton
不会以任何方式更改您指定的输出文件名。它不会添加或删除扩展名或附加_
s 来解决命名冲突。如果outfile
已经存在,将被覆盖。
您可能想要这样,但如果不想,并且您使用这种语法,那么您必须自己解决问题,方法是让脚本调整输出文件名。lepton
当只使用一个路径参数运行时,您可能能够以比自己的方案更好的方式执行此操作。但我不会试图猜测您的具体需求和偏好;我只会使用单路径语法。
3. 将多条路径从 传递find
到file
您展示的脚本尝试通过运行来file $(find ./ )
传递每个参数的一个路径file
find
命令替换。这通常不起作用,因为$(find ./ )
文件名中可能包含空格。文件(尤其是图像!)和文件夹的名称中通常包含空格。您展示的脚本将路径./abc/foo bar.jpg
视为两个路径,./abc/foo
和bar.jpg
。在最好的情况下,两者都不存在;如果它们存在,您会无意中操作错误的东西。并且原始路径根本不会被处理。
IFS=$'\n'
虽然可以通过设置来减少这个问题的严重程度单词拆分仅在行与行之间执行(\n
代表新队字符),这不是一个好的解决方案。除了不方便之外,它仍然会失败,因为文件和目录名称可能包含换行符。我建议不要用它们命名文件或目录,除非是为了测试程序或脚本是否有错误。但可以创建这样的名称,包括意外地文件名中不能包含的字符只有路径分隔符/
和空字符。因此,空字符是唯一不能出现在路径中的字符,也是分隔任意路径列表的唯一安全选择。这就是为什么find
有-print0
动作和xargs
有-0
选项的原因。
可以使用 正确完成此操作find . -print0 | xargs -0 ...
,但您不需要第三个实用程序将路径从 传递find
到file
。find
的-exec
操作就足够了。-exec
构建要运行的命令之后的参数,直到\;
或+
。find ... -exec ... ;
为每个文件运行一次命令,而find ... -exec ... +
每次运行该命令时传递尽可能多的路径,这通常更快。通常所有参数都适合,命令只运行一次。在极少数情况下,命令行太长并且find
运行命令不止一次。因此,该+
形式仅适用于运行(A)最后采用路径参数并(二)在一次运行中使用多个文件名的工作方式与在单独运行中使用多个文件名的工作方式相同。
lepton
是命令的示例一定不+
不能使用的形式运行-exec
,因为它不接受多个源文件名。第一个是输入,第二个是输出,其他的就太多了。但许多命令做使用多个参数运行一次和使用一个参数运行多次执行相同的操作,是file
其中之一。
此命令将生成表:
find . -exec file --mime-type -r0F '' {} +
find
{}
在调用时用路径替换参数file
,并用+
尽可能多的其他路径参数替换。
--mime-type -r0F ''
传递给的选项find
解释如下。
有些人引用 {}
,例如,'{}'
。这样做没问题,但 Bash 和其他 Bourne 风格的 shell 都不需要它。Bash 和一些其他 shell 支持括号扩展,但一对空括号不会展开。我选择不是引用{}
,因为人们误解引用{}
会妨碍find
执行单词拆分。即使你的 shell 需要{}
引用,这也与分词无关,因为find
never 会这样做。(如果你想要分词,你必须告诉find
shell -exec
。)并且find
无法判断你是否写了{}
或'{}'
——shell 变成'{}'
了{}
(在删除引文) 然后将其传递给find
。
4. 使用以下代码生成可用的⟨Path, File Type⟩表file
问题
file
我必须将一些选项传递给-- 而不能直接使用-- 的原因是默认生成的find . -exec file {} +
表不明确:file
01. Milan wide-angle sunset: JPEG image data, JFIF standard 1.01, resolution (DPI), density 1x1, segment length 16, baseline, precision 8, 1400x1400, frames 3
02. Kyle birthday party prep - blooper cakes: JPEG image data, JFIF standard 1.01, aspect ratio, density 1x1, segment length 16, baseline, precision 8, 512x512, frames 3
first line
second line: JPEG image data, JFIF standard 1.01, aspect ratio, density 1x1, segment length 16, baseline, precision 8, 500x500, frames 3
这三行看起来像四行;一个文件名包含换行符。文件名也可以包含冒号,因此文件名的结尾并不总是很清楚。可能出现比上面显示的更令人困惑的例子。
描述栏中的信息也比我们需要的多得多。Byte Commander 解释grep
每一行中的一个原因JPEG
都会返回错误的结果:名称中包含的非 JPEG 文件JPEG
会产生误报。(检查类型的重点是你不能依赖名称,所以这是你展示的脚本中的一个相当自相矛盾的错误。)但即使你知道你正在查看描述列,它仍然可能包含JPEG
即使不是这种类型:
$ touch empty.JPEG # not a JPEG
$ gzip -k empty.JPEG
$ file empty.JPEG*
empty.JPEG: empty
empty.JPEG.gz: gzip compressed data, was "empty.JPEG", last modified: Mon Aug 28 16:37:56 2017, from Unix
字节指挥官的回答解决了这个问题(A)将选项传递-b
给file
,使其省略:
类型前面的路径、分隔符和空格,然后(二)用于grep
检查描述开始与JPEG
(^
锚在模式中^JPEG image data,
这样做)。如果您跟踪传递给的路径,这将有效file
- 这对 Byte Commander 的方法来说不是问题,因为它对file
每个路径分别运行。
解决方案
我必须使用不同的解决方案,因为我的目标是解析两者路径和类型来自file
的输出,这样就file
不需要为每个文件单独运行。幸运的是file
在 Ubuntu 中有很多选项。 我用:file --mime-type -r0F '' paths
--mime-type
打印一个MIME 类型而不是详细描述。这就是我所需要的,然后我就可以对整个内容进行精确匹配。对于 JPEG,file --mime-type
显示image/jpeg
在描述栏中。(另请参阅αғsнιη 的回答。- 根据
man file
,-r
导致无法打印的字符不会被替换为八进制转义符,如\003
。我相信否则我需要添加一个步骤来将此类序列转换回实际字符,这可能不能可靠地完成——如果这样的序列确实出现在文件名中会怎么样?(file
没有转义\
为\\
。)我说“我相信”,因为我还没有设法file
打印出这样的转义序列,而且我不确定它是否真的在文件名列中这样做。无论如何,-r
这里是安全的。 -0
是这里的关键选项。如果没有它,这个方法就无法可靠地工作。它使file
print 文件名后面紧接着一个空字符(路径中永远不允许使用的一个字符,因为它通常用于标记 C 程序中的字符串结尾)。这标记了表格中每行两列之间的分隔符。-F ''
使file
打印内容为空(''
为空参数)而不是:
。冒号不可靠(它可以出现在文件名中)并且在这里没有任何好处,因为已经打印了一个空字符来指示路径列的结束和描述列的开始。
为了运行,find
我使用.的操作替换路径。file --mime-type -r0F '' paths
-exec file --mime-type -r0F '' {} +
find
-exec
{} +
5. 消费餐桌
我是这样创建表格的:
find . -exec file --mime-type -r0F '' {} +
如上所述,这会在每个路径后放置一个空字符。如果描述也是以空字符结尾的,那会很方便,但file
不会这样做——描述总是以换行符结尾。所以我必须交替读取直到出现空字符,然后假设还有更多文本并读取直到出现换行符。我必须这样做每个文件并在没有剩余内容时停止。
读取每一行
这种组合——读取可能包含换行符直到空字符的文本,然后读取不能包含换行符直到换行符的文本——不是任何常见 Unix 实用程序的正常使用方式。我将采用的方法是将输出通过管道传输find
到循环。循环的每次迭代都使用 shell 内置命令两次读取表中的一行read
,并使用不同的选项。
读取路径, 我用:
read -rd ''
-r
是read
唯一标准选项你应该总是使用它。如果没有它,\n
输入中的反斜杠转义符就会被翻译成它们所代表的字符。我们不希望这样。- 通常,
read
它会一直读取直到看到换行符。要忽略换行符并在空字符处停止,我使用-d
Bash 提供的选项来指定其他字符。对于空字符,请传递空参数''
。 - 我已经在使用 Bash 扩展(选项
-d
),因此当没有将变量名传递给 时,我也可以充分利用 Bash 的默认行为read
。它会将读取的所有内容放入--除了特殊变量中的终止符$REPLY
。通常会从输入的开头和结尾read
去除空格(字符),并且写代码来防止这种情况发生是一种常见的习惯做法。在 Bash 中隐式读取时,这不是必需的。$IFS
IFS= read ...
$REPLY
阅读说明, 我用:
read -r mimetype
- MIME 类型中不应出现反斜杠,但最好传递
-r
到,read
除非你想\
逃脱翻译。 - 这次,我是明确指定变量名。随便你怎么叫。我选择了
mimetype
。 - 这次,为了防止前导和尾随空格被删除,缺少了这
IFS=
一点很重要。我想把它去掉。这会从写入的描述开头删除空格,find
以使表格在终端中显示时更易于阅读。
编写循环
只要有另一条路径需要读取,循环就应该继续。read
当命令成功读取某些内容时,它返回 true(在 shell 编程中,与几乎所有其他编程语言不同,这是零),当它没有成功读取某些内容时,它返回 false(在 shell 编程中,任何非零值)。所以常见的while read
习惯用法在这里很有用。我将--|
的输出find
(一个或多个(很少)file
命令的输出)通过管道 ( ) 传输到while
循环中。
find . -exec file --mime-type -r0F '' {} + | while read -rd ''; do
read -r mimetype
# Commands using "$REPLY" and "$mimetype" go here.
done
在循环中,我读取了行的其余部分以获取描述(read -r mimetype
)。我不费心检查这是否成功。file
应该只输出完整的行即使遇到错误. (file
发送错误和警告消息至标准误差, 所以他们不会出现在管道中破坏表格。您应该能够依赖这一点。
如果无论如何你都想检查是否read -r mimetype
成功,你可以使用if
。或者你可以将其包含在while
循环条件中:
find . -exec file --mime-type -r0F '' {} + |
while read -rd '' && read -r mimetype; do
# Commands using "$REPLY" and "$mimetype" go here.
done
您可以看到,为了便于阅读,我还拆分了顶行。(无需\
在 处拆分|
。)
测试循环
如果您想在继续之前测试循环,则可以将此命令放在注释下(或代替注释)# Commands...
:
printf '[%s] [%s]\n\n' "$REPLY" "$mimetype"
循环输出看起来像这样,取决于目录中的内容(为了简洁,我省略了大多数条目):
[.] [inode/directory]
[./stuv] [inode/x-empty]
[./ghi
jkl] [inode/x-empty]
[./fo.o/abc
def ] [image/jpeg]
[./fo.o/wyz.lep] [application/octet-stream]
[./fo.o/wyz] [image/jpeg]
这只是为了看看循环是否正常工作。[
]
像这样放置表格的条目不会帮助脚本完成它需要做的事情,因为路径可能包含[
、]
和连续的换行符。
6.使用提取的路径和文件类型
在循环的每次迭代中,"$REPLY"
包含路径并"$mimetype"
包含类型描述。要找出是否"$REPLY"
命名 JPEG 文件,请检查是否"$mimetype"
恰好为image/jpeg
。
您可以使用if
and [
/ test
(或[[
)与 来比较字符串=
。但我更喜欢case
:
find -exec file --mime-type -r0F '' {} + | while read -rd ''; do
read -r mimetype
case "$mimetype" in image/jpeg)
# Put commands here that use "$REPLY".
;;
esac
done
如果您只是想以与上述相同的格式显示 JPEG 路径(以帮助测试包含换行符的路径),则整个case
...esac
语句可以是:
case "$mimetype" in image/jpeg) printf '[%s]\n\n' "$REPLY";; esac
但目标是lepton
在每个 JPEG 文件上运行。为此,请使用:
case "$mimetype" in image/jpeg) lepton "$REPLY";; esac
7. 综合起来
添加该lepton
命令,以及哈希邦线运行它使用 Bash,这是完整的脚本:
#!/bin/bash
find . -exec file --mime-type -r0F '' {} + | while read -rd ''; do
read -r mimetype
case "$mimetype" in image/jpeg) lepton "$REPLY";; esac
done
lepton
报告正在做什么,但不显示文件名。此替代脚本在运行每个路径之前会打印一条消息lepton
:
#!/bin/bash
find . -exec file --mime-type -r0F '' {} + | while read -rd ''; do
read -r mimetype
case "$mimetype" in image/jpeg)
printf '\nProcessing "%s":\n' "$REPLY" >&2
lepton "$REPLY"
esac
done
我已将消息打印到标准误差( >&2
),因为这就是lepton
发送其自身消息的地方。这样,输出在通过管道传输或重定向时会全部保持在一起。运行该脚本会产生如下输出(但如果您有两个以上的 JPEG,则输出会更多):
Processing "./art.jpg":
lepton v1.0-1.2.1-104-g209463a
6777856 bytes needed to decompress this file
56363 86007
65.53%
2635854 bytes needed to decompress this file
56363 86007
65.53%
Processing "./fo.o/abc
def ":
lepton v1.0-1.2.1-104-g209463a
6643508 bytes needed to decompress this file
36332 46875
77.51%
2456117 bytes needed to decompress this file
36332 46875
77.51%
每个节中的重复(在运行时lepton
不打印文件名也会出现)是因为lepton
检查其输出文件是否可以正确解压缩。
您展示的脚本exit 0
最后有。您可以根据需要这样做。它会导致脚本始终报告成功。否则,脚本将返回上次命令运行的退出状态——这可能是更好的选择。无论哪种方式,即使 、 或 遇到问题,它也可能报告find
成功file
,lepton
如果最后的 lepton
命令成功。当然,您可以使用更复杂的错误处理代码来扩展脚本。
8. 也许你也想要路径
如果你想生成路径列表分离从lepton
自己的输出中,您可以利用 的lepton
写入标准错误的行为,通过打印路径标准输出而是。在这种情况下,您可能只想打印路径而不是“正在处理”消息。您可能希望使用空字符而不是换行符来终止路径,因为这样您就可以处理列表而不会在包含换行符的路径上中断。
#!/bin/bash
case "$1" in
-0) format='%s\0';;
*) format='%s\n';;
esac
find . -exec file --mime-type -r0F '' {} + | while read -rd ''; do
read -r mimetype
case "$mimetype" in image/jpeg)
printf "$format" "$REPLY"
lepton "$REPLY"
esac
done
运行该脚本时,可以传递标志-0
,使其发出空字符而不是换行符。该脚本不执行正确的 Unix 样式选项处理:它仅检查第一的您传递的参数;在同一个参数 ( ) 中重复传递标志-00
不起作用;并且不会生成任何与选项相关的错误消息。此限制是为了简洁起见,因为您可能不需要任何更复杂的东西,因为脚本不支持任何非选项参数并且-0
是唯一可能的选项。
在我的系统上,我调用了该脚本jpeg-lep3
并将其放入~/source
,然后运行~/source/jpeg-lep3 -0 > out
,它将 just 的输出打印lepton
到我的终端。如果您执行了类似操作,则可以使用以下命令测试路径之间是否正确写入了空字符:
xargs -0 printf '[%s]\n\n' < out
答案2
先写代码:
让我们使用 Bash 的特殊 glob 和for
循环来完成此操作:
#!/bin/bash
shopt -s globstar dotglob
for f in ./** ; do
if file -b -- "$f" | grep -q '^JPEG image data,' ; then
# do whatever you want with the JPEG file "$f" in here:
md5sum -- "$f"
fi
done
解释:
首先,我们需要通过启用globstar
和shell 选项来使 Bash 通配符更加有用。以下是有关 的 SHELL BUILTIN COMMANDS 部分中dotglob
的描述:man bash
shopt
dotglob
If set, bash includes filenames beginning with a `.' in the results of
pathname expansion.
globstar
If set, the pattern ** used in a pathname expansion context will match
all files and zero or more directories and subdirectories. If the pattern
is followed by a /, only directories and subdirectories match.
./**
然后我们在循环中使用这个新的“递归 glob”for
来迭代当前目录及其所有子目录中的所有文件和文件夹。请在 glob 中始终使用绝对路径或以./
或开头的显式相对路径../
,而不仅仅是**
,以防止出现特殊文件名(如 )的问题~
。
现在我们用该命令测试每个文件(和文件夹)名称file
的内容。该-b
选项可防止它在内容信息字符串之前再次打印文件名,从而使过滤更加安全。
现在我们知道所有有效的 JPG/JPEG 文件的内容信息都必须以 开头JPEG image data,
,这就是我们用 测试 的输出。我们使用选项来抑制任何输出,因为我们只file
对的退出代码感兴趣,它指示模式是否匹配。grep
-q
grep
如果匹配,则将执行if
/块内的代码。我们可以在这里做任何我们想做的事情。当前的 JPEG 文件名在 shell 变量中可用。我们只需确保始终将其放在双引号中,以防止意外评估带有特殊字符(如空格、换行符或符号)的文件名。通常最好将其放在后面,将其与其他参数分开,这会导致大多数命令将其解释为文件名,即使它是类似或的东西,否则将被解释为选项。then
$f
--
-v
--help
附加问题:
为了科学,是时候炸毁一些代码了!这是你的问题/书中的版本:
for jpeg in `echo "$(file $(find ./ )
| grep JPEG | cut -f 1 -d ':')"`
do
/path/to/command "$jpeg"
done
首先,请允许我提一下他们编写的程序有多复杂。我们有 4 层嵌套子 shell,使用混合命令替换语法(``
和$()
),这仅仅是因为 的使用不正确/不理想而必需的find
。
这里find
只列出所有文件并打印它们的名称,每行一个。然后将完整输出传递给file
检查每个文件。但是等等!每行一个文件名?如果文件名包含换行符怎么办?对,这些会破坏它!
$ ls --escape ne*ne
new\nline
$ file $(find . -name 'ne*ne' )
./new: cannot open `./new' (No such file or directory)
line: cannot open `line' (No such file or directory)
实际上,即使是简单的空格也会破坏它,因为 也会将它们视为分隔符file
。您甚至无法"$(find ./ )"
在此处引用 来补救,因为那样会将整个多行输出引用为一个文件名参数。
$ ls simple*
simple spaces.jpg
$ file $(find ./ -name 'simple*')
./simple: cannot open `./simple' (No such file or directory)
spaces.jpg: cannot open `spaces.jpg' (No such file or directory)
下一步,file
使用 扫描输出grep JPEG
。你不觉得欺骗这样一个简单的模式有点容易吗,尤其是因为 plain 的输出file
总是包含文件名?基本上文件名中带有“JPEG”的所有内容都会触发匹配,无论它包含什么。
$ echo "to be or not to be" > IAmNoJPEG.txt
$ file IAmNoJPEG.txt | grep JPEG
IAmNoJPEG.txt: ASCII text
好的,所以我们有file
所有 JPEG 文件(或假装是 JPEG 文件)的输出,现在他们处理所有行以cut
从第一列中提取原始文件名,并用冒号分隔......猜猜是什么,让我们在名称中带有冒号的文件上尝试一下:
$ ls colon*
colons:evil.jpeg
$ file colon* | grep JPEG | cut -f 1 -d ':'
colons
所以总而言之,您书中的方法有效,但前提是它检查的所有文件都不包含空格、换行符、冒号和其他特殊字符,并且文件名中不包含字符串“JPEG”。这也有点丑陋,但由于情人眼里出西施,我不会对此喋喋不休。
答案3
您已经find
使用命令检查了file
它的 mime 类型。
find . -type f -exec file --mime-type -b '{}' +
或者按照如下方式使其完整:
find . -type f -exec sh -c '
file --mime-type -b "$0" | grep -q "aPATTERN" && printf "$0\n"
' {} \;
find -type f -print0 | xargs -0 identify