PDMS-AVEVA-E3D二次开发教程(04):PML 基础语法与错误处理——变量、函数、控制逻辑与“延迟报错” 版本声明全系列统一口径本篇基线为AVEVA E3D Design 3.1。变量命名限制、文件类型口径来自官方培训材料控制逻辑与函数示例来自官方文档与培训材料的原文摘录错误处理机制出自官方Errors and Error Handling相关章节https://help.aveva.com/AVEVA_Everything3D/1.1/SOFTCG/SOFTCG11.11.2.html。凡本系列自行组合构造、未在任何本机环境跑通的代码均标注**“按官方已发表的写法组合构造需在本地 E3D Design 环境验证”**凡官方材料未给出的具体语法如handle的匹配子句正文只给出结构示意并明确标注待核实不臆造关键字。官方把 PML 交互入口称为Command Window命令窗口Tools 标签 → Display 组 → Commands。一句话结论PML 的语法本身不复杂真正的门槛是两条隐藏规则——变量名有硬限制≤16 字符、不以数字开头、不含.以及错误不会在出错的那一行报出来报不报取决于下一行输入不理解第二条你会把脚本默默跑错当成环境有问题。〇认知问题!var和!!var到底有什么区别我什么时候必须用全局变量为什么我的变量名被系统拒绝或者行为诡异变量名有什么硬限制函数和宏的区别我知道但函数写在哪.pmlfnc是必须的吗IF怎么写才不报错DO循环的BY可以是负数吗BREAK和SKIP有什么现成写法after、trim、upcase、replace、split这些方法是哪来的为什么可以一路点下去我的脚本里明明有一行错的为什么它没有立刻报错甚至有时干脆不报一机制解析1.1 变量的两个层级与三条命名硬限制PML 的变量分两级局部变量!var作用范围限于当前宏/函数/会话脚本的执行范围。全局变量!!var作用范围是整个会话。官方文档对全局变量有一句描述特别值得记住——在 PML2 中窗体本身就是一种用全局变量表示的对象如!!EntryForm。这就是窗体名不能与既有对象/全局变量/窗体重名这条规则的技术根源。三条命名硬限制官方培训材料口径变量名最长 16 个字符不能以数字开头不能包含.。第 3 条尤其容易踩因为点号在 PML2 里是成员访问符!this.formTitle、!!EntryForm.TextField.val。如果变量名里允许点号解析器就无法区分一个带点的变量和某对象的成员。还有一个容易被忽略的性质PML 变量是动态类型的。你没有声明一个 STRING 变量这种写法变量的类型由赋给它的值决定。这一点对写函数特别重要——函数的参数可以带类型官方示例!len is REAL但函数体内的局部变量仍是动态的。补充一条版本口径PML1 的 expressions package 在数据库手册DBRM中单独成章官方原文说一般来说这些设施与 PML 2 兼容https://help.aveva.com/AVEVA_Everything3D/1.1/DBRM/DBRM9.10.01.html。所以查表达式与运算符时DBRM 也是合法入口不必只在 PMLCG 里翻。1.2 函数与.pmlfnc函数与宏最关键的区别是有签名。官方培训材料的函数示例原样摘录define function !!area(!len is REAL, !wid is REAL) is REAL !area !len * !wid return !area endfunction !area !!area(12, 200)拆开看它的结构define function !!area(...)函数名写在全局变量位置上!!area这是 PML 里让函数在整个会话可见的常规写法。(!len is REAL, !wid is REAL)参数带类型——这是宏完全没有的能力。会做类型检查是函数优于宏的第一个理由。is REAL在参数列表之后的这一句声明返回类型。return !area显式返回。endfunction结束。!area !!area(12, 200)调用侧把结果赋给同名局部变量。文件放哪.pmlfnc。按官方培训材料口径PML 的文件类型是.pmlfnc函数、.pmlfrm表单、.pmlobj对象它们依赖环境变量PMLLIB来定位。命名惯例建议函数名即文件名——这样别人看到!!area()就知道去哪个文件找。为什么要把可复用逻辑从宏搬到函数三个直接收益类型检查、可被反复调用与组合、可以被其他函数调用形成层次。这也是本系列的推荐实践把可复用的函数拆到.pmlfnc并按函数名命名文件。1.3 控制逻辑IF 与 DO官方给出的控制逻辑示例PML1 风格原样摘录IF (!Type eq BRAN) THEN P CE is Branch. ELSEIF (!OwnType eq BRAN) THEN P CE is Branch member. ELSE P CE is $!Type, Pls select Branch. ENDIF Do !x From 1 To 100 By 1 !Total !Total !x Enddo要点IF (...) THEN…ELSEIF (...) THEN…ELSE…ENDIF结构完整THEN是必需的。注意P CE is $!Type, Pls select Branch.这一行——$!Type是变量注入把变量值拼进输出文本这正是上一篇讲过的注入机制在输出语句上的应用。!Type与!OwnType在官方这段节选里直接出现说明它们是在同一段脚本更早的位置被赋值的Type是元素类型、OwnType通常表示所属元素的类型这一类信息。Do … From … To … ByDo !x From 1 To 100 By 1表示!x从 1 到 100、步长 1。注意循环体里!Total !Total !x是累加这行代码本身没有初始化——官方节选没有包含初始化行实际写代码时你必须在循环前把!Total置为 0。BREAK与SKIP官方明确二者支持Break if(...)/Skip If(...)这种带条件的简写形式即把判断 跳出/跳过合并成一行。1.4 字符串方法链为什么可以一路点下去官方给出的字符串链示例含官方勘误原样摘录!newline !line.after(hello).trim().upcase() !newline.replace(HOW,WHERE)机制解读after(hello)取hello之后的部分.trim()去掉首尾空白.upcase()转大写.replace(HOW,WHERE)替换子串。为什么可以一路点下去因为这些方法返回的还是字符串于是下一个方法自然可以继续挂在后面。这就是链式调用它让清洗一行文本从五行代码压缩成一行。官方材料在这里还专门给了一处勘误说明这些方法名的记忆容易出错——所以用之前请以本机 SCRM 的对象/方法总参考为准不要凭印象写方法名。社区来源的真实帖里还能看到另两个方法的用法社区来源非官方结论!c !!ce.name.replace(copy-of-,).split(-)即replace(,)做删除子串把第二个参数留空split(-)把字符串按分隔符切成数组之后可以用下标访问如!c[1]、!c[2]。PMLCG 目录里确实有独立的Arrays章数组下标写法以该章为准。字符串清洗在二次开发里的地位所有把值拼进命令字符串的场景$!注入、生成名称、拼接路径都依赖清洗。注入前不清洗就等于允许变量内容改变命令的分词——这是模板宏最典型的事故来源。1.5 错误处理PML 的延迟报错本篇核心先记住官方给出的机制原文含义PML 宏/函数中的错误不会立即报错而取决于下一行输入。如果下一条输入是与该错误匹配的handle命令则错误不输出转而执行匹配的 handle 块elsehandle用于 handle 块不匹配时。这条机制反直觉的地方在于错误的报与不报是由错误之后的代码决定的而不是由错误本身决定的。由此产生三个非常实际的后果脚本悄悄跑错是常态不是异常。如果后面接着的正好是一个能匹配的handle错误就被接住了你不会在 Command Window 里看到任何红字。隔行报错的错觉。你看到报错出现在某一行不等于错误发生在那一行——这是排查时最容易走偏的地方。错误处理不是加个 try那么简单它与你后续代码的排列顺序强耦合。这也解释了为什么 PMLCG 里Errors and Error Handling是一个独立章节而不是附在别的章里。关于handle的匹配子句到底怎么写匹配某一类错误匹配错误码——本篇依据的材料没有给出确切语法。因此下面 2.4 只给出结构示意把所有未核实的部分用占位符标出并明确告诉你该去哪一章补齐。这是本系列的一条硬纪律宁可在教程里留一个显式的待核实标记也不给读者一段看起来能跑、其实靠猜的语法。二完整代码与逐行剖析2.1 函数 循环累加官方示例 必要补全-- 文件sumdemo.pmlfnc -- 来源函数结构取自官方培训材料示例!!area循环结构取自官方控制逻辑示例 -- 说明官方节选未包含累加变量的初始化行本系列补上 !Total 0 —— 这是常规写法需在本地验证 define function !!sumTo(!n is REAL) is REAL !Total 0 -- 累加器必须先初始化否则结果不可预期 Do !i From 1 To !n By 1 !Total !Total !i Enddo return !Total endfunction -- 在 Command Window 里调用 -- !s !!sumTo(100) -- q var !s逐行剖析define function !!sumTo(!n is REAL) is REAL函数名sumTo6 个字符远低于 16 字符上限参数!n声明为REAL返回类型声明为REAL。把上限记在心上这条硬限制会直接限制你的命名风格。!Total 0这一行是官方节选里没有、但你必须写的。PML 变量是动态类型的未初始化的变量参与算术运算不会给你变量未定义的友好提示所以初始化必须靠纪律。Do !i From 1 To !n By 1把官方示例里的To 100改成To !n让循环上界由参数决定——这一步就把宏变成了函数。Enddo循环结束。注意Do/Enddo的大小写在不同材料里不一致Do/Enddo与DO/ENDDO都出现过本篇照录原文不对大小写规则做推断请在 Command Window 里确认后再统一风格。return !Total/endfunction返回并结束。2.2 字符串清洗函数按官方已发表的写法组合构造需在本地 E3D Design 环境验证-- 文件normalizeCode.pmlfnc -- 说明方法名 from、trim、upcase、replace 均在官方字符串链示例中出现过 -- 本函数把它们的组合固化成一个可复用函数属按官方已发表的写法组合构造需本地验证 define function !!normalizeCode(!raw is STRING) is STRING !s !raw.trim().upcase() -- 去首尾空白并转大写 !s !s.replace( ,-) -- 把空格换成连字符replace 的第二参数为空时删除子串 return !s endfunction -- 用法示例在 Command Window 里 -- !code !!normalizeCode( pipe 01 ) -- q var !code逐行剖析!s !raw.trim().upcase()把两个方法串起来。之所以能串是因为trim()返回字符串upcase()才能继续挂上去。!s !s.replace( ,-)replace的第一个参数是被替换内容、第二个参数是替换成什么。社区来源的帖子里用第二个参数留空实现了删除子串replace(copy-of-,)。return !s/endfunction与前面的函数结构一致。这个函数真正的用途在把值注入命令字符串之前做一道过滤。既然$!注入是行级文本替换那么变量里有空格就会改变命令分词——清洗函数就是那道闸门。2.3 控制逻辑IF 与 BREAK/SKIP官方示例 按官方支持形式构造-- 文件classifyCe.mac -- 来源IF 结构为官方控制逻辑示例原样BREAK/SKIP 的带条件形式取自 -- 官方Break if(...) / Skip If(...)的支持口径具体写法需在本地验证 -- 第一步按类型分流官方示例原样复刻 IF (!Type eq BRAN) THEN P CE is Branch. ELSEIF (!OwnType eq BRAN) THEN P CE is Branch member. ELSE P CE is $!Type, Pls select Branch. ENDIF -- 第二步循环里跳过与提前结束按官方支持的带条件形式构造 Do !i From 1 To 100 By 1 Skip If (!i eq 10) -- 跳过本次迭代 Break If (!i gt 20) -- 满足条件即跳出循环 P processing item $!i Enddo逐行剖析IF (!Type eq BRAN) THENPML 的条件表达式放在括号里比较运算符用eq相等。BRAN是官方示例里出现的类型标识本行照录。ELSEIF (!OwnType eq BRAN) THEN第二分支。OwnType用于当前元素本身不是 Branch、但它挂在 Branch 下这类判断。P CE is $!Type, Pls select Branch.把!Type的值注入输出文本。这也是输出即日志的基本手段。Skip If (!i eq 10)跳过!i 10这次迭代。注意Skip If的大小写与括号形式取自官方支持Skip If(...)形式的表述。Break If (!i gt 20)满足即跳出。gt是大于。P processing item $!i输出当前轮次用于确认循环到底跑到哪一轮停的——循环类脚本一定要有这种进度输出否则跑了一半失败你无法定位。一个语义提醒Skip与Break的精确边界行为例如Skip If是判断在前还是执行在前请以 PMLCG 的Control Logic章为准本篇只给出它们支持带条件写法这一确定事实。2.4 错误处理结构示意关键字待本机核实-- 错误处理结构示意伪代码 -- 本篇依据的官方材料给出两条确定事实 -- (1) 错误不会立即报错报不报取决于下一行输入 -- (2) 若下一条是与错误匹配的 handle 命令则错误不输出 -- 转而执行匹配的 handle 块elsehandle 用于 handle 块不匹配时。 -- handle 的匹配子句写法、以及块如何结束本篇材料未给出确切语法 -- 因此下面保留 handle / elsehandle 两个已出现的关键字其余一律用 ... 占位。 -- 请把 ... 替换成本机 PMLCG《Errors and Error Handling》章给出的确切写法。 会出错的命令 -- 例如一条参数不合法或对象不存在的命令 handle 匹配子句 -- ← 匹配子句语法待核实 P handle block executed -- 错误被接住不输出错误转而执行这里 elsehandle -- ← handle 块不匹配时走这里 P no handle matched 块结束写法 -- ← 结束关键字待核实怎么用这张示意把它当作阅读地图而不是可运行代码。去本机 PMLCG 的Errors and Error Handling章把三个...位置逐一填实然后在 Command Window 里用一条故意写错的命令验证错误被接住的效果。验证方法按官方描述的机制构造的演示需在本地 E3D Design 环境验证-- 演示一没有 handle —— 错误会在后续输入时浮出 THISISNOTACOMMAND 1 2 3 P after the bad command -- 演示二紧跟一个能匹配的 handle —— 错误不输出改走 handle 块 THISISNOTACOMMAND 1 2 3 handle 匹配子句 P got it 块结束写法一个必须养成的习惯在写任何批量脚本时先故意制造一次错误确认你的错误处理真的接住了再放它去跑生产数据。因为延迟报错的机制意味着没看到报错完全不能推出没有错误。三常见报错与排查症状一定义了变量但赋值/使用时报错或结果错乱。现象命令执行失败报错指向某一行含变名的语句。原因变量名违反硬限制——超过 16 字符、以数字开头、或含.。含点号的情况尤其隐蔽因为点号在 PML2 里是成员访问符。处置用短名建议 ≤12 字符留余量首字符用字母去掉所有点号全局变量还要额外检查是否与窗体重名在 PML2 里窗体就是全局变量。症状二脚本跑完了但结果不对而且 Command Window 里没有报错。现象没有任何红字数据却是错的。原因延迟报错 handle接住了错误。官方机制说得很清楚错误不会立即报错若下一条是匹配的handle错误不输出。处置不要以没报错当作成功的证据。给关键步骤加结果校验例如读取并断言元素的关键属性、统计命中条数批处理脚本必须在结尾输出汇总。症状三累加结果每次都不一样或者明显偏小/偏大。现象循环求和结果不稳定。原因累加器没有初始化。PML 变量动态类型未初始化就参与运算不会给你友好提示。处置循环前显式!Total 0在循环内打印关键轮次如每 10 次打一条。症状四字符串方法链报方法不存在。现象.after().trim().upcase()中的某一段报错。原因方法名记错。官方材料本身对这类方法名专门给过勘误说明这是高频错误点。处置到 SCRM 的Methods Available to All Objects/Object Type Details两节去核对确切方法名与参数组合链时逐段验证——先只写第一个方法并打印结果再加第二个。四动手练习练习 115 分钟在 Command Window 里故意定义三类非法变量名超 16 字符、以数字开头、含.各记录一条报错原文。合格判定三条都有可复现的报错且你能用一句话说清为什么点号特别容易踩提示PML2 里点号是成员访问符。练习 220 分钟把 2.1 的!!sumTo放进PMLLIB下的sumdemo.pmlfnc调用!!sumTo(100)。合格判定把结果与1 到 100 之和的期望值对齐然后再调用一次!!sumTo(1000)确认两次调用互不干扰这就是累加器初始化在起作用。练习 320 分钟用 2.2 的!!normalizeCode清洗三个含空格与大小写混合的字符串例如 pipe 01 。合格判定三个输出都满足无首尾空格、全部大写、内部空格已转连字符随后再验证一次输入含单引号的情况并记录其行为原样记录即可不要求你推断规则。练习 425 分钟本篇最重要填实 2.4 的三个...占位写出一段能真正接住错误的代码。合格判定故意写一条非法命令后Command Window不输出错误而是打印出你的 handle 块内容再删掉 handle 块重复一次确认错误会浮出。两条现象都能复现才算真正理解延迟报错。五小结与下一篇预告本篇的四条要点变量分!var/!!var两级命名有硬限制≤16 字符、不以数字开头、不含.函数有类型化签名应与宏分工并放进PMLLIB覆盖的.pmlfnc文件控制逻辑是IF/ELSEIF/ENDIF与Do … From … To … ByBREAK/SKIP支持带条件形式字符串方法可链式调用错误处理的核心是延迟报错——报不报由下一行输入决定匹配的handle会把错误吞掉elsehandle负责兜底。下一篇《PML2 对象与自省》把视角从过程抬到对象define object定义带类型的成员member .x is DBREF / STRING / POSITION、define method定义方法、!this的语义、以及methods()与attributes()两个自省方法——并回答一个实用问题什么时候该用 PML2 对象什么时候继续用 PML1 更划算。文末 FAQQ1!var和!!var到底有什么区别我什么时候必须用全局变量!var是局部变量作用范围限于当前宏/函数/脚本的执行范围!!var是全局变量作用范围是整个会话。必须用全局变量的典型场景有三类一是跨宏传递状态一个宏产生结果、另一个宏消费二是在 PML2 里表示对象——官方明确指出窗体就是用全局变量表示的对象如!!EntryForm所以窗体名必须是全局变量名三是需要让 Command Window 里手工输入的命令看见你脚本里准备好的开关或缓存。注意全局变量的代价它占用了全局命名空间会与窗体、其他全局变量互相覆盖。Q2为什么我的变量名被系统拒绝或者行为诡异变量名有什么硬限制按官方培训材料口径有三条硬限制最长 16 个字符、不能以数字开头、不能包含.。第三条约等于 PML2 的语法要求——点号是成员访问符!this.formTitle如果变量名里允许点号解析器就无法区分带点的变量与某对象的成员。另外在 PML2 场景下要额外小心窗体本身就是全局变量所以窗体名不能与既有对象、全局变量或窗体重名否则会互相覆盖。建议命名控制在 12 字符以内全用字母与下划线。Q3函数和宏的区别我知道但函数写在哪.pmlfnc是必须的吗.pmlfnc是官方培训材料口径给出的函数文件类型与.pmlfrm表单、.pmlobj对象并列它们都依赖环境变量PMLLIB来定位。所以严格说函数本身不依赖扩展名但要让环境自动找到它就放进PMLLIB覆盖的目录、用.pmlfnc后缀。推荐命名惯例是函数名即文件名例如函数!!area()放在area.pmlfnc里便于维护。函数相对宏的收益是明确的参数带类型官方示例define function !!area(!len is REAL, !wid is REAL) is REAL、有返回类型、可被反复调用与组合。Q4IF怎么写才不报错DO循环的BY可以是负数吗BREAK和SKIP有什么现成写法IF的官方结构是IF (...) THEN…ELSEIF (...) THEN…ELSE…ENDIFTHEN不可省条件表达式放在括号里比较运算符用eq、gt这类词而不是符号。DO循环的官方结构是Do !x From 1 To 100 By 1…Enddo即起始 到 结束 布长三段齐全。关于BY能否为负本篇依据的材料没有给出说明请在 Command Window 里自行验证后再依赖。BREAK与SKIP的确定事实是官方明确二者支持Break if(...)/Skip If(...)这种带条件形式可以把判断 跳出/跳过合并成一行其精确边界语义请以 PMLCG 的Control Logic章为准。Q5after、trim、upcase、replace、split这些方法是哪来的为什么可以一路点下去它们都是 PML 的字符串方法方法清单可以在 SCRMSoftware Customisation的对象/成员/方法总参考里核对。之所以能一路点下去是因为每个方法返回的仍是字符串下一个方法自然可以继续挂在后面——官方示例!newline !line.after(hello).trim().upcase()就是三段链之后还可以继续.replace(HOW,WHERE)。官方材料在这里专门给过一处勘误说明方法名容易记错所以调用前务必核对 SCRM另外社区来源的帖子里还能看到replace第二参数留空以删除子串以及split(-)按分隔符切分成数组后用!c[1]、!c[2]下标访问的用法数组写法以 PMLCG 的Arrays章为准。Q6我的脚本里明明有一行错的为什么它没有立刻报错甚至有时干脆不报因为这是 PML 的既定机制宏/函数中的错误不会立即报错而取决于下一行输入。如果下一条输入是与该错误匹配的handle命令错误就不输出转而执行匹配的 handle 块elsehandle用于 handle 块不匹配时。所以错误消失其实是被接住了而不是没有发生。这条机制有两个直接推论第一不要把没有红字当作结果正确关键步骤必须加结果校验第二你看到的报错所在行不一定是错误发生的那一行。handle的匹配子句确切语法、以及错误处理块如何结束请到 PMLCG 的Errors and Error Handling章核实后再写——本系列不会给出未经核实的语法。