In my previous post on Argparse I had some issues with validating values and printing help output for values that are out of range. I got some great suggestions from my smart colleagues Mikkel Troest and Patrick Ogenstad who are far more advanced in their Python knowledge.

We started out with the following code in the last post:

#!/usr/bin/env python
# import Argparse
import argparse
# import sys
import sys
# create main function
def main():
    # create ArgumentParser object
    parser = argparse.ArgumentParser(description="Daniel's ping script v0.1")
    # add arguments
    parser.add_argument("-version", "--version", action="version", version="%(prog)s 0.1")
    parser.add_argument("-c", "--c" , help="Number of packets", action="store", type=int, choices=xrange(1, 1000), metavar=("(1-1000)"))
    parser.add_argument("-s", "--s" , help="packetsize in bytes", action="store", type=int, choices=xrange(56, 1500), metavar=("(56-1500)"))
    parser.add_argument("-t", "--t" , help="ttl for icmp packets", action="store", type=int, choices=xrange(1, 255), metavar=("(1-255)"))
    parser.add_argument("-w", "--w" , help="timeout in seconds", action="store", type=int, choices=xrange(1, 10), metavar=("(1-10)"))
    parser.add_argument("-ip", "--ip", help="ip address", action="store", required=True)
    # parse command-line arguments
    parser.parse_args()

if __name__ == "__main__" and len(sys.argv) < 2:
    print "use the -h flag for help on this script"
else:
    main()

First let's clean up this a bit since the length of the lines are more than 80 characters in width. I have also updated the xrange values as Mikkel pointed out to me that the range command is zero indexed which means that the range has to be one number above our maximum value.

#!/usr/bin/env python
# import Argparse
import argparse
# import sys
import sys
# create main function
def main():
    # create ArgumentParser object
    parser = argparse.ArgumentParser(description="Daniel's ping script v0.1")
    # add arguments
    parser.add_argument("-version", "--version", action="version", version="%(prog)s 0.1")
    parser.add_argument("-c", "--c" , help="Number of packets", action="store"\
        , type=int, choices=xrange(1, 1001), metavar=("(1-1000)"))
    parser.add_argument("-s", "--s" , help="packetsize in bytes", action="store"\
        , type=int, choices=xrange(56, 1501), metavar=("(56-1500)"))
    parser.add_argument("-t", "--t" , help="ttl for icmp packets", action="store"\
        , type=int, choices=xrange(1, 256), metavar=("(1-255)"))
    parser.add_argument("-w", "--w" , help="timeout in seconds", action="store"\
        , type=int, choices=xrange(1, 11), metavar=("(1-10)"))
    parser.add_argument("-ip", "--ip", help="ip address", action="store"\
        , required=True)
    # parse command-line arguments
    parser.parse_args()

if __name__ == "__main__" and len(sys.argv) < 2:
    print "use the -h flag for help on this script"
else:
    main()

Let's confirm that the script still works after breaking up the lines.

