#!/usr/bin/env lua local parse = require "moonscript.parse" local compile = require "moonscript.compile" local util = require "moonscript.util" local dump_tree = require"moonscript.dump".tree local alt_getopt = require "alt_getopt" local lfs = require "lfs" local dirsep = package.config:sub(1,1) local opts, ind = alt_getopt.get_opts(arg, "lvhwt:o:pTXb", { print = "p", tree = "T", version = "v", help = "h", lint = "l" }) local read_stdin = arg[1] == "--" local polling_rate = 1.0 local help = [[Usage: %s [options] files... -h Print this message -w Watch file/directory -t path Specify where to place compiled files -o file Write output to file -p Write output to standard out -T Write parse tree instead of code (to stdout) -X Write line rewrite map instead of code (to stdout) -l Perform lint on the file instead of compiling -b Dump parse and compile time (doesn't write output) -v Print version -- Read from standard in, print to standard out (Must be first and only argument) ]] if opts.v then local v = require "moonscript.version" v.print_version() os.exit() end function print_help(err) local help_msg = help:format(arg[0]) if err then io.stderr:write("Error: ".. err .. "\n") io.stderr:write(help_msg .. "\n") os.exit(1) else print(help_msg) os.exit(0) end end function mkdir(path) local chunks = util.split(path, dirsep) local accum for _, dir in ipairs(chunks) do accum = accum and accum.. dirsep ..dir or dir lfs.mkdir(accum) end return lfs.attributes(path, "mode") end function normalize(path) return path:match("^(.-)" .. dirsep .. "*$")..dirsep end function parse_dir(fname) return fname:match("^(.-)[^" .. dirsep .. "]*$") end function parse_file(fname) return fname:match("^.-([^" .. dirsep .. "]*)$") end -- convert .moon to .lua function convert_path(path) local new_path = path:gsub("%.moon$", ".lua") if new_path == path then new_path = path .. ".lua" end return new_path end function log_msg(...) if not opts.p then io.stderr:write(table.concat({...}, " ") .. "\n") end end local gettime = nil if opts.b then pcall(function() require "socket" gettime = socket.gettime end) function format_time(time) return ("%.3fms"):format(time*1000) end if not gettime then print_help"LuaSocket needed for benchmark" end else gettime = function() return 0 end end function write_file(fname, code) if opts.p then if code ~= "" then print(code) end else mkdir(parse_dir(fname)) local out_f = io.open(fname, "w") if not out_f then return nil, "Failed to write output: "..fname end out_f:write(code.."\n") out_f:close() end return true end function compile_file(text, fname) local parse_time = gettime() local tree, err = parse.string(text) parse_time = gettime() - parse_time if not tree then return nil, err end if opts.T then opts.p = true dump_tree(tree) return "" else local compile_time = gettime() local code, posmap_or_err, err_pos = compile.tree(tree) compile_time = gettime() - compile_time if not code then return nil, compile.format_error(posmap_or_err, err_pos, text) end if opts.X then opts.p = true print("Pos", "Lua", ">>", "Moon") print(util.debug_posmap(posmap_or_err, text, code)) return "" end if opts.b then opts.p = true return table.concat({ fname, "Parse time \t" .. format_time(parse_time), "Compile time\t" .. format_time(compile_time), "" }, "\n") end return code end end function compile_and_write(from, to) local f = io.open(from) if not f then return nil, "Can't find file" end local text = f:read("*a") local code, err = compile_file(text, from) if not code then return nil, err end return write_file(to, code) end function scan_directory(root, collected) root = normalize(root) collected = collected or {} for fname in lfs.dir(root) do if not fname:match("^%.") then local full_path = root..fname if lfs.attributes(full_path, "mode") == "directory" then scan_directory(full_path, collected) end if fname:match("%.moon$") then table.insert(collected, full_path) end end end return collected end function remove_dups(tbl, key_fn) local hash = {} local final = {} for _, v in ipairs(tbl) do local dup_key = key_fn and key_fn(v) or v if not hash[dup_key] then table.insert(final, v) hash[dup_key] = true end end return final end local function is_abs_path(path) local first = path:sub(1, 1) if dirsep == "\\" then return first == "/" or first == "\\" or path:sub(2,1) == ":" else return first == dirsep end end -- creates tuples of input and target function get_files(fname, files) files = files or {} if lfs.attributes(fname, "mode") == "directory" then local head = fname:match("^(.-)[^" .. dirsep .. "]*" .. dirsep .."?$") for _, sub_fname in ipairs(scan_directory(fname)) do local target_fname = convert_path(sub_fname) if opts.t then if head then local start, stop = target_fname:find(head, 1, true) if start == 1 then target_fname = target_fname:sub(stop + 1) end end target_fname = normalize(opts.t) .. target_fname end table.insert(files, {sub_fname, target_fname}) end else local target_fname = convert_path(fname) if opts.t then local prefix = normalize(opts.t) if is_abs_path(target_fname) then target_fname = parse_file(target_fname) end target_fname = prefix .. target_fname end table.insert(files, {fname, target_fname}) end return files end if opts.h then print_help() end if read_stdin then local text = io.stdin:read("*a") local tree, err = parse.string(text) if not tree then error(err) end local code, err, pos = compile.tree(tree) if not code then error(compile.format_error(err, pos, text)) end print(code) os.exit() end local inputs = {} for i = ind, #arg do table.insert(inputs, arg[i]) end if #inputs == 0 then print_help("No files specified") end local files = {} for _, input in ipairs(inputs) do get_files(input, files) end files = remove_dups(files, function(f) return f[2] end) function get_sleep_func() local sleep if not pcall(function() require "socket" sleep = socket.sleep end) then -- This is set by moonc.c in windows binaries sleep = require("moonscript")._sleep end if not sleep then error("Missing sleep function; install LuaSocket") end return sleep end function plural(count, word) if count ~= 1 then word = word .. "s" end return table.concat({count, word}, " ") end -- returns an iterator that returns files that have been updated function create_watcher(files) local msg = "Starting watch loop (Ctrl-C to exit)" local inotify pcall(function() inotify = require "inotify" end) if inotify then local dirs = {} for _, tuple in ipairs(files) do local dir = parse_dir(tuple[1]) if dir == "" then dir = "./" end table.insert(dirs, dir) end dirs = remove_dups(dirs) return coroutine.wrap(function() io.stderr:write(("%s with inotify [%s]"):format(msg, plural(#dirs, "dir")) .. "\n") local wd_table = {} local handle = inotify.init() for _, dir in ipairs(dirs) do local wd = handle:addwatch(dir, inotify.IN_CLOSE_WRITE, inotify.IN_MOVED_TO) wd_table[wd] = dir end while true do local events = handle:read() if not events then break end for _, ev in ipairs(events) do local fname = ev.name if fname:match("%.moon$") then local dir = wd_table[ev.wd] if dir ~= "./" then fname = dir .. fname end -- TODO: check to make sure the file was in the original set coroutine.yield(fname) end end end end) else -- poll the filesystem instead local sleep = get_sleep_func() return coroutine.wrap(function() io.stderr:write(("%s with polling [%s]"):format(msg, plural(#files, "file")) .. "\n") local mod_time = {} while true do for _, tuple in ipairs(files) do local file = tuple[1] local time = lfs.attributes(file, "modification") if not mod_time[file] then mod_time[file] = time else if time ~= mod_time[file] then if time > mod_time[file] then coroutine.yield(file) mod_time[file] = time end end end end sleep(polling_rate) end end) end end if opts.w then local watcher = create_watcher(files) -- catches interrupt error for ctl-c local protected = function() local status, file = true, watcher() if status then return file elseif file ~= "interrupted!" then error(file) end end for fname in protected do local target = convert_path(fname) local success, err = compile_and_write(fname, target) if not success then io.stderr:write(table.concat({ "", "Error: " .. fname, err, "\n", }, "\n")) else log_msg("Built:", fname, "->", target) end end io.stderr:write("\nQuitting...\n") elseif opts.l then for _, tuple in pairs(files) do local fname = tuple[1] lint = require "moonscript.cmd.lint" local res, err = lint.lint_file(fname) if res then io.stderr:write(res .. "\n\n") elseif err then io.stderr:write(fname .. "\n" .. err.. "\n\n") end end else for _, tuple in ipairs(files) do local fname, target = unpack(tuple) local success, err = compile_and_write(fname, target) if not success then io.stderr:write(fname .. "\t" .. err .. "\n") os.exit(1) else log_msg("Built", fname, "->", target) end end end