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

Posted

技术标签:

【中文标题】如何在 bash 循环列表中转义空格?【英文标题】:How can I escape white space in a bash loop list? 【发布时间】:2010-09-22 23:55:07 【问题描述】:

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

这是输出:

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

Cherry Hill 和 New York City 被视为 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="$@" 完全丢弃原始值的列表性,将其折叠为字符串。请完全按照给定遵循我的回答中的做法——在其中的任何地方都不鼓励这样的分配;如果你想将命令行参数列表传递给程序,你应该将它们收集到一个数组中,然后直接展开该数组。 【参考方案1】:

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

【讨论】:

请提供解释。 IFS 指定了分隔符是什么,那么带有空格的文件名不会被截断。 $IFS=$OLD_IFS 最后应该是:IFS=$OLD_IFS【参考方案2】:

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

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

我是这样解决的:

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

对我来说看起来很简单

【讨论】:

【参考方案3】:
find Downloads -type f | while read file; do printf "%q\n" "$file"; done

【讨论】:

【参考方案4】:

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

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

另一种安全的方法是使用以 NUL 结尾的列表,但这需要您的 find 支持 -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) 以防止包含 glob 字符(例如 []*?)的字符串被扩展:

# 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

【讨论】:

不知道 -exec 的“+”风格。甜 tho 看起来它也可以像 xargs 一样,只将参数放在给定命令的末尾:/ 这有时会困扰我 我以前从未见过 $'\n' 语法。这是如何运作的? (我原以为 IFS='\n' 或 IFS="\n" 会起作用,但两者都不会。) @crosstalk 它肯定在 Solaris 10 中,我刚刚使用它。 @TomRussel,这里的echo 是一个占位符,可以替换为您的实际命令——您将在循环中运行的命令。这不是答案本身的一部分。【参考方案5】:

我需要相同的概念来顺序压缩某个文件夹中的多个目录或文件。我已经解决了使用 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!!!"

你怎么看?

【讨论】:

我认为如果文件名中有换行符,这将无法正常工作。也许你应该尝试一下。【参考方案6】:

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

测试目录

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;' 如果你使用的是zsh,你也可以这样做:alias duc='du -sh *(/)' @cbliard 这仍然是错误的。尝试使用带有制表符序列或多个空格的文件名运行它;您会注意到它会将其中任何一个更改为单个空格,因为您没有在回声中引用。然后是文件名包含换行符的情况...... @CharlesDuffy 我尝试使用制表符序列和多个空格。它适用于引号。我也尝试过换行,但它根本不起作用。我相应地更新了答案。感谢您指出这一点。 @cbliard 对——在你的 echo 命令中添加引号就是我的意思。至于换行符,您可以使用 find -print0IFS='' read -r -d '' f 来完成这项工作。【参考方案7】:

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

#!/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

【讨论】:

回首过去——实际上一个使用 POSIX sh 的解决方案:您可以重用 "$@" 数组,并在其上附加 set -- "$@" "$f"【参考方案8】:

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

read artist;

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

【讨论】:

【参考方案9】:

将文件列表转换为 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 的命令行大小限制,或者会占用大量内存。

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

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

【讨论】:

如果您在同一行使用IFS='' read,则 IFS 设置仅针对读取命令存在,并且不会对其进行转义。没有理由不喜欢以这种方式设置 IFS。【参考方案10】:

我用

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) 的选项非常有用,其中空格将一个参数拆分为多个参数,而这些参数只用于一个目的 不,这还不够。它效率低下(由于不必要地使用 $(echo ...)),不能正确处理带有 glob 表达式的文件名,不能正确处理包含 $'\b' 或 $'\n' 字符的文件名,而且还会转换多次运行的空白由于引用不正确,在输出端转换为单个空白字符。【参考方案11】:
find . -print0|while read -d $'\0' file; do echo "$file"; done

【讨论】:

-d $'\0'-d '' 完全相同——因为 bash 使用以 NUL 结尾的字符串,所以空字符串的第一个字符是 NUL,出于同样的原因,NUL 不能完全可以在 C 字符串中表示。【参考方案12】:

为什么不直接放

IFS='\n'

在for命令前面?这会将字段分隔符从 更改为

【讨论】:

【参考方案13】:

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

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

【讨论】:

不要使用 function 关键字——它会使你的代码与 POSIX sh 不兼容,但没有其他有用的用途。您可以使用 recursedir() 定义一个函数,添加两个括号并删除 function 关键字,这将与所有 POSIX 兼容的 shell 兼容。【参考方案14】:

对我来说,这很有效,而且非常“干净”:

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

【讨论】:

但这更糟。 find 周围的双引号导致所有路径名连接为单个字符串。将 echo 更改为 ls 以查看问题。【参考方案15】:
#!/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" | ... 如果name-n 则不起作用,并且它对带有反斜杠转义序列的名称的行为取决于您的实现——POSIX 使echo 的行为在那个case 明确未定义(而 XSI 扩展的 POSIX 使反斜杠转义序列扩展为标准定义的行为,而 GNU 系统——包括 bash——没有 POSIXLY_CORRECT=1 通过实现 -e 打破了 POSIX 标准(而规范要求 @987654329 @ 在输出上打印-e)。printf '%s\n' "$name" | ... 更安全。【参考方案16】:

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

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

递归查找所有 Macintosh 用户 flash 文件并将它们转换为音频(复制,无转码)...就像上面的 while,注意 read 而不是只是 'for file in ' 会转义。

【讨论】:

in 之后的 read 是您正在迭代的列表中的另一个单词。您发布的内容是提问者所拥有的略微损坏的版本,这是行不通的。您可能打算发布一些不同的东西,但无论如何这里的其他答案可能已经涵盖了它。【参考方案17】:

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

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

按顺序打印出来

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

注意 $@ 被双引号包围,一些注释 here

【讨论】:

【参考方案18】:

要添加到Jonathan 所说的内容:使用find-print0 选项和xargs,如下所示:

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

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

【讨论】:

【参考方案19】:
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:对于大量文件,xargs 效率更高:它只产生一个进程。 -exec 选项为每个文件派生一个新进程,这可能会慢一个数量级。 我更喜欢 xargs。这两个基本上似乎都做同样的事情,而 xargs 有更多的选择,比如并行运行 亚当,不,'+' 将聚合尽可能多的文件名,然后执行。但它不会有并行运行这样简洁的功能:) 请注意,如果你想对文件名做一些事情,你将不得不引用它们。例如:find . -type d | while read file; do ls "$file"; done【参考方案20】:

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

【讨论】:

以上是关于如何在 bash 循环列表中转义空格?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Bash/Grep 中转义单引号?

如何在 ssh / 远程 bash 命令中转义单引号字符?

如何在 Pyspark 的动态列列表中转义列名

在osx bash中转义多层级联单引号和双引号

如何使用 msys (mingw32) 在 tcl 中转义字符串

如何在映射到Spring配置类列表中的环境变量中转义逗号