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

Tsecer的回音岛

Tsecer的博客

 
 
 

日志

 
 

bash的展开  

2013-05-14 23:43:12|  分类: bash分析 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
一、各种展开
从大体上看,一个shell本身包含了两个部分,
一个是shell的语法部分,这些语法通常由各个关键字以及不同的关键字的语法结构组成。例如,在shell中for是一个关键字,case也是一个关键字,这些关键字组成了整个shell的基本语法和语义,而这些语义再组合起来成为各种复杂的控制结构。
shell的另一个主要部分就是各种展开的部分,这些展开包含了变量的展开,命令输出的展开,引号的展开等。这个展开是在语法之外的一个结构,就是说它更详细的是是一个手动约定的内容。
在bash的代码中可以看到,语法文件是通过传统的bison文件来定义和解析,而对于变量的展开则是通过专门的特殊处理代码来完成,从这种意义上看,变量的展开要显得更加的混乱和不规范。这些展开使用了各种没有语法文件描述的格式,它们对上下文的依赖非常严重,传统的yacc格式的上下文无关语法不能准确的表达这种上下文关系。
二、展开在词法分析阶段
bash的主要词法分析是在read_token函数完成,该函数被标准的词法分析文件yylex所调用,而真正的词法分析在read_token函数中完成。
在词法分析阶段,由于变量的替换对上下文的依赖,所以它对于复杂的各种变量定义都是按照一个单词来解释的,在我们看来,即使再复杂的一个变量,如果只是包含变量的展开和分组,那么在词法分析阶段它就是一个最为普通和常见的token文件。
这个结构的特殊性在词法分析阶段被屏蔽和封装,而后在语法文件分析之后再由特殊的代码进行专门的“解包”,而这个解包的操作的主题代码位于subst.c文件中。和它名字一样,它的确是进行各种变量的展开及替换。
这里可以更加通用的说,所有的脚本文件大体都是这两个基本功能,例如makefile,它里面也有变量的定义和展开,只是makefile中的变量展开结构和语法结构相对比较简单和特优化而已。
1、分组的现实
read_token_word (character)
……
    if MBTEST(character == '\\')
    {
      peek_char = shell_getc (0);

  ……
          quoted = 1;
          goto got_character;
        }
    }

      /* Parse a matched pair of quote characters. */
      if MBTEST(shellquote (character))
    {
      push_delimiter (dstack, character);
      ttok = parse_matched_pair (character, character, character, &ttoklen, (character == '`') ? P_COMMAND : 0);
      pop_delimiter (dstack);
      if (ttok == &matched_pair_error)
        return -1;        /* Bail immediately. */
      RESIZE_MALLOCED_BUFFER (token, token_index, ttoklen + 2,
                  token_buffer_size, TOKEN_DEFAULT_GROW_SIZE);
      token[token_index++] = character;
      strcpy (token + token_index, ttok);
      token_index += ttoklen;
      all_digit_token = 0;
      quoted = 1;
      dollar_present |= (character == '"' && strchr (ttok, '$') != 0);
      FREE (ttok);
      goto next_character;
    }
这里可以看到的是,转义字符的处理在每次词法读取的最开始处理,这里包含两层意思:
转义的优先级最高,转义字符本身被删除,但是它之后的字符被完整的原封不动保存(燃烧自己,照亮别人)。
在每次新的一个词法分析的开始生效。如果是在其它结构解析的过程中,这个转义字符需要由各自结构专门处理。
2、匹配对的实现parse_matched_pair
该函数用来处理大部分的引用和分组,它的功能就是提供shell语法文件确实的一个重要部分:待替换结构的完整提取,提取为一个token。
从大体上来说,这个结构处理两种情况的分区,一种是 开始符号和结束符号 相同的结构,典型的就是 双引号、单引号及反引号;另一种是不同的大括号、中括号和小括号。这一点看起来比较直观,但是这个不同对于结构的区分却又更加深刻的影响。
可以想象,如果开始符号和结束符号相同,那么当我们遇到第二个相同符号的时候,我们并没有办法知道这个符号是上个结构的结束还是新结构的开始,这导致的一个直接后果就是这种开始和结束符号相同的结构不能嵌套;相反,对于开始和结束不同的符号就不存在这种问题,因为开始符号始终就是开始符号,而结束符号始终是不同的结束符号,所以嵌套不是任何问题。
  count = 1;
