Support builtin commands in helpdb (#2241)

* support builtin commands in helpdb, implement list API, document api
develop
Myk 2022-07-08 14:43:41 -07:00 committed by myk002
parent 35a4d19ac9
commit 4ad8e7199a
No known key found for this signature in database
GPG Key ID: 8A39CA0FA0C16E78
4 changed files with 559 additions and 380 deletions

@ -0,0 +1,269 @@
.. _built-in-commands:
Built-in Commands
=================
The following commands are provided by the 'core' components of DFHack, rather
than plugins or scripts.
.. contents::
:local:
.. _alias:
alias
-----
The ``alias`` command allows configuring aliases to other DFHack commands.
Aliases are resolved immediately after built-in commands, which means that an
alias cannot override a built-in command, but can override a command implemented
by a plugin or script.
Usage:
:``alias list``: lists all configured aliases
:``alias add <name> <command> [arguments...]``: adds an alias
:``alias replace <name> <command> [arguments...]``: replaces an existing
alias with a new command, or adds the alias if it does not already exist
:``alias delete <name>``: removes the specified alias
Aliases can be given additional arguments when created and invoked, which will
be passed to the underlying command in order. An example with
`devel/print-args`::
[DFHack]# alias add pargs devel/print-args example
[DFHack]# pargs text
example
text
.. _cls:
cls
---
Clear the terminal. Does not delete command history.
.. _die:
die
---
Instantly kills DF without saving.
.. _disable:
.. _enable:
enable
------
Many plugins and scripts can be in a distinct enabled or disabled state. Some of
them activate and deactivate automatically depending on the contents of the
world raws. Others store their state in world data. However a number of them
have to be enabled globally, and the init file is the right place to do it.
Most such plugins or scripts support the built-in ``enable`` and ``disable``
commands. Calling them at any time without arguments prints a list of enabled
and disabled plugins, and shows whether that can be changed through the same
commands. Passing plugin names to these commands will enable or disable the
specified plugins. For example, to enable the `manipulator` plugin::
enable manipulator
It is also possible to enable or disable multiple plugins at once::
enable manipulator search
.. _fpause:
fpause
------
Forces DF to pause. This is useful when your FPS drops below 1 and you lose
control of the game.
.. _help:
help
----
Most commands support using the ``help <command>`` built-in command to retrieve
further help without having to look online. ``? <cmd>`` and ``man <cmd>`` are
aliases.
Some commands (including many scripts) instead take ``help`` or ``?`` as an
option on their command line - ie ``<cmd> help``.
.. _hide:
hide
----
Hides the DFHack terminal window. Only available on Windows.
.. _keybinding:
keybinding
----------
To set keybindings, use the built-in ``keybinding`` command. Like any other
command it can be used at any time from the console, but bindings are not
remembered between runs of the game unless re-created in `dfhack.init`.
Currently, any combinations of Ctrl/Alt/Shift with A-Z, 0-9, or F1-F12 are
supported.
Possible ways to call the command:
``keybinding list <key>``
List bindings active for the key combination.
``keybinding clear <key> <key>...``
Remove bindings for the specified keys.
``keybinding add <key> "cmdline" "cmdline"...``
Add bindings for the specified key.
``keybinding set <key> "cmdline" "cmdline"...``
Clear, and then add bindings for the specified key.
The ``<key>`` parameter above has the following *case-sensitive* syntax::
[Ctrl-][Alt-][Shift-]KEY[@context[|context...]]
where the *KEY* part can be any recognized key and [] denote optional parts.
When multiple commands are bound to the same key combination, DFHack selects
the first applicable one. Later ``add`` commands, and earlier entries within one
``add`` command have priority. Commands that are not specifically intended for
use as a hotkey are always considered applicable.
The ``context`` part in the key specifier above can be used to explicitly
restrict the UI state where the binding would be applicable. If called without
parameters, the ``keybinding`` command among other things prints the current
context string.
Only bindings with a ``context`` tag that either matches the current context
fully, or is a prefix ending at a ``/`` boundary would be considered for
execution, i.e. when in context ``foo/bar/baz``, keybindings restricted to any
of ``@foo/bar/baz``, ``@foo/bar``, ``@foo`` or none will be active.
Multiple contexts can be specified by separating them with a pipe (``|``) - for
example, ``@foo|bar|baz/foo`` would match anything under ``@foo``, ``@bar``, or
``@baz/foo``.
Interactive commands like `liquids` cannot be used as hotkeys.
.. _kill-lua:
kill-lua
--------
Stops any currently-running Lua scripts. By default, scripts can only be
interrupted every 256 instructions. Use ``kill-lua force`` to interrupt the next
instruction.
.. _load:
.. _unload:
.. _reload:
load
----
``load``, ``unload``, and ``reload`` control whether a plugin is loaded into
memory - note that plugins are loaded but disabled unless you explicitly enable
them. Usage::
load|unload|reload PLUGIN|(-a|--all)
Allows dealing with plugins individually by name, or all at once.
Note that plugins do not maintain their enabled state if they are reloaded, so
you may need to use `enable` to re-enable a plugin after reloading it.
.. _ls:
.. _dir:
ls
--
``ls`` (or ``dir``) does not list files like the Unix command, but rather
available commands. In order to group related commands, each command is
associated with a list of tags. You can filter the listed commands by a
tag or a substring of the command name. Usage:
:``ls``: Lists all available commands and the tags associated with them
(if any).
:``ls TAG``: Shows only commands that have the given tag. Use the `tags` command
to see the list of available tags.
:``ls STRING``: Shows commands that include the given string. E.g. ``ls auto``
will show all the commands with "auto" in their names. If the string is also
the name of a tag, then it will be interpreted as a tag name.
You can also pass some optional parameters to change how ``ls`` behaves:
:``--notags``: Don't print out the tags associated with each command.
:``--dev``: Include commands intended for developers and modders.
.. _plug:
plug
----
Lists available plugins and whether they are enabled.
``plug``
Lists available plugins (*not* commands implemented by plugins)
``plug [PLUGIN] [PLUGIN] ...``
List state and detailed description of the given plugins,
including commands implemented by the plugin.
.. _sc-script:
sc-script
---------
Allows additional scripts to be run when certain events occur (similar to
onLoad\*.init scripts)
.. _script:
script
------
Reads a text file, and runs each line as a DFHack command as if it had been
typed in by the user - treating the input like `an init file <init-files>`.
Some other tools, such as `autobutcher` and `workflow`, export their settings as
the commands to create them - which can later be reloaded with ``script``.
.. _show:
show
----
Shows the terminal window after it has been `hidden <hide>`. Only available on
Windows. You'll need to use it from a `keybinding` set beforehand, or the
in-game `command-prompt`.
.. _tags:
tags
----
List the strings that the DFHack tools can be tagged with. You can find groups
of related tools by passing the tag name to `ls`.
.. _type:
type
----
``type command`` shows where ``command`` is implemented.
Other Commands
--------------
The following commands are *not* built-in, but offer similarly useful functions.
* `command-prompt`
* `hotkeys`
* `lua`
* `multicmd`
* `nopause`
* `quicksave`
* `rb`
* `repeat`

@ -11,22 +11,23 @@ DFHack Core
Command Implementation Command Implementation
====================== ======================
DFHack commands can be implemented in three ways, all of which DFHack commands can be implemented in any of three ways:
are used in the same way:
:builtin: commands are implemented by the core of DFHack. They manage :builtin: commands are implemented by the core of DFHack. They manage
other DFHack tools, interpret commands, and control basic other DFHack tools, interpret commands, and control basic
aspects of DF (force pause or quit). aspects of DF (force pause or quit). They are documented
`here <built-in-commands>`.
:plugins: are stored in ``hack/plugins/`` and must be compiled with the :plugins: are stored in ``hack/plugins/`` and must be compiled with the
same version of DFHack. They are less flexible than scripts, same version of DFHack. They are less flexible than scripts,
but used for complex or ongoing tasks because they run faster. but used for complex or ongoing tasks because they run faster.
Plugins included with DFHack are documented `here <plugins-index>`.
:scripts: are Ruby or Lua scripts stored in ``hack/scripts/``. :scripts: are Ruby or Lua scripts stored in ``hack/scripts/``.
Because they don't need to be compiled, scripts are Because they don't need to be compiled, scripts are
more flexible about versions, and easier to distribute. more flexible about versions, and easier to distribute.
Most third-party DFHack addons are scripts. Most third-party DFHack addons are scripts. All scripts included
with DFHack are documented `here <scripts-index>`.
Using DFHack Commands Using DFHack Commands
===================== =====================
@ -113,259 +114,6 @@ second (Windows) example uses `kill-lua` to stop a Lua script.
you have multiple copies of DF running simultaneously. To assign a different you have multiple copies of DF running simultaneously. To assign a different
port, see `remote-server-config`. port, see `remote-server-config`.
Built-in Commands
=================
The following commands are provided by the 'core' components
of DFHack, rather than plugins or scripts.
.. contents::
:local:
.. _alias:
alias
-----
The ``alias`` command allows configuring aliases to other DFHack commands.
Aliases are resolved immediately after built-in commands, which means that an
alias cannot override a built-in command, but can override a command implemented
by a plugin or script.
Usage:
:``alias list``: lists all configured aliases
:``alias add <name> <command> [arguments...]``: adds an alias
:``alias replace <name> <command> [arguments...]``: replaces an existing
alias with a new command, or adds the alias if it does not already exist
:``alias delete <name>``: removes the specified alias
Aliases can be given additional arguments when created and invoked, which will
be passed to the underlying command in order. An example with `devel/print-args`::
[DFHack]# alias add pargs devel/print-args example
[DFHack]# pargs text
example
text
.. _cls:
cls
---
Clear the terminal. Does not delete command history.
.. _die:
die
---
Instantly kills DF without saving.
.. _disable:
.. _enable:
enable
------
Many plugins can be in a distinct enabled or disabled state. Some of
them activate and deactivate automatically depending on the contents
of the world raws. Others store their state in world data. However a
number of them have to be enabled globally, and the init file is the
right place to do it.
Most such plugins or scripts support the built-in ``enable`` and ``disable``
commands. Calling them at any time without arguments prints a list
of enabled and disabled plugins, and shows whether that can be changed
through the same commands. Passing plugin names to these commands will enable
or disable the specified plugins. For example, to enable the `manipulator`
plugin::
enable manipulator
It is also possible to enable or disable multiple plugins at once::
enable manipulator search
.. _fpause:
fpause
------
Forces DF to pause. This is useful when your FPS drops below 1 and you lose
control of the game.
.. _help:
help
----
Most commands support using the ``help <command>`` built-in command
to retrieve further help without having to look at this document.
``? <cmd>`` and ``man <cmd>`` are aliases.
Some commands (including many scripts) instead take ``help`` or ``?``
as an option on their command line - ie ``<cmd> help``.
.. _hide:
hide
----
Hides the DFHack terminal window. Only available on Windows.
.. _keybinding:
keybinding
----------
To set keybindings, use the built-in ``keybinding`` command. Like any other
command it can be used at any time from the console, but bindings are not
remembered between runs of the game unless re-created in `dfhack.init`.
Currently, any combinations of Ctrl/Alt/Shift with A-Z, 0-9, or F1-F12 are supported.
Possible ways to call the command:
``keybinding list <key>``
List bindings active for the key combination.
``keybinding clear <key> <key>...``
Remove bindings for the specified keys.
``keybinding add <key> "cmdline" "cmdline"...``
Add bindings for the specified key.
``keybinding set <key> "cmdline" "cmdline"...``
Clear, and then add bindings for the specified key.
The ``<key>`` parameter above has the following *case-sensitive* syntax::
[Ctrl-][Alt-][Shift-]KEY[@context[|context...]]
where the *KEY* part can be any recognized key and [] denote optional parts.
When multiple commands are bound to the same key combination, DFHack selects
the first applicable one. Later ``add`` commands, and earlier entries within one
``add`` command have priority. Commands that are not specifically intended for use
as a hotkey are always considered applicable.
The ``context`` part in the key specifier above can be used to explicitly restrict
the UI state where the binding would be applicable. If called without parameters,
the ``keybinding`` command among other things prints the current context string.
Only bindings with a ``context`` tag that either matches the current context fully,
or is a prefix ending at a ``/`` boundary would be considered for execution, i.e.
when in context ``foo/bar/baz``, keybindings restricted to any of ``@foo/bar/baz``,
``@foo/bar``, ``@foo`` or none will be active.
Multiple contexts can be specified by separating them with a
pipe (``|``) - for example, ``@foo|bar|baz/foo`` would match
anything under ``@foo``, ``@bar``, or ``@baz/foo``.
Interactive commands like `liquids` cannot be used as hotkeys.
.. _kill-lua:
kill-lua
--------
Stops any currently-running Lua scripts. By default, scripts can
only be interrupted every 256 instructions. Use ``kill-lua force``
to interrupt the next instruction.
.. _load:
.. _unload:
.. _reload:
load
----
``load``, ``unload``, and ``reload`` control whether a plugin is loaded
into memory - note that plugins are loaded but disabled unless you do
something. Usage::
load|unload|reload PLUGIN|(-a|--all)
Allows dealing with plugins individually by name, or all at once.
Note that plugins do not maintain their enabled state if they are reloaded, so
you may need to use `enable` to re-enable a plugin after reloading it.
.. _ls:
ls
--
``ls`` does not list files like the Unix command, but rather
available commands - first built in commands, then plugins,
and scripts at the end. Usage:
:ls -a: Also list scripts in subdirectories of ``hack/scripts/``,
which are generally not intended for direct use.
:ls <plugin>: List subcommands for the given plugin.
.. _plug:
plug
----
Lists available plugins, including their state and detailed description.
``plug``
Lists available plugins (*not* commands implemented by plugins)
``plug [PLUGIN] [PLUGIN] ...``
List state and detailed description of the given plugins,
including commands implemented by the plugin.
.. _sc-script:
sc-script
---------
Allows additional scripts to be run when certain events occur
(similar to onLoad*.init scripts)
.. _script:
script
------
Reads a text file, and runs each line as a DFHack command
as if it had been typed in by the user - treating the
input like `an init file <init-files>`.
Some other tools, such as `autobutcher` and `workflow`, export
their settings as the commands to create them - which are later
loaded with ``script``
.. _show:
show
----
Shows the terminal window after it has been `hidden <hide>`.
Only available on Windows. You'll need to use it from a
`keybinding` set beforehand, or the in-game `command-prompt`.
.. _type:
type
----
``type command`` shows where ``command`` is implemented.
Other Commands
--------------
The following commands are *not* built-in, but offer similarly useful functions.
* `command-prompt`
* `hotkeys`
* `lua`
* `multicmd`
* `nopause`
* `quicksave`
* `rb`
* `repeat`
.. _dfhack-config: .. _dfhack-config:
Configuration Files Configuration Files

@ -30,6 +30,7 @@ User Manual
/docs/Installing /docs/Installing
/docs/Support /docs/Support
/docs/Core /docs/Core
/docs/Builtin
/docs/Plugins /docs/Plugins
/docs/Scripts /docs/Scripts
/docs/Tags /docs/Tags

@ -1,80 +1,122 @@
-- The help text database. -- The help text database and query interface.
-- --
-- Command help is read from the the following sources: -- Help text is read from the rendered text in hack/docs/docs/. If no rendered
-- 1. rendered text in hack/docs/docs/ -- text exists, it is read from the script sources (for scripts) or the string
-- 2. (for scripts) the script sources if no pre-rendered text exists or if the -- passed to the PluginCommand initializer (for plugins).
-- script file has a modification time that is more recent than the
-- pre-rendered text
-- 3. (for plugins) the string passed to the PluginCommand initializer if no
-- pre-rendered text exists
-- --
-- For plugins that don't register any commands, the plugin name serves as the -- For plugins that don't register a command with the same name as the plugin,
-- command so documentation on what happens when you enable the plugin can be -- the plugin name is registered as a separate entry so documentation on what
-- found. -- happens when you enable the plugin can be found.
--
-- The database is lazy-loaded when an API method is called. It rechecks its
-- help sources for updates if an API method has not been called in the last
-- 60 seconds.
local _ENV = mkmodule('helpdb') local _ENV = mkmodule('helpdb')
-- paths
local RENDERED_PATH = 'hack/docs/docs/tools/' local RENDERED_PATH = 'hack/docs/docs/tools/'
local BUILTIN_HELP = 'hack/docs/docs/Builtin.txt'
local TAG_DEFINITIONS = 'hack/docs/docs/Tags.txt' local TAG_DEFINITIONS = 'hack/docs/docs/Tags.txt'
-- used when reading help text embedded in script sources
local SCRIPT_DOC_BEGIN = '[====[' local SCRIPT_DOC_BEGIN = '[====['
local SCRIPT_DOC_END = ']====]' local SCRIPT_DOC_END = ']====]'
local SCRIPT_DOC_BEGIN_RUBY = '=begin'
local SCRIPT_DOC_END_RUBY = '=end'
local ENTRY_TYPES = {
BUILTIN='builtin',
PLUGIN='plugin',
COMMAND='command'
}
local SOURCES = { local HELP_SOURCES = {
STUB='stub', STUB='stub',
RENDERED='rendered', RENDERED='rendered',
PLUGIN='plugin', PLUGIN='plugin',
SCRIPT='script', SCRIPT='script',
} }
-- command name -> {short_help, long_help, tags, source, source_timestamp} -- entry name -> {
-- also includes a script_source_path element if the source is a script -- entry_types (set of ENTRY_TYPES),
-- and a unrunnable boolean if the source is a plugin that does not provide any -- short_help (string),
-- commands to invoke directly. -- long_help (string),
-- tags (set),
-- help_source (element of HELP_SOURCES),
-- source_timestamp (mtime, 0 for non-files),
-- source_path (string, nil for non-files)
-- }
--
-- entry_types is a set because plugin commands can also be the plugin names.
db = db or {} db = db or {}
-- tag name -> list of command names -- tag name -> list of entry names
-- Tags defined in the TAG_DEFINITIONS file that have no associated db entries
-- will have an empty list.
tag_index = tag_index or {} tag_index = tag_index or {}
local function get_rendered_path(command) local function get_rendered_path(entry_name)
return RENDERED_PATH .. command .. '.txt' return RENDERED_PATH .. entry_name .. '.txt'
end end
local function has_rendered_help(command) local function has_rendered_help(entry_name)
return dfhack.filesystem.mtime(get_rendered_path(command)) ~= -1 return dfhack.filesystem.mtime(get_rendered_path(entry_name)) ~= -1
end end
local DEFAULT_HELP_TEMPLATE = [[ local DEFAULT_HELP_TEMPLATE = [[
%s %s
%s %s
Tags: None
No help available. No help available.
]] ]]
local function make_default_entry(entry_name, entry_types, source,
local function make_default_entry(command, source) source_timestamp, source_path)
local default_long_help = DEFAULT_HELP_TEMPLATE:format( local default_long_help = DEFAULT_HELP_TEMPLATE:format(
command, ('*'):rep(#command)) entry_name, ('*'):rep(#entry_name))
return {short_help='No help available.', long_help=default_long_help, return {
tags={}, source=source, source_timestamp=0} entry_types=entry_types,
short_help='No help available.',
long_help=default_long_help,
tags={},
help_source=source,
source_timestamp=source_timestamp or 0,
source_path=source_path}
end end
-- updates the short_text, the long_text, and the tags in the given entry -- updates the short_text, the long_text, and the tags in the given entry based
-- on the text returned from the iterator.
-- if defined, opts can have the following fields:
-- begin_marker (string that marks the beginning of the help text; all text
-- before this marker is ignored)
-- end_marker (string that marks the end of the help text; text will stop
-- being parsed after this marker is seen)
-- no_header (don't try to find the entity name at the top of the help text)
-- first_line_is_short_help (read the short help text from the first commented
-- line of the script instead of using the first
-- sentence of the long help text)
local function update_entry(entry, iterator, opts) local function update_entry(entry, iterator, opts)
opts = opts or {} opts = opts or {}
local lines = {} local lines = {}
local first_line_is_short_help = opts.first_line_is_short_help
local begin_marker_found,header_found = not opts.begin_marker,opts.no_header local begin_marker_found,header_found = not opts.begin_marker,opts.no_header
local tags_found, short_help_found, in_short_help = false, false, false local tags_found, short_help_found, in_short_help = false, false, false
for line in iterator do for line in iterator do
if not short_help_found and opts.first_line_is_short_help then if not short_help_found and first_line_is_short_help then
local _,_,text = line:trim():find('^%-%-%s*(.*)') line = line:trim()
if not text:endswith('.') then local _,_,text = line:find('^%-%-%s*(.*)') or line:find('^#%s*(.*)')
text = text .. '.' if not text then
-- if no first-line short help found, fall back to getting the
-- first sentence of the help text.
first_line_is_short_help = false
else
if not text:endswith('.') then
text = text .. '.'
end
entry.short_help = text
short_help_found = true
goto continue
end end
entry.short_help = text
short_help_found = true
goto continue
end end
if not begin_marker_found then if not begin_marker_found then
local _, endpos = line:find(opts.begin_marker, 1, true) local _, endpos = line:find(opts.begin_marker, 1, true)
@ -119,80 +161,131 @@ local function update_entry(entry, iterator, opts)
entry.long_help = table.concat(lines, '\n') entry.long_help = table.concat(lines, '\n')
end end
local function make_rendered_entry(old_entry, command) -- create db entry based on parsing sphinx-rendered help text
local rendered_path = get_rendered_path(command) local function make_rendered_entry(old_entry, entry_name, entry_types)
local rendered_path = get_rendered_path(entry_name)
local source_timestamp = dfhack.filesystem.mtime(rendered_path) local source_timestamp = dfhack.filesystem.mtime(rendered_path)
if old_entry and old_entry.source == SOURCES.RENDERED and if old_entry and old_entry.source == HELP_SOURCES.RENDERED and
old_entry.source_timestamp >= source_timestamp then old_entry.source_timestamp >= source_timestamp then
-- we already have the latest info -- we already have the latest info
return old_entry return old_entry
end end
local entry = make_default_entry(command, SOURCES.RENDERED) local entry = make_default_entry(entry_name, entry_types,
HELP_SOURCES.RENDERED, source_timestamp, rendered_path)
update_entry(entry, io.lines(rendered_path)) update_entry(entry, io.lines(rendered_path))
entry.source_timestamp = source_timestamp
return entry return entry
end end
local function make_plugin_entry(old_entry, command) -- create db entry based on the help text in the plugin source (used by
if old_entry and old_entry.source == SOURCES.PLUGIN then -- out-of-tree plugins)
local function make_plugin_entry(old_entry, entry_name, entry_types)
if old_entry and old_entry.source == HELP_SOURCES.PLUGIN then
-- we can't tell when a plugin is reloaded, so we can either choose to -- we can't tell when a plugin is reloaded, so we can either choose to
-- always refresh or never refresh. let's go with never for now for -- always refresh or never refresh. let's go with never for now for
-- performance. -- performance.
return old_entry return old_entry
end end
local entry = make_default_entry(command, SOURCES.PLUGIN) local entry = make_default_entry(entry_name, entry_types,
local long_help = dfhack.internal.getCommandHelp(command) HELP_SOURCES.PLUGIN)
local long_help = dfhack.internal.getCommandHelp(entry_name)
if long_help and #long_help:trim() > 0 then if long_help and #long_help:trim() > 0 then
update_entry(entry, long_help:trim():gmatch('[^\n]*'), {no_header=true}) update_entry(entry, long_help:trim():gmatch('[^\n]*'), {no_header=true})
end end
return entry return entry
end end
local function make_script_entry(old_entry, command, script_source_path) -- create db entry based on the help text in the script source (used by
-- out-of-tree scripts)
local function make_script_entry(old_entry, entry_name, script_source_path)
local source_timestamp = dfhack.filesystem.mtime(script_source_path) local source_timestamp = dfhack.filesystem.mtime(script_source_path)
if old_entry and old_entry.source == SOURCES.SCRIPT and if old_entry and old_entry.source == HELP_SOURCES.SCRIPT and
old_entry.script_source_path == script_source_path and old_entry.script_source_path == script_source_path and
old_entry.source_timestamp >= source_timestamp then old_entry.source_timestamp >= source_timestamp then
-- we already have the latest info -- we already have the latest info
return old_entry return old_entry
end end
local entry = make_default_entry(command, SOURCES.SCRIPT) local entry = make_default_entry(entry_name, {[ENTRY_TYPES.COMMAND]=true},
HELP_SOURCES.SCRIPT, source_timestamp, script_source_path)
local is_rb = script_source_path:endswith('.rb')
update_entry(entry, io.lines(script_source_path), update_entry(entry, io.lines(script_source_path),
{begin_marker=SCRIPT_DOC_BEGIN, end_marker=SCRIPT_DOC_END, {begin_marker=(is_rb and SCRIPT_DOC_BEGIN_RUBY or SCRIPT_DOC_BEGIN),
first_line_is_short_help=true}) end_marker=(is_rb and SCRIPT_DOC_BEGIN_RUBY or SCRIPT_DOC_END),
entry.source_timestamp = source_timestamp first_line_is_short_help=true})
return entry return entry
end end
local function update_db(old_db, db, source, command, flags) -- updates the db (and associated tag index) with a new entry if the entry_name
if db[command] then -- doesn't already exist in the db.
local function update_db(old_db, db, source, entry_name, kwargs)
if db[entry_name] then
-- already in db (e.g. from a higher-priority script dir); skip -- already in db (e.g. from a higher-priority script dir); skip
return return
end end
local entry, old_entry = nil, old_db[command] local entry, old_entry = nil, old_db[entry_name]
if source == SOURCES.RENDERED then if source == HELP_SOURCES.RENDERED then
entry = make_rendered_entry(old_entry, command) entry = make_rendered_entry(old_entry, entry_name, kwargs.entry_types)
elseif source == SOURCES.PLUGIN then elseif source == HELP_SOURCES.PLUGIN then
entry = make_plugin_entry(old_entry, command) entry = make_plugin_entry(old_entry, entry_name, kwargs.entry_types)
elseif source == SOURCES.SCRIPT then elseif source == HELP_SOURCES.SCRIPT then
entry = make_script_entry(old_entry, command, flags.script_source) entry = make_script_entry(old_entry, entry_name, kwargs.script_source)
elseif source == SOURCES.STUB then elseif source == HELP_SOURCES.STUB then
entry = make_default_entry(command, SOURCES.STUB) entry = make_default_entry(entry_name, kwargs.entry_types,
HELP_SOURCES.STUB)
else else
error('unhandled help source: ' .. source) error('unhandled help source: ' .. source)
end end
db[entry_name] = entry
entry.unrunnable = (flags or {}).unrunnable
db[command] = entry
for _,tag in ipairs(entry.tags) do for _,tag in ipairs(entry.tags) do
-- unknown tags are ignored -- ignore unknown tags
if tag_index[tag] then if tag_index[tag] then
table.insert(tag_index[tag], command) table.insert(tag_index[tag], entry_name)
end end
end end
end end
local BUILTINS = {
alias='Configure helper aliases for other DFHack commands.',
cls='Clear the console screen.',
clear='Clear the console screen.',
die='Force DF to close immediately, without saving.',
enable='Enable a plugin or persistent script.',
disable='Disable a plugin or persistent script.',
fpause='Force DF to pause.',
help='Usage help for the given plugin, command, or script.',
hide='Hide the terminal window (Windows only).',
keybinding='Modify bindings of commands to in-game key shortcuts.',
['kill-lua']='Stop a misbehaving Lua script.',
['load']='Load and register a plugin library.',
unload='Unregister and unload a plugin.',
reload='Unload and reload a plugin library.',
ls='List commands, optionally filtered by a tag or substring.',
dir='List commands, optionally filtered by a tag or substring.',
plug='List plugins and whether they are enabled.',
['sc-script']='Automatically run specified scripts on state change events.',
script='Run commands specified in a file.',
show='Show a hidden terminal window (Windows only).',
tags='List the tags that the DFHack tools are grouped by.',
['type']='Discover how a command is implemented.',
}
-- add the builtin commands to the db
local function scan_builtins(old_db, db)
local entry = make_default_entry('builtin',
{[ENTRY_TYPES.BUILTIN]=true, [ENTRY_TYPES.COMMAND]=true},
HELP_SOURCES.RENDERED, 0, BUILTIN_HELP)
-- read in builtin help
local f = io.open(BUILTIN_HELP)
if f then
entry.long_help = f:read('*all')
end
for b,short_help in pairs(BUILTINS) do
local builtin_entry = copyall(entry)
builtin_entry.short_help = short_help
db[b] = builtin_entry
end
end
-- scan for plugins and plugin-provided commands and add their help to the db
local function scan_plugins(old_db, db) local function scan_plugins(old_db, db)
local plugin_names = dfhack.internal.listPlugins() local plugin_names = dfhack.internal.listPlugins()
for _,plugin in ipairs(plugin_names) do for _,plugin in ipairs(plugin_names) do
@ -202,36 +295,41 @@ local function scan_plugins(old_db, db)
-- documentation to -- documentation to
update_db(old_db, db, update_db(old_db, db,
has_rendered_help(plugin) and has_rendered_help(plugin) and
SOURCES.RENDERED or SOURCES.STUB, HELP_SOURCES.RENDERED or HELP_SOURCES.STUB,
plugin, {unrunnable=true}) plugin, {entry_types={[ENTRY_TYPES.PLUGIN]=true}})
goto continue goto continue
end end
for _,command in ipairs(commands) do for _,command in ipairs(commands) do
local entry_types = {[ENTRY_TYPES.COMMAND]=true}
if command == plugin then
entry_types[ENTRY_TYPES.PLUGIN]=true
end
update_db(old_db, db, update_db(old_db, db,
has_rendered_help(command) and has_rendered_help(command) and
SOURCES.RENDERED or SOURCES.PLUGIN, HELP_SOURCES.RENDERED or HELP_SOURCES.PLUGIN,
command) command, {entry_types=entry_types})
end end
::continue:: ::continue::
end end
end end
-- scan for scripts and add their help to the db
local function scan_scripts(old_db, db) local function scan_scripts(old_db, db)
for _,script_path in ipairs(dfhack.internal.getScriptPaths()) do for _,script_path in ipairs(dfhack.internal.getScriptPaths()) do
local files = dfhack.filesystem.listdir_recursive( local files = dfhack.filesystem.listdir_recursive(
script_path, nil, false) script_path, nil, false)
if not files then goto skip_path end if not files then goto skip_path end
for _,f in ipairs(files) do for _,f in ipairs(files) do
if f.isdir or not f.path:endswith('.lua') or if f.isdir or
(not f.path:endswith('.lua') and not f.path:endswith('.rb')) or
f.path:startswith('test/') or f.path:startswith('test/') or
f.path:startswith('internal/') then f.path:startswith('internal/') then
goto continue goto continue
end end
local script_source = script_path .. '/' .. f.path local script_source = script_path .. '/' .. f.path
local script_is_newer = dfhack.filesystem.mtime(script_source) >
dfhack.filesystem.mtime(get_rendered_path(f.path))
update_db(old_db, db, update_db(old_db, db,
script_is_newer and SOURCES.SCRIPT or SOURCES.RENDERED, has_rendered_help(f.path) and
HELP_SOURCES.RENDERED or HELP_SOURCES.SCRIPT,
f.path:sub(1, #f.path - 4), {script_source=script_source}) f.path:sub(1, #f.path - 4), {script_source=script_source})
::continue:: ::continue::
end end
@ -239,6 +337,8 @@ local function scan_scripts(old_db, db)
end end
end end
-- read tags and descriptions from the TAG_DEFINITIONS file and add them all
-- to tag_index, initizlizing each entry with an empty list.
local function initialize_tags() local function initialize_tags()
local tag, desc, in_desc = nil, nil, false local tag, desc, in_desc = nil, nil, false
for line in io.lines(TAG_DEFINITIONS) do for line in io.lines(TAG_DEFINITIONS) do
@ -261,7 +361,7 @@ local function initialize_tags()
end end
-- ensures the db is up to date by scanning all help sources. does not do -- ensures the db is up to date by scanning all help sources. does not do
-- anything if it has already been run within the last 10 seconds. -- anything if it has already been run within the last 60 seconds.
last_refresh_ms = last_refresh_ms or 0 last_refresh_ms = last_refresh_ms or 0
local function ensure_db() local function ensure_db()
local now_ms = dfhack.getTickCount() local now_ms = dfhack.getTickCount()
@ -272,16 +372,21 @@ local function ensure_db()
db, tag_index = {}, {} db, tag_index = {}, {}
initialize_tags() initialize_tags()
scan_builtins(old_db, db)
scan_plugins(old_db, db) scan_plugins(old_db, db)
scan_scripts(old_db, db) scan_scripts(old_db, db)
end end
local function get_db_property(command, property) ---------------------------------------------------------------------------
-- get API
---------------------------------------------------------------------------
local function get_db_property(entry_name, property)
ensure_db() ensure_db()
if not db[command] then if not db[entry_name] then
error(('command not found: "%s"'):format(command)) error(('entry not found: "%s"'):format(entry_name))
end end
return db[command][property] return db[entry_name][property]
end end
-- returns the ~54 char summary blurb associated with the entry -- returns the ~54 char summary blurb associated with the entry
@ -308,6 +413,32 @@ function get_entry_tags(entry)
return set_to_sorted_list(get_db_property(entry, 'tags')) return set_to_sorted_list(get_db_property(entry, 'tags'))
end end
-- returns whether the given string matches a tag name
function is_tag(str)
ensure_db()
return not not tag_index[str]
end
-- returns the defined tags in alphabetical order
function get_tags()
ensure_db()
return set_to_sorted_list(tag_index)
end
-- returns the description associated with the given tag
function get_tag_description(tag)
ensure_db()
if not tag_index[tag] then
error('invalid tag: ' .. tag)
end
return tag_index[tag].description
end
---------------------------------------------------------------------------
-- search API
---------------------------------------------------------------------------
-- returns a list of path elements in reverse order
local function chunk_for_sorting(str) local function chunk_for_sorting(str)
local parts = str:split('/') local parts = str:split('/')
local chunks = {} local chunks = {}
@ -336,11 +467,8 @@ local function sort_by_basename(a, b)
return false return false
end end
local function matches(command, filter) local function matches(entry_name, filter)
local db_entry = db[command] local db_entry = db[entry_name]
if filter.runnable and db_entry.unrunnable then
return false
end
if filter.tag then if filter.tag then
local matched = false local matched = false
for _,tag in ipairs(filter.tag) do for _,tag in ipairs(filter.tag) do
@ -353,10 +481,22 @@ local function matches(command, filter)
return false return false
end end
end end
if filter.types then
local matched = false
for _,etype in ipairs(filter.types) do
if db_entry.entry_types[etype] then
matched = true
break
end
end
if not matched then
return false
end
end
if filter.str then if filter.str then
local matched = false local matched = false
for _,str in ipairs(filter.str) do for _,str in ipairs(filter.str) do
if command:find(str, 1, true) then if entry_name:find(str, 1, true) then
matched = true matched = true
break break
end end
@ -368,6 +508,7 @@ local function matches(command, filter)
return true return true
end end
-- converts strings into single-element lists containing that string
local function normalize_string_list(l) local function normalize_string_list(l)
if not l then return nil end if not l then return nil end
if type(l) == 'string' then if type(l) == 'string' then
@ -376,28 +517,35 @@ local function normalize_string_list(l)
return l return l
end 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(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)
filter.tag = normalize_string_list(f.tag) filter.tag = normalize_string_list(f.tag)
filter.runnable = f.runnable filter.types = normalize_string_list(f.types)
if not filter.str and not filter.tag and not filter.runnable then if not filter.str and not filter.tag and not filter.types then
return nil return nil
end end
return filter return filter
end end
-- returns a list of identifiers, alphabetized by their last path component -- returns a list of entry names, alphabetized by their last path component,
-- (e.g. gui/autobutcher will immediately follow autobutcher). -- 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 -- the optional include and exclude filter params are maps with the following
-- elements: -- 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 commands 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,
-- includes commands that match any of the given tags. -- includes entries that match any of the given tags.
-- runnable - if true, matches only runnable commands, not plugin names. -- types - if a string, matches entries of the given type. if a table of
function get_entries(include, exclude) -- strings, includes entries that match any of the given types. valid
-- 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.
function search_entries(include, exclude)
ensure_db() ensure_db()
include = normalize_filter(include) include = normalize_filter(include)
exclude = normalize_filter(exclude) exclude = normalize_filter(exclude)
@ -412,6 +560,10 @@ function get_entries(include, exclude)
return commands return commands
end end
---------------------------------------------------------------------------
-- list API (outputs to console)
---------------------------------------------------------------------------
local function get_max_width(list, min_width) local function get_max_width(list, min_width)
local width = min_width or 0 local width = min_width or 0
for _,item in ipairs(list) do for _,item in ipairs(list) do
@ -420,43 +572,52 @@ local function get_max_width(list, min_width)
return width return width
end end
-- prints the defined tags and their descriptions to the console
function list_tags()
local tags = get_tags()
local width = get_max_width(tags, 10)
for _,tag in ipairs(tags) do
print((' %-'..width..'s %s'):format(tag, get_tag_description(tag)))
end
end
-- prints the requested entries to the console. include and exclude filters are -- prints the requested entries to the console. include and exclude filters are
-- as in get_entries above. -- defined as in search_entries() above.
function list_entries(include_tags, include, exclude) function list_entries(skip_tags, include, exclude)
local entries = get_entries(include, exclude) local entries = search_entries(include, exclude)
local width = get_max_width(entries, 10) local width = get_max_width(entries, 10)
for _,entry in ipairs(entries) do for _,entry in ipairs(entries) do
print((' %-'..width..'s %s'):format( print((' %-'..width..'s %s'):format(
entry, get_entry_short_help(entry))) entry, get_entry_short_help(entry)))
if include_tags then if not skip_tags then
print((' '..(' '):rep(width)..' tags(%s)'):format( local tags = get_entry_tags(entry)
table.concat(get_entry_tags(entry), ','))) if #tags > 0 then
print((' tags: %s'):format(table.concat(tags, ', ')))
end
end end
end end
end if #entries == 0 then
print('no entries found.')
-- returns the defined tags in alphabetical order
function get_tags()
ensure_db()
return set_to_sorted_list(tag_index)
end
-- returns the description associated with the given tag
function get_tag_description(tag)
ensure_db()
if not tag_index[tag] then
error('invalid tag: ' .. tag)
end end
return tag_index[tag].description
end end
-- prints the defined tags and their descriptions to the console -- wraps the list_entries() API to provide a more convenient interface for Core
function list_tags() -- to implement the 'ls' builtin command.
local tags = get_tags() -- filter_str - if a tag name, will filter by that tag. otherwise, will filter
local width = get_max_width(tags, 10) -- as a substring
for _,tag in ipairs(tags) do -- skip_tags - whether to skip printing tag info
print((' %-'..width..'s %s'):format(tag, get_tag_description(tag))) -- 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)
local include = {types={ENTRY_TYPES.COMMAND}}
if is_tag(filter_str) then
include.tag = filter_str
else
include.str = filter_str
end end
list_entries(skip_tags, include,
show_dev_commands and {} or {str={'modtools/', 'devel/'}})
end end
return _ENV return _ENV