shell tools: globbing

This commit is contained in:
kepler155c@gmail.com
2019-05-10 08:08:49 -04:00
parent 8ea2598254
commit 86cba3b585
35 changed files with 303 additions and 432 deletions

View File

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

View File

@@ -1,331 +0,0 @@
local computer = require("openos.computer")
local shell = require("openos.shell")
local fs = require("openos.filesystem")
local tx = require("openos.transforms")
local text = require("openos.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

7
shellex/.package Normal file
View File

@@ -0,0 +1,7 @@
{
title = 'Shell utilities',
repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/shellex',
description = [[Experimental!
Linux style shell commands: grep, cat, touch, df, etc ]],
licence = 'MIT',
}

50
shellex/apis/glob.lua Normal file
View File

@@ -0,0 +1,50 @@
local GtoP = require('shellex.globtopattern')
local Glob = { }
local fs = _G.fs
local function splitpath(path)
local parts = { }
for match in string.gmatch(path, "[^/]+") do
table.insert(parts, match)
end
return parts
end
function Glob.matches(path, spec)
local t = { }
local ss = splitpath(spec)
local abs = string.sub(spec, 1, 1) == '/'
local function dirMatches(dir, i)
local files = fs.list(dir)
local s = GtoP.globtopattern(ss[i])
for _, f in pairs(files) do
if f:match(s) then
local fp = fs.combine(dir, f)
if not ss[i + 1] then
table.insert(t, '/' .. fp)
elseif ss[i + 1] and fs.isDir(fp) then
dirMatches(fp, i + 1)
end
end
end
end
path = '/' .. fs.combine('', path) -- normalize
dirMatches(abs and '' or path, 1)
if not abs then
local len = path == '/' and #path + 1 or #path + 2
for k, v in pairs(t) do
t[k] = v:sub(len)
end
end
return t
end
return Glob

View File

@@ -0,0 +1,119 @@
-- see: https://github.com/davidm/lua-glob-pattern
local M = {_TYPE='module', _NAME='globtopattern', _VERSION='0.2.1.20120406'}
function M.globtopattern(g)
-- Some useful references:
-- - apr_fnmatch in Apache APR. For example,
-- http://apr.apache.org/docs/apr/1.3/group__apr__fnmatch.html
-- which cites POSIX 1003.2-1992, section B.6.
local p = "^" -- pattern being built
local i = 0 -- index in g
local c -- char at index i in g.
-- unescape glob char
local function unescape()
if c == '\\' then
i = i + 1; c = g:sub(i,i)
if c == '' then
p = '[^]'
return false
end
end
return true
end
-- escape pattern char
local function escape(c)
return c:match("^%w$") and c or '%' .. c
end
-- Convert tokens at end of charset.
local function charset_end()
while 1 do
if c == '' then
p = '[^]'
return false
elseif c == ']' then
p = p .. ']'
break
else
if not unescape() then break end
local c1 = c
i = i + 1; c = g:sub(i,i)
if c == '' then
p = '[^]'
return false
elseif c == '-' then
i = i + 1; c = g:sub(i,i)
if c == '' then
p = '[^]'
return false
elseif c == ']' then
p = p .. escape(c1) .. '%-]'
break
else
if not unescape() then break end
p = p .. escape(c1) .. '-' .. escape(c)
end
elseif c == ']' then
p = p .. escape(c1) .. ']'
break
else
p = p .. escape(c1)
i = i - 1 -- put back
end
end
i = i + 1; c = g:sub(i,i)
end
return true
end
-- Convert tokens in charset.
local function charset()
i = i + 1; c = g:sub(i,i)
if c == '' or c == ']' then
p = '[^]'
return false
elseif c == '^' or c == '!' then
i = i + 1; c = g:sub(i,i)
if c == ']' then
-- ignored
else
p = p .. '[^'
if not charset_end() then return false end
end
else
p = p .. '['
if not charset_end() then return false end
end
return true
end
-- Convert tokens.
while 1 do
i = i + 1; c = g:sub(i,i)
if c == '' then
p = p .. '$'
break
elseif c == '?' then
p = p .. '.'
elseif c == '*' then
p = p .. '.*'
elseif c == '[' then
if not charset() then break end
elseif c == '\\' then
i = i + 1; c = g:sub(i,i)
if c == '' then
p = p .. '\\$'
break
end
p = p .. escape(c)
else
p = p .. escape(c)
end
end
return p
end
return M

View File

@@ -1,5 +1,5 @@
local unicode = require("openos.unicode")
local tx = require("openos.transforms")
local unicode = require("shellex.unicode")
local tx = require("shellex.transforms")
local text = {}
text.internal = {}

View File

@@ -1,6 +1,7 @@
local fs = require("openos.filesystem")
local shell = require("openos.shell")
local text = require("openos.text")
local fs = require("shellex.filesystem")
local glob = require('shellex.glob')
local shell = require("shellex.shell")
local text = require("shellex.text")
local lib = {}
local function perr(ops, format, ...)
@@ -236,28 +237,31 @@ function lib.batch(args, options)
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
for _, arg in ipairs(args) do
local files = glob.matches(shell.getWorkingDirectory(), arg)
for _, fromArg in pairs(files) 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
end
local result, reason = lib.recurse(fromArg, toPath, options, origin, true)
local result, reason = lib.recurse(fromArg, toPath, options, origin, true)
if not result then
perr(options, reason)
if not result then
perr(options, reason)
end
end
end
end

View File

@@ -1,4 +1,3 @@
local lib={}
lib.internal={}

View File

@@ -1,5 +1,5 @@
local shell = require("openos.shell")
local fs = require("openos.filesystem")
local shell = require("shellex.shell")
local fs = require("shellex.filesystem")
local args = shell.parse(...)
local ec = 0

View File

@@ -1,5 +1,5 @@
local shell = require("openos.shell")
local transfer = require("openos.transfer")
local shell = require("shellex.shell")
local transfer = require("shellex.transfer")
local args, options = shell.parse(...)
options.h = options.h or options.help

View File

@@ -1,6 +1,6 @@
local fs = require("openos.filesystem")
local shell = require("openos.shell")
local text = require("openos.text")
local fs = require("shellex.filesystem")
local shell = require("shellex.shell")
local text = require("shellex.text")
local args, options = shell.parse(...)

View File

@@ -1,4 +1,4 @@
local tty = require("openos.tty")
local tty = require("shellex.tty")
local args = {...}
local gpu = tty.gpu()

View File

@@ -1,5 +1,6 @@
local shell = require("openos.shell")
local fs = require("openos.filesystem")
local fs = require("shellex.filesystem")
local glob = require('shellex.glob')
local shell = require("shellex.shell")
local args, options = shell.parse(...)
if #args == 0 then
@@ -107,22 +108,24 @@ local function visitor(rpath)
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)
local files = glob.matches(shell.getWorkingDirectory(), arg)
for _, v in pairs(files) do
local path = shell.resolve(v)
if not fs.exists(path) then
io.stderr:write(string.format("du: cannot access '%s': no such file or directory\n", v))
return 1
else
printSize(fs.size(path), arg)
if fs.isDirectory(path) then
local total = visitor(v)
if bSummary then
printSize(total, v)
end
elseif fs.isLink(path) then
printSize(0, v)
else
printSize(fs.size(path), v)
end
end
end
end

