GNU bash实现机制与源代码简析



林健

摘要

本文是本人学习shell实现机理,分析GNU bash源代码时总结的笔记性文档。通过分析bash源代码,阐述了其主要功能模块的组织和实现方式,同时对几个特定的工作流程进行了说明。

第 1 章 概述

1.1. bash

GNU bash是各类UNIX系统,特别是Linux下经典的shell。作为一个命令行解释器,它提供了强大的可编程功能,为用户提供了操作系统功能的良好接口。作为一个经典的开源项目,它的源代码结构较为清晰,可靠性、性能和易用性经历了考验。

本文分析的bash版本为3.2.0(1),源代码为configure之后的版本,因为部分源代码是在configure过程中由辅助工具生成的。 builtins目录下的*.c文件是make之后的版本(需要注释掉builtins/Makefile中删除*.c文件的语句),因为生成这些源代码的辅助工具需要在make过程中生成。

1.2. 环境与工具

项目configure和make环境为Ubuntu 7.10(Linux 2.6.22),Intel Pentium III。

源代码分析工具为Windows下的Source Insight 3.5。Source Insight提供的交叉参考功能和调用链分析功能有助于理清复杂的函数调用和依赖关系。

第 2 章 程序结构分析

2.1. 系统架构

通常而言,shell的功能是从终端或其它输入取得命令行,将其解析为一系列操作指令,调用系统内核或相应的外部程序执行,然后将执行结果返回给终端或其它输出。因此,实现一个简单的shell是一项容易的工作。但bash的功能不仅限于此,它支持用管道和重定向协同执行命令,提供了强大的脚本编程能力,具备作业管理功能。一般的Linux发行版中,bash的可执行文件往往是/bin中最大的几个实用程序之一,客观反映了它的复杂性。

依据bash源代码的文件组织及函数调用关系,可分析出它的基本架构。

图 2.1. bash基本架构图

bash基本架构图

bash使用GNU Readline库处理用户命令输入,Readline提供类似于vi或emacs的行编辑功能。

bash运行时的调度中心是其主控循环。主控循环的功能较为简单,它循环读取用户(或脚本)输入,传递给语法分析器,同时处理下层递归返回的错误。

语法分析器对文本形式的输入首先进行通配符、别名、算术和变量展开等工作,然后通过命令生成器得到规范的命令结构,并由专门的重定向处理机制填写重定向语义,交由命令执行器执行。命令执行器依据命令种类不同,执行内部命令函数、外部程序或文件系统调用。在命令执行过程中,执行器要对系统信号进行捕获和处理。

在支持作业管理的操作系统中,命令执行器将进程信息加入作业控制机制,并允许用户使用内部命令或键盘信号来启停作业。如果在不支持作业管理的操作系统中编译bash,会使用另一套接口相同的机制对进程信息进行简单的维护。

2.2. 主要数据结构

2.2.1. WORD_DESC与WORD_LIST

/* A structure which represents a word. */
typedef struct word_desc {
  char *word;		/* Zero terminated string. */
  int flags;		/* Flags associated with this word. */
} WORD_DESC;

/* A linked list of words. */
typedef struct word_list {
  struct word_list *next;
  WORD_DESC *word;
} WORD_LIST;

命令等各种结构都可能引用字符串或字符串数组。bash对于有必要做进一步语义处理的字符串使用WORD_DESC、WORD_LIST结构进行封装。

WORD_DESC结构是对字符指针的一层封装,可称为字符串描述符。它在字符指针的基础上附加了一个flags标志位集合。其标志位包含 W_HASDOLLAR、W_QUOTED、W_DQUOTE、W_GLOBEXP等,分别表示该字符串中包含了“$”、引号、双引号、通配符等,目的是便于在变量代入、通配展开等过程中处理相应字符串。

WORD_LIST结构是WORD_DESC对象的链表。

2.2.2. COMMAND

/* What a command looks like. */
typedef struct command {
  enum command_type type;	/* FOR CASE WHILE IF CONNECTION or SIMPLE. */
  int flags;			/* Flags controlling execution environment. */
  int line;			/* line number the command starts on */
  REDIRECT *redirects;		/* Special redirects for FOR CASE, etc. */
  union {
    struct for_com *For;
    struct case_com *Case;
    struct while_com *While;
    struct if_com *If;
    struct connection *Connection;
    struct simple_com *Simple;
    struct function_def *Function_def;
    struct group_com *Group;
#if defined (SELECT_COMMAND)
    struct select_com *Select;
#endif
#if defined (DPAREN_ARITHMETIC)
    struct arith_com *Arith;
#endif
#if defined (COND_COMMAND)
    struct cond_com *Cond;
#endif
#if defined (ARITH_FOR_COMMAND)
    struct arith_for_com *ArithFor;
#endif
    struct subshell_com *Subshell;
  } value;
} COMMAND;

COMMAND结构描述一条bash命令,这里的“命令”概念指语法分析器通过定界符、管道或控制语句分析出的相对独立的执行单元,它可以是内部或外部命令、函数、控制结构、算术表达式等。

枚举参数type表示了一个命令对象代表的命令属于上述何种类型。整型变量flags记录了一系列有关运行环境的标志位,包括:

