├── cmds ├── README.md ├── add.lua ├── multiply.lua ├── mkdir.lua ├── cd.lua ├── download.lua ├── touch.lua ├── rm.lua ├── echo.lua ├── cat.lua └── ls.lua ├── LICENSE ├── README.md └── core.lua /cmds/README.md: -------------------------------------------------------------------------------- 1 | # Commands 2 | This is the folder for sample commands. See how commands are defined. 3 | -------------------------------------------------------------------------------- /cmds/add.lua: -------------------------------------------------------------------------------- 1 | local add = {} 2 | 3 | add.args = { 4 | {1, "number", true, nil, "num1"}, 5 | {2, "number", true, nil, "num2"} 6 | } 7 | add.usage = "add num1 num2" 8 | 9 | function add.Handler(num1, num2) 10 | print(num1 + num2) 11 | end 12 | 13 | return add -------------------------------------------------------------------------------- /cmds/multiply.lua: -------------------------------------------------------------------------------- 1 | local multiply = {} 2 | 3 | multiply.args = { 4 | {1, "number", true, nil, "num1"}, 5 | {2, "number", true, nil, "num2"} 6 | } 7 | 8 | multiply.usage = "multiply num1 num2" 9 | 10 | function multiply.Handler(num1, num2) 11 | print(num1 * num2) 12 | end 13 | 14 | return multiply 15 | -------------------------------------------------------------------------------- /cmds/mkdir.lua: -------------------------------------------------------------------------------- 1 | local lfs = require("lfs") 2 | 3 | local mkdir = {} 4 | 5 | mkdir.args = { 6 | {1, "string", true, nil, "dir"} 7 | } 8 | 9 | mkdir.usage = "mkdir dir" 10 | 11 | function mkdir.Handler(dir) 12 | local success, err = lfs.mkdir(dir) 13 | if not success then 14 | print(err) 15 | end 16 | end 17 | 18 | return mkdir 19 | -------------------------------------------------------------------------------- /cmds/cd.lua: -------------------------------------------------------------------------------- 1 | -- Change directory command 2 | local lfs = require("lfs") 3 | 4 | local cd = {} 5 | 6 | cd.args = { 7 | {1, "string", true, nil, "directory"} 8 | } 9 | 10 | cd.usage = "cd directory" 11 | 12 | function cd.Handler(dir) 13 | local success, errMsg = lfs.chdir(dir) 14 | if not success then 15 | print(errMsg) 16 | end 17 | end 18 | 19 | return cd -------------------------------------------------------------------------------- /cmds/download.lua: -------------------------------------------------------------------------------- 1 | local http = require("socket.http") 2 | 3 | local download = {} 4 | 5 | download.args = { 6 | {1, "string", true, nil, "url"} 7 | } 8 | 9 | download.usage = "download url" 10 | 11 | function download.Handler(url) 12 | local response, status, headers = http.request(url) 13 | 14 | if status == 200 then 15 | print("Download succesful") 16 | else 17 | print("Download failed, status: " .. status) 18 | end 19 | end 20 | 21 | return download -------------------------------------------------------------------------------- /cmds/touch.lua: -------------------------------------------------------------------------------- 1 | local touch = {} 2 | 3 | touch.args = { 4 | {1, "string", true, nil, "filename"} 5 | } 6 | 7 | touch.usage = "touch filename" 8 | 9 | function touch.Handler(filename) 10 | local file = io.open(filename, "rb") 11 | if file then 12 | file:close() 13 | print("File " .. filename .. " already exists") 14 | else 15 | file = io.open(filename, "wb") 16 | if file then 17 | file:close() 18 | else 19 | print("Failed to create the file.") 20 | end 21 | end 22 | end 23 | 24 | 25 | return touch -------------------------------------------------------------------------------- /cmds/rm.lua: -------------------------------------------------------------------------------- 1 | local lfs = require("lfs") 2 | 3 | local rm = {} 4 | 5 | rm.args = { 6 | {1, "string", true, nil, "fileordir"} 7 | } 8 | 9 | rm.usage = "rm fileordir" 10 | 11 | function rm.Handler(fileordir) 12 | local fileType = lfs.attributes(fileordir, "mode") 13 | 14 | -- Check if it exists 15 | if fileType then 16 | -- Check if it's a directory 17 | if fileType.mode == "directory" then 18 | lfs.rmdir(fileordir) 19 | else 20 | os.remove(fileordir) 21 | end 22 | else 23 | print("File or directory doesn't exist") 24 | end 25 | end 26 | 27 | return rm -------------------------------------------------------------------------------- /cmds/echo.lua: -------------------------------------------------------------------------------- 1 | local echo = {} 2 | 3 | echo.args = { 4 | {1, "string", true, nil, "msg"}, 5 | {2, "string", false, "", "outputFile"} -- Additional argument for file redirection 6 | } 7 | echo.usage = "echo msg num [outputFile]" 8 | 9 | function echo.Handler(msg, outputFile) 10 | print(msg) 11 | 12 | if outputFile ~= "" then 13 | local file = io.open(outputFile, "w") 14 | if file then 15 | file:write(msg .. "\n") 16 | file:close() 17 | print("Output written to " .. outputFile) 18 | else 19 | print("Error opening file " .. outputFile) 20 | end 21 | end 22 | end 23 | 24 | return echo 25 | -------------------------------------------------------------------------------- /cmds/cat.lua: -------------------------------------------------------------------------------- 1 | local lfs = require("lfs") 2 | 3 | local cat = {} 4 | 5 | cat.args = { 6 | {1, "string", true, nil, "file"} 7 | } 8 | 9 | cat.usage = "cat file" 10 | 11 | function cat.Handler(file) 12 | local fileObj, err = io.open(file, "r") 13 | local fileType = lfs.attributes(file, "mode") 14 | 15 | if not fileObj and file ~= "~" then 16 | print(err) 17 | else 18 | if fileType == "file" then 19 | local contents 20 | if fileObj then 21 | contents = fileObj:read("a") 22 | end 23 | 24 | if not contents then 25 | print("Failed to read contents of " .. file) 26 | elseif contents == "" then 27 | print(file .. " is empty") 28 | else 29 | io.write(contents) 30 | end 31 | 32 | if fileObj then 33 | fileObj:close() 34 | end 35 | io.write("\n") 36 | else 37 | print(file .. " is not a file") 38 | end 39 | end 40 | end 41 | 42 | return cat 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 dl-tg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cmds/ls.lua: -------------------------------------------------------------------------------- 1 | local lfs = require("lfs") 2 | 3 | local ls = {} 4 | 5 | ls.args = { 6 | {1, "string", false, lfs.currentdir(), "dir"} 7 | } 8 | 9 | ls.usage = "ls [dir] (default: current directory)" 10 | 11 | local WHITE_ANSI = "\27[0m" 12 | local AQUA_ANSI = "\27[36m" 13 | 14 | function ls.Handler(dir) 15 | -- If dir equals ~ (home directory), replace it with environment variable HOME 16 | -- Else, if dir starts with ~, replace it with environment variable HOME and then get the rest 17 | -- of the path 18 | if dir == "~" then 19 | dir = os.getenv("HOME") 20 | elseif dir:sub(1, 1) == "~" then 21 | dir = os.getenv("HOME") .. dir:sub(2) 22 | end 23 | 24 | -- Check if directory exists by checking it's mode 25 | local dirExist = lfs.attributes(dir, "mode") == "directory" 26 | 27 | if dirExist then 28 | for file in lfs.dir(dir) do 29 | if file ~= "." and file ~= ".." then 30 | local fullPath = dir .. "/" .. file 31 | local mode = lfs.attributes(fullPath, "mode") 32 | 33 | if mode == "file" then 34 | io.write(WHITE_ANSI .. file .. "\n") 35 | else 36 | io.write(AQUA_ANSI .. file .. "\n") 37 | end 38 | end 39 | end 40 | else 41 | io.write("Directory '" .. dir .. "' does not exist\n") 42 | end 43 | 44 | io.write(WHITE_ANSI) -- Reset text color 45 | end 46 | 47 | return ls 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Luash 2 | 3 | Luash is a simple interactive shell written in Lua that allows you to execute custom commands defined in separate Lua script files. It provides a convenient way to extend the shell with your own commands, making it more versatile and tailored to your needs. 4 | 5 | ## Features 6 | 7 | - Easy-to-use interactive shell environment 8 | - Customizable commands through Lua script files 9 | - Support for required and optional arguments with type validation 10 | - Default values for optional arguments 11 | 12 | ## Requirements 13 | 14 | - Lua (5.1 or above) 15 | - LuaFileSystem (lfs) library 16 | - Luasocket library 17 | - Luasec library 18 | 19 | ## Usage 20 | 21 | 1. Clone or download the repository to your local machine 22 | 2. Make sure you have Lua and the LuaFileSystem, Luasec and Luasocket library installed 23 | - Download Lua here https://www.lua.org/download.html 24 | - To install the libraries, install luarocks and run 25 | ```shell 26 | luarocks install luafilesystem 27 | luarocks install luasocket 28 | luarocks install luasec 29 | ``` 30 | 31 | 3. Navigate to the Luash directory 32 | 4. Run the core.lua script using the Lua interpreter: 33 | 34 | ```bash 35 | 36 | lua core.lua 37 | ``` 38 | You will be presented with the Luash prompt (>) where you can enter commands. 39 | 40 | ## Custom Commands 41 | 42 | To add your custom commands, create Lua script files (the filename will be command's name, e.g hello.lua would create a hello command) in the cmds folder, defining your commands with the required structure. Each command should be defined as a Lua table with the following properties: 43 | 44 | - `args` (table): A table defining the arguments for the command, including type and whether they are required or optional. 45 | 46 | - Argument definition format: {argIndex, argType, isRequired, defaultValue, argAlias} 47 | 48 | - `argIndex` (number): The position of the argument. 49 | 50 | - `argType` (string): The data type of the argument. Supported types: "string", "number", "boolean". 51 | 52 | - `isRequired` (boolean): Whether the argument is required (true) or optional (false). 53 | 54 | - `defaultValue` (any, optional): The default value for optional arguments. Only applicable if isRequired is false, set to `nil` if arg is required. The type of default value should match the type of the argument 55 | 56 | - `argAlias` (string, optional): An alias for the argument, used in usage error messages. 57 | 58 | Note: To pass a string argument that contains spaces, wrap the entire string in single or double quotes. 59 | 60 | - `usage` (string): A usage string that provides information about how to use the command. 61 | 62 | - `Handler` (function): The function that implements the behavior of the command. 63 | 64 | Example of a custom command file (echo.lua): 65 | 66 | ```lua 67 | 68 | local echo = {} 69 | 70 | echo.args = { 71 | {1, "string", true, nil, "msg"}, 72 | {2, "number", false, 14, "num"} 73 | } 74 | echo.usage = "echo msg num" 75 | 76 | function echo.Handler(msg, num) 77 | print(msg) 78 | print(num) 79 | end 80 | 81 | return echo 82 | ``` 83 | ## Reloading 84 | 85 | At any time, you can type reload at the Luash prompt (>) to manually reload the custom commands. This is useful if you make changes to the command script files and want to apply the changes during runtime. 86 | 87 | To exit the Luash shell, simply type exit. 88 | 89 | ## Examples 90 | 91 | 1. Execute the echo command with a string and a number: 92 | 93 | ```shell 94 | 95 | > echo 'Hello, Luash!' 42 96 | Hello, Luash! 97 | 42 98 | ``` 99 | 2. Execute the echo command with only a string (optional argument will use the default value): 100 | 101 | ```shell 102 | 103 | > echo 'Hello, Luash!' 104 | Hello, Luash! 105 | 14 106 | ``` 107 | 3. Execute the reload command to reload the custom commands after making changes: 108 | 109 | ```shell 110 | 111 | > reload 112 | Reloading the commands... 113 | Success! 114 | ``` 115 | ## Contributions 116 | 117 | If you encounter any issues, have suggestions, or want to contribute new features, feel free to create a pull request or open an issue on GitHub. 118 | If you want to contribute but are unsure how, refer to the official GitHub guide on [Contributing to projects](https://docs.github.com/en/get-started/quickstart/contributing-to-projects). 119 | 120 | ## License 121 | 122 | Luash is open-source software licensed under the [MIT License](LICENSE). 123 | -------------------------------------------------------------------------------- /core.lua: -------------------------------------------------------------------------------- 1 | local lfs = require("lfs") 2 | local cmds = {} 3 | 4 | -- Get all commands from cmds folder and insert them in cmds dictionary 5 | -- Key is the alias (filename without extension), value is require(command file here) 6 | -- Loop through cmds folder 7 | 8 | local function getCmds() 9 | cmds = {} 10 | for file in lfs.dir("./cmds") do 11 | -- Skip special directory entries (current/parent folder) 12 | if file ~= "." and file ~= ".." then 13 | -- Construct the file path for command module 14 | local filePath = "./cmds/" .. file 15 | -- Extract the metadata and check if it's a Lua file 16 | local attr = lfs.attributes(filePath) 17 | if attr.mode == "file" and filePath:match("%.lua$") then 18 | -- Extract the file name without the extension (.lua) 19 | local cmdAlias = file:match("(.+)%.lua$") 20 | 21 | -- Remove the module from package.loaded to force reloading 22 | package.loaded["cmds." .. cmdAlias] = nil 23 | 24 | -- Insert the command module into the cmds dictionary 25 | cmds[cmdAlias] = require("cmds." .. cmdAlias) 26 | end 27 | end 28 | end 29 | end 30 | 31 | local function getUsername() 32 | local username 33 | local handle = io.popen("whoami") 34 | if handle ~= nil then 35 | username = handle:read("*a"):gsub("\n", "") 36 | handle:close() 37 | end 38 | return username 39 | end 40 | 41 | -- Function to handle usage errors 42 | local function UsageError(error, usage) 43 | print(error) 44 | if usage ~= nil then 45 | print("Usage: " .. usage) 46 | else 47 | print("Note: Please, provide usage for the command") 48 | end 49 | end 50 | 51 | -- Function to check arguments and perform type conversion 52 | local function checkArgs(args, requiredArgs, usage) 53 | for key, value in ipairs(requiredArgs) do 54 | local argIndex = value[1] 55 | local argType = value[2] 56 | local isRequired = value[3] 57 | local argAlias = value[5] 58 | local arg = args[key] 59 | 60 | if #value < 4 then 61 | print("Please fill out the argument definition fully (expected 4 elements in total, got only " .. #value .. " elements)") 62 | break 63 | end 64 | 65 | -- Check if the argument is missing 66 | if arg == nil then 67 | if isRequired then 68 | if argAlias ~= nil then 69 | UsageError("Missing required argument value (arg index: " .. argIndex .. ", arg alias: " .. argAlias .. ")", usage) 70 | else 71 | UsageError("Missing required argument value (arg index: " .. argIndex .. ")", usage) 72 | print("Note: Please, add alias for arguments!") 73 | end 74 | return false 75 | end 76 | else 77 | -- Perform type conversion based on argType 78 | if argType == "number" then 79 | local convertedArg = tonumber(arg) 80 | -- Check if the conversion was successful 81 | if convertedArg == nil then 82 | if argAlias ~= nil then 83 | UsageError("Invalid argument type, expected number for " .. argAlias .. " (arg index: " .. argIndex .. ")") 84 | else 85 | UsageError("Invalid argument type, expected number for arg index: " .. argIndex) 86 | end 87 | return false 88 | end 89 | args[key] = convertedArg 90 | elseif argType == "boolean" then 91 | -- Convert true/false strings to boolean values 92 | if arg:lower() == "true" then 93 | args[key] = true 94 | elseif arg:lower() == "false" then 95 | args[key] = false 96 | else 97 | if argAlias ~= nil then 98 | UsageError("Invalid argument type, expected boolean (true/false) for " .. argAlias .. " (arg index: " .. argIndex .. ")") 99 | else 100 | UsageError("Invalid argument type, expected boolean (true/false) for arg index: " .. argIndex) 101 | end 102 | return false 103 | end 104 | end 105 | end 106 | end 107 | return true 108 | end 109 | 110 | -- Clear the terminal 111 | if os.execute("clear") == nil then 112 | os.execute("cls") -- For Windows 113 | end 114 | 115 | getCmds() 116 | print("Luash v0.1") 117 | 118 | -- Main loop to read and process user input 119 | while true do 120 | io.write("[" .. getUsername() .. "@ " .. (lfs.currentdir():match("([^/\\]+)$") or "/") .. "] > ") 121 | local input = io.read() 122 | local cmd, argStr = input:match("(%S+)%s*(.*)") 123 | 124 | -- Handle empty command 125 | if input == "" then 126 | goto continue 127 | end 128 | 129 | -- Exit command 130 | if cmd == "exit" then 131 | break 132 | end 133 | 134 | -- Reload command 135 | if cmd == "reload" then 136 | print("Reloading the commands...") 137 | getCmds() 138 | print("Success!") 139 | end 140 | 141 | local curCmd = cmds[cmd] 142 | if cmd ~= "reload" then 143 | if curCmd then 144 | if type(curCmd) == "table" then 145 | -- Check if the command has arguments defined 146 | if curCmd.args then 147 | local args = {} 148 | local defaultValues = {} 149 | 150 | -- Extract default values for optional arguments 151 | for _, value in ipairs(curCmd.args) do 152 | local isRequired = value[3] 153 | local defaultValue = value[4] 154 | if not isRequired then 155 | if defaultValue then 156 | table.insert(defaultValues, defaultValue) 157 | else 158 | print("Warning: No default argument provided for arg " .. value[1]) 159 | end 160 | end 161 | end 162 | 163 | -- Use a loop to handle both quoted and unquoted arguments 164 | for arg in argStr:gmatch("%s*'([^']*)'%s*") do 165 | table.insert(args, arg) 166 | argStr = argStr:gsub("'" .. arg .. "'", "", 1) 167 | end 168 | 169 | -- Split remaining arguments using space as the delimiter 170 | for arg in argStr:gmatch("%S+") do 171 | table.insert(args, arg) 172 | end 173 | 174 | -- Assign default values to optional arguments if not provided by the user 175 | for i, value in ipairs(curCmd.args) do 176 | if not args[i] then 177 | if not value[3] then -- Check if the argument is required 178 | args[i] = value[4] -- Assign the default values 179 | end 180 | end 181 | end 182 | 183 | -- Check if the number of args is bigger than specified max amount of args, 184 | -- and if all required args were provided, print error and usage if false 185 | if #args > #curCmd.args then 186 | UsageError("Too many arguments!", curCmd.usage) 187 | elseif checkArgs(args, curCmd.args, curCmd.usage) == true then 188 | curCmd.Handler(table.unpack(args)) 189 | end 190 | else 191 | -- Execute command without arguments 192 | curCmd.Handler() 193 | end 194 | else 195 | print("Invalid command module. Try adding at the end of your command module script return command_name_here") 196 | end 197 | else 198 | print(cmd .. ": Command not found") 199 | end 200 | end 201 | ::continue:: 202 | end --------------------------------------------------------------------------------