Repository navigation
Examples
- UNIX-like shell is assumed except where noted
- a2kit must be installed and in the path
- Some examples require Python 3.8 or higher be installed and in the path
The simplest way to shuttle files around is with the smart copy. It is designed to give you what you most likely expect with minimal typing.
- extract file from disk image
a2kit cp /path/to/dimg.woz/basic.system /path/to/destination/directory
- extract matching files from disk image
a2kit cp "/path/to/dimg.woz/*.system" .- using dot-notation to specify current directory as target
- double-quotes suppress the bash glob expansion so that a2kit receives the
*
- recursive file extraction
a2kit cp "/path/to/dimg.woz/apps/**" .- all files go to the same destination directory
- write matching files to disk image
a2kit cp /path/to/*.txt /path/to/myimg.2mg- in this case do not suppress the shell's glob expansion
- handle the load address
a2kit cp -a 768 /path/to/mybinary /path/to/myimg.2mg- only for host-to-image with some apple files
- file extensions and Apple disks
a2kit cp /path/to/myfile.txt ./myimg.2mga2kit cp /path/to/myfile.txt ./myimg.2mg/myfile- these will do the same thing because the smart copy omits some extensions on Apple disks automatically
Actually, some of the quotes in the above examples are not strictly necessary, but they may help clarify which entity supplies the expansion.
You can use the pipeline more effectively if you know your shell. As an example, suppose you want to delete all .txt files on a disk image. Here is how to do it in PowerShell:
a2kit glob -d disk.td0 -f *.txt | ConvertFrom-Json | % {a2kit del -d disk.td0 -f $_}For something more interesting suppose you want to extract all .txt files from all IMD images in the working directory. The PowerShell command for this would be
a2kit cp ((ls *.imd).Name | % {$_+'/*.txt'}) .Notice how this combines a shell expansion with an a2kit expansion.
This is a shell session to create a ProDOS disk with an Applesoft greeting program. For the moment, suppose that the Applesoft source code startup.bas and the file images for PRODOS and BASIC.SYSTEM are already in the working directory.
# first create the disk image
a2kit mkdsk -o prodos -t woz2 -v demo -d demo.woz
# put the startup files on the new disk image
a2kit cp prodos.json basic.system.json startup.bas demo.wozThe source code startup.bas can be created in any text editor. As an example we might have
10 home
20 print "hello from my new disk"The file images can be obtained from another disk image containing the actual files. For example, if you already have a bootable disk image legit.woz, you can create the PRODOS file image like this:
a2kit get -f prodos -t any -d /path/to/disks/legit.woz > /path/to/files/prodos.jsonTo gather the paths to all the files on a disk
a2kit glob -d adventures.imd -f "**"The quotes prevent the shell from doing its own glob expansion, you can usually omit them on Windows. For ProDOS and FAT disks, you can omit the root path (remember on ProDOS this includes the volume name). For example,
a2kit glob -d adventures.po -f "/adventure.disk/fantasy/**/orc"
a2kit glob -d adventures.po -f "/*/fantasy/**/orc"
a2kit glob -d adventures.po -f "fantasy/**/orc"will all find orcs anywhere in the fantasy directory.
Here is a PowerShell script that builds a BASIC + Merlin project to a disk image.
For this to succeed, the Merlin32 cross-assembler must be installed and in the path.
# PowerShell script to build and deploy to disk image.
# This would be run from the host project directory.
# PS 7.4 doesn't need native wrappers anymore...hooray!
# (following is a directive)
#Requires -Version 7.4
# If you want to check a2kit version it can be done like this
$min_a2kit_vers = "3.7.0"
$a2kit_vers = (a2kit -V).Split()[1]
if ([Version]$a2kit_vers -lt [Version]$min_a2kit_vers) {
Write-Error ("requires a2kit v" + $min_a2kit_vers)
}
# Tell PowerShell we want to stop if any a2kit command fails
Set-Variable ErrorActionPreference "Stop"
Set-Variable PSNativeCommandUseErrorActionPreference $true
# Edit values in this block for your case
#-------------------------------------------------------------------------
# Path on host where disk image will be created
$dimg = "build/project.woz"
# Path on the host where Merlin macros are found
$macroPath = "."
# Setup lists of files we want to move as tuples of (path,address), omit file extensions
$asmFiles = @(
[Tuple]::Create("bin1","32768"),
[Tuple]::Create("bin2","32768")
)
$basicFiles = @(
[Tuple]::Create("program1","2049"),
[Tuple]::Create("program2","2049")
)
#-------------------------------------------------------------------------
# Create or clean build directory
if (!(Test-Path ./build)) {
mkdir ./build
} else {
Remove-Item ./build/*
}
# Create disk image
a2kit mkdsk -d $dimg -t woz2 -o prodos -v project -k 5.25in-apple-16
# Assemble and deploy Merlin sources
foreach ($f in $asmFiles) {
$basename = $f.item1
$addr = $f.item2
Merlin32 $macroPath ($basename + ".S")
a2kit get -f $basename | a2kit put -d $dimg -f $basename -t bin -a $addr
Move-Item $basename build/$basename
}
# Tokenize and deploy BASIC sources
foreach ($f in $basicFiles) {
$basename = $f.item1
$addr = $f.item2
a2kit get -f ($basename + ".bas") | a2kit tokenize -a $addr -t atxt | a2kit put -d $dimg -f $basename -t atok
}
# Catalog the disk image so we can see if it looks alright
a2kit catalog -d $dimgThe following python module is a simple Python front-end. This is used by some of the other examples, see below. Save this as a2kit.py in the directory with whatever script is importing it.
'''Simple example of a Python front-end. The backend interface is mirrored exactly.'''
import subprocess
import warnings
def verify(beg_vers: tuple, end_vers: tuple):
'''Exit if a2kit version falls outside range beg_vers..end_vers'''
full_vers = cmd(['-V']).decode('utf-8').split()[1].split('-')
if len(full_vers) > 1:
warnings.warn("this is a prerelease ({})".format(full_vers[1]))
vers = tuple(map(int, full_vers[0].split('.')))
if vers < beg_vers or vers >= end_vers:
print("a2kit version outside range",beg_vers,"..",end_vers)
exit(1)
def cmd(args, pipe_in=None):
'''run a CLI command as a subprocess'''
compl = subprocess.run(['a2kit']+args,input=pipe_in,capture_output=True,text=False)
if compl.returncode>0:
print(compl.stderr)
exit(1)
return compl.stdoutN.b. string inputs have to be encoded (e.g. using encode('utf-8')), and string outputs have to decoded (e.g. using decode('utf-8')).
In this example we use a Python script to create a bootable CP/M disk for a Kaypro 4. To make a bootable CP/M disk, you will generally need to know some details about how the target vendor chose to lay out their format. In the case of the Kaypro 4, the tricky thing is there are reserved blocks in the user area.
'''Script to make a bootable Kaypro DSDD disk.
This illustrates some low level copying.'''
import sys
import a2kit # front end, see above
a2kit.verify((4,0,0),(5,0,0))
# Parse command line
if len(sys.argv)!=3:
print("usage: python "+sys.argv[0]+" <path in> <path out>")
print("<path in> path to an existing bootable disk image")
print("<path out> path to the new disk image")
exit(1)
path_in = sys.argv[1]
path_out = sys.argv[2]
# Make a blank image, this sets up the track layout etc. for a Kaypro DSDD disk.
a2kit.cmd(["mkdsk","-o","cpm2","-t","imd","-k","5.25in-kay-dsdd","-d", path_out])
# Copy the reserved track - it is on cylinder 0, side 0.
# Kaypro DSDD has sectors 0-9 on side 0, sectors 10-19 on side 1
# Important note: in this scheme, the head map value is always 0.
print("copying reserved track")
chs = "0,0,0..10"
sec_data = a2kit.cmd(["get", "-f", chs, "-t", "sec", "-d", path_in])
a2kit.cmd(["put", "-f", chs, "-t", "sec", "-d", path_out], sec_data)
# This format has a reserved block at block 1. We need to copy that too.
# (this could also be done by copying cylinder 0, side 1, sectors 14-17)
print("copying reserved block")
block_data = a2kit.cmd(["get", "-f", "1", "-t", "block", "-d", path_in])
a2kit.cmd(["put", "-f", "1", "-t", "block", "-d", path_out], block_data)
# Suppose we want ED.COM on this disk. Copy it over.
# Obviously this means the source disk must have ED.COM.
# Note the use of the `any` type: we are sending a file image through the pipe.
print("copying files we want")
ed_dot_com = a2kit.cmd(["get", "-f", "ed.com", "-t", "any", "-d", path_in])
a2kit.cmd(["put", "-f", "ed.com", "-t", "any", "-d", path_out], ed_dot_com)Here is a reasonably fast way to locate multiple occurrences of a pattern within a disk image.
'''Script to search any supported disk image for a pattern in the decoded data.
The search is bounded by ranges of cylinders, heads, and sectors.
The bounding cylinder and head are *geometrically* ordered.'''
import sys
import json
import a2kit # front end, see above
a2kit.verify((4,0,0),(5,0,0))
# Parse command line
if len(sys.argv)!=6:
print("usage: python "+sys.argv[0]+" <path> <cyl beg>,<cyl end> <head beg>,<head end> <sec beg>,<sec end> <pattern>")
print("pattern can be comma-delimited decimals or an ASCII string")
exit(1)
img_path = sys.argv[1]
cyl_rng = range(int(sys.argv[2].split(',')[0]),int(sys.argv[2].split(',')[1]))
head_rng = range(int(sys.argv[3].split(',')[0]),int(sys.argv[3].split(',')[1]))
sec_rng = range(int(sys.argv[4].split(',')[0]),int(sys.argv[4].split(',')[1]))
raw_patt = sys.argv[5]
try:
search_pattern = list(map(int,raw_patt.split(',')))
except:
search_pattern = list(map(ord,raw_patt))
def search(dat: bytes,addr,c,h,s):
matches = 0
for i,val in enumerate(list(dat)):
if search_pattern[matches]==val:
matches += 1
else:
matches = 0
if matches==len(search_pattern):
print("match completed:")
print(" geometric CHS =",[c,h,s])
print(" physical addr =",addr)
print(" offset =",hex(i+1-len(search_pattern)))
matches = 0
# Strategy is to gather data by track, this is quite a bit faster than gathering by sector
geometry = json.loads(a2kit.cmd(["geometry","-d",img_path]))
for trk in geometry["tracks"]:
c = trk["cylinder"] # geometric order
h = trk["head"] # geometric order
if c in cyl_rng and h in head_rng:
search_range = str(c) + ".." + str(c+1) + ","
search_range += str(h) + ".." + str(h+1) + ","
search_range += str(sec_rng.start) + ".." + str(sec_rng.stop)
dat = a2kit.cmd(["get","-d",img_path,"-t","sec","-f",search_range])
if type(trk["solution"]) == dict:
addr_map = trk["solution"]["addr_map"]
size_map = trk["solution"]["size_map"]
addr_type = trk["solution"]["addr_type"]
for i in range(len(addr_map)):
l = size_map[i]
addr_str = addr_map[i]
s_idx = addr_type.find("S")
s = int(addr_str[s_idx*2:s_idx*2+2],16)
if s in sec_rng:
search(dat[l*(s-sec_rng.start): l*(s-sec_rng.start+1)],addr_str,c,h,s)
else:
print("track was",trk["solution"])Converting an image is a matter of creating an output image and then copying the data from the input image. The following are examples of a general pattern that can be applied to any two compatible images.
Note the file system and volume name of the destination don't matter, as everything is overwritten by the copy.
a2kit mkdsk -o prodos -t woz2 -v x -d dst.woz
a2kit get -d src.do -t sec -f 0..35,0,0..16 | a2kit put -d dst.woz -t sec -f 0..35,0,0..16When copying by sector, avoid images where order is variable (DO is fine, DSK may not be).
For this you must use blocks, because there might be a logical volume involved. The ordering of the source disk doesn't matter, the destination will be DOS ordered either way.
a2kit mkdsk -o prodos -t do -v x -d dst.dsk
a2kit get -d src.dsk -t block -f 0..280 | a2kit put -d dst.dsk -t block -f 0..280What is going on under the hood is that by using blocks, we force a2kit to solve the file system, which in turn lets us figure out the ordering.
This script was developed before the smart copy was implemented. However it may still be useful because it can recreate the directory structure of the source at the destination, which the smart copy cannot yet do.
'''Easily extract files from a variety of retro disk images.
Files are put in a friendly format if possible.
Example: `python extract.py mydisk.woz path/to/my/target
Scope:
* Apple DOS, ProDOS, Apple Pascal, CP/M (various), MS-DOS (or other FAT)
* Applesoft BASIC, Integer BASIC, Pascal, various text
* 2MG, IMD, NIB, TD0, WOZ, DO/PO/DSK/IMG/IMA
Dependencies:
* a2kit CLI - `cargo install a2kit` or download from https://github.com/dfgordon/a2kit/releases/latest
Notes:
* CP/M user numbers will become directories'''
import sys
import os
import pathlib
import glob
import json
import subprocess
import platform
# simple front end for a2kit
def cmd(args, pipe_in=None, tolerant=False):
'''run a CLI command as a subprocess'''
compl = subprocess.run(['a2kit']+args,input=pipe_in,capture_output=True,text=False)
if compl.returncode>0:
if not tolerant:
raise ValueError(str(compl.stderr))
else:
return compl.stdout,compl.stderr
return compl.stdout,None
# check dependencies
if tuple(map(int,platform.python_version_tuple())) < (3,8,0):
print("requires python 3.8")
exit(1)
vers = tuple(map(int,cmd(["-V"])[0].decode('utf-8').split()[1].split('-')[0].split('.')))
if vers < (4,0,0) or vers >= (5,0,0):
print("a2kit version out of range")
exit(1)
# parse command line
if len(sys.argv)!=3:
print("usage: python "+sys.argv[0]+" <img_path> <target_dir>")
print("<path> path to disk image")
print("<target_dir> where to put the files")
exit(1)
dimg = sys.argv[1]
target = pathlib.Path(sys.argv[2])
failures = []
# verify target directory is empty
os.makedirs(target,exist_ok=True)
cleanstr = glob.glob(str(target/"*"))
if len(cleanstr)>0:
print("target directory needs to be empty")
exit(1)
print("glob the disk image...")
all_paths = cmd(["glob","-f","**","-d",dimg])[0]
print("gather all file images...")
all_files,err = cmd(["mget","-d",dimg],all_paths,True)
if err:
print("mget failed")
exit(1)
print("unpack and save...")
all_files = json.loads(all_files)
for file in all_files:
print(file["full_path"])
file_data,err = cmd(["unpack","-t","auto"],bytearray(json.dumps(file),"utf8"),True)
if file_data:
fs_type = int(file["fs_type"],16)
if file["file_system"] == "prodos" and fs_type == 0xfa:
file_data,err = cmd(["detokenize","-t","itok"],file_data,True)
if file["file_system"] == "prodos" and fs_type == 0xfc:
file_data,err = cmd(["detokenize","-t","atok"],file_data,True)
if file["file_system"] == "a2 dos" and fs_type & 0x7f == 1:
file_data,err = cmd(["detokenize","-t","itok"],file_data,True)
if file["file_system"] == "a2 dos" and fs_type & 0x7f == 2:
file_data,err = cmd(["detokenize","-t","atok"],file_data,True)
if err!=None:
failures += [file["full_path"]+": "+str(err)]
elif file_data:
dest_path = file["full_path"]
if file["file_system"] == "cpm":
dest_path = dest_path.replace(":","/")
while dest_path[0] == "/":
dest_path = dest_path[1:]
host_path = target / pathlib.Path(dest_path)
os.makedirs(host_path.parent,exist_ok=True)
with open(host_path,'b+w') as f:
f.write(file_data)
else:
raise RuntimeError("unreachable was reached")
if len(failures)>0:
print()
print("ERROR REPORT")
print("------------")
for err in failures:
print(err)