/* Possible values for command->flags. */
#define CMD_WANT_SUBSHELL  0x01	/* User wants a subshell: ( command ) */
#define CMD_FORCE_SUBSHELL 0x02	/* Shell needs to force a subshell. */
#define CMD_INVERT_RETURN  0x04	/* Invert the exit value. */
#define CMD_IGNORE_RETURN  0x08	/* Ignore the exit value.  For set -e. */
#define CMD_NO_FUNCTIONS   0x10 /* Ignore functions during command lookup. */
#define CMD_INHIBIT_EXPANSION 0x20 /* Do not expand the command words. */
#define CMD_NO_FORK	   0x40	/* Don't fork; just call execve */
#define CMD_TIME_PIPELINE  0x80 /* Time a pipeline */
#define CMD_TIME_POSIX	   0x100 /* time -p; use POSIX.2 time output spec. */
#define CMD_AMPERSAND	   0x200 /* command & */
#define CMD_STDIN_REDIR	   0x400 /* async command needs implicit </dev/null */
#define CMD_COMMAND_BUILTIN 0x0800 /* command executed by `command' builtin */

整型变量line表示该命令在一个脚本中所处的行数。指针redirects指向说明本命令重定向信息的REDIRECT结构对象。最后,针对不同的命令类型,COMMAND结构包含不同命令类型具体内部结构的联合:

对于内部或外部命令,使用联合中的simple_com结构,这个结构主要记录了命令名和命令行参数。它们存储于WORD_LIST链表结构,表元结点是WORD_DESC结构。

对于分支、循环等控制结构,它们的内部结构中主要包含指向相关控制流对应命令的指针。例如while_com结构包含了测试条件和循环体对应命令的指针;if_com结构包含了测试条件、真值执行体和假值执行体对应命令的指针。

对于函数定义,function_def结构包含了指向函数名的WORD_DESC结构指针、函数对应命令的指针以及指定函数所在文件名的指针。

2.2.3. REDIRECT与REDIRECTEE

/* Structure describing a redirection.  If REDIRECTOR is negative, the parser
   (or translator in redir.c) encountered an out-of-range file descriptor. */
typedef struct redirect {
  struct redirect *next;	/* Next element, or NULL. */
  int redirector;		/* Descriptor to be redirected. */
  int flags;			/* Flag value for `open'. */
  enum r_instruction  instruction; /* What to do with the information. */
  REDIRECTEE redirectee;	/* File descriptor or filename */
  char *here_doc_eof;		/* The word that appeared in <<foo. */
} REDIRECT;

REDIRECT结构是对命令输入输出重定向的定义。一条命令可以设置多个(输入、输出、错误)重定向,因此REDIRECT结构本身包含指向下一个REDIRECT对象的next指针,以便构成一条命令的重定向链表。

整型参数redirector为重定向源的文件描述符,标志位集合flags定义目标文件打开方式。instruction枚举定义了重定向的具体类型,它们包括:

/* Instructions describing what kind of thing to do for a redirection. */
enum r_instruction {
  r_output_direction, r_input_direction, r_inputa_direction,
  r_appending_to, r_reading_until, r_reading_string,
  r_duplicating_input, r_duplicating_output, r_deblank_reading_until,
  r_close_this, r_err_and_out, r_input_output, r_output_force,
  r_duplicating_input_word, r_duplicating_output_word,
  r_move_input, r_move_output, r_move_input_word, r_move_output_word
};

重定向的目标记录在REDIRECTEE联合中,它可以是一个文件名或文件描述符。定义如下:

/* What a redirection descriptor looks like.  If the redirection instruction
   is ri_duplicating_input or ri_duplicating_output, use DEST, otherwise
   use the file in FILENAME.  Out-of-range descriptors are identified by a
   negative DEST. */

typedef union {
  int dest;			/* Place to redirect REDIRECTOR to, or ... */
  WORD_DESC *filename;		/* filename to redirect to. */
} REDIRECTEE;

此外,对于Here Document类型的重定向,REDIRECT结构中的here_doc_eof指针指向Here Document。

2.2.4. VAR_CONTEXT与SHELL_VAR

bash本身的shell变量以及其中运行的函数的局部变量上下文存储在VAR_CONTEXT结构中。

/* A variable context. */
typedef struct var_context {
  char *name;		/* empty or NULL means global context */
  int scope;		/* 0 means global context */
  int flags;
  struct var_context *up;	/* previous function calls */
  struct var_context *down;	/* down towards global context */
  HASH_TABLE *table;		/* variables at this scope */
} VAR_CONTEXT;

VAR_CONTEXT的字符指针name如果为空则表示它存储的是bash全局上下文,否则表示某一个函数的局部上下文,name指向函数的名称。整型变量scope是本上下文在栈中的层数,0表示全局上下文,每深入一层函数调用scope递增1,这样可以体现出该上下文的作用域。标志位集合flags 记录该上下文是否为局部的、是否属于函数、是否属于内部命令,或者是不是临时建立的等信息。up和down指针指向函数调用栈中上一个和下一个局部上下文。哈希表table的内容是该上下文中的变量名值对。

bash中的变量不强调类型,可以认为都是字符串。其存储结构如下:

typedef struct variable {
  char *name;			/* Symbol that the user types. */
  char *value;			/* Value that is returned. */
  char *exportstr;		/* String for the environment. */
  sh_var_value_func_t *dynamic_value;	/* Function called to return a `dynamic'
				   value for a variable, like $SECONDS
				   or $RANDOM. */
  sh_var_assign_func_t *assign_func; /* Function called when this `special
				   variable' is assigned a value in
				   bind_variable. */
  int attributes;		/* export, readonly, array, invisible... */
  int context;			/* Which context this variable belongs to. */
} SHELL_VAR;

