aboutsummaryrefslogtreecommitdiffstats
path: root/src/cli/cli.h
blob: 017966ecaaa8362023a894aa8adacfecf222eb22 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
/*
* (C) 2015 Jack Lloyd
*
* Botan is released under the Simplified BSD License (see license.txt)
*/

#ifndef BOTAN_CLI_H__
#define BOTAN_CLI_H__

#include <botan/build.h>
#include <botan/parsing.h>
#include <botan/rng.h>

#include <fstream>
#include <iostream>
#include <functional>
#include <map>
#include <memory>
#include <set>
#include <string>
#include <vector>

namespace Botan_CLI {

class CLI_Error : public std::runtime_error
   {
   public:
      CLI_Error(const std::string& s) : std::runtime_error(s) {}
   };

class CLI_IO_Error : public CLI_Error
   {
   public:
      CLI_IO_Error(const std::string& op, const std::string& who) :
         CLI_Error("Error " + op + " " + who) {}
   };

class CLI_Usage_Error : public CLI_Error
   {
   public:
      CLI_Usage_Error(const std::string& what) : CLI_Error(what) {}
   };

/* Thrown eg when a requested feature was compiled out of the library
   or is not available, eg hashing with
*/
class CLI_Error_Unsupported : public CLI_Error
   {
   public:
      CLI_Error_Unsupported(const std::string& what,
                            const std::string& who) :
         CLI_Error(what + " with '" + who + "' unsupported or not available") {}
   };

struct CLI_Error_Invalid_Spec : public CLI_Error
   {
   public:
      CLI_Error_Invalid_Spec(const std::string& spec) :
         CLI_Error("Invalid command spec '" + spec + "'") {}
   };

class Command
   {
   public:
      /**
      * The spec string specifies the format of the command line, eg for
      * a somewhat complicated command:
      * cmd_name --flag --option1= --option2=opt2val input1 input2 *rest
      *
      * By default this is the value returned by help_text()
      *
      * The first value is always the command name. Options may appear
      * in any order. Named arguments are taken from the command line
      * in the order they appear in the spec.
      *
      * --flag can optionally be specified, and takes no value.
      * Check for it in go() with flag_set()
      *
      * --option1 is an option whose default value (if the option
      * does not appear on the command line) is the empty string.
      *
      * --option2 is an option whose default value is opt2val
      * Read the values in go() using get_arg or get_arg_sz.
      *
      * The values input1 and input2 specify named arguments which must
      * be provided. They are also access via get_arg/get_arg_sz
      * Because options and arguments for a single command share the same
      * namespace you can't have a spec like:
      *   cmd --input input
      * but you hopefully didn't want to do that anyway.
      *
      * The leading '*' on '*rest' specifies that all remaining arguments
      * should be packaged in a list which is available as get_arg_list("rest").
      * This can only appear on a single value and should be the final
      * named argument.
      *
      * Every command has implicit flags --help, --verbose and implicit
      * options --output= and --error-output= which override the default
      * use of std::cout and std::cerr.
      *
      * Use of --help is captured in run() and returns help_text().
      * Use of --verbose can be checked with verbose() or flag_set("verbose")
      */
      Command(const std::string& cmd_spec) : m_spec(cmd_spec)
         {
         // for checking all spec strings at load time
         //parse_spec();
         }

