add --exclude option for ls

develop
myk002 2022-10-05 13:30:14 -07:00
parent 821c74c1e4
commit 9817106c30
No known key found for this signature in database
GPG Key ID: 8A39CA0FA0C16E78
6 changed files with 136 additions and 29 deletions

@ -3150,8 +3150,8 @@ Each entry has several properties associated with it:
alphabetized by their last path component, with populated path components alphabetized by their last path component, with populated path components
coming before null path components (e.g. ``autobutcher`` will immediately coming before null path components (e.g. ``autobutcher`` will immediately
follow ``gui/autobutcher``). follow ``gui/autobutcher``).
The optional ``include`` and ``exclude`` filter params are maps with the The optional ``include`` and ``exclude`` filter params are maps (or lists of
following elements: maps) with the following elements:
:str: if a string, filters by the given substring. if a table of strings, :str: if a string, filters by the given substring. if a table of strings,
includes entry names that match any of the given substrings. includes entry names that match any of the given substrings.
@ -3160,6 +3160,13 @@ Each entry has several properties associated with it:
:entry_type: if a string, matches entries of the given type. if a table of :entry_type: if a string, matches entries of the given type. if a table of
strings, includes entries that match any of the given types. strings, includes entries that match any of the given types.
Elements in a map are ANDed together (e.g. if both ``str`` and ``tag`` are
specified, the match is on any of the ``str`` elements AND any of the ``tag``
elements).
If lists of filters are passed instead of a single map, the maps are ORed
(that is, the match succeeds if any of the filters match).
If ``include`` is ``nil`` or empty, then all entries are included. If If ``include`` is ``nil`` or empty, then all entries are included. If
``exclude`` is ``nil`` or empty, then no entries are filtered out. ``exclude`` is ``nil`` or empty, then no entries are filtered out.

@ -40,3 +40,5 @@ Options
Don't print out the tags associated with each command. Don't print out the tags associated with each command.
``--dev`` ``--dev``
Include commands intended for developers and modders. Include commands intended for developers and modders.
``--exclude <string>[,<string>...]``
Exclude commands that match any of the given strings.

@ -39,6 +39,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
## Misc Improvements ## Misc Improvements
- `ls`: indent tag listings and wrap them in the right column for better readability - `ls`: indent tag listings and wrap them in the right column for better readability
- `ls`: new ``--exclude`` option for hiding matched scripts from the output. this can be especially useful for modders who don't want their mod scripts to be included in ``ls`` output.
## Documentation ## Documentation

