From 352a9e94534933aca7403e91d1dc55c26bf633ce Mon Sep 17 00:00:00 2001 From: An-Cheng Huang Date: Thu, 2 Dec 2010 14:35:25 -0800 Subject: implement load function in new config input/output infrastructure. * add "commands diff" functionality to config input/output infrastructure. * consolidate similar logic in "commands diff" and "show diff". * add loadFile functionality to cstore using "commands diff". * export loadFile through shell API. --- src/cli_shell_api.cpp | 31 ++- src/cnode/cnode-algorithm.cpp | 487 +++++++++++++++++++++++++++++++++++------- src/cnode/cnode-algorithm.hpp | 15 +- src/cstore/cstore.cpp | 75 +++++++ src/cstore/cstore.hpp | 8 +- 5 files changed, 521 insertions(+), 95 deletions(-) (limited to 'src') diff --git a/src/cli_shell_api.cpp b/src/cli_shell_api.cpp index af0a320..b9b8188 100644 --- a/src/cli_shell_api.cpp +++ b/src/cli_shell_api.cpp @@ -24,6 +24,7 @@ #include #include #include +#include /* This program provides an API for shell scripts (e.g., snippets in * templates, standalone scripts, etc.) to access the CLI cstore library. @@ -405,18 +406,27 @@ showCfg(const vector& args) cnode::CfgNode aroot(cstore, nargs, true, true); if (active_only) { - // just show the active config - cnode::show_diff(aroot, aroot, op_show_show_defaults, - op_show_hide_secrets); - } else if (working_only) { - // just show the working config without diff markers - cnode::CfgNode wroot(cstore, nargs, false, true); - cnode::show_diff(wroot, wroot, op_show_show_defaults, - op_show_hide_secrets); + // just show the active config (no diff) + cnode::show_cfg(aroot, op_show_show_defaults, op_show_hide_secrets); } else { cnode::CfgNode wroot(cstore, nargs, false, true); - cnode::show_diff(aroot, wroot, op_show_show_defaults, - op_show_hide_secrets); + if (working_only) { + // just show the working config (no diff) + cnode::show_cfg(wroot, op_show_show_defaults, op_show_hide_secrets); + } else { + cnode::show_cfg_diff(aroot, wroot, op_show_show_defaults, + op_show_hide_secrets); + } + } +} + +static void +loadFile(const vector& args) +{ + UnionfsCstore cstore(true); + if (!cstore.loadFile(args[0].c_str())) { + // loadFile failed + exit(1); } } @@ -463,6 +473,7 @@ static OpT ops[] = { OP(validateTmplValPath, -1, NULL, 1, "Must specify config path"), OP(showCfg, -1, NULL, -1, NULL), + OP(loadFile, 1, "Must specify config file", -1, NULL), {NULL, -1, NULL, -1, NULL, NULL} }; diff --git a/src/cnode/cnode-algorithm.cpp b/src/cnode/cnode-algorithm.cpp index 4480352..3a7ccdd 100644 --- a/src/cnode/cnode-algorithm.cpp +++ b/src/cnode/cnode-algorithm.cpp @@ -39,6 +39,133 @@ static void _show_diff(CfgNode *cfg1, CfgNode *cfg2, int level, bool show_def, bool hide_secret); +static void +_get_cmds_diff(CfgNode *cfg1, CfgNode *cfg2, vector& cur_path, + vector >& del_list, + vector >& set_list, + vector >& com_list); + +/* compare the values of a "multi" node in the two configs. the values and + * the "prefix" of each value are returned in "values" and "pfxs", + * respectively. + * + * return value indicates whether the node is different in the two configs. + * + * comparison follows the original perl logic. + */ +static bool +_cmp_multi_values(CfgNode *cfg1, CfgNode *cfg2, vector& values, + vector& pfxs) +{ + const vector& ovec = cfg1->getValues(); + const vector& nvec = cfg2->getValues(); + Cstore::MapT nmap; + bool changed = false; + for (size_t i = 0; i < nvec.size(); i++) { + nmap[nvec[i]] = true; + } + Cstore::MapT omap; + for (size_t i = 0; i < ovec.size(); i++) { + omap[ovec[i]] = true; + if (nmap.find(ovec[i]) == nmap.end()) { + values.push_back(ovec[i]); + pfxs.push_back(PFX_DIFF_DEL.c_str()); + changed = true; + } + } + + for (size_t i = 0; i < nvec.size(); i++) { + values.push_back(nvec[i]); + if (omap.find(nvec[i]) == omap.end()) { + pfxs.push_back(PFX_DIFF_ADD.c_str()); + changed = true; + } else if (i < ovec.size() && nvec[i] == ovec[i]) { + pfxs.push_back(PFX_DIFF_NONE.c_str()); + } else { + pfxs.push_back(PFX_DIFF_UPD.c_str()); + changed = true; + } + } + + return changed; +} + +static void +_cmp_non_leaf_nodes(CfgNode *cfg1, CfgNode *cfg2, vector& rcnodes1, + vector& rcnodes2, bool& not_tag_node, + bool& is_value, bool& is_leaf_typeless, + string& name, string& value) +{ + CfgNode *cfg = (cfg1 ? cfg1 : cfg2); + is_value = cfg->isValue(); + not_tag_node = (!cfg->isTag() || is_value); + is_leaf_typeless = cfg->isLeafTypeless(); + bool is_tag_node = !not_tag_node; + name = cfg->getName(); + if (is_value) { + value = cfg->getValue(); + } + + // handle child nodes + vector cnodes1, cnodes2; + if (cfg1) { + cnodes1 = cfg1->getChildNodes(); + } + if (cfg2) { + cnodes2 = cfg2->getChildNodes(); + } + + Cstore::MapT map; + Cstore::MapT nmap1, nmap2; + for (size_t i = 0; i < cnodes1.size(); i++) { + string key = (is_tag_node + ? cnodes1[i]->getValue() : cnodes1[i]->getName()); + map[key] = true; + nmap1[key] = cnodes1[i]; + } + for (size_t i = 0; i < cnodes2.size(); i++) { + string key = (is_tag_node + ? cnodes2[i]->getValue() : cnodes2[i]->getName()); + map[key] = true; + nmap2[key] = cnodes2[i]; + } + + vector cnodes; + Cstore::MapT::iterator it = map.begin(); + for (; it != map.end(); ++it) { + cnodes.push_back((*it).first); + } + Cstore::sortNodes(cnodes); + + for (size_t i = 0; i < cnodes.size(); i++) { + bool in1 = (nmap1.find(cnodes[i]) != nmap1.end()); + bool in2 = (nmap2.find(cnodes[i]) != nmap2.end()); + CfgNode *c1 = (in1 ? nmap1[cnodes[i]] : NULL); + CfgNode *c2 = (in2 ? nmap2[cnodes[i]] : NULL); + rcnodes1.push_back(c1); + rcnodes2.push_back(c2); + } +} + +static void +_add_path_to_list(vector >& list, vector& path, + const string *nptr, const string *vptr) +{ + if (nptr) { + path.push_back(*nptr); + } + if (vptr) { + path.push_back(*vptr); + } + list.push_back(path); + if (vptr) { + path.pop_back(); + } + if (nptr) { + path.pop_back(); + } +} + static void _print_value_str(const string& name, const char *vstr, bool hide_secret) { @@ -161,35 +288,9 @@ _diff_check_and_show_leaf(CfgNode *cfg1, CfgNode *cfg2, int level, } } else { // need to actually do a diff. - // this follows the original perl logic. - const vector& ovec = cfg1->getValues(); - const vector& nvec = cfg2->getValues(); vector values; vector pfxs; - Cstore::MapT nmap; - for (size_t i = 0; i < nvec.size(); i++) { - nmap[nvec[i]] = true; - } - Cstore::MapT omap; - for (size_t i = 0; i < ovec.size(); i++) { - omap[ovec[i]] = true; - if (nmap.find(ovec[i]) == nmap.end()) { - values.push_back(ovec[i]); - pfxs.push_back(PFX_DIFF_DEL.c_str()); - } - } - - for (size_t i = 0; i < nvec.size(); i++) { - values.push_back(nvec[i]); - if (omap.find(nvec[i]) == omap.end()) { - pfxs.push_back(PFX_DIFF_ADD.c_str()); - } else if (i < ovec.size() && nvec[i] == ovec[i]) { - pfxs.push_back(PFX_DIFF_NONE.c_str()); - } else { - pfxs.push_back(PFX_DIFF_UPD.c_str()); - } - } - + _cmp_multi_values(cfg1, cfg2, values, pfxs); for (size_t i = 0; i < values.size(); i++) { _diff_print_indent(cfg1, cfg2, level, pfxs[i]); printf("%s ", cfg->getName().c_str()); @@ -224,13 +325,10 @@ static void _diff_show_other(CfgNode *cfg1, CfgNode *cfg2, int level, bool show_def, bool hide_secret) { - CfgNode *cfg = NULL; const char *pfx_diff = PFX_DIFF_NONE.c_str(); if (!cfg1) { - cfg = cfg2; pfx_diff = PFX_DIFF_ADD.c_str(); } else { - cfg = cfg1; if (!cfg2) { pfx_diff = PFX_DIFF_DEL.c_str(); } else if (cfg1 == cfg2) { @@ -238,76 +336,41 @@ _diff_show_other(CfgNode *cfg1, CfgNode *cfg2, int level, bool show_def, } } + string name, value; + bool not_tag_node, is_value, is_leaf_typeless; + vector rcnodes1, rcnodes2; + _cmp_non_leaf_nodes(cfg1, cfg2, rcnodes1, rcnodes2, not_tag_node, is_value, + is_leaf_typeless, name, value); + /* only print "this" node if it * (1) is a tag value or an intermediate node, * (2) is not "root", and * (3) has a "name". */ - const string& name = cfg->getName(); - bool print_this = (((cfg1 && (!cfg1->isTag() || cfg1->isValue())) - || (cfg2 && (!cfg2->isTag() || cfg2->isValue()))) - && level >= 0 && name.size() > 0); + bool print_this = (not_tag_node && level >= 0 && name.size() > 0); if (print_this) { _diff_print_comment(cfg1, cfg2, level); _diff_print_indent(cfg1, cfg2, level, pfx_diff); - if (cfg->isValue()) { + if (is_value) { // at tag value - printf("%s %s", name.c_str(), cfg->getValue().c_str()); + printf("%s %s", name.c_str(), value.c_str()); } else { // at intermediate node printf("%s", name.c_str()); } - printf("%s\n", (cfg->isLeafTypeless() ? "" : " {")); + printf("%s\n", (is_leaf_typeless ? "" : " {")); } - // handle child nodes - vector cnodes1, cnodes2; - if (cfg1) { - cnodes1 = cfg1->getChildNodes(); - } - if (cfg2) { - cnodes2 = cfg2->getChildNodes(); - } - - Cstore::MapT map; - Cstore::MapT nmap1, nmap2; - for (size_t i = 0; i < cnodes1.size(); i++) { - string key - = ((cfg->isTag() && !cfg->isValue()) - ? cnodes1[i]->getValue() : cnodes1[i]->getName()); - map[key] = true; - nmap1[key] = cnodes1[i]; - } - for (size_t i = 0; i < cnodes2.size(); i++) { - string key - = ((cfg->isTag() && !cfg->isValue()) - ? cnodes2[i]->getValue() : cnodes2[i]->getName()); - map[key] = true; - nmap2[key] = cnodes2[i]; - } - - vector cnodes; - Cstore::MapT::iterator it = map.begin(); - for (; it != map.end(); ++it) { - cnodes.push_back((*it).first); - } - Cstore::sortNodes(cnodes); - - for (size_t i = 0; i < cnodes.size(); i++) { - bool in1 = (nmap1.find(cnodes[i]) != nmap1.end()); - bool in2 = (nmap2.find(cnodes[i]) != nmap2.end()); - CfgNode *c1 = (in1 ? nmap1[cnodes[i]] : NULL); - CfgNode *c2 = (in2 ? nmap2[cnodes[i]] : NULL); - + for (size_t i = 0; i < rcnodes1.size(); i++) { int next_level = level + 1; if (!print_this) { next_level = (level >= 0 ? level : 0); } - _show_diff(c1, c2, next_level, show_def, hide_secret); + _show_diff(rcnodes1[i], rcnodes2[i], next_level, show_def, hide_secret); } // finish printing "this" node if necessary - if (print_this && !cfg->isLeafTypeless()) { + if (print_this && !is_leaf_typeless) { _diff_print_indent(cfg1, cfg2, level, pfx_diff); printf("}\n"); } @@ -353,11 +416,227 @@ _show_diff(CfgNode *cfg1, CfgNode *cfg2, int level, bool show_def, } } +static void +_get_comment_diff_cmd(CfgNode *cfg1, CfgNode *cfg2, vector& cur_path, + vector >& com_list, const string *val) +{ + const string *comment = NULL; + const string *name = NULL; + string empty = ""; + string c1, c2; + if (cfg1 != cfg2) { + c1 = (cfg1 ? cfg1->getComment() : ""); + c2 = (cfg2 ? cfg2->getComment() : ""); + if (c1 != "") { + name = &(cfg1->getName()); + if (c2 != "") { + // in both + if (c1 != c2) { + // updated + comment = &c2; + } + } else { + // only in cfg1 => deleted + comment = ∅ + } + } else { + if (c2 != "") { + // only in cfg2 => added + name = &(cfg2->getName()); + comment = &c2; + } + } + } else { + // cfg1 == cfg2 => just getting all commands + c1 = cfg1->getComment(); + if (c1 != "") { + name = &(cfg1->getName()); + comment = &c1; + } + } + if (comment) { + if (val) { + cur_path.push_back(*name); + name = val; + } + _add_path_to_list(com_list, cur_path, name, comment); + if (val) { + cur_path.pop_back(); + } + } +} + +static bool +_get_cmds_diff_leaf(CfgNode *cfg1, CfgNode *cfg2, vector& cur_path, + vector >& del_list, + vector >& set_list, + vector >& com_list) +{ + if ((cfg1 && !cfg1->isLeaf()) || (cfg2 && !cfg2->isLeaf())) { + // not a leaf node + return false; + } + + CfgNode *cfg = NULL; + vector > *list = NULL; + if (cfg1) { + cfg = cfg1; + if (!cfg2) { + // exists in cfg1 but not in cfg2 => delete and stop recursion + _add_path_to_list(del_list, cur_path, &(cfg1->getName()), NULL); + return true; + } else if (cfg1 == cfg2) { + // same config => just translating config to set commands + list = &set_list; + } + } else { + // !cfg1 => cfg2 must not be NULL + cfg = cfg2; + list = &set_list; + } + + _get_comment_diff_cmd(cfg1, cfg2, cur_path, com_list, NULL); + if (cfg->isMulti()) { + // multi-value node + if (list) { + const vector& vvec = cfg->getValues(); + for (size_t i = 0; i < vvec.size(); i++) { + _add_path_to_list(*list, cur_path, &(cfg->getName()), &(vvec[i])); + } + } else { + // need to actually do a diff. + vector dummy_vals; + vector dummy_pfxs; + if (_cmp_multi_values(cfg1, cfg2, dummy_vals, dummy_pfxs)) { + /* something changed. to get the correct ordering for multi-node + * values, need to delete the node and then set the new values. + */ + const vector& nvec = cfg2->getValues(); + _add_path_to_list(del_list, cur_path, &(cfg->getName()), NULL); + for (size_t i = 0; i < nvec.size(); i++) { + _add_path_to_list(set_list, cur_path, &(cfg->getName()), &(nvec[i])); + } + } + } + } else { + // single-value node + string val = cfg->getValue(); + if (!list) { + const string& val1 = cfg1->getValue(); + val = cfg2->getValue(); + if (val != val1) { + // changed => need to set it + list = &set_list; + } + } + if (list) { + _add_path_to_list(*list, cur_path, &(cfg->getName()), &val); + } + } + + return true; +} + +static void +_get_cmds_diff_other(CfgNode *cfg1, CfgNode *cfg2, vector& cur_path, + vector >& del_list, + vector >& set_list, + vector >& com_list) +{ + vector > *list = NULL; + if (cfg1) { + if (!cfg2) { + // exists in cfg1 but not in cfg2 => delete and stop recursion + _add_path_to_list(del_list, cur_path, &(cfg1->getName()), + (cfg1->isValue() ? &(cfg1->getValue()) : NULL)); + return; + } else if (cfg1 == cfg2) { + // same config => just translating config to set commands + list = &set_list; + } + } else { + // !cfg1 => cfg2 must not be NULL + list = &set_list; + } + + string name, value; + bool not_tag_node, is_value, is_leaf_typeless; + vector rcnodes1, rcnodes2; + _cmp_non_leaf_nodes(cfg1, cfg2, rcnodes1, rcnodes2, not_tag_node, is_value, + is_leaf_typeless, name, value); + if (rcnodes1.size() < 1 && list) { + // subtree is empty + _add_path_to_list(*list, cur_path, &name, (is_value ? &value : NULL)); + return; + } + + bool add_this = (not_tag_node && name.size() > 0); + if (add_this) { + const string *val = (is_value ? &value : NULL); + _get_comment_diff_cmd(cfg1, cfg2, cur_path, com_list, val); + + cur_path.push_back(name); + if (is_value) { + cur_path.push_back(value); + } + } + for (size_t i = 0; i < rcnodes1.size(); i++) { + _get_cmds_diff(rcnodes1[i], rcnodes2[i], cur_path, del_list, set_list, + com_list); + } + if (add_this) { + if (is_value) { + cur_path.pop_back(); + } + cur_path.pop_back(); + } +} + +static void +_get_cmds_diff(CfgNode *cfg1, CfgNode *cfg2, vector& cur_path, + vector >& del_list, + vector >& set_list, + vector >& com_list) +{ + // if doesn't exist, treat as NULL + if (cfg1 && !cfg1->exists()) { + cfg1 = NULL; + } + if (cfg2 && !cfg2->exists()) { + cfg2 = NULL; + } + + if (!cfg1 && !cfg2) { + fprintf(stderr, "_get_cmds_diff error (both config NULL)\n"); + exit(1); + } + + if (_get_cmds_diff_leaf(cfg1, cfg2, cur_path, del_list, set_list, + com_list)) { + // leaf node has been shown. done. + return; + } else { + // intermediate node, tag node, or tag value + _get_cmds_diff_other(cfg1, cfg2, cur_path, del_list, set_list, com_list); + } +} + +static void +_print_cmds_list(const char *op, vector >& list) +{ + for (size_t i = 0; i < list.size(); i++) { + printf("%s", op); + for (size_t j = 0; j < list[i].size(); j++) { + printf(" '%s'", list[i][j].c_str()); + } + printf("\n"); + } +} ////// algorithms void -cnode::show_diff(const CfgNode& cfg1, const CfgNode& cfg2, bool show_def, - bool hide_secret) +cnode::show_cfg_diff(const CfgNode& cfg1, const CfgNode& cfg2, bool show_def, + bool hide_secret) { if (cfg1.isInvalid() || cfg2.isInvalid()) { printf("Specified configuration path is not valid\n"); @@ -372,3 +651,51 @@ cnode::show_diff(const CfgNode& cfg1, const CfgNode& cfg2, bool show_def, show_def, hide_secret); } +void +cnode::show_cfg(const CfgNode& cfg, bool show_def, bool hide_secret) +{ + show_cfg_diff(cfg, cfg, show_def, hide_secret); +} + +void +cnode::show_cmds_diff(const CfgNode& cfg1, const CfgNode& cfg2) +{ + vector cur_path; + vector > del_list; + vector > set_list; + vector > com_list; + _get_cmds_diff(const_cast(&cfg1), const_cast(&cfg2), + cur_path, del_list, set_list, com_list); + + _print_cmds_list("delete", del_list); + _print_cmds_list("set", set_list); + _print_cmds_list("comment", com_list); +} + +void +cnode::show_cmds(const CfgNode& cfg) +{ + show_cmds_diff(cfg, cfg); +} + +void +cnode::get_cmds_diff(const CfgNode& cfg1, const CfgNode& cfg2, + vector >& del_list, + vector >& set_list, + vector >& com_list) +{ + vector cur_path; + _get_cmds_diff(const_cast(&cfg1), const_cast(&cfg2), + cur_path, del_list, set_list, com_list); +} + +void +cnode::get_cmds(const CfgNode& cfg, vector >& set_list, + vector >& com_list) +{ + vector cur_path; + vector > del_list; + _get_cmds_diff(const_cast(&cfg), const_cast(&cfg), + cur_path, del_list, set_list, com_list); +} + diff --git a/src/cnode/cnode-algorithm.hpp b/src/cnode/cnode-algorithm.hpp index 86bce81..07faf07 100644 --- a/src/cnode/cnode-algorithm.hpp +++ b/src/cnode/cnode-algorithm.hpp @@ -21,8 +21,19 @@ namespace cnode { -void show_diff(const CfgNode& cfg1, const CfgNode& cfg2, bool show_def, - bool hide_secret); +void show_cfg_diff(const CfgNode& cfg1, const CfgNode& cfg2, bool show_def, + bool hide_secret); +void show_cfg(const CfgNode& cfg, bool show_def, bool hide_secret); + +void show_cmds_diff(const CfgNode& cfg1, const CfgNode& cfg2); +void show_cmds(const CfgNode& cfg); + +void get_cmds_diff(const CfgNode& cfg1, const CfgNode& cfg2, + vector >& del_list, + vector >& set_list, + vector >& com_list); +void get_cmds(const CfgNode& cfg, vector >& set_list, + vector >& com_list); } // namespace cnode diff --git a/src/cstore/cstore.cpp b/src/cstore/cstore.cpp index ed18c9e..0ff8ba2 100644 --- a/src/cstore/cstore.cpp +++ b/src/cstore/cstore.cpp @@ -31,6 +31,9 @@ #include #include #include +#include +#include +#include ////// constants @@ -1803,6 +1806,63 @@ Cstore::unmarkCfgPathDeactivated(const vector& path_comps) return ret; } +// load specified config file +bool +Cstore::loadFile(const char *filename) +{ + if (!inSession()) { + output_user("Cannot load config outside configuration session\n"); + // exit handled by assert below + } + ASSERT_IN_SESSION; + + FILE *fin = fopen(filename, "r"); + if (!fin) { + output_user("Failed to open specified config file\n"); + return false; + } + + // get the config tree from the file + cnode::CfgNode *froot = cparse::parse_file(fin, *this); + if (!froot) { + output_user("Failed to parse specified config file\n"); + return false; + } + + // get the config tree from the active config + vector args; + cnode::CfgNode aroot(*this, args, true, true); + + // get the "commands diff" between the two + vector > del_list; + vector > set_list; + vector > com_list; + cnode::get_cmds_diff(aroot, *froot, del_list, set_list, com_list); + + // "apply" the changes to the working config + for (size_t i = 0; i < del_list.size(); i++) { + vtw_def def; + if (!validateDeletePath(del_list[i], def) + || !deleteCfgPath(del_list[i], def)) { + print_str_vec("Delete [", "] failed\n", del_list[i], "'"); + } + } + for (size_t i = 0; i < set_list.size(); i++) { + if (!validateSetPath(set_list[i]) || !setCfgPath(set_list[i])) { + print_str_vec("Set [", "] failed\n", set_list[i], "'"); + } + } + for (size_t i = 0; i < com_list.size(); i++) { + vtw_def def; + if (!validateCommentArgs(com_list[i], def) + || !commentCfgPath(com_list[i], def)) { + print_str_vec("Comment [", "] failed\n", com_list[i], "'"); + } + } + + return true; +} + /* "changed" status handling. * the "changed" status is used during commit to check if a node has been * changed. note that if a node is "changed", all of its ancestors are also @@ -2797,3 +2857,18 @@ Cstore::shell_escape_squotes(string& str) } } +// print a vector of strings +void +Cstore::print_str_vec(const char *pre, const char *post, + const vector& vec, const char *quote) +{ + output_user("%s", pre); + for (size_t i = 0; i < vec.size(); i++) { + if (i > 0) { + output_user(" "); + } + output_user("%s%s%s", quote, vec[i].c_str(), quote); + } + output_user("%s", post); +} + diff --git a/src/cstore/cstore.hpp b/src/cstore/cstore.hpp index 267dabe..d1d01fa 100644 --- a/src/cstore/cstore.hpp +++ b/src/cstore/cstore.hpp @@ -112,7 +112,7 @@ public: * edit-related commands (invoked from shell functions) * completion-related (for completion script) * session-related (setup, teardown, etc.) - * load (XXX currently still implemented in perl) + * load * * these operate on the "working config" and the session and MUST NOT * be used by anything other than the listed operations. @@ -165,8 +165,8 @@ public: virtual bool inSession() = 0; // commit bool unmarkCfgPathChanged(const vector& path_comps); - // XXX load - //bool unmarkCfgPathDeactivatedDescendants(const vector& path_comps); + // load + bool loadFile(const char *filename); /****** * these functions are observers of the current "working config" or @@ -476,6 +476,8 @@ private: // util functions string get_shell_prompt(const string& level); void shell_escape_squotes(string& str); + void print_str_vec(const char *pre, const char *post, + const vector& vec, const char *quote); }; #endif /* _CSTORE_H_ */ -- cgit v1.2.3