      int run(const std::vector<std::string>& params)
         {
         try
            {
            // avoid parsing specs except for the command actually running
            parse_spec();

            std::vector<std::string> args;
            for(auto&& param : params)
               {
               if(param.find("--") == 0)
                  {
                  // option
                  const auto eq = param.find('=');

                  if(eq == std::string::npos)
                     {
                     const std::string opt_name = param.substr(2, std::string::npos);

                     if(m_spec_flags.count(opt_name) == 0)
                        {
                        if(m_spec_opts.count(opt_name))
                           throw CLI_Usage_Error("Invalid usage of option --" + opt_name +
                                                 " without value");
                        else
                           throw CLI_Usage_Error("Unknown flag --" + opt_name);
                        }

                     m_user_flags.insert(opt_name);
                     }
                  else
                     {
                     const std::string opt_name = param.substr(2, eq - 2);
                     const std::string opt_val = param.substr(eq + 1, std::string::npos);

                     if(m_spec_opts.count(opt_name) == 0)
                        {
                        throw CLI_Usage_Error("Unknown option --" + opt_name);
                        }

                     m_user_args.insert(std::make_pair(opt_name, opt_val));
                     }
                  }
               else
                  {
                  // argument
                  args.push_back(param);
                  }
               }

            bool seen_stdin_flag = false;
            size_t arg_i = 0;
            for(auto&& arg : m_spec_args)
               {
               if(arg_i >= args.size())
                  {
                  // not enough arguments
                  throw CLI_Usage_Error("Invalid argument count, got " +
                                        std::to_string(args.size()) +
                                        " expected " +
                                        std::to_string(m_spec_args.size()));
                  }

               m_user_args.insert(std::make_pair(arg, args[arg_i]));

               if(args[arg_i] == "-")
                  {
                  if(seen_stdin_flag)
                     throw CLI_Usage_Error("Cannot specifiy '-' (stdin) more than once");
                  seen_stdin_flag = true;
                  }

               ++arg_i;
               }

            if(m_spec_rest.empty())
               {
               if(arg_i != args.size())
                  throw CLI_Usage_Error("Too many arguments");
               }
            else
               {
               m_user_rest.assign(args.begin() + arg_i, args.end());
               }

            if(flag_set("help"))
               {
               output() << help_text() << "\n";
               return 1;
               }

            if(m_user_args.count("output"))
               {
               m_output_stream.reset(new std::ofstream(get_arg("output")));
               }

            if(m_user_args.count("error_output"))
               {
               m_error_output_stream.reset(new std::ofstream(get_arg("error_output")));
               }

            // Now insert any defaults for options not supplied by the user
            for(auto&& opt : m_spec_opts)
               {
               if(m_user_args.count(opt.first) == 0)
                  {
                  m_user_args.insert(opt);
                  }
               }

            this->go();
            return 0;
            }
         catch(CLI_Usage_Error& e)
            {
            error_output() << "Usage error: " << e.what() << "\n";
            error_output() << help_text() << "\n";
            return 1;
            }
         catch(std::exception& e)
            {
            error_output() << "Error: " << e.what() << "\n";
            return 2;
            }
         catch(...)
            {
            error_output() << "Error: unknown exception\n";
            return 2;
            }
         }

      virtual std::string help_text() const
         {
         return "Usage: " + m_spec;
         }

      const std::string& cmd_spec() const { return m_spec; }

      std::string cmd_name() const
         {
         return m_spec.substr(0, m_spec.find(' '));
         }

   protected:

      void parse_spec()
         {
         const std::vector<std::string> parts = Botan::split_on(m_spec, ' ');

         if(parts.size() == 0)
            throw CLI_Error_Invalid_Spec(m_spec);

         for(size_t i = 1; i != parts.size(); ++i)
            {
            const std::string s = parts[i];

            if(s.empty()) // ?!? (shouldn't happen)
               throw CLI_Error_Invalid_Spec(m_spec);

            if(s.size() > 2 && s[0] == '-' && s[1] == '-')
               {
               // option or flag

               auto eq = s.find('=');

               if(eq == std::string::npos)
                  {
                  m_spec_flags.insert(s.substr(2, std::string::npos));
                  }
               else
                  {
                  m_spec_opts.insert(std::make_pair(s.substr(2, eq - 2),
                                                    s.substr(eq + 1, std::string::npos)));
                  }
               }
            else if(s[0] == '*')
               {
               // rest argument
               if(m_spec_rest.empty() && s.size() > 2)
                  {
                  m_spec_rest = s.substr(1, std::string::npos);
                  }
               else
                  {
                  throw CLI_Error_Invalid_Spec(m_spec);
                  }
               }
            else
               {
               // named argument
               if(!m_spec_rest.empty()) // rest arg wasn't last
                  throw CLI_Error("Invalid command spec " + m_spec);

               m_spec_args.push_back(s);
               }
            }

         m_spec_flags.insert("verbose");
         m_spec_flags.insert("help");
         m_spec_opts.insert(std::make_pair("output", ""));
         m_spec_opts.insert(std::make_pair("error-output", ""));
         }

      /*
      * The actual functionality of the cli command implemented in subclas
      */
      virtual void go() = 0;

      std::ostream& output()
         {
         if(m_output_stream.get())
            return *m_output_stream;
         return std::cout;
         }

      std::ostream& error_output()
         {
         if(m_error_output_stream.get())
            return *m_error_output_stream;
         return std::cerr;
         }

      bool verbose() const
         {
         return flag_set("verbose");
         }

      bool flag_set(const std::string& flag_name) const
         {
         return m_user_flags.count(flag_name) > 0;
         }

