///|
priv suberror TestError {
TestError(String)
}
///|
priv struct Parser {
args : Array[String]
mut pos : Int
}
///|
fn Parser::peek(self : Parser) -> String? {
self.args.get(self.pos)
}
///|
fn Parser::take(self : Parser) -> String raise TestError {
guard self.args.get(self.pos) is Some(value) else {
raise TestError("missing argument")
}
self.pos += 1
value
}
///|
fn is_unary(op : String) -> Bool {
op == "-n" ||
op == "-z" ||
op == "-e" ||
op == "-a" ||
op == "-f" ||
op == "-d" ||
op == "-L" ||
op == "-h" ||
op == "-p" ||
op == "-S" ||
op == "-b" ||
op == "-c" ||
op == "-s" ||
op == "-N" ||
op == "-r" ||
op == "-w" ||
op == "-x"
}
///|
fn is_binary(op : String) -> Bool {
op == "=" ||
op == "==" ||
op == "!=" ||
op == "<" ||
op == ">" ||
op == "-eq" ||
op == "-ne" ||
op == "-lt" ||
op == "-le" ||
op == "-gt" ||
op == "-ge" ||
op == "-nt" ||
op == "-ot"
}
///|
async fn eval_unary(op : String, value : String) -> Bool {
match op {
"-n" => value != ""
"-z" => value == ""
"-e" | "-a" => @fs.exists(value)
"-f" => (@fs.kind(value) catch { _ => Unknown }) == Regular
"-d" => (@fs.kind(value) catch { _ => Unknown }) == Directory
"-L" | "-h" =>
(@fs.kind(value, follow_symlink=false) catch { _ => Unknown }) == SymLink
"-p" => (@fs.kind(value) catch { _ => Unknown }) == Pipe
"-S" => (@fs.kind(value) catch { _ => Unknown }) == Socket
"-b" => (@fs.kind(value) catch { _ => Unknown }) == BlockDevice
"-c" => (@fs.kind(value) catch { _ => Unknown }) == CharDevice
"-s" =>
match @fsops.metadata_if_exists(value, follow_symlink=true) {
Some({ kind: Regular, size: Some(size), .. }) => size > 0L
_ => false
}
"-N" => {
let modified = @fsops.read_file_timestamp(value, ModificationTime) catch {
_ => return false
}
let accessed = @fsops.read_file_timestamp(value, AccessTime) catch {
_ => return false
}
@fsops.compare_timestamps(modified, accessed) > 0
}
"-r" => @fs.can_read(value)
"-w" => @fs.can_write(value)
"-x" => @fs.can_execute(value)
_ => raise TestError("unknown unary operator: '\{op}'")
}
}
///|
fn parse_integer(text : String) -> Int64 raise TestError {
@string.parse_int64(text) catch {
_ => raise TestError("integer expression expected: '\{text}'")
}
}
///|
async fn optional_mtime(path : String) -> @fsops.FileTimestamp? {
try @fsops.read_file_timestamp(path, ModificationTime) catch {
_ => None
} noraise {
timestamp => Some(timestamp)
}
}
///|
async fn eval_binary(left : String, op : String, right : String) -> Bool {
match op {
"=" | "==" => left == right
"!=" => left != right
"<" => left < right
">" => left > right
"-eq" => parse_integer(left) == parse_integer(right)
"-ne" => parse_integer(left) != parse_integer(right)
"-lt" => parse_integer(left) < parse_integer(right)
"-le" => parse_integer(left) <= parse_integer(right)
"-gt" => parse_integer(left) > parse_integer(right)
"-ge" => parse_integer(left) >= parse_integer(right)
"-nt" =>
match (optional_mtime(left), optional_mtime(right)) {
(Some(left), Some(right)) => @fsops.compare_timestamps(left, right) > 0
(Some(_), None) => true
_ => false
}
"-ot" =>
match (optional_mtime(left), optional_mtime(right)) {
(Some(left), Some(right)) => @fsops.compare_timestamps(left, right) < 0
(None, Some(_)) => true
_ => false
}
_ => raise TestError("unknown binary operator: '\{op}'")
}
}
///|
async fn Parser::parse_primary(self : Parser) -> Bool {
let token = self.take()
if token == "(" {
let value = self.parse_or()
if self.take() != ")" {
raise TestError("expected ')'")
}
return value
}
if is_unary(token) {
return eval_unary(token, self.take())
}
match self.peek() {
Some(op) if is_binary(op) => {
ignore(self.take())
eval_binary(token, op, self.take())
}
_ => token != ""
}
}
///|
async fn Parser::parse_not(self : Parser) -> Bool {
if self.peek() == Some("!") {
ignore(self.take())
!self.parse_not()
} else {
self.parse_primary()
}
}
///|
async fn Parser::parse_and(self : Parser) -> Bool {
let mut value = self.parse_not()
while self.peek() == Some("-a") {
ignore(self.take())
let right = self.parse_not()
value = value && right
}
value
}
///|
async fn Parser::parse_or(self : Parser) -> Bool {
let mut value = self.parse_and()
while self.peek() == Some("-o") {
ignore(self.take())
let right = self.parse_and()
value = value || right
}
value
}
///|
async fn main {
let args = @env.args()[1:].to_owned()
if args == ["--help"] {
@stdio.stdout.write(
"Usage: test EXPRESSION\nSupports string, integer, portable file-kind, access, size, and timestamp expressions.\n",
)
return
}
if args.is_empty() {
@sys.exit(1)
return
}
let parser : Parser = { args, pos: 0, }
let result = parser.parse_or() catch {
TestError(message) => {
@stdio.stderr.write("test: \{message}\n")
@sys.exit(2)
return
}
err => {
@stdio.stderr.write("test: \{err}\n")
@sys.exit(2)
return
}
}
if parser.pos != args.length() {
@stdio.stderr.write("test: unexpected argument: '\{args[parser.pos]}'\n")
@sys.exit(2)
return
}
if !result {
@sys.exit(1)
}
}