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