CLI: support defining custom commands via C++ & Python API

Add support for add-ons to define commands using the new argument
`-c` or `--command`.

Commands behave as follows:

- Passing in a command enables background mode without the need to pass
  in `--background`.
- All arguments following the command are passed to the command
  (without the need to use the `--` argument).
- Add-ons can define their own commands via
  `bpy.utils.register_cli_command` (see examples in API docs).
- Passing in `--command help` lists all available commands.

Ref !119115
This commit is contained in:
Campbell Barton 2024-03-08 11:07:41 +11:00
parent 5214e6f35d
commit 9372e0dfe0
15 changed files with 776 additions and 1 deletions

View File

@ -0,0 +1,74 @@
"""
Using Python Argument Parsing
-----------------------------
This example shows how the Python ``argparse`` module can be used with a custom command.
Using ``argparse`` is generally recommended as it has many useful utilities and
generates a ``--help`` message for your command.
"""
import os
import sys
import bpy
def argparse_create():
import argparse
parser = argparse.ArgumentParser(
prog=os.path.basename(sys.argv[0]) + " --command keyconfig_export",
description="Write key-configuration to a file.",
)
parser.add_argument(
"-o", "--output",
dest="output",
metavar='OUTPUT',
type=str,
help="The path to write the keymap to.",
required=True,
)
parser.add_argument(
"-a", "--all",
dest="all",
action="store_true",
help="Write all key-maps (not only customized key-maps).",
required=False,
)
return parser
def keyconfig_export(argv):
parser = argparse_create()
args = parser.parse_args(argv)
# Ensure the key configuration is loaded in background mode.
bpy.utils.keyconfig_init()
bpy.ops.preferences.keyconfig_export(
filepath=args.output,
all=args.all,
)
return 0
cli_commands = []
def register():
cli_commands.append(bpy.utils.register_cli_command("keyconfig_export", keyconfig_export))
def unregister():
for cmd in cli_commands:
bpy.utils.unregister_cli_command(cmd)
cli_commands.clear()
if __name__ == "__main__":
register()

View File

@ -0,0 +1,43 @@
"""
Custom Commands
---------------
Registering commands makes it possible to conveniently expose command line
functionality via commands passed to (``-c`` / ``--command``).
"""
import sys
import os
def sysinfo_command(argv):
import tempfile
import sys_info
if argv and argv[0] == "--help":
print("Print system information & exit!")
return 0
with tempfile.TemporaryDirectory() as tempdir:
filepath = os.path.join(tempdir, "system_info.txt")
sys_info.write_sysinfo(filepath)
with open(filepath, "r", encoding="utf-8") as fh:
sys.stdout.write(fh.read())
return 0
cli_commands = []
def register():
cli_commands.append(bpy.utils.register_cli_command("sysinfo", sysinfo_command))
def unregister():
for cmd in cli_commands:
bpy.utils.unregister_cli_command(cmd)
cli_commands.clear()
if __name__ == "__main__":
register()

View File

