使用 find 和 -exec 查找文件并创建指向父级的符号链接

tim*_*c22 5 find shell-script quoting command-substitution

我正在尝试使用find查找匹配特定模式的文件,然后将它们的父目录符号链接到另一个目录,这是我当前的脚本(我在 mac 上执行此操作,因此 -printf 在这里不起作用):

#!/usr/bin/env bash

PROCESS_DIR="/Users/me/Google Drive/Me"
OUTPUT_DIR="/Users/me/OfflineFolder"

# Copy directory structure and then make symlinks
rm -R "$OUTPUT_DIR";
mkdir "$OUTPUT_DIR";
cd "$PROCESS_DIR";

find . -name *.md -exec ln -vs $(dirname {}) "$OUTPUT_DIR" \;
Run Code Online (Sandbox Code Playgroud)

但它似乎不起作用。但是,此脚本确实有效(为我计算机上的所有 md 文件建立符号链接:

# Copy directory structure and then make symlinks
rm -R "$OUTPUT_DIR";
mkdir "$OUTPUT_DIR";
cd "$PROCESS_DIR";
find . -type d -exec mkdir -vp "$OUTPUT_DIR/{}" \;
find . -name *.md -exec ln -vs "$(pwd)/{}" "$OUTPUT_DIR/{}" \;
find "$OUTPUT_DIR" -type d -empty -delete
Run Code Online (Sandbox Code Playgroud)

知道为什么这不起作用吗?我尝试了几种不同的方法(包括使用find $PROCESS_DIRECTORY而不是cd $PROCESS_DIRECTORY)。谢谢

Gil*_*il' 3

你有两个问题,都与事情发生的顺序有关。

\n\n

find . -name *.md -exec ln -vs $(dirname {}) "$OUTPUT_DIR" \\;是一个单一的命令。shell 在执行之前对其进行解析。解析步骤中:

\n\n
    \n
  • $(\xe2\x80\xa6)是一个命令替换,因此dirname {}被执行,产生.(没有目录部分{})。
  • \n
  • $OUTPUT_DIR是一个变量替换,因此它被它的值替换。
  • \n
  • *.md是一个全局模式,因此它被匹配文件列表替换。如果没有匹配项,则模式保持不变。
  • \n
\n\n

这就完成了确定要执行什么命令所需的工作。

\n\n
    \n
  • 如果当前目录中没有匹配的文件,*.md则使用给定参数执行以下命令:find, ., -name, *.md, -exec, , ln, -vs, ., /Users/me/OfflineFolder;
  • \n
  • 如果*.md匹配bar.mdfoo.md则命令为find, ., -name, foo.md, -exec, \xe2\x80\xa6 (并且find将错过something-other-than-foo.md子目录中调用的任何文件)。
  • \n
  • 如果*.md匹配bar.mdfoo.md则命令为find, ., -name, bar.md, foo.md, -exec, \xe2\x80\xa6 (并且find会抱怨语法错误)。
  • \n
\n\n

您想要执行dirname查找结果。这意味着您需要指示find运行dirname。您可以这样做,但 find 没有任何机制来收集输出dirname并将其作为参数传递给ln. 为此,您需要一个工具,例如 shell,它是命令替换。

\n\n

因此,策略如下:告诉 find 调用 shell,并告诉该 shell 运行涉及ln和 的命令dirname。您需要注意引用。将 shell 命令放在单引号中以避免其特殊字符被外壳解释。还将模式放在-name引号中,以便它传递到外壳find而不是由外壳扩展。

\n\n
find . -name \'*.md\' -exec sh -c \'ln -vs \xe2\x80\xa6\' \\;\n
Run Code Online (Sandbox Code Playgroud)\n\n

下一步是完成\xe2\x80\xa6. 不要{}在 shell 命令内部使用:这只会将文件名作为 shell 代码片段,任何特殊字符都将由内 shell 进行解析。相反,将由 给出的文件名find作为参数传递给 shell 脚本。之后的第一个参数是 shell 实例的名称 ( ),但您可以将其用于任何您喜欢的目的;后续参数是位置参数 ( ,sh -c CODE$0$1$2 , \xe2\x80\xa6)。

\n\n
find . -name \'*.md\' -exec sh -c \'ln -vs "$(dirname "$0")" "$1"\' {} "$OUTPUT_DIR" \\;\n
Run Code Online (Sandbox Code Playgroud)\n\n

我已$OUTPUT_DIR作为参数传递给脚本。这里并不重要,因为该值不包含任何 shell 特殊字符,但这是一个好习惯,你永远不知道什么时候有人可能会更改路径,例如包含空格。另一种可能性是将其通过环境传递:

\n\n
export OUTPUT_DIR\nfind . -name \'*.md\' -exec sh -c \'ln -vs "$(dirname "$0")" "$OUTPUT_DIR"\' {} \\;\n
Run Code Online (Sandbox Code Playgroud)\n\n

dirname您可以使用文本替换来代替:删除最后一个斜杠之后的所有内容。您不必担心没有目录部分的特殊情况,因为通过 传递的文件名不会发生这种情况find

\n\n
export OUTPUT_DIR\nfind . -name \'*.md\' -exec sh -c \'ln -vs "${0%/*}" "$OUTPUT_DIR"\' {} \\;\n
Run Code Online (Sandbox Code Playgroud)\n\n

您可以使用+的形式-exec来加快速度。我传递_as ,后续参数是循环迭代的$0文件名。for

\n\n
export OUTPUT_DIR\nfind . -name \'*.md\' -exec sh -c \'for x; do ln -vs "${x%/*}" "$OUTPUT_DIR"; done\' _ {} +\n
Run Code Online (Sandbox Code Playgroud)\n