///|
priv enum SortMode {
SortByName
SortByTime
SortBySize
} derive(Eq)
///|
priv enum LinkMode {
NeverFollowLinks
FollowCommandLineDirectories
FollowCommandLineLinks
FollowAllLinks
} derive(Eq)
///|
priv struct LsOptions {
all : Bool
almost_all : Bool
directory : Bool
classify : Bool
recursive : Bool
reverse : Bool
sort : SortMode
time : @fsops.TimestampKind
links : LinkMode
layout : Layout
width : Int64
tabsize : UInt64
quoting : QuoteStyle
hide_controls : Bool
color : Bool
}
///|
priv struct LsEntry {
name : String
path : String
kind : @fs.FileKind?
timestamp : @fsops.FileTimestamp?
size : Int64?
}
///|
fn join_path(parent : String, name : String) -> String {
if parent == "/" {
"/" + name
} else {
parent + "/" + name
}
}
///|
fn c_locale_compare(left : String, right : String) -> Int32 {
let left = @utf8.encode(left)
let right = @utf8.encode(right)
let shared = left.length().min(right.length())
for index = 0; index < shared; index = index + 1 {
if left[index] < right[index] {
return -1
}
if left[index] > right[index] {
return 1
}
}
if left.length() < right.length() {
-1
} else if left.length() > right.length() {
1
} else {
0
}
}
///|
async fn displayed_kind(
path : String,
links : LinkMode,
command_line : Bool,
) -> @fs.FileKind {
let kind = @fs.kind(path, follow_symlink=false)
if kind == SymLink {
if links == FollowAllLinks ||
(links == FollowCommandLineLinks && command_line) {
// Explicit dereferencing must report dangling links and access failures.
return @fs.kind(path, follow_symlink=true)
}
if links == FollowCommandLineDirectories && command_line {
let target = @fs.kind(path, follow_symlink=true) catch {
@os_error.OSError(_) as error if error.is_ENOENT() ||
is_symlink_loop(error) => SymLink
error => raise error
}
return if target == Directory { Directory } else { SymLink }
}
}
kind
}
///|
fn is_symlink_loop(error : @os_error.OSError) -> Bool {
match (@async.platform, error) {
(Linux, @os_error.OSError(40, ..)) | (MacOS, @os_error.OSError(62, ..)) =>
true
_ => false
}
}
///|
async fn indicator(path : String, kind : @fs.FileKind) -> String {
match kind {
Directory => "/"
SymLink => "@"
Pipe => "|"
Socket => "="
Regular => if @fs.can_execute(path) { "*" } else { "" }
_ => ""
}
}
///|
async fn read_entry(
name : String,
path : String,
options : LsOptions,
command_line : Bool,
) -> LsEntry {
let kind = displayed_kind(path, options.links, command_line)
let follow = kind != SymLink
let timestamp = if options.sort == SortByTime {
Some(@fsops.read_file_timestamp(path, options.time, follow_symlink=follow))
} else {
None
}
let size = if options.sort == SortBySize &&
!(kind == Directory && command_line && !options.directory) {
if kind != Regular {
raise @fsops.FsOpError(
"-S is limited to sets of regular files: '\{path}'",
)
}
let file = @fs.open(path, mode=ReadOnly)
defer file.close()
Some(file.size())
} else {
None
}
{ name, path, kind: Some(kind), timestamp, size, }
}
///|
fn compare_entries(
left : LsEntry,
right : LsEntry,
options : LsOptions,
) -> Int32 {
let primary = match options.sort {
SortByName => c_locale_compare(left.name, right.name)
SortByTime =>
match (left.timestamp, right.timestamp) {
(Some(left), Some(right)) => -@fsops.compare_timestamps(left, right)
(Some(_), None) => -1
(None, Some(_)) => 1
(None, None) => 0
}
SortBySize =>
if left.size.unwrap_or(-1L) > right.size.unwrap_or(-1L) {
-1
} else if left.size.unwrap_or(-1L) < right.size.unwrap_or(-1L) {
1
} else {
0
}
}
let compared = if primary == 0 {
c_locale_compare(left.name, right.name)
} else {
primary
}
if options.reverse {
-compared
} else {
compared
}
}
///|
async fn sorted_entries(
path : String,
options : LsOptions,
status : Ref[Int32],
) -> Array[LsEntry] {
let names = @fs.readdir(
path,
include_hidden=true,
include_special=false,
sort=false,
)
if options.all {
names.push(".")
names.push("..")
}
let entries : Array[LsEntry] = []
for name in names {
if name.has_prefix(".") && !options.all && !options.almost_all {
continue
}
let full_path = join_path(path, name)
let entry = read_entry(name, full_path, options, false) catch {
@fsops.FsOpError(_) as error => raise error
error => {
@stdio.stderr.write("ls: cannot access '\{full_path}': \{error}\n")
status.val = status.val.max(1)
let fallback_kind = try
@fs.kind(full_path, follow_symlink=false)
catch {
_ => None
} noraise {
kind => Some(kind)
}
{
name,
path: full_path,
kind: fallback_kind,
timestamp: None,
size: None,
}
}
}
entries.push(entry)
}
entries.sort_by((left, right) => compare_entries(left, right, options))
entries
}
///|
async fn print_entries(
entries : Array[LsEntry],
options : LsOptions,
palette : Palette?,
used : Ref[Bool],
) -> Unit {
let names : Array[DisplayName] = []
let quoted_flags : Array[Bool] = []
let mut some_quoted = false
for entry in entries {
let (text, width, quoted) = quote_name(
entry.name,
options.quoting,
options.hide_controls,
extra=if options.classify { "=>@|" } else { "" },
)
let suffix = match entry.kind {
Some(kind) if options.classify => indicator(entry.path, kind)
_ => ""
}
let color = match palette {
Some(palette) => entry_color(entry, palette)
None => None
}
names.push({
text,
width: width + suffix.length(),
color,
suffix,
pad: false,
})
quoted_flags.push(quoted)
some_quoted = some_quoted || quoted
}
if some_quoted &&
options.width > 0 &&
options.layout is (Columns | Across) &&
options.quoting is (Shell | ShellEscape | CMaybe) {
for index, name in names {
if !quoted_flags[index] {
names[index] = { ..name, width: name.width + 1, pad: true, }
}
}
}
write_layout(names, options, palette, used, async fn(bytes) {
@stdio.stdout.write(bytes)
})
}
///|
async fn print_directory(
path : String,
options : LsOptions,
show_header : Bool,
printed_section : Ref[Bool],
status : Ref[Int32],
palette : Palette?,
used : Ref[Bool],
) -> Array[String] {
if show_header {
if printed_section.val {
@stdio.stdout.write("\n")
}
let (header, _, _) = quote_name(
path,
options.quoting,
options.hide_controls,
extra=":",
)
@stdio.stdout.write(header + ":\n")
}
printed_section.val = true
let entries = sorted_entries(path, options, status)
print_entries(entries, options, palette, used)
let child_dirs : Array[String] = []
for entry in entries {
if options.recursive &&
entry.kind == Some(Directory) &&
entry.name != "." &&
entry.name != ".." {
child_dirs.push(entry.path)
}
}
child_dirs
}
///|
fn parse_time(value : String) -> @fsops.TimestampKind raise @cli.CliError {
match value {
"atime" | "access" | "use" => AccessTime
"ctime" | "status" => StatusChangeTime
"mtime" | "modification" => ModificationTime
_ =>
raise @cli.CliError(
kind=InvalidValue,
option="time",
message="invalid time selector",
)
}
}
///|
fn parse_arguments(
args : ArrayView[String],
) -> @cli.ParsedArgs raise @cli.CliError {
let normalized : Array[String] = []
let mut options = true
for arg in args {
if arg == "--" {
options = false
}
normalized.push(
if options && arg == "--color" {
"--color=always"
} else {
arg
},
)
}
@cli.parse(normalized[:], [
@cli.flag("all", short='a'),
@cli.flag("almost-all", short='A'),
@cli.flag("directory", short='d'),
@cli.flag("classify", short='F'),
@cli.flag("recursive", short='R'),
@cli.flag("one", short='1'),
@cli.flag("columns", short='C'),
@cli.flag("across", short='x'),
@cli.flag("commas", short='m'),
@cli.option("width", short='w'),
@cli.option("tabsize", short='T'),
@cli.flag("escape", short='b'),
@cli.flag("literal", short='N'),
@cli.flag("quote", short='Q'),
@cli.flag("hide-control-chars", short='q'),
@cli.flag("show-control-chars"),
@cli.option("quoting-style"),
@cli.option("color"),
@cli.flag("sort-time", short='t'),
@cli.flag("access-time", short='u'),
@cli.flag("change-time", short='c'),
@cli.flag("reverse", short='r'),
@cli.flag("sort-size", short='S'),
@cli.flag("dereference-command-line", short='H'),
@cli.flag("dereference", short='L'),
@cli.flag("no-dereference", short='P'),
@cli.option("time"),
@cli.flag("help"),
])
}
///|
fn ls_options(parsed : @cli.ParsedArgs) -> LsOptions raise @cli.CliError {
let layout = match
parsed.last_occurrence(["one", "columns", "across", "commas"]) {
Some("columns") => Columns
Some("across") => Across
Some("commas") => Commas
_ => Lines
}
let quoting = match
parsed.last_occurrence(["escape", "literal", "quote", "quoting-style"]) {
Some("escape") => Escape
Some("literal") => Literal
Some("quote") => CQuote
Some("quoting-style") => {
let value = parsed.last_value("quoting-style").unwrap()
guard quote_style(value) is Some(style) else {
raise @cli.CliError(
kind=InvalidValue,
option=value,
message="invalid quoting style",
)
}
style
}
_ =>
quote_style(@env.get_env_var("QUOTING_STYLE").unwrap_or("literal")).unwrap_or(
Literal,
)
}
let color = match parsed.last_value("color") {
None | Some("never") | Some("no") | Some("none") => false
Some("always") | Some("yes") | Some("force") => true
Some(value) =>
raise @cli.CliError(
kind=InvalidValue,
option=value,
message="color must be always or never; automatic terminal detection is unavailable",
)
}
let (width, _) = dimension(
parsed,
"width",
"COLUMNS",
80UL,
layout != Lines || color,
)
let (tabsize, _) = dimension(
parsed,
"tabsize",
"TABSIZE",
8UL,
layout != Lines,
)
let selected_time = match
parsed.last_occurrence(["access-time", "change-time", "time"]) {
Some("access-time") => @fsops.TimestampKind::AccessTime
Some("change-time") => @fsops.TimestampKind::StatusChangeTime
Some("time") => parse_time(parsed.last_value("time").unwrap())
_ => @fsops.TimestampKind::ModificationTime
}
let links = match
parsed.last_occurrence([
"dereference-command-line", "dereference", "no-dereference",
]) {
Some("dereference") => FollowAllLinks
Some("dereference-command-line") => FollowCommandLineLinks
Some("no-dereference") => NeverFollowLinks
_ =>
if parsed.contains("directory") || parsed.contains("classify") {
NeverFollowLinks
} else {
FollowCommandLineDirectories
}
}
let sort = match parsed.last_occurrence(["sort-time", "sort-size"]) {
Some("sort-time") => SortByTime
Some("sort-size") => SortBySize
_ =>
if parsed.last_occurrence(["access-time", "change-time", "time"])
is Some(_) {
SortByTime
} else {
SortByName
}
}
{
all: parsed.last_occurrence(["all", "almost-all"]) == Some("all"),
almost_all: parsed.contains("almost-all"),
directory: parsed.contains("directory"),
classify: parsed.contains("classify"),
recursive: parsed.contains("recursive"),
reverse: parsed.contains("reverse"),
sort,
time: selected_time,
links,
layout,
width: width.reinterpret_as_int64(),
tabsize,
quoting,
hide_controls: parsed.last_occurrence([
"hide-control-chars", "show-control-chars",
]) ==
Some("hide-control-chars"),
color,
}
}
///|
async fn main {
let metadata_args = @env.args()[1:]
if metadata_args.length() == 1 && metadata_args[0] == "--version" {
@stdio.stdout.write("ls 0.2.3\n")
return
}
let parsed = parse_arguments(@env.args()[1:]) catch {
@cli.CliError(option~, message~, ..) => {
@stdio.stderr.write("ls: \{message}: '\{option}'\n")
@sys.exit(2)
return
}
}
if parsed.contains("help") {
@stdio.stdout.write(
"Usage: ls [-aAdF1RCxmrtucSbqNQ] [-w WIDTH] [-T TABSIZE] [-H|-L|-P] [FILE...]\n --help display this help and exit\n --version display version and exit\n -C lists down columns; -x across rows; -m uses commas; -1 uses lines.\n -w, --width=COLS sets line width (0 unlimited); -T, --tabsize=COLS sets tabs.\n -b escapes names; -N is literal; -Q uses C quotes; -q replaces control bytes.\n --quoting-style=STYLE selects GNU C-locale quoting.\n --show-control-chars displays remaining control bytes.\n --color=always,never controls explicit type/extension colors.\n Active su/sg/ex/ca/mh or tw/ow/st colors require unavailable metadata.\n -H follows command-line links; -L follows all links; -P follows none (extension).\n -u, -c and --time select and imply time sorting unless -S or -t selects a sort.\n -S requires regular files; metadata-rich long listing is not supported.\n",
)
return
}
if invalid_display_option(parsed) is Some((message, status)) {
@stdio.stderr.write(message)
@sys.exit(status)
return
}
let mut options = ls_options(parsed) catch {
@cli.CliError(option~, message~, ..) => {
@stdio.stderr.write("ls: \{message}: '\{option}'\n")
@sys.exit(2)
return
}
}
for
(option, env, default, needed) in [
("width", "COLUMNS", 80UL, options.layout != Lines || options.color),
("tabsize", "TABSIZE", 8UL, options.layout != Lines),
] {
let (_, warning) = dimension(parsed, option, env, default, needed) catch {
_ => (default, None)
}
if warning is Some(warning) {
@stdio.stderr.write(warning)
}
}
if parsed.last_occurrence(["escape", "literal", "quote", "quoting-style"])
is None &&
@env.get_env_var("QUOTING_STYLE") is Some(value) &&
quote_style(value) is None {
@stdio.stderr.write(
"ls: ignoring invalid value of environment variable QUOTING_STYLE: " +
@quoting.locale_quote(@utf8.encode(value)[:]) +
"\n",
)
}
let (palette, color_warning) = color_palette(options.color)
@stdio.stderr.write(color_warning)
if palette is Some(_) {
options = { ..options, tabsize: 0UL, }
}
let used_color = Ref(false)
let operands = parsed.operands
let paths = if operands.is_empty() { ["."] } else { operands }
let status = Ref(0)
let directories : Array[LsEntry] = []
let files : Array[LsEntry] = []
for path in paths {
let entry = read_entry(path, path, options, true) catch {
err => {
@stdio.stderr.write("ls: cannot access '\{path}': \{err}\n")
status.val = 2
continue
}
}
if entry.kind == Some(Directory) && !options.directory {
directories.push(entry)
} else {
files.push(entry)
}
}
files.sort_by((left, right) => compare_entries(left, right, options))
if options.sort == SortBySize && directories.length() > 1 {
@stdio.stderr.write(
"ls: -S cannot sort multiple directory operands without directory size metadata\n",
)
@sys.exit(2)
return
}
directories.sort_by((left, right) => compare_entries(left, right, options))
print_entries(files, options, palette, used_color) catch {
@fsops.FsOpError(message) => {
@stdio.stderr.write("ls: \{message}\n")
@sys.exit(2)
return
}
err => {
@stdio.stderr.write("ls: \{err}\n")
@sys.exit(2)
return
}
}
let printed_section = Ref(!files.is_empty())
let show_top_header = paths.length() > 1 || options.recursive
for directory in directories {
let pending : Array[(String, Array[String])] = [(directory.path, [])]
let mut first = true
while pending.pop() is Some((path, ancestors)) {
let real = @fs.realpath(path) catch {
err => {
@stdio.stderr.write("ls: cannot resolve '\{path}': \{err}\n")
status.val = 2
continue
}
}
if ancestors.contains(real) {
@stdio.stderr.write("ls: recursive symbolic-link cycle at '\{path}'\n")
status.val = 2
continue
}
let next_ancestors = ancestors.copy()
next_ancestors.push(real)
let children = print_directory(
path,
options,
show_top_header || !first,
printed_section,
status,
palette,
used_color,
) catch {
@fsops.FsOpError(message) => {
@stdio.stderr.write("ls: \{message}\n")
status.val = 2
first = false
continue
}
err => {
@stdio.stderr.write("ls: cannot open '\{path}': \{err}\n")
status.val = 2
first = false
continue
}
}
let mut index = children.length()
while index > 0 {
index -= 1
pending.push((children[index], next_ancestors.copy()))
}
first = false
if !options.recursive {
pending.clear()
}
}
}
if used_color.val &&
palette is Some(palette) &&
!(palette.codes["lc"] == b"\x1b[" && palette.codes["rc"] == b"m") {
@stdio.stdout.write(palette.codes["lc"] + palette.codes["rc"])
}
if status.val != 0 {
@sys.exit(status.val)
}
}