diff --git a/docs/Lua API.rst b/docs/Lua API.rst index 4aa8d78cb..d8a76dd28 100644 --- a/docs/Lua API.rst +++ b/docs/Lua API.rst @@ -3150,8 +3150,8 @@ Each entry has several properties associated with it: alphabetized by their last path component, with populated path components coming before null path components (e.g. ``autobutcher`` will immediately follow ``gui/autobutcher``). - The optional ``include`` and ``exclude`` filter params are maps with the - following elements: + The optional ``include`` and ``exclude`` filter params are maps (or lists of + maps) with the following elements: :str: if a string, filters by the given substring. if a table of strings, 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 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 ``exclude`` is ``nil`` or empty, then no entries are filtered out. diff --git a/docs/builtins/ls.rst b/docs/builtins/ls.rst index 7305a0256..cd6bc4126 100644 --- a/docs/builtins/ls.rst +++ b/docs/builtins/ls.rst @@ -40,3 +40,5 @@ Options Don't print out the tags associated with each command. ``--dev`` Include commands intended for developers and modders. +``--exclude [,...]`` + Exclude commands that match any of the given strings. diff --git a/docs/changelog.txt b/docs/changelog.txt index d00f4c246..fb11def8e 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -39,6 +39,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## Misc Improvements - `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 diff --git a/library/Core.cpp b/library/Core.cpp index 87f78f56c..73336b2e7 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -622,12 +622,18 @@ void ls_helper(color_ostream &con, const vector ¶ms) { vector filter; bool skip_tags = false; bool show_dev_commands = false; + string exclude_strs = ""; + bool in_exclude = false; for (auto str : params) { - if (str == "--notags") + if (in_exclude) + exclude_strs = str; + else if (str == "--notags") skip_tags = true; else if (str == "--dev") show_dev_commands = true; + else if (str == "--exclude") + in_exclude = true; else filter.push_back(str); } @@ -636,7 +642,7 @@ void ls_helper(color_ostream &con, const vector ¶ms) { auto L = Lua::Core::State; Lua::StackUnwinder top(L); - if (!lua_checkstack(L, 4) || + if (!lua_checkstack(L, 5) || !Lua::PushModulePublic(con, L, "helpdb", "ls")) { con.printerr("Failed to load helpdb Lua code\n"); return; @@ -645,8 +651,9 @@ void ls_helper(color_ostream &con, const vector ¶ms) { Lua::PushVector(L, filter); Lua::Push(L, skip_tags); 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"); } } diff --git a/library/lua/helpdb.lua b/library/lua/helpdb.lua index d07445399..5af41c929 100644 --- a/library/lua/helpdb.lua +++ b/library/lua/helpdb.lua @@ -14,6 +14,8 @@ local _ENV = mkmodule('helpdb') +local argparse = require('argparse') + local MAX_STALE_MS = 60000 -- paths @@ -588,6 +590,8 @@ function sort_by_basename(a, b) return false 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) if filter.tag then local matched = false @@ -630,9 +634,18 @@ local function matches(entry_name, filter) return true 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 -- populated -local function normalize_filter(f) +local function normalize_filter_map(f) if not f then return nil end local filter = {} filter.str = normalize_string_list(f.str) @@ -644,11 +657,21 @@ local function normalize_filter(f) return filter 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, -- with populated path components coming before null path components (e.g. -- autobutcher will immediately follow gui/autobutcher). --- the optional include and exclude filter params are maps with the following --- elements: +-- the optional include and exclude filter params are maps (or lists of maps) +-- with the following elements: -- str - if a string, filters by the given substring. if a table of strings, -- 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, @@ -658,14 +681,18 @@ end -- types are: "builtin", "plugin", "command". note that many plugin -- commands have the same name as the plugin, so those entries will -- 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) ensure_db() - include = normalize_filter(include) - exclude = normalize_filter(exclude) + include = normalize_filter_list(include) + exclude = normalize_filter_list(exclude) local entries = {} for entry in pairs(entrydb) do - if (not include or matches(entry, include)) and - (not exclude or not matches(entry, exclude)) then + if (not include or matches_any(entry, include)) and + (not exclude or not matches_any(entry, exclude)) then table.insert(entries, entry) end end @@ -743,21 +770,30 @@ end -- wraps the list_entries() API to provide a more convenient interface for Core -- to implement the 'ls' builtin command. --- filter_str - if a tag name, will filter by that tag. otherwise, will filter --- as a substring +-- filter_str - if a tag name (or a list of tag names), will filter by that +-- tag/those tags. otherwise, will filter as a substring/list of +-- substrings -- skip_tags - whether to skip printing tag info -- show_dev_commands - if true, will include scripts in the modtools/ and -- devel/ directories. otherwise those scripts will be -- 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}} if is_tag(filter_str) then include.tag = filter_str else include.str = filter_str end - list_entries(skip_tags, include, - show_dev_commands and {} or {tag='dev'}) + local excludes = {} + 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 local function list_tags() diff --git a/test/library/helpdb.lua b/test/library/helpdb.lua index efa9e93d5..1f1e58ba9 100644 --- a/test/library/helpdb.lua +++ b/test/library/helpdb.lua @@ -36,6 +36,7 @@ local mock_script_db = { inscript_docs=true, inscript_short_only=true, nodocs_script=true, + dev_script=true, } local files = { @@ -48,6 +49,8 @@ local files = { * units: Tools that interact with units. +* dev: Dev tools. + * nomembers: Nothing is tagged with this. ]], ['hack/docs/docs/tools/hascommands.txt']=[[ @@ -113,6 +116,20 @@ Command: "subdir/scriptname" Documented subdir/scriptname. 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']=[[ -- in-file short description for basic @@ -216,6 +233,9 @@ Command: "inscript_docs" Documented full help. ]====] +script contents + ]], + ['other/scriptpath/dev_script.lua']=[[ script contents ]], } @@ -495,7 +515,7 @@ function test.is_tag() end function test.get_tags() - expect.table_eq({'armok', 'fort', 'map', 'nomembers', 'units'}, + expect.table_eq({'armok', 'dev', 'fort', 'map', 'nomembers', 'units'}, h.get_tags()) end @@ -534,8 +554,8 @@ end function test.search_entries() -- all entries, in alphabetical order by last path component local expected = {'?', 'alias', 'basic', 'bindboxers', 'boxbinders', - 'clear', 'cls', 'die', 'dir', 'disable', 'devel/dump-rpc', 'enable', - 'fpause', 'hascommands', 'help', 'hide', 'inscript_docs', + 'clear', 'cls', 'dev_script', 'die', 'dir', 'disable', 'devel/dump-rpc', + 'enable', 'fpause', 'hascommands', 'help', 'hide', 'inscript_docs', 'inscript_short_only', 'keybinding', 'kill-lua', 'load', 'ls', 'man', 'nocommand', 'nodoc_command', 'nodocs_hascommands', 'nodocs_nocommand', 'nodocs_samename', 'nodocs_script', 'plug', 'reload', 'samename', @@ -555,19 +575,26 @@ function test.search_entries() expect.table_eq(expected, h.search_entries({str='script', entry_type='builtin'})) - expected = {'inscript_docs', 'inscript_short_only','nodocs_script', - 'subdir/scriptname'} + expected = {'dev_script', 'inscript_docs', 'inscript_short_only', + 'nodocs_script', 'subdir/scriptname'} expect.table_eq(expected, h.search_entries({str='script'}, {entry_type='builtin'})) expected = {'bindboxers', 'boxbinders'} 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 function test.get_commands() local expected = {'?', 'alias', 'basic', 'bindboxers', 'boxbinders', - 'clear', 'cls', 'die', 'dir', 'disable', 'devel/dump-rpc', 'enable', - 'fpause', 'help', 'hide', 'inscript_docs', 'inscript_short_only', + 'clear', 'cls', 'dev_script', 'die', 'dir', 'disable', 'devel/dump-rpc', + 'enable', 'fpause', 'help', 'hide', 'inscript_docs', 'inscript_short_only', 'keybinding', 'kill-lua', 'load', 'ls', 'man', 'nodoc_command', 'nodocs_samename', 'nodocs_script', 'plug', 'reload', 'samename', 'script', 'subdir/scriptname', 'sc-script', 'show', 'tags', 'type', @@ -598,21 +625,23 @@ function test.tags() local mock_print = mock.func() mock.patch(h, 'print', mock_print, function() 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', mock_print.call_args[1][1]) expect.eq(' game or provide access to information that the game', mock_print.call_args[2][1]) expect.eq(' intentionally keeps hidden.', 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]) - 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]) - expect.eq('nomembers Nothing is tagged with this.', + expect.eq('map Tools that interact with the game map.', 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]) + expect.eq('units Tools that interact with units.', + mock_print.call_args[8][1]) end) end @@ -670,4 +699,29 @@ function test.ls() expect.eq(1, mock_print.call_count) expect.eq('No matches.', mock_print.call_args[1][1]) 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