字符指针name和value分别指向上下文变量的名和值字符串。对于导出(export)环境变量,exportstr指向一个形如“名=值”的字符串。对于返回一个动态变化值的变量(如RANDOM),函数指针dynamic_value指向生成该值的函数。对于特定的变量,在被赋值的时候可以设置一个回调函数,其指针是assign_func。整型变量attributes记录该上下文变量的可访问性,比如是否为导出的、只读的或隐藏的等。整型变量context记录该上下文变量属于可访问的作用域内局部变量栈的哪一层。

第 3 章 主要文件分析

3.1. 根目录

  • shell.c/shell.h

    shell.c是main()函数的所在,它定义了shell启动和运行过程中的一些状态量,依据不同的启动参数、环境变量等来初始化shell的工作状态(包括受限模式等),之后进入eval.c中的交互循环函数reader_loop()解析命令直到退出。

    初始化函数shell_initialize()调用了variables.c中的initialize_shell_variables()、 set.c中的initialize_shell_options()等一系列子模块初始化函数。如果要新增功能模块,可以将它们的初始化调用放在这里。

    run_startup_files()函数执行~/.profile、~/.bash_profile、~/.bash_login等配置文件,同时判断了bash是否是由sshd或rshd启动的。对于login shell,不执行~/.bashrc;对于non-login交互式shell,或通过sshd、rshd启动的shell,执行~/.bashrc。

    run_one_command()函数处理了-c参数运行一条命令的模式。

    open_shell_script()函数处理运行脚本文件的模式。

    退出函数exit_shell()处理了挂起作业、保存历史等善后工作。

  • eval.c

    读取并解释执行shell命令。主循环为reader_loop()函数,它调用read_command(),read_command()调用parse_command(),parse_command()调用语法分析器y.tab.c中的yyparse()。得到命令后,reader_loop()调用execute_cmd.c中的execute_command()执行命令。

    注:token查找优先顺序:别名>关键字>函数>内部命令>脚本或可执行程序。

  • execute_cmd.c/execute_cmd.h

    执行命令(COMMAND结构)。外部调用接口是execute_command(),内部通过execute_command_internal()执行命令。execute_command_internal()包含可选的管道重定向以及后台运行的参数。

    针对不同类型的命令(控制结构、函数、算术等),execute_command_internal()调用不同的函数来完成相应功能。其中 execute_builtin()执行内部命令;execute_disk_command()执行外部文件。 execute_disk_command()通过调用jobs.c或nojobs.c中的make_child()来fork新进程执行。

    本文件中维护了一个文件描述符的位图。

  • make_cmd.c/make_cmd.h

    构造各类命令、重定向等语法结构实例所需的函数。由语法分析器、redir.c等调用。

    其中make_redirection()填写命令结构的重定向参数。

  • copy_command.c

    用来递归复制各种COMMAND结构的一系列函数。

    作者注释称:This is needed primarily for making function definitions, but I'm not sure that anyone else will need it.

  • dispose_command.c/dispose_command.h

    清理COMMAND结构占用的资源,dispose_redirects()清理重定向语句。

  • print_cmd.c

    将命令结构转化为可打印的字符串。在execute_cmd.c的execute_command_internal()中有调用。

  • redir.c/redir.h

    实现输入输出重定向。在执行之前,命令结构的redirects参数已由make_cmd.c填好,外部主要由execute_cmd.c调用以执行重定向操作。

    外部调用接口是do_redirections(),它解析命令结构的重定向参数,内部交由do_redirection_internal()执行。接着依据不同的重定向类型,redir_open()分别使用常规的open()、redir_special_open()、 noclobber_open()等函数打开重定向的文件描述符。如果要添加新的重定向方式(如重定向到FTP),可考虑在这里添加代码。

    重定向原理参见参考文献《Unix/Linux编程实践教程》10.3节。

  • paser.y

    yacc语法定义文件。

  • y.tab.c/y.tab.h

    yacc生成的语法分析器。

    解析token,调用make_cmd.c中的函数,生成命令结构,便于execute_cmd.c中的函数执行。

    其中包括调用make_redirection()填写命令结构的重定向参数。

  • alias.c/alias.h

    别名操作相关函数,包括增、删、查、改等。内部命令alias的实现是调用本文件中的函数。

  • array.c/array.h/arrayfunc.c/arrayfunc.c

    字符串数组定义及相关函数,实现了数组的一些高级操作。bash程序中一些字符串数组使用了这里定义的ARRAY结构。

  • bashansi.h

    针对不同的编译器处理一些系统头文件的包含关系。

  • bashhist.c/bashhist.h

    命令历史记录功能相关的函数,包括历史记录的启、停、增、查等。

  • bashintl.h

    引入locate.h、gettext.h等国际化支持。

  • bashjmp.h

    对setjmp.h的一层封装,定义了longjmp()的几种状态参数。

  • bashline.c/bashline.h

    与readline库的接口,解决命令自动补全、类emacs与vi行编辑功能等。

  • bashtypes.h

    定义了word类型。

  • bracecomp.c/braces.c

    使用大括号通配文件名功能的函数。

  • builtins.h

    定义内部命令的基本结构struct builtin。

  • command.h

    各类命令(控制结构、函数、算术、重定向等)的结构定义。

  • config.h/config-top.h/config-bot.h

    config.h由configure生成,决定有哪些特性要被编译进bash。如果要新增功能,可以加一个“开关”宏定义。

  • conftypes.h

    定义主机体系结构和操作系统类型的名称。

  • error.c/error.h

    错误处理与报告函数。

  • expr.c

    处理算术表达式。外部调用接口是evalexp()。

  • externs.h

    声明一些源文件中的函数,它们在自己的头文件中没有声明。

  • findcmd.c/findcmd.h

    通过名字查找命令。主要是从PATH变量位置查找外部可执行程序。

  • flags.c/flags.h

    存储和处理各个运行状态标志,如Standard Sh Flags与Non-Standard Flags。

  • general.c/general.h

    很多文件可能公用的一些基础的、不便分类的函数。

  • hashcmd.c/hashcmd.h

    管理哈希表,主要用于将命令名字映射到完整路径。

  • hashlib.c/hashlib.h

    哈希表的数据结构。

  • input.c/input.h

    处理输入流缓冲。

  • jobs.c/jobs.h

    作业控制。主要入口是make_child(),用来创建进程并执行。

    jobs、fg、bg、kill等命令的内部实现都在这里。

    作业管理详细流程暂未分析。

  • nojobs.c

    在未实现作业控制的操作系统中代替jobs.c编译。

  • list.c

    链表数据结构。

  • locale.c

    与国际化相关的函数,包括对“LC_”系列环境变量的操作。

  • lsignames.h/signames.h

    定义了各种信号的名称字符串。signames.h是由编译时辅助工具mksignames生成的,mksignames的源代码在support子目录。

  • mailcheck.c/mailcheck.h

    检查账户邮箱的函数。

  • mksyntax.c

    用来生成编译时辅助工具mksyntax。mksyntax与用来构建词法分析文件syntax.c。

  • syntax.c/syntax.h

    syntax.c是由mksyntax生成的词法分析文件,syntax.h定义了词法分析工作中需要的宏和标志位等。

  • parser.h

    parse.y和bashhist.c所需的定界符栈结构(struct dstack)的定义。

  • patchlevel.h

    记录bash的修正版本号。

  • pathexp.c/pathexp.h

    与通配(globbing)库的接口。

  • pathnames.h

    记录一些操作系统配置文件的路径。

  • pcomplete.c/pcomplete.h/pcomplib.c

    可编程的命令补全功能。

  • quit.h

    定义通用的异常退出宏,是对SIGINT信号的响应。

  • sig.c/sig.h/siglist.c/siglist.h

    信号处理相关函数。

  • stringlib.c

    字符串处理相关函数。包括从字符串-整数键值对结构(ALIST)中查找数据项等函数。

  • subst.c/subst.h

    负责参数、命令、算术、路径扩展、引号等的代入、展开工作。

  • test.c/test.h

    GNU test program,各类条件比较条令,在shell脚本中常用。

  • trap.c

    操作trap命令所需的一些对象的函数。

  • unwind_prot.c/unwind_prot.h

    通用的函数执行保护和退出处理机制。

  • variables.c/variables.h

    处理shell变量。用不同的哈希表分别存储不同生命周期的shell变量与函数。

    变量列表是由当前环境来初始化的。bash启动时环境由main()的char **env参数传入。

    对于函数,使用栈来保存和切换局部变量的上下文。

  • version.c/version.h

    显示bash版本号。

  • xmalloc.c/xmalloc.h

    安全版本的malloc封装。

