///|
fn digit(value : Int) -> Char {
if value < 10 {
('0'.to_int() + value).to_char().unwrap()
} else {
('a'.to_int() + value - 10).to_char().unwrap()
}
}
///|
fn radix_string(value : Int, radix : Int) -> String {
if value == 0 {
return "0"
}
let mut number = value
let mut result = ""
while number > 0 {
result = digit(number % radix).to_string() + result
number /= radix
}
result
}
///|
fn printable(byte : Byte) -> Bool {
byte >= 0x20 && byte <= 0x7E
}
///|
fn records(data : Bytes, minimum : Int) -> Array[(Int, String)] {
let result : Array[(Int, String)] = []
let mut start = 0
let mut index = 0
while index <= data.length() {
if index == data.length() || !printable(data[index]) {
if index - start >= minimum {
let text = @utf8.decode(data[start:index].to_owned()) catch { _ => "" }
if text != "" {
result.push((start, text))
}
}
index += 1
start = index
} else {
index += 1
}
}
result
}
///|
fn help_message() -> String {
let message =
#|Usage: strings [OPTION]... [FILE]...
#|Print the sequences of printable characters in each FILE.
#| -a, --all scan the whole file
#| -f, --print-file-name print the file name before each string
#| -n, --bytes=N print sequences at least N bytes long
#| -o display offsets in octal
#| -t, --radix=RADIX display offsets in b, o, or x radix
#| --help display this help and exit
#| --version display version and exit
message
}
///|
fn parse_radix(value : String) -> Int? {
match value {
"b" => Some(2)
"o" => Some(8)
"x" => Some(16)
_ => None
}
}
///|
async fn emit(
path : String,
data : Bytes,
minimum : Int,
prefix : Bool,
radix : Int?,
) -> Unit {
for (offset, text) in records(data, minimum) {
let mut line = ""
if prefix {
line += path + ": "
}
match radix {
Some(base) => line += "\{radix_string(offset, base)} "
None => ()
}
@stdio.stdout.write(line + text + "\n")
}
}
///|
async fn main {
let parsed = @cli.parse(@env.args()[1:], [
@cli.flag("all", short='a'),
@cli.flag("print-file-name", short='f'),
@cli.option("bytes", short='n'),
@cli.flag("octal", short='o'),
@cli.option("radix", short='t'),
@cli.option("encoding", short='e'),
@cli.flag("help"),
@cli.flag("version"),
]) catch {
@cli.CliError(option~, message~, ..) => {
@stdio.stderr.write("strings: \{message}: '\{option}'\n")
@sys.exit(2)
return
}
}
if parsed.contains("help") {
@stdio.stdout.write(help_message())
return
}
if parsed.contains("version") {
@stdio.stdout.write("strings (cli) \{command_version}\n")
return
}
let minimum = @string.parse_int(parsed.last_value("bytes").unwrap_or("4")) catch {
_ => 0
}
if minimum < 1 {
@stdio.stderr.write("strings: invalid minimum string length\n")
@sys.exit(2)
return
}
if parsed.contains("encoding") && parsed.last_value("encoding") != Some("b") {
@stdio.stderr.write(
"strings: unsupported encoding; only C-locale bytes are supported\n",
)
@sys.exit(2)
return
}
let radix = if parsed.contains("octal") {
Some(8)
} else if parsed.contains("radix") {
match parse_radix(parsed.last_value("radix").unwrap_or("")) {
Some(value) => Some(value)
None => {
@stdio.stderr.write("strings: invalid radix\n")
@sys.exit(2)
return
}
}
} else {
None
}
let sources = if parsed.operands.is_empty() { ["-"] } else { parsed.operands }
let prefix = parsed.contains("print-file-name") || sources.length() > 1
let mut failed = false
for path in sources {
let data = read_source(path) catch {
err => {
@stdio.stderr.write("strings: \{path}: \{err}\n")
failed = true
continue
}
}
emit(path, data, minimum, prefix, radix)
}
if failed {
@sys.exit(1)
}
}
///|
async fn read_source(path : String) -> Bytes {
if path == "-" {
@stdio.stdin.read_all().binary()
} else {
@fs.read_file(path).binary()
}
}