2017-09-29 07:36:56 +00:00
2017-10-11 09:57:42 +00:00
-- ||| Suggest-Require ||| -----------------------------------------------------
2017-09-29 07:36:56 +00:00
2017-10-11 09:57:42 +00:00
-- 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.
2020-07-06 18:18:53 +00:00
-- Known to work with Lua 5.1 and LuaJIT, on Linux.
2017-10-11 09:57:42 +00:00
-- 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.
2020-07-06 18:18:53 +00:00
--[[ 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)
2017-10-11 09:57:42 +00:00
--------------------------------------------------------------------------------
2018-01-09 12:10:28 +00:00
-- 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
2020-10-31 16:27:22 +00:00
local iterate_files_in_subfiles
function iterate_files_in_subfiles ( ... )
2018-01-09 12:29:15 +00:00
-- 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
2017-09-29 07:36:56 +00:00
2018-01-09 12:29:15 +00:00
-----
2017-09-29 07:36:56 +00:00
2018-01-09 12:29:15 +00:00
return iterate_files_in_subfiles ( ... )
2017-09-29 07:36:56 +00:00
end
2017-10-11 09:57:42 +00:00
--------------------------------------------------------------------------------
2017-09-29 07:36:56 +00:00
-- Util
2018-01-09 12:10:28 +00:00
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
2018-01-06 17:01:52 +00:00
end
2018-01-09 12:10:28 +00:00
seq [ # seq + 1 ] = str : sub ( last_i )
return seq
2017-09-29 07:36:56 +00:00
end
2018-01-09 12:10:28 +00:00
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
2017-10-11 09:57:42 +00:00
2018-01-09 12:10:28 +00:00
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
2017-09-29 07:36:56 +00:00
2018-01-09 12:10:28 +00:00
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
2017-10-11 09:57:42 +00:00
2018-01-09 12:10:28 +00:00
local function get_modules_fitting_path ( root_path , modules )
-- Finds modules that fit the given path, and places them in the
-- modules table.
2017-10-11 09:57:42 +00:00
2018-01-09 12:10:28 +00:00
assert ( type ( root_path ) == ' string ' )
assert ( type ( modules ) == ' table ' )
2018-01-06 17:01:52 +00:00
2018-01-09 12:10:28 +00:00
-- Construct a pattern for finding wildcards, in the paths of possible modules.
local module_path_pattern = module_pattern_to_file_pattern ( root_path )
2018-01-06 17:01:52 +00:00
2018-01-09 12:10:28 +00:00
-- Look through the file list, and find probable files.
for path in iterate_files_in_subfiles ( root_path ) do
2018-01-06 17:01:52 +00:00
2018-01-09 12:10:28 +00:00
-- 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 , ' . ' )
2018-01-06 17:01:52 +00:00
end
end
2017-09-29 07:36:56 +00:00
end
2017-10-11 09:57:42 +00:00
--------------------------------------------------------------------------------
2017-09-29 07:36:56 +00:00
-- Finding Module Names
2018-01-09 12:10:28 +00:00
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.
2017-09-30 12:39:53 +00:00
2018-01-09 12:10:28 +00:00
assert ( type ( path_list ) == ' string ' )
local paths = split_string ( path_list , package_config ( ) . temp_sep )
2017-09-29 07:36:56 +00:00
2018-01-06 17:01:52 +00:00
local modules = { }
for _ , path in pairs ( paths ) do
get_modules_fitting_path ( path , modules )
end
2017-09-29 07:36:56 +00:00
2018-01-06 17:01:52 +00:00
return modules
2017-09-29 07:36:56 +00:00
end
2018-01-09 12:10:28 +00:00
local PACKAGE_SEARCH_METHODS = {
-- Modules already loaded:
function ( ) return keys_of_table ( package.loaded ) end ,
2017-09-29 07:36:56 +00:00
2018-01-09 12:10:28 +00:00
-- Preloaded modules:
function ( ) return keys_of_table ( package.preload ) end ,
2017-09-29 07:36:56 +00:00
2018-01-09 12:10:28 +00:00
-- Lua modules not loaded yet:
function ( ) return get_module_names_from_path_list ( package.path ) end ,
2017-09-29 07:36:56 +00:00
2018-01-09 12:10:28 +00:00
-- C modules not loaded yet:
function ( ) return get_module_names_from_path_list ( package.cpath ) end ,
2017-09-30 12:39:53 +00:00
}
2017-09-29 07:36:56 +00:00
local function get_available_module_names ( )
2018-01-06 17:01:52 +00:00
-- Searches through the package system to determine which modules can be
-- imported by using `require`.
2017-10-11 09:57:42 +00:00
2018-01-06 17:01:52 +00:00
-- Returns a sequence of strings, each of which can be directly used by
-- `require` to import the module.
2017-09-29 07:36:56 +00:00
2018-01-09 12:10:28 +00:00
error_check_package_table ( )
2017-09-29 07:36:56 +00:00
2018-01-06 17:01:52 +00:00
-- 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
2017-09-30 12:39:53 +00:00
2018-01-06 17:01:52 +00:00
-- Sort
local module_names = { }
for name in pairs ( dedub ) do module_names [ # module_names + 1 ] = name end
table.sort ( module_names )
2017-09-29 07:36:56 +00:00
2018-01-09 12:29:15 +00:00
-- Assert that output is correctly formatted
2018-01-06 17:01:52 +00:00
assert ( type ( module_names ) == ' table ' )
for i = 1 , # module_names do
assert ( type ( module_names [ i ] ) == ' string ' )
end
2017-09-29 07:36:56 +00:00
2018-01-06 17:01:52 +00:00
-- Return
return module_names
2017-09-29 07:36:56 +00:00
end
2017-10-11 09:57:42 +00:00
--------------------------------------------------------------------------------
-- Run or return
2018-01-09 12:10:28 +00:00
-- If run from terminal, it will display all available modules.
-- If imported from another lua file, it will return itself as a
-- library
2017-09-29 07:36:56 +00:00
if ... then
2018-01-06 17:01:52 +00:00
return get_available_module_names
2017-09-29 07:36:56 +00:00
else
2018-01-06 17:01:52 +00:00
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
2017-09-29 07:36:56 +00:00
end
2018-01-06 17:01:52 +00:00