cortav  cli.lua at [c50482b020]

File cli.lua artifact 9a8697d2cc part of check-in c50482b020


-- [Źž] cli.lua
--  ~ lexi hale <lexi@hale.su>
--  šŸ„Æ AGPLv3
--  ? simple command line driver for the cortav library
local ct = require 'cortav'
local ss = require 'sirsem'

local native = _G.native

local default_mode = {
	['render:format'] = 'html';
	['html:gen-styles'] = true;
	['groff:color'] = true;
}

local function
main(input, output, log, mode, suggestions, vars, extrule)
	local doc = ct.parse(input.stream, input.src, mode, function(c)
		                     c.doc.ext = extrule
	                     end)
	input.stream:close()
	if mode['parse:show-tree'] then
		log:write(ss.dump(doc))
	end

	-- the document has now had a chance to give its say; if it hasn't specified
	-- any modes of its own, we now merge in the 'weak modes' (suggestions)
	for k,v in pairs(suggestions) do
		if not mode[k] then mode[k] = v end
	end

	if not mode['render:format'] then
		error 'what output format should i translate the input to?'
	end
	if mode['render:format'] == 'none' then return 0 end
	if not ct.render[mode['render:format']] then
		if (not ct.render.html) and not _G.native then
			-- we may be running uncompiled; otherwise something is seriously broken
			require('render.' .. mode['render:format'])
		else
			ct.exns.unimpl('output format ā€œ%sā€ unsupported', mode['render:format']):throw()
		end
	end
	
	local render_opts = ss.kmap(function(k,v)
		return k:sub(2+#mode['render:format'])
	end, ss.kfilter(mode, function(m)
		return ss.str.begins(m, mode['render:format']..':')
	end))

	doc.vars = vars

	output:write(ct.render[mode['render:format']](
		doc, render_opts, function(stage)
			stage.mode = mode
		end))
	return 0
end

local inp,outp,log = io.stdin, io.stdout, io.stderr

local function entry_cli()
	local suggestions, vars, input = default_mode, {}, {
		stream = inp;
		src = {
			file = '(stdin)';
		}
	}

	local mode = {}

	local optnparams = function(o)
		local param_opts = {
			out = 1;
			log = 1;
			define = 2; -- key value

			['mode-set'] = 1;
			['mode-clear'] = 1;
			mode = 2;

			['mode-set-weak'] = 1;
			['mode-clear-weak'] = 1;
			['mode-weak'] = 2;
			['use'] = 1;
			['inhibit'] = 1;
			['need'] = 1;
			['load'] = 1;
			['enc'] = 1;
		}
		return param_opts[o] or 0
	end

	local optmap = {
		o = 'out';
		l = 'log';
		d = 'define';
		V = 'version';
		h = 'help';
		y = 'mode-set',   Y = 'mode-set-weak';
		n = 'mode-clear', N = 'mode-clear-weak';
		m = 'mode',       M = 'mode-weak';
		L = 'load',
		u = 'use', i = 'inhibit', r = 'require';
		e = 'enc';
	}

	local extrule = {use={},inhibit={},need={}}

	local checkmodekey = function(key)
		if not key:match '[^:]+:.+' then
			ct.exns.cli('invalid mode key %s', key):throw()
		end
		return key
	end
	local onswitch = {
		out = function(file)
			local nf = io.open(file,'wb')
			if nf then outp:close() outp = nf else
				ct.exns.io('could not open output file for writing', 'open',file):throw()
			end
		end;
		log = function(file)
			local nf = io.open(file,'wb')
			if nf then log:close() log = nf else
				ct.exns.io('could not open log file for writing', 'open',file):throw()
			end
		end;
		define = function(key,value)
			if ss.str.begins(key, 'cortav.') or ss.str.begins(key, 'env.') then
				ct.exns.cli 'cannot define variable in restricted namespace':throw()
			end
			vars[key] = value
		end;
		mode = function(key,value) mode[checkmodekey(key)] = value end;
		['mode-set'] = function(key) mode[checkmodekey(key)] = true end;
		['mode-clear'] = function(key) mode[checkmodekey(key)] = false end;

		['mode-weak'] = function(key,value) suggestions[checkmodekey(key)] = value end;
		['mode-set-weak'] = function(key) suggestions[checkmodekey(key)] = true end;
		['mode-clear-weak'] = function(key) suggestions[checkmodekey(key)] = false end;
		['use'    ] = function(ext) extrule.use    [ext] = true end;
		['inhibit'] = function(ext) extrule.inhibit[ext] = true end;
		['require'] = function(ext) extrule.need   [ext] = true end;
		['load'] = function(extpath) end;
		['enc'] = function(enc) end;
		['version'] = function()
			outp:write(ct.info:about())
			if next(ct.ext.loaded) then
				outp:write('\nactive extensions:\n')
				for k,v in pairs(ct.ext.loaded) do
					outp:write(string.format(' * %s', v.id ..
						(v.version and (' ' .. v.version:string()) or '')))
					if v.desc then
						outp:write(string.format(': %s', v.desc))
						if v.contributors then
							outp:write(string.format(' [%s]', table.concat(
								ss.map(function(ctr)
									return ctr.name or ctr.handle
								end, v.contributors), ', ')))
						end
					else
						outp:write'\n'
					end
				end
			end
			os.exit(0)
		end
	}

	local args = {}
	local keepParsing = true
	do local i = 1 while i <= #arg do local v = arg[i]
		local execLongOpt = function(longopt)
			if not onswitch[longopt] then
				ct.exns.cli('switch --%s unrecognized', longopt):throw()
			end
			local nargs = optnparams(longopt)

			if nargs > 1 then
				if i + nargs > #arg then
					ct.exns.cli('not enough arguments for switch --%s (%s expected)', longopt, nargs):throw()
				end
				local nt = {}
				for j = i+1, i+nargs do
					table.insert(nt, arg[j])
				end
				onswitch[longopt](table.unpack(nt))
			elseif nargs == 1 then
				onswitch[longopt](arg[i+1])
			else
				onswitch[longopt]()
			end
			i = i + nargs
		end
		if v == '--' then
			keepParsing = false
		else
			local longopt = v:match '^%-%-(.+)$'
			if keepParsing and longopt then
				execLongOpt(longopt)
			else
				if keepParsing and v:sub(1,1) == '-' then
					for c,p in ss.str.each(ss.str.enc.utf8, v:sub(2)) do
						if optmap[c] then
							execLongOpt(optmap[c])
						else
							ct.exns.cli('switch -%s unrecognized', c):throw()
						end
					end
				else
					table.insert(args, v)
				end
			end

		end
	i = i + 1 end end

	if args[1] and args[1] ~= '' then
		local file = io.open(args[1], "rb")
		if not file then error('unable to load file ' .. args[1]) end
		input.stream = file
		input.src.file = args[1]
	end

	return main(input, outp, log, mode, suggestions, vars, extrule)
end

-- local ok, e = pcall(entry_cli)
local ok, e = true, entry_cli()
if not ok then
	local str = 'translation failure'
	if ss.exn.is(e) then
		str = e.kind.desc
	end
	local color = false
	if native then
		if native.posix.isatty(log) then
			color = true
		end
	else
		if log:seek() == nil then
			-- this is not a very reliable heuristic for detecting
			-- attachment to a tty but it's better than nothing
			if os.getenv('COLORTERM') then
				color = true
			else
				local term = os.getenv('TERM')
				if term:find 'color' then color = true end
			end
		end
	end
	if color then
		str = string.format('\27[1;31m%s\27[m', str)
	end
	log:write(string.format('%s: %s\n', str, e))
	os.exit(1)
end
os.exit(e)