I’m fiddling around a bit with Python. I’m planning to write a little script that pings a host or hosts and logs it to a file. Should be pretty basic but it’s good practice for me to take user input, read from a file, write to a file and so on. I wanted to support having arguments in the script to do things like a continuous ping or ping with x number of packets and then stop. To help the user pick the arguments I planned to do a little help text that printed out the arguments. Something like:

# Import sys
import sys
# Function for printing help text
def help_text():
    print "Daniel's awesome ping script."
    print "\n"
    print "-c    The number of packets that should be sent(integer)"
    print "-t    Timeout in seconds to wait for ICMP Echo Reply"
    print "-i    Run continuous ping"

# Check if user has input any options otherwise print help text
if len(sys.argv) < 2:
    help_text()

The problem with doing this manually is that you have to parse the arguments and build the logic yourself. For example the script should not allow someone to input both -c and -i since those arguments should be mutually exclusive. Either you run a continuous ping or you run it for x number of packets. It's kind of tedious to build that logic yourself than to focus that time and effort on building the core part of the script.

There is a module in Python called Argparse. The Argparse module's sole purpose in life is to help build this logic in a simple way so that users can build scripts that require arguments and make arguments mandatory or optional and mutually exclusive etc.

The first step to start using this module is to import it into Python.

# Import Argparse
import argparse

We must then create an ArgumentParser object.

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

Then we tell this object to parse the arguments. Arguments will be converted to the correct type and the appropriate action will be taken.

parser.parse_args()

This is the code we have so far.

#!/usr/bin/env python
# Import Argparse
import argparse
# Create main function
def main():
    # Create ArgumentParser object
    parser = argparse.ArgumentParser(description="Daniel's ping script v0.1")
    # Parse arguments              
    parser.parse_args()

# Run script if not imported as module
if __name__ == "__main__":
    main()

If we run the script without any arguments there is nothing output but if we run it with -h we get my description and that -h should be used to print this help.

[email protected]:~$ python pingscript.py 
[email protected]:~$ python pingscript.py -h
usage: pingscript.py [-h]

Daniel's ping script v0.1

optional arguments:
  -h, --help  show this help message and exit

Now that this first part is built we want to add arguments to the script and make some of them required. Arguments in Argparse can either be positional or optional. A positional argument is something that is expected at a certain place in the command such as using "copy src dest" where it's expected that the source is specified first and then the destination. It's not possible to leave out any arguments. Arguments can also be optional meaning that they may be included or not.

Arguments in Argparse are created by using the add_argument() method.

parser.add_argument("-f", "--foo")

This is an optional argument. The following is a positional argument.

parser.add_argument("bar")

The optional argument is detected by the - or -- prefix.

Different actions can then be taken on the arguments where the default action is to store the value. It's also possible to store constants, print the version of the program and print the help text. This link shows all of the actions that can be taken.

Let's add the version of the script to Argparse.

parser.add_argument("-version", "--version", action="version", version="%(prog)s 0.1")

The %(prog)s will get the script name from sys.argv[0]. Let's see if this works.

[email protected]:~$ python pingscript.py --version
pingscript.py 0.1

Let us then add some arguments to the script. The first argument will be -c for count, which is the number of packets to be sent.

parser.add_argument("-c", "--c" , help="number of packets", action="store", type=int)

Argparse will by default store the arguments as strings but we want to store this value as an integer. Let's check that we can use the argument and get help for it.

[email protected]:~$ python pingscript.py -c 10
[email protected]:~$ python pingscript.py -h
usage: pingscript.py [-h] [-version] [-c C]

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 C, --c C          number of packets

Now let's add some more arguments. The following arguments should be added.

-s packetsize in bytes
-t ttl
-w timeout in seconds

parser.add_argument("-s", "--s" , help="packetsize in bytes", action="store", type=int)
parser.add_argument("-t", "--t" , help="ttl for icmp packets", action="store", type=int)
parser.add_argument("-w", "--w" , help="timeout in seconds", action="store", type=int)

All optional arguments are stored as integers. Let's see if this works so far.