rflags = (qc == '"') ? P_DQUOTE : (flags & P_DQUOTE);
 while (count)
    {
      ch = shell_getc (qc != '\'' && (tflags & LEX_PASSNEXT) == 0);
……
  else if MBTEST(ch == close)        /* ending delimiter */
    count--
;
      /* handle nested ${...} specially. */
      else if MBTEST(open != close && (tflags & LEX_WASDOL) && open == '{' && ch == open) /* } */
    count++;
      else if MBTEST(((flags & P_FIRSTCLOSE) == 0) && ch == open)    /* nested begin */
    count++;
      /* If we just read the ending character, don't bother continuing. */
      if (count == 0)
    break
;
……
       /* Could also check open == '`' if we want to parse grouping constructs
     inside old-style command substitution. */
      if (open != close)        /* a grouping construct */
    {
      if MBTEST(shellquote (ch))
        {
……
          if MBTEST((tflags & LEX_WASDOL) && ch == '\'')    /* $'...' inside group */
        nestret = parse_matched_pair (ch, ch, ch, &nestlen, P_ALLOWESC|rflags);
          else
        nestret = parse_matched_pair (ch, ch, ch, &nestlen, rflags);
}
……
   /* Parse an old-style command substitution within double quotes as a
     single word. */
      /* XXX - sh and ksh93 don't do this - XXX */
      else if MBTEST(open == '"' && ch == '`')
    {
      nestret = parse_matched_pair (0, '`', '`', &nestlen, rflags);

      CHECK_NESTRET_ERROR ();
      APPEND_NESTRET ();

      FREE (nestret);
    }
      else if MBTEST(open != '`' && (tflags & LEX_WASDOL) && (ch == '(' || ch == '{' || ch == '['))    /* ) } ] */
    /* check for $(), $[], or ${} inside quoted string. */
    {
parse_dollar_word:
      if (open == ch)    /* undo previous increment */
        count--;
      if (ch == '(')        /* ) */
        nestret = parse_comsub (0, '(', ')', &nestlen, (rflags|P_COMMAND) & ~P_DQUOTE);
      else if (ch == '{')        /* } */
        nestret = parse_matched_pair (0, '{', '}', &nestlen, P_FIRSTCLOSE|rflags);
      else if (ch == '[')        /* ] */
        nestret = parse_matched_pair (0, '[', ']', &nestlen, rflags);

      CHECK_NESTRET_ERROR ();
      APPEND_NESTRET ();

      FREE (nestret);
    }
从上面的结构可以看到,如果结束符和开始符相同,则在遇到一个符号时,它首先判断是否等于结束符号(同时也是开始符号),如果时则数量递减,所以在这个循环中,count不会增加,遇到第一个未转义的引号后直接跳出循环返回。
但是在结构中,分组符号(大、小、中括号)可以改变这种限制,从后面的代码可以看到,一个$引导的分组符号可以开始新的一轮pair匹配执行,在新的结构中,之前的引号意义可以被消除,从而开始新的一个轮回。
一些变量的辅助说明:
在这个函数中:
P_COMMAND 表示在解析一条命令,所以在命令中可能使用注释。
P_FIRSTCLOSE 表示不进行分组符号的嵌套,只要求第一个结束符就理解返回。
三、变量在展开阶段
1、一个典型的调用链
(gdb) bt
#0  expand_word_list_internal (list=0x92eef08, eflags=31) at subst.c:8962
#1  0x08098a05 in expand_words (list=0x92eef08) at subst.c:8632
#2  0x0807774b in execute_simple_command (simple_command=0x92e2208,
    pipe_in=-1, pipe_out=-1, async=0, fds_to_close=0x92e5f08)
    at execute_cmd.c:3645
#3  0x08072a7e in execute_command_internal (command=0x92e6628, asynchronous=0,
    pipe_in=-1, pipe_out=-1, fds_to_close=0x92e5f08) at execute_cmd.c:730
#4  0x0807229a in execute_command (command=0x92e6628) at execute_cmd.c:375
#5  0x080609ea in reader_loop () at eval.c:152
#6  0x0805ea89 in main (argc=1, argv=0xbf8c2a44, env=0xbf8c2a4c) at shell.c:749
(gdb)

expand_word_list_internal (list, eflags)

