1. Command-line Arguments
  2. Command-line Options
  3. logging
  4. Homework

Command-line Arguments

  1. In the previous talk, we mentioned that it is not a good practice to hardcode your hostnames in the program, because you'll need to modify your program whenever you want to ping different hosts.
  2. Our solution, at that time, is to store those hostnames in a file. By doing so, we only need to modify the data file, but keep the program intact.
  3. However, imagine that you have 3 groups of hosts, whose names are stored in group1.txt, group2.txt, group3.txt, respectively. You want to ping them every week, every day, and every hour, respectively. Does this imply that you must modify your program to specify the data file name?
  4. Of course not. If some parameters change frequently, we usually require users to input that information at run-time. For example,
    #include <iostream>
    #include <fstream>
    #include <string>
    using std::cin;
    using std::endl;
    using std::ofstream;
    using std::string;
    
    int main()
    {
        ofstream fout("a.txt", std::ios::app);      // Append mode
        string user, fruit;
        cin >> user;
        cin >> fruit;
        fout << user << " buys " << fruit << endl;
        fout.close();
        return 0;
    } 
  5. Then when you run the program, you may input the username and the fruit he/she wants to buy.
  6. This implies that we can "echo" the username/fruit and pipe them to the program, as we did in at(1):
  7. If you want to type only "a.out" instead of "./a.out", please make sure your environment variable PATH include the current working directory ".".
  8. From time to time we need to pass information to a program, just like we need to pass arguments to a function. Linux (and also Windows) allows you to pass these information via Command-line Arguments.
    #include <iostream>
    #include <fstream>
    #include <string>
    using std::cin;
    using std::endl;
    using std::ofstream;
    using std::string;
    
    int main(int argc, char* argv[])
    {
        ofstream fout("a.txt", std::ios::app);      // Append mode
        string user, fruit;
        user = argv[1];
        fruit = argv[2];
        fout << user << " buys " << fruit << endl;
        fout.close();
        return 0;
    } 
  9. When you run "a.out alice apple", argv[1] refers to the first argument "alice", and argv[2] refers to the second argument "apple". Notably, argv[0] refers to the command "a.out".
  10. The argument "argc" refers to the "argument count", the total number of arguments. Try "a.out This is a book" with the following code:
    #include <iostream>
    using std::cout;
    using std::endl;
    
    int main(int argc, char* argv[])
    {
        for (int i=0; i<argc; ++i)
            cout << i << " - " << argv[i] << endl;
        return 0;
    } 
  11. Please try it.
  12. In Python, we have the same mechanism. You may retrieve the command-line argument from "sys.argv". All you need is to import the sys module.
    import sys
    for i in range(len(sys.argv)):
        print(f'{i} - {sys.argv[i]}') 
  13. Try "python3 a.py This is a book".
  14. Question: Why doesn't Python have "argc"?
  15. If you want your script to invoke the Python interpreter, find out the location of your Python interpreter (e.g., "type python3" will tell you it is at "/usr/bin/python3"), Then you can run "a.py This is a book".
  16. Exercise: Write a utility to print lines that match a string (similar to grep(1)).

Command-line Options

  1. The above approach requires the arguments order to be "user", "fruit". However, you must have seen some commands which allow you to supply information flexibly by command-line options:
  2. The argparse module provides convenient tools for this:
    #!/usr/bin/python3
    import argparse
    
    parser = argparse.ArgumentParser()
    
    parser.add_argument('--user', '-u', action='store',
            dest='user', default='alice')
    parser.add_argument('--fruit', '-f', action='store',
            default='apple')        # if you omit "dest", it will use the
                                    # option name ("fruit")
    
    # Parse arguments
    args = parser.parse_args()
    
    print(args.user, 'buys', args.fruit) 
  3. The argument can be any string. If you want to limit the choices, try
    parser.add_argument('-f', '--fruit', action='store',
            choices=('apple', 'banana'),
            default='apple')
  4. If you want a boolean flag, try
    parser.add_argument('--line-number', '-n',
    action='store_true',
            default=False) 
  5. You can retrieve the value with "args.line_number". (Dashes are converted to underlines.)
  6. It is also possible to use argparse for position arguments without flags. It can guarantee the number of arguments, and the valid choices of an argument.
    #!/usr/bin/python3
    import argparse
    
    parser = argparse.ArgumentParser()
    
    parser.add_argument('user')
    parser.add_argument('fruit', choices=('apple', 'banana'))
    
    # Parse arguments
    args = parser.parse_args()
    
    print(args.user, 'buys', args.fruit)
  7. Exercises: Enhance your "grep.py" to add an option "--line-number", "-n", which will prefix each line of output with the 1-based line number within its input file.

logging

  1. When you are debugging a program, you often need to inspect the values of several variables:
    import random
    a = random.randint(1, 99)
    print("[DEBUG] a =", a)
    
    b = random.randint(1, 99)
    print("[DEBUG] b =", b)
    
    c = a + b
    print("[DEBUG] c =", c)
    print(c)
  2. However, we usually want to "turn off" these debugging statements when a bug is solved, and "turn on" them when the next bug occurs.
  3. In C language we can do this conveniently with
    #ifdef _DEBUG
    #endif
  4. If Python does not provide this mechanism, it will be tedious to locate all these lines and add/remove remarks for each line.
  5. Fortunately, Python provide a logging module:
    import logging
    import sys
    
    LEVELS = {
        'debug': logging.DEBUG,     # 10
        'info': logging.INFO,       # 20
        'warning': logging.WARNING, # 30
        'error': logging.ERROR,     # 40
        'critical': logging.CRITICAL, # 50
    }
    
    if len(sys.argv) > 1:
        level_name = sys.argv[1]
        level = LEVELS.get(level_name, logging.NOTSET)  # 0, but default is WARNING
        logging.basicConfig(level=level)
    
    logging.debug('This is a debug message')
    logging.info('This is an info message')
    logging.warning('This is a warning message')
    logging.error('This is an error message')
    logging.critical('This is a critical error message')
  6. Exercise: Modify the "c = a + b" program at the beginning of this section and replace the "print" with "logging.debug".

Homework

  1. Modify your previous exercise about ICMP. Pass the host filename as a command-line argument to your program.
  2. If an option "-d" is present in the command line, show debug messages.
  3. Debug messages show the status of each host. For example,
    DEBUG:root:www.ncnu.edu.tw (163.22.12.5) RTT=0.75ms 0.0% Packet Loss
    DEBUG:root:moodle.ncnu.edu.tw (163.22.16.130) RTT=0.824ms 0.0% Packet Loss
  4. Output file is the same as in the previous exercise.