mirror of
https://github.com/NeoFlock/neonucleus.git
synced 2025-09-24 09:03:32 +02:00
324 lines
8.7 KiB
Lua
324 lines
8.7 KiB
Lua
--[[
|
|
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("filesystem")
|
|
local shell = require("shell")
|
|
local tty = require("tty")
|
|
local computer = require("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 i,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
|