#!/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