diff --git a/tools/Makefile b/tools/Makefile index 8f9fa148..37fb6274 100644 --- a/tools/Makefile +++ b/tools/Makefile @@ -13,6 +13,8 @@ USR_CPPFLAGS += -I$(TOP)/src PROD_LIBS += pvxs Com +PROD_SRCS += cliutil.cpp + PROD += pvxvct pvxvct_SRCS += pvxvct.cpp @@ -39,6 +41,10 @@ pvxmshim_SRCS += mshim.cpp # tests of CLI tools +TESTPROD_HOST += testcliutil +testcliutil_SRCS += testcliutil.cpp +TESTS += testcliutil + TESTPROD_HOST += testxput testxput_SRCS += testxput.cpp TESTS += testxput diff --git a/tools/cliutil.cpp b/tools/cliutil.cpp new file mode 100644 index 00000000..8a5a15ea --- /dev/null +++ b/tools/cliutil.cpp @@ -0,0 +1,83 @@ +/** + * Copyright - See the COPYRIGHT that is included with this distribution. + * pvxs is distributed subject to a Software License Agreement found + * in file LICENSE that is included with this distribution. + */ + +#include "cliutil.h" + +namespace pvxs { + +bool operator==(const ArgVal& rhs, const ArgVal& lhs) { + return rhs.defined==lhs.defined && rhs.value==lhs.value; +} + +GetOpt::GetOpt(int argc, char *argv[], const char *spec) + :argv0("") +{ + if(argc>=1) + argv0 = argv[0]; + + bool allpos = false; // after "--", treat all remaining as positional + for(int i=1; i +#include +#include // for std::pair + +namespace pvxs { + +struct ArgVal { + std::string value; + bool defined = false; + + ArgVal() = default; + ArgVal(std::nullptr_t) {} + ArgVal(const std::string& value) :value(value), defined(true) {} + ArgVal(const char* value) :value(value), defined(true) {} +}; + +bool operator==(const ArgVal& rhs, const ArgVal& lhs); + +struct GetOpt { + GetOpt(int argc, char *argv[], const char *spec); + + const char *argv0; + std::vector positional; + std::vector> arguments; + bool success = false; +}; + +} // namespace pvxs + +#endif // CLIUTIL_H diff --git a/tools/put.cpp b/tools/put.cpp index 62edad3e..7f88a642 100644 --- a/tools/put.cpp +++ b/tools/put.cpp @@ -9,7 +9,6 @@ #include #include -#include #include #include @@ -17,6 +16,7 @@ #include #include "utilpvt.h" #include "evhelper.h" +#include "cliutil.h" #ifndef REALMAIN # define REALMAIN main @@ -49,55 +49,53 @@ int REALMAIN(int argc, char *argv[]) bool verbose = false; std::string request; - { - int opt; - while ((opt = getopt(argc, argv, "hvVdw:r:")) != -1) { - switch(opt) { - case 'h': - usage(argv[0]); - return 0; - case 'V': - std::cout<(optarg); - break; - case 'r': - request = optarg; - break; - default: - usage(argv[0]); - std::cerr<<"\nUnknown argument: "<(optarg); + break; + case 'r': + request = optarg; + break; + default: + usage(opts.argv0); + std::cerr<<"\nUnknown argument: "< values; - if(argc-optind==1 && std::string(argv[optind]).find_first_of('=')==std::string::npos) { + if(opts.positional.size()==2 && std::string(opts.positional[1]).find_first_of('=')==std::string::npos) { // only one field assignment, and field name omitted. // if JSON map, treat as entire struct. Others imply .value - auto sval(argv[optind]); + const auto& sval = opts.positional[1]; values[sval[0]=='{' ? "" : "value"] = sval; } else { - for(auto n : range(optind, argc)) { - std::string fv(argv[n]); + for(auto n : range(size_t(1), opts.positional.size())) { + std::string fv(opts.positional[n]); auto sep = fv.find_first_of('='); if(sep==std::string::npos) { diff --git a/tools/testcliutil.cpp b/tools/testcliutil.cpp new file mode 100644 index 00000000..6a004c30 --- /dev/null +++ b/tools/testcliutil.cpp @@ -0,0 +1,110 @@ +/** + * Copyright - See the COPYRIGHT that is included with this distribution. + * pvxs is distributed subject to a Software License Agreement found + * in file LICENSE that is included with this distribution. + */ + +#include +#include + +#include + +#include +#include + +#include +#include +#include + +#include "cliutil.h" + +namespace pvxs { namespace detail { + +template +struct test_print> { + template + static inline void op(C& strm, const std::vector& v) { + bool first = true; + for(auto& e : v) { + if(first) { + first = false; + } else { + strm<<' '; + } + test_print::op(strm, e); + } + } +}; + +template<> +struct test_print> { + template + static inline void op(C& strm, const std::pair& v) { + strm<<'-'<(argv), "a:v"); + testEq(opts.argv0, "exe"); + decltype (opts.arguments) arguments({{'v', nullptr}, {'a',"Aa"}}); + testArrEq(opts.arguments, arguments); + decltype (opts.positional) positional({"hello"}); + testArrEq(opts.positional, positional); + } + + { + testDiag("case @%d", __LINE__); + const char* argv[] = {"exe", "-v", "-a", "Aa", "hello"}; + pvxs::GetOpt opts(NELEMENTS(argv), const_cast(argv), "va:"); + testEq(opts.argv0, "exe"); + decltype (opts.arguments) arguments({{'v', nullptr}, {'a',"Aa"}}); + testArrEq(opts.arguments, arguments); + decltype (opts.positional) positional({"hello"}); + testArrEq(opts.positional, positional); + } + + { + testDiag("case @%d", __LINE__); + const char* argv[] = {"exe", "-v", "hello", "-aAa"}; + pvxs::GetOpt opts(NELEMENTS(argv), const_cast(argv), "va:"); + testEq(opts.argv0, "exe"); + decltype (opts.arguments) arguments({{'v', nullptr}, {'a',"Aa"}}); + testArrEq(opts.arguments, arguments); + decltype (opts.positional) positional({"hello"}); + testArrEq(opts.positional, positional); + } + + { + testDiag("case @%d", __LINE__); + const char* argv[] = {"exe", "-v", "hello", "-a"}; // missing value + pvxs::GetOpt opts(NELEMENTS(argv), const_cast(argv), "va:"); + testEq(opts.argv0, "exe"); + decltype (opts.arguments) arguments({{'v', nullptr}, {'?',nullptr}}); + testArrEq(opts.arguments, arguments); + decltype (opts.positional) positional({"hello"}); + testArrEq(opts.positional, positional); + } + + { + testDiag("case @%d", __LINE__); + const char* argv[] = {"exe", "-vvaTest", "hello"}; + pvxs::GetOpt opts(NELEMENTS(argv), const_cast(argv), "va:"); + testEq(opts.argv0, "exe"); + decltype (opts.arguments) arguments({{'v', nullptr}, {'v', nullptr}, {'a', "Test"}}); + testArrEq(opts.arguments, arguments); + decltype (opts.positional) positional({"hello"}); + testArrEq(opts.positional, positional); + } + + return testDone(); +} diff --git a/tools/testxput.cpp b/tools/testxput.cpp index e792f72a..8b7cdee5 100644 --- a/tools/testxput.cpp +++ b/tools/testxput.cpp @@ -75,9 +75,6 @@ struct Run { { Redirect rd_cout(std::cout, out); Redirect rd_cerr(std::cerr, err); - // reset getopt()... portable? - // "man 3 getopt" says optind=1, but does not work with glibc - optind = 0; ret = pvxput(argv.size(), (char**)argv.data()); } if(doWithOut) @@ -99,18 +96,28 @@ struct Run { MAIN(testxput) { - testPlan(0); + testPlan(25); - auto valI32(nt::NTScalar{TypeCode::Int32}.create()); auto pvI32(server::SharedPV::buildMailbox()); - pvI32.open(valI32.cloneEmpty() + pvI32.open(nt::NTScalar{TypeCode::Int32}.create() .update("value", 5)); + auto pvS(server::SharedPV::buildMailbox()); + pvS.open(nt::NTScalar{TypeCode::String}.create() + .update("value", "foo")); + + auto pvE(server::SharedPV::buildMailbox()); + pvE.open(nt::NTEnum{}.create() + .update("value.index", 0) + .update("value.choices", shared_array({"one", "two"}))); + // setup isolated server auto srv(server::Config::isolated() .build() - .addPV("testI32", pvI32)); - srv.start(); + .addPV("testI32", pvI32) + .addPV("testS", pvS) + .addPV("testE", pvE) + .start()); // setup environment for pvxput() to find only our isolated server { @@ -138,9 +145,52 @@ MAIN(testxput) }) .test(); - Run({"testI32", "6"}) - .test(); - testEq(pvI32.fetch()["value"].as(), 6); + { + Run({"testI32", "6"}) + .test(); + testEq(pvI32.fetch()["value"].as(), 6); + + Run({"testI32", "value=7"}) + .test(); + testEq(pvI32.fetch()["value"].as(), 7); + + Run({"testI32", R"({"value":8})"}) + .test(); + testEq(pvI32.fetch()["value"].as(), 8); + } + + { + Run({"testS", "hello"}) + .test(); + testEq(pvS.fetch()["value"].as(), "hello"); + + Run({"testS", "value=world"}) + .test(); + testEq(pvS.fetch()["value"].as(), "world"); + + Run({"testS", R"({"value":"baz"})"}) + .test(); + testEq(pvS.fetch()["value"].as(), "baz"); + } + + { + Run({"testE", "hello"}) + .exitWith(1) // invalid choice + .test(); + testEq(pvE.fetch()["value.index"].as(), 0); + + Run({"testE", "two"}) + .test(); + testEq(pvE.fetch()["value.index"].as(), 1); + + Run({"testE", "0"}) + .test(); + testEq(pvE.fetch()["value.index"].as(), 0); + + Run({"testE", "42"}) // can set arbitrary index + .test(); + testEq(pvE.fetch()["value.index"].as(), 42); + } return testDone(); }