gnucash stable: Multiple changes pushed
John Ralls
jralls at code.gnucash.org
Thu Jul 9 19:16:32 EDT 2026
Updated via https://github.com/Gnucash/gnucash/commit/9db7b443 (commit)
via https://github.com/Gnucash/gnucash/commit/f737847a (commit)
from https://github.com/Gnucash/gnucash/commit/88670f22 (commit)
commit 9db7b4439cc436e8e77b7454ee1ce9c2684dd098
Merge: 88670f2298 f737847af2
Author: John Ralls <jralls at ceridwen.us>
Date: Thu Jul 9 16:14:33 2026 -0700
Merge Thomas Teixeira's 'bug799774' into stable.
commit f737847af2fda683214dc6435a018621066a8a8d
Author: Thomas Teixeira <tjteixeira at earthlink.net>
Date: Mon Jun 29 09:13:29 2026 -1000
Bug 799774 - Import transaction from CSV skips fields if CSV
file has backslash just before final quote in a quoted field
https://bugs.gnucash.org/show_bug.cgi?id=799774
Added a checkbox to the "File Format" section of the "Import Preview" step
of the CSV Import Assistant. This checkbox is labeled "Enable escape character"
and is enabled by default. If this checkbox is not enabled, GncCsvTokenizer
will insert an escape character (backslash) before any escape found in the
input which will prevent boost::tokenizer with escaped_list_separator from
processing this backslash as part of an escape sequence.
Ensured that saved import settings that have no current value for the
new "EnableEscape" field will set the "Enable escape character" checkbox.
Changed the test framework in GncTokenizerTest to specify whether escape
characters are enabled, and added test cases for escape characters disabled.
diff --git a/gnucash/gtkbuilder/assistant-csv-price-import.glade b/gnucash/gtkbuilder/assistant-csv-price-import.glade
index 26fb248380..6671a3d130 100644
--- a/gnucash/gtkbuilder/assistant-csv-price-import.glade
+++ b/gnucash/gtkbuilder/assistant-csv-price-import.glade
@@ -276,7 +276,7 @@ There are two reserved names which can't be used to save custom settings:
</packing>
</child>
<child>
- <!-- n-columns=3 n-rows=3 -->
+ <!-- n-columns=3 n-rows=4 -->
<object class="GtkGrid" id="separator_table">
<property name="visible">True</property>
<property name="can-focus">False</property>
@@ -410,6 +410,25 @@ There are two reserved names which can't be used to save custom settings:
<property name="top-attach">2</property>
</packing>
</child>
+ <child>
+ <object class="GtkCheckButton" id="escape_cbutton">
+ <property name="label" translatable="yes">Enable escape character (\)</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">False</property>
+ <property name="tooltip-text" translatable="yes">If enabled, the escape character, backslash (\) is processed specially: backslash-quote (\")becomes a single quote ("); backslash-backslash (\\) becomes a single backslash (\); backslash-n (\n) becomes a newline character; a backslash followed by any other character is treated literally: both the backslash and the following character become part of the data field.</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="halign">start</property>
+ <property name="active">True</property>
+ <property name="draw-indicator">True</property>
+ <signal name="toggled" handler="csv_price_imp_preview_sep_button_cb" swapped="no"/>
+ </object>
+ <packing>
+ <property name="left-attach">0</property>
+ <property name="top-attach">3</property>
+ <property name="width">3</property>
+ </packing>
+ </child>
<child>
<placeholder/>
</child>
diff --git a/gnucash/gtkbuilder/assistant-csv-trans-import.glade b/gnucash/gtkbuilder/assistant-csv-trans-import.glade
index 56e16187b7..84d6c002f0 100644
--- a/gnucash/gtkbuilder/assistant-csv-trans-import.glade
+++ b/gnucash/gtkbuilder/assistant-csv-trans-import.glade
@@ -265,7 +265,7 @@ There are two reserved names which can't be used to save custom settings:
</packing>
</child>
<child>
- <!-- n-columns=3 n-rows=3 -->
+ <!-- n-columns=3 n-rows=4 -->
<object class="GtkGrid" id="separator_table">
<property name="visible">True</property>
<property name="can-focus">False</property>
@@ -400,6 +400,25 @@ There are two reserved names which can't be used to save custom settings:
<property name="width">2</property>
</packing>
</child>
+ <child>
+ <object class="GtkCheckButton" id="escape_cbutton">
+ <property name="label" translatable="yes">Enable escape character (\)</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">False</property>
+ <property name="tooltip-text" translatable="yes">If enabled, the escape character, backslash (\) is processed specially: backslash-quote (\")becomes a single quote ("); backslash-backslash (\\) becomes a single backslash (\); backslash-n (\n) becomes a newline character; a backslash followed by any other character is treated literally: both the backslash and the following character become part of the data field.</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="halign">start</property>
+ <property name="active">True</property>
+ <property name="draw-indicator">True</property>
+ <signal name="toggled" handler="csv_tximp_preview_sep_button_cb" swapped="no"/>
+ </object>
+ <packing>
+ <property name="left-attach">0</property>
+ <property name="top-attach">3</property>
+ <property name="width">3</property>
+ </packing>
+ </child>
</object>
<packing>
<property name="expand">False</property>
diff --git a/gnucash/import-export/csv-imp/assistant-csv-price-import.cpp b/gnucash/import-export/csv-imp/assistant-csv-price-import.cpp
index 3543b51747..7cc181b636 100644
--- a/gnucash/import-export/csv-imp/assistant-csv-price-import.cpp
+++ b/gnucash/import-export/csv-imp/assistant-csv-price-import.cpp
@@ -167,6 +167,7 @@ private:
GOCharmapSel *encselector; /**< The widget for selecting the encoding */
GtkWidget *separator_table; /**< Container for the separator checkboxes */
GtkCheckButton *sep_button[SEP_NUM_OF_TYPES]; /**< Checkbuttons for common separators */
+ GtkCheckButton *escape_cbutton; /**< The checkbutton for escape processing */
GtkWidget *fw_instructions_hbox; /**< Container for fixed-width instructions */
GtkCheckButton *custom_cbutton; /**< The checkbutton for a custom separator */
GtkEntry *custom_entry; /**< The entry for custom separators */
@@ -613,16 +614,21 @@ CsvImpPriceAssist::CsvImpPriceAssist ()
};
for (int i = 0; i < SEP_NUM_OF_TYPES; i++)
sep_button[i]
- = (GtkCheckButton*)GTK_WIDGET(gtk_builder_get_object (builder, sep_button_names[i]));
+ = GTK_CHECK_BUTTON(gtk_builder_get_object (builder, sep_button_names[i]));
/* Load and connect the custom separator checkbutton in the same way
* as the other separator buttons. */
custom_cbutton
- = (GtkCheckButton*)GTK_WIDGET(gtk_builder_get_object (builder, "custom_cbutton"));
+ = GTK_CHECK_BUTTON(gtk_builder_get_object (builder, "custom_cbutton"));
/* Load the entry for the custom separator entry. Connect it to the
* sep_button_clicked event handler as well. */
- custom_entry = (GtkEntry*)GTK_WIDGET(gtk_builder_get_object (builder, "custom_entry"));
+ custom_entry = GTK_ENTRY(gtk_builder_get_object (builder, "custom_entry"));
+
+ /* Load and connect the escape checkbutton in the same way
+ * as the other separator buttons. */
+ escape_cbutton
+ = GTK_CHECK_BUTTON(gtk_builder_get_object (builder, "escape_cbutton"));
/* Create the encoding selector widget and add it to the assistant */
encselector = GO_CHARMAP_SEL(go_charmap_sel_new(GO_CHARMAP_SEL_TO_UTF8));
@@ -689,7 +695,7 @@ CsvImpPriceAssist::CsvImpPriceAssist ()
fixed_button = GTK_WIDGET(gtk_builder_get_object (builder, "fixed_button"));
/* Load the data treeview and connect it to its resizing event handler. */
- treeview = (GtkTreeView*)GTK_WIDGET(gtk_builder_get_object (builder, "treeview"));
+ treeview = GTK_TREE_VIEW(gtk_builder_get_object (builder, "treeview"));
gtk_tree_view_set_headers_clickable (treeview, true);
/* This is true only after encoding_selected is called, so we must
@@ -1008,8 +1014,8 @@ void CsvImpPriceAssist::preview_over_write (bool over)
/** Event handler for separator changes. This function is called
* whenever one of the widgets for configuring the separators (the
- * separator checkbuttons or the custom separator entry) is
- * changed.
+ * separator checkbuttons, the escape checkbutton or the custom
+ * separator entry) is changed.
* @param widget The widget that was changed
* @param info The data that is being configured
*/
@@ -1040,7 +1046,8 @@ void CsvImpPriceAssist::preview_update_separators (GtkWidget* widget)
/* Set the parse options using the checked_separators list. */
price_imp->separators (checked_separators);
-
+ price_imp->enable_escape (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON(escape_cbutton)));
+
/* if there are no separators, there will only be one column
* so make sure column header is NONE */
if (checked_separators.empty())
@@ -1780,6 +1787,7 @@ CsvImpPriceAssist::preview_refresh ()
if (price_imp->file_format() == GncImpFileFormat::CSV)
{
auto separators = price_imp->separators();
+ auto escape = price_imp->enable_escape();
const auto stock_sep_chars = std::string (" \t,:;-");
for (int i = 0; i < SEP_NUM_OF_TYPES; i++)
@@ -1798,6 +1806,11 @@ CsvImpPriceAssist::preview_refresh ()
separators.erase(pos);
pos = separators.find_first_of (stock_sep_chars);
}
+ g_signal_handlers_block_by_func (escape_cbutton, (gpointer) csv_price_imp_preview_sep_button_cb, this);
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON(escape_cbutton),
+ escape);
+ g_signal_handlers_unblock_by_func (escape_cbutton, (gpointer) csv_price_imp_preview_sep_button_cb, this);
+
g_signal_handlers_block_by_func (custom_cbutton, (gpointer) csv_price_imp_preview_sep_button_cb, this);
g_signal_handlers_block_by_func (custom_entry, (gpointer) csv_price_imp_preview_sep_button_cb, this);
@@ -1894,7 +1907,8 @@ CsvImpPriceAssist::assist_preview_page_prepare ()
{
/* Parsing failed ... */
gnc_error_dialog (GTK_WINDOW(csv_imp_asst), "%s", _(e.what()));
- go_back = true;
+ /* Stay in this step so user can override */
+ go_back = false;
}
}
diff --git a/gnucash/import-export/csv-imp/assistant-csv-trans-import.cpp b/gnucash/import-export/csv-imp/assistant-csv-trans-import.cpp
index c62ed927ae..b09d054d8f 100644
--- a/gnucash/import-export/csv-imp/assistant-csv-trans-import.cpp
+++ b/gnucash/import-export/csv-imp/assistant-csv-trans-import.cpp
@@ -242,6 +242,7 @@ private:
GOCharmapSel *encselector; /**< The widget for selecting the encoding */
GtkWidget *separator_table; /**< Container for the separator checkboxes */
GtkCheckButton *sep_button[SEP_NUM_OF_TYPES]; /**< Checkbuttons for common separators */
+ GtkCheckButton *escape_cbutton; /**< The checkbutton for escape processing */
GtkWidget *fw_instructions_hbox; /**< Container for fixed-width instructions */
GtkCheckButton *custom_cbutton; /**< The checkbutton for a custom separator */
GtkEntry *custom_entry; /**< The entry for custom separators */
@@ -565,16 +566,21 @@ CsvImpTransAssist::CsvImpTransAssist ()
};
for (int i = 0; i < SEP_NUM_OF_TYPES; i++)
sep_button[i]
- = (GtkCheckButton*)GTK_WIDGET(gtk_builder_get_object (builder, sep_button_names[i]));
+ = GTK_CHECK_BUTTON(gtk_builder_get_object (builder, sep_button_names[i]));
/* Load and connect the custom separator checkbutton in the same way
* as the other separator buttons. */
custom_cbutton
- = (GtkCheckButton*)GTK_WIDGET(gtk_builder_get_object (builder, "custom_cbutton"));
+ = GTK_CHECK_BUTTON(gtk_builder_get_object (builder, "custom_cbutton"));
/* Load the entry for the custom separator entry. Connect it to the
* sep_button_clicked event handler as well. */
- custom_entry = (GtkEntry*)GTK_WIDGET(gtk_builder_get_object (builder, "custom_entry"));
+ custom_entry = GTK_ENTRY(gtk_builder_get_object (builder, "custom_entry"));
+
+ /* Load and connect the escape checkbutton in the same way
+ * as the other separator buttons. */
+ escape_cbutton
+ = GTK_CHECK_BUTTON(gtk_builder_get_object (builder, "escape_cbutton"));
/* Add account selection widget */
acct_selector = gnc_account_sel_new();
@@ -637,7 +643,7 @@ CsvImpTransAssist::CsvImpTransAssist ()
fixed_button = GTK_WIDGET(gtk_builder_get_object (builder, "fixed_button"));
/* Load the data treeview and connect it to its resizing event handler. */
- treeview = (GtkTreeView*)GTK_WIDGET(gtk_builder_get_object (builder, "treeview"));
+ treeview = GTK_TREE_VIEW(gtk_builder_get_object (builder, "treeview"));
gtk_tree_view_set_headers_clickable (treeview, true);
/* This is true only after encoding_selected is called, so we must
@@ -986,8 +992,8 @@ void CsvImpTransAssist::preview_multi_split (bool multi)
/** Event handler for separator changes. This function is called
* whenever one of the widgets for configuring the separators (the
- * separator checkbuttons or the custom separator entry) is
- * changed.
+ * separator checkbuttons, the escape checkbutton or the custom
+ * separator entry) is changed.
* @param widget The widget that was changed
*/
void CsvImpTransAssist::preview_update_separators (GtkWidget* widget)
@@ -1018,6 +1024,7 @@ void CsvImpTransAssist::preview_update_separators (GtkWidget* widget)
/* Set the parse options using the checked_separators list. */
tx_imp->separators (checked_separators);
+ tx_imp->enable_escape (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON(escape_cbutton)));
/* Parse the data using the new options. We don't want to reguess
* the column types because we want to leave the user's
@@ -1704,6 +1711,7 @@ CsvImpTransAssist::preview_refresh ()
if (tx_imp->file_format() == GncImpFileFormat::CSV)
{
auto separators = tx_imp->separators();
+ auto enable_escape = tx_imp->enable_escape();
const auto stock_sep_chars = std::string (" \t,:;-");
for (int i = 0; i < SEP_NUM_OF_TYPES; i++)
{
@@ -1726,8 +1734,15 @@ CsvImpTransAssist::preview_refresh ()
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON(custom_cbutton),
!separators.empty());
gtk_entry_set_text (GTK_ENTRY(custom_entry), separators.c_str());
+
+ g_signal_handlers_block_by_func (escape_cbutton, (gpointer) csv_tximp_preview_sep_button_cb, this);
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON(escape_cbutton),
+ enable_escape);
+ g_signal_handlers_unblock_by_func (escape_cbutton, (gpointer) csv_tximp_preview_sep_button_cb, this);
+
g_signal_handlers_unblock_by_func (custom_cbutton, (gpointer) csv_tximp_preview_sep_button_cb, this);
g_signal_handlers_unblock_by_func (custom_entry, (gpointer) csv_tximp_preview_sep_button_cb, this);
+ g_signal_handlers_unblock_by_func (custom_cbutton, (gpointer) csv_tximp_preview_sep_button_cb, this);
try
{
tx_imp->tokenize (false);
@@ -2036,7 +2051,8 @@ CsvImpTransAssist::assist_preview_page_prepare ()
{
/* Parsing failed ... */
gnc_error_dialog (GTK_WINDOW (csv_imp_asst), "%s", _(e.what()));
- go_back = true;
+ /* Stay in this step so user can override */
+ go_back = false;
}
}
diff --git a/gnucash/import-export/csv-imp/gnc-imp-settings-csv.cpp b/gnucash/import-export/csv-imp/gnc-imp-settings-csv.cpp
index ae5e6f396f..bf435d7b8f 100644
--- a/gnucash/import-export/csv-imp/gnc-imp-settings-csv.cpp
+++ b/gnucash/import-export/csv-imp/gnc-imp-settings-csv.cpp
@@ -54,6 +54,7 @@ const std::string gnc_exp_4{N_("GnuCash Export Format (4.x and older)")};
#define CSV_SKIP_END "SkipEndLines"
#define CSV_SEP "Separators"
+#define CSV_ESC "EnableEscape"
#define CSV_CUSTOM "Custom"
#define CSV_CUSTOM_ENTRY "CustomEntry"
@@ -150,6 +151,14 @@ CsvImportSettings::load (void)
if (key_char)
g_free (key_char);
+ m_enable_escape = g_key_file_get_boolean (keyfile, group.c_str(), CSV_ESC, &key_error);
+ // Older import settings will not have a value for escape processing
+ // so set it to true if no value is found in the file format settings
+ if (key_error && key_error->code == G_KEY_FILE_ERROR_KEY_NOT_FOUND)
+ m_enable_escape = true;
+ else
+ m_load_error |= handle_load_error (&key_error, group);
+
m_date_format = g_key_file_get_integer (keyfile, group.c_str(), CSV_DATE, &key_error);
m_load_error |= handle_load_error (&key_error, group);
@@ -203,6 +212,7 @@ CsvImportSettings::save (void)
(m_file_format == GncImpFileFormat::CSV) ? true : false);
g_key_file_set_string (keyfile, group.c_str(), CSV_SEP, m_separators.c_str());
+ g_key_file_set_boolean (keyfile, group.c_str(), CSV_ESC, m_enable_escape);
g_key_file_set_integer (keyfile, group.c_str(), CSV_DATE, m_date_format);
std::ostringstream cmt_ss;
cmt_ss << "Supported date formats: ";
diff --git a/gnucash/import-export/csv-imp/gnc-imp-settings-csv.hpp b/gnucash/import-export/csv-imp/gnc-imp-settings-csv.hpp
index 40d8c7009e..bc0f0bc4a3 100644
--- a/gnucash/import-export/csv-imp/gnc-imp-settings-csv.hpp
+++ b/gnucash/import-export/csv-imp/gnc-imp-settings-csv.hpp
@@ -53,7 +53,7 @@ struct CsvImportSettings
CsvImportSettings() : m_file_format (GncImpFileFormat::CSV), m_encoding {"UTF-8"},
m_date_format {0}, m_currency_format {0},
m_skip_start_lines{0}, m_skip_end_lines{0}, m_skip_alt_lines (false),
- m_separators {","}, m_load_error {false} { }
+ m_separators {","}, m_enable_escape {true}, m_load_error {false} { }
virtual ~CsvImportSettings() = default;
/** Save the gathered widget properties to a key File.
@@ -82,6 +82,7 @@ uint32_t m_skip_start_lines; // Number of header rows to skip
uint32_t m_skip_end_lines; // Number of footer rows to skip
bool m_skip_alt_lines; // Skip alternate rows
std::string m_separators; // Separators for csv format
+bool m_enable_escape; // Process escapes in quoted csv entries
bool m_load_error; // Was there an error while parsing the state file ?
std::vector<uint32_t> m_column_widths; // The Column widths
diff --git a/gnucash/import-export/csv-imp/gnc-import-price.cpp b/gnucash/import-export/csv-imp/gnc-import-price.cpp
index ee3cb0b714..31c5daec9d 100644
--- a/gnucash/import-export/csv-imp/gnc-import-price.cpp
+++ b/gnucash/import-export/csv-imp/gnc-import-price.cpp
@@ -114,9 +114,12 @@ void GncPriceImport::file_format(GncImpFileFormat format)
load_file(new_imp_file);
// Restore potentially previously set separators or column_widths
- if ((file_format() == GncImpFileFormat::CSV)
- && !m_settings.m_separators.empty())
- separators (m_settings.m_separators);
+ if (file_format() == GncImpFileFormat::CSV)
+ {
+ if (!m_settings.m_separators.empty())
+ separators (m_settings.m_separators);
+ enable_escape (m_settings.m_enable_escape);
+ }
else if ((file_format() == GncImpFileFormat::FIXED_WIDTH)
&& !m_settings.m_column_widths.empty())
{
@@ -287,6 +290,16 @@ void GncPriceImport::separators (std::string separators)
}
std::string GncPriceImport::separators () { return m_settings.m_separators; }
+void GncPriceImport::enable_escape(bool enable)
+{
+ if (file_format() != GncImpFileFormat::CSV)
+ return;
+ m_settings.m_enable_escape = enable;
+ auto csvtok = dynamic_cast<GncCsvTokenizer*>(m_tokenizer.get());
+ csvtok->set_enable_escape (enable);
+}
+bool GncPriceImport::enable_escape() { return m_settings.m_enable_escape; }
+
void GncPriceImport::settings (const CsvPriceImpSettings& settings)
{
/* First apply file format as this may recreate the tokenizer */
diff --git a/gnucash/import-export/csv-imp/gnc-import-price.hpp b/gnucash/import-export/csv-imp/gnc-import-price.hpp
index f0ffa89458..90cf168eac 100644
--- a/gnucash/import-export/csv-imp/gnc-import-price.hpp
+++ b/gnucash/import-export/csv-imp/gnc-import-price.hpp
@@ -117,6 +117,9 @@ public:
void separators (std::string separators);
std::string separators ();
+ void enable_escape (bool enable);
+ bool enable_escape ();
+
void settings (const CsvPriceImpSettings& settings);
bool save_settings ();
diff --git a/gnucash/import-export/csv-imp/gnc-import-tx.cpp b/gnucash/import-export/csv-imp/gnc-import-tx.cpp
index 9484e0a26a..2e369df22c 100644
--- a/gnucash/import-export/csv-imp/gnc-import-tx.cpp
+++ b/gnucash/import-export/csv-imp/gnc-import-tx.cpp
@@ -114,9 +114,12 @@ void GncTxImport::file_format(GncImpFileFormat format)
load_file(new_imp_file);
// Restore potentially previously set separators or column_widths
- if ((file_format() == GncImpFileFormat::CSV)
- && !m_settings.m_separators.empty())
- separators (m_settings.m_separators);
+ if (file_format() == GncImpFileFormat::CSV)
+ {
+ if (!m_settings.m_separators.empty())
+ separators (m_settings.m_separators);
+ enable_escape (m_settings.m_enable_escape);
+ }
else if ((file_format() == GncImpFileFormat::FIXED_WIDTH)
&& !m_settings.m_column_widths.empty())
{
@@ -313,6 +316,16 @@ void GncTxImport::separators (std::string separators)
}
std::string GncTxImport::separators () { return m_settings.m_separators; }
+void GncTxImport::enable_escape(bool enable)
+{
+ if (file_format() != GncImpFileFormat::CSV)
+ return;
+ m_settings.m_enable_escape = enable;
+ auto csvtok = dynamic_cast<GncCsvTokenizer*>(m_tokenizer.get());
+ csvtok->set_enable_escape (enable);
+}
+bool GncTxImport::enable_escape() { return m_settings.m_enable_escape; }
+
void GncTxImport::settings (const CsvTransImpSettings& settings)
{
/* First apply file format as this may recreate the tokenizer */
@@ -324,7 +337,10 @@ void GncTxImport::settings (const CsvTransImpSettings& settings)
encoding (m_settings.m_encoding);
if (file_format() == GncImpFileFormat::CSV)
+ {
separators (m_settings.m_separators);
+ enable_escape (m_settings.m_enable_escape);
+ }
else if (file_format() == GncImpFileFormat::FIXED_WIDTH)
{
auto fwtok = dynamic_cast<GncFwTokenizer*>(m_tokenizer.get());
diff --git a/gnucash/import-export/csv-imp/gnc-import-tx.hpp b/gnucash/import-export/csv-imp/gnc-import-tx.hpp
index e3f7016c5d..e2bdf4efd4 100644
--- a/gnucash/import-export/csv-imp/gnc-import-tx.hpp
+++ b/gnucash/import-export/csv-imp/gnc-import-tx.hpp
@@ -136,6 +136,9 @@ public:
void separators (std::string separators);
std::string separators ();
+ void enable_escape (bool enable);
+ bool enable_escape ();
+
void settings (const CsvTransImpSettings& settings);
bool save_settings ();
diff --git a/gnucash/import-export/csv-imp/gnc-tokenizer-csv.cpp b/gnucash/import-export/csv-imp/gnc-tokenizer-csv.cpp
index 65523c1f50..6194aebd57 100644
--- a/gnucash/import-export/csv-imp/gnc-tokenizer-csv.cpp
+++ b/gnucash/import-export/csv-imp/gnc-tokenizer-csv.cpp
@@ -43,6 +43,12 @@ GncCsvTokenizer::set_separators(const std::string& separators)
m_sep_str = separators;
}
+void
+GncCsvTokenizer::set_enable_escape(const bool enable)
+{
+ m_enable_escape = enable;
+}
+
int GncCsvTokenizer::tokenize()
{
@@ -71,7 +77,7 @@ int GncCsvTokenizer::tokenize()
{
if (last_quote == 0) // Test separately because last_quote - 1 would be out of range
inside_quotes = !inside_quotes;
- else if (buffer[ last_quote - 1 ] != '\\')
+ else if (!m_enable_escape || buffer[ last_quote - 1 ] != '\\')
inside_quotes = !inside_quotes;
last_quote = buffer.find_first_of('"',last_quote+1);
@@ -91,10 +97,21 @@ int GncCsvTokenizer::tokenize()
auto bs_pos = line.find ('\\');
while (bs_pos != std::string::npos)
{
- if ((bs_pos == line.size()) || // got trailing single backslash
- (line.find_first_of ("\"\\n", bs_pos + 1) != bs_pos + 1)) // backslash is not part of known escapes \\, \" or \n
- line = line.substr(0, bs_pos) + "\\\\" + line.substr(bs_pos + 1);
- bs_pos += 2;
+ if (! m_enable_escape)
+ {
+ // Put an escape before this escape and look for the next
+ line.insert (bs_pos, 1, '\\');
+ bs_pos += 2;
+ }
+ else
+ {
+ // Process as before
+ if ((bs_pos == line.size()) || // got trailing single backslash
+ (line.find_first_of ("\"\\n", bs_pos + 1) != bs_pos + 1)) // backslash is not part of known escapes \\, \" or \n
+ line = line.substr(0, bs_pos) + "\\\\" + line.substr(bs_pos + 1);
+ bs_pos += 2;
+ }
+
bs_pos = line.find ('\\', bs_pos);
}
diff --git a/gnucash/import-export/csv-imp/gnc-tokenizer-csv.hpp b/gnucash/import-export/csv-imp/gnc-tokenizer-csv.hpp
index c60910b5a4..3c9df45e23 100644
--- a/gnucash/import-export/csv-imp/gnc-tokenizer-csv.hpp
+++ b/gnucash/import-export/csv-imp/gnc-tokenizer-csv.hpp
@@ -53,10 +53,13 @@ public:
~GncCsvTokenizer() = default; // destructor
void set_separators(const std::string& separators);
+ void set_enable_escape(const bool enable);
int tokenize() override;
private:
std::string m_sep_str = ",";
+
+ bool m_enable_escape = true;
};
#endif
diff --git a/gnucash/import-export/csv-imp/test/test-tokenizer.cpp b/gnucash/import-export/csv-imp/test/test-tokenizer.cpp
index b51cf9a10c..42ca1ebf81 100644
--- a/gnucash/import-export/csv-imp/test/test-tokenizer.cpp
+++ b/gnucash/import-export/csv-imp/test/test-tokenizer.cpp
@@ -42,6 +42,7 @@
struct tokenize_csv_test_data
{
+ bool enable_escape;
const char *csv_line;
uint num_fields;
const char *fields [8];
@@ -160,7 +161,9 @@ GncTokenizerTest::test_gnc_tokenize_helper (const std::string& separators,
{
tokenize_csv_test_data cur_line = test_data[i];
+
set_utf8_contents (csv_tok, std::string(cur_line.csv_line));
+ csvtok->set_enable_escape (cur_line.enable_escape);
csv_tok->tokenize();
// The tests only come with one line, so get the first row only
@@ -176,15 +179,25 @@ GncTokenizerTest::test_gnc_tokenize_helper (const std::string& separators,
}
static tokenize_csv_test_data comma_separated [] = {
- { "Date,Num,Description,Notes,Account,Deposit,Withdrawal,Balance", 8, { "Date","Num","Description","Notes","Account","Deposit","Withdrawal","Balance" } },
- { "05/01/15,45,Typical csv import line - including quoted empty field,,Miscellaneous,\"\",\"1,100.00\",", 8, { "05/01/15","45","Typical csv import line - including quoted empty field","","Miscellaneous","","1,100.00","" } },
- { "05/01/15,45,Acme Inc.,,Miscellaneous,", 6, { "05/01/15","45","Acme Inc.","","Miscellaneous","",NULL,NULL } },
- { "Test\\ with backslash,nextfield", 2, { "Test\\ with backslash","nextfield",NULL,NULL,NULL,NULL,NULL,NULL } },
- { "Test with \\\" escaped quote,nextfield", 2, { "Test with \" escaped quote","nextfield",NULL,NULL,NULL,NULL,NULL,NULL } },
- { "Test with \"\" escaped quote,nextfield", 2, { "Test with \" escaped quote","nextfield",NULL,NULL,NULL,NULL,NULL,NULL } },
- { "\"Test (quoted) with \"\" escaped quote\",\"nextfield\"", 2, { "Test (quoted) with \" escaped quote","nextfield",NULL,NULL,NULL,NULL,NULL,NULL } },
- { "\"Unescaped quote test\",nextfield", 2, { "Unescaped quote test","nextfield",NULL,NULL,NULL,NULL,NULL,NULL } },
- { NULL, 0, { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL } },
+ { true, "Date,Num,Description,Notes,Account,Deposit,Withdrawal,Balance", 8, { "Date","Num","Description","Notes","Account","Deposit","Withdrawal","Balance" } },
+ { false, "Date,Num,Description,Notes,Account,Deposit,Withdrawal,Balance", 8, { "Date","Num","Description","Notes","Account","Deposit","Withdrawal","Balance" } },
+ { true, "05/01/15,45,Typical csv import line - including quoted empty field,,Miscellaneous,\"\",\"1,100.00\",", 8, { "05/01/15","45","Typical csv import line - including quoted empty field","","Miscellaneous","","1,100.00","" } },
+ { false, "05/01/15,45,Typical csv import line - including quoted empty field,,Miscellaneous,\"\",\"1,100.00\",", 8, { "05/01/15","45","Typical csv import line - including quoted empty field","","Miscellaneous","","1,100.00","" } },
+ { true, "05/01/15,45,Acme Inc.,,Miscellaneous,", 6, { "05/01/15","45","Acme Inc.","","Miscellaneous","",NULL,NULL } },
+ { false, "05/01/15,45,Acme Inc.,,Miscellaneous,", 6, { "05/01/15","45","Acme Inc.","","Miscellaneous","",NULL,NULL } },
+ { true, "Test\\ with backslash,nextfield", 2, { "Test\\ with backslash","nextfield",NULL,NULL,NULL,NULL,NULL,NULL } },
+ { false, "Test\\ with backslash,nextfield", 2, { "Test\\ with backslash","nextfield",NULL,NULL,NULL,NULL,NULL,NULL } },
+ // This test string is invalid without escape characters because there is only one quote
+ { true, "Test with \\\" escaped quote,nextfield", 2, { "Test with \" escaped quote","nextfield",NULL,NULL,NULL,NULL,NULL,NULL } },
+ { true, "Test with \"\" escaped quote,nextfield", 2, { "Test with \" escaped quote","nextfield",NULL,NULL,NULL,NULL,NULL,NULL } },
+ { false, "Test with \"\" escaped quote,nextfield", 2, { "Test with \" escaped quote","nextfield",NULL,NULL,NULL,NULL,NULL,NULL } },
+ { true,"\"Test (quoted) with \"\" escaped quote\",\"nextfield\"", 2, { "Test (quoted) with \" escaped quote","nextfield",NULL,NULL,NULL,NULL,NULL,NULL } },
+ { true, "\"Unescaped quote test\",nextfield", 2, { "Unescaped quote test","nextfield",NULL,NULL,NULL,NULL,NULL,NULL } },
+ { false, "\"Unescaped quote test\",nextfield", 2, { "Unescaped quote test","nextfield",NULL,NULL,NULL,NULL,NULL,NULL } },
+ // The following has a parse error with escape processing true
+ { false, "\"Unescaped quote test with trailing backslash and escape off\\\",nextfield", 2, { "Unescaped quote test with trailing backslash and escape off\\","nextfield",NULL,NULL,NULL,NULL,NULL,NULL } },
+
+ {true, NULL, 0, { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL } },
};
TEST_F (GncTokenizerTest, tokenize_comma_sep)
@@ -193,10 +206,10 @@ TEST_F (GncTokenizerTest, tokenize_comma_sep)
}
static tokenize_csv_test_data semicolon_separated [] = {
- { "Date;Num;Description;Notes;Account;Deposit;Withdrawal;Balance", 8, { "Date","Num","Description","Notes","Account","Deposit","Withdrawal","Balance" } },
- { "05/01/15;45;Typical csv import line - including quoted empty field;;Miscellaneous;\"\";\"1,100.00\";", 8, { "05/01/15","45","Typical csv import line - including quoted empty field","","Miscellaneous","","1,100.00","" } },
- { "05/01/15;45;Acme Inc.;;Miscellaneous;", 6, { "05/01/15","45","Acme Inc.","","Miscellaneous","",NULL,NULL } },
- { NULL, 0, { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL } },
+ { true, "Date;Num;Description;Notes;Account;Deposit;Withdrawal;Balance", 8, { "Date","Num","Description","Notes","Account","Deposit","Withdrawal","Balance" } },
+ { true, "05/01/15;45;Typical csv import line - including quoted empty field;;Miscellaneous;\"\";\"1,100.00\";", 8, { "05/01/15","45","Typical csv import line - including quoted empty field","","Miscellaneous","","1,100.00","" } },
+ { true, "05/01/15;45;Acme Inc.;;Miscellaneous;", 6, { "05/01/15","45","Acme Inc.","","Miscellaneous","",NULL,NULL } },
+ { true, NULL, 0, { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL } },
};
TEST_F (GncTokenizerTest, tokenize_semicolon_sep)
{
Summary of changes:
.../gtkbuilder/assistant-csv-price-import.glade | 21 +++++++++++-
.../gtkbuilder/assistant-csv-trans-import.glade | 21 +++++++++++-
.../csv-imp/assistant-csv-price-import.cpp | 30 ++++++++++++-----
.../csv-imp/assistant-csv-trans-import.cpp | 30 +++++++++++++----
.../import-export/csv-imp/gnc-imp-settings-csv.cpp | 10 ++++++
.../import-export/csv-imp/gnc-imp-settings-csv.hpp | 3 +-
gnucash/import-export/csv-imp/gnc-import-price.cpp | 19 +++++++++--
gnucash/import-export/csv-imp/gnc-import-price.hpp | 3 ++
gnucash/import-export/csv-imp/gnc-import-tx.cpp | 22 ++++++++++--
gnucash/import-export/csv-imp/gnc-import-tx.hpp | 3 ++
.../import-export/csv-imp/gnc-tokenizer-csv.cpp | 27 ++++++++++++---
.../import-export/csv-imp/gnc-tokenizer-csv.hpp | 3 ++
.../import-export/csv-imp/test/test-tokenizer.cpp | 39 ++++++++++++++--------
13 files changed, 189 insertions(+), 42 deletions(-)
More information about the gnucash-changes
mailing list