From 967539c4e18151f266a982b2cef89a49b48a915a Mon Sep 17 00:00:00 2001 From: Dirk Zimoch Date: Mon, 2 Mar 2020 09:10:28 +0100 Subject: [PATCH] allow commas inside matching pairs of parentheses in protocol parameters --- docs/protocol.html | 61 +++++++++++++++++++-- src/StreamCore.cc | 21 +++++-- src/StreamEpics.cc | 4 +- streamApp/tests/testParenthesesInParameters | 33 ++++++++++- 4 files changed, 105 insertions(+), 14 deletions(-) diff --git a/docs/protocol.html b/docs/protocol.html index 2d55710..01ada2e 100644 --- a/docs/protocol.html +++ b/docs/protocol.html @@ -482,12 +482,63 @@ To make this easier, protocol arguments can be used: move { out "\$1 GOTO %d"; }

-Now, the protocol can be references in the OUT link +Now the same protocol can be used in the OUT link of three different records as move(X), move(Y) and move(Z). -Up to 9 parameters, referenced as $1 ... $9 -can be specified in parentheses, separated by comma. -The variable $0 is replaced by the name of the protocol. +

+

+Up to 9 parameters can be specified in parentheses, separated by comma. +In the protocol, they are referenced as $1 ... +$9 outside quotes or \$1 ... \$9 +within quotes. The parameter $0 resolves to the protocol name. +

+
+

+To make links more readable, one space is allowed before and after each comma +and the enclosing parentheses. This space is not part of the parameter string. +Any additional space is part of the parameter. +

+

+If a parameter contains matching pairs of parentheses, these and all commas +inside are part of the parameter. +This allows to pass parameter strings like (1,2) easily without +much escaping. +

+

+Unmatched parentheses must be escaped with double backslash \\ +as well as must be commas outside pairs of parentheses. +Double backslash is necessary because one backslash is already consumed by +the db file parser. +To pass a literal backslash in a parameter string use 4 backslashes +\\\\. +

+
+

+Note that macros can be used in parameters. That makes it possible to +pass part of the record name to the protocol to be used in +redirections. +

+ +

Example:

+
+record(ai, "$(PREFIX)recX5") {
+    field(DTYP, "stream")
+    field(INP,  "@$(PROTOCOLFILE) read(5, X\\,Y $(PREFIX)) $(PORT)")
+}
+record(ai, "$(PREFIX)recY5") {}
+
+read { out 0x8$1 "READ \$2"; in "%f,%(\$3recY\$1)f" }
+
+

+The protocol resolves to: +

+
+read { out 0x85 "READ X,Y"; in "%f,%($(PREFIX)recY5)f" }
+
+

+Here $(PREFIX) is replaced with its macro value. +But be aware that the macro is actually replaced before the link is parsed so +that macro values containing comma or parentheses may have unintended effects.

@@ -565,7 +616,7 @@ There is a fixed set of exception handler names starting with -

Example:

+