      std::string get_arg(const std::string& opt_name) const
         {
         auto i = m_user_args.find(opt_name);
         if(i == m_user_args.end())
            {
            // this shouldn't occur unless you passed the wrong thing to get_arg
            throw CLI_Error("Unknown option " + opt_name + " used (program bug)");
            }
         return i->second;
         }

      /*
      * Like get_arg() but if the argument was not specified or is empty, returns otherwise
      */
      std::string get_arg_or(const std::string& opt_name, const std::string& otherwise) const
         {
         auto i = m_user_args.find(opt_name);
         if(i == m_user_args.end() || i->second.empty())
            {
            return otherwise;
            }
         return i->second;
         }

      size_t get_arg_sz(const std::string& opt_name) const
         {
         const std::string s = get_arg(opt_name);

         try
            {
            return static_cast<size_t>(std::stoul(s));
            }
         catch(std::exception& e)
            {
            throw CLI_Usage_Error("Invalid integer value '" + s + "' for option " + opt_name);
            }
         }

      std::vector<std::string> get_arg_list(const std::string& what) const
         {
         if(what != m_spec_rest)
            throw CLI_Error("Unexpected list name '" + what + "'");

         return m_user_rest;
         }

      /*
      * Read an entire file into memory and return the contents
      */
      std::vector<uint8_t> slurp_file(const std::string& input_file) const
         {
         std::vector<uint8_t> buf;
         auto insert_fn = [&](const uint8_t b[], size_t l)
            { buf.insert(buf.end(), b, b + l); };
         this->read_file(input_file, insert_fn);
         return buf;
         }

      std::string slurp_file_as_str(const std::string& input_file)
         {
         std::string str;
         auto insert_fn = [&](const uint8_t b[], size_t l)
            { str.append(reinterpret_cast<const char*>(b), l); };
         this->read_file(input_file, insert_fn);
         return str;
         }

      /*
      * Read a file calling consumer_fn() with the inputs
      */
      void read_file(const std::string& input_file,
                     std::function<void (uint8_t[], size_t)> consumer_fn,
                     size_t buf_size = 0) const
         {
         if(input_file == "-")
            {
            do_read_file(std::cin, consumer_fn, buf_size);
            }
         else
            {
            std::ifstream in(input_file, std::ios::binary);
            do_read_file(in, consumer_fn, buf_size);
            }
         }

      void do_read_file(std::istream& in,
                        std::function<void (uint8_t[], size_t)> consumer_fn,
                        size_t buf_size = 0) const
         {
         // Avoid an infinite loop on --buf-size=0
         std::vector<uint8_t> buf(buf_size == 0 ? 4096 : buf_size);

         while(in.good())
            {
            in.read(reinterpret_cast<char*>(buf.data()), buf.size());
            consumer_fn(buf.data(), in.gcount());
            }
         }

      template<typename Alloc>
      void write_output(const std::vector<uint8_t, Alloc>& vec)
         {
         output().write(reinterpret_cast<const char*>(vec.data()), vec.size());
         }

   private:
      // set in constructor
      std::string m_spec;

      // set in parse_spec() from m_spec
      std::vector<std::string> m_spec_args;
      std::set<std::string> m_spec_flags;
      std::map<std::string, std::string> m_spec_opts;
      std::string m_spec_rest;

      // set in run() from user args
      std::map<std::string, std::string> m_user_args;
      std::set<std::string> m_user_flags;
      std::vector<std::string> m_user_rest;

      std::unique_ptr<std::ofstream> m_output_stream;
      std::unique_ptr<std::ofstream> m_error_output_stream;


   public:
      // the registry interface:

      static std::map<std::string, std::unique_ptr<Command>>& global_registry()
         {
         static std::map<std::string, std::unique_ptr<Command>> g_cmds;
         return g_cmds;
         }

      static Command* get_cmd(const std::string& name)
         {
         auto& reg = Command::global_registry();
         auto i = reg.find(name);
         if(i == reg.end())
            {
            return nullptr;
            }

         return i->second.get();
         }

      class Registration
         {
         public:
            Registration(Command* cmd)
               {
               const std::string name = cmd->cmd_name();
               auto& reg = Command::global_registry();

               if(reg.count(name) > 0)
                  {
                  throw CLI_Error("Duplicated registration of command " + name);
                  }

               Command::global_registry().insert(
                  std::make_pair(name, std::unique_ptr<Command>(cmd)));
               }
         };
   };

#define BOTAN_REGISTER_COMMAND(CLI_Class) \
   namespace { Botan_CLI::Command::Registration reg_cmd_ ## CLI_Class(new CLI_Class); }

}

#endif