Bash工作手册

发布时间 2023-04-12 19:22:00作者: 切糕茶叶蛋

本文主要讲述bash的配置,各种命令(包含内置命令以及类似内置命令的指令),运行原理等主题

Bash配置

自动补全

$ type complete
complete is a shell builtin

https://juejin.cn/post/6844904096411942926
https://jasonkayzk.github.io/2020/12/06/Bash命令自动补全的原理/

系统自带的命令补全功能有限,自动补全功能仅限于命令和文件名。可以安装 Bash 命令补全增强软件包 bash-completion来实现更多命令的补全。

CentOS 默认会安装一个 bash-completion 包,这里面包含了常用命令的大部分自动补齐脚本,在编写脚本时可以直接参考这个包里的内容;

很多特有命令的自动补全支持不在bash-completion内,这时候可以手动添加进去。 比如git、docker等经常使用的命令。

安装bash-completion之后,一般会生成一个bash_completion.d的目录, 这个目录下的配置会被bash_completion加载,所以不用配置,只需要把自己的配置脚本放到这个目录下!

git安装之后文档里会有git-completion.bash文件, 移动到里面bash-completion目录里就行了。

mv git-completion.bash /etc/bash_completion.d

bash的特殊变量

变量 描述
$0 当前脚本的文件名。
$n(n≥1) 传递给脚本或函数的参数。n 是一个数字,表示第几个参数。例如,第一个参数是 $1,第二个参数是 $2
$# 传递给脚本或函数的参数个数。
$* 传递给脚本或函数的所有参数。
$@ 传递给脚本或函数的所有参数。当被双引号" "包含时,$@$* 稍有不同,我们将在《Shell $*$@的区别》一节中详细讲解。
$? 上个命令的退出状态,或函数的返回值,我们将在《Shell $?》一节中详细讲解。
$$ 当前 Shell 进程 ID。对于 Shell 脚本,就是这些脚本所在的进程 ID。

特殊环境变量

  • PATH PATH 变量是由 Bash shell 程序维护和使用的环境变量,存储了一组以冒号分隔的目录路径,用于查找可执行程序的位置。
  • LANG
  • LC_*
  • PROMPT_COMMAND 这个变量的中内容是作为一个普通的bash命令执行的,执行时机是在bash显示prompt之前。
  • TERM 当前终端

更改命令提示符

PS1='\[\e[32m\]\u@\h:\w \$\[\e[0m\] '

bash的初始化

passwd 文件中指定了用户登录后的默认 shell。

export LD_LIBRARY_PATH=/usr/local/lib
  • /etc/profile此文件为系统的每个用户设置环境信息, 当用户第一次登录时,该文件被执行.并从/etc/profile.d目录的配置文件中搜集shell的设置. 是全局的。
  • /etc/bashrc 是shell 全局自定义配置文件,主要用于自定义 shell,该配置对所有用户的shell都生效;
  • /root/.bashrc 用于单独自定义root用户的 bash,只对root用户的bash生效,如果要使elk用户生效,则需要配置/home/elk/.bashrc文件;
  • /root/.bash_profile 用于单独自定义root用户的系统环境,只对root用户生效。
  • ~/.bash_profile 是home目录主人即当前用户特有有环境配置。会调用~/.bashrc

加载顺序:

要验证顺序,打印出来即可,在四个文件末位追加echo命令就可以验证 echo "echo this is xx" >> xx

  1. 首先读取 /etc/profile 文件,该文件对于所有的登录用户都会执行。该文件定义全局的环境变量和启动程序,例如系统的 PATH 变量等。
  2. 然后读取 /etc/bashrc 文件,该文件也对所有的登录用户都会执行。该文件定义全局的 Bash Shell 的特定的设置,例如 Bash 的别名,颜色输出等。
  3. 当某个用户登录时,系统会读取该用户的家目录下的 .bash_profile 文件,如果该文件存在的话。该文件主要用来定义用户特定的环境变量和启动程序等。
  4. 如果 .bash_profile 文件不存在,则会读取用户的家目录下的 .bashrc 文件。.bashrc 文件也定义了用户特定的环境变量和启动程序等,但是和 .bash_profile 不同的是,.bashrc 文件对于交互式的和非交互式的 Shell 都会执行。

总的来说,/etc/profile/etc/bashrc 文件定义了全局的环境变量和启动程序,而 /root/.bash_profile/root/.bashrc 文件定义了特定用户的环境变量和启动程序。对于每个用户的 Bash Shell,系统会首先读取 /etc/profile/etc/bashrc 文件,然后读取该用户的家目录下的 .bash_profile 文件,最后读取 .bashrc 文件。

.bashrc 中是自动执行的初始化脚本,建议将export PATH=... alias=...写到此处。

https://www.cnblogs.com/liang-io/p/9825363.html

shell的内置命令

