Python CLI application
In this section, you'll see how to implement a CLI application using Python features, instead of relying on shell features. First, you'll learn how to work with command line arguments using the sys
module. Followed by argparse
module, which is specifically designed for creating CLI applications.
sys.argv
Command line arguments passed when executing a Python program can be accessed using the sys.argv
list. The first element (index 0
) contains the name of the Python script or -c
or empty string, depending on how the interpreter was called. See docs.python: sys.argv for details.
Rest of the elements will have the command line arguments, if any were passed along the script to be executed. The data type of sys.argv
elements is str
class. The eval()
function allows you to execute a string as a Python instruction. Here's an example:
$ python3.9 -c 'import sys; print(eval(sys.argv[1]))' '23 ** 2'
529
# bash shortcut
$ pc() { python3.9 -c 'import sys; print(eval(sys.argv[1]))' "$1" ; }
$ pc '23 ** 2'
529
$ pc '0x2F'
47
Using
eval()
function isn't recommended if the input passed to it isn't under your control, for example an input typed by a user from a website application. The arbitrary code execution issue would apply to thebash
shortcuts seen in previous section as well, because the input argument is interpreted without any sanity check.However, for the purpose of this calculator project, it is assumed that you are the sole user of the application. See stackoverflow: evaluating a mathematical expression for more details about the dangers of using
eval()
function and alternate ways to evaluate a string as mathematical expression.
argparse
Quoting from docs.python: argparse:
The
argparse
module makes it easy to write user-friendly command-line interfaces. The program defines what arguments it requires, andargparse
will figure out how to parse those out ofsys.argv
. Theargparse
module also automatically generates help and usage messages and issues errors when users give the program invalid arguments.
argparse initialization
If this is your first time using the argparse
module, it is recommended to understand the initialization instructions and see the effect they provide by default. Quoting from docs.python: argparse:
The
ArgumentParser
object will hold all the information necessary to parse the command line into Python data types.
ArgumentParser
parses arguments through theparse_args()
method. This will inspect the command line, convert each argument to the appropriate type and then invoke the appropriate action.
# arg_help.py
import argparse
parser = argparse.ArgumentParser()
args = parser.parse_args()
The documentation for the CLI application is generated automatically based on the information passed to the parser. You can use help options (which is added automatically too) to view the content, as shown below:
$ python3.9 arg_help.py -h
usage: arg_help.py [-h]
optional arguments:
-h, --help show this help message and exit
In addition, any option or argument that are not defined will generate an error.
$ python3.9 arg_help.py -c
usage: arg_help.py [-h]
arg_help.py: error: unrecognized arguments: -c
$ python3.9 arg_help.py '2 + 3'
usage: arg_help.py [-h]
arg_help.py: error: unrecognized arguments: 2 + 3
A required argument wasn't declared in this program, so there's no error for the below usage.
$ python3.9 arg_help.py
See also docs.python: Argparse Tutorial.
Accepting an input expression
# single_arg.py
import argparse, sys
parser = argparse.ArgumentParser()
parser.add_argument('ip_expr',
help="input expression to be evaluated")
args = parser.parse_args()
try:
result = eval(args.ip_expr)
print(result)
except (NameError, SyntaxError):
sys.exit("Error: Not a valid input expression")
The add_argument()
method allows you to add details about an option/argument for the CLI application. The first parameter names an argument or options (starts with -
). The optional help
parameter lets you add documentation for that particular option/argument. See docs.python: add_argument for documentation and details about other parameters.
The value for ip_expr
passed by the user will be available as an attribute of args
, which stores the object returned by the parse_args()
method. The default data type for arguments is str
, which is good enough here for eval()
.
The help documentation for this script is shown below:
$ python3.9 single_arg.py -h
usage: single_arg.py [-h] ip_expr
positional arguments:
ip_expr input expression to be evaluated
optional arguments:
-h, --help show this help message and exit
Note that the script uses try-except
block to give user friendly feedback for some of the common issues. Passing a string to sys.exit()
gets printed to the stderr
stream and sets the exit status as 1
to indicate something has gone wrong. See docs.python: sys.exit for documentation. Here's some usage examples:
$ python3.9 single_arg.py '40 + 2'
42
# if no argument is passed to the script
$ python3.9 single_arg.py
usage: single_arg.py [-h] ip_expr
single_arg.py: error: the following arguments are required: ip_expr
$ echo $?
2
# SyntaxError
$ python3.9 single_arg.py '5 \ 2'
Error: Not a valid input expression
$ echo $?
1
# NameError
$ python3.9 single_arg.py '5 + num'
Error: Not a valid input expression
Adding optional flags
To add an option, use -<char>
for short option and --<name>
for long option. You can add both as well, '-v', '--verbose'
for example. If you use both short and long options, the attribute name will be whichever option is the latest. For the CLI application, five short options have been added, as shown below.
# options.py
import argparse, sys
parser = argparse.ArgumentParser()
parser.add_argument('ip_expr',
help="input expression to be evaluated")
parser.add_argument('-f', type=int,
help="specify floating point output precision")
parser.add_argument('-b', action="store_true",
help="output in binary format")
parser.add_argument('-o', action="store_true",
help="output in octal format")
parser.add_argument('-x', action="store_true",
help="output in hexadecimal format")
parser.add_argument('-v', action="store_true",
help="verbose mode, shows both input and output")
args = parser.parse_args()
try:
result = eval(args.ip_expr)
if args.f:
result = f'{result:.{args.f}f}'
elif args.b:
result = f'{int(result):#b}'
elif args.o:
result = f'{int(result):#o}'
elif args.x:
result = f'{int(result):#x}'
if args.v:
print(f'{args.ip_expr} = {result}')
else:
print(result)
except (NameError, SyntaxError):
sys.exit("Error: Not a valid input expression")
The type
parameter for add_argument()
method allows you to specify what data type should be applied for that option. The -f
option is used here to set the precision for floating-point output. The code doesn't actually check if the output is floating-point type, that is left as an exercise for you.
The -b
, -o
, -x
and -v
options are intended as boolean data types. Using action="store_true"
indicates that the associated attribute should be set to False
as their default value. When the option is used from the command line, their value will be set to True
. The -b
, -o
and -x
options are used here to get the output in binary, octal and hexadecimal formats respectively. The -v
option will print both the input expression and the evaluated result.
The help documentation for this script is shown below. By default, uppercase of the option name will be used to describe the value expected for that option. Which is why you see -f F
here. You can use metavar='precision'
to change it to -f precision
instead.
$ python3.9 options.py -h
usage: options.py [-h] [-f F] [-b] [-o] [-x] [-v] ip_expr
positional arguments:
ip_expr input expression to be evaluated
optional arguments:
-h, --help show this help message and exit
-f F specify floating point output precision
-b output in binary format
-o output in octal format
-x output in hexadecimal format
-v verbose mode, shows both input and output
Here's some usage examples:
$ python3.9 options.py '22 / 7'
3.142857142857143
$ python3.9 options.py -f3 '22 / 7'
3.143
$ python3.9 options.py -f2 '32 ** 2'
1024.00
$ python3.9 options.py -bv '543 * 2'
543 * 2 = 0b10000111110
$ python3.9 options.py -x '0x1F * 0xA'
0x136
$ python3.9 options.py -o '0xdeadbeef'
0o33653337357
Since -f
option expects an int
value, you'll get an error if you don't pass a value or if the value passed isn't a valid integer.
$ python3.9 options.py -fa '22 / 7'
usage: options.py [-h] [-f F] [-b] [-o] [-x] [-v] ip_expr
options.py: error: argument -f: invalid int value: 'a'
$ python3.9 options.py -f
usage: options.py [-h] [-f F] [-b] [-o] [-x] [-v] ip_expr
options.py: error: argument -f: expected one argument
$ python3.9 options.py -f '22 / 7'
usage: options.py [-h] [-f F] [-b] [-o] [-x] [-v] ip_expr
options.py: error: argument -f: invalid int value: '22 / 7'
$ python3.9 options.py -f '22'
usage: options.py [-h] [-f F] [-b] [-o] [-x] [-v] ip_expr
options.py: error: the following arguments are required: ip_expr
Accepting stdin
The final feature to be added is the ability to accept both stdin
and argument value as the input expression. The sys.stdin
filehandle can be used to read stdin
data. The modified script is shown below.
# py_calc.py
import argparse, sys
parser = argparse.ArgumentParser()
parser.add_argument('ip_expr', nargs='?',
help="input expression to be evaluated")
parser.add_argument('-f', type=int,
help="specify floating point output precision")
parser.add_argument('-b', action="store_true",
help="output in binary format")
parser.add_argument('-o', action="store_true",
help="output in octal format")
parser.add_argument('-x', action="store_true",
help="output in hexadecimal format")
parser.add_argument('-v', action="store_true",
help="verbose mode, shows both input and output")
args = parser.parse_args()
if args.ip_expr in (None, '-'):
args.ip_expr = sys.stdin.readline().strip()
try:
result = eval(args.ip_expr)
if args.f:
result = f'{result:.{args.f}f}'
elif args.b:
result = f'{int(result):#b}'
elif args.o:
result = f'{int(result):#o}'
elif args.x:
result = f'{int(result):#x}'
if args.v:
print(f'{args.ip_expr} = {result}')
else:
print(result)
except (NameError, SyntaxError):
sys.exit("Error: Not a valid input expression")
The nargs
parameter allows to specify how many arguments can be accepted with a single action. You can use an integer value to get that many arguments as a list or use specific regular expression like metacharacters to indicate varying number of arguments. The ip_expr
argument is made optional here by setting nargs
to ?
.
If ip_expr
isn't passed as an argument by the user, the attribute will get None
as the value. The -
character is often used to indicate stdin
as the input data. So, if ip_expr
is None
or -
, the code will try to read a line from stdin
as the input expression. The strip()
string method is applied to the stdin
data mainly to prevent newline from messing up the output for -v
option. Rest of the code is the same as seen before.
The help documentation for this script is shown below. The only difference is that the input expression is now optional as indicated by [ip_expr]
.
$ python3.9 py_calc.py -h
usage: py_calc.py [-h] [-f F] [-b] [-o] [-x] [-v] [ip_expr]
positional arguments:
ip_expr input expression to be evaluated
optional arguments:
-h, --help show this help message and exit
-f F specify floating point output precision
-b output in binary format
-o output in octal format
-x output in hexadecimal format
-v verbose mode, shows both input and output
Here's some usage examples:
# stdin from output of another command
$ echo '40 + 2' | python3.9 py_calc.py
42
# manual stdin data after pressing enter key
$ python3.9 py_calc.py
43 / 5
8.6
# strip() will remove whitespace from start/end of string
$ echo ' 0b101 + 3' | python3.9 py_calc.py -vx
0b101 + 3 = 0x8
$ echo '0b101 + 3' | python3.9 py_calc.py -vx -
0b101 + 3 = 0x8
# expression passed as argument, works the same as seen before
$ python3.9 py_calc.py '5 % 2'
1
Shortcuts
To simplify calling the Python CLI calculator, you can create an alias or an executable Python script.
Use absolute path of the script to create the alias and add it to .bashrc
, so that it will work from any working directory. The path used below would differ for you.
alias pc='python3.9 /home/learnbyexample/python_projs/py_calc.py'
To create an executable, you'll have to first add a shebang as the first line of the Python script. You can use type
built-in command to get the path of the Python interpreter.
$ type python3.9
python3.9 is /usr/local/bin/python3.9
So, the shebang
for this case will be #!/usr/local/bin/python3.9
. After adding execute permission, copy the file to one of the PATH
directories. I have ~/cbin/
as one of the paths. See unix.stackexchange: How to correctly modify PATH variable for more details about the PATH
environment variable.
$ chmod +x py_calc.py
$ cp py_calc.py ~/cbin/pc
$ pc '40 + 2'
42
With that, the lessons for this project comes to an end. Solve the practice problems given in the exercises section to test your understanding.