1 // Copyright (C) 2006 Timothy Brownawell <tbrownaw@gmail.com>
2 //
3 // This program is made available under the GNU GPL version 2.0 or
4 // greater. See the accompanying file COPYING for details.
5 //
6 // This program is distributed WITHOUT ANY WARRANTY; without even the
7 // implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
8 // PURPOSE.
9 
10 #include "base.hh"
11 #include "file_io.hh"
12 #include "option.hh"
13 #include "sanity.hh"
14 
15 using std::map;
16 using std::pair;
17 using std::set;
18 using std::string;
19 using std::vector;
20 
21 namespace option {
22 
option_error(std::string const & str)23 option_error::option_error(std::string const & str)
24  : std::invalid_argument((F("option error: %s") % str).str())
25 {}
26 
unknown_option(std::string const & opt)27 unknown_option::unknown_option(std::string const & opt)
28  : option_error((F("unknown option '%s'") % opt).str())
29 {}
30 
missing_arg(std::string const & opt)31 missing_arg::missing_arg(std::string const & opt)
32  : option_error((F("missing argument to option '%s'") % opt).str())
33 {}
34 
extra_arg(std::string const & opt)35 extra_arg::extra_arg(std::string const & opt)
36  : option_error((F("option '%s' does not take an argument") % opt).str())
37 {}
38 
bad_arg(std::string const & opt,arg_type const & arg)39 bad_arg::bad_arg(std::string const & opt, arg_type const & arg)
40  : option_error((F("bad argument '%s' to option '%s'") % arg() % opt).str())
41 {}
42 
bad_arg(std::string const & opt,arg_type const & arg,std::string const & reason)43 bad_arg::bad_arg(std::string const & opt,
44                  arg_type const & arg,
45                  std::string const & reason)
46  : option_error((F("bad argument '%s' to option '%s': %s")
47                    % arg() % opt % reason).str())
48 {}
49 
bad_arg_internal(string const & str)50 bad_arg_internal::bad_arg_internal(string const & str)
51  : reason(str)
52 {}
53 
54 
55 
splitname(char const * f,string & name,string & n,string & cancel)56 void splitname(char const * f, string & name, string & n, string & cancel)
57 {
58   string from(f);
59   if (from.find("/") != string::npos)
60     {
61       string::size_type slash = from.find("/");
62       cancel = from.substr(slash+1);
63       from.erase(slash);
64     }
65   // from looks like "foo" or "foo,f"
66   string::size_type comma = from.find(',');
67   name = from.substr(0, comma);
68   if (comma != string::npos)
69     n = from.substr(comma+1, 1);
70   else
71     n = "";
72 
73   // "o" is equivalent to ",o"; it gives an option
74   // with only a short name
75   if (name.size() == 1)
76     {
77       I(n.empty());
78       n = name;
79       name = "";
80     }
81 }
82 
83 
concrete_option()84 concrete_option::concrete_option()
85   : has_arg(false)
86 {}
87 
concrete_option(char const * names,char const * desc,bool arg,boost::function<void (std::string)> set,boost::function<void ()> reset,bool hide,char const * deprecate)88 concrete_option::concrete_option(char const * names,
89                                  char const * desc,
90                                  bool arg,
91                                  boost::function<void (std::string)> set,
92                                  boost::function<void ()> reset,
93                                  bool hide,
94                                  char const * deprecate)
95 {
96   description = desc;
97   splitname(names, longname, shortname, cancelname);
98   I((desc && desc[0]) || !longname.empty() || !shortname.empty());
99   // not sure how to display if it can only be reset (and what would that mean?)
100   I((!longname.empty() || !shortname.empty()) || cancelname.empty());
101   // If an option has a name (ie, can be set), it must have a setter function
102   I(set || (longname.empty() && shortname.empty()));
103   // If an option can be canceled, it must have a resetter function
104   I(reset || cancelname.empty());
105   has_arg = arg;
106   setter = set;
107   resetter = reset;
108   hidden = hide;
109   deprecated = deprecate;
110 }
111 
operator <(concrete_option const & other) const112 bool concrete_option::operator<(concrete_option const & other) const
113 {
114   if (longname != other.longname)
115     return longname < other.longname;
116   if (shortname != other.shortname)
117     return shortname < other.shortname;
118   if (cancelname != other.cancelname)
119     return cancelname < other.cancelname;
120   return description < other.description;
121 }
122 
123 concrete_option_set
operator |(concrete_option const & a,concrete_option const & b)124 operator | (concrete_option const & a, concrete_option const & b)
125 {
126   return concrete_option_set(a) | b;
127 }
128 
concrete_option_set()129 concrete_option_set::concrete_option_set()
130 {}
131 
concrete_option_set(std::set<concrete_option> const & other)132 concrete_option_set::concrete_option_set(std::set<concrete_option> const & other)
133   : options(other)
134 {}
135 
concrete_option_set(concrete_option const & opt)136 concrete_option_set::concrete_option_set(concrete_option const & opt)
137 {
138   options.insert(opt);
139 }
140 
141 // essentially the opposite of std::bind1st
142 class discard_argument
143 {
144   boost::function<void()> functor;
145  public:
discard_argument(boost::function<void ()> const & from)146   discard_argument(boost::function<void()> const & from)
147     : functor(from)
148     {}
operator ()(std::string const &)149     void operator()(std::string const &)
150     { return functor(); }
151 };
152 
153 concrete_option_set &
operator ()(char const * names,char const * desc,boost::function<void ()> set,boost::function<void ()> reset,bool hide,char const * deprecate)154 concrete_option_set::operator()(char const * names,
155                                 char const * desc,
156                                 boost::function<void ()> set,
157                                 boost::function<void ()> reset,
158                                 bool hide,
159                                 char const * deprecate)
160 {
161   options.insert(concrete_option(names, desc, false, discard_argument(set),
162                                  reset, hide, deprecate));
163   return *this;
164 }
165 
166 concrete_option_set &
operator ()(char const * names,char const * desc,boost::function<void (string)> set,boost::function<void ()> reset,bool hide,char const * deprecate)167 concrete_option_set::operator()(char const * names,
168                                 char const * desc,
169                                 boost::function<void (string)> set,
170                                 boost::function<void ()> reset,
171                                 bool hide,
172                                 char const * deprecate)
173 {
174   options.insert(concrete_option(names, desc, true, set, reset, hide, deprecate));
175   return *this;
176 }
177 
178 concrete_option_set
operator |(concrete_option_set const & other) const179 concrete_option_set::operator | (concrete_option_set const & other) const
180 {
181   concrete_option_set combined;
182   std::set_union(options.begin(), options.end(),
183                  other.options.begin(), other.options.end(),
184                  std::inserter(combined.options, combined.options.begin()));
185   return combined;
186 }
187 
reset() const188 void concrete_option_set::reset() const
189 {
190   for (std::set<concrete_option>::const_iterator i = options.begin();
191        i != options.end(); ++i)
192     {
193       if (i->resetter)
194         i->resetter();
195     }
196 }
197 
198 static void
tokenize_for_command_line(string const & from,args_vector & to)199 tokenize_for_command_line(string const & from, args_vector & to)
200 {
201   // Unfortunately, the tokenizer in basic_io is too format-specific
202   to.clear();
203   enum quote_type {none, one, two};
204   string cur;
205   quote_type type = none;
206   bool have_tok(false);
207 
208   for (string::const_iterator i = from.begin(); i != from.end(); ++i)
209     {
210       if (*i == '\'')
211         {
212           if (type == none)
213             type = one;
214           else if (type == one)
215             type = none;
216           else
217             {
218               cur += *i;
219               have_tok = true;
220             }
221         }
222       else if (*i == '"')
223         {
224           if (type == none)
225             type = two;
226           else if (type == two)
227             type = none;
228           else
229             {
230               cur += *i;
231               have_tok = true;
232             }
233         }
234       else if (*i == '\\')
235         {
236           if (type != one)
237             ++i;
238           E(i != from.end(), origin::user, F("invalid escape in '--xargs' file"));
239           cur += *i;
240           have_tok = true;
241         }
242       else if (string(" \n\t").find(*i) != string::npos)
243         {
244           if (type == none)
245             {
246               if (have_tok)
247                 to.push_back(arg_type(cur, origin::user));
248               cur.clear();
249               have_tok = false;
250             }
251           else
252             {
253               cur += *i;
254               have_tok = true;
255             }
256         }
257       else
258         {
259           cur += *i;
260           have_tok = true;
261         }
262     }
263   if (have_tok)
264     to.push_back(arg_type(cur, origin::user));
265 }
266 
from_command_line(int argc,char const * const * argv)267 void concrete_option_set::from_command_line(int argc,
268                                             char const * const * argv)
269 {
270   args_vector arguments;
271   for (int i = 1; i < argc; ++i)
272     arguments.push_back(arg_type(argv[i], origin::user));
273   from_command_line(arguments);
274 }
275 
276 // checks a multi-word option like 'no-builtin-rcfile' against a
277 // possible abbreviated given option 'nbr' which is only compiled
278 // of the first character of each word
279 static bool
abbrev_match(string const & option,string const & part)280 abbrev_match(string const & option, string const & part)
281 {
282   if (option.find('-') == 0)
283     return false;
284 
285   string::const_iterator it = option.begin();
286   string opt_part(1, *it);
287   for (; it != option.end(); ++it)
288     {
289       if (*it == '-' && it != option.end())
290         opt_part += *(it+1);
291     }
292 
293   return part == opt_part;
294 }
295 
296 static concrete_option const &
getopt(map<string,concrete_option> const & by_name,string & name)297 getopt(map<string, concrete_option> const & by_name, string & name)
298 {
299   // try to match the option name as a whole first, so if the user
300   // specified "--foo" and we have "--foo" and "--foo-bar", don't
301   // display both choices
302   map<string, concrete_option>::const_iterator i = by_name.find(name);
303   if (i != by_name.end())
304     return i->second;
305 
306   if (name.size() == 0)
307     throw unknown_option(name);
308 
309   // try to find the option by partial name
310   set<string> candidates;
311   for (i = by_name.begin(); i != by_name.end(); ++i)
312     {
313       if (i->first.find(name) == 0)
314         candidates.insert(i->first);
315       if (abbrev_match(i->first, name))
316         candidates.insert(i->first);
317     }
318 
319   if (candidates.size() == 0)
320     throw unknown_option(name);
321 
322   if (candidates.size() == 1)
323     {
324        string expanded_name = *candidates.begin();
325        i = by_name.find(expanded_name);
326        I(i != by_name.end());
327        L(FL("expanding option '%s' to '%s'") % name % expanded_name);
328        name = expanded_name;
329        return i->second;
330     }
331 
332   string err = (F("option '%s' has multiple ambiguous expansions:")
333                 % name).str();
334 
335   for (set<string>::const_iterator j = candidates.begin();
336        j != candidates.end(); ++j)
337     {
338         i = by_name.find(*j);
339         I(i != by_name.end());
340 
341         if (*j == "--")
342           continue;
343 
344         if (*j == i->second.shortname)
345           err += "\n'-" + *j + "' (" + i->second.description + ")";
346         else if (*j == i->second.cancelname)
347           err += "\n'--" + *j + "' (" + (F("negation of '--%s'") % i->second.longname).str() + ")";
348         else
349           err += "\n'--" + *j + "' (" + i->second.description + ")";
350     }
351 
352   E(false, origin::user, i18n_format(err));
353 }
354 
355 // helper for get_by_name
356 // Make sure that either:
357 //   * There are no duplicate options, or
358 //   * If we're only parsing options (and not applying them), any duplicates
359 //     are consistent WRT whether they take an option
360 typedef pair<map<string, concrete_option>::iterator, bool> by_name_res_type;
check_by_name_insertion(by_name_res_type const & res,concrete_option const & opt,concrete_option_set::preparse_flag pf)361 static void check_by_name_insertion(by_name_res_type const & res,
362                                     concrete_option const & opt,
363                                     concrete_option_set::preparse_flag pf)
364 {
365   switch (pf)
366     {
367     case concrete_option_set::preparse:
368       if (!res.second)
369         {
370           string const & name = res.first->first;
371           concrete_option const & them = res.first->second;
372           bool const i_have_arg = (name != opt.cancelname && opt.has_arg);
373           bool const they_have_arg = (name != them.cancelname && them.has_arg);
374           I(i_have_arg == they_have_arg);
375         }
376       break;
377     case concrete_option_set::no_preparse:
378       I(res.second);
379       break;
380     }
381 }
382 
383 // generate an index that lets us look options up by name
384 static map<string, concrete_option>
get_by_name(std::set<concrete_option> const & options,concrete_option_set::preparse_flag pf)385 get_by_name(std::set<concrete_option> const & options,
386             concrete_option_set::preparse_flag pf)
387 {
388   map<string, concrete_option> by_name;
389   for (std::set<concrete_option>::const_iterator i = options.begin();
390        i != options.end(); ++i)
391     {
392       if (!i->longname.empty())
393         check_by_name_insertion(by_name.insert(make_pair(i->longname, *i)),
394                                 *i, pf);
395       if (!i->shortname.empty())
396         check_by_name_insertion(by_name.insert(make_pair(i->shortname, *i)),
397                                 *i, pf);
398       if (!i->cancelname.empty())
399         check_by_name_insertion(by_name.insert(make_pair(i->cancelname, *i)),
400                                 *i, pf);
401     }
402   return by_name;
403 }
404 
from_command_line(args_vector & args,preparse_flag pf)405 void concrete_option_set::from_command_line(args_vector & args,
406                                             preparse_flag pf)
407 {
408   map<string, concrete_option> by_name = get_by_name(options, pf);
409 
410   bool seen_dashdash = false;
411   for (args_vector::size_type i = 0; i < args.size(); ++i)
412     {
413       concrete_option o;
414       string name;
415       arg_type arg;
416       bool is_cancel;
417       bool separate_arg(false);
418       if (idx(args,i)() == "--" || seen_dashdash)
419         {
420           if (!seen_dashdash)
421             {
422               seen_dashdash = true;
423               continue;
424             }
425           name = "--";
426           o = getopt(by_name, name);
427           arg = idx(args,i);
428           is_cancel = false;
429         }
430       else if (idx(args,i)().substr(0,2) == "--")
431         {
432           string::size_type equals = idx(args,i)().find('=');
433           if (equals == string::npos)
434             name = idx(args,i)().substr(2);
435           else
436             name = idx(args,i)().substr(2, equals-2);
437 
438           o = getopt(by_name, name);
439           is_cancel = (name == o.cancelname);
440           if ((!o.has_arg || is_cancel) && equals != string::npos)
441               throw extra_arg(name);
442 
443           if (o.has_arg && !is_cancel)
444             {
445               if (equals == string::npos)
446                 {
447                   separate_arg = true;
448                   if (i+1 == args.size())
449                     throw missing_arg(name);
450                   arg = idx(args,i+1);
451                 }
452               else
453                 arg = arg_type(idx(args,i)().substr(equals+1), origin::user);
454             }
455         }
456       else if (idx(args,i)().substr(0,1) == "-")
457         {
458           name = idx(args,i)().substr(1,1);
459 
460           map<string, concrete_option>::const_iterator j = by_name.find(name);
461           if (j == by_name.end())
462             throw unknown_option(name);
463           o = j->second;
464 
465           is_cancel = (name == o.cancelname);
466           I(!is_cancel);
467           if (!o.has_arg && idx(args,i)().size() != 2)
468             throw extra_arg(name);
469 
470           if (o.has_arg)
471             {
472               if (idx(args,i)().size() == 2)
473                 {
474                   separate_arg = true;
475                   if (i+1 == args.size())
476                     throw missing_arg(name);
477                   arg = idx(args,i+1);
478                 }
479               else
480                 arg = arg_type(idx(args,i)().substr(2), origin::user);
481             }
482         }
483       else
484         {
485           name = "--";
486           o = getopt(by_name, name);
487           arg = idx(args,i);
488           is_cancel = false;
489         }
490 
491       if (name == "xargs" || name == "@")
492         {
493           // expand the --xargs in place
494           data dat;
495           read_data_for_command_line(arg, dat);
496           args_vector fargs;
497           tokenize_for_command_line(dat(), fargs);
498 
499           args.erase(args.begin() + i);
500           if (separate_arg)
501             args.erase(args.begin() + i);
502           args.insert(args.begin()+i, fargs.begin(), fargs.end());
503           --i;
504         }
505       else
506         {
507           if (separate_arg)
508             ++i;
509           try
510             {
511               if (o.deprecated)
512                 W(F("deprecated option '%s' used: %s")
513                   % o.longname % gettext(o.deprecated));
514               if (!is_cancel)
515                 {
516                   if (o.setter)
517                     o.setter(arg());
518                 }
519               else
520                 {
521                   if (o.resetter)
522                     o.resetter();
523                 }
524             }
525           catch (boost::bad_lexical_cast)
526             {
527               throw bad_arg(o.longname, arg);
528             }
529           catch (bad_arg_internal & e)
530             {
531               if (e.reason == "")
532                 throw bad_arg(o.longname, arg);
533               else
534                 throw bad_arg(o.longname, arg, e.reason);
535             }
536         }
537     }
538 }
539 
from_key_value_pairs(vector<pair<string,string>> const & keyvals)540 void concrete_option_set::from_key_value_pairs(vector<pair<string, string> > const & keyvals)
541 {
542   map<string, concrete_option> by_name = get_by_name(options, no_preparse);
543 
544   for (vector<pair<string, string> >::const_iterator i = keyvals.begin();
545        i != keyvals.end(); ++i)
546     {
547       string key(i->first);
548       arg_type const & value(arg_type(i->second, origin::user));
549 
550       concrete_option o = getopt(by_name, key);
551       bool const is_cancel = (key == o.cancelname);
552 
553       try
554         {
555           if (o.deprecated)
556             W(F("deprecated option '%s' used: %s")
557               % o.longname % gettext(o.deprecated));
558 
559           if (!is_cancel)
560             {
561               if (o.setter)
562                 o.setter(value());
563             }
564           else
565             {
566               if (o.resetter)
567                 o.resetter();
568             }
569         }
570       catch (boost::bad_lexical_cast)
571         {
572           throw bad_arg(o.longname, value);
573         }
574       catch (bad_arg_internal & e)
575         {
576           if (e.reason == "")
577             throw bad_arg(o.longname, value);
578           else
579             throw bad_arg(o.longname, value, e.reason);
580         }
581     }
582 }
583 
584 // Get the non-description part of the usage string,
585 // looks like "--long [ -s ] <arg> / --cancel".
usagestr(concrete_option const & opt)586 static string usagestr(concrete_option const & opt)
587 {
588   string out;
589   if (opt.longname == "--")
590     return "";
591   if (!opt.longname.empty() && !opt.shortname.empty())
592     out = "--" + opt.longname + " [ -" + opt.shortname + " ]";
593   else if (!opt.longname.empty())
594     out = "--" + opt.longname;
595   else if (!opt.shortname.empty())
596     out = "-" + opt.shortname;
597 
598   if (out.empty())
599     return out;
600 
601   if (opt.has_arg)
602     out += " <arg>";
603 
604   if (!opt.cancelname.empty())
605     {
606       if (!out.empty())
607         out += " / ";
608       out += "--" + opt.cancelname;
609     }
610 
611   return out;
612 }
613 
614 void
get_usage_strings(vector<string> & names,vector<string> & descriptions,unsigned int & maxnamelen,bool show_hidden) const615 concrete_option_set::get_usage_strings(vector<string> & names,
616                                        vector<string> & descriptions,
617                                        unsigned int & maxnamelen,
618                                        bool show_hidden) const
619 {
620   unsigned int namelen = 0; // the longest option name string
621   names.clear();
622   descriptions.clear();
623   for (std::set<concrete_option>::const_iterator i = options.begin();
624        i != options.end(); ++i)
625     {
626       if (i->hidden && !show_hidden)
627         continue;
628       if (i->deprecated)
629         continue;
630       string name = usagestr(*i);
631       if (name.size() > namelen)
632         namelen = name.size();
633       names.push_back(name);
634       descriptions.push_back(gettext(i->description));
635     }
636   maxnamelen = namelen;
637 }
638 
639 } // namespace option
640 
641 
642 
643 // Local Variables:
644 // mode: C++
645 // fill-column: 76
646 // c-file-style: "gnu"
647 // indent-tabs-mode: nil
648 // End:
649 // vim: et:sw=2:sts=2:ts=2:cino=>2s,{s,\:s,+s,t0,g0,^-2,e-2,n-2,p2s,(0,=s:
650