如何在Shell脚本参数中处理"--"?

5
这个问题有3个部分,每一部分都很容易,但是结合在一起并不容易(至少对我来说不容易):)
需要编写一个脚本,该脚本应该将以下内容作为其参数:
  1. 另一个命令的名称
  2. 用于该命令的多个参数
  3. 文件列表
例子:
./my_script head -100 a.txt b.txt ./xxx/*.txt
./my_script sed -n 's/xxx/aaa/' *.txt

等等。

出于某种原因,我的脚本需要区分:

  • 是哪个命令
  • 命令的参数是什么
  • 文件是什么

所以,最常规的写法可能是:

./my_script head -100 -- a.txt b.txt ./xxx/*.txt
./my_script sed -n 's/xxx/aaa/' -- *.txt

问题1:有更好的解决方案吗?

在./my_script中处理(第一次尝试):

command="$1";shift
args=`echo $* | sed 's/--.*//'`
filenames=`echo $* | sed 's/.*--//'`

#... some additional processing ...

"$command" "$args" $filenames #execute the command with args and files

这个解决方案在filenames中包含空格和/或'--'时会失败,例如:
/some--path/to/more/idiotic file name.txt
问题2:如何正确获取$command$args$filenames以供后续执行?
问题3:如何实现以下执行方式?
echo $filenames | $command $args #but want one filename = one line (like ls -1)

这里有一个不错的shell解决方案,或者需要使用例如perl的其他工具吗?

1
这需要一些时间来适当回答。1.为什么使用反引号?它们已经过时了,除非你希望作为root在Solaris或AIX上进行大量工作,否则不必要。使用$(cmd)!2.(更多关于你的问题),阅读有关getopt和getopts(用于shell)的内容。我在这里看到了许多例子,至少有一个相当详细的例子。再加上像while (( $# > 0 )) do processArgs ; shift ; done这样的东西会有所帮助。3.考虑使用case语句。对于“--”,您可能需要转义它们或将其引用或将其转换为charclass,例如“[-][-]”。 - shellter
3个回答

6
首先,听起来你想编写一个脚本,接受一个命令和文件名列表,并依次在每个文件名上运行该命令。这可以在bash中的一行代码中完成:
$ for file in a.txt b.txt ./xxx/*.txt;do head -100 "$file";done
$ for file in *.txt; do sed -n 's/xxx/aaa/' "$file";done
但是,也许我误解了你的意图,所以让我逐个回答你的问题。
相比使用“--”(已经有不同的含义),以下语法对我来说更自然:
./my_script -c "head -100" a.txt b.txt ./xxx/*.txt
./my_script -c "sed -n 's/xxx/aaa/'" *.txt
要在bash中提取参数,请使用getopts
SCRIPT=$0

while getopts "c:" opt; do
    case $opt in
        c)
            command=$OPTARG
            ;;
    esac
done

shift $((OPTIND-1))

if [ -z "$command" ] || [ -z "$*" ]; then
    echo "Usage: $SCRIPT -c <command> file [file..]"
    exit
fi

如果您想针对剩余的每个参数运行一个命令,它会像这样:
for target in "$@";do
     eval $command \"$target\"
done

如果您想从标准输入读取文件名,代码应该类似于这样:
while read target; do
     eval $command \"$target\"
done

@TomShaw:不错的第一篇帖子。对于最后一段代码块,也许你想在循环内部加上“eval”前缀。感谢你做了这么多工作。;-) - shellter
3
在本来很好的代码中,行 filenames=$* 是非常糟糕的。一般情况下,不要使用 $*。特别是在这种情况下,不要使用 $*。如果使用 Bash 数组,则 "$@" 就可以了。因为现在没有使用数组,所以最好将循环写成 for target in "$@"; do ...; done。如果文件名实际上不包含空格,则 $* 算是可以接受的。但只要名称中有空格,$* 就会出错。 - Jonathan Leffler
谢谢@Jonathan,我已经采纳了你的修复。 - Tom Shaw

3
< p >当引用$@变量时,它可以正确地分组参数:

for parameter in "$@"
do
    echo "The parameter is '$parameter'"
done

If given:

head -100 test this "File name" out

将会打印

the parameter is 'head'
the parameter is '-100'
the parameter is 'test'
the parameter is 'this'
the parameter is 'File name'
the parameter is 'out'

现在,您需要做的就是解析循环。您可以使用一些非常简单的规则:
  1. 第一个参数始终是文件名
  2. 随后以破折号开头的参数是参数
  3. 在“--”之后或者没有以“-”开头的参数之后,剩下的都是文件名。
您可以通过使用以下方法检查参数中的第一个字符是否为破折号:
if [[ "x${parameter}" == "x${parameter#-}" ]]

如果你以前没有看过这个语法,它是一个左过滤器。 # 分隔变量名的两部分。第一部分是变量名,第二部分是 glob 过滤器(不是正则表达式),表示要截取的内容为单个破折号。只要该语句不成立,就说明你有一个参数。顺便说一下,在这种情况下 x 可能需要也可能不需要。当你运行测试并且字符串中有一个破折号时,测试可能会误认为它是测试的参数而不是值。
整体拼接起来应该像这样:
parameterFlag=""
for parameter in "$@"     #Quotes are important!
do
    if [[ "x${parameter}" == "x${parameter#-}" ]]
    then
         parameterFlag="Tripped!"
    fi
    if [[ "x${parameter}" == "x--" ]]
    then
         print "Parameter \"$parameter\" ends the parameter list"
         parameterFlag="TRIPPED!"
    fi
    if [ -n $parameterFlag ]
    then
        print "\"$parameter\" is a file"
    else
        echo "The parameter \"$parameter\" is a parameter"
    fi
done

“x” hack 只有在古老的 shell 中才需要。请不要使用它,它会让代码变得丑陋。 - alexia
@nyuszika7h,好吧——在现代shell中确实还存在一些特殊情况可能需要它,但这些情况只存在于[](不是[[]])中,并且当test传递超过三个参数时(因此,当使用-a-o时)……这就是为什么POSIX的最新版本将它们描述为已弃用的部分的原因。 - Charles Duffy

0

问题1

我认为不行,至少对于需要针对任意命令执行此操作的情况不行。

问题3

command=$1
shift
while [ $1 != '--' ]; do
    args="$args $1"
    shift
done
shift
while [ -n "$1" ]; do
    echo "$1"
    shift
done | $command $args
问题2

那跟问题3有什么不同?


如果文件名包含空格,则 echo $filenames | sed - 将会出错。例如,echo "file1.txt" "file2 with spaces.txt"。因此要求重新尝试,我的第一次尝试是错误的。 - clt60

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