From 361fcde22d3c9729882505968c3370effb0ac772 Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Thu, 14 Mar 2019 22:03:59 +0300 Subject: Add support for c++ source file extensions granular customization --- bdep/bdep.cxx | 2 +- bdep/buildfile | 7 ++- bdep/new-parsers.cxx | 64 +++++++++++++------ bdep/new-types.hxx | 11 +++- bdep/new-types.ixx | 64 +++++++++++++++++++ bdep/new.cli | 137 +++++++++++++++++++++++++++++++--------- bdep/new.cxx | 172 +++++++++++++++++++++++++++++++++++++++------------ tests/new.testscript | 170 ++++++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 535 insertions(+), 92 deletions(-) create mode 100644 bdep/new-types.ixx diff --git a/bdep/bdep.cxx b/bdep/bdep.cxx index 00d70f7..e4146e5 100644 --- a/bdep/bdep.cxx +++ b/bdep/bdep.cxx @@ -203,7 +203,7 @@ try // First parse common options and --version/--help. // - options o; + bdep::options o; o.parse (scan, unknown_mode::stop); if (o.version ()) diff --git a/bdep/buildfile b/bdep/buildfile index 74e8a0e..a328629 100644 --- a/bdep/buildfile +++ b/bdep/buildfile @@ -127,9 +127,10 @@ if $cli.configured cli.options += -I $src_root --include-with-brackets --include-prefix bdep \ --guard-prefix BDEP --cxx-prologue "#include " \ --cli-namespace bdep::cli --generate-vector-scanner --generate-file-scanner \ ---generate-group-scanner --keep-separator --generate-specifier --generate-modifier \ ---generate-parse --page-usage 'bdep::print_$name$_' --ansi-color \ ---include-base-last --suppress-undocumented --option-length 23 +--generate-group-scanner --keep-separator --generate-specifier \ +--generate-modifier --generate-description --generate-parse \ +--page-usage 'bdep::print_$name$_' --ansi-color --include-base-last \ +--suppress-undocumented --option-length 23 cli.cxx{common-options}: cli.options += --short-usage --long-usage # Both. cli.cxx{bdep-options}: cli.options += --short-usage diff --git a/bdep/new-parsers.cxx b/bdep/new-parsers.cxx index ca45447..f8a8c75 100644 --- a/bdep/new-parsers.cxx +++ b/bdep/new-parsers.cxx @@ -21,34 +21,62 @@ namespace bdep static O parse_options (const char* o, const string v, size_t pos) { - // Use vector_scanner to parse the comma-separated list as options. + // Use vector_scanner to parse the comma-separated list of + // parameter-specific options. Make sure that option values are only + // specified if required (no value for flags, etc). // vector os; + const options& ods (O::description ()); + for (size_t i (pos), j; i != string::npos; ) { j = i + 1; i = v.find (',', j); - os.push_back (string (v, j, i != string::npos ? i - j : i)); + + string po (v, j, i != string::npos ? i - j : i); + + // Split the parameter-specific option into the name and value. + // + optional pv; + { + size_t i (po.find ('=')); + + if (i != string::npos) + { + pv = string (po, i + 1); + po.resize (i); + } + } + + // Verify that the option is known and its value is only specified if + // required. + // + { + auto i (ods.find (po)); + + if (i == ods.end ()) + throw invalid_value (o, po); + + bool flag (!pv); + + if (flag != i->flag ()) + throw invalid_value (o, + po, + string (flag ? "missing" : "unexpected") + + " value for '" + po + "'"); + } + + os.push_back (move (po)); + + if (pv) + os.push_back (move (*pv)); } vector_scanner s (os); - try - { - O r; - r.parse (s, - unknown_mode::fail /* unknown_option */, - unknown_mode::fail /* unknown_argument */); - return r; - } - catch (const unknown_option& e) - { - throw invalid_value (o, e.option ()); - } - catch (const unknown_argument& e) - { - throw invalid_value (o, e.argument ()); - } + O r; + r.parse (s); + return r; } void parser:: diff --git a/bdep/new-types.hxx b/bdep/new-types.hxx index f80b973..0f36e42 100644 --- a/bdep/new-types.hxx +++ b/bdep/new-types.hxx @@ -82,7 +82,14 @@ namespace bdep // Default is C++ with no options. // - cmd_new_lang_template (): lang (cxx) {cxx_opt = CXX ();} + cmd_new_lang_template (): lang (cxx), cxx_opt (CXX ()) {} + + cmd_new_lang_template (cmd_new_lang_template&&); + cmd_new_lang_template (const cmd_new_lang_template&); + cmd_new_lang_template& operator= (cmd_new_lang_template&&); + cmd_new_lang_template& operator= (const cmd_new_lang_template&); + + ~cmd_new_lang_template (); }; using cmd_new_lang = cmd_new_lang_template<>; @@ -114,4 +121,6 @@ namespace bdep using cmd_new_vcs = cmd_new_vcs_template<>; } +#include + #endif // BDEP_NEW_TYPES_HXX diff --git a/bdep/new-types.ixx b/bdep/new-types.ixx new file mode 100644 index 0000000..a8eafc5 --- /dev/null +++ b/bdep/new-types.ixx @@ -0,0 +1,64 @@ +// file : bdep/new-types.ixx -*- C++ -*- +// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include // move() + +namespace bdep +{ + template + inline cmd_new_lang_template:: + ~cmd_new_lang_template () + { + if (lang == c) + c_opt.~C (); + else + cxx_opt.~CXX (); + } + + template + inline cmd_new_lang_template:: + cmd_new_lang_template (cmd_new_lang_template&& l): lang (l.lang) + { + if (lang == c) + new (&c_opt) C (move (l.c_opt)); + else + new (&cxx_opt) CXX (move (l.cxx_opt)); + } + + template + inline cmd_new_lang_template:: + cmd_new_lang_template (const cmd_new_lang_template& l): lang (l.lang) + { + if (lang == c) + new (&c_opt) C (l.c_opt); + else + new (&cxx_opt) CXX (l.cxx_opt); + } + + template + inline cmd_new_lang_template& cmd_new_lang_template:: + operator= (cmd_new_lang_template&& l) + { + if (this != &l) + { + this->~cmd_new_lang_template (); + + // Assume noexcept move-construction. + // + new (this) cmd_new_lang_template (move (l)); + } + + return *this; + } + + template + inline cmd_new_lang_template& cmd_new_lang_template:: + operator= (const cmd_new_lang_template& l) + { + if (this != &l) + *this = cmd_new_lang_template (l); // Reduce to move-assignment. + + return *this; + } +} diff --git a/bdep/new.cli b/bdep/new.cli index 38572c8..8f64e8f 100644 --- a/bdep/new.cli +++ b/bdep/new.cli @@ -119,45 +119,66 @@ namespace bdep \li|\cb{exe} - A project that builds a sample executable. Recognized executable - project options: + A project that builds a sample executable. Recognized + executable project options:| - \cb{no-tests} \- Don't add support for functional/integration testing. + \li|\n\ \ \ \cb{no-tests} - \cb{unit-tests} \- Add support for unit testing. + Don't add support for functional/integration testing.| - \cb{alt-naming} \- Use the alternative build file/directory naming scheme. - | + \li|\n\ \ \ \cb{unit-tests} + + Add support for unit testing.| + + \li|\n\ \ \ \cb{alt-naming} + + Use the alternative build file/directory naming scheme.|| + + \dl| \li|\cb{lib} - A project that builds a sample library. Recognized library project - options: + A project that builds a sample library. Recognized library + project options:| + + \li|\n\ \ \ \cb{no-tests} + + Don't add support for functional/integration testing.| + + \li|\n\ \ \ \cb{unit-tests} + + Add support for unit testing.| + + \li|\n\ \ \ \cb{no-version} - \cb{no-tests} \- Don't add support for functional/integration testing. + Don't add support for generating the version header.| - \cb{unit-tests} \- Add support for unit testing. + \li|\n\ \ \ \cb{alt-naming} - \cb{no-version} \- Don't add support for generating the version header. + Use the alternative build file/directory naming scheme.|| - \cb{alt-naming} \- Use the alternative build file/directory naming scheme. - | + \dl| \li|\cb{bare} - A project without any source code that can be filled later (see - \cb{--subdirectory}). Recognized bare project options: + A project without any source code that can be filled later + (see \cb{--subdirectory}). Recognized bare project options:| + + \li|\n\ \ \ \cb{no-tests} + + Don't add support for testing.| - \cb{no-tests} \- Don't add support for testing. + \li|\n\ \ \ \cb{alt-naming} - \cb{alt-naming} \- Use the alternative build file/directory naming scheme. - | + Use the alternative build file/directory naming scheme.|| + + \dl| \li|\cb{empty} An empty project that can be filled with packages (see - \cb{--package}). Note that the project language is ignored for - this project type.|| + \cb{--package}). Note that the project language is ignored for this + project type.|| The project language can be specified with the \c{\b{--lang}|\b{-l}} option. Valid values for this option and their semantics are described @@ -167,19 +188,65 @@ namespace bdep \li|\cb{c} - A C project.| + A C project.|| + + \dl| \li|\cb{c++} - A C++ project. Recognized language options: + A C++ project. Recognized language options:| + + \li|\n\ \ \ \cb{binless} + + Create a header-only library.| + + \li|\n\ \ \ \cb{cpp} - \cb{cxx} \- Use the \cb{.cxx}, \cb{.hxx}, \cb{.ixx}, \cb{.txx}, and - \cb{.mxx} source file extensions (default). + Use the \cb{.cpp}, \cb{.hpp}, \cb{.ipp}, \cb{.tpp}, and \cb{.mpp} + source file extensions (alias for \cb{extension=?pp}).| - \cb{cpp} \- Use the \cb{.cpp}, \cb{.hpp}, \cb{.ipp}, \cb{.tpp}, and - \cb{.mpp} source file extensions. + \li|\n\ \ \ \c{\b{extension=}\i{pattern}} - \cb{binless} \- Create a header-only library.|| + Derive source file extensions from \ci{pattern} by replacing + every \cb{?} with one of the \cb{c} (source), \cb{h} (header), + \cb{i} (inline), \cb{t} (template), or \cb{m} (module interface) + letters. If unspecified and no individual extensions are specified + with the below options, then \cb{?xx} is used by default.| + + \li|\n\ \ \ \c{\b{hxx=}\i{extension}} + + Use the specified \ci{extension} for header files instead of + the default \cb{.hxx}.| + + \li|\n\ \ \ \c{\b{cxx=}\i{extension}} + + Use the specified \ci{extension} for source files instead of + the default \cb{.cxx}.| + + \li|\n\ \ \ \c{\b{ixx=}\i{extension}} + + Use the specified \ci{extension} for inline files. If + unspecified, then assume no inline files are used by the + project.| + + \li|\n\ \ \ \c{\b{txx=}\i{extension}} + + Use the specified \ci{extension} for template files. If + unspecified, then assume no template files are used by the + project.| + + \li|\n\ \ \ \c{\b{mxx=}\i{extension}} + + Use the specified \ci{extension} for module interface files. If + unspecified, then assume no modules are used by the project.|| + + As an example, the following command creates a header-only C++ library + that uses the \cb{.h} extension for header files and \cb{.cpp} \- for + source files: + + \ + $ bdep new -t lib -l c++,binless,hxx=h,cxx=cpp libhello + \ The project version control system can be specified with the \c{\b{--vcs}|\b{-s}} option. Valid values for this option and their @@ -191,7 +258,9 @@ namespace bdep \li|\cb{git} Initialize a \cb{git(1)} repository inside the project and generate - \cb{.gitignore} files.| + \cb{.gitignore} files.|| + + \dl| \li|\cb{none} @@ -232,11 +301,19 @@ namespace bdep { }; + // The cpp flag is the "extension=?pp" alias and is mutually exclusive with + // extension=. + // class cmd_new_cxx_options { - bool cpp; - bool cxx; bool binless; + bool cpp; + string extension; + string hxx; + string cxx; + string ixx; + string txx; + string mxx; }; // --vcs options diff --git a/bdep/new.cxx b/bdep/new.cxx index 91b1bc5..80bb4d8 100644 --- a/bdep/new.cxx +++ b/bdep/new.cxx @@ -4,6 +4,8 @@ #include +#include // replace() + #include #include @@ -127,8 +129,24 @@ namespace bdep { auto& o (l.cxx_opt); - if (o.cpp () && o.cxx ()) - fail << "'cxx' and 'cpp' are mutually exclusive c++ options"; + if (o.cpp () && o.extension_specified ()) + fail << "'extension' and 'cpp' are mutually exclusive c++ options"; + + // Verify that none of the extensions are specified as empty, except + // for hxx. + // + auto empty_ext = [] (const string& v, const char* o) + { + if (v.empty () || (v.size () == 1 && v[0] == '.')) + fail << "empty extension specified with '" << o << "' c++ option"; + }; + + if (o.extension_specified ()) empty_ext (o.extension (), "extension"); + + if (o.cxx_specified ()) empty_ext (o.cxx (), "cxx"); + if (o.ixx_specified ()) empty_ext (o.ixx (), "ixx"); + if (o.txx_specified ()) empty_ext (o.txx (), "txx"); + if (o.mxx_specified ()) empty_ext (o.mxx (), "mxx"); if (o.binless () && t != type::lib) fail << "'binless' is only valid for libraries"; @@ -523,8 +541,13 @@ namespace bdep string m; // Language module. string x; // Source target type. string h; // Header target type. - string hs; // All header target types. - string es; // Source file extension suffix (pp, xx). + string hs; // All header-like target types. + string xe; // Source file extension (including leading dot). + string he; // Header file extension (including leading dot unless empty). + + optional ie; // Inline file extension. + optional te; // Template file extension. + optional me; // Module interface extension. switch (l) { @@ -534,19 +557,84 @@ namespace bdep x = "c"; h = "h"; hs = "h"; + xe = ".c"; + he = ".h"; break; } case lang::cxx: { + const auto& opt (l.cxx_opt); + m = "cxx"; x = "cxx"; h = "hxx"; - hs = "hxx ixx txx"; - es = l.cxx_opt.cpp () ? "pp" : "xx"; + hs = "hxx"; + + // Return the extension (v), if specified (s), derive the extension + // from the pattern and type (t), or return the default (d), if + // specified. + // + auto ext = [&opt] (bool s, + const string& v, + char t, + const char* d = nullptr) -> optional + { + optional r; + + if (s) + r = v; + else if (opt.extension_specified () || opt.cpp ()) + { + string p (opt.extension_specified () ? opt.extension () : + opt.cpp () ? "?pp" : ""); + + replace (p.begin (), p.end (), '?', t); + r = move (p); + } + else if (d != nullptr) + r = d; + + // Add leading dot if absent. + // + if (r && !r->empty () && r->front () != '.') + r = '.' + *r; + + return r; + }; + + xe = *ext (opt.cxx_specified (), opt.cxx (), 'c', "cxx"); + he = *ext (opt.hxx_specified (), opt.hxx (), 'h', "hxx"); + + // We only want default .ixx/.txx/.mxx if the user didn't specify + // any of the extension-related options explicitly. + // + bool d (!opt.cxx_specified () && + !opt.hxx_specified () && + !opt.ixx_specified () && + !opt.txx_specified () && + !opt.mxx_specified ()); + + ie = ext (opt.ixx_specified (), opt.ixx (), 'i', d ? "ixx" : nullptr); + te = ext (opt.txx_specified (), opt.txx (), 't', d ? "txx" : nullptr); + me = ext (opt.mxx_specified (), opt.mxx (), 'm', d ? "mxx" : nullptr); + + if (ie) hs += " ixx"; + if (te) hs += " txx"; + if (me) hs += " mxx"; + break; } } + // Return the pointer to the extension suffix after the leading dot or + // to the extension beginning if it is empty. + // + auto pure_ext = [] (const string& e) + { + assert (e.empty () || e[0] == '.'); + return e.c_str () + (e.empty () ? 0 : 1); + }; + // build/ // dir_path bd; @@ -595,11 +683,14 @@ namespace bdep os << "cxx.std = latest" << endl << endl << "using cxx" << endl - << endl - << "hxx{*}: extension = h" << es << endl - << "ixx{*}: extension = i" << es << endl - << "txx{*}: extension = t" << es << endl - << "cxx{*}: extension = c" << es << endl; + << endl; + + if (me) os << "mxx{*}: extension = " << pure_ext (*me) << endl; + os << "hxx{*}: extension = " << pure_ext (he) << endl; + if (ie) os << "ixx{*}: extension = " << pure_ext (*ie) << endl; + if (te) os << "txx{*}: extension = " << pure_ext (*te) << endl; + os << "cxx{*}: extension = " << pure_ext (xe) << endl; + break; } } @@ -676,9 +767,9 @@ namespace bdep } case lang::cxx: { - // /.c(xx|pp) + // /. // - os.open (f = sd / s + ".c" + es); + os.open (f = sd / s + xe); os << "#include " << endl << endl << "int main (int argc, char* argv[])" << endl @@ -804,9 +895,9 @@ namespace bdep } case lang::cxx: { - // /.test.c(xx|pp) + // /.test. // - os.open (f = sd / s + ".test.c" + es); + os.open (f = sd / s + ".test" + xe); os << "#include " << endl << "#include " << endl << endl @@ -890,10 +981,10 @@ namespace bdep case lang::cxx: if (l.cxx_opt.binless ()) { - apih = s + ".h" + es; - verh = ver ? "version.h" + es : string (); + apih = s + he; + verh = ver ? "version" + he : string (); - // .h(xx|pp) + // [.] // os.open (f = sd / apih); os << "#pragma once" << endl @@ -924,11 +1015,11 @@ namespace bdep } else { - apih = s + ".h" + es; - exph = "export.h" + es; - verh = ver ? "version.h" + es : string (); + apih = s + he; + exph = "export" + he; + verh = ver ? "version" + he : string (); - // .h(xx|pp) + // [.] // os.open (f = sd / apih); os << "#pragma once" << endl @@ -948,9 +1039,9 @@ namespace bdep << "}" << endl; os.close (); - // .c(xx|pp) + // . // - os.open (f = sd / s + ".c" + es); + os.open (f = sd / s + xe); os << "#include <" << ip << apih << ">" << endl << endl << "#include " << endl @@ -1246,9 +1337,9 @@ namespace bdep } case lang::cxx: { - // /.test.c(xx|pp) + // /.test. // - os.open (f = sd / s + ".test.c" + es); + os.open (f = sd / s + ".test" + xe); os << "#include " << endl << "#include " << endl << endl @@ -1324,22 +1415,25 @@ namespace bdep os << "cxx.std = latest" << endl << endl << "using cxx" << endl - << endl - << "hxx{*}: extension = h" << es << endl - << "ixx{*}: extension = i" << es << endl - << "txx{*}: extension = t" << es << endl - << "cxx{*}: extension = c" << es << endl; + << endl; + + if (me) os << "mxx{*}: extension = " << pure_ext (*me) << endl; + os << "hxx{*}: extension = " << pure_ext (he) << endl; + if (ie) os << "ixx{*}: extension = " << pure_ext (*ie) << endl; + if (te) os << "txx{*}: extension = " << pure_ext (*te) << endl; + os << "cxx{*}: extension = " << pure_ext (xe) << endl; + break; } } - os << endl + os << endl << "# Every exe{} in this subproject is by default a test."<< endl - << "#" << endl - << "exe{*}: test = true" << endl - << endl + << "#" << endl + << "exe{*}: test = true" << endl + << endl << "# The test target for cross-testing (running tests under Wine, etc)." << endl - << "#" << endl - << "test.target = $" << m << ".target" << endl; + << "#" << endl + << "test.target = $" << m << ".target" << endl; os.close (); // tests/build/.gitignore @@ -1425,9 +1519,9 @@ namespace bdep } case lang::cxx: { - // tests/basics/driver.c(xx|pp) + // tests/basics/driver. // - os.open (f = td / "driver.c" + es); + os.open (f = td / "driver" + xe); os << "#include " << endl << "#include " << endl << "#include " << endl diff --git a/tests/new.testscript b/tests/new.testscript index 8e8fd75..6662eee 100644 --- a/tests/new.testscript +++ b/tests/new.testscript @@ -294,6 +294,176 @@ status += -d prj EOE } } + + : extensions + : + { + : default + : + { + $* -t lib -l c++ libprj 2>>/"EOE" &libprj/***; + created new library project libprj in $~/libprj/ + EOE + + test -f libprj/libprj/prj.cxx; + test -f libprj/libprj/prj.hxx; + + $build libprj/ $cxx 2>>~%EOE% + %(version\.in|c\+\+|ar|ld) .+%{7} + EOE + } + + : cpp + : + { + $* -t lib -l c++,cpp libprj 2>>/"EOE" &libprj/***; + created new library project libprj in $~/libprj/ + EOE + + test -f libprj/libprj/prj.cpp; + test -f libprj/libprj/prj.hpp; + + $build libprj/ $cxx 2>>~%EOE% + %(version\.in|c\+\+|ar|ld) .+%{7} + EOE + } + + : extension-c++ + : + { + $* -t lib -l c++,extension=?++ libprj 2>>/"EOE" &libprj/***; + created new library project libprj in $~/libprj/ + EOE + + test -f libprj/libprj/prj.c++; + test -f libprj/libprj/prj.h++; + + $build libprj/ $cxx 2>>~%EOE% + %(version\.in|c\+\+|ar|ld) .+%{7} + EOE + } + + : extension-cc + : + { + $* -t lib -l c++,extension=?? libprj 2>>/"EOE" &libprj/***; + created new library project libprj in $~/libprj/ + EOE + + test -f libprj/libprj/prj.cc; + test -f libprj/libprj/prj.hh; + + $build libprj/ $cxx 2>>~%EOE% + %(version\.in|c\+\+|ar|ld) .+%{7} + EOE + } + + : cxx-hxx + : + { + $* -t lib -l c++,cxx=c,hxx=h libprj 2>>/"EOE" &libprj/***; + created new library project libprj in $~/libprj/ + EOE + + test -f libprj/libprj/prj.c; + test -f libprj/libprj/prj.h; + + $build libprj/ $cxx 2>>~%EOE% + %(version\.in|c\+\+|ar|ld) .+%{7} + EOE + } + + : empty-hxx + : + { + $* -t lib -l c++,hxx= libprj 2>>/"EOE" &libprj/***; + created new library project libprj in $~/libprj/ + EOE + + test -f libprj/libprj/prj.cxx; + test -f libprj/libprj/prj; + + $build libprj/ $cxx 2>>~%EOE% + %(version\.in|c\+\+|ar|ld) .+%{7} + EOE + } + + : ixx + : + { + $* -t lib -l c++,ixx=ixx libprj 2>>/"EOE" &libprj/***; + created new library project libprj in $~/libprj/ + EOE + + sed -n -e 's/(.*\bixx\b.*)/\1/p' libprj/build/root.build >>EOO; + ixx{*}: extension = ixx + EOO + + sed -n -e 's/(.*\bixx\b.*)/\1/p' libprj/libprj/buildfile >>~%EOO%; + %.*\{hxx ixx cxx\}.*% + {hxx ixx}{*}: + EOO + + $build libprj/ $cxx 2>>~%EOE% + %(version\.in|c\+\+|ar|ld) .+%{7} + EOE + } + + : leading-dot + : + { + $* -t lib -l c++,cxx=.cpp libprj 2>>/"EOE" &libprj/***; + created new library project libprj in $~/libprj/ + EOE + + test -f libprj/libprj/prj.cpp; + test -f libprj/libprj/prj.hxx; + + $build libprj/ $cxx 2>>~%EOE% + %(version\.in|c\+\+|ar|ld) .+%{7} + EOE + } + + : errors + : + { + : missing-value + : + $* -t lib -l c++,cxx libprj 2>>EOE != 0 + error: invalid value 'cxx' for option '-l': missing value for 'cxx' + EOE + + : unexpected-value + : + $* -t lib -l c++,cpp=cxx libprj 2>>EOE != 0 + error: invalid value 'cpp' for option '-l': unexpected value for 'cpp' + EOE + + : empty-value + : + $* -t lib -l c++,extension= libprj 2>>EOE != 0 + error: empty extension specified with 'extension' c++ option + EOE + + : dot-only + : + $* -t lib -l c++,extension=. libprj 2>>EOE != 0 + error: empty extension specified with 'extension' c++ option + EOE + + : unknown-value + : + $* -t lib -l c++,zxx= libprj 2>>EOE != 0 + error: invalid value 'zxx' for option '-l' + EOE + + : mutually-exclusive + : + $* -t lib -l c++,cpp,extension=?pp libprj 2>>EOE != 0 + error: 'extension' and 'cpp' are mutually exclusive c++ options + EOE + } + } } : cfg -- cgit v1.1