当我必须编写一个BASH脚本来处理任意目录树并查看任意文件并试图确定它们之间的比较时,这一天到来了。我认为这将是一个简单的几个小时 tops!过程 - 不是这样!
我的挂断是有时候有些白痴 - 唉! - 对不起,可爱的用户选择在目录和文件名中放置空格。这会导致我的脚本失败。
完美的解决方案,除了为那些坚持在这些地方使用空间的人威胁断头台(更不用说那些把它放在操作系统代码中的人了!),可能是一个例程为我们“逃避”文件和目录名称,有点像cygwin如何将例程从unix转换为dos文件格式。在标准的Unix / Linux发行版中有这样的东西吗?
请注意,简单for file in *
构造在尝试比较目录树时效果不佳,因为 ONLY 在“当前目录”上工作 - 在这种情况下和许多其他人一样,不断地CD到各种目录位置带来了它自己的问题。所以,在完成我的作业时,我发现了这个问题Handle special characters in bash for...in loop,并且建议的解决方案挂起了目录名称中的空格,但可以像这样简单地克服:
dir="dirname with spaces"
ls -1 "$dir" | while read x; do
echo $x
done
请注意:以上代码并不是特别精彩,因为while循环中使用的变量在while循环之外是不可接受的。这是因为当ls命令的输出被管道传输时,会创建一个隐含的子shell。 这是我查询的关键推动因素!
...好吧,上面的代码对很多情况有帮助,但“转义”字符也会非常强大。例如,上面的dir可能包含:
dir\ with\ spaces
这是否已经存在且我一直在忽视它?
如果没有,有没有人有一个简单的建议写一个 - 也许与sed或lex? (我对两者都不太称职。)
答案 0 :(得分:4)
为测试制作一个非常讨厌的文件名:
mkdir escapetest
cd escapetest && touch "m'i;x&e\"d u(p\nmulti)\nlines'\nand\015ca&rr\015re;t"
[编辑:有可能我打算touch
命令:
touch $'m\'i;x&e\"d u(p\nmulti)\nlines\'\nand\015ca&rr\015re;t'
会在文件名中添加更难看的字符。输出看起来会有点不同。的 强>
然后运行:
find -print0 | while read -d '' -r line; do echo -en "--[${line}]--\t\t"; echo "$line"|sed -e ':t;N;s/\n/\\n/;bt' | sed 's/\([ \o47()"&;\\]\)/\\\1/g;s/\o15/\\r/g'; done
输出应如下所示:
--[./m'i;x&e"d u(p multi) lines' re;t]-- ./m\'i\;x\&e\"d\ u\(p\\nmulti\)\\nlines\'\\nand\\015ca\&rr\\015re\;t
这包括 Pascal Thivent的 sed
怪物的精简版本,以及回车和换行的处理以及可能更多。
第一次通过sed
将多行合并为一个由“\ n”分隔的行,用于具有换行符的文件名。第二遍从一个字符列表中替换任何一个字符列表,并在其前面加上反斜杠。最后一部分用“\ r”替换回车符。
需要注意的一点是,正如您所知,while
将处理空格而for
将不会,但通过发送find
的输出为空终止并设置分隔符read
为null,您还可以处理文件名中的换行符。 -r
选项会导致read
接受反斜杠而不解释它们。
修改:
另一种逃避特殊字符的方法,这次不使用sed
,使用Bash printf
内置的引用和变量创建功能(这也说明了使用进程替换而不是管道) :
while read -d '' -r file; do echo "$file"; printf -v name "%q" "$file"; echo "$name"; done< <(find -print0)
变量$name
将在循环外部可用,因为使用进程替换会阻止在循环周围创建子shell。
答案 1 :(得分:2)
我在谷歌搜索时发现了这个How to escape file names in bash shell scripts,我在下面引用:
与Bash战斗后相当 一段时间,我发现了 以下代码提供了很好的基础 用于逃避特殊字符。 cource它不完整,但是 最重要的人物是 过滤。
如果有人有更好的解决方案, 请告诉我。它的确有效 可读但不漂亮。
FILE_ESCAPED=`echo "$FILE" | \ sed s/\\ /\\\\\\\\\\\\\\ /g | \ sed s/\\'/\\\\\\\\\\\\\\'/g | \ sed s/\&/\\\\\\\\\\\\\\&/g | \ sed s/\;/\\\\\\\\\\\\\\;/g | \ sed s/\(/\\\\\\\\\\(/g | \ sed s/\)/\\\\\\\\\\)/g `
也许你可以用它作为起点。
答案 2 :(得分:2)
以下代码段处理所有文件名(包括空格,引号,换行符等):
startdir="${1:-.}" # first parameter or working directory
#-------------------------------------------------------------------------------
# IFS is undefined
# read:
# -r do not allow backslashes to escape any characters
# -d delimiter is \0 (not a valid character in a filename)
# done < <( find ... ) . redirection from a process substitution
#-------------------------------------------------------------------------------
while IFS= read -r -d '' file; do
echo "'$file'"
done < <( find "$startdir" -type f -print0 )
另见BashFAQ。
答案 3 :(得分:2)
转义方法存在一个非常严重的问题:需要哪些转义取决于变量将被扩展的上下文,并且在通常情况下,没有转义它将起作用。例如,如果您要做一些简单的事情:
touch a "b c" d
files="a b\ c d"
ls $files
...它不起作用(ls查找4个文件:“a”,“b \”,“c”和“d”)因为shell在单词时没有注意转义-splits $ files。您可以使用eval ls $files
,但这会在文件名中的选项卡等方面失败。
建议使用while ... read ... done < <(find ... -print0)
方法fgm(并且由于find的搜索模式的灵活性非常强大),但它也是一堆相当混乱的各种可能问题的解决方法;如果您不需要发现权力,那么使用for
和*
完成任务并不困难:
shopt -s nullglob # In case of empty directories...
for filepath in "$dir"/*; do # loop over all files in the specified directory
filename="${filepath##*/}" # You just wanted the files' names? No problem.
echo "$filename"
done
如果(正如你在问题中提到的那样)你对比较两个目录树感兴趣,那么循环其中一个目录树并不是你想要的;把它们的内容放到数组中会更好,比如:
shopt -s nullglob
pathlist1=("$dir1"/*) # Get a list of paths of files in dir1
filelist1=("${pathlist1[@]##*/}") # Parse off just the filenames
pathlist2=("$dir2"/*) # Same for dir2
filelist2=("${pathlist2[@]##*/}")
# now compare filelist1 with filelist2...
(请注意,"${pathlist2[@]##*/}"
构造的AFAIK不是标准的,但现在bash和zsh似乎都支持了。)
答案 4 :(得分:1)
#!/bin/bash
while read filename; do
echo 'I am doing something with "'"$filename"'".'
done < <(find)
请注意,当<( )
调用bash时,/bin/sh
表示法不起作用。
答案 5 :(得分:0)
find命令有时适用于这种情况:
find . -exec ls {} \;
例如