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.
Very useful. Thanks 🙂