我是这方面的新手,想知道如何构建这个 shell 脚本:
我在目录 1 中有文件名的文件A1-001.xyz A29-002.xyz A82-003.xyz
,我想根据文件名的第二部分将这些文件移动001 002 003
到目录 2 中,文件夹名称为 001 002 003。
这是我到目前为止所做的:
for file in /path/to/directory1/** ; do
echo "$file" | awk -F '[-]' '{print $2}' | cut -f 1 -d '.' ;
done >> dummy.txt
input="dummy.txt"
while IFS= read -r file; do
echo "$file" | mv "$file" /path/to/directory2/$file ;
done
我的想法是将第一部分的输出文件名放入 dummy.txt 中,然后读取文件名并移动它。脚本的第二部分似乎不起作用,所以有关于如何执行此操作的任何建议吗?
答案1
从小事做起
将你的问题分解成更小的部分。您陷入困境的部分原因是您试图一次性构建整个解决方案,即使您正在尝试学习如何操作用于构建解决方案本身的工具。
我希望这里有一个技巧可以帮助您解决问题,并且当您将来必须分解和分析类似问题时,您和其他新手脚本编写人员都会从中受益:
首先指定需要对每个文件执行的操作的确切性质。事实上,您应该能够手动编写处理从文件列表中采样的一个特定文件名所需的命令。 不做工作,只写命令。在您的示例中,每个文件都需要移动,是吗?因此,每个文件都需要一个mv
命令。而不是纠结如何做做命令mv
,只需关心如何创建它。你会如何手动编写一这样的mv
命令来移动文件?那么问题就变成了如何awk
(或您想要使用的任何工具)输出该命令:
mv (filename) (to-where-you-want-it)
对于您指定的每个文件名。当您学习新工具时,调试脚本会更容易,该脚本只需创建一系列 shell 命令作为其输出,而无需实际操作正在做任何事情都比调试一个脚本更重要,该脚本只是横向移动并将数百个错误的文件移动到数百个错误的目录中,现在您不再确定任何东西在哪里。
对于初学者,请查阅man
您认为适合您的工具的页面。然后在手动模式下试验该命令,只是为了了解需要做什么才能让该工具按照您想要的方式解析您的输入并创建您需要的输出。在编写移动 100 或 1000 个文件的脚本之前,您需要一个能够正确移动的脚本只有一个文件。因此,创建一个测试用例,并花一些时间与您认为可行的工具“交朋友”。您的帖子已被标记awk我认为这是一个明智的选择,所以我们就这样做吧。
awk
有一个-F
参数可用于指定awk
用于将字符串分解为组件字段的分隔符。该分隔符可以是一个简单的字符,也可以是括号中的多个字符中的任何一个。用正则表达式的说法,这被称为字符类。您的输入同时使用连字符'-'
和句点'.'
作为字段分隔符,因此我们可以指定字符类[-.]
来告诉awk
在连字符或句点上进行分割。请仔细注意,awk
并不关心哪个是哪个,并确保您的源目录不包含任何连字符或句点。
用于awk
将每个文件名分解为组件字段
获取文件名的示例A1-001.xyz
并尝试通过此awk
命令手动运行它,以了解awk
该文件名的作用:
$ awk -F[-.] '{print $0 " " $1 " " $2 " " $3}' <<< 'A1-001.xyz'
该命令告诉awk
:“使用连字符和句点作为字段分隔符,打印整个输入行 ( $0
)、一个空格、字段 1、一个空格、字段 2、一个空格,最后是字段 3。
输出是:
A1-001.xyz A1 001 xyz
希望这向您展示了很多内容:这$0
就是您在mv
命令源中需要的内容,因为这是完整的原始文件名;这$2
就是您在命令目标中所需要的mv
,因为这是您想要的数字目录名称。最大的实现是awk
可以完全为你格式化mv
命令,并将其打印出来。所需要的只是稍微调整一下awk
的声明。print
与其试图让脚本完成所有事情,不如让脚本完成所有事情创造您需要执行的命令。这样,脚本中的错误就不会导致脚本崩溃并将文件移动到错误的位置。它只会打印一些错误的输出,您会注意到它是错误的,但不会造成任何损害。
awk
细化命令的第二次迭代
文件名前面可能有一个源路径。但请确保路径中没有任何.
或字符!-
因此,mv
每个文件的命令显然以mv
一个空格开头,然后是文件名(可能包括完整的源路径)、另一个空格以及要将文件移动到的目录。为了更好地衡量,我们将在目标目录后面添加一个斜杠。既然你是不是更改文件名,我们只需指定目标目录并省略目标文件名。这样做也更容易,这一点值得注意。不要让事情变得比需要的更困难。
$ awk -F[-.] '{print "mv " $0 " " $2 "/"}' <<< '/path/to/directory1/A1-001.xyz'
mv /path/to/directory1/A1-001.xyz 001/
看print
命令:以空格开头mv
,然后$0
是完整的文件名;另一个空格,则$2
是输出子目录。同样,您必须确保您的源路径名称不要包含任何连字符或句点,因为它们作为文件名中的字段分隔符具有特殊含义。 More 是问题所在,awk
不会正确分割您的字段,并且您的脚本将会中断。
但目标目录不仅仅是$2
,它前面还有一个前缀,就像源文件名一样。我们可以awk
为我们打印它,因为每次都是一样的:
$ awk -F[-.] '{print "mv " $0 " /path/to/directory2/" $2 "/"}' <<< '/path/to/directory1/A1-001.xyz'
mv /path/to/directory1/A1-001.xyz /path/to/directory2/001/
在整个文件列表上测试解决方案
所以这看起来很有希望。现在创建一个文件列表file-list.txt
:
$ cat file-list.txt
A1-001.xyz
A29-002.xyz
A82-003.xyz
然后awk
对整个文件列表运行命令。请记住,这里没有什么坏处,因为awk
所做的一切都是印刷东西。它实际上并没有做任何关于移动文件的事情。它只是向您展示将执行您想做的事情的命令。
$ awk -F[-.] '{print "mv " $0 " /path/to/directory2/" $2 "/"}' < file-list.txt
mv A1-001.xyz /path/to/directory2/001/
mv A29-002.xyz /path/to/directory2/002/
mv A82-003.xyz /path/to/directory2/003/
仔细检查输出、测试并执行
如果您有很多文件要移动,您需要将awk
上面的命令通过管道传输到其中,less
以便您可以仔细检查它。查找错误位置的点和破折号,或者文件或目录名称中的其他奇怪字符。如果您愿意,可以将该输出的示例行复制并粘贴到 shell 提示符中,以确保它执行正确的操作。但这是一个足够简单的例子,我们可以通过检查来测试。一旦您确信此mv
命令列表就是您想要执行的操作,只需将 的输出awk
直接通过管道传输sh
即可执行它。如果您想在命令执行时查看命令,请使用sh -v
而不是仅仅使用sh
:
$ awk -F[-.] '{print "mv " $0 " /path/to/directory2/" $2 "/"}' < file-list.txt | sh -v
mv A1-001.xyz /path/to/directory2/001/
mv A29-002.xyz /path/to/directory2/002/
mv A82-003.xyz /path/to/directory2/003/
$
结论
我希望您不反对进行如此详细的细分,但此类问题在 Stack Exchange 上经常出现,许多新手脚本编写者认为他们的问题是一个独特的一次性问题,需要独特的解决方案。
脚本编写的真正关键是要认识到脚本编写提供了可以解决各种问题的通用工具,而获得熟练程度的第一步就是学习如何使用这些工具做小事情,然后将这些小事情组合成越来越大的事情。
第一步只是学习如何awk
按照我们需要的方式分解文件名。每当您尝试从嵌入了多条信息的文件名中解析组件字段时,这都是关键的一步。
第二步是告诉 awk 自动打印每个文件始终相同的命令部分(开头的mv
,$2
字段之前的目标路径),并将提取的文件名字段放在正确的位置。 print
语句及其同类是任何类型编码中最基本的部分之一,我不记得适当的print
语句带来过多大的伤害。可以肯定的是,您只想输出必要的内容,但是当您学习时,如果您不知道变量是什么,请将其打印出来,问一下也没什么坏处。从长远来看,您将取消该打印语句,但是脚本编写的“打印然后管道到外壳”技术的全部要点是您内置了“试运行”,因为您总是查看在将脚本实际通过管道传输到 shell 执行之前,先查看脚本输出的 shell 命令。在复杂的情况下,甚至在输出中添加注释也是公平的游戏,以“展示您的工作”:
$ awk -F[-.] '{print "# move file " $0 " to subdir " $2; print "mv " $0 " /path/to/directory2/" $2 "/"}' < file-list.txt
# move file A1-001.xyz to subdir 001
mv A1-001.xyz /path/to/directory2/001/
# move file A29-002.xyz to subdir 002
mv A29-002.xyz /path/to/directory2/002/
# move file A82-003.xyz to subdir 003
mv A82-003.xyz /path/to/directory2/003/
第三个关键,也许与我的第二点密切相关,但我认为经常被忽视的是,当你正在做一些对你来说有点困难的事情时,不要编写一个可能会出错的脚本并离开你的文件散落在无数不同但错误的地方。只需编写一个脚本即可编写脚本做这项工作。通过这种方式排除故障要容易得多。然后,当您最终获得正确的脚本时,只需将脚本输出(在您的示例中,一系列mv
命令,每个文件一个)通过管道传输到 shell 中,它们就会运行。
答案2
脚本的第二部分失败有两个问题。首先,您实际上并没有读取循环中的任何输入。你有过:
while IFS= read -r file; do something; done
但你需要:
while IFS= read -r file; do something; done < "$inputFile"
然后,mv
无法从输入流中读取数据,将数据传输到其中是没有意义的。它需要文件名作为输入而不仅仅是文本,而且它无论如何也不会从标准输入读取。所以echo "$file" | mv "$file" "/somewhere"
和刚才跑步是完全一样的mv "$file" "/somewhere"
。这echo $file
是毫无意义的。它不起作用,因为$file
只有文件名的第二部分(001
等002
),而不是实际的文件名。
无论如何,您可以直接用一个循环完成整个事情,不需要中间文件:
for file in /path/to/directory1/** ; do
dirName=$(awk -F[-.] '{print $2}' <<<"$file");
echo mv "$file" "/path/to/directory2/$dirName";
done
如果打印出您需要的内容,请删除echo
并再次运行它以实际移动文件。