GnuCash  5.6-150-g038405b370+
gnucash-commands.cpp
1 /*
2  * gnucash-cli.cpp -- The command line entry point for GnuCash
3  *
4  * Copyright (C) 2020 Geert Janssens <geert@kobaltwit.be>
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU General Public License as
8  * published by the Free Software Foundation; either version 2 of
9  * the License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, contact:
18  *
19  * Free Software Foundation Voice: +1-617-542-5942
20  * 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652
21  * Boston, MA 02110-1301, USA gnu@gnu.org
22  */
23 #include <config.h>
24 
25 #include <libguile.h>
26 #include <guile-mappings.h>
27 #ifdef __MINGW32__
28 #include <Windows.h>
29 #include <fcntl.h>
30 #endif
31 
32 #include "gnucash-commands.hpp"
33 #include "gnucash-core-app.hpp"
34 
35 #include <gnc-filepath-utils.h>
36 #include <gnc-engine-guile.h>
37 #include <gnc-prefs.h>
38 #include <gnc-prefs-utils.h>
39 #include <gnc-session.h>
40 #include <qoflog.h>
41 
42 #include <boost/locale.hpp>
43 #include <fstream>
44 #include <iostream>
45 #include <iomanip>
46 #include <gnc-report.h>
47 #include <gnc-quotes.hpp>
48 
49 namespace bl = boost::locale;
50 
51 static std::string empty_string{};
52 
53 /* This static indicates the debugging module that this .o belongs to. */
54 static QofLogModule log_module = GNC_MOD_GUI;
55 
56 static int
57 cleanup_and_exit_with_failure (QofSession *session)
58 {
59  if (session)
60  {
61  auto error{qof_session_get_error (session)};
62  if (error != ERR_BACKEND_NO_ERR)
63  {
64  if (error == ERR_BACKEND_LOCKED)
65  PERR ("File is locked, won't open.");
66  else
67  PERR ("Session Error: %s\n",
68  qof_session_get_error_message (session));
69  }
70  qof_session_destroy (session);
71  }
73  return 1;
74 }
75 
76 static void gnc_shutdown_cli (int exit_status)
77 {
78  gnc_hook_run (HOOK_SHUTDOWN, NULL);
80  exit (exit_status);
81 }
82 
83 /* scm_boot_guile doesn't expect to return, so call shutdown ourselves here */
84 static void
85 scm_cleanup_and_exit_with_failure (QofSession *session)
86 {
87  cleanup_and_exit_with_failure (session);
88  gnc_shutdown_cli (1);
89 }
90 
91 static void
92 report_session_percentage (const char *message, double percent)
93 {
94  static double previous = 0.0;
95  if ((percent - previous) < 5.0)
96  return;
97  PINFO ("\r%3.0f%% complete...", percent);
98  previous = percent;
99  return;
100 }
101 
102 /* Don't try to use std::string& for the members of the following struct, it
103  * results in the values getting corrupted as it passes through initializing
104  * Scheme when compiled with Clang.
105  */
107  const std::string& file_to_load;
108  const std::string& run_report;
109  const std::string& export_type;
110  const std::string& output_file;
111 };
112 
113 static inline void
114 write_report_file (const char *html, const char* file)
115 {
116  if (!file || !html || !*html) return;
117  auto ofs{gnc_open_filestream(file)};
118  if (!ofs)
119  {
120  std::cerr << "Failed to open file " << file << " for writing\n";
121  return;
122  }
123  ofs << html << std::endl;
124  // ofs destructor will close the file
125 }
126 
127 static void
128 scm_run_report (void *data,
129  [[maybe_unused]] int argc, [[maybe_unused]] char **argv)
130 {
131  auto args = static_cast<run_report_args*>(data);
132 
133  scm_c_eval_string("(debug-set! stack 200000)");
134  scm_c_use_module ("gnucash utilities");
135  scm_c_use_module ("gnucash app-utils");
136  scm_c_use_module ("gnucash reports");
137 
138  gnc_report_init ();
139  Gnucash::gnc_load_scm_config ([](const gchar *msg){ PINFO ("%s", msg); });
140  gnc_prefs_init ();
142 
143  auto datafile = args->file_to_load.c_str();
144  auto check_report_cmd = scm_c_eval_string ("gnc:cmdline-check-report");
145  auto get_report_cmd = scm_c_eval_string ("gnc:cmdline-get-report-id");
146  auto run_export_cmd = scm_c_eval_string ("gnc:cmdline-template-export");
147  /* We generally insist on using scm_from_utf8_string() throughout GnuCash
148  * because all GUI-sourced strings and all file-sourced strings are encoded
149  * that way. In this case, though, the input is coming from a shell window
150  * and Microsoft Windows shells are generally not capable of entering UTF8
151  * so it's necessary here to allow guile to read the locale and interpret
152  * the input in that encoding.
153  */
154  auto report = scm_from_locale_string (args->run_report.c_str());
155  auto type = !args->export_type.empty() ?
156  scm_from_locale_string (args->export_type.c_str()) : SCM_BOOL_F;
157 
158  if (scm_is_false (scm_call_2 (check_report_cmd, report, type)))
159  scm_cleanup_and_exit_with_failure (nullptr);
160 
161  PINFO ("Loading datafile %s...\n", datafile);
162 
163  auto session = gnc_get_current_session ();
164  if (!session)
165  scm_cleanup_and_exit_with_failure (session);
166 
167  qof_session_begin (session, datafile, SESSION_READ_ONLY);
168  if (qof_session_get_error (session) != ERR_BACKEND_NO_ERR)
169  scm_cleanup_and_exit_with_failure (session);
170 
171  qof_session_load (session, report_session_percentage);
172  if (qof_session_get_error (session) != ERR_BACKEND_NO_ERR)
173  scm_cleanup_and_exit_with_failure (session);
174 
175  if (!args->export_type.empty())
176  {
177  SCM retval = scm_call_2 (run_export_cmd, report, type);
178  SCM query_result = scm_c_eval_string ("gnc:html-document?");
179  SCM get_export_string = scm_c_eval_string ("gnc:html-document-export-string");
180  SCM get_export_error = scm_c_eval_string ("gnc:html-document-export-error");
181 
182  if (scm_is_false (scm_call_1 (query_result, retval)))
183  {
184  std::cerr << _("This report must be upgraded to \
185 return a document object with export-string or export-error.") << std::endl;
186  scm_cleanup_and_exit_with_failure (nullptr);
187  }
188 
189  SCM export_string = scm_call_1 (get_export_string, retval);
190  SCM export_error = scm_call_1 (get_export_error, retval);
191 
192  if (scm_is_string (export_string))
193  {
194  auto output = scm_to_utf8_string (export_string);
195  if (!args->output_file.empty())
196  {
197  write_report_file(output, args->output_file.c_str());
198  }
199  else
200  {
201  std::cout << output << std::endl;
202  }
203  g_free (output);
204  }
205  else if (scm_is_string (export_error))
206  {
207  auto err = scm_to_utf8_string (export_error);
208  std::cerr << err << std::endl;
209  g_free (err);
210  scm_cleanup_and_exit_with_failure (nullptr);
211  }
212  else
213  {
214  std::cerr << _("This report must be upgraded to \
215 return a document object with export-string or export-error.") << std::endl;
216  scm_cleanup_and_exit_with_failure (nullptr);
217  }
218  }
219  else
220  {
221  SCM id = scm_call_1(get_report_cmd, report);
222 
223  if (scm_is_false (id))
224  scm_cleanup_and_exit_with_failure (nullptr);
225  char *html, *errmsg;
226 
227  if (gnc_run_report_with_error_handling (scm_to_int(id), &html, &errmsg))
228  {
229  if (!args->output_file.empty())
230  {
231  write_report_file(html, args->output_file.c_str());
232  }
233  else
234  {
235  std::cout << html << std::endl;
236  }
237  g_free (html);
238  }
239  else
240  {
241  std::cerr << errmsg << std::endl;
242  g_free (errmsg);
243  }
244  }
245 
246  qof_session_destroy (session);
247 
248  qof_event_resume ();
249  gnc_shutdown_cli (0);
250  return;
251 }
252 
253 
255  const std::string& file_to_load;
256  const std::string& show_report;
257 };
258 
259 static void
260 scm_report_show (void *data,
261  [[maybe_unused]] int argc, [[maybe_unused]] char **argv)
262 {
263  auto args = static_cast<show_report_args*>(data);
264 
265  scm_c_eval_string("(debug-set! stack 200000)");
266  scm_c_use_module ("gnucash utilities");
267  scm_c_use_module ("gnucash app-utils");
268  scm_c_use_module ("gnucash reports");
269  gnc_report_init ();
270  Gnucash::gnc_load_scm_config ([](const gchar *msg){ PINFO ("%s", msg); });
271 
272  if (!args->file_to_load.empty())
273  {
274  auto datafile = args->file_to_load.c_str();
275  PINFO ("Loading datafile %s...\n", datafile);
276 
277  auto session = gnc_get_current_session ();
278  if (session)
279  {
280  qof_session_begin (session, datafile, SESSION_READ_ONLY);
281  if (qof_session_get_error (session) == ERR_BACKEND_NO_ERR)
282  qof_session_load (session, report_session_percentage);
283  }
284  }
285 
286  scm_call_2 (scm_c_eval_string ("gnc:cmdline-report-show"),
287  scm_from_locale_string (args->show_report.c_str ()),
288  scm_current_output_port ());
289  gnc_shutdown_cli (0);
290  return;
291 }
292 
293 
294 static void
295 scm_report_list ([[maybe_unused]] void *data,
296  [[maybe_unused]] int argc, [[maybe_unused]] char **argv)
297 {
298  scm_c_eval_string("(debug-set! stack 200000)");
299  scm_c_use_module ("gnucash app-utils");
300  scm_c_use_module ("gnucash reports");
301  gnc_report_init ();
302  Gnucash::gnc_load_scm_config ([](const gchar *msg){ PINFO ("%s", msg); });
303 
304  scm_call_1 (scm_c_eval_string ("gnc:cmdline-report-list"),
305  scm_current_output_port ());
306  gnc_shutdown_cli (0);
307  return;
308 }
309 
310 int
311 Gnucash::check_finance_quote (void)
312 {
313  gnc_prefs_init ();
314  try
315  {
316  GncQuotes quotes;
317  std::cout << bl::format (bl::translate ("Found Finance::Quote version {1}.")) % quotes.version() << "\n";
318  std::cout << bl::translate ("Finance::Quote sources:\n");
319  int count{0};
320  const auto width{12};
321  for (auto source : quotes.sources())
322  {
323  auto mul{source.length() / width + 1};
324  count += mul;
325  if (count > 6)
326  {
327  count = mul;
328  std::cout << "\n";
329  }
330  std::cout << std::setw(mul * (width + 1)) << std::left << source;
331  }
332  std::cout << std::endl;
333  return 0;
334  }
335  catch (const GncQuoteException& err)
336  {
337  std::cout << err.what() << std::endl;
338  return 1;
339  }
340 }
341 
342 int
343 Gnucash::add_quotes (const bo_str& uri)
344 {
345  gnc_prefs_init ();
347 
348  auto session = gnc_get_current_session();
349  if (!session)
350  return 1;
351 
352  qof_session_begin(session, uri->c_str(), SESSION_NORMAL_OPEN);
353  if (qof_session_get_error(session) != ERR_BACKEND_NO_ERR)
354  return cleanup_and_exit_with_failure (session);
355 
356  qof_session_load(session, NULL);
357  if (qof_session_get_error(session) != ERR_BACKEND_NO_ERR)
358  return cleanup_and_exit_with_failure (session);
359 
360  try
361  {
362  GncQuotes quotes;
363  std::cout << bl::format (bl::translate ("Found Finance::Quote version {1}.")) % quotes.version() << std::endl;
364  auto quote_sources = quotes.sources();
365  gnc_quote_source_set_fq_installed (quotes.version().c_str(), quote_sources);
366  quotes.fetch(qof_session_get_book(session));
367  if (quotes.had_failures())
368  std::cerr << quotes.report_failures() << std::endl;
369  }
370  catch (const GncQuoteException& err)
371  {
372  std::cerr << bl::translate("Price retrieval failed: ") << err.what() << std::endl;
373  }
374  qof_session_save(session, NULL);
375  if (qof_session_get_error(session) != ERR_BACKEND_NO_ERR)
376  return cleanup_and_exit_with_failure (session);
377 
378  qof_session_destroy(session);
380  return 0;
381 }
382 
383 int
384 Gnucash::report_quotes (const char* source, const StrVec& commodities, bool verbose)
385 {
386  gnc_prefs_init();
387  try
388  {
389  GncQuotes quotes;
390  quotes.report(source, commodities, verbose);
391  if (quotes.had_failures())
392  std::cerr << quotes.report_failures() << std::endl;
393  }
394  catch (const GncQuoteException& err)
395  {
396  std::cerr << bl::translate("Price retrieval failed: ") << err.what() << std::endl;
397  return -1;
398  }
399  return 0;
400 }
401 
402 int
403 Gnucash::run_report (const bo_str& file_to_load,
404  const bo_str& run_report,
405  const bo_str& export_type,
406  const bo_str& output_file)
407 {
408  auto args = run_report_args { file_to_load ? *file_to_load : empty_string,
409  run_report ? *run_report : empty_string,
410  export_type ? *export_type : empty_string,
411  output_file ? *output_file : empty_string };
412  if (run_report && !run_report->empty())
413  scm_boot_guile (0, nullptr, scm_run_report, &args);
414 
415  return 0;
416 }
417 
418 int
419 Gnucash::report_show (const bo_str& file_to_load,
420  const bo_str& show_report)
421 {
422  auto args = show_report_args { file_to_load ? *file_to_load : empty_string,
423  show_report ? *show_report : empty_string };
424  if (show_report && !show_report->empty())
425  scm_boot_guile (0, nullptr, scm_report_show, &args);
426 
427  return 0;
428 }
429 
430 int
431 Gnucash::report_list (void)
432 {
433  scm_boot_guile (0, nullptr, scm_report_list, NULL);
434  return 0;
435 }
void qof_session_save(QofSession *session, QofPercentageFunc percentage_func)
The qof_session_save() method will commit all changes that have been made to the session.
Definition: qofsession.cpp:625
#define PINFO(format, args...)
Print an informational note.
Definition: qoflog.h:256
void gnc_engine_shutdown(void)
Called to shutdown the engine.
Definition: gnc-engine.cpp:141
void qof_session_begin(QofSession *session, const char *uri, SessionOpenMode mode)
Begins a new session.
Definition: qofsession.cpp:610
bool had_failures() noexcept
Report if there were quotes requested but not retrieved.
in use by another user (ETXTBSY)
Definition: qofbackend.h:66
#define PERR(format, args...)
Log a serious error.
Definition: qoflog.h:244
Create a new store at the URI even if a store already exists there.
Definition: qofsession.h:128
QofBook * qof_session_get_book(const QofSession *session)
Returns the QofBook of this session.
Definition: qofsession.cpp:574
Preferences initialization function.
QofBackendError qof_session_get_error(QofSession *session)
The qof_session_get_error() routine can be used to obtain the reason for any failure.
Definition: qofsession.cpp:728
void gnc_quote_source_set_fq_installed(const char *version_string, const std::vector< std::string > &sources_list)
Update gnucash internal tables based on what Finance::Quote sources are installed.
Generic api to store and retrieve preferences.
void gnc_prefs_init(void)
This function is called early in the load process to preload a number of preferences from the setting...
void qof_event_suspend(void)
Suspend all engine events.
Definition: qofevent.cpp:145
void report(const char *source, const StrVec &commodities, bool verbose=false)
Report quote results from Finance::Quote to std::cout.
void fetch(QofBook *book)
Fetch quotes for all commodities in our db that have a quote source set.
void qof_event_resume(void)
Resume engine event generation.
Definition: qofevent.cpp:156
File path resolution utility functions.
const std::string & version() noexcept
Get the installed Finance::Quote version.
const QuoteSources & sources() noexcept
Get the available Finance::Quote sources as a std::vector.