14

There is a lot of literature on how to run shell commands from python, but I am interested in doing the opposite. I have a python module mycommands.py which contains functions like below

def command(arg1, arg2):
    pass

def command1(arg1, arg2, arg3):
    pass

where the function arguments are all strings. The objective is to be able to run these functions from bash like below

$ command arg1 arg2 
$ command1 arg1 arg2 arg3 

So far I have the following brute setup in .bash_profile where I have to provide bash bindings to each python function manually

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)"
}

It would be nice if one could have a single bash command like

$ import_python mycommands.py

which would automatically import all the python functions in the module as bash commands. Does there exist a library which implements such a command?

5
  • 4
    Why do you want to do this? Commented Apr 19, 2015 at 19:05
  • 3
    Just use Click. Commented Apr 19, 2015 at 19:17
  • 1
    @tzaman Click doesn't provide multiple dispatch for different functions in one module, though, which is most of what the question is. Aside from that, it's just a matter of importing argparse and writing parsers for the different functions. Commented Apr 19, 2015 at 23:23
  • @Dzhelil Why not just kick off a python interpreter and call your functions directly? Commented Apr 19, 2015 at 23:25
  • One reason I want to this is because shells have excellent file name completion support, and the python interpreter does not. It is far easier to call functions that operates on files from the shell, than it is from the python interpreter. Commented Apr 20, 2015 at 1:23

5 Answers 5

8

You can create a base script, let's say command.py and check with what name this script was called (don't forget to make it executable):

#!/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:])

Then you can create soft links to this script:

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

and finally test it:

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

$ ./command2 world
Command2
('world',)
Sign up to request clarification or add additional context in comments.

1 Comment

Additional touch if you don't want symbolic links and want to call your Python functions from a bash script like you would do with regular bash functions. Make command.py executable, then use it like this: ./command.py command1 arg1 arg2. You just need to shift up array indices in final if block, because the command1 has now become the first argument. I mention this because that's my use case that led me to this answer (which I upvoted :-) ).
5

Depending on your actual use case, the best solution could be to simply use Click (or at least the argparse module from the Standard Library) to build your Python script and call it as

command sub-command arg1 args2

from the shell.

See Mercurial for one prominent example of this.

If you really must have your commands as first-level shell commands, use symlinks or aliases as described in other answers.

With a only a few methods, you can symlink and dispatch on 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

With more than a couple of sub-commands, you could add something like the following to your Python script to "automate" the process:

#!/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)

You would use it like this (assuming an executable Python script called cmd.py somewhere in your $PATH):

$ 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

NB: The above is a very rudimentary example to show how to use eval in the shell.

Comments

4

You could run the script with arguments, and then do something like this:

(f1 is called without arguments, f2 with 2 arguments. Error handling is not fool prove yet.)

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()

This doesn't let you call the functions like

commmand arg1 arg2

but you can do

python test.py f1
python test.py f2 arg1 arg2

Edit:

You can create aliases:

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

to achieve what you requested:

$ f1
$ f2 arg1 arg2

Comments

1

Fabric can do just this:

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
Done.
Disconnecting from localhost... done.
Disconnecting from linuxbox... done.

Or more specific to the question:

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

first second
Done.

4 Comments

Not sure why anyone downvoted this.. Using an existing library at the cost of the fab prefix seems preferable to me over anything homegrown and more complicated.
Not sure why too. Of course fabric does it directly.
fabric does not what the OP is actually asking for. arguably, it makes the whole thing more cumbersome in this case.
@hop while it's true that this answer isn't an exact match to the question, in my opinion it's still a valid approach given the problem and complete lack of background. I'd strongly recommend other readers to use a library like fabric, over the more complicated argument handling / bash alias combinations above.
1

autocommand seems to be relevant to consider (cli directly from function signatures)

example:



@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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.