制作BASH脚本`for`处理带空格的文件名(或解决方法)


12

虽然我使用BASH已有好几年了,但我在BASH脚本编写方面的经验相对有限。

我的代码如下。它应该从当前目录中获取整个目录结构并将其复制到其中 $OUTDIR

for DIR in `find . -type d -printf "\"%P\"\040"`
do
  echo mkdir -p \"${OUTPATH}${DIR}\"        # Using echo for debug; working script will simply execute mkdir
  echo Created $DIR
done

问题是,这是我的文件结构的示例:

$ ls
Expect The Impossible-Stellar Kart
Five Iron Frenzy - Cheeses...
Five Score and Seven Years Ago-Relient K
Hello-After Edmund
I Will Go-Starfield
Learning to Breathe-Switchfoot
MMHMM-Relient K

注意空格:-S And for 逐个接受参数,所以我的脚本的输出看起来像这样:

Creating directory structure...
mkdir -p "/myfiles/multimedia/samjmusicmp3test/Learning"
Created Learning
mkdir -p "/myfiles/multimedia/samjmusicmp3test/to"
Created to
mkdir -p "/myfiles/multimedia/samjmusicmp3test/Breathe-Switchfoot"
Created Breathe-Switchfoot

但我需要它从输出中获取整个文件名(一次一行) find。我也试过制作 find 在每个文件名周围加上双引号。但这没有用。

for DIR in `find . -type d -printf "\"%P\"\040"`

并使用此更改的行输出:

Creating directory structure...
mkdir -p "/myfiles/multimedia/samjmusicmp3test/"""
Created ""
mkdir -p "/myfiles/multimedia/samjmusicmp3test/"Learning"
Created "Learning
mkdir -p "/myfiles/multimedia/samjmusicmp3test/to"
Created to
mkdir -p "/myfiles/multimedia/samjmusicmp3test/Breathe-Switchfoot""
Created Breathe-Switchfoot"

现在,我需要一些可以像这样迭代的方法,因为我也希望运行更复杂的命令 gstreamer 在每个文件上有以下类似的结构。我该怎么做?

编辑: 我需要一个代码结构,它允许我为每个目录/文件/循环运行多行代码。对不起,如果我不清楚。

解: 我最初尝试过:

find . -type d | while read DIR
do
  mkdir -p "${OUTPATH}${DIR}"
  echo Created $DIR
done

这在大多数情况下都很好。但是,后来我发现由于管道导致while循环在子shell中运行,因此循环中设置的任何变量以后都不可用,这使得实现错误计数器非常困难。我的最终解决方案(来自 关于SO的答案 ):

while read DIR
do
  mkdir -p "${OUTPATH}${DIR}"
  echo Created $DIR
done < <(find . -type d)

后来这允许我有条件地增加循环中的变量,这些变量将在稍后的脚本中保持可用。


Why_would_you_ever_need_a_space_in_a_file_name?
Kevin Panko

没错,不是我的偏好。但是,要删除空格,首先需要处理带空格的文件;)
Samuel Jaeschke

1
实际上,文件名应该允许空格。我会允许任何事情 / 和不可打印的字符。但是除了之外什么都是允许 /\0 所以你必须允许他们。
Kevin Panko

Answers:


11

你需要管道 find 进入 while 环。

find ... | while read -r dir
do
    something with "$dir"
done

此外,您不需要使用 -printf 在这种情况下。

如果您愿意,可以使用nullbyte分隔符(这是唯一不能出现在* nix文件路径中的字符),对名称中带有换行符的文件进行此证明:

find ... -print0 | while read -d '' -r dir
do
    something with "$dir"
done

你也会发现使用 $() 而不是反叛,以更多样化和更容易。它们可以更容易嵌套,并且可以更容易地进行引用。这个人为的例子将说明以下几点:

echo "$(echo "$(echo "hello")")"

尝试用反引号做到这一点。


2
而且,而不是 "$dir",最好使用 "${dir}" - 很容易区分$ {dir} name和$ {dirname},但$ dirname可以解释。
James Polley

这里重要的是 read 将整行读入 ${dir}所以IFS并不重要。
James Polley

1
感谢您找到$ /“拼写错误。如果变量名称后面没有任何内容,则无需括号。
Dennis Williamson

4
这将处理带空格的路径名(U + 0020),但仍然无法正确处理带换行符的路径名(U + 000A)。我更喜欢 find … -print0 | xargs -0 … 因为它使用的分隔符恰好对应于POSIX pathanames中不允许的唯一字符:NUL(U + 0000)。
Chris Johnsen

2
完善!正是我在寻找的东西。我从来没有想过你可以管道 while。 @Chris Johnsen:是的,但即使是音乐翻录程序也不倾向于将换行符放在他们的文件名中。如果他们这样做,我想知道(即出现问题)并立即摆脱它们......
Samuel Jaeschke

7

看到 这个答案 我几天前写了一个处理带空格的文件名的脚本示例。

