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

Tsecer的回音岛

Tsecer的博客

 
 
 

日志

 
 

从test命令谈起  

2013-02-23 23:40:48|  分类: bash分析 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
一、bash功能及语法
不论如何,bash始终是linux系统下一个极其重要的基础设施工具,在很多的操作系统教材中,都会说明一个计算机的大致结构就是在"kernel+shell+app"的一模型。其中的这个shell的名称正是和kernel对应的一个名词,一个是核,一个是壳。无论新兴的perl或者python等工具有多么的花俏和新潮,shell始终是作为系统的基础部件没有被替换。很多我们看见看不见的自动化工具都是使用了shell作为底层的支撑,例如常见的终端设备,自动构建中的configure及make机制,C库中的system系统调用等。
也正是由于它的广泛实用性,它也的确是随着历史的沉淀,里面加入和各种复杂而晦涩的语法和规则,这一般是一个具有悠久历史的事物所不能避免的。例如新兴的美国就有较少的历史遗留问题,相反中国和俄罗斯等国家就具有不少所谓的“历史遗留”问题。这些历史遗留问题在代码中就表现为兼容性问题,兼容性的原则就是对于之前的功能我们不加修改,只是添加新的功能。添加的结果就是最后功能越来越多,可能功能也越来越强大,但是这个强大的背后却是繁琐的实现和对初学者的迷茫。对于初学者来说,所有的事物都是一个新的事物,并且看不到历史的断代,这些沉淀下来的内容混淆在一起,显得没有头绪,但是如果你知道历史上发生了什么,那么这些诡异的现象可能会有一个合理的解释。比如说,我们为什么要实行一国两制呢?如果你不了解历史,那么这个问题看起来就非常的诡异。软件中的功能和逻辑有时候也是这样,所以如果能够对软件的功能进行断代和区分,那么调理会更加的清晰一点。
二、test和[
我想找个应该是bash的一个经典问题,随便搜索一下都可以得到一个完美而合理的解释。
[root@Harry builtins]# which [
/usr/bin/[
也就是说,[可以作为一个独立的命令来使用,它是一个可执行文件的名称,但是和test一样,它也有一个builtin的实现,也就是说bash内部对于[的处理和test的处理一样,是作为一个独立的内置任务看待的。在bash的源代码中,如果直接查找这个函数的定义是查找不到的,和其它的builtin命令一样,这些命令时被放在builtins的test.def文件中,bash中的def文件会在编译的时候被bash的构建系统逐个处理,然后动态生成一个配置文件。这个处理def文件的可执行程序源代码为mkbuiltins.c,它就是处理def文件中不同的所谓的"directive"指示
HANDLER_ENTRY handlers[] = {
  { "BUILTIN", builtin_handler },
  { "DOCNAME", docname_handler },
  { "FUNCTION", function_handler },
  { "SHORT_DOC", short_doc_handler },
  { "$", comment_handler },
  { "COMMENT", comment_handler },
  { "DEPENDS_ON", depends_on_handler },
  { "PRODUCES", produces_handler },
  { "END", end_handler },
  { (char *)NULL, (mk_handler_func_t *)NULL }
};

其中test.def文件中定义为
$BUILTIN [
$DOCNAME test_bracket
$FUNCTION test_builtin
$SHORT_DOC [ arg... ]
Evaluate conditional expression.

This is a synonym for the "test" builtin, but the last argument must
be a literal `]'
, to match the opening `['.
$END

事实上test命令和[公用相同的代码,但是当执行[时其会强制要求整个命令的最后一个单词为匹配的中括号结束符,具体代码为test.c中
test_command (margc, margv)
{
……

  if (margv[0] && margv[0][0] == '[' && margv[0][1] == '\0')
    {
      --margc;

      if (margv[margc] && (margv[margc][0] != ']' || margv[margc][1]))
    test_syntax_error (_("missing `]'"), (char *)NULL);

      if (margc < 2)
    test_exit (SHELL_BOOLEAN (FALSE));
    }
}
在动态生成的builtins.c文件中
     40 struct builtin static_shell_builtins[] = {
     41 #if defined (ALIAS)
     42   { "alias", alias_builtin, BUILTIN_ENABLED | STATIC_BUILTIN | ASSIGNMEN        T_BUILTIN | POSIX_BUILTIN, alias_doc,
     43      N_("alias [-p] [name[=value] ... ]"), (char *)NULL },
……
   157   { "test", test_builtin, BUILTIN_ENABLED | STATIC_BUILTIN, test_doc,
    158      N_("test [expr]"), (char *)NULL },
    159   { "[", test_builtin, BUILTIN_ENABLED | STATIC_BUILTIN, test_bracket_do        c,
    160      N_("[ arg... ]"), (char *)NULL },
    161   { "times", times_builtin, BUILTIN_ENABLED | STATIC_BUILTIN | SPECIAL_B        UILTIN, times_doc,

这里就不再详细说明具体实现,值得注意的地方是
1、每个test只能接收一个表达式,也就是不接受组合表达式,例如 x and y and z的形式。
2、首先计算参数个数,根据参数个数做相应的处理,区分使用的是单目操作,三目操作等。
3、在执行逻辑的时候,没有尽心变量展开,意味着所有的变量已经被展开,空字符不会作为空字符出现。
说这个的目的是为了和接下来的条件命令进行比较。
三、条件命令(condition [[)
知道了test命令的问题,之后出现了更见进步的条件命令,它的格式和之前的[功能和语法都非常相似,有专有的名字叫做条件命令,其格式为"[["开始,"]]"结束的结果。正如大家看到的,这个新添加的表达式并不是一个单独的命令,连一个内置函数也不是。为什么这么说呢?因为我在代码里面查找过。此时这个功能该如何实现呢?
1、语法定义
非常诡异是不是,好在一般的文本都有搜索功能,搜索一下就可以看到这个是作为关键字来实现的。所谓的关键字就是"[["写成这样的单词老子霸占了,你们脚本里谁都别想自己定义这个单词的意义。所以在词法分析的时候,会对这个单词进行特殊判断,判断出它是一个和 if for一样的关键字,并且有自己特殊的结构,该结构定义于语法文件parse.y中的
/* Reserved words.  These are only recognized as the first word of a
   command. */
STRING_INT_ALIST word_token_alist[] = {
……
#if defined (COND_COMMAND)
  { "[[", COND_START },
  { "]]", COND_END },
#endif
……
}
在词法分析器每次读入一个完整的单词之后,都会先检测是不是关键字,如果时则返回关键字结构而不进行特殊处理。这里还有一个非常重要的背景没有说,就是在bash的语法分析中,'['并不作为关键字来识别,除非是在数组初始化或者美元符号后面。作为类比,单独的'>'和'('就具有强烈的定界符意义,也就是说,它们放在哪里都想漆黑中的萤火虫一样,例如(echo hehe)>nosense.txt这样的结构,虽然括号和重定向符号和单词连接的非常紧密,之间没有任何空白的,按时shell能够进行正确的断句,识别出这个语法的意义。事实上,shell能识别的单个单词的列表在syntax.h中定义
#define slashify_in_quotes "\\`$\"\n"
#define slashify_in_here_document "\\`$"

#define shell_meta_chars   "()<>;&|"  元字符,就是不论被夹杂在那里都会被是被出来
#define shell_break_chars  "()<>;&| \t\n"  能够定界的单词

#define shell_quote_chars    "\"`'"
和早期的test相比,它解决了前者不具备的一些功能,例如它可以使用复杂的组合命令,能够识别变量值展开为空的格式。下面是这个语法的解析过程。在事实上这个condition语法结构的解析在语法文件中非常简单,在parse.y中
cond_command:    COND_START COND_CMD COND_END
            { $$ = $2; }
    ;
其中的COND_CMD只是一个简单的终结符(terminal),进一步说,它是一个字符串结构
2、词法分析
在词法分析阶段
read_token (command)函数

#if defined (COND_COMMAND)
  if ((parser_state & (PST_CONDCMD|PST_CONDEXPR)) == PST_CONDCMD)这个标志位在识别出condition关键字之后设置(参考CHECK_FOR_RESERVED_WORD宏定义)。
    {
      cond_lineno = line_number;
      parser_state |= PST_CONDEXPR;
      yylval.command = parse_cond_command ();这个地方完成对于条件命令的读入
      if (cond_token != COND_END)
    {
      cond_error ();
      return (-1);
    }
      token_to_read = COND_END;
      parser_state &= ~(PST_CONDEXPR|PST_CONDCMD);
      return (COND_CMD);
    }
#endif
3、命令执行
既然知道这是一种特殊的语法格式,那么处理起来就比较简单了,因为“特殊处理”是最不负责任但是最灵活的处理方式。在execute_command_internal函数中会直接识别出它的格式
  switch (command->type)
……
#if defined (COND_COMMAND)
    case cm_cond:
……
      exec_result = execute_cond_command (command->value.Cond);
……
}
接着execute_cond_command执行execute_cond_node,execute_cond_node递归的执行自己,从而完成复杂的条件命令的执行,其中相关处理
      arg1 = cond_expand_word (cond->left->op, 0);
      if (ignore)
    comsub_ignore_return--;
      if (arg1 == 0)
    arg1 = nullstr
;如果变量展开为空,则使用nullstr,有参数占位符,不会出现test中的缺失操作符问题
……
#if defined (COND_REGEXP)
      if (rmatch)
    {
      mflags = SHMAT_PWARN;
#if defined (ARRAY_VARS)
      mflags |= SHMAT_SUBEXP;
#endif

      result = sh_regmatch (arg1, arg2, mflags);正则表达式匹配
    }
      else
#endif /* COND_REGEXP */
……
    {
      int oe;
      oe = extended_glob;
      extended_glob = 1;
      result = binary_test (cond->op->word, arg1, arg2, TEST_PATMATCH|TEST_ARITHEXP|TEST_LOCALE)
                  ? EXECUTION_SUCCESS
                  : EXECUTION_FAILURE;这里的二元操作和test中调用的函数相同
      extended_glob = oe;
    }
这里大家会经常看到在configure脚本中有
if [x"$var" ==xsomeval]来判断一个字符串是不是和一个期望的字符串是否相等,因为如果不是这样,而直接使用$var则可能展开内容为空,从而出现语法错误。
四、(())和$(())
两者都是进行算术运算的语法,但是两者有细微的区别,首先,((是一个特定的语法方式,它是和[[等同的概念,在语法中有自己的type,其真正的执行代码为
#if defined (DPAREN_ARITHMETIC)
    case cm_arith:
      was_error_trap = signal_is_trapped (ERROR_TRAP) && signal_is_ignored (ERROR_TRAP) == 0;
      if (ignore_return)
    command->value.Arith->flags |= CMD_IGNORE_RETURN;
      line_number_for_err_trap = save_line_number = line_number;
      exec_result = execute_arith_command (command->value.Arith);
……
#endif
在这种情况狂下,shell会对((中的变量进行常规的展开,进而取到变量的数值。
对于$(())结构来说,它是一个变量的展开格式,由于有$开头,它将会被识别为另一种普通的变量引用和展开结构,而不再作为特殊的cm_arith类型。从实现方式上来看,它的展开位于param_expand函数中
 /* Do command or arithmetic substitution. */
    case LPAREN:
      /* We have to extract the contents of this paren substitution. */
      t_index = zindex + 1;
      temp = extract_command_subst (string, &t_index, 0);
      zindex = t_index;
      /* For Posix.2-style `$(( ))' arithmetic substitution,
     extract the expression and pass it to the evaluator. */
      if (temp && *temp == LPAREN)
……
      /* Expand variables found inside the expression. */
      temp1 = expand_arith_string (temp2, Q_DOUBLE_QUOTES);这个函数中会对遇到的所有标识符当做变量进行展开,我们可以认为这种$(())格式的给够是将(())结构中所有变量引用的$字符提到了最外面
      free (temp2);
五、数组引用
数组的声明可以通过 array[sub]=val的格式来说明它是一个数组。之前曾经说过,中括号在bash的词法分析中并不是关键字,所以这个结构的识别和普通的变量赋值一样,是在变量展开的时候进行结构识别的,或者再进一步说,它只是一种特殊的赋值语句。
do_assignment->>do_assignment_internal

#if defined (ARRAY_VARS)
  if (t = mbschr (name, '['))    /*]*/ 如果变量名中包含中括号,则认为是数组变量赋值,之后通过bind_array_variable识别其为一个数组变量
    {
      if (assign_list)
    {
      report_error (_("%s: cannot assign list to array member"), name);
      ASSIGN_RETURN (0);
    }
      entry = assign_array_element (name, value, aflags);
      if (entry == 0)
    ASSIGN_RETURN (0);
    }
  else if (assign_list)
    {
      if (word->flags & W_ASSIGNARG)
    aflags |= ASS_MKLOCAL;
      if (word->flags & W_ASSIGNASSOC)
    aflags |= ASS_MKASSOC;
      entry = do_compound_assignment (name, value, aflags);
    }
  else
#endif /* ARRAY_VARS */

param_expand (string, sindex, quoted, expanded_something,
          contains_dollar_at, quoted_dollar_at_p, had_quoted_null_p,
          pflags)
#if defined (ARRAY_VARS)
      if (assoc_p (var) || array_p (var))
        {
          temp = array_p (var) ? array_reference (array_cell (var), 0)
                   : assoc_reference (assoc_cell (var), "0");
          if (temp)
        temp = (*temp && (quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES)))
              ? quote_string (temp)
              : quote_escapes (temp);
          else if (unbound_vars_is_error)
        goto unbound_variable;
        }
      else
#endif


/*
 * Return the value of a[i].
 */
char *
array_reference(a, i)
ARRAY    *a;
arrayind_t    i;
{
    register ARRAY_ELEMENT *ae;

    if (a == 0 || array_empty(a))
        return((char *) NULL);
    if (i > array_max_index(a))
        return((char *)NULL);
    /* Keep roving pointer into array to optimize sequential access */
    if (lastref && IS_LASTREF(a))
        ae = (i >= element_index(lastref)) ? lastref : element_forw(a->head);
    else
        ae = element_forw(a->head);
    for ( ; ae != a->head; ae = element_forw(ae))
        if (element_index(ae) == i) {
            SET_LASTREF(a, ae);
            return(element_value(ae));
        }
    UNSET_LASTREF();
    return((char *) NULL);如果没有找到数组元素,展开空
}
  评论这张
 
阅读(898)| 评论(0)
推荐 转载

历史上的今天

评论

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

页脚

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