--:Minify:-- -- Supports: s/pat/repl/[gip], d, p, q, =, addr1[,addr2]cmd -- Addressing: line numbers, $, /regex/ -- Flags: -n (silent), -e script, -i (in-place) local name = syscall.getTask(syscall.getpid()).name local scripts = {} local files = {} local silent = false local inplace = false local args = { ... } local i = 1 while i <= #args do local a = args[i] if a == "-n" then silent = true elseif a == "-i" then inplace = true elseif a == "-e" then i = i + 1 if not args[i] then print(name .. ": option -e requires an argument"); syscall.exit(1); return end table.insert(scripts, args[i]) elseif a:sub(1,2) == "-e" then table.insert(scripts, a:sub(3)) elseif a == "--help" then print("Usage: " .. name .. " [OPTION]... SCRIPT [FILE...]") print(" " .. name .. " [OPTION]... -e SCRIPT... [FILE...]") print("Stream editor. Reads FILE(s) (or stdin) line by line,") print("applies SCRIPT, and writes results to stdout.") print("") print("Commands:") print(" s/REGEX/REPL/[flags] substitute (flags: g global, i ignore-case, p print)") print(" d delete line (skip to next)") print(" p print current line") print(" q quit") print(" = print current line number") print(" y/src/dst/ transliterate characters") print("") print("Addressing (prefix any command):") print(" N line number N") print(" $ last line") print(" /REGEX/ lines matching regex") print(" N,M line range") print(" N,/REGEX/ from line N until regex match") print("") print("Options:") print(" -n suppress default output") print(" -e SCRIPT add script expression") print(" -i edit file in-place") print(" --help display this help and exit") return elseif a:sub(1,1) == "-" then print(name .. ": unknown option: " .. a) syscall.exit(1); return else if #scripts == 0 then table.insert(scripts, a) else table.insert(files, a) end end i = i + 1 end if #scripts == 0 then print(name .. ": no script specified"); syscall.exit(1); return end local script = table.concat(scripts, "\n") local function patEscape(s) return s:gsub("([%(%)%.%%%+%-%*%?%[%^%$])", "%%%1") end local function sedPatToLua(pat, icase) pat = pat:gsub("\\%(", "("):gsub("\\%)", ")") pat = pat:gsub("\\1", "%%1"):gsub("\\2", "%%2") return pat end local function parseDelim(s, pos, delim) local out = {} while pos <= #s do local c = s:sub(pos, pos) if c == "\\" and pos < #s then pos = pos + 1 local nc = s:sub(pos, pos) if nc == delim then table.insert(out, delim) elseif nc == "n" then table.insert(out, "\n") else table.insert(out, "\\" .. nc) end elseif c == delim then return table.concat(out), pos + 1 else table.insert(out, c) end pos = pos + 1 end return table.concat(out), pos end local function parseAddr(s, pos) local c = s:sub(pos, pos) if c == "" then return nil, pos end if c:match("%d") then local numstr = s:match("^(%d+)", pos) return { type="line", n=tonumber(numstr) }, pos + #numstr elseif c == "$" then return { type="last" }, pos + 1 elseif c == "/" then local pat, npos = parseDelim(s, pos + 1, "/") return { type="regex", pat=pat }, npos end return nil, pos end local function parseCommands(src) local cmds = {} local pos = 1 local len = #src local function skip() while pos <= len and (src:sub(pos,pos) == " " or src:sub(pos,pos) == "\t") do pos = pos + 1 end end while pos <= len do skip() if pos > len then break end local c = src:sub(pos, pos) if c == "\n" or c == ";" then pos = pos + 1 goto continue end if c == "#" then while pos <= len and src:sub(pos,pos) ~= "\n" do pos = pos + 1 end goto continue end local addr1, addr2 addr1, pos = parseAddr(src, pos) skip() if addr1 and pos <= len and src:sub(pos,pos) == "," then pos = pos + 1 skip() addr2, pos = parseAddr(src, pos) end skip() if pos > len then break end local cmd = src:sub(pos, pos) pos = pos + 1 if cmd == "s" then local delim = src:sub(pos, pos); pos = pos + 1 local pat, p1 = parseDelim(src, pos, delim); pos = p1 local repl, p2 = parseDelim(src, pos, delim); pos = p2 local flags = "" while pos <= len and src:sub(pos,pos):match("[giIp]") do flags = flags .. src:sub(pos,pos); pos = pos + 1 end table.insert(cmds, { addr1=addr1, addr2=addr2, cmd="s", pat=pat, repl=repl, flags=flags }) elseif cmd == "y" then local delim = src:sub(pos, pos); pos = pos + 1 local srcch, p1 = parseDelim(src, pos, delim); pos = p1 local dstch, p2 = parseDelim(src, pos, delim); pos = p2 table.insert(cmds, { addr1=addr1, addr2=addr2, cmd="y", src=srcch, dst=dstch }) elseif cmd == "d" or cmd == "p" or cmd == "q" or cmd == "=" then table.insert(cmds, { addr1=addr1, addr2=addr2, cmd=cmd }) elseif cmd == "{" then local depth = 1 local start = pos while pos <= len and depth > 0 do local ch = src:sub(pos,pos) if ch == "{" then depth = depth + 1 elseif ch == "}" then depth = depth - 1 end pos = pos + 1 end local inner = src:sub(start, pos - 2) local innerCmds = parseCommands(inner) for _, ic in ipairs(innerCmds) do ic.addr1 = ic.addr1 or addr1 ic.addr2 = ic.addr2 or addr2 end for _, ic in ipairs(innerCmds) do table.insert(cmds, ic) end elseif cmd == "\n" or cmd == ";" then else end ::continue:: end return cmds end local cmds = parseCommands(script) local inRange = {} local function addrMatch(cmd, lineNum, line, isLast, ci) local a1 = cmd.addr1 local a2 = cmd.addr2 if not a1 then return true end local function matchOne(addr, ln, l) if addr.type == "line" then return ln == addr.n elseif addr.type == "last" then return isLast elseif addr.type == "regex" then return l:find(sedPatToLua(addr.pat)) ~= nil end return false end if not a2 then return matchOne(a1, lineNum, line) end if inRange[ci] then local endMatch if a2.type == "line" then endMatch = (lineNum >= a2.n) elseif a2.type == "last" then endMatch = isLast elseif a2.type == "regex" then endMatch = (line:find(sedPatToLua(a2.pat)) ~= nil) end if endMatch then inRange[ci] = false end return true else if matchOne(a1, lineNum, line) then if a2.type == "line" and a2.n <= lineNum then else inRange[ci] = true end return true end return false end end local function doSubst(line, pat, repl, flags) local global = flags:find("g") ~= nil local icase = flags:find("[iI]") ~= nil local luaPat = sedPatToLua(pat, icase) local function buildRepl(whole, ...) local caps = { ... } local out = {} local rp = repl local ri = 1 while ri <= #rp do local rc = rp:sub(ri, ri) if rc == "&" then table.insert(out, whole) elseif rc == "\\" and ri < #rp then ri = ri + 1 local nc = rp:sub(ri, ri) if nc:match("%d") then local idx = tonumber(nc) table.insert(out, caps[idx] or "") elseif nc == "n" then table.insert(out, "\n") else table.insert(out, nc) end else table.insert(out, rc) end ri = ri + 1 end return table.concat(out) end local result local changed = false if global then result = line:gsub(luaPat, buildRepl) changed = (result ~= line) else local s, e, whole local parts = { line:find(luaPat) } if parts[1] then s = parts[1]; e = parts[2] local caps = {} for ci = 3, #parts do caps[#caps+1] = parts[ci] end local wmatch = line:sub(s, e) local replStr = buildRepl(wmatch, table.unpack(caps)) result = line:sub(1, s-1) .. replStr .. line:sub(e+1) changed = true else result = line end end return result, changed end local function doTranslit(line, src, dst) local out = {} for ci = 1, #line do local c = line:sub(ci, ci) local idx = src:find(c, 1, true) if idx and idx <= #dst then table.insert(out, dst:sub(idx, idx)) else table.insert(out, c) end end return table.concat(out) end local function processLines(lines, outputLines) local total = #lines for lineNum, line in ipairs(lines) do local isLast = (lineNum == total) local deleted = false local printed = false local quit = false local bare = line:gsub("\n$", "") for ci, cmd in ipairs(cmds) do if addrMatch(cmd, lineNum, bare, isLast, ci) then if cmd.cmd == "d" then deleted = true; break elseif cmd.cmd == "p" then table.insert(outputLines, bare) elseif cmd.cmd == "=" then table.insert(outputLines, tostring(lineNum)) elseif cmd.cmd == "q" then if not silent then table.insert(outputLines, bare) end quit = true; break elseif cmd.cmd == "s" then local newLine, changed = doSubst(bare, cmd.pat, cmd.repl, cmd.flags) bare = newLine if changed and cmd.flags:find("p") then table.insert(outputLines, bare) end elseif cmd.cmd == "y" then bare = doTranslit(bare, cmd.src, cmd.dst) end end end if quit then break end if not deleted and not silent then table.insert(outputLines, bare) end end end local function readLines(fd) local lines = {} local buf = "" while true do local chunk = syscall.read(fd, 1024) if not chunk or chunk == "" then break end buf = buf .. chunk end for line in (buf .. "\n"):gmatch("([^\n]*)\n") do table.insert(lines, line) end if buf ~= "" and buf:sub(-1) ~= "\n" and lines[#lines] == "" then table.remove(lines) end return lines end local function runOnFile(path) local fd if path then local ok, err = pcall(function() fd = syscall.open(path, "r") end) if not ok then print(name .. ": " .. path .. ": " .. tostring(err)) return false end else fd = 0 end local lines = readLines(fd) if path then syscall.close(fd) end inRange = {} local outputLines = {} processLines(lines, outputLines) if inplace and path then local wfd = syscall.open(path, "w") for _, ol in ipairs(outputLines) do syscall.write(wfd, ol .. "\n") end syscall.close(wfd) else for _, ol in ipairs(outputLines) do print(ol) end end return true end if #files == 0 then runOnFile(nil) else for _, f in ipairs(files) do local absf = f if absf:sub(1,1) ~= "/" then absf = syscall.getcwd() .. "/" .. f end runOnFile(absf) end end