[email protected]:~$ python pingscript.py -c 10 -s 100 -t 20 -w 2
[email protected]:~$ 
[email protected]:~$ python pingscript.py -h
usage: pingscript.py [-h] [-version] [-c C] [-s S] [-t T] [-w W]

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 C, --c C          number of packets
  -s S, --s S          packetsize in bytes
  -t T, --t T          ttl for icmp packets
  -w W, --w W          timeout in seconds

This looks good so far. We aren't doing any checking to see if the arguments are reasonable though. What if the user tries to set the TTL to 1000?

[email protected]:~$ python pingscript.py -t 1000
[email protected]:~$ 

The script is accepting this obviously not valid value. Let's restrict the values that the user can input. This is done by using the choices container.

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

Running this produces quite an ugly output though...

[email protected]:~$ python pingscript.py -t 1000
usage: pingscript.py [-h] [-version] [-c C] [-s S]
                     [-t {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254}]
                     [-w W]
pingscript.py: error: argument -t/--t: invalid choice: 1000 (choose from 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254)

Also the help text is not very pretty...

[email protected]:~$ python pingscript.py -h
usage: pingscript.py [-h] [-version] [-c C] [-s S]
                     [-t {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254}]
                     [-w W]

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 C, --c C           Number of packets
  -s S, --s S           packetsize in bytes
  -t {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254}, --t {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254}
                        ttl for icmp packets
  -w W, --w W           timeout in seconds

We can change the help text by using the metavar value which changes the displayed name of the argument.

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

This looks better now.

[email protected]:~$ python pingscript.py -h
usage: pingscript.py [-h] [-version] [-c C] [-s S] [-t 1-255] [-w W]

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 C, --c C           Number of packets
  -s S, --s S           packetsize in bytes
  -t (1-255), --t (1-255)
                        ttl for icmp packets
  -w W, --w W           timeout in seconds

Unfortunately this doesn't help with the output from when the user enters a faulty value.

[email protected]:~$ python pingscript.py -t 1000
usage: pingscript.py [-h] [-version] [-c C] [-s S] [-t 1-255] [-w W]
pingscript.py: error: argument -t/--t: invalid choice: 1000 (choose from 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254)

Unfortunately I haven't found a solution for changing this error message yet. If you know how to, please post in the comments.

It's also possible to supply default values for the arguments. Something like.

parser.add_argument("-t", "--t" , help="ttl for icmp packets", action="store", type=int, choices=xrange(1, 255), metavar=("(1-255)"), default=100)

It's also possible to make an argument required. For a ping script we would expect an IP to be input by the user.

parser.add_argument("-ip", "--ip", help="ip address", action="store", required=True)

If we then don't input an IP we will get this message.

[email protected]:~$ python pingscript.py
usage: pingscript.py [-h] [-version] [-c C] [-s S] [-t 1-255] [-w W] -ip IP
pingscript.py: error: argument -ip/--ip is required

Let's try it again with an IP address.

[email protected]:~$ python pingscript.py -ip 8.8.8.8
[email protected]:~$

This is the final Argparse code so far.

#!/usr/bin/env python
# Import Argparse
import argparse
# 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)
    parser.add_argument("-s", "--s" , help="packetsize in bytes", action="store", type=int)
    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)
    parser.add_argument("-ip", "--ip", help="ip address", action="store", required=True)
    # parse command-line arguments
    parser.parse_args()

if __name__ == "__main__":
    main()

As a final touch let's add that we print a message to use the -h flag if the user runs the script without any arguments. To do this we must first import sys.

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

If the length of sys.argv is less than two then the user didn't enter any arguments and we print the text above. Otherwise run the main function.

Finally let's print the arguments from the script to see what values have been stored.

print parser.parse_args()

This is then the output we get.

[email protected]:~$ python pingscript.py -c 100 -s 100 -t 100 -w 2 -ip 8.8.8.8
Namespace(c=100, ip='8.8.8.8', s=100, t=100, w=2)

The final Argparse code here:

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

I hope this post has given you a good introduction into what can be done with Argparse. I'm trying to find out if there's a better way to display the error message with Argparse in combination with using choices. The code in this post is not formatted according to PEP standards so some lines should be split in the final version of the code.

Python – Introduction to Argparse
Tagged on:         

Leave a Reply

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

%d bloggers like this: