Mojo CLI Tool Project
Building a complete command-line tool ties together everything from this course — argument parsing, file I/O, error handling, structs, functions, string formatting, and output. This topic walks through constructing a real-world CSV statistics tool called mojostat that reads a numeric CSV column and reports count, sum, mean, min, max, and standard deviation.
What We Are Building
$ magic run mojo mojostat.mojo scores.csv --column 1 --verbose Output: ───────────────────────────────── mojostat — Column Statistics ───────────────────────────────── File : scores.csv Column : 1 Count : 5 Sum : 447.00 Mean : 89.40 Min : 78.00 Max : 97.00 Std Dev: 7.09 ───────────────────────────────── Tool structure: ├── parse_args() — read CLI arguments ├── read_column() — parse CSV and extract one column ├── Statistics — struct holding computed stats ├── compute() — fill Statistics from raw values └── print_report() — format and display the result
Step 1: Argument Parsing
from sys import argv
struct Config:
var filepath: String
var column: Int
var verbose: Bool
var separator: String
fn __init__(inout self):
self.filepath = ""
self.column = 0
self.verbose = False
self.separator = ","
fn parse_args() raises -> Config:
var args = argv()
var cfg = Config()
if len(args) < 2:
raise Error(
"Usage: mojostat <file.csv> [--column N] [--sep CHAR] [--verbose]"
)
cfg.filepath = args[1]
var i = 2
while i < len(args):
if args[i] == "--column" and i + 1 < len(args):
cfg.column = Int(args[i + 1])
i += 2
elif args[i] == "--sep" and i + 1 < len(args):
cfg.separator = args[i + 1]
i += 2
elif args[i] == "--verbose":
cfg.verbose = True
i += 1
else:
i += 1
return cfg
Step 2: Reading the CSV Column
fn read_column(filepath: String, col_index: Int, sep: String) raises -> List[Float64]:
var values = List[Float64]()
with open(filepath, "r") as f:
var header = f.readline() # skip header row
if header == "":
raise Error("File is empty: " + filepath)
var line = f.readline()
var row_num = 1
while line != "":
var trimmed = line.strip()
if trimmed != "":
var parts = trimmed.split(sep)
if col_index >= len(parts):
raise Error(
"Row " + String(row_num) +
" has only " + String(len(parts)) +
" columns — cannot read column " + String(col_index)
)
try:
var val = Float64(parts[col_index].strip())
values.append(val)
except:
raise Error(
"Non-numeric value '" + parts[col_index] +
"' at row " + String(row_num) +
", column " + String(col_index)
)
row_num += 1
line = f.readline()
if len(values) == 0:
raise Error("No numeric data found in column " + String(col_index))
return values
Step 3: Statistics Struct
from math import sqrt
struct Statistics:
var count: Int
var sum: Float64
var mean: Float64
var minimum: Float64
var maximum: Float64
var std_dev: Float64
fn __init__(inout self):
self.count = 0
self.sum = 0.0
self.mean = 0.0
self.minimum = 0.0
self.maximum = 0.0
self.std_dev = 0.0
fn compute(values: List[Float64]) -> Statistics:
var stats = Statistics()
stats.count = len(values)
if stats.count == 0:
return stats
stats.minimum = values[0]
stats.maximum = values[0]
for i in range(stats.count):
var v = values[i]
stats.sum += v
if v < stats.minimum: stats.minimum = v
if v > stats.maximum: stats.maximum = v
stats.mean = stats.sum / Float64(stats.count)
var variance: Float64 = 0.0
for i in range(stats.count):
var diff = values[i] - stats.mean
variance += diff * diff
stats.std_dev = sqrt(variance / Float64(stats.count))
return stats
Step 4: Formatted Report
from python import Python
fn fmt2(value: Float64) raises -> String:
var py = Python.import_module("builtins")
return str(py.str("{:.2f}").format(value))
fn print_report(cfg: Config, stats: Statistics) raises:
var border = "─" * 34
print(border)
print(" mojostat — Column Statistics")
print(border)
print(" File : " + cfg.filepath)
print(" Column : " + String(cfg.column))
print(" Count : " + String(stats.count))
print(" Sum : " + fmt2(stats.sum))
print(" Mean : " + fmt2(stats.mean))
print(" Min : " + fmt2(stats.minimum))
print(" Max : " + fmt2(stats.maximum))
print(" Std Dev: " + fmt2(stats.std_dev))
print(border)
Step 5: Main Entry Point
fn main() raises:
try:
var cfg = parse_args()
if cfg.verbose:
print("[verbose] Reading:", cfg.filepath,
"| column:", cfg.column,
"| sep: '" + cfg.separator + "'")
var values = read_column(cfg.filepath, cfg.column, cfg.separator)
if cfg.verbose:
print("[verbose] Loaded", len(values), "values")
var stats = compute(values)
print_report(cfg, stats)
except e:
print("Error:", str(e))
return
Sample Data File
# scores.csv name,score,attempts Alice,97,3 Bob,82,5 Carol,91,2 David,78,4 Elena,99,1
Running the Tool
# Basic run (column 0 = name — will fail on non-numeric, shows error handling) magic run mojo mojostat.mojo scores.csv --column 0 # Correct: column 1 = scores magic run mojo mojostat.mojo scores.csv --column 1 # With verbose output magic run mojo mojostat.mojo scores.csv --column 1 --verbose # Custom separator (semicolon-separated file) magic run mojo mojostat.mojo data.csv --column 2 --sep ";"
Output for --column 1:
────────────────────────────────── mojostat — Column Statistics ────────────────────────────────── File : scores.csv Column : 1 Count : 5 Sum : 447.00 Mean : 89.40 Min : 78.00 Max : 99.00 Std Dev: 7.81 ──────────────────────────────────
Project Architecture Review
main()
│
├── parse_args() → Config struct
│ └── argv(), Int(), string parsing
│
├── read_column() → List[Float64]
│ └── open(), readline(), split(), Float64()
│ error handling for bad rows
│
├── compute() → Statistics struct
│ └── single-pass min/max/sum, two-pass std dev
│
└── print_report() → terminal output
└── fmt2() for 2-decimal formatting, Python interop
Extending the Tool
Ideas for extension exercises: 1. Add --output flag to write report to a file 2. Add --skip-header flag for files without a header row 3. Support multiple columns in one run: --columns 1,2,3 4. Add median computation (requires sorting the values) 5. Add histogram output using asterisks in the terminal 6. Add --json flag to output results as JSON for piping to other tools
Key Takeaways
A real CLI tool combines argument parsing, file I/O, data validation, computation, and formatted output into a single coherent program. Separating each concern into its own function or struct makes the code testable, readable, and easy to extend. A Config struct carries all parsed settings through the program cleanly. Error handling at every I/O boundary — argument parsing, file reading, type conversion — turns a fragile script into a robust tool. This project structure (parse → load → compute → report) applies to data pipelines of any size.
