AEL纯粹是为了描述asterisk拨号方案而专门发明的一种语言。
目前的版本是Steve Murphy写的,是原始版本的翻写版。
这个新的版本是对AEL的扩展,提供了更加灵活的语法,更好的出错信息,并增加了一些缺失的功能。
AEL是4中不同语言(或者说是语法)的合集:
* 第一种,也是最显而易见的就是AEL语法本身。文档的最后我们会提供BNF
* 第二种,就是表达式语法,是类似于括在$[...]中的表达式,是被asterisk分机引擎(asterisk extension engine)正常处理的语法。赋值语句的右边被AEL括在$[...]当中,同样存在于if和while的表达式中。
* 第三种就是值引用语法(variable referencesyntax),被大括号${...}括起来的那一堆东西。它不仅仅是把一个变量的名字放在里面这么简单。可以包含一个函数(function)以及它们的参数,甚至会有一些字符串的操作记法存在其中。
* 最后一种语法是AEL的基础,并且没有被AEL直接使用,它就是分机语言语法(Extension Language Syntax)。这种语言是你在extensions.conf当中看到的那样,AEL将更高层次的AEL语言编译成extensions和priorities,并且通过函数将其传递给asterisk。嵌入在这种语言中的是Application/AGI命令,每一步的application调用,或者priority都可能是有该命令组成的。你可以将其想象成一种AEL编译进去的宏汇编(”macro assembler”)语言。
任何的AEL的程序员要熟悉它的语法,当然还有表达式语法和变量引用语法。
asterisk担当的是服务器的责任。在电话通信中的设备,诸如DAHDI卡,或voip电话,都会指明他们自己的上下文(context),这些上下文应该由他们自己激活。可以看看IAX,SIP,dahdi.conf等配置文件的格式。他们用来帮助描述设备,他们全都会指定上下文,当有人拿起电话,或是从voip话机或是从电话公司来了一通通话,这些上下文都将被激活。
上下文是分机扩展(extensions)的组合。
上下文也可以包含其他的上下文。可以把它想象成为一种运行时的操作的组合,由是包含在上下文A中的分机扩展被添加进了包含A的上下文B中。
一段上下文包含0个或多个分机。也有几个预定义的分机。“s”分机是“开始”分机,当一个设备激活一段上下文的时候,“s”分机将会执行。其他的分机是超时分机“t”,无效应答或是“i”分机,还有“fax”分机。例如,一个正常的通话会激活“s”分机,但是一个传真呼入会到“fax”分机中(如果其存在的话)去执行。(顺便说一句,asterisk可以通过传真机每几秒中发一次的“beep”声音来识别是否是一通传真通话。)
分机包含一些优先级(priorities),他们是单独可以执行的语句。一些语句相当简单,就是一些赋值的语句。而有些比较复杂,如语音信箱应用。优先级按照顺序被执行。
当“s”分机执行完成后,asterisk会等到用户应答结束为止。如果应答符合这段上下文中的分机样式,那么将转到该分机处去执行。通常,应答是用户按键发出的。譬如,和一个桌面话机相关的一段上下文可能没有“s”分机。用户拨号之前,一直会播放拨号音,用户拨号时,则会采集用户输入的数字,找到对应的分机样式,然后开始执行这段分机。这段分机可能会为该用户通过电话线外呼某个号码,然后将两者连接在一起。
分机也可以包含“goto”或是“jump”命令跳转到其他上下文中的分机。
可以把宏看作是拥有未命名分机的一段上下文的结合或是一段子程序(subroutine)。它也有参数。宏可以在一个分机中被调用,在宏执行结束后,再去执行宏调用之后的下一条语句。宏也可以调用其他的宏。他们就像函数一样工作。
应用调用,像“Dial()”或是“Hangup()”又或是“Answer()”都可以被用户用来完成拨号方案的执行。在本篇文章完成的时候,已经有超过145个应用了,并且数量还会随着需求的增加而增长。一些应用提供比较简单功能,而有一些则会提供非常复杂的服务。
但愿,上面的那些对象可以让你在asterisk的环境中作任何你需要作的事!
AEL解析器(res_ael.so)不同于解析extensions.conf(pbx_config.so)的模块。要使用AEL,唯一要做的一件事情就是加载res_ael.so模块。如果在/etc/asterisk/modules.conf中autoload选项设为yes,那么该模块将被自动加载。当模块加载的时候,asterisk会在/etc/asterisk文件夹下面找到extensions.ael文件。如果有需要的话extensions.conf和extensions.ael文件会被同时使用。有些用户可能会想要使用extensions.conf中”general”本分的配置功能。
要重新加在extensions.ael,可以在CLI中输入下面的命令:
*CLI>ael reload
目前,下面的命令可以被使用,但是没有用:
打开AEL上下文调试
*CLI>ael debug contexts
打开AEL宏调试
*CLI>ael debug macros
打开AEL读调试
*CLI>ael debug read
打开AEL记号调试
*CLI>ael debug tokens
关闭AEL调试信息
*CLI>ale no debug
如果你的拨号方案出现了问题,你可以使用下面的方法来调试你的文件:
1.查看/var/log/asterisk中的日志文件。2.asterisk中的”show dialplan”命令。3.一个标准的可执行程序”aelparse”
你可以使用”aelparse”程序来检查你的extensions.ael文件。在将该文件提交给asterisk之前解决掉大部分的错误岂不是一件非常美好的事么?
aelparse在asterisk发行版的utils文件夹下面被编译。它没有被安装,但是你可以将起拷贝到你的PATH路径下面。
Aelparse有下面两个参数:
* -d 使用当前的文件夹作为当前的配置文件夹。Aelparse将去找到当前文件夹下面的extensions.ael文件以及包含在该文件中的当前文件夹下的所有ael文件。
* -n 只显示出错和告警信息。
现在的语法和风格是比较自由的。大括号不必要同在其之前的关键字放在同一行。每一个语句也可以分开多行放置,只要记号(token)不被拆开就行。多个语句也可以放在同一行中。
譬如,你可以这样来写,
* if(${x}=1) { NoOp(hello!); gotos,3; } else { NoOp(Goodbye!); goto s,12; }
也可以象这样来写:
* if(${x}=1)
{
NoOp(hello!);
goto s,3;
}
else
{
NoOp(Goodbye!);
goto s,12;
}
或者:
if(${x}=1) {
NoOp(hello!);
goto s,3;
} else {
NoOp(Goodbye!);
goto s,12;
}
又或者:
if (${x}=1) {
NoOp(hello!); goto s,3;
} else {
NoOp(Goodbye!); goto s,12;
}
AEL的关键字是大小写敏感的。如果应用(application)的名字和关键字重叠了,那么可能是有原因的,你应该考虑用AEL语句来替代该应用调用(application call)。如果你不希望这么做,那么你仍然可以通过在它的名字中加入大写字母来使用该应用。在AEL中,应用的名字是大小写无关的。
下面是AEL中的一些关键字:
abstract
context
macro
globals
ignorepat
switch
if
ifTime
else
random
goto
jump
local
return
break
continue
regexten
hint
for
while
case
pattern
default NOTE: the"default" keyword can be used as a context name, for those who wouldlike to do so.
catch
switches
eswitches
includes
AEL首先将extensions.ael文件解析到内存结构中。整个文件的内容被一棵pval结构的树所替代。这棵树然后被语义解析流程处理,之后又被编译处理,再之后从内存中被释放。
可以写一个程序来构建一棵pval结构的树,还可以提供打印的函数将数据转存为文件,或者可以将数据整合进asterisk的拨号方案中。这种设计的模块化为开发者简化产生拨号方案数据提供了机会。
“//” 注释掉整行
注释符会被词法扫描器移出,但是在应用和表达式中的“//”不会被辨认出来。最安全的放置注释符的地方是在”;”后或在空行中
最新的语法,可以像C格式的注释一样
/* this is a
multiline comment that
makes little or no sense!
*/
AEL中的上下文和extensions.conf中一样,都是分机(extensions)的集合。
context default {
}
上下文也可以被声明为“abstract”,在这种情况下,这种声明表现了作者的意图。这种上下文只用来被包含在另一个上下文中,不会单独使用(译者注:单独使用也是可以的)。目前这个关键字被用来防止“goto”语句被检查。
abstract context longdist {
_1NXXNXXXXXX => NoOp(generic long distance dialing actions in the US);
}
要在上下文中指定分机,那么要使用下面的语法。如果在分机中不止一个应用被调用,那么可以按照顺序将它们列在一个块(block)中。
context default {
1234 => Playback(tt-monkeys);
8000 => {
NoOp(one);
NoOp(two);
NoOp(three);
};
_5XXX => NoOp(it's a pattern!);
}
有两条可供选择的功能被添加进了AEL语法中,一条是描述,一条就是关键字匹配(本条功能会将该优先级强制变为2.)
你可以使用‘/’和CID数字结合的方式来匹配CID,见下面的例子:
context default {
regexten _5XXX => NoOp(it's a pattern!);
}
context default {
hint(Sip/1) _5XXX => NoOp(it's a pattern!);
}
context default {
regexten hint(Sip/1) _5XXX => NoOp(it's a pattern!);
}
如果两者要同时存在,那么必须将后者放在前者的前面。
CID匹配和extensions.conf一样。’/’后面的数字会匹配呼叫着的ID。
context zoombo
{
819/7079953345 => { NoOp(hello, 3345); }
}
上例中,如果呼叫着的ID是7079953345,并且拨叫的号码是819,那么将进入这个分机中进行处理。
上下文可以被包含在另外的上下文中。所有被包含的上下文要在一个单独的block中列出。
context default {
includes {
local;
longdistance;
international;
}
}
也可以指定限制的时间,就像extensions.conf文件格式一样。
context default {
includes {
local;
longdistance|16:00-23:59|mon-fri|*|*;
international;
}
}
你可以使用#include+“文件路径”的方式来包含其他的文件。
#include "/etc/asterisk/testfor.ael"
#include一个有意思的特点就是你可以在ael文件中的任何位置使用。甚至于在宏,上下文,分机中都可以包含其他文件。#include不必写在一行的开始处。被包含文件可以包含其他文件,可以达到50级。如果你提供的路径是相对路径,那么解析器会在配置文件的目录中去寻找包含的文件。(通常是/etc/asterisk)
Switch在上下文中被列在它们自己的block当中。使用它们的原因可以在asterisk-dual servers, 以及asterisk 配置文件extensions.conf中找到。
context default {
switches {
DUNDi/e164;
IAX2/box5;
};
eswitches {
IAX2/context@${CURSERVER};
}
}
Ignorepat可以用来告诉通道驱动在收到匹配的样式之前不停止拨号音。最常见的例子是‘9’。一般放在最前面,放在后面可能会出问题。
context outgoing {
ignorepat => 9;
}
Asterisk中的变量没有类型,所以如果要定义一个变量,必须要指定它的值先。
全局变量有它们自己的块(block)
globals {
CONSOLE=Console/dsp;
TRUNK=DAHDI/g2;
}
变量也可以在分机中被定义。
context foo {
555 => {
x=5;
y=blah;
divexample=10/2
NoOp(x is ${x} and y is ${y} !);
}
}
注意:AEL可以使用$[]来将赋值语句的右边的部分括起来以使用表达式。如果你不想这么干,那么可以通过使用Set()应用括在右边部分。可以查看README的variables部分来了解表达式$[]的要求和行为。
注意:下面的这些情况请使用$[]表达式:while();if();for(x;y;z)中的y表达式;赋值语句的右半部份,a=b -> Set(a=$[b])
向拨号方案函数写入数值可以同赋值变量相当对待。
context blah {
s => {
CALLERID(name)=ChickenMan;
NoOp(My name is ${CALLERID(name)} !);
}
}
你也可以像下面这样在一个宏中声明变量:
Macro myroutine(firstarg, secondarg)
{
Myvar=1;
NoOp(Myvar is set to ${myvar});
}
1.2和1.4的asterisk中,所有的变量都是通道变量(CHANNEL variables),包括函数的参数和相关的ARG1,ARG2还有其他的一些变量等。
而在1.6以及更高的版本中,对于一个宏调用,我们将变量都变成了本地变量。他们不会影响和他们同名的其他通道变量。这个包括ARG1,ARG2等等变量。
用户可以使用关键字local来声明他们自己的本地变量。
Macro myroutine(firstarg, secondarg)
{
local Myvar=1;
NoOp(Myvar is set to ${Myvar}, and firstarg is ${firstarg}, and secondarg is ${secondarg});
}
在上面的例子中,Myvar,firstarg,和secondarg都是本地变量,对于调用的代码(可能是一个分机,也可能是其他的宏)他们是不可见的
如果你需要在Set()应用中定义本地变量,你可以这样做:
Macro myroutine(firstarg, secondarg)
{
Set(LOCAL(Myvar)=1);
NoOp(Myvar is set to ${Myvar}, and firstarg is ${firstarg}, and secondarg is ${secondarg});
}
AEL实现了‘for’和‘while’的循环。
context loops {
1 => {
for (x=0; ${x} < 3; x=${x} + 1) {
Verbose(x is ${x} !);
}
}
2 => {
y=10;
while (${y} >= 0) {
Verbose(y is ${y} !);
y=${y}-1;
}
}
}
注意:条件表达式(上面的”${y} >= 0”)在$[]中括着,所以它的值可以被计算出来。
注意:for循环表达式(上面的”$x < 3”)在$[]中括着,所以它的值可以被计算出来。
AEL支持if和switch声明,并增加了ifTime和random。不像之前的AEL,不需要在if(),random()和ifTime()条件语句添加大括号。也允许有else语句。
context conditional {
_8XXX => {
Dial(SIP/${EXTEN});
if ("${DIALSTATUS}" = "BUSY")
{
NoOp(yessir);
Voicemail(${EXTEN},b);
}
else
Voicemail(${EXTEN},u);
ifTime (14:00-25:00,sat-sun,*,*)
Voicemail(${EXTEN},b);
else
{
Voicemail(${EXTEN},u);
NoOp(hi, there!);
}
random(51) NoOp(This should appear 51% of the time);
random( 60 )
{
NoOp( This should appear 60% of the time );
}
else
{
random(75)
{
NoOp( This should appear 30% of the time! );
}
else
{
NoOp( This should appear 10% of the time! );
}
}
}
_777X => {
switch (${EXTEN}) {
case 7771:
NoOp(You called 7771!);
break;
case 7772:
NoOp(You called 7772!);
break;
case 7773:
NoOp(You called 7773!);
// fall thru-
pattern 777[4-9]:
NoOp(You called 777 something!);
default:
NoOp(In the default clause!);
}
}
}
三个关键字,break,continue和return,为循环和switch提供流程的控制。
Break可以被用在switch和循环中,用来结束循环或是switch。
Continue可以用在循环中(while和for),立刻结束本次操作。
Return将结束context或macro,可以用在任何地方。
这个AEL中如何进行goto的例子。
context gotoexample {
s => {
begin:
NoOp(Infinite Loop! yay!);
Wait(1);
goto begin; // go to label in same extension
}
3 => {
goto s,begin; // go to label in different extension
}
4 => {
goto gotoexample,s,begin; // overkill go to label in same context
}
}
context gotoexample2 {
s => {
end:
goto gotoexample,s,begin; // go to label in different context
}
}
context gotoexample {
s => {
begin:
NoOp(Infinite Loop! yay!);
Wait(1);
jump s; // go to first extension in same extension
}
3 => {
jump s,begin; // go to label in different extension
}
4 => {
jump s,begin@gotoexample; // overkill go to label in same context
}
}
context gotoexample2 {
s => {
end:
jump s@gotoexample; // go to label in different context
}
}
“catch”block可以在宏中被标识一些特殊的分级(extensions)
macro std-exten( ext , dev ) {
Dial(${dev}/${ext},20);
switch(${DIALSTATUS) {
case BUSY:
Voicemail(${ext},b);
break;
default:
Voicemail(${ext},u);
}
catch a {
VoiceMailMain(${ext});
return;
}
}
宏可以使用下面的方法被引用,&std-exten。
context example {
_5XXX => &std-exten(${EXTEN}, "IAX2");
_6XXX => &std-exten(, "IAX2");
_7XXX => &std-exten(${EXTEN},);
_8XXX => &std-exten(,);
}
context demo {
s => {
Wait(1);
Answer();
TIMEOUT(digit)=5;
TIMEOUT(response)=10;
restart:
Background(demo-congrats);
instructions:
for (x=0; ${x} < 3; x=${x} + 1) {
Background(demo-instruct);
WaitExten();
}
}
2 => {
Background(demo-moreinfo);
goto s,instructions;
}
3 => {
LANGUAGE()=fr;
goto s,restart;
}
500 => {
Playback(demo-abouttotry);
Dial(IAX2/guest@misery.digium.com);
Playback(demo-nogo);
goto s,instructions;
}
600 => {
Playback(demo-echotest);
Echo();
Playback(demo-echodone);
goto s,instructions;
}
# => {
hangup:
Playback(demo-thanks);
Hangup();
}
t => goto #,hangup;
i => Playback(invalid);
}
AEL在解析后,编译前,将对拨号方案树做一些检查:
l 调用不存在的宏
l 宏调用上下文
l 参数不相符的宏调用
l 缺少&的宏调用
l 在“GotoIf”,“GotoIfTime”,“while”,“endwhile”,“Random”,和“execIf”的调用,将使得通话转到AEL goto,while等结构当中去。
l Goto到一个空的分机中的标识
l Goto到一个内嵌的分机,内嵌的上下文,或是一个不同上下文,或是任何包含(included)的上下文中不存在的标识。还会检查姊妹上下文引用(?)
l 在拨号方案中,总会对时间的值做检查的,包括ifTime()和包含的时间中的时间值:1、标明时间的范围需要使用破折号来进行分隔;2、小时必须在0到24之间3、如果提供了工作日列表,那么工作日必须在这个内部列表中4、一个月中的第几天必须是在1到31之间的范围当中;月份的名字则也必须在内部列表当中。
l 如果表达式被括在$[…]中,那么编译器将把它在括起来一边,将产生警告
l 会检查是否有重复的上下文名称。
l 会检查没有被其他上下文包含的抽象上下文。
l 如果一个标识是一个数值,那么产生警告
如果计划中的AAL(asterisk argument language)被开发出来并被整合进asterisk,那么一部分检查将会被移除。
检查一个字符串是否为null的最安全的方法是$[“${x}” = “”]。以前的做法和shell脚本中的做法一致,就是在两边都加上一些字符,如:$[${x}foo = foo]。但是这种做法的麻烦就是,如果x包含一些空格,则会产生语法错误。更加安全的做法就是如一开始提到的加上双引号。现在也有一些针对变量引用的函数可以被使用,如ISNULL()和LEN(),他们可以被用来测试一个空字符串:${ISNULL(${X})}或$[${LEN(${x})} = 0]。
你要记住的是赋值有两种不同的方法。如果你选择’x=y;’那么AEL将使用$[]括起右边的变量。所以,当编译的时候,结果就是’Set(x=$[y])’。如果你不想产生这样的效果,那么就像如下这样来替代‘Set(x=y)‘。
Asterisk的一个初学者看了上面的介绍和结构可能会问:“字符串处理函数在什么地方?”,“类似于其他语言提供的很酷的操作在什么地方?”等等
答案就是asterisk的这些丰富的功能都可以通过AEL来提供:
l applications:可以参考asterisk的application命令文档
l functions:函数通过${…}实现,并可以提供许多有用的功能
l expressions:在$[…]包含的表达式被表达式计算引擎处理。包括一些字符串的处理,算法表达式等
l 应用网关接口(Application Gateway interface):asterisk可以通过管道同它产生的进程进行通信。AGI应用可以使用任何语言实现。一些强大的应用可以通过这种方式来实现。
l 变量(variables):通信的通道拥有一些变量,并且asterisk也提供一些公共的变量。这些变量可以被上面提到的机制操作。