D R
D R

Reputation: 22510

Running a python function as a bash command

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?

Upvotes: 14

Views: 11719

Answers (5)

Red Pill
Red Pill

Reputation: 551

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

Upvotes: 1

Tim
Tim

Reputation: 20777

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.

Upvotes: 1

user3850
user3850

Reputation:

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.

Upvotes: 5

Wouter
Wouter

Reputation: 1568

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

Upvotes: 4

JuniorCompressor
JuniorCompressor

Reputation: 20025

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',)

Upvotes: 8

Related Questions