这个问题是由我在 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 *
或不(; \ $!|
存在换行符或其他任何内容。
如何通过使用 or 类型将文件巧妙地捕获到变量中,或者通过使用 with或for
其他方法while
避免这样做?find
-exec
洞察和演示图片中的代码有什么问题的奖励。
我用 [bash] 标记了这个问题,因为它是关于 bash 脚本的,但是如果您想用一种不使用 bash 的方法来回答,那么请随时这样做。
0. 脚本想做这样的事情。
您的问题中显示的脚本尝试枚举文件并检查它们是否为 JPEG,但两者都不可靠。它尝试在一次运行中传递所有路径并
file
从 的输出中提取文件名和类型file
,这是合理的,因为它可能比file
为每个文件一次又一次地运行更快。但是要正确地做到这一点,您需要注意路径是如何传递给 的file
,如何file
界定其输出,以及如何使用该输出。你可以使用这个:这是几种正确方法之一。(它不需要设置
IFS=
;见下文。)find
with+
将多个路径参数传递给file
并且只运行它所需的次数来处理它们,通常只运行一次。归功于 αғsнιη传递的--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
存储库( ) 并按照 README 中的说明git clone https://github.com/dropbox/lepton.git
构建源代码。或者您也许可以找到 PPA。根据您的安装方式,
lepton
可能位于/usr/bin
、/usr/local/bin
或其他位置。可能你会想要它在某个地方$PATH
;然后你可以将它作为lepton
. 您显示的脚本使用标准实用程序 和 的绝对路径,但不使用其他标准实用程序、和。(这是 Bash,所以——无论如何,在那个脚本中毫无意义——是一个 shell builtin。总是一个 builtin。)虽然这不是脚本的严重缺陷之一,但没有明显的原因导致这种不一致。除非你正在编写一个脚本来容忍没有lepton
mv
rm
file
find
grep
cut
echo
exit
$PATH
合理设置——在这种情况下,您必须对所有外部命令使用绝对路径——我建议对标准命令和已安装的命令使用相对路径。2. 跑步
lepton
注意事项和一般信息
我用 lepton v1.0-1.2.1-104-g209463a(来自 Git)进行了测试。
lepton
于2016 年 7 月发布,所以我猜当前的语法会继续工作。但未来的版本可能会增加功能。如果您从现在开始阅读今年,您可能会检查是否lepton
增加了对曾经需要编写脚本的任务的支持。请注意您传递的命令行参数。例如,我尝试将
lepton
with-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
中删除扩展名(如果有),并根据创建的文件类型添加 a.jpg
或扩展名。.lep
但它不使用它正在删除的扩展名(如果有的话)来推断它正在操作的文件类型。它将最后一个
.
和之后的任何内容视为扩展。如果infile
是a.b.c
,你得到a.b.lep
或a.b.jpg
。如果文件名以 a 开头而.
没有其他.
s,lepton
仍将其视为扩展名:从名为.abc
you get的 JPEG 中获取.lep
。只有.
在文件名中——不是目录名——触发这个,所以从轻子文件中x/fo.o/abc
你得到x/fo.o/abc.jpg
(你想要的),而不是x/fo.jpg
(这会很糟糕)。如果以这种方式获得的输出文件名命名一个现有文件,
_
则将 s 添加到末尾,在扩展名之后,直到没有,并且使用添加下划线的名称:abc.lep
,abc.lep_
,abc.lep__
等,xyz.jpg
,xyz.jpg_
,xyz.jpg__
等。当您的文件以合理的方式命名时,这最有效。
自动删除和添加扩展名以及添加下划线避免了您必须自己管理的问题——当输出文件已经存在时防止数据丢失。但它也暴露了您展示的脚本中可能存在的深层设计缺陷。如果您的文件命名合理,那么您所有的 JPEG 文件都以
.jpg
或.jpeg
(可能大写)结尾,并且没有非 JPEG 文件被如此命名。但是,您不必检查文件file
以找出哪些是 JPEG!因此,您显示的脚本的前提是文件可能无法合理命名。脚本在包含空格
*
、 和其他特殊字符的文件名上出现错误或意外行为总是很糟糕的。因此,它在空格上拆分和扩展 glob 的行为(外部不带引号的命令替换,仅用于拆分单独的文件名,这样做)特别糟糕。有关详细信息,请参阅Byte Commander 的出色答案。这可能是您展示的脚本中最严重的缺陷。但同样值得考虑的是,最后一个在概念
.
上不是以文件扩展名开头的文件名会发生什么。假设有四个文件,都是 JPEG:、、和. 然后创建, ,和-- 可能不是你想要的。Pictures
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 的非 JPEG。然而即便如此,也可能有更好的解决方案。如果问题是
._
来自 macOS 的文件并且您不想删除它们,只需排除带有前导._
(甚至是前导.
)的文件。尽管如此,只通过一条路径来lepton
避免数据丢失(由于其_
附加规则);如果主要目标是排除非 JPEG,则基本思想是合理的,即使实现需要修复。所以我将使用单路径语法
lepton infile
。但是任何考虑在奇怪命名的文件上进行这样的自动化的人都lepton
应该记住,生成的.lep
文件可能会以不显示输入文件名的方式命名。两个文件名 —完全符合您的预期。
lepton infile outfile
但仅仅因为你期望它并不能使它成为正确的事情。
与其他运行方式一样
lepton
,通过检查其内容lepton
来确定infile
是要压缩的 JPEG 文件还是要解压缩的 Lepton 文件。如果infile
是 JPEG,则lepton
写入名为outfile
;的 Lepton 文件。如果infile
是 Lepton 文件,则lepton
写入名为outfile
. 使用这种双路径语法,lepton
不会以任何方式更改您指定的输出文件名。它不会添加或删除扩展名或 append_
来解决命名冲突。如果outfile
已经存在,则将其覆盖。您可能希望这样做,但如果不这样做并且您使用此语法,那么您必须通过让脚本调整输出文件名来自己解决问题。
lepton
当仅使用一个路径参数运行时,您可能能够以一种比自己的方案更好的方式来执行此操作。但我不会试图猜测您的具体需求和偏好;我将只使用单路径语法。3. 传递多条路径 from
find
tofile
您展示的脚本试图通过在command substitution中运行
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)最后采用路径参数,并且(b)在具有多个文件名的一次运行中的工作方式与在单独运行中的方式相同。lepton
是一个命令示例,该命令不能使用 的+
形式运行,-exec
因为它不接受多个源文件名。第一个是输入,第二个是输出,其他的都是过度的。但是,许多命令在使用多个参数运行一次时与使用一个参数运行多次时会执行相同的操作,并且是file
其中之一。此命令将生成表:
find
{}
调用时用路径替换参数file
,并替换+
为适合的其他路径参数。--mime-type -r0F ''
下面解释传递给的选项find
。有些人引用
{}
,例如,'{}'
。这样做很好,但是 Bash 和其他 Bourne 风格的 shell 都不需要它。Bash 和其他一些 shell 支持大括号扩展,但一对空的大括号不会被扩展。我选择不引用{}
,因为引用{}
阻止find
执行分词的误解。即使您的 shell 需要{}
被引用,这仍然与分词无关,因为find
从不这样做。(如果你想要分词,你必须告诉find
一个-exec
shell。)并且find
无法判断你是否写过{}
或者'{}'
--shell转'{}'
into{}
(在引号删除期间),然后将其传递给find
.4. 发出一个可用的⟨Path, File Type⟩ Table with
file
问题
我必须将一些选项传递给
file
-- 并且不能只使用 --的原因find . -exec file {} +
是该表file
默认生成是不明确的:那三行看起来像四行;一个文件名包含换行符。文件名也可以包含冒号,因此文件名的结束位置并不总是很清楚。比上面显示的更令人困惑的例子是可能的。
描述列的信息也比我们需要的多。Byte Commander 解释
grep
了在每一整行中返回错误结果的一个原因JPEG
:名称中包含非 JPEG 文件JPEG
会产生误报。(检查类型的重点是您不能依赖名称,所以这在您展示的脚本中是一个相当自欺欺人的错误。)但即使您知道您正在查看描述列,它也可能JPEG
即使那不是类型,仍然包含:Byte Commander 的回答通过(a)将
-b
选项传递给 来解决这个问题file
,使其省略:
类型前面的路径、分隔符和空格,然后(b)使用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
打印一个空字符——路径中永远不允许出现的一个字符,因为它通常用于在 C 程序中标记字符串的结尾——紧跟在文件名之后。这标志着表格的两列之间的每一行中的中断。-F ''
使file
print nothing (''
是一个空参数) 而不是:
. 冒号是不可靠的(它可以出现在文件名中)并且在这里没有任何好处,因为已经打印了一个空字符来指示路径列的结尾和描述列的开头。为了
find
运行,我使用. 的动作替换为路径。file --mime-type -r0F '' paths
-exec file --mime-type -r0F '' {} +
find
-exec
{} +
5. 消费表
我以这种方式创建了表格:
如上所述,这会在每个路径之后放置一个空字符。如果描述也是空终止的,那会很方便,但
file
不会这样做——描述总是以换行符结尾。所以我必须交替阅读直到一个空字符,然后假设有更多文本并阅读它直到换行。我必须为每个文件执行此操作,并在没有任何内容时停止。阅读每一行
这种组合——读取可能包含换行符直到出现空字符的文本,然后读取不能包含换行符直到换行符的文本——不是任何常见的 Unix 实用程序通常使用的方式。我将采取的方法是将输出管道
find
传输到一个循环。read
循环的每次迭代都通过两次使用内置的 shell 读取表的单行,并具有不同的选项。要阅读路径,我使用:
-r
是read
唯一的标准选项,您几乎应该始终使用它。没有它,来自输入的反斜杠转义\n
将被转换为它们所代表的字符。我们不希望那样。read
读取直到看到换行符。为了忽略换行符并改为在空字符处停止,我使用-d
Bash 提供的选项来指定不同的字符。对于空字符,传递空参数''
。-d
选项),所以当没有变量名传递给read
. 它将它读取的所有内容——除了终止字符——放在特殊变量$REPLY
中。通常从输入的开头和结尾read
去除空格(字符),这是一种常见的习惯用法,可以防止这种情况发生。在 Bash 中隐式读取时,这不是必需的。$IFS
IFS= read ...
$REPLY
要阅读说明,我使用:
-r
给它,read
除非您希望\
翻译转义符。mimetype
。IFS=
防止前导和尾随空格被剥离是很重要的。我想把它去掉。这会删除描述开头的空格,find
以使表格在终端中显示时更易于阅读。组成循环
只要有另一个要读取的路径,循环就应该继续。该
read
命令在成功读取某些内容时返回 true(在 shell 编程中,这是零,与几乎所有其他编程语言不同),如果没有,则返回 false(在 shell 编程中,任何非零值)。所以常用的while read
成语在这里很有用。我将 (|
) 的输出——find
它是一个或(很少)更多file
命令的输出——传递给while
循环。在循环内部,我阅读了该行的其余部分以获取描述 (
read -r mimetype
)。我不费心检查这是否成功。即使遇到错误,也file
应该只输出完整的行。(将错误和警告消息发送到标准错误,因此它们不会出现在管道中以破坏表。)您应该能够依赖它。file
如果你想检查是否
read -r mimetype
成功,你可以使用if
. 或者您可以将其包含在while
循环条件中:您可以看到我还拆分了第一行以提高可读性。(不需要
\
在 处拆分|
。)测试循环
如果您想在继续之前测试循环,您可以将此命令放在(或代替)
# Commands...
注释下:循环输出看起来像这样,具体取决于目录中的内容(为简洁起见,我省略了大多数条目):
这只是为了看看循环是否正常工作。像这样放置表的条目
[
]
不会帮助脚本完成它需要做的事情,因为路径可能包含[
,]
和连续的换行符。6.使用提取的路径和文件类型
在循环的每次迭代中,
"$REPLY"
包含路径并"$mimetype"
包含类型描述。要确定是否"$REPLY"
命名 JPEG 文件,请检查是否"$mimetype"
为image/jpeg
.if
您可以使用和[
/test
(或[[
)与比较字符串=
。但我更喜欢case
:如果您只想以与上述相同的格式显示 JPEG 的路径——以帮助测试包含换行符的路径——整个
case
...esac
语句可能是:但目标是
lepton
在每个 JPEG 文件上运行。为此,请使用:7. 把它们放在一起
添加该
lepton
命令和一个hashbang行以使用 Bash运行它,这是完整的脚本:lepton
报告它在做什么,但它不显示文件名。此替代脚本在运行之前会在每个路径lepton
上打印一条消息:我已将消息打印到标准错误(
>&2
),因为这是lepton
发送自己的消息的地方。这样,输出在管道或重定向时都保持在一起。运行该脚本会产生如下输出(但如果您有两个以上的 JPEG,则会产生更多输出):每个节中的重复——当你运行
lepton
而不打印文件名时也会出现——是因为lepton
检查它的输出文件是否可以正确解压缩。您显示的脚本
exit 0
在最后。如果你愿意,你可以这样做。它会导致脚本始终报告成功。否则脚本返回最后一个命令运行的退出状态——这可能是更可取的。无论哪种方式,它可能会报告成功,即使find
,file
或lepton
遇到问题,如果最后一个lepton
命令成功。当然,您可以使用更复杂的错误处理代码来扩展脚本。8. 也许你也想要路径
如果要生成与 ' 自己的输出分开的路径列表,则可以通过打印到标准输出
lepton
的路径来利用lepton
' 写入标准错误的行为。在这种情况下,您可能只想打印路径而不是“正在处理”消息。您可能希望使用空字符而不是换行符来终止路径,因为这样您就可以处理列表而不会破坏包含换行符的路径。当您运行该脚本时,您可以传递
-0
标志以使其发出空字符而不是换行符。该脚本没有进行正确的 Unix 风格的选项处理:它只检查您传递的第一个参数;在同一个参数中重复传递标志 (-00
) 不起作用;并且不会生成与选项相关的错误消息。这个限制是为了简洁,因为您可能不需要任何更复杂的东西,因为脚本不支持任何非选项参数并且-0
是唯一可能的选项。在我的系统上,我调用了该脚本
jpeg-lep3
并将其放入~/source
,然后运行,它仅将 ' 的输出~/source/jpeg-lep3 -0 > out
打印到我的终端。lepton
如果您这样做,您可以使用以下方法测试路径之间是否正确写入了空字符:先上代码:
让我们用 Bash 的特殊 glob 和
for
循环来做这件事:解释:
首先,我们需要通过启用
globstar
和dotglob
shell 选项使 Bash glob 更有用。以下是它们man bash
在 SHELL BUILTIN COMMANDS 部分中的描述shopt
:./**
然后我们在循环中使用这个新的“递归 glob”for
来迭代当前目录及其所有子目录中的所有文件和文件夹。./
请始终使用绝对路径或以 a 开头的显式相对路径,../
而不仅仅是.**
~
现在我们使用命令测试每个文件(和文件夹)名称的
file
内容。该-b
选项防止它在内容信息字符串之前再次打印文件名,这使得过滤更加安全。现在我们知道所有有效的 JPG/JPEG 文件的内容信息都必须以 开头
JPEG image data,
,这就是我们测试file
for with的输出grep
。我们使用该-q
选项来抑制任何输出,因为我们只对grep
的退出代码感兴趣,它指示模式是否匹配。如果匹配,将执行
if
/块内的代码。then
我们可以在这里做任何我们想做的事情。当前的 JPEG 文件名在 shell 变量中可用$f
。我们只需要确保始终将其放在双引号中,以防止意外评估带有空格、换行符或符号等特殊字符的文件名。通常最好将其与其他参数分开,将其放在 之后--
,这会导致大多数命令将其解释为文件名,即使它类似于-v
或--help
将被解释为选项。奖金问题:
为了科学,是时候炸掉一些代码了!这是您的问题/书中的版本:
首先,请允许我提及他们编写它的复杂程度。我们有 4 个级别的嵌套子shell,使用混合命令替换语法(
``
和$()
),这是必要的,因为find
.这里
find
只列出所有文件并打印它们的名称,每行一个。然后将完整的输出传递file
给检查它们中的每一个。可是等等!每行一个文件名?包含换行符的文件名呢?对,那些会破坏它!实际上,即使是简单的空格也会破坏它,因为它们也被视为分隔符
file
。您甚至不能引用"$(find ./ )"
此处作为补救措施,因为这会将整个多行输出引用为一个文件名参数。下一步,使用
file
扫描输出grep JPEG
。你不觉得欺骗这样一个简单的模式有点容易,尤其是当 plain 的输出file
总是包含文件名时?基本上,文件名中带有“JPEG”的所有内容都会触发匹配,无论它包含什么。好的,所以我们有
file
所有 JPEG 文件(或假装是一个)的输出,现在他们处理所有行以cut
从第一列中提取原始文件名,用冒号分隔......猜猜看,让我们试试这在其名称中带有冒号的文件上:所以总而言之,你书中的方法有效,但前提是它检查的所有文件都不包含任何空格、换行符、冒号和可能的其他特殊字符,并且文件名中的任何地方都不包含字符串“JPEG”。这也有点丑陋,但由于情人眼中的美,我不打算谈论这个。
您也
find
可以使用命令检查file
它的 mime 类型。或使其完整,如下所示:
或
identify
ImageMagic 包中的选项。