From 426c856dfbd21339ce09cd1f27ed2aadd7e0e7da Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Fri, 5 Apr 2019 17:32:22 -0400 Subject: [PATCH] refactor parallel code --- common/multiMiner.lua | 42 ++-- milo/apis/craft2.lua | 22 +- milo/apis/taskRunner.lua | 35 +++ milo/plugins/exportTask.lua | 130 +++++----- milo/plugins/importTask.lua | 103 ++++---- milo/plugins/inputChestTask.lua | 36 ++- milo/plugins/trashcanView.lua | 26 +- openos/.package | 6 + openos/apis/computer.lua | 3 + openos/apis/filesystem.lua | 59 +++++ openos/apis/keyboard.lua | 3 + openos/apis/sh.lua | 6 + openos/apis/shell.lua | 9 + openos/apis/term.lua | 1 + openos/apis/text.lua | 405 ++++++++++++++++++++++++++++++++ openos/apis/transfer.lua | 269 +++++++++++++++++++++ openos/apis/transforms.lua | 200 ++++++++++++++++ openos/apis/tty.lua | 23 ++ openos/apis/unicode.lua | 3 + openos/autorun/startup.lua | 11 + openos/cat.lua | 30 +++ openos/cp.lua | 36 +++ openos/df.lua | 76 ++++++ openos/dmesg.lua | 33 +++ openos/du.lua | 128 ++++++++++ openos/find.lua | 132 +++++++++++ openos/grep.lua | 323 +++++++++++++++++++++++++ openos/head.lua | 137 +++++++++++ openos/hostname.lua | 17 ++ openos/less.lua | 149 ++++++++++++ openos/ln.lua | 35 +++ openos/ls.lua | 374 +++++++++++++++++++++++++++++ openos/mv.lua | 33 +++ openos/ps.lua | 155 ++++++++++++ openos/rm.lua | 158 +++++++++++++ openos/rmdir.lua | 104 ++++++++ openos/set.lua | 23 ++ openos/source.lua | 36 +++ openos/time.lua | 18 ++ openos/touch.lua | 54 +++++ openos/tree.lua | 331 ++++++++++++++++++++++++++ openos/unalias.lua | 19 ++ openos/unset.lua | 9 + openos/which.lua | 25 ++ 44 files changed, 3645 insertions(+), 182 deletions(-) create mode 100644 milo/apis/taskRunner.lua create mode 100644 openos/.package create mode 100644 openos/apis/computer.lua create mode 100644 openos/apis/filesystem.lua create mode 100644 openos/apis/keyboard.lua create mode 100644 openos/apis/sh.lua create mode 100644 openos/apis/shell.lua create mode 100644 openos/apis/term.lua create mode 100644 openos/apis/text.lua create mode 100644 openos/apis/transfer.lua create mode 100644 openos/apis/transforms.lua create mode 100644 openos/apis/tty.lua create mode 100644 openos/apis/unicode.lua create mode 100644 openos/autorun/startup.lua create mode 100644 openos/cat.lua create mode 100644 openos/cp.lua create mode 100644 openos/df.lua create mode 100644 openos/dmesg.lua create mode 100644 openos/du.lua create mode 100644 openos/find.lua create mode 100644 openos/grep.lua create mode 100644 openos/head.lua create mode 100644 openos/hostname.lua create mode 100644 openos/less.lua create mode 100644 openos/ln.lua create mode 100644 openos/ls.lua create mode 100644 openos/mv.lua create mode 100644 openos/ps.lua create mode 100644 openos/rm.lua create mode 100644 openos/rmdir.lua create mode 100644 openos/set.lua create mode 100644 openos/source.lua create mode 100644 openos/time.lua create mode 100644 openos/touch.lua create mode 100644 openos/tree.lua create mode 100644 openos/unalias.lua create mode 100644 openos/unset.lua create mode 100644 openos/which.lua diff --git a/common/multiMiner.lua b/common/multiMiner.lua index 65263f8..61aae52 100644 --- a/common/multiMiner.lua +++ b/common/multiMiner.lua @@ -233,27 +233,29 @@ local function run(member, point) end local function drawContainer(pos) - canvas3d.clear() + if canvas3d then + canvas3d.clear() - local function addBox(b) - canvas3d.addBox( - b.x - offset.x + .25, - b.y - offset.y + .25 , - b.z - offset.z + .25 , - .5, .5, .5).setDepthTested(false) - end - if box and box.ex then - addBox({ x = box.x, y = box.y, z = box.z }) - addBox({ x = box.x, y = box.y, z = box.ez }) - addBox({ x = box.ex, y = box.y, z = box.z }) - addBox({ x = box.ex, y = box.y, z = box.ez }) - addBox({ x = box.x, y = box.ey, z = box.z }) - addBox({ x = box.x, y = box.ey, z = box.ez }) - addBox({ x = box.ex, y = box.ey, z = box.z }) - addBox({ x = box.ex, y = box.ey, z = box.ez }) - elseif box then - canvas3d.recenter({ -(pos.x % 1), -(pos.y % 1), -(pos.z % 1) }) - addBox(box) + local function addBox(b) + canvas3d.addBox( + b.x - offset.x + .25, + b.y - offset.y + .25 , + b.z - offset.z + .25 , + .5, .5, .5).setDepthTested(false) + end + if box and box.ex then + addBox({ x = box.x, y = box.y, z = box.z }) + addBox({ x = box.x, y = box.y, z = box.ez }) + addBox({ x = box.ex, y = box.y, z = box.z }) + addBox({ x = box.ex, y = box.y, z = box.ez }) + addBox({ x = box.x, y = box.ey, z = box.z }) + addBox({ x = box.x, y = box.ey, z = box.ez }) + addBox({ x = box.ex, y = box.ey, z = box.z }) + addBox({ x = box.ex, y = box.ey, z = box.ez }) + elseif box then + canvas3d.recenter({ -(pos.x % 1), -(pos.y % 1), -(pos.z % 1) }) + addBox(box) + end end end diff --git a/milo/apis/craft2.lua b/milo/apis/craft2.lua index 5203052..d3ddabd 100644 --- a/milo/apis/craft2.lua +++ b/milo/apis/craft2.lua @@ -1,8 +1,8 @@ local itemDB = require('core.itemDB') +local Tasks = require('milo.taskRunner') local Util = require('util') local fs = _G.fs -local parallel = _G.parallel local turtle = _G.turtle local Craft = { @@ -37,12 +37,18 @@ end function Craft.clearGrid(storage) local success = true + local tasks = Tasks() for index, slot in pairs(storage.turtleInventory.adapter.list()) do - if storage:import(storage.turtleInventory, index, slot.count, slot) ~= slot.count then - success = false - end + tasks:add(function() + if storage:import(storage.turtleInventory, index, slot.count, slot) ~= slot.count then + success = false + end + end) end + + tasks:run() + return success end @@ -133,10 +139,11 @@ local function turtleCraft(recipe, storage, request, count) end local failed - local fns = { } + local tasks = Tasks() + for k,v in pairs(recipe.ingredients) do local item = splitKey(v) - table.insert(fns, function() + tasks:add(function() if storage:export(storage.turtleInventory, k, count, item) ~= count then request.status = 'rescan needed ?' request.statusCode = Craft.STATUS_ERROR @@ -146,7 +153,8 @@ local function turtleCraft(recipe, storage, request, count) end) end - parallel.waitForAll(table.unpack(fns)) + tasks:run() + if failed then Craft.clearGrid(storage) return diff --git a/milo/apis/taskRunner.lua b/milo/apis/taskRunner.lua new file mode 100644 index 0000000..ac1a2b3 --- /dev/null +++ b/milo/apis/taskRunner.lua @@ -0,0 +1,35 @@ +local class = require('class') + +local parallel = _G.parallel + +local TaskRunner = class() + +function TaskRunner:init(args) + self.tasks = { } + self.errorMsg = 'Task failed: ' + + for k,v in pairs(args or { }) do + self[k] = v + end +end + +function TaskRunner:add(fn) + table.insert(self.tasks, function() + local s, m = pcall(fn) + if not s and m then + self:onError(m) + end + end) +end + +function TaskRunner:run() + if #self.tasks > 0 then + parallel.waitForAll(table.unpack(self.tasks)) + end +end + +function TaskRunner:onError(msg) + _G._debug(msg.errorMsg .. msg) +end + +return TaskRunner diff --git a/milo/plugins/exportTask.lua b/milo/plugins/exportTask.lua index dd791a3..c80159e 100644 --- a/milo/plugins/exportTask.lua +++ b/milo/plugins/exportTask.lua @@ -1,7 +1,6 @@ local itemDB = require('core.itemDB') local Milo = require('milo') - -local parallel = _G.parallel +local Tasks = require('milo.taskRunner') local ExportTask = { name = 'exporter', @@ -13,86 +12,81 @@ local function filter(a) end function ExportTask:cycle(context) - local tasks = { } + local tasks = Tasks({ + errorMsg = 'EXPORTER error: ', + }) for node in context.storage:filterActive('machine', filter) do - table.insert(tasks, function() - local s, m = pcall(function() - for _, entry in pairs(node.exports) do + tasks:add(function() + for _, entry in pairs(node.exports) do - if not entry.filter then - -- exports must have a filter - -- TODO: validate in exportView - break + if not entry.filter then + -- exports must have a filter + -- TODO: validate in exportView + break + end + + local function exportSingleSlot() + local slot = node.adapter.getItemMeta(entry.slot) + + if slot and slot.count == slot.maxCount then + return end - local function exportSingleSlot() - local slot = node.adapter.getItemMeta(entry.slot) - - if slot and slot.count == slot.maxCount then - return - end - - if slot then - -- something is in the slot, find what we can export - for key in pairs(entry.filter) do - local filterItem = itemDB:splitKey(key) - if (slot.name == filterItem.name and - (entry.ignoreDamage or slot.damage == filterItem.damage) and - (entry.ignoreNbtHash or slot.nbtHash == filterItem.nbtHash)) then - - local items = Milo:getMatches(filterItem, entry) - local _, item = next(items) - if item then - local count = math.min(item.count, slot.maxCount - slot.count) - context.storage:export(node, entry.slot, count, item) - end - break - end - end - return - end - - -- slot is empty - export first matching item we have in storage + if slot then + -- something is in the slot, find what we can export for key in pairs(entry.filter) do - local items = Milo:getMatches(itemDB:splitKey(key), entry) - local _, item = next(items) - if item then - local count = math.min(item.count, itemDB:getMaxCount(item)) - context.storage:export(node, entry.slot, count, item) + local filterItem = itemDB:splitKey(key) + if (slot.name == filterItem.name and + (entry.ignoreDamage or slot.damage == filterItem.damage) and + (entry.ignoreNbtHash or slot.nbtHash == filterItem.nbtHash)) then + + local items = Milo:getMatches(filterItem, entry) + local _, item = next(items) + if item then + local count = math.min(item.count, slot.maxCount - slot.count) + context.storage:export(node, entry.slot, count, item) + end + break + end + end + return + end + + -- slot is empty - export first matching item we have in storage + for key in pairs(entry.filter) do + local items = Milo:getMatches(itemDB:splitKey(key), entry) + local _, item = next(items) + if item then + local count = math.min(item.count, itemDB:getMaxCount(item)) + context.storage:export(node, entry.slot, count, item) + break + end + end + end + + local function exportItems() + for key in pairs(entry.filter) do + local items = Milo:getMatches(itemDB:splitKey(key), entry) + for _,item in pairs(items) do + if context.storage:export(node, nil, item.count, item) == 0 then + -- TODO: really shouldn't break here as there may be room in other slots + -- leaving for now for performance reasons break end end end - - local function exportItems() - for key in pairs(entry.filter) do - local items = Milo:getMatches(itemDB:splitKey(key), entry) - for _,item in pairs(items) do - if context.storage:export(node, nil, item.count, item) == 0 then - -- TODO: really shouldn't break here as there may be room in other slots - -- leaving for now for performance reasons - break - end - end - end - end - if type(entry.slot) == 'number' then - exportSingleSlot() - else - exportItems() - end end - end) - - if not s and m then - _G._debug('EXPORTER error: ' .. m) + if type(entry.slot) == 'number' then + exportSingleSlot() + else + exportItems() + end end end) end - if #tasks > 0 then - parallel.waitForAll(table.unpack(tasks)) - end + + tasks:run() end Milo:registerTask(ExportTask) diff --git a/milo/plugins/importTask.lua b/milo/plugins/importTask.lua index f27a830..1ec1568 100644 --- a/milo/plugins/importTask.lua +++ b/milo/plugins/importTask.lua @@ -1,7 +1,6 @@ local itemDB = require('core.itemDB') local Milo = require('milo') - -local parallel = _G.parallel +local Tasks = require('milo.taskRunner') local ImportTask = { name = 'importer', @@ -13,73 +12,67 @@ local function filter(a) end function ImportTask:cycle(context) - local tasks = { } + local tasks = Tasks({ + errorMsg = 'IMPORT error: ' + }) for node in context.storage:filterActive('machine', filter) do - table.insert(tasks, function() - local s, m = pcall(function() - for _, entry in pairs(node.imports) do + tasks:add(function() + for _, entry in pairs(node.imports) do - local function itemMatchesFilter(item) - if not entry.ignoreDamage and not entry.ignoreNbtHash then - local key = itemDB:makeKey(item) - return entry.filter[key] - end - - for key in pairs(entry.filter) do - local v = itemDB:splitKey(key) - if item.name == v.name and - (entry.ignoreDamage or item.damage == v.damage) and - (entry.ignoreNbtHash or item.nbtHash == v.nbtHash) then - return true - end - end + local function itemMatchesFilter(item) + if not entry.ignoreDamage and not entry.ignoreNbtHash then + local key = itemDB:makeKey(item) + return entry.filter[key] end - local function matchesFilter(item) - if not entry.filter then + for key in pairs(entry.filter) do + local v = itemDB:splitKey(key) + if item.name == v.name and + (entry.ignoreDamage or item.damage == v.damage) and + (entry.ignoreNbtHash or item.nbtHash == v.nbtHash) then return true end - - if entry.blacklist then - return not itemMatchesFilter(item) - end - - return itemMatchesFilter(item) - end - - local list = node.adapter.list() - - local function importSlot(slotNo) - local item = itemDB:get(list[slotNo], function() - return node.adapter.getItemMeta(slotNo) - end) - if item and matchesFilter(item) then - context.storage:import(node, slotNo, item.count, item) - end - end - - if type(entry.slot) == 'number' then - if list[entry.slot] then - importSlot(entry.slot) - end - else - for i in pairs(list) do - importSlot(i) - end end end - end) - if not s and m then - _G._debug('IMPORTER error: ' .. m) + local function matchesFilter(item) + if not entry.filter then + return true + end + + if entry.blacklist then + return not itemMatchesFilter(item) + end + + return itemMatchesFilter(item) + end + + local list = node.adapter.list() + + local function importSlot(slotNo) + local item = itemDB:get(list[slotNo], function() + return node.adapter.getItemMeta(slotNo) + end) + if item and matchesFilter(item) then + context.storage:import(node, slotNo, item.count, item) + end + end + + if type(entry.slot) == 'number' then + if list[entry.slot] then + importSlot(entry.slot) + end + else + for i in pairs(list) do + importSlot(i) + end + end end end) end - if #tasks > 0 then - parallel.waitForAll(table.unpack(tasks)) - end + tasks:run() end Milo:registerTask(ImportTask) diff --git a/milo/plugins/inputChestTask.lua b/milo/plugins/inputChestTask.lua index 09dce5b..1190dd0 100644 --- a/milo/plugins/inputChestTask.lua +++ b/milo/plugins/inputChestTask.lua @@ -1,6 +1,5 @@ -local Milo = require('milo') - -local parallel = _G.parallel +local Milo = require('milo') +local Tasks = require('milo.taskRunner') local InputChest = { name = 'input', @@ -8,29 +7,24 @@ local InputChest = { } function InputChest:cycle(context) - local tasks = { } - for node in context.storage:filterActive('input') do - table.insert(tasks, function() - local s, m = pcall(function() - for slot, item in pairs(node.adapter.list()) do - local s, m = pcall(function() - context.storage:import(node, slot, item.count, item) - end) - if not s and m then - _G._debug('INPUT error: ' .. m) - end - end - end) + local tasks = Tasks({ + errorMsg = 'INPUT error: ' + }) - if not s and m then - _G._debug('INPUT error: ' .. m) + for node in context.storage:filterActive('input') do + local s, m = pcall(function() + for slot, item in pairs(node.adapter.list()) do + tasks:add(function() + context.storage:import(node, slot, item.count, item) + end) end end) + if not s and m then + _G._debug('INPUT error: ' .. m) + end end - if #tasks > 0 then - parallel.waitForAll(table.unpack(tasks)) - end + tasks:run() end Milo:registerTask(InputChest) diff --git a/milo/plugins/trashcanView.lua b/milo/plugins/trashcanView.lua index 33a0ab7..52929b7 100644 --- a/milo/plugins/trashcanView.lua +++ b/milo/plugins/trashcanView.lua @@ -1,9 +1,9 @@ -local Event = require('event') -local Milo = require('milo') -local UI = require('ui') +local Milo = require('milo') +local Tasks = require('milo.taskRunner') +local UI = require('ui') -local colors = _G.colors -local device = _G.device +local colors = _G.colors +local device = _G.device --[[ Configuration Screen ]] local wizardPage = UI.WizardPage { @@ -76,16 +76,20 @@ local function filter(a) end function task:cycle(context) + local tasks = Tasks() + for node in context.storage:filterActive('trashcan', filter) do - Event.onTimeout(0, function() -- run on a background thread - pcall(function() - for k in pairs(node.adapter.list()) do - local direction = node.dropDirection or 'down' + pcall(function() + for k in pairs(node.adapter.list()) do + local direction = node.dropDirection or 'down' + tasks:add(function() node.adapter.drop(k, 64, direction) - end - end) + end) + end end) end + + tasks:run() end Milo:registerTask(task) diff --git a/openos/.package b/openos/.package new file mode 100644 index 0000000..a7219a0 --- /dev/null +++ b/openos/.package @@ -0,0 +1,6 @@ +{ + title = 'Shell utilities', + repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/openos', + description = [[ Utilties for shell: grep, cat, touch, etc ]], + licence = 'MIT', +} diff --git a/openos/apis/computer.lua b/openos/apis/computer.lua new file mode 100644 index 0000000..5e95cf8 --- /dev/null +++ b/openos/apis/computer.lua @@ -0,0 +1,3 @@ +return { + uptime = os.clock, +} \ No newline at end of file diff --git a/openos/apis/filesystem.lua b/openos/apis/filesystem.lua new file mode 100644 index 0000000..6d6cc75 --- /dev/null +++ b/openos/apis/filesystem.lua @@ -0,0 +1,59 @@ +local fs = _G.fs + +local function get(path) + local label = fs.getDrive(path) + + if label then + local proxy = { + getLabel = function() return label end, + isReadOnly = function() return fs.isReadOnly(path) end, + spaceTotal = function() return fs.getSize(path, true) + fs.getFreeSpace(path) end, + spaceUsed = function() return fs.getSize(path, true) end, + } + return proxy, path + end +end + +local function mounts() + local t = { + [ fs.getDrive('/') ] = '/' + } + for _,path in pairs(fs.list('/')) do + local label = fs.getDrive(path) + if not t[label] then + t[label] = path + end + end + + return function() + local label, path = next(t) + if label then + t[label] = nil + return get(path) + end + end +end + +local function list(path) + local set = fs.list(path) + return function() + local key, value = next(set) + set[key or false] = nil + return value + end +end + +return { + canonical = function(...) return ... end, + concat = fs.combine, + exists = fs.exists, + get = get, + isDirectory = fs.isDir, + isLink = function() return false end, + list = list, + mounts = mounts, + name = fs.getName, + open = function(n, m) return fs.open(n, m or 'r') end, + realPath = function(...) return ... end, + size = fs.getSize, +} diff --git a/openos/apis/keyboard.lua b/openos/apis/keyboard.lua new file mode 100644 index 0000000..2c98c23 --- /dev/null +++ b/openos/apis/keyboard.lua @@ -0,0 +1,3 @@ +return { + keys = _G.keys, +} \ No newline at end of file diff --git a/openos/apis/sh.lua b/openos/apis/sh.lua new file mode 100644 index 0000000..6a45865 --- /dev/null +++ b/openos/apis/sh.lua @@ -0,0 +1,6 @@ +local shell = _ENV.shell + +return { + execute = function(_, ...) return shell.run(...) end, + getLastExitCode = function() return 0 end, +} \ No newline at end of file diff --git a/openos/apis/shell.lua b/openos/apis/shell.lua new file mode 100644 index 0000000..a7bb257 --- /dev/null +++ b/openos/apis/shell.lua @@ -0,0 +1,9 @@ +local Util = require('util') + +local shell = _ENV.shell + +return { + getWorkingDirectory = shell.dir, + resolve = shell.resolve, + parse = Util.parse, +} diff --git a/openos/apis/term.lua b/openos/apis/term.lua new file mode 100644 index 0000000..7e2ec89 --- /dev/null +++ b/openos/apis/term.lua @@ -0,0 +1 @@ +return _G.term \ No newline at end of file diff --git a/openos/apis/text.lua b/openos/apis/text.lua new file mode 100644 index 0000000..4a2b0fd --- /dev/null +++ b/openos/apis/text.lua @@ -0,0 +1,405 @@ +local unicode = require("openos.unicode") +local tx = require("openos.transforms") + +local text = {} +text.internal = {} + +text.syntax = {"^%d?>>?&%d+","^%d?>>?",">>?","<%&%d+","<",";","&&","||?"} + +local function checkArg(n, have, ...) + have = type(have) + local function check(want, ...) + if not want then + return false + else + return have == want or check(...) + end + end + if not check(...) then + local msg = string.format("bad argument #%d (%s expected, got %s)", + n, table.concat({...}, " or "), have) + error(msg, 3) + end +end + +function text.trim(value) -- from http://lua-users.org/wiki/StringTrim + local from = string.match(value, "^%s*()") + return from > #value and "" or string.match(value, ".*%S", from) +end + +-- used by lib/sh +function text.escapeMagic(txt) + return txt:gsub('[%(%)%.%%%+%-%*%?%[%^%$]', '%%%1') +end + +function text.removeEscapes(txt) + return txt:gsub("%%([%(%)%.%%%+%-%*%?%[%^%$])","%1") +end + +function text.internal.tokenize(value, options) + checkArg(1, value, "string") + checkArg(2, options, "table", "nil") + options = options or {} + local delimiters = options.delimiters + local custom = not not options.delimiters + delimiters = delimiters or text.syntax + + local words, reason = text.internal.words(value, options) + + local splitter = text.escapeMagic(custom and table.concat(delimiters) or "<>|;&") + if type(words) ~= "table" or + #splitter == 0 or + not value:find("["..splitter.."]") then + return words, reason + end + + return text.internal.splitWords(words, delimiters) +end + +-- tokenize input by quotes and whitespace +function text.internal.words(input, options) + checkArg(1, input, "string") + checkArg(2, options, "table", "nil") + options = options or {} + local quotes = options.quotes + local show_escapes = options.show_escapes + local qr = nil + quotes = quotes or {{"'","'",true},{'"','"'},{'`','`'}} + local function append(dst, txt, _qr) + local size = #dst + if size == 0 or dst[size].qr ~= _qr then + dst[size+1] = {txt=txt, qr=_qr} + else + dst[size].txt = dst[size].txt..txt + end + end + -- token meta is {string,quote rule} + local tokens, token = {}, {} + local escaped, start = false, -1 + for i = 1, unicode.len(input) do + local char = unicode.sub(input, i, i) + if escaped then -- escaped character + escaped = false + -- include escape char if show_escapes + -- or the followwing are all true + -- 1. qr active + -- 2. the char escaped is NOT the qr closure + -- 3. qr is not literal + if show_escapes or (qr and not qr[3] and qr[2] ~= char) then + append(token, '\\', qr) + end + append(token, char, qr) + elseif char == "\\" and (not qr or not qr[3]) then + escaped = true + elseif qr and qr[2] == char then -- end of quoted string + -- if string is empty, we can still capture a quoted empty arg + if #token == 0 or #token[#token] == 0 then + append(token, '', qr) + end + qr = nil + elseif not qr and tx.first(quotes,function(Q) + qr=Q[1]==char and Q or nil return qr end) then + start = i + elseif not qr and string.find(char, "%s") then + if #token > 0 then + table.insert(tokens, token) + end + token = {} + else -- normal char + append(token, char, qr) + end + end + if qr then + return nil, "unclosed quote at index " .. start + end + + if #token > 0 then + table.insert(tokens, token) + end + + return tokens +end + +-- separate string value into an array of words delimited by whitespace +-- groups by quotes +-- options is a table used for internal undocumented purposes +function text.tokenize(value, options) + checkArg(1, value, "string") + checkArg(2, options, "table", "nil") + options = options or {} + + local tokens, reason = text.internal.tokenize(value, options) + + if type(tokens) ~= "table" then + return nil, reason + end + + if options.doNotNormalize then + return tokens + end + + return text.internal.normalize(tokens) +end + +------------------------------------------------------------------------------- +-- like tokenize, but does not drop any text such as whitespace +-- splits input into an array for sub strings delimited by delimiters +-- delimiters are included in the result if not dropDelims +function text.split(input, delimiters, dropDelims, di) + checkArg(1, input, "string") + checkArg(2, delimiters, "table") + checkArg(3, dropDelims, "boolean", "nil") + checkArg(4, di, "number", "nil") + + if #input == 0 then return {} end + di = di or 1 + local result = {input} + if di > #delimiters then return result end + + local function add(part, index, r, s, e) + local sub = part:sub(s,e) + if #sub == 0 then return index end + local subs = r and text.split(sub,delimiters,dropDelims,r) or {sub} + for i=1,#subs do + table.insert(result, index+i-1, subs[i]) + end + return index+#subs + end + + local i,d=1,delimiters[di] + while true do + local next = table.remove(result,i) + if not next then break end + local si,ei = next:find(d) + if si and ei and ei~=0 then -- delim found + i=add(next, i, di+1, 1, si-1) + i=dropDelims and i or add(next, i, false, si, ei) + i=add(next, i, di, ei+1) + else + i=add(next, i, di+1, 1, #next) + end + end + + return result +end + +----------------------------------------------------------------------------- + +-- splits each word into words at delimiters +-- delimiters are kept as their own words +-- quoted word parts are not split +function text.internal.splitWords(words, delimiters) + checkArg(1,words,"table") + checkArg(2,delimiters,"table") + + local split_words = {} + local next_word + local function add_part(part) + if next_word then + split_words[#split_words+1] = {} + end + table.insert(split_words[#split_words], part) + next_word = false + end + for wi=1,#words do local word = words[wi] + next_word = true + for pi=1,#word do local part = word[pi] + local qr = part.qr + if qr then + add_part(part) + else + local part_text_splits = text.split(part.txt, delimiters) + tx.foreach(part_text_splits, function(sub_txt) + local delim = #text.split(sub_txt, delimiters, true) == 0 + next_word = next_word or delim + add_part({txt=sub_txt,qr=qr}) + next_word = delim + end) + end + end + end + + return split_words +end + +function text.internal.normalize(words, omitQuotes) + checkArg(1, words, "table") + checkArg(2, omitQuotes, "boolean", "nil") + local norms = {} + for _,word in ipairs(words) do + local norm = {} + for _,part in ipairs(word) do + norm = tx.concat(norm, not omitQuotes and part.qr and {part.qr[1], part.txt, part.qr[2]} or {part.txt}) + end + norms[#norms+1]=table.concat(norm) + end + return norms +end + +function text.internal.stream_base(binary) + return + { + binary = binary, + plen = binary and string.len or unicode.len, + psub = binary and string.sub or unicode.sub, + seek = function (handle, whence, to) + if not handle.txt then + return nil, "bad file descriptor" + end + to = to or 0 + local offset = handle:indexbytes() + if whence == "cur" then + offset = offset + to + elseif whence == "set" then + offset = to + elseif whence == "end" then + offset = handle.len + to + end + offset = math.max(0, math.min(offset, handle.len)) + handle:byteindex(offset) + return offset + end, + indexbytes = function (handle) + return handle.psub(handle.txt, 1, handle.index):len() + end, + byteindex = function (handle, offset) + local sub = string.sub(handle.txt, 1, offset) + handle.index = handle.plen(sub) + end, + } +end + +function text.internal.reader(txt, mode) + checkArg(1, txt, "string") + local reader = setmetatable( + { + txt = txt, + len = string.len(txt), + index = 0, + read = function(_, n) + checkArg(1, n, "number") + if not _.txt then + return nil, "bad file descriptor" + end + if _.index >= _.plen(_.txt) then + return nil + end + local next = _.psub(_.txt, _.index + 1, _.index + n) + _.index = _.index + _.plen(next) + return next + end, + close = function(_) + if not _.txt then + return nil, "bad file descriptor" + end + _.txt = nil + return true + end, + }, {__index=text.internal.stream_base(mode:match("b"))}) + + return require("buffer").new("r", reader) +end + +function text.internal.writer(ostream, mode, append_txt) + if type(ostream) == "table" then + local mt = getmetatable(ostream) or {} + checkArg(1, mt.__call, "function") + end + checkArg(1, ostream, "function", "table") + checkArg(2, append_txt, "string", "nil") + local writer = setmetatable( + { + txt = "", + index = 0, -- last location of write + len = 0, + write = function(_, ...) + if not _.txt then + return nil, "bad file descriptor" + end + local pre = _.psub(_.txt, 1, _.index) + local vs = {} + local pos = _.psub(_.txt, _.index + 1) + for _,v in ipairs({...}) do + table.insert(vs, v) + end + vs = table.concat(vs) + _.index = _.index + _.plen(vs) + _.txt = pre .. vs .. pos + _.len = string.len(_.txt) + return true + end, + close = function(_) + if not _.txt then + return nil, "bad file descriptor" + end + ostream((append_txt or "") .. _.txt) + _.txt = nil + return true + end, + }, {__index=text.internal.stream_base(mode:match("b"))}) + + return require("buffer").new("w", writer) +end + +function text.detab(value, tabWidth) + checkArg(1, value, "string") + checkArg(2, tabWidth, "number", "nil") + tabWidth = tabWidth or 8 + local function rep(match) + local spaces = tabWidth - match:len() % tabWidth + return match .. string.rep(" ", spaces) + end + local result = value:gsub("([^\n]-)\t", rep) -- truncate results + return result +end + +function text.padLeft(value, length) + checkArg(1, value, "string", "nil") + checkArg(2, length, "number") + if not value or unicode.wlen(value) == 0 then + return string.rep(" ", length) + else + return string.rep(" ", length - unicode.wlen(value)) .. value + end +end + +function text.padRight(value, length) + checkArg(1, value, "string", "nil") + checkArg(2, length, "number") + if not value or unicode.wlen(value) == 0 then + return string.rep(" ", length) + else + return value .. string.rep(" ", length - unicode.wlen(value)) + end +end + +function text.wrap(value, width, maxWidth) + checkArg(1, value, "string") + checkArg(2, width, "number") + checkArg(3, maxWidth, "number") + local line, nl = value:match("([^\r\n]*)(\r?\n?)") -- read until newline + if unicode.wlen(line) > width then -- do we even need to wrap? + local partial = unicode.wtrunc(line, width) + local wrapped = partial:match("(.*[^a-zA-Z0-9._()'`=])") + if wrapped or unicode.wlen(line) > maxWidth then + partial = wrapped or partial + return partial, unicode.sub(value, unicode.len(partial) + 1), true + else + return "", value, true -- write in new line. + end + end + local start = unicode.len(line) + unicode.len(nl) + 1 + return line, start <= unicode.len(value) and unicode.sub(value, start) or nil, unicode.len(nl) > 0 +end + +function text.wrappedLines(value, width, maxWidth) + local line + return function() + if value then + line, value = text.wrap(value, width, maxWidth) + return line + end + end +end + +return text \ No newline at end of file diff --git a/openos/apis/transfer.lua b/openos/apis/transfer.lua new file mode 100644 index 0000000..bd3868a --- /dev/null +++ b/openos/apis/transfer.lua @@ -0,0 +1,269 @@ +local fs = require("openos.filesystem") +local shell = require("openos.shell") +local text = require("openos.text") +local lib = {} + +local function perr(ops, format, ...) + if format then + io.stderr:write(ops.cmd .. string.format(": " .. format, ...) .. "\n") + ops.exit_code = 1 + return 1 + end +end + +local function contents_check(arg, options, bMustExist) + if arg == "" then + return perr(options, "cannot create regular file '' No such file or directory") + end + local path = shell.resolve(arg) + local content_pattern = "^(%.*)(.?)" + local contents_of, of_dir = arg:reverse():match(content_pattern) + of_dir = of_dir:match("^/?$") + local dots = contents_of and contents_of:len() or 0 + contents_of = of_dir and ({true,true})[dots] + + if (not bMustExist or fs.exists(path)) and of_dir and not fs.isDirectory(path) then + perr(options, "'%s' is not a directory", arg) + os.exit(1) + end + + return contents_of, path +end + +local function areEqual(path1, path2) + local f1, f2 = fs.open(path1, "rb") + local result = true + if f1 then + f2 = fs.open(path2, "rb") + if f2 then + local chunkSize = 4 * 1024 + repeat + local s1, s2 = f1:read(chunkSize), f2:read(chunkSize) + if s1 ~= s2 then + result = false + break + end + until not s1 or not s2 + f2:close() + end + f1:close() + end + assert(f1 and f2, "could not open files for reading: " .. path1 .. ", " .. path2) + return result +end + +local function status(verbose, from, to) + if verbose then + to = to and (" -> " .. to) or "" + io.write(from .. to .. "\n") + end + os.sleep(0) -- allow interrupting +end + +local function prompt(message) + io.write(message .. " [Y/n] ") + local result = io.read() + if not result then -- closed pipe + os.exit(1) + end + return result and (result == "" or result:sub(1, 1):lower() == "y") +end + +local function stat(path, ops, P) + local real, reason = fs.realPath(path) + if not real and not P then + perr(ops, "cannot read '%s': '%s'", path, reason) + return false + end + local isLink, linkTarget = fs.isLink(path) + return true, + real, + reason, + isLink, + linkTarget, + fs.exists(path), + fs.get(path), + real and fs.isDirectory(real) +end + +function lib.recurse(fromPath, toPath, options, origin, top) + fromPath = fromPath:gsub("/+", "/") + toPath = toPath:gsub("/+", "/") + local fromPathFull = shell.resolve(fromPath) + local toPathFull = shell.resolve(toPath) + local mv = options.cmd == "mv" + local verbose = options.v and (not mv or top) + if select(2, fromPathFull:find(options.skip)) == #fromPathFull then + status(verbose, string.format("skipping %s", fromPath)) + return true + end + local function release(result, reason) + if result and mv and top then + local rm_result = not fs.get(fromPathFull).isReadOnly() and fs.remove(fromPathFull) + if not rm_result then + perr(options, "cannot remove '%s': filesystem is readonly", fromPath) + result = false + end + end + return result, reason + end + + local + ok, + fromReal, + _, --fromError, + fromIsLink, + fromLinkTarget, + fromExists, + fromFs, + fromIsDir = stat(fromPathFull, options, options.P) + if not ok then return nil end + local + ok, + toReal, + _,--toError, + toIsLink, + _,--toLinkTarget, + toExists, + toFs, + toIsDir = stat(toPathFull, options) + if not ok then os.exit(1) end + if toFs.isReadOnly() then + perr(options, "cannot create target '%s': filesystem is readonly", toPath) + return + end + + local same_path = fromReal == toReal + + local same_fs = fromFs == toFs + local is_mount = origin[fromReal] + + if mv and is_mount then + return false, string.format("cannot move '%s', it is a mount point", fromPath) + end + + if fromIsLink and options.P and not (toExists and same_path and not toIsLink) then + if toExists and options.n then + return true + end + fs.remove(toPathFull) + if toExists then + status(verbose, string.format("removed '%s'", toPath)) + end + status(verbose, fromPath, toPath) + return release(fs.link(fromLinkTarget, toPathFull)) + elseif fromIsDir then + if not options.r then + status(true, string.format("omitting directory '%s'", fromPath)) + options.exit_code = 1 + return true + end + if toExists and not toIsDir then + -- my real cp always does this, even with -f, -n or -i. + return nil, "cannot overwrite non-directory '" .. toPath .. "' with directory '" .. fromPath .. "'" + end + if options.x and not top and is_mount then + return true + end + if same_fs then + if (toReal.."/"):find(fromReal.."/",1,true) then + return nil, "cannot write a directory, '" .. fromPath .. "', into itself, '" .. toPath .. "'" + end + end + if mv then + if fs.list(toReal)() then -- to is NOT empty + return nil, "cannot move '" .. fromPath .. "' to '" .. toPath .. "': Directory not empty" + end + status(verbose, fromPath, toPath) + end + if not toExists then + status(verbose, fromPath, toPath) + fs.makeDirectory(toPathFull) + end + for file in fs.list(fromPathFull) do + local result, reason = lib.recurse(fromPath .."/".. file, toPath.."/"..file, options, origin, false) + -- false, no longer top + if not result then + return false, reason + end + end + return release(true) + elseif fromExists then + if toExists then + if same_path then + return nil, "'" .. fromPath .. "' and '" .. toPath .. "' are the same file" + end + if options.n then + return true + end + if options.u and not toIsDir and areEqual(fromReal, toReal) then + return true + end + if options.i then + if not prompt("overwrite '" .. toPath .. "'?") then + return true + end + end + if toIsDir then + return nil, "cannot overwrite directory '" .. toPath .. "' with non-directory" + end + fs.remove(toReal) + end + status(verbose, fromPath, toPath) + return release(fs.copy(fromPathFull, toPathFull)) + else + return nil, "'" .. fromPath .. "': No such file or directory" + end +end + +function lib.batch(args, options) + options.exit_code = 0 + + -- standardized options + options.i = options.i and not options.f + options.P = options.P or options.r + options.skip = text.escapeMagic(options.skip or "") + + local origin = {} + for dev,path in fs.mounts() do + origin[path] = dev + end + + local toArg = table.remove(args) + local _, ok = contents_check(toArg, options) + if not ok then + return 1 + end + local originalToIsDir = fs.isDirectory(ok) + + for _, fromArg in ipairs(args) do + -- a "contents of" copy is where src path ends in . or .. + -- a source path ending with . is not sufficient - could be the source filename + local contents_of + contents_of, ok = contents_check(fromArg, options, true) + if ok then + -- we do not append fromPath name to toPath in case of contents_of copy + local toPath = toArg + if contents_of and options.cmd == "mv" then + perr(options, "invalid move path '%s'", fromArg) + else + if not contents_of and originalToIsDir then + local fromName = fs.name(fromArg) + if fromName then + toPath = toPath .. "/" .. fromName + end + end + + local result, reason = lib.recurse(fromArg, toPath, options, origin, true) + + if not result then + perr(options, reason) + end + end + end + end + + return options.exit_code +end + +return lib \ No newline at end of file diff --git a/openos/apis/transforms.lua b/openos/apis/transforms.lua new file mode 100644 index 0000000..d2ee40d --- /dev/null +++ b/openos/apis/transforms.lua @@ -0,0 +1,200 @@ + +local lib={} +lib.internal={} + +local function checkArg(n, have, ...) + have = type(have) + local function check(want, ...) + if not want then + return false + else + return have == want or check(...) + end + end + if not check(...) then + local msg = string.format("bad argument #%d (%s expected, got %s)", + n, table.concat({...}, " or "), have) + error(msg, 3) + end +end + +function lib.internal.range_adjust(f,l,s) + checkArg(1,f,'number','nil') + checkArg(2,l,'number','nil') + checkArg(3,s,'number') + if f==nil then f=1 elseif f<0 then f=s+f+1 end + if l==nil then l=s elseif l<0 then l=s+l+1 end + return f,l +end +function lib.internal.table_view(tbl,f,l) + return setmetatable({}, + { + __index = function(_, key) + return (type(key) ~= 'number' or (key >= f and key <= l)) and tbl[key] or nil + end, + __len = function(_) + return l + end, + }) +end +local adjust=lib.internal.range_adjust +local view=lib.internal.table_view + +-- first(p1,p2) searches for the first range in p1 that satisfies p2 +function lib.first(tbl,pred,f,l) + checkArg(1,tbl,'table') + checkArg(2,pred,'function','table') + if type(pred)=='table'then + local set;set,pred=pred,function(_,fi,tbl) + for vi=1,#set do + local v=set[vi] + if lib.begins(tbl,v,fi) then return true,#v end + end + end + end + local s=#tbl + f,l=adjust(f,l,s) + tbl=view(tbl,f,l) + for i=f,l do + local si,ei=pred(tbl[i],i,tbl) + if si then + return i,i+(ei or 1)-1 + end + end +end + +-- returns true if p1 at first p3 equals element for element p2 +function lib.begins(tbl,v,f,l) + checkArg(1,tbl,'table') + checkArg(2,v,'table') + local vs=#v + f,l=adjust(f,l,#tbl) + if vs>(l-f+1)then return end + for i=1,vs do + if tbl[f+i-1]~=v[i] then return end + end + return true +end + +function lib.concat(...) + local r,rn,k={},0 + for _,tbl in ipairs({...})do + if type(tbl)~='table'then + return nil,'parameter '..tostring(_)..' to concat is not a table' + end + local n=tbl.n or #tbl + k=k or tbl.n + for i=1,n do + rn=rn+1;r[rn]=tbl[i] + end + end + r.n=k and rn or nil + return r +end + +-- works like string.sub but on elements of an indexed table +function lib.sub(tbl,f,l) + checkArg(1,tbl,'table') + local r,s={},#tbl + f,l=adjust(f,l,s) + l=math.min(l,s) + for i=math.max(f,1),l do + r[#r+1]=tbl[i] + end + return r +end + +-- Returns a list of subsets of tbl where partitioner acts as a delimiter. +function lib.partition(tbl,partitioner,dropEnds,f,l) + checkArg(1,tbl,'table') + checkArg(2,partitioner,'function','table') + checkArg(3,dropEnds,'boolean','nil') + if type(partitioner)=='table'then + return lib.partition(tbl,function(_,i,tbl) + return lib.first(tbl,partitioner,i) + end,dropEnds,f,l) + end + local s=#tbl + f,l=adjust(f,l,s) + local cut=view(tbl,f,l) + local result={} + local need=true + local exp=function()if need then result[#result+1]={}need=false end end + local i=f + while i<=l do + local e=cut[i] + local ds,de=partitioner(e,i,cut) + -- true==partition here + if ds==true then ds,de=i,i + elseif ds==false then ds,de=nil,nil end + if ds~=nil then + ds,de=adjust(ds,de,l) + ds=ds>=i and ds--no more + end + if not ds then -- false or nil + exp() + table.insert(result[#result],e) + else + local sub=lib.sub(cut,i,not dropEnds and de or (ds-1)) + if #sub>0 then + exp() + result[#result+math.min(#result[#result],1)]=sub + end + -- ensure i moves forward + local ensured=math.max(math.max(de or ds,ds),i) + if de and ds and de + -i: prompt before overwrite (overrides -n option). + -n: do not overwrite an existing file. + -r: copy directories recursively. + -u: copy only when the SOURCE file differs from the destination + file or when the destination file is missing. + -P: preserve attributes, e.g. symbolic links. + -v: verbose output. + -x: stay on original source file system. + --skip=P: skip files matching lua regex P +]]) + return not not options.h +end + +-- clean options for copy (as opposed to move) +options = +{ + cmd = "cp", + i = options.i, + f = options.f, + n = options.n, + r = options.r, + u = options.u, + P = options.P, + v = options.v, + x = options.x, + skip = options.skip, +} + +return transfer.batch(args, options) diff --git a/openos/df.lua b/openos/df.lua new file mode 100644 index 0000000..192dda4 --- /dev/null +++ b/openos/df.lua @@ -0,0 +1,76 @@ +local fs = require("openos.filesystem") +local shell = require("openos.shell") +local text = require("openos.text") + +local args, options = shell.parse(...) + +local function formatSize(size) + if not options.h then + return tostring(size) + elseif type(size) == "string" then + return size + end + local sizes = {"", "K", "M", "G"} + local unit = 1 + local power = options.si and 1000 or 1024 + while size > power and unit < #sizes do + unit = unit + 1 + size = size / power + end + return math.floor(size * 10) / 10 .. sizes[unit] +end + +local mounts = {} +if #args == 0 then + for proxy, path in fs.mounts() do + if not mounts[proxy] or mounts[proxy]:len() > path:len() then + mounts[proxy] = path + end + end +else + for i = 1, #args do + local proxy, path = fs.get(shell.resolve(args[i])) + if not proxy then + io.stderr:write(args[i], ": no such file or directory\n") + else + mounts[proxy] = path + end + end +end + +local result = {{"Filesystem", "Used", "Available", "Use%", "Mounted on"}} +for proxy, path in pairs(mounts) do + local label = proxy.getLabel() or proxy.address + local used, total = proxy.spaceUsed(), proxy.spaceTotal() + local available, percent + if total == math.huge then + used = used or "N/A" + available = "unlimited" + percent = "0%" + else + available = total - used + percent = used / total + if percent ~= percent then -- NaN + available = "N/A" + percent = "N/A" + else + percent = math.ceil(percent * 100) .. "%" + end + end + table.insert(result, {label, formatSize(used), formatSize(available), tostring(percent), path}) +end + +local m = {} +for _, row in ipairs(result) do + for col, value in ipairs(row) do + m[col] = math.max(m[col] or 1, value:len()) + end +end + +for _, row in ipairs(result) do + for col, value in ipairs(row) do + local padding = col == #row and 0 or 2 + io.write(text.padRight(value, m[col] + padding)) + end + print() +end diff --git a/openos/dmesg.lua b/openos/dmesg.lua new file mode 100644 index 0000000..6e944f5 --- /dev/null +++ b/openos/dmesg.lua @@ -0,0 +1,33 @@ +local tty = require("openos.tty") + +local args = {...} +local gpu = tty.gpu() +io.write("Press 'Ctrl-C' to exit\n") +local events = { } +for _, e in pairs(args) do + events[e] = true +end +--pcall(function() + repeat + local evt = table.pack(os.pullEventRaw()) + if #args == 0 or events[evt[1]] then + gpu.setForeground(0xCC2200) + io.write("[" .. math.floor(os.clock("utc")) .. "] ") + gpu.setForeground(0x44CC00) + io.write(tostring(evt[1]) .. string.rep(" ", math.max(12 - #tostring(evt[1]), 0) + 1)) + gpu.setForeground(0xB0B00F) + io.write(tostring(evt[2]) .. string.rep(" ", 37 - #tostring(evt[2]))) + gpu.setForeground(0xFFFFFF) + if evt.n > 2 then + for i = 3, evt.n do + io.write(" " .. tostring(evt[i])) + end + end + + io.write("\n") + end + until evt[1] == "terminate" +--end) + +gpu.setForeground(0xFFFFFF) + diff --git a/openos/du.lua b/openos/du.lua new file mode 100644 index 0000000..5f19153 --- /dev/null +++ b/openos/du.lua @@ -0,0 +1,128 @@ +local shell = require("openos.shell") +local fs = require("openos.filesystem") + +local args, options = shell.parse(...) +if #args == 0 then + args[1] = '.' +end + +local TRY=[[ +Try 'du --help' for more information.]] + +local VERSION=[[ +du (OpenOS bin) 1.0 +Written by payonel, patterned after GNU coreutils du]] + +local HELP=[[ +Usage: du [OPTION]... [FILE]... +Summarize disk usage of each FILE, recursively for directories. + + -h, --human-readable print sizes in human readable format (e.g., 1K 234M 2G) + -s, --summarize display only a total for each argument + --help display this help and exit + --version output version information and exit]] + +if options.help then + print(HELP) + return true +end + +if options.version then + print(VERSION) + return true +end + +local function addTrailingSlash(path) + if path:sub(-1) ~= '/' then + return path .. '/' + else + return path + end +end + +local function opCheck(shortName, longName) + local enabled = options[shortName] or options[longName] + options[shortName] = nil + options[longName] = nil + return enabled +end + +local bHuman = opCheck('h', 'human-readable') +local bSummary = opCheck('s', 'summarize') + +if next(options) then + for op in pairs(options) do + io.stderr:write(string.format("du: invalid option -- '%s'\n", op)) + end + io.stderr:write(TRY..'\n') + return 1 +end + +local function formatSize(size) + if not bHuman then + return tostring(size) + end + local sizes = {"", "K", "M", "G"} + local unit = 1 + local power = options.si and 1000 or 1024 + while size > power and unit < #sizes do + unit = unit + 1 + size = size / power + end + + return math.floor(size * 10) / 10 .. sizes[unit] +end + +local function printSize(size, rpath) + local displaySize = formatSize(size) + io.write(string.format("%s%s\n", string.format("%-12s", displaySize), rpath)) +end + +local function visitor(rpath) + local subtotal = 0 + local dirs = 0 + local spath = shell.resolve(rpath) + + if fs.isDirectory(spath) then + local list_result = fs.list(spath) + for list_item in list_result do + local vtotal, vdirs = visitor(addTrailingSlash(rpath) .. list_item) + subtotal = subtotal + vtotal + dirs = dirs + vdirs + end + + if dirs == 0 then -- no child dirs + if not bSummary then + printSize(subtotal, rpath) + end + end + + elseif not fs.isLink(spath) then + subtotal = fs.size(spath) + end + + return subtotal, dirs +end + +for _,arg in ipairs(args) do + local path = shell.resolve(arg) + + if not fs.exists(path) then + io.stderr:write(string.format("du: cannot access '%s': no such file or directory\n", arg)) + return 1 + else + if fs.isDirectory(path) then + local total = visitor(arg) + + if bSummary then + printSize(total, arg) + end + elseif fs.isLink(path) then + printSize(0, arg) + else + printSize(fs.size(path), arg) + end + end +end + +return true diff --git a/openos/find.lua b/openos/find.lua new file mode 100644 index 0000000..d0cb53b --- /dev/null +++ b/openos/find.lua @@ -0,0 +1,132 @@ +local shell = require("shell") +local fs = require("filesystem") +local text = require("text") + +local USAGE = +[===[Usage: find [path] [--type=[dfs]] [--[i]name=EXPR] + --path if not specified, path is assumed to be current working directory + --type returns results of a given type, d:directory, f:file, and s:symlinks + --name specify the file name pattern. Use quote to include *. iname is + case insensitive + --help display this help and exit]===] + +local args, options = shell.parse(...) + +if (not args or not options) or options.help then + print(USAGE) + if not options.help then + return 1 + else + return -- nil return, meaning no error + end +end + +if #args > 1 then + io.stderr:write(USAGE..'\n') + return 1 +end + +local path = #args == 1 and args[1] or "." + +local bDirs = true +local bFiles = true +local bSyms = true + +local fileNamePattern = "" +local bCaseSensitive = true + +if options.iname and options.name then + io.stderr:write("find cannot define both iname and name\n") + return 1 +end + +if options.type then + bDirs = false + bFiles = false + bSyms = false + + if options.type == "f" then + bFiles = true + elseif options.type == "d" then + bDirs = true + elseif options.type == "s" then + bSyms = true + else + io.stderr:write(string.format("find: Unknown argument to type: %s\n", options.type)) + io.stderr:write(USAGE..'\n') + return 1 + end +end + +if options.iname or options.name then + bCaseSensitive = options.iname ~= nil + fileNamePattern = options.iname or options.name + + if type(fileNamePattern) ~= "string" then + io.stderr:write('find: missing argument to `name\'\n') + return 1 + end + + if not bCaseSensitive then + fileNamePattern = fileNamePattern:lower() + end + + -- prefix any * with . for gnu find glob matching + fileNamePattern = text.escapeMagic(fileNamePattern) + fileNamePattern = fileNamePattern:gsub("%%%*", ".*") +end + +local function isValidType(spath) + if not fs.exists(spath) then + return false + end + + if fileNamePattern:len() > 0 then + local fileName = spath:gsub('.*/','') + + if fileName:len() == 0 then + return false + end + + local caseFileName = fileName + + if not bCaseSensitive then + caseFileName = caseFileName:lower() + end + + local s, e = caseFileName:find(fileNamePattern) + if not s or not e then + return false + end + + if s ~= 1 or e ~= caseFileName:len() then + return false + end + end + + if fs.isDirectory(spath) then + return bDirs + elseif fs.isLink(spath) then + return bSyms + else + return bFiles + end +end + +local function visit(rpath) + local spath = shell.resolve(rpath) + + if isValidType(spath) then + local result = rpath:gsub('/+$','') + print(result) + end + + if fs.isDirectory(spath) then + local list_result = fs.list(spath) + for list_item in list_result do + visit(rpath:gsub('/+$', '') .. '/' .. list_item) + end + end +end + +visit(path) diff --git a/openos/grep.lua b/openos/grep.lua new file mode 100644 index 0000000..f758660 --- /dev/null +++ b/openos/grep.lua @@ -0,0 +1,323 @@ +--[[ +An adaptation of Wobbo's grep +https://raw.githubusercontent.com/OpenPrograms/Wobbo-Programs/master/grep/grep.lua +]]-- + +-- POSIX grep for OpenComputers +-- one difference is that this version uses Lua regex, not POSIX regex. + +local fs = require("openos.filesystem") +local shell = require("openos.shell") +local tty = require("openos.tty") +local computer = require("openos.computer") + +-- Process the command line arguments + +local args, options = shell.parse(...) + +local gpu = tty.gpu() + +local function printUsage(ostream, msg) + local s = ostream or io.stdout + if msg then + s:write(msg,'\n') + end + s:write([[Usage: grep [OPTION]... PATTERN [FILE]... +Example: grep -i "hello world" menu.lua main.lua +for more information, run: man grep +]]) +end + +local PATTERNS = {args[1]} +local FILES = {select(2, table.unpack(args))} + +local LABEL_COLOR = 0xb000b0 +local LINE_NUM_COLOR = 0x00FF00 +local MATCH_COLOR = 0xFF0000 +local COLON_COLOR = 0x00FFFF + +local function pop(...) + local result + for _,key in ipairs({...}) do + result = options[key] or result + options[key] = nil + end + return result +end + +-- Specify the variables for the options +local plain = pop('F','fixed-strings') + plain = not pop('e','--lua-regexp') and plain +local pattern_file = pop('file') +local match_whole_word = pop('w','word-regexp') +local match_whole_line = pop('x','line-regexp') +local ignore_case = pop('i','ignore-case') +local stdin_label = pop('label') or '(standard input)' +local stderr = pop('s','no-messages') and {write=function()end} or io.stderr +local invert_match = not not pop('v','invert-match') + +-- no version output, just help +if pop('V','version','help') then + printUsage() + return 0 +end + +local max_matches = tonumber(pop('max-count')) or math.huge +local print_line_num = pop('n','line-number') +local search_recursively = pop('r','recursive') + +-- Table with patterns to check for +if pattern_file then + local pattern_file_path = shell.resolve(pattern_file) + if not fs.exists(pattern_file_path) then + stderr:write('grep: ',pattern_file,': file not found') + return 2 + end + table.insert(FILES, 1, PATTERNS[1]) + PATTERNS = {} + for line in io.lines(pattern_file_path) do + PATTERNS[#PATTERNS+1] = line + end +end + +if #PATTERNS == 0 then + printUsage(stderr) + return 2 +end + +if #FILES == 0 then + FILES = search_recursively and {'.'} or {'-'} +end + +if not options.h and search_recursively then + options.H = true +end + +if #FILES < 2 then + options.h = true +end + +local f_only = pop('l','files-with-matches') +local no_only = pop('L','files-without-match') and not f_only + +local include_filename = pop('H','with-filename') + include_filename = not pop('h','no-filename') or include_filename + +local m_only = pop('o','only-matching') +local quiet = pop('q','quiet','silent') + +local print_count = pop('c','count') +local colorize = pop('color','colour') and io.output().tty and tty.isAvailable() + +local noop = function(...)return ...;end +local setc = colorize and gpu.setForeground or noop +local getc = colorize and gpu.getForeground or noop + +local trim = pop('t','trim') +local trim_front = trim and function(s)return s:gsub('^%s+','')end or noop +local trim_back = trim and function(s)return s:gsub('%s+$','')end or noop + +if next(options) then + if not quiet then + printUsage(stderr, 'unexpected option: '..next(options)) + return 2 + end + return 0 +end +-- Resolve the location of a file, without searching the path +local function resolve(file) + if file:sub(1,1) == '/' then + return fs.canonical(file) + else + if file:sub(1,2) == './' then + file = file:sub(3, -1) + end + return fs.canonical(fs.concat(shell.getWorkingDirectory(), file)) + end +end + +--- Builds a case insensitive patterns, code from stackoverflow +--- (questions/11401890/case-insensitive-lua-pattern-matching) +if ignore_case then + for i=1,#PATTERNS do + -- find an optional '%' (group 1) followed by any character (group 2) + PATTERNS[i] = PATTERNS[i]:gsub("(%%?)(.)", function(percent, letter) + if percent ~= "" or not letter:match("%a") then + -- if the '%' matched, or `letter` is not a letter, return "as is" + return percent .. letter + else -- case-insensitive + return string.format("[%s%s]", letter:lower(), letter:upper()) + end + end) + end +end + +local function getAllFiles(dir, file_list) + for node in fs.list(shell.resolve(dir)) do + local rel_path = dir:gsub("/+$","") .. '/' .. node + local resolved_path = shell.resolve(rel_path) + if fs.isDirectory(resolved_path) then + getAllFiles(rel_path, file_list) + else + file_list[#file_list+1] = rel_path + end + end +end + +if search_recursively then + local files = {} + for _,arg in ipairs(FILES) do + if fs.isDirectory(arg) then + getAllFiles(arg, files) + else + files[#files+1]=arg + end + end + FILES=files +end + +-- Prepare an iterator for reading files +local function readLines() + local curHand = nil + local curFile = nil + local meta = nil + return function() + if not curFile then + local file = table.remove(FILES, 1) + if not file then + return + end + meta = {line_num=0,hits=0} + if file == "-" then + curFile = file + meta.label = stdin_label + curHand = io.input() + else + meta.label = file + local file, reason = resolve(file) + if fs.exists(file) then + curHand, reason = io.open(file, 'r') + if not curHand then + local msg = string.format("failed to read from %s: %s", meta.label, reason) + stderr:write("grep: ",msg,"\n") + return false, 2 + else + curFile = meta.label + end + else + stderr:write("grep: ",file,": file not found\n") + return false, 2 + end + end + end + meta.line = nil + if not meta.close and curHand then + meta.line_num = meta.line_num + 1 + meta.line = curHand:read("*l") + end + if not meta.line then + curFile = nil + if curHand then + curHand:close() + end + return false, meta + else + return meta, curFile + end + end +end + +local function write(part, color) + local prev_color = color and getc() + if color then setc(color) end + io.write(part) + if color then setc(prev_color) end +end +local flush=(f_only or no_only or print_count) and function(m) + if no_only and m.hits == 0 or f_only and m.hits ~= 0 then + write(m.label, LABEL_COLOR) + write('\n') + elseif print_count then + if include_filename then + write(m.label, LABEL_COLOR) + write(':', COLON_COLOR) + end + write(m.hits) + write('\n') + end +end +local ec = nil +local any_hit_ec = 1 +local function test(m,p) + local empty_line = true + local last_index, slen = 1, #m.line + local needs_filename, needs_line_num = include_filename, print_line_num + local hit_value = 1 + while last_index <= slen and not m.close do + local i, j = m.line:find(p, last_index, plain) + local word_fail, line_fail = + match_whole_word and not (i and not (m.line:sub(i-1,i-1)..m.line:sub(j+1,j+1)):find("[%a_]")), + match_whole_line and not (i==1 and j==slen) + local matched = not ((m_only or last_index==1) and not i) + if (hit_value == 1 and word_fail) or line_fail then + matched,i,j = false + end + if invert_match == matched then break end + if max_matches == 0 then os.exit(1) end + any_hit_ec = 0 + m.hits, hit_value = m.hits + hit_value, 0 + if f_only or no_only then + m.close = true + end + if flush or quiet then return end + if needs_filename then + write(m.label, LABEL_COLOR) + write(':', COLON_COLOR) + needs_filename = nil + end + if needs_line_num then + write(m.line_num, LINE_NUM_COLOR) + write(':', COLON_COLOR) + needs_line_num = nil + end + local s=m_only and '' or m.line:sub(last_index,(i or 0)-1) + local g=i and m.line:sub(i,j) or '' + if i==1 then g=trim_front(g) elseif last_index==1 then s=trim_front(s) end + if j==slen then g=trim_back(g) elseif not i then s=trim_back(s) end + write(s) + write(g, MATCH_COLOR) + empty_line = false + last_index = (j or slen)+1 + if m_only or last_index>slen then + write("\n") + empty_line = true + needs_filename, needs_line_num = include_filename, print_line_num + elseif p:find("^^") and not plain then p="^$" end + end + if not empty_line then write("\n") end + if max_matches ~= math.huge and max_matches >= m.hits then + m.close = true + end +end + +local uptime = computer.uptime +local last_sleep = uptime() +for meta,status in readLines() do + if uptime() - last_sleep > 1 then + os.sleep(0) + last_sleep = uptime() + end + if not meta then + if type(status) == 'table' then if flush then + flush(status) end -- this was the last object, closing out + elseif status then + ec = status or ec + end + else + for _,p in ipairs(PATTERNS) do + test(meta,p) + end + end +end + +return ec or any_hit_ec \ No newline at end of file diff --git a/openos/head.lua b/openos/head.lua new file mode 100644 index 0000000..7925f7d --- /dev/null +++ b/openos/head.lua @@ -0,0 +1,137 @@ +local shell = require("openos.shell") +local fs = require("openos.filesystem") + +local args, options = shell.parse(...) +local error_code = 0 + +local function pop(key, convert) + local result = options[key] + options[key] = nil + if result and convert then + local c = tonumber(result) + if not c then + io.stderr:write(string.format("use --%s=n where n is a number\n", key)) + options.help = true + error_code = 1 + end + result = c + end + return result +end + +local bytes = pop('bytes', true) +local lines = pop('lines', true) +local quiet = {pop('q'), pop('quiet'), pop('silent')} +quiet = quiet[1] or quiet[2] or quiet[3] +local verbose = {pop('v'), pop('verbose')} +verbose = verbose[1] or verbose[2] +local help = pop('help') +local invalid_key = next(options) + +if bytes and lines then + invalid_key = 'bytes and lines both specified' +end + +if help or next(options) then + local invalid_key = next(options) + if invalid_key then + invalid_key = string.format('invalid option: %s\n', invalid_key) + error_code = 1 + else + invalid_key = '' + end + print(invalid_key .. [[Usage: head [--lines=n] file +Print the first 10 lines of each FILE to stdout. +For more info run: man head]]) + os.exit(error_code) +end + +if #args == 0 then + args = {'-'} +end + +if quiet and verbose then + quiet = false +end + +local function new_stream() + return + { + open=true, + capacity=math.abs(lines or bytes or 10), + bytes=bytes, + buffer=(lines and lines < 0 and {}) or (bytes and bytes < 0 and '') + } +end + +local function close(stream) + if stream.buffer then + if type(stream.buffer) == 'table' then + stream.buffer = table.concat(stream.buffer) + end + io.stdout:write(stream.buffer) + stream.buffer = nil + end + stream.open = false +end + +local function push(stream, line) + if not line then + return close(stream) + end + + local cost = stream.bytes and line:len() or 1 + stream.capacity = stream.capacity - cost + + if not stream.buffer then + if stream.bytes and stream.capacity < 0 then + line = line:sub(1,stream.capacity-1) + end + io.write(line) + if stream.capacity <= 0 then + return close(stream) + end + else + if type(stream.buffer) == 'table' then -- line storage + stream.buffer[#stream.buffer+1] = line + if stream.capacity < 0 then + table.remove(stream.buffer, 1) + stream.capacity = 0 -- zero out + end + else -- byte storage + stream.buffer = stream.buffer .. line + if stream.capacity < 0 then + stream.buffer = stream.buffer:sub(-stream.capacity+1) + stream.capacity = 0 -- zero out + end + end + end + +end + +for i=1,#args do + local arg = args[i] + local file + if arg == '-' then + arg = 'standard input' + file = io.stdin + else + file, reason = io.open(arg, 'r') + if not file then + io.stderr:write(string.format([[head: cannot open '%s' for reading: %s]], arg, reason)) + end + end + if file then + if verbose or #args > 1 then + io.write(string.format('==> %s <==\n', arg)) + end + + local stream = new_stream() + + while stream.open do + push(stream, file:read('*L')) + end + + file:close() + end +end diff --git a/openos/hostname.lua b/openos/hostname.lua new file mode 100644 index 0000000..ae1060d --- /dev/null +++ b/openos/hostname.lua @@ -0,0 +1,17 @@ +local os = _G.os + +local shell = require("openos.shell") +local args = shell.parse(...) +local hostname = args[1] + +if hostname then + os.setComputerLabel(hostname) +else + hostname = os.getComputerLabel() + if hostname then + print(hostname) + else + io.stderr:write("Hostname not set\n") + return 1 + end +end diff --git a/openos/less.lua b/openos/less.lua new file mode 100644 index 0000000..9bf882f --- /dev/null +++ b/openos/less.lua @@ -0,0 +1,149 @@ +local keys = require("openos.keyboard").keys +local shell = require("openos.shell") +local unicode = require("openos.unicode") +local term = require("openos.term") -- using term for negative scroll feature + +local args, ops = shell.parse(...) +if #args > 1 then + io.write("Usage: ", os.getenv("_"):match("/([^/]+)%.lua$"), " \n") + io.write("- or no args reads stdin\n") + return 1 +end + +local cat_cmd = table.concat({"cat", ...}, " ") +if not io.output().tty then + return os.execute(cat_cmd) +end + +local preader = io.popen(cat_cmd) +local scrollback = not ops.noback and {} +local bottom = 0 +local end_of_buffer = false + +local width, height = term.getViewport() + +local function split(full_line) + local index = 1 + local parts = {} + while true do + local sub = full_line:sub(index, index + width*3) + -- checking #sub < width first is faster, save a unicode call + if #sub < width or unicode.wlen(sub) <= width then + parts[#parts + 1] = sub + break + end + parts[#parts + 1] = unicode.wtrunc(sub, width + 1) + index = index + #parts[#parts] + if index > #full_line then + break + end + end + return parts +end + +local function scan(num) + local result = {} + local line_count = 0 + for i=1, num do + local lines = {} + if scrollback and (bottom + i) <= #scrollback then + lines = {scrollback[bottom + i]} + else + local full_line = preader:read() + if not full_line then preader:close() break end + -- with buffering, we can buffer ahead too, and read more smoothly + local buffering = false + for _,line in ipairs(split(full_line)) do + if not buffering then + lines[#lines + 1] = line + end + if scrollback then + buffering = true + scrollback[#scrollback + 1] = line + end + end + end + + for _,line in ipairs(lines) do + result[#result + 1] = line + line_count = line_count + 1 + if #result > height then + table.remove(result, 1) + end + end + + if line_count >= num then + break + end + end + return result, line_count +end + +local function status() + if end_of_buffer then + if ops.noback then + os.exit() + end + io.write("(END)") + end + io.write(":") +end + +local function goback(n) + if not scrollback then return end + local current_top = bottom - height + 1 + n = math.min(current_top, n) + if n < 1 then return end + local top = current_top - n + 1 + term.scroll(-n) + term.setCursor(1, 1) + for i=1, n do + if i >= height then + break + end + print(scrollback[top + i - 1]) + end + term.setCursor(1, height) + bottom = bottom - n + end_of_buffer = false +end + +local function goforward(n) + term.clearLine() + local update, line_count = scan(n) + for _,line in ipairs(update) do + print(line) + end + if line_count < n then + end_of_buffer = true + end + bottom = bottom + line_count +end + +goforward(height - 1) + +while true do + term.clearLine() + status() + local e, _, _, code = term.pull() + if e == "interrupted" then + break + elseif e == "key_down" then + if code == keys.q then + term.clearLine() + os.exit() -- abort + elseif code == keys["end"] then + goforward(math.huge) + elseif code == keys.space or code == keys.pageDown then + goforward(height - 1) + elseif code == keys.enter or code == keys.down then + goforward(1) + elseif code == keys.up then + goback(1) + elseif code == keys.pageUp then + goback(height - 1) + elseif code == keys.home then + goback(math.huge) + end + end +end diff --git a/openos/ln.lua b/openos/ln.lua new file mode 100644 index 0000000..b89ca53 --- /dev/null +++ b/openos/ln.lua @@ -0,0 +1,35 @@ +local component = require("component") +local fs = require("filesystem") +local shell = require("shell") + +local args = shell.parse(...) +if #args == 0 then + io.write("Usage: ln []\n") + return 1 +end + +local target_name = args[1] +local target = shell.resolve(target_name) + +-- don't link from target if it doesn't exist, unless it is a broken link +if not fs.exists(target) and not fs.isLink(target) then + io.stderr:write("ln: failed to access '" .. target_name .. "': No such file or directory\n") + return 1 +end + +local linkpath +if #args > 1 then + linkpath = shell.resolve(args[2]) +else + linkpath = fs.concat(shell.getWorkingDirectory(), fs.name(target)) +end + +if fs.isDirectory(linkpath) then + linkpath = fs.concat(linkpath, fs.name(target)) +end + +local result, reason = fs.link(target_name, linkpath) +if not result then + io.stderr:write(reason..'\n') + return 1 +end diff --git a/openos/ls.lua b/openos/ls.lua new file mode 100644 index 0000000..93ac93c --- /dev/null +++ b/openos/ls.lua @@ -0,0 +1,374 @@ +local fs = require("openos.filesystem") +local shell = require("openos.shell") +local tty = require("openos.tty") +local unicode = require("openos.unicode") +local tx = require("openos.transforms") +local text = require("openos.text") + +local dirsArg, ops = shell.parse(...) + +if ops.help then + print([[Usage: ls [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 and/or -s, print human readable sizes + --si likewise, but use powers of 1000 not 1024 + -l use a long listing format + -r, --reverse reverse order while sorting + -R, --recursive list subdirectories recursively + -S sort by file size + -X sort alphabetically by entry extension + -1 list one file per line + -p append / indicator to directories + -M display Microsoft-style file and directory + count after listing + --no-color Do not colorize the output (default colorized) + --help display this help and exit +]]) + return 0 +end + +if #dirsArg == 0 then + table.insert(dirsArg, ".") +end + +local ec = 0 +local fOut = true -- tty.isAvailable() and io.output().tty +local function perr(msg) io.stderr:write(msg,"\n") ec = 2 end +local function stat(names, index) + local name = names[index] + if type(name) == "table" then + return name + end + local info = {} + info.key = name + info.path = name:sub(1, 1) == "/" and "" or names.path + info.full_path = fs.concat(info.path, name) + info.isDir = fs.isDirectory(info.full_path) + info.name = name:gsub("/+$", "") .. (ops.p and info.isDir and "/" or "") + info.sort_name = info.name:gsub("^%.","") + info.isLink, info.link = fs.isLink(info.full_path) + info.size = info.isLink and 0 or fs.size(info.full_path) + info.fs = fs.get(info.full_path) + info.ext = info.name:match("(%.[^.]+)$") or "" + names[index] = info + return info +end +local function toArray(i) local r={} for n in i do r[#r+1]=n end return r end +local set_color = function() end +local function colorize() return end +if fOut and not ops["no-color"] then + local LSC = tx.foreach(text.split(os.getenv("LS_COLORS") or "", {":"}, true), function(e) + local parts = text.split(e, {"="}, true) + return parts[2], parts[1] + end) + colorize = function(info) + return + info.isLink and LSC.ln or + info.isDir and LSC.di or + LSC['*'..info.ext] or + LSC.fi + end + set_color=function(c) + local cmap = { + [ '30' ] = _G.colors.black, + [ '31' ] = _G.colors.red, + [ '32' ] = _G.colors.green, + [ '33' ] = _G.colors.yellow, + [ '34' ] = _G.colors.blue, + [ '35' ] = _G.colors.magenta, + [ '36' ] = _G.colors.cyan, + [ '37' ] = _G.colors.white, + } + + if c then + c:gsub('(%d+)', function(code) + if cmap[code] then + term.setTextColor(cmap[code]) + elseif cmap[code] == '0' then + term.setTextColor(colors.white) + end + end) + else + term.setTextColor(colors.white) + end +-- io.write(string.char(0x1b), "[", c or "", "m") + end +end +local msft={reports=0,proxies={}} +function msft.report(files, dirs, used, proxy) + local free = proxy.spaceTotal() - proxy.spaceUsed() + set_color() + local pattern = "%5i File(s) %s bytes\n%5i Dir(s) %11s bytes free\n" + io.write(string.format(pattern, files, tostring(used), dirs, tostring(free))) +end +function msft.tail(names) + local fsproxy = fs.get(names.path) + if not fsproxy then + return + end + local totalSize, totalFiles, totalDirs = 0, 0, 0 + for i=1,#names do + local info = stat(names, i) + if info.isDir then + totalDirs = totalDirs + 1 + else + totalFiles = totalFiles + 1 + end + totalSize = totalSize + info.size + end + msft.report(totalFiles, totalDirs, totalSize, fsproxy) + local ps = msft.proxies + ps[fsproxy] = ps[fsproxy] or {files=0,dirs=0,used=0} + local p = ps[fsproxy] + p.files = p.files + totalFiles + p.dirs = p.dirs + totalDirs + p.used = p.used + totalSize + msft.reports = msft.reports + 1 +end +function msft.final() + if msft.reports < 2 then return end + local groups = {} + for proxy,report in pairs(msft.proxies) do + table.insert(groups, {proxy=proxy,report=report}) + end + set_color() + print("Total Files Listed:") + for _,pair in ipairs(groups) do + local proxy, report = pair.proxy, pair.report + if #groups>1 then + print("As pertaining to: "..proxy.address) + end + msft.report(report.files, report.dirs, report.used, proxy) + end +end + +if not ops.M then + msft.tail=function()end + msft.final=function()end +end + +local function nod(n) + return n and (tostring(n):gsub("(%.[0-9]+)0+$","%1")) or "0" +end + +local function formatSize(size) + if not ops.h and not ops['human-readable'] and not ops.si then + return tostring(size) + end + local sizes = {"", "K", "M", "G"} + local unit = 1 + local power = ops.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 filter(names) + if ops.a then + return names + end + local set = {} + for key, value in pairs(names) do + if type(key) == "number" then + local info = stat(names, key) + if fs.name(info.name):sub(1, 1) ~= "." then + table.insert(set, names[key]) + end + else + set[key] = value + end + end + return set +end + +local function sort(names) + local once = false + local function ni(v) + local vname = type(v) == "string" and v or v.key + for i=1,#names do + local info = stat(names, i) + if info.key == vname then + return i + end + end + end + local function sorter(key) + once = true + table.sort(names, function(a, b) + local ast = stat(names, ni(a)) + local bst = stat(names, ni(b)) + return ast[key] > bst[key] + end) + end + if ops.t then sorter("time") end + if ops.X then sorter("ext") end + if ops.S then sorter("size") end + local rev = ops.r or ops.reverse + if not once then sorter("sort_name") rev=not rev end + if rev then + for i=1,#names/2 do + names[i], names[#names - i + 1] = names[#names - i + 1], names[i] + end + end + return names +end + +local function dig(names, dirs, dir) + if ops.R then + local di = 1 + for i=1,#names do + local info = stat(names, i) + if info.isDir then + local path = dir..(dir:sub(-1) == "/" and "" or "/") + table.insert(dirs, di, path..info.name) + di = di + 1 + end + end + end + return names +end + +local first_display = true +local function display(names) + local mt={} + local lines = setmetatable({}, mt) + if ops.l then + lines.n = #names + local max_size_width = 1 + for i=1,lines.n do + local info = stat(names, i) + max_size_width = math.max(max_size_width, formatSize(info.size):len()) + end + mt.__index = function(_, index) + local info = stat(names, index) + local file_type = info.isLink and 'l' or info.isDir and 'd' or 'f' + local link_target = info.isLink and string.format(" -> %s", info.link:gsub("/+$", "") .. (info.isDir and "/" or "")) or "" + local write_mode = info.fs.isReadOnly() and '-' or 'w' + local size = formatSize(info.size) + local format = "%s-r%s %+"..tostring(max_size_width)..'s ' + local meta = string.format(format, file_type, write_mode, size) + local item = info.name..link_target + + return {{name = meta}, {color = colorize(info), name = item}} + end + elseif ops["1"] or not fOut then + lines.n = #names + mt.__index = function(_, index) + local info = stat(names, index) + return {{color = colorize(info), name = info.name}} + end + else -- columns + local num_columns, items_per_column, width = 0, 0, tty.getViewport() - 1 + local function real(x, y) + local index = y + ((x-1) * items_per_column) + return index <= #names and index or nil + end + local function max_name(column_index) + local max = 0 -- return the width of the max element in column_index + for r=1,items_per_column do + local ri = real(column_index, r) + if not ri then break end + local info = stat(names, ri) + max = math.max(max, unicode.wlen(info.name)) + end + return max + end + local function measure() + local total = 0 + for column_index=1,num_columns do + total = total + max_name(column_index) + (column_index > 1 and 2 or 0) + end + return total + end + while items_per_column<#names do + items_per_column = items_per_column + 1 + num_columns = math.ceil(#names/items_per_column) + if measure() < width then + break + end + end + lines.n = items_per_column + mt.__index=function(_, line_index) + return setmetatable({},{ + __len=function()return num_columns end, + __index=function(_, column_index) + local ri = real(column_index, line_index) + if not ri then return end + local info = stat(names, ri) + local name = info.name + return {color = colorize(info), name = name .. string.rep(' ', max_name(column_index) - unicode.wlen(name) + (column_index < num_columns and 2 or 0))} + end, + }) + end + end + + for line_index=1,lines.n do + local line = lines[line_index] + for element_index=1,#line do + local e = line[element_index] + if not e then break end + first_display = false + set_color(e.color) + io.write(e.name) + end + print() + end + msft.tail(names) +end +local header = function() end +if #dirsArg > 1 or ops.R then + header = function(path) + if not first_display then print() end + set_color() + io.write(path,":\n") + end +end +local function displayDirList(dirs) + while #dirs > 0 do + local dir = table.remove(dirs, 1) + header(dir) + local path = shell.resolve(dir) + local list, reason = fs.list(path) + if not list then + perr(reason) + else + local names = toArray(list) + names.path = path + display(dig(sort(filter(names)), dirs, dir)) + end + end +end +local dir_set, file_set = {}, {path=shell.getWorkingDirectory()} +for _,dir in ipairs(dirsArg) do + local path = shell.resolve(dir) + local real, why = fs.realPath(path) + local access_msg = "cannot access " .. tostring(path) .. ": " + if not real then + perr(access_msg .. why) + elseif not fs.exists(path) then + perr(access_msg .. "No such file or directory") + elseif fs.isDirectory(path) then + table.insert(dir_set, dir) + else -- file or link + table.insert(file_set, dir) + end +end + +io.output():setvbuf("line") + +--local ok, msg = pcall(function() + if #file_set > 0 then display(sort(file_set)) end + displayDirList(dir_set) + msft.final() +--end) + +io.output():flush() +io.output():setvbuf("no") +set_color() + +--assert(ok, msg) + +return ec diff --git a/openos/mv.lua b/openos/mv.lua new file mode 100644 index 0000000..9567d6f --- /dev/null +++ b/openos/mv.lua @@ -0,0 +1,33 @@ +local shell = require("shell") +local transfer = require("tools/transfer") + +local args, options = shell.parse(...) +options.h = options.h or options.help +if #args < 2 or options.h then + io.write([[Usage: mv [OPTIONS] + -f overwrite without prompt + -i prompt before overwriting + unless -f + -v verbose + -n do not overwrite an existing file + --skip=P ignore paths matching lua regex P + -h, --help show this help +]]) + return not not options.h +end + +-- clean options for move (as opposed to copy) +options = +{ + cmd = "mv", + f = options.f, + i = options.i, + v = options.v, + n = options.n, -- no clobber + skip = options.skip, + P = true, -- move operations always preserve + r = true, -- move is allowed to move entire dirs + x = true, -- cannot move mount points +} + +return transfer.batch(args, options) diff --git a/openos/ps.lua b/openos/ps.lua new file mode 100644 index 0000000..2c96027 --- /dev/null +++ b/openos/ps.lua @@ -0,0 +1,155 @@ +local process = require("process") +local unicode = require("unicode") +local event = require("event") +local thread = require("thread") +local event_mt = getmetatable(event.handlers) + +-- WARNING this code does not use official kernel API and is likely to change + +local data = {} +local widths = {} +local sorted = {} +local moved_indexes = {} + +local elbow = unicode.char(0x2514) + +local function thread_id(t,p) + if t then + return tostring(t):gsub("^thread: 0x", "") + end + -- find the parent thread + for k,v in pairs(process.list) do + if v == p then + return thread_id(k) + end + end + return "-" +end + +local cols = +{ + {"PID", thread_id}, + {"EVENTS", function(_,p) + local handlers = {} + if event_mt.threaded then + handlers = rawget(p.data, "handlers") or {} + elseif not p.parent then + handlers = event.handlers + end + local count = 0 + for _ in pairs(handlers) do + count = count + 1 + end + return count == 0 and "-" or tostring(count) + end}, + {"THREADS", function(_,p) + -- threads are handles with mt.close == thread.waitForAll + local count = 0 + for h in pairs(p.data.handles) do + local mt = getmetatable(h) + if mt and mt.__status then + count = count + 1 + end + end + return count == 0 and "-" or tostring(count) + end}, + {"PARENT", function(_,p) + for _,process_info in pairs(process.list) do + for handle in pairs(process_info.data.handles) do + local mt = getmetatable(handle) + if mt and mt.__status then + if mt.process == p then + return thread_id(nil, process_info) + end + end + end + end + return thread_id(nil, p.parent) + end}, + {"HANDLES", function(_, p) + local count = 0 + for _,closure in pairs(p.data.handles) do + if closure then + count = count + 1 + end + end + return count == 0 and "-" or tostring(count) + end}, + {"CMD", function(_,p) return p.command end}, +} + +local function add_field(key, value) + if not data[key] then data[key] = {} end + table.insert(data[key], value) + widths[key] = math.max(widths[key] or 0, #value) +end + +for _,key in ipairs(cols) do + add_field(key[1], key[1]) +end + +for thread_handle, process_info in pairs(process.list) do + for _,key in ipairs(cols) do + add_field(key[1], key[2](thread_handle, process_info)) + end +end + +local parent_index +for index,set in ipairs(cols) do + if set[1] == "PARENT" then + parent_index = index + break + end +end +assert(parent_index, "did not find a parent column") + +local function move_to_sorted(index) + if moved_indexes[index] then + return false + end + local entry = {} + for k,v in pairs(data) do + entry[k] = v[index] + end + sorted[#sorted + 1] = entry + moved_indexes[index] = true + return true +end + +local function make_elbow(depth) + return (" "):rep(depth - 1) .. (depth > 0 and elbow or "") +end + +-- remove COLUMN labels to simplify sort +move_to_sorted(1) + +local function update_family(parent, depth) + depth = depth or 0 + parent = parent or "-" + for index in ipairs(data.PID) do + local this_parent = data[cols[parent_index][1]][index] + if this_parent == parent then + local dash_cmd = make_elbow(depth) .. data.CMD[index] + data.CMD[index] = dash_cmd + widths.CMD = math.max(widths.CMD or 0, #dash_cmd) + if move_to_sorted(index) then + update_family(data.PID[index], depth + 1) + end + end + end +end + +update_family() +table.remove(cols, parent_index) -- don't show parent id + +for _,set in ipairs(sorted) do + local split = "" + for _,key in ipairs(cols) do + local label = key[1] + local format = split .. "%-" .. tostring(widths[label]) .. "s" + io.write(string.format(format, set[label])) + split = " " + end + print() +end + diff --git a/openos/rm.lua b/openos/rm.lua new file mode 100644 index 0000000..48ed9d0 --- /dev/null +++ b/openos/rm.lua @@ -0,0 +1,158 @@ +local fs = require("filesystem") +local shell = require("shell") + +local function usage() + print("Usage: rm [options] [ [...]]"..[[ + + -f ignore nonexistent files and arguments, never prompt + -r remove directories and their contents recursively + -v explain what is being done + --help display this help and exit + +For complete documentation and more options, run: man rm]]) +end + +local args, options = shell.parse(...) +if #args == 0 or options.help then + usage() + return 1 +end + +local bRec = options.r or options.R or options.recursive +local bForce = options.f or options.force +local bVerbose = options.v or options.verbose +local bEmptyDirs = options.d or options.dir +local promptLevel = (options.I and 3) or (options.i and 1) or 0 + +bVerbose = bVerbose and not bForce +promptLevel = bForce and 0 or promptLevel + +local function perr(...) + if not bForce then + io.stderr:write(...) + end +end + +local function pout(...) + if not bForce then + io.stdout:write(...) + end +end + +local metas = {} + +-- promptLevel 3 done before fs.exists +-- promptLevel 1 asks for each, displaying fs.exists on hit as it visits + +local function _path(m) return shell.resolve(m.rel) end +local function _link(m) return fs.isLink(_path(m)) end +local function _exists(m) return _link(m) or fs.exists(_path(m)) end +local function _dir(m) return not _link(m) and fs.isDirectory(_path(m)) end +local function _readonly(m) return not _exists(m) or fs.get(_path(m)).isReadOnly() end +local function _empty(m) return _exists(m) and _dir(m) and (fs.list(_path(m))==nil) end + +local function createMeta(origin, rel) + local m = {origin=origin,rel=rel:gsub("/+$", "")} + if _dir(m) then + m.rel = m.rel .. '/' + end + return m +end + +local function unlink(path) + os.remove(path) + return true +end + +local function confirm() + if bForce then + return true + end + local r = io.read() + return r == 'y' or r == 'yes' +end + +local function remove_all(parent) + if parent == nil or not _dir(parent) or _empty(parent) then + return true + end + + local all_ok = true + if bRec and promptLevel == 1 then + pout(string.format("rm: descend into directory `%s'? ", parent.rel)) + if not confirm() then + return false + end + + for file in fs.list(_path(parent)) do + local child = createMeta(parent.origin, parent.rel .. file) + all_ok = remove(child) and all_ok + end + end + + return all_ok +end + +local function remove(meta) + if not remove_all(meta) then + return false + end + + if not _exists(meta) then + perr(string.format("rm: cannot remove `%s': No such file or directory\n", meta.rel)) + return false + elseif _dir(meta) and not bRec and not (_empty(meta) and bEmptyDirs) then + if not bEmptyDirs then + perr(string.format("rm: cannot remove `%s': Is a directory\n", meta.rel)) + else + perr(string.format("rm: cannot remove `%s': Directory not empty\n", meta.rel)) + end + return false + end + + local ok = true + if promptLevel == 1 then + if _dir(meta) then + pout(string.format("rm: remove directory `%s'? ", meta.rel)) + elseif meta.link then + pout(string.format("rm: remove symbolic link `%s'? ", meta.rel)) + else -- file + pout(string.format("rm: remove regular file `%s'? ", meta.rel)) + end + + ok = confirm() + end + + if ok then + if _readonly(meta) then + perr(string.format("rm: cannot remove `%s': Is read only\n", meta.rel)) + return false + elseif not unlink(_path(meta)) then + perr(meta.rel .. ": failed to be removed\n") + ok = false + elseif bVerbose then + pout("removed '" .. meta.rel .. "'\n"); + end + end + + return ok +end + +for _,arg in ipairs(args) do + metas[#metas+1] = createMeta(arg, arg) +end + +if promptLevel == 3 and #metas > 3 then + pout(string.format("rm: remove %i arguments? ", #metas)) + if not confirm() then + return + end +end + +local ok = true +for _,meta in ipairs(metas) do + local result = remove(meta) + ok = ok and result +end + +return bForce or ok diff --git a/openos/rmdir.lua b/openos/rmdir.lua new file mode 100644 index 0000000..98ef870 --- /dev/null +++ b/openos/rmdir.lua @@ -0,0 +1,104 @@ +local shell = require("shell") +local fs = require("filesystem") +local text = require("text") + +local args, options = shell.parse(...) + +local function usage() + print( +[[Usage: rmdir [OPTION]... DIRECTORY... +Removes the DIRECTORY(ies), if they are empty. + + -q, --ignore-fail-on-non-empty + ignore failures due solely to non-empty directories + -p, --parents remove DIRECTORY and its empty ancestors + e.g. 'rmdir -p a/b/c' is similar to 'rmdir a/b/c a/b a' + -v, --verbose output a diagnostic for every directory processed + --help display this help and exit]]) +end + +if options.help then + usage() + return 0 +end + +if #args == 0 then + io.stderr:write("rmdir: missing operand\n") + return 1 +end + +options.p = options.p or options.parents +options.v = options.v or options.verbose +options.q = options.q or options['ignore-fail-on-non-empty'] + +local ec = 0 +local function ec_bump() + ec = 1 + return 1 +end + +local function remove(path, ...) + -- check to end recursion + if path == nil then + return true + end + + if options.v then + print(string.format('rmdir: removing directory, %s', path)) + end + + local rpath = shell.resolve(path) + if path == '.' then + io.stderr:write('rmdir: failed to remove directory \'.\': Invalid argument\n') + return ec_bump() + elseif not fs.exists(rpath) then + io.stderr:write("rmdir: cannot remove " .. path .. ": path does not exist\n") + return ec_bump() + elseif fs.isLink(rpath) or not fs.isDirectory(rpath) then + io.stderr:write("rmdir: cannot remove " .. path .. ": not a directory\n") + return ec_bump() + else + local list, reason = fs.list(rpath) + + if not list then + io.stderr:write(tostring(reason)..'\n') + return ec_bump() + else + if list() then + if not options.q then + io.stderr:write("rmdir: failed to remove " .. path .. ": Directory not empty\n") + end + return ec_bump() + else + -- path exists and is empty? + local ok, reason = fs.remove(rpath) + if not ok then + io.stderr:write(tostring(reason)..'\n') + return ec_bump(), reason + end + return remove(...) -- the final return of all else + end + end + end +end + +for _,path in ipairs(args) do + -- clean up the input + path = path:gsub('/+', '/') + + local segments = {} + if options.p and path:len() > 1 and path:find('/') then + chain = text.split(path, {'/'}, true) + local prefix = '' + for _,e in ipairs(chain) do + table.insert(segments, 1, prefix .. e) + prefix = prefix .. e .. '/' + end + else + segments = {path} + end + + remove(table.unpack(segments)) +end + +return ec diff --git a/openos/set.lua b/openos/set.lua new file mode 100644 index 0000000..6608af5 --- /dev/null +++ b/openos/set.lua @@ -0,0 +1,23 @@ +local args = {...} + +if #args < 1 then + for k,v in pairs(os.getenv()) do + io.write(k .. "='" .. string.gsub(v, "'", [['"'"']]) .. "'\n") + end +else + local count = 0 + for _, expr in ipairs(args) do + local e = expr:find('=') + if e then + os.setenv(expr:sub(1,e-1), expr:sub(e+1)) + else + if count == 0 then + for i = 1, os.getenv('#') do + os.setenv(i, nil) + end + end + count = count + 1 + os.setenv(count, expr) + end + end +end diff --git a/openos/source.lua b/openos/source.lua new file mode 100644 index 0000000..58d8f20 --- /dev/null +++ b/openos/source.lua @@ -0,0 +1,36 @@ +local shell = require("shell") +local process = require("process") + +local args, options = shell.parse(...) + +if #args ~= 1 then + io.stderr:write("specify a single file to source\n"); + return 1 +end + +local file, open_reason = io.open(args[1], "r") + +if not file then + if not options.q then + io.stderr:write(string.format("could not source %s because: %s\n", args[1], open_reason)); + end + return 1 +end + +local lines = file:lines() + +while true do + local line = lines() + if not line then + break + end + local current_data = process.info().data + + local source_proc = process.load((assert(os.getenv("SHELL"), "no $SHELL set"))) + local source_data = process.list[source_proc].data + source_data.aliases = current_data.aliases -- hacks to propogate sub shell env changes + source_data.vars = current_data.vars + process.internal.continue(source_proc, _ENV, line) +end + +file:close() diff --git a/openos/time.lua b/openos/time.lua new file mode 100644 index 0000000..0b9fa79 --- /dev/null +++ b/openos/time.lua @@ -0,0 +1,18 @@ +local computer = require('openos.computer') +local sh = require('openos.sh') + +local real_before, cpu_before = computer.uptime(), os.clock() +local cmd_result = 0 +if ... then + sh.execute(nil, ...) + cmd_result = sh.getLastExitCode() +end +local real_after, cpu_after = computer.uptime(), os.clock() + +local real_diff = real_after - real_before +local cpu_diff = cpu_after - cpu_before + +print(string.format('real%5dm%.3fs', math.floor(real_diff/60), real_diff%60)) +print(string.format('cpu %5dm%.3fs', math.floor(cpu_diff/60), cpu_diff%60)) + +return cmd_result diff --git a/openos/touch.lua b/openos/touch.lua new file mode 100644 index 0000000..74e541d --- /dev/null +++ b/openos/touch.lua @@ -0,0 +1,54 @@ +--[[Lua implementation of the UN*X touch command--]] +local shell = require("shell") +local fs = require("filesystem") + +local args, options = shell.parse(...) + +local function usage() + print( +[[Usage: touch [OPTION]... FILE... +Update the modification times of each FILE to the current time. +A FILE argument that does not exist is created empty, unless -c is supplied. + + -c, --no-create do not create any files + --help display this help and exit]]) +end + +if options.help then + usage() + return 0 +elseif #args == 0 then + io.stderr:write("touch: missing operand\n") + return 1 +end + +options.c = options.c or options["no-create"] +local errors = 0 + +for _,arg in ipairs(args) do + local path = shell.resolve(arg) + + if fs.isDirectory(path) then + io.stderr:write(string.format("`%s' ignored: directories not supported\n", arg)) + else + local real, reason = fs.realPath(path) + if real then + local file + if fs.exists(real) or not options.c then + file = io.open(real, "a") + end + if not file then + real = options.c + reason = "permission denied" + else + file:close() + end + end + if not real then + io.stderr:write(string.format("touch: cannot touch `%s': %s\n", arg, reason)) + errors = 1 + end + end +end + +return errors diff --git a/openos/tree.lua b/openos/tree.lua new file mode 100644 index 0000000..e67246c --- /dev/null +++ b/openos/tree.lua @@ -0,0 +1,331 @@ +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 + diff --git a/openos/unalias.lua b/openos/unalias.lua new file mode 100644 index 0000000..ceea94d --- /dev/null +++ b/openos/unalias.lua @@ -0,0 +1,19 @@ +local shell = require("shell") + +local args = shell.parse(...) +if #args < 1 then + io.write("Usage: unalias ...\n") + return 2 +end +local e = 0 + +for _,arg in ipairs(args) do + local result = shell.getAlias(arg) + if not result then + io.stderr:write(string.format("unalias: %s: not found\n", arg)) + e = 1 + else + shell.setAlias(arg, nil) + end +end +return e diff --git a/openos/unset.lua b/openos/unset.lua new file mode 100644 index 0000000..1fb68e4 --- /dev/null +++ b/openos/unset.lua @@ -0,0 +1,9 @@ +local args = {...} + +if #args < 1 then + io.write("Usage: unset [ [...]]\n") +else + for _, k in ipairs(args) do + os.setenv(k, nil) + end +end diff --git a/openos/which.lua b/openos/which.lua new file mode 100644 index 0000000..503d442 --- /dev/null +++ b/openos/which.lua @@ -0,0 +1,25 @@ +local shell = require("shell") + +local args = shell.parse(...) +if #args == 0 then + io.write("Usage: which \n") + return 255 +end + +for i = 1, #args do + local result, reason = shell.resolve(args[i], "lua") + + if not result then + result = shell.getAlias(args[i]) + if result then + result = args[i] .. ": aliased to " .. result + end + end + + if result then + print(result) + else + io.stderr:write(args[i] .. ": " .. reason .. "\n") + return 1 + end +end