IonutParau 687cfebd00 testing version of LuaBIOS and OpenOS
people were having issues getting them to work so now we promote consistency
2025-06-28 20:41:49 +02:00

332 lines
8.3 KiB
Lua

local computer = require("computer")
local shell = require("shell")
local fs = require("filesystem")
local tx = require("transforms")
local text = require("text")
local args, opts = shell.parse(...)
local function die(...)
io.stderr:write(...)
os.exit(1)
end
do -- handle cli
if opts.help then
print([[Usage: tree [OPTION]... [FILE]...
-a, --all do not ignore entries starting with .
--full-time with -l, print time in full iso format
-h, --human-readable with -l, print human readable sizes
--si likewise, but use powers of 1000 not 1024
--level=LEVEL descend only LEVEL directories deep
--color=WHEN WHEN can be
auto - colorize output only if writing to a tty,
always - always colorize output,
never - never colorize output; (default: auto)
-l use a long listing format
-f print the full path prefix for each file
-i do not print indentation lines
-p append "/" indicator to directories
-Q, --quote quote filenames with double quotes
-r, --reverse reverse order while sorting
-S sort by file size
-t sort by modification type, newest first
-X sort alphabetically by entry extension
-C do not count files and directories
-R count root directories like other files
--help print this help and exit]])
return 0
end
if #args == 0 then
table.insert(args, ".")
end
opts.level = tonumber(opts.level) or math.huge
if opts.level < 1 then
die("Invalid level, must be greater than 0")
end
opts.color = opts.color or "auto"
if opts.color == "auto" then
opts.color = io.stdout.tty and "always" or "never"
end
if opts.color ~= "always" and opts.color ~= "never" then
die("Invalid value for --color=WHEN option; WHEN should be auto, always or never")
end
end
local lastYield = computer.uptime()
local function yieldopt()
if computer.uptime() - lastYield > 2 then
lastYield = computer.uptime()
os.sleep(0)
end
end
local function peekable(iterator, state, var1)
local nextItem = {iterator(state, var1)}
return setmetatable({
peek = function()
return table.unpack(nextItem)
end
}, {
__call = coroutine.wrap(function()
while true do
local item = nextItem
nextItem = {iterator(state, nextItem[1])}
coroutine.yield(table.unpack(item))
if nextItem[1] == nil then break end
end
end)
})
end
local function filter(entry)
return opts.a or entry:sub(1, 1) ~= "."
end
local function stat(path)
local st = {}
st.path = path
st.name = fs.name(path) or "/"
st.sortName = st.name:gsub("^%.","")
st.time = fs.lastModified(path)
st.isLink = fs.isLink(path)
st.isDirectory = fs.isDirectory(path)
st.size = st.isLink and 0 or fs.size(path)
st.extension = st.name:match("(%.[^.]+)$") or ""
st.fs = fs.get(path)
return st
end
local colorize
if opts.color == "always" then
-- from /lib/core/full_ls.lua
local colors = tx.foreach(text.split(os.getenv("LS_COLORS") or "", {":"}, true), function(e)
local parts = text.split(e, {"="}, true)
return parts[2], parts[1]
end)
function colorize(stat)
return stat.isLink and colors.ln or
stat.isDirectory and colors.di or
colors["*" .. stat.extension] or
colors.fi
end
end
local function list(path)
return coroutine.wrap(function()
local l = {}
for entry in fs.list(path) do
if filter(entry) then
table.insert(l, stat(fs.concat(path, entry)))
end
end
if opts.S then
table.sort(l, function(a, b)
return a.size < b.size
end)
elseif opts.t then
table.sort(l, function(a, b)
return a.time < b.time
end)
elseif opts.X then
table.sort(l, function(a, b)
return a.extension < b.extension
end)
else
table.sort(l, function(a, b)
return a.sortName < b.sortName
end)
end
for i = opts.r and #l or 1, opts.r and 1 or #l, opts.r and -1 or 1 do
coroutine.yield(l[i])
end
end)
end
local function digRoot(rootPath)
coroutine.yield(stat(rootPath), {})
if not fs.isDirectory(rootPath) then
return
end
local iterStack = {peekable(list(rootPath))}
local pathStack = {rootPath}
local levelStack = {not not iterStack[#iterStack]:peek()}
repeat
local entry = iterStack[#iterStack]()
if entry then
levelStack[#levelStack] = not not iterStack[#iterStack]:peek()
local path = fs.concat(fs.concat(table.unpack(pathStack)), entry.name)
coroutine.yield(entry, levelStack)
if entry.isDirectory and opts.level > #levelStack then
table.insert(iterStack, peekable(list(path)))
table.insert(pathStack, entry.name)
table.insert(levelStack, not not iterStack[#iterStack]:peek())
end
else
table.remove(iterStack)
table.remove(pathStack)
table.remove(levelStack)
end
until #iterStack == 0
end
local function dig(roots)
return coroutine.wrap(function()
for _, root in ipairs(roots) do
digRoot(root)
end
end)
end
local function nod(n) -- from /lib/core/full_ls.lua
return n and (tostring(n):gsub("(%.[0-9]+)0+$","%1")) or "0"
end
local function formatFSize(size) -- from /lib/core/full_ls.lua
if not opts.h and not opts["human-readable"] and not opts.si then
return tostring(size)
end
local sizes = {"", "K", "M", "G"}
local unit = 1
local power = opts.si and 1000 or 1024
while size > power and unit < #sizes do
unit = unit + 1
size = size / power
end
return nod(math.floor(size*10)/10)..sizes[unit]
end
local function pad(txt) -- from /lib/core/full_ls.lua
txt = tostring(txt)
return #txt >= 2 and txt or "0" .. txt
end
local function formatTime(epochms) -- from /lib/core/full_ls.lua
local month_names = {"January","February","March","April","May","June",
"July","August","September","October","November","December"}
if epochms == 0 then return "" end
local d = os.date("*t", epochms)
local day, hour, min, sec = nod(d.day), pad(nod(d.hour)), pad(nod(d.min)), pad(nod(d.sec))
if opts["full-time"] then
return string.format("%s-%s-%s %s:%s:%s ", d.year, pad(nod(d.month)), pad(day), hour, min, sec)
else
return string.format("%s %2s %2s:%2s ", month_names[d.month]:sub(1,3), day, hour, pad(min))
end
end
local function writeEntry(entry, levelStack)
for i, hasNext in ipairs(levelStack) do
if opts.i then break end
if i == #levelStack then
if hasNext then
io.write("├── ")
else
io.write("└── ")
end
else
if hasNext then
io.write("│   ")
else
io.write(" ")
end
end
end
if opts.l then
io.write("[")
io.write(entry.isDirectory and "d" or entry.isLink and "l" or "f", "-")
io.write("r", entry.fs.isReadOnly() and "-" or "w", " ")
io.write(formatFSize(entry.size), " ")
io.write(formatTime(entry.time))
io.write("] ")
end
if opts.Q then io.write('"') end
if opts.color == "always" then
io.write("\27[" .. colorize(entry) .. "m")
end
if opts.f then
io.write(entry.path)
else
io.write(entry.name)
end
if opts.color == "always" then
io.write("\27[0m")
end
if opts.p and entry.isDirectory then
io.write("/")
end
if opts.Q then io.write('"') end
io.write("\n")
end
local function writeCount(dirs, files)
io.write("\n")
io.write(dirs, " director", dirs == 1 and "y" or "ies")
io.write(", ")
io.write(files, " file", files == 1 and "" or "s")
io.write("\n")
end
local dirs, files = 0, 0
local roots = {}
for _, arg in ipairs(args) do
local path = shell.resolve(arg)
local real, reason = fs.realPath(path)
if not real then
die("cannot access ", path, ": ", reason or "unknown error")
elseif not fs.exists(path) then
die("cannot access ", path, ":", "No such file or directory")
else
table.insert(roots, real)
end
end
for entry, levelStack in dig(roots) do
if opts.R or #levelStack > 0 then
if entry.isDirectory then
dirs = dirs + 1
else
files = files + 1
end
end
writeEntry(entry, levelStack)
yieldopt()
end
if not opts.C then
writeCount(dirs, files)
end