#if defined (BRACE_EXPANSION)
  /* Do brace expansion on this word if there are any brace characters
     in the string. */
  if ((eflags & WEXP_BRACEEXP) && brace_expansion && new_list)
    new_list = brace_expand_word_list (new_list, eflags);
#endif /* BRACE_EXPANSION */

  /* Perform the `normal' shell expansions: tilde expansion, parameter and
     variable substitution, command substitution, arithmetic expansion,
     and word splitting. */
  new_list = shell_expand_word_list (new_list, eflags);

  /* Okay, we're almost done.  Now let's just do some filename
     globbing. */
  if (new_list)
    {
      if ((eflags & WEXP_PATHEXP) && disallow_filename_globbing == 0)
    /* Glob expand the word list unless globbing has been disabled. */
    new_list = glob_expand_word_list (new_list, eflags);
      else
    /* Dequote the words, because we're not performing globbing. */
    new_list = dequote_list (new_list);
    }
2、shell内部转义
在shell内部结构中,很多时候也需要进行转义,例如一个双引号内的空白字符可能需要被转义,转义的目的是为了避免被特殊处理,例如文件展开(file glob)等。
在shell_expand_word_list--->>>expand_word_internal
……
      /* break; */

    default:
      /* This is the fix for " $@ " */
    add_ifs_character:
      if ((quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES)) || (isexp == 0 && isifs (c)))
        {
          if (string[sindex])    /* from old goto dollar_add_string */
        sindex++;
          if (c == 0)
        {
          c = CTLNUL;
          goto add_character;
        }
          else
        {
#if HANDLE_MULTIBYTE
          if (MB_CUR_MAX > 1)
            sindex--;

          if (MB_CUR_MAX > 1)
            {
              SADD_MBQCHAR_BODY(temp, string, sindex, string_size);
            }
          else
#endif
            {
              twochars[0] = CTLESC;
              twochars[1] = c
;
              goto add_twochars;
            }
        }
双引号内所有的非特殊字符都会在前面加上一个shell内部的转义字符,这个转义字符带来的好处是在glob文件的时候,导致即使是glob的元字符也会由于前导字符而不被识别。另一方面就是这个添加的额外字符在何时被再次擦除掉的。
3、对文件glob的影响
glob_expand_word_list--->>>unquoted_glob_pattern_p
/* Return nonzero if STRING has any unquoted special globbing chars in it.  */
int
unquoted_glob_pattern_p (string)
     register char *string;
{
  register int c;
  char *send;
  int open;

  DECLARE_MBSTATE;

  open = 0;
  send = string + strlen (string);

  while (c = *string++)
    {
      switch (c)
    {
    case '?':
    case '*':
      return (1);

    case '[':
      open++;
      continue;

    case ']':
      if (open)
        return (1);
      continue;

    case '+':
    case '@':
    case '!':
      if (*string == '(')    /*)*/
        return (1);
      continue;

    case CTLESC:
    case '\\':
      if (*string++ == '\0') 通配符在这个++操作中被无视。
        return (0)
;
    }

      /* Advance one fewer byte than an entire multibyte character to
     account for the auto-increment in the loop above. */
#ifdef HANDLE_MULTIBYTE
      string--;
      ADVANCE_CHAR_P (string, send - string);
      string++;
#else
      ADVANCE_CHAR_P (string, send - string);
#endif
    }
  return (0);
}
4、转义字符的删除
 if ((tlist->word->flags & W_NOGLOB) == 0 &&
      unquoted_glob_pattern_p (tlist->word->word))
    {
……
}
      else
    {
      /* Dequote the string. */
      temp_string = dequote_string (tlist->word->word);
      free (tlist->word->word);
      tlist->word->word = temp_string;
      PREPEND_LIST (tlist, output_list);
    }
在上层的expand_word_list_internal函数中,如果没有进行glob展开,则也会执行一次删除操作
  if (new_list)
    {
      if ((eflags & WEXP_PATHEXP) && disallow_filename_globbing == 0)
    /* Glob expand the word list unless globbing has been disabled. */
    new_list = glob_expand_word_list (new_list, eflags);
      else
    /* Dequote the words, because we're not performing globbing. */
    new_list = dequote_list (new_list);
    }
  评论这张
 
阅读(1021)| 评论(0)
推荐 转载

历史上的今天

评论

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

页脚

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