注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

Tsecer的回音岛

Tsecer的博客

 
 
 

日志

 
 

expect源代码分析  

2012-08-12 23:30:30|  分类: Linux系统编程 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
一、expect简介
该工具的初衷是为了实现交互式工具的自动化处理,比如在某些场合下比较常用到的ssh,telnet等交互式工具,或者是另外一些定制化的程序,例如一些商业软件的自动化测试,需要根据测试的结果执行不同的动作。ssh并没有提供让用户主动通过命令发送自己密码的功能,而是服务器主动的在开始要求用户输入密码,这样对客户端来说,这个密码的输入是一个简单的字节流而不是一个协议。和这个对应的是ftp协议,它在协议层支持通过PASS命令来发送密码。
二、expect中用到的tcl
expect的实现同样是通过脚本来完成良好的通用性和可配置性的,但是和其它的脚本语言不同,expect并没有定义自己的语法文件,而是直接使用了tcl的语言解析库,也就是说,它的底层实现依赖于tcl对于脚本的解析。由于tcl是一个扩展性比较强的脚本语言,所以这样减少了代码作者的工作量。但是tcl相对于yacc语法文件来说还是比较生僻的,所以又必须看tcl语言的基础,大致看了个皮毛,只是大致描述一下tcl的一些基本语言特征。
在tcl中,所有的元素都是由一个一个的
command args
形式的命令组成的,这些命令可以使用分号分隔,也可以使用自然换行来分隔,可以使用\newline来实现逻辑续行,可以使用#表示注释。但是tcl也带有自己的流程控制结构,例如while、for循环,以及if判断等。
其中的基本单词组合关系归为下面三大类,:
1、双引号
双引号是为了进行分组或者说是定界,它和bash中的双引号功能相似,都是为了将一组使用空白或者关键字分隔的单词组合为一个大的单词,并且其中的变量取值会进行展开。
2、大括号
大括号和双引号功能相似,也会为了将零散的单词组合成一个简单的词法单词。但是它限制比较严格,那就是其中的变量不会被展开,而是使用字面量来引用。这一点和bash中的单引号类似。
3、中括号
这个是执行一个命令,然后把命令的输出作为替换结果,这一点和bash中的反引号类似。
三、expect的基本用法
expect和awk类似,通过匹配一定的正则表达式,然后根据是否匹配来执行对应的动作,而这一点又和C语言中的switch语句结构类似,只是expect对于没有任何匹配的语义处理比较特殊。具体的特殊之处是如果expect的内容没有一个匹配,那么它会丢弃结果,然后再次执行expect语句,这一点无论awk还是switch都不具备这个语义。
1、复杂类expect的解析过程简析
以expect自带的例子passwdprompt文件为例,看一下一个expect文件的解析过程:
    expect {
    -re "\r" {
        send_user \r\n
    } -re "\010|\177" {
        if {[string length $passwd] > 0} {
        # not all ttys support destructive spaces
        send "\010 \010"
        regexp (.*). $passwd x passwd
        }
        exp_continue
    } -re . {
        send_user *
        append passwd $expect_out(0,string)
        exp_continue
    }
    }
根据之前的解释,其实这个expect虽然占用了这么多行,但是事实上它是一个只有一个参数的expect命令,因为在大括号内换行符也是作为字面量来理解的。而这么一个复杂的语句对于expect来说,它只有定义自己的expect命令来对这个脚本进行进一步的解释。
在对于expect解析的
Exp_ExpectObjCmd(clientData, interp, objc, objv)
函数中,其中开始进行的判断为
    if ((objc == 2) && exp_one_arg_braced(objv[1])) {对于上面的例子,是满足这个分支的,其中exp_one_arg_braced是判断这个唯一的参数中是否包含了换行符,如果包含了换行符,则认为是一个包含了多个参数的大括号缩写形式,对于这种复杂形式的expect语句,expect本身需要进行进一步的解析,将参数中的各个层次逐个剥离出来,而exp_eval_with_one_arg函数则是完成将其中的 <pattern,action>解析出来的是闲着者
    return(exp_eval_with_one_arg(clientData,interp,objv));
    } else if ((objc == 3) && streq(Tcl_GetString(objv[1]),"-brace")) {
    Tcl_Obj *new_objv[2];
    new_objv[0] = objv[0];
    new_objv[1] = objv[2];
    return(exp_eval_with_one_arg(clientData,interp,new_objv));
    }
