我有一个任务:
编写一个 shell 脚本,将当前目录中所有“.c”文件的前 3 行代码复制到作为位置参数提供的临时文件中。显示临时文件的内容。
最初我创建了以“.c”结尾的文件,然后我曾经head -3 *.c> touch $1
能够复制具有该扩展名的每个文件的前 3 行,但我想知道我是否做得好或者可以用其他方式解决。
答案1
head
乍一看,您的方法看起来不错,但是您会注意到,当您head
同时运行多个文件时,该实用程序如何将标头放入输出中,其中包含文件名。
你可能会可能想要避免获得此标题,只需遵循作业文本即可。
touch
您也根本不必使用。我注意到在尝试解决其他家庭作业时,人们有时认为他们必须先“创建文件”,然后再将数据重定向到该文件,然后他们用来touch
执行此操作。
当您使用 重定向时>filename
,如果给定文件尚不存在(并且当前目录的权限允许),则将自动创建给定文件。如果文件存在,它将被截断(清空)。
好的,那么我们如何停止head
在输出中生成带有文件名的标头呢?好吧,如果您使用的是 Linux 系统,那么您可能拥有 GNU head
。这个实现head
有A非标准 -q
选项抑制标题。
因此该脚本可以写为
#!/bin/sh
head -q -n 3 -- *.c >"$1"
...假设用户有 GNU head
。请注意--
.需要发出“命令行选项结束”信号,以防head
任何与*.c
文件名通配模式匹配的文件名以破折号开头。该破折号可能会被视为选项字符串的开头。
另一种方法是使用head -q -n 3 ./*.c
where./*.c
显式引用当前目录中的文件。由于每个文件名都以 开头./
,因此任何参数都没有机会以破折号开头,因此--
不再需要。使用哪种方式执行此操作取决于您,但请使用--help.c
当前目录 ( touch -- --help.c
) 中调用的文件来测试您的脚本。
我选择使用/bin/sh
该脚本的解释器,而不是/bin/bash
.我这样做是因为脚本没有使用它bash
所需的任何内容bash
,例如数组、进程替换、大括号扩展、正则表达式匹配等。
如果你不使用 Linux 系统,或者你想遵循 POSIX 标准并编写便携的脚本,您不应该使用-q
with head
。
相反,您可能希望循环遍历文件并head
在每个单独的文件上使用:
#!/bin/sh
for name in *.c; do
head -n 3 -- "$name"
done >"$1"
注意我们如何重定向输出循环的到一个文件。
您还会注意到,即使您有,通过使用循环解决此问题也将使脚本正常工作数千文件数量.c
。如果没有循环,当 shell 尝试head
以所有数千个文件名的扩展运行时,您可能会收到“参数列表太长”错误。这缺点一个问题是,单独针对每个文件运行head
相当慢,尤其是当您有数千个文件时。
下一个问题是弄清楚如果该脚本的用户没有提供正确的参数会发生什么。假设用户使用已存在的文件名或根本没有文件名运行此脚本。让我们抓住这一点并抱怨而不做任何其他事情:
#!/bin/sh
if [ "$#" -ne 1 ]; then
printf 'expecting 1 argument, got %d\n' "$#" >&2
exit 1
elif [ -e "$1" ]; then
printf 'the name "%s" already exist, refusing to over-write\n' "$1" >&2
exit 1
fi
for name in *.c; do
head -n 3 -- "$name"
done >"$1"
这引入了一个if
语句,该语句首先测试提供给脚本的命令行参数的数量。如果不完全是一个,抱怨并退出。如果是一个,但它指的是一个已经存在的名称,请抱怨并退出。
请注意,诊断消息(如错误)应写入标准错误流。我在这里通过使用 重定向输出来做到这一点>&2
。当很明显我们无法继续时,我还会以非零退出状态终止脚本。这使得可以测试您的脚本是否成功运行:
if ./your-script.sh hello world; then
echo ok
else
echo something went wrong
fi
剩下的问题是处理以下情况:不 .c
当前目录中的文件。发生这种情况时,您会注意到脚本如何生成一个奇怪的错误:
head: *.c: No such file or directory
这是因为当诸如 之类的模式*.c
不匹配任何内容时,它仍然保持未扩展状态。我们可以通过在循环中添加一个小测试来解决这个问题:
for name in *.c; do
[ ! -e "$name" ] && continue
head -n 3 -- "$name"
done >"$1"
这意味着“如果文件$name
不存在,则跳过本次循环迭代”。
如果将此脚本编写为bash
脚本,则可以保留原始循环,然后nullglob
在循环之前设置 shell 选项shopt -s nullglob
,以使 shell 删除不匹配的模式,而不是保留它们未展开。
现在,当您运行脚本时,当当前目录中没有.c
文件时,它不会生成任何错误,但如果发生这种情况,输出文件将为空。如果这是不希望的,那么您可能需要*.c
在开始循环并重定向到输出文件之前测试是否实际匹配任何内容:
outfile=$1
set -- *.c
if [ -e "$1" ]; then
for name do
head -n 3 -- "$name"
done >"$outfile"
fi
在这里,我们首先将输出文件的名称保存在一个单独的变量中,因为我们将用所有文件的名称覆盖位置参数(给脚本的参数).c
(这是通过 完成的set
)。
如果第一个位置参数是现有文件名,那么我们允许循环运行并创建/截断输出文件。
当循环没有给出任何要迭代的内容时(如最后一段代码所示),默认情况下将迭代位置参数列表。我们已经将此列表设置为名称以.c
.
其他变体是head
根本不使用,而是使用等效命令,例如sed 3q
orawk '1; NR == 3 { exit }'
或其他一些命令。然而,通过使用该head
实用程序,您可以清楚地了解您打算做什么。