View File

@@ -1,6 +1,6 @@
local shell = require("openos.shell")
local fs = require("openos.filesystem")
local text = require("openos.text")
local shell = require("shellex.shell")
local fs = require("shellex.filesystem")
local text = require("shellex.text")
local USAGE =
[===[Usage: find [path] [--type=[dfs]] [--[i]name=EXPR]

View File

@@ -6,10 +6,11 @@ https://raw.githubusercontent.com/OpenPrograms/Wobbo-Programs/master/grep/grep.l
-- 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")
local computer = require("shellex.computer")
local fs = require("shellex.filesystem")
local glob = require('shellex.glob')
local shell = require("shellex.shell")
local tty = require("shellex.tty")
-- Process the command line arguments
@@ -28,8 +29,14 @@ for more information, run: man grep
]])
end
local PATTERNS = {args[1]}
local FILES = {select(2, table.unpack(args))}
local PATTERNS = { table.remove(args, 1) }
local FILES = { }
for _, arg in pairs(args) do
local files = glob.matches(shell.getWorkingDirectory(), arg)
for _, v in pairs(files) do
table.insert(FILES, v)
end
end
local LABEL_COLOR = 0xb000b0
local LINE_NUM_COLOR = 0x00FF00
@@ -167,7 +174,7 @@ end
if search_recursively then
local files = {}
for _,arg in ipairs(FILES) do
if fs.isDirectory(arg) then
if fs.isDirectory(shell.resolve(arg)) then
getAllFiles(arg, files)
else
files[#files+1]=arg

View File

@@ -1,5 +1,4 @@
local shell = require("openos.shell")
local fs = require("openos.filesystem")
local shell = require("shellex.shell")
local args, options = shell.parse(...)
local error_code = 0

View File

@@ -1,6 +1,6 @@
local os = _G.os
local shell = require("openos.shell")
local shell = require("shellex.shell")
local args = shell.parse(...)
local hostname = args[1]

View File

@@ -1,7 +1,7 @@
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 keys = require("shellex.keyboard").keys
local shell = require("shellex.shell")
local unicode = require("shellex.unicode")
local term = require("shellex.term") -- using term for negative scroll feature
local args, ops = shell.parse(...)
if #args > 1 then

View File

@@ -1,5 +1,5 @@
local fs = require("openos.filesystem")
local shell = require("openos.shell")
local fs = require("shellex.filesystem")
local shell = require("shellex.shell")
local args = shell.parse(...)
if #args == 0 then

View File

@@ -1,9 +1,10 @@
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 fs = require("shellex.filesystem")
local glob = require('shellex.glob')
local shell = require("shellex.shell")
local tty = require("shellex.tty")
local unicode = require("shellex.unicode")
local tx = require("shellex.transforms")
local text = require("shellex.text")
local dirsArg, ops = shell.parse(...)
@@ -42,7 +43,6 @@ local function stat(names, index)
end
local info = {}
info.key = name
info._path = 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)
@@ -248,7 +248,6 @@ local function display(names)
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 ""
_G._p = info
local write_mode = info.fs.isReadOnly() and '-' or 'w'
local size = formatSize(info.size)
local format = "%s-r%s %+"..tostring(max_size_width)..'s '
@@ -345,6 +344,7 @@ local function displayDirList(dirs)
end
end
end
--[[
local dir_set, file_set = {}, {path=shell.getWorkingDirectory()}
for _,dir in ipairs(dirsArg) do
local path = shell.resolve(dir)
@@ -360,6 +360,23 @@ for _,dir in ipairs(dirsArg) do
table.insert(file_set, dir)
end
end
]]
local dir_set, file_set = {}, {path=shell.getWorkingDirectory()}
for _,dir in ipairs(dirsArg) do
local path = shell.resolve(dir)
if fs.isDirectory(path) then
table.insert(dir_set, dir)
else
local files = glob.matches(shell.getWorkingDirectory(), dir)
for _, v in pairs(files) do
table.insert(file_set, v)
end
if #files == 0 then
local access_msg = "cannot access " .. tostring(dir) .. ": "
perr(access_msg .. "No such file or directory")
end
end
end
io.output():setvbuf("line")

