-- ||| Suggest-Require ||| ----------------------------------------------------- -- Version 1.0.0 ( 11. October 2017 ) -- This is a small library to discover which modules are importable using -- `require`. It's useful for seeing which modules your Lua environment -- can access. It's intended usage is in an auto-complete system for Lua. -- Author: Jmaa -- Email: jonjmaa@gmail.com -- Website: aanes.xyz -- "THE BEER-WARE LICENSE" (Revision 42): -- wrote this file. As long as you retain this notice you -- can do whatever you want with this stuff. If we meet some day, and you think -- this stuff is worth it, you can buy me a beer in return. -- TODO: Ensure it works under both Windows and MacOS. -- TODO: Add support for alternative package.config -------------------------------------------------------------------------------- -- Constants local SCAN_DIR_TEMPLATE, SCAN_DIR_SEP_PATTERN if package.config:sub(1, 1) == '/' then -- On unix SCAN_DIR_TEMPLATE = 'find -L "%s" -maxdepth 1 -mindepth 1 -print0' SCAN_DIR_SEP_PATTERN = '%z' else -- On windows SCAN_DIR_TEMPLATE = 'dir "%s" /b /ad' SCAN_DIR_SEP_PATTERN = '\n' end -------------------------------------------------------------------------------- -- Util local function get_module_paths (path_str) -- Gets the paths of contained in `path_str` based on the format in -- package.config. -- Error check local path_str = path_str or package.path assert(type(package.config) == 'string') assert(type(path_str) == 'string') -- Work work local paths = {} for path in path_str:gmatch '[^;]+' do paths[#paths+1] = path end -- Return return paths end local function get_modules_fitting_path (root_path, module_names) -- First builds up a list of paths to files in `root_path`, and then goes -- through and ensures they match the possible paths for modules. assert(type(root_path) == 'string') assert(type(module_names) == 'table') -- Use `find` to find files in folders below the given matching. local pfile = io.popen ('find -L "'..(root_path:match '^(.-)?' or root_path)..'" -type f -not -path \'*/\\.*\' -print0') local list_str = pfile:read '*all' pfile:close() -- Construct a pattern for the expected path of possible modules. local module_path_pattern = '^' .. root_path:gsub('[%^%(%)%.%[%]%*%+%-]', function (a) return '%' .. a end) :gsub('?', '(.+)') .. '$' -- Look through the file list, and find importable modules. for path in list_str:gmatch '[^%z]+' do local matches = { path:match(module_path_pattern) } local identical = #matches > 0 for i = 1, #matches do if matches[1] ~= matches[i] then identical = false break end end -- Check if match if identical then module_names[#module_names+1] = matches[1]:gsub('/', '.') end end end -------------------------------------------------------------------------------- -- Finding Module Names local function get_loaded_module_names () -- Get the names of modules already loaded. local l = {} for k in pairs(package.loaded) do l[#l+1] = k end return l end local function get_preloaded_module_names () -- Get the names of preloaded modules. local l = {} for k in pairs(package.loaded) do l[#l+1] = k end return l end local function get_module_names_from_path () -- Get the names of modules not loaded yet. local paths = get_module_paths(package.path) local modules = {} for _, path in pairs(paths) do get_modules_fitting_path(path, modules) end return modules end local function get_c_module_names_from_path () -- Get the names of c-modules not loaded yet. local paths = get_module_paths(package.cpath) local modules = {} for _, path in pairs(paths) do get_modules_fitting_path(path, modules) end return modules end local PACKAGE_SEARCH_METHODS = { get_loaded_module_names, get_preloaded_module_names, get_module_names_from_path, get_c_module_names_from_path, } local function get_available_module_names () -- Searches through the package system to determine which modules can be -- imported by using `require`. -- Returns a sequence of strings, each of which can be directly used by -- `require` to import the module. assert(type(package) == 'table') -- Find and De-duplicate local dedub = {} for _, method in ipairs(PACKAGE_SEARCH_METHODS) do for _, name in ipairs(method()) do dedub[name] = true end end -- Sort local module_names = {} for name in pairs(dedub) do module_names[#module_names+1] = name end table.sort(module_names) -- Output assertions assert(type(module_names) == 'table') for i = 1, #module_names do assert(type(module_names[i]) == 'string') end -- Return return module_names end -------------------------------------------------------------------------------- -- Run or return -- Can be run directly from the terminal to display the available modules, or -- if imported using "require", will return itself as a library. if ... then return get_available_module_names else local names = get_available_module_names() io.write 'Following modules are available: \n' for _, module_name in ipairs(names) do io.write ' - ' io.write(module_name) io.write '\n' end end