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


121

我有一个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

这是输出:

测试/巴尔的摩
测试/樱桃
爬坡道
测试/爱迪生 
测试/新
约克
市
测试/费城

樱桃山和纽约市被视为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"产生以下输出:

樱桃
爬坡道
新
约克
市

re:编辑,list="$@"完全丢弃原始值的列表性,将其折叠为字符串。请严格按照我给出的答案进行操作 -不鼓励在其中的任何地方进行此类作业;如果要将命令行参数列表传递给程序,则应将它们收集到一个数组中,然后直接扩展该数组。
Charles Duffy

Answers:


105

首先,不要那样做。最好的方法是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
)

最后,对于命令行参数的情况,如果您的外壳支持数组,则应使用数组(即ksh,bash或zsh):

# this is safe
for d in "$@"; do
  printf '%s\n' "$d"
done

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

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

1
不知道-exec的那种'+'风格。甜蜜
约翰尼斯·绍布

1
看起来像xargs一样,它也只能将参数放在给定命令的末尾://这有时候让我
很烦

我认为-exec [name] {} +是GNU和4.4-BSD扩展。(至少,它在Solaris 8上没有出现,而且我也不认为它在AIX 4.3中出现。)我想我们其余的人可能都在使用管道传递给xargs了……
Michael Ratanapintha

2
我以前从未看过$'\ n'语法。这是如何运作的?(我曾以为IFS ='\ n'或IFS =“ \ n”都可以,但是都没有。)
MCS

1
@crosstalk肯定在Solaris 10中,我只是用过它。
尼克,

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

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

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

不需要xargs,请参见find -exec ... {} +
Charles Duffy

4
@Charles:对于大量文件,xargs效率更高:它仅产生一个进程。-exec选项为每个文件派生一个新进程,这可能会慢一个数量级。
亚当·罗森菲尔德

1
我更喜欢xargs。两者似乎基本上都相同,而xargs具有更多选择,例如并行运行
Johannes Schaub-litb

2
亚当,不会,“ +”号会聚集尽可能多的文件名,然后执行。但它不会具有并行运行这样的巧妙功能:)
Johannes Schaub-litb

2
请注意,如果您想对文件名进行操作,则必须将它们引号。例如:find . -type d | while read file; do ls "$file"; done
David Moles

23

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

测试目录

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

谢谢,这对我创建的别名起作用,以列出当前文件夹中每个目录使用的空间,这使前一个版本中带有空格的某些目录感到窒息。这在zsh中有效,但其他一些答案却无效:alias duc='ls -d * | while read D; do du -sh "$D"; done;'
Ted Naleid 2011年

2
如果您使用的是zsh,也可以执行以下操作:alias duc='du -sh *(/)'
cbliard 2011年

@cbliard这仍然是越野车。尝试使用带有一个制表符序列或多个空格的文件名运行它;您会注意到,它将所有这些更改为一个空格,因为您没有在回声中引用。然后是文件名包含换行符的情况……
Charles Duffy 2013年

@CharlesDuffy我尝试了制表符序列和多个空格。它可以使用引号。我也尝试过换行符,它根本不起作用。我相应地更新了答案。感谢您指出这一点。
cbliard

1
@cbliard对-我正在得到加引号到您的echo命令。至于换行符,您可以使用find -print0和来完成IFS='' read -r -d '' f
查尔斯·达菲

7

在标准Unix中,这非常棘手,大多数解决方案都使用换行符或其他一些字符。但是,如果您使用的是GNU工具集,则可以利用该find选项-print0xargs与相应的选项一起使用-0(减零)。有两个字符不能显示在简单文件名中;这些是斜杠和NUL'\ 0'。显然,斜杠出现在路径名中,因此使用NUL'\ 0'标记名称结尾的GNU解决方案非常巧妙且可靠。


4

为什么不放

IFS='\n'

在for命令前面?这会将字段分隔符从<Space> <Tab> <Newline>更改为<Newline>


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

1
-d $'\0'完全相同-d ''-因为bash使用NUL终止的字符串,空字符串的第一个字符是NUL,并且出于同样的原因,NUL根本无法在C字符串内部表示。
查尔斯·达菲

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


很棒的提示:这对于命令行osascript(OS X AppleScript)的选项非常有帮助,其中空格将一个参数拆分为多个参数,并且只打算使用一个参数
tim 2012年

不,这还不够。它效率低下(由于不必要地使用$(echo ...)),不能正确处理带有glob表达式的文件名,不能正确处理包含$'\b'或$'\ n'个字符的文件名,而且还会将多个空格转换为单个空格字符报价错误导致输出侧出现错误。
查尔斯·达菲

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

1
回顾一下-POSIX sh 实际上一个解决方案:您可以重用"$@"数组,并使用追加到数组set -- "$@" "$f"
Charles Duffy 2015年

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


请提供解释。
史蒂夫·K

IFS指定分隔符是什么,那么带有空格的文件名将不会被截断。
amazingthere

$ IFS = $ OLD_IFS最后应为:IFS = $ OLD_IFS
Michel Donais

3

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

read artist;

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

2

要补充乔纳森所说的话:结合使用该-print0选项,如下所示:findxargs

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

这将command使用适当的参数执行命令;其中包含空格的目录将正确加引号(即,它们将作为一个参数传递)。


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!

干杯!


echo "$name" | ...如果nameis是不起作用-n,并且它如何与带有反斜杠转义序列的名称一起使用取决于您的实现-POSIX使echo在那种情况下的行为明确地未定义(而XSI扩展的POSIX使反斜杠转义序列的扩展成为标准定义的行为)和GNU系统-包括bash的-无POSIXLY_CORRECT=1断裂通过实施POSIX标准-e(而规范要求echo -e进行打印-e。上输出)printf '%s\n' "$name" | ...更安全。
查尔斯达菲

1

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

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

1
不要使用function关键字-它使您的代码与POSIX sh不兼容,但是没有其他有用的目的。您可以使用定义一个函数recursedir() {,添加两个括号并删除function关键字,这将与所有POSIX兼容的shell兼容。
查尔斯·达菲

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,它会弄乱其他代码。


如果IFS='' read在同一行上使用,则IFS设置仅适用于read命令,而不会转义它。没有理由不喜欢以这种方式设置IFS。
查尔斯·达菲

1

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

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

我这样解决:

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

对我来说看起来很简单


0

刚刚发现我的问题和您的问题有一些相似之处。如果您想将参数传递给命令

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

按顺序打印出来

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

注意$ @用双引号引起来,这里有一些注释


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!!!"

你怎么看?


我认为如果文件名中包含换行符,这将无法正常工作。也许您应该尝试一下。
user000001 2013年


-3

对我来说,这行得通,而且几乎是“干净的”:

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

4
但这更糟。查找周围的双引号引起所有路径名称被连接为单个字符串。改变回声LS看问题。
NVRAM

-4

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

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

递归地找到所有Macintosh用户flash文件,然后将它们转换为音频(复制,不进行转码)...就像上面的代码一样,请注意,读取的内容将不只是“ for file in ”。


2
read以后in是你遍历列表中的一个多个单词。您发布的内容是问号者拥有的内容的稍作分解,无法正常工作。您可能打算发布不同的内容,但是无论如何这里可能还会有其他答案。
吉尔斯(Gilles)'“ SO-不要邪恶”
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.