Skip to content

Commit

Permalink
re-write CLI argument parsing for testability
Browse files Browse the repository at this point in the history
  • Loading branch information
mdavidsaver committed Aug 1, 2024
1 parent 2b55aa5 commit f931113
Show file tree
Hide file tree
Showing 6 changed files with 495 additions and 38 deletions.
14 changes: 14 additions & 0 deletions tools/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ USR_CPPFLAGS += -I$(TOP)/src

PROD_LIBS += pvxs Com

PROD_SRCS += cliutil.cpp

PROD += pvxvct
pvxvct_SRCS += pvxvct.cpp

Expand All @@ -38,6 +40,18 @@ pvxlist_SRCS += list.cpp
PROD += pvxmshim
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

TESTSCRIPTS_HOST += $(TESTS:%=%.t)

#===========================

include $(TOP)/configure/RULES
Expand Down
83 changes: 83 additions & 0 deletions tools/cliutil.cpp
Original file line number Diff line number Diff line change
@@ -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("<program name>")
{
if(argc>=1)
argv0 = argv[0];

bool allpos = false; // after "--", treat all remaining as positional
for(int i=1; i<argc; i++) {
const char * arg = argv[i];
if(!allpos && arg[0]=='-') {
arg++;

if(arg[1]=='-') {
if(arg[2]=='\0') { // "--"
allpos = true;
continue;
}
// "--..." not supported
arguments.emplace_back(-1, argv[i]);
return;
}
// process as short args

for(; *arg; arg++) {
for(auto s=spec; *s; s++) {
if(*s==*arg) { // match
if(s[1]==':') { // need arg value
if(arg[1]=='\0') { // "-a", "value"
if(i+1==argc) {
// oops. no value
arguments.emplace_back('?', nullptr);
return;
}
arguments.emplace_back(*arg, argv[i+1]);
i++;

} else {
// "-avalue"
arguments.emplace_back(*arg, &arg[1]);
}
goto nextarg;

} else { // flag
arguments.emplace_back(*s, nullptr);
// continue scanning for more flags. eg. "-vv"
goto nextchar;
}
} else {
if(s[1]==':')
s++;
}
}
// unrecognized
arguments.emplace_back(-1, nullptr);
return;
nextchar:
(void)0; // need a statement after label...
}
nextarg:
(void)0;

} else {
positional.push_back(arg);
}
success = true;
}
}

} // namespace pvxs
58 changes: 58 additions & 0 deletions tools/cliutil.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* 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.
*/

#ifndef CLIUTIL_H
#define CLIUTIL_H

#include <vector>
#include <string>
#include <stdexcept>
#include <map> // for std::pair

#include <utilpvt.h>

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) {}

inline explicit
operator bool() const { return defined; }

inline
const std::string& operator*() const {
if(defined)
return value;
throw std::logic_error("Undefined argument value");
}

template<typename V>
inline
V as() const {
return parseTo<V>(**this);
}
};

bool operator==(const ArgVal& rhs, const ArgVal& lhs);

struct GetOpt {
GetOpt(int argc, char *argv[], const char *spec);

const char *argv0;
std::vector<std::string> positional;
std::vector<std::pair<char, ArgVal>> arguments;
bool success = false;
};

} // namespace pvxs

#endif // CLIUTIL_H
78 changes: 40 additions & 38 deletions tools/put.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,18 @@
#include <atomic>

#include <epicsVersion.h>
#include <epicsGetopt.h>
#include <epicsThread.h>

#include <pvxs/client.h>
#include <pvxs/log.h>
#include <pvxs/json.h>
#include "utilpvt.h"
#include "evhelper.h"
#include "cliutil.h"

#ifndef REALMAIN
# define REALMAIN main
#endif

using namespace pvxs;

Expand All @@ -35,65 +39,63 @@ void usage(const char* argv0)
;
}

}
} // namespace

int main(int argc, char *argv[])
int REALMAIN(int argc, char *argv[])
{
try {
logger_config_env(); // from $PVXS_LOG
double timeout = 5.0;
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<<pvxs::version_information;
return 0;
case 'v':
verbose = true;
break;
case 'd':
logger_level_set("pvxs.*", Level::Debug);
break;
case 'w':
timeout = parseTo<double>(optarg);
break;
case 'r':
request = optarg;
break;
default:
usage(argv[0]);
std::cerr<<"\nUnknown argument: "<<char(opt)<<std::endl;
return 1;
}
GetOpt opts(argc, argv, "hvVdw:r:");
for(auto& pair : opts.arguments) {
switch(pair.first) {
case 'h':
usage(opts.argv0);
return 0;
case 'V':
std::cout<<pvxs::version_information;
return 0;
case 'v':
verbose = true;
break;
case 'd':
logger_level_set("pvxs.*", Level::Debug);
break;
case 'w':
timeout = pair.second.as<double>();
break;
case 'r':
request = *pair.second;
break;
default:
usage(opts.argv0);
std::cerr<<"\nUnknown argument: "<<pair.first<<std::endl;
return 1;
}
}

if(optind==argc) {
usage(argv[0]);
std::cerr<<"\nExpected PV name\n";
if(opts.positional.size()<2) {
usage(opts.argv0);
std::cerr<<"\nExpected PV name and at least one value\n";
return 1;
}

std::string pvname(argv[optind++]);
const auto& pvname = opts.positional.front();
std::map<std::string, std::string> 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) {
Expand Down
Loading

0 comments on commit f931113

Please sign in to comment.