#!/usr/bin/env lua
--[[--
Front end for LuaMacro, a Lua macro preprocessor.
The default action is to preprocess and run a Lua file. To just dump
the preprocessed output, use the `-d` flag. Like `lua`, the `-l` flag can
be used to load a library first, but you need to explicitly say `-i` to
get an interactive prompt.
The package loader is modified so that `require 'mod'` will preprocess `mod` if it is found as `mod.m.lua`.
Dumping is the only action available when preprocessing C code with `-C`.
@script luam
]]
-- adjust the path so that this script can see the macro package
local path = arg[0]:gsub('[^/\\]+$','')
package.path = package.path .. ';' .. path .. '?.lua;'..path .. 'macro/?.lua'
local macro = require 'macro'
require 'macro.builtin'
--- Using luam.
-- @usage follows
local usage = [[
LuaMacro 2.5.0, a Lua macro preprocessor and runner
-l require a library
-e statement to be executed
-V set a variable (-VX or -VY=1)
-c error context to be shown (default 2)
-d dump preprocessed output to stdout
-o write to this file
-C C lexer
-N No #line directives when generating C
-i interactive prompt
-v verbose error trace
Lua source file
]]
-- parsing the args, the hard way:
local takes_value = {l = '', e = '', c = 2, o = '',V = ';'}
local args = {}
local idx,i = 1,1
while i <= #arg do
local a = arg[i]
local flag = a:match '^%-(.+)'
local val
if flag then
if #flag > 1 then -- allow for -lmod, like Lua
val = flag:sub(2)
flag = flag:sub(1,1)
end
-- grab the next argument if we need a value
if takes_value[flag] and not val then
i = i + 1
val = arg[i]
end
-- convert the argument, if required
local def = takes_value[flag]
if type(def) == 'number' then
val = tonumber(val)
elseif def == ';' and args[flag] then
val = args[flag]..';'..val
end
args[flag] = val or true
else
args[idx] = a
idx = idx + 1
end
i = i + 1
end
if not args[1] and not args.i then
print(usage)
os.exit()
elseif args[1] then
args.input_name = args[1]
args.input,err = io.open(args[1],'r')
if err then return print(err) end
table.remove(args,1)
end
-- set defaults, if flags not specified
for k,v in pairs(takes_value) do
if not args[k] then
args[k] = v
end
end
---------- compiling and running the output ------
-- the tricky bit here is presenting the errors so that they refer to the
-- original line numbers. In addition, we also present a few lines of context
-- in the output.
local function lookup_line (lno,li)
for i = 1,#li-1 do
--print(li[i].il,li[i].ol,lno,'match')
if lno < li[i+1].ol then
return li[i].il + (lno - li[i].ol) - 1
end
end
return li[#li].il + (lno - li[#li].ol) - 1
end
-- iterating over all lines in a string can be awkward;
-- gmatch doesn't handle the empty-line cases properly.
local function split_nl (t)
local k1 = 1
local k2 = t:find ('[\r\n]',k1)
return function()
if not k2 then return nil end
local res = t:sub(k1,k2-1)
k1 = k2+1
k2 = t:find('[\r\n]',k1)
return res
end
end
local function fix_error_trace (err,li)
local strname,lno = err:match '%[string "(%S+)"%]:(%d+)'
local ino
if strname then
lno = tonumber(lno)
if li then
ino = lookup_line(lno,li)
err = err:gsub('%[string "%S+"%]:'..(lno or '?')..':',strname..':'..(ino or '?'))
end
end
return err,lno,ino
end
local function runstring (code,name,li,...)
local res,err = loadstring(code,name)
local lno,ok
if not res then
err,lno,ino = fix_error_trace(err,li)
if ino then
print 'preprocessed context of error:'
local l1,l2 = lno-args.c,lno+args.c
local l = 1
for line in split_nl(code) do
if l >= l1 and l <= l2 then
if l == lno then io.write('*') else io.write(' ') end
print(l,line)
end
l = l + 1
end
end
io.stderr:write(err,'\n')
os.exit(1)
end
ok,err = xpcall(function(...) return res(...) end, debug.traceback)
if not ok then
err = err:gsub("%[C%]: in function 'xpcall'.+",'')
if li then
repeat
err,lno = fix_error_trace(err,li)
until not lno
end
io.stderr:write(err,'\n')
end
return ok
end
local function subst (ins,name)
local C
if args.C then
C = args.N and true or 'line'
end
return macro.substitute_tostring(ins,name,C,args.v)
end
local function subst_runstring (ins,name,...)
local buf,li = subst(ins,name)
if not buf then
io.stderr:write(li,'\n')
os.exit(1)
end
if args.d or args.C or args.o ~= '' then
if args.o == '' then
print(buf)
else
local f = io.open(args.o,'w')
f:write(buf)
f:close()
end
else
return runstring(buf,name,li,...)
end
end
-- Lua 5.1/5.2 compatibility
local pack = table.pack
if not pack then
function pack(...)
return {n=select('#',...),...}
end
end
if not unpack then unpack = table.unpack end
local function eval(code)
local status,val,f,err,rcnt
code,rcnt = code:gsub('^%s*=','return ')
f,err = loadstring(code,'TMP')
if f then
res = pack(pcall(f))
if not res[1] then err = res[2]
else
return res
end
end
if err then
err = tostring(err):gsub('^%[string "TMP"%]:1:','')
return {nil,err}
end
end
local function interactive_loop ()
os.execute(arg[-1]..' -v') -- for the Lua copyright
print 'Lua Macro 2.5.0 Copyright (C) 2007-2011 Steve Donovan'
local function readline()
io.write(_PROMPT or '> ')
return io.read()
end
require 'macro.all'
_G.macro = macro
macro.define 'quit os.exit()'
macro._interactive = true
local line = readline()
while line do
local s,err = subst(line..'\n')
if not s then
err = err:gsub('.-:%d+:','')
print('macro error: '..err)
elseif not s:match '^%s*$' then
if args.d then print(s) end
local res = eval(s)
if not res[1] then
print('expanded: '..s)
print('error: '..res[2])
elseif res[2] ~= nil then
print(unpack(res,2))
end
end
line = readline()
end
end
macro.set_package_loader()
if args.l ~= '' then require(args.l) end
if args.V ~= ';' then
for varset in args.V:gmatch '([^;]+)' do
local sym,val = varset:match '([^=]+)=(.+)'
if not sym then
sym = varset
val = true
end
_G[sym] = val
end
end
require 'macro.ifelse'
if args.e ~= '' then
subst_runstring(args.e,"")
else
if args.input then
arg = args
arg[0] = args.input_name
arg[-1] = 'luam'
subst_runstring(args.input,args.input_name,unpack(args))
elseif args.i then
interactive_loop()
end
end