mirror of
https://github.com/Gnucash/gnucash.git
synced 2025-02-25 18:55:30 -06:00
[gnucash-commands.cpp] scripting & REPL for guile & python
Syntax: gnucash-cli [datafile.gnucash [--readwrite]] [--interactive] [--script script.py or script.scm] [--language python|guile] 1. --script or --interactive will launch scripting 2. a datafile will trigger loading, will attempt r/w if requested 3. --language python will choose python 4. --interactive will run the Guile or Python commandline with access to gnucash modules
This commit is contained in:
parent
4e2978d6de
commit
47e6f09993
@ -111,6 +111,7 @@ target_link_libraries (gnucash
|
|||||||
gnc-bi-import gnc-customer-import gnc-report
|
gnc-bi-import gnc-customer-import gnc-report
|
||||||
PkgConfig::GTK3 ${GUILE_LDFLAGS} PkgConfig::GLIB2
|
PkgConfig::GTK3 ${GUILE_LDFLAGS} PkgConfig::GLIB2
|
||||||
${Boost_LIBRARIES}
|
${Boost_LIBRARIES}
|
||||||
|
${Python3_LIBRARIES}
|
||||||
)
|
)
|
||||||
|
|
||||||
set(gnucash_cli_SOURCES
|
set(gnucash_cli_SOURCES
|
||||||
@ -137,15 +138,20 @@ endif()
|
|||||||
|
|
||||||
add_dependencies (gnucash-cli gnucash)
|
add_dependencies (gnucash-cli gnucash)
|
||||||
|
|
||||||
target_compile_definitions(gnucash-cli PRIVATE -DG_LOG_DOMAIN=\"gnc.bin\")
|
target_compile_definitions(gnucash-cli PRIVATE
|
||||||
|
-DG_LOG_DOMAIN=\"gnc.bin\"
|
||||||
|
$<$<BOOL:${WITH_PYTHON}>:HAVE_PYTHON_H>)
|
||||||
|
|
||||||
target_link_libraries (gnucash-cli
|
target_link_libraries (gnucash-cli
|
||||||
gnc-app-utils
|
gnc-app-utils
|
||||||
gnc-engine gnc-core-utils gnucash-guile gnc-report
|
gnc-engine gnc-core-utils gnucash-guile gnc-report
|
||||||
${GUILE_LDFLAGS} PkgConfig::GLIB2
|
${GUILE_LDFLAGS} PkgConfig::GLIB2
|
||||||
${Boost_LIBRARIES}
|
${Boost_LIBRARIES}
|
||||||
|
${Python3_LIBRARIES}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
target_include_directories (gnucash-cli PRIVATE ${Python3_INCLUDE_DIRS})
|
||||||
|
|
||||||
if (BUILDING_FROM_VCS)
|
if (BUILDING_FROM_VCS)
|
||||||
target_compile_definitions(gnucash PRIVATE -DGNC_VCS=\"git\")
|
target_compile_definitions(gnucash PRIVATE -DGNC_VCS=\"git\")
|
||||||
target_compile_definitions(gnucash-cli PRIVATE -DGNC_VCS=\"git\")
|
target_compile_definitions(gnucash-cli PRIVATE -DGNC_VCS=\"git\")
|
||||||
|
@ -62,6 +62,12 @@ namespace Gnucash {
|
|||||||
boost::optional <std::string> m_namespace;
|
boost::optional <std::string> m_namespace;
|
||||||
bool m_verbose = false;
|
bool m_verbose = false;
|
||||||
|
|
||||||
|
boost::optional <std::string> m_script;
|
||||||
|
std::vector<std::string> m_script_args;
|
||||||
|
std::string m_language;
|
||||||
|
bool m_interactive;
|
||||||
|
bool m_open_readwrite;
|
||||||
|
|
||||||
boost::optional <std::string> m_report_cmd;
|
boost::optional <std::string> m_report_cmd;
|
||||||
boost::optional <std::string> m_report_name;
|
boost::optional <std::string> m_report_name;
|
||||||
boost::optional <std::string> m_export_type;
|
boost::optional <std::string> m_export_type;
|
||||||
@ -107,6 +113,17 @@ Gnucash::GnucashCli::configure_program_options (void)
|
|||||||
m_opt_desc_display->add (quotes_options);
|
m_opt_desc_display->add (quotes_options);
|
||||||
m_opt_desc_all.add (quotes_options);
|
m_opt_desc_all.add (quotes_options);
|
||||||
|
|
||||||
|
bpo::options_description cli_options(_("Scripting and/or Interactive Session Options"));
|
||||||
|
cli_options.add_options()
|
||||||
|
("script,S", bpo::value (&m_script), _("Script to run"))
|
||||||
|
("script-args", bpo::value (&m_script_args), _("Script arguments"))
|
||||||
|
("interactive,I", bpo::bool_switch (&m_interactive), _("Interactive session"))
|
||||||
|
("language,L", bpo::value (&m_language)->default_value("guile"), _("Specify language for script or interactive session; guile (default) or python"))
|
||||||
|
("readwrite,W", bpo::bool_switch (&m_open_readwrite), _("Open datafile read-write for script and/or interactive session"));
|
||||||
|
m_pos_opt_desc.add("script-args", -1);
|
||||||
|
m_opt_desc_display->add (cli_options);
|
||||||
|
m_opt_desc_all.add (cli_options);
|
||||||
|
|
||||||
bpo::options_description report_options(_("Report Generation Options"));
|
bpo::options_description report_options(_("Report Generation Options"));
|
||||||
report_options.add_options()
|
report_options.add_options()
|
||||||
("report,R", bpo::value (&m_report_cmd),
|
("report,R", bpo::value (&m_report_cmd),
|
||||||
@ -127,10 +144,32 @@ may be specified to describe some saved options.\n"
|
|||||||
}
|
}
|
||||||
|
|
||||||
int
|
int
|
||||||
Gnucash::GnucashCli::start ([[maybe_unused]] int argc, [[maybe_unused]] char **argv)
|
Gnucash::GnucashCli::start (int argc, char **argv)
|
||||||
{
|
{
|
||||||
Gnucash::CoreApp::start();
|
Gnucash::CoreApp::start();
|
||||||
|
|
||||||
|
if (m_interactive || m_script)
|
||||||
|
{
|
||||||
|
std::vector<const char*> newArgv =
|
||||||
|
{ argc ? argv[0] : "", m_file_to_load ? m_file_to_load->c_str() : ""};
|
||||||
|
std::transform (m_script_args.begin(), m_script_args.end(), std::back_inserter(newArgv),
|
||||||
|
[](const std::string& s) { return s.c_str(); });
|
||||||
|
// note the vector<const char*> is valid as long as script_args's strings are not damaged!
|
||||||
|
|
||||||
|
std::cout << "\n\nScript args:";
|
||||||
|
for (const auto& arg : newArgv)
|
||||||
|
std::cout << ' ' << arg;
|
||||||
|
std::cout << '\n';
|
||||||
|
std::cout << "File to load: " << (m_file_to_load ? *m_file_to_load : "(null)") << std::endl;
|
||||||
|
std::cout << "Language: " << m_language << std::endl;
|
||||||
|
std::cout << "Script: " << (m_script ? *m_script : "(null)") << std::endl;
|
||||||
|
std::cout << "Readwrite: " << (m_open_readwrite ? 'Y' : 'N') << std::endl;
|
||||||
|
std::cout << "Interactive: " << (m_interactive ? 'Y' : 'N') << "\n\n" << std::endl;
|
||||||
|
|
||||||
|
return Gnucash::run_scripting (newArgv, m_file_to_load, m_language, m_script,
|
||||||
|
m_open_readwrite, m_interactive);
|
||||||
|
}
|
||||||
|
|
||||||
if (!m_quotes_cmd.empty())
|
if (!m_quotes_cmd.empty())
|
||||||
{
|
{
|
||||||
if (m_quotes_cmd.front() == "info")
|
if (m_quotes_cmd.front() == "info")
|
||||||
|
@ -31,6 +31,8 @@
|
|||||||
|
|
||||||
#include "gnucash-commands.hpp"
|
#include "gnucash-commands.hpp"
|
||||||
#include "gnucash-core-app.hpp"
|
#include "gnucash-core-app.hpp"
|
||||||
|
#include "gnc-ui-util.h"
|
||||||
|
#include "gnc-path.h"
|
||||||
|
|
||||||
#include <gnc-filepath-utils.h>
|
#include <gnc-filepath-utils.h>
|
||||||
#include <gnc-engine-guile.h>
|
#include <gnc-engine-guile.h>
|
||||||
@ -40,12 +42,17 @@
|
|||||||
#include <qoflog.h>
|
#include <qoflog.h>
|
||||||
|
|
||||||
#include <boost/locale.hpp>
|
#include <boost/locale.hpp>
|
||||||
|
#include <boost/filesystem.hpp>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <gnc-report.h>
|
#include <gnc-report.h>
|
||||||
#include <gnc-quotes.hpp>
|
#include <gnc-quotes.hpp>
|
||||||
|
|
||||||
|
#ifdef HAVE_PYTHON_H
|
||||||
|
#include <Python.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace bl = boost::locale;
|
namespace bl = boost::locale;
|
||||||
|
|
||||||
static std::string empty_string{};
|
static std::string empty_string{};
|
||||||
@ -112,12 +119,48 @@ get_line (const char* prompt)
|
|||||||
static std::string
|
static std::string
|
||||||
get_choice (const char* prompt, const std::vector<std::string> choices)
|
get_choice (const char* prompt, const std::vector<std::string> choices)
|
||||||
{
|
{
|
||||||
while (true)
|
std::string response;
|
||||||
|
do
|
||||||
{
|
{
|
||||||
auto response = get_line (prompt);
|
response = get_line (prompt);
|
||||||
if (std::find (choices.begin(), choices.end(), response) != choices.end())
|
|
||||||
return response;
|
|
||||||
}
|
}
|
||||||
|
while (std::none_of (choices.begin(), choices.end(),
|
||||||
|
[&response](auto& choice) { return choice == response; } ));
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct scripting_args
|
||||||
|
{
|
||||||
|
const boost::optional<std::string>& script;
|
||||||
|
bool interactive;
|
||||||
|
};
|
||||||
|
|
||||||
|
// when guile or python script/interactive session succeed
|
||||||
|
static void
|
||||||
|
cleanup_and_exit_with_save ()
|
||||||
|
{
|
||||||
|
if (gnc_current_session_exist())
|
||||||
|
{
|
||||||
|
auto session = gnc_get_current_session();
|
||||||
|
auto book = gnc_get_current_book();
|
||||||
|
auto is_yes = [] (const char* prompt)
|
||||||
|
{
|
||||||
|
auto s = get_choice (prompt, {"Y","y","N","n"});
|
||||||
|
return s == "Y" || s == "y";
|
||||||
|
};
|
||||||
|
|
||||||
|
std::cerr << _("Warning: session was not cleared.") << std::endl;
|
||||||
|
|
||||||
|
if (!qof_book_session_not_saved (book))
|
||||||
|
std::cerr << _("Please don't forget to clear session before shutdown.") << std::endl;
|
||||||
|
else if (qof_book_is_readonly (book))
|
||||||
|
std::cerr << _("Book is readonly. Unsaved changes will be lost.") << std::endl;
|
||||||
|
else if (is_yes (_("There are unsaved changes. Save before clearing [YN]?")))
|
||||||
|
qof_session_save (session, report_session_percentage);
|
||||||
|
|
||||||
|
gnc_clear_current_session ();
|
||||||
|
}
|
||||||
|
gnc_shutdown_cli (0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Don't try to use std::string& for the members of the following struct, it
|
/* Don't try to use std::string& for the members of the following struct, it
|
||||||
@ -469,3 +512,147 @@ Gnucash::report_list (void)
|
|||||||
scm_boot_guile (0, nullptr, scm_report_list, NULL);
|
scm_boot_guile (0, nullptr, scm_report_list, NULL);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// scripting code follows:
|
||||||
|
|
||||||
|
static void
|
||||||
|
run_guile_cli (void *data, [[maybe_unused]] int argc, [[maybe_unused]] char **argv)
|
||||||
|
{
|
||||||
|
auto args = static_cast<scripting_args*>(data);
|
||||||
|
if (args->script)
|
||||||
|
{
|
||||||
|
PINFO ("Running script from %s... ", args->script->c_str());
|
||||||
|
scm_c_primitive_load (args->script->c_str());
|
||||||
|
}
|
||||||
|
if (args->interactive)
|
||||||
|
{
|
||||||
|
std::cout << _("Welcome to Gnucash Interactive Guile Session") << std::endl;
|
||||||
|
std::vector<const char*> modules =
|
||||||
|
{ "gnucash core-utils", "gnucash engine", "gnucash app-utils", "gnucash report",
|
||||||
|
"system repl repl", "ice-9 readline" };
|
||||||
|
auto show_and_load = [](const auto& mod)
|
||||||
|
{
|
||||||
|
std::cout << bl::format ("(use-modules ({1}))") % mod << std::endl;
|
||||||
|
scm_c_use_module (mod);
|
||||||
|
};
|
||||||
|
std::for_each (modules.begin(), modules.end(), show_and_load);
|
||||||
|
scm_c_eval_string ("(activate-readline)");
|
||||||
|
scm_c_eval_string ("(start-repl)");
|
||||||
|
}
|
||||||
|
cleanup_and_exit_with_save ();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef HAVE_PYTHON_H
|
||||||
|
static void
|
||||||
|
python_cleanup_and_exit (PyConfig& config, PyStatus& status)
|
||||||
|
{
|
||||||
|
if (qof_book_session_not_saved (gnc_get_current_book()))
|
||||||
|
std::cerr << _("Book is readonly. Unsaved changes will be lost.") << std::endl;
|
||||||
|
gnc_clear_current_session ();
|
||||||
|
|
||||||
|
PyConfig_Clear(&config);
|
||||||
|
if (status.err_msg && *status.err_msg)
|
||||||
|
std::cerr << bl::format (_("Python Config failed with error {1}")) % status.err_msg
|
||||||
|
<< std::endl;
|
||||||
|
gnc_shutdown_cli (status.exitcode);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
run_python_cli (int argc, char **argv, scripting_args* args)
|
||||||
|
{
|
||||||
|
PyConfig config;
|
||||||
|
PyConfig_InitPythonConfig(&config);
|
||||||
|
|
||||||
|
PyStatus status = PyConfig_SetBytesArgv(&config, argc, argv);
|
||||||
|
if (PyStatus_Exception(status))
|
||||||
|
python_cleanup_and_exit (config, status);
|
||||||
|
|
||||||
|
status = Py_InitializeFromConfig(&config);
|
||||||
|
if (PyStatus_Exception(status))
|
||||||
|
python_cleanup_and_exit (config, status);
|
||||||
|
|
||||||
|
PyConfig_Clear(&config);
|
||||||
|
|
||||||
|
if (args->script)
|
||||||
|
{
|
||||||
|
auto script_filename = args->script->c_str();
|
||||||
|
PINFO ("Running python script %s...", script_filename);
|
||||||
|
auto fp = fopen (script_filename, "rb");
|
||||||
|
if (!fp)
|
||||||
|
{
|
||||||
|
std::cerr << bl::format (_("Unable to load Python script {1}")) % script_filename
|
||||||
|
<< std::endl;
|
||||||
|
python_cleanup_and_exit (config, status);
|
||||||
|
}
|
||||||
|
else if (PyRun_SimpleFileEx (fp, script_filename, 1) != 0)
|
||||||
|
{
|
||||||
|
std::cerr << bl::format (_("Python script {1} execution failed.")) % script_filename
|
||||||
|
<< std::endl;
|
||||||
|
python_cleanup_and_exit (config, status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (args->interactive)
|
||||||
|
{
|
||||||
|
std::cout << _("Welcome to Gnucash Interactive Python Session") << std::endl;
|
||||||
|
PyRun_InteractiveLoop (stdin, "foo");
|
||||||
|
}
|
||||||
|
Py_Finalize();
|
||||||
|
cleanup_and_exit_with_save ();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
int
|
||||||
|
Gnucash::run_scripting (std::vector<const char*> newArgv,
|
||||||
|
const bo_str& file_to_load,
|
||||||
|
std::string& language,
|
||||||
|
const bo_str& script,
|
||||||
|
bool open_readwrite,
|
||||||
|
bool interactive)
|
||||||
|
{
|
||||||
|
std::vector<std::string> errors;
|
||||||
|
static const std::vector<std::string> languages = { "guile", "python" };
|
||||||
|
|
||||||
|
if (open_readwrite && !file_to_load)
|
||||||
|
errors.push_back (_ ("--readwrite: missing datafile!"));
|
||||||
|
|
||||||
|
if (script && (!boost::filesystem::is_regular_file (*script)))
|
||||||
|
errors.push_back ((bl::format (_("--script: {1} is not a file")) % *script).str());
|
||||||
|
|
||||||
|
if (std::none_of (languages.begin(), languages.end(),
|
||||||
|
[&language](auto& lang){ return language == lang; }))
|
||||||
|
errors.push_back (_ ("--language: must be 'python' or 'guile'"));
|
||||||
|
#ifndef HAVE_PYTHON_H
|
||||||
|
else if (language == "python")
|
||||||
|
errors.push_back (_("--language: python wasn't compiled in this build"));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (!errors.empty())
|
||||||
|
{
|
||||||
|
std::cerr << _("Errors parsing arguments:") << std::endl;
|
||||||
|
auto to_console = [](const auto& str){ std::cerr << str << std::endl; };
|
||||||
|
std::for_each (errors.begin(), errors.end(), to_console);
|
||||||
|
gnc_shutdown_cli (1);
|
||||||
|
}
|
||||||
|
|
||||||
|
gnc_prefs_init ();
|
||||||
|
gnc_ui_util_init();
|
||||||
|
if (file_to_load && boost::filesystem::is_regular_file (*file_to_load))
|
||||||
|
[[maybe_unused]] auto session = load_file (*file_to_load, open_readwrite);
|
||||||
|
|
||||||
|
scripting_args args { script, interactive };
|
||||||
|
if (language == "guile")
|
||||||
|
{
|
||||||
|
scm_boot_guile (newArgv.size(), (char**)newArgv.data(), run_guile_cli, &args);
|
||||||
|
return 0; // never reached...
|
||||||
|
}
|
||||||
|
#ifdef HAVE_PYTHON_H
|
||||||
|
else if (language == "python")
|
||||||
|
{
|
||||||
|
run_python_cli (newArgv.size(), (char**)newArgv.data(), &args);
|
||||||
|
return 0; // never reached...
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return 0; // never reached
|
||||||
|
}
|
||||||
|
@ -46,5 +46,11 @@ namespace Gnucash {
|
|||||||
int report_list (void);
|
int report_list (void);
|
||||||
int report_show (const bo_str& file_to_load,
|
int report_show (const bo_str& file_to_load,
|
||||||
const bo_str& run_report);
|
const bo_str& run_report);
|
||||||
|
int run_scripting (std::vector<const char*> newArgv,
|
||||||
|
const bo_str& m_file_to_load,
|
||||||
|
std::string& m_language,
|
||||||
|
const bo_str& m_script,
|
||||||
|
bool m_open_readwrite,
|
||||||
|
bool m_interactive);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
@ -294,7 +294,7 @@ Gnucash::CoreApp::add_common_program_options (void)
|
|||||||
("input-file", bpo::value (&m_file_to_load),
|
("input-file", bpo::value (&m_file_to_load),
|
||||||
_("[datafile]"));
|
_("[datafile]"));
|
||||||
|
|
||||||
m_pos_opt_desc.add("input-file", -1);
|
m_pos_opt_desc.add("input-file", 1);
|
||||||
|
|
||||||
m_opt_desc_all.add (common_options);
|
m_opt_desc_all.add (common_options);
|
||||||
m_opt_desc_all.add (hidden_options);
|
m_opt_desc_all.add (hidden_options);
|
||||||
|
Loading…
Reference in New Issue
Block a user