3.2. 其它目录

  • builtins

    该目录下是内部命令的源代码。

    每个内部命令是一个def文件,Makefile中DEFSRC声明了所有内部命令的def文件。

    由mkbuiltins.c生成编译时辅助工具mkbuiltins,mkbuiltins处理*.def文件,生成命令的*.c源程序以及builtins.c、builtext.h。builtins.c和builtext.h相当于各个内部命令的索引。

    所有文件最后编译得到libbuiltins.a。

    此例试验加入一个了内部命令。

  • cross-build

    此目录下的文件是用于为其它系统交叉编译而缓存的configure结果。

  • CWRU

    杂项文件,可能是来自CWRU,暂未分析。

  • doc

    Te文档。

  • examples

    脚本编程示例(可用于对扩展后的bash的验证)。

  • include/lib

    bash所需头文件、库文件(源代码)。

  • po

    用于国际化的语言定义文件。

  • support

    编译过程所需的支持工具及其源代码。

  • tests

    make tests所用测试脚本,可用于对扩展后的bash的验证。

第 4 章 主要流程分析

4.1. 命令解析与执行

命令解析与执行的外部视图见参考文献中《详解Bash命令行处理》一文。

bash启动并初始化完成后,进入eval.c中的交互循环函数reader_loop()开始解析命令。reader_loop()不断循环读取和执行命令,直到遇到EOF。

reader_loop()中读取命令调用的是read_command()函数,read_command()调用parse_command(),parse_command()调用语法分析器y.tab.c中的yyparse(),最终取到命令。read_command()将读到的命令存入了全局变量global_command。其中:

read_command()的额外工作是执行“shell空闲一段时间后自动登出”功能(环境变量TMOUT)。

parse_command()的额外工作是执行PROMPT_COMMAND指定的命令,调用处理here document的函数。

yyparse()由yacc通过parse.y生成。它分析出命令语法后,调用make_cmd.c中的各种函数生成不同的COMMAND结构对象,用以执行。

读到命令后,reader_loop()调用execute_cmd.c中的execute_command()执行命令。针对不同类型的命令(控制结构、函数、算术、重定向等),execute_command_internal()调用不同的函数来完成相应功能。其中 execute_builtin()执行内部命令;execute_disk_command()执行外部文件。 execute_disk_command()通过调用jobs.c或nojobs.c中的make_child()来fork新进程执行。

make_child()同步了输入流缓冲区,然后fork新进程。对于jobs.c版的make_child(),对作业做一些初始化工作,再将待执行的命令通过add_process()函数加入启动进程链表。

从make_child()返回后,execute_disk_command()判断pid,如果是子进程,就调用shell_execve()函数在该函数中执行(exec)目标命令,同时做一些错误处理。

源程序对execute_disk_command()的注释如下:

/* Execute a simple command that is hopefully defined in a disk file
   somewhere.
   1) fork ()
   2) connect pipes
   3) look up the command
   4) do redirections
   5) execve ()
   6) If the execve failed, see if the file has executable mode set.
   If so, and it isn't a directory, then execute its contents as
   a shell script.
   Note that the filename hashing stuff has to take place up here,
   in the parent.  This is probably why the Bourne style shells
   don't handle it, since that would require them to go through
   this gnarly hair, for no good reason.
   NOTE: callers expect this to fork or exit(). */

4.2. 重定向的实现

COMMAND结构有一个REDIRECT类型的指针(redirects),指向了本命令的重定向信息。

REDIRECT结构记录了重定向的源描述符和目标:redirectee(类型为REDIRECTEE),REDIRECTEE是一个联合类型,它可以是目标描述符或目标文件名。REDIRECT本身包含指向下一个REDIRECT对象的指针,因此对于一个COMMAND对象,可以有一系列重定向信息构成的链表。

语法分析器在遇到重定向语法时,调用make_cmd.c中的make_redirection()函数填写COMMAND结构的REDIRECT参数,并设置表示重定向方式的标志位。

execute_cmd.c中的函数执行命令时,调用redir.c中的do_redirections()实现重定向。对于重定向信息链表中的每个REDIRECT对象,分别交由do_redirection_internal()处理。

do_redirection_internal()针对重定向方式的标志位,做一些特定的设置,然后调用redir_open()。

redir_open()对于不同的重定向目标,调用不同的函数完成文件描述符的打开操作。例如软驱和网络设备文件调用 redir_special_open(),对于noclobber mode(禁止覆盖变量模式)调用redir_special_open(),一般情况下调用常规的open(),打开系统最小未用的文件描述符,实现重定向。

4.3. 内部命令(built-in)的构建

源代码目录(记为$(srcdir))下的builtins目录存储的是各个内部命令的源代码预定义文件(*.def)。在make的过程中,由 mkbuiltins工具将它们预编译为源程序(*.c),进而编译为目标文件(*.o)。mkbuiltins工具是由同一目录下的 mkbuiltins.c编译生成的,它在处理*.def文件的同时,还会生成builtins.c和builtext.h两个文件,用做bash主程序调用内部命令的接口以及各个内部命令的索引。

要添加一条新内部命令,只需参考原有命令的存在形式即可,步骤如下:

1、新建预定义文件:$(srcdir)/builtins/[命令名].def。可复制已有命令的预定义文件,修改其中的$PRODUCES、$BUILTIN、$FUNCTION、$SHORT_DOC等定义,使之与命令名相符。

2、在预定义文件中建立命令处理函数,原型参考已有命令的处理函数,函数名与$FUNCTION的定义一致。参数为WORD_LIST *list,该结构的定义在$(srcdir)/command.h中。处理参数的具体方法同样可参考已有的命令(如echo)的处理函数。

3、修改$(srcdir)/builtins/Makefile.in,参照已有的命令,分别在DEFSRC、OFILES添加对[命令名].def、[命令名].o的定义;添加[命令名].o对[命令名].def以及其它头文件的依赖关系。

4、回到$(srcdir)下,对源代码进行configure、make,如果一切顺利的话,此时生成的bash程序将包含新添加的内部命令。

例 4.1. 新建一条“linjian”命令

本例中添加的命令处理函数为:

int linjian_builtin (list)
     WORD_LIST *list;
{
  printf ("This is a built-in for test by Lin Jian.\n");
  if (list)
    printf("Parameter: %s\n", list->word->word);
  return (EXECUTION_SUCCESS);
}

编译后试验结果如下:

#在原版bash下工作:
lj@lj-laptop:~/bash-3.2$ ps
  PID TTY          TIME CMD
 6212 pts/2    00:00:00 bash
 9893 pts/2    00:00:00 ps
lj@lj-laptop:~/bash-3.2$ linjian
-bash: linjian: command not found 
#进入修改后的bash:
lj@lj-laptop:~/bash-3.2$ ./bash
lj@lj-laptop:~/bash-3.2$ ps
  PID TTY          TIME CMD
 6212 pts/2    00:00:00 bash
 9904 pts/2    00:00:00 bash
 9922 pts/2    00:00:00 ps
lj@lj-laptop:~/bash-3.2$ linjian hello!
This is a built-in for test by Lin Jian.
Parameter: hello!
lj@lj-laptop:~/bash-3.2$ type linjian
linjian is a shell builtin


4.4. 环境变量与上下文

Linux中每个进程都有自己的环境(main函数的char *env[]参数指向),环境是由一组变量组成的,这些变量中存有进程可能需要引用的上下文信息。bash将环境变量的复本保存在variables.c 中名为shell_variables的全局VAR_CONTEXT结构中。要导出给子进程的变量由全局字符串指针char **export_env记录,形式是“名=值”字符串数组,也就是键入export命令看到的内容。

bash启动后,调用variables.c中的initialize_shell_variables()函数,传入来自main函数的env参数,将env中的环境变量存入shell_variables。对于PATH、IFS、PS1之类bash本身要使用的环境变量,如果env中尚无,则在此时建立。另外一些有关bash版本、命令历史、邮件检查等内部辅助功能的环境变量也在这里建立。

execute_cmd.c中调用各类命令的函数在执行命令之前,首先调用variables.c中的maybe_make_export_env()函数,构建导出给子进程的环境,即export_env。shell_execve()执行外部命令时使用的是exec族中的execve()函数,因此可以将export_env传递给bash启动的子进程。

凡需要增改环境变量的地方,调用variables.c中的bind_variable()函数实现。例如在cd命令执行后需要重设PWD。

4.5. 由sshd启动bash的过程

bash启动时,shell.c中的run_startup_files()通过查找SSH_CLIENT、SSH2_CLIENT环境变量是否存在,来判断自己是不是由sshd启动的,记录在变量run_by_ssh中。此外可以通过检查stdin的文件描述符是否被重定向为网络文件或套接字来判断 bash是不是由rshd启动的。如果bash启动自sshd或rshd,并且是顶层shell(非子shell),则执行~/.bashrc脚本。

防止~/.bashrc被多次执行的方法是只在bash是顶层shell时加载之。作者曾考虑在 initialize_shell_variables()过程中设置SSH_CLIENT、SSH2_CLIENT环境变量为非导出的,来避免子 shell知道自己的父shell是由sshd启动的,从而不执行~/.bashrc。但他最终放弃了这个方法。

除此以外,bash对ssh没有其它特殊处理。

4.6. 子shell

shell启动的shell子进程称为子shell。直接以文件名运行可执行文件时,bash并不知道它调用的一个可执行是二进制文件还是脚本,只是在 exec过程中交给系统内核处理。对于shell脚本,通常以“#![shell可执行文件名]”开头,“#!”是一种magic number。当内核通过magic number断定执行的是脚本时,就会调用一个新的指定的shell的实例来解释执行脚本,这个实例就是子shell。父子shell是两个进程,所以各自的变量是独立的。除非父shell将自己的变量导出到环境中,否则子shell无法获得父shell中定义的变量。

bash通过变量SHLVL记录自己是进程调用栈中哪一层的shell,即bash被嵌套的深度。bash启动时,调用variables.c中的 initialize_shell_level()设置SHLVL。系统login之后启动的bash的SHLVL为1,每层shell启动的子 shell的SHLVL在其环境中读到的SHLVL基础上加1。

使用source命令(“.”命令)执行脚本时,不开启子shell。bash内部的实现是将脚本文件内容读入一个缓冲区,然后执行语法分析,因此效果与直接从键盘输入脚本内容相同。

第 5 章 杂记

5.1. bash编程风格

bash的C语言函数声明使用了“__P”宏,定义遵循K&R规范,因此可以使用旧的非ANSI的编译器编译。bash源代码充分考虑了不同的CPU体系结构、不同的操作系统和不同的编译器的差异,使用宏和条件编译处理这类问题,增强可移植性的同时不增加代码复杂性。对于某些可选择编译的特性,bash通过定义宏作为开关,这些宏可以在 configure时由用户参数决定取值,不需要编译者显式修改源代码。

bash的代码缩进风格并不是很统一,可能是有来自不同贡献者的代码。多数代码与常见的Java风格差异较大,有些地方不借助Source Insight这样的工具很容易找不到头绪。bash并不十分避讳goto的使用,一些使用goto的地方的可读性还是比较好的。

由于多数函数名称都是完整的动宾短语,所以并非每个函数都有注释,通过名称可以了解其大致功能。对于某些复杂的流程,函数内部有一些注释,不少注释具有讨论和建议的性质,对未来的贡献者有启发性。和大多数GNU程序一样,bash中也有不少风趣的注释,体现出西方特色的幽默。

附录 A. 学习备注(Q&A)

  • Q:__P是什么?

    A:ANSI C之前的旧编译器不支持函数原型定义。使用“__P”宏为ANSI和非ANSI的编译器提供一种可移植的方案。“__P”的实现通常如下:

      # if defined(__STDC__) || defined(__GNUC__)
      #   definec__P(x) x
      # else
      #   define __P(x) ()
      # endif

  • Q:IFS是什么?

    shell环境变量IFS(Internal Field Separator)中可能存储的值是空格、TAB、换行符等,在bash中默认是0x0a。它用来在语法分析、变量代入等过程中界定命令。

  • Q:Here Document是什么?

    一个here document就是一段带有特殊目的的代码段。它使用I/O重定向的形式将一个命令序列传递到一个交互程序或者命令中,比如ftp、cat或者ex文本编辑器。例如:

    COMMAND <<InputComesFromHERE
    ...
    InputComesFromHERE

附录 B. 参考文献

  1. Mendel Cooper.Advanced Bash-Scripting Guide,2006.http://personal.riverusers.com/~thegrendel/abs-guide.pdf

  2. Bruce Molay.Unix/Linux编程实践教程,北京:清华大学出版社,2004.

  3. Eric S. Raymond.Unix编程艺术,北京:电子工业出版社,2006.

  4. home_king.详解Bash命令行处理,2005.http://www.linuxsir.org/main/?q=node/134

  5. Chet Ramey.Bash FAQ version 3.36,2006.ftp://ftp.cwru.edu/pub/bash/FAQ

附录 C. 作者信息

林健,北京理工大学计算机科学技术学院学生。转载本文请注明出处。如发现文中不妥或错误之处,望不吝赐教。

作者网站:http://www.linjian.cn

作者Blog:http://blog.linjian.cn

 
 
 
 
 
 
 
 
 
 
 
readlineBash readline使用技巧

很多人会用 Bash,但是很少有人知道 readline 是怎么回事。readline 是一个强大的库,只要使用了它的程序,都可以用同一个配置文件配置,而且用同样的方法操作命令行,让你可以方便的编辑命令行。

使用 readline 的程序现在主要有 Bash, GDB,ftp 等。readline 付予这些程序强大的 Emacs 似的命令行编辑方式,你可以随意绑定你的键盘。

