diff --git a/suggest-require.lua b/suggest-require.lua index 85a1e0f..98de5bd 100644 --- a/suggest-require.lua +++ b/suggest-require.lua @@ -1,7 +1,7 @@ -- ||| Suggest-Require ||| ----------------------------------------------------- --- Version 1.0.0 ( 11. October 2017 ) +-- 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 @@ -17,78 +17,130 @@ -- 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 +-- Platform dependant -local SCAN_DIR_TEMPLATE, SCAN_DIR_SEP_PATTERN +local PACKAGE_CONFIG_PATTERN = '([^\n]+)\n([^\n]+)\n([^\n]+)\n([^\n]+)\n([^\n]+)' -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' +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 + +if package_config().dir_sep == '/' then + function iterate_files_in_subfiles (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 -- On windows - SCAN_DIR_TEMPLATE = 'dir "%s" /b /ad' - SCAN_DIR_SEP_PATTERN = '\n' + error '[suggest-require]: Windows is not currently supported.' 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 +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 - -- Return - return paths + seq[#seq+1] = str:sub(last_i) + return seq 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. +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(module_names) == 'table') + assert(type(modules) == '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 finding wildcards, in the paths of possible modules. + local module_path_pattern = module_pattern_to_file_pattern(root_path) - -- 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 probable files. + for path in iterate_files_in_subfiles(root_path) do - -- 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('/', '.') + -- 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 @@ -97,35 +149,13 @@ 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_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. -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) + assert(type(path_list) == 'string') + local paths = split_string(path_list, package_config().temp_sep) local modules = {} for _, path in pairs(paths) do @@ -136,10 +166,17 @@ local function get_c_module_names_from_path () end local PACKAGE_SEARCH_METHODS = { - get_loaded_module_names, - get_preloaded_module_names, - get_module_names_from_path, - get_c_module_names_from_path, + -- 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 () @@ -149,7 +186,7 @@ local function get_available_module_names () -- Returns a sequence of strings, each of which can be directly used by -- `require` to import the module. - assert(type(package) == 'table') + error_check_package_table() -- Find and De-duplicate local dedub = {} @@ -175,8 +212,9 @@ 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 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