diff options
author | Karen Arutyunov <karen@codesynthesis.com> | 2019-09-06 22:20:46 +0300 |
---|---|---|
committer | Karen Arutyunov <karen@codesynthesis.com> | 2019-09-27 17:08:05 +0300 |
commit | 1c6758009e82c47b5b341d418be2be401ef31482 (patch) | |
tree | d3ef8c053280477086f6230e3d25ff90b25871a2 | |
parent | 070871d97b4f6440c3f0fc647ece73b53a5837db (diff) |
Add builtins support
-rw-r--r-- | build/root.build | 8 | ||||
-rw-r--r-- | libbutl/buildfile | 40 | ||||
-rw-r--r-- | libbutl/builtin-options.cxx | 3332 | ||||
-rw-r--r-- | libbutl/builtin-options.hxx | 1006 | ||||
-rw-r--r-- | libbutl/builtin-options.ixx | 284 | ||||
-rw-r--r-- | libbutl/builtin.cli | 82 | ||||
-rw-r--r-- | libbutl/builtin.cxx | 2111 | ||||
-rw-r--r-- | libbutl/builtin.mxx | 174 | ||||
-rw-r--r-- | tests/builtin/buildfile | 9 | ||||
-rw-r--r-- | tests/builtin/cat.testscript | 82 | ||||
-rw-r--r-- | tests/builtin/cp-dir/cp-file | 0 | ||||
-rw-r--r-- | tests/builtin/cp.testscript | 447 | ||||
-rw-r--r-- | tests/builtin/driver.cxx | 159 | ||||
-rw-r--r-- | tests/builtin/echo.testscript | 26 | ||||
-rw-r--r-- | tests/builtin/ln.testscript | 217 | ||||
-rw-r--r-- | tests/builtin/mkdir.testscript | 102 | ||||
-rw-r--r-- | tests/builtin/mv.testscript | 182 | ||||
-rw-r--r-- | tests/builtin/rm.testscript | 105 | ||||
-rw-r--r-- | tests/builtin/rmdir.testscript | 95 | ||||
-rw-r--r-- | tests/builtin/sed.testscript | 350 | ||||
-rw-r--r-- | tests/builtin/sleep.testscript | 42 | ||||
-rw-r--r-- | tests/builtin/test.testscript | 84 | ||||
-rw-r--r-- | tests/builtin/touch.testscript | 92 |
23 files changed, 9027 insertions, 2 deletions
diff --git a/build/root.build b/build/root.build index 7842948..9ff95cd 100644 --- a/build/root.build +++ b/build/root.build @@ -16,3 +16,11 @@ if ($cxx.class == 'msvc') cxx.poptions += -D_CRT_SECURE_NO_WARNINGS -D_SCL_SECURE_NO_WARNINGS cxx.coptions += /wd4251 /wd4275 /wd4800 } + +# Load the cli module but only if it's available. This way a distribution +# that includes pre-generated files can be built without installing cli. +# This is also the reason why we need to explicitly spell out individual +# source file prerequisites instead of using the cli.cxx{} group (it won't +# be there unless the module is configured). +# +using? cli diff --git a/libbutl/buildfile b/libbutl/buildfile index a0b789a..e6841ab 100644 --- a/libbutl/buildfile +++ b/libbutl/buildfile @@ -13,8 +13,11 @@ # # @@ If/when going back to using mxx{}, make sure to cleanup explicit .mxx. # -lib{butl}: {hxx ixx txx cxx}{** -uuid-* +uuid-io -win32-utility -version} \ - hxx{**.mxx} {hxx}{version} +lib{butl}: {hxx ixx txx cxx}{** -uuid-* +uuid-io \ + -win32-utility \ + -version \ + -builtin-options} \ + hxx{**.mxx} {hxx}{version} {hxx ixx cxx}{builtin-options} tclass = $cxx.target.class tsys = $cxx.target.system @@ -93,6 +96,39 @@ if $version.pre_release else lib{butl}: bin.lib.version = @"-$version.major.$version.minor" +# Generated options parser. +# +if $cli.configured +{ + cli.cxx{builtin-options}: cli{builtin} + + cli.options += --std c++11 -I $src_root --include-with-brackets \ +--include-prefix libbutl --guard-prefix LIBBUTL --cli-namespace butl::cli \ +--generate-vector-scanner --generate-parse --keep-separator \ +--generate-specifier --suppress-usage + + cli.cxx{*}: + { + # Include the generated cli files into the distribution and don't remove + # them when cleaning in src (so that clean results in a state identical to + # distributed). But don't install their headers since they are only used + # internally in the builtins implementation. + # + dist = true + clean = ($src_root != $out_root) + install = false + + # We keep the generated code in the repository so copy it back to src in + # case of a forwarded configuration. + # + backlink = overwrite + } +} +else + # No install for the pre-generated case. + # + hxx{builtin-options}@./ ixx{builtin-options}@./: install = false + # Install into the libbutl/ subdirectory of, say, /usr/include/ # recreating subdirectories. # diff --git a/libbutl/builtin-options.cxx b/libbutl/builtin-options.cxx new file mode 100644 index 0000000..70d56c1 --- /dev/null +++ b/libbutl/builtin-options.cxx @@ -0,0 +1,3332 @@ +// -*- C++ -*- +// +// This file was generated by CLI, a command line interface +// compiler for C++. +// + +// Begin prologue. +// +// +// End prologue. + +#include <libbutl/builtin-options.hxx> + +#include <map> +#include <set> +#include <string> +#include <vector> +#include <ostream> +#include <sstream> + +namespace butl +{ + namespace cli + { + // unknown_option + // + unknown_option:: + ~unknown_option () throw () + { + } + + void unknown_option:: + print (::std::ostream& os) const + { + os << "unknown option '" << option ().c_str () << "'"; + } + + const char* unknown_option:: + what () const throw () + { + return "unknown option"; + } + + // unknown_argument + // + unknown_argument:: + ~unknown_argument () throw () + { + } + + void unknown_argument:: + print (::std::ostream& os) const + { + os << "unknown argument '" << argument ().c_str () << "'"; + } + + const char* unknown_argument:: + what () const throw () + { + return "unknown argument"; + } + + // missing_value + // + missing_value:: + ~missing_value () throw () + { + } + + void missing_value:: + print (::std::ostream& os) const + { + os << "missing value for option '" << option ().c_str () << "'"; + } + + const char* missing_value:: + what () const throw () + { + return "missing option value"; + } + + // invalid_value + // + invalid_value:: + ~invalid_value () throw () + { + } + + void invalid_value:: + print (::std::ostream& os) const + { + os << "invalid value '" << value ().c_str () << "' for option '" + << option ().c_str () << "'"; + + if (!message ().empty ()) + os << ": " << message ().c_str (); + } + + const char* invalid_value:: + what () const throw () + { + return "invalid option value"; + } + + // eos_reached + // + void eos_reached:: + print (::std::ostream& os) const + { + os << what (); + } + + const char* eos_reached:: + what () const throw () + { + return "end of argument stream reached"; + } + + // scanner + // + scanner:: + ~scanner () + { + } + + // argv_scanner + // + bool argv_scanner:: + more () + { + return i_ < argc_; + } + + const char* argv_scanner:: + peek () + { + if (i_ < argc_) + return argv_[i_]; + else + throw eos_reached (); + } + + const char* argv_scanner:: + next () + { + if (i_ < argc_) + { + const char* r (argv_[i_]); + + if (erase_) + { + for (int i (i_ + 1); i < argc_; ++i) + argv_[i - 1] = argv_[i]; + + --argc_; + argv_[argc_] = 0; + } + else + ++i_; + + return r; + } + else + throw eos_reached (); + } + + void argv_scanner:: + skip () + { + if (i_ < argc_) + ++i_; + else + throw eos_reached (); + } + + // vector_scanner + // + bool vector_scanner:: + more () + { + return i_ < v_.size (); + } + + const char* vector_scanner:: + peek () + { + if (i_ < v_.size ()) + return v_[i_].c_str (); + else + throw eos_reached (); + } + + const char* vector_scanner:: + next () + { + if (i_ < v_.size ()) + return v_[i_++].c_str (); + else + throw eos_reached (); + } + + void vector_scanner:: + skip () + { + if (i_ < v_.size ()) + ++i_; + else + throw eos_reached (); + } + + template <typename X> + struct parser + { + static void + parse (X& x, bool& xs, scanner& s) + { + using namespace std; + + const char* o (s.next ()); + if (s.more ()) + { + string v (s.next ()); + istringstream is (v); + if (!(is >> x && is.peek () == istringstream::traits_type::eof ())) + throw invalid_value (o, v); + } + else + throw missing_value (o); + + xs = true; + } + }; + + template <> + struct parser<bool> + { + static void + parse (bool& x, scanner& s) + { + s.next (); + x = true; + } + }; + + template <> + struct parser<std::string> + { + static void + parse (std::string& x, bool& xs, scanner& s) + { + const char* o (s.next ()); + + if (s.more ()) + x = s.next (); + else + throw missing_value (o); + + xs = true; + } + }; + + template <typename X> + struct parser<std::vector<X> > + { + static void + parse (std::vector<X>& c, bool& xs, scanner& s) + { + X x; + bool dummy; + parser<X>::parse (x, dummy, s); + c.push_back (x); + xs = true; + } + }; + + template <typename X> + struct parser<std::set<X> > + { + static void + parse (std::set<X>& c, bool& xs, scanner& s) + { + X x; + bool dummy; + parser<X>::parse (x, dummy, s); + c.insert (x); + xs = true; + } + }; + + template <typename K, typename V> + struct parser<std::map<K, V> > + { + static void + parse (std::map<K, V>& m, bool& xs, scanner& s) + { + const char* o (s.next ()); + + if (s.more ()) + { + std::string ov (s.next ()); + std::string::size_type p = ov.find ('='); + + K k = K (); + V v = V (); + std::string kstr (ov, 0, p); + std::string vstr (ov, (p != std::string::npos ? p + 1 : ov.size ())); + + int ac (2); + char* av[] = + { + const_cast<char*> (o), 0 + }; + + bool dummy; + if (!kstr.empty ()) + { + av[1] = const_cast<char*> (kstr.c_str ()); + argv_scanner s (0, ac, av); + parser<K>::parse (k, dummy, s); + } + + if (!vstr.empty ()) + { + av[1] = const_cast<char*> (vstr.c_str ()); + argv_scanner s (0, ac, av); + parser<V>::parse (v, dummy, s); + } + + m[k] = v; + } + else + throw missing_value (o); + + xs = true; + } + }; + + template <typename X, typename T, T X::*M> + void + thunk (X& x, scanner& s) + { + parser<T>::parse (x.*M, s); + } + + template <typename X, typename T, T X::*M, bool X::*S> + void + thunk (X& x, scanner& s) + { + parser<T>::parse (x.*M, x.*S, s); + } + } +} + +#include <map> +#include <cstring> + +namespace butl +{ + // cat_options + // + + cat_options:: + cat_options () + { + } + + bool cat_options:: + parse (int& argc, + char** argv, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (argc, argv, erase); + bool r = _parse (s, opt, arg); + return r; + } + + bool cat_options:: + parse (int start, + int& argc, + char** argv, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (start, argc, argv, erase); + bool r = _parse (s, opt, arg); + return r; + } + + bool cat_options:: + parse (int& argc, + char** argv, + int& end, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (argc, argv, erase); + bool r = _parse (s, opt, arg); + end = s.end (); + return r; + } + + bool cat_options:: + parse (int start, + int& argc, + char** argv, + int& end, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (start, argc, argv, erase); + bool r = _parse (s, opt, arg); + end = s.end (); + return r; + } + + bool cat_options:: + parse (::butl::cli::scanner& s, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + bool r = _parse (s, opt, arg); + return r; + } + + typedef + std::map<std::string, void (*) (cat_options&, ::butl::cli::scanner&)> + _cli_cat_options_map; + + static _cli_cat_options_map _cli_cat_options_map_; + + struct _cli_cat_options_map_init + { + _cli_cat_options_map_init () + { + } + }; + + static _cli_cat_options_map_init _cli_cat_options_map_init_; + + bool cat_options:: + _parse (const char* o, ::butl::cli::scanner& s) + { + _cli_cat_options_map::const_iterator i (_cli_cat_options_map_.find (o)); + + if (i != _cli_cat_options_map_.end ()) + { + (*(i->second)) (*this, s); + return true; + } + + return false; + } + + bool cat_options:: + _parse (::butl::cli::scanner& s, + ::butl::cli::unknown_mode opt_mode, + ::butl::cli::unknown_mode arg_mode) + { + // Can't skip combined flags (--no-combined-flags). + // + assert (opt_mode != ::butl::cli::unknown_mode::skip); + + bool r = false; + bool opt = true; + + while (s.more ()) + { + const char* o = s.peek (); + + if (std::strcmp (o, "--") == 0) + { + opt = false; + } + + if (opt) + { + if (_parse (o, s)) + { + r = true; + continue; + } + + if (std::strncmp (o, "-", 1) == 0 && o[1] != '\0') + { + // Handle combined option values. + // + std::string co; + if (const char* v = std::strchr (o, '=')) + { + co.assign (o, 0, v - o); + ++v; + + int ac (2); + char* av[] = + { + const_cast<char*> (co.c_str ()), + const_cast<char*> (v) + }; + + ::butl::cli::argv_scanner ns (0, ac, av); + + if (_parse (co.c_str (), ns)) + { + // Parsed the option but not its value? + // + if (ns.end () != 2) + throw ::butl::cli::invalid_value (co, v); + + s.next (); + r = true; + continue; + } + else + { + // Set the unknown option and fall through. + // + o = co.c_str (); + } + } + + // Handle combined flags. + // + char cf[3]; + { + const char* p = o + 1; + for (; *p != '\0'; ++p) + { + if (!((*p >= 'a' && *p <= 'z') || + (*p >= 'A' && *p <= 'Z') || + (*p >= '0' && *p <= '9'))) + break; + } + + if (*p == '\0') + { + for (p = o + 1; *p != '\0'; ++p) + { + std::strcpy (cf, "-"); + cf[1] = *p; + cf[2] = '\0'; + + int ac (1); + char* av[] = + { + cf + }; + + ::butl::cli::argv_scanner ns (0, ac, av); + + if (!_parse (cf, ns)) + break; + } + + if (*p == '\0') + { + // All handled. + // + s.next (); + r = true; + continue; + } + else + { + // Set the unknown option and fall through. + // + o = cf; + } + } + } + + switch (opt_mode) + { + case ::butl::cli::unknown_mode::skip: + { + s.skip (); + r = true; + continue; + } + case ::butl::cli::unknown_mode::stop: + { + break; + } + case ::butl::cli::unknown_mode::fail: + { + throw ::butl::cli::unknown_option (o); + } + } + + break; + } + } + + switch (arg_mode) + { + case ::butl::cli::unknown_mode::skip: + { + s.skip (); + r = true; + continue; + } + case ::butl::cli::unknown_mode::stop: + { + break; + } + case ::butl::cli::unknown_mode::fail: + { + throw ::butl::cli::unknown_argument (o); + } + } + + break; + } + + return r; + } + + // cp_options + // + + cp_options:: + cp_options () + : recursive_ (), + preserve_ () + { + } + + bool cp_options:: + parse (int& argc, + char** argv, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (argc, argv, erase); + bool r = _parse (s, opt, arg); + return r; + } + + bool cp_options:: + parse (int start, + int& argc, + char** argv, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (start, argc, argv, erase); + bool r = _parse (s, opt, arg); + return r; + } + + bool cp_options:: + parse (int& argc, + char** argv, + int& end, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (argc, argv, erase); + bool r = _parse (s, opt, arg); + end = s.end (); + return r; + } + + bool cp_options:: + parse (int start, + int& argc, + char** argv, + int& end, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (start, argc, argv, erase); + bool r = _parse (s, opt, arg); + end = s.end (); + return r; + } + + bool cp_options:: + parse (::butl::cli::scanner& s, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + bool r = _parse (s, opt, arg); + return r; + } + + typedef + std::map<std::string, void (*) (cp_options&, ::butl::cli::scanner&)> + _cli_cp_options_map; + + static _cli_cp_options_map _cli_cp_options_map_; + + struct _cli_cp_options_map_init + { + _cli_cp_options_map_init () + { + _cli_cp_options_map_["--recursive"] = + &::butl::cli::thunk< cp_options, bool, &cp_options::recursive_ >; + _cli_cp_options_map_["-R"] = + &::butl::cli::thunk< cp_options, bool, &cp_options::recursive_ >; + _cli_cp_options_map_["-r"] = + &::butl::cli::thunk< cp_options, bool, &cp_options::recursive_ >; + _cli_cp_options_map_["--preserve"] = + &::butl::cli::thunk< cp_options, bool, &cp_options::preserve_ >; + _cli_cp_options_map_["-p"] = + &::butl::cli::thunk< cp_options, bool, &cp_options::preserve_ >; + } + }; + + static _cli_cp_options_map_init _cli_cp_options_map_init_; + + bool cp_options:: + _parse (const char* o, ::butl::cli::scanner& s) + { + _cli_cp_options_map::const_iterator i (_cli_cp_options_map_.find (o)); + + if (i != _cli_cp_options_map_.end ()) + { + (*(i->second)) (*this, s); + return true; + } + + return false; + } + + bool cp_options:: + _parse (::butl::cli::scanner& s, + ::butl::cli::unknown_mode opt_mode, + ::butl::cli::unknown_mode arg_mode) + { + // Can't skip combined flags (--no-combined-flags). + // + assert (opt_mode != ::butl::cli::unknown_mode::skip); + + bool r = false; + bool opt = true; + + while (s.more ()) + { + const char* o = s.peek (); + + if (std::strcmp (o, "--") == 0) + { + opt = false; + } + + if (opt) + { + if (_parse (o, s)) + { + r = true; + continue; + } + + if (std::strncmp (o, "-", 1) == 0 && o[1] != '\0') + { + // Handle combined option values. + // + std::string co; + if (const char* v = std::strchr (o, '=')) + { + co.assign (o, 0, v - o); + ++v; + + int ac (2); + char* av[] = + { + const_cast<char*> (co.c_str ()), + const_cast<char*> (v) + }; + + ::butl::cli::argv_scanner ns (0, ac, av); + + if (_parse (co.c_str (), ns)) + { + // Parsed the option but not its value? + // + if (ns.end () != 2) + throw ::butl::cli::invalid_value (co, v); + + s.next (); + r = true; + continue; + } + else + { + // Set the unknown option and fall through. + // + o = co.c_str (); + } + } + + // Handle combined flags. + // + char cf[3]; + { + const char* p = o + 1; + for (; *p != '\0'; ++p) + { + if (!((*p >= 'a' && *p <= 'z') || + (*p >= 'A' && *p <= 'Z') || + (*p >= '0' && *p <= '9'))) + break; + } + + if (*p == '\0') + { + for (p = o + 1; *p != '\0'; ++p) + { + std::strcpy (cf, "-"); + cf[1] = *p; + cf[2] = '\0'; + + int ac (1); + char* av[] = + { + cf + }; + + ::butl::cli::argv_scanner ns (0, ac, av); + + if (!_parse (cf, ns)) + break; + } + + if (*p == '\0') + { + // All handled. + // + s.next (); + r = true; + continue; + } + else + { + // Set the unknown option and fall through. + // + o = cf; + } + } + } + + switch (opt_mode) + { + case ::butl::cli::unknown_mode::skip: + { + s.skip (); + r = true; + continue; + } + case ::butl::cli::unknown_mode::stop: + { + break; + } + case ::butl::cli::unknown_mode::fail: + { + throw ::butl::cli::unknown_option (o); + } + } + + break; + } + } + + switch (arg_mode) + { + case ::butl::cli::unknown_mode::skip: + { + s.skip (); + r = true; + continue; + } + case ::butl::cli::unknown_mode::stop: + { + break; + } + case ::butl::cli::unknown_mode::fail: + { + throw ::butl::cli::unknown_argument (o); + } + } + + break; + } + + return r; + } + + // ln_options + // + + ln_options:: + ln_options () + : symbolic_ () + { + } + + bool ln_options:: + parse (int& argc, + char** argv, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (argc, argv, erase); + bool r = _parse (s, opt, arg); + return r; + } + + bool ln_options:: + parse (int start, + int& argc, + char** argv, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (start, argc, argv, erase); + bool r = _parse (s, opt, arg); + return r; + } + + bool ln_options:: + parse (int& argc, + char** argv, + int& end, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (argc, argv, erase); + bool r = _parse (s, opt, arg); + end = s.end (); + return r; + } + + bool ln_options:: + parse (int start, + int& argc, + char** argv, + int& end, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (start, argc, argv, erase); + bool r = _parse (s, opt, arg); + end = s.end (); + return r; + } + + bool ln_options:: + parse (::butl::cli::scanner& s, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + bool r = _parse (s, opt, arg); + return r; + } + + typedef + std::map<std::string, void (*) (ln_options&, ::butl::cli::scanner&)> + _cli_ln_options_map; + + static _cli_ln_options_map _cli_ln_options_map_; + + struct _cli_ln_options_map_init + { + _cli_ln_options_map_init () + { + _cli_ln_options_map_["--symbolic"] = + &::butl::cli::thunk< ln_options, bool, &ln_options::symbolic_ >; + _cli_ln_options_map_["-s"] = + &::butl::cli::thunk< ln_options, bool, &ln_options::symbolic_ >; + } + }; + + static _cli_ln_options_map_init _cli_ln_options_map_init_; + + bool ln_options:: + _parse (const char* o, ::butl::cli::scanner& s) + { + _cli_ln_options_map::const_iterator i (_cli_ln_options_map_.find (o)); + + if (i != _cli_ln_options_map_.end ()) + { + (*(i->second)) (*this, s); + return true; + } + + return false; + } + + bool ln_options:: + _parse (::butl::cli::scanner& s, + ::butl::cli::unknown_mode opt_mode, + ::butl::cli::unknown_mode arg_mode) + { + // Can't skip combined flags (--no-combined-flags). + // + assert (opt_mode != ::butl::cli::unknown_mode::skip); + + bool r = false; + bool opt = true; + + while (s.more ()) + { + const char* o = s.peek (); + + if (std::strcmp (o, "--") == 0) + { + opt = false; + } + + if (opt) + { + if (_parse (o, s)) + { + r = true; + continue; + } + + if (std::strncmp (o, "-", 1) == 0 && o[1] != '\0') + { + // Handle combined option values. + // + std::string co; + if (const char* v = std::strchr (o, '=')) + { + co.assign (o, 0, v - o); + ++v; + + int ac (2); + char* av[] = + { + const_cast<char*> (co.c_str ()), + const_cast<char*> (v) + }; + + ::butl::cli::argv_scanner ns (0, ac, av); + + if (_parse (co.c_str (), ns)) + { + // Parsed the option but not its value? + // + if (ns.end () != 2) + throw ::butl::cli::invalid_value (co, v); + + s.next (); + r = true; + continue; + } + else + { + // Set the unknown option and fall through. + // + o = co.c_str (); + } + } + + // Handle combined flags. + // + char cf[3]; + { + const char* p = o + 1; + for (; *p != '\0'; ++p) + { + if (!((*p >= 'a' && *p <= 'z') || + (*p >= 'A' && *p <= 'Z') || + (*p >= '0' && *p <= '9'))) + break; + } + + if (*p == '\0') + { + for (p = o + 1; *p != '\0'; ++p) + { + std::strcpy (cf, "-"); + cf[1] = *p; + cf[2] = '\0'; + + int ac (1); + char* av[] = + { + cf + }; + + ::butl::cli::argv_scanner ns (0, ac, av); + + if (!_parse (cf, ns)) + break; + } + + if (*p == '\0') + { + // All handled. + // + s.next (); + r = true; + continue; + } + else + { + // Set the unknown option and fall through. + // + o = cf; + } + } + } + + switch (opt_mode) + { + case ::butl::cli::unknown_mode::skip: + { + s.skip (); + r = true; + continue; + } + case ::butl::cli::unknown_mode::stop: + { + break; + } + case ::butl::cli::unknown_mode::fail: + { + throw ::butl::cli::unknown_option (o); + } + } + + break; + } + } + + switch (arg_mode) + { + case ::butl::cli::unknown_mode::skip: + { + s.skip (); + r = true; + continue; + } + case ::butl::cli::unknown_mode::stop: + { + break; + } + case ::butl::cli::unknown_mode::fail: + { + throw ::butl::cli::unknown_argument (o); + } + } + + break; + } + + return r; + } + + // mkdir_options + // + + mkdir_options:: + mkdir_options () + : parents_ () + { + } + + bool mkdir_options:: + parse (int& argc, + char** argv, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (argc, argv, erase); + bool r = _parse (s, opt, arg); + return r; + } + + bool mkdir_options:: + parse (int start, + int& argc, + char** argv, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (start, argc, argv, erase); + bool r = _parse (s, opt, arg); + return r; + } + + bool mkdir_options:: + parse (int& argc, + char** argv, + int& end, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (argc, argv, erase); + bool r = _parse (s, opt, arg); + end = s.end (); + return r; + } + + bool mkdir_options:: + parse (int start, + int& argc, + char** argv, + int& end, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (start, argc, argv, erase); + bool r = _parse (s, opt, arg); + end = s.end (); + return r; + } + + bool mkdir_options:: + parse (::butl::cli::scanner& s, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + bool r = _parse (s, opt, arg); + return r; + } + + typedef + std::map<std::string, void (*) (mkdir_options&, ::butl::cli::scanner&)> + _cli_mkdir_options_map; + + static _cli_mkdir_options_map _cli_mkdir_options_map_; + + struct _cli_mkdir_options_map_init + { + _cli_mkdir_options_map_init () + { + _cli_mkdir_options_map_["--parents"] = + &::butl::cli::thunk< mkdir_options, bool, &mkdir_options::parents_ >; + _cli_mkdir_options_map_["-p"] = + &::butl::cli::thunk< mkdir_options, bool, &mkdir_options::parents_ >; + } + }; + + static _cli_mkdir_options_map_init _cli_mkdir_options_map_init_; + + bool mkdir_options:: + _parse (const char* o, ::butl::cli::scanner& s) + { + _cli_mkdir_options_map::const_iterator i (_cli_mkdir_options_map_.find (o)); + + if (i != _cli_mkdir_options_map_.end ()) + { + (*(i->second)) (*this, s); + return true; + } + + return false; + } + + bool mkdir_options:: + _parse (::butl::cli::scanner& s, + ::butl::cli::unknown_mode opt_mode, + ::butl::cli::unknown_mode arg_mode) + { + // Can't skip combined flags (--no-combined-flags). + // + assert (opt_mode != ::butl::cli::unknown_mode::skip); + + bool r = false; + bool opt = true; + + while (s.more ()) + { + const char* o = s.peek (); + + if (std::strcmp (o, "--") == 0) + { + opt = false; + } + + if (opt) + { + if (_parse (o, s)) + { + r = true; + continue; + } + + if (std::strncmp (o, "-", 1) == 0 && o[1] != '\0') + { + // Handle combined option values. + // + std::string co; + if (const char* v = std::strchr (o, '=')) + { + co.assign (o, 0, v - o); + ++v; + + int ac (2); + char* av[] = + { + const_cast<char*> (co.c_str ()), + const_cast<char*> (v) + }; + + ::butl::cli::argv_scanner ns (0, ac, av); + + if (_parse (co.c_str (), ns)) + { + // Parsed the option but not its value? + // + if (ns.end () != 2) + throw ::butl::cli::invalid_value (co, v); + + s.next (); + r = true; + continue; + } + else + { + // Set the unknown option and fall through. + // + o = co.c_str (); + } + } + + // Handle combined flags. + // + char cf[3]; + { + const char* p = o + 1; + for (; *p != '\0'; ++p) + { + if (!((*p >= 'a' && *p <= 'z') || + (*p >= 'A' && *p <= 'Z') || + (*p >= '0' && *p <= '9'))) + break; + } + + if (*p == '\0') + { + for (p = o + 1; *p != '\0'; ++p) + { + std::strcpy (cf, "-"); + cf[1] = *p; + cf[2] = '\0'; + + int ac (1); + char* av[] = + { + cf + }; + + ::butl::cli::argv_scanner ns (0, ac, av); + + if (!_parse (cf, ns)) + break; + } + + if (*p == '\0') + { + // All handled. + // + s.next (); + r = true; + continue; + } + else + { + // Set the unknown option and fall through. + // + o = cf; + } + } + } + + switch (opt_mode) + { + case ::butl::cli::unknown_mode::skip: + { + s.skip (); + r = true; + continue; + } + case ::butl::cli::unknown_mode::stop: + { + break; + } + case ::butl::cli::unknown_mode::fail: + { + throw ::butl::cli::unknown_option (o); + } + } + + break; + } + } + + switch (arg_mode) + { + case ::butl::cli::unknown_mode::skip: + { + s.skip (); + r = true; + continue; + } + case ::butl::cli::unknown_mode::stop: + { + break; + } + case ::butl::cli::unknown_mode::fail: + { + throw ::butl::cli::unknown_argument (o); + } + } + + break; + } + + return r; + } + + // mv_options + // + + mv_options:: + mv_options () + : force_ () + { + } + + bool mv_options:: + parse (int& argc, + char** argv, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (argc, argv, erase); + bool r = _parse (s, opt, arg); + return r; + } + + bool mv_options:: + parse (int start, + int& argc, + char** argv, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (start, argc, argv, erase); + bool r = _parse (s, opt, arg); + return r; + } + + bool mv_options:: + parse (int& argc, + char** argv, + int& end, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (argc, argv, erase); + bool r = _parse (s, opt, arg); + end = s.end (); + return r; + } + + bool mv_options:: + parse (int start, + int& argc, + char** argv, + int& end, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (start, argc, argv, erase); + bool r = _parse (s, opt, arg); + end = s.end (); + return r; + } + + bool mv_options:: + parse (::butl::cli::scanner& s, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + bool r = _parse (s, opt, arg); + return r; + } + + typedef + std::map<std::string, void (*) (mv_options&, ::butl::cli::scanner&)> + _cli_mv_options_map; + + static _cli_mv_options_map _cli_mv_options_map_; + + struct _cli_mv_options_map_init + { + _cli_mv_options_map_init () + { + _cli_mv_options_map_["--force"] = + &::butl::cli::thunk< mv_options, bool, &mv_options::force_ >; + _cli_mv_options_map_["-f"] = + &::butl::cli::thunk< mv_options, bool, &mv_options::force_ >; + } + }; + + static _cli_mv_options_map_init _cli_mv_options_map_init_; + + bool mv_options:: + _parse (const char* o, ::butl::cli::scanner& s) + { + _cli_mv_options_map::const_iterator i (_cli_mv_options_map_.find (o)); + + if (i != _cli_mv_options_map_.end ()) + { + (*(i->second)) (*this, s); + return true; + } + + return false; + } + + bool mv_options:: + _parse (::butl::cli::scanner& s, + ::butl::cli::unknown_mode opt_mode, + ::butl::cli::unknown_mode arg_mode) + { + // Can't skip combined flags (--no-combined-flags). + // + assert (opt_mode != ::butl::cli::unknown_mode::skip); + + bool r = false; + bool opt = true; + + while (s.more ()) + { + const char* o = s.peek (); + + if (std::strcmp (o, "--") == 0) + { + opt = false; + } + + if (opt) + { + if (_parse (o, s)) + { + r = true; + continue; + } + + if (std::strncmp (o, "-", 1) == 0 && o[1] != '\0') + { + // Handle combined option values. + // + std::string co; + if (const char* v = std::strchr (o, '=')) + { + co.assign (o, 0, v - o); + ++v; + + int ac (2); + char* av[] = + { + const_cast<char*> (co.c_str ()), + const_cast<char*> (v) + }; + + ::butl::cli::argv_scanner ns (0, ac, av); + + if (_parse (co.c_str (), ns)) + { + // Parsed the option but not its value? + // + if (ns.end () != 2) + throw ::butl::cli::invalid_value (co, v); + + s.next (); + r = true; + continue; + } + else + { + // Set the unknown option and fall through. + // + o = co.c_str (); + } + } + + // Handle combined flags. + // + char cf[3]; + { + const char* p = o + 1; + for (; *p != '\0'; ++p) + { + if (!((*p >= 'a' && *p <= 'z') || + (*p >= 'A' && *p <= 'Z') || + (*p >= '0' && *p <= '9'))) + break; + } + + if (*p == '\0') + { + for (p = o + 1; *p != '\0'; ++p) + { + std::strcpy (cf, "-"); + cf[1] = *p; + cf[2] = '\0'; + + int ac (1); + char* av[] = + { + cf + }; + + ::butl::cli::argv_scanner ns (0, ac, av); + + if (!_parse (cf, ns)) + break; + } + + if (*p == '\0') + { + // All handled. + // + s.next (); + r = true; + continue; + } + else + { + // Set the unknown option and fall through. + // + o = cf; + } + } + } + + switch (opt_mode) + { + case ::butl::cli::unknown_mode::skip: + { + s.skip (); + r = true; + continue; + } + case ::butl::cli::unknown_mode::stop: + { + break; + } + case ::butl::cli::unknown_mode::fail: + { + throw ::butl::cli::unknown_option (o); + } + } + + break; + } + } + + switch (arg_mode) + { + case ::butl::cli::unknown_mode::skip: + { + s.skip (); + r = true; + continue; + } + case ::butl::cli::unknown_mode::stop: + { + break; + } + case ::butl::cli::unknown_mode::fail: + { + throw ::butl::cli::unknown_argument (o); + } + } + + break; + } + + return r; + } + + // rm_options + // + + rm_options:: + rm_options () + : recursive_ (), + force_ () + { + } + + bool rm_options:: + parse (int& argc, + char** argv, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (argc, argv, erase); + bool r = _parse (s, opt, arg); + return r; + } + + bool rm_options:: + parse (int start, + int& argc, + char** argv, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (start, argc, argv, erase); + bool r = _parse (s, opt, arg); + return r; + } + + bool rm_options:: + parse (int& argc, + char** argv, + int& end, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (argc, argv, erase); + bool r = _parse (s, opt, arg); + end = s.end (); + return r; + } + + bool rm_options:: + parse (int start, + int& argc, + char** argv, + int& end, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (start, argc, argv, erase); + bool r = _parse (s, opt, arg); + end = s.end (); + return r; + } + + bool rm_options:: + parse (::butl::cli::scanner& s, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + bool r = _parse (s, opt, arg); + return r; + } + + typedef + std::map<std::string, void (*) (rm_options&, ::butl::cli::scanner&)> + _cli_rm_options_map; + + static _cli_rm_options_map _cli_rm_options_map_; + + struct _cli_rm_options_map_init + { + _cli_rm_options_map_init () + { + _cli_rm_options_map_["--recursive"] = + &::butl::cli::thunk< rm_options, bool, &rm_options::recursive_ >; + _cli_rm_options_map_["-r"] = + &::butl::cli::thunk< rm_options, bool, &rm_options::recursive_ >; + _cli_rm_options_map_["--force"] = + &::butl::cli::thunk< rm_options, bool, &rm_options::force_ >; + _cli_rm_options_map_["-f"] = + &::butl::cli::thunk< rm_options, bool, &rm_options::force_ >; + } + }; + + static _cli_rm_options_map_init _cli_rm_options_map_init_; + + bool rm_options:: + _parse (const char* o, ::butl::cli::scanner& s) + { + _cli_rm_options_map::const_iterator i (_cli_rm_options_map_.find (o)); + + if (i != _cli_rm_options_map_.end ()) + { + (*(i->second)) (*this, s); + return true; + } + + return false; + } + + bool rm_options:: + _parse (::butl::cli::scanner& s, + ::butl::cli::unknown_mode opt_mode, + ::butl::cli::unknown_mode arg_mode) + { + // Can't skip combined flags (--no-combined-flags). + // + assert (opt_mode != ::butl::cli::unknown_mode::skip); + + bool r = false; + bool opt = true; + + while (s.more ()) + { + const char* o = s.peek (); + + if (std::strcmp (o, "--") == 0) + { + opt = false; + } + + if (opt) + { + if (_parse (o, s)) + { + r = true; + continue; + } + + if (std::strncmp (o, "-", 1) == 0 && o[1] != '\0') + { + // Handle combined option values. + // + std::string co; + if (const char* v = std::strchr (o, '=')) + { + co.assign (o, 0, v - o); + ++v; + + int ac (2); + char* av[] = + { + const_cast<char*> (co.c_str ()), + const_cast<char*> (v) + }; + + ::butl::cli::argv_scanner ns (0, ac, av); + + if (_parse (co.c_str (), ns)) + { + // Parsed the option but not its value? + // + if (ns.end () != 2) + throw ::butl::cli::invalid_value (co, v); + + s.next (); + r = true; + continue; + } + else + { + // Set the unknown option and fall through. + // + o = co.c_str (); + } + } + + // Handle combined flags. + // + char cf[3]; + { + const char* p = o + 1; + for (; *p != '\0'; ++p) + { + if (!((*p >= 'a' && *p <= 'z') || + (*p >= 'A' && *p <= 'Z') || + (*p >= '0' && *p <= '9'))) + break; + } + + if (*p == '\0') + { + for (p = o + 1; *p != '\0'; ++p) + { + std::strcpy (cf, "-"); + cf[1] = *p; + cf[2] = '\0'; + + int ac (1); + char* av[] = + { + cf + }; + + ::butl::cli::argv_scanner ns (0, ac, av); + + if (!_parse (cf, ns)) + break; + } + + if (*p == '\0') + { + // All handled. + // + s.next (); + r = true; + continue; + } + else + { + // Set the unknown option and fall through. + // + o = cf; + } + } + } + + switch (opt_mode) + { + case ::butl::cli::unknown_mode::skip: + { + s.skip (); + r = true; + continue; + } + case ::butl::cli::unknown_mode::stop: + { + break; + } + case ::butl::cli::unknown_mode::fail: + { + throw ::butl::cli::unknown_option (o); + } + } + + break; + } + } + + switch (arg_mode) + { + case ::butl::cli::unknown_mode::skip: + { + s.skip (); + r = true; + continue; + } + case ::butl::cli::unknown_mode::stop: + { + break; + } + case ::butl::cli::unknown_mode::fail: + { + throw ::butl::cli::unknown_argument (o); + } + } + + break; + } + + return r; + } + + // rmdir_options + // + + rmdir_options:: + rmdir_options () + : force_ () + { + } + + bool rmdir_options:: + parse (int& argc, + char** argv, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (argc, argv, erase); + bool r = _parse (s, opt, arg); + return r; + } + + bool rmdir_options:: + parse (int start, + int& argc, + char** argv, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (start, argc, argv, erase); + bool r = _parse (s, opt, arg); + return r; + } + + bool rmdir_options:: + parse (int& argc, + char** argv, + int& end, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (argc, argv, erase); + bool r = _parse (s, opt, arg); + end = s.end (); + return r; + } + + bool rmdir_options:: + parse (int start, + int& argc, + char** argv, + int& end, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (start, argc, argv, erase); + bool r = _parse (s, opt, arg); + end = s.end (); + return r; + } + + bool rmdir_options:: + parse (::butl::cli::scanner& s, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + bool r = _parse (s, opt, arg); + return r; + } + + typedef + std::map<std::string, void (*) (rmdir_options&, ::butl::cli::scanner&)> + _cli_rmdir_options_map; + + static _cli_rmdir_options_map _cli_rmdir_options_map_; + + struct _cli_rmdir_options_map_init + { + _cli_rmdir_options_map_init () + { + _cli_rmdir_options_map_["--force"] = + &::butl::cli::thunk< rmdir_options, bool, &rmdir_options::force_ >; + _cli_rmdir_options_map_["-f"] = + &::butl::cli::thunk< rmdir_options, bool, &rmdir_options::force_ >; + } + }; + + static _cli_rmdir_options_map_init _cli_rmdir_options_map_init_; + + bool rmdir_options:: + _parse (const char* o, ::butl::cli::scanner& s) + { + _cli_rmdir_options_map::const_iterator i (_cli_rmdir_options_map_.find (o)); + + if (i != _cli_rmdir_options_map_.end ()) + { + (*(i->second)) (*this, s); + return true; + } + + return false; + } + + bool rmdir_options:: + _parse (::butl::cli::scanner& s, + ::butl::cli::unknown_mode opt_mode, + ::butl::cli::unknown_mode arg_mode) + { + // Can't skip combined flags (--no-combined-flags). + // + assert (opt_mode != ::butl::cli::unknown_mode::skip); + + bool r = false; + bool opt = true; + + while (s.more ()) + { + const char* o = s.peek (); + + if (std::strcmp (o, "--") == 0) + { + opt = false; + } + + if (opt) + { + if (_parse (o, s)) + { + r = true; + continue; + } + + if (std::strncmp (o, "-", 1) == 0 && o[1] != '\0') + { + // Handle combined option values. + // + std::string co; + if (const char* v = std::strchr (o, '=')) + { + co.assign (o, 0, v - o); + ++v; + + int ac (2); + char* av[] = + { + const_cast<char*> (co.c_str ()), + const_cast<char*> (v) + }; + + ::butl::cli::argv_scanner ns (0, ac, av); + + if (_parse (co.c_str (), ns)) + { + // Parsed the option but not its value? + // + if (ns.end () != 2) + throw ::butl::cli::invalid_value (co, v); + + s.next (); + r = true; + continue; + } + else + { + // Set the unknown option and fall through. + // + o = co.c_str (); + } + } + + // Handle combined flags. + // + char cf[3]; + { + const char* p = o + 1; + for (; *p != '\0'; ++p) + { + if (!((*p >= 'a' && *p <= 'z') || + (*p >= 'A' && *p <= 'Z') || + (*p >= '0' && *p <= '9'))) + break; + } + + if (*p == '\0') + { + for (p = o + 1; *p != '\0'; ++p) + { + std::strcpy (cf, "-"); + cf[1] = *p; + cf[2] = '\0'; + + int ac (1); + char* av[] = + { + cf + }; + + ::butl::cli::argv_scanner ns (0, ac, av); + + if (!_parse (cf, ns)) + break; + } + + if (*p == '\0') + { + // All handled. + // + s.next (); + r = true; + continue; + } + else + { + // Set the unknown option and fall through. + // + o = cf; + } + } + } + + switch (opt_mode) + { + case ::butl::cli::unknown_mode::skip: + { + s.skip (); + r = true; + continue; + } + case ::butl::cli::unknown_mode::stop: + { + break; + } + case ::butl::cli::unknown_mode::fail: + { + throw ::butl::cli::unknown_option (o); + } + } + + break; + } + } + + switch (arg_mode) + { + case ::butl::cli::unknown_mode::skip: + { + s.skip (); + r = true; + continue; + } + case ::butl::cli::unknown_mode::stop: + { + break; + } + case ::butl::cli::unknown_mode::fail: + { + throw ::butl::cli::unknown_argument (o); + } + } + + break; + } + + return r; + } + + // sed_options + // + + sed_options:: + sed_options () + : quiet_ (), + in_place_ (), + expression_ (), + expression_specified_ (false) + { + } + + bool sed_options:: + parse (int& argc, + char** argv, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (argc, argv, erase); + bool r = _parse (s, opt, arg); + return r; + } + + bool sed_options:: + parse (int start, + int& argc, + char** argv, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (start, argc, argv, erase); + bool r = _parse (s, opt, arg); + return r; + } + + bool sed_options:: + parse (int& argc, + char** argv, + int& end, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (argc, argv, erase); + bool r = _parse (s, opt, arg); + end = s.end (); + return r; + } + + bool sed_options:: + parse (int start, + int& argc, + char** argv, + int& end, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (start, argc, argv, erase); + bool r = _parse (s, opt, arg); + end = s.end (); + return r; + } + + bool sed_options:: + parse (::butl::cli::scanner& s, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + bool r = _parse (s, opt, arg); + return r; + } + + typedef + std::map<std::string, void (*) (sed_options&, ::butl::cli::scanner&)> + _cli_sed_options_map; + + static _cli_sed_options_map _cli_sed_options_map_; + + struct _cli_sed_options_map_init + { + _cli_sed_options_map_init () + { + _cli_sed_options_map_["--quiet"] = + &::butl::cli::thunk< sed_options, bool, &sed_options::quiet_ >; + _cli_sed_options_map_["-n"] = + &::butl::cli::thunk< sed_options, bool, &sed_options::quiet_ >; + _cli_sed_options_map_["--in-place"] = + &::butl::cli::thunk< sed_options, bool, &sed_options::in_place_ >; + _cli_sed_options_map_["-i"] = + &::butl::cli::thunk< sed_options, bool, &sed_options::in_place_ >; + _cli_sed_options_map_["--expression"] = + &::butl::cli::thunk< sed_options, std::vector<std::string>, &sed_options::expression_, + &sed_options::expression_specified_ >; + _cli_sed_options_map_["-e"] = + &::butl::cli::thunk< sed_options, std::vector<std::string>, &sed_options::expression_, + &sed_options::expression_specified_ >; + } + }; + + static _cli_sed_options_map_init _cli_sed_options_map_init_; + + bool sed_options:: + _parse (const char* o, ::butl::cli::scanner& s) + { + _cli_sed_options_map::const_iterator i (_cli_sed_options_map_.find (o)); + + if (i != _cli_sed_options_map_.end ()) + { + (*(i->second)) (*this, s); + return true; + } + + return false; + } + + bool sed_options:: + _parse (::butl::cli::scanner& s, + ::butl::cli::unknown_mode opt_mode, + ::butl::cli::unknown_mode arg_mode) + { + // Can't skip combined flags (--no-combined-flags). + // + assert (opt_mode != ::butl::cli::unknown_mode::skip); + + bool r = false; + bool opt = true; + + while (s.more ()) + { + const char* o = s.peek (); + + if (std::strcmp (o, "--") == 0) + { + opt = false; + } + + if (opt) + { + if (_parse (o, s)) + { + r = true; + continue; + } + + if (std::strncmp (o, "-", 1) == 0 && o[1] != '\0') + { + // Handle combined option values. + // + std::string co; + if (const char* v = std::strchr (o, '=')) + { + co.assign (o, 0, v - o); + ++v; + + int ac (2); + char* av[] = + { + const_cast<char*> (co.c_str ()), + const_cast<char*> (v) + }; + + ::butl::cli::argv_scanner ns (0, ac, av); + + if (_parse (co.c_str (), ns)) + { + // Parsed the option but not its value? + // + if (ns.end () != 2) + throw ::butl::cli::invalid_value (co, v); + + s.next (); + r = true; + continue; + } + else + { + // Set the unknown option and fall through. + // + o = co.c_str (); + } + } + + // Handle combined flags. + // + char cf[3]; + { + const char* p = o + 1; + for (; *p != '\0'; ++p) + { + if (!((*p >= 'a' && *p <= 'z') || + (*p >= 'A' && *p <= 'Z') || + (*p >= '0' && *p <= '9'))) + break; + } + + if (*p == '\0') + { + for (p = o + 1; *p != '\0'; ++p) + { + std::strcpy (cf, "-"); + cf[1] = *p; + cf[2] = '\0'; + + int ac (1); + char* av[] = + { + cf + }; + + ::butl::cli::argv_scanner ns (0, ac, av); + + if (!_parse (cf, ns)) + break; + } + + if (*p == '\0') + { + // All handled. + // + s.next (); + r = true; + continue; + } + else + { + // Set the unknown option and fall through. + // + o = cf; + } + } + } + + switch (opt_mode) + { + case ::butl::cli::unknown_mode::skip: + { + s.skip (); + r = true; + continue; + } + case ::butl::cli::unknown_mode::stop: + { + break; + } + case ::butl::cli::unknown_mode::fail: + { + throw ::butl::cli::unknown_option (o); + } + } + + break; + } + } + + switch (arg_mode) + { + case ::butl::cli::unknown_mode::skip: + { + s.skip (); + r = true; + continue; + } + case ::butl::cli::unknown_mode::stop: + { + break; + } + case ::butl::cli::unknown_mode::fail: + { + throw ::butl::cli::unknown_argument (o); + } + } + + break; + } + + return r; + } + + // sleep_options + // + + sleep_options:: + sleep_options () + { + } + + bool sleep_options:: + parse (int& argc, + char** argv, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (argc, argv, erase); + bool r = _parse (s, opt, arg); + return r; + } + + bool sleep_options:: + parse (int start, + int& argc, + char** argv, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (start, argc, argv, erase); + bool r = _parse (s, opt, arg); + return r; + } + + bool sleep_options:: + parse (int& argc, + char** argv, + int& end, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (argc, argv, erase); + bool r = _parse (s, opt, arg); + end = s.end (); + return r; + } + + bool sleep_options:: + parse (int start, + int& argc, + char** argv, + int& end, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (start, argc, argv, erase); + bool r = _parse (s, opt, arg); + end = s.end (); + return r; + } + + bool sleep_options:: + parse (::butl::cli::scanner& s, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + bool r = _parse (s, opt, arg); + return r; + } + + typedef + std::map<std::string, void (*) (sleep_options&, ::butl::cli::scanner&)> + _cli_sleep_options_map; + + static _cli_sleep_options_map _cli_sleep_options_map_; + + struct _cli_sleep_options_map_init + { + _cli_sleep_options_map_init () + { + } + }; + + static _cli_sleep_options_map_init _cli_sleep_options_map_init_; + + bool sleep_options:: + _parse (const char* o, ::butl::cli::scanner& s) + { + _cli_sleep_options_map::const_iterator i (_cli_sleep_options_map_.find (o)); + + if (i != _cli_sleep_options_map_.end ()) + { + (*(i->second)) (*this, s); + return true; + } + + return false; + } + + bool sleep_options:: + _parse (::butl::cli::scanner& s, + ::butl::cli::unknown_mode opt_mode, + ::butl::cli::unknown_mode arg_mode) + { + // Can't skip combined flags (--no-combined-flags). + // + assert (opt_mode != ::butl::cli::unknown_mode::skip); + + bool r = false; + bool opt = true; + + while (s.more ()) + { + const char* o = s.peek (); + + if (std::strcmp (o, "--") == 0) + { + opt = false; + } + + if (opt) + { + if (_parse (o, s)) + { + r = true; + continue; + } + + if (std::strncmp (o, "-", 1) == 0 && o[1] != '\0') + { + // Handle combined option values. + // + std::string co; + if (const char* v = std::strchr (o, '=')) + { + co.assign (o, 0, v - o); + ++v; + + int ac (2); + char* av[] = + { + const_cast<char*> (co.c_str ()), + const_cast<char*> (v) + }; + + ::butl::cli::argv_scanner ns (0, ac, av); + + if (_parse (co.c_str (), ns)) + { + // Parsed the option but not its value? + // + if (ns.end () != 2) + throw ::butl::cli::invalid_value (co, v); + + s.next (); + r = true; + continue; + } + else + { + // Set the unknown option and fall through. + // + o = co.c_str (); + } + } + + // Handle combined flags. + // + char cf[3]; + { + const char* p = o + 1; + for (; *p != '\0'; ++p) + { + if (!((*p >= 'a' && *p <= 'z') || + (*p >= 'A' && *p <= 'Z') || + (*p >= '0' && *p <= '9'))) + break; + } + + if (*p == '\0') + { + for (p = o + 1; *p != '\0'; ++p) + { + std::strcpy (cf, "-"); + cf[1] = *p; + cf[2] = '\0'; + + int ac (1); + char* av[] = + { + cf + }; + + ::butl::cli::argv_scanner ns (0, ac, av); + + if (!_parse (cf, ns)) + break; + } + + if (*p == '\0') + { + // All handled. + // + s.next (); + r = true; + continue; + } + else + { + // Set the unknown option and fall through. + // + o = cf; + } + } + } + + switch (opt_mode) + { + case ::butl::cli::unknown_mode::skip: + { + s.skip (); + r = true; + continue; + } + case ::butl::cli::unknown_mode::stop: + { + break; + } + case ::butl::cli::unknown_mode::fail: + { + throw ::butl::cli::unknown_option (o); + } + } + + break; + } + } + + switch (arg_mode) + { + case ::butl::cli::unknown_mode::skip: + { + s.skip (); + r = true; + continue; + } + case ::butl::cli::unknown_mode::stop: + { + break; + } + case ::butl::cli::unknown_mode::fail: + { + throw ::butl::cli::unknown_argument (o); + } + } + + break; + } + + return r; + } + + // test_options + // + + test_options:: + test_options () + : file_ (), + directory_ () + { + } + + bool test_options:: + parse (int& argc, + char** argv, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (argc, argv, erase); + bool r = _parse (s, opt, arg); + return r; + } + + bool test_options:: + parse (int start, + int& argc, + char** argv, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (start, argc, argv, erase); + bool r = _parse (s, opt, arg); + return r; + } + + bool test_options:: + parse (int& argc, + char** argv, + int& end, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (argc, argv, erase); + bool r = _parse (s, opt, arg); + end = s.end (); + return r; + } + + bool test_options:: + parse (int start, + int& argc, + char** argv, + int& end, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (start, argc, argv, erase); + bool r = _parse (s, opt, arg); + end = s.end (); + return r; + } + + bool test_options:: + parse (::butl::cli::scanner& s, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + bool r = _parse (s, opt, arg); + return r; + } + + typedef + std::map<std::string, void (*) (test_options&, ::butl::cli::scanner&)> + _cli_test_options_map; + + static _cli_test_options_map _cli_test_options_map_; + + struct _cli_test_options_map_init + { + _cli_test_options_map_init () + { + _cli_test_options_map_["--file"] = + &::butl::cli::thunk< test_options, bool, &test_options::file_ >; + _cli_test_options_map_["-f"] = + &::butl::cli::thunk< test_options, bool, &test_options::file_ >; + _cli_test_options_map_["--directory"] = + &::butl::cli::thunk< test_options, bool, &test_options::directory_ >; + _cli_test_options_map_["-d"] = + &::butl::cli::thunk< test_options, bool, &test_options::directory_ >; + } + }; + + static _cli_test_options_map_init _cli_test_options_map_init_; + + bool test_options:: + _parse (const char* o, ::butl::cli::scanner& s) + { + _cli_test_options_map::const_iterator i (_cli_test_options_map_.find (o)); + + if (i != _cli_test_options_map_.end ()) + { + (*(i->second)) (*this, s); + return true; + } + + return false; + } + + bool test_options:: + _parse (::butl::cli::scanner& s, + ::butl::cli::unknown_mode opt_mode, + ::butl::cli::unknown_mode arg_mode) + { + // Can't skip combined flags (--no-combined-flags). + // + assert (opt_mode != ::butl::cli::unknown_mode::skip); + + bool r = false; + bool opt = true; + + while (s.more ()) + { + const char* o = s.peek (); + + if (std::strcmp (o, "--") == 0) + { + opt = false; + } + + if (opt) + { + if (_parse (o, s)) + { + r = true; + continue; + } + + if (std::strncmp (o, "-", 1) == 0 && o[1] != '\0') + { + // Handle combined option values. + // + std::string co; + if (const char* v = std::strchr (o, '=')) + { + co.assign (o, 0, v - o); + ++v; + + int ac (2); + char* av[] = + { + const_cast<char*> (co.c_str ()), + const_cast<char*> (v) + }; + + ::butl::cli::argv_scanner ns (0, ac, av); + + if (_parse (co.c_str (), ns)) + { + // Parsed the option but not its value? + // + if (ns.end () != 2) + throw ::butl::cli::invalid_value (co, v); + + s.next (); + r = true; + continue; + } + else + { + // Set the unknown option and fall through. + // + o = co.c_str (); + } + } + + // Handle combined flags. + // + char cf[3]; + { + const char* p = o + 1; + for (; *p != '\0'; ++p) + { + if (!((*p >= 'a' && *p <= 'z') || + (*p >= 'A' && *p <= 'Z') || + (*p >= '0' && *p <= '9'))) + break; + } + + if (*p == '\0') + { + for (p = o + 1; *p != '\0'; ++p) + { + std::strcpy (cf, "-"); + cf[1] = *p; + cf[2] = '\0'; + + int ac (1); + char* av[] = + { + cf + }; + + ::butl::cli::argv_scanner ns (0, ac, av); + + if (!_parse (cf, ns)) + break; + } + + if (*p == '\0') + { + // All handled. + // + s.next (); + r = true; + continue; + } + else + { + // Set the unknown option and fall through. + // + o = cf; + } + } + } + + switch (opt_mode) + { + case ::butl::cli::unknown_mode::skip: + { + s.skip (); + r = true; + continue; + } + case ::butl::cli::unknown_mode::stop: + { + break; + } + case ::butl::cli::unknown_mode::fail: + { + throw ::butl::cli::unknown_option (o); + } + } + + break; + } + } + + switch (arg_mode) + { + case ::butl::cli::unknown_mode::skip: + { + s.skip (); + r = true; + continue; + } + case ::butl::cli::unknown_mode::stop: + { + break; + } + case ::butl::cli::unknown_mode::fail: + { + throw ::butl::cli::unknown_argument (o); + } + } + + break; + } + + return r; + } + + // touch_options + // + + touch_options:: + touch_options () + : after_ (), + after_specified_ (false) + { + } + + bool touch_options:: + parse (int& argc, + char** argv, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (argc, argv, erase); + bool r = _parse (s, opt, arg); + return r; + } + + bool touch_options:: + parse (int start, + int& argc, + char** argv, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (start, argc, argv, erase); + bool r = _parse (s, opt, arg); + return r; + } + + bool touch_options:: + parse (int& argc, + char** argv, + int& end, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (argc, argv, erase); + bool r = _parse (s, opt, arg); + end = s.end (); + return r; + } + + bool touch_options:: + parse (int start, + int& argc, + char** argv, + int& end, + bool erase, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + ::butl::cli::argv_scanner s (start, argc, argv, erase); + bool r = _parse (s, opt, arg); + end = s.end (); + return r; + } + + bool touch_options:: + parse (::butl::cli::scanner& s, + ::butl::cli::unknown_mode opt, + ::butl::cli::unknown_mode arg) + { + bool r = _parse (s, opt, arg); + return r; + } + + typedef + std::map<std::string, void (*) (touch_options&, ::butl::cli::scanner&)> + _cli_touch_options_map; + + static _cli_touch_options_map _cli_touch_options_map_; + + struct _cli_touch_options_map_init + { + _cli_touch_options_map_init () + { + _cli_touch_options_map_["--after"] = + &::butl::cli::thunk< touch_options, std::string, &touch_options::after_, + &touch_options::after_specified_ >; + } + }; + + static _cli_touch_options_map_init _cli_touch_options_map_init_; + + bool touch_options:: + _parse (const char* o, ::butl::cli::scanner& s) + { + _cli_touch_options_map::const_iterator i (_cli_touch_options_map_.find (o)); + + if (i != _cli_touch_options_map_.end ()) + { + (*(i->second)) (*this, s); + return true; + } + + return false; + } + + bool touch_options:: + _parse (::butl::cli::scanner& s, + ::butl::cli::unknown_mode opt_mode, + ::butl::cli::unknown_mode arg_mode) + { + // Can't skip combined flags (--no-combined-flags). + // + assert (opt_mode != ::butl::cli::unknown_mode::skip); + + bool r = false; + bool opt = true; + + while (s.more ()) + { + const char* o = s.peek (); + + if (std::strcmp (o, "--") == 0) + { + opt = false; + } + + if (opt) + { + if (_parse (o, s)) + { + r = true; + continue; + } + + if (std::strncmp (o, "-", 1) == 0 && o[1] != '\0') + { + // Handle combined option values. + // + std::string co; + if (const char* v = std::strchr (o, '=')) + { + co.assign (o, 0, v - o); + ++v; + + int ac (2); + char* av[] = + { + const_cast<char*> (co.c_str ()), + const_cast<char*> (v) + }; + + ::butl::cli::argv_scanner ns (0, ac, av); + + if (_parse (co.c_str (), ns)) + { + // Parsed the option but not its value? + // + if (ns.end () != 2) + throw ::butl::cli::invalid_value (co, v); + + s.next (); + r = true; + continue; + } + else + { + // Set the unknown option and fall through. + // + o = co.c_str (); + } + } + + // Handle combined flags. + // + char cf[3]; + { + const char* p = o + 1; + for (; *p != '\0'; ++p) + { + if (!((*p >= 'a' && *p <= 'z') || + (*p >= 'A' && *p <= 'Z') || + (*p >= '0' && *p <= '9'))) + break; + } + + if (*p == '\0') + { + for (p = o + 1; *p != '\0'; ++p) + { + std::strcpy (cf, "-"); + cf[1] = *p; + cf[2] = '\0'; + + int ac (1); + char* av[] = + { + cf + }; + + ::butl::cli::argv_scanner ns (0, ac, av); + + if (!_parse (cf, ns)) + break; + } + + if (*p == '\0') + { + // All handled. + // + s.next (); + r = true; + continue; + } + else + { + // Set the unknown option and fall through. + // + o = cf; + } + } + } + + switch (opt_mode) + { + case ::butl::cli::unknown_mode::skip: + { + s.skip (); + r = true; + continue; + } + case ::butl::cli::unknown_mode::stop: + { + break; + } + case ::butl::cli::unknown_mode::fail: + { + throw ::butl::cli::unknown_option (o); + } + } + + break; + } + } + + switch (arg_mode) + { + case ::butl::cli::unknown_mode::skip: + { + s.skip (); + r = true; + continue; + } + case ::butl::cli::unknown_mode::stop: + { + break; + } + case ::butl::cli::unknown_mode::fail: + { + throw ::butl::cli::unknown_argument (o); + } + } + + break; + } + + return r; + } +} + +// Begin epilogue. +// +// +// End epilogue. + diff --git a/libbutl/builtin-options.hxx b/libbutl/builtin-options.hxx new file mode 100644 index 0000000..99d955d --- /dev/null +++ b/libbutl/builtin-options.hxx @@ -0,0 +1,1006 @@ +// -*- C++ -*- +// +// This file was generated by CLI, a command line interface +// compiler for C++. +// + +#ifndef LIBBUTL_BUILTIN_OPTIONS_HXX +#define LIBBUTL_BUILTIN_OPTIONS_HXX + +// Begin prologue. +// +// +// End prologue. + +#include <vector> +#include <iosfwd> +#include <string> +#include <cstddef> +#include <exception> + +#ifndef CLI_POTENTIALLY_UNUSED +# if defined(_MSC_VER) || defined(__xlC__) +# define CLI_POTENTIALLY_UNUSED(x) (void*)&x +# else +# define CLI_POTENTIALLY_UNUSED(x) (void)x +# endif +#endif + +namespace butl +{ + namespace cli + { + class unknown_mode + { + public: + enum value + { + skip, + stop, + fail + }; + + unknown_mode (value); + + operator value () const + { + return v_; + } + + private: + value v_; + }; + + // Exceptions. + // + + class exception: public std::exception + { + public: + virtual void + print (::std::ostream&) const = 0; + }; + + ::std::ostream& + operator<< (::std::ostream&, const exception&); + + class unknown_option: public exception + { + public: + virtual + ~unknown_option () throw (); + + unknown_option (const std::string& option); + + const std::string& + option () const; + + virtual void + print (::std::ostream&) const; + + virtual const char* + what () const throw (); + + private: + std::string option_; + }; + + class unknown_argument: public exception + { + public: + virtual + ~unknown_argument () throw (); + + unknown_argument (const std::string& argument); + + const std::string& + argument () const; + + virtual void + print (::std::ostream&) const; + + virtual const char* + what () const throw (); + + private: + std::string argument_; + }; + + class missing_value: public exception + { + public: + virtual + ~missing_value () throw (); + + missing_value (const std::string& option); + + const std::string& + option () const; + + virtual void + print (::std::ostream&) const; + + virtual const char* + what () const throw (); + + private: + std::string option_; + }; + + class invalid_value: public exception + { + public: + virtual + ~invalid_value () throw (); + + invalid_value (const std::string& option, + const std::string& value, + const std::string& message = std::string ()); + + const std::string& + option () const; + + const std::string& + value () const; + + const std::string& + message () const; + + virtual void + print (::std::ostream&) const; + + virtual const char* + what () const throw (); + + private: + std::string option_; + std::string value_; + std::string message_; + }; + + class eos_reached: public exception + { + public: + virtual void + print (::std::ostream&) const; + + virtual const char* + what () const throw (); + }; + + // Command line argument scanner interface. + // + // The values returned by next() are guaranteed to be valid + // for the two previous arguments up until a call to a third + // peek() or next(). + // + class scanner + { + public: + virtual + ~scanner (); + + virtual bool + more () = 0; + + virtual const char* + peek () = 0; + + virtual const char* + next () = 0; + + virtual void + skip () = 0; + }; + + class argv_scanner: public scanner + { + public: + argv_scanner (int& argc, char** argv, bool erase = false); + argv_scanner (int start, int& argc, char** argv, bool erase = false); + + int + end () const; + + virtual bool + more (); + + virtual const char* + peek (); + + virtual const char* + next (); + + virtual void + skip (); + + private: + int i_; + int& argc_; + char** argv_; + bool erase_; + }; + + class vector_scanner: public scanner + { + public: + vector_scanner (const std::vector<std::string>&, std::size_t start = 0); + + std::size_t + end () const; + + void + reset (std::size_t start = 0); + + virtual bool + more (); + + virtual const char* + peek (); + + virtual const char* + next (); + + virtual void + skip (); + + private: + const std::vector<std::string>& v_; + std::size_t i_; + }; + + template <typename X> + struct parser; + } +} + +#include <string> + +#include <vector> + +namespace butl +{ + class cat_options + { + public: + cat_options (); + + // Return true if anything has been parsed. + // + bool + parse (int& argc, + char** argv, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (int start, + int& argc, + char** argv, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (int& argc, + char** argv, + int& end, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (int start, + int& argc, + char** argv, + int& end, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (::butl::cli::scanner&, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + // Option accessors. + // + // Implementation details. + // + protected: + bool + _parse (const char*, ::butl::cli::scanner&); + + private: + bool + _parse (::butl::cli::scanner&, + ::butl::cli::unknown_mode option, + ::butl::cli::unknown_mode argument); + + public: + }; + + class cp_options + { + public: + cp_options (); + + // Return true if anything has been parsed. + // + bool + parse (int& argc, + char** argv, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (int start, + int& argc, + char** argv, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (int& argc, + char** argv, + int& end, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (int start, + int& argc, + char** argv, + int& end, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (::butl::cli::scanner&, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + // Option accessors. + // + const bool& + recursive () const; + + const bool& + preserve () const; + + // Implementation details. + // + protected: + bool + _parse (const char*, ::butl::cli::scanner&); + + private: + bool + _parse (::butl::cli::scanner&, + ::butl::cli::unknown_mode option, + ::butl::cli::unknown_mode argument); + + public: + bool recursive_; + bool preserve_; + }; + + class ln_options + { + public: + ln_options (); + + // Return true if anything has been parsed. + // + bool + parse (int& argc, + char** argv, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (int start, + int& argc, + char** argv, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (int& argc, + char** argv, + int& end, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (int start, + int& argc, + char** argv, + int& end, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (::butl::cli::scanner&, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + // Option accessors. + // + const bool& + symbolic () const; + + // Implementation details. + // + protected: + bool + _parse (const char*, ::butl::cli::scanner&); + + private: + bool + _parse (::butl::cli::scanner&, + ::butl::cli::unknown_mode option, + ::butl::cli::unknown_mode argument); + + public: + bool symbolic_; + }; + + class mkdir_options + { + public: + mkdir_options (); + + // Return true if anything has been parsed. + // + bool + parse (int& argc, + char** argv, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (int start, + int& argc, + char** argv, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (int& argc, + char** argv, + int& end, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (int start, + int& argc, + char** argv, + int& end, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (::butl::cli::scanner&, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + // Option accessors. + // + const bool& + parents () const; + + // Implementation details. + // + protected: + bool + _parse (const char*, ::butl::cli::scanner&); + + private: + bool + _parse (::butl::cli::scanner&, + ::butl::cli::unknown_mode option, + ::butl::cli::unknown_mode argument); + + public: + bool parents_; + }; + + class mv_options + { + public: + mv_options (); + + // Return true if anything has been parsed. + // + bool + parse (int& argc, + char** argv, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (int start, + int& argc, + char** argv, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (int& argc, + char** argv, + int& end, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (int start, + int& argc, + char** argv, + int& end, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (::butl::cli::scanner&, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + // Option accessors. + // + const bool& + force () const; + + // Implementation details. + // + protected: + bool + _parse (const char*, ::butl::cli::scanner&); + + private: + bool + _parse (::butl::cli::scanner&, + ::butl::cli::unknown_mode option, + ::butl::cli::unknown_mode argument); + + public: + bool force_; + }; + + class rm_options + { + public: + rm_options (); + + // Return true if anything has been parsed. + // + bool + parse (int& argc, + char** argv, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (int start, + int& argc, + char** argv, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (int& argc, + char** argv, + int& end, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (int start, + int& argc, + char** argv, + int& end, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (::butl::cli::scanner&, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + // Option accessors. + // + const bool& + recursive () const; + + const bool& + force () const; + + // Implementation details. + // + protected: + bool + _parse (const char*, ::butl::cli::scanner&); + + private: + bool + _parse (::butl::cli::scanner&, + ::butl::cli::unknown_mode option, + ::butl::cli::unknown_mode argument); + + public: + bool recursive_; + bool force_; + }; + + class rmdir_options + { + public: + rmdir_options (); + + // Return true if anything has been parsed. + // + bool + parse (int& argc, + char** argv, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (int start, + int& argc, + char** argv, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (int& argc, + char** argv, + int& end, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (int start, + int& argc, + char** argv, + int& end, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (::butl::cli::scanner&, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + // Option accessors. + // + const bool& + force () const; + + // Implementation details. + // + protected: + bool + _parse (const char*, ::butl::cli::scanner&); + + private: + bool + _parse (::butl::cli::scanner&, + ::butl::cli::unknown_mode option, + ::butl::cli::unknown_mode argument); + + public: + bool force_; + }; + + class sed_options + { + public: + sed_options (); + + // Return true if anything has been parsed. + // + bool + parse (int& argc, + char** argv, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (int start, + int& argc, + char** argv, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (int& argc, + char** argv, + int& end, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (int start, + int& argc, + char** argv, + int& end, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (::butl::cli::scanner&, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + // Option accessors. + // + const bool& + quiet () const; + + const bool& + in_place () const; + + const std::vector<std::string>& + expression () const; + + bool + expression_specified () const; + + // Implementation details. + // + protected: + bool + _parse (const char*, ::butl::cli::scanner&); + + private: + bool + _parse (::butl::cli::scanner&, + ::butl::cli::unknown_mode option, + ::butl::cli::unknown_mode argument); + + public: + bool quiet_; + bool in_place_; + std::vector<std::string> expression_; + bool expression_specified_; + }; + + class sleep_options + { + public: + sleep_options (); + + // Return true if anything has been parsed. + // + bool + parse (int& argc, + char** argv, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (int start, + int& argc, + char** argv, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (int& argc, + char** argv, + int& end, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (int start, + int& argc, + char** argv, + int& end, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (::butl::cli::scanner&, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + // Option accessors. + // + // Implementation details. + // + protected: + bool + _parse (const char*, ::butl::cli::scanner&); + + private: + bool + _parse (::butl::cli::scanner&, + ::butl::cli::unknown_mode option, + ::butl::cli::unknown_mode argument); + + public: + }; + + class test_options + { + public: + test_options (); + + // Return true if anything has been parsed. + // + bool + parse (int& argc, + char** argv, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (int start, + int& argc, + char** argv, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (int& argc, + char** argv, + int& end, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (int start, + int& argc, + char** argv, + int& end, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (::butl::cli::scanner&, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + // Option accessors. + // + const bool& + file () const; + + const bool& + directory () const; + + // Implementation details. + // + protected: + bool + _parse (const char*, ::butl::cli::scanner&); + + private: + bool + _parse (::butl::cli::scanner&, + ::butl::cli::unknown_mode option, + ::butl::cli::unknown_mode argument); + + public: + bool file_; + bool directory_; + }; + + class touch_options + { + public: + touch_options (); + + // Return true if anything has been parsed. + // + bool + parse (int& argc, + char** argv, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (int start, + int& argc, + char** argv, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (int& argc, + char** argv, + int& end, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (int start, + int& argc, + char** argv, + int& end, + bool erase = false, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + bool + parse (::butl::cli::scanner&, + ::butl::cli::unknown_mode option = ::butl::cli::unknown_mode::fail, + ::butl::cli::unknown_mode argument = ::butl::cli::unknown_mode::stop); + + // Option accessors. + // + const std::string& + after () const; + + bool + after_specified () const; + + // Implementation details. + // + protected: + bool + _parse (const char*, ::butl::cli::scanner&); + + private: + bool + _parse (::butl::cli::scanner&, + ::butl::cli::unknown_mode option, + ::butl::cli::unknown_mode argument); + + public: + std::string after_; + bool after_specified_; + }; +} + +#include <libbutl/builtin-options.ixx> + +// Begin epilogue. +// +// +// End epilogue. + +#endif // LIBBUTL_BUILTIN_OPTIONS_HXX diff --git a/libbutl/builtin-options.ixx b/libbutl/builtin-options.ixx new file mode 100644 index 0000000..b2275c6 --- /dev/null +++ b/libbutl/builtin-options.ixx @@ -0,0 +1,284 @@ +// -*- C++ -*- +// +// This file was generated by CLI, a command line interface +// compiler for C++. +// + +// Begin prologue. +// +// +// End prologue. + +#include <cassert> + +namespace butl +{ + namespace cli + { + // unknown_mode + // + inline unknown_mode:: + unknown_mode (value v) + : v_ (v) + { + } + + // exception + // + inline ::std::ostream& + operator<< (::std::ostream& os, const exception& e) + { + e.print (os); + return os; + } + + // unknown_option + // + inline unknown_option:: + unknown_option (const std::string& option) + : option_ (option) + { + } + + inline const std::string& unknown_option:: + option () const + { + return option_; + } + + // unknown_argument + // + inline unknown_argument:: + unknown_argument (const std::string& argument) + : argument_ (argument) + { + } + + inline const std::string& unknown_argument:: + argument () const + { + return argument_; + } + + // missing_value + // + inline missing_value:: + missing_value (const std::string& option) + : option_ (option) + { + } + + inline const std::string& missing_value:: + option () const + { + return option_; + } + + // invalid_value + // + inline invalid_value:: + invalid_value (const std::string& option, + const std::string& value, + const std::string& message) + : option_ (option), + value_ (value), + message_ (message) + { + } + + inline const std::string& invalid_value:: + option () const + { + return option_; + } + + inline const std::string& invalid_value:: + value () const + { + return value_; + } + + inline const std::string& invalid_value:: + message () const + { + return message_; + } + + // argv_scanner + // + inline argv_scanner:: + argv_scanner (int& argc, char** argv, bool erase) + : i_ (1), argc_ (argc), argv_ (argv), erase_ (erase) + { + } + + inline argv_scanner:: + argv_scanner (int start, int& argc, char** argv, bool erase) + : i_ (start), argc_ (argc), argv_ (argv), erase_ (erase) + { + } + + inline int argv_scanner:: + end () const + { + return i_; + } + + // vector_scanner + // + inline vector_scanner:: + vector_scanner (const std::vector<std::string>& v, std::size_t i) + : v_ (v), i_ (i) + { + } + + inline std::size_t vector_scanner:: + end () const + { + return i_; + } + + inline void vector_scanner:: + reset (std::size_t i) + { + i_ = i; + } + } +} + +namespace butl +{ + // cat_options + // + + // cp_options + // + + inline const bool& cp_options:: + recursive () const + { + return this->recursive_; + } + + inline const bool& cp_options:: + preserve () const + { + return this->preserve_; + } + + // ln_options + // + + inline const bool& ln_options:: + symbolic () const + { + return this->symbolic_; + } + + // mkdir_options + // + + inline const bool& mkdir_options:: + parents () const + { + return this->parents_; + } + + // mv_options + // + + inline const bool& mv_options:: + force () const + { + return this->force_; + } + + // rm_options + // + + inline const bool& rm_options:: + recursive () const + { + return this->recursive_; + } + + inline const bool& rm_options:: + force () const + { + return this->force_; + } + + // rmdir_options + // + + inline const bool& rmdir_options:: + force () const + { + return this->force_; + } + + // sed_options + // + + inline const bool& sed_options:: + quiet () const + { + return this->quiet_; + } + + inline const bool& sed_options:: + in_place () const + { + return this->in_place_; + } + + inline const std::vector<std::string>& sed_options:: + expression () const + { + return this->expression_; + } + + inline bool sed_options:: + expression_specified () const + { + return this->expression_specified_; + } + + // sleep_options + // + + // test_options + // + + inline const bool& test_options:: + file () const + { + return this->file_; + } + + inline const bool& test_options:: + directory () const + { + return this->directory_; + } + + // touch_options + // + + inline const std::string& touch_options:: + after () const + { + return this->after_; + } + + inline bool touch_options:: + after_specified () const + { + return this->after_specified_; + } +} + +// Begin epilogue. +// +// +// End epilogue. diff --git a/libbutl/builtin.cli b/libbutl/builtin.cli new file mode 100644 index 0000000..284a111 --- /dev/null +++ b/libbutl/builtin.cli @@ -0,0 +1,82 @@ +// file : libbutl/builtin.cli +// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +include <string>; +include <vector>; + +// Note that options in this file are undocumented because we generate neither +// the usage printing code nor man pages. Instead, they are documented in the +// Testscript Language Manual's builtin descriptions. +// +// @@ Update the above reference when the documentation is moved to another +// place. +// +// Also note that the string type is used for the path options because their +// parsing depends on the working directory (see parse_path() for details) and +// passing this information to the CLI custom parser would not be easy. +// +namespace butl +{ + class cat_options + { + // No options so far. + // + }; + + class cp_options + { + bool --recursive|-R|-r; + bool --preserve|-p; + }; + + class ln_options + { + bool --symbolic|-s; + }; + + class mkdir_options + { + bool --parents|-p; + }; + + class mv_options + { + bool --force|-f; + }; + + class rm_options + { + bool --recursive|-r; + bool --force|-f; + }; + + class rmdir_options + { + bool --force|-f; + }; + + class sed_options + { + bool --quiet|-n; + bool --in-place|-i; + std::vector<std::string> --expression|-e; + }; + + class sleep_options + { + // No options so far. + // + }; + + class test_options + { + bool --file|-f; + bool --directory|-d; + }; + + class touch_options + { + std::string --after; // Path (see above). + }; +} diff --git a/libbutl/builtin.cxx b/libbutl/builtin.cxx new file mode 100644 index 0000000..3340271 --- /dev/null +++ b/libbutl/builtin.cxx @@ -0,0 +1,2111 @@ +// file : libbutl/builtin.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef __cpp_modules_ts +#include <libbutl/builtin.mxx> +#endif + +#ifdef _WIN32 +# include <libbutl/win32-utility.hxx> +#endif + +#include <cassert> + +#ifndef __cpp_lib_modules_ts +#include <map> +#include <string> +#include <vector> +#include <thread> +#include <utility> // move(), forward() +#include <cstdint> // uint*_t +#include <functional> + +#include <ios> +#include <chrono> +#include <cerrno> +#include <ostream> +#include <sstream> +#include <cstdlib> // strtoull() +#include <cstring> // strcmp() +#include <exception> +#include <system_error> + +#endif + +#include <libbutl/builtin-options.hxx> + +#ifdef __cpp_modules_ts +module butl.builtin; + +// Only imports additional to interface. +#ifdef __clang__ +#ifdef __cpp_lib_modules_ts +import std.core; +import std.io; +import std.threading; +#endif +import butl.path; +import butl.fdstream; +import butl.timestamp; +#endif + +import butl.regex; +import butl.path_io; +import butl.optional; +import butl.filesystem; +import butl.small_vector; +#else +#include <libbutl/regex.mxx> +#include <libbutl/path-io.mxx> +#include <libbutl/optional.mxx> +#include <libbutl/filesystem.mxx> +#include <libbutl/small-vector.mxx> +#endif + +// Strictly speaking a builtin which reads/writes from/to standard streams +// must be asynchronous so that the caller can communicate with it through +// pipes without being blocked on I/O operations. However, as an optimization, +// we allow builtins that only print diagnostics to STDERR to be synchronous +// assuming that their output will always fit the pipe buffer. Synchronous +// builtins must not read from STDIN and write to STDOUT. Later we may relax +// this rule to allow a "short" output for such builtins. +// +using namespace std; + +namespace butl +{ + using strings = vector<string>; + using io_error = ios_base::failure; + + using builtin_impl = uint8_t (const strings& args, + auto_fd in, auto_fd out, auto_fd err, + const dir_path& cwd, + const builtin_callbacks&); + + // Operation failed, diagnostics has already been issued. + // + struct failed {}; + + // Accumulate an error message, print it atomically in dtor to the provided + // stream and throw failed afterwards if requested. Prefixes the message + // with the builtin name. + // + // Move constructible-only, not assignable (based to diag_record). + // + class error_record + { + public: + template <typename T> + friend const error_record& + operator<< (const error_record& r, const T& x) + { + r.ss_ << x; + return r; + } + + error_record (ostream& o, bool fail, const char* name) + : os_ (o), fail_ (fail), empty_ (false) + { + ss_ << name << ": "; + } + + // Older versions of libstdc++ don't have the ostringstream move support. + // Luckily, GCC doesn't seem to be actually needing move due to copy/move + // elision. + // +#ifdef __GLIBCXX__ + error_record (error_record&&); +#else + error_record (error_record&& r) + : os_ (r.os_), + ss_ (move (r.ss_)), + fail_ (r.fail_), + empty_ (r.empty_) + { + r.empty_ = true; + } +#endif + + ~error_record () noexcept (false) + { + if (!empty_) + { + // The output stream can be in a bad state (for example as a result of + // unsuccessful attempt to report a previous error), so we check it. + // + if (os_.good ()) + { + ss_.put ('\n'); + os_ << ss_.str (); + os_.flush (); + } + + if (fail_) + throw failed (); + } + } + + private: + ostream& os_; + mutable ostringstream ss_; + + bool fail_; + bool empty_; + }; + + // Call a function returning its resulting value. Fail if an exception is + // thrown by the function. + // + template <typename F, typename... A> + static inline auto + call (const function<error_record ()>& fail, + const function<F>& fn, + A&&... args) -> decltype (fn (forward<A> (args)...)) + { + assert (fn); + + try + { + return fn (forward<A> (args)...); + } + catch (const std::exception& e) + { + fail () << e; + } + catch (...) + { + fail () << "unknown error"; + } + + assert (false); // Can't be here. + throw failed (); + } + + // Parse builtin options. Call the callback to parse unknown options and + // throw cli::unknown_option if the callback is not specified or doesn't + // parse the option. + // + template <typename O> + static O + parse (cli::vector_scanner& scan, + const strings& args, + const function<builtin_callbacks::parse_option_function>& parse, + const function<error_record ()>& fail) + { + O ops; + + while (true) + { + // Parse the next chunk of options until we reach an argument, --, + // unknown option, or eos. + // + ops.parse (scan, cli::unknown_mode::stop); + + // Bail out on eos. + // + if (!scan.more ()) + break; + + const char* o (scan.peek ()); + + // Bail out on --. + // + if (strcmp (o, "--") == 0) + { + scan.next (); // Skip --. + break; + } + + // Bail out on an argument. + // + if (!(o[0] == '-' && o[1] != '\0')) + break; + + // Parse the unknown option if the callback is specified and fail if + // that's not the case or the callback doesn't recognize the option + // either. + // + size_t n (parse ? call (fail, parse, args, scan.end ()) : 0); + + if (n == 0) + throw cli::unknown_option (o); + + // Skip the parsed arguments and continue. + // + assert (scan.end () + n <= args.size ()); + scan.reset (scan.end () + n); + } + + return ops; + } + + // Parse and normalize a path. Also, unless it is already absolute, make the + // path absolute using the specified directory (must be an absolute path). + // Fail if the path is empty, and on parsing and normalization errors. + // + static path + parse_path (string s, + const dir_path& d, + const function<error_record ()>& fail) + { + assert (d.absolute ()); + + try + { + path p (move (s)); + + if (p.empty ()) + throw invalid_path (""); + + if (p.relative ()) + p = d / move (p); + + p.normalize (); + return p; + } + catch (const invalid_path& e) + { + fail () << "invalid path '" << e.path << "'"; + } + + assert (false); // Can't be here. + return path (); + } + + // Return the current working directory if wd is empty and wd otherwise, + // completed against the current directory if it is relative. Fail if + // std::system_error is thrown by the underlying function call. + // + dir_path + current_directory (const dir_path& wd, const function<error_record ()>& fail) + { + try + { + if (wd.empty ()) + return dir_path::current_directory (); + + if (wd.relative ()) + return move (dir_path (wd).complete ()); + } + catch (const system_error& e) + { + fail () << "unable to obtain current directory: " << e; + } + + return wd; + } + + // Builtin commands functions. + // + + // cat <file>... + // + // Note that POSIX doesn't specify if after I/O operation failure the + // command should proceed with the rest of the arguments. The current + // implementation exits immediatelly in such a case. + // + // @@ Shouldn't we check that we don't print a nonempty regular file to + // itself, as that would merely exhaust the output device? POSIX allows + // (but not requires) such a check and some implementations do this. That + // would require to fstat() file descriptors and complicate the code a + // bit. Was able to reproduce on a big file (should be bigger than the + // stream buffer size) with the test `cat file >+file`. + // + // Note: must be executed asynchronously. + // + static uint8_t + cat (const strings& args, + auto_fd in, auto_fd out, auto_fd err, + const dir_path& cwd, + const builtin_callbacks& cbs) noexcept + try + { + uint8_t r (1); + ofdstream cerr (err != nullfd ? move (err) : fddup (stderr_fd ())); + + auto error = [&cerr] (bool fail = false) + { + return error_record (cerr, fail, "cat"); + }; + + auto fail = [&error] () {return error (true /* fail */);}; + + try + { + ifdstream cin (in != nullfd ? move (in) : fddup (stdin_fd ()), + fdstream_mode::binary); + + ofdstream cout (out != nullfd ? move (out) : fddup (stdout_fd ()), + fdstream_mode::binary); + + // Parse arguments. + // + cli::vector_scanner scan (args); + parse<cat_options> (scan, args, cbs.parse_option, fail); + + // Print files. + // + // Copy input stream to STDOUT. + // + auto copy = [&cout] (istream& is) + { + if (is.peek () != ifdstream::traits_type::eof ()) + cout << is.rdbuf (); + + is.clear (istream::eofbit); // Sets eofbit. + }; + + // Path of a file being printed to STDOUT. An empty path represents + // STDIN. Used in diagnostics. + // + path p; + + try + { + // Print STDIN. + // + if (!scan.more ()) + copy (cin); + + dir_path wd; + + // Print files. + // + while (scan.more ()) + { + string f (scan.next ()); + + if (f == "-") + { + if (!cin.eof ()) + { + p.clear (); + copy (cin); + } + + continue; + } + + if (wd.empty () && cwd.relative ()) + wd = current_directory (cwd, fail); + + p = parse_path (move (f), !wd.empty () ? wd : cwd, fail); + + ifdstream is (p, fdopen_mode::binary); + copy (is); + is.close (); + } + } + catch (const io_error& e) + { + error_record d (fail ()); + d << "unable to print "; + + if (p.empty ()) + d << "stdin"; + else + d << "'" << p << "'"; + + d << ": " << e; + } + + cin.close (); + cout.close (); + r = 0; + } + // Can be thrown while creating/closing cin, cout or writing to cerr. + // + catch (const io_error& e) + { + error () << e; + } + catch (const failed&) + { + // Diagnostics has already been issued. + } + catch (const cli::exception& e) + { + error () << e; + } + + cerr.close (); + return r; + } + // In particular, handles io_error exception potentially thrown while + // creating, writing to, or closing cerr. + // + catch (const std::exception&) + { + return 1; + } + + // Make a copy of a file at the specified path, preserving permissions, and + // calling the hook for a newly created file. The file paths must be + // absolute and normalized. Fail if an exception is thrown by the underlying + // copy operation. + // + static void + cpfile (const path& from, const path& to, + bool overwrite, + bool attrs, + const builtin_callbacks& cbs, + const function<error_record ()>& fail) + { + assert (from.absolute () && from.normalized ()); + assert (to.absolute () && to.normalized ()); + + try + { + if (cbs.create) + call (fail, cbs.create, to, true /* pre */); + + cpflags f (overwrite + ? cpflags::overwrite_permissions | cpflags::overwrite_content + : cpflags::none); + + if (attrs) + f |= cpflags::overwrite_permissions | cpflags::copy_timestamps; + + cpfile (from, to, f); + + if (cbs.create) + call (fail, cbs.create, to, false /* pre */); + } + catch (const system_error& e) + { + fail () << "unable to copy file '" << from << "' to '" << to << "': " + << e; + } + } + + // Make a copy of a directory at the specified path, calling the hook for + // the created filesystem entries. The directory paths must be absolute and + // normalized. Fail if the destination directory already exists or an + // exception is thrown by the underlying copy operation. + // + static void + cpdir (const dir_path& from, const dir_path& to, + bool attrs, + const builtin_callbacks& cbs, + const function<error_record ()>& fail) + { + assert (from.absolute () && from.normalized ()); + assert (to.absolute () && to.normalized ()); + + try + { + if (cbs.create) + call (fail, cbs.create, to, true /* pre */); + + if (try_mkdir (to) == mkdir_status::already_exists) + throw_generic_error (EEXIST); + + if (cbs.create) + call (fail, cbs.create, to, false /* pre */); + + for (const auto& de: dir_iterator (from, false /* ignore_dangling */)) + { + path f (from / de.path ()); + path t (to / de.path ()); + + if (de.type () == entry_type::directory) + cpdir (path_cast<dir_path> (move (f)), + path_cast<dir_path> (move (t)), + attrs, + cbs, + fail); + else + cpfile (f, t, false /* overwrite */, attrs, cbs, fail); + } + + // Note that it is essential to copy timestamps and permissions after + // the directory content is copied. + // + if (attrs) + { + path_permissions (to, path_permissions (from)); + dir_time (to, dir_time (from)); + } + } + catch (const system_error& e) + { + fail () << "unable to copy directory '" << from << "' to '" << to + << "': " << e; + } + } + + // cp [-p|--preserve] <src-file> <dst-file> + // cp [-p|--preserve] -R|-r|--recursive <src-dir> <dst-dir> + // cp [-p|--preserve] <src-file>... <dst-dir>/ + // cp [-p|--preserve] -R|-r|--recursive <src-path>... <dst-dir>/ + // + // Note: can be executed synchronously. + // + static uint8_t + cp (const strings& args, + auto_fd in, auto_fd out, auto_fd err, + const dir_path& cwd, + const builtin_callbacks& cbs) noexcept + try + { + uint8_t r (1); + ofdstream cerr (err != nullfd ? move (err) : fddup (stderr_fd ())); + + auto error = [&cerr] (bool fail = false) + { + return error_record (cerr, fail, "cp"); + }; + + auto fail = [&error] () {return error (true /* fail */);}; + + try + { + in.close (); + out.close (); + + // Parse arguments. + // + cli::vector_scanner scan (args); + cp_options ops (parse<cp_options> (scan, args, cbs.parse_option, fail)); + + // Copy files or directories. + // + if (!scan.more ()) + fail () << "missing arguments"; + + // Note that the arguments semantics depends on the last argument, so we + // read out and cache them. + // + small_vector<string, 2> args; + while (scan.more ()) + args.push_back (scan.next ()); + + const dir_path& wd (cwd.absolute () + ? cwd + : current_directory (cwd, fail)); + + auto i (args.begin ()); + auto j (args.rbegin ()); + path dst (parse_path (move (*j++), wd, fail)); + auto e (j.base ()); + + if (i == e) + fail () << "missing source path"; + + // If destination is not a directory path (no trailing separator) then + // make a copy of the filesystem entry at the specified path (the only + // source path is allowed in such a case). Otherwise copy the source + // filesystem entries into the destination directory. + // + if (!dst.to_directory ()) + { + path src (parse_path (move (*i++), wd, fail)); + + // If there are multiple sources but no trailing separator for the + // destination, then, most likelly, it is missing. + // + if (i != e) + fail () << "multiple source paths without trailing separator for " + << "destination directory"; + + if (!ops.recursive ()) + // Synopsis 1: make a file copy at the specified path. + // + cpfile (src, dst, true /* overwrite */, ops.preserve (), cbs, fail); + else + // Synopsis 2: make a directory copy at the specified path. + // + cpdir (path_cast<dir_path> (src), path_cast<dir_path> (dst), + ops.preserve (), + cbs, + fail); + } + else + { + for (; i != e; ++i) + { + path src (parse_path (move (*i), wd, fail)); + + if (ops.recursive () && dir_exists (src)) + // Synopsis 4: copy a filesystem entry into the specified + // directory. Note that we handle only source directories here. + // Source files are handled below. + // + cpdir (path_cast<dir_path> (src), + path_cast<dir_path> (dst / src.leaf ()), + ops.preserve (), + cbs, + fail); + else + // Synopsis 3: copy a file into the specified directory. Also, + // here we cover synopsis 4 for the source path being a file. + // + cpfile (src, dst / src.leaf (), + true /* overwrite */, + ops.preserve (), + cbs, + fail); + } + } + + r = 0; + } + // Can be thrown while closing in, out or writing to cerr. + // + catch (const io_error& e) + { + error () << e; + } + catch (const failed&) + { + // Diagnostics has already been issued. + } + catch (const cli::exception& e) + { + error () << e; + } + + cerr.close (); + return r; + } + // In particular, handles io_error exception potentially thrown while + // creating, writing to, or closing cerr. + // + catch (const std::exception&) + { + return 1; + } + + // echo <string>... + // + // Note: must be executed asynchronously. + // + static uint8_t + echo (const strings& args, + auto_fd in, auto_fd out, auto_fd err, + const dir_path&, + const builtin_callbacks&) noexcept + try + { + uint8_t r (1); + ofdstream cerr (err != nullfd ? move (err) : fddup (stderr_fd ())); + + try + { + in.close (); + ofdstream cout (out != nullfd ? move (out) : fddup (stdout_fd ())); + + for (auto b (args.begin ()), i (b), e (args.end ()); i != e; ++i) + cout << (i != b ? " " : "") << *i; + + cout << '\n'; + cout.close (); + r = 0; + } + // Can be thrown while closing cin or creating, writing to, or closing + // cout. + // + catch (const io_error& e) + { + cerr << "echo: " << e << endl; + } + + cerr.close (); + return r; + } + // In particular, handles io_error exception potentially thrown while + // creating, writing to, or closing cerr. + // + catch (const std::exception&) + { + return 1; + } + + // false + // + // Failure to close the file descriptors is silently ignored. + // + // Note: can be executed synchronously. + // + static builtin + false_ (uint8_t& r, + const strings&, + auto_fd, auto_fd, auto_fd, + const dir_path&, + const builtin_callbacks&) + { + return builtin (r = 1); + } + + // true + // + // Failure to close the file descriptors is silently ignored. + // + // Note: can be executed synchronously. + // + static builtin + true_ (uint8_t& r, + const strings&, + auto_fd, auto_fd, auto_fd, + const dir_path&, + const builtin_callbacks&) + { + return builtin (r = 0); + } + + // Create a symlink to a file or directory at the specified path and calling + // the hook for the created filesystem entries. The paths must be absolute + // and normalized. Fall back to creating a hardlink, if symlink creation is + // not supported for the link path. If hardlink creation is not supported + // either, then fall back to copies. Fail if the target filesystem entry + // doesn't exist or an exception is thrown by the underlying filesystem + // operation (specifically for an already existing filesystem entry at the + // link path). + // + // Note that supporting optional removal of an existing filesystem entry at + // the link path (the -f option) tends to get hairy. Also removing non-empty + // directories doesn't look very natural, but would be required if we want + // the behavior on POSIX and Windows to be consistent. + // + static void + mksymlink (const path& target, const path& link, + const builtin_callbacks& cbs, + const function<error_record ()>& fail) + { + assert (target.absolute () && target.normalized ()); + assert (link.absolute () && link.normalized ()); + + // Determine the target type, fail if the target doesn't exist. + // + bool dir (false); + + try + { + pair<bool, entry_stat> pe (path_entry (target)); + + if (!pe.first) + fail () << "unable to create symlink to '" << target << "': no such " + << "file or directory"; + + dir = pe.second.type == entry_type::directory; + } + catch (const system_error& e) + { + fail () << "unable to stat '" << target << "': " << e; + } + + // First we try to create a symlink. If that fails (e.g., "Windows + // happens"), then we resort to hard links. If that doesn't work out + // either (e.g., not on the same filesystem), then we fall back to copies. + // So things are going to get a bit nested. + // + // Note: similar to mkanylink() but with support for directories. + // + try + { + if (cbs.create) + call (fail, cbs.create, link, true /* pre */); + + mksymlink (target, link, dir); + + if (cbs.create) + call (fail, cbs.create, link, false /* pre */); + } + catch (const system_error& e) + { + // Note that we are not guaranteed (here and below) that the + // system_error exception is of the generic category. + // + int c (e.code ().value ()); + if (!(e.code ().category () == generic_category () && + (c == ENOSYS || // Not implemented. + c == EPERM))) // Not supported by the filesystem(s). + fail () << "unable to create symlink '" << link << "' to '" + << target << "': " << e; + + try + { + mkhardlink (target, link, dir); + + if (cbs.create) + call (fail, cbs.create, link, false /* pre */); + } + catch (const system_error& e) + { + c = e.code ().value (); + if (!(e.code ().category () == generic_category () && + (c == ENOSYS || // Not implemented. + c == EPERM || // Not supported by the filesystem(s). + c == EXDEV))) // On different filesystems. + fail () << "unable to create hardlink '" << link << "' to '" + << target << "': " << e; + + if (dir) + cpdir (path_cast<dir_path> (target), path_cast<dir_path> (link), + false /* attrs */, + cbs, + fail); + else + cpfile (target, link, + false /* overwrite */, + true /* attrs */, + cbs, + fail); + } + } + } + + // ln -s|--symbolic <target-path> <link-path> + // ln -s|--symbolic <target-path>... <link-dir>/ + // + // Note: can be executed synchronously. + // + static uint8_t + ln (const strings& args, + auto_fd in, auto_fd out, auto_fd err, + const dir_path& cwd, + const builtin_callbacks& cbs) noexcept + try + { + uint8_t r (1); + ofdstream cerr (err != nullfd ? move (err) : fddup (stderr_fd ())); + + auto error = [&cerr] (bool fail = false) + { + return error_record (cerr, fail, "ln"); + }; + + auto fail = [&error] () {return error (true /* fail */);}; + + try + { + in.close (); + out.close (); + + // Parse arguments. + // + cli::vector_scanner scan (args); + ln_options ops (parse<ln_options> (scan, args, cbs.parse_option, fail)); + + if (!ops.symbolic ()) + fail () << "missing -s|--symbolic option"; + + // Create file or directory symlinks. + // + if (!scan.more ()) + fail () << "missing arguments"; + + // Note that the arguments semantics depends on the last argument, so we + // read out and cache them. + // + small_vector<string, 2> args; + while (scan.more ()) + args.push_back (scan.next ()); + + const dir_path& wd (cwd.absolute () + ? cwd + : current_directory (cwd, fail)); + + auto i (args.begin ()); + auto j (args.rbegin ()); + path link (parse_path (move (*j++), wd, fail)); + auto e (j.base ()); + + if (i == e) + fail () << "missing target path"; + + // If link is not a directory path (no trailing separator), then create + // a symlink to the target path at the specified link path (the only + // target path is allowed in such a case). Otherwise create links to the + // target paths inside the specified directory. + // + if (!link.to_directory ()) + { + path target (parse_path (move (*i++), wd, fail)); + + // If there are multiple targets but no trailing separator for the + // link, then, most likelly, it is missing. + // + if (i != e) + fail () << "multiple target paths with non-directory link path"; + + // Synopsis 1: create a target path symlink at the specified path. + // + mksymlink (target, link, cbs, fail); + } + else + { + for (; i != e; ++i) + { + path target (parse_path (move (*i), wd, fail)); + + // Synopsis 2: create a target path symlink in the specified + // directory. + // + mksymlink (target, link / target.leaf (), cbs, fail); + } + } + + r = 0; + } + // Can be thrown while closing in, out or writing to cerr. + // + catch (const io_error& e) + { + error () << e; + } + catch (const failed&) + { + // Diagnostics has already been issued. + } + catch (const cli::exception& e) + { + error () << e; + } + + cerr.close (); + return r; + } + // In particular, handles io_error exception potentially thrown while + // creating, writing to, or closing cerr. + // + catch (const std::exception&) + { + return 1; + } + + // Create a directory if not exist and its parent directories if necessary, + // calling the hook for the created directories. The directory path must be + // absolute and normalized. Throw system_error on failure. + // + static void + mkdir_p (const dir_path& p, + const builtin_callbacks& cbs, + const function<error_record ()>& fail) + { + assert (p.absolute () && p.normalized ()); + + if (!dir_exists (p)) + { + if (!p.root ()) + mkdir_p (p.directory (), cbs, fail); + + if (cbs.create) + call (fail, cbs.create, p, true /* pre */); + + try_mkdir (p); // Returns success or throws. + + if (cbs.create) + call (fail, cbs.create, p, false /* pre */); + } + } + + // mkdir [-p|--parents] <dir>... + // + // Note that POSIX doesn't specify if after a directory creation failure the + // command should proceed with the rest of the arguments. The current + // implementation exits immediatelly in such a case. + // + // Note: can be executed synchronously. + // + static uint8_t + mkdir (const strings& args, + auto_fd in, auto_fd out, auto_fd err, + const dir_path& cwd, + const builtin_callbacks& cbs) noexcept + try + { + uint8_t r (1); + ofdstream cerr (err != nullfd ? move (err) : fddup (stderr_fd ())); + + auto error = [&cerr] (bool fail = false) + { + return error_record (cerr, fail, "mkdir"); + }; + + auto fail = [&error] () {return error (true /* fail */);}; + + try + { + in.close (); + out.close (); + + // Parse arguments. + // + cli::vector_scanner scan (args); + + mkdir_options ops ( + parse<mkdir_options> (scan, args, cbs.parse_option, fail)); + + // Create directories. + // + if (!scan.more ()) + fail () << "missing directory"; + + const dir_path& wd (cwd.absolute () + ? cwd + : current_directory (cwd, fail)); + + while (scan.more ()) + { + dir_path p ( + path_cast<dir_path> (parse_path (scan.next (), wd, fail))); + + try + { + if (ops.parents ()) + mkdir_p (p, cbs, fail); + else + { + if (cbs.create) + call (fail, cbs.create, p, true /* pre */); + + if (try_mkdir (p) == mkdir_status::success) + { + if (cbs.create) + call (fail, cbs.create, p, false /* pre */); + } + else // == mkdir_status::already_exists + throw_generic_error (EEXIST); + } + } + catch (const system_error& e) + { + fail () << "unable to create directory '" << p << "': " << e; + } + } + + r = 0; + } + // Can be thrown while closing in, out or writing to cerr. + // + catch (const io_error& e) + { + error () << e; + } + catch (const failed&) + { + // Diagnostics has already been issued. + } + catch (const cli::exception& e) + { + error () << e; + } + + cerr.close (); + return r; + } + // In particular, handles io_error exception potentially thrown while + // creating, writing to, or closing cerr. + // + catch (const std::exception&) + { + return 1; + } + + // mv [-f|--force] <src-path> <dst-path> + // mv [-f|--force] <src-path>... <dst-dir>/ + // + // Note: can be executed synchronously. + // + static uint8_t + mv (const strings& args, + auto_fd in, auto_fd out, auto_fd err, + const dir_path& cwd, + const builtin_callbacks& cbs) noexcept + try + { + uint8_t r (1); + ofdstream cerr (err != nullfd ? move (err) : fddup (stderr_fd ())); + + auto error = [&cerr] (bool fail = false) + { + return error_record (cerr, fail, "mv"); + }; + + auto fail = [&error] () {return error (true /* fail */);}; + + try + { + in.close (); + out.close (); + + // Parse arguments. + // + cli::vector_scanner scan (args); + mv_options ops (parse<mv_options> (scan, args, cbs.parse_option, fail)); + + // Move filesystem entries. + // + if (!scan.more ()) + fail () << "missing arguments"; + + // Note that the arguments semantics depends on the last argument, so we + // read out and cache them. + // + small_vector<string, 2> args; + while (scan.more ()) + args.push_back (scan.next ()); + + const dir_path& wd (cwd.absolute () + ? cwd + : current_directory (cwd, fail)); + + auto i (args.begin ()); + auto j (args.rbegin ()); + path dst (parse_path (move (*j++), wd, fail)); + auto e (j.base ()); + + if (i == e) + fail () << "missing source path"; + + auto mv = [ops, &fail, cbs] (const path& from, const path& to) + { + if (cbs.move) + call (fail, cbs.move, from, to, ops.force (), true /* pre */); + + try + { + bool exists (entry_exists (to)); + + // Fail if the source and destination paths are the same. + // + // Note that for mventry() function (that is based on the POSIX + // rename() function) this is a noop. + // + if (exists && to == from) + fail () << "unable to move entity '" << from << "' to itself"; + + // Rename/move the filesystem entry, replacing an existing one. + // + mventry (from, to, + cpflags::overwrite_permissions | + cpflags::overwrite_content); + + if (cbs.move) + call (fail, cbs.move, from, to, ops.force (), false /* pre */); + } + catch (const system_error& e) + { + fail () << "unable to move entity '" << from << "' to '" << to + << "': " << e; + } + }; + + // If destination is not a directory path (no trailing separator) then + // move the filesystem entry to the specified path (the only source path + // is allowed in such a case). Otherwise move the source filesystem + // entries into the destination directory. + // + if (!dst.to_directory ()) + { + path src (parse_path (move (*i++), wd, fail)); + + // If there are multiple sources but no trailing separator for the + // destination, then, most likelly, it is missing. + // + if (i != e) + fail () << "multiple source paths without trailing separator for " + << "destination directory"; + + // Synopsis 1: move an entity to the specified path. + // + mv (src, dst); + } + else + { + // Synopsis 2: move entities into the specified directory. + // + for (; i != e; ++i) + { + path src (parse_path (move (*i), wd, fail)); + mv (src, dst / src.leaf ()); + } + } + + r = 0; + } + // Can be thrown while closing in, out or writing to cerr. + // + catch (const io_error& e) + { + error () << e; + } + catch (const failed&) + { + // Diagnostics has already been issued. + } + catch (const cli::exception& e) + { + error () << e; + } + + cerr.close (); + return r; + } + // In particular, handles io_error exception potentially thrown while + // creating, writing to, or closing cerr. + // + catch (const std::exception&) + { + return 1; + } + + // rm [-r|--recursive] [-f|--force] <path>... + // + // The implementation deviates from POSIX in a number of ways. It doesn't + // interact with a user and fails immediatelly if unable to process an + // argument. It doesn't check for dots containment in the path, and doesn't + // consider files and directory permissions in any way just trying to remove + // a filesystem entry. Always fails if empty path is specified. + // + // Note: can be executed synchronously. + // + static uint8_t + rm (const strings& args, + auto_fd in, auto_fd out, auto_fd err, + const dir_path& cwd, + const builtin_callbacks& cbs) noexcept + try + { + uint8_t r (1); + ofdstream cerr (err != nullfd ? move (err) : fddup (stderr_fd ())); + + auto error = [&cerr] (bool fail = false) + { + return error_record (cerr, fail, "rm"); + }; + + auto fail = [&error] () {return error (true /* fail */);}; + + try + { + in.close (); + out.close (); + + // Parse arguments. + // + cli::vector_scanner scan (args); + rm_options ops (parse<rm_options> (scan, args, cbs.parse_option, fail)); + + // Remove entries. + // + if (!scan.more () && !ops.force ()) + fail () << "missing file"; + + const dir_path& wd (cwd.absolute () + ? cwd + : current_directory (cwd, fail)); + + while (scan.more ()) + { + path p (parse_path (scan.next (), wd, fail)); + + if (cbs.remove) + call (fail, cbs.remove, p, ops.force (), true /* pre */); + + try + { + dir_path d (path_cast<dir_path> (p)); + + pair<bool, entry_stat> es (path_entry (d)); + if (es.first && es.second.type == entry_type::directory) + { + if (!ops.recursive ()) + fail () << "'" << p << "' is a directory"; + + // The call can result in rmdir_status::not_exist. That's not very + // likelly but there is also nothing bad about it. + // + try_rmdir_r (d); + } + else if (try_rmfile (p) == rmfile_status::not_exist && + !ops.force ()) + throw_generic_error (ENOENT); + + if (cbs.remove) + call (fail, cbs.remove, p, ops.force (), false /* pre */); + } + catch (const system_error& e) + { + fail () << "unable to remove '" << p << "': " << e; + } + } + + r = 0; + } + // Can be thrown while closing in, out or writing to cerr. + // + catch (const io_error& e) + { + error () << e; + } + catch (const failed&) + { + // Diagnostics has already been issued. + } + catch (const cli::exception& e) + { + error () << e; + } + + cerr.close (); + return r; + } + // In particular, handles io_error exception potentially thrown while + // creating, writing to, or closing cerr. + // + catch (const std::exception&) + { + return 1; + } + + // rmdir [-f|--force] <path>... + // + // Note: can be executed synchronously. + // + static uint8_t + rmdir (const strings& args, + auto_fd in, auto_fd out, auto_fd err, + const dir_path& cwd, + const builtin_callbacks& cbs) noexcept + try + { + uint8_t r (1); + ofdstream cerr (err != nullfd ? move (err) : fddup (stderr_fd ())); + + auto error = [&cerr] (bool fail = false) + { + return error_record (cerr, fail, "rmdir"); + }; + + auto fail = [&error] () {return error (true /* fail */);}; + + try + { + in.close (); + out.close (); + + // Parse arguments. + // + cli::vector_scanner scan (args); + + rmdir_options ops ( + parse<rmdir_options> (scan, args, cbs.parse_option, fail)); + + // Remove directories. + // + if (!scan.more () && !ops.force ()) + fail () << "missing directory"; + + const dir_path& wd (cwd.absolute () + ? cwd + : current_directory (cwd, fail)); + + while (scan.more ()) + { + dir_path p ( + path_cast<dir_path> (parse_path (scan.next (), wd, fail))); + + if (cbs.remove) + call (fail, cbs.remove, p, ops.force (), true /* pre */); + + try + { + rmdir_status s (try_rmdir (p)); + + if (s == rmdir_status::not_empty) + throw_generic_error (ENOTEMPTY); + else if (s == rmdir_status::not_exist && !ops.force ()) + throw_generic_error (ENOENT); + + if (cbs.remove) + call (fail, cbs.remove, p, ops.force (), false /* pre */); + } + catch (const system_error& e) + { + fail () << "unable to remove '" << p << "': " << e; + } + } + + r = 0; + } + // Can be thrown while closing in, out or writing to cerr. + // + catch (const io_error& e) + { + error () << e; + } + catch (const failed&) + { + // Diagnostics has already been issued. + } + catch (const cli::exception& e) + { + error () << e; + } + + cerr.close (); + return r; + } + // In particular, handles io_error exception potentially thrown while + // creating, writing to, or closing cerr. + // + catch (const std::exception&) + { + return 1; + } + + // sed [-n|--quiet] [-i|--in-place] -e|--expression <script> [<file>] + // + // Note: must be executed asynchronously. + // + static uint8_t + sed (const strings& args, + auto_fd in, auto_fd out, auto_fd err, + const dir_path& cwd, + const builtin_callbacks& cbs) noexcept + try + { + uint8_t r (1); + ofdstream cerr (err != nullfd ? move (err) : fddup (stderr_fd ())); + + auto error = [&cerr] (bool fail = false) + { + return error_record (cerr, fail, "sed"); + }; + + auto fail = [&error] () {return error (true /* fail */);}; + + try + { + // Automatically remove a temporary file (used for in place editing) on + // failure. + // + auto_rmfile rm; + + // Do not throw when failbit is set (getline() failed to extract any + // character). + // + ifdstream cin (in != nullfd ? move (in) : fddup (stdin_fd ()), + ifdstream::badbit); + + ofdstream cout (out != nullfd ? move (out) : fddup (stdout_fd ())); + + // Parse arguments. + // + cli::vector_scanner scan (args); + + sed_options ops ( + parse<sed_options> (scan, args, cbs.parse_option, fail)); + + if (ops.expression ().empty ()) + fail () << "missing script"; + + // Only a single script is supported. + // + if (ops.expression ().size () != 1) + fail () << "multiple scripts"; + + struct + { + string regex; + string replacement; + bool icase = false; + bool global = false; + bool print = false; + } subst; + + { + const string& v (ops.expression ()[0]); + if (v.empty ()) + fail () << "empty script"; + + if (v[0] != 's') + fail () << "only 's' command supported"; + + // Parse the substitute command. + // + if (v.size () < 2) + fail () << "no delimiter for 's' command"; + + char delim (v[1]); + if (delim == '\\' || delim == '\n') + fail () << "invalid delimiter for 's' command"; + + size_t p (v.find (delim, 2)); + if (p == string::npos) + fail () << "unterminated 's' command regex"; + + subst.regex.assign (v, 2, p - 2); + + // Empty regex matches nothing, so not of much use. + // + if (subst.regex.empty ()) + fail () << "empty regex in 's' command"; + + size_t b (p + 1); + p = v.find (delim, b); + if (p == string::npos) + fail () << "unterminated 's' command replacement"; + + subst.replacement.assign (v, b, p - b); + + // Parse the substitute command flags. + // + char c; + for (++p; (c = v[p]) != '\0'; ++p) + { + switch (c) + { + case 'i': subst.icase = true; break; + case 'g': subst.global = true; break; + case 'p': subst.print = true; break; + default: + { + fail () << "invalid 's' command flag '" << c << "'"; + } + } + } + } + + // Path of a file to edit. An empty path represents stdin. + // + path p; + if (scan.more ()) + { + string f (scan.next ()); + + if (f != "-") + p = parse_path (move (f), + (cwd.absolute () + ? cwd + : current_directory (cwd, fail)), + fail); + } + + if (scan.more ()) + fail () << "unexpected argument '" << scan.next () << "'"; + + // Edit file. + // + // If we edit file in place make sure that the file path is specified + // and obtain a temporary file path. We will be writing to the temporary + // file (rather than to stdout) and will move it to the original file + // path afterwards. + // + path tp; + if (ops.in_place ()) + { + if (p.empty ()) + fail () << "-i|--in-place option specified while reading from " + << "stdin"; + + try + { + tp = path::temp_path ("build2-sed"); + + cout.close (); // Flush and close. + + cout.open (fdopen (tp, + fdopen_mode::out | + fdopen_mode::truncate | + fdopen_mode::create, + path_permissions (p))); + } + catch (const io_error& e) + { + fail () << "unable to open '" << tp << "': " << e; + } + catch (const system_error& e) + { + fail () << "unable to obtain temporary file: " << e; + } + + rm = auto_rmfile (tp); + } + + // Note that ECMAScript is implied if no grammar flag is specified. + // + regex re (subst.regex, subst.icase ? regex::icase : regex::ECMAScript); + + // Edit a file or STDIN. + // + try + { + // Open a file if specified. + // + if (!p.empty ()) + { + cin.close (); // Flush and close. + cin.open (p); + } + + // Read until failbit is set (throw on badbit). + // + string s; + while (getline (cin, s)) + { + auto r (regex_replace_search ( + s, + re, + subst.replacement, + subst.global + ? regex_constants::format_default + : regex_constants::format_first_only)); + + // Add newline regardless whether the source line is newline- + // terminated or not (in accordance with POSIX). + // + if (!ops.quiet () || (r.second && subst.print)) + cout << r.first << '\n'; + } + + cin.close (); + cout.close (); + + if (ops.in_place ()) + { + mvfile (tp, p, + cpflags::overwrite_content | + cpflags::overwrite_permissions); + + rm.cancel (); + } + + r = 0; + } + catch (const io_error& e) + { + error_record d (fail ()); + d << "unable to edit "; + + if (p.empty ()) + d << "stdin"; + else + d << "'" << p << "'"; + + d << ": " << e; + } + } + catch (const regex_error& e) + { + // Print regex_error description if meaningful (no space). + // + error () << "invalid regex" << e; + } + // Can be thrown while creating cin, cout or writing to cerr. + // + catch (const io_error& e) + { + error () << e; + } + catch (const system_error& e) + { + error () << e; + } + catch (const failed&) + { + // Diagnostics has already been issued. + } + catch (const cli::exception& e) + { + error () << e; + } + + cerr.close (); + return r; + } + // In particular, handles io_error exception potentially thrown while + // creating, writing to, or closing cerr. + // + catch (const std::exception&) + { + return 1; + } + + // sleep <seconds> + // + // Note: can be executed synchronously. + // + static uint8_t + sleep (const strings& args, + auto_fd in, auto_fd out, auto_fd err, + const dir_path&, + const builtin_callbacks& cbs) noexcept + try + { + uint8_t r (1); + ofdstream cerr (err != nullfd ? move (err) : fddup (stderr_fd ())); + + auto error = [&cerr] (bool fail = false) + { + return error_record (cerr, fail, "sleep"); + }; + + auto fail = [&error] () {return error (true /* fail */);}; + + try + { + in.close (); + out.close (); + + // Parse arguments. + // + cli::vector_scanner scan (args); + parse<sleep_options> (scan, args, cbs.parse_option, fail); + + if (!scan.more ()) + fail () << "missing time interval"; + + uint64_t n; + + for (;;) // Breakout loop. + { + string a (scan.next ()); + + // Note: strtoull() allows these. + // + if (!a.empty () && a[0] != '-' && a[0] != '+') + { + char* e (nullptr); + n = strtoull (a.c_str (), &e, 10); // Can't throw. + + if (errno != ERANGE && e == a.c_str () + a.size ()) + break; + } + + fail () << "invalid time interval '" << a << "'"; + } + + if (scan.more ()) + fail () << "unexpected argument '" << scan.next () << "'"; + + // Sleep. + // + using namespace chrono; + + seconds d (n); + + if (cbs.sleep) + call (fail, cbs.sleep, d); + else + { + // MinGW GCC 4.9 doesn't implement this_thread so use Win32 Sleep(). + // +#ifndef _WIN32 + this_thread::sleep_for (d); +#else + Sleep (static_cast<DWORD> (duration_cast<milliseconds> (d).count ())); +#endif + } + + r = 0; + } + // Can be thrown while closing in, out or writing to cerr. + // + catch (const io_error& e) + { + error () << e; + } + catch (const failed&) + { + // Diagnostics has already been issued. + } + catch (const cli::exception& e) + { + error () << e; + } + + cerr.close (); + return r; + } + // In particular, handles io_error exception potentially thrown while + // creating, writing to, or closing cerr. + // + catch (const std::exception&) + { + return 1; + } + + // test (-f|--file)|(-d|--directory) <path> + // + // Note: can be executed synchronously. + // + static uint8_t + test (const strings& args, + auto_fd in, auto_fd out, auto_fd err, + const dir_path& cwd, + const builtin_callbacks& cbs) noexcept + try + { + uint8_t r (2); + ofdstream cerr (err != nullfd ? move (err) : fddup (stderr_fd ())); + + auto error = [&cerr] (bool fail = false) + { + return error_record (cerr, fail, "test"); + }; + + auto fail = [&error] () {return error (true /* fail */);}; + + try + { + in.close (); + out.close (); + + // Parse arguments. + // + cli::vector_scanner scan (args); + + test_options ops ( + parse<test_options> (scan, args, cbs.parse_option, fail)); + + if (!ops.file () && !ops.directory ()) + fail () << "either -f|--file or -d|--directory must be specified"; + + if (ops.file () && ops.directory ()) + fail () << "both -f|--file and -d|--directory specified"; + + if (!scan.more ()) + fail () << "missing path"; + + const dir_path& wd (cwd.absolute () + ? cwd + : current_directory (cwd, fail)); + + path p (parse_path (scan.next (), wd, fail)); + + if (scan.more ()) + fail () << "unexpected argument '" << scan.next () << "'"; + + try + { + r = (ops.file () ? file_exists (p) : dir_exists (p)) ? 0 : 1; + } + catch (const system_error& e) + { + fail () << "cannot test '" << p << "': " << e; + } + } + // Can be thrown while closing in, out or writing to cerr. + // + catch (const io_error& e) + { + error () << e; + } + catch (const failed&) + { + // Diagnostics has already been issued. + } + catch (const cli::exception& e) + { + error () << e; + } + + cerr.close (); + return r; + } + // In particular, handles io_error exception potentially thrown while + // creating, writing to, or closing cerr. + // + catch (const std::exception&) + { + return 2; + } + + // touch [--after <ref-file>] <file>... + // + // Note that POSIX doesn't specify the behavior for touching an entry other + // than file. + // + // Also note that POSIX doesn't specify if after a file touch failure the + // command should proceed with the rest of the arguments. The current + // implementation exits immediatelly in such a case. + // + // Note: can be executed synchronously. + // + static uint8_t + touch (const strings& args, + auto_fd in, auto_fd out, auto_fd err, + const dir_path& cwd, + const builtin_callbacks& cbs) noexcept + try + { + uint8_t r (1); + ofdstream cerr (err != nullfd ? move (err) : fddup (stderr_fd ())); + + auto error = [&cerr] (bool fail = false) + { + return error_record (cerr, fail, "touch"); + }; + + auto fail = [&error] () {return error (true /* fail */);}; + + try + { + in.close (); + out.close (); + + // Parse arguments. + // + cli::vector_scanner scan (args); + + touch_options ops ( + parse<touch_options> (scan, args, cbs.parse_option, fail)); + + auto mtime = [&fail] (const path& p) -> timestamp + { + try + { + timestamp t (file_mtime (p)); + + if (t == timestamp_nonexistent) + throw_generic_error (ENOENT); + + return t; + } + catch (const system_error& e) + { + fail () << "cannot obtain file '" << p << "' modification time: " + << e; + } + assert (false); // Can't be here. + return timestamp (); + }; + + const dir_path& wd (cwd.absolute () + ? cwd + : current_directory (cwd, fail)); + + optional<timestamp> after; + if (ops.after_specified ()) + after = mtime (parse_path (ops.after (), wd, fail)); + + if (!scan.more ()) + fail () << "missing file"; + + // Create files. + // + while (scan.more ()) + { + path p (parse_path (scan.next (), wd, fail)); + + try + { + if (cbs.create) + call (fail, cbs.create, p, true /* pre */); + + touch_file (p); + + if (cbs.create) + call (fail, cbs.create, p, false /* pre */); + + if (after) + { + while (mtime (p) <= *after) + touch_file (p, false /* create */); + } + } + catch (const system_error& e) + { + fail () << "cannot create/update '" << p << "': " << e; + } + } + + r = 0; + } + // Can be thrown while closing in, out or writing to cerr. + // + catch (const io_error& e) + { + error () << e; + } + catch (const failed&) + { + // Diagnostics has already been issued. + } + catch (const cli::exception& e) + { + error () << e; + } + + cerr.close (); + return r; + } + // In particular, handles io_error exception potentially thrown while + // creating, writing to, or closing cerr. + // + catch (const std::exception&) + { + return 1; + } + + // Run builtin implementation asynchronously. + // + static builtin + async_impl (builtin_impl* fn, + uint8_t& r, + const strings& args, + auto_fd in, auto_fd out, auto_fd err, + const dir_path& cwd, + const builtin_callbacks& cbs) + { + return builtin ( + r, + thread ([fn, &r, &args, + in = move (in), + out = move (out), + err = move (err), + &cwd, + &cbs] () mutable noexcept + { + r = fn (args, move (in), move (out), move (err), cwd, cbs); + })); + } + + template <builtin_impl fn> + static builtin + async_impl (uint8_t& r, + const strings& args, + auto_fd in, auto_fd out, auto_fd err, + const dir_path& cwd, + const builtin_callbacks& cbs) + { + return async_impl ( + fn, r, args, move (in), move (out), move (err), cwd, cbs); + } + + // Run builtin implementation synchronously. + // + template <builtin_impl fn> + static builtin + sync_impl (uint8_t& r, + const strings& args, + auto_fd in, auto_fd out, auto_fd err, + const dir_path& cwd, + const builtin_callbacks& cbs) + { + r = fn (args, move (in), move (out), move (err), cwd, cbs); + return builtin (r, thread ()); + } + + const builtin_map builtins + { + {"cat", &async_impl<&cat>}, + {"cp", &sync_impl<&cp>}, + {"echo", &async_impl<&echo>}, + {"false", &false_}, + {"ln", &sync_impl<&ln>}, + {"mkdir", &sync_impl<&mkdir>}, + {"mv", &sync_impl<&mv>}, + {"rm", &sync_impl<&rm>}, + {"rmdir", &sync_impl<&rmdir>}, + {"sed", &async_impl<&sed>}, + {"sleep", &sync_impl<&sleep>}, + {"test", &sync_impl<&test>}, + {"touch", &sync_impl<&touch>}, + {"true", &true_} + }; +} diff --git a/libbutl/builtin.mxx b/libbutl/builtin.mxx new file mode 100644 index 0000000..8c9d295 --- /dev/null +++ b/libbutl/builtin.mxx @@ -0,0 +1,174 @@ +// file : libbutl/builtin.mxx -*- C++ -*- +// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef __cpp_modules_ts +#pragma once +#endif + +// C includes. + +#ifndef __cpp_lib_modules_ts +#include <map> +#include <string> +#include <vector> +#include <thread> +#include <cstddef> // size_t +#include <utility> // move() +#include <cstdint> // uint8_t +#include <functional> +#endif + +// Other includes. + +#ifdef __cpp_modules_ts +export module butl.builtin; +#ifdef __cpp_lib_modules_ts +import std.core; +import std.threading; +#endif +import butl.path; +import butl.fdstream; +import butl.timestamp; +#else +#include <libbutl/path.mxx> +#include <libbutl/fdstream.mxx> +#include <libbutl/timestamp.mxx> +#endif + +#include <libbutl/export.hxx> + +LIBBUTL_MODEXPORT namespace butl +{ + // A process/thread-like object representing a running builtin. + // + // For now, instead of allocating the result storage dynamically, we + // expect it to be provided by the caller. + // + class builtin + { + public: + std::uint8_t + wait () {if (t_.joinable ()) t_.join (); return r_;} + + ~builtin () {wait ();} + + public: + builtin (std::uint8_t& r, std::thread&& t = std::thread ()) + : r_ (r), t_ (move (t)) {} + + builtin (builtin&&) = default; + + private: + std::uint8_t& r_; + std::thread t_; + }; + + // Builtin execution callbacks that can be used for checking/handling the + // filesystem entries being acted upon (enforcing that they are sub-entries + // of some "working" directory, registering cleanups for new entries, etc) + // and for providing custom implementations for some functions used by + // builtins. + // + // Note that the filesystem paths passed to the callbacks are absolute and + // normalized with directories distinguished from non-directories based on + // the lexical representation (presence of the trailing directory separator; + // use path::to_directory() to check). + // + // Also note that builtins catch any exceptions that may be thrown by the + // callbacks and, if that's the case, issue diagnostics and exit with the + // non-zero status. + // + struct builtin_callbacks + { + // If specified, called before (pre is true) and after (pre is false) a + // new filesystem entry is created or an existing one is re-created or + // updated. + // + using create_hook = void (const path&, bool pre); + + std::function<create_hook> create; + + // If specified, called before (pre is true) and after (pre is false) a + // filesystem entry is moved. The force argument is true if the builtin is + // executed with the --force option. + // + using move_hook = void (const path& from, + const path& to, + bool force, + bool pre); + + std::function<move_hook> move; + + // If specified, called before (pre is true) and after (pre is false) a + // filesystem entry is removed. The force argument is true if the builtin + // is executed with the --force option. + // + using remove_hook = void (const path&, bool force, bool pre); + + std::function<remove_hook> remove; + + // If specified, called on encountering an unknown option passing the + // argument list and the position of the option in question. Return the + // number of parsed arguments. + // + using parse_option_function = + std::size_t (const std::vector<std::string>&, std::size_t); + + std::function<parse_option_function> parse_option; + + // If specified, called by the sleep builtin instead of the default + // implementation. + // + using sleep_function = void (const duration&); + + std::function<sleep_function> sleep; + + explicit + builtin_callbacks (std::function<create_hook> c = {}, + std::function<move_hook> m = {}, + std::function<remove_hook> r = {}, + std::function<parse_option_function> p = {}, + std::function<sleep_function> s = {}) + : create (std::move (c)), + move (std::move (m)), + remove (std::move (r)), + parse_option (std::move (p)), + sleep (std::move (s)) {} + + explicit + builtin_callbacks (std::function<sleep_function> sl) + : sleep (std::move (sl)) {} + }; + + // Start a builtin command. Use the current process' standard streams for + // the unopened in, out, and err file descriptors. Use the process' current + // working directory unless an alternative is specified. Throw + // std::system_error on failure. + // + // Note that unlike argc/argv, args don't include the program name. + // + using builtin_function = builtin (std::uint8_t& result, + const std::vector<std::string>& args, + auto_fd in, auto_fd out, auto_fd err, + const dir_path& cwd, + const builtin_callbacks&); + + class builtin_map: public std::map<std::string, builtin_function*> + { + public: + using base = std::map<std::string, builtin_function*>; + using base::base; + + // Return NULL if not a builtin. + // + builtin_function* + find (const std::string& n) const + { + auto i (base::find (n)); + return i != end () ? i->second : nullptr; + } + }; + + LIBBUTL_SYMEXPORT extern const builtin_map builtins; +} diff --git a/tests/builtin/buildfile b/tests/builtin/buildfile new file mode 100644 index 0000000..26acc13 --- /dev/null +++ b/tests/builtin/buildfile @@ -0,0 +1,9 @@ +# file : tests/builtin/buildfile +# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +import libs = libbutl%lib{butl} + +./: exe{driver} file{cp-dir/cp-file} + +exe{driver}: {hxx cxx}{*} $libs testscript{*} diff --git a/tests/builtin/cat.testscript b/tests/builtin/cat.testscript new file mode 100644 index 0000000..336bb03 --- /dev/null +++ b/tests/builtin/cat.testscript @@ -0,0 +1,82 @@ +# file : tests/builtin/cat.testscript +# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +test.arguments = "cat" + +: unknown-option +: +$* -u 2>"cat: unknown option '-u'" == 1 + +: in +: +$* <<EOF >>EOF + foo + bar + EOF + +: dash +: +$* - <<EOF >>EOF + foo + bar + EOF + +: file +: +{ + $* <<EOF >=out; + foo + bar + EOF + + cat out >>EOO + foo + bar + EOO +} + +: in-repeat +: +$* - - <<EOF >>EOF + foo + bar + EOF + +: non-existent +: +$* in 2>>/~%EOE% != 0 + %cat: unable to print '.+/in': .+% + EOE + +: empty-path +: +: Cat an empty path. +: +$* '' 2>"cat: invalid path ''" == 1 + +: big +: +: Cat a big file (about 100K) to test that the builtin is asynchronous. +: +{ + s="--------------------------------"; + s="$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s"; + s="$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s"; + s="$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s"; + $* <"$s" | $* >"$s" +} + +: cwd +: +: When cross-testing we cannot guarantee that host absolute paths are +: recognized by the target process. +: +if ($test.target == $build.host) +{ + test.options += -d $~/a; + mkdir a; + echo 'foo' >=a/b; + + $* b >'foo' +} diff --git a/tests/builtin/cp-dir/cp-file b/tests/builtin/cp-dir/cp-file new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/builtin/cp-dir/cp-file diff --git a/tests/builtin/cp.testscript b/tests/builtin/cp.testscript new file mode 100644 index 0000000..4f83586 --- /dev/null +++ b/tests/builtin/cp.testscript @@ -0,0 +1,447 @@ +# file : tests/builtin/cp.testscript +# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +test.arguments = "cp" +test.options += -c + +: unknown-option +: +$* -u >'option -u' 2>"cp: unknown option '-u'" == 1 + +: args +: +{ + : none + : + $* 2>"cp: missing arguments" == 1 + + : no-source + : + $* -R a 2>"cp: missing source path" == 1 + + : no-trailing-sep + : + $* a b c 2>"cp: multiple source paths without trailing separator for destination directory" == 1 + + : empty + : + { + : dest + : + $* '' 2>"cp: invalid path ''" == 1 + + : src1 + : + $* '' a 2>"cp: invalid path ''" == 1 + + : src2 + : + $* '' a b/ 2>"cp: invalid path ''" == 1 + } +} + +: file +: +: Test synopsis 1: make a file copy at the specified path. +: +{ + : existing + : + { + : to-non-existing + : + { + touch a; + + $* a b >>/~%EOO% &b; + %create .+/b true% + %create .+/b false% + EOO + + test -f b + } + + : to-existing + : + { + touch a b; + + $* a b >>/~%EOO% + %create .+/b true% + %create .+/b false% + EOO + } + + : to-dir + : + { + touch a; + mkdir b; + + $* a b >>/~%EOO% 2>>/~%EOE% != 0 + %create .+/b true% + EOO + %cp: unable to copy file '.+/a' to '.+/b': .+% + EOE + } + } + + : non-existing + : + { + $* a b >>/~%EOO% 2>>/~%EOE% != 0 + %create .+/b true% + EOO + %cp: unable to copy file '.+/a' to '.+/b': .+% + EOE + } + + : non-file + : + { + mkdir a; + + $* a b >>/~%EOO% 2>>/~%EOE% != 0 + %create .+/b true% + EOO + %cp: unable to copy file '.+/a' to '.+/b': .+% + EOE + } +} + +: dir +: +: Test synopsis 2: make a directory copy at the specified path. +: +{ + : existing + : + { + : to-non-existing + : + { + mkdir a; + + $* -r a b >>/~%EOO% &b/; + %create .+/b/ true% + %create .+/b/ false% + EOO + + test -d b + } + + : to-existing + : + { + mkdir a b; + + $* -R a b >>/~%EOO% 2>>/~%EOE% != 0 + %create .+/b/ true% + EOO + %cp: unable to copy directory '.+/a' to '.+/b': .+% + EOE + } + + : to-file + : + { + mkdir a; + touch b; + + $* -r a b >>/~%EOO% 2>>/~%EOE% != 0 + %create .+/b/ true% + EOO + %cp: unable to copy directory '.+/a' to '.+/b': .+% + EOE + } + + : recursively + : + { + mkdir -p a/b/c; + touch a/x a/b/y; + + $* -r a d >>/~%EOO% &d/ &d/x &d/b/ &d/b/y &d/b/c/; + %create .+/d/ true% + %create .+/d/ false% + %( + %create .+/d/.+ true% + %create .+/d/.+ false% + %){4} + EOO + + test -d d/b/c && test -f d/x && test -f d/b/y + } + } + + : non-existing + : + { + $* -r a b >>/~%EOO% 2>>/~%EOE% &b/ != 0 + %create .+/b/ true% + %create .+/b/ false% + EOO + %cp: unable to copy directory '.+/a' to '.+/b': .+% + EOE + } + + : non-dir + : + { + touch a; + + $* -r a b >>/~%EOO% 2>>/~%EOE% &b/ != 0 + %create .+/b/ true% + %create .+/b/ false% + EOO + %cp: unable to copy directory '.+/a' to '.+/b': .+% + EOE + } +} + +: files +: +: Test synopsis 3: copy files into the specified directory. +: +{ + : existing + : + { + : into-dir + : + { + : over-non-existing + : + { + mkdir b; + touch a; + + $* a b/ >>/~%EOO% &b/a; + %create .+/b/a true% + %create .+/b/a false% + EOO + + test -f b/a + } + + : over-dir + : + { + mkdir -p b/a; + touch a; + + $* a b/ >>/~%EOO% 2>>/~%EOE% != 0 + %create .+/b/a true% + EOO + %cp: unable to copy file '.+/a' to '.+/b/a': .+% + EOE + } + + : multiple + : + { + touch a b; + mkdir c; + + $* a b c/ >>/~%EOO% &c/a &c/b &c/; + %create .+/c/a true% + %create .+/c/a false% + %create .+/c/b true% + %create .+/c/b false% + EOO + + test -f c/a && test -f c/b + } + } + + : into-non-existing-dir + : + { + touch a; + + $* a b/ >>/~%EOO% 2>>/~%EOE% != 0 + %create .+/b/a true% + EOO + %cp: unable to copy file '.+/a' to '.+/b/a': .+% + EOE + } + + : into-non-dir + : + { + touch a b; + + $* a b/ >>/~%EOO% 2>>/~%EOE% != 0 + %create .+/b/a true% + EOO + %cp: unable to copy file '.+/a' to '.+/b/a': .+% + EOE + } + } + + : non-existing + : + { + mkdir b; + + $* a b/ >>/~%EOO% 2>>/~%EOE% != 0 + %create .+/b/a true% + EOO + %cp: unable to copy file '.+/a' to '.+/b/a': .+% + EOE + } + + : non-file + : + { + mkdir a b; + + $* a b/ >>/~%EOO% 2>>/~%EOE% != 0 + %create .+/b/a true% + EOO + %cp: unable to copy file '.+/a' to '.+/b/a': .+% + EOE + } +} + +: filesystem-entries +: +: Test synopsis 4: copy filesystem entries into the specified directory. +: +{ + : file + : + { + mkdir b; + touch a; + + $* -R a b/ >>/~%EOO% &b/a; + %create .+/b/a true% + %create .+/b/a false% + EOO + + test -f b/a + } + + : dir + : + { + : over-non-existing + : + { + mkdir a b; + touch a/c; + + $* -R a b/ >>/~%EOO% &b/a/ &b/a/c; + %create .+/b/a/ true% + %create .+/b/a/ false% + %create .+/b/a/c true% + %create .+/b/a/c false% + EOO + + test -f b/a/c + } + + : over-existing + : + { + mkdir -p a b/a; + + $* -R a b/ >>/~%EOO% 2>>/~%EOE% != 0 + %create .+/b/a/ true% + EOO + %cp: unable to copy directory '.+/a' to '.+/b/a': .+% + EOE + } + } +} + +: attrs +: +if ($cxx.target.class != 'windows') +{ + fs = 's/.+ (\S+\s+\S+\s+\S+)\s+cp-file/\1/p' + ds = 's/.+ (\S+\s+\S+\s+\S+)\s+cp-dir/\1/p' + + : copy + : + { + : file + : + { + ls -l $src_base/cp-dir | sed -n -e "$fs" | set t; + + $* -p $src_base/cp-dir/cp-file ./ >! &cp-file; + + ls -l | sed -n -e "$fs" >"$t" + } + + : dir + : + { + ls -l $src_base | sed -n -e "$ds" | set t; + + $* -pr $src_base/cp-dir ./ >! &cp-dir/ &cp-dir/cp-file; + + ls -l | sed -n -e "$ds" >"$t" + } + } + + : no-copy + : + : Note that the `ls -l` command by default displays the filesystem entry + : modification time with the minute resolution and building from git + : repository may not preserve the filesystem entry original modification + : times. That is why we also pass --full-time and enable the test for only + : platforms where ls supports this option. + : + if ($cxx.target.class == 'linux') + { + : file + : + { + ls -l --full-time $src_base/cp-dir | sed -n -e "$fs" | set t; + + $* $src_base/cp-dir/cp-file ./ >! &cp-file; + + ls -l --full-time | sed -n -e "$fs" | set tn; + + if ("$tn" == "$t") + exit "unexpectedly copied timestamp \($t\)" + end + } + + : dir + : + { + ls -l --full-time $src_base | sed -n -e "$ds" | set t; + + $* -r $src_base/cp-dir ./ >! &cp-dir/ &cp-dir/cp-file; + + ls -l --full-time | sed -n -e "$ds" | set tn; + + if ("$tn" == "$t") + exit "unexpectedly copied timestamp \($t\)" + end + } + } +} + +: cwd +: +: When cross-testing we cannot guarantee that host absolute paths are +: recognized by the target process. +: +if ($test.target == $build.host) +{ + test.options += -d $~/a; + mkdir -p a/b; + + $* -R b c >>/~%EOO% &a/c/; + %create .+/a/c/ true% + %create .+/a/c/ false% + EOO + + test -d a/c +} diff --git a/tests/builtin/driver.cxx b/tests/builtin/driver.cxx new file mode 100644 index 0000000..bf171cb --- /dev/null +++ b/tests/builtin/driver.cxx @@ -0,0 +1,159 @@ +// file : tests/builtin/driver.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include <cassert> + +#ifndef __cpp_lib_modules_ts +#include <string> +#include <vector> +#include <utility> // move() +#include <ostream> +#include <iostream> +#endif + +// Other includes. + +#ifdef __cpp_modules_ts +#ifdef __cpp_lib_modules_ts +import std.core; +import std.io; +#endif +import butl.path; +import butl.utility; // eof() +import butl.builtin; +import butl.optional; +import butl.timestamp; // to_stream(duration) +#else +#include <libbutl/path.mxx> +#include <libbutl/utility.mxx> +#include <libbutl/builtin.mxx> +#include <libbutl/optional.mxx> +#include <libbutl/timestamp.mxx> +#endif + +using namespace std; +using namespace butl; + +inline ostream& +operator<< (ostream& os, const path& p) +{ + return os << p.representation (); +} + +// Usage: argv[0] [-d <dir>] [-o <opt>] [-c] [-i] <builtin> <builtin-args> +// +// Execute the builtin and exit with its exit status. +// +// -d <dir> use as a current working directory +// -c use callbacks that, in particular, trace calls to stdout +// -o <opt> additional builtin option recognized by the callback +// -i read lines from stdin and append them to the builtin arguments +// +int +main (int argc, char* argv[]) +{ + using butl::optional; + + cin.exceptions (ios::badbit); + cout.exceptions (ios::failbit | ios::badbit); + cerr.exceptions (ios::failbit | ios::badbit); + + bool in (false); + dir_path cwd; + string option; + builtin_callbacks callbacks; + + string name; + vector<string> args; + + auto flag = [] (bool v) {return v ? "true" : "false";}; + + // Parse the driver options and arguments. + // + int i (1); + for (; i != argc; ++i) + { + string a (argv[i]); + + if (a == "-d") + { + ++i; + + assert (i != argc); + cwd = dir_path (argv[i]); + } + else if (a == "-o") + { + ++i; + + assert (i != argc); + option = argv[i]; + } + else if (a == "-c") + { + callbacks = builtin_callbacks ( + [&flag] (const path& p, bool pre) + { + cout << "create " << p << ' ' << flag (pre) << endl; + }, + [&flag] (const path& from, const path& to, bool force, bool pre) + { + cout << "move " << from << ' ' << to << ' ' << flag (force) << ' ' + << flag (pre) << endl; + }, + [&flag] (const path& p, bool force, bool pre) + { + cout << "remove " << p << ' ' << flag (force) << ' ' << flag (pre) + << endl; + }, + [&option] (const vector<string>& args, size_t i) + { + cout << "option " << args[i] << endl; + return !option.empty () && args[i] == option ? 1 : 0; + }, + [] (const duration& d) + { + cout << "sleep "; + to_stream (cout, d, false /* nanoseconds */); + cout << endl; + } + ); + } + else if (a == "-i") + in = true; + else + break; + } + + // Parse the builtin name and arguments. + // + assert (i != argc); + name = argv[i++]; + + for (; i != argc; ++i) + args.push_back (argv[i]); + + // Read out additional arguments from stdin. + // + if (in) + { + string s; + while (!eof (getline (cin, s))) + args.push_back (move (s)); + } + + // Execute the builtin. + // + builtin_function* bf (builtins.find (name)); + + if (bf == nullptr) + { + cerr << "unknown builtin '" << name << "'" << endl; + return 1; + } + + uint8_t r; // Storage. + builtin b (bf (r, args, nullfd, nullfd, nullfd, cwd, callbacks)); + return b.wait (); +} diff --git a/tests/builtin/echo.testscript b/tests/builtin/echo.testscript new file mode 100644 index 0000000..562a14c --- /dev/null +++ b/tests/builtin/echo.testscript @@ -0,0 +1,26 @@ +# file : tests/builtin/echo.testscript +# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +test.arguments = "echo" + +: string +: +$* foo >foo + +: strings +: +$* foo bar >"foo bar" + +: big +: +: Echo a big string (about 100K) to test that the builtin is asynchronous. +: +{ + s="--------------------------------"; + s="$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s"; + s="$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s"; + s="$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s"; + test.options += -i; # Pass the echo argument via the driver's stdin. + $* <"$s" | cat >"$s" +} diff --git a/tests/builtin/ln.testscript b/tests/builtin/ln.testscript new file mode 100644 index 0000000..ed4ca67 --- /dev/null +++ b/tests/builtin/ln.testscript @@ -0,0 +1,217 @@ +# file : tests/builtin/ln.testscript +# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +test.arguments = "ln" +test.options += -c + +: unknown-option +: +$* -u >'option -u' 2>"ln: unknown option '-u'" == 1 + +: args +: +{ + : -s-option + : + $* 2>"ln: missing -s|--symbolic option" == 1 + + : none + : + $* -s 2>"ln: missing arguments" == 1 + + : no-target + : + $* -s a 2>"ln: missing target path" == 1 + + : no-trailing-sep + : + $* -s a b c 2>"ln: multiple target paths with non-directory link path" == 1 + + : empty + : + { + : link + : + $* -s '' 2>"ln: invalid path ''" == 1 + + : target1 + : + $* -s '' a 2>"ln: invalid path ''" == 1 + + : target2 + : + $* -s '' a b/ 2>"ln: invalid path ''" == 1 + } +} + +: file +: +: Test creating a file symlink. +: +{ + : non-existing-link-path + : + { + touch a; + + $* -s a b >>/~%EOO% &b; + %create .+/b true% + %create .+/b false% + EOO + + test -f b + } + + : existing-link + : + { + : file + : + { + touch a b; + + $* -s a b >>/~%EOO% 2>>/~%EOE% != 0 + %create .+/b true% + EOO + %( + %ln: unable to create .+link '.+/b' to '.+/a': .+%| + %ln: unable to copy file '.+/a' to '.+/b': .+% + %) + EOE + } + + : dir + : + { + touch a; + mkdir b; + + $* -s a b >>/~%EOO% 2>>/~%EOE% != 0 + %create .+/b true% + EOO + %( + %ln: unable to create .+link '.+/b' to '.+/a': .+%| + %ln: unable to copy file '.+/a' to '.+/b': .+% + %) + EOE + } + } + + : non-existing + { + : target + : + { + $* -s a b 2>>/~%EOE% != 0 + %ln: unable to create symlink to '.+/a': no such file or directory% + EOE + } + + : link-dir + : + { + touch a; + + $* -s a b/c >>/~%EOO% 2>>/~%EOE% != 0 + %create .+/b/c true% + EOO + %( + %ln: unable to create .+link '.+/b/c' to '.+/a': .+%| + %ln: unable to copy file '.+/a' to '.+/b/c': .+% + %) + EOE + } + } +} + +: dir +: +: Test creating a directory symlink. +: +{ + : non-existing-link-path + : + { + mkdir a; + touch a/b; + + $* -s a c >>/~%EOO% &c; + %create .+/c true% + %create .+/c false% + EOO + + test -f c/b + } + + : existing-link + : + { + : dir + : + { + mkdir a b; + + $* -s a b >>/~%EOO% 2>>/~%EOE% != 0 + %create .+/b true% + EOO + %( + %ln: unable to create .+link '.+/b' to '.+/a': .+%| + %ln: unable to copy directory '.+/a' to '.+/b': .+% + %) + EOE + } + + : file + : + { + mkdir a; + touch b; + + $* -s a b >>/~%EOO% 2>>/~%EOE% != 0 + %create .+/b true% + EOO + %( + %ln: unable to create .+link '.+/b' to '.+/a': .+%| + %ln: unable to copy directory '.+/a' to '.+/b': .+% + %) + EOE + } + } + + : non-existing + { + : link-dir + : + { + mkdir a; + + $* -s a b/c >>/~%EOO% 2>>/~%EOE% != 0 + %create .+/b/c true% + EOO + %( + %ln: unable to create .+link '.+/b/c' to '.+/a': .+%| + %ln: unable to copy directory '.+/a' to '.+/b/c': .+% + %) + EOE + } + } +} + +: multiple-targets +: +: Test creating links for multiple targets in the specified directory. +: +{ + touch a; + mkdir b c; + + $* -s a b c/ >>/~%EOO% &c/a &c/b; + %create .+/c/a true% + %create .+/c/a false% + %create .+/c/b true% + %create .+/c/b false% + EOO + + test -f c/a && test -d c/b +} diff --git a/tests/builtin/mkdir.testscript b/tests/builtin/mkdir.testscript new file mode 100644 index 0000000..5225caa --- /dev/null +++ b/tests/builtin/mkdir.testscript @@ -0,0 +1,102 @@ +# file : tests/builtin/mkdir.testscript +# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +test.arguments = "mkdir" +test.options += -c + +: unknown-option +: +$* -u >'option -u' 2>"mkdir: unknown option '-u'" == 1 + +: parent +: +{ + $* -p a/b >>/~%EOO% &a/ &a/b/; + %create .+/a/ true% + %create .+/a/ false% + %create .+/a/b/ true% + %create .+/a/b/ false% + EOO + + touch a/a a/b/b +} + +: exists +: +{ + $* -p a a a/b a/b >>/~%EOO% &a/ &a/b/ + %create .+/a/ true% + %create .+/a/ false% + %create .+/a/b/ true% + %create .+/a/b/ false% + EOO +} + +: dirs +: +{ + $* a b >>/~%EOO% &a/ &b/; + %create .+/a/ true% + %create .+/a/ false% + %create .+/b/ true% + %create .+/b/ false% + EOO + + touch a/a b/b +} + +: double-dash +: +: Make sure '-p' directory is created. +: +{ + $* -p -- -p >>/~%EOO% &-p/; + %create .+/-p/ true% + %create .+/-p/ false% + EOO + + touch -- -p/a +} + +: no-args +: +: Test passing no arguments. +: +{ + $* 2>"mkdir: missing directory" == 1 +} + +: empty-path +: +: Test creation of empty directory path. +: +{ + $* '' 2>"mkdir: invalid path ''" == 1 +} + +: already-exists +: +: Test creation of an existing directory. +: +{ + $* a a >>/~%EOO% 2>>/~%EOE% &a/ == 1 + %create .+/a/ true% + %create .+/a/ false% + %create .+/a/ true% + EOO + %mkdir: unable to create directory '.+/a': .+% + EOE +} + +: not-exists +: +: Test creation of a directory with non-existent parent. +: +{ + $* a/b >>/~%EOO% 2>>/~%EOE% == 1 + %create .+/a/b/ true% + EOO + %mkdir: unable to create directory '.+/a/b': .+% + EOE +} diff --git a/tests/builtin/mv.testscript b/tests/builtin/mv.testscript new file mode 100644 index 0000000..2647d0f --- /dev/null +++ b/tests/builtin/mv.testscript @@ -0,0 +1,182 @@ +# file : tests/builtin/mv.testscript +# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +test.arguments = "mv" +test.options += -c + +: unknown-option +: +$* -u >'option -u' 2>"mv: unknown option '-u'" == 1 + +: args +: +{ + : none + : + $* 2>"mv: missing arguments" == 1 + + : no-source + : + $* a 2>"mv: missing source path" == 1 + + : no-trailing-sep + : + $* a b c 2>"mv: multiple source paths without trailing separator for destination directory" == 1 + + : empty + : + { + : dest + : + $* '' 2>"mv: invalid path ''" == 1 + + : src1 + : + $* '' a 2>"mv: invalid path ''" == 1 + + : src2 + : + $* '' a b/ 2>"mv: invalid path ''" == 1 + } +} + +: synopsis-1 +: +: Move an entity to the specified path. +: +{ + : file + : + { + : existing + : + { + : to-non-existing + : + { + touch a &!a; + + $* a b >>/~%EOO% &b; + %move .+/a .+/b false true% + %move .+/a .+/b false false% + EOO + + test -f b && test -f a == 1 + } + + : to-existing + : + { + touch a b &!a; + + $* a b >>/~%EOO%; + %move .+/a .+/b false true% + %move .+/a .+/b false false% + EOO + + test -f b && test -f a == 1 + } + + : to-self + : + { + touch a; + + $* a a >/~'%move .+/a .+/a false true%' 2>>/~%EOE% != 0 + %mv: unable to move entity '.+/a' to itself% + EOE + } + + : to-dir + : + { + touch a; + mkdir b; + + $* a b >/~'%move .+/a .+/b false true%' 2>>/~%EOE% != 0 + %mv: unable to move entity '.+/a' to '.+/b': .+% + EOE + } + } + } + + : dir + : + { + : existing + : + { + : to-non-existing + : + { + mkdir a &!a/; + + $* a b &b/ >>/~%EOO%; + %move .+/a .+/b false true% + %move .+/a .+/b false false% + EOO + + test -d b && test -d a == 1 + } + + : to-non-empty + : + { + mkdir a b; + touch b/c; + + $* a b >/~'%move .+/a .+/b false true%' 2>>/~%EOE% != 0 + %mv: unable to move entity '.+/a' to '.+/b': .+% + EOE + } + + : to-non-dir + : + { + mkdir a; + touch b; + + $* a b >/~'%move .+/a .+/b false true%' 2>>/~%EOE% != 0 + %mv: unable to move entity '.+/a' to '.+/b': .+% + EOE + } + } + + : overlap + : + { + mkdir a; + + $* a a/b >/~'%move .+/a .+/a/b false true%' 2>>/~%EOE% != 0 + %mv: unable to move entity '.+/a' to '.+/a/b': .+% + EOE + } + } + + : non-existing + : + { + $* a b >/~'%move .+/a .+/b false true%' 2>>/~%EOE% != 0 + %mv: unable to move entity '.+/a' to '.+/b': .+% + EOE + } +} + +: synopsis-2 +: +: Move entities into the specified directory. +: +{ + mkdir a c &!a/; + touch a/b b &!a/b &!b; + + $* a b c/ >>/~%EOO% &c/a/ &c/a/b &c/b; + %move .+/a .+/c/a false true% + %move .+/a .+/c/a false false% + %move .+/b .+/c/b false true% + %move .+/b .+/c/b false false% + EOO + + test -d c/a && test -f c/a/b && test -f c/b +} diff --git a/tests/builtin/rm.testscript b/tests/builtin/rm.testscript new file mode 100644 index 0000000..991b0f6 --- /dev/null +++ b/tests/builtin/rm.testscript @@ -0,0 +1,105 @@ +# file : tests/builtin/rm.testscript +# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +test.arguments = "rm" +test.options += -c + +: unknown-option +: +$* -u >'option -u' 2>"rm: unknown option '-u'" == 1 + +: no-args +: +{ + : fail + : + : Removing with no arguments fails. + : + $* 2>"rm: missing file" == 1 + + : force + : + : Removing with no arguments succeeds with -f option. + : + $* -f +} + +: file +: +{ + : exists + : + : Removing existing file succeeds. + : + { + touch a &!a; + + $* a >>/~%EOO% + %remove .+/a false true% + %remove .+/a false false% + EOO + } + + : not-exists + : + { + : fail + : + : Removing non-existing file fails. + : + $* a >/~'%remove .+/a false true%' 2>>/~%EOE% == 1 + %rm: unable to remove '.+/a': .+% + EOE + + : force + : + : Removing non-existing file succeeds with -f option. + : + $* -f a >>/~%EOO% + %remove .+/a true true% + %remove .+/a true false% + EOO + } +} + +: dir +: +{ + : default + : + : Removing directory fails by default. + : + { + mkdir a; + + $* a >/~'%remove .+/a false true%' 2>>/~%EOE% == 1 + %rm: '.+/a' is a directory% + EOE + } + + : recursive + : + : Removing directory succeeds with -r option. + : + { + mkdir -p a/b &!a &!a/b; + + $* -r a >>/~%EOO% + %remove .+/a false true% + %remove .+/a false false% + EOO + } +} + +: path +: +{ + : empty + : + : Removing an empty path fails. + : + { + $* '' 2>"rm: invalid path ''" == 1 + } +} diff --git a/tests/builtin/rmdir.testscript b/tests/builtin/rmdir.testscript new file mode 100644 index 0000000..a63f701 --- /dev/null +++ b/tests/builtin/rmdir.testscript @@ -0,0 +1,95 @@ +# file : tests/builtin/rmdir.testscript +# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +test.arguments = "rmdir" +test.options += -c + +: unknown-option +: +rmdir -u 2>"rmdir: unknown option '-u'" == 1 + +: no-args +: +{ + : fail + : + : Removing with no arguments fails. + : + $* 2>"rmdir: missing directory" == 1 + + : force + : + : Removing with no arguments succeeds with -f option. + : + $* -f +} + +: dir +: +{ + : empty-path + : + : Removing an empty path fails. + : + $* '' 2>"rmdir: invalid path ''" == 1 + + : exists + : + : Removing existing directory succeeds. + : + { + mkdir a &!a; + + $* a >>/~%EOO% + %remove .+/a/ false true% + %remove .+/a/ false false% + EOO + } + + : not-exists + : + { + : fail + : Removing non-existing directory fails. + : + { + $* a >/~'%remove .+/a/ false true%' 2>>/~%EOE% == 1 + %rmdir: unable to remove '.+/a': .+% + EOE + } + + : force + : + : Removing non-existing directory succeeds with -f option. + : + $* -f a >>/~%EOO% + %remove .+/a/ true true% + %remove .+/a/ true false% + EOO + } + + : not-empty + : + : Removing non-empty directory fails. + : + { + mkdir -p a/b; + + $* a >/~'%remove .+/a/ false true%' 2>>/~%EOE% == 1 + %rmdir: unable to remove '.+/a': .+% + EOE + } + + : not-dir + : + : Removing not a directory path fails. + : + { + touch a; + + $* a >/~'%remove .+/a/ false true%' 2>>/~%EOE% == 1 + %rmdir: unable to remove '.+/a': .+% + EOE + } +} diff --git a/tests/builtin/sed.testscript b/tests/builtin/sed.testscript new file mode 100644 index 0000000..0d032bc --- /dev/null +++ b/tests/builtin/sed.testscript @@ -0,0 +1,350 @@ +# file : tests/builtin/sed.testscript +# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +test.arguments = "sed" +test.options += -c + +: unknown-option +: +{ + : unparsed + : + $* -u >'option -u' 2>"sed: unknown option '-u'" == 1 + + : parsed + : + { + test.options += -o -u + + : start + : + $* -u -n -e 's/a/b/p' <'a' >>EOO + option -u + b + EOO + + : middle + : + $* -n -u -e 's/a/b/p' <'a' >>EOO + option -u + b + EOO + + : end + : + $* -n -e 's/a/b/p' -u <'a' >>EOO + option -u + b + EOO + + : before-args + : + { + echo 'a' >=f; + + $* -n -e 's/a/b/p' -u f >>EOO + option -u + b + EOO + } + } + + : arg + : + { + echo 'a' >=-u; + + $* -n -e 's/a/b/p' -- -u >'b' + } +} + +: arg +: +{ + : auto-prn + : + { + $* -n -e 's/fox/bar/' <'foo' : on + $* -e 's/fox/bar/' <'foo' >'foo' : off + } + + : script + : + { + : missed + : + $* 2>>EOE != 0 + sed: missing script + EOE + + : missed-val + : + $* -e 2>>EOE != 0 + sed: missing value for option '-e' + EOE + + : empty + : + $* -e '' 2>>EOE != 0 + sed: empty script + EOE + + : multiple + : + $* -e 's/a//' -e 's/a//' 2>>EOE != 0 + sed: multiple scripts + EOE + + : invalid + : + $* -e 'z' 2>>EOE != 0 + sed: only 's' command supported + EOE + } + + : file + : + { + : exist + : + { + cat <'foo' >=f; + + $* -e 's/foo/bar/' f >'bar' + } + + : none + : + $* -e 's/foo/bar/' <'foo' >'bar' + + : dash + : + $* -e 's/foo/bar/' - <'foo' >'bar' + + : not-exist + : + $* -e 's/foo/bar/' f 2>>/~%EOE% != 0 + %sed: unable to edit '.+/f': .+% + EOE + + : empty + : + $* -e 's/foo/bar/' '' 2>>EOE != 0 + sed: invalid path '' + EOE + } + + : unexpected + : + $* -e 's/a//' a b 2>>EOE != 0 + sed: unexpected argument 'b' + EOE +} + +: command +: +{ + : subst + : + { + : parsing + : + { + : delim + : + { + : none + : + $* -e 's' 2>>EOE != 0 + sed: no delimiter for 's' command + EOE + + : invalid + : + $* -e 's\\' 2>>EOE != 0 + sed: invalid delimiter for 's' command + EOE + } + + : regex + : + { + : unterminated + : + $* -e 's/foo' 2>>/EOE != 0 + sed: unterminated 's' command regex + EOE + + : empty + : + $* -e 's///' 2>>EOE != 0 + sed: empty regex in 's' command + EOE + + : invalid + : + : Note that old versions of libc++ (for example 1.1) do not detect some + : regex errors. For example '*' is parsed successfully. + : + $* -e 's/foo[/bar/' 2>>~%EOE% != 0 + %sed: invalid regex.*% + EOE + } + + : unterminated-replacement + : + $* -e 's/foo/bar' 2>>/EOE != 0 + sed: unterminated 's' command replacement + EOE + + : invalid-flags + : + $* -e 's/foo/bar/a' 2>>EOE != 0 + sed: invalid 's' command flag 'a' + EOE + } + + : exec + : + { + : flags + : + { + : global + : + { + $* -e 's/o/a/g' <'foo' >'faa' : on + $* -e 's/o/a/' <'foo' >'fao' : off + } + + : icase + : + { + $* -e 's/O/a/i' <'foo' >'fao' : on + $* -e 's/O/a/' <'foo' >'foo' : off + } + + : print + : + { + $* -n -e 's/o/a/p' <'foo' >'fao' : on-match + $* -n -e 's/o/a/' <'foo' : off-match + $* -n -e 's/u/a/p' <'foo' : on-no-match + } + } + + : search + { + : anchor + : + { + $* -n -e 's/^o/a/gp' <'oof' >'aof' : begin + $* -n -e 's/o$/a/gp' <'foo' >'foa' : end + } + + : match + : Match corner cases + : + { + $* -n -e 's/a/b/p' <'a' >'b' : full + $* -n -e 's/a/b/p' <'ac' >'bc' : left + $* -n -e 's/a/b/p' <'ca' >'cb' : right + $* -n -e 's/a/b/pg' <'xaax' >'xbbx' : adjacent + } + } + + : replacement + : + { + : ecma-escape + : + { + $* <'xay' -e 's/a/$b/' >'x$by' : none + $* <'xay' -e 's/a/$/' >'x$y' : none-term + $* <'xay' -e 's/a/$$/' >'x$y' : self + $* <'xay' -e 's/a/b$&c/' >'xbacy' : match + $* <'xay' -e 's/a/b$`c/' >'xbxcy' : match-precede + $* <'xay' -e "s/a/b\$'c/" >'xbycy' : match-follow + + : capture + : + $* <'abcdefghij' -e 's/(a)(b)(c)(d)(e)(f)(g)(h)(i)(j)/$1$10/' >'aj' + } + + : perl-escape + : + { + $* <'xay' -e 's/a/\b/' >'xby' : none + $* <'xay' -e 's/a/\/' >'xy' : none-term + $* <'xay' -e 's/a/\\/' >'x\y' : self + + : capture + : + $* <'abcdefghij' -e 's/(a)(b)(c)(d)(e)(f)(g)(h)(i)(j)/\1\10/' >'aa0' + + : upper + : + { + $* <'xay' -e 's/a/\U/' >'xy' : none + $* <'xay' -e 's/a/\Uvz/' >'xVZy' : repl + $* <'xay' -e 's/a/\Uv\Ez/' >'xVzy' : end + $* <'aa' -e 's/a/v\Uz/g' >'vZvZ' : locality + $* <'xay' -e 's/(a)/\U\1/' >'xAy' : capt + $* <'x-y' -e 's/(a?)-/\U\1z/' >'xZy' : capt-empty + $* <'xay' -e 's/a/\uvz/' >'xVzy' : once + } + + : lower + : + { + $* <'xay' -e 's/a/\lVZ/' >'xvZy' : once + } + } + } + + $* -e 's/a//' <:'b' >'b' : no-newline + $* -e 's/a//' <:'' : empty-stdin + + : empty-file + : + { + touch f; + + $* -e 's/a//' f + } + } + } +} + +: in-place +: +{ + : no-file + : + $* -i -e 's/a/b/' 2>>EOE != 0 + sed: -i|--in-place option specified while reading from stdin + EOE + + : edit + : + { + cat <'foo' >=f; + + $* -i -e 's/foo/bar/' f; + + cat f >'bar' + } +} + +: big +: +: Sed a big file (about 100K) to test that the builtin is asynchronous. +: +{ + s="--------------------------------" + s="$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s" + s="$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s" + s="$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s" + cat <"$s" | $* -e 's/^x//' | cat >"$s" +} diff --git a/tests/builtin/sleep.testscript b/tests/builtin/sleep.testscript new file mode 100644 index 0000000..a334cba --- /dev/null +++ b/tests/builtin/sleep.testscript @@ -0,0 +1,42 @@ +# file : tests/builtin/sleep.testscript +# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +test.arguments = "sleep" + +: unknown-option +: +$* -u 2>"sleep: unknown option '-u'" == 1 + +: success +: +{ + : custom + : + { + test.options += -c; + $* 1 >'sleep 01 seconds' + } + + : own + : + $* 1 +} + +: no-time +: +: Test passing no time interval. +: +$* 2>"sleep: missing time interval" != 0 + +: invalid-time +: +: Test passing invalid time interval. +: +$* 1a 2>"sleep: invalid time interval '1a'" != 0 + +: unexpected-arg +: +: Test passing extra argument. +: +$* 1 1 2>"sleep: unexpected argument '1'" != 0 diff --git a/tests/builtin/test.testscript b/tests/builtin/test.testscript new file mode 100644 index 0000000..3e132d9 --- /dev/null +++ b/tests/builtin/test.testscript @@ -0,0 +1,84 @@ +# file : tests/builtin/test.testscript +# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +test.arguments = "test" + +: file +: +{ + : exists + : + touch a; + $* -f a + + : not-exists + : + $* -f a == 1 + + : not-file + : + $* -f . == 1 +} + +: dir +: +{ + : exists + : + $* -d . + + : not-exists + : + $* -d a == 1 + + : not-dir + : + touch a; + $* -d a == 1 +} + +: options +: +{ + : unknown + : + $* -u 2>"test: unknown option '-u'" == 2 + + : none + : + $* 2>"test: either -f|--file or -d|--directory must be specified" == 2 + + : both-file-dir + : + $* -fd 2>"test: both -f|--file and -d|--directory specified" == 2 +} + +: args +: +{ + : none + : + $* -f 2>"test: missing path" == 2 + + : unexpected + : + $* -f a b 2>"test: unexpected argument 'b'" == 2 + + : empty-path + : + $* -d '' 2>"test: invalid path ''" == 2 +} + +: cwd +: +: When cross-testing we cannot guarantee that host absolute paths are +: recognized by the target process. +: +if ($test.target == $build.host) +{ + test.options += -d $~/a; + mkdir -p a/b; + + $* -d b +} diff --git a/tests/builtin/touch.testscript b/tests/builtin/touch.testscript new file mode 100644 index 0000000..aa0ea10 --- /dev/null +++ b/tests/builtin/touch.testscript @@ -0,0 +1,92 @@ +# file : tests/builtin/touch.testscript +# copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +test.arguments = "touch" + +: file +: +$* a &a + +: file-create +: +: Test that file is created. If it didn't then 'rm' would fail. +: +{ + $* a &!a; + + rm a +} + +: file-update +: +: Test that existing file touch doesn't fail. +: +{ + cat <'' >=a; + + $* a +} + +: callback +: +: Test that the callback is not called for touching an existing file. +: +{ + test.options += -c; + + $* a >>/~%EOO% &a; + %create .+/a true% + %create .+/a false% + EOO + + $* >>/~%EOO% a + %create .+/a true% + %create .+/a false% + EOO +} + +: unknown-option +: +$* -u 2>"touch: unknown option '-u'" == 1 + +: no-args +: +: Test passing no arguments. +: +$* 2>"touch: missing file" != 0 + +: empty-path +: +: Test touching an empty path. +: +$* '' 2>"touch: invalid path ''" != 0 + +: dir-update +: +: Test touching an existing directory. +: +{ + mkdir a; + + $* a 2>~'%touch: cannot create/update .+: .+%' != 0 +} + +: after +: +{ + : success + : + { + $* a &a; + $* --after a b &b + } + + : no-value + : + $* --after 2>"touch: missing value for option '--after'" != 0 + + : not-exists + : + touch --after a b 2>~"%touch: cannot obtain file '.+a' modification time: .+%" != 0 +} |