trun

Script for parsing any output. Yes, it is all it does.
git clone git://gtms.dev/trun.git
Log | Files | Refs | README | LICENSE

commit b19e7c94c42e1faa6a9fd3381e26accc41735927
parent b79b706d70fe6025eae2df981f106eb9aa6d4aae
Author: Tomas Nemec <nemi@skaut.cz>
Date:   Sun,  8 Aug 2021 17:08:58 +0200

feat: Make it public

Diffstat:
MMakefile | 6++----
AREADME | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aexamples/handler_simple.lua | 45+++++++++++++++++++++++++++++++++++++++++++++
Aexamples/handler_temp_output_file.lua | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/print_status.lua | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/trun_to_neovim_quickfix.lua | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtrun.lua | 108++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Dtrun_status.lua | 64----------------------------------------------------------------
8 files changed, 436 insertions(+), 99 deletions(-)

diff --git a/Makefile b/Makefile @@ -1,15 +1,13 @@ -BINDIR = /usr/local/bin -LUAPATH = /usr/lib/lua/5.4 +PREFIX = /usr/local +BINDIR = $(PREFIX)/bin all: @echo "Nothing to do, try \"make install\" instead." install: @install -v -d "$(BINDIR)/" && install -m 0755 -v "./trun.lua" "$(BINDIR)/trun" - @install -v -d "$(BINDIR)/" && install -m 0755 -v "./trun_status.lua" "$(BINDIR)/trun_status" uninstall: trun.lua trun_status.lua @rm -vrf "$(BINDIR)/trun" - @rm -vrf "$(BINDIR)/trun_status" .PHONY: all install uninstall diff --git a/README b/README @@ -0,0 +1,91 @@ +# Track run +TrackRun parse stdin of command and cache status to file. +Status is taken from handler module via `handle` function. + +! Lots of thing are explained inside scripts so check them out. + +# Dependencies + +It is written in lua and used/tested on linux. I have no plan to make it work +for windows. But besides that nothing else is really needed. +- If you want to trun to better handle process signals you will want to install + luaposix via loarocks (luarocks install luaposix). So trun can cleanup files on sig{term,kill,int,hup} + +And for some specific task you would need other tools: +- For sending command to neovim instances you will need neovim-remote + (https://github.com/mhinz/neovim-remote). + +# Folders +Inside project you will find number of different files. +- examples/ - handler examples +- handlers/ - real life handlers i use. (contact me to add more) + - TODO +- tools/ - other tools that uses trun to make life easier + - print_status.lua: i use it for show semaphor of cmd (green,orange,red) + - trun_to_neovim_quickfix.lua: plugin to fill qf-list from cmd + +# Install +You can install trun with: +make install + +Also uninstall via `make uninstall`. But take a look at Makefile and see what +it does... It is straightforward. + + +# Usage + +<cmd> 1> >(trun <handler> <name>) + +_explanation_ +`1>` - copy output of cmd to stdout +`>(...)` - copy output of cmd to stdin of subprocess +So if you don't need output in terminal you can simply `|` (pipe) to trun + + +# Handler +Handler is module that implements specific functions to catch some key +moment of tracking. Every handler must implement 'handle' function that +returns status for that line. It takes two arguments: +1. `userdata` is table for storing data between hooks +2. `line` is current line to get status from. + +Returned status can be anything you want to store. Everything is just written +to status_file + +Handlers must have `lua` ext. and be inside specific folder. viz->#Config + +## Handle function +handle function can return either table or string. When table is returned, +every item will end up on new line in status_file so it can be easily used +by other scripts. + +## Hooks +There are these hook functions you can implement: +- on_start(userdata): before first line is passed to handle function +- on_update(userdata, line, status): when status change +- on_end(userdata): when script end + +## Userdata +userdata has pre filled `name` key with value of trun name + +# Config +There are 2 ENV variables you may want to set: +- TRUN_HANDLERS_DIR: directory where handlers are stored (default to $XDG_CONFIG_HOME/trun) +- TRUN_STATUS_DIR: directory to store files with statuses (default to $XDG_CACHE_HOME/trun) + + +# Example +1. angular handler +inside $TRUN_HANDLERS_DIR/angular.lua you have module for angular serving +output. Inside it you have simple handling just for status if build is +running, succeeded or has error. +viz -> examples/handler_simple.lua for way to run neovim command + +2. You can run angular serving cmd and pipe output to trun like `<cmd> | trun angular my_app`. + +3. angular status file +Inside $TRUN_STATUS_DIR is created `my_app.angular` with current status in it. + +# Contact - Bugs, Suggestions, Questions... + +Feel free to contact me via mail on: <nemi@skaut.cz> diff --git a/examples/handler_simple.lua b/examples/handler_simple.lua @@ -0,0 +1,45 @@ +--- Trun simple handler for just status of command +-- +-- interface +-- - handle(userdata, line): return status +-- - (optional) on_start(userdata): call once before start reading +-- - (optional) on_update(userdata, line, status): calls every time when status changed +-- - (optional) on_end(userdata): calls once after reading end +-- +local status_map = {['running'] = 0, ['success'] = 1, ['error'] = -1} + +-- send nvim_cmd to all neovim instances +local nvim_cmd = function(nvim_cmd) + local servers = io.popen('nvr --serverlist') + for socket in servers:lines() do + local cmd = string.format('nvr --servername "%s" -cc "%s" --nostart -s &', socket, nvim_cmd) + print(cmd) + os.execute(cmd) + end + servers:close() +end + +return { + -- handle(userdata, line): return status + handle = function(_, line) + if line:find('SUCCESS') then + return status_map.success + elseif line:find('RUNNING') then + return status_map.running + else + return status_map.error + end + end, + + -- on_update(userdata, line, status): calls every time when status changed + on_update = function(data, _, status) + + -- you can send linux notifications + os.execute(string.format('notify-send "trun status changed for %s" "%s"', data.name, status)) + + -- or you can run neovim command to trigger neotification. + -- You need neovim-remote (https://github.com/mhinz/neovim-remote) to make it work + nvim_cmd(string.format('lua require(\'notify\')(\'%s\', \'%s\')', data.name, 'error')) + -- make sure to use single quotes so vim can properly handle command. + end, +} diff --git a/examples/handler_temp_output_file.lua b/examples/handler_temp_output_file.lua @@ -0,0 +1,66 @@ +-- Trun advanced handler for *track status* and *cache* last output betwen status +-- changes in tmp_file. It saves path to that file in status_file on second line. +-- +-- So for exmple, on error you can load this output to quickfix list +-- inside vim. +-- viz->tools/trun_to_neovim_quickfix.lua and examples/handler_simple.lua for +-- examples of neovim-remote (to notify inside neovim or you can run Trunqf to +-- load quickfix list on fly) +-- +-- interface +-- - handle(userdata, line): return status +-- - (optional) on_start(userdata): call once before start reading +-- - (optional) on_update(userdata, line, status): calls every time when status changed +-- - (optional) on_end(userdata): calls once after reading end +-- +local status_map = {['running'] = 0, ['success'] = 1, ['error'] = -1} + +local notify = + function(status) os.execute('notify-send "trun status changed" "' .. status .. '"') end + +local function get_status(line) + if line:find('SUCCESS') then + return status_map.success + elseif line:find('RUNNING') then + return status_map.running + else + return status_map.error + end +end + +return { + + -- on_start(userdata): call once before start reading + on_start = function(data) + -- re-create tmp_file and save it to data table. + data.tmpfile = '/tmp/' .. data.name .. '.trun' + os.execute('rm ' .. data.tmpfile .. ' 2>/dev/null') + end, + + -- handle(userdata, line): return status + handle = function(line, data) + -- write each line to tmp_file + local tmpfile = io.open(data.tmpfile, 'a') + tmpfile:write(line, '\n') + tmpfile:close() + return {get_status(line), data.tmpfile} + end, + + -- on_update(userdata, line, status): calls every time when status changed + on_update = function(_, status, data) + -- on success, clear tmp_file. + if status[1] == status_map.success then + io.open(data.tmpfile, 'w'):close() + end + notify() + + -- viz->examples/handler_simple.lua for other examples wit neovim-remote + end, + + -- on_end(userdata): calls once after reading end + on_end = function(data) + notify() + -- remove tmp_file + os.execute('rm ' .. data.tmpfile) + end, +} diff --git a/tools/print_status.lua b/tools/print_status.lua @@ -0,0 +1,91 @@ +#!/usr/bin/env lua + +-- Usage: trun_status.lua [-] [<name>] +-- +-- Return formatted string for all truns to use inside status bar of your WM. +-- It depends on status codes to be: +-- - `0` - running +-- - `1` - success +-- - `-1` - error +-- +-- You can change colors. viz->#Config +-- +-- example output: [%{F#ff0000}NAME%{F-} %{F#00ff00}OTHERNAME%{F-}] +-- +-- To ignore formatting add `-`(single dash) argument. Mostly for debugging. +-- - It will just print output of status_file +-- +-- To get only single trun, add `name` as argument. +-- +-- It does not print any errors. (TODO) +-- +-- Config +local trun_status = os.getenv('TRUN_STATUS_DIR') or os.getenv('XDG_CONFIG_HOME') .. '/trun' +-- map status to foreground colors. +local status_map = {[0] = 'ffa500', [1] = '00ff00', [-1] = 'ff0000'} +--- + +-- silent fail if dir not exists +local status_dir = trun_status +if not io.open(status_dir) then + return '' +end + +local trun_name = arg[1] +local raw -- single dash argument means - no formatting, just print +if arg[1] == '-' then + raw = true + trun_name = arg[2] +end + +-- get all status files +local status_files = {} +local list = io.popen('ls ' .. status_dir) +for f in list:lines() do + table.insert(status_files, f) +end + +-- format status_file to string +local format = function(file, name) + local output + if not file then + table.insert(output, '') + else + if raw then + local status = file:read('*a') + output = status + else + local status = file:read('*n') + -- Edit this to your liking output + output = '%{F#' .. status_map[tonumber(status)] .. '} ' .. name:upper() .. ' %{F-}' + file:close() + end + end + return output +end + +local result = {} +-- For every file fill out results +for _, status_file_name in pairs(status_files) do + local name, _ = status_file_name:match('(.*)%.(.*)') + if name then + local status_file_path = status_dir .. '/' .. status_file_name + local status_file = io.open(status_file_path, 'r') + if trun_name then + if name == trun_name then + table.insert(result, format(status_file, name)) + end + else + table.insert(result, format(status_file, name)) + end + end +end + +-- print out results +if #result > 0 then + if raw then + print(table.concat(result, '\n')) + else + print('[' .. table.concat(result, ',') .. ']') + end +end diff --git a/tools/trun_to_neovim_quickfix.lua b/tools/trun_to_neovim_quickfix.lua @@ -0,0 +1,64 @@ +-- Add this to your nvim folder to `plugin/trun.lua`. +-- +-- use `:Trunqf <name>` to fill quickfix list with lines after last +-- success status. It has autocompletion for running truns, press <tab> to list +-- those. +-- +-- !important +-- Handler needs to store the output inside file and path to that file must be +-- added as second line to status file. viz->examples/handler_tmp_output_file.lua +-- +-- returns list of running truns +local complete = function() + local dir = os.getenv('TRUN_STATUS_DIR') or os.getenv('XDG_CACHE_HOME') .. '/trun' + local ls = io.popen('ls ' .. dir) + local truns = {} + for trun in ls:lines() do + local name = trun:match('(.*)%..*') + table.insert(truns, name) + end + return truns +end + +-- add trun to quickfix list +-- it needs to have its tempfile for output +local to_qf = function(name) + if not name then + return + end + vim.fn.setqflist({}, 'r') + local handle = io.popen('trun_status - ' .. name) + local o = {} + for line in handle:lines() do + table.insert(o, line) + end + if #o == 0 then + print('No running trun for "' .. name .. '"') + return + end + -- edit this if path to tmpfile(errorfile) is on another line + local errorfile = o[2] + if errorfile then + local errfile = io.open(errorfile, 'r') + local lines = {} + for line in errfile:lines() do + table.insert(lines, line) + end + vim.fn.setqflist({}, ' ', {lines = lines}) + vim.cmd [[ copen ]] + vim.cmd [[ normal G ]] + else + print('Trun for "' .. name .. '" does not have tmp file') + return + end +end + +-- Make functions globally accessible +_G.Trun = {complete = complete, qf = to_qf} + +vim.cmd [[ +fun! TrunComplete(A,L,P) + return v:lua.Trun.complete() +endfun +]] +vim.cmd [[command! -nargs=1 -complete=customlist,TrunComplete Trunqf v:lua.Trun.qf("<args>")]] diff --git a/trun.lua b/trun.lua @@ -1,59 +1,106 @@ #!/usr/bin/env lua --- <run cmd> | run_track <handler> <name> -local handlerName = arg[1] -if not handlerName then - error('No handler provided!') +-- Version: 1.0.0 ( 08.08.2021 ) +-- +-- Usage +-- <cmd> 1> >(trun <handler> <name>) +-- +-- # Track run +-- TrackRun parse stdin of command and cache status to file. +-- Status is taken from handler module via `handle` function. +-- +-- # Handler +-- Handler is module that implements specific functions to catch some key +-- moment of tracking. Every handler must implement 'handle' function that +-- returns status for that line. It takes two arguments: +-- 1. `userdata` is table for storing data between hooks +-- 2. `line` is current line to get status from. +-- +-- Handlers must have `lua` ext. and be inside specific folder. viz->#Config +-- +-- ## Handle function +-- handle function can return either table or string. When table is returned, +-- every item will end up on new line in status_file so it can be easily used +-- by other scripts. +-- +-- ## Hooks +-- There are these hook functions you can implement: +-- - on_start(userdata): before first line is passed to handle function +-- - on_update(userdata, line, status): when status change +-- - on_end(userdata): when script end +-- +-- ## Userdata +-- userdata has pre filled `name` key with value of trun name +-- +-- # Config +-- There are 2 ENV VAR you may want to set: +-- - TRUN_HANDLERS_DIR: directory where handlers are stored (default to XDG_CONFIG_HOME/trun) +local handlers_dir = os.getenv('TRUN_HANDLERS_DIR') or os.getenv('XDG_CONFIG_HOME') .. '/trun' +-- - TRUN_STATUS_DIR: directory to store files with statuses (default to XDG_CACHE_HOME/trun) +local status_dir = os.getenv('TRUN_STATUS_DIR') or os.getenv('XDG_CACHE_HOME') .. '/trun' +-- + +local handler_name = arg[1] +if not handler_name then + io.write('No handler provided!\n') + os.exit(1) end local name = arg[2] or 'trun' -local handlerPath = os.getenv('CONFIG') .. '/trun/?.lua' -package.path = package.path .. ';' .. handlerPath -local handler = require(handlerName) +-- load handler +local handler_path = string.format('%s/?.lua', handlers_dir) +package.path = handler_path .. ';' .. package.path +local handler = require(handler_name) +if not handler then + io.write('handler "' .. handler_name .. '" not found!\n') + os.exit(1) +end local userdata = {name = name} - -- status dir -local status_dir = os.getenv('TRUN_CACHE') or os.getenv('HOME') .. '/.cache/trun' if not io.open(status_dir) then os.execute('mkdir ' .. status_dir) end -- status file -local status_file = status_dir .. '/' .. name .. '.' .. handlerName +local status_file = status_dir .. '/' .. name .. '.' .. handler_name local file = io.open(status_file, 'w') if not file then os.execute('touch ' .. status_file) - file = io.open(status_file, 'w') end ------------------------------------------------------------------------------------------------------------------------ --- handling signals ------------------------------------------------------------------------------------------------------------------------ -local signal = require 'posix.signal' - +-- cleanup removes status file and notify handlers. local function cleanup() os.execute('rm ' .. status_file) - if handler.onEnd then - handler.onEnd(userdata) + if handler.on_end then + handler.on_end(userdata) end end -local function onExit(signum) +----------------------------------------------------------------------------------------------------------------------- +-- handling signals +----------------------------------------------------------------------------------------------------------------------- + +local function on_exit(signum) cleanup() os.exit(signum) end -signal.signal(signal.SIGINT, onExit) -signal.signal(signal.SIGTERM, onExit) -signal.signal(signal.SIGKILL, onExit) -signal.signal(signal.SIGHUP, onExit) +local signal = require 'posix.signal' +if signal then + signal.signal(signal.SIGINT, on_exit) + signal.signal(signal.SIGTERM, on_exit) + signal.signal(signal.SIGKILL, on_exit) + signal.signal(signal.SIGHUP, on_exit) +else + io.write('For signal handle install luaposix (`luarocks install luaposix`).\n') +end ----------------------------------------------------------------------------------------------------------------------- local status -local function updateFile(output) +local function update_file(output) file = io.open(status_file, 'w') if type(output) == 'table' then file:write(table.concat(output, '\n')) @@ -63,21 +110,20 @@ local function updateFile(output) file:close() end -if handler.onStart then - handler.onStart(userdata) +if handler.on_start then + handler.on_start(userdata) end local lastStatus for line in io.lines() do lastStatus = status - status = handler.handle(line, userdata) + status = handler.handle(userdata, line) if status and status ~= lastStatus then - updateFile(status) - if handler.onUpdate then - handler.onUpdate(line, status, userdata) + update_file(status) + if handler.on_update then + handler.on_update(userdata, line, status) end end end --- cleanup cleanup() diff --git a/trun_status.lua b/trun_status.lua @@ -1,64 +0,0 @@ -#!/usr/bin/env lua - -local status_dir = os.getenv('HOME') .. '/.cache/trun' -if not io.open(status_dir) then - return '' -end - -local name = arg[1] -local raw -if arg[1] == '-' then - raw = true - name = arg[2] -end - -local ds = {[0] = 'ffa500', [1] = '00ff00', [-1] = 'ff0000'} - -local files = {} -local list = io.popen('ls ' .. status_dir) -for f in list:lines() do - table.insert(files, f) -end - -local function format(file, fname) - local result - if not file then - table.insert(result, '') - else - if raw then - local status = file:read('*a') - result = status - else - local status = file:read('*n') - result = '%{F#' .. ds[tonumber(status)] .. '} ' .. fname:upper() .. ' %{F-}' - file:close() - end - end - return result -end - -local result = {} -for _, f in pairs(files) do - local fname, _ = f:match('(.*)%.(.*)') - if fname then - if name then - if fname == name then - local status_file = status_dir .. '/' .. f - local file = io.open(status_file, 'r') - table.insert(result, format(file, fname)) - end - else - local status_file = status_dir .. '/' .. f - local file = io.open(status_file, 'r') - table.insert(result, format(file, fname)) - end - end -end - -if #result > 0 then - if raw then - print(table.concat(result, '\n')) - else - print('[' .. table.concat(result, ',') .. ']') - end -end