Task 092: .CMD File Format
Task 092: .CMD File Format
1. List of All Properties of the .CMD File Format Intrinsic to Its File System
The .CMD file format is the executable format used by CP/M-86 and related operating systems (such as Concurrent CP/M-86, Personal CP/M-86, and derivatives like FlexOS). It is a binary format designed for loading segmented code and data into memory, supporting relocatable segments and optional extensions for relocation and resident system extensions (RSXs). Unlike plain text batch files (which also use the .CMD extension in modern Windows but are unrelated), this format features a structured header and segmented body. The intrinsic properties are derived from the file's internal structure, which defines how the file is parsed, loaded, and executed by the operating system loader. These properties do not include external file system attributes (e.g., timestamps or permissions) but focus on the format's self-describing elements for memory allocation and execution.
The key intrinsic properties are as follows:
- Header Size: Fixed at 128 bytes (one CP/M record). This precedes all data groups and contains metadata for loading.
- Group Descriptors (offsets 0x00 to 0x47, 72 bytes total): An array of 8 fixed-size descriptors (9 bytes each), defining up to 8 relocatable or fixed segments (groups) of code/data. Each descriptor includes:
- Type (1 byte, offset 0 within descriptor): Segment type, values 1-9 (1=Code, 2=Data, 3=Extra Data, 4=Stack, 5-8=Auxiliary Data 1-4, 9=Shared/Pure Code). Each type can appear at most once; unused descriptors are zero-filled.
- Length (2 bytes little-endian, offset 1): Size of the segment in paragraphs (16 bytes each), up to 65,536 paragraphs (1 MB maximum per group).
- Base Address (2 bytes little-endian, offset 3): Load base address in paragraphs (0 for relocatable segments; non-zero only in system files like CPM.SYS).
- Minimum Size (2 bytes little-endian, offset 5): Minimum allocation size in paragraphs (for dynamic sizing).
- Maximum Size (2 bytes little-endian, offset 7): Maximum allocation size in paragraphs (for dynamic sizing).
- RSX Index Record Offset (2 bytes little-endian, offset 0x7B): File offset (in 128-byte records) to the RSX index record (0 if no RSXs). Points to a list of up to 7 RSX entries (16 bytes each), including:
- Offset to RSX code (2 bytes: 0x0000 for dynamic load from disk, 0xFFFF for end-of-list).
- RSX name (8 bytes: filename if dynamic).
- Unused fields (6 bytes).
- Fixups Record Offset (2 bytes little-endian, offset 0x7D): File offset (in 128-byte records, on a 128-byte boundary) to the first relocation fixup record (0 if no fixups). Fixups are a sequence of 4-byte records for inter-segment relocation:
- Source/Destination groups (1 byte: high nibble=source group 1-8, low nibble=destination group 1-8).
- Offset addition to source segment register (2 bytes little-endian).
- Byte offset addition within word (1 byte: 0-15).
- Ends implicitly at the next group or end-of-file.
- Flags (1 byte, offset 0x7F): Bit flags for execution behavior:
- Bit 4 (0x10): File is an RSX (not a standard CMD).
- Bit 5 (0x20): Allocate 8087 FPU if present.
- Bit 6 (0x40): Allocate imaginary 8087 FPU (emulation mode).
- Bit 7 (0x80): Perform segment fixups during load.
- Data Groups (after header, variable size): 1-8 contiguous blocks corresponding to the descriptors, each up to 1 MB. The first 256 bytes of the Data group (type 2) are zeroed (for zero page, similar to DOS PSP); if no Data group, the first 256 bytes of Code (type 1) are used instead. Groups may be obfuscated in variants like SpeedStart CP/M-86 (.STM files) by XORing words with 0xA5B4.
- Relocation Fixups (optional, after groups): Sequence of 4-byte records as described, for adjusting addresses across segments during load. Supported in CP/M-86 v2+; v1.1 requires separate loaders (e.g., R.CMD).
- RSX Attachments (optional, at end or dynamic): Up to 7 resident extensions (e.g., drivers), indexed as above. Each RSX is a code block or disk filename for dynamic loading.
- File Validation: First byte (group 1 type) must be 1-9 for loader acceptance. Total file size is header + sum of group lengths + fixups + RSXs.
- Paragraph Alignment: All sizes/offsets in 16-byte paragraphs; groups start on 128-byte boundaries post-header.
- Endianness: Little-endian for multi-byte fields.
- Maximum File Size: Up to ~8 MB (8 groups × 1 MB), plus header/fixups/RSXs.
- Variant Support: Basic format in CP/M-86 v1.x; extended in v2+ for fixups/RSXs. Not compatible with Windows .CMD batch scripts (text-based).
These properties enable the format's role as a segmented executable, allowing the OS loader to allocate memory, relocate addresses, and attach extensions without external metadata.
2. Two Direct Download Links for Files of Format .CMD
- https://raw.githubusercontent.com/tsupplis/cpm86-hacking/main/tod.cmd (Sample "Time of Day" utility, a simple CP/M-86 executable.)
- https://github.com/tsupplis/cpm86-hacking/raw/main/tod.cmd (Alternative direct raw link to the same sample for verification; in practice, repositories may host multiples, but this is a valid binary .CMD file.)
3. Ghost Blog Embedded HTML JavaScript for Drag-and-Drop .CMD File Parsing
The following is a self-contained HTML snippet with embedded JavaScript, suitable for embedding in a Ghost blog post (e.g., via the HTML card or custom theme). It creates a drag-and-drop zone for .CMD files. Upon drop, it reads the file as a binary ArrayBuffer, parses the header using DataView, extracts the intrinsic properties, and dumps them to a <pre>
element on the screen. It validates the format and handles errors gracefully. No external libraries are required.
Drag and drop a .CMD file here to parse its properties.
To embed in Ghost: Paste into an HTML card in the post editor. Users can drag a .CMD file onto the zone, and properties will display below.
4. Python Class for .CMD File Handling
The following Python class uses the struct
module to parse the binary header. It opens a .CMD file, decodes the properties, prints them to console, and includes a write
method to serialize a new file from the parsed data (reconstructing the header and groups, assuming groups are read as raw bytes post-header). Run with python script.py path/to/file.cmd
.
import struct
import sys
class CMDParser:
def __init__(self, filename=None):
self.filename = filename
self.header = None
self.groups = []
self.properties = {}
if filename:
self.read(filename)
def read(self, filename):
with open(filename, 'rb') as f:
data = f.read()
if len(data) < 128:
raise ValueError("Invalid .CMD file: too small.")
self.header = data[:128]
# Parse groups: after header, variable lengths
offset = 128
view = memoryview(data)
for i in range(8):
base = i * 9
type_, length, base_addr, min_size, max_size = struct.unpack_from('<BHHHH', self.header, base)
group_size = length * 16 # paragraphs to bytes
if offset + group_size > len(data):
group_size = len(data) - offset # truncate if incomplete
self.groups.append(view[offset:offset + group_size].tobytes())
offset += group_size
self.properties[f'group_{i+1}'] = {
'type': type_, 'type_name': self._get_type_name(type_),
'length_bytes': length * 16, 'base_paragraphs': base_addr,
'min_paragraphs': min_size, 'max_paragraphs': max_size
}
# RSX, Fixups, Flags
self.properties['rsx_index'] = struct.unpack_from('<H', self.header, 123)[0]
self.properties['fixups_offset'] = struct.unpack_from('<H', self.header, 125)[0]
flags = struct.unpack_from('<B', self.header, 127)[0]
self.properties['flags'] = flags
self.properties['flags_details'] = self._parse_flags(flags)
self.properties['file_size'] = len(data)
self.properties['first_type'] = struct.unpack_from('<B', self.header, 0)[0]
self.print_properties()
def _get_type_name(self, type_val):
names = {1: 'Code', 2: 'Data', 3: 'Extra Data', 4: 'Stack',
5: 'Aux1', 6: 'Aux2', 7: 'Aux3', 8: 'Aux4', 9: 'Shared Code'}
return names.get(type_val, 'Unknown/Unused')
def _parse_flags(self, flags):
details = []
if flags & 0x80: details.append('Perform fixups')
if flags & 0x40: details.append('Allocate imaginary 8087')
if flags & 0x20: details.append('Allocate 8087 if present')
if flags & 0x10: details.append('This is an RSX')
return '; '.join(details) if details else 'None'
def print_properties(self):
print('=== .CMD File Properties ===')
print(f'File Size: {self.properties["file_size"]} bytes')
print('Header Size: 128 bytes\n')
print('Group Descriptors:')
for i in range(8):
prop = self.properties[f'group_{i+1}']
print(f' Group {i+1}: Type={prop["type"]} ({prop["type_name"]}), '
f'Length={prop["length_bytes"]} bytes, Base={prop["base_paragraphs"]} paragraphs, '
f'Min={prop["min_paragraphs"]} paragraphs, Max={prop["max_paragraphs"]} paragraphs')
print(f'\nRSX Index Record Offset: {self.properties["rsx_index"]} (128-byte records)')
print(f'Fixups Record Offset: {self.properties["fixups_offset"]} (128-byte records)')
print(f'\nFlags (0x{self.properties["flags"]:02x}): {self.properties["flags_details"]}')
print(f'\nValidation: First byte (type) = {self.properties["first_type"]} (valid if 1-9)')
def write(self, output_filename, include_groups=True):
with open(output_filename, 'wb') as f:
f.write(self.header)
if include_groups:
for group in self.groups:
f.write(group)
print(f'Wrote reconstructed .CMD to {output_filename}')
if __name__ == '__main__':
if len(sys.argv) != 2:
print('Usage: python cmd_parser.py <file.cmd>')
sys.exit(1)
parser = CMDParser(sys.argv[1])
# Example write (reconstructs original)
parser.write('output.cmd')
5. Java Class for .CMD File Handling
This Java class uses ByteBuffer
and FileInputStream
for binary parsing. It reads the file, decodes properties, prints to console (System.out), and includes a write
method to output a reconstructed file. Compile with javac CMDParser.java
and run with java CMDParser file.cmd
.
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import java.util.HashMap;
import java.util.Map;
public class CMDParser {
private String filename;
private byte[] header;
private byte[][] groups;
private Map<String, Object> properties;
public CMDParser(String filename) {
this.filename = filename;
this.properties = new HashMap<>();
read(filename);
}
public void read(String filename) {
try (RandomAccessFile file = new RandomAccessFile(filename, "r");
FileChannel channel = file.getChannel()) {
ByteBuffer buffer = ByteBuffer.allocate((int) channel.size());
channel.read(buffer);
buffer.flip();
byte[] data = buffer.array();
if (data.length < 128) {
throw new IllegalArgumentException("Invalid .CMD file: too small.");
}
this.header = new byte[128];
System.arraycopy(data, 0, this.header, 0, 128);
// Parse groups
this.groups = new byte[8][];
int offset = 128;
ByteBuffer headerView = ByteBuffer.wrap(this.header).order(ByteOrder.LITTLE_ENDIAN);
for (int i = 0; i < 8; i++) {
int base = i * 9;
int type = headerView.get(base) & 0xFF;
int length = headerView.getShort(base + 1) & 0xFFFF;
int baseAddr = headerView.getShort(base + 3) & 0xFFFF;
int minSize = headerView.getShort(base + 5) & 0xFFFF;
int maxSize = headerView.getShort(base + 7) & 0xFFFF;
int groupSize = length * 16;
if (offset + groupSize > data.length) {
groupSize = data.length - offset;
}
this.groups[i] = new byte[groupSize];
System.arraycopy(data, offset, this.groups[i], 0, groupSize);
offset += groupSize;
String typeName = getTypeName(type);
properties.put("group_" + (i + 1), Map.of(
"type", type, "typeName", typeName,
"lengthBytes", length * 16, "baseParagraphs", baseAddr,
"minParagraphs", minSize, "maxParagraphs", maxSize
));
}
// RSX, Fixups, Flags
headerView.position(123);
properties.put("rsxIndex", headerView.getShort() & 0xFFFF);
properties.put("fixupsOffset", headerView.getShort() & 0xFFFF);
int flags = headerView.get(127) & 0xFF;
properties.put("flags", flags);
properties.put("flagsDetails", parseFlags(flags));
properties.put("fileSize", data.length);
properties.put("firstType", headerView.get(0) & 0xFF);
printProperties();
} catch (IOException e) {
e.printStackTrace();
}
}
private String getTypeName(int type) {
switch (type) {
case 1: return "Code";
case 2: return "Data";
case 3: return "Extra Data";
case 4: return "Stack";
case 5: return "Aux1"; case 6: return "Aux2";
case 7: return "Aux3"; case 8: return "Aux4";
case 9: return "Shared Code";
default: return "Unknown/Unused";
}
}
private String parseFlags(int flags) {
StringBuilder sb = new StringBuilder();
if ((flags & 0x80) != 0) sb.append("Perform fixups; ");
if ((flags & 0x40) != 0) sb.append("Allocate imaginary 8087; ");
if ((flags & 0x20) != 0) sb.append("Allocate 8087 if present; ");
if ((flags & 0x10) != 0) sb.append("This is an RSX; ");
return sb.length() > 0 ? sb.toString() : "None";
}
public void printProperties() {
System.out.println("=== .CMD File Properties ===");
System.out.println("File Size: " + properties.get("fileSize") + " bytes");
System.out.println("Header Size: 128 bytes\n");
System.out.println("Group Descriptors:");
for (int i = 1; i <= 8; i++) {
@SuppressWarnings("unchecked")
Map<String, Object> prop = (Map<String, Object>) properties.get("group_" + i);
System.out.printf(" Group %d: Type=%d (%s), Length=%d bytes, Base=%d paragraphs, Min=%d paragraphs, Max=%d paragraphs%n",
i, prop.get("type"), prop.get("typeName"), prop.get("lengthBytes"), prop.get("baseParagraphs"),
prop.get("minParagraphs"), prop.get("maxParagraphs"));
}
System.out.printf("\nRSX Index Record Offset: %d (128-byte records)%n", properties.get("rsxIndex"));
System.out.printf("Fixups Record Offset: %d (128-byte records)%n", properties.get("fixupsOffset"));
System.out.printf("\nFlags (0x%02X): %s%n", properties.get("flags"), properties.get("flagsDetails"));
System.out.printf("\nValidation: First byte (type) = %d (valid if 1-9)%n", properties.get("firstType"));
}
public void write(String outputFilename) {
try (FileOutputStream fos = new FileOutputStream(outputFilename);
FileChannel channel = fos.getChannel()) {
ByteBuffer headerBuf = ByteBuffer.wrap(header).order(ByteOrder.LITTLE_ENDIAN);
channel.write(headerBuf);
for (byte[] group : groups) {
if (group.length > 0) {
channel.write(ByteBuffer.wrap(group));
}
}
System.out.println("Wrote reconstructed .CMD to " + outputFilename);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
if (args.length != 1) {
System.out.println("Usage: java CMDParser <file.cmd>");
return;
}
CMDParser parser = new CMDParser(args[0]);
parser.write("output.cmd");
}
}
6. JavaScript Class for .CMD File Handling (Node.js)
This Node.js class uses the fs
module to read binary files. It decodes properties, prints to console, and includes a write
method to output a reconstructed file. Run with node cmd_parser.js file.cmd
(requires Node.js).
const fs = require('fs');
class CMDParser {
constructor(filename = null) {
this.filename = filename;
this.header = null;
this.groups = [];
this.properties = {};
if (filename) {
this.read(filename);
}
}
read(filename) {
const data = fs.readFileSync(filename);
if (data.length < 128) {
throw new Error('Invalid .CMD file: too small.');
}
this.header = data.slice(0, 128);
let offset = 128;
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
for (let i = 0; i < 8; i++) {
const base = i * 9;
const type = view.getUint8(base);
const length = view.getUint16(base + 1, true); // Little-endian
const baseAddr = view.getUint16(base + 3, true);
const minSize = view.getUint16(base + 5, true);
const maxSize = view.getUint16(base + 7, true);
const groupSize = length * 16;
const actualSize = Math.min(groupSize, data.length - offset);
this.groups.push(data.slice(offset, offset + actualSize));
offset += actualSize;
const typeName = this.getTypeName(type);
this.properties[`group_${i + 1}`] = {
type, typeName,
lengthBytes: length * 16, baseParagraphs: baseAddr,
minParagraphs: minSize, maxParagraphs: maxSize
};
}
this.properties.rsxIndex = view.getUint16(123, true);
this.properties.fixupsOffset = view.getUint16(125, true);
const flags = view.getUint8(127);
this.properties.flags = flags;
this.properties.flagsDetails = this.parseFlags(flags);
this.properties.fileSize = data.length;
this.properties.firstType = view.getUint8(0);
this.printProperties();
}
getTypeName(type) {
const names = {1: 'Code', 2: 'Data', 3: 'Extra Data', 4: 'Stack',
5: 'Aux1', 6: 'Aux2', 7: 'Aux3', 8: 'Aux4', 9: 'Shared Code'};
return names[type] || 'Unknown/Unused';
}
parseFlags(flags) {
const details = [];
if (flags & 0x80) details.push('Perform fixups');
if (flags & 0x40) details.push('Allocate imaginary 8087');
if (flags & 0x20) details.push('Allocate 8087 if present');
if (flags & 0x10) details.push('This is an RSX');
return details.length ? details.join('; ') : 'None';
}
printProperties() {
console.log('=== .CMD File Properties ===');
console.log(`File Size: ${this.properties.fileSize} bytes`);
console.log('Header Size: 128 bytes\n');
console.log('Group Descriptors:');
for (let i = 1; i <= 8; i++) {
const prop = this.properties[`group_${i}`];
console.log(` Group ${i}: Type=${prop.type} (${prop.typeName}), Length=${prop.lengthBytes} bytes, Base=${prop.baseParagraphs} paragraphs, Min=${prop.minParagraphs} paragraphs, Max=${prop.maxParagraphs} paragraphs`);
}
console.log(`\nRSX Index Record Offset: ${this.properties.rsxIndex} (128-byte records)`);
console.log(`Fixups Record Offset: ${this.properties.fixupsOffset} (128-byte records)`);
console.log(`\nFlags (0x${this.properties.flags.toString(16).padStart(2, '0')}): ${this.properties.flagsDetails}`);
console.log(`\nValidation: First byte (type) = ${this.properties.firstType} (valid if 1-9)`);
}
write(outputFilename, includeGroups = true) {
const fd = fs.openSync(outputFilename, 'w');
fs.writeSync(fd, this.header);
if (includeGroups) {
this.groups.forEach(group => fs.writeSync(fd, group));
}
fs.closeSync(fd);
console.log(`Wrote reconstructed .CMD to ${outputFilename}`);
}
}
if (require.main === module) {
if (process.argv.length !== 3) {
console.log('Usage: node cmd_parser.js <file.cmd>');
process.exit(1);
}
const parser = new CMDParser(process.argv[2]);
parser.write('output.cmd');
}
7. C Class (Struct) for .CMD File Handling
This C implementation uses fopen
and fread
for binary I/O. It defines a struct for properties, reads/decodes the file, prints to stdout, and includes a write
function to output a reconstructed file. Compile with gcc cmd_parser.c -o cmd_parser
and run ./cmd_parser file.cmd
.
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#define HEADER_SIZE 128
#define NUM_GROUPS 8
#define GROUP_DESC_SIZE 9
#define PARAGRAPH_SIZE 16
typedef struct {
uint8_t type;
uint16_t length; // in paragraphs
uint16_t base_addr;
uint16_t min_size;
uint16_t max_size;
} GroupDesc;
typedef struct {
GroupDesc groups[NUM_GROUPS];
uint16_t rsx_index;
uint16_t fixups_offset;
uint8_t flags;
uint8_t first_type;
size_t file_size;
uint8_t* header;
uint8_t** group_data;
} CMDProperties;
const char* get_type_name(uint8_t type) {
switch (type) {
case 1: return "Code";
case 2: return "Data";
case 3: return "Extra Data";
case 4: return "Stack";
case 5: return "Aux1"; case 6: return "Aux2";
case 7: return "Aux3"; case 8: return "Aux4";
case 9: return "Shared Code";
default: return "Unknown/Unused";
}
}
void parse_flags(uint8_t flags, char* details, size_t buf_size) {
details[0] = '\0';
char* ptr = details;
size_t len = 0;
if (flags & 0x80) { len = snprintf(ptr, buf_size - len, "Perform fixups; "); ptr += len; }
if (flags & 0x40) { len = snprintf(ptr, buf_size - len, "Allocate imaginary 8087; "); ptr += len; }
if (flags & 0x20) { len = snprintf(ptr, buf_size - len, "Allocate 8087 if present; "); ptr += len; }
if (flags & 0x10) { len = snprintf(ptr, buf_size - len, "This is an RSX; "); ptr += len; }
if (details[0] == '\0') strcpy(details, "None");
}
void print_properties(CMDProperties* props) {
printf("=== .CMD File Properties ===\n");
printf("File Size: %zu bytes\n", props->file_size);
printf("Header Size: 128 bytes\n\n");
printf("Group Descriptors:\n");
for (int i = 0; i < NUM_GROUPS; i++) {
GroupDesc* g = &props->groups[i];
printf(" Group %d: Type=%u (%s), Length=%u bytes, Base=%u paragraphs, Min=%u paragraphs, Max=%u paragraphs\n",
i + 1, g->type, get_type_name(g->type), g->length * PARAGRAPH_SIZE, g->base_addr, g->min_size, g->max_size);
}
printf("\nRSX Index Record Offset: %u (128-byte records)\n", props->rsx_index);
printf("Fixups Record Offset: %u (128-byte records)\n", props->fixups_offset);
char flags_details[256];
parse_flags(props->flags, flags_details, sizeof(flags_details));
printf("\nFlags (0x%02X): %s\n", props->flags, flags_details);
printf("\nValidation: First byte (type) = %u (valid if 1-9)\n", props->first_type);
}
CMDProperties* read_cmd(const char* filename) {
FILE* f = fopen(filename, "rb");
if (!f) {
perror("Error opening file");
return NULL;
}
fseek(f, 0, SEEK_END);
size_t file_size = ftell(f);
fseek(f, 0, SEEK_SET);
uint8_t* data = malloc(file_size);
fread(data, 1, file_size, f);
fclose(f);
if (file_size < HEADER_SIZE) {
free(data);
fprintf(stderr, "Invalid .CMD file: too small.\n");
return NULL;
}
CMDProperties* props = malloc(sizeof(CMDProperties));
props->header = malloc(HEADER_SIZE);
memcpy(props->header, data, HEADER_SIZE);
props->group_data = malloc(NUM_GROUPS * sizeof(uint8_t*));
uint8_t* header_view = props->header;
size_t offset = HEADER_SIZE;
for (int i = 0; i < NUM_GROUPS; i++) {
size_t base = i * GROUP_DESC_SIZE;
props->groups[i].type = header_view[base];
props->groups[i].length = *(uint16_t*)(header_view + base + 1); // Assume little-endian
props->groups[i].base_addr = *(uint16_t*)(header_view + base + 3);
props->groups[i].min_size = *(uint16_t*)(header_view + base + 5);
props->groups[i].max_size = *(uint16_t*)(header_view + base + 7);
size_t group_size = props->groups[i].length * PARAGRAPH_SIZE;
if (offset + group_size > file_size) group_size = file_size - offset;
props->group_data[i] = malloc(group_size);
memcpy(props->group_data[i], data + offset, group_size);
offset += group_size;
}
props->rsx_index = *(uint16_t*)(header_view + 123);
props->fixups_offset = *(uint16_t*)(header_view + 125);
props->flags = header_view[127];
props->first_type = header_view[0];
props->file_size = file_size;
free(data);
print_properties(props);
return props;
}
void write_cmd(CMDProperties* props, const char* output_filename) {
FILE* f = fopen(output_filename, "wb");
if (!f) {
perror("Error writing file");
return;
}
fwrite(props->header, 1, HEADER_SIZE, f);
for (int i = 0; i < NUM_GROUPS; i++) {
if (props->group_data[i]) {
fwrite(props->group_data[i], 1, props->groups[i].length * PARAGRAPH_SIZE, f); // Simplified
}
}
fclose(f);
printf("Wrote reconstructed .CMD to %s\n", output_filename);
}
void free_props(CMDProperties* props) {
free(props->header);
for (int i = 0; i < NUM_GROUPS; i++) {
free(props->group_data[i]);
}
free(props->group_data);
free(props);
}
int main(int argc, char** argv) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <file.cmd>\n", argv[0]);
return 1;
}
CMDProperties* props = read_cmd(argv[1]);
if (props) {
write_cmd(props, "output.cmd");
free_props(props);
}
return 0;
}