
edisc.py reads its command line by hand: word 1 is the command, word 2
the image, words 3 and 4 whatever that command wants. Leave a word out and
it stops with IndexError; type a word for a number and it stops with
ValueError; and nothing tells you what the commands are. Python comes
with a module that does all of this properly, argparse. This section
replaces the hand-made command line with it.
You describe the command line - the commands, and what each one takes -
and argparse does the rest:
python3 edisc.py -h lists every command, and python3
edisc.py add -h explains one.Each command is a sub-command, made with add_parser, and each word a
command takes is an argument, made with add_argument. type=int
makes a word a number. nargs="?" makes a word optional; if it is left
out, its value is None.
parse_args() reads the command line and gives back one value, args,
with each word under its name: args.image, args.track, args.name.
args.command says which command was given.
The new command-line code is longer, so it goes in a function of its own,
main, and the last line of the program calls it. Inside main is a small
function, command, that makes a sub-command that takes an image - every
one of them does - so each command needs only one line to describe.
args.name or os.path.basename(args.path) uses or: if args.name is
None (no disc name given), the result is the part after or.
At the very top of the file, a description of the program - text in three
sets of quotes, which Python keeps as the program's own description - and
the new import:
"""edisc.py - list, read and write Tatung Einstein .dsk disc images.
Built section by section in the TatungBytes course "Inside the
Einstein: The Disc". One file, standard library only, Python 3.9 or later.
"""
import argparse
import os
import sys
Then replace everything from command = sys.argv[1] to the end of the
file with:
def main():
parser = argparse.ArgumentParser(
prog="edisc.py",
description="List, read and write Tatung Einstein .dsk disc images.")
commands = parser.add_subparsers(dest="command", required=True)
def command(name, help_text):
sub = commands.add_parser(name, help=help_text, description=help_text)
sub.add_argument("image", help="the .dsk disc image")
return sub
command("info", "check an image's header and size")
command("header", "dump the image's 256-byte disk-info header")
command("tracks", "list every track's header")
command("sectors", "list one track's sectors").add_argument("track", type=int)
sub = command("dump", "dump one sector")
sub.add_argument("track", type=int)
sub.add_argument("sector", type=int)
command("directory", "dump the directory slots in use")
command("entries", "decode every directory entry in use")
command("list", "list the files on the disc")
command("block", "show where a block is, and its first bytes").add_argument("block", type=int)
command("type", "show a text file").add_argument("name")
sub = command("extract", "copy a file off the disc")
sub.add_argument("name")
sub.add_argument("out", nargs="?", help="the file to write (default: the disc name)")
command("free", "show the free blocks and directory slots")
command("create", "make a new, blank disc image")
sub = command("add", "copy a file onto the disc")
sub.add_argument("path", help="the file on your computer")
sub.add_argument("name", nargs="?", help="the name on the disc (default: the file's own)")
sub = command("rename", "rename a file on the disc")
sub.add_argument("old")
sub.add_argument("new")
command("remove", "delete a file from the disc").add_argument("name")
args = parser.parse_args()
if args.command == "create":
create(args.image)
return
disc = read_image(args.image)
if args.command == "info":
info(args.image, disc)
elif args.command == "header":
dump(disc, 0, 256)
elif args.command == "tracks":
tracks(disc)
elif args.command == "sectors":
sectors(disc, args.track)
elif args.command == "dump":
dump_sector(disc, args.track, args.sector)
elif args.command == "directory":
show_directory(disc)
elif args.command == "entries":
show_entries(disc)
elif args.command == "list":
list_files(disc)
elif args.command == "block":
show_block(disc, args.block)
elif args.command == "type":
type_file(disc, args.name)
elif args.command == "extract":
extract(disc, args.name, args.out)
elif args.command == "free":
show_free(disc)
else:
disc = bytearray(disc)
if args.command == "add":
add_file(disc, args.path, args.name or os.path.basename(args.path))
elif args.command == "rename":
rename_file(disc, args.old, args.new)
elif args.command == "remove":
remove_file(disc, args.name)
save_image(args.image, disc)
main()
command("sectors", ...).add_argument("track", type=int) makes the
sub-command and adds its word in one line, because command gives back
the sub-command it made. return in main with no value simply leaves
the function - for create, which has no image to read. The three writing
commands share their last steps - make the image changeable, change it,
save it - in the final else.
This is the finished tool: s26-edisc.py in the course download, and
Appendix III.
Run it
python3 edisc.py -h
What you should see (Python 3.13; 3.9 says optional arguments: where
3.13 says options:, and breaks the long first line in a different place)
usage: edisc.py [-h]
{info,header,tracks,sectors,dump,directory,entries,list,block,type,extract,free,create,add,rename,remove} ...
List, read and write Tatung Einstein .dsk disc images.
positional arguments:
{info,header,tracks,sectors,dump,directory,entries,list,block,type,extract,free,create,add,rename,remove}
info check an image's header and size
header dump the image's 256-byte disk-info header
tracks list every track's header
sectors list one track's sectors
dump dump one sector
directory dump the directory slots in use
entries decode every directory entry in use
list list the files on the disc
block show where a block is, and its first bytes
type show a text file
extract copy a file off the disc
free show the free blocks and directory slots
create make a new, blank disc image
add copy a file onto the disc
rename rename a file on the disc
remove delete a file from the disc
options:
-h, --help show this help message and exit
And help for one command:
python3 edisc.py add -h
usage: edisc.py add [-h] image path [name]
copy a file onto the disc
positional arguments:
image the .dsk disc image
path the file on your computer
name the name on the disc (default: the file's own)
options:
-h, --help show this help message and exit
Square brackets in a usage line mean "optional". Every command works as
before - python3 edisc.py list FILES.DSK gives exactly S14's list.
Predict each message before you run it:
python3 edisc.py sectors FILES.DSK twopython3 edisc.py lst FILES.DSKpython3 edisc.py on its ownpython3 edisc.py dump FILES.DSK 2 - one number shortThen add a new sub-command of your own: count, which prints the number
of files on a disc. (len of the list of names in list_files is the
number - you could make a function that works it out and returns it.)
argparse's own messages, as Python 3.9 prints them:
| Message | Cause |
|---|---|
edisc.py sectors: error: argument track: invalid int value: 'two' |
A word that must be a number is not |
edisc.py: error: argument command: invalid choice: 'lst' (choose from 'info', 'header', ... |
No such command (3.13 lists them without quotes) |
edisc.py: error: the following arguments are required: command |
No command at all |
Each comes after a usage: line showing what the command expects.
argparse reads the command line from a description of it: sub-commands
with add_parser, their words with add_argument (type=int for
numbers, nargs="?" for optional ones), and parse_args to get them. It
checks the words, explains its errors, and writes the help. edisc.py is
finished: one file, sixteen commands, no module outside Python itself.
S27, Where You Go From Here.
This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.