Refactor luash to modularize components, introducing injected library loading, preprocessing functions, and a dedicated runner for file execution. Enhance REPL with improved command handling and delegate to modules for better organization.
This commit is contained in:
452
luash
452
luash
@@ -1,21 +1,33 @@
|
||||
#!/usr/bin/env lua
|
||||
-- luash: Minimal Lua shell scripting preprocessor
|
||||
|
||||
-- Load injected library into global scope for REPL
|
||||
-- Module path for local src/
|
||||
package.path = (debug.getinfo(1, "S").source:match("@(.*/)") or "./") .. "src/?.lua;" .. package.path
|
||||
|
||||
-- Load injected library via module
|
||||
local function load_injected_lib()
|
||||
local injected_lib_path = debug.getinfo(1, "S").source:match("@(.*/)") or "./"
|
||||
local lib_file = io.open(injected_lib_path .. "__injected_lib.lua", "r")
|
||||
if lib_file then
|
||||
local lib_code = lib_file:read("*a")
|
||||
lib_file:close()
|
||||
local lib_func = load(lib_code)
|
||||
if lib_func then lib_func() end
|
||||
end
|
||||
local injected = require('injected')
|
||||
injected.load()
|
||||
end
|
||||
|
||||
-- REPL (Interactive Shell) Implementation
|
||||
function start_repl()
|
||||
load_injected_lib()
|
||||
local repl_mod = require('repl')
|
||||
|
||||
-- Preprocessing functions (simplified versions for REPL)
|
||||
local function process_env_vars_repl(code)
|
||||
local pre = require('preprocess')
|
||||
return pre.process_env_vars(code)
|
||||
end
|
||||
local function process_shell_commands_repl(code)
|
||||
local pre = require('preprocess')
|
||||
return pre.process_shell_commands(code)
|
||||
end
|
||||
local function process_interactive_commands_repl(code)
|
||||
local pre = require('preprocess')
|
||||
return pre.process_interactive_commands(code)
|
||||
end
|
||||
|
||||
-- Preprocessing functions (simplified versions for REPL)
|
||||
local function process_env_vars_repl(code)
|
||||
@@ -133,12 +145,8 @@ function start_repl()
|
||||
|
||||
-- Luash preprocessing function for REPL
|
||||
local function preprocess_luash_repl(code)
|
||||
-- Remove shebang line if present and preserve line numbers
|
||||
local has_shebang = code:match("^#!")
|
||||
code = code:gsub("^#![^\r\n]*[\r\n]?", "")
|
||||
if has_shebang then
|
||||
code = "\n" .. code
|
||||
end
|
||||
local pre = require('preprocess')
|
||||
code = pre.remove_shebang_preserve_lines(code)
|
||||
code = process_env_vars_repl(code)
|
||||
code = process_interactive_commands_repl(code)
|
||||
code = process_shell_commands_repl(code)
|
||||
@@ -210,218 +218,8 @@ function start_repl()
|
||||
return s:match("^%s*(.-)%s*$")
|
||||
end
|
||||
|
||||
-- Raw mode (optional) for immediate key handling like Ctrl+L
|
||||
local function detect_tty()
|
||||
local r = os.execute("test -t 0 >/dev/null 2>&1")
|
||||
if type(r) == "number" then return r == 0 end
|
||||
return r == true
|
||||
end
|
||||
local env_raw = os.getenv("LUASH_RAW")
|
||||
local raw_mode_enabled = (env_raw == "1") or (env_raw == nil and detect_tty())
|
||||
|
||||
local function set_raw_mode(enable)
|
||||
if enable then
|
||||
os.execute("stty -echo -icanon -isig min 1 time 0")
|
||||
else
|
||||
os.execute("stty sane")
|
||||
end
|
||||
end
|
||||
|
||||
local function raw_read_line(prompt)
|
||||
io.write(prompt)
|
||||
io.flush()
|
||||
local buf = {}
|
||||
while true do
|
||||
local ch = io.read(1)
|
||||
if not ch then
|
||||
return nil
|
||||
end
|
||||
local b = string.byte(ch)
|
||||
if b == 10 or b == 13 then -- Enter
|
||||
io.write("\n")
|
||||
return table.concat(buf)
|
||||
elseif b == 12 then -- Ctrl+L
|
||||
-- Clear screen and reprint banner + prompt + current buffer
|
||||
repl.buffer = table.concat(buf)
|
||||
io.write("\27[2J\27[H")
|
||||
io.flush()
|
||||
print("🚀 Luash Interactive Shell")
|
||||
print("Type Lua expressions, use $VAR for env vars, `commands` for shell")
|
||||
print("Special commands: .help .exit .clear .history (Ctrl+L to clear, Ctrl+D to exit)")
|
||||
print("")
|
||||
io.write(prompt)
|
||||
io.write(repl.buffer)
|
||||
io.flush()
|
||||
-- Keep current input for usability, but allow immediate Ctrl+D by not forcing buf non-empty
|
||||
-- buf remains as-is
|
||||
elseif b == 3 then -- Ctrl+C
|
||||
return "__CTRL_C__"
|
||||
elseif b == 4 then -- Ctrl+D
|
||||
-- Exit immediately (treat as EOF) regardless of current buffer state
|
||||
return nil
|
||||
elseif b == 127 or b == 8 then -- Backspace/Delete
|
||||
if #buf > 0 then
|
||||
table.remove(buf)
|
||||
io.write("\b \b")
|
||||
io.flush()
|
||||
end
|
||||
else
|
||||
table.insert(buf, ch)
|
||||
io.write(ch)
|
||||
io.flush()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Main REPL loop
|
||||
print("🚀 Luash Interactive Shell")
|
||||
print("Type Lua expressions, use $VAR for env vars, `commands` for shell")
|
||||
print("Special commands: .help .exit .clear .history (Ctrl+L to clear, Ctrl+D to exit)")
|
||||
print("")
|
||||
|
||||
if raw_mode_enabled then set_raw_mode(true) end
|
||||
local ok_loop, loop_err = pcall(function()
|
||||
while true do
|
||||
local prompt = repl.buffer == "" and "luash> " or " >> "
|
||||
local line
|
||||
if raw_mode_enabled then
|
||||
line = raw_read_line(prompt)
|
||||
if line == "__CTRL_C__" then
|
||||
print("\nInterrupted (Ctrl+C)")
|
||||
break
|
||||
end
|
||||
else
|
||||
io.write(prompt)
|
||||
io.flush()
|
||||
-- Safely read a line; handle Ctrl+C gracefully
|
||||
local ok, l = pcall(io.read, "*line")
|
||||
if not ok then
|
||||
print("\nInterrupted (Ctrl+C)")
|
||||
break
|
||||
end
|
||||
line = l
|
||||
end
|
||||
|
||||
if not line then -- EOF (Ctrl+D)
|
||||
print("\nGoodbye!")
|
||||
break
|
||||
end
|
||||
|
||||
-- Handle Ctrl+L (form feed) to clear screen in non-raw mode
|
||||
if (not raw_mode_enabled) and line:find(string.char(12), 1, true) then
|
||||
repl.buffer = ""
|
||||
io.write("\27[2J\27[H") -- Clear screen and move cursor to top
|
||||
io.flush()
|
||||
print("🚀 Luash Interactive Shell")
|
||||
print("Type Lua expressions, use $VAR for env vars, `commands` for shell")
|
||||
print("Special commands: .help .exit .clear .history (Ctrl+L to clear, Ctrl+D to exit)")
|
||||
print("")
|
||||
goto continue
|
||||
end
|
||||
|
||||
line = trim(line)
|
||||
|
||||
-- Handle special dot commands
|
||||
if line == ".exit" or line == ".quit" then
|
||||
break
|
||||
elseif line == ".clear" then
|
||||
repl.buffer = ""
|
||||
print("Buffer cleared")
|
||||
elseif line == ".history" then
|
||||
for i, cmd in ipairs(repl.history) do
|
||||
print(string.format("%3d: %s", i, cmd))
|
||||
end
|
||||
elseif line == ".help" then
|
||||
print([[
|
||||
Luash Interactive Shell Help:
|
||||
|
||||
Luash Features:
|
||||
$VAR - Access environment variables
|
||||
$VAR = "value" - Set environment variables
|
||||
`command` - Execute shell commands
|
||||
#{var} - Variable interpolation in commands
|
||||
!command - Interactive shell commands
|
||||
|
||||
Special Commands:
|
||||
.help - Show this help
|
||||
.exit, .quit - Exit the shell
|
||||
.clear - Clear input buffer
|
||||
.history - Show command history
|
||||
|
||||
Examples:
|
||||
print("Hello " .. $USER)
|
||||
files = `ls -la`
|
||||
$GREETING = "Hello World"
|
||||
result = `echo #{$GREETING}`
|
||||
!ps aux | head -5
|
||||
|
||||
Multi-line input supported for functions, loops, etc.]])
|
||||
else
|
||||
-- Add to buffer
|
||||
if repl.buffer ~= "" then
|
||||
repl.buffer = repl.buffer .. "\n" .. line
|
||||
else
|
||||
repl.buffer = line
|
||||
end
|
||||
|
||||
-- Try to compile to determine if we need continuation (parser-driven)
|
||||
if line ~= "" then
|
||||
local processed = preprocess_luash_repl(repl.buffer)
|
||||
|
||||
-- First, try as an expression (so results print automatically)
|
||||
local func, err = load("return " .. processed, "@repl")
|
||||
if not func then
|
||||
if err and err:match("<eof>") then
|
||||
-- Incomplete chunk; wait for more input
|
||||
-- keep buffer as-is
|
||||
else
|
||||
-- Try as a statement block
|
||||
func, err = load(processed, "@repl")
|
||||
if not func then
|
||||
if err and err:match("<eof>") then
|
||||
-- Incomplete; wait for more input
|
||||
else
|
||||
-- Definitive syntax error; report and clear buffer
|
||||
print("Syntax error: " .. err)
|
||||
repl.buffer = ""
|
||||
end
|
||||
else
|
||||
-- Executable statement block
|
||||
table.insert(repl.history, repl.buffer)
|
||||
local success, result = pcall(func)
|
||||
if success then
|
||||
if result ~= nil then
|
||||
print(tostring(result))
|
||||
end
|
||||
else
|
||||
print("Error: " .. tostring(result))
|
||||
end
|
||||
repl.buffer = ""
|
||||
end
|
||||
end
|
||||
else
|
||||
-- Executable expression
|
||||
table.insert(repl.history, repl.buffer)
|
||||
local success, result = pcall(func)
|
||||
if success then
|
||||
if result ~= nil then
|
||||
print(tostring(result))
|
||||
end
|
||||
else
|
||||
print("Error: " .. tostring(result))
|
||||
end
|
||||
repl.buffer = ""
|
||||
end
|
||||
end
|
||||
end
|
||||
::continue::
|
||||
end
|
||||
end)
|
||||
if raw_mode_enabled then set_raw_mode(false) end
|
||||
if not ok_loop then
|
||||
if raw_mode_enabled then set_raw_mode(false) end
|
||||
print("\nREPL error: " .. tostring(loop_err))
|
||||
end
|
||||
-- Delegate to module REPL
|
||||
repl_mod.start(preprocess_luash_repl, load_injected_lib)
|
||||
end
|
||||
|
||||
-- 1. Check for an input file or REPL mode
|
||||
@@ -436,201 +234,7 @@ elseif not filename or filename == "-i" or filename == "--interactive" then
|
||||
start_repl()
|
||||
return
|
||||
end
|
||||
|
||||
local file = io.open(filename, "r")
|
||||
if not file then
|
||||
print("Error: Cannot open file '" .. filename .. "'")
|
||||
return
|
||||
end
|
||||
local source_code = file:read("*a")
|
||||
file:close()
|
||||
|
||||
-- Remove shebang line if present and preserve line numbers
|
||||
local has_shebang = source_code:match("^#!")
|
||||
source_code = source_code:gsub("^#![^\r\n]*[\r\n]?", "")
|
||||
|
||||
-- If we removed a shebang, add an empty line to preserve line numbers
|
||||
if has_shebang then
|
||||
source_code = "\n" .. source_code
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- Preprocessing functions
|
||||
local function process_env_vars(code)
|
||||
local lines = {}
|
||||
for line in code:gmatch("([^\r\n]*)") do
|
||||
if not line:match("^%s*%-%-") then -- Skip comments
|
||||
|
||||
-- Handle environment variable assignment: $VAR=value
|
||||
if line:match("^%$([%w_]+)%s*=") then
|
||||
line = line:gsub("^%$([%w_]+)%s*=%s*(.+)", function(var, value)
|
||||
return "env_set('" .. var .. "', " .. value .. ")"
|
||||
end)
|
||||
else
|
||||
-- Handle $VAR substitution, but not inside strings
|
||||
local in_string = false
|
||||
local quote_char = nil
|
||||
local result = ""
|
||||
local i = 1
|
||||
|
||||
while i <= #line do
|
||||
local char = line:sub(i, i)
|
||||
|
||||
-- Track string boundaries
|
||||
if (char == '"' or char == "'") and (i == 1 or line:sub(i-1, i-1) ~= "\\") then
|
||||
if not in_string then
|
||||
in_string = true
|
||||
quote_char = char
|
||||
elseif char == quote_char then
|
||||
in_string = false
|
||||
quote_char = nil
|
||||
end
|
||||
result = result .. char
|
||||
i = i + 1
|
||||
elseif char == "$" and not in_string then
|
||||
-- Found $ outside of string, check for variable pattern
|
||||
local var_match = line:sub(i):match("^%$([%w_]+)")
|
||||
if var_match then
|
||||
result = result .. "env_get('" .. var_match .. "')"
|
||||
i = i + #var_match + 1
|
||||
else
|
||||
result = result .. char
|
||||
i = i + 1
|
||||
end
|
||||
else
|
||||
result = result .. char
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
|
||||
line = result
|
||||
end
|
||||
end
|
||||
table.insert(lines, line)
|
||||
end
|
||||
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
|
||||
local function process_shell_commands(code)
|
||||
local lines = {}
|
||||
for line in code:gmatch("([^\r\n]*)") do
|
||||
-- Skip processing backticks inside strings
|
||||
local in_string = false
|
||||
local quote_char = nil
|
||||
local result = ""
|
||||
local i = 1
|
||||
|
||||
while i <= #line do
|
||||
local char = line:sub(i, i)
|
||||
|
||||
-- Track string boundaries
|
||||
if (char == '"' or char == "'") and (i == 1 or line:sub(i-1, i-1) ~= "\\") then
|
||||
if not in_string then
|
||||
in_string = true
|
||||
quote_char = char
|
||||
elseif char == quote_char then
|
||||
in_string = false
|
||||
quote_char = nil
|
||||
end
|
||||
result = result .. char
|
||||
i = i + 1
|
||||
elseif char == "`" and not in_string then
|
||||
-- Found backtick outside of string, find the closing backtick
|
||||
local end_pos = line:find("`", i + 1)
|
||||
if end_pos then
|
||||
local command = line:sub(i + 1, end_pos - 1)
|
||||
|
||||
-- Handle variable interpolation with #{var} syntax
|
||||
if command:find("#{") then
|
||||
-- Escape existing quotes first, then do interpolation
|
||||
local escaped_cmd = command:gsub('"', '\\"')
|
||||
local interpolated_cmd = escaped_cmd:gsub("#{([%w_.]+)}", '" .. tostring(%1) .. "')
|
||||
result = result .. 'shell("' .. interpolated_cmd .. '")'
|
||||
else
|
||||
result = result .. 'shell("' .. command:gsub('"', '\\"') .. '")'
|
||||
end
|
||||
i = end_pos + 1
|
||||
else
|
||||
result = result .. char
|
||||
i = i + 1
|
||||
end
|
||||
else
|
||||
result = result .. char
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
|
||||
table.insert(lines, result)
|
||||
end
|
||||
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
|
||||
local function process_interactive_commands(code)
|
||||
local lines = {}
|
||||
for line in code:gmatch("([^\r\n]*)") do
|
||||
-- Only match ! at the beginning of the line (ignoring whitespace)
|
||||
local command = line:match("^%s*!(.*)")
|
||||
if command then
|
||||
local indent = line:match("^(%s*)")
|
||||
table.insert(lines, indent .. 'run("' .. command:gsub('"', '\\"') .. '")')
|
||||
else
|
||||
table.insert(lines, line)
|
||||
end
|
||||
end
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
|
||||
-- Load injected helper functions (minimal)
|
||||
local injected_lib_path = debug.getinfo(1, "S").source:match("@(.*/)") or "./"
|
||||
local lib_file = io.open(injected_lib_path .. "__injected_lib.lua", "r")
|
||||
local injected_lib = ""
|
||||
if lib_file then
|
||||
injected_lib = lib_file:read("*a")
|
||||
lib_file:close()
|
||||
end
|
||||
|
||||
-- Apply preprocessing
|
||||
source_code = process_env_vars(source_code)
|
||||
source_code = process_interactive_commands(source_code)
|
||||
source_code = process_shell_commands(source_code)
|
||||
|
||||
-- Debug output
|
||||
if os.getenv("LUASH_DEBUG") then
|
||||
print("-- Generated Lua code:")
|
||||
print(source_code)
|
||||
print("-- End generated code")
|
||||
end
|
||||
|
||||
-- Pre-load injected library into global environment
|
||||
if injected_lib ~= "" then
|
||||
local lib_func, lib_err = load(injected_lib, "@__injected_lib.lua")
|
||||
if lib_func then
|
||||
local success, result = pcall(lib_func)
|
||||
if not success then
|
||||
print("--- ERROR loading injected library ---")
|
||||
print(result)
|
||||
return
|
||||
end
|
||||
else
|
||||
print("--- SYNTAX ERROR in __injected_lib.lua ---")
|
||||
print(lib_err)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
-- Execute the user's source code (with correct line numbers)
|
||||
local func, err = load(source_code, "@" .. filename)
|
||||
if not func then
|
||||
print("--- SYNTAX ERROR in " .. filename .. " ---")
|
||||
print(err)
|
||||
else
|
||||
local success, result = pcall(func)
|
||||
if not success then
|
||||
print("--- RUNTIME ERROR in " .. filename .. " ---")
|
||||
print(result)
|
||||
end
|
||||
end
|
||||
-- Delegate to runner for file execution
|
||||
local runner = require('runner')
|
||||
runner.run_file(filename)
|
||||
|
||||
|
||||
24
src/injected.lua
Normal file
24
src/injected.lua
Normal file
@@ -0,0 +1,24 @@
|
||||
local M = {}
|
||||
|
||||
function M.load()
|
||||
local base = debug.getinfo(1, "S").source:match("@(.*/)") or "./"
|
||||
base = base:gsub("/src/?$", "/")
|
||||
local lib_file = io.open(base .. "src/lib/injected_lib.lua", "r")
|
||||
if lib_file then
|
||||
local lib_code = lib_file:read("*a")
|
||||
lib_file:close()
|
||||
local lib_func, err = load(lib_code, "@__injected_lib.lua")
|
||||
if lib_func then
|
||||
local ok, res = pcall(lib_func)
|
||||
if not ok then
|
||||
error("Error loading injected lib: " .. tostring(res))
|
||||
end
|
||||
else
|
||||
error("Syntax error in injected lib: " .. tostring(err))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
|
||||
35
src/main.lua
Normal file
35
src/main.lua
Normal file
@@ -0,0 +1,35 @@
|
||||
local runner = require('runner')
|
||||
|
||||
local M = {}
|
||||
|
||||
function M.run(argv)
|
||||
local filename = argv[1]
|
||||
if filename == "-h" or filename == "--help" then
|
||||
print("Usage: luash <script.luash>")
|
||||
print(" luash -i # Interactive mode")
|
||||
print(" luash -h # Show this help")
|
||||
return
|
||||
elseif not filename or filename == "-i" or filename == "--interactive" then
|
||||
local entry = require('repl')
|
||||
local preprocess = require('preprocess')
|
||||
local injected = require('injected')
|
||||
local function preprocess_luash_repl(code)
|
||||
code = preprocess.remove_shebang_preserve_lines(code)
|
||||
code = preprocess.process_env_vars(code)
|
||||
code = preprocess.process_interactive_commands(code)
|
||||
code = preprocess.process_shell_commands(code)
|
||||
return code
|
||||
end
|
||||
local function load_injected_lib()
|
||||
injected.load()
|
||||
end
|
||||
entry.start(preprocess_luash_repl, load_injected_lib)
|
||||
return
|
||||
else
|
||||
runner.run_file(filename)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
|
||||
120
src/preprocess.lua
Normal file
120
src/preprocess.lua
Normal file
@@ -0,0 +1,120 @@
|
||||
local M = {}
|
||||
|
||||
function M.remove_shebang_preserve_lines(source_code)
|
||||
local has_shebang = source_code:match("^#!")
|
||||
source_code = source_code:gsub("^#![^\r\n]*[\r\n]?", "")
|
||||
if has_shebang then
|
||||
source_code = "\n" .. source_code
|
||||
end
|
||||
return source_code
|
||||
end
|
||||
|
||||
function M.process_env_vars(code)
|
||||
local lines = {}
|
||||
for line in code:gmatch("([^\r\n]*)") do
|
||||
if not line:match("^%s*%-%-") then
|
||||
if line:match("^%$([%w_]+)%s*=") then
|
||||
line = line:gsub("^%$([%w_]+)%s*=%s*(.+)", function(var, value)
|
||||
return "env_set('" .. var .. "', " .. value .. ")"
|
||||
end)
|
||||
else
|
||||
local in_string = false
|
||||
local quote_char = nil
|
||||
local result = ""
|
||||
local i = 1
|
||||
while i <= #line do
|
||||
local char = line:sub(i, i)
|
||||
if (char == '"' or char == "'") and (i == 1 or line:sub(i-1, i-1) ~= "\\") then
|
||||
if not in_string then
|
||||
in_string = true
|
||||
quote_char = char
|
||||
elseif char == quote_char then
|
||||
in_string = false
|
||||
quote_char = nil
|
||||
end
|
||||
result = result .. char
|
||||
i = i + 1
|
||||
elseif char == "$" and not in_string then
|
||||
local var_match = line:sub(i):match("^%$([%w_]+)")
|
||||
if var_match then
|
||||
result = result .. "env_get('" .. var_match .. "')"
|
||||
i = i + #var_match + 1
|
||||
else
|
||||
result = result .. char
|
||||
i = i + 1
|
||||
end
|
||||
else
|
||||
result = result .. char
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
line = result
|
||||
end
|
||||
end
|
||||
table.insert(lines, line)
|
||||
end
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
|
||||
function M.process_interactive_commands(code)
|
||||
local lines = {}
|
||||
for line in code:gmatch("([^\r\n]*)") do
|
||||
local command = line:match("^%s*!(.*)")
|
||||
if command then
|
||||
local indent = line:match("^(%s*)")
|
||||
table.insert(lines, indent .. 'run("' .. command:gsub('"', '\\"') .. '")')
|
||||
else
|
||||
table.insert(lines, line)
|
||||
end
|
||||
end
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
|
||||
function M.process_shell_commands(code)
|
||||
local lines = {}
|
||||
for line in code:gmatch("([^\r\n]*)") do
|
||||
local in_string = false
|
||||
local quote_char = nil
|
||||
local result = ""
|
||||
local i = 1
|
||||
while i <= #line do
|
||||
local char = line:sub(i, i)
|
||||
if (char == '"' or char == "'") and (i == 1 or line:sub(i-1, i-1) ~= "\\") then
|
||||
if not in_string then
|
||||
in_string = true
|
||||
quote_char = char
|
||||
elseif char == quote_char then
|
||||
in_string = false
|
||||
quote_char = nil
|
||||
end
|
||||
result = result .. char
|
||||
i = i + 1
|
||||
elseif char == "`" and not in_string then
|
||||
local end_pos = line:find("`", i + 1)
|
||||
if end_pos then
|
||||
local command = line:sub(i + 1, end_pos - 1)
|
||||
if command:find("#{") then
|
||||
local escaped_cmd = command:gsub('"', '\\"')
|
||||
local interpolated_cmd = escaped_cmd:gsub("#{([%w_.]+)}", '" .. tostring(%1) .. "')
|
||||
result = result .. 'shell("' .. interpolated_cmd .. '")'
|
||||
else
|
||||
result = result .. 'shell("' .. command:gsub('"', '\\"') .. '")'
|
||||
end
|
||||
i = end_pos + 1
|
||||
else
|
||||
result = result .. char
|
||||
i = i + 1
|
||||
end
|
||||
else
|
||||
result = result .. char
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
table.insert(lines, result)
|
||||
end
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
|
||||
130
src/repl.lua
Normal file
130
src/repl.lua
Normal file
@@ -0,0 +1,130 @@
|
||||
local M = {}
|
||||
|
||||
function M.start(preprocess_fn, load_injected_lib)
|
||||
load_injected_lib()
|
||||
|
||||
local util = require('util')
|
||||
local function detect_tty() return util.is_tty() end
|
||||
local env_raw = os.getenv("LUASH_RAW")
|
||||
local raw_mode_enabled = (env_raw == "1") or (env_raw == nil and detect_tty())
|
||||
|
||||
local repl = { buffer = "", history = {}, history_index = 0 }
|
||||
|
||||
local function set_raw_mode(enable) util.set_raw_mode(enable) end
|
||||
|
||||
local function trim(s) return util.trim(s) end
|
||||
|
||||
local function raw_read_line(prompt)
|
||||
io.write(prompt); io.flush()
|
||||
local buf = {}
|
||||
while true do
|
||||
local ch = io.read(1)
|
||||
if not ch then return nil end
|
||||
local b = string.byte(ch)
|
||||
if b == 10 or b == 13 then io.write("\n"); return table.concat(buf)
|
||||
elseif b == 12 then -- Ctrl+L
|
||||
repl.buffer = table.concat(buf)
|
||||
io.write("\27[2J\27[H"); io.flush()
|
||||
print("🚀 Luash Interactive Shell")
|
||||
print("Type Lua expressions, use $VAR for env vars, `commands` for shell")
|
||||
print("Special commands: .help .exit .clear .history (Ctrl+L to clear, Ctrl+D to exit)")
|
||||
print("")
|
||||
io.write(prompt); io.write(repl.buffer); io.flush()
|
||||
elseif b == 3 then return "__CTRL_C__"
|
||||
elseif b == 4 then return nil -- Ctrl+D as EOF
|
||||
elseif b == 127 or b == 8 then if #buf > 0 then table.remove(buf); io.write("\b \b"); io.flush() end
|
||||
else table.insert(buf, ch); io.write(ch); io.flush() end
|
||||
end
|
||||
end
|
||||
|
||||
print("🚀 Luash Interactive Shell")
|
||||
print("Type Lua expressions, use $VAR for env vars, `commands` for shell")
|
||||
print("Special commands: .help .exit .clear .history (Ctrl+L to clear, Ctrl+D to exit)")
|
||||
print("")
|
||||
|
||||
if raw_mode_enabled then set_raw_mode(true) end
|
||||
local ok_loop, loop_err = pcall(function()
|
||||
while true do
|
||||
local prompt = repl.buffer == "" and "luash> " or " >> "
|
||||
local line
|
||||
if raw_mode_enabled then
|
||||
line = raw_read_line(prompt)
|
||||
if line == "__CTRL_C__" then print("\nInterrupted (Ctrl+C)"); break end
|
||||
else
|
||||
io.write(prompt); io.flush()
|
||||
local ok, l = pcall(io.read, "*line")
|
||||
if not ok then print("\nInterrupted (Ctrl+C)"); break end
|
||||
line = l
|
||||
end
|
||||
if not line then print("\nGoodbye!"); break end
|
||||
|
||||
if (not raw_mode_enabled) and line:find(string.char(12), 1, true) then
|
||||
repl.buffer = ""; io.write("\27[2J\27[H"); io.flush()
|
||||
print("🚀 Luash Interactive Shell")
|
||||
print("Type Lua expressions, use $VAR for env vars, `commands` for shell")
|
||||
print("Special commands: .help .exit .clear .history (Ctrl+L to clear, Ctrl+D to exit)")
|
||||
print(""); goto continue
|
||||
end
|
||||
|
||||
line = trim(line)
|
||||
if line == ".exit" or line == ".quit" then break
|
||||
elseif line == ".clear" then repl.buffer = ""; print("Buffer cleared")
|
||||
elseif line == ".history" then for i, cmd in ipairs(repl.history) do print(string.format("%3d: %s", i, cmd)) end
|
||||
elseif line == ".help" then print([[Luash Interactive Shell Help:
|
||||
|
||||
Luash Features:
|
||||
$VAR - Access environment variables
|
||||
$VAR = "value" - Set environment variables
|
||||
`command` - Execute shell commands
|
||||
#{var} - Variable interpolation in commands
|
||||
!command - Interactive shell commands
|
||||
|
||||
Special Commands:
|
||||
.help - Show this help
|
||||
.exit, .quit - Exit the shell
|
||||
.clear - Clear input buffer
|
||||
.history - Show command history
|
||||
|
||||
Multi-line input supported for functions, loops, etc.]])
|
||||
else
|
||||
if repl.buffer ~= "" then repl.buffer = repl.buffer .. "\n" .. line else repl.buffer = line end
|
||||
|
||||
if line ~= "" then
|
||||
local processed = preprocess_fn(repl.buffer)
|
||||
local func, err = load("return " .. processed, "@repl")
|
||||
if not func then
|
||||
if err and err:match("<eof>") then
|
||||
-- incomplete
|
||||
else
|
||||
func, err = load(processed, "@repl")
|
||||
if not func then
|
||||
if err and err:match("<eof>") then
|
||||
-- incomplete
|
||||
else
|
||||
print("Syntax error: " .. err); repl.buffer = ""
|
||||
end
|
||||
else
|
||||
table.insert(repl.history, repl.buffer)
|
||||
local success, result = pcall(func)
|
||||
if success then if result ~= nil then print(tostring(result)) end else print("Error: " .. tostring(result)) end
|
||||
repl.buffer = ""
|
||||
end
|
||||
end
|
||||
else
|
||||
table.insert(repl.history, repl.buffer)
|
||||
local success, result = pcall(func)
|
||||
if success then if result ~= nil then print(tostring(result)) end else print("Error: " .. tostring(result)) end
|
||||
repl.buffer = ""
|
||||
end
|
||||
end
|
||||
end
|
||||
::continue::
|
||||
end
|
||||
end)
|
||||
if raw_mode_enabled then set_raw_mode(false) end
|
||||
if not ok_loop then if raw_mode_enabled then set_raw_mode(false) end; print("\nREPL error: " .. tostring(loop_err)) end
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
|
||||
39
src/runner.lua
Normal file
39
src/runner.lua
Normal file
@@ -0,0 +1,39 @@
|
||||
local preprocess = require('preprocess')
|
||||
local injected = require('injected')
|
||||
|
||||
local M = {}
|
||||
|
||||
function M.run_file(filename)
|
||||
local file = io.open(filename, "r")
|
||||
if not file then
|
||||
print("Error: Cannot open file '" .. filename .. "'")
|
||||
return
|
||||
end
|
||||
local source_code = file:read("*a")
|
||||
file:close()
|
||||
|
||||
source_code = preprocess.remove_shebang_preserve_lines(source_code)
|
||||
source_code = preprocess.process_env_vars(source_code)
|
||||
source_code = preprocess.process_interactive_commands(source_code)
|
||||
source_code = preprocess.process_shell_commands(source_code)
|
||||
|
||||
-- Load injected helpers into global scope (not prepended to source)
|
||||
injected.load()
|
||||
|
||||
-- Execute
|
||||
local func, err = load(source_code, "@" .. filename)
|
||||
if not func then
|
||||
print("--- SYNTAX ERROR in " .. filename .. " ---")
|
||||
print(err)
|
||||
return
|
||||
end
|
||||
local success, result = pcall(func)
|
||||
if not success then
|
||||
print("--- RUNTIME ERROR in " .. filename .. " ---")
|
||||
print(result)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
|
||||
27
src/util.lua
Normal file
27
src/util.lua
Normal file
@@ -0,0 +1,27 @@
|
||||
local M = {}
|
||||
|
||||
function M.trim(s)
|
||||
return s:match("^%s*(.-)%s*$")
|
||||
end
|
||||
|
||||
function M.is_tty()
|
||||
local r = os.execute("test -t 0 >/dev/null 2>&1")
|
||||
if type(r) == "number" then return r == 0 end
|
||||
return r == true
|
||||
end
|
||||
|
||||
function M.set_raw_mode(enable)
|
||||
if enable then
|
||||
os.execute("stty -echo -icanon -isig min 1 time 0")
|
||||
else
|
||||
os.execute("stty sane")
|
||||
end
|
||||
end
|
||||
|
||||
function M.script_dir()
|
||||
return (debug.getinfo(2, "S").source:match("@(.*/)") or "./")
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
|
||||
Reference in New Issue
Block a user