-- ||| Suggest-Require ||| ----------------------------------------------------- -- Version 1, minor 2 ( 9. January 2018 ) -- 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 -- Initially created in September 2017. -- "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. -------------------------------------------------------------------------------- -- Platform dependant local PACKAGE_CONFIG_PATTERN = '([^\n]+)\n([^\n]+)\n([^\n]+)\n([^\n]+)\n([^\n]+)' local function error_check_package_table () local err_msg if type(package) ~= 'table' then err_msg = '"package" global must be a table, but was a '..type(package)..'.' elseif type(package.loaded) ~= 'table' then err_msg = '"package.loaded" must be a table, but was a '..type(package.loaded)..'.' elseif type(package.config) ~= 'string' then err_msg = '"package.config" must be a string, but was a '..type(package.config)..'.' elseif not package.config:find(PACKAGE_CONFIG_PATTERN) then err_msg = '"package.config" must fit the package.config format.' elseif type(package.path) ~= 'string' then err_msg = '"package.path" must be a string, but was a '..type(package.path)..'.' elseif type(package.cpath) ~= 'string' then err_msg = '"package.cpath" must be a string, but was a '..type(package.cpath)..'.' end if err_msg then error('[suggest-require]: The package table is corrupted, and suggest-require cannot procede. '..err_msg..' Please note that the package table has been deliberately messed with.', 2) end end local function package_config () assert(type(package.config) == 'string') local l1, l2, l3, l4, l5 = package.config:match(PACKAGE_CONFIG_PATTERN) return { dir_sep = l1, temp_sep = l2, sub_char = l3, exec_dir = l4, ignore = l5, } end local iterate_files_in_subfiles = function (...) -- This outer function is a wrapper, that lazily loads the correct -- version of the iterate function, based on the availability of -- the commands. if os.execute 'find -false' == 0 then iterate_files_in_subfiles = function (root_path) -- On unix -- Use `find` to find files in folders below the given matching. assert(type(root_path) == 'string') local start_directory = root_path:match '^(.-)?' or root_path local pfile = io.popen ('find -L "'..start_directory..'" -type f ! -path \'*/\\.*\' -print0 2> /dev/null') local list_str = pfile:read '*all' pfile:close() return list_str:gmatch '[^%z]+' end else -- Other platforms error '[suggest-require]: Your platform does not possess the "find" utility, and is thus not currently supported.' end ----- return iterate_files_in_subfiles(...) end -------------------------------------------------------------------------------- -- Util local function split_string (str, split) assert(type(str) == 'string') assert(type(split) == 'string') -- local seq, last_i = {}, 1 while true do local next_i, after_i = str:find(split, last_i) if not next_i then break end seq[#seq+1], last_i = str:sub(last_i, next_i - 1), after_i + 1 end seq[#seq+1] = str:sub(last_i) return seq end local function keys_of_table (t) -- Returns a new sequence, including only the keys from the given -- table. assert(type(t) == 'table') local seq = {} for k in pairs(t) do seq[#seq+1] = k end return seq end local function module_pattern_to_file_pattern (module_pattern) -- Escapes the module pattern, and replaces wildcards "?" with -- Lua's captures. assert(type(module_pattern) == 'string') local escaped = module_pattern:gsub('[%^%(%)%.%[%]%*%+%-]', function (a) return '%' .. a end) return string.format('^%s$', escaped:gsub(package_config().sub_char, '(.+)')) end local function consistent_module_wildcards (...) -- For paths involving several wildcards, we need to ensure they are -- consistent. Therefore collect them and check them through. -- If they are consistent, return one of them. local first = (...) if not first then return false end -- for i = 2, select('#', ...) do if first ~= select(i, ...) then return false end end -- return first end local function get_modules_fitting_path (root_path, modules) -- Finds modules that fit the given path, and places them in the -- modules table. assert(type(root_path) == 'string') assert(type(modules) == 'table') -- Construct a pattern for finding wildcards, in the paths of possible modules. local module_path_pattern = module_pattern_to_file_pattern(root_path) -- Look through the file list, and find probable files. for path in iterate_files_in_subfiles(root_path) do -- Find wildcards, and ensure consistency local wildcard = consistent_module_wildcards(path:match(module_path_pattern)) if wildcard then modules[#modules+1] = wildcard:gsub(package_config().dir_sep, '.') end end end -------------------------------------------------------------------------------- -- Finding Module Names local function get_module_names_from_path_list (path_list) -- Given a path list embedded into a string, using the -- package.config format, return the modules that can be imported -- using "require", that fit that list. assert(type(path_list) == 'string') local paths = split_string(path_list, package_config().temp_sep) local modules = {} for _, path in pairs(paths) do get_modules_fitting_path(path, modules) end return modules end local PACKAGE_SEARCH_METHODS = { -- Modules already loaded: function () return keys_of_table(package.loaded) end, -- Preloaded modules: function () return keys_of_table(package.preload) end, -- Lua modules not loaded yet: function () return get_module_names_from_path_list(package.path) end, -- C modules not loaded yet: function () return get_module_names_from_path_list(package.cpath) end, } 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. error_check_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) -- Assert that output is correctly formatted 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 -- If run from terminal, it will display all available modules. -- If imported from another lua file, it 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