266 lines
8.7 KiB
Lua
266 lines
8.7 KiB
Lua
|
|
-- ||| Suggest-Require ||| -----------------------------------------------------
|
|
|
|
-- 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.
|
|
|
|
-- Known to work with Lua 5.1 and LuaJIT, on Linux.
|
|
|
|
-- Author: Jmaa
|
|
-- Email: jonjmaa@gmail.com
|
|
-- Website: aanes.xyz
|
|
|
|
-- "THE BEER-WARE LICENSE" (Revision 42):
|
|
-- <jonjmaa@gmail.com> 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.
|
|
|
|
--[[ Usage ]]--
|
|
|
|
-- Standalone: The library be called as a script `luajit
|
|
-- suggest-require.lua` to print available packages in the Lua
|
|
-- environment.
|
|
|
|
-- Library: Import through require. A single function is be returned.
|
|
-- Calling this function will return the available packages, as a list
|
|
-- of strings.
|
|
|
|
--[[ Example ]]--
|
|
|
|
-- Replicating the script functionality of this library is as simple as:
|
|
|
|
-- local package_names = require 'suggest-require' ()
|
|
-- for _, name in ipairs(package_names) do
|
|
-- print('- '..name)
|
|
-- end
|
|
|
|
--[[ Changelog ]]--
|
|
|
|
-- 1.3: Usage and example (6. July 2020)
|
|
-- 1.2: Unknown change (9. January 2018)
|
|
-- 1.1: Unknown change (Unknown)
|
|
-- 1.0: Initial version (September 2017)
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- 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 iterate_files_in_subfiles (...)
|
|
-- 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
|
|
|