Create a Shell Plugin
Shell plugins extend the mariadb-shell shell with new global objects and functions that
are available from the interactive shell, scripts, and the command-line interface
(CLI). This skill scaffolds a plugin that follows the conventions enforced by the
plugin registrar, so the generated code loads and documents itself correctly on the
first try.
How plugins are loaded (the contract you must satisfy)
The loader is Mysql_shell::get_plugins in src/mysqlsh/mysql_shell.cc.
- Each plugin lives in its own folder under a
plugins/search path. - The folder must contain exactly one entry point:
init.py(Python) orinit.js(JavaScript). Having both makes the loader skip the plugin with a warning. - Search paths (see
Mysql_shell::get_pluginsat src/mysqlsh/mysql_shell.cc:998):<library_folder>/plugins— built-in/bundled plugins (skipped if--disable-builtin-plugins).${MARIADB_SHELL_USER_CONFIG_HOME}/plugins— per-user (default~/.mariadb-shell/pluginson Linux/macOS,%AppData%\MariaDB\mariadb-shell\pluginson Windows). Skipped if--disable-plugins. If theplugins_pathshell option is set, it replaces this user path with the listed folders (:-separated,;on Windows).
- Loading is recursive: a folder without an init file may contain sub-folders that are themselves plugins.
- The plugin folder name is importable as a Python package, so helper modules go
beside
init.pyand are imported asfrom <folder_name>.<module> import ...(see python/plugins/debug/ for the pattern).
Decide where to write before scaffolding:
- Bundled / repo-tracked plugin →
python/plugins/<name>/init.pyin this repo. - Personal / user plugin →
${MARIADB_SHELL_USER_CONFIG_HOME:-~/.mariadb-shell}/plugins/<name>/init.py.
If the user has not said which, ask. Default to the user config path for one-off
plugins, the repo python/plugins/ for something meant to ship.
Always prefer the decorators
Do not hand-call shell.create_extension_object() /
shell.add_extension_object_member() / shell.register_global(). The decorators in
python/packages/mysqlsh/plugin_manager/registrar.py
wrap those low-level APIs, derive the help from docstrings, register the function for
JS/Python/CLI/Web, and wrap exceptions cleanly. Use them.
from mysqlsh.plugin_manager import plugin, plugin_function
@plugin — register a global (or nested) object
- The class name becomes the object name (
myCloud→myCloudglobal object). - The class docstring becomes its help (first paragraph = brief, rest = details).
- Inner classes are registered as nested objects automatically.
@plugin(parent="util")attaches the object as a child of an existing object (e.g. extend the built-inutil). The parent must already exist.@plugin(shell_version_min="9.0.0", shell_version_max="9.9.0")gates loading by shell version; out-of-range plugins are rejected with a clear message.
@plugin_function — register a function on an object
@plugin_function("myCloud.create.instance", cli=True)
def create_instance(name, region="us-east-1", **options):
...
- The first argument is the fully-qualified name in
camelCase:object[.child].functionName. The shell auto-exposes it asfunctionNamein JS andfunction_name(snake_case) in Python — define the Python function in snake_case; the camelCase FQN drives the JS/CLI name. - The object referenced by the FQN must exist (declared via
@pluginor a parent chain). A function can also attach to a nested object declared as an inner class. - Flags control availability:
shell=True(default) — available in the interactive shell and scripts.cli=True— also exposed as a CLI command (mariadb-shell -- mycloud create instance ...).cli=Truerequiresshell=True.web=True— registered for web/MRS clients. Can beweb=False/True.
- The decorator wraps the call so tracebacks are logged at
debuglevel instead of leaking to the user; raisemysqlsh.Error("message")for user-facing errors.
sql_handler — intercept SQL statements (advanced)
For plugins that handle custom SQL, use from mysqlsh.plugin_manager import sql_handler with @sql_handler(name, prefixes); the callback is
callback(session, sql) -> Optional[Result]. Only reach for this when the user
specifically wants to intercept SQL, not for ordinary commands.
The docstring grammar (this is parsed — get it right)
FunctionData._parse_docs in registrar.py parses docstrings into shell help and
validates them. Registration fails loudly if docs are wrong, so follow this
exactly:
- Brief: the first line(s) up to the first blank line. Required.
- Details: free paragraphs after the brief. Optional. Arbitrary
Section:headers (a line ending in:) render as bold sections. - Every parameter must be documented, and only real parameters may be documented — a missing or extra entry raises an exception at load time.
Args:section, one entry per parameter:
Types map to shell types:Args: name (type): One-line brief, continued indented lines allowed.str→string,int→integer,bool→boolean,dict→dictionary,list→array,objectfor session/handles, or omit the(type)for an untyped/any parameter.- A parameter with a default value is automatically optional — do not say "optional"
in the type. Mark a required dict option with
(str,required). - For
**options(or anydictparameter), document the keys in a companion section:**options→Keyword Args:section.- a named dict parameter
cfg→Allowed options for cfg:section. Nested dict options get their ownAllowed options for <optName>:section.
- Bullet lists use sphinx syntax: lines starting with
*become@liitems.
A correct multi-section example lives in unittest/scripts/auto/py_shell/scripts/plugin_decorator_norecord.py — consult it for edge cases (required dict params, nested options, web-only functions).
Accessing shell facilities from a plugin
from mysqlsh import globals # globals.shell, globals.session, globals.db
from mysqlsh import Error, DBError, mysql
globals.shell— the shell global object (parse_uri,open_session,get_session,log,create_extension_object, ...).globals.session— the currently active session (may beNone).- Raise
mysqlsh.Error(...)for clean user errors; the decorator suppresses the traceback and logs it at debug level.
Workflow when asked to create a plugin
- Clarify the shape if not already clear: object name(s) and hierarchy, the
functions and their parameters, whether it extends an existing object (
parent=), whether CLI/web exposure is wanted, and Python vs JavaScript (default Python — that is where the decorators live and what the user asked for). - Pick the target path (repo
python/plugins/<name>/vs user configplugins/<name>/). Ask if ambiguous. - Write
init.pyusing the template below. One@pluginclass per object,@plugin_functionper command, fully documented docstrings. - Split helpers into sibling modules (
<name>/<helper>.py) imported asfrom <name>.<helper> import ...when the logic is non-trivial; keepinit.pyfocused on registration. - Mirror the project license header at the top of every file — copy the GPLv2 header used by existing plugins in python/plugins/debug/init.py.
- Tell the user how to try it: it loads on the next
mariadb-shellstart; verify with\? <object>(help), call it from Python, ormariadb-shell -- <object> <function> ...for CLI. If running from the repo build, plugins underpython/plugins/are bundled into the build's share path.
Template (Python)
# <GPLv2 license header — copy from python/plugins/debug/init.py>
from mysqlsh.plugin_manager import plugin, plugin_function
from mysqlsh import globals, Error
@plugin
class myPlugin:
"""Short brief shown in `\\? myPlugin`.
Longer details paragraph describing what the plugin does. Supports
bullet lists:
* does this
* and that
"""
class sub:
"""A nested object: myPlugin.sub."""
pass
@plugin_function("myPlugin.doThing", cli=True)
def do_thing(target, retries=3, **options):
"""One-line brief for the command.
Optional longer description of the behavior.
Args:
target (str): What to act on.
retries (int): Number of attempts. Defaults to 3 (optional).
**options (dict): Optional arguments.
Keyword Args:
force (bool): Skip confirmation. Default false.
label (str): Human-readable label.
"""
session = globals.shell.get_session()
if session is None:
raise Error("This command requires an active session.")
# ... implementation ...
JavaScript option
If the user wants JS, create init.js (never alongside an init.py). There are no
decorators in JS — register explicitly via the shell API:
shell.createExtensionObject(), shell.addExtensionObjectMember(obj, name, fn, {...help...}),
and shell.registerGlobal(name, obj, {...help...}). See
unittest/scripts/auto/js_shell/scripts/extensible_objects_norecord.js
for the member/help dictionary shape. Prefer Python unless the user needs JS.