blob: 9015b4fc6677afa74527faa9c7ee330088d630c0 [file] [log] [blame] [raw]
--[[
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 term = require("term")
-- Process the command line arguments
local args, options = shell.parse(...)
local gpu = term.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 term.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 = 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 max_matches == m.hits or 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("^^") then break end
end
if not empty_line then write("\n") end
end
for meta,status in readLines() do
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