Inside the Einstein: The Disc, a TatungBytes course: XtalDOS listing the first practice disc image, on a rainbow sunburst
← Back to Modules
module
26

A Proper Command Line

Introduction

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.

What argparse Does

You describe the command line - the commands, and what each one takes - and argparse does the rest:

  • reads the words and checks there are the right number;
  • turns the ones you said are numbers into numbers, and says so plainly if they are not;
  • writes help: 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.

A main Function

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.

The code

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.

Change one thing

Predict each message before you run it:

  • python3 edisc.py sectors FILES.DSK two
  • python3 edisc.py lst FILES.DSK
  • python3 edisc.py on its own
  • python3 edisc.py dump FILES.DSK 2 - one number short

Then 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.)

When it goes wrong

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.

Summary

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.

Next

S27, Where You Go From Here.

Get the Newsletter

New guides, disk images and community finds, roughly once a quarter. No spam, we promise, this isn't Tatung's marketing department.
Your subscription could not be saved. Please try again.
Your subscription has been successful.

Newsletter

Subscribe to our newsletter and stay updated.

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.