aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKaren Arutyunov <karen@codesynthesis.com>2019-09-06 19:20:46 (GMT)
committerKaren Arutyunov <karen@codesynthesis.com>2019-09-27 14:08:05 (GMT)
commit1c6758009e82c47b5b341d418be2be401ef31482 (patch)
treed3ef8c053280477086f6230e3d25ff90b25871a2
parent070871d97b4f6440c3f0fc647ece73b53a5837db (diff)
Add builtins support
-rw-r--r--build/root.build8
-rw-r--r--libbutl/buildfile40
-rw-r--r--libbutl/builtin-options.cxx3332
-rw-r--r--libbutl/builtin-options.hxx1006
-rw-r--r--libbutl/builtin-options.ixx284
-rw-r--r--libbutl/builtin.cli82
-rw-r--r--libbutl/builtin.cxx2111
-rw-r--r--libbutl/builtin.mxx174
-rw-r--r--tests/builtin/buildfile9
-rw-r--r--tests/builtin/cat.testscript82
-rw-r--r--tests/builtin/cp-dir/cp-file0
-rw-r--r--tests/builtin/cp.testscript447
-rw-r--r--tests/builtin/driver.cxx159
-rw-r--r--tests/builtin/echo.testscript26
-rw-r--r--tests/builtin/ln.testscript217
-rw-r--r--tests/builtin/mkdir.testscript102
-rw-r--r--tests/builtin/mv.testscript182
-rw-r--r--tests/builtin/rm.testscript105
-rw-r--r--tests/builtin/rmdir.testscript95
-rw-r--r--tests/builtin/sed.testscript350
-rw-r--r--tests/builtin/sleep.testscript42
-rw-r--r--tests/builtin/test.testscript84
-rw-r--r--tests/builtin/touch.testscript92
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
+}