2、exp_eval_with_one_arg函数实现
 /*
     * Treat the pattern/action block like a series of Tcl commands.
     * For each command, parse the command words, perform substititions
     * on each word, and add the words to an array of values.  We don't
     * actually evaluate the individual commands, just the substitutions.
     */

    do {
    if (Tcl_ParseCommand(interp, p, bytesLeft, 0, &parse)
            != TCL_OK) {
        rc = TCL_ERROR;
        goto done;
    }
    numWords = parse.numWords;此时此地,对于前面的例子,这里的numWords的值为9,即从-re开始,到包含append passwd $expect_out(0,string)的大括号结束,
……
    rc = Tcl_EvalObjv(interp, objc, objs, 0);此时再次执行expect,但是此时命令已经被线性化,也就是展现出一个 -re xx -re 之类的形式。
在该函数的开始,主动再次调用Tcl_ParseCommand对于括号内的字符串再次作为一个命令来解析,此时根据大括号和独立单词进行划分,总共划分出9个单词。
3、再次进入Exp_ExpectObjCmd
当再次进入时,它的参数个数已经不是只有一个,所以会继续执行下去,此时的处理就是一条流水线的处理,就是一次判断是否存在关键字,如果是则进行记录和整理。此时代码由parse_expect_args函数实现,
 for (i = 1;i<objc;i++) {
    int index;
    string = Tcl_GetString(objv[i]);
    if (string[0] == '-') {
        static char *flags[] = {
        "-glob", "-regexp", "-exact", "-notransfer", "-nocase",
        "-i", "-indices", "-iread", "-timestamp", "-timeout",
        "-nobrace", "--", (char *)0
        };
……
   switch ((enum flags) index) {
        case EXP_ARG_GLOB:
        case EXP_ARG_DASH:
        i++;
        /* assignment here is not actually necessary */
        /* since cases are initialized this way above */
        /* ec.use = PAT_GLOB; */
        if (i >= objc) {
            Tcl_WrongNumArgs(interp, 1, objv,"-glob pattern");
            return TCL_ERROR;
        }
        goto pattern;
        case EXP_ARG_REGEXP:
        i++;
        if (i >= objc) {
            Tcl_WrongNumArgs(interp, 1, objv,"-regexp regexp");
            return TCL_ERROR;
        }
        ec.use = PAT_RE;

        /*
         * Try compiling the expression so we can report
         * any errors now rather then when we first try to
         * use it.
         */

        if (!(Tcl_GetRegExpFromObj(interp, objv[i],
                       TCL_REG_ADVANCED))) {
            goto error;
        }
        goto pattern;
……
    } else {这个else意味着所有不是以'-'开始的单词都将会首先作为关键字来处理,如果不是关键字,则作为正则表达式的patten来处理
        /*
         * We have a pattern or keyword.
         */

        static char *keywords[] = {
        "timeout", "eof", "full_buffer", "default", "null",
        (char *)NULL
        };
……
pattern:
        /* if no -i, use previous one */
        if (!ec.i_list) {
        /* if no -i flag has occurred yet, use default */
        if (!eg->i_list) {
            if (default_esPtr != EXP_SPAWN_ID_BAD) {
            eg->i_list = exp_new_i_simple(default_esPtr,eg->duration);
            } else {
                default_esPtr = expStateCurrent(interp,0,0,1);
                if (!default_esPtr) goto error;
                eg->i_list = exp_new_i_simple(default_esPtr,eg->duration);
            }
        }
        ec.i_list = eg->i_list;
        }
……
   ec.pat = objv[i];
        if (eg->duration == EXP_PERMANENT) Tcl_IncrRefCount(ec.pat);

        i++;
        if (i < objc) {这个判断意味着,在整个expect参数的中间,必须是 pattern body的形式出现,不能出现只有pattern而没有body的情况,除非是pattern作为expect的最后一个参数
        ec.body = objv[i];
        if (eg->duration == EXP_PERMANENT) Tcl_IncrRefCount(ec.body);
        } else {如果expect只有一个参数,body可以为空
        ec.body = NULL;
        }
4、expect如何匹配
同样是在Exp_ExpectObjCmd函数中
   /* remtime and current_time updated at bottom of loop */
    remtime = timeout;

    for (;;) {
    if ((timeout != EXP_TIME_INFINITY) && (remtime < 0)) {
        cc = EXP_TIMEOUT;
    } else {
        cc = expRead(interp,esPtrs,mcount,&esPtr,remtime,key);这个expRead是从一个文件描述符中读取数据,也就是供expect中的pattern进行匹配的匹配空间
    }
……
cc = eval_cases(interp,&exp_cmds[EXP_CMD_BEFORE],
        esPtr,&eo,&last_esPtr,&last_case,cc,esPtrs,mcount,"");处理expect_before和expect_after内置命令指定的命令内容
    cc = eval_cases(interp,&eg,
        esPtr,&eo,&last_esPtr,&last_case,cc,esPtrs,mcount,"");进行真正的内容匹配工作,遍历各个pattern,并找到对应的body,然会匹配情况
    cc = eval_cases(interp,&exp_cmds[EXP_CMD_AFTER],
        esPtr,&eo,&last_esPtr,&last_case,cc,esPtrs,mcount,"");
    if (cc == EXP_TCLERROR) goto error;
    /* special eof code that cannot be done in eval_cases */
    /* or above, because it would then be executed several times */
    if (cc == EXP_EOF) {
        eo.esPtr = esPtr;
        eo.match = expSizeGet(eo.esPtr);
        eo.buffer = eo.esPtr->buffer;
        expDiagLogU("expect: read eof\r\n");
        break;
    } else if (cc == EXP_TIMEOUT) break;
    /* break if timeout or eof and failed to find a case for it */

    if (eo.e) break;退出无限循环的条件是找到一个匹配的pattern,其中的e就是一个expect的分支,或者说一个expect的匹配反过来说,如果没有任何个分支满足匹配,很抱歉,请继续循环

    /* no match was made with current data, force a read */
    esPtr->force_read = TRUE;

    if (timeout != EXP_TIME_INFINITY) {
        time(&current_time);
        remtime = end_time - current_time;
    }
在eval_cases中处理比较简单,就是对一个expect的各个patten进行尝试性匹配,但是我们这里关心的一个问题是如果没有任何一个匹配成功,它的返回值是EXP_NOMATCH,而在调用函数Exp_ExpectObjCmd中,对于这种没有任何匹配的情况。
5、表达式的匹配空间以什么为单位
这里可以看到,在例子中,表达式的匹配几乎是以单个字符为单位进行的,而其它的通常则不是这样。此处的问题在于那个之前的read何时返回。这一点需要对expect使用的协议有一定的了解。我们看expect中对于spawn的实现可以知道,expect和派生进程之间并不是通过管道通讯,而是通过伪终端。这对于expect来说是比较自然的,因为它本来就是为了完成交互式操作是实现的,这种典型的交互式协作都是以tty为媒介的。反过来看一下pipe,它是一个字节流的形式,它对回车换行字符并不敏感,也无法提供底层的框架性支持。而伪终端则很好的模拟和满足了这个特征,所以是实现expect和spawn子进程通讯的良好链路基础。伪终端底层天然支持对于换行的处理,从而满足了expect中正则表达式的匹配空间也是以行为单位的特征。
而对于我们这里的例子来说,它是在脚本的开始执行了
    set sttyOld [stty -echo raw]
其中禁止掉了回显,并且设置为raw形式,从而需要以字节为单位进行匹配,而这个也是实现password交互的一个惯例。
6、spawn命令实现
Exp_SpawnCmd
 if (!chanName) {
    if (echo) {
        expStdoutLogU(argv0,0);
        for (a = argv;*a;a++) {
        expStdoutLogU(" ",0);
        expStdoutLogU(*a,0);
        }
        expStdoutLogU("\r\n",0);
    }

    if (0 > (master = exp_getptymaster())) {
其中进一步调用的是
    if ((master = open("/dev/ptmx", O_RDWR)) == -1) return(-1);
这个就是主控tty文件,每次打开都会分配一个新的伪终端对,这一点和虚拟网卡的/dev/tun设备相似,所谓年年岁岁花相似、岁岁年年人不同。
四、另一些简单的expect用法示例
同样是expect自带的decryptdir文件,其中的expect语法就比较简单,其中的原因在上面已经描述过,这里不再分析。
    expect "key:"
    send "$passwd\r"
    expect
  评论这张
 
阅读(1353)| 评论(0)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017