Python: Kommandozeilen­interpretation

Vitalij Mast · 22 März 2020

Wer Python verwendet, der will früher oder später seine Anwendung in ein Kommandozeilen-Tool umwandeln um das Verhalten der Anwendung über unterschiedliche Parameter zu steuern. In diesem Artikel wird eine einfache HexDump Anwendung mit Kommandozeilenparameter erweitert. Verwendet wird Python in der Version 3.

Das folgende Listing zeigt eine HexDump Anwendung namens hexdump.py welche sich momentan einfach selbst in einer Hexadezimal Ansicht darstellt:

import math
def main():
# options variables
arg_file_name = "hexdump.py"
arg_address_show = True
arg_address_length = 4
arg_address_hex_prefix = "0x"
arg_dump_column_count = 16
arg_hex_dump_show = True
arg_ascii_dump_show = True
# load file data
with open(arg_file_name, "rb") as fd:
raw_data = fd.read()
# support variables
printable = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~"
out_address_fill = 0
out_data_hex_fill = 0
# pre process argument variables
if arg_address_length == 0:
arg_address_length = int(math.log(len(raw_data), 256)) + 1
if arg_address_show:
out_address_fill = (len(arg_address_hex_prefix) + 2 * arg_address_length + 1)
if arg_dump_column_count <= 0:
arg_dump_column_count = 1
if arg_hex_dump_show:
out_data_hex_fill = arg_dump_column_count * 3
print("Hex view of file '%s' with size %d byte(s).\n" % (arg_file_name, len(raw_data)))
# process, format and print data
for i in range(0, len(raw_data), arg_dump_column_count):
out_address = out_data_hex = out_data_chr = ""
if arg_address_show:
out_address = "%s%0*X" % (arg_address_hex_prefix, 2 * arg_address_length, i)
if arg_hex_dump_show:
out_data_hex = " ".join("%02x" % c for c in raw_data[i:i + arg_dump_column_count])
if arg_ascii_dump_show:
out_data_chr = "".join(chr(c) if chr(c) in printable else "." for c in raw_data[i:i + arg_dump_column_count])
print("%- *s%- *s%s" % (out_address_fill, out_address, out_data_hex_fill, out_data_hex, out_data_chr))
if __name__ == '__main__':
main()

Hierbei finden sich bereits vorbereitete Variablen mit dem Prefix arg_. Diese sollen über die Kommandozeilen befüllt werden. Python bringt hierfür eine Bibliothek namens argparse mit.

Als Implementierungsbeispiel soll das folgende Listing dienen:

import math
import argparse
def main():
# create parser instance
parser = argparse.ArgumentParser(description='Python Hexdump',
formatter_class=argparse.RawTextHelpFormatter)
# remove default action group
parser._action_groups.pop()
# create custom action groups and assing argument informations
required = parser.add_argument_group('required arguments')
required.add_argument("-i", "--input", type=str, required=True,
help="Input file to show hexadecimal content for.")
optional = parser.add_argument_group('optional arguments')
optional.add_argument("-w", "--width", type=int, default=4,
help="Address display width in bytes.\nDefault is 4.")
optional.add_argument("-p", "--prefix", type=str, default="0x",
help="Address prefix.\nDefault is '0x'.")
optional.add_argument("-n", "--noaddress", default=False, action="store_true",
help="Do not show any addresses.")
optional.add_argument("-c", "--columns", type=int, default=16,
help="Number of columns to show per row.")
# parse input arguments
args = parser.parse_args()
# options variables
arg_file_name = args.input
arg_address_show = not args.noaddress
arg_address_length = args.width
arg_address_hex_prefix = args.prefix
arg_dump_column_count = args.columns
# ...

Hier wird zunächst eine ArgumentParser Instanz erstellt (Zeile 6) und die Standardgruppe entfernt (Zeile 10). Persöhnlich mag ich es nicht wenn die Standardgruppe mit dem Hilfstext für die Hilfe angezeigt wird.

Die ArgumentParser Klasse bietet die Möglichkeit Parametergruppen zu erstellen welche, wenn das Programm mit dem Argument -h Aufgerufen wird, alle Befehlsbeschreibungen mit entsprechenden Befehlsgruppenüberschriften versieht und deren Hilfetexte ausgibt.

