refactor parallel code

This commit is contained in:
kepler155c@gmail.com
2019-04-05 17:32:22 -04:00
parent d5eb07c3b9
commit 426c856dfb
44 changed files with 3645 additions and 182 deletions

View File

@@ -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

View File

@@ -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

35
milo/apis/taskRunner.lua Normal file
View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

6
openos/.package Normal file
View File

@@ -0,0 +1,6 @@
{
title = 'Shell utilities',
repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/openos',
description = [[ Utilties for shell: grep, cat, touch, etc ]],
licence = 'MIT',
}

3
openos/apis/computer.lua Normal file
View File

@@ -0,0 +1,3 @@
return {
uptime = os.clock,
}

View File

@@ -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,
}

3
openos/apis/keyboard.lua Normal file
View File

@@ -0,0 +1,3 @@
return {
keys = _G.keys,
}

6
openos/apis/sh.lua Normal file
View File

@@ -0,0 +1,6 @@
local shell = _ENV.shell
return {
execute = function(_, ...) return shell.run(...) end,
getLastExitCode = function() return 0 end,
}

9
openos/apis/shell.lua Normal file
View File

@@ -0,0 +1,9 @@
local Util = require('util')
local shell = _ENV.shell
return {
getWorkingDirectory = shell.dir,
resolve = shell.resolve,
parse = Util.parse,
}

1
openos/apis/term.lua Normal file
View File

@@ -0,0 +1 @@
return _G.term

405
openos/apis/text.lua Normal file
View File

@@ -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

269
openos/apis/transfer.lua Normal file
View File

@@ -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

200
openos/apis/transforms.lua Normal file
View File

@@ -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<ds and ensured==i then
if #result==0 then result[1]={} end
table.insert(result[#result],e)
end
i=ensured
need=true
end
i=i+1
end
return result
end
-- calls callback(e,i,tbl) for each ith element e in table tbl from first
function lib.foreach(tbl,c,f,l)
checkArg(1,tbl,'table')
checkArg(2,c,'function','string')
local ck=c
c=type(c)=="string" and function(e) return e[ck] end or c
local s=#tbl
f,l=adjust(f,l,s)
tbl=view(tbl,f,l)
local r={}
for i=f,l do
local n,k=c(tbl[i],i,tbl)
if n~=nil then
if k then r[k]=n
else r[#r+1]=n end
end
end
return r
end
function lib.where(tbl,p,f,l)
return lib.foreach(tbl,
function(e,i,tbl)
return p(e,i,tbl)and e or nil
end,f,l)
end
-- works with pairs on tables
-- returns the kv pair, or nil and the number of pairs iterated
function lib.at(tbl, index)
checkArg(1, tbl, "table")
checkArg(2, index, "number", "nil")
local current_index = 1
for k,v in pairs(tbl) do
if current_index == index then
return k,v
end
current_index = current_index + 1
end
return nil, current_index - 1 -- went one too far
end
return lib

23
openos/apis/tty.lua Normal file
View File

@@ -0,0 +1,23 @@
local colors = _G.colors
local term = _G.term
local w, h = term.getSize()
local cmap = {
[ 0xCC2200 ] = colors.red,
[ 0x44CC00 ] = colors.lime,
[ 0xB0B00F ] = colors.yellow,
[ 0xFFFFFF ] = colors.white,
}
return {
gpu = function()
return {
setForeground = function(c) term.setTextColor(cmap[c]) end,
}
end,
getViewport = term.getSize,
window = {
width = w, height = h,
}
}

3
openos/apis/unicode.lua Normal file
View File

@@ -0,0 +1,3 @@
return {
wlen = string.len
}

View File

@@ -0,0 +1,11 @@
local os = _G.os
local settings = _G.settings
settings.set('LS_COLORS', "di=0;36:fi=0:ln=0;33:*.lua=0;32")
function os.setenv(k, v)
settings.set(k, v)
end
function os.getenv(k)
return settings.get(k)
end

30
openos/cat.lua Normal file
View File

@@ -0,0 +1,30 @@
local shell = require("openos.shell")
local fs = require("openos.filesystem")
local args = shell.parse(...)
local ec = 0
if #args == 0 then
args = {"-"}
end
for i = 1, #args do
local arg = shell.resolve(args[i])
if fs.isDirectory(arg) then
io.stderr:write(string.format('cat %s: Is a directory\n', arg))
ec = 1
else
local file, reason = fs.open(arg)
if not file then
io.stderr:write(string.format("cat: %s: %s\n", args[i], tostring(reason)))
ec = 1
else
local chunk = file.readAll()
file:close()
if chunk then
io.write(chunk)
end
end
end
end
return ec

36
openos/cp.lua Normal file
View File

@@ -0,0 +1,36 @@
local shell = require("openos.shell")
local transfer = require("openos.transfer")
local args, options = shell.parse(...)
options.h = options.h or options.help
if #args < 2 or options.h then
io.write([[Usage: cp [OPTIONS] <from...> <to>
-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)

76
openos/df.lua Normal file
View File

@@ -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

33
openos/dmesg.lua Normal file
View File

@@ -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)

128
openos/du.lua Normal file
View File

@@ -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

132
openos/find.lua Normal file
View File

@@ -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)

323
openos/grep.lua Normal file
View File

@@ -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

137
openos/head.lua Normal file
View File

@@ -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

17
openos/hostname.lua Normal file
View File

@@ -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

149
openos/less.lua Normal file
View File

@@ -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$"), " <filename>\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

35
openos/ln.lua Normal file
View File

@@ -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 <target> [<name>]\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

374
openos/ls.lua Normal file
View File

@@ -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

33
openos/mv.lua Normal file
View File

@@ -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] <from> <to>
-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)

155
openos/ps.lua Normal file
View File

@@ -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

158
openos/rm.lua Normal file
View File

@@ -0,0 +1,158 @@
local fs = require("filesystem")
local shell = require("shell")
local function usage()
print("Usage: rm [options] <filename1> [<filename2> [...]]"..[[
-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

104
openos/rmdir.lua Normal file
View File

@@ -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

23
openos/set.lua Normal file
View File

@@ -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

36
openos/source.lua Normal file
View File

@@ -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()

18
openos/time.lua Normal file
View File

@@ -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

54
openos/touch.lua Normal file
View File

@@ -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

331
openos/tree.lua Normal file
View File

@@ -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

19
openos/unalias.lua Normal file
View File

@@ -0,0 +1,19 @@
local shell = require("shell")
local args = shell.parse(...)
if #args < 1 then
io.write("Usage: unalias <name>...\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

9
openos/unset.lua Normal file
View File

@@ -0,0 +1,9 @@
local args = {...}
if #args < 1 then
io.write("Usage: unset <varname>[ <varname2> [...]]\n")
else
for _, k in ipairs(args) do
os.setenv(k, nil)
end
end

25
openos/which.lua Normal file
View File

@@ -0,0 +1,25 @@
local shell = require("shell")
local args = shell.parse(...)
if #args == 0 then
io.write("Usage: which <program>\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