Example:

 setPosition {
     out "POS %f";
diff --git a/src/StreamCore.cc b/src/StreamCore.cc
index 4101e4c..3c1ee68 100644
--- a/src/StreamCore.cc
+++ b/src/StreamCore.cc
@@ -189,14 +189,26 @@ parse(const char* filename, const char* _protocolname)
     ssize_t i = protocolname.find('(');
     if (i >= 0)
     {
-        while (i >= 0)
+        while (i < (ssize_t)protocolname.length())
         {
             if (protocolname[i-1] == ' ')
                 protocolname.remove(--i, 1); // remove trailing space
-            protocolname[i] = '\0'; // replace '(' and ',' with '\0'
+            protocolname[i] = '\0'; // replace initial '(' and separating ',' with '\0'
             if (protocolname[i+1] == ' ')
                 protocolname.remove(i+1, 1); // remove leading space
-            i = protocolname.find(',', i+1);
+            int brackets = 0;
+            do {
+                i++;
+                i += strcspn(protocolname(i), ",()\\");
+                char c = protocolname[i];
+                if (c == '(') brackets++;
+                else if (c == ')') brackets--;
+                else if (c == ',' && brackets <= 0) break;
+                else if (c == '\\') {
+                    if (protocolname[i+1] == '\\') i++; // keep '\\'
+                    else protocolname.remove(i, 1); // else skip over next char
+                }
+            } while (i < (ssize_t)protocolname.length());
         }
         // should have closing parentheses
         if (protocolname[-1] != ')')
@@ -206,9 +218,8 @@ parse(const char* filename, const char* _protocolname)
         }
         protocolname.truncate(-1); // remove ')'
         if (protocolname[-1] == ' ')
-        {
             protocolname.truncate(-1); // remove trailing space
-        }
+        debug("StreamCore::parse \"%s\" -> \"%s\"\n", _protocolname, protocolname.expand()());
     }
     StreamProtocolParser::Protocol* protocol;
     protocol = StreamProtocolParser::getProtocol(filename, protocolname);
diff --git a/src/StreamEpics.cc b/src/StreamEpics.cc
index 55b4da4..c6799de 100644
--- a/src/StreamEpics.cc
+++ b/src/StreamEpics.cc
@@ -764,7 +764,7 @@ initRecord(char* linkstring /* modifiable copy */)
     }
 
     // attach to bus interface
-    debug("Stream::initRecord %s: attachBus(%s, %ld, \"%s\")\n",
+    debug("Stream::initRecord %s: attachBus(\"%s\", %ld, \"%s\")\n",
         name(), busname, addr, busparam);
     if (!attachBus(busname, addr, busparam))
     {
@@ -774,7 +774,7 @@ initRecord(char* linkstring /* modifiable copy */)
     }
 
     // parse protocol file
-    debug("Stream::initRecord %s: parse(%s, %s)\n",
+    debug("Stream::initRecord %s: parse(\"%s\", \"%s\")\n",
         name(), filename, protocol);
     if (!parse(filename, protocol))
     {
diff --git a/streamApp/tests/testParenthesesInParameters b/streamApp/tests/testParenthesesInParameters
index a4c2b4b..de98c23 100755
--- a/streamApp/tests/testParenthesesInParameters
+++ b/streamApp/tests/testParenthesesInParameters
@@ -10,7 +10,28 @@ set records {
     record(ao, "DZ:test1")
     {
         field (DTYP, "stream")
-        field (OUT, "@test.proto test1(ARG(10)) device")
+        field (OUT, "@test.proto test1(ARG(10),20,30) device")
+    }
+
+    record(ao, "DZ:test2")
+    {
+        field (DTYP, "stream")
+        field (OUT, "@test.proto test1 (ARG(10,20), 30) device")
+    }
+    record(ao, "DZ:test3")
+    {
+        field (DTYP, "stream")
+        field (OUT, "@test.proto test1 ( ARG ( 10 , 20 ) , 30 )  device")
+    }
+    record(ao, "DZ:test4")
+    {
+        field (DTYP, "stream")
+        field (OUT, "@test.proto test1(      ARG    \\(  10    ,   20   ,   30    )    device")
+    }
+    record(ao, "DZ:test5")
+    {
+        field (DTYP, "stream")
+        field (OUT,  "@test.proto test1(\\ ARG\\,\\\\(10,20)\\,30) device")
     }
 }
 
@@ -28,5 +49,13 @@ startioc
 
 put DZ:test1 "1"
 assure "VAL:ARG(10):1\n"
+put DZ:test2 "1"
+assure "VAL:ARG(10,20):1\n"
+put DZ:test3 "1"
+assure "VAL:ARG ( 10 , 20 ):1\n"
+put DZ:test4 "1"
+assure "VAL:     ARG    (  10   :1\n"
+put DZ:test5 "1"
+assure "VAL: ARG,\\(10,20),30:1\n"
 
-finish
+#finish