使用Python将复杂的参数解析为shell脚本

dim*_*414 8 python bash command-line-arguments

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

因此,我希望能够让Python做这个繁重的工作,然后在我的shell脚本中获取这些简化和清理的值,而无需进一步担心用户指定的参数.

举一个具体的例子,我工作的许多shell脚本已被定义为按特定顺序接受它们的参数.你可以调用start_server.sh --server myserver --port 80start_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
Run Code Online (Sandbox Code Playgroud)

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

所以这是我的问题:1)是否有任何现有的(优选的P​​ython)实用程序通过比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
Run Code Online (Sandbox Code Playgroud)

两行代码,以及对期望参数的简明描述,以及我们对实际脚本行为的看法.也许我疯了,但似乎这样比我觉得我现在要做的更好.


我已经看到解析shell脚本参数和像这个wiki页面这样简单的CLI参数解析,但是这些模式中的许多都感觉笨重且容易出错,我不喜欢每次编写shell脚本时都要重新实现它们,特别是当Python,Java等有很好的参数处理库.

dim*_*414 2

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


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

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

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)
Run Code Online (Sandbox Code Playgroud)

用法示例:

#!/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 Code Online (Sandbox Code Playgroud)

如果运行为:./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
Run Code Online (Sandbox Code Playgroud)

忽略调试输出,您将查看大约四行来构造一个强大的参数解析器。想法?