type命令

type type
type alias      //
type cd
type pwd
type echo
type bg
type fg
type jobs
type export     //环境变量导出
type ulimit     //资源限定
type umask      //同上
type history    //历史
type source     //bash内执行脚本
type exit       //退出

type passwd
type ll
type touch
type rm
type ps
type reset
type chroot     //change root directory

type命令会返回一个命令的性质:

  • 是shell内置命令
  • 还是exe命令
  • 别名

内置命令是shell内部调用的集成在shell中的,执行时可能改变shell这个进程内部状态。比如cd命令会改变进程的“当前目录”,这是一个进程的属性在内核中有数据结构表示“当前目录”,cd会改变这个。再比如ulimit umask也是设定shell进程某些属性的。

而exe是.sh脚本或者elf可执行文件(不区分两者因为对shell来说无逻辑上区别)。

alias 别名

alias mstat='cat /proc/meminfo'

alias的效力仅及于该次登入的操作。若要每次登入是即自动设好别名,可在.profile.cshrc中设定指令的别名。

若仅输入alias 则可列出目前所有的别名设置。

echo 回声

echo 本没什么好说,放这是为了说明 * 通配符。*/~的解释是shell做的, 操作系统内核是不认*/~的!!!

echo *
echo *.exe
echo ~

匹配所有目录下的.exe文件。echo可以用来测试参数输入是否是预期的

根据实验,shell是根据$HOME变量解释~的。

$ export HOME=/root
$ echo ~
/root

reset

reset 通过向终端写控制字符来调整输出格式。

//strace -o strace.txt reset
//cat strace.txt

...C
ioctl(2, TIOCGWINSZ, {ws_row=58, ws_col=237, ws_xpixel=0, ws_ypixel=0}) = 0
ioctl(2, SNDCTL_TMR_STOP or SNDRV_TIMER_IOCTL_GINFO or TCSETSW, {B38400 -opost isig icanon echo ...}) = 0
write(2, "\r", 1)                       = 1
write(2, "\33", 1)                      = 1
write(2, "[", 1)                        = 1
write(2, "3", 1)                        = 1
write(2, "g", 1)                        = 1
int main() {
    printf("This is a \033[1;35m test \033[0m!\n");
    printf("This is a \033[1;32;43m test \033[0m!\n");
    printf("\033[1;33;44mThis is a test !\033[0m'\n");
    return 0;
}

export 环境变量

bash 变量与环境变量:变量不能遗传,环境变量可以

bash 内部也有一些变量影响行为 echo $PS1
export 导出

PATH=/usr/new:$PATH
export PATH

export VAR=blahblahblah

export 'CLICKHOUSE_EXAMPLES_CLICKHOUSE_API_ENV'='{"Port":9000,"Host":"127.0.0.1","Username":"default","Password":"","Database":"tutorial"}'

注意变量名有特殊字符时需要用'' ""引起来,不然会被bash转义掉。

PATH 是shell找可执行命令或者脚本的搜索目录。:隔开的每项路径中依次查找,先找到的被执行。

set&unset 设置

set显示环境变量

unset删除环境变量

特殊字符

globs * 是由bash解释的。 意思是比如cat *.txt 其实bash会将~转成a.txt b.txt传给catcat收到的不会是*.txt

