命令的种类
Bash
支持的命令包括以下三类:
-
shell
函数:按照shell
编程的语法构造的可多次调用的代码块, 与其他语言不同的是,shell
中的函数没有形参列表, 但可以在调用函数时传递任意数量的参数, 函数内部通过$N
的方式获取指定位置的参数. 我们可以用typeset -f
命令查看当前shell
中定义的所有函数函数, 通过下列命令可以直接显示函数名.
> typeset -f | awk '$2~/\(\)/ { print $1 }'
> typeset -f | sed -En "s/(.*) \(\)/\1/ p"
> typeset -f | grep "()"
- 内置命令:由
shell
在源代码层面提供的命令,而不是存在于文件系统中的某个可执行文件。例如,用于进入或者切换目录的cd
命令并不是某个外部文件,在执行内置命令时不需要fork
子进程,也不需要使用exec
加载外部可执行文件,因此不会触发磁盘I/O
,执行内置命令相当于执行当前shell
源代码中的一个函数。我们可以用type
判断一个命令是否为内置命令:
> type cd alias
cd is a shell builtin
alias is a shell builtin
> compgen -b # 显示所有内置命令
- 外部可执行文件:一般为编译好的二进制文件,
bash
会在当前路径和PATH
环境变量中的路径下寻找可执行文件,bash
使用哈希表(内存中的数据存储区)记住可执行文件的完整路径名, 用于避免多次重复的全局搜索.
> man
What manual page do you want?
> type man
man is hashed (/usr/bin/man) # 再次访问就已经被哈希表缓存了
> which man # 查看外部命令的路径
/usr/bin/man
在进行命令查询时, 优先级为shell 函数 > 内置命令 > 外部可执行文件
, 例如Ubuntu
系统中/usr/bin/
路径下自带GNU
标准echo
可执行文件, 而bash
中也存在内置命令echo
, 按照命令查询顺序, 在bash
中使用echo
时会优先执行内置命令. 在bash
中还可以使用alias
为常用的命令添加别名, 别名的优先级要高于以上三种常规命令.
> which ls
/usr/bin/ls # 是外部命令
> type ls
ls is aliased to `ls --color=auto' # 别名 ls 会自动显示颜色
命令的结构
实现 shell 函数命令
在bash
中定义函数时, 关键字function
可以省略, 函数内部可能包括内置命令, 外部命令, 变量, 数组, 关键字等, 其本质就是代码块, 不过有一些类似其他语言中函数的特性. 函数的其他语法细节可以参考GNU Bash 手册.
function pskill()
{
# 这个函数的功能类似于 pkill
# 判断参数数量是否正确
if [ $# -lt 1 ]; then
echo "Usage: pskill <proc-name>"
return -127
fi
local pid # 设置作用域为本地, 也就是代码块内部
# 不这么写就默认是全局变量
pid=$(ps -ax | grep $1 | grep -v grep | awk '{ print $1 }')
kill -9 $pid & # 提交作业
echo -n "killing $1 (process $pid)..."
wait # 等待作业完成
echo "slaughtered."
}
相比其他语言, bash
中的函数没有固定的形参列表, 好处是其调用形式更像普通的命令, 例如:函数名 <参数1> <参数2> ...
, 但坏处是如果不在函数内部对参数的数量以及合法性进行判断, 很容易产生严重的运行时错误.
> pskill
Usage: pskill <proc-name>
> xclock &
[1] 3003
> pskill xclock
[2] 3043
killing xclock (process 3003)...[1]- Killed xclock
[2]+ Done kill -9 $pid
slaughtered.
实现内置命令
本节使用5.0
版本的GUN bash
源代码进行讲解, 在修改源代码之前, 建议先确认你的环境是否能够成功配置并编译bash
:
> git clone https://git.savannah.gnu.org/git/bash.git
> ./configure
> make # 成功则会在当前目录下生成可执行文件
现在我们开始实现一个名为scarlet
的内置命令, 其功能和eval
相同. 我们令源代码根目录为$(topdir)
, 首先我们要在$(topdir)/builtins/
路径下建立scarlet.def
文件并填充如下内容:
$PRODUCES scarlet.c
$BUILTIN scarlet
$FUNCTION scarlet_builtin
$SHORT_DOC scarlet [arg ...]
Execute arguments as a shell command.
Combine ARGs into a single string, use the result as input to the shell,
and execute the resulting commands.
Exit Status:
Returns exit status of command or success if command is null.
$END
*.def
格式的文件是bash
内置命令的预定义文件, 在make
过程中会先将$(topdir)/builtins/mkbuiltins.c
文件编译为mkbuiltins
, 然后使用这个工具将预定义文件转换为*.c
格式, 再通过gcc
将其编译为*.o
文件, 并最终成为bash
可执行文件的一部分. 使用mkbuiltins
可以显著提升内置命令的编写效率, 因为这个工具帮你自动生成了大部分重复的代码, 你只需要配置以下几个变量:
-
$PRODUCES
: 用于表示转换源代码目标文件的名字, 变量的命名注意不要与其他文件冲突, 之后我们还要在Makefile.in
中进行配置. -
$BUILTIN
: 最终可调用的内置命令的名字, 命名规则与函数相同. -
$FUNCTION
: 命令的入口函数名, 命名规则与函数相同, 这个函数的实现也需要在这个文件中. -
$SHORT_DOC
: 命令的简要帮助文档, 以$END
作为结束标志, 这部分内容在运行时可以通过命令 --help
的方式查看.
接下来在我们需要在scarlet.def
文件中实现scarlet_builtin
函数:
#include <config.h>
#if defined (HAVE_UNISTD_H)
# ifdef _MINIX
# include <sys/types.h>
# endif
# include <unistd.h>
#endif
#include "../shell.h"
#include "bashgetopt.h"
#include "common.h"
int
scarlet_builtin (list)
WORD_LIST *list;
{
if (no_options (list))
return (EX_USAGE);
list = loptend;
return (list ? evalstring (string_list (list), "scarlet", SEVAL_NOHIST) : EXECUTION_SUCCESS);
}
我们首先引用了必要的头文件用于处理输入, 主要是为了使用WORD_LIST
这个数据结构, 它存储了bash
对当前命令的分词结果token
, 然后通过evalstring
解析并执行这些token
. 可以看出, 我们在编写这个内置命令时只需要处理核心业务逻辑, 其他部分都可以通过合理配置预定义变量让mkbuiltins
自动生成. 接下来需要在Makefile.in
文件中对编译生成的中间文件和引用的头文件进行配置:
# DEFSRC 变量中添加
$(srcdir)/scarlet.def
# OFILES 变量中添加
scarlet.o
# dependencies 区域中添加
scarlet.o: scarlet.def
# def files 区域中添加
scarlet.o: $(topdir)/command.h ../config.h $(BASHINCDIR)/memalloc.h
scarlet.o: $(topdir)/error.h $(topdir)/general.h $(topdir)/xmalloc.h
scarlet.o: $(topdir)/quit.h $(topdir)/dispose_cmd.h $(topdir)/make_cmd.h
scarlet.o: $(topdir)/subst.h $(topdir)/externs.h $(topdir)/sig.h
scarlet.o: $(topdir)/shell.h $(topdir)/syntax.h $(topdir)/unwind_prot.h $(topdir)/variables.h $(topdir)/conftypes.h
scarlet.o: $(BASHINCDIR)/maxpath.h ../pathnames.h
处理完所有的预定义文件后, mkbuiltins
还会生成用于存放所有内置命令接口的builtins.c
和builtext.h
文件, 我们现在可以重新配置并编译bash
来测试我们实现的内置命令scarlet
的效果:
# 注意要进到 $(topdir)/ 路径下
> make clean; ./configure; make -j6
> ./bash
> scarlet --help
scarlet: scarlet [arg ...]
Execute arguments as a shell command.
Combine ARGs into a single string, use the result as input to the shell,
and execute the resulting commands.
Exit Status:
Returns exit status of command or success if command is null.
> VAR=1; POINT+VAR
> echo \$$POINT
$VAR # bash 默认只解释一次变量名
> scarlet echo \$$POINT
1 # 自定义内置变量实现了 eval 多次解释的功能
实现外部命令
大部分外部命令都是事先编译好的二进制可执行文件, 在bash
中会通过fork-exec
模式处理每个外部命令, 也就是说外部命令都运行在子进程中. 在本节将通过实现ls
命令的最基础功能来讲解外部命令的实现过程:
#include "apue.h" // 这个头文件需要自己去 APUE 网站下载
#include <dirent.h> // 路径处理相关头文件
int main(int argc, char const *argv[])
{
DIR *dp; // 路径数据结构
struct dirent *dirp; // 子路径数据结构
// 判断参数数量是否正确
if (argc != 2)
err_quit("usage: myls directory_name");
// 判断传入的目录是否能打开
if ((dp = opendir(argv[1])) == NULL)
err_sys("can't open %s", argv[1]);
// 遍历指定目录
while ((dirp = readdir(dp)) != NULL)
{
printf("%s\n", dirp->d_name);
}
return 0;
}
这个例子来自于《Unix环境高级编程》第一章, 实现一个命令至少需要完成输出参数处理, 核心业务逻辑, 结果输出三个部分. 而且建议在参数输入错误时给出用法提示, 在运行出现问题时应该给出适当的退出状态码. 现在我们编译这个命令并将其放到PATH
包含的目录中, 然后测试这个命令是否能正确输出.
> sudo gcc 1/ls.c -o /usr/bin/myls
> myls # 用法错误
usage: myls directory_name # 给出提示
> myls . # 遍历当前路径
snap
peko
pekosh
R
.ssh
脚本的结构
在bash
中, 脚本就是命令和控制逻辑的组合, 我们先看一个普通脚本的例子:
#!/bin/bash
clear
echo "This is information provided by mysystem.sh. Program starts now."
printf "Hello, $USER\n\n"
printf "Today's date is `date`, this is week `date +"%V"`.\n\n"
echo "These users are currently connected:"
w | cut -d " " -f 1 - | grep -v USER | sort -u
echo
printf "This is `uname -s` running on a `uname -m` processor.\n\n"
echo "This is the uptime information:"
uptime
echo
echo "That's all folks!"
-
#!/bin/bash
是shabang
, 用于指定这个脚本的解释器。 -
/usr/bin/clear
是一个外部命令,用于清除当前shell
中的输出信息。 -
echo
和printf
都是bash
的内置命令,不同的是echo
始终以0
状态码退出(退出状态永远是成功),并且仅在标准输出上打印参数,然后打印行尾字符,而printf
允许定义格式字符串,并在失败时给出非零的退出状态代码。 -
USER
是一个变量,用于存储当前用户的名字,需要使用$
取出变量的值。
脚本有两种执行方式:
-
source
或.
: 这种方式是在当前shell
中执行脚本, 等价于用{}
包裹的代码块, 但要注意这种执行方法可能会污染当前shell
中的变量. -
./脚本名
或bash 脚本名
: 这种方式本质上是执行外部变量bash
并将脚本名作为参数传递给命令, 这种方法启动的脚本会运行在child shell
中, 不可以访问父shell
的全局变量, 只能访问环境变量. 需要注意的是使用./脚本名
方法执行脚本需要在脚本第一行配置shabang
.
我们再来看一个初始化脚本upon-sound
的例子:
#!/bin/bash
case "$1" in # 脚本的控制逻辑,根据输入选择要执行的分支
'start') # 服务启动时执行
cat /usr/share/audio/at_your_service.au > /dev/audio
;;
'stop') # 服务停止时执行
cat /usr/share/audio/oh_no_not_again.au > /dev/audio
;;
esac
exit 0
初始化脚本(启动脚本)存储在/etc/rc.d/init.d
或/etc/init.d
目录下,用于启动系统服务,例如:系统日志服务,电源管理服务,名称和邮件服务。PID=1
的初始化进程init
读取其配置文件,并决定在每个运行级别中启动或停止哪些服务。
> mv upon-sound /etc/init.d/upon-sound
> ln -s /etc/init.d/upon-sound /etc/rc3.d/S99upon-sound # 运行级别为3 启动时调用 start
> ln -s /etc/init.d/upon-soudecanshund /etc/rc0.d/K01upon-sound # 运行级别为0 关机时调用 stop
参考内容
Something you didn't know about functions in bash
Adding builtins for bash
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。