术语解释

在下文中,我们经常提到 'C-x r' 这类键操作。'C-x r' 其实就是按Ctrl-x,然后按 r。同理 'C-M-@' 就是按 ctrl-alt-@(M表示meta, 在 PC 上就是 Alt 键),但是其实 @ 是shift-2 (看看你的键盘)。所以 'C-M-@' 实际上要你按 ctrl-alt-shift-2。

但是在配置文件里的键序列中,我们把 'C-x r' 表示为 '\C-xr', 把 'C-M-@' 表示为 '\C-\M-@',你自己看看就知道怎么回事了。同理 'Esc a' 别表示成 '\ea'。

这就是 Emacs 里的按键的通常标记方法。EMACS = Esc Meta Alt Ctrl Shift :)

技巧篇

在自己配置命令行之前,我们先来看看利用缺省的键绑定能够进行的一些巧妙的用法:

第一招:使用以前的命令行参数


你是否经常出现这种情况?你想把 ~/text-browser/ 目录下的3个.tar.gz文件搬到/usr3/software/,于是你输入:

$mv ~/text-browser/*.tar.gz /usr3/software/

我想你一定已经知道,打入 ~/text 之后按 TAB 就可以补全text-browser这个长文件名吧?这是Bash 的基本功能。我废话?好了,就当你知道吧。不过今天我要讲的东西比这个复杂一些。
Go on! 刚刚输入到这里,你突然想起,应该在 /usr3/software/ 下先建立一个目录叫browsers,这样放进去的文件比较好管理。
于是你 Ctrl-u,删掉了这行命令。唉呀,这么长的命令一下就没了。是不是有点可惜?这还不算麻烦。然后你

mkdir /usr3/software/browser
mv ~/text-browser/*.tar.gz /usr3/software/browser

嗯。TAB 是帮了你不少忙。可是你实际上有更好的办法来完成这项工作。好吧,看看 readline 怎样神奇的完成你的任务: 我们回到这种情况:

$mv ~/text-browser/*.tar.gz /usr3/software/

你刚才是按了 Ctrl-u 删除了所有输入的东西。可惜啊!你要是按 M-#(也就是按住 PC 机的 Alt 键,再按 #,实际上就是 Alt-Shift-3),那么 Bash 就会在这样最开头插入一个 '#',然后输入这行。这样命令就被作为一行注释载入了历史。 这有什么好处?这样你的这行命令里的内容就可以被再次利用。看着:你接着输入:

mkdir ...

等等,你是不是想输入 /usr3/software/?你不用再敲一遍了!直接按 M-.(Alt 加句号),看看, /usr3/software/ 是不是出现在命令行上了?M-. 就是调用了 yank-last-arg 函数,把上一条命令的最后一个参数放在命令行上。好了,回车吧! 你接着输入:

mv ...
等等,这下是该输入 ~/text-browser/*.tar.gz 了。烦不烦啊?换一种方式吧。请按:'M-1 M-.'(把上一条命令的第一个参数放在命令行上)。这样命令行成为了:

mv /usr3/software/browser

怎么成这样了?看看你的“上一条命令”是什么吧?是……你自己看。所以这个参数不是你想要的。那么继续再按一次 'M-.'。看到了吧?你的命令行已经成为:

mv ~/text-browser/*.tar.gz
好。打一个空格。再按一下 'M-.'。命令行变成了:

mv ~/text-browser/*.tar.gz /usr3/software/browser
这就是你想要的!
是不是看起来你还是花了不少工夫?但是想一想,如果你是要执行这样一个命令呢?

mv /data/ftp/pub/TUG/texmf/tex/latex/CJK/GB/GB.cap \
/usr/local/texlive/texmf-local/tex/latex/CJK/GB/
嗯。记住这个有用命令:M-. , 它的前面可以用 M-0, ... 作为数字参数。

第二招:补全命令名,文件名和变量名


你知道 TAB 可以补全命令行上很多东西。可是你遇到这种情况的时候怎么办?

man a-very-very-long-command-name
你输入了 man a-ver... 之后,按 TAB,什么反应也没有。因为 TAB 执行的是 “按情况补全”(complete),它看到 man,知道这应该是一个命令,那么它认为: “后面应该是一个文件名参数。” 但是你想要的是命令的名字怎么办?答案:按 'M-!'.
再来看:你需要设置 XMODIFIERS='@im=fcitx'。你输入了

export XM...
按 TAB? 没有反应。为什么呢?因为 TAB 的补全想要一个文件名,而当前目录没有开头是 'XM...' 的文件。那么你怎么补全?答案:'M-$'。
其实 readline 的补全方式被 Bash 扩充了很多。看看有多少吧!

'TAB': complete
'\M-!': complete-command
'\M-/': complete-filename
'\M-@': complete-hostname
'\M-~': complete-username
'\M-$': complete-variable
自己试试吧!

第三招:扩展命令行


你的一个目录里有很多类似的文件,名字叫 T12.txt, T12.log, T23.txt, T23.log, T13.txt, T13.log…… 有后缀 txt 的,也有后缀 log 的。... 你想把其中的某些 T*.txt 都移动到另外一个目录,而T*.log都不动。但是T*.txt 也不是全部都要移动。所以你想把T*.txt 都放在命令行上,然后选择其中一些。你输入:

mv T...
接着按 'M-*'(insert-completions)。结果 T 开头的文件都被放到命令行上了。嗯。这在某些时候是有用的,可是现在它把 T*.log 的文件也放上去了。不行。我们于是继续输入:

mv T*.txt
好了,现在我们可以使用 'C-x*'(先按ctrl-x,然后按*)。结果所有名字T*.txt 的文件都被放到了命令行上面。'C-x*' 执行的函数叫做 glob-expand-word.

配置篇


你是不是觉得那些命令很难记住?不顺手?别怕!它们都是可以改变的,就像Emacs的键绑定那样,可以被任意的改变!
所有使用readline的程序,都使用一个配置文件来决定它的行为和键绑定。这个文件一般是 INPUTRC 环境变量确定的。如果这个环境变量没有值,那么缺省使用 ~/.inputrc。
~/.inputrc 文件很简单,只有4种语句:
  • 注释
  • 变量设置语句(set variable value)
  • 键绑定('keyseq':function)
  • 条件语句($if ... $endif)
  • 我们先不说其它的,先来看看键绑定吧!

键绑定

  1. 绑定语句
    你现在就可以动手设置你喜欢的控制方式。比如,我发现有些时候我需要在命令行上做上 mark(Emacs 术语),然后把mark 和光标之间的 region(Emacs术语) 删掉,这个操作在 Emacs 里叫做kill-region. 但是我们发现这个函数在 Bash 里缺省是没有绑定的。如果我希望得到跟 Emacs 一样的绑定 C-w 的话,就把这行插入到 ~/.inputrc:
    '\C-w':kill-region
  2. 使绑定生效。
    为了使这个键绑定生效,你需要执行 re-read-init-file 函数。这个函数缺省绑定在了 'C-x C-r'。你修改 ~/.inputrc 之后在 Bash 里输入 'C-x C-r' 就可以使新的配置生效了。
  3. 列出可用的函数。
    不过你怎么知道那些函数可以被绑定呢?readline 的 info 页列出了很多函数,可是你不会每次都去info里查询吧,很麻烦啊。其实你可以使用bash的 bind 命令来得到所有的键绑定:
    $bind -p
    可以显示所有现有的已经绑定和没有绑定的函数。没有被绑定的函数被显示为 '(not bound)',并被加上了注释。就像这样:

    '\C-g': abort
    '\C-x\C-g': abort
    '\M-\C-g': abort
    '\C-j': accept-line
    '\C-m': accept-line
    # alias-expand-line (not bound)
    # arrow-key-prefix (not bound)
    # backward-byte (not bound)
    '\C-b': backward-char
    '\M-OD': backward-char
    '\C-h': backward-delete-char
    '\C-?': backward-delete-char

    你可以把这个命令的输出作为一个模板,嵌入到 ~/.inputrc 文件。把你喜欢的函数绑定到方便的按键。 其实 readline 有三个函数可以让你方便的查询函数,变量和宏的绑定情况,它们是:

    dump-functions
    dump-variables
    dump-macros
    可是它们缺省都没有被绑定到任何按键。你可以为它们分别设置类似 'C-xf', 'C-xv', 'C-xm' 这样容易记忆的绑定。
  4. 如果忘了绑定
    这样你就可以设置你需要的绑定啦!但是你还是有可能在需要的时候突然记不起哪些键绑定可以补全。这时候你输入:

    $bind -p | grep compl
    得到结果:

    '\C-i': complete
    '\M-\e': complete
    'TAB': complete
    '\M-!': complete-command
    '\M-/': complete-filename
    '\M-@': complete-hostname
    '\M-{': complete-into-braces
    '\M-~': complete-username
    '\M-$': complete-variable
    '\M-\C-i': dynamic-complete-history
    '\M-g': glob-complete-word
    '\M-*': insert-completions
    .......
    这样你记不住一个键的时候就可以方便的查询,这样几次之后,你就会把自己需要的按键都记住了。

配置变量

  1. 体验:
    Bash 的 readline 有一些变量可以控制它的行为。比如:
    bell-style 可以控制出错时是 audible(发出响声),visible(闪动屏幕),还是none(什么都不做);editing-mode 可以控制你是用 Emacs 的输入方式还是用 vi 的; 比如可以在~/.inputrc里面设置set editing-mode emacs;也可以直接在命令行里面设置:set -o emacs(注:减号是“设置”,加号是“取消”。比如set +o vi就是取消vi editing mode进入默认的emacs editing mode。)
    completion-query-times 的值控制在补全的个数超过多少N时,bash 提示: “Display all N possibilities? (y or n)”;
    如果我设置 expand-tilde 为 on,当输入“ls ~/doc”,按 TAB 时,命令行会自动变成 'ls /home/wy/doc'.
    如果把 visible-stats 设置为 on,那么列出补全的时候,目录,可执行文件,符号连接,会被分别使用 /, *, @ 来标记,就像 ls -F 的到的结果。
  2. 设置:
    设置的方法极其简单,就在 ~/.inputrc 文件里写入类似语句:
    set visible-stats on
    然后 'C-x C-r' 使设置生效。
  3. 怎样知道有哪些设置?
    可以设置的参数是很多的。使用命令
    $bind -v
    就可以得到所有这些可以设置的变量和它们的值了。

或许很多人已经知道 readline,但是总有人不知道。readline 从字面上来理解,就是从“行”上面读取。实际上就是一个行编辑库,bash 在用,mysql 也在用,mutt 也在用。

通过 readline,可以方便的在命令行上面移动,增删,复制,粘贴,搜索。比如:

  • ctrl+r 可以搜索历史命令,很常用的一个
  • ctrl+a 到行首
  • ctrl+e 到行尾
  • ctrl+u 删除到行首
  • ctrl+k 删除到行尾
  • ctrl+l 类似 clear 命令效果
  • ctrl+y 粘贴
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值