libjmmcg  release_579_6_g8cffd
A C++ library containing an eclectic mix of useful, advanced components.
cmd_line_processing.hpp
Go to the documentation of this file.
1 /******************************************************************************
2 ** Copyright © 2002 by J.M.McGuiness, coder@hussar.me.uk
3 **
4 ** This library is free software; you can redistribute it and/or
5 ** modify it under the terms of the GNU Lesser General Public
6 ** License as published by the Free Software Foundation; either
7 ** version 2.1 of the License, or (at your option) any later version.
8 **
9 ** This library is distributed in the hope that it will be useful,
10 ** but WITHOUT ANY WARRANTY; without even the implied warranty of
11 ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 ** Lesser General Public License for more details.
13 **
14 ** You should have received a copy of the GNU Lesser General Public
15 ** License along with this library; if not, write to the Free Software
16 ** Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17 */
18 
19 #include "logging.hpp"
20 #include "shared_ptr.hpp"
21 #include <libxml++/libxml++.h>
22 
23 namespace jmmcg { namespace LIBJMMCG_VER_NAMESPACE {
24 
25  template<typename InputIterator,typename ValueType> inline std::pair<bool,InputIterator>
26  BestStringMatch(const InputIterator &matches_begin,const InputIterator &matches_end,const ValueType &str) {
27  typedef std::map<typename ValueType::size_type,std::vector<InputIterator> > comparison_sets_type;
28  comparison_sets_type comparison_sets;
29  // Construct a map of comparison values to sets of strings that match at that edit distance.
30  for (InputIterator i(matches_begin);i!=matches_end;++i) {
31  const ValueType tmp(*i);
32  const typename comparison_sets_type::key_type edit_dist=tmp.compare(str);
33 // JMMCG_TRACE(_T("Comparing to: ")<<tmp<<_T(", result: ")<<tostring(edit_dist));
34  if (edit_dist<tmp.size()) {
35 // JMMCG_TRACE(_T("Adding value: ")<<tmp<<_T(", to key: ")<<tostring(edit_dist));
36  comparison_sets[edit_dist].push_back(i);
37  }
38  }
39  // See if there is an exact match, i.e. zero. Due to the ordering properties of of a map, this will be the first element.
40  if (comparison_sets.begin()->first==0) {
41  assert(!comparison_sets.begin()->second.empty());
42  // Is it ambiguous? i.e. is there more than one item in the matching set?
43  // Unique match means only one element in the set, ambiguous morethan one, return it indicated thusly.
44  return std::make_pair(comparison_sets.begin()->second.size()>1,*comparison_sets.begin()->second.begin());
45  } else {
46  // O.k. no exact match - due to the curious properties of "std::string::compare(...)" we now choose the one with the highest value. As a value of 1 seems to indicate no match. We've excluded complete nonsense matches by storing only those comparison values that are less than the target string length.
47  // As we want the highest value, this will be the last element in the map.
48  const typename comparison_sets_type::mapped_type &best_matches=(typename comparison_sets_type::const_iterator(comparison_sets.end()))->second;
49 // JMMCG_TRACE(_T("No. matches: ")<<tostring(best_matches.size())<<_T(", first match: ")+tostring(*best_matches.begin()));
50  assert(!best_matches.empty());
51  return std::make_pair(best_matches.size()>1,*best_matches.begin());
52  }
53  }
54 
56  public:
57  __stdcall CmdLineParamsData();
59  __stdcall ~CmdLineParamsData() {
60  }
61 
62  protected:
63  class ParamType : protected non_assignable {
64  public:
65  const tstring param;
67 
68  __stdcall ParamType(const tstring &p,const tstring &d)
69  : param(p),description(d) {
70  }
71  __stdcall ParamType(const ParamType &pt)
73  }
74  __stdcall ~ParamType() {
75  }
76  };
77 
78  static const ParamType params[];
79  static const tchar * const authors[];
80  static const tchar version[];
81  static const tchar copyright[];
82  static const tchar warranty[];
83  static const tchar contact_details[];
84  static const tchar help_text[];
87  };
88 
89  template<typename ProgOpts,class Except_>
90  class CmdLineProcessorBase : virtual protected CmdLineParamsData {
91  public:
92  typedef ProgOpts ProgramOptions;
93  typedef Except_ exception_type;
94 
97  virtual __stdcall ~CmdLineProcessorBase(void) {
98  }
99 
100  virtual bool __fastcall ProcessParams(const unsigned int argv,const tchar * const * const argc,tostream &o);
101 
102  const shared_ptr<Logger<exception>,exception> & __fastcall Log(void) const noexcept(true) {
103  assert(logger);
104  return logger;
105  }
106 
107  protected:
109  const tchar *app_name;
110 
111  virtual bool __fastcall ProcessParamInternal(const unsigned int argv,const tchar * const * const argc,unsigned long &i,tostream &o);
112  virtual void __fastcall DumpHelp(tostream &o) const;
113 
114  private:
115  shared_ptr<Logger<exception>,exception> logger;
116  const ParamType log_param;
117 
118  /// Instantiate this function to implement your own processing for you command line parameters.
119  /**
120  * i.e. when you recognise a parameter, set the appropriate value(s) in the "prog_opts" object.
121  * You can adjust the value of the pointer into the list of parameters as you like.
122  * For example to finish processing, set it to any value > argc, e.g. "std::numeric_limits<unsigned long>::max()".
123  */
124  virtual bool __fastcall ProcessParam(const unsigned long recognised_param,const tchar * const * const argc,unsigned long &i,tostream &o)=0;
125 
126  /// Override this to perform post-processing after the command-line arguments have been processed, such as loading the contents of a configuration file, if the default configuration file name is to be used.
127  virtual bool __fastcall PostProcessParams(tostream &) {
128  return true;
129  }
130 
131  static tstring MakeLogLevelHelpStr(void);
132  bool ProcessLogLevel(const unsigned int,const tchar * const * const,unsigned long &,tostream &);
133  void DumpUnknownArg(const tchar * const arg,tostream &o) const;
134  void DumpLogHelp(tostream &o) const;
135  void DumpVersion(tostream &o) const;
136  };
137 
138  /// A specialization for helping to making use of configuration files.
139  template<typename ProgOpts,class Except_>
140  class ConfigFile : virtual public CmdLineProcessorBase<ProgOpts,Except_> {
141  public:
142  typedef typename CmdLineProcessorBase<ProgOpts,Except_>::ProgramOptions ProgramOptions;
143  typedef typename CmdLineProcessorBase<ProgOpts,Except_>::exception exception_t;
144 
145  explicit __stdcall ConfigFile(ProgramOptions &p);
146  ConfigFile(ConfigFile const &)=delete;
147  virtual __stdcall ~ConfigFile() {
148  }
149 
150  protected:
151  virtual bool __fastcall ProcessParamInternal(const unsigned int argv,const tchar * const * const argc,unsigned long &i,tostream &o);
152  virtual void __fastcall DumpHelp(tostream &o) const;
153 
154  private:
155  typedef typename file<tifstream, Except_::thread_traits::api_params_type::api_type, typename Except_::thread_traits::model_type> config_file_type;
156  typedef CmdLineParamsData::ParamType ParamType;
157 
158  const ParamType config_param;
159  tstring config_file_path;
160 
161  /// Implement this function to process the various data nodes read from the configuration file.
162  /**
163  * \return "false" for success, otherwise it failed.
164  */
165  virtual bool __fastcall PostProcessConfigFileItems(const xmlpp::NodeSet &,tostream &) = 0;
166 
167  bool __fastcall PostProcessParams(tostream &);
168  xmlpp::NodeSet __fastcall CheckConfigFile(xmlpp::DomParser &,tostream &) const;
169  xmlpp::NodeSet __fastcall CheckConfigFileValid(config_file_type &,xmlpp::DomParser &,tostream &) const;
170 
171  tstring __fastcall DefaultConfigFilePath(void) const;
172  };
173 
174  inline
176  : help_param(_T("--help"),_T("Show this help information.")),
177  version_param(_T("--version"),_T("Display the version & copyright information.")) {
178  }
179 
180  template<typename ProgOpts,class Except_> inline
182  : prog_opts(p),app_name('\0'),logger(),
183  log_param(_T("--loglevel"),MakeLogLevelHelpStr()) {
184  assert(!help_param.param.empty() && !help_param.description.empty());
185  assert(!log_param.param.empty() && !log_param.description.empty());
186  assert(!version_param.param.empty() && !version_param.description.empty());
187  assert(copyright);
188  assert(warranty);
189  assert(contact_details);
190  assert(help_text);
191  // If this assertion fails the number of strings in the "log_level_strs" array does not match the number of enumeration values in the "log_level_type" enum.
192  // The "+1" is because of the zero-based indexing of arrays. There's n of them, but the last one is at [n-1].
193  assert(sizeof(log_level_strs)/sizeof(tchar *)==static_cast<size_t>(log_info+1));
194  }
195 
196  template<typename ProgOpts,class Except_> inline bool
197  CmdLineProcessorBase<ProgOpts,Except_>::ProcessParams(const unsigned int argv, const tchar * const * const argc,tostream &o) {
198  assert(argv>=0);
199  assert(argc && argc[0]);
200  app_name=argc[0];
201  assert(app_name);
202  logger.reset(new Logger<exception>(_T("/var/log/")+tstring(app_name).substr(tstring(app_name).rfind(_T("/"))+1,tstring::npos)+_T(".log"),app_name));
203  for (unsigned long i=1;i<argv;++i) {
204  assert(argc[i]);
205  if (!ProcessParamInternal(argv,argc,i,o)) {
206  return false;
207  }
208  }
209  return PostProcessParams(o);
210  }
211 
212  template<typename ProgOpts,class Except_> inline bool
213  CmdLineProcessorBase<ProgOpts,Except_>::ProcessParamInternal(const unsigned int argv,const tchar * const * const argc,unsigned long &i,tostream &o) {
214  assert(argc);
215  assert(argc[i]);
216  std::vector<std::string> args(3+sizeof(params)/sizeof(ParamType));
217  args[0]=help_param.param;
218  args[1]=log_param.param;
219  args[2]=version_param.param;
220  for (std::vector<std::string>::size_type j=3;j<args.size();++j) {
221  args[j]=params[j-3].param;
222  }
223  const std::pair<bool,std::vector<std::string>::const_iterator> result(BestStringMatch(args.begin(),args.end(),tstring(argc[i])));
224  JMMCG_TRACE(_T("Matching parameter: ")<<*result.second);
225  if (*result.second==help_param.param) {
226  DumpHelp(o);
227  } else if (*result.second==log_param.param) {
228  if (!ProcessLogLevel(argv,argc,i,o)) {
229  DumpLogHelp(o);
230  return false;
231  }
232  } else if (*result.second==version_param.param) {
233  DumpVersion(o);
234  } else if (!ProcessParam(result.second-args.begin()-3,argc,i,o)) {
235  DumpUnknownArg(argc[i],o);
236  return false;
237  }
238  return true;
239  }
240 
241  template<typename ProgOpts,class Except_> inline void
242  CmdLineProcessorBase<ProgOpts,Except_>::DumpHelp(tostream &o) const {
243  assert(app_name);
244  const tchar options_str[]=_T("[options]");
245  o<<_T("Usage is:\n");
246  o<<app_name;
247  o<<_T(" ")<<options_str<<_T("\nWhere ")<<options_str<<_T(" must unambiguously match one (or more) of the following strings:\n");
248  o<<_T("\t")<<help_param.param<<_T("\t\t")<<help_param.description<<_T("\n");
249  DumpLogHelp(o);
250  o<<_T("\t")<<version_param.param<<_T("\t")<<version_param.description<<_T("\n");
251  for (unsigned long i=0;i<(sizeof(params)/sizeof(ParamType));++i) {
252  assert(!params[i].param.empty() && !params[i].description.empty());
253  o<<_T("\t")<<params[i].param<<_T("\t")<<params[i].description<<std::endl;
254  }
255  }
256 
257  template<typename ProgOpts,class Except_> inline tstring
258  CmdLineProcessorBase<ProgOpts,Except_>::MakeLogLevelHelpStr(void) {
259  const tchar param_str[]=_T("level");
260  const unsigned long penultimate=(sizeof(log_level_strs)/sizeof(tchar*)-1);
261  tostringstream ss;
262  ss<<_T(" ")<<param_str<<_T("\tSet the logging level to '")<<param_str<<_T("'={");
263  for (unsigned long i=0;i<penultimate;++i) {
264  ss<<log_level_strs[i]<<_T("|");
265  }
266  ss<<log_level_strs[penultimate]<<_T("}, which must match unambiguously, the default is '")<<log_level_strs[0]<<_T("'; each level logs increasingly more information. The log file is: '");
267  return ss.str();
268  }
269 
270  template<typename ProgOpts,class Except_> inline bool
271  CmdLineProcessorBase<ProgOpts,Except_>::ProcessLogLevel(const unsigned int argv,const tchar * const * const argc,unsigned long &i,tostream &o) {
272  if (++i<=argv && argc[i] && !tstring(argc[i]).empty()) {
273  const std::pair<bool,const tchar * const *> result(BestStringMatch(&(log_level_strs[0]),&(log_level_strs[sizeof(log_level_strs)/sizeof(tchar*)]),tstring(argc[i])));
274  if (result.first) {
275  o<<_T("Ambiguous match for supplied 'level' parameter: '")<<argc[i]<<_T("'.\n");
276  DumpHelp(o);
277  } else {
278 // JMMCG_TRACE(_T("Matched with parameter: ")<<tostring(*result.second));
279  logger->Level(static_cast<log_level_type>(result.second-&(log_level_strs[0])));
280 // JMMCG_TRACE(_T("Resultant log level: ")<<tostring(logger->Level()));
281  }
282  return true;
283  o<<_T("Unrecognised 'level' parameter: '")<<argc[i]<<_T("' for ")<<log_param.param<<_T(" option.\n");
284  DumpHelp(o);
285  return true;
286  }
287  o<<_T("Missing 'level' parameter for ")<<log_param.param<<_T(" option.\n");
288  DumpHelp(o);
289  return true;
290  }
291 
292  template<typename ProgOpts,class Except_> inline void
293  CmdLineProcessorBase<ProgOpts,Except_>::DumpUnknownArg(const tchar * const arg,tostream &o) const {
294  o<<_T("Unrecognised command line parameter '")<<arg<<_T("'.\n");
295  DumpHelp(o);
296  }
297 
298  template<typename ProgOpts,class Except_> inline void
299  CmdLineProcessorBase<ProgOpts,Except_>::DumpLogHelp(tostream &o) const {
300  o<<_T("\t")<<log_param.param<<log_param.description<<logger->Path()<<_T("'.\n");
301  }
302 
303  template<typename ProgOpts,class Except_> inline void
304  CmdLineProcessorBase<ProgOpts,Except_>::DumpVersion(tostream &o) const {
305  assert(app_name);
306  o<<app_name<<_T(", Version: ")<<version<<_T("\n");
307  o<<_T("Author(s):\n");
308  for (unsigned long i=0;i<(sizeof(authors)/sizeof(tstring *));++i) {
309  assert(authors[i]);
310  o<<_T("\t")<<authors[i]<<_T("\n");
311  }
312  assert(copyright);
313  o<<_T("Copyright © ")<<copyright<<_T("\n");
314  assert(warranty);
315  o<<_T("Warranty Information: ")<<warranty<<_T("\n");
316  assert(contact_details);
317  o<<_T("Contact details: ")<<contact_details<<_T("\n");
318  assert(help_text);
319  o<<_T("Brief description:\n")<<help_text<<std::endl;
320  }
321 
322  template<typename ProgOpts,class Except_> inline
323  ConfigFile<ProgOpts,Except_>::ConfigFile(ProgramOptions &p)
324  : CmdLineProcessorBase<ProgramOptions,Except_>(p),config_param(_T("--config"),_T(" file_path\tSpecify the location (file_path) of the configuration file. The configuration file should be in XML format with a valid XDR schema (of the same name, in the same location, but with an '.xdr' extension). This schema referenced by the XML will be used as one of the tests to verify the appropriateness of the specified configuration file for this program. By default it is called: '")) {
325  }
326 
327  template<typename ProgOpts,class Except_> inline bool
328  ConfigFile<ProgOpts,Except_>::ProcessParamInternal(const unsigned int argv,const tchar * const * const argc,unsigned long &i,tostream &o) {
329  const tstring::size_type hc=config_param.param.compare(argc[i]);
330  if (hc>=0 && hc<config_param.param.size()-2) {
331  if (++i<=argv && argc[i] && !tstring(argc[i]).empty()) {
332  config_file_path=argc[i];
333  return true;
334  } else {
335  o<<_T("Missing 'file_path' parameter for ")<<config_param.param<<_T(" option.\n");
336  DumpHelp(o);
337  return false;
338  }
339  } else {
340  return CmdLineProcessorBase<ProgOpts,Except_>::ProcessParamInternal(argv,argc,i,o);
341  }
342  }
343 
344  template<typename ProgOpts,class Except_> inline void
345  ConfigFile<ProgOpts,Except_>::DumpHelp(tostream &o) const {
346  CmdLineProcessorBase<ProgOpts,Except_>::DumpHelp(o);
347  o<<_T("\t")<<config_param.param<<config_param.description<<DefaultConfigFilePath()<<_T("'.")<<std::endl;
348  }
349 
350  template<typename ProgOpts,class Except_> inline bool
351  ConfigFile<ProgOpts,Except_>::PostProcessParams(tostream &o) {
352  if (config_file_path.empty()) {
353  config_file_path=DefaultConfigFilePath();
354  }
355  xmlpp::DomParser parser;
356  const xmlpp::NodeSet data_items(CheckConfigFile(parser,o));
357  if (!data_items.empty()) {
358  // Process the data items and put them into the program options stucture...
359  JMMCG_TRACE(_T("Process ")<<tostring(data_items.size())+_T(" node(s)..."));
360  JMMCG_LOG((ConfigFile<ProgOpts,Except_>::Log()),log_info,_T("Process ")+tostring(data_items.size())+_T(" node(s)..."));
361  if (!PostProcessConfigFileItems(data_items,o)) {
362  o<<_T("There was an error whilst processing the configuration file.\n");
363  DumpHelp(o);
364  return false;
365  }
366  return true;
367  } else {
368  DumpHelp(o);
369  return false;
370  }
371  }
372 
373  template<typename ProgOpts,class Except_> inline xmlpp::NodeSet
374  ConfigFile<ProgOpts,Except_>::CheckConfigFile(xmlpp::DomParser &parser,tostream &o) const {
375  try {
376  // Check the file exists by opening it read-only...
377  config_file_type config_file(config_file_path,std::ios_base::in,false);
378  return CheckConfigFileValid(config_file,parser,o);
379  } catch (const typename config_file_type::exception &e) {
380  // Not interested in the details of the exception really - just the fact that it happened, so signifying a problem with opening the file to read it.
381  o<<_T("The configuration file '")<<config_file_path<<_T("' was not found in the specified location.\n");
382  JMMCG_LOG((ConfigFile<ProgOpts,Except_>::Log()),log_info,e.what());
383  }
384  return xmlpp::NodeSet();
385  }
386 
387  template<typename ProgOpts,class Except_> inline xmlpp::NodeSet
388  ConfigFile<ProgOpts,Except_>::CheckConfigFileValid(config_file_type &config_file,xmlpp::DomParser &parser,tostream &o) const {
389  // Now check the contents. I use XML with an XDR schema defined, so just run the XML processor on it with the schema attached. If it is valid, we know that the structure of the file is good! (One of the *very* *few* useful things about XML...)
390  try {
391 #warning TODO JMG: Seems to only like DTD validation: parser.set_validate();
392  parser.parse_stream(config_file);
393  if (parser) {
394  assert(dynamic_cast<const xmlpp::Element *>(parser.get_document()->get_root_node()));
395  const xmlpp::NodeSet data_items(parser.get_document()->get_root_node()->find(_T("/Config/Data/*")));
396 // JMMCG_TRACE(_T("X-Path to first item: ")<<(*data_items.begin())->get_path());
397  if (data_items.empty()) {
398  o<<_T("The configuration file '")<<config_file_path<<_T("' is not valid for this program.\n");
399  }
400  return data_items;
401  }
402  } catch (const std::exception &e) {
403  o<<_T("Error processing the configuration file: '")<<e.what()<<_T("'\n");
404  }
405  return xmlpp::NodeSet();
406  }
407 
408  template<typename ProgOpts,class Except_> inline tstring
409  ConfigFile<ProgOpts,Except_>::DefaultConfigFilePath(void) const {
410  return _T("~/.")+tstring(ConfigFile<ProgOpts,Except_>::app_name).substr(tstring(ConfigFile<ProgOpts,Except_>::app_name).rfind(_T("/"))+1,tstring::npos)+_T("rc.xml");
411  }
412 
413 } }