From 10d66593000ad13540da2452421eb65ed09370bd Mon Sep 17 00:00:00 2001 From: Cormac Shannon Date: Fri, 29 Aug 2025 00:33:31 +0100 Subject: [PATCH] combine shell and language --- README.md | 2 +- luash | 309 +++++++++++++++++++++++++++++++++++++++++++++++- luash_shell.lua | 295 --------------------------------------------- repl_demo.luash | 2 +- 4 files changed, 307 insertions(+), 301 deletions(-) delete mode 100755 luash_shell.lua diff --git a/README.md b/README.md index 142d29a..b2d50f1 100644 --- a/README.md +++ b/README.md @@ -259,7 +259,7 @@ wc("-l", "file.txt") -- Count lines ```bash ./luash -i # or -./luash_shell.lua +./luash --interactive ``` The interactive shell supports: diff --git a/luash b/luash index 25393e1..3032752 100755 --- a/luash +++ b/luash @@ -1,6 +1,305 @@ #!/usr/bin/env lua -- luash: Minimal Lua shell scripting preprocessor +-- Load injected library into global scope for REPL +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 +end + +-- REPL (Interactive Shell) Implementation +function start_repl() + load_injected_lib() + + -- Preprocessing functions (simplified versions for REPL) + local function process_env_vars_repl(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) + + 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 + + local function process_shell_commands_repl(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 + + local function process_interactive_commands_repl(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 + + -- Luash preprocessing function for REPL + local function preprocess_luash_repl(code) + code = code:gsub("^#![^\r\n]*[\r\n]?", "") -- Remove shebang + code = process_env_vars_repl(code) + code = process_interactive_commands_repl(code) + code = process_shell_commands_repl(code) + return code + end + + -- Check if expression needs more input + local function needs_continuation(code) + local open_blocks = 0 + local open_parens = 0 + local open_brackets = 0 + local open_braces = 0 + + local in_string = false + local quote_char = nil + + for i = 1, #code do + local char = code:sub(i, i) + + if not in_string then + if char == '"' or char == "'" then + in_string = true + quote_char = char + elseif char == "(" then + open_parens = open_parens + 1 + elseif char == ")" then + open_parens = open_parens - 1 + elseif char == "[" then + open_brackets = open_brackets + 1 + elseif char == "]" then + open_brackets = open_brackets - 1 + elseif char == "{" then + open_braces = open_braces + 1 + elseif char == "}" then + open_braces = open_braces - 1 + end + else + if char == quote_char and code:sub(i-1, i-1) ~= "\\" then + in_string = false + quote_char = nil + end + end + end + + -- Check for Lua keywords that need 'end' + local keywords = {"function", "if", "for", "while", "do", "then"} + local end_count = 0 + + for _, keyword in ipairs(keywords) do + local _, count = code:gsub("%f[%w]" .. keyword .. "%f[%W]", "") + open_blocks = open_blocks + count + end + + _, end_count = code:gsub("%f[%w]end%f[%W]", "") + open_blocks = open_blocks - end_count + + return open_blocks > 0 or open_parens > 0 or open_brackets > 0 or open_braces > 0 or in_string + end + + -- REPL state + local repl = { + buffer = "", + history = {}, + history_index = 0 + } + + -- Utility function + local function trim(s) + return s:match("^%s*(.-)%s*$") + 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") + print("") + + while true do + local prompt = repl.buffer == "" and "luash> " or " ... " + io.write(prompt) + io.flush() + + local line = io.read("*line") + + if not line then -- EOF (Ctrl+D) + print("\nGoodbye!") + break + 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 + + -- Check if we have a complete expression + if not needs_continuation(repl.buffer) and line ~= "" then + -- Process and execute + table.insert(repl.history, repl.buffer) + + local processed = preprocess_luash_repl(repl.buffer) + local func, err = load("return " .. processed, "@repl") + + if not func then + -- Try without return (for statements) + func, err = load(processed, "@repl") + end + + if func then + local success, result = pcall(func) + if success then + if result ~= nil then + print(tostring(result)) + end + else + print("Error: " .. tostring(result)) + end + else + print("Syntax error: " .. err) + end + + repl.buffer = "" + end + end + end +end + -- 1. Check for an input file or REPL mode local filename = arg[1] if not filename then @@ -8,9 +307,8 @@ if not filename then print(" luash -i # Interactive mode") return elseif filename == "-i" or filename == "--interactive" then - -- Start interactive REPL - local script_dir = debug.getinfo(1, "S").source:match("@(.*/)") - dofile(script_dir .. "luash_shell.lua") + -- Start interactive REPL (defined below) + start_repl() return end @@ -25,6 +323,8 @@ file:close() -- Remove shebang line if present source_code = source_code:gsub("^#![^\r\n]*[\r\n]?", "") + + -- Preprocessing functions local function process_env_vars(code) local lines = {} @@ -187,4 +487,5 @@ else print("--- RUNTIME ERROR in " .. filename .. " ---") print(result) end -end \ No newline at end of file +end + diff --git a/luash_shell.lua b/luash_shell.lua deleted file mode 100755 index 9d61f5a..0000000 --- a/luash_shell.lua +++ /dev/null @@ -1,295 +0,0 @@ -#!/usr/bin/env lua --- Luash Interactive Shell - A proper REPL --- Supports multiline input, command history, and all luash features - --- Load the injected library functions -local script_dir = debug.getinfo(1, "S").source:match("@(.*/)") -local lib_file = io.open(script_dir .. "__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 - --- Luash preprocessing functions (copied from main luash script) -local function preprocess_luash(code) - -- Remove shebang - code = code:gsub("^#![^\r\n]*[\r\n]?", "") - - -- Environment variable processing - 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) - - 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 - code = table.concat(lines, "\n") - - -- Shell command processing - 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 - code = table.concat(lines, "\n") - - -- Interactive commands - 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 - code = table.concat(lines, "\n") - - return code -end - --- Check if expression needs more input (simple heuristic) -local function needs_continuation(code) - local open_blocks = 0 - local open_parens = 0 - local open_brackets = 0 - local open_braces = 0 - - local in_string = false - local quote_char = nil - - for i = 1, #code do - local char = code:sub(i, i) - - if not in_string then - if char == '"' or char == "'" then - in_string = true - quote_char = char - elseif char == "(" then - open_parens = open_parens + 1 - elseif char == ")" then - open_parens = open_parens - 1 - elseif char == "[" then - open_brackets = open_brackets + 1 - elseif char == "]" then - open_brackets = open_brackets - 1 - elseif char == "{" then - open_braces = open_braces + 1 - elseif char == "}" then - open_braces = open_braces - 1 - end - else - if char == quote_char and code:sub(i-1, i-1) ~= "\\" then - in_string = false - quote_char = nil - end - end - end - - -- Check for Lua keywords that need 'end' - local keywords = {"function", "if", "for", "while", "do", "then"} - local end_count = 0 - - for _, keyword in ipairs(keywords) do - local _, count = code:gsub("%f[%w]" .. keyword .. "%f[%W]", "") - open_blocks = open_blocks + count - end - - _, end_count = code:gsub("%f[%w]end%f[%W]", "") - open_blocks = open_blocks - end_count - - return open_blocks > 0 or open_parens > 0 or open_brackets > 0 or open_braces > 0 or in_string -end - --- REPL state -local repl = { - buffer = "", - history = {}, - history_index = 0 -} - --- Add utility functions -function trim(s) - return s:match("^%s*(.-)%s*$") -end - --- Main REPL function -local function run_repl() - print("🚀 Luash Interactive Shell") - print("Type Lua expressions, use $VAR for env vars, `commands` for shell") - print("Special commands: .help .exit .clear .history") - print("") - - while true do - local prompt = repl.buffer == "" and "luash> " or " ... " - io.write(prompt) - io.flush() - - local line = io.read("*line") - - if not line then -- EOF (Ctrl+D) - print("\nGoodbye!") - break - 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 - - -- Check if we have a complete expression - if not needs_continuation(repl.buffer) and line ~= "" then - -- Process and execute - table.insert(repl.history, repl.buffer) - - local processed = preprocess_luash(repl.buffer) - local func, err = load("return " .. processed, "@repl") - - if not func then - -- Try without return (for statements) - func, err = load(processed, "@repl") - end - - if func then - local success, result = pcall(func) - if success then - if result ~= nil then - print(tostring(result)) - end - else - print("Error: " .. tostring(result)) - end - else - print("Syntax error: " .. err) - end - - repl.buffer = "" - end - end - end -end - --- Start the REPL -run_repl() diff --git a/repl_demo.luash b/repl_demo.luash index 6ad05e2..2f8ad5b 100644 --- a/repl_demo.luash +++ b/repl_demo.luash @@ -51,7 +51,7 @@ end print() print("🚀 Try it now: ./luash -i") -print(" Or run: ./luash_shell.lua") +print(" Or: ./luash --interactive") print() print("💡 The REPL supports:")