character name Uses
* star 正则,glob
. dot 当前目录, 扩展名
! bang 取反,history
| pipe 命令管线
/ slash 目录分隔符, 搜索命令(例如vim)
\ backslash 字面值,转义引导
$ dollar 变量取值,行末
' tick, quote 字符串字面值
` backtick, backquote 命令替换
" double dollar 半字面值
^ caret 取反,行首
~ tilde home目录
# sharp 注释,预处理,替换
[] brackets 范围
{} braces 语句块
_ underscore ...

env命令

env命令是个elf文件,原理是利用bash运行新命令env时会调用execv("env", ${oldenv})将环境变量传递下去, 然后env调用api获取env。

chroot命令

在 linux 系统中,系统默认的目录结构都是以 /,即以根 (root) 开始的。而在使用 chroot 之后,系统的目录结构将以指定的位置作为 / 位置。

chroot NEWROOT [COMMAND [ARG]...]

chroot是一个可执行程序 /usr/sbin/chroot 原以为是一个built-in命令,结果不是。这个命令需要在

chroot的基本逻辑如下:

int main(int argc, char *argv[])
{
  if(argc<2){ printf("Usage: chroot NEWROOT [COMMAND...] \n"); return 1; }

  if(chroot(argv[1])) { perror("chroot"); return 1; }
 
  if(chdir("/")) { perror("chdir"); return 1; }
 
  if(argc == 2) {
    argv[0] = (char *)"/bin/sh";
    argv[1] = (char *) "-i";
    argv[2] = NULL;
  } else {
    argv += 2;
  }
 
  execvp (argv[0], argv);
  printf("chroot: cannot run command `%s`\n", *argv);
  return 0;
}

用处:

  • 增加了系统的安全性,限制了用户的权力
  • 建立一个与原系统隔离的系统目录结构,方便用户的开发
  • 切换系统的根目录位置,引导 Linux 系统启动以及急救系统等

用例

  • 安装archlinux时,装完之后,需要用chroot切换到新安装的mnt下,以继续
  • 通过 chroot 重新设置 root 密码

chroot总结

chroot 是一个很有意思的命令,我们可以用它来简单的实现文件系统的隔离。但在一个容器技术繁荣的时代,用 chroot 来进行资源的隔离实在是 low 了点。所以 chroot 的主要用途还是集中在系统救援、维护等一些特殊的场景中。

source命令(当前shell下解释执行)

source是bash的内置命令,source命令主要是读取文件信息,然后执行里面的脚本。与sh xxx/bash xxx的区别是,这个内置命令是在当前bash进程内解释执行本脚本的,不是新启动子bash进程中执行脚本的。

source还有一个等效的.命令,也是同样的效果。. my-script.sh 等价于 source my-script.sh

实验,例如有个脚本:

# demo.sh
export ABC=12345
  • sh demo.sh
  • env | grep ABC 发现没有ABC环境变量
  • source demo.sh
  • env | grep ABC 发现ABC环境变量已经定义
  • exit 之后 重新登录 ABC环境变量消失

常见用法:

source /etc/profile

etc/profile是一个sh文件,主要用于配置环境。

比如导入一些路径:

export PATH:=$PATH:/usr/local/path/to

man 命令

man -k keyword 搜索手册中关键字。如果不知道准确名字就用关键字搜。

man SECTION PAGE man 可以查询不同类型的帮助手册,类型SECTION如下

  1. 可执行程序或 Shell 命令
    1p. 可执行程序或 Shell 命令(POSIX 版)
  2. 系统调用(内核提供的函数)
  3. 库调用(程序库中的函数)
  4. 特殊文件(通常在/dev中找到)
  5. 文件格式和约定,如 /etc/passwd
  6. 游戏
  7. 杂项(包括宏包和约定),例如 man(7)、groff(7)
  8. 系统管理命令(通常只针对 root 用户)
  9. 内核相关文件[非标准]
  • man 2 read 查看系统调用 read 的帮助手册。
  • man 3 printf 查看库函数 printf 的帮助手册。
  • man 4 tty 查看特殊的设备文件 tty 的帮助手册。

sh命令参数

man sh GNU Bourne-Again SHell

  • -i interactive 模式,就是执行完命令之后 进入交互模式

Bash 运行逻辑

继承

fork 会继承句柄表 信号表等

比如strace会fork+exec. strace启动的子进程会继承stdout

基本逻辑

shell执行exe的一般逻辑是 fork+exec+wait4

shell 会将> >> | & 特殊对待

>会导致 open+dup2>之后的参数会被shell认为是文件名 这部分逻辑是shell干的。

比如 nohup java -jar > test.txt springboot.jar 首先shell 识别 > test.txt ,单独摘出来形成nohup java -jar springboot.jar 然后将剩余的参数传递给exe。 从实验来看, 管线符|是优先最高的,首先做的事情是将|分隔的各命令区分开。然后依次解析各个隔开的子命令。

实验

package main
import "fmt"
import "os"
func main() {
    fmt.Println(os.Args);
}

将以上go代码编译成 ./cmdparser

[pxfgod@VM-188-255-centos ~/test]$ ./cmdparser -a > redirect.txt -b -c
[pxfgod@VM-188-255-centos ~/test]$ cat redirect.txt 
[./cmdparser -a -b -c]

bash主进程fork + wait4, 子进程 open("test.txt", O_RD) + dup2()调用。如此子进程的stdout就是 test.txt

子进程再以nohup java -jar springboot.jar 执行nohupjava -jar springboot.jar都是nohup的argv由nohup解释。

|会运行多个进程。会调用pipe

& 就是简单的不要wait4

重定向与管线

> 重定向输出到新文件 >> 重定向且append模式到文件。2> 重定向标准错误。

$ command > file
$ command >> file
$ head < /proc/cpuinfo

遇到cmdA | cmdB bash会:

  • bash fork出新进程记为X
  • 父进程bash wait4 X
  • X调用pipe创建通信的管道:他们是双生子 一个的输出pout是另一个的输入pin
  • X调用forkY 此时X Y有相同的文件句柄表
  • X调用dup2 pin置换掉stdin 并且close掉pin/pout
  • Y调用dup2 pout置换掉stout 并且close掉pout/pout
  • X调用execv 执行cmdB
  • Y调用execv 执行cmdA

shell实现重定向的原理

在I/O重定向的过程中

  • 不变的是FD 0/1/2代表STDIN/STDOUT/STDERR
  • 变化的是文件描述符表中FD 0/1/2对应的具体文件,这是通过系统调用dup2()做到的。

重定向命令> >>open() + dup2() 实现
管道命令|fork() + pipe() + dup2() 实现
dup2()偷天换日,把fd索引的内核层的文件描述符偷偷替换掉。

int main() {
    int pid = 0;
    // fork a worker process
    if (pid = fork()) {
        // wait for completion of the child process
        int status; 
        waitpid(pid, &status, 0);
    }
    else {
        // open input and output files
        int fd_in = open("in.txt", O_RDONLY);
        int fd_out = open("out.txt", O_CREAT | O_RDWR, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
        if (fd_in > 0 && fd_out > 0) {
            // redirect STDIN/STDOUT for this process
            dup2(fd_in, 0);
            dup2(fd_out, 1);  
            // call shell command
            system("sort");
            close(fd_in);
            close(fd_out);
        }
        else {
            // ... error handling
        }
    }
    return 0;
}
{
    //parse > <
    //if > out.txt
    //  open out.txt
    //fork()
    //      dup2
    // 子进程 exec
    for each cmd {
        k = fork();
        if (k == 0) {
            dup2
        }
        wait4
    }
}

fork+exec+wait4 && EXPORT 环境变量

execve 可以设定新程序的环境变量。

exec 系列函数只清除一些特定属性。 比如地址空间里的程序、数据, mmap出来的之类的都被清除。大部分属性保留,比如:文件句柄表就是保留的。
参见 https://man7.org/linux/man-pages/man2/execve.2.html

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    printf("PID=%d\n", getpid());
    char *newargv[] = { NULL, "hello", "world", NULL };
    char *newenviron[] = { NULL };

    if (argc != 2) {
        fprintf(stderr, "Usage: %s <file-to-exec>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    newargv[0] = argv[1];

    execve(argv[1], newargv, newenviron);
    perror("execve");   /* execve() returns only on error */
    exit(EXIT_FAILURE);
}


#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    printf("PID=%d\n", getpid());
    for (int j = 0; j < argc; j++)
        printf("argv[%d]: %s\n", j, argv[j]);

    exit(EXIT_SUCCESS);
}
[pxfgod@VM-113-33-centos ~]$ ll /proc/24012/fd
total 0
lrwx------ 1 pxfgod pxfgod 64 Feb 16 15:22 0 -> /dev/pts/0
lrwx------ 1 pxfgod pxfgod 64 Feb 16 15:22 1 -> /dev/pts/0
lrwx------ 1 pxfgod pxfgod 64 Feb 16 15:22 2 -> /dev/pts/0
lr-x------ 1 pxfgod pxfgod 64 Feb 16 15:22 3 -> pipe:[8362837]
l-wx------ 1 pxfgod pxfgod 64 Feb 16 15:22 4 -> pipe:[8362837]

可以看到pipe被保留了。


我们来看一下top | grep管线命令下两进程的文件列表就一目了然了。

[pxfgod@VM-113-33-centos ~]$ ll /proc/25914/fd
total 0
lrwx------ 1 pxfgod pxfgod 64 Feb 16 15:35 0 -> /dev/pts/0
l-wx------ 1 pxfgod pxfgod 64 Feb 16 15:35 1 -> pipe:[8341741]
l-wx------ 1 pxfgod pxfgod 64 Feb 16 15:35 2 -> /dev/null
lrwx------ 1 pxfgod pxfgod 64 Feb 16 15:35 3 -> /dev/pts/0
lr-x------ 1 pxfgod pxfgod 64 Feb 16 15:35 4 -> /proc/uptime
lr-x------ 1 pxfgod pxfgod 64 Feb 16 15:35 5 -> /proc/meminfo
lr-x------ 1 pxfgod pxfgod 64 Feb 16 15:35 6 -> /proc/loadavg
lr-x------ 1 pxfgod pxfgod 64 Feb 16 15:35 7 -> /proc/stat

[pxfgod@VM-113-33-centos ~]$ ll /proc/25915/fd
total 0
lr-x------ 1 pxfgod pxfgod 64 Feb 16 15:35 0 -> pipe:[8341741]
lrwx------ 1 pxfgod pxfgod 64 Feb 16 15:35 1 -> /dev/pts/0
lrwx------ 1 pxfgod pxfgod 64 Feb 16 15:35 2 -> /dev/pts/0

可看到top的标准输出被pipe到了grep的标准输入。

bash的``

`引起来的内容` 是命令的stdout输出内容,将被解释为bash的命令的一部分

cat /proc/`pgrep top|grep -v grep`/status

bash的 && ||

短路性质

References

https://explainshell.com/ 这个解析bash命令的工具非常有用