2008-11-19 74 views
109

我有一个bash shell脚本,可以遍历某个目录的所有子目录(但不是文件)。问题是一些目录名称包含空格。如何逃避bash循环列表中的空白区域?

这里是我的测试目录的内容:

$ls -F test 
Baltimore/ Cherry Hill/ Edison/ New York City/ Philadelphia/ cities.txt 

并通过目录循环代码:

for f in `find test/* -type d`; do 
    echo $f 
done 

这里的输出:

 
test/Baltimore 
test/Cherry 
Hill 
test/Edison 
test/New 
York 
City 
test/Philadelphia 

樱桃山和纽约市被视为2或3个单独的条目。

我想引用的文件名,例如:

for f in `find test/* -type d | sed -e 's/^/\"/' | sed -e 's/$/\"/'`; do 
    echo $f 
done 

,但无济于事。

有一个简单的方法来做到这一点。


下面的答案很好。但是为了使这更加复杂 - 我并不总是希望使用我的测试目录中列出的目录。有时我想将目录名称作为命令行参数传入。

我把设置IFS的查尔斯的建议,并与下面的上前:

dirlist="${@}" 
(
    [[ -z "$dirlist" ]] && dirlist=`find test -mindepth 1 -type d` && IFS=$'\n' 
    for d in $dirlist; do 
    echo $d 
    done 
) 

,除非有在命令行参数(即使这些参数均以)空间这个工作得很好。例如,调用这样的脚本:test.sh "Cherry Hill" "New York City"输出如下:

 
Cherry 
Hill 
New 
York 
City 

回答

99

首先,不要那样做。最好的方法是使用find -exec正确:

# this is safe 
find test -type d -exec echo '{}' + 

的其他安全的方法是使用NULL结尾的名单,不过这需要你寻找支撑-print0

# this is safe 
while IFS= read -r -d '' n; do 
    printf '%q\n' "$n" 
done < <(find test -mindepth 1 -type d -print0) 

您还可以填充数组从寻找,并通过该数组后:

# this is safe 
declare -a myarray 
while IFS= read -r -d '' n; do 
    myarray+=("$n") 
done < <(find test -mindepth 1 -type d -print0) 
printf '%q\n' "${myarray[@]}" # printf is an example; use it however you want 

如果您发现不支持-print0,你的结果是再不安全的 - 如果存在的文件包含换行符在其名称中根据需要将以下行为不(其中,是的,是合法的):

# this is unsafe 
while IFS= read -r n; do 
    printf '%q\n' "$n" 
done < <(find test -mindepth 1 -type d) 

如果一个人不打算使用上述任何一种,第三种方法(在时间和内存使用方面效率较低,因为它在进行分词之前读取子进程的整个输出)是使用不包含空格字符的IFS变量。关闭通配符(set -f),以防止含水珠字符的字符串被扩大,如[]*?

# this is unsafe (but less unsafe than it would be without the following precautions) 
(
IFS=$'\n' # split only on newlines 
set -f # disable globbing 
for n in $(find test -mindepth 1 -type d); do 
    printf '%q\n' "$n" 
done 
) 

最后,对于命令行参数的情况下,你应该使用数组,如果你的shell支持它们(即它是ksh,bash或zsh):

# this is safe 
for d in "[email protected]"; do 
    printf '%s\n' "$d" 
done 

将保持分离。请注意,报价(以及使用[email protected]而不是$*)非常重要。数组可以通过其他方式来填充为好,如水珠的表达式:

# this is safe 
entries=(test/*) 
for d in "${entries[@]}"; do 
    printf '%s\n' "$d" 
done 
+1

不知道有关-exec是 '+' 的味道。 sweet – 2008-11-19 05:27:33

+1

tho看起来像它也可以,就像xargs一样,只会将参数放在给定命令的末尾:/这有时会给我带来麻烦 – 2008-11-19 05:35:21

+0

我认为-exec [name] {} +是一个GNU和4.4-BSD扩展。 (至少,它不会出现在Solaris 8上,我认为它也不在AIX 4.3中。)我想我们其他人可能会被塞进xargs ... – 2008-11-19 06:00:51

25
find . -type d | while read file; do echo $file; done 

但是,如果文件名中包含换行不起作用。以上是我知道的唯一解决方案,当你真的想在变量中拥有目录名称时。如果你只是想执行一些命令,使用xargs。

find . -type d -print0 | xargs -0 echo 'The directory is: ' 
+0

无需xargs的,看到找到-exec ... {} + – 2008-11-19 05:53:22

+4

@Charles:对于大量文件,xargs的效率要高得多:它只是一个派生过程。 -exec选项会为每个文件分叉一个新进程,速度可能会降低一个数量级。 – 2008-11-19 05:54:25

7

这是标准的Unix非常棘手,而且大多数解决方案运行新行或其它字符的犯规。但是,如果您正在使用GNU工具集,则可以利用find选件-print0并使用xargs和相应的选项-0(零 - 零)。有两个字符不能以简单的文件名出现;那些是斜线和NUL'\ 0'。显然,斜杠出现在路径名中,所以使用NUL'\ 0'来标记名称末尾的GNU解决方案是巧妙的和防呆的。

2

要添加什么Jonathan说:连同xargs使用-print0选项find如下:

find test/* -type d -print0 | xargs -0 command 

将执行用正确的参数的命令command;带有空格的目录将被正确引用(即它们将作为一个参数传入)。

4

不要将列表存储为字符串;将它们存储为数组以避免所有这些分隔符混淆。这里有一个例子脚本会无论是在测试的所有子目录进行操作,或者它的命令行上提供的列表:

#!/bin/bash 
if [ $# -eq 0 ]; then 
     # if no args supplies, build a list of subdirs of test/ 
     dirlist=() # start with empty list 
     for f in test/*; do # for each item in test/ ... 
       if [ -d "$f" ]; then # if it's a subdir... 
         dirlist=("${dirlist[@]}" "$f") # add it to the list 
       fi 
     done 
else 
     # if args were supplied, copy the list of args into dirlist 
     dirlist=("[email protected]") 
fi 
# now loop through dirlist, operating on each one 
for dir in "${dirlist[@]}"; do 
     printf "Directory: %s\n" "$dir" 
done 

现在,让我们尝试了这一点与曲线或两个测试目录抛出:

$ ls -F test 
Baltimore/ 
Cherry Hill/ 
Edison/ 
New York City/ 
Philadelphia/ 
this is a dirname with quotes, lfs, escapes: "\''?'?\e\n\d/ 
this is a file, not a directory 
$ ./test.sh 
Directory: test/Baltimore 
Directory: test/Cherry Hill 
Directory: test/Edison 
Directory: test/New York City 
Directory: test/Philadelphia 
Directory: test/this is a dirname with quotes, lfs, escapes: "\'' 
' 
\e\n\d 
$ ./test.sh "Cherry Hill" "New York City" 
Directory: Cherry Hill 
Directory: New York City 
0

刚发现我的question和你的有一些相似之处。 Aparrently如果你想传递参数到命令

test.sh "Cherry Hill" "New York City" 

打印出来,以便

for SOME_ARG in "[email protected]" 
do 
    echo "$SOME_ARG"; 
done; 

通知$ @被双引号包围,一些注意事项here

1

必须处理路径名中也有空格。

function recursedir { 
    local item 
    for item in "${1%/}"/* 
    do 
     if [ -d "$item" ] 
     then 
      recursedir "$item" 
     else 
      command 
     fi 
    done 
} 
19

这里有一个简单的解决方案,处理选项卡和/或空格的文件名:我终于做到用递归和for item in /path/*了。如果您必须处理文件名中其他奇怪的字符(如换行符),请选择另一个答案。

test目录

ls -F test 
Baltimore/ Cherry Hill/ Edison/ New York City/ Philadelphia/ cities.txt 

的代码进入,如果作为参数的目录

find test -type d | while read f ; do 
    echo "$f" 
done 

文件名必须用引号("$f")。如果没有引号,则空格将充当参数分隔符,并为调用的命令提供多个参数。

和输出:

test/Baltimore 
test/Cherry Hill 
test/Edison 
test/New York City 
test/Philadelphia 
-4

只是进行了简单的变形问题...转换类型的FLV文件的为.mp3(打哈欠)。

for file in read `find . *.flv`; do ffmpeg -i ${file} -acodec copy ${file}.mp3;done 

递归地发现所有的Macintosh用户的Flash文件,并把它们变成音频(复印件,无转码)......它像上面的同时,指出阅读,而不是仅仅“为文件”将难逃。

1
#!/bin/bash 

dirtys=() 

for folder in * 
do  
if [ -d "$folder" ]; then  
    dirtys=("${dirtys[@]}" "$folder")  
fi  
done  

for dir in "${dirtys[@]}"  
do  
    for file in "$dir"/\*.mov # <== *.mov 
    do  
     #dir_e=`echo "$dir" | sed 's/[[:space:]]/\\\ /g'` -- This line will replace each space into '\ ' 
     out=`echo "$file" | sed 's/\(.*\)\/\(.*\)/\2/'`  # These two line code can be written in one line using multiple sed commands.  
     out=`echo "$out" | sed 's/[[:space:]]/_/g'`  
     #echo "ffmpeg -i $out_e -sameq -vcodec msmpeg4v2 -acodec pcm_u8 $dir_e/${out/%mov/avi}"  
     `ffmpeg -i "$file" -sameq -vcodec msmpeg4v2 -acodec pcm_u8 "$dir"/${out/%mov/avi}`  
    done  
done 

上述代码会将.mov文件转换为.avi。 .mov文件位于不同的文件夹中,并且 文件夹名称也有白色空间。我的上面的脚本会将.mov文件转换为.avi文件在同一个文件夹中。我不知道它是否有助于你们的人民。

案例:

[[email protected] shell_tutorial]$ ls 
Chapter 01 - Introduction Chapter 02 - Your First Shell Script 
[[email protected] shell_tutorial]$ cd Chapter\ 01\ -\ Introduction/ 
[[email protected] Chapter 01 - Introduction]$ ls 
0101 - About this Course.mov 0102 - Course Structure.mov 
[[email protected] Chapter 01 - Introduction]$ ./above_script 
... successfully executed. 
[[email protected] Chapter 01 - Introduction]$ ls 
0101_-_About_this_Course.avi 0102_-_Course_Structure.avi 
0101 - About this Course.mov 0102 - Course Structure.mov 
[[email protected] Chapter 01 - Introduction]$ CHEERS! 

干杯!

-3

对于我这个工作,这是非常 “干净”:

for f in "$(find ./test -type d)" ; do 
    echo "$f" 
done 
4

为什么不干脆把

IFS='\n' 

在前面的命令?这从<空间> <标签> <换行符>更改字段分隔符只是<换行符>

3
find . -print0|while read -d $'\0' file; do echo "$file"; done 
1

采取
思想转换文件列表到击阵列。这使用Matt McClure的方法从Bash函数返回数组: http://notes-matthewlmcclure.blogspot.com/2009/12/return-array-from-bash-function-v-2.html 结果是将任何多行输入转换为Bash数组的方法。

#!/bin/bash 

# This is the command where we want to convert the output to an array. 
# Output is: fileSize fileNameIncludingPath 
multiLineCommand="find . -mindepth 1 -printf '%s %p\\n'" 

# This eval converts the multi-line output of multiLineCommand to a 
# Bash array. To convert stdin, remove: < <(eval "$multiLineCommand") 
eval "declare -a myArray=`(arr=(); while read -r line; do arr[${#arr[@]}]="$line"; done; declare -p arr | sed -e 's/^declare -a arr=//') < <(eval "$multiLineCommand")`" 

for f in "${myArray[@]}" 
do 
    echo "Element: $f" 
done 

这种做法甚至出现不好的时候人物的存在是为了工作,而对任何输入转换为猛砸阵列的通用方法。缺点是如果输入很长,可能会超出Bash的命令行大小限制,或者占用大量内存。

最终在列表中工作的循环也有列表管道的方法存在读取stdin不容易的缺点(如询问用户输入),并且循环是一个新进程,因此您可以想知道为什么你在循环内设置的变量在循环结束后不可用。

我也不喜欢设置IFS,它可以搞砸其他代码。

3

PS如果只是关于输入空间,然后一些双引号顺利工作对我来说...

read artist; 

find "/mnt/2tb_USB_hard_disc/p_music/$artist" -type f -name *.mp3 -exec mpg123 '{}' \; 
0

我需要同样的理念,从特定的文件夹压缩顺序几个目录或文件。我已经解决了使用awk从ls解析列表并避免名称中出现空格的问题。

source="/xxx/xxx" 
dest="/yyy/yyy" 

n_max=`ls . | wc -l` 

echo "Loop over items..." 
i=1 
while [ $i -le $n_max ];do 
item=`ls . | awk 'NR=='$i'' ` 
echo "File selected for compression: $item" 
tar -cvzf $dest/"$item".tar.gz "$item" 
i=$((i + 1)) 
done 
echo "Done!!!" 

您认为如何?

0
find Downloads -type f | while read file; do printf "%q\n" "$file"; done 
3

你可以使用IFS(内部字段分隔符)暂时使用:

OLD_IFS=$IFS  # Stores Default IFS 
IFS=$'\n'  # Set it to line break 
for f in `find test/* -type d`; do 
    echo $f 
done 

$IFS=$OLD_IFS 

0

好了,我看到了太多复杂的答案。我不想传递find实用程序的输出或编写循环,因为find具有“exec”选项。

我的问题是,我想将所有带有dbf扩展名的文件移动到当前文件夹,其中一些文件包含空格。

我解决它,以便:

find . -name \*.dbf -print0 -exec mv '{}' . ';' 

看起来更简单,我