bash中命令前设置子进程变量的绿色方法

一、语法
这其实是一个比较小的细节问题,但是觉得比较有创意(而且一用就会让人产生“当时我就震惊鸟”了感觉),而且bash的这个功能的实现代码为bash代码的晦涩性也做了不少贡献,所以这里还是看一下这个比较有创意的语法。这个功能和管道一样,感觉是一个绿色环保的命令,说它绿色,就是它"事了拂衣去,深藏身与名”。这里先从大家耳熟能详的管道说起,它的优点就是两个进程共享一个文件,但是这个文件只在内存中存在,不会在系统中留下文件,当管道两端(或者一端)结束之后,系统是干净的。如果使用实体文件,首先是同步性没有保证,然后就是污染系统有残留文件。有人说,管道是UNIX最重要的发明,它把不同的进程粘合在一起,从而可以让每个程序只专注自己的功能,并把这个功能做好。
而bash的语法中支持一种命令前设置子进程环境变量的方法,当然这并不神奇,神奇的是它影响且只影响子进程的环境变量,而对父进程没有影响,所以说它是一种绿色设置环境变量的方法。我第一次见到它是在执行一个工程的configure配置的时候使用的,因为要设置一些特殊的PATH,但是又不影响父进程的环境变量,所以使用的命令是
PATH=SomeSpecialPath:$PATH    ./configure CFLAGS=-g
大家可以看到,其中这个PATH和接下来要执行的命令之间没有分号,但是子进程中会看到这个变量。下面是最为原始的Demo程序
[root@Harry GreenVar]# cat GreenVar.sh 
#!/bin/sh 
echo ME is $ME
[root@Harry GreenVar]# echo $ME  执行脚本之前,父进程环境变量中没有定义ME

[root@Harry GreenVar]# ME=tsecer . GreenVar.sh  在执行子进程之前设置ME变量的值为tsecer,然后子进程中可以看到该变量
ME is tsecer
[root@Harry GreenVar]# echo $ME 子进程退出之后父进程环境变量依然是纯洁的。
二、最基础两个骨架函数和功能说明
对于整个bash的代码,主入口位于parse_and_execute函数中,它的作用和函数名称还是比较一致的,就是一个是解析,另一个是执行;函数内对应两个实现,一个是parse_command,一个是execute_command_internal。这两个函数笼统的说(代码没看那么深,自己YY的):
1、parse_command
负责语法分析,并进行“断句”(或者说将句子切割为不同的WORD,要考虑到引号,括弧等),但是不负责展开。例如ME=$WHO这样的内容是作为一个WORD来返回的,包括其中的取值操作符$都会被原封不动的保存;但是它可以识别出语义,例如for do之类的句式和语义。在parse_command中分析出来的语法放在全局变量global_command中,然后在parse_and_execute函数中通过else if (command = global_command)将这个变量放在command中,从而供execute_command_internal来执行这个命令其中变量。在 struct command 结构的enum command_type type;成员中说明了语法分析出来的是一个什么句式,大家可以看一下command_type这个枚举的所有类型。
2、execute_command_internal
它是真正的执行命令,这个执行包括了最为各种变量展开和替换,当然包括之后的执行。在该函数中,有一个很大的switch,而switch的条件就是这里说的enum command_type,而这个就是parse.y中适当的时候赋值进来的,这就是语法/语义分析的功劳。这里命令的执行还是有很多冗长的操作的,要看懂代码,首先要知道bash的各个功能。其实代码实现本身并不重要,例如我感觉bash的代码就很乱,但是重要的是功能或者说是需求,一个工具是否能够提供你所需要的(或者你一下子没想到但是之后会用到)功能,并且让你觉得非常好用,这就是一款产品的意义,例如瑞士军刀,例如iphone。
三、命令前变量赋值语法分析
这个毫无疑问是在read_token_word中进行的,但是这里还要考虑的情况,就是最开始写的那个命令
PATH=SomeSpecialPath:$PATH    ./configure CFLAGS=-g
这里在真正的configure命令前后都有一个变量赋值命令,之前的肯定是要设置子进程的环境变量的,但是接下来的那些明显是给子进程configure使用的,如果bash拍马屁拍到马腿上,把这个东西也当做环境变量吃掉了,那用户就会相当的惊诧莫名了。
该函数的这个功能实现代码为
  /* A word is an assignment if it appears at the beginning of a 作者很nice的给了一个注释,说这个就是用来处理赋值的,这个赋值需要出现在简单
     simple command, or after another assignment word.  This is 命令的开始,或者在另一个赋值的后面,也就是ME=tsecer YOU=who都是可以接受
     context-dependent, so it cannot be handled in the grammar. */的。作者说这是上下文相关的,所以不能在语法(上下文无关)中处理。
  if (assignment (token, (parser_state & PST_COMPASSIGN) != 0))
    {
      the_word->flags |= W_ASSIGNMENT;
      /* Don't perform word splitting on assignment statements. */
      if (assignment_acceptable (last_read_token) || (parser_state & PST_COMPASSIGN) != 0)如果说可以被接受为赋值指令,  
    the_word->flags |= W_NOSPLIT;                                                                                              则添加W_NOSPLIT属性,这个属性和W_ASSIGNMENT
    }                                                                                                                                                  满足接下来的判断,返回ASSIGNMENT_WORD,进而