@ -622,12 +622,18 @@ void ls_helper(color_ostream &con, const vector<string> &params) {
vector<string> filter; vector<string> filter;
bool skip_tags = false; bool skip_tags = false;
bool show_dev_commands = false; bool show_dev_commands = false;
string exclude_strs = "";
bool in_exclude = false;
for (auto str : params) { for (auto str : params) {
if (str == "--notags") if (in_exclude)
exclude_strs = str;
else if (str == "--notags")
skip_tags = true; skip_tags = true;
else if (str == "--dev") else if (str == "--dev")
show_dev_commands = true; show_dev_commands = true;
else if (str == "--exclude")
in_exclude = true;
else else
filter.push_back(str); filter.push_back(str);
} }
@ -636,7 +642,7 @@ void ls_helper(color_ostream &con, const vector<string> &params) {
auto L = Lua::Core::State; auto L = Lua::Core::State;
Lua::StackUnwinder top(L); Lua::StackUnwinder top(L);
if (!lua_checkstack(L, 4) || if (!lua_checkstack(L, 5) ||
!Lua::PushModulePublic(con, L, "helpdb", "ls")) { !Lua::PushModulePublic(con, L, "helpdb", "ls")) {
con.printerr("Failed to load helpdb Lua code\n"); con.printerr("Failed to load helpdb Lua code\n");
return; return;
@ -645,8 +651,9 @@ void ls_helper(color_ostream &con, const vector<string> &params) {
Lua::PushVector(L, filter); Lua::PushVector(L, filter);
Lua::Push(L, skip_tags); Lua::Push(L, skip_tags);
Lua::Push(L, show_dev_commands); Lua::Push(L, show_dev_commands);
Lua::Push(L, exclude_strs);
if (!Lua::SafeCall(con, L, 3, 0)) { if (!Lua::SafeCall(con, L, 4, 0)) {
con.printerr("Failed Lua call to helpdb.ls.\n"); con.printerr("Failed Lua call to helpdb.ls.\n");
} }
} }

@ -14,6 +14,8 @@
local _ENV = mkmodule('helpdb') local _ENV = mkmodule('helpdb')
local argparse = require('argparse')
local MAX_STALE_MS = 60000 local MAX_STALE_MS = 60000
-- paths -- paths
@ -588,6 +590,8 @@ function sort_by_basename(a, b)
return false return false
end end
-- returns true if all filter elements are matched (i.e. any of the tags AND
-- any of the strings AND any of the entry_types)
local function matches(entry_name, filter) local function matches(entry_name, filter)
if filter.tag then if filter.tag then
local matched = false local matched = false
@ -630,9 +634,18 @@ local function matches(entry_name, filter)
return true return true
end end
local function matches_any(entry_name, filters)
for _,filter in ipairs(filters) do
if matches(entry_name, filter) then
return true
end
end
return false
end
-- normalizes the lists in the filter and returns nil if no filter elements are -- normalizes the lists in the filter and returns nil if no filter elements are
-- populated -- populated
local function normalize_filter(f) local function normalize_filter_map(f)
if not f then return nil end if not f then return nil end
local filter = {} local filter = {}
filter.str = normalize_string_list(f.str) filter.str = normalize_string_list(f.str)
@ -644,11 +657,21 @@ local function normalize_filter(f)
return filter return filter
end end
local function normalize_filter_list(fs)
if not fs then return nil end
local filter_list = {}
for _,f in ipairs(#fs > 0 and fs or {fs}) do
table.insert(filter_list, normalize_filter_map(f))
end
if #filter_list == 0 then return nil end
return filter_list
end
-- returns a list of entry names, alphabetized by their last path component, -- returns a list of entry names, alphabetized by their last path component,
-- with populated path components coming before null path components (e.g. -- with populated path components coming before null path components (e.g.
-- autobutcher will immediately follow gui/autobutcher). -- autobutcher will immediately follow gui/autobutcher).
-- the optional include and exclude filter params are maps with the following -- the optional include and exclude filter params are maps (or lists of maps)
-- elements: -- with the following elements:
-- str - if a string, filters by the given substring. if a table of strings, -- str - if a string, filters by the given substring. if a table of strings,
-- includes entry names that match any of the given substrings. -- includes entry names that match any of the given substrings.
-- tag - if a string, filters by the given tag name. if a table of strings, -- tag - if a string, filters by the given tag name. if a table of strings,
@ -658,14 +681,18 @@ end
-- types are: "builtin", "plugin", "command". note that many plugin -- types are: "builtin", "plugin", "command". note that many plugin
-- commands have the same name as the plugin, so those entries will -- commands have the same name as the plugin, so those entries will
-- match both "plugin" and "command" types. -- match both "plugin" and "command" types.
-- filter elements in a map are ANDed together (e.g. if both str and tag are
-- specified, the match is on any of the str elements AND any of the tag
-- elements). If lists of maps are passed, the maps are ORed (that is, the match
-- succeeds if any of the filters match).
function search_entries(include, exclude) function search_entries(include, exclude)
ensure_db() ensure_db()
include = normalize_filter(include) include = normalize_filter_list(include)
exclude = normalize_filter(exclude) exclude = normalize_filter_list(exclude)
local entries = {} local entries = {}
for entry in pairs(entrydb) do for entry in pairs(entrydb) do
if (not include or matches(entry, include)) and if (not include or matches_any(entry, include)) and
(not exclude or not matches(entry, exclude)) then (not exclude or not matches_any(entry, exclude)) then
table.insert(entries, entry) table.insert(entries, entry)
end end
end end
@ -743,21 +770,30 @@ end
-- wraps the list_entries() API to provide a more convenient interface for Core -- wraps the list_entries() API to provide a more convenient interface for Core
-- to implement the 'ls' builtin command. -- to implement the 'ls' builtin command.
-- filter_str - if a tag name, will filter by that tag. otherwise, will filter -- filter_str - if a tag name (or a list of tag names), will filter by that
-- as a substring -- tag/those tags. otherwise, will filter as a substring/list of
-- substrings
-- skip_tags - whether to skip printing tag info -- skip_tags - whether to skip printing tag info
-- show_dev_commands - if true, will include scripts in the modtools/ and -- show_dev_commands - if true, will include scripts in the modtools/ and
-- devel/ directories. otherwise those scripts will be -- devel/ directories. otherwise those scripts will be
-- excluded -- excluded
function ls(filter_str, skip_tags, show_dev_commands) -- exclude_strs - comma-separated list of strings. entries are excluded if
-- they match any of the strings.
function ls(filter_str, skip_tags, show_dev_commands, exclude_strs)
local include = {entry_type={ENTRY_TYPES.COMMAND}} local include = {entry_type={ENTRY_TYPES.COMMAND}}
if is_tag(filter_str) then if is_tag(filter_str) then
include.tag = filter_str include.tag = filter_str
else else
include.str = filter_str include.str = filter_str
end end
list_entries(skip_tags, include, local excludes = {}
show_dev_commands and {} or {tag='dev'}) if exclude_strs and #exclude_strs > 0 then
table.insert(excludes, {str=argparse.stringList(exclude_strs)})
end
if not show_dev_commands then
table.insert(excludes, {tag='dev'})
end
list_entries(skip_tags, include, excludes)
end end
local function list_tags() local function list_tags()

@ -36,6 +36,7 @@ local mock_script_db = {
inscript_docs=true, inscript_docs=true,
inscript_short_only=true, inscript_short_only=true,
nodocs_script=true, nodocs_script=true,
dev_script=true,
} }
local files = { local files = {
@ -48,6 +49,8 @@ local files = {
* units: Tools that interact with units. * units: Tools that interact with units.
* dev: Dev tools.
* nomembers: Nothing is tagged with this. * nomembers: Nothing is tagged with this.
]], ]],
['hack/docs/docs/tools/hascommands.txt']=[[ ['hack/docs/docs/tools/hascommands.txt']=[[
@ -113,6 +116,20 @@ Command: "subdir/scriptname"
Documented subdir/scriptname. Documented subdir/scriptname.
Documented full help. Documented full help.
]],
['hack/docs/docs/tools/dev_script.txt']=[[
dev_script
==========
Tags: dev
Command: "dev_script"
Short desc.
Full help.
]====]
script contents
]], ]],
['scripts/scriptpath/basic.lua']=[[ ['scripts/scriptpath/basic.lua']=[[
-- in-file short description for basic -- in-file short description for basic
@ -216,6 +233,9 @@ Command: "inscript_docs"
Documented full help. Documented full help.
]====] ]====]
script contents
]],
['other/scriptpath/dev_script.lua']=[[
script contents script contents
]], ]],
} }
@ -495,7 +515,7 @@ function test.is_tag()
end end
function test.get_tags() function test.get_tags()
expect.table_eq({'armok', 'fort', 'map', 'nomembers', 'units'}, expect.table_eq({'armok', 'dev', 'fort', 'map', 'nomembers', 'units'},
h.get_tags()) h.get_tags())
end end
@ -534,8 +554,8 @@ end
function test.search_entries() function test.search_entries()
-- all entries, in alphabetical order by last path component -- all entries, in alphabetical order by last path component
local expected = {'?', 'alias', 'basic', 'bindboxers', 'boxbinders', local expected = {'?', 'alias', 'basic', 'bindboxers', 'boxbinders',
'clear', 'cls', 'die', 'dir', 'disable', 'devel/dump-rpc', 'enable', 'clear', 'cls', 'dev_script', 'die', 'dir', 'disable', 'devel/dump-rpc',
'fpause', 'hascommands', 'help', 'hide', 'inscript_docs', 'enable', 'fpause', 'hascommands', 'help', 'hide', 'inscript_docs',
'inscript_short_only', 'keybinding', 'kill-lua', 'load', 'ls', 'man', 'inscript_short_only', 'keybinding', 'kill-lua', 'load', 'ls', 'man',
'nocommand', 'nodoc_command', 'nodocs_hascommands', 'nodocs_nocommand', 'nocommand', 'nodoc_command', 'nodocs_hascommands', 'nodocs_nocommand',
'nodocs_samename', 'nodocs_script', 'plug', 'reload', 'samename', 'nodocs_samename', 'nodocs_script', 'plug', 'reload', 'samename',
@ -555,19 +575,26 @@ function test.search_entries()
expect.table_eq(expected, h.search_entries({str='script', expect.table_eq(expected, h.search_entries({str='script',
entry_type='builtin'})) entry_type='builtin'}))
expected = {'inscript_docs', 'inscript_short_only','nodocs_script', expected = {'dev_script', 'inscript_docs', 'inscript_short_only',
'subdir/scriptname'} 'nodocs_script', 'subdir/scriptname'}
expect.table_eq(expected, h.search_entries({str='script'}, expect.table_eq(expected, h.search_entries({str='script'},
{entry_type='builtin'})) {entry_type='builtin'}))
expected = {'bindboxers', 'boxbinders'} expected = {'bindboxers', 'boxbinders'}
expect.table_eq(expected, h.search_entries({str='box'})) expect.table_eq(expected, h.search_entries({str='box'}))
expected = {'bindboxers', 'boxbinders', 'inscript_docs',
'inscript_short_only', 'nodocs_script', 'subdir/scriptname'}
expect.table_eq(expected, h.search_entries({{str='script'}, {str='box'}},
{{entry_type='builtin'},
{tag='dev'}}),
'multiple filters for include and exclude')
end end
function test.get_commands() function test.get_commands()
local expected = {'?', 'alias', 'basic', 'bindboxers', 'boxbinders', local expected = {'?', 'alias', 'basic', 'bindboxers', 'boxbinders',
'clear', 'cls', 'die', 'dir', 'disable', 'devel/dump-rpc', 'enable', 'clear', 'cls', 'dev_script', 'die', 'dir', 'disable', 'devel/dump-rpc',
'fpause', 'help', 'hide', 'inscript_docs', 'inscript_short_only', 'enable', 'fpause', 'help', 'hide', 'inscript_docs', 'inscript_short_only',
'keybinding', 'kill-lua', 'load', 'ls', 'man', 'nodoc_command', 'keybinding', 'kill-lua', 'load', 'ls', 'man', 'nodoc_command',
'nodocs_samename', 'nodocs_script', 'plug', 'reload', 'samename', 'nodocs_samename', 'nodocs_script', 'plug', 'reload', 'samename',
'script', 'subdir/scriptname', 'sc-script', 'show', 'tags', 'type', 'script', 'subdir/scriptname', 'sc-script', 'show', 'tags', 'type',
@ -598,21 +625,23 @@ function test.tags()
local mock_print = mock.func() local mock_print = mock.func()
mock.patch(h, 'print', mock_print, function() mock.patch(h, 'print', mock_print, function()
h.tags() h.tags()
expect.eq(7, mock_print.call_count) expect.eq(8, mock_print.call_count)
expect.eq('armok Tools that give you complete control over an aspect of the', expect.eq('armok Tools that give you complete control over an aspect of the',
mock_print.call_args[1][1]) mock_print.call_args[1][1])
expect.eq(' game or provide access to information that the game', expect.eq(' game or provide access to information that the game',
mock_print.call_args[2][1]) mock_print.call_args[2][1])
expect.eq(' intentionally keeps hidden.', expect.eq(' intentionally keeps hidden.',
mock_print.call_args[3][1]) mock_print.call_args[3][1])
expect.eq('fort Tools that are useful while in fort mode.', expect.eq('dev Dev tools.',
mock_print.call_args[4][1]) mock_print.call_args[4][1])
expect.eq('map Tools that interact with the game map.', expect.eq('fort Tools that are useful while in fort mode.',
mock_print.call_args[5][1]) mock_print.call_args[5][1])
expect.eq('nomembers Nothing is tagged with this.', expect.eq('map Tools that interact with the game map.',
mock_print.call_args[6][1]) mock_print.call_args[6][1])
expect.eq('units Tools that interact with units.', expect.eq('nomembers Nothing is tagged with this.',
mock_print.call_args[7][1]) mock_print.call_args[7][1])
expect.eq('units Tools that interact with units.',
mock_print.call_args[8][1])
end) end)
end end
@ -670,4 +699,29 @@ function test.ls()
expect.eq(1, mock_print.call_count) expect.eq(1, mock_print.call_count)
expect.eq('No matches.', mock_print.call_args[1][1]) expect.eq('No matches.', mock_print.call_args[1][1])
end) end)
-- test skipping tags and excluding strings
mock_print = mock.func()
mock.patch(h, 'print', mock_print, function()
h.ls('armok', true, false, 'boxer,binder')
expect.eq(1, mock_print.call_count)
expect.eq('samename Samename.', mock_print.call_args[1][1])
end)
-- test excluding dev scripts
mock_print = mock.func()
mock.patch(h, 'print', mock_print, function()
h.ls('_script', true, false, 'inscript,nodocs')
expect.eq(1, mock_print.call_count)
expect.eq('No matches.', mock_print.call_args[1][1])
end)
-- test including dev scripts
mock_print = mock.func()
mock.patch(h, 'print', mock_print, function()
h.ls('_script', true, true, 'inscript,nodocs')
expect.eq(1, mock_print.call_count)
expect.eq('dev_script Short desc.',
mock_print.call_args[1][1])
end)
end end