daniel@daniel-iperf5:~/python$ ./pingscript3.py 
use the -h flag for help on this script
daniel@daniel-iperf5:~/python$ ./pingscript3.py -h
usage: pingscript3.py [-h] [-version] [-c 1-1000)] [-s (56-1500)] [-t (1-255)]
                      [-w (1-10] -ip IP

Daniel's ping script v0.1

optional arguments:
  -h, --help            show this help message and exit
  -version, --version   show program's version number and exit
  -c (1-1000), --c (1-1000)
                        Number of packets
  -s (56-1500), --s (56-1500)
                        packetsize in bytes
  -t (1-255), --t (1-255)
                        ttl for icmp packets
  -w (1-10), --w (1-10)
                        timeout in seconds
  -ip IP, --ip IP       ip address

This works so far but produces the ugly output as shown below when entering invalid values:

daniel@daniel-iperf5:~/python$ ./pingscript3.py -w 20 1.1.1.1
usage: pingscript3.py [-h] [-version] [-c 1-1000)] [-s (56-1500)] [-t (1-255)]
                      [-w (1-10] -ip IP
pingscript3.py: error: argument -w/--w: invalid choice: 20 (choose from 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

We can create a class that Argparse will use and in this class the valid numbers will be validated. A class is created with the code below:

class Ttl():

    def __init__(self, num):
        self.val = int(num)
        if 1 <= self.val <= 255:
            pass
        else:
            raise ValueError()

The class is created and the "init" method is used to instantiate an object of the class "Ttl". The variable "num" is sent into the method and this value has to be between 1 to 255 for it to be valid. If it isn't a "ValueError" gets raised. To use this class in Argparse we have to reference it in our previous code.

parser.add_argument("-t", "--t" , help="ttl for icmp packets", action="store"\
        , type=Ttl, choices=xrange(1, 256), metavar=("(1-255)"))

The class has been changed from "int" to "Ttl". There is also no need to use "choices" since we now have our class to validate the values so let's remove that from the code.

parser.add_argument("-t", "--t" , help="ttl for icmp packets", action="store"\
        , type=Ttl, metavar=("(1-255)"))

Running the script now produces the following output:

daniel@daniel-iperf5:~/python$ ./pingscript4.py -t 300 1.1.1.1
usage: pingscript4.py [-h] [-version] [-c 1-1000)] [-s (56-1500)] [-t (1-255)]
                      [-w (1-10] -ip IP
pingscript4.py: error: argument -t/--t: invalid Ttl value: '300'

That looks a lot better. Let's update the rest of the script to also use classes for validating values.

#!/usr/bin/env python
# import Argparse
import argparse
# import sys
import sys
# create classes for argparse
class PktCnt():

    def __init__(self, num):
        self.val = int(num)
        if 1 <= self.val <= 1000:
            pass
        else:
            raise ValueError()

class PktSize():

    def __init__(self, num):
        self.val = int(num)
        if 56 <= self.val <= 1500:
            pass
        else:
            raise ValueError()

class Ttl():

    def __init__(self, num):
        self.val = int(num)
        if 1 <= self.val <= 255:
            pass
        else:
            raise ValueError()

class IcmpTimeout():

    def __init__(self, num):
        self.val = int(num)
        if 1 <= self.val <= 10:
            pass
        else:
            raise ValueError()

# create main function
def main():
    # create ArgumentParser object
    parser = argparse.ArgumentParser(description="Daniel's ping script v0.1")
    # add arguments
    parser.add_argument("-version", "--version", action="version", version="%(prog)s 0.1")
    parser.add_argument("-c", "--c" , help="Number of packets", action="store"\
        , type=PktCnt, metavar=("(1-1000)"))
    parser.add_argument("-s", "--s" , help="packetsize in bytes", action="store"\
        , type=PktSize, metavar=("(56-1500)"))
    parser.add_argument("-t", "--t" , help="ttl for icmp packets", action="store"\
        , type=Ttl, metavar=("(1-255)"))    
    parser.add_argument("-w", "--w" , help="timeout in seconds", action="store"\
        , type=IcmpTimeout, metavar=("(1-10)"))
    parser.add_argument("-ip", "--ip", help="ip address", action="store"\
        , required=True)
    # parse command-line arguments
    parser.parse_args()

if __name__ == "__main__" and len(sys.argv) < 2:
    print "use the -h flag for help on this script"
else:
    main()

Running the script with invalid values looks a lot better now:

daniel@daniel-iperf5:~/python$ ./pingscript5.py -c 2000
usage: pingscript5.py [-h] [-version] [-c 1-1000)] [-s (56-1500)] [-t (1-255)]
                      [-w (1-10] -ip IP
pingscript5.py: error: argument -c/--c: invalid PktCnt value: '2000'
daniel@daniel-iperf5:~/python$ ./pingscript5.py -s 2000
usage: pingscript5.py [-h] [-version] [-c 1-1000)] [-s (56-1500)] [-t (1-255)]
                      [-w (1-10] -ip IP
pingscript5.py: error: argument -s/--s: invalid PktSize value: '2000'
daniel@daniel-iperf5:~/python$ ./pingscript5.py -t 2000
usage: pingscript5.py [-h] [-version] [-c 1-1000)] [-s (56-1500)] [-t (1-255)]
                      [-w (1-10] -ip IP
pingscript5.py: error: argument -t/--t: invalid Ttl value: '2000'
daniel@daniel-iperf5:~/python$ ./pingscript5.py -w 2000
usage: pingscript5.py [-h] [-version] [-c 1-1000)] [-s (56-1500)] [-t (1-255)]
                      [-w (1-10] -ip IP
pingscript5.py: error: argument -w/--w: invalid IcmpTimeout value: '2000'

When we run the script with -h flag the output is still a bit cluttered:

daniel@daniel-iperf5:~/python$ ./pingscript5.py -h
usage: pingscript5.py [-h] [-version] [-c 1-1000)] [-s (56-1500)] [-t (1-255)]
                      [-w (1-10] -ip IP

Daniel's ping script v0.1

optional arguments:
  -h, --help            show this help message and exit
  -version, --version   show program's version number and exit
  -c (1-1000), --c (1-1000)
                        Number of packets
  -s (56-1500), --s (56-1500)
                        packetsize in bytes
  -t (1-255), --t (1-255)
                        ttl for icmp packets
  -w (1-10), --w (1-10)
                        timeout in seconds
  -ip IP, --ip IP       ip address

We can make this a bit less cluttered by using the "formatter_class" set to "RawTextHelpFormatter".

parser = argparse.ArgumentParser(description="Daniel's ping script v0.1",\
     formatter_class=argparse.RawTextHelpFormatter)

It is then possible to insert tabs, newlines etc. into the help text like the one below:

parser.add_argument("-c", "--c" , help="\tNumber of packets", action="store"\
        , type=PktCnt, metavar=("(1-1000)"))

The output then looks slightly better:

daniel@daniel-iperf5:~/python$ ./pingscript6.py -h
usage: pingscript6.py [-h] [-version] [-c 1-1000)] [-s (56-1500)] [-t (1-255)]
                      [-w (1-10] -ip IP

Daniel's ping script v0.1

optional arguments:
  -h, --help            show this help message and exit
  -version, --version   show program's version number and exit
  -c (1-1000), --c (1-1000)
                                Number of packets
  -s (56-1500), --s (56-1500)
                                packetsize in bytes
  -t (1-255), --t (1-255)
                                ttl for icmp packets
  -w (1-10), --w (1-10)
                                timeout in seconds
  -ip IP, --ip IP               ip address

I haven't figured out yet if I can put the help for the argument on the same line or not. If anyone know, please ping me.

Finally. Since I've promised to move towards Python3, let's update the script to use Python3 syntax for the print statement. This is then our final code:

#!/usr/bin/env python
# import Argparse
import argparse
# import sys
import sys
# create classes for argparse
class PktCnt():

    def __init__(self, num):
        self.val = int(num)
        if 1 <= self.val <= 1000:
            pass
        else:
            raise ValueError()

class PktSize():

    def __init__(self, num):
        self.val = int(num)
        if 56 <= self.val <= 1500:
            pass
        else:
            raise ValueError()

class Ttl():

    def __init__(self, num):
        self.val = int(num)
        if 1 <= self.val <= 255:
            pass
        else:
            raise ValueError()

class IcmpTimeout():

    def __init__(self, num):
        self.val = int(num)
        if 1 <= self.val <= 10:
            pass
        else:
            raise ValueError()

# create main function
def main():
    # create ArgumentParser object
    parser = argparse.ArgumentParser(description="Daniel's ping script v0.1",\
     formatter_class=argparse.RawTextHelpFormatter)
    # add arguments
    parser.add_argument("-version", "--version", action="version", version="%(prog)s 0.1")
    parser.add_argument("-c", "--c" , help="\tNumber of packets", action="store"\
        , type=PktCnt, metavar=("(1-1000)"))
    parser.add_argument("-s", "--s" , help="\tpacketsize in bytes", action="store"\
        , type=PktSize, metavar=("(56-1500)"))
    parser.add_argument("-t", "--t" , help="\tttl for icmp packets", action="store"\
        , type=Ttl, metavar=("(1-255)"))    
    parser.add_argument("-w", "--w" , help="\ttimeout in seconds", action="store"\
        , type=IcmpTimeout, metavar=("(1-10)"))
    parser.add_argument("-ip", "--ip", help="\tip address", action="store"\
        , required=True)
    # parse command-line arguments
    parser.parse_args()

if __name__ == "__main__" and len(sys.argv) < 2:
    print("use the -h flag for help on this script")
else:
    main()

Hopefully this post gives you some more insight how to use Argparse for your Python scripts. A big thanks to my colleagues Mikkel Troest and Patrick Ogenstad for providing feedback and help on the previous code.

Python – Argparse Part II
Tagged on:             

One thought on “Python – Argparse Part II

  • February 5, 2017 at 6:16 am
    Permalink

    Very useful. Thanks 🙂

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *