OptionParser - supporting any option at the end of the command line - python

I'm writing a small program that's supposed to execute a command on a remote server (let's say a reasonably dumb wrapper around ssh [hostname] [command]).
I want to execute it as such:
./floep [command]
However, I need to pass certain command lines from time to time:
./floep -v [command]
so I decided to use optparse.OptionParser for this. Problem is, I sometimes the command also has argument, which works fine if I do:
./floep -v "uname -a"
But I also want it to work when I use:
./floep -v uname -a
The idea is, as soon as I come across the first non-option argument, everything after that should be part of my command.
This, however, gives me:
Usage: floep [options]
floep: error: no such option: -a
Does OptionParser support this syntax? If so: how?
If not: what's the best way to fix this?

Try using disable_interspersed_args()
#!/usr/bin/env python
from optparse import OptionParser
parser = OptionParser()
parser.disable_interspersed_args()
parser.add_option("-v", action="store_true", dest="verbose")
(options, args) = parser.parse_args()
print "Options: %s args: %s" % (options, args)
When run:
$ ./options.py foo -v bar
Options: {'verbose': None} args: ['foo', '-v', 'bar']
$ ./options.py -v foo bar
Options: {'verbose': True} args: ['foo', 'bar']
$ ./options.py foo -a bar
Options: {'verbose': None} args: ['foo', '-a', 'bar']

OptionParser instances can actually be manipulated during the parsing operation for complex cases. In this case, however, I believe the scenario you describe is supported out-of-the-box (which would be good news if true! how often does that happen??). See this section in the docs: Querying and manipulating your option parser.
To quote the link above:
disable_interspersed_args()
Set parsing to stop on the first non-option. Use this if you have a
command processor which runs another command which has options of its
own and you want to make sure these options don’t get confused. For example,
each command might have a different set of options.

from optparse import OptionParser
import subprocess
import os
import sys
parser = OptionParser()
parser.add_option("-q", "--quiet",
action="store_true", dest="quiet", default=False,
help="don't print output")
parser.add_option("-s", "--signal",
action="store_true", dest="signal", default=False,
help="signal end of program and return code")
parser.disable_interspersed_args()
(options, command) = parser.parse_args()
if not command:
parser.print_help()
sys.exit(1)
if options.quiet:
ret = subprocess.call(command, stdout=open(os.devnull, 'w'),
stderr=subprocess.STDOUT)
else:
ret = subprocess.call(command)
if options.signal:
print "END OF PROGRAM!!! Code: %d" % ret

You can use a bash script like this:
#!/bin/bash
while [ "-" == "${1:0:1}" ] ; do
if [ "-v" == "${1}" ] ; then
# do something
echo "-v"
elif [ "-s" == "${1}" ] ; then
# do something
echo "-s"
fi
shift
done
${#}
The ${#} gives you the rest of the command line that was not consumed by the shift calls.
To use ssh you simply change the line from
${#}
to
ssh ${user}#${host} ${#}
test.sh echo bla
bla
test.sh -v echo bla
-v
bla
test.sh -v -s echo bla
-v
-s
bla

Related

argparse command line -option after given path

I'm new to python and currently experimenting using argparse to add command line options. However my code is not working, despite looking at various online tutorials and reading up on argparse I still don't fully understand it. My problem is whenever I try to call my -option it gives me a find.py error: argument regex:
Here is my call:
./find.py ../Python -name '[0-9]*\.txt'
../Python is one directory behind my current one and has a list of files/directories. Without the -name option I print out the files with their path (this works fine) but with the -name option I want to print out files matching the regex but it won't work. Here is what I currently have:
#!/usr/bin/python2.7
import os, sys, argparse,re
from stat import *
def regex_type(s, pattern=re.compile(r"[a-f0-9A-F]")):
if not pattern.match(s):
raise argparse.ArgumentTypeError
return s
def main():
direc = sys.argv[1]
for f in os.listdir(direc):
pathname = os.path.join(direc, f)
mode = os.stat(pathname).st_mode
if S_ISREG(mode):
print pathname
parser = argparse.ArgumentParser()
parser.add_argument(
'-name', default=[sys.stdin], nargs="*")
parser.add_argument('regex', type=regex_type)
args = parser.parse_args()
if __name__ == '__main__':
main()
I tweaked your type function to be more informative:
def regex_type(s, pattern=re.compile(r"[a-f0-9A-F]")):
print('regex string', s)
if not pattern.match(s):
raise argparse.ArgumentTypeError('pattern not match')
return s
Called with
2104:~/mypy$ python2 stack50072557.py .
I get:
<director list>
('regex string', '.')
usage: stack50072557.py [-h] [-name [NAME [NAME ...]]] regex
stack50072557.py: error: argument regex: pattern not match
So it tries to pass sys.argv[1], the first string after the script name, to the regex_type function. If it fails it issues the error message and usage.
OK, the problem was the ..; I'll make a directory:
2108:~/mypy$ mkdir foo
2136:~/mypy$ python2 stack50072557.py foo
('regex string', 'foo')
Namespace(name=[<open file '<stdin>', mode 'r' at 0x7f3bea2370c0>], regex='foo')
2138:~/mypy$ python2 stack50072557.py foo -name a b c
('regex string', 'foo')
Namespace(name=['a', 'b', 'c'], regex='foo')
The strings following '-name' are allocated to that attribute. There's nothing in your code that will test them or pass them through the regex_type function. Only the first non-flag string does that.
Reading sys.argv[1] initially does not remove it from the list. It's still there for use by the parser.
I would set up a parser that uses a store_true --name argument, and 2 positionals - one for the dir and the other for regex.
After parsing check args.name. If false print the contents of args.dir. If true, perform your args.regex filter on those contents. glob might be useful.
The parser finds out what your user wants. Your own code acts on it. Especially as a beginner, it is easier and cleaner to separate the two steps.
With:
def parse(argv=None):
parser = argparse.ArgumentParser()
parser.add_argument('-n', '--name', action='store_true')
parser.add_argument('--dir', default='.')
parser.add_argument('--regex', default=r"[a-f0-9A-F]")
args = parser.parse_args(argv)
print(args)
return args
def main(argv=None):
args = parse(argv)
dirls = os.listdir(args.dir)
if args.name:
dirls = [f for f in dirls if re.match(args.regex, f)]
print(dirls)
else:
print(dirls)
I get runs like:
1005:~/mypy$ python stack50072557.py
Namespace(dir='.', name=False, regex='[a-f0-9A-F]')
['test.npz', 'stack49909128.txt', 'stack49969840.txt', 'stack49824248.py', 'test.h5', 'stack50072557.py', 'stack49963862.npy', 'Mcoo.npz', 'test_attribute.h5', 'stack49969861.py', 'stack49969605.py', 'stack49454474.py', 'Mcsr.npz', 'Mdense.npy', 'stack49859957.txt', 'stack49408644.py', 'Mdok', 'test.mat5', 'stack50012754.py', 'foo', 'test']
1007:~/mypy$ python stack50072557.py -n
Namespace(dir='.', name=True, regex='[a-f0-9A-F]')
['foo']
1007:~/mypy$ python stack50072557.py -n --regex='.*\.txt'
Namespace(dir='.', name=True, regex='.*\\.txt')
['stack49909128.txt', 'stack49969840.txt', 'stack49859957.txt']
and help:
1007:~/mypy$ python stack50072557.py -h
usage: stack50072557.py [-h] [-n] [--dir DIR] [--regex REGEX]
optional arguments:
-h, --help show this help message and exit
-n, --name
--dir DIR
--regex REGEX
If I change the dir line to:
parser.add_argument('dir', default='.')
help is now
1553:~/mypy$ python stack50072557.py -h
usage: stack50072557.py [-h] [-n] [--regex REGEX] dir
positional arguments:
dir
optional arguments:
-h, --help show this help message and exit
-n, --name
--regex REGEX
and runs are:
1704:~/mypy$ python stack50072557.py -n
usage: stack50072557.py [-h] [-n] [--regex REGEX] dir
stack50072557.py: error: too few arguments
1705:~/mypy$ python stack50072557.py . -n
Namespace(dir='.', name=True, regex='[a-f0-9A-F]')
['foo']
1705:~/mypy$ python stack50072557.py ../mypy -n --regex='.*\.txt'
Namespace(dir='../mypy', name=True, regex='.*\\.txt')
['stack49909128.txt', 'stack49969840.txt', 'stack49859957.txt']
I get the error because it now requires a directory, even it is '.'.
Note that the script still uses:
if __name__ == '__main__':
main()
My main loads the dir, and applies the regex filter to that list of names. My args.dir replaces your direc.

Python argparse versatility ability for true/false and string?

I have the following arguments parser using argparse in a python 2.7 script:
parser = argparse.ArgumentParser(description=scriptdesc)
parser.add_argument("-l", "--list", help="Show current running sesssions", dest="l_list", type=str, default=None)
I want to be able to run:
./script -l and ./script -l session_1
So that the script returns either all sessions or a single session without an extra parameter such as -s
However I can't find a way to do this in a single arg.
This is a bit of a hack since it relies on accessing sys.argv outside of any argparse function but you can do something like:
import argparse
import sys
parser = argparse.ArgumentParser(description='')
parser.add_argument("-l", "--list", help="Show current running sesssions", dest="l_list", nargs='?')
args = parser.parse_args()
if args.l_list == None:
if '-l' in sys.argv or '--list' in sys.argv:
print('display all')
else:
print('display %s only' %args.l_list)
And you would obviously replace the print statements with your actual code. This works by allowing 0 or 1 argument (using nargs='?'). This allows you to either pass an argument with -l or not. This means that in the args namespace, l_list can be None (the default) if you call -l without an argument OR if you don't use -l at all. Then later you can check if -l was called without an argument (if l_list == None and -l or --list is in sys.argv).
If I name this script test.py I get the following outputs when calling it from the command line.
$python test.py
$python test.py -l
display all
$python test.py -l session1
display session1 only
EDIT
I figured out an argparse only solution!! No relying on sys.argv:
import argparse
parser = argparse.ArgumentParser(description='')
parser.add_argument("-l", "--list", help="Show current running sesssions", dest="l_list", nargs='?', default=-1)
args = parser.parse_args()
if args.l_list == None:
print('display all')
elif args.l_list != -1:
print('display %s only' %args.l_list)
So it turns out that the default keyword in .add_argument only applies when the argument flag is not called at all. If the flag is used without anything following it, it will default to None regardless of what the default keyword is. So if we set the default to something that is not None and not an expected argument value (in this case I chose -1), then we can handle all three of your cases:
$ python test.py
$ python test.py -l
display all
$ python test.py -l session1
display session1 only

Defining Argument values with docopt

Im working on my first python "app" and after some advice from the participants on Stackoverflow. Ive decided to scrap what I had and start from scratch.
It seems to be parsing the arguments nicely for usage etc but im not sure how I am meant to assign the values to the args?
Do I have to create a nest of ifs? if so how do i do that for the args in docopt?
maybe like this?
if opt in ("-f", "--file"):
FWORD = arg
CODE
#!/usr/bin/python
"""
Basic domain bruteforcer
Usage:
your_script.py (-f <file>) (-d <domain>) [-t 10] [-v]
your_script.py -h | --help
Options:
-h --help Show this screen.
-f --file File to read potential Sub-domains from. (Required argument)
-p --proxy Proxy address and port. [default: http://127.0.0.1:8080] (Optional)
-d --domain Domain to bruteforce.(Required argument)
-t --thread Thread count. (Optional)
-v --verbose Turn debug on. (Optional)
"""
from docopt import docopt
def fread(FWORD, *args):
flist = open(FWORD).readlines()
return flist
if __name__ == "__main__":
arguments = docopt(__doc__, version='0.1a')
print fread(fword)
You almost got it. Your arguments variable contains the argument and you look them up as you would in a dict. So if you want to call the fread function with the file argument your main would look like this:
if __name__ == "__main__":
arguments = docopt(__doc__, version='0.1a')
fread(arguments['<file>'])
If you call the script like this:
> python your_script.py -f myfiles/file.txt -d google.com
Then your arguments will look like this:
>>> print arguments
{'--domain': True,
'--file': True,
'--help': False,
'--thread': False,
'--verbose': False,
'10': False,
'<domain>': 'google.com',
'<file>': 'myfiles/file.txt'}
You should take a look at argparse from the python standard library.

Need help with python script with bash commands

I copied this script from internet but idon't know how to use it. i am newbiw to python so please help. When i execute it using
./test.py then i can only see
usage: py4sa [option]
A unix toolbox
options:
--version show program's version number and exit
-h, --help show this help message and exit
-i, --ip gets current IP Address
-u, --usage gets disk usage of homedir
-v, --verbose prints verbosely
when i type py4sa then it says bash command not found
The full script is
#!/usr/bin/env python
import subprocess
import optparse
import re
#Create variables out of shell commands
#Note triple quotes can embed Bash
#You could add another bash command here
#HOLDING_SPOT="""fake_command"""
#Determines Home Directory Usage in Gigs
HOMEDIR_USAGE = """
du -sh $HOME | cut -f1
"""
#Determines IP Address
IPADDR = """
/sbin/ifconfig -a | awk '/(cast)/ { print $2 }' | cut -d':' -f2 | head -1
"""
#This function takes Bash commands and returns them
def runBash(cmd):
p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
out = p.stdout.read().strip()
return out #This is the stdout from the shell command
VERBOSE=False
def report(output,cmdtype="UNIX COMMAND:"):
#Notice the global statement allows input from outside of function
if VERBOSE:
print "%s: %s" % (cmdtype, output)
else:
print output
#Function to control option parsing in Python
def controller():
global VERBOSE
#Create instance of OptionParser Module, included in Standard Library
p = optparse.OptionParser(description='A unix toolbox',
prog='py4sa',
version='py4sa 0.1',
usage= '%prog [option]')
p.add_option('--ip','-i', action="store_true", help='gets current IP Address')
p.add_option('--usage', '-u', action="store_true", help='gets disk usage of homedir')
p.add_option('--verbose', '-v',
action = 'store_true',
help='prints verbosely',
default=False)
#Option Handling passes correct parameter to runBash
options, arguments = p.parse_args()
if options.verbose:
VERBOSE=True
if options.ip:
value = runBash(IPADDR)
report(value,"IPADDR")
elif options.usage:
value = runBash(HOMEDIR_USAGE)
report(value, "HOMEDIR_USAGE")
else:
p.print_help()
#Runs all the functions
def main():
controller()
#This idiom means the below code only runs when executed from command line
if __name__ == '__main__':
main()
It seems to me you have stored the script under another name: test.py rather than py4sa. So typing ./test.py, like you did, is correct for you. The program requires arguments, however, so you have to enter one of the options listed under 'usage'.
Normally 'py4sa [OPTIONS]' would mean that OPTIONS is optional, but looking at the code we can see that it isn't:
if options.verbose:
# ...
if options.ip:
# ...
elif options.usage:
# ...
else:
# Here's a "catch all" in case no options are supplied.
# It will show the help text you get:
p.print_help()
Note that the program probably would not be recognized by bash even if you renamed it to py4sa, as the current directory is often not in bash's PATH. It says 'usage: py4sa (..)' because that's hard-coded into the program.
The script is called "test.py". Either invoke it as such, or rename it to "py4sa".
you run a Python script using the interpreter, so
$ python py4sa

Implementing a "[command] [action] [parameter]" style command-line interfaces?

What is the "cleanest" way to implement an command-line UI, similar to git's, for example:
git push origin/master
git remote add origin git://example.com master
Ideally also allowing the more flexible parsing, for example,
jump_to_folder app theappname v2
jump_to_folder app theappname source
jump_to_folder app theappname source v2
jump_to_folder app theappname build v1
jump_to_folder app theappname build 1
jump_to_folder app theappname v2 build
jump_to_folder is the scripts name, app is the command, theappname is a "fixed-location" parameter, "build" and "v2" etc are arguments (For example, possible arguments would be any number/any number prefixed with a v, or build/source/tmp/config)
I could just manually parse the arguments with a series of if/else/elifs, but there must be a more elegant way to do this?
As an entirely theoretically example, I could describe the UI schema..
app:
fixed: application_name
optional params:
arg subsection:
"build"
"source"
"tmp"
"config"
arg version:
integer
"v" + integer
Then parse the supplied arguments though the above schema, and get a dictionary:
>>> print schema.parse(["app", "theappname", "v1", "source"])
{
"application_name": "theappname",
"params":{
"subsection": "source",
"version":"v1"
}
}
Does such a system exist? If not, how would I go about implementing something along these lines?
argparse is perfect for this, specifically "sub-commands" and positional args
import argparse
def main():
arger = argparse.ArgumentParser()
# Arguments for top-level, e.g "subcmds.py -v"
arger.add_argument("-v", "--verbose", action="count", default=0)
subparsers = arger.add_subparsers(dest="command")
# Make parser for "subcmds.py info ..."
info_parser = subparsers.add_parser("info")
info_parser.add_argument("-m", "--moo", dest="moo")
# Make parser for "subcmds.py create ..."
create_parser = subparsers.add_parser("create")
create_parser.add_argument("name")
create_parser.add_argument("additional", nargs="*")
# Parse
opts = arger.parse_args()
# Print option object for debug
print opts
if opts.command == "info":
print "Info command"
print "--moo was %s" % opts.moo
elif opts.command == "create":
print "Creating %s" % opts.name
print "Additional: %s" % opts.additional
else:
# argparse will error on unexpected commands, but
# in case we mistype one of the elif statements...
raise ValueError("Unhandled command %s" % opts.command)
if __name__ == '__main__':
main()
This can be used like so:
$ python subcmds.py create myapp v1 blah
Namespace(additional=['v1', 'blah'], command='create', name='myapp', verbose=0)
Creating myapp
Additional: ['v1', 'blah']
$ python subcmds.py info --moo
usage: subcmds.py info [-h] [-m MOO]
subcmds.py info: error: argument -m/--moo: expected one argument
$ python subcmds.py info --moo 1
Namespace(command='info', moo='1', verbose=0)
Info command
--moo was 1
The cmd module would probably work well for this.
Example:
import cmd
class Calc(cmd.Cmd):
def do_add(self, arg):
print sum(map(int, arg.split()))
if __name__ == '__main__':
Calc().cmdloop()
Run it:
$python calc.py
(Cmd) add 4 5
9
(Cmd) help
Undocumented commands:
======================
add help
(Cmd)
See the Python docs or PyMOTW site for more info.
Straight from one of my scripts:
import sys
def prog1_func1_act1(): print "pfa1"
def prog2_func2_act2(): print "pfa2"
commands = {
"prog1 func1 act1": prog1_func1_act1,
"prog2 func2 act2": prog2_func2_act2
}
try:
commands[" ".join(sys.argv[1:])]()
except KeyError:
print "Usage: ", commands.keys()
It's a pretty quick and dirty solution, but works great for my usage. If I were to clean it up a bit, I would probably add argparse to the mix for parsing positional and keyword arguments.
Python has a module for parsing command line options, optparse.
You might want to take a look at cliff – Command Line Interface Formulation Framework
Here's my suggestion.
Change your grammar slightly.
Use optparse.
Ideally also allowing the more flexible parsing, for example,
jump_to_folder -n theappname -v2 cmd
jump_to_folder -n theappname cmd source
jump_to_folder -n theappname -v2 cmd source
jump_to_folder -n theappname -v1 cmd build
jump_to_folder -n theappname -1 cmd build
jump_to_folder -n theappname -v2 cmd build
Then you have 1 or 2 args: the command is always the first arg. It's optional argument is always the second arg.
Everything else is options, in no particular order.

Categories