“sh -c”的内联 shell 脚本未按预期运行?

“sh -c”的内联 shell 脚本未按预期运行?

我正在尝试find在目录上运行命令并根据找到的项目生成输出文件。

这是我到目前为止所拥有的:

docker2:~/src/docker# cat test.sh
#!/bin/sh

find /data -type d -mindepth 1 -maxdepth 1 \( ! -name "." \) -exec sh -c "\
     echo \"Full path: {}\"; echo \"Basename: `basename {}`\"
" \;

然而,这个命令输出如下:

Full path: /data/nc
Basename: /data/nc
Full path: /data/src
Basename: /data/src
Full path: /data/sql0
Basename: /data/sql0
Full path: /data/proxy
Basename: /data/proxy

我知道反引号应该有效:

docker2:~/src/docker# cat test.sh
#!/bin/sh

find /data -type d -mindepth 1 -maxdepth 1 \( ! -name "." \) -exec sh -c "\
     echo \"Date: `date`\"; echo \"Full path: {}\"; echo \"Basename: `basename {}`\"
" \;

给出:

Date: Wed Dec 18 22:56:32 UTC 2019
Full path: /data/nc
Basename: /data/nc
Date: Wed Dec 18 22:56:32 UTC 2019
Full path: /data/src
Basename: /data/src
...

而且我在变量方面也遇到了麻烦:

#!/bin/sh

find /data -type d -mindepth 1 -maxdepth 1 \( ! -name "." \) -exec sh -c "\
     export THIS_DIR={}; THIS_DIRECTORY={}; echo \"$THIS_DIR $THIS_DIRECTORY\";
" \;

该命令的输出为空,. 中的每个目录都有一个空行/data。这意味着即使导出也不会存储变量。

我究竟做错了什么?我猜这与某种奇怪的嵌套或操作顺序或一些奇怪的 shell 代码有关。

答案1

这是预期的,因为您的`basename {}`内容用双引号引起来。这将使其在调用之前执行find,这将用字符串替换命令替换{}。这也是后面示例中的一个问题,变量在实际设置之前会被扩展。

当您使用 时sh -c,它作为参数的脚本-c实际上应该始终用单引号引起来,因为这使得脚本内的引用更简单。内联脚本的任何参数都应在其命令行上传递,而不是作为注入到代码中的文本(请参阅“是否可以安全地使用“find -exec sh -c”?“出于原因)。就您而言:

#!/bin/sh

find /data -type d -mindepth 1 -maxdepth 1 -exec sh -c '
    echo "Full path: $1"
    echo "Basename: $(basename "$1")"' sh {} \;

或者,使用几个变量(并切换到 using printf):

#!/bin/sh

find /data -type d -mindepth 1 -maxdepth 1 -exec sh -c '
    pathname=$1
    filename=$(basename "$pathname")
    printf "Full path: %s\n" "$pathname"
    printf "Basename: %s\n" "$filename"' sh {} \;

当您使用 运行内联脚本时sh -c,传递的参数将放置在$0、等中$1$2由于$0通常是 shell 或脚本的名称,sh因此我们传递该字符串,然后将找到的路径名传递为$1

或者,更有效的是,使用尽可能少的调用sh -c,并使用参数替换:

#!/bin/sh

find /data -type d -mindepth 1 -maxdepth 1 -exec sh -c '
    for pathname do
        printf "Full path: %s\n" "$pathname"
        printf "Basename: %s\n" "${pathname##*/}"
    done' sh {} +

在这里,find安排使用尽可能多的参数(尽可能少的次数)来调用内联脚本。在每次调用中,$0都会像以前一样设置为sh,而其他位置参数则从找到的路径名中获取它们的值。循环迭代传递的路径名($0不是位置参数,因此不会迭代)。

我假设您的! -name .测试是为了避免找到/data目录本身。这不是必需的,因为它已经被跳过-mindepth 1/data路径位于“深度 0”,因为它是搜索路径的顶部)。另外,find不会从目录中返回条目,因此如果您未使用过,.则测试不会删除。在这种情况下,您可以改为使用./data-mindepth 1! -path /data

因为您只是迭代单个目录中的目录,并且因为您已经使用了标签,并且由于您不习惯find对时间戳等进行任何花哨的测试,因此您可以执行基本的 shell 循环:

#!/bin/bash

shopt -s dotglob nullglob

for pathname in /data/*; do
    if [[ -d $pathname ]] && [[ ! -L $pathname ]]; then
        printf 'Full path: %s\n' "$pathname"
        printf 'Basename: %s\n' "${pathname##*/}"
    fi
done

在这里,在打印有关路径名和名称的信息之前,会显式测试 中的名称/data是否为目录。中的shelldotglob选项bash允许*通配模式匹配隐藏名称,并且如果为空或模式不匹配任何内容,则nullglob完全阻止循环形式运行。/data

看 ”了解“find”的 -exec 选项“有关使用findwith 的一般信息-exec

相关内容