将Python函数作为Bash命令运行

14

如何从Python运行shell命令已经有很多文献,但我却想要相反的操作。我有一个Python模块mycommands.py,其中包含以下函数:

def command(arg1, arg2):
    pass

def command1(arg1, arg2, arg3):
    pass

函数参数都是字符串。目标是能够像下面这样从bash中运行这些函数。

$ command arg1 arg2 
$ command1 arg1 arg2 arg3 

到目前为止,我在.bash_profile中使用以下粗略设置,在此我必须手动为每个Python函数提供Bash绑定。

function command() {
    python -c "import mycommand as m; out=m.command('$1', '$2'); print(out)"
}

function command1() {
    python -c "import mycommand as m; out=m.command1('$1', '$2', '$3'); print(out)"
}

如果有一个单一的Bash命令能够实现这样,那将是非常好的:

It would be nice if one could have a single bash command like
$ import_python mycommands.py

有没有一个库能够自动将Python模块中所有的函数作为Bash命令导入?


4
为什么你想这样做? - Padraic Cunningham
3
请使用Click - tzaman
1
@tzaman 点击(Click)不为一个模块中的不同函数提供多重分派,这正是问题的大部分所在。除此之外,只需要导入 argparse 并为不同的函数编写解析器即可。 - Blacklight Shining
@Dzhelil 为什么不直接启动 Python 解释器并直接调用您的函数呢? - Blacklight Shining
我想这样做的一个原因是因为shell具有出色的文件名自动完成支持,而Python解释器则没有。从shell中调用操作文件的函数比从Python解释器中调用要容易得多。 - D R
5个回答

8

您可以创建一个基础脚本,比如说command.py,并检查它被调用时的名称(不要忘记使其可执行):

#!/usr/bin/python
import os.path
import sys

def command1(*args):
    print 'Command1'
    print args

def command2(*args):
    print 'Command2'
    print args


commands = {
    'command1': command1,
    'command2': command2
}

if __name__ == '__main__':
    command = os.path.basename(sys.argv[0])
    if command in commands:
        commands[command](*sys.argv[1:])

然后,您可以创建指向此脚本的软链接:

ln -s command.py command1
ln -s command.py command2

最后测试它:

$ ./command1 hello
Command1
('hello',)

$ ./command2 world
Command2
('world',)

如果您不想使用符号链接,并且想要像使用常规 bash 函数一样从 bash 脚本中调用 Python 函数,则可以采取以下额外步骤。首先,将 command.py 设为可执行文件,然后像这样使用它:./command.py command1 arg1 arg2。最后的 if 块中只需要将数组索引上移即可,因为现在的第一个参数是 _command1_。我提到这一点是因为这正是我使用此答案的情况(我已经点赞了 :-))。 - Dimitri Hautot

5
根据实际使用情况,最佳解决方案可能是简单地使用Click(或至少来自标准库的argparse模块)构建您的Python脚本并调用它。
command sub-command arg1 args2

来自shell。

Mercurial为一个突出的例子。

如果您真的必须将您的命令作为一级shell命令使用,请使用其他答案中描述的符号链接或别名。

只需使用几个方法,您就可以在sys.argv[0]上创建符号链接并进行分发:

$ cat cmd.py 
#!/usr/bin/env python

if __name__ == '__main__':
    import sys
    from os.path import basename
    cmd = basename(sys.argv[0])
    print("This script was called as " + cmd)

$ ln -s cmd.py bar
$ ln -s cmd.py foo
$ ./foo 
This script was called as foo
$ ./bar
This script was called as bar

如果您的Python脚本需要使用多个子命令,您可以添加以下内容来“自动化”该过程:

#!/usr/bin/env python
import sys

def do_repeat(a, b):
    print(a*int(b))

def do_uppercase(args):
    print(''.join(args).upper())

def do_eval():
    print("alias repeat='python cmd.py repeat';")
    print("alias uppercase='python cmd.py uppercase';")

if __name__ == '__main__':
    cmd = sys.argv[1]
    if cmd=='--eval':
        do_eval()

    else:
        args = sys.argv[2:]
        if cmd=='repeat':
            do_repeat(*args)
        elif cmd=='uppercase':
            do_uppercase(args)
        else:
            print('Unknown command: ' + cmd)

您可以按照以下方式使用它(假设在您的$PATH中有一个名为cmd.py的可执行Python脚本):
$ cmd.py --eval          # just to show what the output looks like
alias repeat='python cmd.py repeat';
alias uppercase='python cmd.py uppercase';

$ eval $(python cmd.py --eval)  # this would go in your .bashrc
$ repeat asdf 3
asdfasdfasdf
$ uppercase qwer
QWER

注意:以上是一个非常基础的示例,展示了如何在shell中使用eval。

4
您可以使用参数运行脚本,然后执行以下操作:
(f1不带参数调用,f2带有2个参数。错误处理尚不完美。)
import sys

def run():
    if len(sys.argv)<2:
        error()
        return

    if sys.argv[1] == 'f1':
        f1()

    elif sys.argv[1] == 'f2':
        if(len(sys.argv)!=4):
            error()
            return
        f2(sys.argv[2],sys.argv[3])

    else:
        error()

def f1():
    print("f1")

def f2(a,b):
    print("f2, arguments:",a,b)

def error():
    print("error")

run()

这不会让您像调用函数一样调用它们

commmand arg1 arg2

但你可以做的是:
python test.py f1
python test.py f2 arg1 arg2

编辑:

你可以创建别名:

alias f1="python test.py f1"
alias f1="python test.py f2"

为了实现您的要求:

$ f1
$ f2 arg1 arg2

1

autocommand似乎是值得考虑的(从函数签名直接使用cli)

示例:



@autocommand(__name__)
def cat(*files):
    for filename in files:
        with open(filename) as file:
            for line in file:
                print(line.rstrip())

$ python cat.py -h
usage: ipython [-h] [file [file ...]]

positional arguments:
  file

optional arguments:
  -h, --help  show this help message and exit


1
"

Fabric可以做到这一点:

"
from fabric.api import run
def host_type():
    run('uname -s')

fab -H localhost,linuxbox host_type
[localhost] run: uname -s
[localhost] out: Darwin
[linuxbox] run: uname -s
[linuxbox] out: Linux
完成。
断开与localhost的连接...完成。
断开与linuxbox的连接...完成。

更具体地回答问题:

$ cat fabfile.py
def command1(arg1, arg2):
    print arg1, arg2
$ fab command1:first,second

第一 第二
完成。

1
不确定为什么有人会对此进行负投票。在使用现有库的情况下,以 fab 前缀为代价似乎比任何自行开发的更复杂的方案更可取。 - Tim
不确定为什么。当然,Fabric可以直接做到这一点。 - Roman Susi
2
Fabric并没有提供OP实际需要的功能。可以说,在这种情况下,它使整个过程更加繁琐。 - user3850
@hop 虽然这个答案不是与问题完全匹配,但我认为考虑到问题和缺乏背景知识,这仍然是一个有效的方法。 我强烈建议其他读者使用像 fabric 这样的库,而不是上面更复杂的参数处理 / bash 别名组合。 - Tim

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