///|
priv enum Layout {
Lines
Columns
Across
Commas
} derive(Eq)
///|
priv enum QuoteStyle {
Literal
Shell
ShellAlways
ShellEscape
ShellEscapeAlways
CQuote
CMaybe
Escape
Locale
} derive(Eq)
///|
fn quote_style(value : String) -> QuoteStyle? {
match value {
"literal" => Some(Literal)
"shell" => Some(Shell)
"shell-always" => Some(ShellAlways)
"shell-escape" => Some(ShellEscape)
"shell-escape-always" => Some(ShellEscapeAlways)
"c" | "clocale" => Some(CQuote)
"c-maybe" => Some(CMaybe)
"escape" => Some(Escape)
"locale" => Some(Locale)
_ => {
let candidates = [
"literal", "shell", "shell-always", "shell-escape", "shell-escape-always",
"c", "c-maybe", "escape", "locale", "clocale",
].filter(style => style.has_prefix(value))
if candidates.length() == 1 {
quote_style(candidates[0])
} else {
None
}
}
}
}
///|
fn invalid_display_option(parsed : @cli.ParsedArgs) -> (String, Int32)? {
let counts : Map[String, Int32] = Map([])
for key in parsed.order {
if !["width", "tabsize", "quoting-style", "color"].contains(key) {
continue
}
let index = counts.get(key).unwrap_or(0)
counts[key] = index + 1
let value = parsed.values(key)[index]
if key == "width" || key == "tabsize" {
let number = parse_dimension(value)
let range = key == "tabsize" &&
(
number is Some((_, true)) ||
(number is Some((n, _)) && n > 9223372036854775807UL)
)
if number is None || range {
let what = if key == "width" { "line width" } else { "tab size" }
return Some(
(
"ls: invalid " +
what +
": " +
@quoting.locale_quote(@utf8.encode(value)[:]) +
(if range { ": Value too large for defined data type" } else { "" }) +
"\n",
2,
),
)
}
} else if (key == "quoting-style" && quote_style(value) is None) ||
(
key == "color" &&
![
"always", "yes", "force", "never", "no", "none", "auto", "tty", "if-tty",
].contains(value)
) {
let valid = if key == "quoting-style" {
" - 'literal'\n - 'shell'\n - 'shell-always'\n - 'shell-escape'\n - 'shell-escape-always'\n - 'c'\n - 'c-maybe'\n - 'escape'\n - 'locale'\n - 'clocale'\n"
} else {
" - 'always', 'yes', 'force'\n - 'never', 'no', 'none'\n - 'auto', 'tty', 'if-tty'\n"
}
let ambiguous = key == "quoting-style" &&
[
"literal", "shell", "shell-always", "shell-escape", "shell-escape-always",
"c", "c-maybe", "escape", "locale", "clocale",
].any(name => name.has_prefix(value))
return Some(
(
(if ambiguous {
"ls: ambiguous argument "
} else {
"ls: invalid argument "
}) +
@quoting.locale_quote(@utf8.encode(value)[:]) +
" for '--" +
key +
"'\nValid arguments are:\n" +
valid +
"Try 'ls --help' for more information.\n",
1,
),
)
}
}
None
}
///|
fn c_escape(bytes : BytesView, quotes : Bool, extra~ : String = "") -> String {
let out = StringBuilder()
if quotes {
out.write_char('"')
}
for byte in bytes {
let escaped = match byte {
7 => Some('a')
8 => Some('b')
9 => Some('t')
10 => Some('n')
11 => Some('v')
12 => Some('f')
13 => Some('r')
b'\\' => Some('\\')
b'"' if quotes => Some('"')
b' ' if !quotes => Some(' ')
_ =>
if !quotes && extra.contains(byte.to_int().unsafe_to_char().to_string()) {
Some(byte.to_int().unsafe_to_char())
} else {
None
}
}
match escaped {
Some(c) => {
out.write_char('\\')
out.write_char(c)
}
None => {
let code = byte.to_int()
if byte >= 32 && byte < 127 {
out.write_char(code.unsafe_to_char())
} else {
out.write_char('\\')
out.write_char((48 + code / 64).unsafe_to_char())
out.write_char((48 + code / 8 % 8).unsafe_to_char())
out.write_char((48 + code % 8).unsafe_to_char())
}
}
}
}
if quotes {
out.write_char('"')
}
out.to_string()
}
///|
fn shell_literal(name : String, force : Bool) -> String {
let bytes = @utf8.encode(name)
let mut quote = force || bytes.is_empty()
let mut apostrophe = false
let mut double_compatible = true
for index, byte in bytes {
if byte == b'\'' {
apostrophe = true
}
if !(byte
is (b'0'..<=b'9'
| b'A'..<=b'Z'
| b'a'..<=b'z'
| b'%'
| b'+'
| b','
| b'-'
| b'.'
| b'/'
| b':'
| b']'
| b'_'
| b'@'
| b' '
| b'\'')) {
double_compatible = false
}
if byte
is (b' '
| b'\t'
| b'\n'
| b'\r'
| b'\''
| b'"'
| b'$'
| b'`'
| b'\\'
| b'&'
| b';'
| b'|'
| b'<'
| b'='
| b'>'
| b'('
| b')'
| b'['
| b'?'
| b'*') ||
(index == 0 && byte is (b'#' | b'~')) ||
(bytes.length() == 1 && byte is (b'{' | b'}')) {
quote = true
}
}
if !quote {
name
} else if apostrophe && double_compatible {
"\"" + name + "\""
} else {
"'" + name.replace_all(old="'", new="'\\''") + "'"
}
}
///|
fn quote_name(
name : String,
style : QuoteStyle,
hide : Bool,
extra~ : String = "",
) -> (String, Int32, Bool) {
let bytes = @utf8.encode(name)
let special = bytes
.iter()
.any(byte => extra.contains(byte.to_int().unsafe_to_char().to_string()))
let mut quoted = true
let text = match style {
Literal => {
quoted = false
name
}
Shell | ShellAlways => {
let result = shell_literal(name, style == ShellAlways || special)
quoted = result != name
result
}
ShellEscape | ShellEscapeAlways => {
let result = @quoting.shell_escape(
bytes[:],
force=style == ShellEscapeAlways || special,
)
quoted = result != name
result
}
CQuote => c_escape(bytes[:], true)
CMaybe => {
let need = special ||
bytes.iter().any(b => b < 32 || b >= 127 || b == b'"')
quoted = need
if need {
c_escape(bytes[:], true)
} else {
name
}
}
Escape => {
quoted = false
c_escape(bytes[:], false, extra~)
}
Locale => @quoting.locale_quote(bytes[:])
}
let encoded = @utf8.encode(text)
let mut width = 0
if hide {
let out = StringBuilder()
for byte in encoded {
if byte >= 32 && byte < 127 {
out.write_char(byte.to_int().unsafe_to_char())
} else {
out.write_char('?')
}
}
(out.to_string(), encoded.length(), quoted)
} else {
for byte in encoded {
if byte >= 32 && byte < 127 {
width += 1
}
}
(text, width, quoted)
}
}
///|
// A line width above PTRDIFF_MAX means unlimited in the fixed GNU profile.
fn parse_dimension(value : String) -> (UInt64, Bool)? {
let bytes = @utf8.encode(value)
let mut index = 0
while index < bytes.length() &&
bytes[index] is (b' ' | b'\t' | b'\n' | b'\r' | 11 | 12) {
index += 1
}
if index < bytes.length() && bytes[index] == b'+' {
index += 1
}
guard index < bytes.length() && bytes[index] != b'-' else { return None }
let mut base = 10UL
if bytes[index] == b'0' {
base = 8UL
if index + 1 < bytes.length() && bytes[index + 1] is (b'x' | b'X') {
base = 16UL
index += 2
}
}
let start = index
let mut result = 0UL
let mut overflow = false
while index < bytes.length() {
let digit = match bytes[index] {
b'0'..<=b'9' as b => b.to_int() - 48
b'a'..<=b'f' as b => b.to_int() - 87
b'A'..<=b'F' as b => b.to_int() - 55
_ => return None
}
guard digit.to_uint64() < base else { return None }
if result > (18446744073709551615UL - digit.to_uint64()) / base {
overflow = true
}
result = result * base + digit.to_uint64()
index += 1
}
if index == start {
None
} else {
Some((result, overflow))
}
}
///|
fn dimension(
parsed : @cli.ParsedArgs,
option : String,
env : String,
default : UInt64,
needed : Bool,
) -> (UInt64, String?) raise @cli.CliError {
if parsed.last_value(option) is Some(value) {
let result = parse_dimension(value)
let good = result is Some((_, false))
if option == "width" && result is Some((number, overflow)) {
return (
if overflow || number > 9223372036854775807UL {
0UL
} else {
number
},
None,
)
}
if good && result is Some((number, _)) && number <= 9223372036854775807UL {
return (number, None)
}
raise @cli.CliError(
kind=InvalidValue,
option=value,
message=if option == "width" {
"invalid line width"
} else {
"invalid tab size"
},
)
}
if needed &&
@env.get_env_var(env) is Some(value) &&
(env != "COLUMNS" || value != "") {
if parse_dimension(value) is Some((number, overflow)) {
if option == "width" {
return (
if overflow || number > 9223372036854775807UL {
0UL
} else {
number
},
None,
)
}
if !overflow {
return (number, None)
}
}
let what = if option == "width" { "width" } else { "tab size" }
return (
default,
Some(
"ls: ignoring invalid \{what} in environment variable \{env}: " +
@quoting.locale_quote(@utf8.encode(value)[:]) +
"\n",
),
)
}
(default, None)
}
///|
priv struct DisplayName {
text : String
width : Int32
color : Bytes?
suffix : String
pad : Bool
}
///|
fn column_widths(
names : Array[DisplayName],
columns : Int32,
down : Bool,
) -> Array[Int32] {
let widths = Array::make(columns, 3)
let rows = (names.length() + columns - 1) / columns
for index, name in names {
let col = if down { index / rows } else { index % columns }
widths[col] = widths[col].max(
name.width + (if col == columns - 1 { 0 } else { 2 }),
)
}
widths
}
///|
fn indent(
out : @buffer.Buffer,
from : Int32,
to : Int32,
tabs : UInt64,
) -> Unit {
let mut position = from
while position < to {
if tabs != 0UL && to.to_uint64() / tabs > (position + 1).to_uint64() / tabs {
out.write_byte(b'\t')
position += (tabs - position.to_uint64() % tabs).to_int()
} else {
out.write_byte(b' ')
position += 1
}
}
}
///|
async fn write_layout(
names : Array[DisplayName],
options : LsOptions,
palette : Palette?,
used : Ref[Bool],
emit : async (Bytes) -> Unit,
) -> Unit {
if names.is_empty() {
return
}
let out = @buffer.Buffer()
let flush = async fn() {
if !out.is_empty() {
emit(out.to_bytes())
out.reset()
}
}
let put_color = (palette : Palette, sequence : Bytes) => {
if !used.val {
used.val = true
out.write_bytes(color_reset(palette)[:])
}
out.write_bytes(sequence[:])
}
let write_name = (index : Int32, position : Int32) => {
let name = names[index]
let mut colored = false
if palette is Some(palette) {
if active(palette, "no") {
put_color(palette, color_sequence(palette, palette.codes["no"]))
}
}
if name.pad {
out.write_byte(b' ')
}
if palette is Some(palette) {
if name.color is Some(sequence) {
if active(palette, "no") {
put_color(palette, palette.codes["lc"] + palette.codes["rc"])
}
put_color(palette, color_sequence(palette, sequence))
colored = true
}
}
out.write_string_utf8(name.text)
if palette is Some(palette) && (colored || active(palette, "no")) {
put_color(palette, color_reset(palette))
let length = @utf8.encode(name.text).length() +
(if name.pad { 1 } else { 0 })
if options.width > 0 &&
length > 0 &&
position.to_int64() / options.width !=
(position + length - 1).to_int64() / options.width {
put_color(palette, palette.codes.get("cl").unwrap_or(b""))
}
}
out.write_string_utf8(name.suffix)
}
if options.layout == Lines {
for index = 0; index < names.length(); index = index + 1 {
write_name(index, 0)
out.write_byte(b'\n')
if out.length() >= 65536 {
flush()
}
}
} else if options.layout == Commas {
let mut position = 0
for index, name in names {
if index > 0 {
out.write_byte(b',')
if options.width == 0 ||
(position + name.width + 2).to_int64() < options.width {
out.write_byte(b' ')
position += 2
} else {
out.write_byte(b'\n')
position = 0
}
}
write_name(index, position)
position += name.width
if out.length() >= 65536 {
flush()
}
}
out.write_byte(b'\n')
} else if options.width == 0 {
let mut position = 0
for index, name in names {
if index > 0 {
out.write_bytes(b" ")
position += 2
}
write_name(index, position)
position += name.width
if out.length() >= 65536 {
flush()
}
}
out.write_byte(b'\n')
} else {
let down = options.layout == Columns
let maximum = options.width / 3L +
(if options.width % 3L != 0L { 1L } else { 0L })
let mut columns = names.length().to_int64().min(maximum).to_int().max(1)
let mut widths = column_widths(names, columns, down)
while columns > 1 &&
widths.fold(init=0L, (sum, width) => sum + width.to_int64()) >=
options.width {
columns -= 1
widths = column_widths(names, columns, down)
}
let rows = (names.length() + columns - 1) / columns
for row = 0; row < rows; row = row + 1 {
let mut position = 0
for col = 0; col < columns; col = col + 1 {
let index = if down { col * rows + row } else { row * columns + col }
if index >= names.length() {
break
}
write_name(index, position)
let next = if down { index + rows } else { index + 1 }
if col + 1 < columns && next < names.length() {
indent(
out,
position + names[index].width,
position + widths[col],
options.tabsize,
)
position += widths[col]
}
if out.length() >= 65536 {
flush()
}
}
out.write_byte(b'\n')
if out.length() >= 65536 {
flush()
}
}
}
flush()
}