View File

@@ -1,5 +1,5 @@
local shell = require("openos.shell")
local transfer = require("openos.transfer")
local shell = require("shellex.shell")
local transfer = require("shellex.transfer")
local args, options = shell.parse(...)
options.h = options.h or options.help

View File

@@ -1,5 +1,6 @@
local fs = require("openos.filesystem")
local shell = require("openos.shell")
local fs = require("shellex.filesystem")
local glob = require('shellex.glob')
local shell = require("shellex.shell")
local function usage()
print("Usage: rm [options] <filename1> [<filename2> [...]]"..[[
@@ -40,14 +41,14 @@ local function pout(...)
end
local metas = {}
local remove
-- 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 _exists(m) return fs.exists(_path(m)) end
local function _dir(m) return 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
@@ -94,7 +95,7 @@ local function remove_all(parent)
return all_ok
end
local function remove(meta)
remove = function(meta)
if not remove_all(meta) then
return false
end
@@ -140,7 +141,10 @@ local function remove(meta)
end
for _,arg in ipairs(args) do
metas[#metas+1] = createMeta(arg, arg)
local files = glob.matches(shell.getWorkingDirectory(), arg)
for _, v in pairs(files) do
metas[#metas+1] = createMeta(v, v)
end
end
if promptLevel == 3 and #metas > 3 then

View File

@@ -1,6 +1,6 @@
local shell = require("openos.shell")
local fs = require("openos.filesystem")
local text = require("openos.text")
local shell = require("shellex.shell")
local fs = require("shellex.filesystem")
local text = require("shellex.text")
local args, options = shell.parse(...)

View File

@@ -1,5 +1,5 @@
local computer = require('openos.computer')
local sh = require('openos.sh')
local computer = require('shellex.computer')
local sh = require('shellex.sh')
local real_before, cpu_before = computer.uptime(), os.clock()
local cmd_result = 0

View File

@@ -1,6 +1,6 @@
--[[Lua implementation of the UN*X touch command--]]
local shell = require("openos.shell")
local fs = require("openos.filesystem")
local shell = require("shellex.shell")
local fs = require("shellex.filesystem")
local args, options = shell.parse(...)

View File

@@ -1,4 +1,4 @@
local shell = require("openos.shell")
local shell = require("shellex.shell")
local args = shell.parse(...)
if #args == 0 then