……                                                                                                                                              使 command_token_position函数满足
  result = ((the_word->flags & (W_ASSIGNMENT|W_NOSPLIT)) == (W_ASSIGNMENT|W_NOSPLIT))
        ? ASSIGNMENT_WORD : WORD;

其中使用到的相关宏定义

#define command_token_position(token)
  (((token) == ASSIGNMENT_WORD) || (parser_state&PST_REDIRLIST) ||
   ((token) != SEMI_SEMI && (token) != SEMI_AND && (token) != SEMI_SEMI_AND && reserved_word_acceptable(token)))

#define assignment_acceptable(token)
  (command_token_position(token) && ((parser_state & PST_CASEPAT) == 0))
四、变量展开
expand_word_list_internal---->>>separate_out_assignments
  /* Separate out variable assignments at the start of the command.
     Loop invariant: vp->next == lp
     Loop postcondition:
    lp = list of words left after assignment statements skipped
    tlist = original list of words
  */
  while (lp && (lp->word->flags & W_ASSIGNMENT))开始遍历一个命令中所有的,具有W_ASSIGNMENT属性的单词,遇到一个无该属性即结束循环
    {
      vp = lp;
      lp = lp->next;
    }

  /* If lp != tlist, we have some initial assignment statements.
     We make SUBST_ASSIGN_VARLIST point to the list of assignment
     words and TLIST point to the remaining words.  */
  if (lp != tlist)
    {
      subst_assign_varlist = tlist;      这里的subst_assign_varlist是全局变量,赋值之后父函数将会使用
      /* ASSERT(vp->next == lp); */
      vp->next = (WORD_LIST *)NULL;    /* terminate variable list */
      tlist = lp;            /* remainder of word list */
    }
五、环境变量的设置
expand_word_list_internal函数中
  if ((eflags & WEXP_VARASSIGN) && subst_assign_varlist)
    {
      sh_wassign_func_t *assign_func;

      /* If the remainder of the words expand to nothing, Posix.2 requires
     that the variable and environment assignments affect the shell's
     environment. */
      assign_func = new_list ? assign_in_env : do_word_assignment;由于命令开始所有赋值之后还有命令,所以使用assign_in_env
      tempenv_assign_error = 0;

      for (temp_list = subst_assign_varlist; temp_list; temp_list = temp_list->next)
    {
      this_command_name = (char *)NULL;
      assigning_in_environment = (assign_func == assign_in_env);
      tint = (*assign_func) (temp_list->word);
      assigning_in_environment = 0;
从assign_in_env函数可以看到,
  var = hash_lookup (name, temporary_env);
  if (var == 0)
    var = make_new_variable (name, temporary_env);
  else
    FREE (value_cell (var));
在放置的时候是添加到了temporary_env全局变量中,这个是和当前shell使用的变量使用的是两个独立的地址空间。
六、环境变量的传递
maybe_make_export_env()
  if (temporary_env)
    {
      tcxt = new_var_context ((char *)NULL, 0);
      tcxt->table = temporary_env;
      tcxt->down = shell_variables;
    }
      else
    tcxt = shell_variables;
      
      temp_array = make_var_export_array (tcxt);
      if (temp_array)
    add_temp_array_to_env (temp_array, 0, 0);将temp_array添加到export_env中
最后在execute_disk_command函数中使用了这个变量
exit (shell_execve (command, args, export_env));

原文地址:https://www.cnblogs.com/tsecer/p/10486101.html