@ -21,6 +21,8 @@ __all__ = (
"refresh_script_paths",
"app_template_paths",
"register_class",
"register_cli_command",
"unregister_cli_command",
"register_manual_map",
"unregister_manual_map",
"register_classes_factory",
@ -49,9 +51,11 @@ from _bpy import (
flip_name,
unescape_identifier,
register_class,
register_cli_command,
resource_path,
script_paths as _bpy_script_paths,
unregister_class,
unregister_cli_command,
user_resource as _user_resource,
system_resource,
)

View File

@ -12,6 +12,7 @@
struct Main;
struct UserDef;
struct bContext;
/**
* Only to be called on exit Blender.

View File

@ -0,0 +1,67 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup bke
* \brief Blender CLI Generic `--command` Support.
*
* \note all registered commands must print help to the STDOUT & exit with a zero exit-code
* when `--help` is passed in as the first argument to a command.
*/
#include "BLI_utility_mixins.hh"
#include <memory>
#include <string>
/**
* Each instance of this class can run the command with an argument list.
* The arguments begin at the first argument after the command identifier.
*/
class CommandHandler : blender::NonCopyable, blender::NonMovable {
public:
CommandHandler(const std::string &id) : id(id) {}
virtual ~CommandHandler() = default;
/** Matched against `--command {id}`. */
const std::string id;
/**
* The main execution function.
* The return value is used as the commands exit-code.
*/
virtual int exec(struct bContext *C, int argc, const char **argv) = 0;
/** True when one or more registered commands share an ID. */
bool is_duplicate = false;
};
/**
* \param cmd: The memory for a command type (ownership is transferred).
*/
void BKE_blender_cli_command_register(std::unique_ptr<CommandHandler> cmd);
/**
* Unregister a previously registered command.
*/
bool BKE_blender_cli_command_unregister(CommandHandler *cmd);
/**
* Run the command by `id`, passing in the argument list & context.
* The argument list must begin after the command identifier.
*/
int BKE_blender_cli_command_exec(struct bContext *C,
const char *id,
const int argc,
const char **argv);
/**
* Print all known commands (used for passing `--command help` in the command-line).
*/
void BKE_blender_cli_command_print_help();
/**
* Frees all commands (using their #CommandFreeFn call-backs).
*/
void BKE_blender_cli_command_free_all();

View File

@ -76,6 +76,7 @@ set(SRC
intern/bake_items_serialize.cc
intern/bake_items_socket.cc
intern/blender.cc
intern/blender_cli_command.cc
intern/blender_copybuffer.cc
intern/blender_undo.cc
intern/blender_user_menu.cc
@ -345,6 +346,7 @@ set(SRC
BKE_bake_items_serialize.hh
BKE_bake_items_socket.hh
BKE_blender.hh
BKE_blender_cli_command.hh
BKE_blender_copybuffer.hh
BKE_blender_undo.hh
BKE_blender_user_menu.hh

View File

@ -0,0 +1,181 @@
/* SPDX-FileCopyrightText: 2001-2002 NaN Holding BV. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup bke
*
* Generic CLI "--command" declarations.
*
* Duplicate Commands
* ==================
*
* When two or more commands share the same identifier, a warning is printed and both are disabled.
*
* This is done because command-line actions may be destructive so the down-side of running the
* wrong command could be severe. The reason this is not considered an error is we can't prevent
* it so easily, unlike operator ID's which may be longer, commands are typically short terms
* which wont necessarily include an add-ons identifier as a prefix for e.g.
* Further, an error would break loading add-ons who's primary is *not*
* necessarily to provide command-line access.
* An alternative solution could be to generate unique names (number them for example)
* but this isn't reliable as it would depend on it the order add-ons are loaded which
* isn't under user control.
*/
#include <iostream>
#include "BLI_vector.hh"
#include "BKE_blender_cli_command.hh" /* own include */
#include "MEM_guardedalloc.h"
/* -------------------------------------------------------------------- */
/** \name Internal API
* \{ */
using CommandHandlerPtr = std::unique_ptr<CommandHandler>;
/**
* All registered command handlers.
* \note the order doesn't matter as duplicates are detected and prevented from running.
*/
blender::Vector<CommandHandlerPtr> g_command_handlers;
static CommandHandler *blender_cli_command_lookup(const std::string &id)
{
for (CommandHandlerPtr &cmd_iter : g_command_handlers) {
if (id == cmd_iter->id) {
return cmd_iter.get();
}
}
return nullptr;
}
static int blender_cli_command_index(const CommandHandler *cmd)
{
int index = 0;
for (CommandHandlerPtr &cmd_iter : g_command_handlers) {
if (cmd_iter.get() == cmd) {
return index;
}
index++;
}
return -1;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Public API
* \{ */
void BKE_blender_cli_command_register(std::unique_ptr<CommandHandler> cmd)
{
bool is_duplicate = false;
if (CommandHandler *cmd_exists = blender_cli_command_lookup(cmd->id)) {
std::cerr << "warning: registered duplicate command \"" << cmd->id
<< "\", this will be inaccessible" << std::endl;
cmd_exists->is_duplicate = true;
is_duplicate = true;
}
cmd->is_duplicate = is_duplicate;
g_command_handlers.append(std::move(cmd));
}
bool BKE_blender_cli_command_unregister(CommandHandler *cmd)
{
const int cmd_index = blender_cli_command_index(cmd);
if (cmd_index == -1) {
std::cerr << "failed to unregister command handler" << std::endl;
return false;
}
/* Update duplicates after removal. */
if (cmd->is_duplicate) {
CommandHandler *cmd_other = nullptr;
for (CommandHandlerPtr &cmd_iter : g_command_handlers) {
/* Skip self. */
if (cmd == cmd_iter.get()) {
continue;
}
if (cmd_iter->is_duplicate && (cmd_iter->id == cmd->id)) {
if (cmd_other) {
/* Two or more found, clear and break. */
cmd_other = nullptr;
break;
}
cmd_other = cmd_iter.get();
}
}
if (cmd_other) {
cmd_other->is_duplicate = false;
}
}
g_command_handlers.remove_and_reorder(cmd_index);
return true;
}
int BKE_blender_cli_command_exec(bContext *C, const char *id, const int argc, const char **argv)
{
CommandHandler *cmd = blender_cli_command_lookup(id);
if (cmd == nullptr) {
std::cerr << "Unrecognized command: \"" << id << "\"" << std::endl;
return EXIT_FAILURE;
}
if (cmd->is_duplicate) {
std::cerr << "Command: \"" << id
<< "\" was registered multiple times, must be resolved, aborting!" << std::endl;
return EXIT_FAILURE;
}
return cmd->exec(C, argc, argv);
}
void BKE_blender_cli_command_print_help()
{
/* As this is isn't ordered sorting in-place is acceptable,
* sort alphabetically for display purposes only. */
std::sort(g_command_handlers.begin(),
g_command_handlers.end(),
[](const CommandHandlerPtr &a, const CommandHandlerPtr &b) { return a->id < b->id; });
for (int pass = 0; pass < 2; pass++) {
std::cout << ((pass == 0) ? "Blender Command Listing:" :
"Duplicate Command Listing (ignored):")
<< std::endl;
const bool is_duplicate = pass > 0;
bool found = false;
bool has_duplicate = false;
for (CommandHandlerPtr &cmd_iter : g_command_handlers) {
if (cmd_iter->is_duplicate) {
has_duplicate = true;
}
if (cmd_iter->is_duplicate != is_duplicate) {
continue;
}
std::cout << "\t" << cmd_iter->id << std::endl;
found = true;
}
if (!found) {
std::cout << "\tNone found" << std::endl;
}
/* Don't print that no duplicates are found as it's not helpful. */
if (pass == 0 && !has_duplicate) {
break;
}
}
}
void BKE_blender_cli_command_free_all()
{
g_command_handlers.clear();
}
/** \} */

View File

@ -40,6 +40,7 @@ set(SRC
bpy_app_translations.cc
bpy_app_usd.cc
bpy_capi_utils.cc
bpy_cli_command.cc
bpy_driver.cc
bpy_gizmo_wrap.cc
bpy_interface.cc
@ -87,6 +88,7 @@ set(SRC
bpy_app_translations.h
bpy_app_usd.h
bpy_capi_utils.h
bpy_cli_command.h
bpy_driver.h
bpy_gizmo_wrap.h
bpy_intern_string.h

View File

@ -34,6 +34,7 @@
#include "bpy.h"
#include "bpy_app.h"
#include "bpy_cli_command.h"
#include "bpy_driver.h"
#include "bpy_library.h"
#include "bpy_operator.h"
@ -734,6 +735,10 @@ void BPy_init_modules(bContext *C)
PYMODULE_ADD_METHOD(mod, &meth_bpy_owner_id_get);
PYMODULE_ADD_METHOD(mod, &meth_bpy_owner_id_set);
/* Register command functions. */
PYMODULE_ADD_METHOD(mod, &BPY_cli_command_register_def);
PYMODULE_ADD_METHOD(mod, &BPY_cli_command_unregister_def);
#undef PYMODULE_ADD_METHOD
/* add our own modules dir, this is a python package */

View File

@ -0,0 +1,315 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup pythonintern
*
* Wrap `BKE_command_cli_*` to support custom CLI commands.
*/
#include <Python.h>
#include "BLI_utildefines.h"
#include "bpy_capi_utils.h"
#include "MEM_guardedalloc.h"
#include "BKE_blender_cli_command.hh"
#include "../generic/py_capi_utils.h"
#include "../generic/python_compat.h"
#include "../generic/python_utildefines.h"
#include "bpy_cli_command.h" /* Own include. */
static const char *bpy_cli_command_capsule_name = "bpy_cli_command";
static const char *bpy_cli_command_capsule_name_invalid = "bpy_cli_command<invalid>";
/* -------------------------------------------------------------------- */
/** \name Internal Utilities
* \{ */
/**
* Return a list of strings, compatible with the construction of Python's `sys.argv`.
*/
static PyObject *py_argv_from_bytes(const int argc, const char **argv)
{
/* Copy functionality from Python's internal `sys.argv` initialization. */
PyConfig config;
PyConfig_InitPythonConfig(&config);
PyStatus status = PyConfig_SetBytesArgv(&config, argc, (char *const *)argv);
PyObject *py_argv = nullptr;
if (UNLIKELY(PyStatus_Exception(status))) {
PyErr_Format(PyExc_ValueError, "%s", status.err_msg);
}
else {
BLI_assert(argc == config.argv.length);
py_argv = PyList_New(config.argv.length);
for (Py_ssize_t i = 0; i < config.argv.length; i++) {
PyList_SET_ITEM(py_argv, i, PyUnicode_FromWideChar(config.argv.items[i], -1));
}
}
PyConfig_Clear(&config);
return py_argv;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Internal Implementation
* \{ */
static int bpy_cli_command_exec(struct bContext *C,
PyObject *py_exec_fn,
const int argc,
const char **argv)
{
int exit_code = EXIT_FAILURE;
PyGILState_STATE gilstate;
bpy_context_set(C, &gilstate);
/* For the most part `sys.argv[-argc:]` is sufficient & less trouble than re-creating this
* list. Don't do this because:
* - Python scripts *could* have manipulated `sys.argv` (although it's bad practice).
* - We may want to support invoking commands directly,
* where the arguments aren't necessarily from `sys.argv`.
*/
bool has_error = false;
PyObject *py_argv = py_argv_from_bytes(argc, argv);
if (py_argv == nullptr) {
has_error = true;
}
else {
PyObject *exec_args = PyTuple_New(1);
PyTuple_SET_ITEM(exec_args, 0, py_argv);
PyObject *result = PyObject_Call(py_exec_fn, exec_args, nullptr);
Py_DECREF(exec_args); /* Frees `py_argv` too. */
/* Convert `sys.exit` into a return-value.
* NOTE: typically `sys.exit` *doesn't* need any special handling,
* however it's neater if we use the same code paths for exiting either way. */
if ((result == nullptr) && PyErr_ExceptionMatches(PyExc_SystemExit)) {
PyObject *error_type, *error_value, *error_traceback;
PyErr_Fetch(&error_type, &error_value, &error_traceback);
if (PyObject_TypeCheck(error_value, (PyTypeObject *)PyExc_SystemExit) &&
(((PySystemExitObject *)error_value)->code != nullptr))
{
/* When `SystemExit(..)` is raised. */
result = ((PySystemExitObject *)error_value)->code;
}
else {
/* When `sys.exit()` is called. */
result = error_value;
}
Py_INCREF(result);
PyErr_Restore(error_type, error_value, error_traceback);
PyErr_Clear();
}
if (result == nullptr) {
has_error = true;
}
else {
if (!PyLong_Check(result)) {
PyErr_Format(PyExc_TypeError,
"Expected an int return value, not a %.200s",
Py_TYPE(result)->tp_name);
has_error = true;
}
else {
const int exit_code_test = PyC_Long_AsI32(result);
if ((exit_code_test == -1) && PyErr_Occurred()) {
exit_code = EXIT_SUCCESS;
has_error = true;
}
else {
exit_code = exit_code_test;
}
}
Py_DECREF(result);
}
}
if (has_error) {
PyErr_Print();
PyErr_Clear();
}
bpy_context_clear(C, &gilstate);
return exit_code;
}
static void bpy_cli_command_free(PyObject *py_exec_fn)
{
/* An explicit unregister clears to avoid acquiring a lock. */
if (py_exec_fn) {
PyGILState_STATE gilstate = PyGILState_Ensure();
Py_DECREF(py_exec_fn);
PyGILState_Release(gilstate);
}
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Internal Class
* \{ */
class BPyCommandHandler : public CommandHandler {
public:
BPyCommandHandler(const std::string &id, PyObject *py_exec_fn)
: CommandHandler(id), py_exec_fn(py_exec_fn)
{
}
~BPyCommandHandler() override
{
bpy_cli_command_free(this->py_exec_fn);
}
int exec(struct bContext *C, int argc, const char **argv) override
{
return bpy_cli_command_exec(C, this->py_exec_fn, argc, argv);
}
PyObject *py_exec_fn = nullptr;
};
/** \} */
/* -------------------------------------------------------------------- */
/** \name Public Methods
* \{ */
PyDoc_STRVAR(
/* Wrap. */
bpy_cli_command_register_doc,
".. method:: register_cli_command(id, execute)\n"
"\n"
" Register a command, accessible via the (``-c`` / ``--command``) command-line argument.\n"
"\n"
" :arg id: The command identifier (must pass an ``str.isidentifier`` check).\n"
"\n"
" If the ``id`` is already registered, a warning is printed and "
"the command is inaccessible to prevent accidents invoking the wrong command."
" :type id: str\n"
" :arg execute: Callback, taking a single list of strings and returns an int.\n"
" The arguments are built from all command-line arguments following the command id.\n"
" The return value should be 0 for success, 1 on failure "
"(specific error codes from the ``os`` module can also be used).\n"
" :type execute: callable\n"
" :return: The command handle which can be passed to :func:`unregister_cli_command`.\n"
" :rtype: capsule\n");
static PyObject *bpy_cli_command_register(PyObject * /*self*/, PyObject *args, PyObject *kw)
{
PyObject *py_id;
PyObject *py_exec_fn;
static const char *_keywords[] = {
"id",
"execute",
nullptr,
};
static _PyArg_Parser _parser = {
PY_ARG_PARSER_HEAD_COMPAT()
"O!" /* `id` */
"O" /* `execute` */
":register_cli_command",
_keywords,
nullptr,
};
if (!_PyArg_ParseTupleAndKeywordsFast(args, kw, &_parser, &PyUnicode_Type, &py_id, &py_exec_fn))
{
return nullptr;
}
if (!PyUnicode_IsIdentifier(py_id)) {
PyErr_SetString(PyExc_ValueError, "The command id is not a valid identifier");
return nullptr;
}
if (!PyCallable_Check(py_exec_fn)) {
PyErr_SetString(PyExc_ValueError, "The execute argument must be callable");
return nullptr;
}
const char *id = PyUnicode_AsUTF8(py_id);
std::unique_ptr<CommandHandler> cmd_ptr = std::make_unique<BPyCommandHandler>(
std::string(id), Py_INCREF_RET(py_exec_fn));
void *cmd_p = cmd_ptr.get();
BKE_blender_cli_command_register(std::move(cmd_ptr));
return PyCapsule_New(cmd_p, bpy_cli_command_capsule_name, nullptr);
}
PyDoc_STRVAR(
/* Wrap. */
bpy_cli_command_unregister_doc,
".. method:: unregister_cli_command(handle)\n"
"\n"
" Unregister a CLI command.\n"
"\n"
" :arg handle: The return value of :func:`register_cli_command`.\n"
" :type handle: capsule\n");
static PyObject *bpy_cli_command_unregister(PyObject * /*self*/, PyObject *value)
{
if (!PyCapsule_CheckExact(value)) {
PyErr_Format(PyExc_TypeError,
"Expected a capsule returned from register_cli_command(...), found a: %.200s",
Py_TYPE(value)->tp_name);
return nullptr;
}
BPyCommandHandler *cmd = static_cast<BPyCommandHandler *>(
PyCapsule_GetPointer(value, bpy_cli_command_capsule_name));
if (cmd == nullptr) {
const char *capsule_name = PyCapsule_GetName(value);
if (capsule_name == bpy_cli_command_capsule_name_invalid) {
PyErr_SetString(PyExc_ValueError, "The command has already been removed");
}
else {
PyErr_Format(PyExc_ValueError,
"Unrecognized capsule ID \"%.200s\"",
capsule_name ? capsule_name : "<null>");
}
return nullptr;
}
/* Don't acquire the GIL when un-registering. */
Py_CLEAR(cmd->py_exec_fn);
/* Don't allow removing again. */
PyCapsule_SetName(value, bpy_cli_command_capsule_name_invalid);
BKE_blender_cli_command_unregister((CommandHandler *)cmd);
Py_RETURN_NONE;
}
#if (defined(__GNUC__) && !defined(__clang__))
# pragma GCC diagnostic push
# pragma GCC diagnostic ignored "-Wcast-function-type"
#endif
PyMethodDef BPY_cli_command_register_def = {
"register_cli_command",
(PyCFunction)bpy_cli_command_register,
METH_STATIC | METH_VARARGS | METH_KEYWORDS,
bpy_cli_command_register_doc,
};
PyMethodDef BPY_cli_command_unregister_def = {
"unregister_cli_command",
(PyCFunction)bpy_cli_command_unregister,
METH_STATIC | METH_O,
bpy_cli_command_unregister_doc,
};
#if (defined(__GNUC__) && !defined(__clang__))
# pragma GCC diagnostic pop
#endif
/** \} */

View File

@ -0,0 +1,20 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup pythonintern
*/
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
extern PyMethodDef BPY_cli_command_register_def;
extern PyMethodDef BPY_cli_command_unregister_def;
#ifdef __cplusplus
}
#endif

View File

@ -51,6 +51,7 @@
#include "BKE_addon.h"
#include "BKE_appdir.hh"
#include "BKE_blender_cli_command.hh"
#include "BKE_mask.h" /* free mask clipboard */
#include "BKE_material.h" /* BKE_material_copybuf_clear */
#include "BKE_studiolight.h"
@ -535,6 +536,14 @@ void WM_exit_ex(bContext *C, const bool do_python_exit, const bool do_user_exit_
}
#endif
/* Perform this early in case commands reference other data freed later in this function.
* This most run:
* - After add-ons are disabled because they may unregister commands.
* - Before Python exits so Python objects can be de-referenced.
* - Before #BKE_blender_atexit runs they free the `argv` on WIN32.
*/
BKE_blender_cli_command_free_all();
BLI_timer_free();
WM_paneltype_clear();

View File

@ -37,6 +37,7 @@
/* Mostly initialization functions. */
#include "BKE_appdir.hh"
#include "BKE_blender.hh"
#include "BKE_blender_cli_command.hh"
#include "BKE_brush.hh"
#include "BKE_cachefile.hh"
#include "BKE_callbacks.hh"
@ -566,8 +567,23 @@ int main(int argc,
#ifndef WITH_PYTHON_MODULE
if (G.background) {
int exit_code;
if (app_state.command.argv) {
const char *id = app_state.command.argv[0];
if (STREQ(id, "help")) {
BKE_blender_cli_command_print_help();
exit_code = EXIT_SUCCESS;
}
else {
exit_code = BKE_blender_cli_command_exec(
C, id, app_state.command.argc - 1, app_state.command.argv + 1);
}
}
else {
exit_code = G.is_break ? EXIT_FAILURE : EXIT_SUCCESS;
}
/* Using window-manager API in background-mode is a bit odd, but works fine. */
WM_exit(C, G.is_break ? EXIT_FAILURE : EXIT_SUCCESS);
WM_exit(C, exit_code);
}
else {
/* Shows the splash as needed. */

View File

@ -698,6 +698,8 @@ static void print_help(bArgs *ba, bool all)
PRINT("\n");
BLI_args_print_arg_doc(ba, "-noaudio");
BLI_args_print_arg_doc(ba, "-setaudio");
PRINT("\n");
BLI_args_print_arg_doc(ba, "--command");
PRINT("\n");
@ -924,6 +926,32 @@ static int arg_handle_background_mode_set(int /*argc*/, const char ** /*argv*/,
return 0;
}
static const char arg_handle_command_set_doc[] =
"<command>\n"
"\tRun a command which consumes all remaining arguments.\n"
"\tUse '-c help' to list all other commands.\n"
"\tPass '--help' after the command to see its help text.\n"
"\n"
"\tThis implies '--background' mode.";
static int arg_handle_command_set(int argc, const char **argv, void * /*data*/)
{
if (argc < 2) {
fprintf(stderr, "%s requires at least one argument\n", argv[0]);
exit(EXIT_FAILURE);
BLI_assert_unreachable();
}
/* See `--background` implementation. */
G.background = true;
BKE_sound_force_device("None");
app_state.command.argc = argc - 1;
app_state.command.argv = argv + 1;
/* Consume remaining arguments. */
return argc - 1;
}
static const char arg_handle_log_level_set_doc[] =
"<level>\n"
"\tSet the logging verbosity level (higher for more details) defaults to 1,\n"
@ -2374,6 +2402,8 @@ void main_args_setup(bContext *C, bArgs *ba, bool all)
ba, nullptr, "--disable-abort-handler", CB(arg_handle_abort_handler_disable), nullptr);
BLI_args_add(ba, "-b", "--background", CB(arg_handle_background_mode_set), nullptr);
/* Command implies background mode. */
BLI_args_add(ba, "-c", "--command", CB(arg_handle_command_set), nullptr);
BLI_args_add(ba, "-a", nullptr, CB(arg_handle_playback_mode), nullptr);

View File

@ -52,6 +52,12 @@ struct ApplicationState {
struct {
unsigned char python;
} exit_code_on_error;
/** Storage for commands (see `--command` argument). */
struct {
int argc;
const char **argv;
} command;
};
extern struct ApplicationState app_state; /* creator.c */