向UNIX shell脚本传递多个参数

3
我有一个(bash)shell脚本,我希望用它来按名称杀死多个进程。
#!/bin/bash
kill `ps -A | grep $* | awk '{ print $1 }'`

然而,如果只传递一个参数,此脚本可以正常工作:

end chrome

(脚本的名称为end)

但是,如果传递多个参数,则无法正常工作:

$end chrome firefox

grep: firefox: No such file or directory

这里发生了什么?

我认为$*按顺序将多个参数传递给shell脚本。 我没有在输入中输错任何内容 - 而我想要关闭的程序(chrome和firefox)已经打开。

非常感谢您的帮助。


1
你为什么不能使用 killall 而不是使用 grep 呢?例如 killall chrome; killall firefox 或者 for pname in $*; do killall "$pname"; done - khachik
killall "$@" 就足够了。顺便提一下,永远不要使用 $@,很少使用 $*,当你想要使用 "$@" 时,请不要使用 "$*" - Mark Edgar
不是答案,只是一个旁线,因此这是一条评论:这有一个我自己无数次犯过的老问题,即在 ps 上使用 grep。如果你 grep 'process',你会匹配到 'process-a'、'process-b' 等等。此外,使用 'grep' 而不是 'fgrep' 意味着你的参数是正则表达式而不是简单的进程名称,这可能是你想要的,也可能不是。 - ijw
4个回答

3
记住当使用多个参数时,grep 的作用 - 第一个参数是要查找的单词,其余参数是要扫描的文件。
还要记住,$*"$*"$@ 在参数中丢失空格,而神奇的 "$@" 符号则不会。
因此,为了处理您的情况,您需要修改调用 grep 的方式。您可以使用每个参数的选项和 grep -F(也称为 fgrep),或者您可以使用交替的 grep -E(也称为 egrep)。在某种程度上,这取决于您是否必须处理包含管道符号的参数。
使用单个 grep 调用可靠地完成这项工作非常棘手;您最好容忍多次运行管道的开销:
for process in "$@"
do
    kill $(ps -A | grep -w "$process" | awk '{print $1}')
done

如果多次运行ps的开销太大(这让我很痛苦,但我没有测量成本),那么你可能会做以下操作:

case $# in
(0) echo "Usage: $(basename $0 .sh) procname [...]" >&2; exit 1;;
(1) kill $(ps -A | grep -w "$1" | awk '{print $1}');;
(*) tmp=${TMPDIR:-/tmp}/end.$$
    trap "rm -f $tmp.?; exit 1" 0 1 2 3 13 15
    ps -A > $tmp.1
    for process in "$@"
    do
         grep "$process" $tmp.1
    done |
    awk '{print $1}' |
    sort -u |
    xargs kill
    rm -f $tmp.1
    trap 0
    ;;
esac

使用普通的xargs是可以的,因为它处理的是进程ID列表,而进程ID不包含空格或换行符。这保持了简单情况下的简单代码;复杂情况下使用临时文件来保存ps命令的输出,然后针对命令行中的每个进程名称扫描一次该文件。sort -u确保如果某个进程恰好与您的所有关键字匹配(例如,grep -E '(firefox|chrome)'将匹配两者),则只发送一个信号。
陷阱行等确保临时文件被清除,除非有人对命令过度粗暴(捕获的信号是HUP、INT、QUIT、PIPE和TERM,即1、2、3、13和15;零捕获任何原因导致shell退出)。任何时候,如果脚本创建临时文件,您应该在使用该文件时周围设置类似的陷阱,以便在进程终止时清理它。
如果您感到谨慎,并且您有GNU Grep,则可以添加-w选项,以便命令行提供的名称仅匹配整个单词。
以上所有内容都适用于Bourne/Korn/POSIX/Bash系列中的几乎所有shell(在Bourne shell中严格使用反引号代替$(...),并且在case条件中的前导括号也不允许使用Bourne shell)。但是,您可以使用数组来正确处理事情。
n=0
unset args  # Force args to be an empty array (it could be an env var on entry)
for i in "$@"
do
    args[$((n++))]="-e"
    args[$((n++))]="$i"
done
kill $(ps -A | fgrep "${args[@]}" | awk '{print $1}')

这个方法会仔细保留参数间的空格,并使用精确匹配进程名称。它避免了使用临时文件。这段代码没有对零个参数进行验证,需要事先处理。或者你可以添加一行args[0]='/collywobbles/'或类似的内容来提供一个默认的、不存在的命令来搜索。

2
回答您的问题,发生的情况是$*会扩展为参数列表,因此第二个及之后的单词看起来像是grep(1)要查找的文件。
为了按顺序处理它们,您需要执行以下操作:
for i in $*; do
    echo $i
done

通常在这种情况下,用带引号的"$@"代替$*。请参考man sh,并查看killall(1)pkill(1)pgrep(1)

0

$* 应该很少使用。我通常建议使用 "$@"。Shell 参数解析相对复杂,容易出错。通常你会犯的错误是让不应该被评估的东西被评估。

例如,如果你输入了这个:

end '`rm foo`'

如果你有一个名为“foo”的文件,你会发现它不再存在了。

这里有一个脚本可以完成你要求的操作。如果任何参数包含'\n''\0'字符,则会失败:

#!/bin/sh

kill $(ps -A | fgrep -e "$(for arg in "$@"; do echo "$arg"; done)" | awk '{ print $1; }')

我非常喜欢使用$(...)语法来完成反引号所做的事情。它更加清晰,而且在嵌套时也不会产生歧义。


你不需要为每个命名参数重复使用“-e”吗?fgrep -e firefox -e chrome - Jonathan Leffler
@Johnathan Leffler - 不是的。"$(...)" 结构会导致输出被视为一个单一的参数。因此,您正在将带有嵌入式换行符的字符串作为 fgrep 的参数发送。fgrep 将接受这样的参数,并将所有单独的“行”视为单独的字符串进行搜索,就好像它们与标准正则表达式中的 | 连接一样。 - Omnifarious

0

可以考虑使用pkill(1),或者像@khachik评论中提到的那样使用killall(1)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接