Weiterführend werden zwei Gruppen angelegt mit benötigten (Zeile 13..15) sowie optionalen Argumenten (Zeile 17..25) angelegt und schliesslich in Zeile 28 verarbeitet. Die Klasse nimmt sich dabei die Eingabeargument von der Befehlszeile.

Interessanterweise erhält man im Erfolgsfall eine Instanz mit Eigenschaften und Werten zurück bei dennen die Eigenschaften die Bezeichner der bei der Argumentdefinition übergebenen zweiten Parameter annehmen. So wird zum Beisüpiel aus "--width" » args.width.

Diese Tatsache wird in Zeile 31 bis 37 genutzt um die vorherigen Konstantendefinitionen mit Benutzerwerten zu füllen.

Die obige Implementierung erzeugt, wenn mit hexdump.py -h aufgerufen, die folgende Aussgabe:

usage: hexdump.py [-h] -i INPUT [-w WIDTH] [-p PREFIX] [-n] [-c COLUMNS]

Python Hexdump

required arguments:
  -i INPUT, --input INPUT
                        Input file to show hexadecimal content for.

optional arguments:
  -w WIDTH, --width WIDTH
                        Address display width in bytes.
                        Default is 4.
  -p PREFIX, --prefix PREFIX
                        Address prefix.
                        Default is '0x'.
  -n, --noaddress       Do not show any addresses.
  -c COLUMNS, --columns COLUMNS
                        Number of columns to show per row.

Bei einer fehlerhaften eingabe wie hexdump.py -o test wird eine entsprechende Fehlernachricht generiert:

usage: hexdump.py [-h] -i INPUT [-w WIDTH] [-p PREFIX] [-n] [-c COLUMNS]
hexdump.py: error: unrecognized arguments: -o xyz

Dem aufmerksamen Leser ist nun sicherlich aufgefallen das die Felder arg_hex_dump_show und arg_ascii_dump_show noch gar nicht abgedeckt sind.

Hierfür habe ich mir etwas spezieles überlegt, da es keinen Sinn macht weder die HEX noch die ASCII anzuzeigen erlaube ich nur eine von beiden oder beide aber nicht keine. Hierfür bietet sich ein Enumdefinition an:

import enum
class HexViewTypeEnum(enum.IntEnum):
BOTH = 1
HEX = 2
ASCII = 3
def __str__(self):
return self.name.lower()
def __repr__(self):
return str(self)
def __call__(self):
try:
return HexViewTypeEnum[self.upper()]
except KeyError:
return self

Diese Klasse kann dann als Argumentationdefinition für argparse verwendet werden, sodass die Werte both, hex und ascii als Eingabeparameter für das Argument -v akzeptiert werden.

optional.add_argument("-v", "--view", type=HexViewTypeEnum.__call__, default=HexViewTypeEnum.BOTH,
choices=list(HexViewTypeEnum),
help="Hex and or Ascii view type to show.")
# ...
arg_hex_dump_show = (args.view != HexViewTypeEnum.ASCII)
arg_ascii_dump_show = (args.view != HexViewTypeEnum.HEX)

Ein Aufruf von hexdump.py -i hexdump.py -w 8 -c 8 -p $ -v hex solte zur folgende Ausgabe führen:

Hex view of file 'hexdump.py' with size 3585 byte(s).

$0000000000000000 69 6d 70 6f 72 74 20 6d 
$0000000000000008 61 74 68 0d 0a 69 6d 70 
$0000000000000010 6f 72 74 20 65 6e 75 6d 
$0000000000000018 0d 0a 69 6d 70 6f 72 74 
$0000000000000020 20 61 72 67 70 61 72 73 
$0000000000000028 65 0d 0a 0d 0a 0d 0a 63 
$0000000000000030 6c 61 73 73 20 48 65 78 
$0000000000000038 56 69 65 77 54 79 70 65 
$0000000000000040 45 6e 75 6d 28 65 6e 75 
$0000000000000048 6d 2e 49 6e 74 45 6e 75 
$0000000000000050 6d 29 3a 0d 0a 20 20 20 

Damit ist dies nun auch das Ende dieses Artikels. Ich hoffe diese kurze Anleitung konnte einen Einblick in die Benutzung und Handhabung von argparse geben.

Twitter, Facebook