Command-line tool
mgrep, a small grep-like tool, parses its command-line arguments by hand into a struct and handles --help and bad arguments with throws.
Code
// mgrep: print the lines of FILEs that contain PATTERN.
use rok::std::env;
use rok::std::fs;
use rok::std::io::IoError;
const USAGE: str = "usage: mgrep [-i] [-n] [-c] [-m NUM] PATTERN [FILE]...
-i, --ignore-case match case-insensitively
-n, --line-number prefix each line with its number
-c, --count print only the number of matching lines
-m, --max NUM stop after NUM matches
-h, --help show this help";
// Used when no FILE is given (the demo never reads stdin).
const SAMPLE: str = "Roses are red,
violets are blue.
rok is fast,
and so are you.";
enum ArgError {
Unknown(str),
MissingValue(str),
BadNumber(str),
NoPattern,
}
impl Display for ArgError {
fn fmt(self, ref f: Formatter) throws (FmtError) {
try f.write_str(match self {
ArgError::Unknown(a) => f"unknown option '{a}'",
ArgError::MissingValue(a) => f"option '{a}' needs a value",
ArgError::BadNumber(v) => f"'{v}' is not a number",
ArgError::NoPattern => "missing PATTERN",
})
}
}
struct Options {
pattern: str;
ignore_case: bool = false;
line_numbers: bool = false;
count_only: bool = false;
max: usize | none = none;
files: List<str> = [];
help: bool = false;
}
fn parse_args(args: List<str>) throws (ArgError) -> Options {
let opts = Options { pattern: "" };
let have_pattern = false;
let i: usize = 0;
while i < args.len() {
let a = args[i];
match a {
"-h" | "--help" => { opts.help = true; },
"-i" | "--ignore-case" => { opts.ignore_case = true; },
"-n" | "--line-number" => { opts.line_numbers = true; },
"-c" | "--count" => { opts.count_only = true; },
"-m" | "--max" => {
i += 1;
if i >= args.len() { throw ArgError::MissingValue(a); }
opts.max = try args[i].parse::<usize>() catch _ { throw ArgError::BadNumber(args[i]); };
},
_ => {
if a.starts_with("-") && a.len() > 1 { throw ArgError::Unknown(a); }
if have_pattern { opts.files.push(a); } else { opts.pattern = a; have_pattern = true; }
},
}
i += 1;
}
if !have_pattern && !opts.help { throw ArgError::NoPattern; }
opts
}
fn search(opts: Options, name: str, text: str) {
let needle = if opts.ignore_case { opts.pattern.to_lowercase() } else { opts.pattern };
let hits: usize = 0;
let n: usize = 0;
for line in text.lines() {
n += 1;
let hay = if opts.ignore_case { line.to_lowercase() } else { line };
if !hay.contains(needle) { continue; }
hits += 1;
if !opts.count_only {
let prefix = if opts.files.len() > 1 { f"{name}:" } else { "" };
if opts.line_numbers { println(f"{prefix}{n}:{line}"); } else { println(f"{prefix}{line}"); }
}
if hits == (opts.max ?? usize::MAX) { break; }
}
if opts.count_only { println(f"{hits}"); }
}
fn run(args: List<str>) throws (ArgError | IoError) {
let opts = try parse_args(args);
if opts.help { println(USAGE); return; }
if opts.files.is_empty() { search(opts, "-", SAMPLE); return; }
for path in opts.files {
search(opts, path, try fs::read_to_str(path));
}
}
fn main() -> i32 {
let argv = env::args();
let args: List<str> = [];
for i in 1..argv.len() { args.push(argv[i]); }
// No arguments: demonstrate a few invocations on the built-in sample.
let runs: List<List<str>> = if args.is_empty() {
[["--help"], ["-n", "are"], ["-i", "-c", "ROK"], ["-m", "1", "are"], ["-x", "are"], ["--max"]]
} else {
[args]
};
let status = 0;
for r in runs {
if args.is_empty() {
let line = StrBuf::new();
line.push_str("$ mgrep");
for a in r { line.push_str(f" {a}"); }
println(line.to_str());
}
try run(r)
catch e: ArgError { println(f"mgrep: {e}\n(try --help)"); status = 2; }
catch e: IoError { println(f"mgrep: {e}"); status = 1; };
}
if args.is_empty() { 0 } else { status }
}
Run without arguments, it tries a few built-in sample invocations in turn.
$ mgrep --help
usage: mgrep [-i] [-n] [-c] [-m NUM] PATTERN [FILE]...
-i, --ignore-case match case-insensitively
-n, --line-number prefix each line with its number
-c, --count print only the number of matching lines
-m, --max NUM stop after NUM matches
-h, --help show this help
$ mgrep -n are
1:Roses are red,
2:violets are blue.
4:and so are you.
$ mgrep -i -c ROK
1
$ mgrep -m 1 are
Roses are red,
$ mgrep -x are
mgrep: unknown option '-x'
(try --help)
$ mgrep --max
mgrep: option '--max' needs a value
(try --help)
Walkthrough
env::args()is aList<str>with the program name at[0], so only1..argv.len()is copied (env::args).- The options live in an
Optionsstruct with field defaults. Fields that have a default can be left out, as inOptions { pattern: "" }(Structs, enums and traits, 2.3.3 field defaults). - String literal patterns joined with
|, as inmatch a { "-i" | "--ignore-case" => ... }, accept the short and long form at once. Assignment is a statement, not an expression, so those arm bodies are wrapped in a{ ...; }block (Control flow). - Bad arguments are thrown as the
ArgErrorenum.rundeclaresthrows (ArgError | IoError), andmainuses onecatchper type to choose the exit code (2 or 1) (Errors, 3.14.4catch). - The number after
-mis read withparse::<usize>(); on failure,catch _ { throw ...; }turns it into a friendlier error.opts.maxisusize | none, so the comparison supplies a default with?? usize::MAX.
Running it
To search real files, build an executable and pass arguments.
rokc build mgrep.rok -o mgrep
./mgrep -n -i rok notes.txt todo.txt
./mgrep --count error app.log