From 818fce324c592946b483b2cf484ea8dff22a4195 Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Mon, 17 Sep 2018 18:05:28 -0700 Subject: [PATCH] Add PVStructure::stream() --- src/factory/printer.cpp | 404 ++++++++++++++++++++++++++++++++++- src/pv/pvData.h | 45 ++++ testApp/misc/Makefile | 4 + testApp/misc/testprinter.cpp | 241 +++++++++++++++++++++ 4 files changed, 689 insertions(+), 5 deletions(-) create mode 100644 testApp/misc/testprinter.cpp diff --git a/src/factory/printer.cpp b/src/factory/printer.cpp index 964ff8d..7c13f61 100644 --- a/src/factory/printer.cpp +++ b/src/factory/printer.cpp @@ -4,12 +4,22 @@ * found in the file LICENSE that is included with the distribution */ +#include +#if defined(__linux__) || defined(__APPLE__) +#include +#define HAVE_ISATTY +#endif + #include -#define epicsExportSharedSymbols -#include +#include -using std::string; +#define epicsExportSharedSymbols +#include +#include +#if EPICS_VERSION_INT>=VERSION_INT(3,15,0,1) +# include +#endif namespace epics { namespace pvData { @@ -40,6 +50,390 @@ array_at_internal operator<<(std::ostream& str, array_at const& manip) { return array_at_internal(manip.index, str); } -}; +} -}} +}} //epics::pvData +namespace { +using namespace epics::pvData; + +bool useEscapes(std::ostream& strm) { + FILE *fp = 0; + // TODO: a better way to do this... + if(&std::cout==&strm) + fp = stdout; + if(&std::cerr==&strm) + fp = stderr; + if(!fp) return false; +#ifdef HAVE_ISATTY + // check $TERM as well? + return isatty(fileno(fp))==1; +#else + // TODO: in theory windows 10 can be made to understand escapes as well + return false; +#endif +} + + +void printAlarmTx(std::ostream& strm, const PVStructure& sub) +{ + PVScalar::const_shared_pointer pvSeverity(sub.getSubField("severity")); + PVScalar::const_shared_pointer pvStatus(sub.getSubField("status")); + PVString::const_shared_pointer pvMessage(sub.getSubField("message")); + + switch(pvSeverity ? pvSeverity->getAs() : 0) { + case 0: return; // nothing more to do here... + case 1: strm<<"MINOR "; break; + case 2: strm<<"MAJOR "; break; + case 3: strm<<"INVALID "; break; + case 4: strm<<"UNDEFINED "; break; + default: strm<getAs()<<' '; + } + + switch(pvStatus ? pvStatus->getAs() : 0) { + case 0: break; + case 1: strm<<"DEVICE "; break; + case 2: strm<<"DRIVER "; break; + case 3: strm<<"RECORD "; break; + case 4: strm<<"DB "; break; + case 5: strm<<"CONF "; break; + case 6: strm<<"UNDEFINED "; break; + case 7: strm<<"CLIENT "; break; + default: strm<getAs()<<' '; + } + + if(pvMessage && !pvMessage->get().empty()) + strm<get()<<' '; +} + + +void printAlarmT(std::ostream& strm, const PVStructure& top) +{ + PVStructure::const_shared_pointer sub(top.getSubField("alarm")); + if(sub) + printAlarmTx(strm, *sub); +} + +void printTimeTx(std::ostream& strm, const PVStructure& tsubop) +{ + char timeText[32]; + epicsTimeStamp epicsTS; + + PVScalar::const_shared_pointer secf(tsubop.getSubField("secondsPastEpoch")), + nsecf(tsubop.getSubField("nanoseconds")), + tagf(tsubop.getSubField("userTag")); + + epicsTS.secPastEpoch = secf ? secf->getAs() : 0; + epicsTS.nsec = nsecf ? nsecf->getAs() : 0; + + if(epicsTS.secPastEpoch > POSIX_TIME_AT_EPICS_EPOCH) + epicsTS.secPastEpoch -= POSIX_TIME_AT_EPICS_EPOCH; + else + epicsTS.secPastEpoch = 0; + + epicsTimeToStrftime(timeText, sizeof(timeText), "%Y-%m-%dT%H:%M:%S.%03f", &epicsTS); + strm << timeText << ' '; + if (tagf) { + int64 tagv = tagf->getAs(); + if(tagv) + strm << tagv << ' '; + } +} + + +void printTimeT(std::ostream& strm, const PVStructure& top) +{ + PVStructure::const_shared_pointer sub(top.getSubField("timeStamp")); + if(sub) + printTimeTx(strm, *sub); +} + +bool printEnumT(std::ostream& strm, const PVStructure& top) +{ + PVScalar::const_shared_pointer idx(top.getSubField("value.index")); + PVStringArray::const_shared_pointer choices(top.getSubFieldT("value.choices")); + if(!idx || !choices) return false; + + strm<view()); + uint32 I = idx->getAs(); + if(I>=ch.size()) { + strm<<'('<("value")); + if(!cf) return false; + + { + const FieldConstPtrArray& fields = cf->getStructure()->getFields(); + if(fields.empty()) return false; + for(size_t i=0, N=fields.size(); igetType()!=scalarArray) + return false; // cowardly refusing to handle anything else... + } + } + + { + bool havets = !!top.getSubField("timeStamp"); + bool haveal = !!top.getSubField("alarm"); + if(havets || haveal) + strm<("labels")); + if(lf) { + PVStringArray::const_svector L(lf->view()); + labels = thaw(L); + } + } + + const PVFieldPtrArray& columns = cf->getPVFields(); + std::vector > coldat(columns.size()); + + std::vector widths(columns.size()); + labels.reserve(columns.size()); + + size_t nrows = size_t(-1); + + for(size_t i=0, N=columns.size(); i=labels.size()) { + labels.push_back(cf->getStructure()->getFieldName(i)); + } + widths[i] = labels[i].size(); + + static_cast(columns[i].get())->getAs(coldat[i]); + + nrows = std::min(nrows, coldat[i].size()); + + for(size_t j=0, M=coldat[i].size(); j=0 && idxgetNextFieldOffset(); igetParent(); parent; parent = parent->getParent()) { + mask.set(parent->getFieldOffset()); + } + } + } + } +} + +} +namespace epics { namespace pvData { + +void printRaw(std::ostream& strm, const PVStructure::Formatter& format, const PVStructure& cur) +{ + typedef std::deque > todo_t; + todo_t todo; + BitSet show, highlight; + + const long orig_indent = format::indent_value(strm); + + { + if(format.xshow) + show = *format.xshow; + else + show.set(0); + + if(format.xhighlight) + highlight = *format.xhighlight; + + expandBS(format.xtop, show, true); + expandBS(format.xtop, highlight, false); + highlight &= show; // can't highlight hidden fields (paranoia) + } + + if(!show.get(0)) return; // nothing to do here... + + todo.push_front(std::make_pair(&format.xtop, orig_indent)); + + while(!todo.empty()) { + todo_t::value_type cur(todo.front()); + todo.pop_front(); + + format::indent_value(strm) = cur.second; + + bool hl = highlight.get(cur.first->getFieldOffset()) && format.xmode==PVStructure::Formatter::ANSI; + if(hl) + strm<<"\x1b[1m"; // Bold + + switch(cur.first->getField()->getType()) { + case structure: { + const PVStructure* str = static_cast(cur.first); + + const PVFieldPtrArray& flds = str->getPVFields(); + + for(PVFieldPtrArray::const_reverse_iterator it(flds.rbegin()), end(flds.rend()); it!=end; ++it) { + const PVField *fld = (*it).get(); + if(!show.get(fld->getFieldOffset())) continue; + + todo.push_front(std::make_pair(fld, cur.second+1)); + } + + std::string id(cur.first->getField()->getID()); + + strm<getFieldName(); + if(id=="alarm_t") { + strm.put(' '); + printAlarmTx(strm, *str); + } else if(id=="time_t") { + strm.put(' '); + printTimeTx(strm, *str); + } + strm.put('\n'); + } + break; + case scalar: + case scalarArray: + strm<getField()->getID()<<' '<getFieldName() + <<' '<<*cur.first + <<'\n'; + break; + case structureArray: + case union_: + case unionArray: + strm<<*cur.first; + break; + } + + if(hl) + strm<<"\x1b[0m"; // reset + } + + format::indent_value(strm) = orig_indent; +} + +std::ostream& operator<<(std::ostream& strm, const PVStructure::Formatter& format) +{ + if(format.xfmt==PVStructure::Formatter::JSON) { +#if EPICS_VERSION_INT>=VERSION_INT(3,15,0,1) + JSONPrintOptions opts; + opts.multiLine = false; + printJSON(strm, format.xtop, format.xshow ? *format.xshow : BitSet().set(0), opts); + strm<<'\n'; + return strm; +#else + // fall through to Raw +#endif + + } else if(format.xfmt==PVStructure::Formatter::NT) { + std::string id(format.xtop.getStructure()->getID()), + idprefix(id.substr(0, id.find_first_of('.'))); + + // NTTable + if(idprefix=="epics:nt/NTTable:1") { + if(printTable(strm, format.xtop)) + return strm; + } else { + //NTScalar, NTScalarArray, NTEnum, or anything with '.value' + + PVField::const_shared_pointer value(format.xtop.getSubField("value")); + if(value) { + switch(value->getField()->getType()) { + case scalar: + strm<(value.get())<<' '; + printAlarmT(strm, format.xtop); + strm<<'\n'; + return strm; + + case scalarArray: + strm<(value.get())<<'\n'; + return strm; + + case structure: + if(printEnumT(strm, format.xtop)) + return strm; + break; + default: + break; + } + } + } + } + // fall through unhandled as Raw + + PVStructure::Formatter format2(format); + + if(format2.xmode==PVStructure::Formatter::Auto) + format2.xmode = useEscapes(strm) ? PVStructure::Formatter::ANSI : PVStructure::Formatter::Plain; + + printRaw(strm, format2, format.xtop); + + return strm; +} + +}} //epics::pvData diff --git a/src/pv/pvData.h b/src/pv/pvData.h index 6f9dfba..83df1ca 100644 --- a/src/pv/pvData.h +++ b/src/pv/pvData.h @@ -886,7 +886,49 @@ public: void copyUnchecked(const PVStructure& from); void copyUnchecked(const PVStructure& from, const BitSet& maskBitSet, bool inverse = false); + struct Formatter { + enum mode_t { + Auto, + Plain, + ANSI, + }; + enum format_t { + Raw, + NT, + JSON, + }; + private: + const PVStructure& xtop; + const BitSet* xshow; + const BitSet* xhighlight; + mode_t xmode; + format_t xfmt; + public: + explicit Formatter(const PVStructure& top) + :xtop(top) + ,xshow(0) + ,xhighlight(0) + ,xmode(Auto) + ,xfmt(NT) + {} + + // those fields (and their parents) to be printed. non-NT mode. + FORCE_INLINE Formatter& show(const BitSet& set) { xshow = &set; return *this; } + // those fields (and not their parents) to be specially highlighted. non-NT mode. + FORCE_INLINE Formatter& highlight(const BitSet& set) { xhighlight = &set; return *this; } + + FORCE_INLINE Formatter& mode(mode_t m) { xmode = m; return *this; } + + FORCE_INLINE Formatter& format(format_t f) { xfmt = f; return *this; } + + friend epicsShareFunc std::ostream& operator<<(std::ostream& strm, const Formatter& format); + friend void printRaw(std::ostream& strm, const PVStructure::Formatter& format, const PVStructure& cur); + }; + + FORCE_INLINE Formatter stream() const { return Formatter(*this); } + private: + inline PVFieldPtr getSubFieldImpl(const std::string& name, bool throws) const { return getSubFieldImpl(name.c_str(), throws); } @@ -900,6 +942,9 @@ private: EPICS_NOT_COPYABLE(PVStructure) }; +epicsShareFunc +std::ostream& operator<<(std::ostream& strm, const PVStructure::Formatter& format); + /** * @brief PVUnion has a single subfield. * diff --git a/testApp/misc/Makefile b/testApp/misc/Makefile index 2ce8143..9e20b7c 100644 --- a/testApp/misc/Makefile +++ b/testApp/misc/Makefile @@ -79,3 +79,7 @@ TESTS += test_reftrack TESTPROD_HOST += testanyscalar testanyscalar_SRCS += testanyscalar.cpp TESTS += testanyscalar + +TESTPROD_HOST += testprinter +testprinter_SRCS += testprinter.cpp +TESTS += testprinter diff --git a/testApp/misc/testprinter.cpp b/testApp/misc/testprinter.cpp new file mode 100644 index 0000000..11ad9e7 --- /dev/null +++ b/testApp/misc/testprinter.cpp @@ -0,0 +1,241 @@ +/* + * Copyright information and license terms for this software can be + * found in the file LICENSE that is included with the distribution + */ + +#include +#include + +#include +#include + +#include +#include + +#include +#include +#include + +#if EPICS_VERSION_INT>=VERSION_INT(3,15,0,1) +# define USE_JSON +#endif + +namespace pvd = epics::pvData; + +typedef std::vector lines_t; + +namespace { + +lines_t lines(const std::string& str) +{ + lines_t ret; + size_t p = 0; + while(true) { + size_t next = str.find_first_of('\n', p); + ret.push_back(str.substr(p, next-p)); // exclude trailing '\n' + if(next==str.npos) + break; + else + p = next+1; + } + return ret; +} + +std::string escape(const std::string& inp) { + size_t N = epicsStrnEscapedFromRawSize(inp.c_str(), inp.size()); + std::vector esc(N+1, '\0'); // include space for trailing nil + epicsStrnEscapedFromRaw(&esc[0], esc.size(), inp.c_str(), inp.size()); + return std::string(&esc[0]); +} + +std::string print(const pvd::PVStructure::Formatter& fmt) +{ + std::ostringstream strm; + strm< actual +::detail::testPassx +testDiff(const std::string& expect, const std::string& actual, const std::string& msg = std::string()) +{ + bool match = expect==actual; + ::detail::testPassx ret(match); + ret<createFieldBuilder() + ->setId("epics:nt/NTScalar:1.0") + ->add("value", pvd::pvInt) + ->add("alarm", pvd::getStandardField()->alarm()) + ->add("timeStamp", pvd::getStandardField()->timeStamp()) + ->createStructure()); + +void showNTScalarNumeric() +{ + testDiag("%s", CURRENT_FUNCTION); + pvd::PVStructurePtr input(pvd::getPVDataCreate()->createPVStructure(scalarNumeric)); + input->getSubFieldT("value")->putFrom(-42); + + testDiff(" -42 \n", print(input->stream())); + + input->getSubFieldT("alarm.severity")->putFrom(1); + input->getSubFieldT("alarm.status")->putFrom(1); + input->getSubFieldT("alarm.message")->put("FOO"); + + testDiff(" -42 MINOR DEVICE FOO \n", print(input->stream())); +} + +static const pvd::StructureConstPtr scalarString(pvd::getFieldCreate()->createFieldBuilder() + ->setId("epics:nt/NTScalar:1.0") + ->add("value", pvd::pvString) + ->add("alarm", pvd::getStandardField()->alarm()) + ->add("timeStamp", pvd::getStandardField()->timeStamp()) + ->createStructure()); + +void showNTScalarString() +{ + testDiag("%s", CURRENT_FUNCTION); + pvd::PVStructurePtr input(pvd::getPVDataCreate()->createPVStructure(scalarString)); + testDiff(" \n", print(input->stream())); + + input->getSubFieldT("value")->put("bar"); + + testDiff(" bar \n", print(input->stream())); + + input->getSubFieldT("alarm.severity")->putFrom(1); + input->getSubFieldT("alarm.status")->putFrom(1); + input->getSubFieldT("alarm.message")->put("FOO"); + + testDiff(" bar MINOR DEVICE FOO \n", print(input->stream())); +} + +static const pvd::StructureConstPtr everything(pvd::getFieldCreate()->createFieldBuilder() + ->setId("omg") + ->add("scalar", pvd::pvString) + ->addArray("scalarArray", pvd::pvString) + ->addNestedStructure("below") + ->add("A", pvd::pvInt) + ->addNestedUnion("select") + ->add("one", pvd::pvInt) + ->add("two", pvd::pvInt) + ->endNested() + ->addNestedUnionArray("arrselect") + ->add("foo", pvd::pvInt) + ->add("bar", pvd::pvInt) + ->endNested() + ->addNestedStructureArray("astruct") + ->add("red", pvd::pvInt) + ->add("blue", pvd::pvInt) + ->endNested() + ->endNested() + ->add("anything", pvd::getFieldCreate()->createVariantUnion()) + ->add("arrayany", pvd::getFieldCreate()->createVariantUnionArray()) + ->createStructure()); + +void testRaw() +{ + testDiag("%s", CURRENT_FUNCTION); + pvd::PVStructurePtr input(pvd::getPVDataCreate()->createPVStructure(everything)); + + testDiff("omg \n" + " string scalar \n" + " string[] scalarArray []\n" + " structure below\n" + " int A 0\n" + " union select\n" + " (none)\n" + " union[] arrselect\n" + " structure[] astruct\n" + " any anything\n" + " (none)\n" + " any[] arrayany\n" + , print(input->stream())); + + testDiff("omg \n" + " string scalar \n" + " structure below\n" + " int A 0\n" + , print(input->stream().show(pvd::BitSet().set(1).set(4)))); + + testDiff("omg \n" + "\033[1m string scalar \n" + "\033[0m\033[1m string[] scalarArray []\n" + "\033[0m structure below\n" + "\033[1m int A 0\n" + "\033[0m union select\n" + " (none)\n" + " union[] arrselect\n" + " structure[] astruct\n" + " any anything\n" + " (none)\n" + " any[] arrayany\n" + , print(input->stream() + .mode(pvd::PVStructure::Formatter::ANSI) // force use of escapes + .highlight(pvd::BitSet().set(1).set(2).set(4)) + )); +} + +} // namespace + +MAIN(testprinter) +{ + testPlan(0); + showNTScalarNumeric(); + showNTScalarString(); + testRaw(); + return testDone(); +}