【问题标题】:Using Python to parse complex arguments to shell script使用 Python 将复杂的参数解析为 shell 脚本
【发布时间】:2012-07-25 18:35:12
【问题描述】:

在编写 shell 脚本时,我经常发现自己大部分时间(尤其是在调试时)都在处理参数处理。我编写或维护的许多脚本很容易超过 80% 的输入解析和清理。我将其与我的 Python 脚本进行比较,argparse 为我处理大部分繁重的工作,并让我轻松构建复杂的选项结构和清理/字符串解析行为。

因此,我希望能够让 Python 完成这项繁重的工作,然后在我的 shell 脚本中获取这些经过简化和清理的值,而无需再担心用户指定的参数。

举一个具体的例子,我工作的许多 shell 脚本都被定义为以特定顺序接受它们的参数。您可以调用start_server.sh --server myserver --port 80,但start_server.sh --port 80 --server myserver 失败并显示You must specify a server to start. - 它使解析代码更简单,但并不直观。

因此,第一次通过的解决方案可能很简单,例如让 Python 接受参数,对它们进行排序(将它们的参数放在它们旁边)并返回排序后的参数。因此,shell 脚本仍然会进行一些解析和清理,但用户可以输入比 shell 脚本本机接受的更多的任意内容,例如:

# script.sh -o -aR --dir /tmp/test --verbose

#!/bin/bash

args=$(order.py "$@")
# args is set to "-a --dir /tmp/test -o -R --verbose"

# simpler processing now that we can guarantee the order of parameters

这里有一些明显的限制,特别是parse.py 无法区分带有参数的最终选项和索引参数的开头,但这似乎并不可怕。

所以这是我的问题:1) 是否有任何现有的(最好是 Python)实用程序可以通过比 bash 更强大的东西启用 CLI 解析,然后我的 bash 脚本的其余部分可以访问它消毒,或 2) 以前有人这样做过吗?是否有我不知道的问题、陷阱或更好的解决方案?愿意分享您的实现吗?


一个(非常不成熟的)想法:

#!/bin/bash

# Some sort of simple syntax to describe to Python what arguments to accept
opts='
"a", "append", boolean, help="Append to existing file"
"dir", str, help="Directory to run from"
"o", "overwrite", boolean, help="Overwrite duplicates"
"R", "recurse", boolean, help="Recurse into subdirectories"
"v", "verbose", boolean, help="Print additional information"
'

# Takes in CLI arguments and outputs a sanitized structure (JSON?) or fails
p=$(parse.py "Runs complex_function with nice argument parsing" "$opts" "$@")
if [ $? -ne 0 ]; exit 1; fi # while parse outputs usage to stderr

# Takes the sanitized structure and an argument to get
append=$(arg.py "$p" append)
overwrite=$(arg.py "$p" overwrite)
recurse=$(arg.py "$p" recurse)
verbose=$(arg.py "$p" verbose)

cd $(python arg.py "$p" dir)

complex_function $append $overwrite $recurse $verbose

两行代码,以及对预期参数的简明描述,我们将进入实际的脚本行为。也许我疯了,但这似乎方式比我现在必须做的更好。


我在easy CLI argument parsing 上看到过Parsing shell script arguments 和类似这个wiki 页面的内容,但其中许多模式感觉笨重且容易出错,我不喜欢每次编写shell 脚本时都必须重新实现它们,尤其是当 Python、Java 等有这么好的参数处理库时。

【问题讨论】:

  • 你试过getopt吗?
  • @tuxuday 你打败了我...... getopt 应该帮助 dimo414
  • 我之前使用过 getopt 和 getopts(请参阅问题底部的 wiki 页面链接),但它们仍然有限制 - 引用链接:“getopt 无法处理空参数字符串或嵌入的参数空白。”和“[getopts] 只能处理短选项(-h)而不会欺骗。”我意识到 bash 中有可用的解决方案,但恕我直言,Python 可用的选项更优越,更容易争论。我很好奇 Python 实用程序的可行性/存在来实现这一点。 “你很笨,使用 bash。”最终可能是这个问题的可接受答案。
  • @dimo414,我们使用getopt 并对此感到满意。检查getopt 是否满足您的要求,如果没有,则放弃它。来自我的 exp getopt 命令足以满足大多数需求。
  • 谢谢,是的,我也使用过getopt。就像我说的,我对此并不满意。它确实有效,但我认为存在或可以制造更好的工具。我正在尝试探索这个想法的可行性,而不是重复 getopt 是解析命令行参数的一种选择。

标签: python bash command-line-arguments


【解决方案1】:

您可能会利用 bash 中的关联数组来帮助实现您的目标。

declare -A opts=($(getopts.py $@))
cd ${opts[dir]}
complex_function ${opts[append]}  ${opts[overwrite]} ${opts[recurse]} \
                 ${opts[verbose]} ${opts[args]}

要完成这项工作,getopts.py 应该是一个解析和清理参数的 Python 脚本。它应该打印如下字符串:

[dir]=/tmp
[append]=foo
[overwrite]=bar
[recurse]=baz
[verbose]=fizzbuzz
[args]="a b c d"

您可以留出一些值来检查选项是否能够被正确解析和清理。

从getopts.py返回:

[__error__]=true

添加到 bash 脚本中:

if ${opts[__error__]}; then
    exit 1
fi

如果您更愿意使用getopts.py 的退出代码,您可以使用eval:

getopts=$(getopts.py $@) || exit 1
eval declare -A opts=($getopts)

或者:

getopts=$(getopts.py $@)
if [[ $? -ne 0 ]]; then
    exit 1;
fi
eval declare -A opts=($getopts)

【讨论】:

  • 关联数组的巧妙构思!那将是相当不错的——尽管不幸的是,在我的工作环境中,我不能保证所有的机器都有这个。也就是说,自己动手并让 python 脚本返回一堆变量赋值并不难,比如$in_dir=/tmp; $in_append=0; 等。
【解决方案2】:

出于同样的需求,我最终为 bash 编写了一个受 optparse 启发的解析器(实际上在内部使用了 python);你可以在这里找到它:

https://github.com/carlobaldassi/bash_optparse

请参阅底部的自述文件以获得快速说明。您可能想在以下位置查看一个简单的示例:

https://github.com/carlobaldassi/bash_optparse/blob/master/doc/example_script_simple

根据我的经验,它非常强大(我是个超级偏执狂)、功能丰富等等,而且我在我的脚本中大量使用它。我希望它对其他人有用。欢迎提供反馈/贡献。

【讨论】:

  • 您创建了一个复杂的工具,它具有优美的声明性语法。为什么不允许人们使用 NORMAL 安装说明?真的,我只是不知道如何安装它。我试过 ./configure;制作; make install 失败了。
【解决方案3】:

编辑: 我还没有使用它(还),但如果我今天发布这个答案,我可能会推荐https://github.com/docopt/docopts,而不是像下面描述的自定义方法.


我已经编写了一个简短的 Python 脚本,它可以完成我想要的大部分工作。我还不相信它的生产质量(特别是缺乏错误处理),但总比没有好。我欢迎任何反馈。

它利用set builtin 重新分配位置参数,允许脚本的其余部分仍按需要处理它们。

bashparse.py

#!/usr/bin/env python

import optparse, sys
from pipes import quote

'''
Uses Python's optparse library to simplify command argument parsing.

Takes in a set of optparse arguments, separated by newlines, followed by command line arguments, as argv[2] and argv[3:]
and outputs a series of bash commands to populate associated variables.
'''

class _ThrowParser(optparse.OptionParser):
    def error(self, msg):
        """Overrides optparse's default error handling
        and instead raises an exception which will be caught upstream
        """
        raise optparse.OptParseError(msg)

def gen_parser(usage, opts_ls):
    '''Takes a list of strings which can be used as the parameters to optparse's add_option function.
    Returns a parser object able to parse those options
    '''
    parser = _ThrowParser(usage=usage)
    for opts in opts_ls:
        if opts:
            # yes, I know it's evil, but it's easy
            eval('parser.add_option(%s)' % opts)
    return parser

def print_bash(opts, args):
    '''Takes the result of optparse and outputs commands to update a shell'''
    for opt, val in opts.items():
        if val:
            print('%s=%s' % (opt, quote(val)))
    print("set -- %s" % " ".join(quote(a) for a in args))

if __name__ == "__main__":
    if len(sys.argv) < 2:
        sys.stderr.write("Needs at least a usage string and a set of options to parse")
        sys.exit(2)
    parser = gen_parser(sys.argv[1], sys.argv[2].split('\n'))

    (opts, args) = parser.parse_args(sys.argv[3:])
    print_bash(opts.__dict__, args)

使用示例:

#!/bin/bash

usage="[-f FILENAME] [-t|--truncate] [ARGS...]"
opts='
"-f"
"-t", "--truncate",action="store_true"
'

echo "$(./bashparse.py "$usage" "$opts" "$@")"
eval "$(./bashparse.py "$usage" "$opts" "$@")"

echo
echo OUTPUT

echo $f
echo $@
echo $0 $2

如果运行为:./run.sh one -f 'a_filename.txt' "two' still two" three 输出以下内容(注意内部位置变量仍然正确):

f=a_filename.txt
set -- one 'two'"'"' still two' three

OUTPUT
a_filename.txt
one two' still two three
./run.sh two' still two

忽略调试输出,您正在查看大约四行代码来构建强大的参数解析器。想法?

【讨论】:

    【解决方案4】:

    我的问题的原始前提假设委托给 Python 是简化参数解析的正确方法。如果我们放弃语言要求,我们实际上可以在 Bash 中做得不错*,使用 getopts 和一点 eval 魔法:

    main() {
      local _usage='foo [-a] [-b] [-f val] [-v val] [args ...]'
      eval "$(parse_opts 'f:v:ab')"
      echo "f=$f v=$v a=$a b=$b -- $#: $*"
    }
    
    main "$@"
    

    parse_opts 的实现在this gist 中,但基本方法是将选项转换为local 变量,然后可以像平常一样处理这些变量。所有标准的getopts 样板都被隐藏起来,错误处理按预期工作。

    因为它在函数中使用local 变量,所以parse_opts 不仅对命令行参数有用,它还可以与脚本中的任何函数一起使用。


    * 我说“体面的工作”是因为 Bash 的 getopts 是一个相当有限的解析器,并且只支持单字母选项。优雅、富有表现力的 CLI 仍然可以更好地用 Python 等其他语言实现。但是对于相当小的函数或脚本,这提供了一个很好的中间地带,而不会增加太多的复杂性或臃肿。

    【讨论】:

      猜你喜欢
      • 2011-02-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-02-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-05-20
      相关资源
      最近更新 更多