如何在bash循环列表中转义空格?

时间:2008-11-19 05:10:43

标签: bash loops whitespace

我有一个bash shell脚本,它遍历某个目录的所有子目录(但不是文件)。问题是某些目录名称包含空格。

以下是我的测试目录的内容:

$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

Cherry Hill和纽约市被视为2或3个单独的条目。

我尝试引用文件名,如下所示:

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

但无济于事。

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


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

我接受了Charles关于设置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

19 个答案:

答案 0 :(得分:103)

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

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

另一种安全方法是使用NUL终止列表,但这需要您的查找支持-print0

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

您也可以从find填充数组,稍后传递该数组:

# 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 "$@"; do
  printf '%s\n' "$d"
done

将保持分离。请注意,引用(以及使用$@而不是$*)非常重要。数组也可以用其他方式填充,例如glob表达式:

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

答案 1 :(得分:25)

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

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

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

答案 2 :(得分:21)

这是一个处理文件名中的制表符和/或空格的简单解决方案。如果您必须处理文件名中的其他奇怪字符(如换行符),请选择其他答案。

测试目录

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

答案 3 :(得分:7)

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

答案 4 :(得分:4)

您可以暂时使用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

答案 5 :(得分:4)

我用

SAVEIFS=$IFS
IFS=$(echo -en "\n\b")
for f in $( find "$1" -type d ! -path "$1" )
do
  echo $f
done
IFS=$SAVEIFS

这还不够吗? 取自http://www.cyberciti.biz/tips/handling-filenames-with-spaces-in-bash.html

的想法

答案 6 :(得分:4)

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

答案 7 :(得分:4)

为什么不放

IFS='\n'
在for命令前面的

?这会将字段分隔符从&lt;空间&GT;&LT;标签&gt;&LT;换行符&GT;只是&lt;换行符&GT;

答案 8 :(得分: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=("$@")
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

答案 9 :(得分:3)

ps如果只是输入中的空格,那么一些双引号对我来说很顺利......

read artist;

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

答案 10 :(得分:2)

要添加Jonathan所说的内容:使用-print0的{​​{1}}选项与find一起使用,如下所示:

xargs

这将使用正确的参数执行命令find test/* -type d -print0 | xargs -0 command ;其中包含空格的目录将被正确引用(即它们将作为一个参数传入)。

答案 11 :(得分: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文件。我不知道它是否对你们有帮助。

案例:

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

干杯!

答案 12 :(得分:1)

也必须处理路径名中的空格。我最后做的是使用递归和for item in /path/*

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

答案 13 :(得分:1)

将文件列表转换为Bash数组。这使用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数组的一般方法。缺点是如果输入很长,你可能超过Bash的命令行大小限制,或者耗尽大量内存。

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

我也不喜欢设置IFS,它可能会搞乱其他代码。

答案 14 :(得分:0)

刚刚发现我question和你之间有一些相似之处。很明显,如果你想将参数传递给命令

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

按顺序打印出来

for SOME_ARG in "$@"
do
    echo "$SOME_ARG";
done;

注意$ @被双引号括起来,有些注释here

答案 15 :(得分: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!!!"
你觉得怎么样?

答案 16 :(得分:0)

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

答案 17 :(得分:-3)

对我而言,这是有效的,而且非常“干净”:

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

答案 18 :(得分:-4)

只是有一个简单的变体问题...将类型为.flv的文件转换为.mp3(哈欠)。

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

以递归方式查找所有Macintosh用户flash文件并将其转换为音频(复制,无转码)......就像上面的那样,注意读取而不只是“for 中的文件”将会逃脱。