虽然有一种稍微复杂(但更简洁)的方式来实现你想要做的事情:

find . -type d -print0 | xargs -0 -I {} mkdir -p ../theredir/{}

-print0 告诉find将参数与null分开; -0到xargs告诉它期望由null分隔的参数。这意味着它可以很好地处理空间。

-I {} 告诉xargs替换字符串 {} 用文件名。这也意味着每个命令行只应该使用一个文件名(xargs通常会填充尽可能多的文件名)

其余应该是显而易见的。


然而,丹尼斯威廉姆森的建议(除了错别字之外)更具可读性,因此几乎在所有方面都是可取的。
James Polley

适用于mkdir,但很抱歉我应该更清楚 - 我希望为每个文件运行一系列命令。你看,对于我以后的类似例程,我希望根据输入文件名生成输出文件名(包括剥离.ogg扩展名并添加.mp3),然后在调用gst-launch时在我的pipline中使用这些多个变量。
Samuel Jaeschke

5

您遇到的问题是for语句将find作为单独的参数进行响应。空间分隔符。您需要使用bash的IFS变量来分割空间。

这里有一个 链接 这解释了如何做到这一点。

IFS内部变量

解决此问题的一种方法是更改​​Bash的内部IFS(内部字段分隔符)变量,以便它通过默认空格(空格,制表符,换行符)以外的其他内容(在本例中为逗号)拆分字段。

#!/bin/bash
IFS=$';'

for I in `find -type d -printf \"%P\"\;`
do
   echo "== $I =="
done

将您的find设置为在%P之后输出字段分隔符并适当设置IFS。我选择了分号,因为它不太可能在你的文件名中找到。

另一种方法是直接从find中调用mkdir -exec 你可以完全跳过for循环吗?如果您不需要进行任何额外的解析,那就是这样。


如果文件名包含IFS怎么办?然后你必须选择一个不同的。但那么,如果......
Dennis Williamson

3
你可以选择 / 在POSIX上,和 : 在DOS文件系统上。您可以为IFS选择不同文件系统的非法字符。任何更复杂的事情,你最好使用perl。
Darren Hall

2
使用/的问题在于它是目录分隔符和 find 返回包含斜杠的路径的文件名。尝试将脚本中的分号更改为斜杠,echo将在单独的行上打印目录和文件名。
Dennis Williamson

这看起来也很有用。我已经去了管道 while 选项,但这看起来也很可行。是的,在我类似的结构中,我需要进一步解析。 (输入文件名为.ogg,将作为传递 filesrc 在gst管道中,将生成基于输出目录的.mp3的等效结尾,并将其作为传递给管道 filesink,这当然需要为每个文件以及一些文件完成 echo 给用户。)
Samuel Jaeschke

4

如果循环的主体不止一个命令,则可以使用 xargs的 驱动shell脚本:

export OUTPATH=/some/where/else/
find . -type d -print0 | xargs -0 bash -c 'for DIR in "$@"; do
  printf "mkdir -p %q\\n" "${OUTPATH}${DIR}"        # Using echo for debug; working script will simply execute mkdir
  echo Created $DIR
done' -

如果shell是Bourne / POSIX类型(它用于在shell脚本中设置$ 0),请确保包括尾随短划线(或其他一些“单词”)。另外,必须小心引用,因为shell脚本是在带引号的字符串内写入的,而不是直接在提示符下写入。


另一个有趣的概念谢谢 - 我相信我以后会找到一个用途:)
Samuel Jaeschke

1

在你的最新问题中

mkdir -p \"${OUTPATH}${DIR}\"

这应该是

mkdir -p "${OUTPATH}${DIR}"

谢谢。固定。它也读取FILENAME而不是DIR - 复制粘贴:P
Samuel Jaeschke

1
find . -type d -exec mkdir -p "{}\040" ';' -exec echo "Created {}\040" ';'

0

或者使整个事情变得更简单:

% rsync -av --include='*/' --exclude='*' SRC DST

这会将SRC的目录结构复制到DST中。


不,我需要一个像这样的迭代结构,它允许我为每个文件运行多行代码。 “现在,我需要一些可以像这样迭代的方法,因为我还希望在以下类似的结构中运行一个涉及gstreamer的更复杂的命令。”对不起,如果我不清楚。
Samuel Jaeschke

我给出的命令解决了你要求的问题,如果这只是你身边一个更大的“管道”的一部分并不重要。对于遇到问题中描述的问题的其他人,rsync-approach将起作用。所以,没有必要抱歉潜在的不清晰:)
akira

是啊。不,我的意思是我会使用类似的 while ... do ... done 结构稍后从find执行类似的处理,这需要在每个文件上运行几行代码(修改字符串,回显,gst-launch等)和 rsync 不会实现这一点。这就是为什么我指定我需要能够在类似的结构中运行更复杂的命令集。我的脚本使用了这个循环结构两次,所以对于这个问题,我发布了一个中间较少的问题。
Samuel Jaeschke

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.