1
0
Fork 0
suggest-require/suggest-require.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